/* ─────────────────────────────────────────────────────────────────────────────
   THE TOKEN LAYER — the one file in the repo allowed to hold a raw value.

   It is read by TWO documents, and that is the whole reason it is a file of
   its own rather than the top of app/globals.css:

     the product      app/globals.css imports it, and `@theme inline` there
                      turns these into Tailwind utilities that resolve the
                      variable at USE time — so one [data-theme] flip restyles
                      every surface without regenerating a class.
     the landing page public/waku/*.html link it directly. The landing page is
                      a prebuilt static bundle framed in an iframe, and an
                      iframe is its own document: custom properties do not
                      cross that boundary. It has to load this file itself.

   Before this split the landing page carried its own copy of the palette, and
   the two agreed — because the palette was MEASURED off the landing page in
   the first place. They agreed right up until the contrast floor moved
   --text-muted and --text-faint on 2026-08-25 and only one side followed.
   Two copies that agree are indistinguishable from one source, until someone
   edits either.

   `npm run check-shared-tokens` refuses a re-declaration of any name here
   inside public/waku/sheet.css. That is the fence which keeps the copy from
   coming back.
   ───────────────────────────────────────────────────────────────────────────── */

:root,
[data-theme="light"] {
  /* ── colour · light ────────────────────────────────────────────────────────
     Warm bone paper and a warm near-black. Two values are picked; the seven
     greys between them are DERIVED, at the landing page's own percentages, so
     moving --surface-bg or --text-ink moves the whole ramp with them.
     Hand-picking a grey is what would break that.

     They are RESTATED in every theme block, not inherited into it, and that
     is not repetition to tidy away. A var() inside a custom property is
     substituted where the property is DECLARED, and the result inherits as a
     flat colour — so a --surface-raised declared once here computes against
     this block's ink and ground and then keeps that value inside a dark
     panel, whatever that panel redefines. The symptom was a selected tab
     painting dark chrome on the bone ground; the cause was one declaration
     doing duty for two palettes. Change a percentage in one block and change
     it in all three.

     [data-theme] is matched on ANY element, not only :root. Custom properties
     inherit, so a panel carrying the stamp re-roots the palette for its own
     subtree — which is what lets /visual-design show both grounds on one page instead
     of asking the reviewer to remember the other one.
     ──────────────────────────────────────────────────────────────────────── */
  --surface-bg: #E1E1D9;
  --text-ink: #161614;

  --surface-paper: color-mix(in srgb, var(--text-ink) 4%, var(--surface-bg));
  --surface-raised: color-mix(in srgb, var(--text-ink) 8%, var(--surface-bg));
  /* A pressed control's ground. It gets its own name rather than reusing
     --surface-raised, which means elevated: a control that reads as raised
     while it is being pressed is the wrong word in the one place someone
     will go looking. Deeper than raised, because with no shadow in this
     system a control cannot go down — only get heavier. */
  --surface-sunk: color-mix(in srgb, var(--text-ink) 12%, var(--surface-bg));
  --rule: color-mix(in srgb, var(--text-ink) 19%, transparent);
  --rule-hard: color-mix(in srgb, var(--text-ink) 42%, transparent);

  /* ── the two text greys, and the floor that set them ─────────────────────
     These were 54% and 36% until 2026-08-25, and neither could be read. Not a
     preference — --text-muted carries real paragraphs and measured 3.62:1 on
     bone, under AA's 4.5 for body text, and --text-faint carries nineteen
     12px labels at 2.21:1. Twelve-pixel mono is text and is NOT large text:
     large starts at 24px, and the largest thing wearing --text-faint is 12.

     The floor each derived value answers to is its JOB, not one number:

       text a person reads       4.5:1   AA
       the edge of a control     3:1     WCAG 1.4.11, at every level
       a surface, a divider      none    it carries no information

     AAA was asked for and was measured before it was written. At 7:1 muted
     needs 76% of ink and faint needs the same 76% — they become one colour,
     and the three steps of text this palette is built on become two. The
     semantics darken into each other on bone as well, which spends the one
     thing having three of them is for.

     So the floor is AA and --text-muted lands at 8.56:1 rather than merely
     clearing it: AAA where AAA is read, and not where the cost is a
     hierarchy. --text-faint clears AA at 4.59, which is the bar its actual
     size asks for. */
  --text-muted: color-mix(in srgb, var(--text-ink) 82%, var(--surface-bg));
  --text-faint: color-mix(in srgb, var(--text-ink) 62%, var(--surface-bg));
  /* A group's heading: Team and Setup in the rail, and the heading over a
     column of Memories. Lighter than --text-faint, so the heading reads as a
     divider and the rows under it read first. Emily picked 40% of ink on
     2026-10-19. It is under the 4.5:1 text floor, and check-contrast waives
     it by name. Nothing but a group heading wears it. */
  --text-quiet: color-mix(in srgb, var(--text-ink) 40%, var(--surface-bg));

  /* The grain on the page ground. body paints it over --surface-bg, and
     nothing else wears it: a card, a field or a button has its own ground
     and stays clean. Chosen as option B, the fine grain, on 2026-09-30.

     It is a 220px tile of SVG fractal noise, turned into greys CENTRED on
     the ground's own lightness (0.88 here, 0.125 in dark) and laid at 20%.
     Half the grains are lighter than the ground and half are darker, so
     the ground's average does not move. A first version used dark grains
     only, and it took --text-faint from 4.58:1 to 4.27:1, under AA.
     color-interpolation-filters is sRGB because the filter otherwise works
     in linear light, and 0.125 linear paints as 0.39, a mid grey. Move
     --surface-bg and the centre in the matrix moves with it. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1 0 0 0 0.38  1 0 0 0 0.38  1 0 0 0 0.38  0 0 0 0 0.2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");

  /* Accent — two tokens, not one value with derived shades. The amber is a
     surface you put ink on; as a foreground on bone it measures 1.56:1 and
     cannot carry a link, a label or a focus ring. --accent-fg is the value
     [data-accent="deep"] already held in the landing page's own stylesheet. */
  --accent: #E1AE05;      /* fill, focus border               */
  --accent-fg: #735903;   /* accent as text — 5.06:1 on bone  */
  /* Accent as an icon stroke, 2026-10-19 (Emily). The download icon on the
     landing page's Get the app and Google Play buttons. --accent is 1.44:1
     on the button fill in light and could not be seen; --accent-fg is 4.68:1
     and did not read as amber. Emily picked this middle amber from three
     (docs/proposals, M1). It measures 2.24:1 on the button fill, under the
     3:1 a non-text mark needs. Emily took that knowingly: the label beside
     the icon carries the meaning. Dark themes use --accent itself. */
  --accent-icon: #B58A00;

  /* Selection, 2026-10-13, from spec 067's drawing: the one thing on a
     screen that is chosen now wears an amber tint and an amber edge. Emily
     made it the selection everywhere on 2026-10-16: the open match, the open
     project, folder and memory, and a picked card. The edge was 70% amber
     and measured 2.81:1 on bone. At 60% it measures 3.5:1 on bone and 3.23:1
     against its own tint, so it clears the 3:1 a non-text edge needs. */
  --select-fill: color-mix(in srgb, var(--accent) 17%, var(--surface-card));
  --select-edge: color-mix(in srgb, var(--accent) 60%, var(--text-ink));

  /* The four kinds of match, 2026-10-13, from spec 067's drawing. Each
     keeps one colour from its chip to its row's dot to the open match:
     Injected the accent, Searched a blue, Recalled a violet, Brief grey.
     Each colour is a dot before the kind's word, on a grey badge.
     Matches uses all four. A memory's type borrows two of them (below). */
  --kind-inject: var(--accent-fg);
  --kind-search: #2F5E8C;
  --kind-recall: #6A4C9C;
  --kind-brief: var(--text-muted);
  /* Triggered, spec 071's act channel: grey until Ian designs it (Emily,
     2026-10-16). It is lighter than Brief's grey, so the two dots differ. */
  --kind-act: var(--text-faint);

  /* A memory's type, 2026-10-16. Emily chose a colour per type (usage rules
     O9), a hue no other meaning uses (O7), and the retro Teenage Engineering
     form: a grey badge with a small painted square before the word (option C
     of docs/proposals/type-colours.html). Each value is the same on both
     grounds, the way paint on a machine is, so the dark block does not
     restate it. Each clears 3:1 on bone (3.0:1) and on #202020 (4.1:1).
     Knowledge is teal, Skill magenta and Activity olive. */
  --type-knowledge: #368B8E;
  --type-skill: #C15C97;
  --type-activity: #728733;

  /* Chip tints, 2026-10-22, from spec 067's drawing. Ian asked for 067's
     small chips on Memories, and Emily agreed through him to review them on
     dev. A status chip is its colour on a tint of itself, with a dot of the
     same colour: Confirmed is green. A category chip on Memories is the same
     tint without the dot: Profile violet, Rules amber, Status green,
     Knowledge blue, Episodes grey. A value chip is muted text on the neutral
     tint. check-contrast measures every word on its tint.

     On bone a tint darkens the ground, so a chip's word is its colour mixed
     a little toward ink (the *-on-tint tokens): plain --ok measured 3.40:1 on
     its tint, and the mix clears 4.5:1. The dark blocks use the plain
     colours, which clear it there. */
  --ok-tint: color-mix(in srgb, var(--ok) 16%, var(--surface-card));
  --warn-tint: color-mix(in srgb, var(--warn) 16%, var(--surface-card));
  --bad-tint: color-mix(in srgb, var(--bad) 16%, var(--surface-card));
  --accent-tint: color-mix(in srgb, var(--accent) 17%, var(--surface-card));
  --neutral-tint: color-mix(in srgb, var(--text-ink) 7%, var(--surface-card));
  --kind-search-tint: color-mix(in srgb, var(--kind-search) 16%, var(--surface-card));
  --kind-recall-tint: color-mix(in srgb, var(--kind-recall) 16%, var(--surface-card));
  --ok-on-tint: color-mix(in srgb, var(--ok) 70%, var(--text-ink));
  --warn-on-tint: color-mix(in srgb, var(--warn) 75%, var(--text-ink));
  --bad-on-tint: color-mix(in srgb, var(--bad) 80%, var(--text-ink));
  --kind-search-on-tint: color-mix(in srgb, var(--kind-search) 85%, var(--text-ink));
  --kind-recall-on-tint: color-mix(in srgb, var(--kind-recall) 80%, var(--text-ink));

  /* Theme-INVARIANT: the amber surface is the same in both palettes, so what
     sits on it must be too. #161614 on #E1AE05 is 8.85:1 in either mode.

     The two darker steps are for a filled control's hover and pressed states,
     8 % and 14 % down, per playbook §5. They are invariant for the same
     reason the fill is: ink stays on top of all three, and the worst of them
     still measures 6.58:1. A dark palette would normally lighten rather than
     darken here — it does not need to, because nothing reads these against
     the ground; the label reads against them. */
  /* ── the chart palette ─────────────────────────────────────────────────────
     Decided 2026-08-26. One hue, five weights: the accent mixed into the
     ground at 100 / 78 / 58 / 40 / 24 %.

     A chart with five picked colours needs five colours that all mean
     something, all clear each other, and all still work beside the accent.
     Nobody was going to keep that true — the first extra series would have got
     whatever looked free. One ramp has an order built into it, so a reader
     sees which slice is bigger without checking the key, and adding a sixth
     series is visibly wrong instead of quietly ugly.

     Derived, not listed, and NOT restated in the dark block: color-mix reads
     --accent and --surface-bg where the variable is USED, so the same five
     names come out amber-on-bone in light and amber-on-charcoal in dark. That
     is the whole point of the ramp being a mix.

     MEASURED, and the number matters: on bone the five weights run 1.56 · 1.43
     · 1.31 · 1.20 · 1.12 against the ground, and neighbours differ by 1.09.
     On charcoal they run 7.96 · 5.39 · 3.57 · 2.40 · 1.64, neighbours 1.5.

     So the light ramp is nowhere near 3:1 — not chart-5, not chart-1 either,
     and no ramp of one hue could be. Five steps that each clear 3:1 from the
     ground AND from each other need about seven stops of range; one hue on a
     pale ground has two. This was a choice between a ramp that orders and a
     palette of five picked colours that identify, and the ramp won for the
     reasons above.

     Which fixes what a chart is allowed to be here: the ramp ORDERS, it does
     not IDENTIFY. Filled areas a reader compares by size, every series
     labelled in place, the value printed beside it. Never a legend the eye has
     to carry back to the chart, never a line or a dot told apart by colour
     alone, never colour as the only carrier of meaning. Same rule as §18's six
     statuses in two colours, and the same reason: colour ranks, words name.

     A chart that genuinely needs five things identified is a chart that needs
     five labels, not a sixth token. */
  --chart-1: var(--accent);
  --chart-2: color-mix(in srgb, var(--accent) 78%, var(--surface-bg));
  --chart-3: color-mix(in srgb, var(--accent) 58%, var(--surface-bg));
  --chart-4: color-mix(in srgb, var(--accent) 40%, var(--surface-bg));
  --chart-5: color-mix(in srgb, var(--accent) 24%, var(--surface-bg));

  --accent-ink: #161614;
  --accent-hover: #CFA005;    /* ink on it — 7.49:1 */
  --accent-active: #C19604;   /* ink on it — 6.58:1 */

  /* Semantic. No single value clears AA on both grounds, so each is a pair.

     warn was absent until 2026-08-24 — the objection was that a second yellow
     beside an amber accent makes the accent mean nothing, and it was right
     about yellow. What settled it on /visual-design was that the screen needing
     one had arrived: "this memory is read by three people, changing its scope
     changes what they see" is neither done nor failed, and collapsing it into
     bad tells someone their own action broke.

     So it is not a second yellow. It is an ORANGE, hue 32 against the accent's
     45, picked to sit as far from the accent as it can while still reading as
     a warning — measured 14.8 ΔE from --accent-fg on bone and 25.5 from the
     accent on dark, against a just-noticeable difference of about 2.3. The
     first proposal for this was #B0741A, which measured 2.98:1 on bone and
     would have shipped a warning nobody with low vision could read; it failed
     even the large-text bar. What is here clears AA for BODY text on both
     grounds, which --ok itself does not. */
  --ok: #2B7754;   /* 4.13:1 on bone */
  --warn: #8C5617; /* 4.62:1 on bone */
  --bad: #AA2E11;  /* 5.14:1 on bone */

  /* ── type ──────────────────────────────────────────────────────────────────
     Faces arrive as next/font variables from app/layout.tsx — never a <link>.
     The fallbacks are for a failed fetch, not a slow one.

     Named --face-* rather than --font-*, deliberately: `@theme inline` emits a
     --font-sans of its own onto :root, so a token sharing that name would be
     declaring --font-sans: var(--font-sans). Today the cascade order happens to
     save it; the day Tailwind emits in a different order it becomes a cycle and
     every face silently falls back to Helvetica. Different names, no ordering
     to get lucky about.
     ──────────────────────────────────────────────────────────────────────── */
  --face-sans: var(--font-instrument-sans), "Helvetica Neue", Arial, sans-serif;
  --face-mono: var(--font-jetbrains-mono), ui-monospace, SFMono-Regular, Menlo, monospace;
  --face-display: var(--font-playfair-sc), "Iowan Old Style", "Hoefler Text", Georgia, serif;

  /* The one tracking value the system has, and the landing page's signature
     before it was anyone's: uppercase mono micro-labels at .16em. It is here
     rather than in globals.css's @theme because BOTH documents set labels
     from it, and a static page cannot link an @theme block. The product
     reads it as tracking-[var(--tracking-label)], which needs no registration.

     Display type is not covered by it. A 132px serif headline is tightened,
     not tracked out, and those negative values belong to the face at that
     size rather than to a rule anyone would reuse. */
  --tracking-label: 0.16em;

  /* The type scale, the line-heights and the two weights live here from
     2026-10-16. Until then they lived in globals.css's @theme, and the phone
     app copied them by hand. The copy drifted: the phone still set body at
     1.72 two days after Emily set it to 1.6. The phone's sync script reads
     this file, so a value here reaches the phone.

     globals.css names each one in `@theme inline reference`. That block makes
     text-sm and leading-snug read these variables, and it declares no value
     of its own.

     Generous scale, ratio 1.333, anchored at the measured 15px body. Below the
     body it stops at one step, 12, because 1.333 downward reaches 8px and no
     label is legible there.

     Five sizes, not six, from 2026-09-15. --text-sm was 13. It is 12 now, the
     same as xs. The name stays until work in progress stops writing text-sm:
     a text-sm left behind would compile to nothing and inherit the 15px body. */
  /* A group's heading, and nothing else (Emily, 2026-10-19): the one step
     under 12. It pairs with --text-quiet. */
  --text-2xs: 10px;
  --text-xs: 12px;
  --text-sm: 12px;
  --text-base: 15px;
  --text-lg: 20px;
  /* The title of a console page, and nothing else. Emily found 20px too small
     and 27px too large on 2026-09-30. PageHeader is its only reader. */
  --text-title: 23px;
  --text-xl: 27px;
  --text-2xl: 36px;

  /* Body is 1.6 since 2026-10-14. It was 1.72, the value measured off the
     landing page. Emily set 1.6 with the usage rules. */
  --leading-tight: 1.12;    /* display                  */
  --leading-snug: 1.3;      /* headings                 */
  --leading-normal: 1.6;    /* body                     */
  --leading-label: 1.4;     /* uppercase micro-labels   */

  /* Two weights, §31. The lint allows only font-normal and font-medium. */
  --font-weight-normal: 400;
  --font-weight-medium: 500;

  /* ── line and ring ─────────────────────────────────────────────────────────
     With no shadow in the system, a hairline is how everything is separated —
     which makes its width a decision rather than a default. One value, so a
     card, a table row, a dialog edge and an input all draw the same line.

     The focus ring's width and offset sit here for the same reason: they were
     settled on /visual-design, they apply to every control, and a primitive that
     wrote its own would be deciding accessibility locally.
     ──────────────────────────────────────────────────────────────────────── */
  --rule-width: 1px;
  --focus-ring-width: 2px;
  --focus-ring-offset: 2px;

  /* ── radius ────────────────────────────────────────────────────────────────
     4px, on every page, since 2026-10-16. Emily chose one corner for
     everything when she merged Ian's spec 067: a field, a button, a card, a
     dialog, a menu and a toast, in the console and on the landing page.
     Ian's branch had three corners (6, 10 and 14px). Emily first took 6px
     for all of them and set 4px the same day.

     History, kept for why it was 2px. The measurement said 0 — border-radius does not appear once in the
     landing page's 1,124 lines, and its own comment says why: "the only round
     thing on the page is still nothing." The design owner reviewed 0 against
     2, 6 and 12 on /visual-design and took 2, which is the decision that counts: a
     marketing page and a settings form are not asked to do the same job, and
     2px is what softens a field or a dialog edge without reading as round.

     Every shadcn radius step derives from this, so the whole set moves
     together. This is the one value that would have cost sixteen restyles if
     it had been settled after T3 instead of before it.
     ──────────────────────────────────────────────────────────────────────── */
  --radius: 4px;

  /* The button, on every page: the console, sign-in and the landing page
     (Emily, 2026-10-13). A primary or destructive button is the paper ground
     inside the hard rule. Secondary keeps the page ground and the lighter
     rule; tertiary keeps its underline. Change a button here and every page
     changes with it. Its corner is --radius since 2026-10-16. */
  --button-radius: var(--radius);
  --button-fill: var(--surface-paper);
  --button-edge: var(--rule-hard);

  /* Two shapes are exempt, and the exemption is a token so that nobody
     "fixes" them later and nobody writes a raw length to get around it.

     A radio is a circle and a switch is a pill because those shapes CARRY
     the meaning: round means one choice out of several, square means many
     independent ones, and a pill reads as a physical thing that slides.
     Rounding them to 2px would make a radio and a checkbox the same object
     and cost the user the only cue that says which rules apply. The radius
     row governs rectangles; these are not rectangles.

     Named --shape-* rather than --radius-*, for the reason the faces are
     named --face-*: `@theme inline` emits a token of its own under each
     theme key it declares, so a raw token sharing the name would resolve to
     itself. */
  --shape-circle: 50%;     /* radio, avatar */
  --shape-pill: 999px;     /* switch track and knob */

  /* A chip is the third exemption, added 2026-08-27 at the design owner's
     call. At 2px a 20px-tall chip reads as a right-angled box — the radius
     row's own reasoning ("softens an edge without reading as round") is about
     a field or a dialog, and a chip is neither: it is a value lifted out of a
     sentence and made pick-up-able, and round ends are what say so.

     Same number as the pill and a separate token on purpose. The pill's
     comment says it "reads as a physical thing that slides", which is true of
     a switch and not of a chip. One reason per token, so changing either does
     not silently change the other.

     To take chips back to a softened rectangle, this is the one line: 6px. */
  --shape-chip: 999px;     /* badge, in every variant */

  /* A card's corner. It was 4px on Matches from 2026-10-13, and every card
     takes --radius since 2026-10-16. */
  --shape-card: var(--radius);

  /* ── spacing ───────────────────────────────────────────────────────────────
     Comfortable: a 4px base unit. ONE base — read the next paragraph before
     touching anything here.

     --spacing is Tailwind's own base. Every numeric spacing utility it builds
     is calc(var(--spacing) * N): p-2, gap-4, size-6, h-10. Setting it here is
     what makes those utilities part of this token layer instead of a second
     one running beside it.

     They WERE a second one, until 2026-08-24. --space-1…8 were written out by
     hand as 4px…32px, Tailwind's default --spacing is .25rem, and at a default
     root font size both painted the same numbers. So the two scales agreed
     perfectly and were indistinguishable — while "change one token and every
     surface follows", which AGENTS.md states as the point of the whole
     layering, was false for all 82 spacing values inside components/ui/.

     It surfaced sideways: /visual-design §16 asked what the table's density
     should be and the answer was that nobody had ever decided. px-2 came from
     shadcn, and the layering lint had only ever been taught to look for a raw
     px length. The lint was working. It was guarding the other scale.

     Now there is one number. --space-N derives from it, Tailwind derives from
     it, and npm run visual-design-coverage refuses a --space-* written as a
     literal — because two scales that agree are indistinguishable from one
     scale right up until someone changes either.
     ──────────────────────────────────────────────────────────────────────── */
  --spacing: 4px;
  /* ── which step means what ─────────────────────────────────────────────
     Named 2026-08-26. Eight steps existed, all eight were in use, and nothing
     said which one went with which relationship — so `space-3` or `space-4`
     was picked by feel every time, and two screens built a week apart used
     different gaps for the same thing with neither being wrong.

     Five relationships, five steps. The other three were migrated to their
     nearest named neighbour the same day — 86 places — because a rule with
     eighty-six exceptions on the day it is written is not a rule. That is what
     happened to line length before §20, and it is why the lint below exists.

       tight    2   inside one thing: an icon and its label, a value and its
                    unit, a label above the thing it names
       inside   3   parts of one block that are not a label and its thing
       between  4   two rows in one list
       group    6   two fields, two labelled blocks, two groups on one screen
       section  8   two sections, and a screen's own outer padding

     The ratio, decided 2026-09-15: the gap between two labelled blocks is at
     least three times the gap inside one. A label sits 8 above its field and
     the next label sits 24 below it, so the ratio is 1:3. The table used to
     say 12 and 16, a ratio of 1:1.33. At that ratio a label floats halfway
     between two inputs, and the reader cannot see which input it names.
     Field already drew 8 inside, so the code was right and the table was
     wrong.

     1, 5 and 7 are not merely unnamed, they are DELETED. A retired step left
     declared is a step someone reaches for, and check-tokens-wired caught them
     the moment nothing read them any more — which is exactly what it is for.

     There are no --gap-* aliases: a second name for `--space-4` would be a
     second thing to keep in sync, and this table is the naming. */
  --space-2: calc(var(--spacing) * 2);
  --space-3: calc(var(--spacing) * 3);
  --space-4: calc(var(--spacing) * 4);
  --space-6: calc(var(--spacing) * 6);
  --space-8: calc(var(--spacing) * 8);

  /* ── the stack ─────────────────────────────────────────────────────────
     Decided 2026-08-25, §33. Before it, dialog, dropdown, select and tooltip
     were ALL z-50: four overlays with no order between them, so a tooltip
     inside a dialog was resolved by React's render order rather than by
     anyone. It had not bitten only because no screen nested two.

     One step per layer, in the order they sit. The scrim is under the dialog
     it belongs to. A tooltip is above everything because it is the one thing
     that can be summoned from inside any of the others. */
  /* Inside one component's own stacking context — a select's scroll arrow over
     its list, an avatar's badge over its image. It does not compete with the
     rungs below it, and it is named so the next one does not pick a number. */
  --z-inside: 10;

  --z-header: 10;
  /* Added 2026-10-02, spec 040 B1: the Waku Agent bird at the bottom right
     of every console page and the panel it opens. Above the sticky header
     (a page's own chrome), under the scrim, so a dialog opened from the page
     still covers the chat. The design board for spec 040 asked for this rung
     and Sean waived the design sign-off for the demo. */
  --z-float: 20;
  --z-scrim: 30;
  --z-dialog: 40;
  --z-menu: 50;
  --z-toast: 60;
  --z-tooltip: 70;

  /* ── the scrim ─────────────────────────────────────────────────────────
     Decided 2026-08-25, §34, option C — a value per ground, and the blur
     stays because it was chosen rather than inherited.

     It was `bg-black/10`, the last literal colour in the product, and the same
     10% on both grounds. Measured: 1.25:1 on bone and 1.03:1 on #202020,
     which is nothing. The blur was doing all the work on the dark ground, in
     a system that says nothing casts a shadow.

     Black cannot make a scrim on a dark ground at any strength — 60% still
     only reaches 1.19:1 — so the dark half is stronger than the light one and
     still leans on the dialog being --surface-paper with a rule around it,
     which is lighter than the page and already separates. The number is
     picked per ground for that reason, not to match. */
  --scrim: rgb(0 0 0 / 0.40);
  --scrim-blur: 3px;

  /* ── disabled ──────────────────────────────────────────────────────────
     Decided 2026-08-25, §32, option B: colour, not opacity.

     Twelve places used opacity-50 and one used 45. Opacity fades the border,
     the label and the background together, so a disabled field's edge fell to
     about 1.5:1 — under the 3:1 that same edge had just been raised to clear.
     Naming the colours instead puts every part of a disabled control on a
     value the contrast floor already knows about, and a disabled control
     still reads as a control. */
  --disabled-fg: var(--text-faint);
  --disabled-edge: var(--rule);

  /* ── icons ─────────────────────────────────────────────────────────────
     Settled 2026-08-24 on /visual-design §25–26.

     The stroke was never set. Every icon in the product was drawn at lucide's
     default of 2 while --rule-width is 1 and this system says it has one value
     for every border — so an icon sat at twice the weight of every line around
     it, and nobody chose that. 1 was drawn beside it and is too light at the
     size these are actually used; 1.5 was taken.

     That is the third library default that turned out to be an undecided
     design value, after the table's px-2 and the nav item's border-l-2. The
     pattern is worth naming: a lint that looks for raw values cannot see a
     number that arrived inside a package.

     The size derives from the spacing base like everything else, so 16px is
     four steps and not a fourth scale. */
  --icon-size: calc(var(--spacing) * 4);
  --icon-stroke: 1.5;

  /* ── panel and page width ──────────────────────────────────────────────
     Text has no line-length cap. A line of text is as wide as the box it
     sits in. Emily removed --measure (58ch) and --measure-narrow (40ch)
     on 2026-10-13.

     Emily confirmed the removal on 2026-10-14, after the trunk had brought
     the 58ch cap back to 104 elements.

     --panel is the width of a small box: a tooltip, a popover, the sign-in
     column, and the agent dock at its narrowest. It is 400px, the width
     --measure-narrow painted before.

     --dialog is the width of a dialog. It is 580px, the width --measure
     painted when 640b799 gave it to the dialog. */
  --panel: 400px;
  --dialog: 580px;

  /* One reading column. The memory list is a table and takes the full width
     on purpose; this is for content read one item at a time. */
  --page: 880px;

  /* ── the page gutter ───────────────────────────────────────────────────
     What a screen keeps clear at its left and right edge. Decided
     2026-09-16, off the console's mobile proposal.

     Every console screen wrote --space-8 for this. There were 57 of them.
     On a 390px phone --space-8 is 64px of 390, a sixth of the width.

     The memory list was worse. Its rows used --space-4, so a phone drew the
     heading 32px from the edge and the rows under it 16px from the edge.
     That is two left edges on one screen.

     This token is --space-4 below the tablet line and --space-8 from it up.
     The media query below writes 768 as a plain number. A media query cannot
     read a custom property, so 768 and --breakpoint-tablet have to be
     changed together. This comment is the only record of that.

     components/ui/dialog.tsx uses this token for its padding. That padding
     was --space-8, under a comment that called it the page gutter. It is the
     page gutter now. */
  --gutter: var(--space-4);

  /* The height of a console page's header band and of the Waku Agent
     dock's header, rule included (spec 040 Q5, 2026-10-03). With the dock
     open the two headers sit side by side, and their bottom rules form one
     line across the screen only if both bands are the same height. They
     were 77px and 67px, and Sean saw two rules a step apart.

     One row a control tall (44px, a touch target), --space-4 above and
     below, and the rule: 77px. It is a minimum. A band whose controls wrap
     onto a second line on a narrow screen grows, and from the wide line up,
     where the dock sits beside the page, they fit on one. */
  --header-height: calc(var(--space-4) * 2 + var(--spacing) * 11 + var(--rule-width));

  /* A dialog you WORK in, as opposed to one you answer. Two numbers rather
     than a width and a ratio: the height is the point. An editor whose box
     grows with what you type moves the Save button while you are typing, and
     the panel beside the field changes length every keystroke. Fixed, the
     dialog is a room rather than a bubble.

     1024 x 640 is 16:10. It is wider than --page because two columns are
     inside it, and the reading measure applies to each column, not to the
     box around them. */
  --dialog-wide: 1024px;
  --dialog-wide-height: 640px;

  /* DEPARTURE 1 — control padding does not sit on the 4px grid.
     Primitives read these rather than writing px of their own, which is why
     values off the grid are allowed to live here and nowhere else.

     14/26 until 2026-08-27, measured off the landing page. The design owner
     read four options side by side on /visual-design's terms — 14/26, 12/24
     (--space-3 / --space-6, the only one on the grid), 10/20 and 12/20 — and
     took 10/20. The grid was available and was not chosen: at 12/24 a row of
     two buttons still read as the loudest thing on a detail header whose
     subject is a paragraph, and this product's screens carry several controls
     at once rather than one call to action.

     10/20 is the floor, not a step on the way to less. `size="sm"` is 8/16;
     any tighter and the two sizes collapse into one and the scale loses a
     rung. */
  --control-pad-y: 10px;
  --control-pad-x: 20px;
  --field-pad-x: 16px;
  --control-gap: 10px;

  /* ── shadow ────────────────────────────────────────────────────────────────
     Since 2026-10-16 a card casts a shadow and has no border, and anything
     over the page casts a larger one. Emily took this from Ian's spec 067.
     See --shadow-card below. What follows is the history.

     Almost none. Separation is done with rules: a card gets a border, a
     dialog gets a scrim. One thing casts a shadow. The onboarding tour's card
     (spec 019) floats over a scrim and over the rail, where a border alone
     did not lift it off the page; the design owner asked for it on
     2026-09-17. --shadow-md held a soft shadow under the Waku Agent bird
     from 2026-10-03 to 2026-10-05 (spec 040 N1). The design owner took it
     off: black cannot darken the #202020 ground, so the bird had a shadow in
     light mode and none in dark. Its hard rule separates it in both. Button's
     `shape="float"` still reads --shadow-md, so a shadow under the bird is
     one value away. --shadow-sm stays none until something else needs it.
     ──────────────────────────────────────────────────────────────────────── */
  --shadow-sm: none;
  --shadow-md: none;
  --shadow-lg: 0 12px 32px rgb(0 0 0 / 0.35);

  /* Depth, 2026-10-16, from spec 067. A card at rest casts --shadow-card. A
     dialog, a menu, a popover or a toast is over the page and casts
     --shadow-over. On bone these are soft real shadows. On #202020 black
     darkens nothing, so the dark blocks replace them with a 1px top
     highlight and a faint ring, and a card there is a lighter grey. */
  --shadow-card: 0 1px 1px rgb(22 22 20 / 0.04), 0 1px 3px rgb(22 22 20 / 0.06);
  --shadow-over: 0 4px 10px rgb(22 22 20 / 0.08), 0 18px 48px rgb(22 22 20 / 0.16);

  /* A card's ground. In light it is the bone itself: Emily took the white
     out of cards on 2026-10-12, so a card on bone is told apart by its
     shadow. In dark it is 6% ink, lighter than the ground. */
  --surface-card: var(--surface-bg);

  /* ── motion ────────────────────────────────────────────────────────────────
     Tone is minimal, and the landing page has exactly one duration and one
     curve for everything that moves. Motion is not a system here; it is a
     single value, and it stays one until something needs otherwise.
     ──────────────────────────────────────────────────────────────────────── */
  --motion-fast: 0.18s;
  --motion-ease: ease;

  /* The console's motion, from Ian's motion catalogue, approved by Emily on
     2026-10-13 without its button press. The two values above stay for the
     landing page. Every motion answers something the person did: a pick, a
     decision, a memory arriving, a Forget. Only the skeleton's light plays
     on its own. A button never moves; only its colour changes.

       quick   140ms   a selected row's ground changing
       base    220ms   something arriving or leaving
       slow    360ms   the space a leaving card held, closing
       roll    600ms   a number counting to its new value, and no longer
       glow   1200ms   the amber on a memory that just arrived, fading out
       shimmer 1400ms  one pass of light over a skeleton, on a loop
       step     34ms   the gap between two rows of a list that arrives one
                       row after another (spec 067, 2026-10-22)

     --motion-spring overshoots a little and settles, for arrivals.
     --motion-out is a plain ease-out for colour and for closing a space.
     --motion-in accelerates away, for things that leave. Reduced motion sets
     every duration to zero, below the dark block. */
  --motion-quick: 140ms;
  --motion-base: 220ms;
  --motion-slow: 360ms;
  --motion-roll: 600ms;
  --motion-glow: 1200ms;
  --motion-shimmer: 1400ms;
  --motion-step: 34ms;
  --motion-spring: cubic-bezier(0.3, 1.32, 0.5, 1);
  --motion-out: cubic-bezier(0.2, 0.8, 0.2, 1);
  --motion-in: cubic-bezier(0.4, 0, 1, 1);

  /* ── color-scheme, and why it is not "light dark" ──────────────────────
     This said `light dark` until 2026-08-24, which means "I support both,
     browser, you pick" — and the browser picks from the OS, not from
     data-theme. So anything the browser draws itself rather than reading from
     this file followed the operating system while the page followed the
     theme, and the two disagreed exactly half the time.

     The scrollbar was where it showed. A person on a light OS looking at the
     dark theme got a light scrollbar on a dark page, and the reverse on the
     other side. Reported as "the scrollbar colours are the wrong way round",
     which is precisely what it was.

     It governs more than the scrollbar: native form controls, the canvas the
     browser paints behind the page, the autofill wash on an input, and the
     default caret. All of them were following the OS.

     One value per theme, restated in all three blocks like every other
     decision here. */
  color-scheme: light;

  /* The scrollbar thumb. Settled 2026-08-24 on /visual-design §04c, at 25%.

     It was --rule-hard, the 42% every hairline uses, and it was reported as
     "the colours are the wrong way round". Two things were wrong and only one
     of them was the colour.

     The real one was color-scheme — see the note above. Until that followed
     data-theme, the browser drew the scrollbar in the OS's scheme while the
     page drew itself in the theme's, so half of all readers saw a light
     scrollbar on the dark ground or the reverse. Literally swapping the two
     values was measured before it was written: 1.08:1 and 1.05:1, a scrollbar
     nobody can see, which is the defect §24 had just been decided to remove.

     What was left after that fix is weight, not colour. A dark mark on bone
     reads heavier than a light mark on #202020 at the same measured contrast,
     so 42% looked correct on one ground and wrong on the other. Three weights
     were drawn as real scrollbars and 25% was taken for both.

     It is its own token rather than --rule-hard because the two are now free
     to move apart: a hairline separates, a scrollbar hints. */
  --scrollbar-thumb: color-mix(in srgb, var(--text-ink) 25%, transparent);
}


@media (min-width: 768px) {
  :root, [data-theme] { --gutter: var(--space-8); }
}

/* ── colour · dark ───────────────────────────────────────────────────────────
   Decided at the same time as light, so no row is carrying a dark half that is
   still outstanding. Only the picked values are restated — the six derived
   greys recompute from these on their own, which is the whole reason they are
   derived rather than listed.

   The accent needs no split here: #E1AE05 on #202020 is 7.96:1, so one value
   carries both fill and text.

   Two values moved on 2026-10-16 with the cards from spec 067. A card on
   #202020 is lighter than the ground, so text on it has less room.
   --text-faint went from 62% to 66% of ink, and --bad from #EC6242 to
   #F07656. Each clears 4.5:1 on --surface-card.
   ──────────────────────────────────────────────────────────────────────────── */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --scrollbar-thumb: color-mix(in srgb, var(--text-ink) 25%, transparent);
    --scrim: rgb(0 0 0 / 0.55);
    --surface-bg: #202020;
    --text-ink: #C9CDD1;
    --accent-fg: #E1AE05;
    --accent-icon: #E1AE05;
    --ok: #4FBF8B;   /* 7.11:1 on #202020 */
    --warn: #E78B23; /* 6.30:1 on #202020 */
    --bad: #F07656;  /* 5.77:1 on #202020, 5.07:1 on a card */

    /* Restated, not inherited. See the note above the light block. */
    --surface-paper: color-mix(in srgb, var(--text-ink) 4%, var(--surface-bg));
    --surface-card: color-mix(in srgb, var(--text-ink) 6%, var(--surface-bg));
    --select-fill: color-mix(in srgb, var(--accent) 10%, var(--surface-card));
    --select-edge: var(--accent);
    --kind-inject: var(--accent);
    --kind-search: #8DB8E8;
    --kind-recall: #BCA6F0;
    --kind-brief: var(--text-muted);
    --kind-act: var(--text-faint);
    --ok-tint: color-mix(in srgb, var(--ok) 13%, var(--surface-card));
    --warn-tint: color-mix(in srgb, var(--warn) 8%, var(--surface-card));
    --bad-tint: color-mix(in srgb, var(--bad) 6%, var(--surface-card));
    --accent-tint: color-mix(in srgb, var(--accent) 12%, var(--surface-card));
    --neutral-tint: color-mix(in srgb, var(--text-ink) 7%, var(--surface-card));
    --kind-search-tint: color-mix(in srgb, var(--kind-search) 16%, var(--surface-card));
    --kind-recall-tint: color-mix(in srgb, var(--kind-recall) 16%, var(--surface-card));
    --ok-on-tint: var(--ok);
    --warn-on-tint: var(--warn);
    --bad-on-tint: var(--bad);
    --kind-search-on-tint: var(--kind-search);
    --kind-recall-on-tint: var(--kind-recall);
    --shadow-card: inset 0 1px 0 rgb(255 255 255 / 0.04);
    --shadow-over: inset 0 1px 0 rgb(255 255 255 / 0.07), 0 0 0 1px rgb(255 255 255 / 0.07), 0 18px 48px rgb(0 0 0 / 0.5);
    /* Re-read here: a token that reads another is resolved where it is declared. */
    --button-fill: var(--surface-paper);
    --button-edge: var(--rule-hard);
    --surface-raised: color-mix(in srgb, var(--text-ink) 8%, var(--surface-bg));
    --surface-sunk: color-mix(in srgb, var(--text-ink) 12%, var(--surface-bg));
    --rule: color-mix(in srgb, var(--text-ink) 19%, transparent);
    --rule-hard: color-mix(in srgb, var(--text-ink) 42%, transparent);
    --text-muted: color-mix(in srgb, var(--text-ink) 82%, var(--surface-bg));
    --text-faint: color-mix(in srgb, var(--text-ink) 66%, var(--surface-bg));
    --text-quiet: color-mix(in srgb, var(--text-ink) 40%, var(--surface-bg));
    --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0.5 0 0 0 -0.125  0.5 0 0 0 -0.125  0.5 0 0 0 -0.125  0 0 0 0 0.2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  }
}
[data-theme="dark"] {
  color-scheme: dark;
  --scrollbar-thumb: color-mix(in srgb, var(--text-ink) 25%, transparent);
  --scrim: rgb(0 0 0 / 0.55);
  --surface-bg: #202020;
  --text-ink: #C9CDD1;
  --accent-fg: #E1AE05;
  --accent-icon: #E1AE05;
  --ok: #4FBF8B;
  --warn: #E78B23;
  --bad: #F07656;

  /* Restated, not inherited. See the note above the light block. */
  --surface-paper: color-mix(in srgb, var(--text-ink) 4%, var(--surface-bg));
  --surface-card: color-mix(in srgb, var(--text-ink) 6%, var(--surface-bg));
  --select-fill: color-mix(in srgb, var(--accent) 10%, var(--surface-card));
  --select-edge: var(--accent);
  --kind-inject: var(--accent);
  --kind-search: #8DB8E8;
  --kind-recall: #BCA6F0;
  --kind-brief: var(--text-muted);
  --kind-act: var(--text-faint);
  --ok-tint: color-mix(in srgb, var(--ok) 13%, var(--surface-card));
  --warn-tint: color-mix(in srgb, var(--warn) 8%, var(--surface-card));
  --bad-tint: color-mix(in srgb, var(--bad) 6%, var(--surface-card));
  --accent-tint: color-mix(in srgb, var(--accent) 12%, var(--surface-card));
  --neutral-tint: color-mix(in srgb, var(--text-ink) 7%, var(--surface-card));
  --kind-search-tint: color-mix(in srgb, var(--kind-search) 16%, var(--surface-card));
  --kind-recall-tint: color-mix(in srgb, var(--kind-recall) 16%, var(--surface-card));
  --ok-on-tint: var(--ok);
  --warn-on-tint: var(--warn);
  --bad-on-tint: var(--bad);
  --kind-search-on-tint: var(--kind-search);
  --kind-recall-on-tint: var(--kind-recall);
  --shadow-card: inset 0 1px 0 rgb(255 255 255 / 0.04);
  --shadow-over: inset 0 1px 0 rgb(255 255 255 / 0.07), 0 0 0 1px rgb(255 255 255 / 0.07), 0 18px 48px rgb(0 0 0 / 0.5);
  /* Re-read here: a token that reads another is resolved where it is declared. */
  --button-fill: var(--surface-paper);
  --button-edge: var(--rule-hard);
  --surface-raised: color-mix(in srgb, var(--text-ink) 8%, var(--surface-bg));
  --surface-sunk: color-mix(in srgb, var(--text-ink) 12%, var(--surface-bg));
  --rule: color-mix(in srgb, var(--text-ink) 19%, transparent);
  --rule-hard: color-mix(in srgb, var(--text-ink) 42%, transparent);
  --text-muted: color-mix(in srgb, var(--text-ink) 82%, var(--surface-bg));
  --text-faint: color-mix(in srgb, var(--text-ink) 66%, var(--surface-bg));
  --text-quiet: color-mix(in srgb, var(--text-ink) 40%, var(--surface-bg));
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0.5 0 0 0 -0.125  0.5 0 0 0 -0.125  0.5 0 0 0 -0.125  0 0 0 0 0.2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── colour · watch · 2026-10-12 ────────────────────────────────────────────
   The Apple Watch app (spec 069): bone ink on true black, chosen by Ian on
   2026-10-12 from four drawn directions. The watch ground is the display's own
   black, and its ink is the light ground's bone, so the two grounds the
   product already has turn inside out rather than adding a third palette.
   Everything else follows from those two by the same formulas as the other
   grounds. The phone's generator (waku-mobile-frontend scripts/sync-tokens.mjs)
   writes this block into the watch's Swift tokens; nothing in the console
   draws it but /visual-design §46.
   ──────────────────────────────────────────────────────────────────────────── */
[data-theme="watch"] {
  color-scheme: dark;
  --surface-bg: #000000;
  --text-ink: #E1E1D9;
  --accent-fg: #E1AE05;
  --accent-icon: #E1AE05;
  --ok: #4FBF8B;
  --warn: #E78B23;
  --bad: #EC6242;

  /* Restated, not inherited. See the note above the light block. */
  --surface-paper: color-mix(in srgb, var(--text-ink) 4%, var(--surface-bg));
  /* Re-read here: a token that reads another is resolved where it is declared. */
  --button-fill: var(--surface-paper);
  --button-edge: var(--rule-hard);
  --surface-raised: color-mix(in srgb, var(--text-ink) 8%, var(--surface-bg));
  --surface-sunk: color-mix(in srgb, var(--text-ink) 12%, var(--surface-bg));
  --rule: color-mix(in srgb, var(--text-ink) 19%, transparent);
  --rule-hard: color-mix(in srgb, var(--text-ink) 42%, transparent);
  --text-muted: color-mix(in srgb, var(--text-ink) 82%, var(--surface-bg));
  --text-faint: color-mix(in srgb, var(--text-ink) 62%, var(--surface-bg));
  --text-quiet: color-mix(in srgb, var(--text-ink) 40%, var(--surface-bg));
}

/* ── reduced motion · 2026-10-13 ────────────────────────────────────────────
   One switch for the console's motion. A person who asked their system for
   less motion gets every duration at zero: nothing slides, glows or rolls.
   app/globals.css also clamps every transition and animation. */
@media (prefers-reduced-motion: reduce) {
  :root, [data-theme] {
    --motion-quick: 0ms;
    --motion-base: 0ms;
    --motion-slow: 0ms;
    --motion-roll: 0ms;
    --motion-glow: 0ms;
    --motion-shimmer: 0ms;
    --motion-step: 0ms;
  }
}
