/* ===========================================================================
   TOKENS — the whole system, in one place.

   Read this before adding a component. Everything in components.css picks
   from these ladders; a new thing that needs a value not on one of them is
   either mis-measured or an argument for adding a rung, and adding a rung is
   a decision made here rather than a literal written at the call site.

   THE INDEX

     COLOURS   --colour-ground --colour-ink  the four colours an app sets.
               --colour-primary              Every other colour, in both
               --colour-accent               themes, is a shade or tint of
                                             one of them
     THEMES    --ground --ink --primary      what a theme makes of the four,
               --accent                      dark (the default) or light
                                             (data-theme="light")
     TYPE      --text-1 .. --text-13         thirteen steps, 10px to 24px
     INK       --on --on-variant --outline   one ramp, loudest first, and
               --dim --faint --fainter       every glyph is on it
     ACCENTS   --primary-cont --accent-2     each accent's containers and the
               --on-primary …                text drawn on them
               --nav-current                 the section you are in, apart
                                             from a filter that is on
     STATE     --info --warn --error         three meanings, three fixed hues
     SHADE     --shade                       the ground near black: shadows,
                                             a dialog's backdrop
     SURFACE   --surface --surface-low       what a thing sits on, plus the
               --surface-cont --surface-high three shell columns
               --surface-est                 (--surface-rail/-nav/-pane)
     SHELL     --shell-ink                   grey text on a shell surface
     LINES     --hairline --edge --rule      shell columns / round a group /
                                             between rows in a group
     SPACE     --space-1 .. --space-6        gaps: 4 8 12 16 24 32
     PANE      --pane-x --pane-y             a column's own inset, which is
               --pane-x-side                 not a gap
     COLUMNS   --side-w                      the one shell width that is a
                                             token, because two apps want two
     DRAWER    --drawer-w --drawer-w-wide    760 / 1240: the sheet from the
                                             right, and the one with a preview
     DOC       --doc-narrow --doc-paper      375px, a phone; the white an
                                             email is previewed on
     ROWS      --row-y --row-x               one inset for every row of every
               --row-y-block                 bordered group
               --row-h-1/-2/-3               52 / 60 / 84, the height of a
                                             one-, two- or three-line row
     CONTROLS  --control-h-sm/-h/-h-lg       26 / 32 / 38, with the matching
               --control-x-sm/-x/-x-lg       10 / 14 / 18
     FIELDS    --field --field-border        a control you type into: it has
               --field-border-hover          an inside, so it is brighter
               --field-y --field-x           than any line
     CHIPS     --chip-y/-x                   a word with a box round it,
               --chip-y-sm/-x-sm             ordinary and tiny
     RADIUS    --radius-sm/--radius          8 / 12 / 16 / pill
               --radius-lg/--radius-pill
     MOTION    --motion-short --motion-long  120ms for a thumb, 260ms for
               --easing                      something entering the page
               --motion-enter --motion-leave 320ms for a pane or a dialog
                                             arriving, 200ms for it going
               --ease-out --ease-in          the curves for arriving, going,
               --ease-spring                 and landing somewhere new
     PRESS     --motion-press --motion-release  50ms down, 260ms back up on
               --ease-press                  the spring
               --press-scale/-sm/-lg         .95 / .88 / .97, and a filled
               --press-lit                   control brightens by 1.12
     FLASH     --flash-out                   6px: how far a flash reaches past
                                             the thing it marks
     SHADOW    --shadow-snackbar             the one shadow, for what floats
     ICONS     --icon-size/-sm/-lg           20 / 16 / 24
     AVATARS   --avatar-sm/--avatar          24 / 32 / 40
               --avatar-lg
     PICKER    --picker-w                    232: the colour picker's width

   THE ONE STYLING RULE

   No component writes a colour or a size in JavaScript. The test for this
   whole file: changing `--colour-primary` in one line restyles the entire
   app, and no pixel value for any of it appears in a script.
   =========================================================================== */

/* Self-hosted, one variable file per family. Same origin because the
   alternative degrades badly: an app that reaches fonts.googleapis.com
   renders in a fallback face on a locked-down network, and tells Google who
   is signing in. Adjust these two URLs to wherever your build serves them. */
@font-face {
  font-family: "DM Sans";
  src: url("../fonts/dm-sans.woff2") format("woff2");
  font-weight: 100 1000;   /* one variable file covers the range */
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/jetbrains-mono.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* ================================================ THE FOUR COLOURS ======
     An app sets four colours and nothing else. Every other colour, in both
     themes, is a shade or a tint of one of them, worked out in THEMES below
     with oklch(from …), so changing one of these lines re-colours every
     screen, dark and light:

       --colour-ground    the dark the page is painted on. Every surface,
                          line and field is a step lighter or darker than it.
       --colour-ink       the text you came to read. Every quieter grey is a
                          mix of it towards the ground.
       --colour-primary   this is ON, this is the ACTION.
       --colour-accent    this is the thing you are LOOKING AT.

     They are picked for the dark theme, which is the system's own. The light
     theme keeps each one's hue and turns its lightness round: the ground
     near white, the ink near black, the accents dark enough to read on
     white. Components never read these four: they read what a theme made of
     them (`--ground`, `--ink`, `--primary`, `--accent` and everything after).

     They are hex, and stay hex, because a person picks them: a colour picker
     opens on hex (DS.colourSettings in support.js), and a value pasted from a
     brand sheet is hex.

     This replaces three numbers and a hue (--ds-ground, --ds-tint,
     --ds-lines, --tint-h) and twenty hand-picked hexes. The numbers made a
     person think in lightness and chroma to get a colour they could have
     named; the hexes could not be moved, because darkening the ground meant
     re-picking every line and grey against it by eye.

     Two things to keep when picking:

       The ground stays nearly grey. The reason a violet switch reads as
       violet is that the frame round it has next to no colour. A ground
       with real chroma gives all of it to every surface, and the accents
       stop standing out.

       The ink has to read on the ground. DS.colourSettings measures every
       pair in both themes as you pick and says so when one drops under
       4.5:1 (3:1 for an edge), and DS.contrastReport() measures any page
       that set its own. */
  --colour-ground: #101012;
  --colour-ink: #f7f1ea;
  --colour-primary: #c39cdc;
  --colour-accent: #babf95;

  /* A field's inset: the room between its border and the text typed in it. */
  --field-y: 7px;
  --field-x: 10px;

  /* ------------------------------------------------------------- metrics -- */
  --radius: 12px;
  --radius-sm: 8px;
  --radius-lg: 16px;
  --radius-pill: 999px;

  /* ----------------------------------------------------------- type scale --
     Thirteen steps, and nothing off them. Every `font-size` in this package
     is one of these, so a new panel picks a step rather than inventing 13.5px
     because it looked right that afternoon — which is how a stylesheet comes
     to have seventeen sizes, four of them used once each.

     What lands where, so a new thing can be placed without measuring:

       1  10px    small-caps group labels, the kind mark on a row
       2  10.5px  the quietest real text: section counts, a sub-slug
       3  11px    hints, slugs, chips, counted asides
       4  11.5px  secondary body — a stat, a quoted paragraph, a row's blurb
       5  12px    controls: buttons, nav items, banners
       6  12.5px  a row's own label, and every form control
       7  13px    names you scan a list by, and text inputs
       8  14px    the document default, and a pane's title
       9  15px    a crumb, a card's heading
      10  16px    the head of a pane or a section of a level
      11  17px    the title of a drilled-in level
      12  19px    the screen's name in the header
      13  24px    a figure read across the room

     Steps 1–4 are half-point apart on purpose: they are the four weights of
     "quieter than the body text", and a dense app leans on all four. */
  --text-1: 10px;
  --text-2: 10.5px;
  --text-3: 11px;
  --text-4: 11.5px;
  --text-5: 12px;
  --text-6: 12.5px;
  --text-7: 13px;
  --text-8: 14px;
  --text-9: 15px;
  --text-10: 16px;
  --text-11: 17px;
  --text-12: 19px;
  --text-13: 24px;

  /* --------------------------------------------------------- row metrics --
     One inset for every row of every bordered group, and one for the taller
     rows that hold a block rather than a line. These exist because "a row in
     a list" is the single most repeated thing in a dense app, and insets
     drift to 9/10/11/12/16px with four different horizontal ones — which is
     exactly the difference a reader notices between two screens without being
     able to name it. */
  --row-y: 8px;
  --row-x: 13px;
  --row-y-block: 11px;
  /* The height of a row, by how many lines the list expects its rows to hold,
     so a switch row and a select row in one group no longer differ by 11px.
     A list says which with data-lines (components.css, section 14); a row
     that needs a fourth line grows. Measured with the fonts loaded: a
     text-only row is 38.5px, a switch row 41, an avatar or select row 51–52,
     a two-line row 56–57, a three-line row 81. */
  --row-h-1: 52px;   /* a name, with a mark, chips, a switch, an avatar, or a
                        field or button up to --control-h */
  --row-h-2: 60px;   /* a name and a line under it */
  --row-h-3: 84px;   /* a name, a line, and a row of chips */

  /* ------------------------------------------------------------ controls --
     Three sizes of clickable thing, each a height and a horizontal inset, and
     every button is one of them. Heights rather than vertical padding,
     because the defect these fix is two buttons of different classes sitting
     on one bar at different heights — a thing you see immediately and cannot
     fix by choosing a padding, since the type inside them differs too.

       sm  26px   an escape hatch beside a control
       md  32px   the ordinary button, the nav item, the quiet icon
       lg  38px   a page's own action: Save, a pane's way out

     Six pixels apart, so two rungs are never mistaken for one rung and a
     rendering difference. The type step goes with the size — --text-3 on sm,
     --text-5 on md, --text-6 on lg — and is written on each rule rather than
     tokenised, because a button's font is one declaration and a token for it
     would only be read once. */
  --control-h-sm: 26px;
  --control-h: 32px;
  --control-h-lg: 38px;
  --control-x-sm: 10px;
  --control-x: 14px;
  --control-x-lg: 18px;

  /* --------------------------------------------------------------- chips --
     A chip is a word with a box round it and it is never clicked to mean
     something — a tag, a state, a scope, a kind. Two densities, because there
     genuinely are two: an ordinary chip beside a name, and the tiny uppercase
     mark that labels a row without competing with it.

     If a third is needed it is one of these two. */
  --chip-y: 2px;
  --chip-x: 9px;
  --chip-y-sm: 1px;
  --chip-x-sm: 7px;

  /* ---------------------------------------------------------- pane inset --
     What a column holds its contents in from its own edges. Not a `--space-*`
     step: those are gaps *between* things and this is the margin of the page
     itself, which is wider than any gap on it and would drag the gap scale up
     with it if they shared a number.

     The side pane is tighter than the content pane because it is narrower —
     the same inset on a 300px column reads as a wide empty border. */
  --pane-x: 18px;
  --pane-y: 14px;
  --pane-x-side: 15px;

  /* --------------------------------------------------------- side column --
     How wide the fourth column is. The only width in the shell that is a
     token rather than a literal on its own rule, and the reason is that it is
     the only one two apps disagree about. The rail is an icon and a word and
     the sub-nav is a list of short names, so both are what they are wherever
     they are drawn; the side pane holds whatever the app puts in it, and an
     app whose pane carries a paragraph of evidence wants more room than one
     whose pane carries a name and four facts.

     So the number lives here with a default, and an app that wants a wider
     one says so in its own stylesheet loaded after this file. Left as a
     literal in shell.css it would be one number two apps had to agree on,
     and the way that argument ends is a second copy of shell.css.

     352px, which is what the column has always been: a consumer that
     overrides nothing sees no change at all. */
  --side-w: 352px;

  /* -------------------------------------------------------------- drawer --
     How wide the sheet from the right opens: one thing edited at length, and
     the wide one for an editor beside its preview. Tokens for the reason
     `--side-w` is: they hold whatever the app edits in them, and an app with
     a long document wants a different answer from one with a short form.
     Each is capped at the viewport where it is used. */
  --drawer-w: 760px;
  --drawer-w-wide: 1240px;

  /* ------------------------------------------------------------ doc frame --
     The width an email is previewed at on a phone. A device's width, not a
     style: 375px is the narrowest phone a newsletter is still designed for. */
  --doc-narrow: 375px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* How far a highlight or saved flash reaches past the thing on every side,
     so it never hugs a label's top or a field's edge. Its container needs this
     much room; every pane inset is wider. */
  --flash-out: 6px;

  /* Motion: the short one is for state a finger is holding (a switch thumb),
     the long one for something entering or leaving the page. */
  --motion-short: 120ms;
  --motion-long: 260ms;
  --easing: cubic-bezier(.2, 0, 0, 1);
  /* Something large arriving over the page, and going. Leaving is the shorter:
     whoever closed it has already looked away. Arriving slows into place,
     going speeds up out of it, and the spring overshoots once, for a thing
     that has just landed somewhere new. */
  --motion-enter: 320ms;
  --motion-leave: 200ms;
  --ease-out: cubic-bezier(.2, .9, .25, 1);
  --ease-in: cubic-bezier(.5, 0, .75, 0);
  --ease-spring: cubic-bezier(.3, 1.5, .5, 1);
  /* A press. Down is near-instant so it is felt on the frame it happens;
     release springs back on --ease-spring with one overshoot, which is the
     click. A filled control also brightens while held. */
  --motion-press: 50ms;
  --motion-release: 260ms;
  --ease-press: cubic-bezier(.3, 0, .7, 1);
  --press-scale: .95;       /* buttons */
  --press-scale-sm: .88;    /* small targets: checkbox, radio, icon button */
  --press-scale-lg: .97;    /* wide targets: side action, add row, form-panel button */
  --press-lit: 1.12;

  --icon-size: 20px;
  --icon-size-sm: 16px;
  --icon-size-lg: 24px;

  /* Who, as a face or a letter: on a card or in a stack, in a row or on the
     account button, and at the head of the account menu. */
  --avatar-sm: 24px;
  --avatar: 32px;
  --avatar-lg: 40px;

  /* How wide the colour picker opens: a plane a thumb can steer across and a
     hex box that holds all seven characters. */
  --picker-w: 232px;

  /* ----------------------------------------------------------------- type --
     Two faces, both self-hosted above, and the reason is in that @font-face
     block: an app that reaches fonts.googleapis.com renders in a fallback on
     a locked-down network.

     `--font-display` is the seam for a brand's titular face. It points at the
     face actually shipped rather than at one that resolves only by luck of a
     local install — two users would otherwise see two apps. Landing a
     different display face is three things: a woff2 beside the other two, an
     @font-face above, and its name at the front of this line. */
  --font-display: "DM Sans", ui-sans-serif, system-ui, sans-serif;
  --font: "DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;
}

/* ===========================================================================
   THEMES — dark and light, both made from the four colours above.

   The dark theme is the system's own and needs nothing. The light one is
   `data-theme="light"` on the root, which DS.theme('light') sets and
   remembers; DS.theme('system') follows the device. Either can also go on
   one element, and everything inside it takes that theme: the contrast check
   measures the theme not on screen that way.

   Three rules, because a token here is worked out where it is declared, and
   an element that changes theme has to work every one of them out again:

     1. Every colour token is declared in a block whose selector reaches a
        themed element: the shared one (`:root, [data-theme]`) or a theme's
        own. None is declared in a bare `:root`.
     2. A formula that holds whichever way round the ground is goes in the
        shared block, once. A step that has to run the other way in the light
        (a surface one rung up is lighter on a dark ground and darker on a
        light one) goes in each theme's block, with its own number.
     3. Every pair that is read is measured in both, at the defaults. The
        numbers in each block's notes are those measurements.
   =========================================================================== */

/* ------------------------------------------------------------ both themes --
   What holds whichever way round the ground is. */
:root, [data-theme] {
  --on:       var(--ink);
  --accent-2: var(--accent);

  /* `--on-primary` goes on the primary itself: the primary's darkest shade
     while the primary is light, and its lightest tint once it is dark (under
     0.62 lightness), so words on a primary button read either way. */
  --on-primary: oklch(from var(--primary) clamp(0.234, (0.62 - l) * 1000, 0.97) calc(c * 0.5) h);

  /* What floats casts a shadow, and a backdrop dims the page under a dialog:
     the ground taken nearly to black, given its opacity where it is drawn. */
  --shade: oklch(from var(--ground) calc(l * 0.2) c h);

  /* The one shadow in the system: the snackbar, a menu, a dialog, anything
     that floats over the page. Everything else is separated by lines and
     surfaces. */
  --shadow-snackbar: 0 8px 24px color-mix(in srgb, var(--shade) 50%, transparent);

  /* The paper a rendered document is shown on: white in both themes. An
     email is designed on white and its own colours assume it, so a preview
     drawn on the ground would show the client a different email from the one
     they send. Not a palette colour: nothing else may use it. */
  --doc-paper: #ffffff;
}

/* --------------------------------------------------------------- dark --
   The system's own. [measured 2026-10-03, at the defaults] `--on` on
   `--surface` 16.95:1; `--fainter` on `--surface-low` 4.54:1, the tightest;
   `--shell-ink` on `--surface-rail` 5.42:1; `--control-edge` on `--surface`
   3.91:1; `--on-primary` on `--primary` 7.36:1. */
:root, [data-theme="dark"] {
  /* The one line that makes every *native* control agree with the palette: a
     select's dropdown, a date picker, a number spinner, the text caret.
     Without it the browser draws those parts in its light default. */
  color-scheme: dark;

  --ground:  var(--colour-ground);
  --ink:     var(--colour-ink);
  --primary: var(--colour-primary);
  --accent:  var(--colour-accent);

  /* ---------------------------------------------------------- surfaces --
     Steps of lightness off the ground, darkest on the outside. The content
     pane sits one step down from the surface, so a card or a row on it reads
     as lit; `--surface` is the ground itself.

     Five content steps rather than three, because a layout stacks a rail on
     a page on a card on a row and three greys cannot keep those apart. */
  --surface:       var(--ground);
  --surface-low:   oklch(from var(--ground) calc(l + 0.0405) c h);
  --surface-cont:  oklch(from var(--ground) calc(l + 0.0800) c h);
  --surface-high:  oklch(from var(--ground) calc(l + 0.1375) c h);
  --surface-est:   oklch(from var(--ground) calc(l + 0.2050) c h);

  /* ---------------------------------------------------- shell surfaces --
     The columns of every screen. The rail and the sub-nav carry a third of
     the ground's chroma. That is not a boundary anybody sees: lightness and
     the hairline are what tell the columns apart. It is a guard, so that a
     ground with some colour in it still leaves the shell's own grey text
     grey, where a tinted grey would read as a faint colour rather than
     quiet text.

     The content pane is one surface at every depth. It carried a lighter
     step once you had drilled in, but the level's own head, the breadcrumb
     and the nav all say that already, and the tint only made two pages of
     the same screen look like two different screens. */
  --surface-rail:  oklch(from var(--ground) calc(l - 0.0630) calc(c / 3) h);
  --surface-nav:   oklch(from var(--ground) calc(l - 0.0290) calc(c / 3) h);
  --surface-pane:  oklch(from var(--ground) calc(l - 0.0155) c h);

  /* Grey text on a shell surface, with the shell's third of the chroma. The
     shell's own ink, and nothing outside it may ask: a component outside the
     rail or the nav reaching for this is claiming to be part of the shell,
     which is the same rule `--hairline` carries. */
  --shell-ink: oklch(from color-mix(in oklab, var(--ink) 55.4%, var(--ground)) l calc(c / 3) h);

  /* -------------------------------------------------------------- lines --
     Three, and nothing else draws a line. All much quieter than a border
     usually is: a divider separates two surfaces without drawing a box round
     either, which is what lets four columns sit side by side without the
     screen reading as a grid of cards.

     Pick by *what the line separates*, never by how dark it should look:

       `--hairline`  between the columns of the shell — and nothing else.
       `--edge`      round a group. A card, a list, a bordered box, a
                     banner, a tile — anything with an inside.
       `--rule`      between two rows inside one group, and under a group's
                     own head. It never closes; it only divides. */
  --hairline: oklch(from var(--ground) calc(l + 0.0380) calc(c / 3) h);
  --rule:     oklch(from var(--ground) calc(l + 0.0600) c h);
  --edge:     oklch(from var(--ground) calc(l + 0.1080) c h);

  /* ------------------------------------------------------------- fields --
     A control you can type into has to look like it has an inside: a hole in
     the surface, and a border brighter than any of the three lines. */
  --field:              oklch(from var(--ground) calc(l - 0.0175) c h);
  --field-border:       oklch(from var(--ground) calc(l + 0.1635) calc(c * 0.7) h);
  --field-border-hover: oklch(from var(--ground) calc(l + 0.2635) c h);

  /* The outline of a control with no fill -- the outlined button. A
     control's boundary is how somebody finds it, so it is held to the 3:1 a
     boundary owes (WCAG 1.4.11), which none of the lines above is. */
  --control-edge: oklch(from var(--ground) calc(l + 0.3760) c h);

  /* ---------------------------------------------------------------- ink --
     One ramp, six steps, loudest first. Every colour a glyph is ever drawn
     in is on it, and a component picks a step by how loudly the thing should
     speak rather than by naming a new colour:

       `--on`          the thing you came to read: the ink itself
       `--on-variant`  a label beside it
       `--outline`     quoted prose, a monospaced value, a switch's track
       `--dim`         a hint, a count, an aside — still meant to be read
       `--faint`       a slug under a name, a unit after a figure
       `--fainter`     the quietest thing on the screen that is still text

     Below `--fainter` is decoration, and decoration takes a line or a
     surface colour, not an ink. Each rung is the ink mixed towards the
     ground, so the ramp keeps its order whatever the two are, and every
     rung clears 4.5:1 on `--surface` and `--surface-low` both. */
  --on-variant: color-mix(in oklab, var(--ink) 95.4%, var(--ground));
  --outline:    color-mix(in oklab, var(--ink) 82.1%, var(--ground));
  --dim:        color-mix(in oklab, var(--ink) 67.5%, var(--ground));
  --faint:      color-mix(in oklab, var(--ink) 57.3%, var(--ground));
  --fainter:    color-mix(in oklab, var(--ink) 54.8%, var(--ground));

  /* ------------------------------------------------------------ accents --
     Each accent's containers are its own hue at fixed lightnesses, so the
     text on each holds its contrast whatever colour is picked; only their
     chroma follows the pick.

     Violet is "this is on, this is the action". Sage, `--accent-2`, is
     "this is the thing you are looking at or editing". Keeping the two jobs
     apart is the whole reason for a second hue; a third accent, or either
     used because a section looked plain, spends the only two meanings the
     system has. */
  --primary-cont:      oklch(from var(--primary) 0.446 calc(c * 1.2) h);
  --on-primary-cont:   oklch(from var(--primary) 0.907 calc(c * 0.42) h);
  --secondary-cont:    oklch(from var(--primary) 0.316 calc(c * 0.75) h);
  --on-secondary-cont: oklch(from var(--primary) 0.851 calc(c * 0.7) h);
  --accent-2-cont:     oklch(from var(--accent) 0.344 calc(c * 0.55) h);
  --on-accent-2-cont:  oklch(from var(--accent) 0.902 calc(c * 0.75) h);
  /* The section of one thing you are in, in the sub-nav: violet, because
     it is navigation, and a step apart from a filter that is on
     (`--secondary-cont`), because the two sit in one column. In the dark
     the primary's container already is that step (1.65:1 between them). */
  --nav-current:       var(--primary-cont);
  --on-nav-current:    var(--on-primary-cont);

  /* -------------------------------------------------------------- state --
     Three, not four: there is no "success". A thing that worked says so by
     looking ordinary. Not settings: each is a meaning, and a meaning keeps
     its hue (blue, amber, pink). Their lightness is a fixed step off the
     ink, so they read on the ground as well as the ink does. */
  --info:  oklch(from var(--ink) calc(l - 0.162) 0.056 239);
  --warn:  oklch(from var(--ink) calc(l - 0.158) 0.088 72);
  --error: oklch(from var(--ink) calc(l - 0.160) 0.085 15.5);
}

/* -------------------------------------------------------------- light --
   The same four colours with their lightness turned round. Every step above
   that rises off the ground falls here, by about two thirds as much: a light
   surface shows a smaller step than a dark one does. The pane still sits
   below the surface, so a card on it is the lightest thing on the screen.
   [measured 2026-10-03, at the defaults] `--on` on `--surface` 15.47:1;
   `--fainter` on `--surface-low` 5.03:1; `--shell-ink` on `--surface-rail`
   5.19:1; `--control-edge` on `--surface-cont` 3.60:1, the tightest;
   `--on-primary` on `--primary` 5.59:1; `--info` on `--surface-low` 5.07:1. */
[data-theme="light"] {
  color-scheme: light;

  --ground:  oklch(from var(--colour-ground) 0.975 c h);
  --ink:     oklch(from var(--colour-ink) 0.235 calc(c * 1.4) h);
  --primary: oklch(from var(--colour-primary) 0.5 calc(c * 1.25) h);
  --accent:  oklch(from var(--colour-accent) 0.47 calc(c * 1.15) h);

  --surface:       var(--ground);
  --surface-low:   oklch(from var(--ground) calc(l - 0.022) c h);
  --surface-cont:  oklch(from var(--ground) calc(l - 0.042) c h);
  --surface-high:  oklch(from var(--ground) calc(l - 0.070) c h);
  --surface-est:   oklch(from var(--ground) calc(l - 0.105) c h);

  --surface-rail:  oklch(from var(--ground) calc(l - 0.045) calc(c / 3) h);
  --surface-nav:   oklch(from var(--ground) calc(l - 0.025) calc(c / 3) h);
  --surface-pane:  oklch(from var(--ground) calc(l - 0.012) c h);

  --shell-ink: oklch(from color-mix(in oklab, var(--ink) 66%, var(--ground)) l calc(c / 3) h);

  --hairline: oklch(from var(--ground) calc(l - 0.085) calc(c / 3) h);
  --rule:     oklch(from var(--ground) calc(l - 0.065) c h);
  --edge:     oklch(from var(--ground) calc(l - 0.120) c h);

  /* A field is the lightest thing there is: white, or as near as the ground
     allows. */
  --field:              oklch(from var(--ground) min(1, calc(l + 0.025)) c h);
  --field-border:       oklch(from var(--ground) calc(l - 0.270) calc(c * 0.7) h);
  --field-border-hover: oklch(from var(--ground) calc(l - 0.400) c h);
  --control-edge:       oklch(from var(--ground) calc(l - 0.400) c h);

  --on-variant: color-mix(in oklab, var(--ink) 92.6%, var(--ground));
  --outline:    color-mix(in oklab, var(--ink) 81.8%, var(--ground));
  --dim:        color-mix(in oklab, var(--ink) 72%, var(--ground));
  --faint:      color-mix(in oklab, var(--ink) 66%, var(--ground));
  --fainter:    color-mix(in oklab, var(--ink) 63%, var(--ground));

  /* Containers go pale and their text dark: the same hue, the other way. */
  --primary-cont:      oklch(from var(--primary) 0.900 calc(c * 0.45) h);
  --on-primary-cont:   oklch(from var(--primary) 0.320 c h);
  --secondary-cont:    oklch(from var(--primary) 0.935 calc(c * 0.3) h);
  --on-secondary-cont: oklch(from var(--primary) 0.360 calc(c * 0.85) h);
  --accent-2-cont:     oklch(from var(--accent) 0.925 calc(c * 0.45) h);
  --on-accent-2-cont:  oklch(from var(--accent) 0.330 calc(c * 0.9) h);
  /* Pale, the primary's container sat 1.12:1 from a filter that is on, and
     a section and a filter read as the same pill. A step darker:
     [measured 2026-10-04] 1.56:1 from `--secondary-cont`, its text 7.03:1. */
  --nav-current:       oklch(from var(--primary) 0.800 calc(c * 0.6) h);
  --on-nav-current:    var(--on-primary-cont);

  /* A step up from the ink rather than down, and more chroma, since a hue
     that dark needs more of it to still read as blue, amber or pink. */
  --info:  oklch(from var(--ink) calc(l + 0.27) 0.10 239);
  --warn:  oklch(from var(--ink) calc(l + 0.27) 0.11 65);
  --error: oklch(from var(--ink) calc(l + 0.27) 0.14 20);
}

/* ===========================================================================
   DOMAIN TOKENS — replace these, do not extend the ones above.

   Everything above is the system. Everything below is one app's vocabulary
   expressed in it, kept here as a worked example of the right way to add a
   meaning: a token named for what it MEANS, derived from the four colours
   above, so the thing and its colour cannot drift apart.

   Delete this block and write your own. The rule to carry over is the naming:
   `--tag-money`, not `--tag-amber`. A colour named for itself gets reused for
   the next thing that looked about right, and then it means two things.

   Its selector is the themes' shared one, for THEMES' first rule: a colour
   declared in a bare `:root` would keep the dark theme's value inside a
   light element.
   =========================================================================== */
:root, [data-theme] {
  /* A tag's colour is its meaning, so it is named for the meaning. Where a
     state already means it, the tag is that state; a new meaning takes a hue
     of its own at a state's lightness, so it moves with the theme as they do. */
  --tag-money: var(--warn);
  --tag-destructive: var(--error);
  --tag-pii: var(--info);
  --tag-beta: oklch(from var(--warn) calc(l - 0.056) 0.115 343);
  --tag-bulk: oklch(from var(--warn) calc(l - 0.085) 0.046 306);

  /* Roles, in a deliberate order of authority. */
  --role-admin: var(--warn);
  --role-operator: var(--primary);
  --role-reader: var(--outline);
}
