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

   The shadcn/ui look, in this house's vocabulary: a neutral ground, one teal
   accent, Geist for reading and Geist Mono for anything to be read exactly;
   soft corners on a single radius scale, hairline borders, and quiet layered
   shadows that lift a surface rather than cast it. Same class names, same
   tags, same API as /v1/ — the dress changed, the words did not.

   /v1/ was the neo-brutalist dress — cream paper, burnt orange, monospace and
   hard offset shadows. It is still served, still keeps its promise, and no
   longer moves. Moving from it is changing two URLs.

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

     <link rel="stylesheet" href="https://orno.meliorex.dev/v2/orno.css">
     <script src="https://orno.meliorex.dev/v2/orno-theme.js"></script>   <!-- NOT deferred -->
     <script src="https://orno.meliorex.dev/v2/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/v2/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:

     /v2/         the moving edge. Never changes meaning — additions may come,
                  removals may not, and whatever would move your layout arrives
                  as /v3/. Cached five minutes. Cannot be hashed, because bytes
                  that change by design carry no integrity hash.
     /v2.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 /v2/ 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. `v2` is the moving edge: always current, never
     pinnable, no integrity hash possible. A pinned copy at /v2.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: '2.1.0';

  /* 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 shadcn's — text-xs is 12, text-sm is 14, and almost everything an app shows is one of
     the two. A proportional face reads larger than a monospace at the same size, which is why
     the small end could move up without the page growing. */
  --fs-nano: 11px; --fs-caps: 11px; --fs-micro: 12px; --fs-fine: 12px;
  --fs-note: 12px; --fs-ui: 13px; --fs-text: 14px; --fs-body: 14px;

  /* The display end, named after what wears it. */
  --fs-lead: 15px;    /* the line under a door title */
  --fs-sub: 20px;     /* 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: 30px;   /* a page title */
  --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: 10px;
  --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. Black at a low alpha in both themes: on a
     dark ground a shadow is barely there, which is exactly how shadcn draws it — the border
     does the separating at night. */
  --sh-btn: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --sh-card: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
  --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;
  }

  /* 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;

  /* ---- the palette ----
     shadcn's neutral base, declared once for both themes. --bg is the page, --sf a card on it;
     --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 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 — a card, a menu, a dialog. */
  --bg:   light-dark(#FAFAFA, #0A0A0A);
  --sf:   light-dark(#FFFFFF, #171717);
  --bd:   light-dark(#8F8F8F, #737373);
  --bd2:  light-dark(#E5E5E5, #2A2A2A);
  --tx:   light-dark(#0A0A0A, #FAFAFA);
  --mut:  light-dark(#6B6B6B, #A3A3A3);
  --edge: light-dark(#000000, #000000);
  --line: light-dark(#E5E5E5, #2E2E2E);
  --acc:  light-dark(var(--accBase), var(--accBaseDark));
  --gain: light-dark(#15803D, #4ADE80);
  --loss: light-dark(#B91C1C, #F87171);
  --warn: light-dark(#A16207, #FACC15);

  /* ---- 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);
}

/* 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. Every one of these is a line /v2 gets to delete. */
[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-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));
}

orno-badge { display:inline-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;
}
.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; }
.in       { font:inherit; font-size:var(--fs-text); width:100%; min-height:36px; padding:7px 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; }
.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-card); }
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-card); }

/* ---- 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);
}
/* 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); }

/* 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. */
.in:disabled {
  color:var(--mut); background: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:1px solid var(--line); border-radius:var(--radius-xl);
               background:var(--sf); overflow:hidden; box-shadow:var(--sh-card); }
.panel-plain { margin-top:14px; border:1px solid var(--bd2); border-radius:var(--radius-lg);
               background:var(--bg); overflow:hidden; }
/* 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; }
/* 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 appears on interaction, so it is raised a little above the page; --danger tints the
   destructive ones. */
.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-card); }
.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 — shadcn's card. A hairline, the xl
   corner, and the smallest shadow that still lifts it off the page. */
.card        { border:1px solid var(--line); border-radius:var(--radius-xl); background:var(--sf);
               color:var(--tx); padding:20px 24px; 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); }

/* 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: shadcn's progress — a pill track in the accent's own tint. */
.meter      { height:8px; border-radius:9999px; overflow:hidden;
              background:color-mix(in oklab, var(--acc) 18%, transparent); }
.meter-fill { height:8px; border-radius:9999px; background:var(--acc);
              transition:width var(--t-mid); }
/* 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); }

/* ---- 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: shadcn's login card — bigger than .card, and 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:1px solid var(--line); border-radius:var(--radius-xl);
             box-shadow: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 select in the form's dress; the arrow stays the browser's own. */
select.in { appearance:auto; -webkit-appearance:auto; cursor:pointer; }

/* 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:11px 16px; }
.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-card);
  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; }

/* 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); }

/* 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));
}
.shell__main { flex:1; min-width:0; max-width:1180px; width:100%; margin:0 auto;
               padding:34px 28px 90px; }

@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; }
  .shell__main { padding:22px max(14px, env(safe-area-inset-left))
                 calc(84px + env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-right)); }
}

/* 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 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:1px solid var(--bd2); border-radius:var(--radius-lg); background:var(--sf); overflow:hidden; }
.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. */
.drawer-backdrop {
  position:fixed; inset:0; z-index:var(--z-dialog);
  background:color-mix(in oklab, var(--edge) 50%, transparent);
}
.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; }
.drawer--left   { top:0; bottom:0; left:0;  width:min(420px, 92vw); border-left:none; }
.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); }
.drawer__head {
  display:flex; align-items:center; gap:var(--s3);
  border-bottom:1px solid var(--bd2); padding:var(--s4) var(--s5);
}
.drawer__body { padding:var(--s4) var(--s5); }
/* 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:1px solid var(--bd2); border-radius:var(--radius-lg); background:var(--sf); }
.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:10px 14px;
}
.tbl tbody td { border-top:1px solid var(--bd2); padding:10px 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); } }
.tbl__sort[aria-sort="ascending"], .tbl__sort[aria-sort="descending"] { 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; }

/* 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)); }
/* One child pushed to the bottom — a card's actions, a rail's foot. */
.stack > .push { margin-top:auto; }

/* 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)); }

/* A column of running text at a width the eye can follow back. */
.measure { max-width:var(--measure); }
