/* ==========================================================================
   ORNO /v3/ — Latin, "I adorn": the dress every app of the house wears.

   Stripe's cards at Linear's density, in this house's vocabulary: a cool grey
   page, white cards edged by a hairline ring and a small shadow, 13px text,
   one teal accent, Geist for reading and Geist Mono for anything to be read
   exactly. Same class names, same tags, same API as /v2/ — the look and the
   sizes changed, the words did not.

   /v2/ was the shadcn/ui look at shadcn's sizes, and /v1/ the neo-brutalist
   dress. Both are still served, still keep their promise, and no longer move:
   `npm run release:check` holds each to its newest release, byte for byte.
   Moving from either is changing URLs, or one package version.

   Consuming it — three lines in <head>, and one before anything paints:

     <link rel="stylesheet" href="https://orno.meliorex.dev/v3/orno.css">
     <script src="https://orno.meliorex.dev/v3/orno-theme.js"></script>   <!-- NOT deferred -->
     <script src="https://orno.meliorex.dev/v3/orno.js" defer></script>

   orno-theme.js is tiny and must run before the first paint, or the page shows
   light for a frame and then turns dark. The house faces are the app's own
   choice to load, and they are served from here rather than from Google — no
   third party learns a visitor's IP address on every page load:

     <link rel="stylesheet" href="https://orno.meliorex.dev/v3/fonts.css">

   Theming: `data-theme="dark"` or `"light"` on <html> decides; without the
   attribute the OS does. (`data-vm-theme` is honoured too, for the app that
   spoke it first.) Every colour is declared once, for both themes, with
   light-dark() — so the two can never drift apart.

   Speaking shadcn: the shadcn variable names (--background, --primary,
   --border, --radius…) are declared too, as aliases of the house tokens, so a
   component copied from shadcn/ui — or a Tailwind theme that maps them — wears
   the same palette without a second source of truth.

   Browsers: the current versions of Chrome, Edge, Safari and Firefox, and no
   further back than light-dark() and :has() reach — Chrome 123, Safari 17.5,
   Firefox 120 (all shipped in 2024). An older browser gets readable, unstyled-
   looking pages rather than broken ones.

   Two addresses, and the choice is yours:

     /v3/         the moving edge. Never changes meaning — additions may come,
                  removals may not, and whatever would move your layout arrives
                  as /v4/. Cached five minutes. Cannot be hashed, because bytes
                  that change by design carry no integrity hash.
     /v3.0.0/     one release, frozen. These bytes never change, so they are
                  served immutable for a year and carry an SRI hash. A fix
                  arrives as a new version; nothing is ever edited in place.

   Pin if you would rather choose when to take a change. Float if you would
   rather not think about it. `npm run release:check` enforces both promises —
   that a cut release is still its own bytes, and that /v3/ has not dropped a
   name somebody already wrote down.
   ========================================================================== */

/* ---- tokens ---- */

/* Every box measures itself the way people expect: padding and border inside the width you
   asked for, not added to it.

   This is here because the same bug arrived six times. A component is given width:100% or
   min-height:100dvh, then padding of its own, and is quietly that padding too big — a wide
   button wider than the card holding it (1.4.0), a login page exactly 96px taller than the
   screen although everything on it fits (found deployed, 1.5.0). Five separate rules in this
   file already carried box-sizing:border-box, each added the day someone met the bug again.
   One rule retires all five, and the ones still waiting to be found.

   One rule retires all five, and the ones still waiting to be found. */
*, *::before, *::after { box-sizing: border-box; }

:root {
  color-scheme: light dark;

  /* Which of the two addresses you loaded. `v3` is the moving edge: always current, never
     pinnable, no integrity hash possible. A pinned copy at /v3.0.0/ says its own version
     here instead — so a page, or a bug report, can answer "what did I actually load?":
       getComputedStyle(document.documentElement).getPropertyValue('--orno-version') */
  --orno-version: 'v3';

  /* Geist for everything a person reads, Geist Mono for everything a person copies — code, a
     key, an IBAN. --font-display is the face of a title, and in this dress it is the same face
     at a heavier weight: shadcn draws hierarchy with weight and size, never with a second
     family. The name stays because /v1 published it and apps already write it down. */
  --font-sans: 'Geist', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
               'Helvetica Neue', Arial, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --font-display: var(--font-sans);

  /* An app may wear its own accent without forking anything — but it is THREE names, not two,
     and the third is the one everybody forgets.

     --acc-ink is what gets painted ON the accent: the label of a primary button, a chip, a
     count. Whichever of paper or ink clears 4.5:1 against your accent is the right value here;
     `npm run contrast` will tell you, and `--accent '#RRGGBB'` tries one before you ship it.

     The house accent is PlugSwitch's teal — teal-700 by day, teal-500 by night, because the
     night one has to work as link text on a near-black ground. Set both to near-black and
     near-white (#171717 / #FAFAFA) for shadcn's own neutral primary. */
  --accBase: #0F766E;
  --accBaseDark: #14B8A6;
  --acc-ink: light-dark(#FAFAFA, #0A0A0A);

  /* The spacing scale. Tailwind's own steps where this scale has one (4, 8, 12, 16, 24, 32), so
     a measurement copied out of a shadcn component lands on a name. */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px;

  /* The type scale, reading end. The names are what /v1 published, so they stay; the values
     are Linear's density — a label 11, fine print 12, the body 13 — and almost everything an
     app shows is one of those. /v2 read at shadcn's 14; /v3 reads a step smaller and closer,
     the way a tool somebody works in all day does. */
  --fs-nano: 11px; --fs-caps: 11px; --fs-micro: 12px; --fs-fine: 12px;
  --fs-note: 12px; --fs-ui: 13px; --fs-text: 13px; --fs-body: 13px;

  /* The display end, named after what wears it. */
  --fs-lead: 15px;    /* the line under a door title */
  --fs-sub: 17px;     /* a stat's figure */
  --fs-mark: 16px;    /* a large avatar's initials, a section head */
  --fs-brand: 16px;   /* the wordmark beside a logo */
  --fs-act: 18px;     /* the one round act on a tab bar */
  --fs-card: 24px;    /* a door card's title */
  --fs-title: 22px;   /* a page title — 20px at the phone width, after this block */
  --fs-hero: clamp(30px, 3.6vw, 48px);  /* the door's own headline */

  --z-menu: 60; --z-dialog: 80; --z-toast: 90;
  --t-fast: 150ms; --t-mid: 200ms; --t-slow: 250ms;

  /* One radius, and its steps. shadcn derives every corner from --radius, so a team that wants
     sharper or rounder corners changes one number. */
  --radius: 8px;
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);

  /* The shadow, by the weight of what casts it — Tailwind's shadow-xs to shadow-xl. In /v1 these
     were the length of a hard offset; here each is the whole shadow, so a rule says
     `box-shadow: var(--sh-card)` and nothing else. --sh-card is a card's whole edge and is told
     below. The rest are black at a low alpha in both themes: on a dark ground they are barely
     there, which is how shadcn draws them, and the border of a menu, a dialog or a drawer does
     the separating at night. */
  --sh-btn: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  /* A card is edged by its shadow: a hairline ring and a small fall, and no border. light-dark()
     takes colours only, so each colour of the shadow is chosen per theme — the day's ring is the
     ink's own grey-blue, the night's a faint light line, because a dark shadow on a dark page
     draws nothing. */
  --sh-card: 0 0 0 1px light-dark(rgb(64 68 82 / .08), rgb(255 255 255 / .06)),
             0 1px 3px light-dark(rgb(64 68 82 / .08), rgb(0 0 0 / .5));
  --sh-menu: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
  --sh-dialog: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  --sh-door: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);

  /* Nothing a thumb must hit may be smaller than this. WCAG 2.2 asks 24px;
     a phone asks more, so the house asks more. */
  --hit: 32px;
  /* And a thumb asks more still. Apple asks 44 and Android 48; 44 keeps a seven-column month
     inside a 390px screen with room to spare. Every use is a floor, not a size. */
  @media (pointer: coarse) {
    --hit: 44px;
  }
  /* One height for every control a row puts side by side — a field, a normal button, a
     segmented choice, a stepper, a date — 36px, and the thumb's 44 at the phone width (the rule
     after this block). /v2 kept it behind the opt-in .orno-even, because it moved every app's
     layout; in /v3 it is how controls are. */
  --ctl-h: 36px;
  /* The phone's tab bar as it stands: 1px of border and a 52px item, with the safe area added
     below it where it is used, so a shell page leaves exactly that much room. */
  --tabbar-h: 53px;
  /* How far the round act rises above the bar's top edge, measured: 5px. */
  --tabbar-rise: 5px;

  /* Two widths, and no more. A media query cannot read a custom property, so
     these are written out at every use — but there are only two numbers to
     remember, and an app that invents a third is inventing a layout nobody
     else in the house has.
       --bp-phone  a hand: one column, tables stack, the bar goes to the edge
       --bp-lap    no room for a rail beside the work: the shell and door fold */
  --bp-phone: 560px; --bp-lap: 900px;
  /* How wide running text may get before it is hard to find the next line. */
  --measure: 68ch;
  /* How wide a form or a page of detail may get before the eye loses its row: label, field and
     unit stay one glance apart. */
  --measure-form: 720px;

  /* ---- the palette ----
     Stripe's ground, declared once for both themes: a cool grey page (--bg) and a white card on
     it (--sf), and at night a near-black page with a card a step lighter; --tx and --mut are
     foreground and muted-foreground.

     Two borders, and the difference between them is not taste. --bd2 is shadcn's own border,
     the hairline that rules one row off from the next and outlines a box inside a card:
     decoration, so it stays quiet. --bd is the visible boundary of a CONTROL — an input, a
     tick, a switch — and WCAG 1.4.11 asks that to clear 3:1 against both grounds. shadcn draws
     inputs in the hairline (1.3:1); this house does not, and `npm run contrast` holds it to that.

     --edge is the colour a shadow and a backdrop are made of; --line the outline of a raised
     surface that floats — a menu, a dialog — and the edge a pressed tile keeps. */
  --bg:   light-dark(#F6F8FA, #0B0D10);
  --sf:   light-dark(#FFFFFF, #15181D);
  --bd:   light-dark(#8F8F8F, #737373);
  --bd2:  light-dark(#E3E8EE, #262B32);
  --tx:   light-dark(#1A1B25, #E6E8EB);
  --mut:  light-dark(#5E6676, #9AA1AB);
  --edge: light-dark(#000000, #000000);
  --line: light-dark(#EBEEF1, #22262C);
  --acc:  light-dark(var(--accBase), var(--accBaseDark));
  --gain: light-dark(#15803D, #4ADE80);
  --loss: light-dark(#B91C1C, #F87171);
  --warn: light-dark(#A16207, #FACC15);

  /* Four series colours, for marks that tell one series from the next: the parts of a bar, a
     meter in a row of meters, a ring in a set of rings, a line on a graph. Never text, and never a
     state — what went up is still --gain and what went wrong --loss, whichever series it belongs
     to. Each clears 3:1 against both grounds in both themes, the bar for a mark that carries
     meaning (WCAG 1.4.11), and `npm run contrast` holds it there. Generic names on purpose: the
     house knows series; the app decides that the first one is protein and the fourth calories. */
  --ser1: light-dark(#0D9488, #2DD4BF);
  --ser2: light-dark(#7C3AED, #A78BFA);
  --ser3: light-dark(#B45309, #FBBF24);
  --ser4: light-dark(#E11D48, #FB7185);

  /* ---- shadcn's names, for anything copied from shadcn/ui ----
     Aliases, never values: the house tokens above are the one source, and these follow them.
     A Tailwind v4 `@theme inline` block that maps --color-primary to var(--primary) and so on
     — the block shadcn's CLI writes — then dresses every utility in this palette. */
  --background: var(--bg);
  --foreground: var(--tx);
  --card: var(--sf);
  --card-foreground: var(--tx);
  --popover: var(--sf);
  --popover-foreground: var(--tx);
  --primary: var(--acc);
  --primary-foreground: var(--acc-ink);
  --secondary: color-mix(in oklab, var(--tx) 5%, var(--sf));
  --secondary-foreground: var(--tx);
  --muted: color-mix(in oklab, var(--tx) 5%, var(--sf));
  --muted-foreground: var(--mut);
  --accent: color-mix(in oklab, var(--acc) 10%, var(--sf));
  --accent-foreground: var(--tx);
  --destructive: var(--loss);
  --border: var(--bd2);
  --input: var(--bd);
  --ring: var(--acc);
}
/* The thumb's height and a phone's page title, out here rather than nested in :root so
   tokens.json reads the base. */
@media (max-width: 560px) {
  :root { --ctl-h: 44px; --fs-title: 20px; }
}

/* .orno-even — the opt-in /v2 had. Everything it brought is how /v3 is: one control height, a
   stack's bare paragraphs, a shell page that leaves only the room it needs, an empty outlet that
   takes no row, a drawer heading on one line. Each of those rules still sits directly after the
   rule it amends and weighs what that rule weighs, so every modifier written after it stays in
   charge: .in--area keeps its 96px, .in--door and the wide acts their own floors. The class is
   accepted and does nothing — an app that still sets it on <html> loses nothing and gains
   nothing — and this empty rule keeps the name /v2 published. */
.orno-even {}

/* An explicit choice overrules the OS, and tells the browser too — so the
   scrollbars, the form controls it still draws itself and the caret all turn
   with the page instead of staying stubbornly light. */
:root[data-theme="light"], :root[data-vm-theme="light"] { color-scheme: light; }
:root[data-theme="dark"],  :root[data-vm-theme="dark"]  { color-scheme: dark; }

/* Tighter, for a screen that is all figures — a ledger, a table, an admin tool.
   Set it on the root or on one pane: <div data-density="compact">. It tightens
   the spacing scale and the rows built on it, and deliberately leaves type size
   and target sizes alone: a row can sit closer to the next without a thumb
   losing its mark or a word losing its legibility. */
[data-density="compact"] {
  --s1: 3px; --s2: 6px; --s3: 9px; --s4: 12px; --s5: 18px; --s6: 24px;
}
/* The rows that carry figures do not sit on the spacing scale yet, so they are
   tightened by name, until the day they do. */
[data-density="compact"] .tbl tbody td,
[data-density="compact"] .tbl thead th { padding:6px 10px; }
[data-density="compact"] .ledger__grid { padding:7px var(--s4); }
[data-density="compact"] .fold-list__head { padding:9px var(--s3); }
[data-density="compact"] .fold-list__body { padding:0 var(--s3) 10px; }

/* ---- what each tag is ----

   A custom element is `display: inline` until something says otherwise, and for the whole life
   of this system two things said otherwise: the Angular components through host metadata, and
   the elements in orno.js through `this.style.display ||= …`. Two implementations of one fact,
   written in two languages, and the stylesheet that is supposed to be the single source of
   truth said nothing at all about it.

   Both wrote it as an INLINE style, which is the highest specificity short of !important — so
   the one declaration an app could not override was the one it was most likely to want to.

   Here instead, once, for both layers: the tags are the same tags. It is also the earlier
   answer. CSS applies at parse time, where an upgrade runs when the script arrives, so a
   server-rendered page no longer lays out inline for a frame and then reflows.
   ---------------------------------------------------------------------------------------- */

orno-accordion, orno-accordion-item, orno-amount-field, orno-back-link, orno-card,
orno-checkbox, orno-code,
orno-combobox, orno-door-card, orno-drawer, orno-empty, orno-error-summary, orno-field,
orno-fieldset,
orno-file-field, orno-fold, orno-frame, orno-ledger, orno-menu, orno-meter, orno-nav-item,
orno-option, orno-page-head, orno-pick, orno-radio, orno-search-field, orno-select, orno-skeleton, orno-stamp,
orno-switch, orno-tab-panel, orno-table, orno-tabs, orno-textarea { display:block; }

orno-facts, orno-rail-brand, orno-tab, orno-tabbar-act,
orno-tabbar-item { display:contents; }

orno-seg-group, orno-theme-toggle { display:inline-flex; }

/* A segmented choice sits in a well — shadcn's tabs list. On the tag, because the group has no
   class of its own in either layer. */
orno-seg-group, orno-theme-toggle {
  align-items:center; gap:2px; padding:3px; width:fit-content; max-width:100%;
  border-radius:var(--radius-lg); background:color-mix(in oklab, var(--tx) 5%, var(--sf));
}
/* The toggle always holds a group, and the group brings its own well. The second well
   around it made the toggle 6px taller than every other control, for a frame nobody could see. */
orno-theme-toggle { padding:0; background:none; }

orno-badge { display:inline-block; }
orno-ring { display:inline-block; }
orno-rings { display:block; }

orno-radio-group { display:grid; }

orno-rail { display:flex; }

orno-pick-group { display:grid; gap:var(--s3); }

orno-date-field { display:block; position:relative; }

/* ---- ground rules ---- */

html, body { margin:0; padding:0; }
body {
  -webkit-font-smoothing:antialiased;
  background:var(--bg); color:var(--tx);
  font-family:var(--font-sans); font-size:var(--fs-body); line-height:1.5;
  font-feature-settings:'cv11', 'ss01';
}

/* What is read exactly is set in the mono face. The body is no longer monospace, so the
   elements that meant "this is literal" have to say so themselves. */
code, kbd, samp, pre { font-family:var(--font-mono); }

/* Corners are each component's own, from the --radius scale. /v1 flattened every radius in the
   document with one wide rule; /v2 has none, so a third-party widget keeps its own corners and
   nothing needs fencing off. `.round` and `[data-orno-ignore]` are still honoured — they simply
   have nothing left to escape from. */
.round { border-radius:9999px; }

/* A bare h1 wears the display face: the same family, heavier and drawn in tight. An app's own
   heading rule beats this one, as it should — there is no !important here. */
h1 {
  font-family:var(--font-display);
  font-weight:700;
  letter-spacing:-0.025em;
}

a { color:var(--acc); text-underline-offset:4px; }
@media (hover: hover) { a:hover { color:var(--tx); } }

@keyframes ornoSpin { to { transform:rotate(360deg); } }
@keyframes vmSpin   { to { transform:rotate(360deg); } } /* the older name, kept alive */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration:0.01ms !important;
    animation-duration:0.01ms !important;
    animation-iteration-count:1 !important;
    scroll-behavior:auto !important;
  }
  /* Except the spinner. A spinner frozen mid-turn says nothing at all — worse than nothing,
     because it reads as a page that has hung. It slows down instead of stopping. */
  .spin,
  [aria-busy='true']::before {
    animation-duration:2.4s !important;
    animation-iteration-count:infinite !important;
  }
}

/* ---- hover & focus ----

   Hover lives IN the component classes below; it only speaks to a pointer that can
   hover (on a touchscreen :hover is sticky). Focus is one deliberate ring, everywhere — the
   accent, offset from the edge, the way shadcn's ring sits off a control.
   The scp0–scp7 classes beneath are DEPRECATED aliases from the first export — they
   keep old templates alive and die in /v2. */

/* The keyboard's ring: every act answers focus the same way.

   The three classes at the end are not decoration. A tick, a radio and a switch are <input>
   elements, which are none of the tags above, so for the whole life of the system they wore
   the BROWSER's ring instead of this one — measured at `1px auto rgb(16,16,16)` in Chrome,
   and something else in every other engine. "One deliberate ring, everywhere" was true of
   everything except the three controls a form is mostly made of. */
:where(button, a, [role="radio"], [role="option"], [tabindex],
       .check, .radio, .switch):focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}
/* A sheet and a dialog take focus only to hold it — where a screen reader starts and where Tab
   goes on from — so a ring round the whole surface marks nothing a person can act on. Their
   heading, buttons and fields keep theirs. The rule above is in :where(), so a class wins. */
.drawer:focus-visible, .dialog:focus-visible { outline:none; }
.in:focus { border-color:var(--acc); outline:none;
            box-shadow:0 0 0 3px color-mix(in oklab, var(--acc) 25%, transparent); }

@media (hover: hover) {
  .btn:hover:not(:disabled)        { background:color-mix(in oklab, var(--tx) 5%, var(--sf)); }
  .btn-acc:hover:not(:disabled)    { opacity:0.9; }
  .btn-ghost:hover:not(:disabled)  { background:color-mix(in oklab, var(--tx) 5%, transparent); }
  .btn-quiet:hover:not(:disabled)  { color:var(--tx); background:color-mix(in oklab, var(--tx) 5%, transparent); }
  .btn-danger:hover:not(:disabled) { opacity:0.9; }
  /* Underlined, as shadcn's link is — and a step toward ink, because an underline alone is a
     line a colour-blind eye and a glance can both miss. */
  .btn-link:hover, .btn-link-sm:hover { text-decoration:underline;
                                        color:color-mix(in oklab, var(--acc) 70%, var(--tx)); }
  .tabbar__act:hover { opacity:0.9; }
  /* The controls answered focus and nothing else, so until a keyboard arrived they read as
     decoration. The boundary darkens a step under the pointer. */
  .in:hover:not(:disabled):not(:focus)     { border-color:var(--mut); }
  .check:hover:not(:disabled):not(:checked),
  .radio:hover:not(:disabled):not(:checked),
  .switch:hover:not(:disabled):not(:checked) { border-color:var(--mut); }
  /* And once it is ticked it is filled with the accent, which has no room for a darker edge —
     so it dims, the way every other accent-filled surface answers. Without this a checked box
     was the one control in the system that went dead under the pointer: you could not tell it
     was still yours to un-tick. */
  .check:hover:not(:disabled):checked,
  .radio:hover:not(:disabled):checked,
  .switch:hover:not(:disabled):checked { opacity:0.9; }
  .tabbar__item:hover:not(.on)             { color:var(--tx); }
  .dropzone:hover                          { border-color:var(--mut); }
  .btn-icon:hover:not(:disabled)   { background:color-mix(in oklab, var(--tx) 5%, transparent); color:var(--tx); }
  .nav-item:not(.on):hover  { background:color-mix(in oklab, var(--tx) 6%, transparent); }
  .opt:hover                { background:color-mix(in oklab, var(--tx) 6%, transparent); }
  /* The same line, in the other kind of list. A menu's option answered the pointer and a
     select's did not, for the whole life of the custom list: the only thing drawn in there was
     --active, which is the KEYBOARD's place and appears under no pointer at all. So a mouse
     user walked the list with nothing following the hand. */
  .combo__option:not(.combo__option--active):not([aria-disabled="true"]):hover {
    background:color-mix(in oklab, var(--tx) 6%, transparent);
  }
  .seg:not(.on):hover       { color:var(--tx); }
  .switch-btn:hover         { background:color-mix(in oklab, var(--tx) 5%, var(--sf)); }
  .settings-btn:hover       { color:var(--tx); }
  .pick:not(.on):hover      { border-color:var(--mut); }
  .pick-dash:not(.on):hover { border-color:var(--mut); }
  .close-btn:hover          { opacity:0.9; }
}

@media (hover: hover) {
  .scp0:hover { background:color-mix(in oklab, var(--tx) 6%, transparent) !important; }
  .scp1:hover { opacity:0.9 !important; }
  .scp2:hover { color:var(--tx) !important; }
  .scp3:hover { border-color:var(--tx) !important; }
  .scp5:hover { border-color:var(--acc) !important; }
  .scp6:hover { border-color:var(--acc) !important; color:var(--acc) !important; }
  .scp7:hover { border-color:var(--acc) !important; color:var(--acc) !important; }
}
.scp4:focus { border-color:var(--acc) !important; outline:none !important; box-shadow:0 0 0 3px color-mix(in oklab, var(--acc) 25%, transparent) !important; }
.scp5:focus { border-color:var(--acc) !important; outline:none !important; box-shadow:0 0 0 3px color-mix(in oklab, var(--acc) 25%, transparent) !important; }

/* ---- pressed ----

   Hover says "this answers to you"; pressed says "it heard you". A surface with only the first
   is a surface you are never quite sure you hit.

   /v1 spent a hard shadow on the distance a button travelled. /v2 has no hard shadow to spend,
   so there are two answers: anything filled with the accent dims, the way its hover does, and
   anything else takes the press into the ground — a hint of ink under the label, gone the moment
   you let go. Neither moves a layout. */

/* It is filled with the accent: it dims. */
.seg.on:active, .tabbar__item.on:active, .btn-acc:active:not(:disabled),
.btn-acc--wide:active:not(:disabled), .close-btn:active, .tabbar__act:active,
.next-btn.ready:active, .save-btn.ready:active { opacity:0.85; }

/* It is flat: the press goes into the paper. */
.btn:active:not(:disabled) { background:color-mix(in oklab, var(--tx) 9%, var(--sf)); }
.nav-item.on:active, .pick.on:active { background:color-mix(in oklab, var(--acc) 18%, var(--sf)); }
.btn-ghost:active:not(:disabled), .btn-quiet:active:not(:disabled),
.btn-icon:active:not(:disabled), .opt:active, .seg:active:not(.on),
.nav-item:active:not(.on), .pick:active:not(.on), .pick-dash:active,
.tabs__tab:active, .tabbar__item:active:not(.on),
.day__pick:active, .switch-btn:active, .dropzone:active {
  background:color-mix(in oklab, var(--tx) 8%, transparent);
}
.btn-danger:active:not(:disabled) { opacity:0.85; }

/* A word darkens to ink under the hand, and there it stops.
 *
 * These four had a :active rule saying exactly what their :hover already said, so holding one
 * down looked identical to pointing at it — measured, not guessed. The rules are gone rather
 * than rewritten: a word has nowhere further to go without moving, and moving text under a
 * finger is worse than saying nothing. What changed is the inventory, which used to count a
 * pressed state these four have never had. Nothing on screen moved.
 *
 *   .btn-link · .btn-link-sm · .back-link · .settings-btn · .tbl__sort
 */

/* ---- utilities ---- */

/* Said to a screen reader, shown to nobody: skip links, live regions, the label a mark cannot carry.

   inset-block-start/inset-inline-start are not decoration, and their absence was a real bug.
   Absolutely positioned with no offsets, the element sits at its STATIC position — which, for a
   label inside a table that is 648px wide inside a 390px pane, is 426px into the document. It is
   one pixel wide and invisible, but the page still has to be wide enough to contain it, so a
   phone rendered the whole book 37px wider than the screen and slid sideways under the thumb.
   Nothing showed: the culprit is the one element on the page nobody can see.

   Pinning it to the origin of whatever contains it costs nothing — a screen reader reads DOM
   order, never coordinates — and makes it arithmetically impossible for the label to widen a
   page ever again. */
.visually-hidden {
  position:absolute !important; inset-block-start:0; inset-inline-start:0;
  width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}

/* The first thing a keyboard meets: out of the way until it is wanted, then unmissable. */
.skip-link {
  position:absolute; left:var(--s2); top:-100px; z-index:calc(var(--z-toast) + 1);
  background:var(--acc); color:var(--acc-ink); border:none; border-radius:var(--radius-md);
  box-shadow:var(--sh-menu); padding:8px 14px;
  font-size:var(--fs-text); font-weight:500;
  text-decoration:none; transition:top var(--t-fast);
}
.skip-link:focus { top:var(--s2); color:var(--acc-ink); }

/* ---- text ---- */

.h-sec   { font-size:var(--fs-text); font-weight:600; letter-spacing:-0.01em; }
.h-sub   { font-size:var(--fs-note); color:var(--mut); margin-top:2px; }
.h2      { font-size:var(--fs-mark); font-weight:600; margin:0 0 12px; letter-spacing:-0.015em; }
.h2-tight{ font-size:var(--fs-mark); font-weight:600; margin:0 0 4px; letter-spacing:-0.015em; }
.mut     { color:var(--mut); }
.note    { font-size:var(--fs-note); color:var(--mut); }
.fine    { font-size:var(--fs-fine); color:var(--mut); }
.micro   { font-size:var(--fs-micro); color:var(--mut); }
.caps    { font-size:var(--fs-caps); color:var(--mut); font-weight:500; letter-spacing:0.06em; text-transform:uppercase; }
.num     { font-variant-numeric:tabular-nums; }
.b       { font-weight:600; }

/* The page's own title and its lead — every screen introduces itself the same way. */
.page-title { font-size:var(--fs-title); margin:0; letter-spacing:-0.025em; line-height:1.2;
              font-family:var(--font-display); font-weight:700; }
.page-lead  { font-size:var(--fs-text); color:var(--mut); margin:8px 0 0; max-width:520px; }

/* A list or card item's own line: its title, and the note under it. */
.item-title { display:block; font-size:var(--fs-text); font-weight:500; }
.item-note  { display:block; font-size:var(--fs-fine); color:var(--mut); margin-top:3px; }
.p-mut      { font-size:var(--fs-ui); color:var(--mut); margin:0; }

/* ---- forms ---- */

.blk      { display:block; }
.blk-wide { display:block; grid-column:1 / -1; }
.lbl      { display:block; font-size:var(--fs-text); font-weight:500; line-height:1.25;
            margin-bottom:8px; }
/* 19.5 of line, 12 of padding and 2 of border make 33.5, so --ctl-h decides the height rather
   than the content. */
.in       { font:inherit; font-size:var(--fs-text); width:100%; min-height:var(--ctl-h); padding:6px 12px;
            border:1px solid var(--bd); border-radius:var(--radius-md); background:var(--sf);
            color:var(--tx); box-shadow:var(--sh-btn);
            transition:border-color var(--t-fast), box-shadow var(--t-fast); }
.in::placeholder { color:var(--mut); }
.in-num   { text-align:end; font-variant-numeric:tabular-nums; }
/* The door's big field: a step taller, the size a sign-in asks for. */
.in--door { min-height:40px; padding:9px 12px; }
/* A step taller than a field, and never shorter than one — at the phone width the
   thumb's 44 is the larger. */
.in--door { min-height:max(40px, var(--ctl-h)); }
.in--door:focus { outline:none; border-color:var(--acc); }
.hint     { display:block; font-size:var(--fs-fine); color:var(--mut); margin-top:6px; }
/* A field that was answered wrongly, and the line that says so. */
.in--error       { border-color:var(--loss); }
.in--error:focus { border-color:var(--loss);
                   box-shadow:0 0 0 3px color-mix(in oklab, var(--loss) 20%, transparent); }
.fault           { display:block; font-size:var(--fs-fine); color:var(--loss); margin-top:6px; }

input[type="range"] { -webkit-appearance:none; appearance:none; height:20px; background:transparent; cursor:pointer; width:100%; }
input[type="range"]::-webkit-slider-runnable-track { height:6px; border-radius:9999px; background:var(--bd2); }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance:none; width:16px; height:16px; margin-top:-5px; border-radius:9999px; background:var(--sf); border:1px solid var(--acc); box-shadow:var(--sh-btn); }
input[type="range"]::-moz-range-track { height:6px; border-radius:9999px; background:var(--bd2); }
input[type="range"]::-moz-range-progress { height:6px; border-radius:9999px; background:var(--acc); }
input[type="range"]::-moz-range-thumb { width:14px; height:14px; border-radius:9999px; background:var(--sf); border:1px solid var(--acc); box-shadow:var(--sh-btn); }

/* ---- buttons ----

   One act, seven dresses — shadcn's button variants under the names /v1 published:

     .btn         outline      a surface, a hairline and a breath of shadow
     .btn-acc     default      the accent, filled: the one primary act on a screen
     .btn-ghost   ghost        nothing until the pointer arrives
     .btn-quiet   ghost, muted an act that waits its turn
     .btn-danger  destructive  filled in loss red: an act you cannot undo
     .btn-link    link         the accent, underlined under the pointer

   Everything they share is said once here; each class below says only what makes it itself. */

.btn, .btn-acc, .btn-ghost, .btn-quiet, .btn-danger, .btn-link, .btn-link-sm, .btn-icon {
  font:inherit; cursor:pointer;
}
/* The five that are shaped like a button: 36px tall, a medium weight, the md corner. An anchor
   wearing one of these is a button to the eye, so it loses the browser's underline; the two
   shaped like a word keep theirs, because there they are the point. */
.btn, .btn-acc, .btn-ghost, .btn-quiet, .btn-danger {
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  min-height:36px; padding:7px 16px; border-radius:var(--radius-md);
  font-size:var(--fs-text); font-weight:500; line-height:1.4; white-space:nowrap;
  text-decoration:none;
  transition:background var(--t-fast), opacity var(--t-fast), box-shadow var(--t-fast);
}
:is(.btn, .btn-acc, .btn-ghost, .btn-quiet, .btn-danger) { min-height:var(--ctl-h); }
/* The two that are shaped like a word. */
.btn-link, .btn-link-sm {
  font-weight:500; border:none; background:transparent; color:var(--acc);
  text-underline-offset:4px;
}

.btn        { border:1px solid var(--bd2);  background:var(--sf);     color:var(--tx);
              box-shadow:var(--sh-btn); }
.btn-ghost  { border:1px solid transparent; background:transparent;   color:var(--tx); }
.btn-quiet  { border:1px solid transparent; background:transparent;   color:var(--mut); padding:7px 12px; }
/* The primary act. */
.btn-acc    { border:1px solid transparent; background:var(--acc);     color:var(--acc-ink);
              box-shadow:var(--sh-btn); }
/* The dangerous act, filled — shadcn's destructive. It is the one red surface on a screen, so it
   is never mistaken for the primary act it sits beside. */
.btn-danger { border:1px solid transparent; background:var(--loss);    color:var(--acc-ink);
              box-shadow:var(--sh-btn); }

.btn-link   { font-size:var(--fs-text);  padding:0 2px; }
.btn-link-sm{ font-size:var(--fs-micro); padding:0; }

/* A square icon affordance: one glyph. 32px is shadcn's icon-sm; the thumb gets --hit anyway,
   because a hit area costs nothing. */
.btn-icon   { position:relative; display:inline-flex; align-items:center; justify-content:center;
              width:32px; height:32px; padding:0; flex:none; border-radius:var(--radius-md);
              border:1px solid transparent; background:transparent; color:var(--mut);
              transition:background var(--t-fast), color var(--t-fast); }
.btn-icon::after { content:''; position:absolute; top:50%; left:50%; translate:-50% -50%;
                   width:max(100%, var(--hit)); height:max(100%, var(--hit)); }

/* The wide act: a door-sized button, block-level, 40px — shadcn's lg. */
.btn--wide      { display:flex; align-items:center; justify-content:center; gap:10px;
                  width:100%; min-height:40px; padding:9px 16px; font-size:var(--fs-text); }
.btn--wide:disabled { color:var(--mut); cursor:default; }
.btn-acc--wide  { display:flex; align-items:center; justify-content:center; gap:10px;
                  width:100%; min-height:40px; padding:9px 16px; font-size:var(--fs-text); }
/* The wide acts stay a step above a field, and never below one. */
:is(.btn--wide, .btn-acc--wide) { min-height:max(40px, var(--ctl-h)); }

/* Every act knows how to be underway.
 *
 * One team building on this system invented twelve of these by hand, in six different visual
 * treatments, with seven different words for the same state — and five of the actions had no
 * re-entrancy guard at all, relying on a `disabled` attribute somebody had remembered to bind.
 * A button with no opinion about waiting is a button every screen has to teach again.
 *
 * `aria-busy` says it, the spinner shows it, and the button is genuinely unpressable while it
 * is true: not a style, a state. */
.btn[aria-busy='true'], .btn-acc[aria-busy='true'], .btn-ghost[aria-busy='true'],
.btn-quiet[aria-busy='true'], .btn-danger[aria-busy='true'], .btn-icon[aria-busy='true'],
.btn-link[aria-busy='true'], .btn-link-sm[aria-busy='true'] {
  cursor:progress; pointer-events:none;
}
/* The mark, in the button's own colour, before whatever it says — drawn by CSS alone, so the
   element layer gets it without a line of JavaScript. It replaces nothing, so the label does
   not jump and the width does not move. */
/* The icon button is the exception, and the states gallery is what found it. Everywhere else
   the mark goes IN FRONT of the label so the width does not jump; an icon button has no label
   and a fixed 30×26 box, so prepending crammed a spinner and a glyph into a space meant for
   one of them. Here the mark replaces the glyph — there is no width to protect and nothing to
   read underneath. */
.btn-icon[aria-busy='true'] { color:transparent; position:relative; }
.btn-icon[aria-busy='true']::before {
  position:absolute; inset:0; margin:auto;
}

.btn[aria-busy='true']::before, .btn-acc[aria-busy='true']::before,
.btn-ghost[aria-busy='true']::before, .btn-quiet[aria-busy='true']::before,
.btn-danger[aria-busy='true']::before, .btn-icon[aria-busy='true']::before,
.btn-link[aria-busy='true']::before, .btn-link-sm[aria-busy='true']::before {
  content:''; display:inline-block; width:10px; height:10px;
  margin-inline-end:7px; vertical-align:-1px;
  border-radius:50%; border:1.5px solid currentColor; border-top-color:transparent;
  animation:ornoSpin 700ms linear infinite;
}
/* An icon button has no label to sit beside; the mark is the whole of it. */
.btn-icon[aria-busy='true']::before { margin-inline-end:0; }

/* Every act knows how to stand down: half its strength, shadowless, unmoved.
   shadcn's answer, and the one this dress takes: a disabled act keeps its own colours at half
   opacity, so it is still recognisably the act it would be — just not now. WCAG 1.4.3 exempts
   an inactive control from the contrast bar, and a disabled act that reads as available is
   worse than one that reads as faint.

   Ten of these were added after counting: a list of options, a card you choose, a place in the
   index, a tab, a day in a calendar — all things an app has to be able to switch off, and none
   of which had any way to look off. */
.btn:disabled, .btn-acc:disabled, .btn-ghost:disabled, .btn-danger:disabled,
.btn-quiet:disabled, .btn-link:disabled, .btn-link-sm:disabled, .btn-icon:disabled,
.close-btn:disabled, .seg:disabled, .switch-btn:disabled,
.opt:disabled, .pick:disabled, .pick-dash:disabled, .nav-item:disabled,
.tabs__tab:disabled, .tabbar__item:disabled, .tabbar__act:disabled,
.settings-btn:disabled, .fold-list__head:disabled, .day__pick:disabled {
  opacity:0.5; box-shadow:none; cursor:not-allowed; transform:none;
}

/* A field had no disabled look at all in /v1's first cut. Here it is the same half strength,
   on a muted ground so a form full of them still reads as a form. The colour only, not the
   shorthand, so a disabled select keeps its mark. */
.in:disabled {
  color:var(--mut); background-color:color-mix(in oklab, var(--tx) 4%, var(--sf));
  box-shadow:none; opacity:0.6; cursor:not-allowed;
}

/* ---- layout & marks ---- */

.grow        { flex:1; }
.row         { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; }
.panel       { position:relative; border:none; border-radius:var(--radius-xl);
               background:var(--sf); overflow:hidden; box-shadow:var(--sh-card); }
.panel-plain { margin-top:14px; border:none; border-radius:var(--radius-lg);
               background:var(--bg); overflow:hidden; box-shadow:var(--sh-card); }
/* shadcn's outline badge: a hairline, the md corner, a medium weight. */
.badge       { display:inline-flex; align-items:center; gap:4px; width:fit-content;
               font-size:var(--fs-micro); font-weight:500; line-height:1.4; white-space:nowrap;
               color:var(--tx); border:1px solid var(--bd2); border-radius:var(--radius-md);
               padding:1px 8px; }
/* A badge in a button sits on the button's line, not above it: its own 1.4 line box plus border
   and padding is 20.8px, which is taller than the 20px a button leaves inside its padding, so the
   button grew to 36.8px. One line of 1 and a 2px pad is 18px, centred by the button's flex. */
:is(.btn, .btn-acc, .btn-ghost, .btn-quiet, .btn-danger) .badge { line-height:1; padding-block:2px; }
/* The quiet badge: shadcn's secondary — a muted fill, no line. */
.badge--quiet{ color:var(--mut); background:color-mix(in oklab, var(--tx) 5%, var(--sf));
               border-color:transparent; font-weight:400; }
/* shadcn's default badge: the accent, filled. The loudest label in the system. */
.chip        { display:inline-flex; align-items:center; width:fit-content;
               background:var(--acc); color:var(--acc-ink); font-size:var(--fs-micro);
               font-weight:500; line-height:1.4; border-radius:var(--radius-md); padding:2px 8px; }

/* A fold: the panel that unfolds under a row for a small task — a confirm, a mini-form.
   It opens inside what holds the row, so it keeps a box's hairline rather than a card's ring —
   one edge, and the one --danger tints on the destructive ones — and only the smallest fall
   lifts it, because it appears on interaction. */
.fold          { border:1px solid var(--line); border-radius:var(--radius-lg); background:var(--sf);
                 padding:14px 16px; margin:10px 0 4px; box-shadow:var(--sh-btn); }
.fold--danger  { border-color:color-mix(in oklab, var(--loss) 45%, var(--bd2)); }
.fold__actions { display:flex; align-items:center; gap:8px; margin-top:12px; flex-wrap:wrap; }

/* A floating menu panel: the one shape a dropdown takes — shadcn's popover. */
.menu-panel { position:absolute; z-index:var(--z-menu); background:var(--sf); border:1px solid var(--line);
              border-radius:var(--radius-md); padding:4px; box-shadow:var(--sh-menu); }
.opt        { font:inherit; display:flex; align-items:baseline; gap:8px; width:100%; text-align:start;
              font-size:var(--fs-text); padding:6px 8px; border:none; border-radius:var(--radius-sm);
              background:transparent; color:var(--tx); font-weight:400; cursor:pointer; }
.opt.on     { background:color-mix(in oklab, var(--acc) 10%, transparent); font-weight:500; }

/* A frame: in /v1 the corner brackets that marked a major panel. A registration mark is the
   brutalist dress's word, not this one's, so here a frame is only the positioning context its
   children were written against. */
.frame          { position:relative; }

/* A card: every standing surface wears the same dress — Stripe's card. No border: --sh-card is
   a hairline ring and a small shadow, which edges it without a line to align against. A panel, a
   tile, a table's pane and every stat tile built on .card wear the same edge. */
.card        { border:none; border-radius:var(--radius-xl); background:var(--sf);
               color:var(--tx); padding:12px; box-shadow:var(--sh-card); }
.card__title { margin:0; font-size:var(--fs-text); font-weight:600; line-height:1.3;
               letter-spacing:-0.01em; color:var(--tx); }
/* No card in a card: a table's pane, a fold list or a plain panel that stands on the page wears
   the card's edge, but inside a card, a panel, a sheet, a dialog or a fold it is a box again —
   the hairline, and no fall. One :is() each side, so the rule weighs two classes wherever it is. */
:is(.card, .panel, .drawer, .dialog, .fold) :is(.tbl-wrap, .fold-list, .panel-plain) {
  border:1px solid var(--bd2); box-shadow:none; }

/* A sentence and, under it, the way out. A grid stacks them and centres both, so the act never
   lands on the same line as the sentence. */
.empty-state { border:1px dashed var(--bd2); border-radius:var(--radius-lg); padding:40px 24px;
               text-align:center; display:grid; gap:var(--s4); justify-items:center;
               font-size:var(--fs-text); color:var(--mut); }

/* ---- lists, meters, segments ---- */

/* A row in a stacked list: ruled off from the one above, and the first from nothing. */
.sep             { border-top:1px solid var(--bd2); }
.sep:first-child { border-top:none; }
/* A thin progress meter: Linear's thin bar — a 4px pill track in the accent's own tint. */
.meter      { height:4px; border-radius:9999px; overflow:hidden;
              background:color-mix(in oklab, var(--acc) 18%, transparent); }
.meter-fill { height:4px; border-radius:9999px; background:var(--acc);
              transition:width var(--t-mid); }
/* A meter in a series colour, or in ink, for when a row of them has to tell one measure from
   the next. The track is always the fill's own tint, as the accent's is. */
.meter--ser1 { background:color-mix(in oklab, var(--ser1) 18%, transparent); }
.meter--ser2 { background:color-mix(in oklab, var(--ser2) 18%, transparent); }
.meter--ser3 { background:color-mix(in oklab, var(--ser3) 18%, transparent); }
.meter--ser4 { background:color-mix(in oklab, var(--ser4) 18%, transparent); }
.meter--tx   { background:color-mix(in oklab, var(--tx) 18%, transparent); }
.meter--ser1 > .meter-fill { background:var(--ser1); }
.meter--ser2 > .meter-fill { background:var(--ser2); }
.meter--ser3 > .meter-fill { background:var(--ser3); }
.meter--ser4 > .meter-fill { background:var(--ser4); }
.meter--tx   > .meter-fill { background:var(--tx); }
/* One bar in proportional parts: the split of a whole, read at a glance — the macros of a
   portion, a budget by category. Each part grows by its share (the element sets flex-grow), and
   the 2px gaps are the ground showing through, so neighbouring colours never touch. With
   nothing to show it is the bare track, the same tint of ink an empty meter would wear. */
.parts             { display:flex; gap:2px; height:6px; border-radius:9999px; overflow:hidden; }
.parts:empty       { background:color-mix(in oklab, var(--tx) 8%, transparent); }
.parts__part       { flex:0 1 0px; min-width:0; background:var(--acc); }
.parts__part--ser1 { background:var(--ser1); }
.parts__part--ser2 { background:var(--ser2); }
.parts__part--ser3 { background:var(--ser3); }
.parts__part--ser4 { background:var(--ser4); }
.parts__part--mut  { background:var(--mut); }
/* A ring: one share of a goal, drawn round — Apple Health's rings in this house's series. An SVG
   circle on its own faint track, filled clockwise from the top with rounded ends; pathLength="100"
   on both circles lets the fill's dash be the percentage itself. 64px by default, 40 small, 112
   large. It paints in its own `color`, so the track is always the fill's own tint. A value of 0,
   or one nobody knows, draws no fill at all: a round cap would otherwise leave a dot. */
.ring        { display:inline-block; flex:none; width:64px; height:64px; color:var(--acc); }
.ring--sm    { width:40px; height:40px; }
.ring--lg    { width:112px; height:112px; }
.ring > svg  { display:block; width:100%; height:100%; }
.ring__track { fill:none; stroke:currentColor; stroke-opacity:0.18; stroke-width:4; }
.ring__fill  { fill:none; stroke:currentColor; stroke-width:4; stroke-linecap:round;
               transition:stroke-dasharray var(--t-mid); }
/* Rings: up to four, one inside the next, with the figure that matters beside them and a legend
   line per ring — the day at a glance. The art is 112px (160 large); in its 100-unit box each
   stroke is 8 wide with 2 between. Every legend line stays one whole line: the text asks for
   its widest legend line (flex-basis auto, each line nowrap) and, beside the art, never grows past
   it, so each detail sits after its label instead of at the card's far edge; where that does not fit beside
   the art the text drops under it, start-aligned — so a label is cut only when even the card is
   too narrow, and then before its detail is. Only the legend decides:
   the figure and its line ask for no width of their own (contain:inline-size) and wrap in
   whatever width the legend sets. An entry that draws no ring keeps its legend line; with no
   ring at all there is no art, and the text stands on its own. */
.rings          { display:flex; flex-wrap:wrap; align-items:center; gap:var(--s4); min-width:0; }
.rings__art     { flex:none; display:block; width:112px; height:112px; }
.rings--lg .rings__art { width:160px; height:160px; }
.rings__art .ring__track, .rings__art .ring__fill { stroke-width:8; }
.rings__ring    { color:var(--acc); }
.rings__text    { display:grid; gap:2px; flex:1 1 auto; min-width:0; }
.rings__art + .rings__text { flex:0 1 auto; }
.rings__figure  { font-size:var(--fs-title); font-weight:600; letter-spacing:-0.02em; line-height:1.2;
                  font-variant-numeric:tabular-nums; }
.rings__of      { font-size:var(--fs-fine); color:var(--mut); }
.rings__figure, .rings__of { contain:inline-size; }
.rings__legend  { list-style:none; margin:var(--s2) 0 0; padding:0; display:grid; gap:2px; min-width:0; }
.rings__key     { display:flex; align-items:center; gap:var(--s2); min-width:0; font-size:var(--fs-ui);
                  white-space:nowrap; }
.rings__dot     { flex:none; width:8px; height:8px; border-radius:9999px; background:currentColor;
                  color:var(--acc); }
.rings__label   { min-width:0; overflow:hidden; text-overflow:ellipsis; }
.rings__detail  { flex:none; margin-inline-start:auto; color:var(--mut); font-variant-numeric:tabular-nums; }
/* A ring's series, one rule per tone for a ring, a ring in a set and its legend dot — after the
   rules above, so a tone overrides the accent each of them starts in. */
.ring--ser1, .rings__ring--ser1, .rings__dot--ser1 { color:var(--ser1); }
.ring--ser2, .rings__ring--ser2, .rings__dot--ser2 { color:var(--ser2); }
.ring--ser3, .rings__ring--ser3, .rings__dot--ser3 { color:var(--ser3); }
.ring--ser4, .rings__ring--ser4, .rings__dot--ser4 { color:var(--ser4); }
/* A floating chart annotation; the template binds its position. */
.chart-lbl  { position:absolute; font-size:var(--fs-caps); color:var(--mut); pointer-events:none; }
/* A legend swatch; the template binds its colour. */
.swatch     { width:10px; height:10px; border-radius:2px; }
/* A segmented choice: shadcn's tabs list — the chosen segment is a raised card in a muted well.
   The well is the group's own ground (see orno-seg-group); a lone segment still reads. */
.seg    { font:inherit; font-size:var(--fs-ui); padding:5px 12px; border:1px solid transparent;
          border-radius:var(--radius-md); background:transparent; color:var(--mut);
          font-weight:500; cursor:pointer; white-space:nowrap;
          transition:background var(--t-fast), color var(--t-fast), box-shadow var(--t-fast); }
.seg.on { background:var(--sf); color:var(--tx); border-color:var(--bd2); box-shadow:var(--sh-btn); }
/* A segment is --ctl-h less the well's 3px either side, so the group stands as tall
   as a field. */
.seg { min-height:calc(var(--ctl-h) - 6px); padding:4px 12px; }

/* ---- wizard ---- */

/* The step rail: a bar per step, lit up to where you are. */
.rail-bar    { display:block; height:4px; border-radius:9999px; background:var(--bd2);
               transition:background 200ms; }
.rail-bar.on { background:var(--acc); }
.rail-step__line { display:flex; align-items:baseline; gap:7px; margin-top:9px; }
.rail-no     { font-size:var(--fs-micro); font-variant-numeric:tabular-nums; color:var(--mut); font-weight:500; }
.rail-no.on  { color:var(--acc); }
.rail-lbl    { font-size:var(--fs-ui); color:var(--mut); font-weight:500; }
.rail-lbl.on { color:var(--tx); }
.rail-step          { font:inherit; flex:1; text-align:start; border:none; background:transparent;
                      padding:0 14px 0 0; cursor:pointer; color:var(--tx); }
.rail-step:disabled { cursor:default; }
/* A choice card: one of a few mutually exclusive answers, lit when it is the answer — the
   accent's edge and a ring of its tint, the way shadcn marks a chosen radio card. */
.pick         { font:inherit; text-align:start; width:100%; border:1px solid var(--bd2);
                border-radius:var(--radius-lg); background:var(--sf); padding:14px 16px;
                cursor:pointer; color:var(--tx); box-shadow:var(--sh-btn);
                transition:border-color var(--t-fast), background var(--t-fast); }
.pick.on      { border-color:var(--acc); background:color-mix(in oklab, var(--acc) 6%, var(--sf));
                box-shadow:0 0 0 1px var(--acc); }
.pick-dash    { font:inherit; text-align:start; width:100%; border:1px dashed var(--bd);
                border-radius:var(--radius-lg); background:transparent; padding:14px 16px;
                cursor:pointer; color:var(--tx); }
.pick-dash.on { border-style:solid; border-color:var(--acc);
                background:color-mix(in oklab, var(--acc) 6%, var(--sf)); }
/* DEPRECATED — kept so a /v1 template still renders. Use .btn-acc plus the real `disabled`
   attribute: these looked disabled without being disabled, so a screen reader was told nothing. */
.next-btn       { font:inherit; font-size:var(--fs-text); padding:8px 20px; border:1px solid var(--bd2);
                  border-radius:var(--radius-md); background:transparent; color:var(--mut);
                  cursor:not-allowed; font-weight:500; opacity:0.55; }
.next-btn.ready { border-color:transparent; background:var(--acc); color:var(--acc-ink); cursor:pointer;
                  opacity:1; box-shadow:var(--sh-btn); }
.save-btn       { font:inherit; font-size:var(--fs-text); padding:9px 24px; border:1px solid var(--bd2);
                  border-radius:var(--radius-md); background:transparent; color:var(--mut);
                  cursor:not-allowed; font-weight:500; }
.save-btn.ready { border-color:transparent; background:var(--acc); color:var(--acc-ink); cursor:pointer;
                  box-shadow:var(--sh-btn); }

/* ---- the spinner ---- */

.spin     { display:inline-block; width:12px; height:12px; border-radius:50%;
            border:1.5px solid var(--bd2); border-top-color:var(--acc);
            animation:ornoSpin 700ms linear infinite; }
.spin--lg { width:16px; height:16px; border-width:2px; }

/* ==========================================================================
   The door — the sign-in storefront Servo wears; any app with a front step
   can wear it too. Pitch left, card right.
   ========================================================================== */

.door {
  min-height:100dvh;
  display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  align-items:center; gap:48px;
  max-width:1060px; margin:0 auto; padding:48px 32px;
}
.door__pitch { border-inline-end:1px solid var(--bd2); padding-inline-end:48px; }
.door__title { margin:36px 0 18px; font-family:var(--font-display); font-weight:700;
               font-size:var(--fs-hero); line-height:1.1;
               letter-spacing:-0.035em; text-transform:none; }
.door__lead  { margin:0 0 30px; color:var(--mut); font-size:var(--fs-lead); max-width:44ch; }
.door__side  { display:grid; gap:18px; justify-items:center; }

.brand       { display:flex; align-items:center; gap:12px; font-weight:600; font-size:var(--fs-brand);
               letter-spacing:-0.01em; }
.brand__mark { display:grid; place-items:center; width:36px; height:36px; background:var(--acc);
               border-radius:var(--radius-md); box-shadow:var(--sh-btn); color:var(--acc-ink); }

.sell        { display:grid; gap:20px; margin:0; padding:0; list-style:none; }
.sell__item  { display:grid; grid-template-columns:26px minmax(0,1fr); gap:12px; }
.sell__icon  { color:var(--acc); padding-top:2px; }
.sell__title { font-weight:600; font-size:var(--fs-text); }
.sell__note  { display:block; color:var(--mut); font-size:var(--fs-text); margin-top:2px; }

/* The door's card: a card's own edge — the ring of --sh-card, no border — under the heaviest
   shadow in the system, because it is the only thing on the page that matters. /v1 drew four
   corner brackets here; .door-card__corners is still accepted and draws nothing. */
.door-card { position:relative; width:min(420px,100%); background:var(--sf);
             border:none; border-radius:var(--radius-xl);
             box-shadow:var(--sh-card), var(--sh-door); padding:32px 32px 28px; }
.door-card__corners { display:none; }
.door-card__title { margin:0; text-align:center; font-family:var(--font-display); font-weight:600;
                    font-size:var(--fs-card); letter-spacing:-0.025em; line-height:1.2; }
.door-card__sub   { margin:8px 0 0; text-align:center; color:var(--mut); font-size:var(--fs-text); }
.door-card__rule  { border:none; border-top:1px solid var(--bd2); margin:20px 0 24px; }
.door-card__foot  { margin:20px 0 0; text-align:center; color:var(--mut); font-size:var(--fs-fine); }
.door-card p      { color:var(--mut); font-size:var(--fs-text); }
.door-card .btn-acc--wide { margin-top:24px; }
.door-card .btn--wide     { margin-top:14px; }

/* The version line under the card. */
.stamp { color:var(--mut); font-size:var(--fs-fine); font-family:var(--font-mono); }

/* The narrow pane: the card is the job; the pitch folds to a header. */
@media (max-width: 900px) {
  .door { grid-template-columns:minmax(0,1fr); gap:28px; align-content:start;
          padding:28px 18px calc(28px + env(safe-area-inset-bottom)); }
  .door__pitch { border-inline-end:none; padding-inline-end:0; }
  .door__title { margin:22px 0 10px; }
  .door__lead  { margin-bottom:0; }
  .sell        { display:none; }
  .door-card   { padding:24px 20px 22px; }
}

/* ==========================================================================
   The shell — the app frame's own vocabulary (Vesto's sidebar speaks it).
   ========================================================================== */

/* One rule for the whole index — shadcn's sidebar menu: the accent's tint means "you are here",
   and everything not chosen is plain ink. */
.nav-item        { font:inherit; display:flex; align-items:baseline; gap:8px; text-align:start;
                   width:calc(100% - 16px); margin-inline:8px;
                   font-size:var(--fs-text); padding:6px 8px; border:1px solid transparent;
                   border-radius:var(--radius-md); background:transparent; color:var(--tx);
                   font-weight:400; cursor:pointer;
                   transition:background var(--t-fast), color var(--t-fast); }
.nav-item.on     { background:color-mix(in oklab, var(--acc) 12%, var(--sf)); color:var(--tx);
                   font-weight:500; }
/* A chapter under a chapter: further in, and quieter. */
.nav-item--child { padding-inline-start:22px; font-size:var(--fs-ui); }
.nav-fig         { font-size:var(--fs-micro); font-variant-numeric:tabular-nums; color:var(--mut); font-weight:400; }
.nav-item.on .nav-fig { color:var(--acc); }
.close-btn       { font:inherit; font-size:var(--fs-text); padding:7px 12px; border:1px solid transparent;
                   border-radius:var(--radius-md); background:var(--acc); color:var(--acc-ink);
                   cursor:pointer; font-weight:500; text-align:start; box-shadow:var(--sh-btn); }
.close-btn.on    { background:var(--tx); border-color:var(--tx); color:var(--bg); }
.settings-btn    { font:inherit; font-size:var(--fs-ui); border:none; background:transparent; color:var(--mut);
                   font-weight:500; cursor:pointer; padding:0 2px; text-align:start; }
.settings-btn.on { color:var(--acc); }
.switch-btn      { font:inherit; display:inline-flex; align-items:center; gap:8px; font-size:var(--fs-text);
                   min-height:36px; padding:6px 12px; border:1px solid var(--bd2);
                   border-radius:var(--radius-md); background:var(--sf); color:var(--tx);
                   cursor:pointer; box-shadow:var(--sh-btn); }
.switch-btn.open { border-color:var(--acc); box-shadow:0 0 0 3px color-mix(in oklab, var(--acc) 20%, transparent); }

/* ==========================================================================
   Later arrivals — additive, so /v1/ keeps its promise.
   ========================================================================== */

/* A stat: one figure with its label above and its note below. */
.stat        { display:grid; gap:3px; }
.stat__value { font-size:var(--fs-sub); font-weight:600; letter-spacing:-0.02em; font-variant-numeric:tabular-nums; }
/* A stat measured against something: the label at the start, the aside at the end — "/ 2,200"
   against "Calories". The aside is the label's own size, so the head is exactly as tall as a
   label alone and a stat with an aside stands level with one without. Tabular figures, so a row
   of asides lines up digit under digit. */
.stat__head  { display:flex; align-items:baseline; justify-content:space-between;
               gap:var(--s2); flex-wrap:nowrap; min-width:0; }
.stat__head > .caps { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.stat__aside { flex:none; font-size:var(--fs-caps); color:var(--mut); font-variant-numeric:tabular-nums;
               white-space:nowrap; }
/* Whatever sits under the figure takes one line of fine print, whatever it is. A meter (4px) or
   a parts bar (6px) is centred in the 18px a one-line .fine note takes at the body's 1.5, so a
   tile with a meter and a tile with a note stand the same height in a row. */
.stat > .meter { margin-block:calc((var(--fs-fine) * 1.5 - 4px) / 2); }
.stat > .parts { margin-block:calc((var(--fs-fine) * 1.5 - 6px) / 2); }
/* A stat with a ring: a small ring at the start, and the label, figure and aside beside it exactly
   where a stat without one keeps them. The 40px ring sits inside the 45px an 11px label, the 3px
   gap and a 17px figure take, so a row of ring stats stands level — as tall as a row of plain
   ones while the ring is beside the text. Only the ring centres itself; the text keeps its top,
   so beside a taller card it stays on the lines a plain stat's does. Anything else the stat
   carries goes under the figure, in the same column — but a meter, which the ring is there
   instead of.

   The stat is a container, so it can ask its own width — and a container has no content width
   to offer a parent that shrinks to fit it (inline-flex, a float, a flex item at its content
   size). There it takes 10rem instead, the 160px the ring beside "Calories" and its "/ 2,100"
   needs (below), so it stands beside its text rather than collapsing. In a grid or block cell it
   fills the cell as any stat does. */
.stat--ring { grid-template-columns:auto minmax(0, 1fr); column-gap:var(--s3);
              container-type:inline-size; contain-intrinsic-inline-size:10rem; }
.stat--ring > * { grid-column:2; }
.stat--ring > .stat__ring { grid-column:1; grid-row:1 / span 2; align-self:center; }
.stat--ring > .meter { display:none; }
/* Too narrow to hold the ring beside the text, the ring stands above it and the text takes the
   whole width. Measured in Geist: "CALORIES" at 11px caps is 61.3px, the 8px gap, "/ 2,100" 35px
   — 104.3px of head, and 52px of ring and gap beside it make 156.3px; 160px leaves a font's
   rounding room. Under that the label would be cut first. The stat's own width decides, not its
   words, so stats of one width — a tile row — change together and the row stays level; stacked,
   a ringed stat is taller than a plain one. (A container restyles only what it holds, so the
   children move rather than the grid's columns: everything spans both.) */
@container (width < 160px) {
  .stat--ring > * { grid-column:1 / -1; }
  .stat--ring > .stat__ring { grid-row:auto; justify-self:start; align-self:start; margin-block-end:var(--s1); }
}

/* A select in the form's dress, as tall as every other control. The browser's own arrow came
   with the browser's own box, and Safari's box is 22px whatever min-height says, so a select
   stood half the height of the field beside it on a phone. The box is the field's now and the
   mark is drawn: two halves of a square in --mut make the ▾ the drawn select wears, on the
   inline end, with the padding to keep a long option off it. The list that opens is still the
   browser's own. */
select.in { appearance:none; -webkit-appearance:none; cursor:pointer; padding-inline-end:32px;
            background-image:linear-gradient(45deg, transparent 50%, var(--mut) 50%),
                             linear-gradient(135deg, var(--mut) 50%, transparent 50%);
            background-size:5px 5px; background-repeat:no-repeat;
            background-position:calc(100% - 19px) 50%, calc(100% - 14px) 50%; }
select.in:dir(rtl) { background-position:14px 50%, 19px 50%; }
/* A list box (multiple, or more than one row) shows its options, so it needs no mark; it keeps
   the browser's own box, which it always had, and draws its rows inside the field's border. */
select.in:is([multiple], [size]:not([size="1"], [size="0"])) {
  appearance:auto; -webkit-appearance:auto; background-image:none; padding-inline-end:12px; }
/* Forced colours repaint the field in the system's colours but leave a drawn mark in --mut,
   which can vanish. The browser's own arrow is drawn in those colours, so it comes back. */
@media (forced-colors: active) {
  select.in { appearance:auto; -webkit-appearance:auto; background-image:none; }
}

/* A dialog: the one surface that darkens the page behind it — shadcn's dialog. */
.dialog-backdrop { position:fixed; inset:0; z-index:var(--z-dialog); display:grid; place-items:center;
                   padding:16px; background:color-mix(in oklab, var(--edge) 50%, transparent); }
.dialog          { background:var(--sf); border:1px solid var(--line); border-radius:var(--radius-lg);
                   padding:24px; width:100%; max-width:480px; box-shadow:var(--sh-dialog); }
.dialog__actions { display:flex; align-items:center; justify-content:flex-end; gap:8px;
                   margin-top:20px; flex-wrap:wrap; }

/* A toast: the short-lived note at the foot of the page, with room for one undo. */
.toast { position:fixed; left:50%; transform:translateX(-50%);
         bottom:calc(24px + env(safe-area-inset-bottom)); z-index:var(--z-toast);
         display:flex; align-items:baseline; gap:14px; max-width:min(440px, calc(100vw - 32px));
         background:var(--sf); border:1px solid var(--line); border-radius:var(--radius-lg);
         box-shadow:var(--sh-dialog); padding:14px 16px; font-size:var(--fs-text); }

/* ==========================================================================
   The missing rooms — banner, tabbar, ledger, skeleton. Additive; /v1 keeps
   its promise.
   ========================================================================== */

/* An inline banner: doubt or failure, spoken on the page itself. */
.banner { display:flex; align-items:baseline; gap:var(--s3);
          border:1px solid color-mix(in oklab, var(--warn) 40%, var(--bd2)); border-radius:var(--radius-lg);
          background:color-mix(in oklab, var(--warn) 7%, var(--sf));
          padding:var(--s3) var(--s4); font-size:var(--fs-text); }
.banner--error  { border-color:color-mix(in oklab, var(--loss) 40%, var(--bd2));
                  background:color-mix(in oklab, var(--loss) 7%, var(--sf)); }
/* The third tone, and the one the system went without for its whole life: a way to say that a
   thing happened. There was a banner for doubt and a banner for failure, and for "it worked"
   there was only the toast — which is fixed to the viewport and gone in a moment, so it could
   never sit beside the thing it was about. Writing the Words chapter is what turned it up: the
   rule that a button keeps its word had no surface to say the word on. */
.banner--ok     { border-color:color-mix(in oklab, var(--gain) 40%, var(--bd2));
                  background:color-mix(in oklab, var(--gain) 7%, var(--sf)); }
.banner__icon   { font-weight:600; color:var(--warn); flex:none; }
/* An action inside a banner is ink, not accent. The banner already carries its tone in the
   border and the ground; a link that competes with it reads worse and says nothing extra — and
   on the tinted dark ground the accent measures 3.97:1, which is below the bar for text.
   It stays a link by its weight and its underline. */
.banner .btn-link, .banner .btn-link-sm { text-decoration:underline; }
.banner .btn-link, .banner .btn-link-sm { color:var(--tx); }
.banner--error .banner__icon { color:var(--loss); }
.banner--ok .banner__icon    { color:var(--gain); }

/* The phone's hand: a fixed bottom bar, the ritual raised in its centre slot. */
.tabbar { position:fixed; left:0; right:0; bottom:0; z-index:var(--z-menu);
          display:flex; align-items:center; gap:var(--s1);
          background:var(--sf); border-top:1px solid var(--bd2);
          padding:0 max(10px, env(safe-area-inset-right)) env(safe-area-inset-bottom)
                  max(10px, env(safe-area-inset-left)); }
.tabbar__item { font:inherit; flex:1; min-height:52px; border:none; border-top:2px solid transparent;
                background:transparent; color:var(--mut); font-size:var(--fs-nano); font-weight:500;
                cursor:pointer; padding:6px 2px; }
.tabbar__item.on { color:var(--acc); border-top-color:var(--acc); }
/* The one round act. */
.tabbar__act  { font:inherit; flex:none; width:48px; height:48px; margin-top:-16px;
                border:4px solid var(--sf); border-radius:9999px; background:var(--acc); color:var(--acc-ink);
                font-size:var(--fs-act); font-weight:600; cursor:pointer; box-shadow:var(--sh-menu); }

/* The ledger: rows on the paper itself — columns are the page's own, given as --cols. */
.ledger        { --cols: minmax(0, 1fr) auto; }
.ledger__grid  { display:grid; grid-template-columns:var(--cols); gap:var(--s2) var(--s4);
                 align-items:baseline; padding:8px 16px; }
/* A row that holds a button centres its cells instead. A button's baseline is its first item's,
   and an icon has none of its own, so on the baseline an "+ Add" rode 4px above the row's words;
   every cell of a row is one size of type, so centred is level. A row of words — a title that
   is a .btn-bare included — keeps the baseline. */
.ledger__grid:has(> :is(.btn, .btn-acc, .btn-ghost, .btn-quiet, .btn-danger, .btn-icon),
                  > * > :is(.btn, .btn-acc, .btn-ghost, .btn-quiet, .btn-danger, .btn-icon)) { align-items:center; }
.ledger__head  { font-size:var(--fs-ui); color:var(--mut); font-weight:500;
                 border-bottom:1px solid var(--bd2); }
.ledger__row   { border-top:1px solid var(--bd2); }
.ledger__row:first-of-type { border-top:none; }
.ledger__row--heading { font-weight:600; }
.ledger__row--clickable { cursor:pointer; }
@media (hover: hover) {
  .ledger__row--clickable:hover { background:color-mix(in oklab, var(--tx) 4%, transparent); }
}
.ledger__num   { text-align:end; font-variant-numeric:tabular-nums; }

/* A skeleton: the shape of what is coming, pulsing quietly. */
.skeleton       { background:color-mix(in oklab, var(--tx) 8%, var(--sf)); border-radius:var(--radius-md);
                  animation:ornoPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
.skeleton--line { height:12px; border-radius:var(--radius-sm); }
@keyframes ornoPulse { 50% { opacity:0.5; } }

/* ==========================================================================
   Controls — the questions a form asks. Native elements, dressed: the browser
   keeps the keyboard, the label and the form, and the system keeps the look.
   ========================================================================== */

/* A box that is ticked. */
.check {
  appearance:none; -webkit-appearance:none;
  flex:none; width:16px; height:16px; margin:0;
  border:1px solid var(--bd); border-radius:4px; background:var(--sf); cursor:pointer;
  display:inline-grid; place-content:center; box-shadow:var(--sh-btn);
  transition:background var(--t-fast), border-color var(--t-fast);
}
.check:checked { background:var(--acc); border-color:var(--acc); }
.check:checked::after {
  content:''; width:4px; height:8px; margin-top:-2px;
  border:solid var(--acc-ink); border-width:0 2px 2px 0; transform:rotate(45deg);
}
.check:indeterminate { background:var(--acc); border-color:var(--acc); }
.check:indeterminate::after {
  content:''; width:8px; height:2px; margin:0; border:none; border-radius:1px;
  background:var(--acc-ink); transform:none;
}

/* One of many. */
.radio {
  appearance:none; -webkit-appearance:none;
  flex:none; width:16px; height:16px; margin:0;
  border:1px solid var(--bd); background:var(--sf); cursor:pointer;
  display:inline-grid; place-content:center; box-shadow:var(--sh-btn);
  border-radius:50%;
  transition:border-color var(--t-fast);
}
.radio:checked { border-color:var(--acc); }
.radio:checked::after { content:''; width:8px; height:8px; background:var(--acc); border-radius:50%; }

/* On or off, and saved the moment it moves — shadcn's switch: a pill track and a round knob.
   Off, the track is --bd rather than shadcn's hairline, because the track IS the control's
   boundary and has to clear 3:1. */
.switch {
  appearance:none; -webkit-appearance:none;
  flex:none; position:relative; width:36px; height:20px; margin:0;
  border:1px solid transparent; border-radius:9999px; background:var(--bd); cursor:pointer;
  box-shadow:var(--sh-btn);
  transition:background var(--t-fast), border-color var(--t-fast);
}
.switch::after {
  /* translate, not left: `left` is a layout property, so every flick of the switch put the
     whole subtree through layout. This is composited and looks identical. */
  content:''; position:absolute; top:1px; left:1px; width:16px; height:16px;
  border-radius:9999px; background:light-dark(#FFFFFF, var(--tx)); box-shadow:var(--sh-btn);
  transition:translate var(--t-fast), background var(--t-fast);
}
/* On is the filled one: the track takes the accent whole and the knob takes the ink that goes
   on it. The travel is the track's 34px inside its border, less a pixel at each end, less the
   16px knob: 16. */
.switch:checked { background:var(--acc); }
.switch:checked::after { translate:16px; background:var(--acc-ink); }

.check:disabled, .radio:disabled, .switch:disabled { cursor:default; opacity:0.5; }

/* A tick is 16px because a tick that size reads as a tick, and a thumb needs 24. So the box
   keeps its size and grows an invisible target around itself — the same trick the icon button
   uses, and the reason neither of them moved a layout to satisfy WCAG 2.5.8.
   (The label beside it is clickable too; this is for the box on its own.) */
.check, .radio, .switch { position:relative; }
.check::before, .radio::before, .switch::before {
  content:''; position:absolute; top:50%; left:50%; translate:-50% -50%;
  width:max(100%, 24px); height:max(100%, 24px);
}

/* The line a control sits on: the box, then what it asks, then the fine print under it. */
.choice { display:flex; align-items:flex-start; gap:10px; cursor:pointer; }
.choice:has(input:disabled) { cursor:default; color:var(--mut); }
.choice__text  { display:grid; gap:2px; }
.choice__label { font-size:var(--fs-text); }
.choice__note  { font-size:var(--fs-fine); color:var(--mut); }
.choices { display:grid; gap:10px; }

/* A question with several answers, grouped and named. */
.fieldset { border:none; margin:0; padding:0; min-width:0; }
.fieldset__legend { padding:0; font-size:var(--fs-text); font-weight:500; }
.fieldset__hint { display:block; font-size:var(--fs-fine); color:var(--mut); margin:6px 0 10px; }

/* Most fields are required, so the rare one says so instead: mark the minority. */
.lbl__optional { font-weight:400; color:var(--mut); }

/* Room to write, and the count of what is left. */
.in--area   { min-height:96px; resize:vertical; line-height:1.5; }
.counter    { display:block; font-size:var(--fs-fine); color:var(--mut); margin-top:6px; text-align:end; }
.counter--over { color:var(--loss); }

/* What went wrong, gathered at the top where a form is read from. */
.summary {
  border:1px solid color-mix(in oklab, var(--loss) 40%, var(--bd2)); border-radius:var(--radius-lg);
  background:color-mix(in oklab, var(--loss) 6%, var(--sf)); padding:var(--s4) var(--s5);
}
.summary__title { font-size:var(--fs-text); font-weight:600; }
.summary__list  { margin:10px 0 0; padding-inline-start:18px; display:grid; gap:4px; }
.summary__link  { font:inherit; font-size:var(--fs-text); color:var(--loss); background:none;
                  border:none; padding:0; text-align:start; cursor:pointer; text-decoration:underline;
                  text-underline-offset:4px; }

/* ==========================================================================
   Pickers — the three controls that are more than a box: a list you filter,
   a day you write, a file you hand over.
   ========================================================================== */

/* A field that suggests. The list hangs from it, so the wrapper sets the origin. */
/* A select that opens a list this system drew.

   It used to be a native <select>, and the book was honest about the trade: no stylesheet
   reaches inside the list a browser opens, so it arrived in whatever dress each engine chose —
   in the one component a form is most made of. The list is ours now. The button keeps the field's own dress so a row of
   controls still lines up; only the caret is added. */
.select        { display:flex; align-items:center; justify-content:space-between; gap:var(--s2);
                 text-align:start; cursor:pointer; }
.select__label { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.select__label--empty { color:var(--mut); }
.select__mark  { flex:none; color:var(--mut); font-size:var(--fs-caps); }
.select[aria-expanded="true"] .select__mark { color:var(--acc); }
/* The native one stays in the DOM and keeps submitting, so a form works before the script does
   and carries on working after it. Hidden from sight and from the tab order, never removed. */
.select__native { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }

.combo { position:relative; }
/* A combobox is an <input>, so its mark cannot live inside it the way the select's does. It
   sits over the end of the box instead, and the box keeps room for it — without the padding
   a long option ran underneath the triangle. pointer-events:none so clicking the mark is
   clicking the field, which is what it looks like. */
.combo__mark {
  position:absolute; inset-block:0; inset-inline-end:11px; display:flex; align-items:center;
  color:var(--mut); pointer-events:none;
}
.combo:has(> .combo__mark) > .in { padding-inline-end:28px; }
/* Past about eight options this scrolls, and it scrolls the way the rail does: no scrollbar,
 * a hairline at whichever end has more beyond it. The same four backgrounds, with one difference
 * that the rail did not need. This box has 4px of padding and a near-black border, so a rule
 * laid on the padding edge would sit 1px inside --line — pale on near-black, which is invisible
 * where it matters and a doubled border where it does not. `background-origin: content-box`
 * moves the four of them inside the padding, where the rule lands between the panel's edge and
 * the first option and reads as what it is: the list carries on past here. The ground stays on
 * the padding box, or the 4px frame would go transparent. */
.combo__list {
  position:absolute; z-index:var(--z-menu); top:calc(100% + 4px); left:0; right:0;
  max-height:240px; overflow-y:auto; scrollbar-width:none;
  background:
    linear-gradient(var(--sf), var(--sf))   local  top    / 100% 1px no-repeat,
    linear-gradient(var(--sf), var(--sf))   local  bottom / 100% 1px no-repeat,
    linear-gradient(var(--bd2), var(--bd2)) scroll top    / 100% 1px no-repeat,
    linear-gradient(var(--bd2), var(--bd2)) scroll bottom / 100% 1px no-repeat,
    var(--sf);
  background-origin:content-box, content-box, content-box, content-box, padding-box;
  border:1px solid var(--line); border-radius:var(--radius-md); box-shadow:var(--sh-menu);
  padding:4px; margin:0; list-style:none;
}
.combo__list::-webkit-scrollbar { width:0; height:0; }
.combo__option {
  padding:6px 8px; border-radius:var(--radius-sm); font-size:var(--fs-text); cursor:pointer;
}
.combo__option[aria-selected="true"] { font-weight:500; }
/* An option that cannot be taken is aria-disabled rather than :disabled — an <li> has no such
   attribute — so it says so in ink as well as to a screen reader. */
.combo__option[aria-disabled="true"] { color:var(--mut); cursor:default; }
/* Pressed lives here rather than in the flat block with everything else, and it has to carry
   the same two :not()s the hover carries. Holding a thing down also hovers it, so the two rules
   are live at the same moment and the more specific one wins: written as
   `.combo__option:active` it was (0,3,0) against the hover's (0,4,0), and pressing an option
   looked exactly like pointing at it. Measured on the deployed site, which is where it showed. */
.combo__option:not(.combo__option--active):not([aria-disabled="true"]):active {
  background:color-mix(in oklab, var(--tx) 14%, transparent);
}
/* The keyboard's place in the list, which is not the mouse's hover. */
.combo__option--active { background:color-mix(in oklab, var(--acc) 10%, transparent); }
.combo__empty { padding:10px; font-size:var(--fs-note); color:var(--mut); }

/* A day, typed the way a person writes one — the calendar is the second way in. */
.day { display:flex; align-items:stretch; gap:0; position:relative; }

/* The calendar, which this system drew.
 *
 * It used to be the browser's: a hidden <input type="date"> and showPicker(), which looks
 * different in every engine. The
 * field is still text first — typing a day is faster than pointing at one, and the text is the
 * control — but the second way in is ours now.
 *
 * A day cell is --hit square, which is the rule the system already states: nothing a thumb must
 * hit is smaller. */
.cal { position:absolute; z-index:var(--z-menu); top:calc(100% + 4px); inset-inline-start:0;
       background:var(--sf); border:1px solid var(--line); border-radius:var(--radius-md);
       box-shadow:var(--sh-menu); padding:var(--s3); }
/* The month at the start, the two steps together at the end, and a rule beneath them. This
   panel had no line anywhere in it — head, weekday row and days ran together as one block,
   in a system that draws structure with hairlines everywhere else. */
.cal__head { display:flex; align-items:center; justify-content:space-between; gap:var(--s2);
             padding-bottom:var(--s2); margin-bottom:var(--s2);
             border-bottom:1px solid var(--bd2); }
.cal__steps { display:flex; gap:4px; }
.cal__month { font-size:var(--fs-text); font-weight:500; }
.cal__step { font:inherit; font-size:var(--fs-text); line-height:1; flex:none;
             width:var(--hit); height:var(--hit); border:1px solid var(--bd2);
             border-radius:var(--radius-md); background:var(--sf); color:var(--tx); cursor:pointer;
             box-shadow:var(--sh-btn); }
.cal__grid { display:grid; grid-template-columns:repeat(7, var(--hit)); gap:2px; }
/* A row is there for the grid's semantics — role="row" is required between a grid and its
   cells — and must not lay anything out, so it dissolves and the cells meet the grid. */
.cal__row  { display:contents; }
/* Two characters wide now, so the tracking comes off — 0.09em on a pair of letters pushes them
   apart into two separate marks. And the row gets room of its own rather than sitting 2px off
   the first week. */
.cal__dow  { font-size:var(--fs-micro); color:var(--mut);
             text-align:center; padding-bottom:6px; }
.cal__day  { font:inherit; font-size:var(--fs-text); font-variant-numeric:tabular-nums;
             width:var(--hit); height:var(--hit); border:1px solid transparent;
             border-radius:var(--radius-md); background:transparent; color:var(--tx); cursor:pointer; }
/* A day either side of this month. Drawn rather than left as a hole — the grid used to begin
   and end ragged — and quiet enough that the month it belongs to is never in doubt. */
.cal__day--muted { color:color-mix(in oklab, var(--mut) 55%, transparent); }
/* Today is named by its edge, the chosen day by its fill — so a day that is both still reads
   as chosen, which is the one that matters. */
.cal__day--today { background:color-mix(in oklab, var(--tx) 6%, transparent); font-weight:600; }
.cal__day--on    { background:var(--acc); color:var(--acc-ink); border-color:var(--acc);
                   font-weight:500; }
@media (hover: hover) {
  .cal__step:hover:not(:disabled) { background:color-mix(in oklab, var(--tx) 5%, var(--sf)); }
  .cal__day:hover:not(:disabled):not(.cal__day--on) {
    background:color-mix(in oklab, var(--tx) 8%, transparent);
  }
}
.cal__step:active:not(:disabled) {
  background:color-mix(in oklab, var(--tx) 11%, var(--sf));
}
.cal__step:disabled, .cal__day:disabled { color:var(--mut); opacity:0.5; cursor:not-allowed; }
/* A day's press carries the same two :not()s its hover carries, and follows it. Written as
   `.cal__day:active:not(--on)` it was one selector short of the hover and lost, so a day looked
   the same held down as pointed at. */
.cal__day:not(:disabled):not(.cal__day--on):active {
  background:color-mix(in oklab, var(--tx) 16%, transparent);
}
/* Two boxes welded into one field: the inner corners go square so the pair reads as one. */
.day__text { flex:1; min-width:0; border-start-end-radius:0; border-end-end-radius:0; }
.day__pick {
  font:inherit; flex:none; width:36px; padding:0; cursor:pointer;
  border:1px solid var(--bd); border-inline-start:none; background:var(--sf); color:var(--mut);
  border-start-end-radius:var(--radius-md); border-end-end-radius:var(--radius-md);
  box-shadow:var(--sh-btn);
}
@media (hover: hover) { .day__pick:hover { color:var(--tx); background:color-mix(in oklab, var(--tx) 5%, var(--sf)); } }
/* After its own hover, which would otherwise win: held down reads deeper than pointed at. */
.day__pick:active { background:color-mix(in oklab, var(--tx) 11%, var(--sf)); }
/* The native picker does the calendar; it is never the thing you type into. */
.day__native { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }

/* A box to search in: the house's own field, with a magnifier at its start and, once there is
   something to empty, a button at its end. The box only gives the two marks somewhere to stand;
   the field keeps room for both, so a long search never runs under either. */
.search__box   { position:relative; }
.search__icon  { position:absolute; inset-block:0; inset-inline-start:12px; display:flex; align-items:center;
                 color:var(--mut); pointer-events:none; }
.search__in    { padding-inline-start:36px; padding-inline-end:40px; }
/* The browser draws its own cross in a search box; one way to empty a box is enough. */
.search__in::-webkit-search-cancel-button,
.search__in::-webkit-search-decoration { -webkit-appearance:none; appearance:none; }
.search__clear {
  position:absolute; inset-block:0; inset-inline-end:4px; margin-block:auto;
  display:inline-flex; align-items:center; justify-content:center; width:28px; height:28px; padding:0;
  border:none; border-radius:var(--radius-sm); background:transparent; color:var(--mut); cursor:pointer;
  transition:background var(--t-fast), color var(--t-fast);
}
/* The thumb gets --hit, as an icon button's does: a hit area costs nothing. */
.search__clear::after { content:''; position:absolute; top:50%; left:50%; translate:-50% -50%;
                        width:max(100%, var(--hit)); height:max(100%, var(--hit)); }
@media (hover: hover) { .search__clear:hover { background:color-mix(in oklab, var(--tx) 5%, transparent); color:var(--tx); } }

/* An amount, with its currency welded to the front.
 *
 * The same joinery as .day: one row, no gap, and the box on the start side gives up its inner
 * border so the pair reads as one field rather than two that happen to touch. Which box stands
 * there depends on how many currencies there are — a picker for several, a stilled mark for one,
 * nothing at all for a rate or a percentage. */
.amount { display:flex; align-items:stretch; gap:0; position:relative; }
.amount__text { flex:1; min-width:0; border-start-end-radius:0; border-end-end-radius:0; }
/* The unit stands at the END, beside the digits, and that is a correction.
 *
 * It began on the start side, where a currency sign goes in prose. But .in-num sets
 * text-align:end, so the amount sat hard against the right edge and EUR hard against the left,
 * seven hundred pixels apart in a wide field: welded in the DOM, unrelated to the eye. Moving
 * the DIGITS would have cost more than it bought — amounts stacked in a column are compared by
 * their right edges, which is the whole reason .in-num exists — so the unit moved instead. It
 * reads 1.250,00 EUR, which is how it is written in this house's own language anyway, and the
 * picker ends up where a picker belongs.
 *
 * The mono face so EUR reads as a unit and not as a word somebody stopped typing. */
.amount__mark {
  font:inherit; font-family:var(--font-mono); font-size:var(--fs-micro); flex:none;
  display:flex; align-items:center; padding:0 12px;
  border:1px solid var(--bd); border-inline-start:none; color:var(--mut);
  background:color-mix(in oklab, var(--tx) 4%, var(--sf));
  border-start-end-radius:var(--radius-md); border-end-end-radius:var(--radius-md);
}
/* The picker. .in is width:100%, which in a flex row would take the whole field and leave the
   amount nothing, so this one is told to be as wide as its longest code and no wider. */
.amount__unit { flex:none; }
.amount__unit .select { width:auto; border-inline-start:none;
                        border-start-start-radius:0; border-end-start-radius:0; }
.amount:has(.amount__text:disabled) .amount__mark { background:var(--bg); color:var(--mut); }

/* A number and the unit it is counted in, inside one border: 2100 kcal, 120 g.
 *
 * The border, the ground and the ring are the wrapper's, so the number and the unit read as one
 * field; the input inside gives up its own. The input is as wide as its `size` — not 100% — and
 * the unit has a slot at least four characters wide with its text at the end, so every field of
 * one size has one outer width whatever the unit, and a column of them lines up on both edges.
 * A longer unit widens its field rather than covering the digits. */
orno-unit-field {
  display: inline-flex;
  max-width: 100%;
  vertical-align: middle;
}
.unit-field {
  position: relative;
  display: inline-flex;
  /* The number is .in, at --fs-text; the unit follows it, not the type of whatever holds the field. */
  font-size:var(--fs-text);
  align-items: stretch;
  max-width: 100%;
  min-height: var(--ctl-h);
  border: 1px solid var(--bd);
  border-radius: var(--radius-md);
  background: var(--sf);
  box-shadow: var(--sh-btn);
  cursor: text;
  transition:
    border-color var(--t-fast),
    box-shadow var(--t-fast);
}
/* As specific as .in--error:focus and .in:disabled and later, so the inner box stays bare in
   every state; the wrapper says the state instead. */
.unit-field .unit-field__text,
.unit-field .unit-field__text:is(:focus, .in--error, :disabled) {
  flex: 0 1 auto;
  width: auto;
  min-width: 0;
  min-height: 0;
  padding-inline-end: 6px;
  border: none;
  border-radius: inherit;
  background: transparent;
  box-shadow: none;
  opacity: 1;
}
.unit-field__unit {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-width: calc(4ch + 12px);
  padding-inline-end: 12px;
  color: var(--mut);
  white-space: nowrap;
}
.unit-field:focus-within {
  border-color: var(--acc);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--acc) 25%, transparent);
}
.unit-field:has(> .unit-field__text:is(.in--error, [aria-invalid='true'])) {
  border-color: var(--loss);
}
.unit-field:has(> .unit-field__text:is(.in--error, [aria-invalid='true'])):focus-within {
  border-color: var(--loss);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--loss) 20%, transparent);
}
/* Disabled as a disabled .in looks: muted ground, no shadow, at 0.6. */
.unit-field:has(> .unit-field__text:disabled) {
  color: var(--mut);
  background-color: color-mix(in oklab, var(--tx) 4%, var(--sf));
  box-shadow: none;
  opacity: 0.6;
  cursor: not-allowed;
}
@media (hover: hover) {
  .unit-field:hover:not(:focus-within):not(:has(> .unit-field__text:disabled)) {
    border-color: var(--mut);
  }
}

/* How much: − the number +. The buttons are icon buttons (32px, and the thumb's --hit pad), but
   with an edge, because an outline-less glyph beside a field reads as decoration rather than as
   an act. They come after .btn-icon in this file on purpose: equal specificity, so the later
   rule wins, and the stepper's edge and ground are the ones that show. The number is centred —
   it is the whole point of the control — and the unit sits after it, read with it. */
.stepper        { display:inline-flex; align-items:center; gap:var(--s2); }
.stepper__btn   { display:inline-flex; align-items:center; justify-content:center; flex:none;
                  width:32px; height:32px; padding:0; font:inherit; cursor:pointer;
                  border:1px solid var(--bd); border-radius:var(--radius-md);
                  background:var(--sf); color:var(--tx); box-shadow:var(--sh-btn);
                  transition:background var(--t-fast); }
.stepper__value { width:calc(6ch + 26px); flex:none; min-height:32px; padding:5px 8px;
                  text-align:center; font-variant-numeric:tabular-nums; }
.stepper__unit  { font-size:var(--fs-text); color:var(--mut); }
@media (hover: hover) {
  .stepper__btn:hover:not(:disabled) { background:color-mix(in oklab, var(--tx) 5%, var(--sf)); }
}
.stepper__btn:active:not(:disabled) { background:color-mix(in oklab, var(--tx) 10%, var(--sf)); box-shadow:none; }
.stepper__btn:disabled { opacity:0.5; box-shadow:none; cursor:not-allowed; }
/* A thumb, not a cursor: under the phone width both buttons and the box are 44px, whatever
   the pointer says. */
@media (max-width: 560px) {
  .stepper__btn   { width:44px; height:44px; }
  .stepper__value { min-height:44px; }
}
/* The buttons are --ctl-h square and the box --ctl-h tall, at every width — the token
   already carries the phone's 44, so the rule above has nothing left to add. */
.stepper__btn   { width:var(--ctl-h); height:var(--ctl-h); }
.stepper__value { min-height:var(--ctl-h); }

/* Somewhere to drop a file, and something to click if you would rather. */
.dropzone {
  position:relative; display:grid; gap:4px; justify-items:center; text-align:center;
  border:1px dashed var(--bd); border-radius:var(--radius-lg); background:transparent;
  padding:var(--s5) var(--s4); cursor:pointer;
  transition:border-color var(--t-fast), background var(--t-fast);
}
.dropzone--over { border-color:var(--acc); background:color-mix(in oklab, var(--acc) 7%, transparent); }
.dropzone--error { border-color:var(--loss); }
/* A <label> cannot be :disabled, so it asks the input it wraps — the same shape .choice uses. */
.dropzone:has(.dropzone__input:disabled) {
  color:var(--mut); border-color:var(--bd2); cursor:default;
}
/* Invisible, but never unreachable: the keyboard still finds it. */
.dropzone__input {
  position:absolute; inset:0; width:100%; height:100%; opacity:0; cursor:pointer;
}
.dropzone__body { display:grid; gap:var(--s1); justify-items:center; }
.dropzone__input:focus-visible + .dropzone__body { outline:2px solid var(--acc); outline-offset:4px; }
.dropzone__title { font-size:var(--fs-text); font-weight:500; }
.dropzone__note  { font-size:var(--fs-fine); color:var(--mut); max-width:42ch; }
.dropzone__cta   { font-size:var(--fs-text); font-weight:500; color:var(--acc); margin-top:6px; }

/* What was handed over, and the way to take it back. */
.filebar {
  display:flex; align-items:baseline; gap:var(--s3);
  border:1px solid var(--bd2); border-radius:var(--radius-md); background:var(--sf); padding:10px 14px;
}
.filebar + .filebar { margin-top:6px; }
.filebar__name { font-size:var(--fs-text); font-weight:500; overflow-wrap:anywhere; }
.filebar__size { font-size:var(--fs-fine); color:var(--mut); font-variant-numeric:tabular-nums; }

/* ==========================================================================
   Structure — the frame three apps had each built for themselves, and the
   ways through it: tabs, a trail, pages, the way back.
   ========================================================================== */

/* The frame: an index down the side, a bar across the top, the work between them.
   On a phone the rail yields to whatever the app puts in the hand, and the content
   pane becomes the one scroller — a page that fits must not be draggable. */
.shell {
  display:grid; grid-template-columns:var(--shell-rail, 184px) minmax(0, 1fr);
  min-height:100dvh; background:var(--bg); color:var(--tx);
}
/* The rail itself does not scroll — the list of chapters inside it does, so the wordmark at the
   top and whatever sits at the foot stay put while the middle moves. */
.shell__rail {
  position:sticky; top:0; height:100dvh;
  display:flex; flex-direction:column;
  background:var(--sf); border-inline-end:1px solid var(--bd2); padding:18px 0 14px;
}
/* The wordmark: the way home. Plain ink, no mark — the door's .brand is a different thing,
   with a badge and a shadow, and it belongs on a sign-in page rather than in a rail. */
.shell__brand {
  font:inherit; text-align:start; border:none; background:transparent; cursor:pointer;
  color:var(--tx); padding:2px 16px 16px;
  font-size:var(--fs-text); font-weight:600; letter-spacing:-0.01em;
}
.shell__rail-heading { font-size:var(--fs-micro); color:var(--mut); font-weight:500;
                       padding:16px 16px 6px; }
/* The app writes its own element for the rail — a <nav>, so the landmark is the app's to name
   — and that element sits between the rail and its contents. It inherits the rail's job rather
   than blocking it: a column the full height of the rail, so the list can scroll inside it and
   the foot can push itself to the bottom. Without this the foot never moved and the list never
   scrolled; it only looked right because nothing was tall enough to prove otherwise.
   (display:contents would be simpler and would throw the navigation landmark away.) */
.shell__rail > * { display:flex; flex-direction:column; flex:1; min-height:0; }

/* Every router wraps its nav items in an <a>, so the rail expects that rather than forbidding
   it: inside the rail a link is dressed by .nav-item, and the browser's own link colour and
   underline would only fight it. */
.shell__rail a { color:inherit; text-decoration:none; display:block; }

/* The part that scrolls when there are more chapters than height.
 *
 * It scrolls without a scrollbar, and says so with a hairline instead. A scrollbar here is the
 * browser's furniture, the same kind the date overlay and the select's list used to be: on a Mac
 * it fades in over the chapter names every time you move, and on Windows it takes 15 of the
 * rail's 184px for good and the names reflow around it. A rail this narrow cannot spend that.
 *
 * What it must not lose is the fact that there IS more. So each end carries a 1px rule in the
 * line colour, and the rule is there only while the list runs past it. Four backgrounds do it
 * and no script does: the two covers are painted `local`, so they travel WITH the content and
 * come to rest exactly over a rule at each end of the travel; the two rules are painted
 * `scroll`, so they stay pinned to the box. At the top the top rule is covered and the bottom
 * one shows; in the middle both show; at the bottom, the other way round. Nothing to keep in
 * step, and nothing to recalculate when the list grows. */
.shell__rail-list {
  display:flex; flex-direction:column; gap:2px; overflow-y:auto; min-height:0;
  scrollbar-width:none;
  background:
    linear-gradient(var(--sf), var(--sf))   local  top    / 100% 1px no-repeat,
    linear-gradient(var(--sf), var(--sf))   local  bottom / 100% 1px no-repeat,
    linear-gradient(var(--bd2), var(--bd2)) scroll top    / 100% 1px no-repeat,
    linear-gradient(var(--bd2), var(--bd2)) scroll bottom / 100% 1px no-repeat;
}
/* scrollbar-width landed in Chrome 121 and Safari 18.2; this is the same sentence for anything
   older, and harmless in everything newer. */
.shell__rail-list::-webkit-scrollbar { width:0; height:0; }
.shell__rail-foot { margin-top:auto; padding:14px 14px 0; display:flex; flex-direction:column; gap:var(--s2); }
/* A 184px rail cannot hold a three-way segmented control at the size a page gives it — the last
   segment was simply cut off. The control is the same control here; it wears the rail's width. */
.shell__rail-foot .seg { padding:4px 6px; font-size:var(--fs-micro); }
.shell__body { display:flex; flex-direction:column; min-width:0; min-height:100dvh; }
.shell__top {
  position:sticky; top:0; z-index:calc(var(--z-menu) - 10);
  display:flex; align-items:center; gap:var(--s3); flex-wrap:wrap;
  background:var(--sf); border-bottom:1px solid var(--bd2);
  padding:10px max(14px, env(safe-area-inset-right)) 10px max(14px, env(safe-area-inset-left));
}
/* The bottom matches the top. It was 90px, room for a bar a desk does not have, and it made a
   short page scroll. */
.shell__main { flex:1; min-width:0; max-width:1180px; width:100%; margin:0 auto;
               padding:34px 28px; }

@media (max-width: 900px) {
  .shell { grid-template-columns:minmax(0, 1fr); height:100dvh; min-height:0; overflow:hidden; }
  .shell__rail { display:none; }
  .shell__body { height:100%; min-height:0; overflow-y:auto; overscroll-behavior:none; }
  /* 84px stood over a 53px bar: 31px of nothing inside the scroller, so a page that fits still
     scrolled. Three cases, in order: no bar over the page, so the top's room again and the safe
     area; a bar fixed over it, so its real height; and that bar with the round act, so its rise
     too. A pinned bar sits in the flow, part of the page rather than over it, so it asks no room.
     The shell asks itself, not the page, so a tab bar elsewhere — a demo, another frame — moves
     nothing; put the bar inside orno-shell. The shell is inside :where() so each rule weighs what
     .shell__main weighs, and the order decides. */
  .shell__main { padding:22px max(14px, env(safe-area-inset-right))
                 calc(22px + env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left)); }
  :where(.shell:has(.tabbar:not([pinned]))) .shell__main {
    padding-bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  }
  :where(.shell:has(.tabbar:not([pinned]) .tabbar__act)) .shell__main {
    padding-bottom:calc(var(--tabbar-h) + var(--tabbar-rise) + env(safe-area-inset-bottom));
  }
}

/* The side column: a third place in the frame, beside the work rather than over it. Not a sheet
   — no backdrop, nothing held captive — so the work narrows to make room and stays usable, and
   the column scrolls on its own, as the rail does. It is there only while it holds something:
   the app opens it by putting content in, and closes it by taking the content away. Below 900px
   there is no room beside the work, so it is not shown at all, and the app puts the same content
   in a bottom orno-drawer instead. */
.shell__aside { display:none; }
.shell--aside { grid-template-columns:var(--shell-rail, 184px) minmax(0, 1fr) var(--shell-aside, 400px); }
.shell--aside > .shell__aside {
  position:sticky; top:0; height:100dvh; overflow-y:auto; overscroll-behavior:contain;
  display:flex; flex-direction:column; min-width:0;
  background:var(--sf); border-inline-start:1px solid var(--bd2);
}
/* The column's inset: what a drawer's body has. Below 900px the same content moves into a bottom
   orno-drawer, so it keeps the same margins on both sides of the fold — and an app no longer
   borrows .drawer__body to get them. */
.shell__aside-body { padding:var(--s4) var(--s5); }
/* Two classes, not one, on purpose: this has to beat .shell--aside above, which is later in the
   file than the shell's own fold and would otherwise put three columns on a phone. */
@media (max-width: 900px) {
  .shell.shell--aside           { grid-template-columns:minmax(0, 1fr); }
  .shell--aside > .shell__aside { display:none; }
}

/* Tabs within a page: the same question, several faces. */
.tabs { display:flex; gap:var(--s5); border-bottom:1px solid var(--bd2); overflow-x:auto; }
.tabs__tab {
  font:inherit; font-size:var(--fs-text); font-weight:500; white-space:nowrap;
  border:none; border-bottom:2px solid transparent; background:transparent;
  color:var(--mut); cursor:pointer; padding:10px 0; margin-bottom:-1px;
  transition:color var(--t-fast), border-color var(--t-fast);
}
.tabs__tab.on { color:var(--tx); border-bottom-color:var(--acc); }
@media (hover: hover) { .tabs__tab:not(.on):hover { color:var(--tx); } }

/* Where you are, and everything you came through. */
/* shadcn's breadcrumb: muted steps that darken under the pointer, the page you are on in ink. */
.trail { display:flex; align-items:baseline; gap:var(--s2); flex-wrap:wrap;
         font-size:var(--fs-text); color:var(--mut); }
.trail__step { font:inherit; color:inherit; background:none; border:none; padding:0;
               cursor:pointer; text-decoration:none; transition:color var(--t-fast); }
@media (hover: hover) { .trail__step:hover:not(:disabled) { color:var(--tx); } }
.trail__step:disabled, .trail__here { cursor:default; text-decoration:none; color:var(--tx); font-weight:400; }
.trail__sep { color:var(--bd); }

/* A list longer than a screen. */
.pager { display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap; }
/* shadcn's pagination: ghost pages, the one you are on drawn as an outline button. */
.pager__page {
  font:inherit; font-size:var(--fs-text); font-variant-numeric:tabular-nums;
  min-width:36px; min-height:36px; padding:6px 10px; border:1px solid transparent;
  border-radius:var(--radius-md); background:transparent; color:var(--tx); cursor:pointer;
}
@media (hover: hover) { .pager__page:hover:not(:disabled):not(.on) { background:color-mix(in oklab, var(--tx) 5%, transparent); } }
.pager__page.on { background:var(--sf); border-color:var(--bd2); font-weight:500; box-shadow:var(--sh-btn); }
.pager__page:disabled { opacity:0.5; cursor:not-allowed; }
.pager__gap { color:var(--mut); font-size:var(--fs-text); padding:0 2px; }
.pager__count { font-size:var(--fs-text); color:var(--mut); margin-inline-start:var(--s2); }
/* The size of a page, chosen at the end of the pager's row — and on a row of its own when the
   row runs out, as on a phone. Its words are the select's name too, so they are said once. */
.pager__size       { display:inline-flex; align-items:center; gap:var(--s2); margin-inline-start:auto; }
.pager__size-label { font-size:var(--fs-text); color:var(--mut); white-space:nowrap; }
.pager__size > orno-select { display:inline-block; position:relative; }
.pager__size .select { width:auto; min-width:72px; }

/* The way back, said once and always in the same place. */
.back-link {
  font:inherit; font-size:var(--fs-text); font-weight:500;
  border:none; background:none; color:var(--mut); padding:0; cursor:pointer;
}
@media (hover: hover) { .back-link:hover { color:var(--tx); } }

/* ==========================================================================
   Overlays — what hangs off something else. The menu had to be positioned by
   every app that used it; this is where that stops.
   ========================================================================== */

/* A panel that hangs off what opened it. The anchor sets the origin; the placement
   picks the corner it grows from. */
.pop { position:relative; display:inline-block; }
.pop__panel {
  position:absolute; z-index:var(--z-menu); min-width:180px;
  background:var(--sf); border:1px solid var(--line); border-radius:var(--radius-md);
  box-shadow:var(--sh-menu); padding:var(--s3) var(--s4);
}
.pop__panel--bottom-start { top:calc(100% + 6px); inset-inline-start:0; }
.pop__panel--bottom-end   { top:calc(100% + 6px); inset-inline-end:0; }
.pop__panel--top-start    { bottom:calc(100% + 6px); inset-inline-start:0; }
.pop__panel--top-end      { bottom:calc(100% + 6px); inset-inline-end:0; }
.pop__panel--right        { left:calc(100% + 6px); top:0; }
.pop__panel--left         { right:calc(100% + 6px); top:0; }

/* A word about the thing under the cursor. Never a place to put a control:
   a tooltip that must be clicked is a popover wearing the wrong clothes. */
.tip { position:relative; display:inline-flex; }
.tip__bubble {
  position:absolute; z-index:var(--z-toast); bottom:calc(100% + 6px); left:50%;
  transform:translateX(-50%); white-space:nowrap;
  background:var(--tx); color:var(--bg); font-size:var(--fs-fine); line-height:1.4;
  padding:6px 12px; border-radius:var(--radius-md); pointer-events:none;
}
.tip__bubble--below { bottom:auto; top:calc(100% + 6px); }
.tip__bubble--wrap  { white-space:normal; width:max-content; max-width:220px; }

/* A list that opens one thing at a time — or several, if the app says so. */
.fold-list { border:none; border-radius:var(--radius-lg); background:var(--sf); overflow:hidden;
             box-shadow:var(--sh-card); }
.fold-list__item { border-top:1px solid var(--bd2); }
.fold-list__item:first-child { border-top:none; }
/* The heading is a level, not a margin: the item draws the spacing. */
.fold-list__item > h3 { margin:0; }
.fold-list__head {
  font:inherit; display:flex; align-items:center; gap:var(--s3); width:100%; text-align:start;
  background:transparent; border:none; cursor:pointer; color:var(--tx);
  padding:14px 16px; font-size:var(--fs-text); font-weight:500;
}
@media (hover: hover) { .fold-list__head:hover { background:color-mix(in oklab, var(--tx) 4%, transparent); } }
/* After its own hover, not up in the shared pressed block. Equal specificity, and the hover
   stood 970 lines further down the file — so the press lost every time, which is only visible
   while a finger is actually on it. */
.fold-list__head:active { background:color-mix(in oklab, var(--tx) 10%, transparent); }
.fold-list__mark { color:var(--mut); font-size:var(--fs-micro); flex:none; transition:transform var(--t-mid); }
.fold-list__head.on .fold-list__mark { transform:rotate(90deg); }
.fold-list__body { padding:0 16px 16px; font-size:var(--fs-text); }

/* A sheet that slides in from an edge: the phone's way of showing detail without
   leaving the page. Same weight as a dialog, because it is one. */
/* The element layer closes a drawer by setting [hidden] on both parts, and display:flex below
   beats the browser's own [hidden] rule — so a closed sheet stood on the page, and its animation
   never began again on opening. Said once here, for both. */
.drawer[hidden], .drawer-backdrop[hidden] { display:none; }
.drawer-backdrop {
  position:fixed; inset:0; z-index:var(--z-dialog);
  background:color-mix(in oklab, var(--edge) 50%, transparent);
  animation:ornoFade var(--t-mid) ease-out;
  view-transition-name:orno-drawer-backdrop;
}
.drawer {
  position:fixed; z-index:calc(var(--z-dialog) + 1);
  background:var(--sf); border:1px solid var(--line); box-shadow:var(--sh-dialog);
  display:flex; flex-direction:column; overflow-y:auto;
}
.drawer--right  { top:0; bottom:0; right:0; width:min(420px, 92vw); border-right:none;
                  animation:ornoSlideRight var(--t-mid) ease-out;
                  view-transition-name:orno-drawer-right; }
.drawer--left   { top:0; bottom:0; left:0;  width:min(420px, 92vw); border-left:none;
                  animation:ornoSlideLeft var(--t-mid) ease-out;
                  view-transition-name:orno-drawer-left; }
.drawer--bottom { left:0; right:0; bottom:0; max-height:85dvh; border-bottom:none;
                  border-radius:var(--radius-xl) var(--radius-xl) 0 0;
                  padding-bottom:env(safe-area-inset-bottom);
                  animation:ornoSlideUp var(--t-mid) ease-out;
                  view-transition-name:orno-drawer-bottom; }
/* Closed, it goes back out the way it came: the sheet over its own edge, the backdrop fading,
   in the same 200ms. Both layers keep it on the page, inert, until this has run, and then take
   it away; `forwards` holds the last frame so nothing flashes back in between. */
.drawer-backdrop--leaving        { animation:ornoFadeOut var(--t-mid) ease-in forwards; }
.drawer--right.drawer--leaving   { animation:ornoSlideRightOut var(--t-mid) ease-in forwards; }
.drawer--left.drawer--leaving    { animation:ornoSlideLeftOut var(--t-mid) ease-in forwards; }
.drawer--bottom.drawer--leaving  { animation:ornoSlideDownOut var(--t-mid) ease-in forwards; }
/* An app whose sheet is a page of its own — closing it is a navigation, which takes it off the
   page at once — gets the same exit from the browser: run that navigation as a view transition
   and the snapshot of the sheet, named above, slides out while the page under it is already
   there. A sheet still open after the change has a new image too, and simply stays. A name is
   one per page: a sheet on its way out gives its names up, so one opening as another leaves
   does not clash, but two open at once on one side (or two backdrops) make the browser skip the
   page change's animation. */
.drawer--leaving, .drawer-backdrop--leaving { view-transition-name:none; }
::view-transition-old(orno-drawer-backdrop):only-child { animation:ornoFadeOut var(--t-mid) ease-in forwards; }
::view-transition-old(orno-drawer-right):only-child    { animation:ornoSlideRightOut var(--t-mid) ease-in forwards; }
::view-transition-old(orno-drawer-left):only-child     { animation:ornoSlideLeftOut var(--t-mid) ease-in forwards; }
::view-transition-old(orno-drawer-bottom):only-child   { animation:ornoSlideDownOut var(--t-mid) ease-in forwards; }
/* The sheet comes in from its own edge and the backdrop fades up, about 200ms. Keyframes with
   only a starting frame: the end is the rule above, so the resting position and size are exactly
   what they were without motion. They run whenever the panel starts to be displayed — an element
   created already open, an [open] that turns true, or a hidden one shown. The exits have only an
   ending frame, for the same reason the other way round. */
@keyframes ornoFade       { from { opacity:0; } }
@keyframes ornoSlideRight { from { transform:translateX(100%); } }
@keyframes ornoSlideLeft  { from { transform:translateX(-100%); } }
@keyframes ornoSlideUp    { from { transform:translateY(100%); } }
@keyframes ornoFadeOut       { to { opacity:0; } }
@keyframes ornoSlideRightOut { to { transform:translateX(100%); } }
@keyframes ornoSlideLeftOut  { to { transform:translateX(-100%); } }
@keyframes ornoSlideDownOut  { to { transform:translateY(100%); } }
/* Reduced motion: no slide and no fade at all, rather than the page-wide 0.01ms — in, out, or
   in a page change's snapshot. With nothing running, a closed sheet goes at once. */
@media (prefers-reduced-motion: reduce) {
  .drawer-backdrop, .drawer--right, .drawer--left, .drawer--bottom { animation:none; }
  /* As specific as the exits, and later, so it beats them too. */
  .drawer-backdrop.drawer-backdrop--leaving, .drawer.drawer--leaving { animation:none; }
  /* With :only-child, as specific as the exits above, so it beats them. */
  ::view-transition-old(orno-drawer-backdrop):only-child, ::view-transition-old(orno-drawer-right):only-child,
  ::view-transition-old(orno-drawer-left):only-child, ::view-transition-old(orno-drawer-bottom):only-child { animation:none; }
}
/* The head stays at the top while a long body scrolls under it, so the title, the way back and
   the close button never leave the sheet. It wears the sheet's own ground, so what passes under
   it is hidden, and stands above the body's own sticky parts (a tall table's head is 1); menus
   and lists still open over it. On a bottom sheet the grip scrolls away above it, and the
   rounded corners still clip it. At rest it is exactly where it was. */
.drawer__head {
  position:sticky; top:0; z-index:2; background:var(--sf);
  display:flex; align-items:center; gap:var(--s3);
  border-bottom:1px solid var(--bd2); padding:var(--s4) var(--s5);
}
/* …and a field reached by Tab or focus() scrolls in below the stuck head instead of under it:
   the head is a 32px icon button, its two paddings and its hairline. Only a sheet with a head
   keeps that strip clear. */
.drawer:has(> .drawer__head) { scroll-padding-top:calc(32px + 2 * var(--s4) + 1px); }
/* A back button in the head is the first flex item, so it sits flush left by the head's own gap;
   a long heading is cut to one line (min-width:0 lets it shrink beside the close button, which
   stays in view). */
.drawer__head .h-sec { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.drawer__body { padding:var(--s4) var(--s5); }
/* The acts under a drawer — Apply and Reset under a filter — stay in view while a long body
   scrolls, as the head does at the top: sticky at the bottom on the sheet's own ground, above
   the body's sticky parts, a hairline between. Padded as the body is. On a side sheet whose body
   is short it still stands at the bottom edge, where acts are looked for. */
.drawer__foot {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin-top: auto;
  background: var(--sf);
  border-top: 1px solid var(--bd2);
  padding: var(--s4) var(--s5);
}
/* …and a field reached by Tab scrolls in above the stuck foot instead of under it: one control,
   the foot's two paddings and its hairline. */
.drawer:has(> .drawer__foot) {
  scroll-padding-bottom: calc(var(--ctl-h) + 2 * var(--s4) + 1px);
}
/* A sheet from the bottom pads itself by the home indicator, but a sticky foot sticks to the
   padding edge, above that padding: with a foot the inset moves into the foot, so the foot clears
   the indicator while the body scrolls, and into the scroll padding. A footless sheet keeps its own. */
.drawer--bottom:has(> .drawer__foot) {
  padding-bottom: 0;
  scroll-padding-bottom: calc(var(--ctl-h) + 2 * var(--s4) + 1px + env(safe-area-inset-bottom));
}
.drawer--bottom > .drawer__foot { padding-bottom: calc(var(--s4) + env(safe-area-inset-bottom)); }
/* The thumb's grip on a sheet that came up from the bottom. */
.drawer__grip { width:100px; height:6px; border-radius:9999px; background:var(--bd2); margin:12px auto 0; }


/* ==========================================================================
   Data — the shapes a row of figures can take. The ledger was always the
   house style; this is the table for everything the ledger cannot do:
   columns you sort, a head that stays, a width that outruns the pane.
   ========================================================================== */

/* The pane the table lives in. A table wider than its pane scrolls here and
   nowhere else — the page body must never move sideways. The two width rules are
   what make that true: without them a flex or grid parent lets the pane grow to
   the table's own width and the whole page starts sliding. */
.tbl-wrap { overflow-x:auto; max-width:100%; min-width:0;
            border:none; border-radius:var(--radius-lg); background:var(--sf);
            box-shadow:var(--sh-card); }
.tbl {
  width:100%; border-collapse:collapse; font-size:var(--fs-text);
  /* Columns keep the width their content asks for rather than being squeezed. */
  min-width:max-content;
}
.tbl thead th {
  background:var(--sf); border-bottom:1px solid var(--bd2);
  font-size:var(--fs-text); font-weight:500;
  color:var(--mut); text-align:start; white-space:nowrap;
  padding:8px 14px;
}
.tbl tbody td { border-top:1px solid var(--bd2); padding:8px 14px; vertical-align:top; }
.tbl tbody tr:first-child td { border-top:none; }
.tbl__num { text-align:end; font-variant-numeric:tabular-nums; }
.tbl thead th.tbl__num { text-align:end; }
.tbl__row--clickable { cursor:pointer; }
@media (hover: hover) {
  .tbl__row--clickable:hover td { background:color-mix(in oklab, var(--tx) 4%, transparent); }
}
.tbl__row.on td { background:color-mix(in oklab, var(--acc) 10%, transparent); }

/* A head only stays put if the rows can move under it, which means the pane needs a
   height. Given one, the head sticks; without one, it has nothing to stick to and
   saying `sticky` would only cost the page four phantom pixels of sideways scroll. */
.tbl-wrap--tall { max-height:var(--tbl-height, 420px); overflow-y:auto; }
.tbl-wrap--tall .tbl thead th { position:sticky; top:0; z-index:1; }

/* A column you can sort by is a button, so a keyboard can reach it. */
.tbl__sort {
  font:inherit; letter-spacing:inherit; text-transform:inherit; color:inherit;
  display:inline-flex; align-items:center; gap:5px;
  background:none; border:none; padding:0; cursor:pointer;
}
@media (hover: hover) { .tbl__sort:hover { color:var(--tx); } }
/* The column the table stands sorted by is written in ink. aria-sort lives on the <th> — the
   columnheader — in both layers, so the rule looks there; the button form stays for a page that
   wrote it by hand. */
.tbl__sort[aria-sort="ascending"], .tbl__sort[aria-sort="descending"],
th[aria-sort="ascending"] > .tbl__sort, th[aria-sort="descending"] > .tbl__sort { color:var(--tx); }
/* The arrow is faint when the table is not sorted by this column and loud when it is — but
   faint is --mut, not --bd: a boundary colour clears 3:1, and this is text, which needs 4.5.
   It carries meaning to the eye even though it is hidden from a screen reader, so it is held to
   the text bar. 9px was also below every step of the type scale. */
.tbl__arrow { color:var(--acc); font-size:var(--fs-nano); }
.tbl__arrow--idle { color:var(--mut); }
.tbl__foot td { border-top:1px solid var(--bd2); font-weight:500;
                background:color-mix(in oklab, var(--tx) 3%, var(--sf)); }

/* The phone. Sideways scrolling is the honest default; `tbl--stack` is the other
   answer — each row becomes a block and every cell says which column it is. */
@media (max-width: 560px) {
  .tbl--stack { min-width:0; }
  .tbl--stack thead { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
  .tbl--stack tbody tr { display:block; border-top:1px solid var(--bd2); padding:8px 0; }
  .tbl--stack tbody tr:first-child { border-top:none; }
  .tbl--stack tbody td { display:flex; gap:var(--s4); border:none; padding:4px 14px; }
  .tbl--stack tbody td::before {
    content:attr(data-label); flex:none; width:38%;
    font-size:var(--fs-ui); font-weight:500; color:var(--mut);
  }
  .tbl--stack .tbl__num { justify-content:space-between; text-align:start; }
}

/* Who, in the space of a letter or two — shadcn's avatar: round, on a muted ground. */
.avatar {
  display:inline-flex; align-items:center; justify-content:center; flex:none; overflow:hidden;
  width:32px; height:32px; border-radius:9999px;
  background:color-mix(in oklab, var(--tx) 8%, var(--sf)); color:var(--tx); border:none;
  font-size:var(--fs-fine); font-weight:500; text-transform:uppercase;
}
.avatar img { width:100%; height:100%; object-fit:cover; }
/* --fs-nano, not 9.5px. The scale bottoms out at 10px because that is the smallest this face
   stays readable at, and initials are the one place a label cannot be guessed from context —
   "SG" at 9.5px was the smallest text in the system and the hardest to be sure of. */
.avatar--sm { width:22px; height:22px; font-size:var(--fs-nano); }
.avatar--lg { width:46px; height:46px; font-size:var(--fs-mark); }
.avatar--quiet { background:var(--bg); color:var(--mut); box-shadow:inset 0 0 0 1px var(--bd2); }
/* Several of them, overlapping, for a room rather than a person — each ringed in the ground,
   so the overlap reads as a cut rather than a smudge. */
.avatar-stack { display:inline-flex; }
.avatar-stack > .avatar { box-shadow:0 0 0 2px var(--sf); }
.avatar-stack > .avatar + .avatar { margin-inline-start:-8px; }
/* A small one's initials fill 17 of its 22px, so 8px of overlap and the ring hid most of a
   letter; at 2px the ring meets the circle just past the last letter and the cut still shows. */
.avatar-stack > .avatar--sm + .avatar { margin-inline-start:-2px; }

/* What a thing is, said as label and value. The detail pane's whole grammar. */
.facts { margin:0; display:grid; grid-template-columns:auto minmax(0, 1fr); gap:var(--s2) var(--s5); }
.facts > dt { font-size:var(--fs-text); color:var(--mut); }
.facts > dd { margin:0; font-size:var(--fs-text); color:var(--tx); }
.facts--rows { grid-template-columns:minmax(0, 1fr); gap:var(--s1); }
.facts--rows > dd { margin-bottom:var(--s3); }

/* A mark of state, beside the thing it is about. */
.pip { display:inline-block; width:8px; height:8px; flex:none; border-radius:9999px; background:var(--mut); }
.pip--gain { background:var(--gain); }
.pip--loss { background:var(--loss); }
.pip--warn { background:var(--warn); }
.pip--acc  { background:var(--acc); }
.pip--idle { background:transparent; border:1px solid var(--bd); }
/* The state and its word, which is how it should nearly always appear. */
.pip-label { display:inline-flex; align-items:center; gap:6px; font-size:var(--fs-text); }

/* How many — on a tab, a nav item, an inbox. Never more than three glyphs. */
.count {
  display:inline-block; min-width:20px; padding:1px 6px; text-align:center; border-radius:9999px;
  background:var(--acc); color:var(--acc-ink); border:1px solid transparent;
  font-size:var(--fs-nano); font-weight:600; font-variant-numeric:tabular-nums; line-height:1.5;
}
.count--quiet { background:color-mix(in oklab, var(--tx) 6%, var(--sf)); color:var(--mut); }

/* Something to be read exactly, and usually copied. */
.code {
  position:relative; background:color-mix(in oklab, var(--tx) 3%, var(--sf));
  border:1px solid var(--bd2); border-radius:var(--radius-md);
  padding:12px 14px; overflow-x:auto;
  font-family:var(--font-mono); font-size:var(--fs-fine); line-height:1.65; white-space:pre; tab-size:2;
}
.code__head {
  display:flex; align-items:center; gap:var(--s3);
  border-bottom:1px solid var(--bd2); background:var(--sf); padding:6px 14px;
  font-size:var(--fs-micro); font-weight:500; color:var(--mut);
}
.code-block { border:1px solid var(--bd2); border-radius:var(--radius-md); overflow:hidden; }
.code-block > .code { border:none; border-radius:0; }
.code__copy {
  font:inherit; font-size:var(--fs-micro); font-weight:500;
  background:none; border:none; color:var(--mut); cursor:pointer; padding:0;
}
@media (hover: hover) { .code__copy:hover { color:var(--acc); } }
/* Likewise: the press was written to darken to ink and the hover to raise it to accent, and
   the hover came later, so the ink never showed. */
.code__copy:active { color:var(--tx); }

/* ==========================================================================
   Layout — the four arrangements every app in this house wrote for itself.

   These dress something you already have rather than adding an element of
   their own, so in Angular they are directives and in plain HTML they are
   classes. There are no <orno-stack> elements on purpose: a layout that
   forces a wrapper into your markup is a layout you end up fighting.

   Each takes its gap from the spacing scale through --gap, so `compact`
   tightens every one of them at once.
   ========================================================================== */

/* One icon from the set: it takes its colour from the text around it and sits on the
   line rather than above or below it. */
.icon { display:inline-flex; align-items:center; justify-content:center; flex:none;
        vertical-align:-0.16em; color:currentColor; }
.icon svg { display:block; }

/* Things above one another, evenly. The commonest arrangement there is. */
.stack { display:grid; gap:var(--gap, var(--s4)); }
/* The gap is the only spacing in a stack. A paragraph or a heading brings the browser's
   margins with it, and in a grid they add to the gap rather than collapse into it — so a stack
   of text stood further apart than a stack of cards at the same --gap. :where keeps this at one
   class, so .push below, at two, still wins. */
.stack > :where(p, h1, h2, h3, h4, h5, h6, .h-sec, .h-sub, .fine, .caps) { margin-block:0; }
/* One child pushed to the bottom — a card's actions, a rail's foot. */
.stack > .push { margin-top:auto; }
/* Angular's empty outlet, last in a stack, is a 0px grid row that still carries a gap. */
.stack > router-outlet { display:none; }

/* Things beside one another that wrap when they run out of room: chips, filters, buttons. */
.cluster { display:flex; flex-wrap:wrap; gap:var(--gap, var(--s3)); align-items:center; }
.cluster--start { align-items:flex-start; }
.cluster--end   { justify-content:flex-end; }

/* Two things pushed apart: a heading and its act, a label and its figure. Below the phone
   width it becomes a stack, because two things pushed apart in 320px are two things on top
   of each other with extra steps. */
.split { display:flex; align-items:center; gap:var(--gap, var(--s4)); }
.split > :first-child { flex:1; min-width:0; }
@media (max-width: 560px) {
  .split--fold { display:grid; align-items:stretch; }
}

/* As many columns as fit, no breakpoint needed: the cards decide. --col is the narrowest a
   column may be before one is dropped. */
.tiles { display:grid; gap:var(--gap, var(--s4));
         grid-template-columns:repeat(auto-fill, minmax(var(--col, 220px), 1fr)); }
/* Exactly --count equal columns — four figures in a row — and two on a phone, so four become
   two by two rather than four slivers or one long column. Auto-fill cannot promise either: it
   leaves empty tracks on a wide screen and drops to one column in a hand. */
.tiles--count { grid-template-columns:repeat(var(--count, 4), minmax(0, 1fr)); }
@media (max-width: 560px) {
  .tiles--count { grid-template-columns:repeat(2, minmax(0, 1fr)); }
}

/* A column at a width the eye can follow, centred in what holds it and the whole width when
   there is less room than that. Bare, it is a form's column; .measure--text holds it to a line of
   running text, --measure, which a page may set to its own length. */
.measure { width:100%; max-width:var(--measure-form); margin-inline:auto; }
.measure--text { max-width:var(--measure); }

/* ==========================================================================
   Layout pieces — what an app would otherwise write in a stylesheet of its
   own. Nutrio keeps none; each of these is a thing it would have had to.
   Last in the file on purpose: several sit on top of a component above
   (a button, the tab bar, the facts list) and win by order, not by weight.
   ========================================================================== */

/* One line, cut with an ellipsis rather than wrapped: a meal's name in a narrow row. min-width:0
   because a flex or grid child refuses to shrink below its content otherwise, and then nothing
   is ever cut — the row just runs off the card. It needs a width to cut against: a block, or a
   flex or grid child. */
.trunc { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }

/* A custom element is inline until somebody says otherwise, and an inline host in a grid or flex
   parent will not shrink. This says both: a block, and allowed to be narrower than its content. */
.blk-fit { display:block; min-width:0; }

/* A <button> that wears its surroundings: the row it is, the word it is. A whole row you can open
   must still be a button — a keyboard and a screen reader need one — but it should look like the
   row. Font and colour come from around it; it keeps the system's focus ring, which comes from
   the global rule, and answers the hand with the faint tint every quiet surface uses. */
.btn-bare { display:block; width:100%; margin:0; padding:0; border:0; border-radius:var(--radius-md);
            background:none; font:inherit; color:inherit; text-align:start; cursor:pointer;
            transition:background var(--t-fast); }
@media (hover: hover) {
  .btn-bare:hover:not(:disabled) { background:color-mix(in oklab, var(--tx) 4%, transparent); }
}
.btn-bare:active:not(:disabled) { background:color-mix(in oklab, var(--tx) 8%, transparent); }
.btn-bare:disabled { opacity:0.5; cursor:not-allowed; }
/* Every act knows how to be underway, and a bare one is no exception: unpressable while busy. */
.btn-bare[aria-busy='true'] { cursor:progress; pointer-events:none; opacity:0.7; }

/* A card's header bar: its title at the start, one control at the end, on one line. The title is
   the part that gives way — it is cut rather than pushing the control onto a second line. */
.card__bar { display:flex; align-items:center; justify-content:space-between; gap:var(--s3);
             min-width:0; margin-bottom:var(--s3); }
.card__bar > :first-child { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* Text in a status colour: weight went down, a figure over its target. The same three colours the
   system already measures as text (4.5:1 on the card and on the page, both themes, in
   `npm run contrast`), so there is nothing new to check — only a name for using them as ink. */
.ink-gain { color:var(--gain); }
.ink-warn { color:var(--warn); }
.ink-loss { color:var(--loss); }

/* A settings section: its title in a column of its own beside the box of settings, the way a
   settings page reads on a desk; stacked at 900px and below, where there is no room for two.
   Inside the box each row is a .split: the label column (.settings__label) holds the label and
   its hint or fault, and the control (.settings__control) sits at the end. */
.settings          { display:grid; grid-template-columns:minmax(0, 14rem) minmax(0, 1fr);
                     gap:var(--s3) var(--s6); align-items:start; }
.settings__label   { display:grid; gap:var(--s1); min-width:0; }
.settings__control { flex:none; justify-self:end; margin-inline-start:auto; }
@media (max-width: 900px) {
  .settings { grid-template-columns:minmax(0, 1fr); }
}

/* Two panels side by side at one height. The second is size-contained: its content no longer
   makes the row taller, so the first panel sets the height, the second stretches to it and
   scrolls inside. A panel that scrolls must be reachable by keyboard — give it tabindex="0" and
   a name. At 900px and below they stack, each its own height. */
.pair { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:var(--gap, var(--s4));
        align-items:stretch; }
.pair > :nth-child(2) { contain:size; overflow-y:auto; min-height:0; }
/* A card as the second panel keeps its frame: the card holds still at the pair's height — its
   bar, its border and its rounded corners all in view — and only its .card__body scrolls, so the
   body takes the tabindex="0" and the name. A card with no .card__body scrolls whole, as before.
   The body bleeds to the card's sides (its 24px) so the scrollbar sits at the edge, not in the
   text. */
.pair > .card:nth-child(2):has(> .card__body) { display:flex; flex-direction:column; overflow:hidden; }
.pair > .card:nth-child(2):has(> .card__body) > * { flex:none; }
.pair > .card:nth-child(2) > .card__body {
  flex:1 1 0; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  margin-inline:-24px; padding-inline:24px;
}
/* The card clips at its frame, so the keyboard's ring is drawn inside the body, not around it. */
.pair > .card:nth-child(2) > .card__body:focus-visible { outline-offset:-2px; }
@media (max-width: 900px) {
  .pair { grid-template-columns:minmax(0, 1fr); }
  .pair > :nth-child(2) { contain:none; overflow-y:visible; }
  .pair > .card:nth-child(2):has(> .card__body) { display:block; overflow:visible; }
  .pair > .card:nth-child(2) > .card__body { overflow-y:visible; margin-inline:0; padding-inline:0; }
}

/* An "of which" line — of which sugars — one step in under the line it belongs to, so it reads as
   part of that line rather than as a fact of its own. */
.facts__sub { padding-inline-start:var(--s4); }

/* A button that is a tile: an icon over a title over one grey line, centred, filling its column
   of a [ornoTiles] row. A card to the eye, a button to everything else. The children are held
   to the tile's width so a long grey line is cut (give it .trunc) instead of widening the tile. */
.tile-btn { display:grid; justify-items:center; align-content:center; gap:var(--s1);
            width:100%; height:100%; min-width:0; padding:var(--s4) var(--s3);
            border:none; border-radius:var(--radius-xl); background:var(--sf);
            box-shadow:var(--sh-card); font:inherit; color:var(--tx); text-align:center;
            cursor:pointer; transition:background var(--t-fast); }
.tile-btn > * { max-width:100%; }
@media (hover: hover) {
  .tile-btn:hover:not(:disabled) { background:color-mix(in oklab, var(--tx) 4%, var(--sf)); }
}
.tile-btn:active:not(:disabled) { background:color-mix(in oklab, var(--tx) 8%, var(--sf)); box-shadow:0 0 0 1px var(--line); }
.tile-btn:disabled { opacity:0.5; box-shadow:0 0 0 1px var(--line); cursor:not-allowed; }

/* A video or a photo at the width of what holds it, never taller than most of the screen, filling
   its box edge to edge. --thumb is the small one: at most 8rem high, its own width, never cut. */
.media        { display:block; width:100%; max-height:60dvh; object-fit:cover;
                border-radius:var(--radius-lg); background:color-mix(in oklab, var(--tx) 5%, var(--sf)); }
.media--thumb { width:auto; max-width:100%; height:auto; max-height:8rem; object-fit:contain; }

/* The parts of an app's own line graph, drawn as inline SVG. Not a chart component — the app
   draws the path; these only dress it. The line is the first series colour (or the second or
   third, on the path or on a <g> round a path and its dots), 2 units, round, unfilled, and stays
   2 units however the SVG is scaled. Dots wear their line's colour through currentColor. The
   goal is a dashed muted rule. The axis is an HTML row of fine labels under the SVG, spread
   from end to end. */
.plot              { display:block; width:100%; height:auto; overflow:visible; color:var(--ser1); }
.plot__line        { fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round;
                     stroke-linejoin:round; vector-effect:non-scaling-stroke; }
.plot__line--ser2  { color:var(--ser2); }
.plot__line--ser3  { color:var(--ser3); }
.plot__line--ser4  { color:var(--ser4); }
.plot__dot         { fill:currentColor; stroke:none; }
.plot__goal        { fill:none; stroke:var(--mut); stroke-width:1; stroke-dasharray:4 4;
                     vector-effect:non-scaling-stroke; }
.plot__axis        { display:flex; justify-content:space-between; gap:var(--s2); margin-top:var(--s1);
                     font-size:var(--fs-fine); color:var(--mut); font-variant-numeric:tabular-nums; }

/* A tab bar whose items put the icon over the word. Four words side by side do not fit a 390px
   bar beside the round act; four words each under an icon do. */
.tabbar--stacked .tabbar__item { display:flex; flex-direction:column; align-items:center;
                                 justify-content:center; gap:2px; }

/* Drawn, holding its place, not shown: an app's frame while it waits for its first answers, so
   nothing jumps when they arrive. Unlike display:none the space is kept; unlike opacity:0 nothing
   in it can be reached by a keyboard or read by a screen reader. */
.veiled { visibility:hidden; }
