/* ============================================================================
   Tendd design system - tokens
   Stage 08, step 3. Two levels and no third.

   PRIMITIVE answers "what value". It carries no opinion about purpose, it is
   never overridden by a theme, and every line of it arrived here from
   design/kit/kit.css with its origin comment intact. That chain runs
   design/_theme.css (Concept) into kit.css (UI + Visual) into this file, and
   the origin is the last link back to the plate pixel it was picked from. Lose
   it here and it cannot be recovered from anywhere.

   SEMANTIC answers "why this colour here". Every role points at a primitive
   through var(), carries the count of usages it grew from, and declares the one
   SURFACE it paints. Three surfaces, three different thresholds:
       ink   text and icon glyphs      4.5:1  (3:1 from 24px, or 19px bold)
       fill  page, card, band, wash    3:1    (WCAG 1.4.11)
       line  border, divider, ring     3:1    (WCAG 1.4.11)
   One role on two surfaces is forbidden even when the value is identical,
   because a fill role placed on a 12px bold caption passes as a surface and
   fails as text, and a table of text-on-background pairs never sees it: nobody
   declared it text. Ink roles carry the -text suffix; measured figures for BOTH
   themes stand next to each role.

   EVERY ROLE CARRIES ITS ORIGIN AND TWO COUNTS, swept in on 2026-08-12 at step 9
   after five roles were found carrying no count at all and one stating it as a
   word. The ORIGIN is the DESIGN.md entry the value was picked from, carried over
   here rather than left there, because DESIGN.md was generated FROM the code and
   the code is the thing that gets edited: a role that cannot name its entry is a
   value nobody is able to refuse to change. The COUNTS are TWO DIFFERENT CORPORA
   and are labelled so, because read as one they look like a contradiction. The
   figure already in a comment is the KIT-ERA count the role grew from, taken in
   kit.css and on the coloured pages before this file existed, and it is the
   evidence that the role was a pattern and not a preference. The figure marked
   "Today" is what reads the role NOW: `var(--role` across base.css and every
   stylesheet in components/, counted mechanically on 2026-08-12, as occurrences
   and as FILES. Written without a glob on purpose: a `*` after a slash inside a
   comment is a false comment opening, and the balance check this file is verified
   with counts markers rather than parsing them.
   The two figures rarely agree and are not meant to. One component file serves
   every page that wears the component, so a role written once here can paint
   thirty screens, and the kit-era figure counted the screens. The finding this
   sweep was looking for is a role at 0 today, which would be a role the product
   stopped reading and nobody noticed; there is none.

   GEOMETRY gets no semantic level. A radius and a spacing have nothing to
   override: no theme and no rebrand moves them, they simply repeat. Components
   read them straight from primitive.

   THE DARK PAIR IS A PROPERTY OF THE LEVEL, NOT AN EVENT. Every semantic role
   is written twice, here and in [data-theme="dark"], at the moment it is
   declared. A role without a pair does not exist. The pair is NOT mirrored:
   contrast is measured against the opposite ground, so on dark the action goes
   LIGHTER up the scale rather than reusing the same primitive.

   NO NEW VALUES. Nothing in the light half of this file is absent from
   kit.css, apart from three consolidations decided at step 2 and listed in
   docs/tokens-audit.md. The dark half is necessarily new: kit.css has no dark
   theme, and the values below were derived from the same hues and measured.

   State tokens (hover, focus, disabled) are NOT here. They arrive at step 5
   with the first component that actually carries them, and in both themes.
   ============================================================================ */

/* `:root, [data-theme="light"]` rather than `:root` alone, so a theme can be pinned to a
   SUBTREE and not only to the document. Without it a page cannot show both themes at once:
   a light sample inside a dark page inherits the dark values and quietly renders the same
   colour twice, which is what the colour page did before this line. No value changes. */
:root, [data-theme="light"] {

  /* ==========================================================================
     PRIMITIVE - colour
     Petrol and Paper, from concept.md (D-Concept, July 2026). The block comment
     is the origin for every value under it that arrived from Concept; the four
     named on 2026-08-10 and the two named on 2026-08-11 carry their own.
     ========================================================================== */

  --paper:            #ffffff;   /* the content surface: cards, rows, the app bar */
  --canvas:           #eef3f4;   /* the ground the paper sits on */
  --panel:            #f4f7f8;   /* the recessed step: row hover, the desktop rail */

  --ink:              #141b1d;   /* near-black, the primary reading colour */
  --slate:            #384349;   /* the body step */
  --muted:            #5a686c;   /* the single secondary tier. There is no third */

  --hairline:         #e4e9ea;   /* container edges */
  --hairline-soft:    #eef2f3;   /* dividers inside a container */
  /* --hairline-strong AND --hairline-strong-dark WERE HERE UNTIL 2026-08-20 and
     they left together with the one thing that read them. The chain is worth
     keeping because it is three deletions and only the first was decided: the
     founder dropped `.plan-opt:hover`, so `--line-container-hover` had no reader
     in the system, so these two primitives had no role. Each step follows from the
     one above it and none of them is a taste call. #cdd7d9 was the light value and
     #303c3f the dark one, computed rather than picked so the container edge gained
     the same amount under the pointer in both themes - which is a good decision
     about a state that no longer exists. DESIGN.md names #cdd7d9 exactly once and
     only NEGATIVELY, as the value the control hover was moved OFF, so nothing in
     the document goes stale with them. */
  --edge:             #7b8d91;   /* origin: the founder's non-text contrast decision,
                                    2026-08-11. The lightest value on the hairline hue
                                    that clears 3:1 on all three grounds a control sits on */

  --skeleton:         #e2e9ea;   /* loading placeholders */

  --petrol:           #1c6a76;   /* THE accent. One voice, spent four ways and no more:
                                    the primary action, the current selection, the trust
                                    line and, since D-Plot on 2026-08-18, the plotted line */
  --petrol-deep:      #175a64;   /* the same voice under the pointer */
  --petrol-tint:      #e7edee;   /* the selected wash */

  --amber:            #8a5c0c;   /* a price change. Desaturated on purpose: the persona
                                    closes any app that shows her red */
  --amber-wash:       #f6efe0;
  --clay:             #9a5842;   /* a genuine failure, ours and never the merchant's */
  --clay-wash:        #f3e9e5;

  --stone:            #e9eeef;   /* the quiet status chip */
  --stone-ink:        #4f5e62;
  /* --teal-wash AND --teal-ink WERE HERE UNTIL 2026-08-20, #dfeef0 and #185862, and
     they were the one COOL pair in a palette of warm ones. They existed for exactly
     one thing, the trial chip, and the founder retired that on the ground D-Concept
     already states: a status in this product is a quiet grey badge, never a colour.
     A hue family with one consumer stops being a family the day that consumer goes. */

  /* ==========================================================================
     PRIMITIVE - colour, the dark ramp
     NEW VALUES, and the only new ones in this file. kit.css has no dark theme,
     so there was nothing to carry. Derived from the same hues and measured
     against the dark grounds; every figure is written next to the role that
     uses it, below. Primitives are never overridden by a theme, so the dark
     values live here as their own names rather than as redefinitions.
     ========================================================================== */

  --paper-dark:       #161f21;   /* the surface. 1.10 against the dark canvas, which is
                                    the same relationship white has to the light canvas
                                    (1.12): the hairline does the separating, not the
                                    luminance. The Flat Paper Rule, in the dark */
  --canvas-dark:      #0e1517;
  --panel-dark:       #1d282a;

  --ink-dark:         #eef3f4;   /* the light canvas, folded back. The palette reuses
                                    itself rather than growing a value */
  --slate-dark:       #c3ced1;
  --muted-dark:       #96a2a6;

  --hairline-dark:      #2a3538;
  --hairline-soft-dark: #212b2e;
  --edge-dark:          #7f9296;

  --skeleton-dark:    #202b2d;

  --petrol-dark:      #6bb0ba;   /* LIGHTER, not darker. Contrast runs against the
                                    opposite ground, so the accent climbs the scale */
  --petrol-deep-dark: #7fbcc5;   /* and the pointer state climbs further, where in the
                                    light theme it descends */
  --petrol-tint-dark: #123037;

  --amber-dark:       #d6a951;
  --amber-wash-dark:  #2e2204;
  --clay-dark:        #d0a292;
  --clay-wash-dark:   #38201a;

  --stone-dark:       #232e31;
  --stone-ink-dark:   #a9b6b9;

  /* ==========================================================================
     PRIMITIVE - geometry. No roles, no theme, no dark pair: nothing here has
     anything to override. Components read these directly.
     ========================================================================== */

  --radius:      14px;   /* containers: the list, cards, the sheet */
  --radius-wash: 12px;   /* origin: the alert banner. Washes and callouts */
  --radius-sm:   10px;   /* controls: buttons, fields, rows, tiles */
  --radius-xs:    6px;   /* origin: the chip and the skeleton, written inline twice
                            in kit.css before it was named */

  --shadow: 0 1px 3px rgba(10,30,35,0.05), 0 8px 24px -12px rgba(10,30,35,0.12);
                         /* the ONE elevation in the system, and it belongs to the
                            dialog sheet alone. The Flat Paper Rule: a new surface
                            earns a hairline and a background change, never a shadow */

  --tap:   44px;         /* the floor a pointer target may not go under */
  --row:   56px;         /* the list row, comfortably over the floor */
  /* THE RAIL ROW, 2026-08-12, and the third size primitive because two were never
     enough. It is neither --tap (44) nor --row (56), and the gap between those two
     is exactly the problem: a destination in the desktop rail is not a bare target,
     so 44 is too tight for a row a person reads down a column of, and it carries one
     line of ink rather than two, so 56 is loose enough to make four destinations
     look like a list of subscriptions. Nor is it --space-48 wearing another hat. A
     min-height is not a margin, a padding or a gap, so the 8px spacing grid does not
     govern it, and folding it there would say that the height of a navigation row
     and the air between two blocks are the same KIND of measurement, which is the
     same mistake --ring, in the spacing block below, refuses for the focus outline.
     PROPOSED AT STAGE 07, DEFERRED TO STAGE 08, AND STAGE 08 DID NOT CREATE IT,
     which is why tab-bar.css:195 calls its own 48 "the one literal size in this
     file" and why the geometry triage closed with two literals still owed a token.
     ITS INTENDED CONSUMER IS `tab-bar.css:min-height: 48px`, in the 760 container
     query where the tab bar becomes the rail, AND IT HAS NOT BEEN REPOINTED YET:
     the component files are another agent's to edit. The name is declared first on
     purpose, because the rule is that a missing detail enters the system and the
     screen reads it second, never the reverse. Nothing moves when it is repointed:
     48 is 48.
     IT IS NOT alert-item.css's 40px. That file's own comment settles it: the 40 is a
     line box on a span with no href, not a rail row and not a target, and the anchor
     around it is what carries the floor. A second consumer at a different number
     would make this token a bag of sizes rather than a form.
     AN UNREAD TOKEN IS NORMALLY THE DEFECT THIS FILE ARGUES AGAINST, in the words
     it uses to refuse --opacity-disabled below. The refusal there holds because
     that token had no consumer and inventing one would have changed how every
     disabled control renders. This one has a consumer already written, named at its
     file and line, at the value it already serves. It is the --space-56 case rather
     than the --opacity-disabled case: a step in a scale, declared whole. */
  --tap-rail: 48px;      /* the rail form's row height */

  /* THE STACKING ORDER, 2026-08-20, FOUNDER, AND IT IS TWO NAMES FOR WHAT LOOKED
     LIKE THREE LITERALS. The whole product's z-index was `1` on the money field's
     currency prefix, `5` on the app bar and `6` on the tab bar, and the backlog
     asked for four stages whether three literals, two of them adjacent, earn a
     scale. They do not. What the two chrome values ARE is not two numbers but ONE
     RELATIONSHIP: where the app bar and the tab bar meet, the bottom chrome wins.
     `app-bar.css` already spent a paragraph saying so, and a comment that has to
     explain a relationship between two literals in two different files is a token
     waiting to be declared - the third literal moves and nothing tells the other
     two.
     THE `1` IS NOT HERE, and that is the whole shape of the decision. It is local
     to one grid item inside one field, it relates to nothing outside its own
     component, and naming it would make this a scale, which is the thing being
     refused. Two names for one relationship, not a ladder for a product that has
     three layers.
     UNITLESS, so they are integers rather than lengths, and they sit here with the
     geometry because a stacking order is geometry: nothing about them is a colour,
     a size or a step on a grid. Zero pixels move; both values are what they were. */
  --z-appbar: 5;         /* the top chrome */
  --z-tabbar: 6;         /* the bottom chrome, and it wins where they meet */

  /* ==========================================================================
     PRIMITIVE - the type scale
     Adopted at the founder's review of the foundations, 2026-08-11, and this IS
     a change of appearance: it is the second of the three legal sources, and
     every fold is a line in docs/tokens-audit.md.

     The product shipped 21 sizes and no variable for any of them, including five
     half steps (14.5, 13.5, 12.5, 11.5, 10.5) that existed for no stated reason.
     Eight steps replace them. The mapping was not chosen value by value: every
     old size goes to the NEAREST step, and a tie goes to the LARGER, which is the
     direction a product for anxious readers should round in. One exception, and
     it is named: 40px is nearer 46 than 32, and 46 is exclusive to the monthly
     total under the One Number Rule, so the share card's total goes down to 32.

     NAMED --type-*, NOT --text-*. The --text- prefix already belongs to the ink
     roles, and the first draft of this block called the scale --text-body, which
     collides head on with the colour role of the same name: the later declaration
     wins, so every component asking for the body ink would have been handed 14px.
     Caught by reading the file back before anything was wired to it. A namespace
     is not decoration; two things called the same name are one thing.

     rem AND NOT px SINCE 2026-08-13, founder's question, and it settles a
     contradiction this file was already carrying. The width block below declares
     both points in rem and gives the reason: a point asks how wide the window is
     RELATIVE TO THE TEXT THE PERSON SET. That reason is only true if the text
     actually moves with the setting, and a px scale does not move at all. A
     browser's own font-size setting changes the root size; an explicit px
     font-size ignores it. (Page zoom is a separate mechanism and scales px and
     rem alike, which is why this was invisible for two stages.)

     WHAT IT COST TO LEAVE IT IN px, MEASURED ON design/home.html at a 24px root:
     the text stayed 16 and 12px, and the 900px window fell BELOW the tablet
     point (47.5rem is 1140px at that root), so the rail went away, the page
     measure came off and .screen went from 680 to 900. The person who asked for
     larger text paid the whole layout change and got no larger text. That is the
     rem point working against the only person it was written for.

     THE SWAP MOVES NOTHING FOR ANYBODY ELSE, and that is measured rather than
     assumed: 8596 element records (box, font-size, line-height, weight) across
     ALL 32 coloured screens, at 390, 760, 1280 and 1600, px against rem,
     identical. RECOUNTED ON 2026-08-14 and the first number was wrong: it read
     7660 off a page list of 28 that predated the four Alerts screens of stage 09
     and included a design/index.html that does not exist. The finding did not
     move, the corpus did, and a count is live in exactly this way. The root font-size is set nowhere in this system, so
     at the browser default every step below is the number it always was. The
     conversions are exact, none of them rounds.

     THE GEOMETRY STAYS px ON PURPOSE. Spacing, radii, the 220px rail and the
     300px column floor are geometry, not text, and the same distinction that
     denies the width block a dark pair applies here. The reading measures were
     already text-relative before this (--container-text is 52ch) and every
     line-height in the system is already unitless, so both scale by themselves.
     ========================================================================== */

  /* THE ONE STEP ABOVE THE TOTAL, ADDED 2026-08-14 FOR THE LANDING, AND IT IS
     THE ONE NUMBER RULE BEING KEPT RATHER THAN BROKEN. Inside the app the
     biggest thing on a screen is a number the person owns, and 46px belongs to
     it alone. On the public page that number does not exist yet: the only figure
     there is an EXAMPLE, labelled "Example, not your data", and the biggest
     thing has to be the promise instead. Giving the landing's headline
     --type-display would put two different jobs on one step and quietly make the
     example look like somebody's real total. So the headline gets its own step
     above, and the example steps DOWN to --type-figure by wearing .amt.figure,
     which is the same move the share card's total made for the same reason.
     Result: --type-display still has exactly one job and the landing carries no
     `.total` at all.
     3.5rem AT THE TOP AND NOT MORE. 56px against the 46 below it is one clear
     step, and it is where restraint stops it: A5 in concept.md is warm restraint
     against clinical, and a 72px hero on a product built to lower anxiety is the
     poster reflex this language was chosen against.

     AND IT IS FLUID, WHICH IS THE FIRST ANSWER STAGE 10 ASKS FOR AND THE SECOND
     ONE I GAVE. The first draft of this token was a flat 3.5rem with a comment
     refusing fluid on the ground that there was no content reason for it. There
     was, and looking at the built page at 390 is what produced it: at a fixed
     56px the headline ran to FOUR lines and took the whole first screen, so the
     lead, both buttons and the proof panel were all below the fold on a phone.
     A headline that pushes its own call to action off the screen is not a
     headline. Fluid is not decoration here, it is the content reason the first
     comment said did not exist.

     THE MIN AND THE MAX ARE rem AND ONLY THE MIDDLE TERM IS vw, which keeps the
     promise the scale below makes. A person at a 24px root gets 60px at the
     floor and 84 at the ceiling, so the headline still moves with their setting;
     the vw term only decides where between the two ends a given width lands.
     Measured: 42px at 390, 52 at the tablet point, 56 from about 920 up. */
  --type-hero:    clamp(2.5rem, 2rem + 2.6vw, 3.5rem);  /* 40 to 56px. the landing
                              headline, and nothing else in the product */
  --type-display: 2.875rem;  /* 46px. THE display figure of a screen, and a screen
                                gets one: a second turns the first into a heading.
                                In the app that figure is the monthly total; on the
                                landing, which has no total, it is the plan price,
                                from 2026-08-15 on the founder's decision. The One
                                Number Rule is unchanged and the count per screen is
                                still one; the earlier wording named the app's
                                occurrence rather than the job. Ground:
                                amount.css, the display size */

  /* THE ORDINAL OF A STEP, ADDED 2026-08-15 ON THE FOUNDER'S WORD ("ети цифирки
     маленькие ну просто кошмар"). The landing's three steps used to mark
     themselves with --type-meta inside a 26px box, which is the size of a
     footnote wearing the job of a section's spine, and it read as one.
     40px AND NOT --type-figure. A step's number and a subscription's amount are
     not one role: the amount is money the person owns and reads, the ordinal is
     structure the person counts. They can part company at any moment (rule 4),
     and they already have: this one is set solid at weight 800 with the tightest
     tracking in the product, which no amount is allowed. Between --type-display
     and --type-figure it is one clear step from each, and it is the only number
     in the system that is not a number about money. It never carries a currency,
     a count or a total: those are --type-display, --type-figure and .amt. */
  --type-step:    2.5rem;    /* 40px. the ordinal of a step in a sequence, and it
                              is never money */
  --type-figure:  2rem;      /* 32px. a subscription's own amount, a plan price */
  --type-section: 2rem;      /* 32px. THE SUBJECT OF A SECTION ON THE PUBLIC
                              PAGE, past the tablet point. Added 2026-08-16 on
                              the founder's landing pass.

                              SAME VALUE AS --type-figure AND A SEPARATE TOKEN,
                              which is rule 4 of this system read literally: two
                              roles means two tokens even at one value today. One
                              of them is a number about money on an app screen
                              and the other is a heading on a marketing page, and
                              the day one moves the other has no reason to.
                              Borrowing --type-figure would also break the naming
                              this scale runs on: every step here is named for
                              its JOB, and that one says "a subscription's own
                              amount" in its own comment.

                              WHY THE STEP EXISTS AT ALL. The landing's section
                              headings rendered at --type-head, 24px, at every
                              width. That is the app's step, sized for the
                              subject of a 780px column, and the landing sets it
                              over a 1280px band under a 56px hero: measured on
                              the page, the argument's headings barely registered
                              against the cards under them. The grey has stepped
                              its own heading up at the same point since the
                              wireframes, 23px to 27px, and landing-shell.css
                              carried a rule inside the tablet query that set
                              --type-head a second time, which is the desync
                              written down in the code rather than found in a
                              browser: the step point was there and the step was
                              not.

                              A PHONE DOES NOT MOVE. Below the tablet point the
                              heading stays --type-head, because 24px over a
                              358px column is already the subject of its band. */
  --type-head:    1.5rem;    /* 24px. the subject of a page */
  --type-title:   1.25rem;   /* 20px. the subject of a block inside a page */
  --type-sub:     1rem;      /* 16px. a merchant name, a door, the reading line */
  --type-body:    0.875rem;  /* 14px. sentences, button labels */
  --type-meta:    0.75rem;   /* 12px. labels, hints, the second tier of information */
  --type-label:   0.625rem;  /* 10px. badges and the eyebrow */

  /* ==========================================================================
     PRIMITIVE - the spacing scale, an 8px grid
     Same review, same rule, same audit trail. The product shipped 27 spacing
     values and no variable for any of them, among them 1, 3, 5, 7 and 9, which
     are not a rhythm but the absence of one.

     The grid is 8, with 4 and 2 below it for the places where 8 is too much, and
     0 as a real value rather than the absence of a value. Above 32 the steps stay
     multiples of 8 because that is where the page frame lives. Nearest step, ties
     to the larger, exactly as the type scale.
     ========================================================================== */

  --space-0:   0;
  --space-2:   2px;      /* a hair: the gap under a row's second line */
  --space-4:   4px;
  --space-8:   8px;      /* the unit */
  --space-16: 16px;      /* the screen's side padding, the common block gap */
  --space-24: 24px;      /* between blocks */
  --space-32: 32px;      /* between sections */
  --space-40: 40px;
  --space-48: 48px;
  /* THE FOCUS RING, 2026-08-12, and it is ONE name for TWO properties on purpose.
     Found by the step 9 geometry triage as the largest repetition left in the
     system: `outline: 2px` and `outline-offset: 2px` written out in 18 places
     across 17 files, which is more occurrences than any spacing step has. The
     width and the offset are the same number BY DESIGN, because a ring that is
     thicker than its own gap reads as a border, and they have to move together or
     not at all. One name makes that impossible to get wrong.
     It is NOT --space-2. A ring is not a spacing step: reading the spacing scale
     here would say the gap between a control and its ring is the same KIND of
     measurement as the gap between two blocks, and the day the spacing scale
     moves, every focus ring in the product would move with it. */
  --ring:      2px;

  --space-56: 56px;      /* no consumer today, and it is KEPT, 2026-08-12. Found by
                            the step 9 audit as the one primitive nothing reads. A
                            scale is declared whole: pull 56 out and the ladder runs
                            48, 64, so the next thing that needs it writes a literal
                            and the rule this file exists to hold is the one that
                            breaks. An unused STEP is not an unused ROLE */
  --space-64: 64px;
  /* THE TWO LANDING STEPS, 2026-08-14. A public page breathes differently from
     an app screen: the app stacks blocks a person is working through, the
     landing stacks arguments a person is deciding between, and 64 between them
     reads as one long column rather than as separate claims. Both are multiples
     of 8 and both have a consumer the day they are declared (landing-shell.css,
     the section rhythm and the gap under a section heading), so neither is the
     --space-56 case of a step kept without a reader. */
  --space-80: 80px;
  --space-96: 96px;

  /* ==========================================================================
     PRIMITIVE - width. Stage 10, step 2. No roles, no theme, NO DARK PAIR, and
     the absence is a decision rather than an omission: a pair of themes is a
     property of the semantic level, which is to say of colour. A width, a
     container and a gutter are geometry, and geometry does not change direction
     in the dark. Do not look for a pair here and do not declare an empty one.

     THE REGISTRY OF POINTS, AND IT IS A REGISTRY BECAUSE CSS FORCES IT TO BE.
     A media or container query is resolved BEFORE the variable cascade, so
     `@container (min-width: var(--bp-tablet))` does not error and does not warn,
     it simply never matches. The number therefore has to be written twice: here,
     where it is the source of truth, and as a literal inside each query. That is
     not two sources of truth, it is a register and its application, and this
     stage turns the constraint into an instrument: EVERY width query in
     design/system/ must resolve to a number declared in this block, and to no
     other number. Checked by grep at step 6, on both instruments.

     rem AND NOT px, AND THE REASON IS A PERSON RATHER THAN A DEVICE. A point in
     px asks how wide the window is. A point in rem asks how wide the window is
     relative to THE TEXT THE PERSON SET, so somebody reading at a 24px root gets
     the one-column form until there is genuinely room for two, instead of
     sitting at a "desktop" width with a phone's worth of words in a line. At the
     browser default of 16px these are 760px and 900px exactly, and the root
     font-size is set nowhere in this system, so naming them in rem moves zero
     pixels for everybody who has not asked for larger text.

     THE VALUES ARE NOT NEW, AND THAT IS THE WHOLE POINT OF THE STAGE. Both ran
     in the code before it. What they lacked was a name, an origin and a register:
     the census in docs/responsive.md, part A, found 18 different width numbers in
     the product and not one token, which is exactly how a third point gets
     declared on top of two that nobody had noticed.

     THE LITERALS IN THE COMPONENT FILES ARE STILL px AND ARE REPOINTED AT STEP 4,
     when those files are opened. Stated here rather than left to be discovered,
     because a register that does not yet match its application is a defect the
     moment it stops being written down.
     ========================================================================== */

  --bp-tablet:  47.5rem;   /* 760px. AUDIT ROW: the shell, docs/responsive.md.
                              WHAT BREAKS BELOW IT: the two-column shell cannot
                              exist. The rail needs 220px (measured floor 180,
                              app-shell.css) beside a content column that still
                              has to hold a subscription row, and a row's own
                              floor is 300px. WHY FLUID CANNOT DO IT: a bottom tab
                              bar fixed to the glass and a left rail in a grid
                              track are two behaviours, not one stretched. Nothing
                              interpolates between them.
                              Read today by app-shell, app-bar, tab-bar, card,
                              grid, chart-placeholder, dialog-sheet and the
                              list-column pattern.
                              THE NUMBER IS THE CONTAINER'S AND NOT THE WINDOW'S,
                              2026-08-13. Every query that reads this point is a
                              `@container` and the container is `body`, so a
                              classic scrollbar sits OUTSIDE the box it measures:
                              the shell flips at a 775px window on Windows and
                              Linux and at exactly 760 where the scrollbar is an
                              overlay. Measured one pixel at a time, table in
                              docs/responsive.md. --bp-desktop below carries no
                              such offset, because past this point the pane is its
                              own scroller and the document stops scrolling. */

  --bp-desktop: 56.25rem;  /* 900px. AUDIT ROWS 7 and 8, Home and Subscription
                              Detail. WHAT BREAKS ABOVE IT: the content stops
                              being a column. The blanket 780px measure comes off,
                              the screen becomes an explicit flex column so its
                              blocks can be reordered, the dashboard head takes
                              two tracks and the detail takes two. WHY FLUID
                              CANNOT DO IT: reordering blocks is not a stretch,
                              and a measure that comes off is a switch.
                              Read today by app-shell, dashboard-head,
                              category-group and groups-column-set. */

  /* THERE IS NO --bp-wide, AND THERE IS NO THIRD NUMBER LEFT TO TOKENIZE.
     groups-column-set.css carried a 1340 until 2026-08-13. What it switched was a
     COUNT, two columns to three, and a count is the one thing this stage refuses
     to switch by hand: measured, the step put a category group at 269px, which is
     31px under the 300px floor that same file declares its rows need, and it held
     it there for a 60px band. Founder's decision at step 4: the set takes
     `columns: 300px` and counts its own, capped at three by the arithmetic. The
     number is gone from the product rather than promoted to a third point, so
     this register and the code now agree completely: every width query in
     design/system/ resolves to one of the two values above. */

  --container-page:   48.75rem;  /* 780px. The widest a screen's content block is
                                    allowed to get between the two points, from
                                    app-shell.css. Above --bp-desktop it HANDS OVER
                                    to --container-wide below; until 2026-08-13 it
                                    came off entirely, and this comment carried the
                                    open question of whether that was right. It was
                                    not, and the founder's report is what settled
                                    it. It is still the reason the four reading
                                    measures below exist at all. */

  --container-wide:   80rem;     /* 1280px. THE WIDEST A SCREEN'S CONTENT GOES,
                                    above --bp-desktop. Added 2026-08-13, founder's
                                    decision, ground in docs/decisions.md.

                                    WHAT IT REPAIRS. Past the desktop point every
                                    block took the whole pane while the category
                                    column set stopped at its own 996px cap, so the
                                    two right edges disagreed by more the wider the
                                    window got: 144px at a 1440 window, 304 at 1600,
                                    624 at 1920. The screen did not run out of
                                    content, it ran out of agreement.

                                    WHY 80rem AND NOT A ROUNDER GUESS. It is the
                                    only band that keeps "three columns is the
                                    widest" true by ARITHMETIC rather than by a
                                    remembered number: a fourth column of the 300px
                                    floor needs 1344px (300 * 4 + 48 * 3), so any
                                    cap below that can never grow one. 1280 sits
                                    above the three-column minimum of 996 and below
                                    1344, so the set fills its measure with exactly
                                    three columns at every width. In rem for the
                                    same reason the points are: a person reading at
                                    a larger root gets the narrower content sooner.

                                    Read by app-shell.css, in the block above
                                    --bp-desktop, where it replaced
                                    `max-width: none`. It is a CAP and not a target:
                                    a screen narrower than this is not padded out to
                                    reach it. */

  --container-column: 38.75rem;  /* 620px. The single-column cap: a flow screen
                                    centred in the pane, and the list column of
                                    the list-column pattern. Two containers and
                                    not one, because a screen that is a form and a
                                    screen that is a pane of content stop at
                                    different places, and folding them would move
                                    ten flow screens to make one number tidier. */

  --container-text:   52ch;      /* THE READING MEASURE, and it is 52 rather than
                                    the 60-75 of a text page because it was read
                                    off this product rather than off a guideline.
                                    The corpus had four: 46ch (readout), 48ch and
                                    50ch (text block, body and status), 52ch
                                    (empty block, ruled muted line). 52 is the
                                    mode. Tendd has no long-form prose: its
                                    longest continuous text is a paragraph that
                                    explains a charge, and a 65ch line would make
                                    a two-sentence explanation look like an
                                    article. Step 4 folds the four with a measured
                                    report on each, and any that genuinely differ
                                    stay as a documented exception rather than
                                    being rounded into agreement. */

  --grid-gap:     var(--space-8);  /* the air in a grid of equal children. The
                                      door row takes --space-16 and overrides
                                      locally, on the axis grid.html already
                                      declares: gap follows the child's WEIGHT,
                                      and a bordered door is heavier than a tile. */

  --grid-col-min: 10rem;           /* 160px, the DEFAULT narrow floor of a grid
                                      child, measured rather than chosen: at a 360
                                      viewport the content zone is 328px and the
                                      preset tile row stands two across with an
                                      8px gap, which is 160px each.
                                      THE COLUMN COUNT IS NOT A TOKEN AND WILL NOT
                                      BECOME ONE. It is counted by the grid from
                                      this floor, so a width nobody predicted still
                                      gets an answer. A child whose floor is not
                                      160 overrides this value in its OWN file,
                                      which is where grid.css has said the floor
                                      belongs since stage 09: "a floor per child,
                                      with the floor in the child's own file".
                                      Wired at step 4, per child and with the
                                      arithmetic shown, because one floor
                                      demonstrably cannot reproduce two columns at
                                      328px and three at 780px. */

  /* ==========================================================================
     MOTION. Stage 11, step 2, 2026-08-16. The fifth thing this file holds,
     after colour, type, space and width.

     IT SITS IN THE PRIMITIVE HALF AND CARRIES BOTH ITS LEVELS INSIDE ITSELF,
     which is not a third level and not an exception hunting for one. The
     primitive/semantic split of this file is a split about the THEME AXIS: a
     colour is named twice because the same role points at a different value in
     the dark, and geometry is named once because no theme moves a radius. Motion
     has no theme axis either, so it belongs on this side of the file. What it
     does have is the other reason a second level exists, and rule 4 is that
     reason read from the other end: **a second level earns its place exactly
     where two roles would otherwise spell the same value.** Today
     `--ease-arrive` and `--ease-state` are both expo-out and can part company
     (a hover is not an entrance), so the curve itself is named once and the two
     jobs point at it. The durations share nothing, so they are named once, by
     job, and there is no shape level under them.

     THE VALUES ARE NOT NEW AND ARE NOT RE-DERIVED. Four of the five curves were
     measured and named on the candidate hero on 2026-08-15, in
     landing-orbit.css, on one selector inside `@supports`, with the note that
     "stage 11 owns the motion scale, and when it runs these go to tokens.css as
     primitives with their roles named there". This is that move. Nothing about
     the landing's motion changes by a millisecond; what changes is that four
     other files stop spelling one of them by hand.

     WHY THE HAND-SPELLING HAPPENED, because it is the argument for this block in
     one line: a custom property declared on one component's pin cannot be read
     from another component, so landing-facts.css, landing-paths.css,
     landing-plan.css and landing-steps.css each wrote
     `cubic-bezier(0.16, 1, 0.3, 1)` for their own entrance. Five files, one
     curve, one name and four literals. That is not the drift a token might
     prevent; it is drift that had already happened when the census counted it.

     THERE IS NO SCALE OF DURATIONS AND THIS FILE WILL NOT INVENT ONE. A scale
     needs a range the product moves across, and this product moves across two
     values: a control answering a pointer, and a placeholder breathing while it
     waits. The landing's own numbers are NOT candidates: 47/39/33/43/36s for the
     marquee rows and 13.3 to 19.3s for the eight strands are LOOP LENGTHS,
     deliberately mutually prime so that nothing on that stage ever repeats in
     step with anything else, and a loop length is not a duration in the sense a
     scale means it. They stay literals in the file that owns the loop.

     ONE PRINCIPLE IS RECORDED HERE WITH NO TOKEN UNDER IT, on purpose: an exit
     is always shorter than its entrance. It governs the landing today through
     `--ease-leave` plus a shorter range, and it has no consumer in the app,
     because the app has no entrances yet. A token with no consumer is how a
     scale starts being fiction, so the rule is written and the token is not.
     ========================================================================== */

  /* ---- the curves, level one: named by SHAPE ------------------------------ */

  --curve-expo-out: cubic-bezier(0.16, 1, 0.3, 1);
                         /* covers the distance early and spends the rest closing
                            the last few pixels. What an object landing looks like */
  --curve-expo-in:  cubic-bezier(0.6, 0, 0.85, 0.2);
                         /* its mirror: slow to let go, then quick */
  --curve-firm:     cubic-bezier(0.45, 0, 0.22, 1);
                         /* accelerates and decelerates evenly. For a distance long
                            enough that expo-out reads as a rocket that then creeps */
  --curve-spring:   linear(0 0%, 0.11 6%, 0.38 17%, 0.66 27%, 0.85 36%,
                           0.95 45%, 1.008 56%, 1.015 66%, 1.006 80%, 1 100%);
                         /* one overshoot of one and a half per cent at 66. Any more
                            and a calm product starts bouncing; this much is a card
                            seating itself. `linear()` needs no @supports guard where
                            it is DECLARED - a custom property is an unparsed token
                            stream - and its only use is already inside the scroll
                            timeline's guard, where every engine has had linear() for
                            longer than it has had a timeline */

  /* ---- the curves, level two: named by JOB -------------------------------- */

  --ease-arrive: var(--curve-expo-out);
                         /* everything that comes ON. Founder, 2026-08-15, on a stage
                            that was already doing everything it had been asked to:
                            "make it soft". Every animation there ran on `linear`, and
                            on a scroll timeline linear is not a neutral default but
                            the strongest decision in the file: it welds the object to
                            the scrollbar, so nothing accelerates, arrives or settles */
  --ease-leave:  var(--curve-expo-in);
                         /* everything that goes OFF, and an exit is always shorter
                            than an entrance */
  --ease-settle: var(--curve-spring);
                         /* used in exactly one place, the deck closing over a
                            cancelled plate, because that is the one moment on that
                            stage where something should feel like an OBJECT arriving
                            at a stop rather than a value being interpolated */
  --ease-travel: var(--curve-firm);
                         /* one journey: the two figures crossing most of a viewport
                            on the wide stage, ten times any other travel there */
  --ease-state:  var(--curve-expo-out);
                         /* A CONTROL ANSWERING A POINTER OR A KEY, and it is its own
                            role at the same value as --ease-arrive by rule 4. They do
                            different jobs and can part company: an entrance is a thing
                            appearing and a state change is a thing already on screen
                            saying it heard you. Expo-out is right for both today
                            because both should feel answered immediately and land
                            softly, which is the whole shape of the curve */

  /* ---- the durations, one level, named by job ----------------------------- */

  --dur-state: 150ms;    /* the state change: a hover, a press, a selection.
                            CHOSEN RATHER THAN COPIED. Under about 100ms a change
                            reads as a cut and the transition buys nothing; over
                            about 200ms a hover starts trailing the pointer, which
                            on a list of rows is the opposite of calm. 150 is the
                            middle of that window on the 10ms grid. The stand's own
                            chrome has run 0.22s and 0.15s since stage 07 and is not
                            the source: the stand is not the product */
  --dur-press: 90ms;     /* A PRESS, and it is faster than a hover on purpose. The
                            founder, 2026-08-16, on the first colour-only pass:
                            "большинство анимаций я не вижу". Part of the answer is
                            that things now move; the other part is that a press has
                            to answer a finger that is ALREADY down, so 150ms reads
                            as lag where the same 150ms on a hover reads as soft.
                            The release goes back on --dur-state: quick to take, calm
                            to let go, which is the same asymmetry --ease-leave
                            carries on the public page */
  --dur-signature: 900ms;/* THE BRAND'S ONE MOVE, and the only duration in this
                            product that is not answering a person. Long because it
                            is an arrival rather than a reply: a hover has to keep
                            up with a hand, and this has to be watched. It runs
                            ONCE, on ONE surface, and the modifier that carries it
                            is the fence. See brand-mark.css, the signature */
  --nudge: 2px;          /* THE ONE DISTANCE ANYTHING IN THIS PRODUCT MOVES, and it
                            is the whole guard on the movement the founder asked for.
                            An arrow advances by it, a mark lifts by it, a control
                            presses down by it, and nothing anywhere travels two.
                            2 rather than 1 because 1px vanishes on a 2x screen at
                            36px, and rather than 4 because at 4 a list of fourteen
                            rows starts jumping under the pointer instead of
                            answering it. It is geometry, so it is a primitive read
                            directly and it has no role above it: what a nudge MEANS
                            is written by the verb that spends it, in the component */
  --dur-pulse: 1.4s;     /* the loading placeholder breathing. Written as a literal
                            in skeleton-bar.css since stage 07, with the note on that
                            component's page that "stage 11 owns motion and will put
                            the duration behind one". This is that. The value did not
                            move: it is slow enough to read as waiting rather than as
                            activity, which is the one thing a skeleton must not
                            promise */

  /* ==========================================================================
     SEMANTIC - fills
     ========================================================================== */

  --bg-page:      var(--canvas);
      /* the ground under every screen: all 32 coloured pages (home, subscription-detail,
         settings). fill, 3:1 not applicable, nothing sits behind it.
         origin: DESIGN.md > Colors > Neutral, Canvas #eef3f4, "the ground the paper sits
         on, and the callout fill". Today: 1 var(), 1 file, at the floor of this file's
         counts and correctly there: the shell paints the ground once and every screen
         inherits it, so a second usage would be a screen repainting what it stands on */
  --bg-surface:   var(--paper);
      /* card, row, app bar, tab bar: 25 usages in kit.css (.app, .appbar, .panel).
         fill. 1.12 against --bg-page in both themes, by design: the hairline separates.
         origin: DESIGN.md > Colors > Neutral, Paper #ffffff, "cards, rows, the app bar,
         the tab bar. The product's content surface". Today: 20 var(), 17 files */
  --bg-recessed:  var(--panel);
      /* row hover, the desktop rail, a disabled control: 14 usages (.row:hover,
         .tabbar at 760, .btn[disabled]). fill.
         origin: DESIGN.md > Colors > Neutral, Panel #f4f7f8, "the row hover and the
         desktop rail". Today: 11 var(), 8 files. The DESIGN.md entry hands one value the
         resting job and the hover job in one breath, which is the sentence --bg-hover
         below was split out of: the document recorded the pair before the file did */
  --bg-callout:   var(--canvas);
      /* the wash inside a card: 5 usages (.wash, the logo placeholder, .chip.cancelled).
         SAME VALUE as --bg-page today, different role: a page ground and a raised
         block's fill can part company, and in the dark half below they already have.
         origin: DESIGN.md > Colors > Neutral, Canvas #eef3f4, and its entry gives the one
         value BOTH jobs in one line, "the ground the paper sits on, AND the callout
         fill", so the split was latent in the prose before it was a token.
         Today: 3 var(), 2 files */
  --bg-selected:  var(--petrol-tint);
      /* the current choice: 8 usages (.tile[aria-pressed], .range button[aria-pressed],
         .trust a). fill. taste: attribute "calm".
         origin: DESIGN.md > Colors > Primary, Petrol Tint #e7edee, "backs the Pro label
         and the selected row in the master pane, and is the only place the accent appears
         as a surface". Today: 2 var(), 2 files */
  --bg-attention: var(--amber-wash);
      /* a price change: 4 usages (.wash.attention, .charges li.marked). fill.
         origin: DESIGN.md > Colors > Secondary, Amber "#8a5c0c on a #f6efe0 wash", "a
         price change. Warm, readable, and never red". Today: 2 var(), 2 files */
  --bg-failure:   var(--clay-wash);
      /* OUR failure, never the merchant's: 3 usages (.wash.error, the invalid field).
         fill.
         origin: DESIGN.md > Colors > Secondary, Clay "#9a5842 on a #f3e9e5 wash", "a
         genuine failure, and only a genuine failure". Today: 3 var(), 3 files */
  --bg-status:    var(--stone);
      /* the quiet chip: 1 rule, and it is the chip base every status badge in the
         product shares, so it repeats on 36 pages. fill.
         origin: DESIGN.md > Components > Badges > Status, "grey on grey (#4f5e62 on
         #e9eeef), 6px radius, not uppercased"; the value has no entry in the Colors
         section, it is named where the badge is. Today: 1 var(), 1 file, which is the
         same 1 rule the kit-era figure counted: this role never grew and never had to */
  /* --bg-trial AND --text-trial WERE HERE, and they are the third pair this system
     has retired for one reason: a tone with no wearer. `.tone-attention` went at
     stage 08, `--line-container-hover` went this morning, and the trial pair goes
     now. THE GROUND IS D-CONCEPT rather than tidiness: status is a quiet gray badge
     and a trial is a status. The one place in the product that says Trial, the
     Peloton row on Home, was already rendering the quiet tone, because its markup
     never carried the modifier - which is how a rule and its wearer can both be
     missing and neither one show up. Zero pixels move anywhere. */
  --bg-placeholder: var(--skeleton);
      /* loading: 2 usages on 89 grey pages (.skel, .logo.is-skel). fill.
         origin: DESIGN.md > Colors > Neutral, Skeleton #e2e9ea, "loading placeholders,
         pulsing at 1.4s and stopped entirely under prefers-reduced-motion".
         Today: 2 var(), 2 files */
  --plot-line:    var(--petrol);
      /* THE PLOTTED LINE, and PETROL'S FOURTH JOB. Added 2026-08-18 by the founder,
         in one sentence: "давай теперь черним поменяем на брендовий, чуть градиентов
         добавим". Until that sentence the line was --text-body and this file, the
         component and its page all carried the same refusal: petrol is spent on the
         primary action, the current selection and the trust line, and a data line
         would be a fourth claim on the one raised voice the system allows. The
         refusal was correct and the founder is the only person who could lift it.
         They did. D-Concept is amended in CLAUDE.md, and the ground is in
         docs/decisions.md under D-Plot.
         WHY IT SURVIVES THE ONE VOICE RULE. The fourth job is not a fourth PLACE: a
         screen that carries a plotted line carries exactly one, and no screen in the
         product carries a plot and a filled action inside the SAME ZONE -
         history-trends puts its only filled button below the trend list, and on the
         Pro gate the plot stands above the lock while the filled action is inside it.
         Counted rather than assumed: 2 elements on 2 screens, one plot each.
         IT READ "the Pro gate has no line at all. 1 element, 1 screen" UNTIL LATER
         THE SAME DAY, and it was true when it was written: the gate drew an empty
         frame. Then the founder gave the free person one real comparison and the gate
         gained a two-month plot of its own, which is the count moving under a
         sentence that was measured correctly and never re-run. Found by the stage 13
         critique.
         Read by chart-placeholder.css for the line, the cursor's dot, the value in
         the readout card, and, inside a @supports, the fill of the area under it. */
  --plot-area:    var(--petrol-tint);
      /* THE GROUND UNDER THE LINE, and it is the FLAT FALLBACK since the founder
         asked for a gradient. Where CSS masking is available, the area takes
         --plot-line and a mask fades it from the curve down to nothing, which is the
         "чуть градиентов"; where it is not, this flat tint is what paints, and it is
         the tint the selected wash already uses, so the two can never disagree about
         what a petrol wash is. It was --stone until 2026-08-18, when the line stopped
         being --text-body and a neutral ground under a coloured line stopped making
         sense. Both themes. */
  --bg-measure:   var(--edge);
      /* THE FILLED PART OF A BAR THAT STATES A QUANTITY. Added 2026-08-19 with the
         category bars, when the founder asked why "By category" was a sentence and
         not a picture. fill, and a NON-TEXT one, which is the whole reason it is
         --edge and not a grey from the text ladder: --edge is the founder's non-text
         contrast decision and it is the only neutral in this file that is guaranteed
         to be seen as a shape.
         IT IS DELIBERATELY NOT PETROL, and that is the decision worth keeping. The
         bars sit on the same screen as the plotted line, and D-Concept spends petrol
         on one element per screen. The line is that element here. A bar chart that
         borrowed the same colour would make the screen argue with itself about which
         picture is the important one, and the answer is the line: the bars say where
         the money goes, the line says how the total moved, and only the second is
         what the person came for. Both themes. */
  --bg-action:    var(--petrol);
      /* the ONE filled action of a zone: 33 buttons across the product.
         fill. taste: the primary action is one of petrol's four permitted jobs.
         origin: DESIGN.md > Colors > Primary, Petrol #1c6a76, "the identity colour and
         the only saturated thing on a screen", spent on four jobs of which this is the
         first. Today: 2 var(), 1 file, and the collapse from 33 to 2 is the whole point
         of the stage: 33 buttons became one .btn.primary that 33 buttons wear.
         THIS COMMENT WAS MOVED BACK UP HERE ON 2026-08-12. The 2026-08-12 split below
         was written between the declaration and its comment, so the paragraph naming
         "the ONE filled action of a zone" had come to sit under --text-chip-accent and
         read as that role's. Nothing but the comment moved */
  /* THREE ROLES SPLIT OFF ON 2026-08-12, sixth named source, and not one pixel moves:
     each keeps the value it is splitting from. The rule is this system's own, that two
     roles are two tokens even at one value today, and the test is whether they can ever
     part company.
     --control-accent  was --bg-action. accent-color is the ONE hook a native checkbox,
       radio and progress give us, and a control is not "the single filled action of a
       zone". The first time a checkbox is designed they separate.
     --bg-chip-accent / --text-chip-accent  were --bg-selected and --text-action. The
       Best value badge is STATUS. It was borrowing the fill of a selected control and
       the ink of the current destination, so a change to what "selected" means would
       have repainted a badge that is not selectable at all. */
  --control-accent: var(--petrol);
      /* origin: DESIGN.md > Colors > Primary, Petrol #1c6a76. DESIGN.md has NO entry for
         a control accent, and that is not an omission: at stage 07 there was no checkbox
         in the product, so the role is younger than the document and inherits only the
         primitive's origin. Today: 2 var(), 2 files (base.css and checkbox.css) */
  --bg-chip-accent: var(--petrol-tint);
      /* origin: DESIGN.md > Components > Badges > Pro, "petrol on petrol tint, the one
         badge allowed to carry the accent, because it names a plan rather than a state".
         That clause is the split's own argument, written before the split: a badge that
         names a plan is not a selected control, so it cannot go on borrowing the fill of
         one. Today: 1 var(), 1 file */
  --text-chip-accent: var(--petrol);
      /* origin: the same Badges > Pro entry, which names the pair and not the fill alone.
         Today: 1 var(), 1 file, the same rule as its fill.
         AN INK ROLE DECLARED IN THE FILL BLOCK, beside the fill it was split with rather
         than among the inks, because the pair is the thing the split created and parting
         them on the page would invite the next reader to part them in the product. The
         -text prefix still says which surface it answers to. It is the one ink role in
         this file with no measured pair standing next to it; that figure was not taken
         at the split and is not invented here */
  --bg-action-strong: var(--petrol-deep);
      /* the same action under the pointer: 3 usages (.btn.primary:hover). fill.
         origin: DESIGN.md > Colors > Primary, Petrol Deep #175a64, "its pressed and hover
         state". Today: 4 var(), 2 files, and the second file is the news: checkbox.css
         reads it too, so the role outgrew the ".btn.primary:hover" the kit-era note
         scopes it to. A pressed state is a pressed state whatever is being pressed */

  /* THE ONE ROLE IN THIS FILE THAT IS NOT A COLOUR, ADDED 2026-08-15 ON THE
     FOUNDER'S WORD ("может на фон какую ненавязчивую спокойную картинку сделать").
     It is an IMAGE and it is here rather than in the component for one mechanical
     reason: rule 6 says a role lives in both themes or it does not exist, and a
     wash that reverses between a paper theme and an ink theme is exactly the kind
     of paint that rule was written for. The alternative was a `[data-theme]`
     selector inside landing-plan.css, and there are ZERO of those in the 68
     component files today: the whole point of two token levels is that a
     component never asks which theme it is in.
     A `--wash-` prefix and not `--bg-`, because it is not a fill and cannot be
     read as one: it needs `background-color` UNDER it in every use, and the
     component says so.
     THE PATH IS WRITTEN FROM components/ AND NOT FROM HERE, and that is a
     browser fact rather than a preference. A `url()` inside a custom property is
     resolved against the stylesheet that USES the property, not the one that
     declares it: written as `assets/...` this token asked for
     design/system/components/assets/ and returned 404, measured on the page
     before it was corrected. Every consumer of a token is one folder deep in
     components/ or patterns/, so `../assets/` is the one path that is right from
     all of them. The asset itself sits at design/system/assets/, which is where
     the system's own picture belongs: it is liftable whole and carries it.
     SCOPE, and it is the decision: ONE band. Not the page, not the trust band,
     not a card. A texture that spreads stops being quiet, and D-Concept's ground
     is paper.
     THIS IS THE THIRD PICTURE AND THE FIRST ONE THAT IS ACTUALLY A PICTURE, and
     the two before it are worth knowing about because each was killed by the same
     mistake. The first pooled its haze in one corner and, cropped by `cover`,
     read as a smudge at the right-hand edge. The second was symmetrical and
     correct and had been LIGHTENED almost to nothing, because every pixel of it
     was made to clear 4.5:1 against the smallest ink on the band; the founder,
     twice: "это ужасно", then "не оно очень скучное". Both are the same error -
     the ASSET was being asked to carry the contrast, so the only asset that
     passed was one with nothing in it.
     THE THIRD SOLVES IT IN THE COMPOSITION INSTEAD, and that is why this one can
     be strong: landing-plan.css lays the band's own ground over the picture at
     BOTH ends, so the picture is a horizon between the cards' tops and their
     feet and no word in the band stands on it at all. The asset's own extremes
     are irrelevant to contrast and the RENDERED pairs are what is measured:
     5.78:1 at the worst, swept over nine widths in both themes, on a ground of
     255,255,255 - the ink's own token contrast on bare paper, because no word
     in that band stands on the picture at all. It is also loaded past the
     tablet point ONLY: below it the three cards are a stack rather than a row,
     a stack has no horizon, and a query that does not match never fetches its
     image. A replacement picture runs the same sweep; it is under no obligation
     to be pale, and it may not change the band's height without the fade being
     read off the page again. The stops are in landing-plan.css with their sweep.
     Today: 1 var(), 1 file */
  --wash-pricing: url("../assets/plan-wash-light.webp");

  /* THE SECOND PAINT, 2026-08-16, and it is an amendment to D-Wash rather than a
     second helping of it. A horizon: still water under a pale sky here, the same
     water at dusk in the dark theme, in loose watercolour on paper. It is a
     different MOTIF from --wash-pricing's mist on purpose - the same palette and
     the same hand, but a page that ends on the picture it already showed reads
     as a page that ran out of ideas, and a horizon is what an ending looks like.
     D-Wash said ONE band, on the ground that a texture which spreads stops being
     quiet. Two is now the number and the edge is narrower than it sounds: the
     two bands that carry a picture are the two that ASK, one for money and one
     for the sign-up, and the seven that argue carry none. The next block that
     wants a picture answers that rule.
     The path is written from components/ for the reason --wash-pricing records:
     a url() in a custom property resolves against the sheet that USES it.
     Loaded past the tablet point only, by landing-final.css: a query that does
     not match never fetches its image, so a phone downloads neither of these.
     Today: 1 var(), 1 file */
  --wash-final: url("../assets/final-wash-light.webp");

  /* ==========================================================================
     SEMANTIC - inks. Threshold 4.5:1, or 3:1 from 24px / 19px bold.
     ========================================================================== */

  --text-primary: var(--ink);
      /* headings, merchant names, amounts: 36 usages, all 28 pages. ink.
         17.4:1 on surface light, 15.0:1 on surface dark.
         origin: DESIGN.md > Colors > Neutral, Ink #141b1d, "every heading and every
         number that matters. Near-black, not black". Today: 29 var(), 23 files */
  --text-body:    var(--slate);
      /* sentences and button labels: 30 usages (.btn, .textblock p, .promises li). ink.
         10.2:1 light, 10.4:1 dark.
         origin: DESIGN.md > Colors > Neutral, Slate #384349, "body copy inside callouts
         and dense blocks". Today: 21 var(), 19 files, and the 19 files are wider than
         that entry: the document scopes slate to callouts and dense blocks, the role
         serves every sentence and every button label in the product. The entry describes
         where the value was FOUND, the role describes what it now answers for */
  --text-muted:   var(--muted);
      /* the single secondary tier, and there is no third: 59 usages, the largest role
         in the system (.muted, .row .when, .field label). ink.
         5.8:1 light, 6.4:1 dark. taste: the Two Tiers Rule.
         origin: DESIGN.md > Colors > Neutral, Muted #5a686c, "the single secondary tier.
         There is no third, lighter tier anywhere, which is what keeps every
         text-on-surface pair above WCAG AA". Today: 44 var(), 30 files, still the
         largest role in the file by both measures and in both corpora, which is what
         having exactly two tiers costs and what it buys */
  --text-action:  var(--petrol);
      /* the trust link and the current destination: 9 ink usages (.trust a,
         .tabbar a.cur, .next:hover). ink, and the SAME VALUE as --bg-action with a
         different role and a different threshold: one is a fill at 3:1, this is text
         at 4.5:1. 6.2:1 light, 6.8:1 dark.
         origin: DESIGN.md > Colors > Primary, Petrol #1c6a76, the second and third of
         its four permitted jobs, "the current selection", "the trust line's shield" and the
         plotted line.
         Today: 17 var(), 10 files */
  --text-on-action: var(--paper);
      /* the label standing ON a filled action: `.btn.primary`, and the accent
         chip. ink. 6.2:1 light. In the dark theme it is NOT paper: a white label
         on the lightened petrol would fail, so it takes the dark canvas at 7.5:1.
         It read "6 usages (.btn.primary, .btn.inverse)" until step 6 deleted
         `.btn.inverse`, which was worn on zero pages in either corpus and was the
         only other reading of this role in button.css.
         origin: DESIGN.md > Components > Buttons > Primary, "petrol fill, white label,
         no border of its own"; the value is Paper #ffffff, but its Colors entry names
         surfaces and this is the one place the paper value is ink.
         Today: 2 var(), 1 file, and both are button.css. THE ACCENT CHIP NAMED ABOVE NO
         LONGER READS THIS ROLE: --text-chip-accent split off on 2026-08-12 and took it,
         so the sentence above is true of the day it was written and not of the count */
  --text-attention: var(--amber);
      /* the price-change sentence: 3 usages. ink. 5.1:1 on its own wash light,
         7.2:1 dark.
         origin: DESIGN.md > Colors > Secondary, Amber #8a5c0c, "a price change. Warm,
         readable, and never red. A price going up is news, not an emergency".
         Today: 1 var(), 1 file (wash-block.css). The marked charge in charge-list.css
         reads the FILL alone and sets no ink of its own, letting the row inherit, so
         the ink half of this pair narrowed to one consumer while the fill kept two */
  --text-failure: var(--clay);
      /* the failure sentence and the invalid hint: 5 usages. ink. 4.6:1 on its own
         wash light, which is the tightest pair in the system, 7.3:1 dark.
         origin: DESIGN.md > Colors > Secondary, Clay #9a5842, "a genuine failure, and
         only a genuine failure ... An empty state never gets clay, because nothing went
         wrong in an empty state". Today: 2 var(), 2 files */
  --text-status:  var(--stone-ink);
      /* the quiet chip's label: 1 rule, 36 pages. ink. 5.8:1 on --bg-status light,
         6.7:1 dark.
         origin: DESIGN.md > Components > Badges > Status, the ink half of "grey on grey
         (#4f5e62 on #e9eeef)". Today: 1 var(), 1 file */

  /* ==========================================================================
     SEMANTIC - lines. Threshold 3:1 for anything that identifies a control;
     a decorative separator carries no threshold and says so.
     ========================================================================== */

  --line-control:  var(--edge);
      /* the boundary that IDENTIFIES a control: 6 usages (.input, .select, .btn,
         .tile, .door, .range button). line, 3:1 required.
         3.46:1 on surface light, 3.10:1 on canvas light; 5.15:1 dark.
         origin: DESIGN.md > Colors > Neutral, Control Edge #7b8d91, "the boundary of a
         control, and nothing else ... the lightest value on the hairline family that
         clears 3:1 on all three grounds a control sits on: paper 3.46, canvas 3.10,
         panel 3.22". Today: 7 var(), 7 files, one apiece, which is what a role scoped
         to a kind of object rather than to a place looks like when it is being obeyed */
  --line-container: var(--hairline);
      /* the edge of a card, panel or list: 26 usages. line, DECORATIVE: nothing has
         to be found by touch, so 1.23:1 light is deliberate and 1.33:1 dark matches it.
         Raising this was rejected on 2026-08-11: it outlines the whole product.
         origin: DESIGN.md > Colors > Neutral, Hairline #e4e9ea, "container edges", and
         DESIGN.md > Shapes carries the argument for the split from --line-control.
         Today: 16 var(), 15 files */
  --line-divider:  var(--hairline-soft);
      /* between rows inside one container: 26 usages. line, decorative.
         origin: DESIGN.md > Colors > Neutral, Hairline Soft #eef2f3, "inner dividers",
         named in the same entry as the hairline because the pair only means anything
         together. Today: 29 var(), 23 files, which makes it the largest LINE role in the
         file: the kit-era figures had it level with --line-container at 26 apiece, and
         componentisation broke the tie toward the divider. The product is more list than
         it is card, which is the shape the List Row signature predicts */
  --line-failure: var(--clay);
      /* the edge of an invalid field, and its focus ring while invalid: 2 usages
         (.field input[aria-invalid], .field select[aria-invalid]). line, 3:1.
         5.46:1 on the light surface, 7.40:1 on the dark one.
         ADDED at step 5 by the atom round rather than at step 3, and the gap is
         instructive: clay had a role as ink and a role as a fill and none as a
         line, so an invalid border had nothing correct to read. The agent
         building the field did not invent one; it read --text-failure on loan
         with a comment saying so, and reported the hole. Same value, so nothing
         moves when the loan is repaid here.
         origin: DESIGN.md > Colors > Secondary, Clay #9a5842, and the entry there scopes
         clay to "the sync error and the could-not-load state" without naming an invalid
         field at all, which is the SAME HOLE told from the document's end: the value had
         two jobs recorded and this role is the third. Today: 4 var(), 2 files
         (select.css and text-input.css, a border and its invalid focus ring in each) */
  --line-selected: var(--petrol);
      /* the current tab and the pressed tile: the line that says "this is the one
         you are on". line, 3:1 required. 6.2:1 light, 6.8:1 dark. THIRD role on the
         same value: a fill, an ink and a line, three thresholds, three tokens.
         IT NO LONGER CLAIMS THE FOCUS RING. Written at step 3 as "the current tab,
         the pressed tile, the focus ring", which made two roles answer for one job:
         --color-focus below is declared for the ring and nothing else, and 8
         component files already read it. base.css, which draws the ring for the
         whole system, was the one place reading this instead, and it was corrected
         at step 6. Same value, so nothing moved; what was wrong was the answer to
         "which role owns this" on the day the two part company.
         THE 8 WAS SPELLED "eight" until 2026-08-12, and a word is not a count: it does
         not sort, it does not diff, and the next reader cannot tell a measurement from a
         figure of speech. It is a digit now, at the value it was measured at on the day
         of step 6; the figure has since grown to 16 component files, 17 with base.css.
         origin: DESIGN.md > Colors > Primary, Petrol #1c6a76, "the current selection (the
         active tab, the selected row, the chosen plan)", the second of its four jobs.
         Today: 4 var(), 3 files (preset-tile, range-picker, tab-bar twice) */
  --line-action-soft: var(--petrol-tint);
      /* the underline under a petrol link, and nothing else: 1 usage
         (.trust a, on 11 grey pages and 10 coloured ones). line, and NOT governed by
         1.4.11: it decorates text that already carries 4.8:1 of its own, so the
         underline is a hint and not a boundary. 1.18:1 light, 1.20:1 dark, which is
         the same hint in both.
         ADDED at the molecule round against the rule that one usage is not yet a
         role, and the exception is named rather than quietly taken. The rule guards
         against palette inflation; the surface axis forbids outright what the
         alternative was, which is --bg-selected, a FILL, painting a line. One of the
         two had to give, and a role that is honest about its surface costs one line
         of this file while a fill on a line costs the next theme its underline.
         origin: DESIGN.md > Colors > Primary, Petrol Tint #e7edee, whose entry calls it
         "the only place the accent appears as a surface" - and this role is the sentence
         going out of date, because it paints a LINE. The document was written when the
         tint had one surface job; the surface axis has since given it three roles on
         three surfaces. The value is unchanged and the entry is still where it came from.
         Today: 1 var(), 1 file (trust-block.css), still the single usage the exception
         was granted for, so the exception has not spread in the way the rule feared */

  /* ==========================================================================
     STATE TOKENS
     Added at step 5 with the etalon, the first component that actually carries
     them, and each one in both themes at the moment it is declared. A state
     written as a value inside :hover is the same hole as a component reading a
     primitive: the dark theme would need forty edits instead of three lines,
     twenty components would each hover differently, and stage 11 would begin by
     collecting states that ought to already exist.

     THERE IS NO --opacity-disabled, and its absence is a decision. The product
     already draws a disabled control by repainting it, and all three of those
     paints land exactly on roles that exist: --text-muted, --bg-recessed,
     --line-divider. Adding an opacity token would either change how every
     disabled control renders, which this stage is not allowed to do, or sit in
     the file unread. An unread token is the shape of a system that documents
     itself rather than the product.
     ========================================================================== */

  --bg-hover: var(--panel);
      /* the fill a control takes under the pointer: the outline button, the row,
         the nav row, the tile, the door, the segment. fill.
         SAME VALUE as --bg-recessed today and a different role: one is a resting
         surface, this is a transient response, and they part company the moment
         a hover wants to be warmer than a rail.
         origin: DESIGN.md > Colors > Neutral, Panel #f4f7f8, "the row hover and the
         desktop rail", the same entry --bg-recessed reads: the document states the two
         jobs of the one value in one line and this role is the first of them.
         Today: 11 var(), 10 files, against 11 var() in 8 files for --bg-recessed: the
         same size, drawn on overlapping files, and still two roles */
  --line-control-hover: var(--muted);
      /* the control edge under the pointer, and it must STRENGTHEN: the resting
         edge is 3.46:1 on the light surface and this is 5.78:1. line.
         The value it replaced (--hairline-strong) was LIGHTER than the resting
         edge, so the boundary weakened under the pointer, which is the opposite
         of what a hover is for. Found at the contrast decision, 2026-08-11.
         origin: DESIGN.md > Colors > Neutral, Control Edge "hover #5a686c", and
         DESIGN.md > Components > Buttons carries the same argument in its own words,
         "the hover value has to be DARKER than the resting edge, which is why it is not
         the old #cdd7d9". The document and this comment were written from one decision.
         Today: 7 var(), 7 files, and they are the SAME 7 files that read --line-control:
         button, door, preset-tile, range-picker, save-focus-candidate, select,
         text-input. Every control that draws a resting edge draws its hover, and no
         other kind of object draws one. That is what a state token being complete looks
         like, and it is checkable rather than asserted */
      /* --line-container-hover WAS HERE, 2026-08-12 to 2026-08-20, and it is
         recorded rather than simply removed because the reason it existed is a good
         one and the reason it went is a better one. It was the edge of a CARD under
         the pointer, a second role beside --line-control-hover because a container
         edge and a control edge do different jobs: a control's edge is how the
         control is FOUND, so its hover has to clear 3:1, and a card's edge is
         decoration, so its hover only has to be visible as a change. That argument
         still holds and is why --line-control-hover was refused for the job.
         IT WAS ADDED FOR A MECHANICAL REASON. Without it the plan card's hover had
         nothing correct to read, the agent building it refused to invent a token,
         and the hover rendered as no change at all - and dropping a state is a
         visual change no named source of that round had asked for. Carrying it was
         the only move that needed permission from nobody.
         THE FOUNDER ANSWERED THE QUESTION UNDERNEATH ON 2026-08-20: a card that
         cannot be clicked should not answer a pointer. The hover went, this role
         had no reader, and --hairline-strong and --hairline-strong-dark had no
         role. Three deletions, one decision. It had exactly one consumer for its
         whole life, which is the shape this file warns about in its own words at
         --opacity-disabled. */
  --color-focus: var(--petrol);
      /* the focus ring, and focus-visible rather than focus: the ring is for the
         person arriving by keyboard and is noise for the person who clicked.
         line, 3:1 required by WCAG 1.4.11 because a ring is a line and not text.
         6.23:1 on the light surface, 5.56:1 on the light canvas.
         origin: DESIGN.md > Colors > Primary, Petrol #1c6a76, and DESIGN.md's role table
         lists this token by name at 6.23 light and 6.82 dark against --bg-surface,
         needing 3.0. It is one of the four state tokens the document puts IN that table
         on the ground that a state is a token and not a style.
         Today: 18 var(), 17 files, 16 of them component files and the seventeenth
         base.css, which draws the ring for the system. It is the widest-read of the four
         state tokens by a distance, 18 against 11, 7 and 1, and the sixth widest role of
         any kind in this file, which is the correct shape: a hover reaches the objects a
         pointer visits, and a focus ring reaches everything a keyboard can land on */
}

/* ============================================================================
   DARK
   Semantic only. Primitives are never touched: the dark values are their own
   names above, and a role simply points at a different one. Not mirrored, and
   the three places it visibly is not are marked.
   ============================================================================ */

[data-theme="dark"] {

  /* fills */
  --bg-page:      var(--canvas-dark);
  --bg-surface:   var(--paper-dark);
  --bg-recessed:  var(--panel-dark);
  /* MIRRORED, 2026-08-12, sixth named source. It read --panel-dark and so landed
     byte for byte on --bg-recessed and --bg-hover: three roles, one value, and the
     one thing a callout is for is being a different ground. In the LIGHT theme this
     role is --canvas, which is also --bg-page: a callout is the page's own ground
     brought inside a surface, and that is the definition rather than a coincidence.
     The dark theme now says the same sentence. Measured: 1.10:1 from the paper,
     where it was 1.11:1, so the separation from the surface is unchanged; 1.22:1
     from --bg-recessed, where it was 1.00:1, which is MORE than the light theme's
     own 1.04:1; and the ink on it goes from 13.51:1 to 16.48:1. */
  --bg-callout:   var(--canvas-dark);
      /* NOT MIRRORED. In the light theme a callout and the page ground share one
         value; here they must not, because a wash on a dark surface has to lift and
         the page ground has to stay put. This is the pair that proves the two roles
         were worth splitting */
  --bg-selected:  var(--petrol-tint-dark);
  --bg-attention: var(--amber-wash-dark);
  --bg-failure:   var(--clay-wash-dark);
  --bg-status:    var(--stone-dark);
  --bg-placeholder: var(--skeleton-dark);
  --plot-line:    var(--petrol-dark);
  --plot-area:    var(--petrol-tint-dark);
  --bg-measure:   var(--edge-dark);
  --bg-action:    var(--petrol-dark);
  --control-accent: var(--petrol-dark);
  --bg-chip-accent: var(--petrol-tint-dark);
  --text-chip-accent: var(--petrol-dark);
  --bg-action-strong: var(--petrol-deep-dark);
      /* NOT MIRRORED. The light theme darkens under the pointer, this one lightens:
         the hover has to move AWAY from the ground, and the ground swapped ends */
  --wash-pricing: url("../assets/plan-wash-dark.webp");
  --wash-final: url("../assets/final-wash-dark.webp");
      /* NOT THE SAME PICTURE, and it could not be. The light wash is a teal haze
         drifting across paper; on the dark surface it would be a bright cloud in a
         dark room, which is the opposite of quiet. Its dark pair is the same
         composition seen at dusk: the haze pools in the same corner, low and to
         the right, so the band reads as one place in two lights rather than as two
         decorations. Generated from the same brief on the same day */

  /* inks */
  --text-primary: var(--ink-dark);
  --text-body:    var(--slate-dark);
  --text-muted:   var(--muted-dark);
  --text-action:  var(--petrol-dark);
  /* IT EQUALS --bg-page HERE AND THAT IS NOT A COLLISION, checked 2026-08-12.
     The two arrive at one value from two correct requirements: in the dark theme the
     action fill INVERTS to the light petrol, so its ink has to be the darkest value
     the palette holds, and the darkest value the palette holds is the canvas. They
     never meet on a screen, one being the ground under everything and the other a
     label on a button. Written down so the next audit does not raise it again. */
  --text-on-action: var(--canvas-dark);
      /* NOT MIRRORED, and this is the one that would have shipped broken. In the
         light theme the label on a filled action is white; on the lightened petrol
         of the dark theme white measures 1.9:1. It takes the dark canvas instead,
         at 7.5:1 */
  --text-attention: var(--amber-dark);
  --text-failure: var(--clay-dark);
  --text-status:  var(--stone-ink-dark);

  /* lines */
  --line-control:  var(--edge-dark);
  --line-container: var(--hairline-dark);
  --line-divider:  var(--hairline-soft-dark);
  --line-failure: var(--clay-dark);
  --line-selected: var(--petrol-dark);
  --line-action-soft: var(--petrol-tint-dark);
      /* 1.20:1 against the dark surface, against 1.18:1 light. The one place in this
         file where the pair is nearly mirrored, and it is allowed to be because the
         role is a hint rather than a threshold */

  /* states, and the direction of two of the three inverts */
  --bg-hover: var(--panel-dark);
      /* NOT MIRRORED in direction. The light theme's hover is DARKER than the
         surface it lifts off; this one is LIGHTER, because the ground swapped
         ends and a hover has to move away from its ground either way.
         1.08 against the surface light, 1.11 dark: the same small step */
  --line-control-hover: var(--muted-dark);
      /* the resting edge is 5.15:1 here and this is 6.40:1, so the boundary
         strengthens in the dark theme too */
      /* 1.33:1 rest to 1.47:1 hover, against 1.23 to 1.47 light. The DIRECTION
         inverts: the light value is darker than the plain hairline and this one is
         lighter, because the ground swapped ends */
  --color-focus: var(--petrol-dark);
      /* 6.82:1 on the dark surface, 7.51:1 on the dark canvas. Both over the 3:1
         a line owes. Had this been left for step 7, the ring would have been the
         light petrol at 1.9:1 on the dark surface: present in the file, invisible
         on screen, and keyboard navigation blind in one theme */
}
