/* ============================================================================
   House of Value - design tokens · F0 · v1.0.0
   Frozen 2026-08-13 from the accepted design spec:
   the frozen shell spec, kept outside this repo (variant D · white · deep-indigo · serif
   display), chosen by Natan after 5 rejected rounds - the full trail is in
   the header of this file. DO NOT re-warm the palette: mocha/brown was
   explicitly rejected ("I like more white").
   Modes: [data-mode="light"] (default) · [data-mode="dark"].
   ============================================================================ */

/* THE CANVAS ITSELF, and it is not a token. `color-scheme` is the only lever that
   tells the user agent what to paint behind the document and what to paint the
   scrollbars, the native form controls, the date pickers and the spellcheck underlines
   with. Without it those stay light-scheme in a dark theme - a pale scrollbar down the
   side of a near-black page - and no amount of custom properties reaches them, because
   they are not styled by this sheet at all.
   It has to be on the ROOT: per CSS Color Adjust 1 section 2.4 it does nothing for the
   canvas when it is set on `body`, which is the usual way this is got wrong.
   `light dark` is the honest declaration for the `system` preference - it means "either,
   follow the user" - and the two explicit preferences pin it with `only`, so a reader
   who chose light on a dark machine gets light scrollbars too. Selecting on data-theme
   (the preference) rather than data-mode (the paint) is deliberate and keeps `system`
   on the declaration that follows the OS. */
:root{color-scheme:light dark}
[data-theme="light"]{color-scheme:only light}
[data-theme="dark"]{color-scheme:only dark}

[data-mode="light"]{
  /* surfaces - cool near-whites, content on pure white */
  --paper:#fafafa;          /* app ground: header, rails */
  --page:#ffffff;           /* reading surface */
  --card:#fff;              /* bordered cards */
  --wash-card:#f4f5fd;      /* accent-tinted callout fill */
  /* lines */
  --rule:rgba(15,18,25,.08);
  --rule2:rgba(15,18,25,.15);
  /* ink - neutral greys, never warm */
  /* --muted was #8d919b and measured 3.02:1 on --page and 3.16:1 on --card. WCAG 1.4.3
     wants 4.5:1 for text at these sizes, and every use of it IS text - the crumb links,
     the search bar and its kbd hint, the home section labels. axe-core reported it as
     `color-contrast`, serious, on the first run of src/app/tests/verify-accessibility.mjs,
     against the live surface. #6f737d measures 4.55 / 4.75 on the same two surfaces,
     keeps the cool neutral hue, and stays visibly lighter than --soft so the four-rung
     ink scale does not collapse into three. */
  --text:#17181c; --body:#3d4149; --soft:#656972; --muted:#6f737d;
  /* the one accent - deep royal indigo, used sparingly */
  --accent:#4338ca; --accent-ink:#3730a3;
  --wash:rgba(67,56,202,.075); --wash2:rgba(67,56,202,.14);
  /* kind colors (one dot per folder, from house-of-value) */
  --g-guide:#16a34a; --g-ref:#2563eb; --g-std:#9333ea; --g-rec:#ea580c; --g-tmp:#ca8a04;
  /* status */
  --warn:#b45309; --warn-wash:rgba(180,83,9,.07); --ok:#15803d;
  /* code */
  --code-bg:rgba(15,18,25,.05); --code-fg:#3730a3;
  /* The admin strip. In light it is a near-black band on a near-white ground - 17.33:1,
     an inversion, and it needs no edge. See the dark block for why that trick has no
     dark-mode twin. */
  --block:#14161c; --block-fg:#e3e6ec; --block-edge:transparent;
  /* elevation - three levels: rest, raised (hover), floating (popover) */
  --sh:0 1px 2px rgba(15,18,25,.04);
  --sh-raise:0 2px 6px rgba(15,18,25,.07),0 1px 2px rgba(15,18,25,.05);
  --sh-pop:0 18px 50px rgba(15,18,25,.15),0 2px 8px rgba(15,18,25,.06);
  --veil:rgba(15,18,28,.35);
  /* skeleton - measured, not chosen. The usual #e0e0e0 default is 1.32:1 against
     this surface, less than half the 3:1 a non-text graphic owes a low-vision
     reader, and #d4d4d4 (the usual "accessible" swap) is 1.48:1. #909090 is
     3.1925:1 against --page #ffffff and 3.0587:1 against --paper #fafafa, so it
     clears on both surfaces a skeleton can land on. It is heavier than a designer
     would pick by eye, which is the point: the eye was the thing that was wrong.
     Ratios are computed to four places by verify-loading.mjs, because a value that
     ROUNDS to 3.00 is not at 3.00 - #959595 is 2.9953:1 and fails. */
  --sk:#909090;
  --sk-strong:#767676;      /* 4.5422:1 - only under prefers-contrast: more */
}
[data-mode="dark"]{
  --paper:#101114; --page:#16171b; --card:#1c1d22; --wash-card:#1f2030;
  --rule:rgba(255,255,255,.09); --rule2:rgba(255,255,255,.17);
  /* Same defect, same fix, other theme: #686c75 measured 3.20:1 on --card, which is the
     tightest of the three dark surfaces and therefore the one to size against. #868a93
     measures 4.86 there, 5.18 on --page and 5.46 on --paper. */
  --text:#eef0f4; --body:#c3c6cd; --soft:#8f939c; --muted:#868a93;
  --accent:#a5b4fc; --accent-ink:#c7d2fe;
  --wash:rgba(165,180,252,.1); --wash2:rgba(165,180,252,.19);
  --g-guide:#4ade80; --g-ref:#60a5fa; --g-std:#c084fc; --g-rec:#fb923c; --g-tmp:#facc15;
  --warn:#d9a441; --warn-wash:rgba(217,164,65,.09); --ok:#4ade80;
  --code-bg:rgba(255,255,255,.07); --code-fg:#c7d2fe;
  /* WAS #0e0f13, which measured 1.015:1 against --paper #101114 - the app header sits
     directly below it, so the strip had no shape at all and read as part of the header.
     Reported from a signed-in admin session, 2026-09-18.
     The light theme separates this band by INVERTING it, and that move has no dark
     twin: a light strip in a dark theme is a glare bar. Tone alone cannot carry it
     either - no value on this surface ladder reaches 3:1 against --paper without
     becoming a wall (#2b2d34 is still only 1.37:1). So it is carried the way every
     other dark surface here is carried: a raised tone plus a hairline, which is the
     same mechanism as --card #1c1d22 on --page #16171b at 1.06:1 plus --rule.
     #24262d is 1.25:1 on --paper, 1.19:1 on --page and sits a step above --card, so
     the strip reads as raised chrome on any page under it. --block-fg holds 10.7:1
     on it, and .ab-who at opacity .85 still holds 8.1:1. */
  --block:#24262d; --block-fg:#d6d9e0; --block-edge:rgba(255,255,255,.14);
  --sh:0 1px 2px rgba(0,0,0,.4);
  --sh-raise:0 2px 8px rgba(0,0,0,.5);
  --sh-pop:0 18px 50px rgba(0,0,0,.6);
  --veil:rgba(0,0,0,.5);
  --sk:#6a6a6a;             /* 3.3114:1 against --page #16171b */
  --sk-strong:#808080;      /* 4.5349:1 */
}

/* type - three voices, no more:
   serif display (doc titles, h2)   = Newsreader, opsz high
   UI                               = SF / system stack
   data, labels, badges             = JetBrains Mono */
:root{
  --font-ui:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",system-ui,sans-serif;
  --font-display:'Newsreader',Georgia,serif;
  --font-mono:'JetBrains Mono',ui-monospace,monospace;
  --fs-ui:14.5px; --lh:1.65;
  --r-xs:4px; --r-sm:5px; --r-md:8px; --r-lg:9px; --r-xl:12px; --r-pill:99px; --r-circle:50%;
  /* motion - two easings, three speeds. Respect prefers-reduced-motion.

     TWO CURVES, NOT ONE, AND WHICH GOES WHERE. An entrance decelerates: it puts
     the destination on screen fast and settles into it, so the reader sees where
     the thing landed rather than watching it travel. An exit accelerates: the
     reader has already decided, and the fastest honest way to say "gone" is to
     leave quickly. One curve used both ways makes every dismissal linger, which
     is the single most common motion defect in a shipped interface.

     The pairing rule in this file: the CLOSED state carries --ease-exit (it is
     the rule that plays while the thing leaves) and the .open state re-declares
     the same transition with --ease (it plays while the thing arrives). Both
     directions stay interruptible, because both are transitions - reverse a
     half-open drawer and it runs back from where it is, not from the start. */
  --ease:cubic-bezier(.25,.7,.3,1);        /* decelerate - entrances, arrivals */
  --ease-exit:cubic-bezier(.4,0,1,1);      /* accelerate - exits, dismissals */
  --dur-micro:120ms;   /* hover, color, border */
  --dur-small:180ms;   /* lift, reveal */
  --dur-med:240ms;     /* drawer, popover, palette */
  /* The fourth step, added 2026-08-25 for the brand reveal and nothing else. A logo
     arriving is not a micro-interaction: the mark's block travels 7.5px at header
     size, and at 240ms that reads as a flicker rather than a movement. 400ms is
     Carbon's slow-01 and sits exactly on this scale's own ceiling, so it is the
     slowest thing that can be expressed here rather than a new licence. Do not reach
     for it for feedback - the reference band for that is 70-150ms. */
  --dur-slow:400ms;    /* a brand reveal, and only that */
}
@media (prefers-reduced-motion:reduce){
  /* .01ms, not 0. This block is load-bearing beyond the durations it names: the
     closed state of every overlay defers `visibility` by `var(--dur-med)`, and this
     is the only thing that collapses that delay - the blanket rule in shell.css
     forces transition-duration and animation-duration, and never touches a delay.
     It says .01ms for the same reason the blanket rule does, and a stranger test
     caught it saying 0ms while both the check and docs/motion.md insisted on .01ms.
     Nothing read this file then. tests/verify-motion.mjs reads it now. */
  :root{--dur-micro:.01ms;--dur-small:.01ms;--dur-med:.01ms;--dur-slow:.01ms}
}

/* ============================================================================
   LAYOUT - breakpoints and widths, and where the numbers come from
   ============================================================================

   This block was added 2026-08-23 after the question "is there a worldwide
   standard we are following, or is this our own custom?" was asked and the
   honest answer was neither: every number was chosen here, and three of the five
   breakpoints sat 8-20px away from a published one. A near-miss is the worst
   case - arbitrary numbers to memorise, with none of a convention's benefit.

   THE SOURCES, read rather than remembered:
     MUI 9.3.1     xs 0 · sm 600 · md 900 · lg 1200 · xl 1536
                   read out of the package itself, not from memory. This repo
                   has no node_modules and no package.json, so re-verify with:
                     npm i @mui/material && grep -oE '(xs|sm|md|lg|xl): [0-9]+' \
                       node_modules/@mui/system/createBreakpoints/createBreakpoints.js
                   Its Container maxWidth is not a separate scale: it resolves to
                   `theme.breakpoints.values[maxWidth]`, so the container widths
                   ARE the breakpoints.
     Bootstrap 5.3 sm 576 · md 768 · lg 992 · xl 1200 · xxl 1400
                   containers 540 · 720 · 960 · 1140 · 1320
     Tailwind 4    sm 640 · md 768 · lg 1024 · xl 1280 · 2xl 1536
                   (defined in rem; 40/48/64/80/96rem at a 16px root)
     Material 3    window size classes, in dp, not device widths:
                   compact <600 · medium 600-839 · expanded 840-1199 ·
                   large 1200-1599 · extra-large >=1600

   THE SELECTION RULE, corrected. It first read "snapped to the nearest value two
   or more systems agree on, which is why 768 and 1024 win - Bootstrap and
   Tailwind both publish them." That is false and a cold reader caught it:
   Bootstrap's lg is 992, stated correctly eleven lines above. Only 768 and 1200
   have two publishers; 640, 1024 and 1400 have one each. Worse than wrong, it
   was the DECISION PROCEDURE - anyone adding a breakpoint would have applied a
   rule the existing set does not satisfy and got a different answer.

   The rule is what the check actually encodes: SNAPPED TO A VALUE AT LEAST ONE
   NAMED SYSTEM PUBLISHES, WITH THE SYSTEM RECORDED. Two publishers is a
   tie-breaker, not a requirement. We did not adopt one vendor throughout
   because the two we already matched exactly came from different systems -
   Tailwind's sm and Bootstrap's xxl - so picking one would have moved numbers
   that were already right.

   ⚠ MATERIAL 3 IS BACKGROUND, NOT A SOURCE FOR A TOKEN. Its classes are window
   size classes in dp, and dp is not a CSS pixel. Listed above because it is what
   the platform reasons in; not admissible in PUBLISHED in the check.

   WHAT NO SYSTEM PUBLISHES: a reading measure. MUI, Bootstrap, Material 3,
   Apple's HIG and Fluent all specify CONTAINER widths - app-shell furniture -
   and none of them states how wide a column of prose should be. The only vendor
   answer is Tailwind's `max-w-prose`, which is 65ch. So --measure below is ours,
   and it is defended by measurement rather than by citation: see the comment on
   .doc in index.html, and note that `ch` is the "0" glyph advance, so 65ch
   renders roughly 70-75 real characters.
   ============================================================================ */
:root{
  /* Breakpoints. DECLARED HERE AND USED LITERALLY, and that is not laziness:
     a custom property CANNOT be used in a media query - `@media (min-width:
     var(--bp-md))` is invalid, never matches, and NEVER ERRORS. It fails
     silently, which is worse than failing. Tokenising them for real needs
     PostCSS @custom-media or a Tailwind build, and this app deliberately has no
     build step. So these five exist to be the single written source of the
     numbers, and tests/verify-breakpoints.mjs holds every @media literal in the
     tree to them - which is the enforcement a var() would have bought.

     SAID PLAINLY: `grep -r "var(--bp-" src/` returns nothing and always will.
     These five are shipped to every browser as documentation with CSS syntax.
     That is the trade - a real declaration is harder to let rot than a comment,
     and the check reads it - but nobody should discover it by grepping. */
  --bp-sm:640px;    /* Tailwind sm. The ONE rule here hides the editor's New
                       button, which pushed the avatar off a 390px viewport.
                       NOT the phone breakpoint - this comment used to say "the
                       drawer replaces the tree" and that happens at --bp-md,
                       128px later. A cold reader following this token would have
                       shipped a phone rule that fires after every other one. */
  --bp-md:768px;    /* Bootstrap md AND Tailwind md. THE PHONE BOUNDARY: the tree
                       goes, the drawer takes over, the shell becomes one column,
                       and the Desk refuses to render below it */
  --bp-lg:1024px;   /* Tailwind lg. Index views take their wider column */
  --bp-xl:1200px;   /* MUI lg AND Bootstrap xl. The outline rail collapses */
  --bp-2xl:1400px;  /* Bootstrap xxl. Prose takes its wider measure */

  /* Widths. These ARE usable as var() and every rule below now takes them.

     THE MEASURE, AND WHY IT IS 630 EVERYDAY AND 690 ON A LAPTOP. 630px is what the
     2026-08-18 type audit derived - but on a 14in MacBook (1512 logical) the shell
     hands the middle column ~1000px and the prose sat in the middle of it, reading as
     a narrow ribbon against a wide screen. The complaint was real and the fix is NOT
     to abandon the measure. MEASURED, not estimated: a rendered 446-character paragraph
     at --measure-wide breaks into 6 lines - 74 characters per line.
     WHERE 74 SITS, and the sourcing matters because the usual number here is folklore.
     The famous "45-75 characters" is Robert Bringhurst, The Elements of Typographic
     Style 2.1.2, stating what is "widely regarded" - for PRINT, serif, single column.
     It is not an empirical finding and never claimed to be. Measured on screen it goes
     the other way: Shaikh & Chaparro 2005 (HFES 49(5):701-705, n=20) found 95 CPL read
     FASTEST, while Dyson & Haselgrove 2001 found better comprehension at 55 than 100 -
     speed and comprehension point in opposite directions. The defensible band is
     60-80 CPL, which is where 74 lands, and the honest claim for this change is "inside
     a defensible band and closer to what readers report preferring", NOT "widened to the
     ceiling of a standard". There is no such standard: WCAG 1.4.8 is Level AAA and asks
     that a MECHANISM be available, not that a default be any particular width.
     An earlier version of this comment called 75 a ceiling and cited "the canon". That
     was the same uncited-authority move this repository spent a night removing from its
     own view documents, committed here an hour later by the session that removed it.
     Below 1400px nothing moves - there the column is not wide enough for the prose to
     look stranded in the first place. And do not read the `ch` CSS unit as the answer:
     it measures the "0" advance, which for a proportional serif reported 58 where the
     real rendered count is 74. The full record, with the rejected alternatives and the
     sources: docs/decisions/0019-the-layout-scale.md */
  --measure:630px;       /* prose, the everyday measure - ~67 real characters */
  --measure-wide:690px;  /* prose above --bp-2xl - measured at 74 real characters */
  --h-header:56px;      /* the app bar's height; anything pinned under it reads this, never the number */
  --w-desk:980px;        /* the Desk. Not a reading width and not an index width: it
                            is the reading measure (630) plus the 28px gutter plus
                            the 320px settings panel, so a writer's canvas is exactly
                            as wide as the column the document will be READ in and the
                            meta sits beside it rather than squeezing it. Below
                            --bp-lg the panel becomes a sheet and this width stops
                            mattering. */
  --w-index:920px;       /* the Lobby, Wings, Vault, Study: indexes, not prose.
                            Between Tailwind 4xl (896) and Bootstrap lg (960) */
  --w-tree:270px;        /* the corpus tree */
  --w-rail:240px;        /* the outline rail */
  --w-tree-md:250px;     /* the tree between --bp-md and --bp-xl */

  /* THE CONTROL KIT'S OWN SCALE (src/app/public/css/kit.css, 2026-09-17).
     Twenty-three controls across the admin wing carried seven heights, nine font
     sizes, two families and three radii for one shape. These are the three heights
     and two sizes that replace them, and nothing in kit.css may state a fourth.
     36 is the everyday control, 32 the dense one a table row and a tab want, 40 the
     one a form field takes when it stands alone. */
  --h-ctl:36px; --h-ctl-sm:32px; --h-ctl-lg:40px;
  --fs-ctl:14px; --fs-ctl-sm:13px;
  /* The admin frame. 256/48 are the widths the collapsible-sidebar archetype
     publishes and the widths this repository already shipped on the Registry; 32 is
     the strip above the reading surface a signed-in writer or admin sees. */
  --w-side:256px; --w-side-rail:48px; --h-adminbar:32px;
  /* Danger is not a new colour. It is --warn under the name a control uses, so a
     destructive button and a stale-freshness chip cannot drift apart. */
  --danger:var(--warn); --danger-wash:var(--warn-wash);
}
