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

   Neo-brutalist, retro-technical: cream paper, charcoal ink, one burnt-orange
   accent; monospace body, condensed black caps for display; sharp corners and
   hard offset shadows on whatever is primary. A pressed button trades its
   shadow for the distance it travelled.

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

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

   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:

     /v1/         the moving edge. Never changes meaning — additions may come,
                  removals may not, and whatever would move your layout arrives
                  as /v2/. Cached five minutes. Cannot be hashed, because bytes
                  that change by design carry no integrity hash.
     /v1.2.3/     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 /v1/ 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.

   It moves layouts, which /v1 promises not to do, and that promise is kept where it matters:
   the only consumer today pins a frozen release, and frozen releases never change. */
*, *::before, *::after { box-sizing: border-box; }

:root {
  color-scheme: light dark;

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

  --font-mono: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-display: 'Archivo Black', Impact, 'Arial Black', sans-serif;

  /* 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, the
     nav item you are standing on. It was hard-coded to the paper colour, which happens to work
     for a burnt orange and fails for half of the colours a team would actually choose — paper
     on a typical amber is 2.0:1, on a lime 1.9:1. Whichever of paper or ink clears 4.5:1
     against your accent is the right value here; `npm run contrast` will tell you. */
  --accBase: #B5451B;
  --accBaseDark: #DD6530;
  /* Paper by day, ink by night — and that is the whole reason this is a token rather than a
     constant. The dark accent is deliberately brighter, because --acc has to work as link text
     on a dark ground; paper on top of that brighter orange is 3.3:1, which is not enough for
     the label of a button. The answer genuinely differs between the two themes. */
  --acc-ink: light-dark(#FAF8F3, #0A0908);

  /* The spacing scale. Components still carry pixel values of their own that do
     not sit on it (9, 10, 11, 13, 14…), which is why `compact` below has to
     retune a handful of them by hand. Putting every component on the scale
     would move layouts, so it waits for /v2 — see "Known debts" in the book. */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 22px; --s6: 32px;

  /* The type scale, reading end. Ten steps inside four pixels is more steps than
     anyone can tell apart; the names are what /v1 published, so the values stay
     exactly where they were, and /v2 collapses them. --fs-nano, --fs-caps and
     --fs-text name three sizes that were only ever literals. */
  --fs-nano: 10px; --fs-caps: 10.5px; --fs-micro: 11px; --fs-fine: 11.5px;
  --fs-note: 12px; --fs-ui: 12.5px; --fs-text: 13px; --fs-body: 13.5px;

  /* The display end, and the reason it exists. The scale used to stop at 13.5 and
     start again at 26, and seven rules lived in that gap as bare numbers — 14,
     15.5, 16, 17, 18, 22 and a clamp — so "every size comes from the scale" was
     true of the small end only. They are named after what wears them rather than
     after a rank, because that is honestly what they are: seven decisions taken
     one at a time, not a ratio. Their values are unchanged, because changing one
     would move a layout and that is what /v2 is for. */
  --fs-lead: 14px;    /* the line under a door title */
  --fs-sub: 15.5px;   /* a stat's figure */
  --fs-mark: 16px;    /* a large avatar's initials, a section head */
  --fs-brand: 17px;   /* the wordmark beside a logo */
  --fs-act: 18px;     /* the one round act on a tab bar */
  --fs-card: 22px;    /* a door card's title */
  --fs-title: 26px;   /* a page title */
  --fs-hero: clamp(26px, 3.4vw, 40px);  /* the door's own headline */

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

  /* The offset shadow, by the weight of what casts it. */
  --sh-btn: 3px; --sh-card: 4px; --sh-menu: 5px; --sh-dialog: 6px; --sh-door: 8px;

  /* 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. 32 is comfortable under a mouse, which is what this number was
     chosen for; it is not comfortable under a finger, and the calendar is forty-two of them in
     a 262px popup — the one place in the system where the target IS the whole control. Apple
     asks 44 and Android 48; 44 keeps a seven-column month inside a 390px screen with room to
     spare. Anything already larger is unaffected: 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 ----
     Declared once, both themes at once. --edge is the hard offset shadow;
     --line the silhouette of an emphasized block. On paper both are ink; at
     night the shadow stays black, but a block needs a light line or it melts
     into the ground.

     --bd is the visible boundary of every control, so it clears 3:1 against
     both grounds (WCAG 1.4.11). --bd2 only rules one row off from the next,
     which is decoration, and stays quiet. */
  --bg:   light-dark(#F0EDE5, #1C1916);
  --sf:   light-dark(#FAF8F3, #262219);
  --bd:   light-dark(#94876F, #766B55);
  --bd2:  light-dark(#DDD7CA, #37312A);
  --tx:   light-dark(#221F1A, #F0EAE0);
  --mut:  light-dark(#655E50, #A49B8A);
  --edge: light-dark(#26221C, #0A0908);
  --line: light-dark(#26221C, #C9C0B0);
  --acc:  light-dark(var(--accBase), var(--accBaseDark));
  --gain: light-dark(#2E7443, #7FBE8E);
  --loss: light-dark(#B02323, #E0705C);
  --warn: light-dark(#8A5F0C, #D9A84E);
}

/* 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: 16px; --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; }

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-mono); font-size:var(--fs-body); line-height:1.5;
}

/* Sharp corners are the system: every stray radius is flattened here rather than policed in a
   hundred class definitions, and that includes the corners a browser puts on native controls.

   It is a wide rule, so it comes with two ways out — because a design system that cannot be
   escaped is one teams work around instead of with:

     .round              this one thing keeps its corners (an avatar, a logo, a map pin)
     [data-orno-ignore]  nothing inside is touched — fence off an embedded third-party widget

   Three :not()s give it the weight of three classes, which is enough to beat ordinary
   component CSS without !important — so an app that really means it can still win. Two things
   are exempt by name, for the same reason: a spinner only reads as a spinner while it is
   round, and a radio only reads as one-of-many while it is. */
*:not(.round):not(.spin):not(.radio):not([data-orno-ignore]):not([data-orno-ignore] *),
*:not(.round):not(.radio)::before,
*:not(.round):not(.radio)::after { border-radius:0; }

/* Headings wear the display face; their sizes stay their own. */
/* A bare h1 wears the display face. 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:400;
  text-transform:uppercase;
  letter-spacing:0.01em;
}

a { color:var(--acc); }
@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 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:2px 2px 0 var(--acc); }

@media (hover: hover) {
  .btn:hover:not(:disabled)        { border-color:var(--tx); }
  .btn-acc:hover:not(:disabled)    { opacity:0.9; }
  .btn-ghost:hover:not(:disabled)  { border-color:var(--tx); }
  .btn-quiet:hover:not(:disabled)  { color:var(--tx); }
  .btn-danger:hover:not(:disabled) { background:color-mix(in oklab, var(--loss) 8%, transparent); }
  .btn-link:hover, .btn-link-sm:hover { color: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 same border-darkens-to-ink the buttons use. */
  .in:hover:not(:disabled):not(:focus)     { border-color:var(--tx); }
  .check:hover:not(:disabled):not(:checked),
  .radio:hover:not(:disabled):not(:checked),
  .switch:hover:not(:disabled):not(:checked) { border-color:var(--tx); }
  /* 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(--tx); }
  .btn-icon:hover:not(:disabled)   { border-color:var(--tx); 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         { border-color:var(--tx); }
  .settings-btn:hover       { color:var(--tx); }
  .pick:not(.on):hover      { border-color:var(--tx); }
  .pick-dash:not(.on):hover { border-color:var(--tx); }
  .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:2px 2px 0 var(--acc) !important; }
.scp5:focus { border-color:var(--acc) !important; outline:none !important; box-shadow:2px 2px 0 var(--acc) !important; }

/* ---- pressed ----

   "A pressed button trades its shadow for the distance it travelled" is the first thing this
   stylesheet says about interaction, and for the whole life of the system three surfaces did
   it: the accent button, the door's close and the tab bar's round act. Everything else you
   press answered nothing — you pushed it and the screen did not agree that you had.

   There are two kinds of surface here, so there are two answers. Anything casting the offset
   shadow spends it on the distance, which is the rule as written. Anything flat takes the press
   into the paper instead: a hint of ink under the label, gone the moment you let go. Anything
   already filled with the accent has no room for ink, so it dims the way its hover does.

   None of the three moves a layout — a transform, a background and an opacity are all outside
   the box — so all three are additions /v1 is allowed to make. 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. */

/* It casts a shadow, so it can spend one. */
.nav-item.on:active,
.pick.on:active { transform:translate(var(--sh-btn),var(--sh-btn)); box-shadow:none; }

/* It is filled with the accent: no room for ink, so it dims. */
.seg.on:active, .tabbar__item.on:active { opacity:0.9; }

/* It is flat: the press goes into the paper. */
.btn:active:not(:disabled) { background:color-mix(in oklab, var(--tx) 10%, 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) { background:color-mix(in oklab, var(--loss) 16%, transparent); }

/* 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:1px solid var(--edge);
  box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); padding:10px 14px;
  font-size:var(--fs-fine); font-weight:700; letter-spacing:0.06em; text-transform:uppercase;
  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:700; text-transform:uppercase; letter-spacing:0.04em; }
.h-sub   { font-size:var(--fs-note); color:var(--mut); margin-top:2px; }
.h2      { font-size:var(--fs-ui); font-weight:700; margin:0 0 12px; letter-spacing:0.06em; text-transform:uppercase; }
.h2-tight{ font-size:var(--fs-ui); font-weight:700; margin:0 0 4px; letter-spacing:0.06em; text-transform:uppercase; }
.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); letter-spacing:0.09em; text-transform:uppercase; }
.num     { font-variant-numeric:tabular-nums; }
.b       { font-weight:700; }

/* 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.015em;
              font-family:var(--font-display); font-weight:400; text-transform:uppercase; }
.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:700; }
.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-caps); font-weight:700; margin-bottom:7px;
            letter-spacing:0.09em; text-transform:uppercase; }
.in       { font:inherit; font-size:var(--fs-text); width:100%; padding:9px 11px;
            border:1px solid var(--bd); background:var(--sf); color:var(--tx); }
.in-num   { text-align:end; font-variant-numeric:tabular-nums; }
/* The door's big field: ink border and the accent's own offset shadow. */
.in--door { padding:12px 13px; border-color:var(--edge); background:var(--bg);
            box-shadow:4px 4px 0 var(--acc); }
.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:2px 2px 0 var(--loss); }
.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:2px; background:var(--bd); }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance:none; width:13px; height:13px; background:var(--acc); margin-top:-6px; border:1px solid var(--line); }
input[type="range"]::-moz-range-track { height:2px; background:var(--bd); }
input[type="range"]::-moz-range-thumb { width:11px; height:11px; background:var(--acc); border:1px solid var(--line); }

/* ---- buttons ----

   One act, seven dresses. Everything they share is said once here; each class below says only
   what makes it itself. The names are exactly what /v1 published and nothing computes
   differently — this is the same wardrobe with the seams taken in, so a change to the act's
   type or weight is one edit rather than seven. */

.btn, .btn-acc, .btn-ghost, .btn-quiet, .btn-danger, .btn-link, .btn-link-sm, .btn-icon {
  font:inherit; cursor:pointer;
}
/* All but the icon, which carries a glyph and no caps machinery. */
.btn, .btn-acc, .btn-ghost, .btn-quiet, .btn-danger, .btn-link, .btn-link-sm {
  text-transform:uppercase;
}
/* The five that are shaped like a button. 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 {
  font-size:var(--fs-fine); font-weight:700; letter-spacing:0.06em; padding:8px 14px;
  text-decoration:none;
}
/* The two that are shaped like a word. */
.btn-link, .btn-link-sm {
  font-weight:700; letter-spacing:0.05em; border:none; background:transparent; color:var(--acc);
}

.btn        { border:1px solid var(--bd);   background:var(--sf);     color:var(--tx); }
.btn-ghost  { border:1px solid var(--bd);   background:transparent;   color:var(--tx); padding:8px 16px; }
.btn-quiet  { border:none;                  background:transparent;   color:var(--mut); padding:8px 10px;
              font-weight:400; }
/* The primary act, and the only one that casts a shadow: pressing it trades the shadow for
   the distance it travelled. */
.btn-acc    { border:1px solid var(--line); background:var(--acc);     color:var(--acc-ink);
              box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); }
.btn-acc:active { transform:translate(var(--sh-btn),var(--sh-btn)); box-shadow:none; }
/* The dangerous act: bordered in loss red, never solid — destruction should not look primary. */
.btn-danger { border:1px solid var(--loss); background:transparent;   color:var(--loss); }

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

/* A square icon affordance: one glyph, no caps machinery. The box stays 30×26 so no row
   moves; the thumb gets --hit anyway, because a hit area costs nothing and growing the button
   itself would have shifted every toolbar in the house. */
.btn-icon   { position:relative; display:inline-flex; align-items:center; justify-content:center;
              width:30px; height:26px; padding:0; flex:none;
              border:1px solid var(--bd); background:transparent; color:var(--mut); }
.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, the card's shadow. */
.btn--wide      { display:flex; align-items:center; justify-content:center; gap:10px;
                  width:100%; padding:12px 14px; font-size:var(--fs-ui); letter-spacing:0.08em; }
.btn--wide:disabled { color:var(--mut); cursor:default; border-style:dashed; }
.btn-acc--wide  { display:block; width:100%; padding:13px 14px; font-size:var(--fs-ui);
                  letter-spacing:0.08em; box-shadow:var(--sh-card) var(--sh-card) 0 var(--edge); }
.btn-acc--wide:active { transform:translate(var(--sh-card),var(--sh-card)); box-shadow:none; }

/* 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: muted, dashed, shadowless, unmoved. */
/* Off, and saying so. Muted ink, no shadow, no cursor that promises anything — and for
   anything that carries a border, the border goes dashed, which is the house's word for "this
   is a shape, not an offer". The second rule is for the surfaces drawn without one: a dashed
   border on something that never had a border is a border appearing on disable.

   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 {
  color:var(--mut); background:transparent; border-color:var(--bd);
  border-style:dashed; box-shadow:none; cursor:default; transform:none;
}
/* Drawn without a border, so they stay without one. */
.btn-quiet:disabled, .btn-link:disabled, .btn-link-sm:disabled,
.opt:disabled, .tabs__tab:disabled, .tabbar__item:disabled,
.settings-btn:disabled, .fold-list__head:disabled { border:none; }

/* And the two that were ALREADY muted ink at rest, so muting them changed nothing: switched
   off, they looked exactly like switched on. The states gallery is what showed it — two tiles
   side by side, one labelled default and one labelled disabled, and no way to tell which was
   which. Half opacity is what the tick, the radio and the switch already use for the same
   problem, so it is the answer the system already had. */
.btn-quiet:disabled, .settings-btn:disabled { opacity:0.5; }

/* A field had no disabled look at all — not a muted one, not a dashed one, nothing. The check
   had been counting `:hover:not(:disabled)` as if it styled the state it excludes. */
.in:disabled {
  color:var(--mut); background:transparent; border-style:dashed; cursor:default;
}

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

.grow        { flex:1; }
.row         { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; }
.panel       { position:relative; border:1px solid var(--line); background:var(--sf); overflow:hidden;
               box-shadow:var(--sh-card) var(--sh-card) 0 var(--edge); }
.panel-plain { margin-top:14px; border:1px solid var(--bd2); background:var(--bg); overflow:hidden; }
.badge       { font-size:var(--fs-nano); color:var(--mut); border:1px solid var(--bd); padding:2px 7px;
               text-transform:uppercase; letter-spacing:0.07em; font-weight:700; }
/* The quiet badge keeps its sentence case: a label, not a shout. */
.badge--quiet{ text-transform:none; letter-spacing:0.02em; font-weight:400; font-size:var(--fs-caps); }
/* A solid ink-on-orange chip: the loudest label in the system. */
.chip        { display:inline-block; background:var(--acc); color:var(--acc-ink); font-size:var(--fs-caps);
               font-weight:700; letter-spacing:0.08em; text-transform:uppercase; padding:5px 11px; }

/* A fold: the panel that unfolds under a row for a small task — a confirm, a mini-form.
   It appears above the page, so it carries the hard shadow, like every surface that shows
   up on interaction; --danger tints the destructive ones. */
.fold          { border:1px solid var(--line); background:var(--sf); padding:14px 16px;
                 margin:10px 4px 4px 0; box-shadow:var(--sh-card) var(--sh-card) 0 var(--edge); }
.fold--danger  { border-color:var(--loss); }
.fold__actions { display:flex; align-items:center; gap:12px; margin-top:12px; flex-wrap:wrap; }

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

/* Corner brackets: the registration marks that frame a major panel. */
.frame          { position:relative; }
.frame::before  { content:''; position:absolute; top:5px; left:5px; width:11px; height:11px;
                  border-top:1px solid var(--tx); border-left:1px solid var(--tx); pointer-events:none; }
.frame::after   { content:''; position:absolute; bottom:5px; right:5px; width:11px; height:11px;
                  border-bottom:1px solid var(--tx); border-right:1px solid var(--tx); pointer-events:none; }

/* A card: every standing surface wears the same dress — silhouette line and the 4px shadow. */
.card        { border:1px solid var(--line); background:var(--sf); padding:16px 18px;
               box-shadow:var(--sh-card) var(--sh-card) 0 var(--edge); }
.card__title { margin:0; font-size:var(--fs-micro); font-weight:450; letter-spacing:0.05em;
               text-transform:uppercase; color:var(--mut); }

/* A dashed invitation: the empty state every list shares. */
/* A sentence and, under it, the way out.
   
   It was text-align:center and nothing else, so an empty state with an act in it put the act on
   the same LINE as the sentence — "No accounts yet Connect a bank" — which reads as one run of
   words with a button wedged in the middle of it. A grid stacks them and centres both, and for
   an empty state that is only a sentence nothing moves at all. */
.empty-state { border:1px dashed var(--bd); padding:34px 20px; text-align:center;
               display:grid; gap:var(--s4); justify-items:center;
               font-size:var(--fs-ui); 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: the track, and a fill whose width the template binds. */
.meter      { height:4px; background:var(--bd2); }
.meter-fill { height:4px; background:var(--acc); }
/* 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:9px; height:9px; }
/* A segmented choice: one segment on, the rest quiet. */
.seg    { font:inherit; font-size:var(--fs-micro); padding:7px 16px; border:1px solid transparent;
          background:transparent; color:var(--mut); font-weight:700; cursor:pointer;
          text-transform:uppercase; letter-spacing:0.06em;
          transition:background 150ms, color 150ms; }
.seg.on { background:var(--acc); color:var(--acc-ink); border-color:var(--line); }

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

/* The step rail: a bar per step, lit up to where you are. */
.rail-bar    { display:block; height:4px; 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-caps); font-variant-numeric:tabular-nums; color:var(--mut); font-weight:700; }
.rail-no.on  { color:var(--acc); }
.rail-lbl    { font-size:var(--fs-micro); color:var(--mut); font-weight:700; text-transform:uppercase; letter-spacing:0.05em; }
.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. */
.pick         { font:inherit; text-align:start; width:100%; border:1px solid var(--bd);
                background:var(--sf); padding:14px 16px; cursor:pointer; color:var(--tx); }
.pick.on      { border-color:var(--line); background:color-mix(in oklab, var(--acc) 10%, var(--sf));
                box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); }
.pick-dash    { font:inherit; text-align:start; width:100%; border:1px dashed var(--bd);
                background:transparent; padding:14px 16px; cursor:pointer; color:var(--tx); }
.pick-dash.on { border-color:var(--line); background:color-mix(in oklab, var(--acc) 10%, var(--sf)); }
/* DEPRECATED — leaves with /v2. Use .btn-acc plus the real `disabled` attribute.
   These looked disabled without being disabled, so a screen reader was told nothing and the
   grey-on-grey text sat below the contrast bar. Kept alive so nothing breaks today. */
.next-btn       { font:inherit; font-size:var(--fs-fine); padding:10px 22px; border:1px solid var(--bd);
                  background:transparent; color:var(--mut); cursor:not-allowed;
                  font-weight:700; text-transform:uppercase; letter-spacing:0.06em; opacity:0.55; }
.next-btn.ready { border-color:var(--line); background:var(--acc); color:var(--acc-ink); cursor:pointer; opacity:1;
                  box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); }
.next-btn.ready:active { transform:translate(var(--sh-btn),var(--sh-btn)); box-shadow:none; }
.save-btn       { font:inherit; font-size:var(--fs-note); padding:12px 24px; border:1px solid var(--bd);
                  background:transparent; color:var(--mut); cursor:not-allowed; font-weight:700;
                  text-transform:uppercase; letter-spacing:0.06em; }
.save-btn.ready { border-color:var(--line); background:var(--acc); color:var(--acc-ink); cursor:pointer;
                  box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); }
.save-btn.ready:active { transform:translate(var(--sh-btn),var(--sh-btn)); box-shadow:none; }

/* ---- the one round thing ---- */

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

/* ==========================================================================
   The door — the sign-in storefront Servo wears; any app with a front step
   can wear it too. Pitch left, bracketed 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:400;
               font-size:var(--fs-hero); line-height:1.12;
               letter-spacing:-0.01em; 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:14px; font-weight:700; font-size:var(--fs-brand);
               letter-spacing:0.1em; text-transform:uppercase; }
.brand__mark { display:grid; place-items:center; width:44px; height:44px; background:var(--acc);
               border:2px solid var(--edge); box-shadow:4px 0 0 var(--edge); 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:700; font-size:var(--fs-text); letter-spacing:0.03em; text-transform:uppercase; }
.sell__note  { display:block; color:var(--mut); font-size:var(--fs-ui); margin-top:2px; }

/* The door's card: bigger than .card, the 8px shadow, and four corner brackets. */
.door-card { position:relative; width:min(430px,100%); background:var(--sf);
             border:1px solid var(--edge); box-shadow:var(--sh-door) var(--sh-door) 0 var(--edge);
             padding:34px 36px 30px; }
.door-card__corners, .door-card__corners::before, .door-card__corners::after,
.door-card::before, .door-card::after { position:absolute; pointer-events:none; }
.door-card__corners { inset:8px; }
.door-card__corners::before, .door-card__corners::after,
.door-card::before, .door-card::after { content:''; width:14px; height:14px; }
.door-card__corners::before { top:0; left:0; border-top:2px solid var(--bd); border-left:2px solid var(--bd); }
.door-card__corners::after  { bottom:0; right:0; border-bottom:2px solid var(--bd); border-right:2px solid var(--bd); }
.door-card::before { top:8px; right:8px; border-top:2px solid var(--bd); border-right:2px solid var(--bd); }
.door-card::after  { bottom:8px; left:8px; border-bottom:2px solid var(--bd); border-left:2px solid var(--bd); }
.door-card__title { margin:0; text-align:center; font-family:var(--font-display); font-weight:400;
                    font-size:var(--fs-card); letter-spacing:0.02em; text-transform:uppercase; }
.door-card__sub   { margin:8px 0 0; text-align:center; color:var(--mut); font-size:var(--fs-ui); }
.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-ui); }
.door-card .lbl   { font-size:var(--fs-note); margin-bottom:8px; letter-spacing:0.06em; }
.door-card .btn-acc--wide { margin-top:26px; }
.door-card .btn--wide     { margin-top:14px; }

/* The version line under the card. */
.stamp { color:var(--mut); font-size:var(--fs-fine); letter-spacing:0.14em; text-transform:uppercase; }

/* 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:26px 22px 24px; }
}

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

/* One rule for the whole index: the orange block means "you are here" — nothing else wears it,
   and everything not chosen is plain ink. Inset by its own margin rather than by the rail's
   padding, so the chosen block's shadow has somewhere to fall. */
.nav-item        { font:inherit; display:flex; align-items:baseline; gap:8px; text-align:start;
                   width:calc(100% - 20px); margin-inline:10px;
                   font-size:var(--fs-micro); padding:7px 9px; border:1px solid transparent;
                   background:transparent; color:var(--tx); font-weight:700;
                   text-transform:uppercase; letter-spacing:0.05em;
                   cursor:pointer; transition:background var(--t-fast), color var(--t-fast),
                                              box-shadow var(--t-fast); }
.nav-item.on     { border-color:var(--line); background:var(--acc); color:var(--acc-ink);
                   box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); }
/* A chapter under a chapter: further in, and quieter. */
.nav-item--child { padding-inline-start:22px; font-size:var(--fs-caps); }
.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-ink); }
.close-btn       { font:inherit; font-size:var(--fs-fine); padding:10px 12px; border:1px solid var(--line);
                   background:var(--acc); color:var(--acc-ink); cursor:pointer; font-weight:700; text-align:start;
                   text-transform:uppercase; letter-spacing:0.05em; box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); }
.close-btn:active { transform:translate(var(--sh-btn),var(--sh-btn)); box-shadow:none; }
.close-btn.on    { background:var(--tx); border-color:var(--tx); color:var(--bg); }
.settings-btn    { font:inherit; font-size:var(--fs-micro); border:none; background:transparent; color:var(--mut);
                   font-weight:700; cursor:pointer; padding:0 2px; text-align:start;
                   text-transform:uppercase; letter-spacing:0.07em; }
.settings-btn.on { color:var(--acc); }
.switch-btn      { font:inherit; display:inline-flex; align-items:center; gap:10px; font-size:var(--fs-ui);
                   padding:6px 12px; border:1px solid var(--bd); background:var(--sf);
                   color:var(--tx); cursor:pointer; }
.switch-btn.open { border-color:var(--acc); }

/* ==========================================================================
   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:700; 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 — sheet scale, 6px. */
.dialog-backdrop { position:fixed; inset:0; z-index:var(--z-dialog); display:grid; place-items:center;
                   padding:20px; background:color-mix(in oklab, var(--edge) 45%, transparent); }
.dialog          { background:var(--sf); border:1px solid var(--line); padding:22px 24px;
                   width:100%; max-width:480px; box-shadow:var(--sh-dialog) var(--sh-dialog) 0 var(--edge); }
.dialog__actions { display:flex; align-items:center; gap:12px; margin-top:18px; 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); box-shadow:var(--sh-menu) var(--sh-menu) 0 var(--edge);
         padding:12px 16px; font-size:var(--fs-ui); }

/* ==========================================================================
   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 var(--warn);
          background:color-mix(in oklab, var(--warn) 7%, var(--sf));
          padding:var(--s3) var(--s4); font-size:var(--fs-ui); }
.banner--error  { border-color:var(--loss); 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:var(--gain); background:color-mix(in oklab, var(--gain) 7%, var(--sf)); }
.banner__icon   { font-weight:700; 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 caps, which is how every link in this system reads. */
.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(--line);
          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:50px; border:none; border-top:3px solid transparent;
                background:transparent; color:var(--mut); font-size:var(--fs-nano); font-weight:700;
                text-transform:uppercase; letter-spacing:0.07em; cursor:pointer; padding:6px 2px; }
.tabbar__item.on { color:var(--acc); border-top-color:var(--acc); }
.tabbar__act  { font:inherit; flex:none; width:48px; height:48px; margin-top:-16px;
                border:1px solid var(--line); background:var(--acc); color:var(--acc-ink);
                font-size:var(--fs-act); font-weight:700; cursor:pointer; box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); }
.tabbar__act:active { transform:translate(var(--sh-btn),var(--sh-btn)); box-shadow:none; }

/* 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-caps); color:var(--mut); letter-spacing:0.09em; text-transform:uppercase;
                 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:700; }
.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:var(--bd2); animation:ornoPulse 1.2s ease-in-out infinite; }
.skeleton--line { height:12px; }
@keyframes ornoPulse { 50% { opacity:0.45; } }

/* ==========================================================================
   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); background:var(--sf); cursor:pointer;
  display:inline-grid; place-content:center;
  transition:background var(--t-fast), border-color var(--t-fast);
}
.check:checked { background:var(--acc); border-color:var(--line); }
.check:checked::after {
  content:''; width:4px; height:8px; margin-top:-2px;
  border:solid var(--sf); border-width:0 2px 2px 0; transform:rotate(45deg);
}
.check:indeterminate { background:var(--acc); border-color:var(--line); }
.check:indeterminate::after {
  content:''; width:8px; height:2px; margin:0; border:none; background:var(--sf); transform:none;
}

/* One of many: the single round thing besides the spinner. */
.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;
  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. */
.switch {
  appearance:none; -webkit-appearance:none;
  flex:none; position:relative; width:34px; height:18px; margin:0;
  border:1px solid var(--bd); background:var(--sf); cursor:pointer;
  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:2px; bottom:2px; left:2px; width:12px;
  /* --bd, not --mut. Off used to be a near-ink knob (#655E50) on a pale track while on was a
     rust knob on an 18% rust tint — so the OFF state carried more contrast than the on one, and
     a control whose loudest reading is "no" is a control read backwards. */
  background:var(--bd); 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. That is the way round every named system draws it, and the way round a person can
   tell across a room. */
.switch:checked { background:var(--acc); border-color:var(--acc); }
/* 16px, and the arithmetic is worth writing down because the old value was `calc(100% - 16px)`
   — and a percentage in `translate` is a percentage of the ELEMENT, not of the track. The knob
   is 12px, so that resolved to −4px and the switch moved its knob four pixels to the LEFT when
   you turned it on. The track is 34 wide, less 2 of border, less the 2px inset at each end,
   less the 12px knob: 16 is the whole of the travel. */
.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-caps); font-weight:700;
                    letter-spacing:0.09em; text-transform:uppercase; }
.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; letter-spacing:0.04em; text-transform:none; 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 var(--loss); background:color-mix(in oklab, var(--loss) 6%, var(--sf));
  box-shadow:var(--sh-card) var(--sh-card) 0 var(--edge); padding:var(--s4) var(--s5);
}
.summary__title { font-size:var(--fs-text); font-weight:700; text-transform:uppercase; letter-spacing:0.04em; }
.summary__list  { margin:10px 0 0; padding-inline-start:18px; display:grid; gap:4px; }
.summary__link  { font:inherit; font-size:var(--fs-ui); color:var(--loss); background:none;
                  border:none; padding:0; text-align:start; cursor:pointer; text-decoration:underline; }

/* ==========================================================================
   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 on a Mac it arrived rounded, translucent and set
   in the system sans — most of the Ground rules broken at once, 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); box-shadow:var(--sh-menu) var(--sh-menu) 0 var(--edge);
  padding:4px; margin:0; list-style:none;
}
.combo__list::-webkit-scrollbar { width:0; height:0; }
.combo__option {
  padding:8px 10px; font-size:var(--fs-text); cursor:pointer;
}
.combo__option[aria-selected="true"] { font-weight:700; }
/* 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) 12%, 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 arrives
 * rounded, translucent and set in the system sans, and 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);
       box-shadow:var(--sh-menu) var(--sh-menu) 0 var(--edge); 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-ui); font-weight:700; letter-spacing:0.04em;
              text-transform:uppercase; }
.cal__step { font:inherit; font-size:var(--fs-text); line-height:1; flex:none;
             width:var(--hit); height:var(--hit); border:1px solid var(--bd);
             background:var(--sf); color:var(--tx); cursor:pointer; }
.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-nano); text-transform:uppercase; color:var(--mut);
             text-align:center; padding-bottom:6px; }
.cal__day  { font:inherit; font-size:var(--fs-note); font-variant-numeric:tabular-nums;
             width:var(--hit); height:var(--hit); border:1px solid transparent;
             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 { border-color:var(--bd); font-weight:700; }
.cal__day--on    { background:var(--acc); color:var(--acc-ink); border-color:var(--line);
                   font-weight:700; }
@media (hover: hover) {
  .cal__step:hover:not(:disabled) { border-color:var(--tx); }
  .cal__day:hover:not(:disabled):not(.cal__day--on) {
    background:color-mix(in oklab, var(--tx) 8%, transparent);
  }
}
.cal__step:active {
  background:color-mix(in oklab, var(--tx) 8%, transparent);
}
.cal__step:disabled, .cal__day:disabled { color:var(--mut); cursor:default; }
/* 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);
}
.day__text { flex:1; min-width:0; }
.day__pick {
  font:inherit; flex:none; width:32px; padding:0; cursor:pointer;
  border:1px solid var(--bd); border-inline-start:none; background:var(--sf); color:var(--mut);
}
@media (hover: hover) { .day__pick:hover { color:var(--tx); border-color:var(--tx); } }
/* 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; }
/* 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.
 *
 * Caps and letter-spacing so EUR reads as a unit and not as a word somebody stopped typing. */
.amount__mark {
  font:inherit; font-size:var(--fs-caps); flex:none; display:flex; align-items:center;
  padding:0 11px; letter-spacing:0.08em; text-transform:uppercase;
  border:1px solid var(--bd); border-inline-start:none; background:var(--sf); color:var(--mut);
}
/* 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; }
.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); 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:700; }
.dropzone__note  { font-size:var(--fs-fine); color:var(--mut); max-width:42ch; }
.dropzone__cta   { font-size:var(--fs-fine); font-weight:700; color:var(--acc);
                   text-transform:uppercase; letter-spacing:0.06em; 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); background:var(--sf); padding:10px 14px;
}
.filebar + .filebar { border-top:none; }
.filebar__name { font-size:var(--fs-text); font-weight:700; 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 18px 16px;
  font-size:var(--fs-text); font-weight:700; letter-spacing:0.06em;
}
.shell__rail-heading { font-size:var(--fs-nano); color:var(--mut); letter-spacing:0.09em;
                       text-transform:uppercase; padding:16px 19px 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: LIGHT,
   DARK and SYSTEM are 110px of words and the padding is another 96, which is 217 in a column
   with 155 to spend — so the last segment was simply cut off, and the one that says SYSTEM is
   the one you cannot see. The control is the same control here; it wears the rail's width. */
.shell__rail-foot .seg { padding:7px 6px; font-size:var(--fs-caps); }
.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:450; 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); font-weight:600; border-bottom-color:var(--acc); }
@media (hover: hover) { .tabs__tab:not(.on):hover { color:var(--tx); } }

/* Where you are, and everything you came through. */
.trail { display:flex; align-items:baseline; gap:var(--s2); flex-wrap:wrap;
         font-size:var(--fs-fine); color:var(--mut); }
.trail__step { font:inherit; color:inherit; background:none; border:none; padding:0;
               cursor:pointer; text-decoration:underline; }
.trail__step:disabled, .trail__here { cursor:default; text-decoration:none; color:var(--tx); font-weight:700; }
.trail__sep { color:var(--bd); }

/* A list longer than a screen. */
.pager { display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap; }
.pager__page {
  font:inherit; font-size:var(--fs-fine); font-variant-numeric:tabular-nums;
  min-width:30px; padding:6px 8px; border:1px solid var(--bd);
  background:var(--sf); color:var(--tx); cursor:pointer;
}
.pager__page.on { background:var(--acc); color:var(--acc-ink); border-color:var(--line);
                  font-weight:700; box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); }
.pager__page:disabled { color:var(--mut); border-style:dashed; cursor:default; }
.pager__gap { color:var(--mut); font-size:var(--fs-fine); padding:0 2px; }
.pager__count { font-size:var(--fs-fine); 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-fine); font-weight:700; letter-spacing:0.06em; text-transform:uppercase;
  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); box-shadow:var(--sh-menu) var(--sh-menu) 0 var(--edge);
  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:5px 9px; box-shadow:var(--sh-btn) var(--sh-btn) 0 var(--edge); 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); background:var(--sf); }
.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:13px 16px; font-size:var(--fs-text); font-weight:700;
}
@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(--acc); font-size:var(--fs-micro); flex:none; transition:transform var(--t-fast); }
.fold-list__head.on .fold-list__mark { transform:rotate(90deg); }
.fold-list__body { padding:0 16px 14px; font-size:var(--fs-ui); }

/* 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) 45%, transparent);
}
.drawer {
  position:fixed; z-index:calc(var(--z-dialog) + 1);
  background:var(--sf); border:1px solid var(--line); box-shadow:var(--sh-dialog) var(--sh-dialog) 0 var(--edge);
  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;
                  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:38px; height:4px; background:var(--bd); margin:10px 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); background:var(--sf); }
.tbl {
  width:100%; border-collapse:collapse; font-size:var(--fs-ui);
  /* 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(--bd);
  font-size:var(--fs-caps); font-weight:700; letter-spacing:0.09em; text-transform:uppercase;
  color:var(--mut); text-align:start; white-space:nowrap;
  padding:9px 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(--bd); font-weight:700; background: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-caps); letter-spacing:0.09em; text-transform:uppercase; color:var(--mut);
  }
  .tbl--stack .tbl__num { justify-content:space-between; text-align:start; }
}

/* Who, in the space of a letter or two. Square, like everything else here. */
.avatar {
  display:inline-flex; align-items:center; justify-content:center; flex:none; overflow:hidden;
  width:32px; height:32px;
  background:var(--acc); color:var(--acc-ink); border:1px solid var(--line);
  font-size:var(--fs-fine); font-weight:700; letter-spacing:0.04em; 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); border-color:var(--bd); }
/* Several of them, overlapping, for a room rather than a person. */
.avatar-stack { display:inline-flex; }
.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-caps); letter-spacing:0.09em; text-transform:uppercase; color:var(--mut); padding-top:2px; }
.facts > dd { margin:0; font-size:var(--fs-ui); 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. Square: a round one would be
   the third exemption in a system that allows two. */
.pip { display:inline-block; width:8px; height:8px; flex:none; 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-fine); }

/* How many — on a tab, a nav item, an inbox. Never more than three glyphs. */
.count {
  display:inline-block; min-width:17px; padding:1px 5px; text-align:center;
  background:var(--acc); color:var(--acc-ink); border:1px solid var(--line);
  font-size:var(--fs-nano); font-weight:700; font-variant-numeric:tabular-nums; line-height:1.5;
}
.count--quiet { background:var(--bg); color:var(--mut); border-color:var(--bd); }

/* Something to be read exactly, and usually copied. */
.code {
  position:relative; background:var(--bg); border:1px solid var(--bd2);
  padding:12px 14px; overflow-x:auto;
  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-nano); letter-spacing:0.09em; text-transform:uppercase; color:var(--mut);
}
.code-block { border:1px solid var(--bd2); }
.code-block > .code { border:none; }
.code__copy {
  font:inherit; font-size:var(--fs-nano); letter-spacing:0.09em; text-transform:uppercase;
  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); }
