/* Modern Fintech component layer.
 *
 * Consumes the tokens in tokens.css (colors + radius/shadow/spacing/typography).
 * Owns the look: rounded cards, soft shadows, roomy spacing, accent buttons, clear
 * up/down. Layered on top of app.css (which keeps the chart/table/portfolio specifics)
 * — this file styles the shell, header, panels, buttons, and forms. */

/* --- shell --- */
body{font:var(--font);margin:0;background:var(--bg);color:var(--fg);
  transition:background .2s,color .2s}
.wrap{max-width:84rem;margin:0 auto;padding:1.5rem 1.25rem 4rem}
h1{font-size:1.5rem;margin:.2em 0 .6em}
h2{margin-top:2.25rem;font-size:1.25rem}
h3{font-size:1.05rem;margin:1.4rem 0 .4rem}
/* Buttons are never text-selectable (owner 2026-10-24, iPhone long-press showed Copy/Look Up on a label).
 * ONE zero-specificity base rule for every control that acts as a button, incl. the link-built nav items;
 * text inputs, textarea, [contenteditable] and prose links are deliberately absent. Guard: tests/test_button_select.py */
:where(button,[role=button],[role=tab],[role=menuitem],[role=menuitemradio],[role=menuitemcheckbox],[role=option],[role=switch],summary,input[type=button],input[type=submit],input[type=reset],.btn,.rail-i,#tabbar .tab,.seg-lbl){
  -webkit-user-select:none;user-select:none;-webkit-touch-callout:none}
a{color:var(--link);text-decoration:none}@media (hover:hover){ a:hover {text-decoration:underline} }

/* --- header bar: title + dark toggle --- */
/* Single sticky header row: title + tab nav + controls, all on one line (pinned to the top). */
.topbar{display:flex;align-items:center;gap:.5rem .9rem;flex-wrap:wrap;
  margin:0 0 1rem;padding:.55rem 0;border-bottom:1px solid var(--bd);
  position:sticky;top:0;z-index:30;background:var(--bg)}
.topbar h1{flex:0 0 auto;margin:0;font-size:1.2rem}
.topbar .ctl{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-left:auto}
/* fix round (pool-gaps, finding 10): #themebtn was retired from every page's markup (nav-settings-
 * widgets Leg 2 moved the theme toggle onto Settings) -- this dead rule outlived it, nothing in the
 * tree creates that id any more (grepped). */

/* --- cards / panels: rounded, soft-shadowed, roomy ---
 * Rule 2 (docs/plans/2026-09-04-module-ui-review.md §0): a .card is for the Today widget grid and
 * for units deliberately placed SIDE BY SIDE (ledger's This month + Recurring bills sidebar, the
 * Games "Happening now" band + tiles, Today widgets). NOT for a box around a whole page or a whole
 * vertically-stacked section, and NOT for a box around every list row — those are plain sections
 * (h2 + content, sections separated by a border-top hairline) or list rows separated by hairlines. */
.card{background:var(--bg);border:1px solid var(--bd);border-radius:var(--radius);
  box-shadow:var(--shadow);padding:var(--pad-card);margin:var(--gap) 0}
/* leg 6 / ADR 0004: hover lift — transform+shadow only, never layout properties */
@media (prefers-reduced-motion: no-preference){
  .card{transition:box-shadow var(--dur) var(--ease),transform var(--dur) var(--ease)}
  @media (hover:hover){ .card:hover {box-shadow:var(--shadow-2);transform:translateY(-2px)} }
}
/* fix round (pool-gaps, finding "card hover fires on non-interactive cells"): `.card:hover`'s lift
 * above applies to EVERY `.card`, including a plain rail cell that carries the class only for its
 * surface paint (#coming, #sixmorail, #reccard) and isn't itself a clickable unit -- `.card--static`
 * opts a card out of the lift without forking the box/paint. */
/* fix round (pool-gaps, finding 7): the rest-state fill this hover rule must PRESERVE is glass.css's
 * `--c-shadow` (loaded last, wins the rest-state cascade) -- the old `var(--shadow)` token here was
 * a leftover from this file's own pre-glass `.card{box-shadow:var(--shadow)}` declaration, which
 * glass.css's own `.card` rule already overrides at rest but this hover-only rule was still
 * quoting, so hovering a static card visibly swapped its glass rim/inset for a flat 24px drop
 * shadow. Re-stating `--c-shadow` here keeps the hover box-shadow byte-identical to rest. */
@media (hover:hover){ .card--static:hover {box-shadow:var(--c-shadow);transform:none} }

/* .cell / .cell--side — the thin-glass content cell (fix round, pool-gaps finding 4, plain-liquid-
 * glass P2 deliverable, docs/plans/2026-09-25-plain-liquid-glass.md line 72): the SAME --c-fill/
 * --c-shadow thin-glass recipe `.card` already carries (declared together in glass.css's card-
 * family rule) under a plain, non-card name for a sidebar panel, a clipped grid, or a "one cell,
 * hairline rows" list surface (the approved mocks: lifeA's sidebars, the clipped calendar grid,
 * lifeB's row lists) -- adopting these is each owning module's own leg, not built here. FIXED:
 * background/box-shadow (glass.css) -- a module never restyles the fill. OPEN: radius (this file's
 * own job, `.cell--side`'s declared variant), padding, width/height, display -- a module
 * positions/sizes a cell freely. */
.cell{border-radius:var(--radius)}
/* `.cell--side` steps the radius up one notch for a full-height sidebar panel (mock: "sidebars
 * 20") -- a value one step off this platform's existing 11/16/22/28 ladder, so it gets its own
 * token (`--radius-side`, tokens.css) rather than reusing `--radius-lg` (22, a step too far). */
.cell--side{border-radius:var(--radius-side)}
/* fix round 1 (pool, finding 11/E1): `.inset-well` -- an INSET well for content that sits INSIDE an
 * existing `.cell`/`.composer` (Settings -> Connections' one-time token reveal, its module-picker
 * list): a `.cell` a second time nested inside one is a stacked-glass "nested card" (the design
 * hook's own nested-cards check), so a well is a DIFFERENT thing, not a `.cell` variant -- flat
 * `--panel` ("grey inset inside a white surface", tokens.css's own definition for exactly this
 * role -- already the same token `.tbl tfoot td` uses for a plain inset row), no rim/shadow, one
 * radius step down from a cell. Never nest a `.inset-well` inside another `.inset-well`/`.cell`
 * either -- if a THIRD level is ever needed, that is its own new pool gap, not more nesting.
 * fix round (pool-gaps-2, finding 31.2): renamed from `.well` -- the admin pool page's usage
 * scanner matches any bare lowercase word inside a page's own JS STRING LITERALS too (not just
 * `class=` attributes, `core/ui_pool.py::_js_token_counts`), so the plain English word "well"
 * (inside ordinary UI copy strings -- "as well", "aswell", toast text, ...) registered as a false
 * "usage" of this class on 22 unrelated pages that never actually reference it. A compound,
 * hyphenated class name is not a coincidental substring of ordinary prose the way a single common
 * word is, so it stops matching anything but its own real uses. */
.inset-well{background:var(--panel);border-radius:var(--radius-sm);padding:var(--sp-3)}

/* --- accent + plain buttons --- */
/* P2 pool (2026-09-25, plain-liquid-glass, docs/plans/…/survey/pool.full.json): geometry -- 32px
 * min-height, 0/14px padding, 13px type (mock .btn). These three (padding/min-height/font-size)
 * are `open` properties on the button row (core/ui_pool.py), which is what lets `.btn--sm` below
 * override them; games-keep (glass.css, near the games section) re-declares this exact selector
 * with the literal pre-P2 values instead of chasing a token, since a plain property is simpler and
 * just as re-pinnable as a custom one here. Paint (background/box-shadow/border/radius) stays
 * glass.css's job, loaded after this file. */
button,.btn{font:inherit;cursor:pointer;border:1px solid var(--bd);
  background:var(--bg);color:var(--fg);border-radius:var(--radius-sm);
  padding:0 14px;min-height:2rem;font-size:13px;white-space:nowrap;
  transition:background .15s,border-color .15s}
/* round 2 (G1, cleanup-list): the pool button had NO :disabled state at all -- a disabled button
 * kept its full rest-state paint (glass rim, still technically reachable hover tint) with only the
 * native `cursor` change, reading as clickable when it isn't. Reduced opacity (the platform's
 * existing pattern for a de-emphasised control, matching disabled fields) + `cursor:not-allowed`. */
button:disabled,.btn[disabled],.btn[aria-disabled=true]{
  opacity:.5;cursor:not-allowed;
}
/* fix round 1 (pool, finding 22.3): `pointer-events:none` (not a hover-cancelling override) is
 * the fix for the leftover hover tint on `[aria-disabled=true]` specifically -- a real, still-
 * focusable/clickable node unless JS also blocks it, so nothing else stops the browser firing
 * hover/press feedback on it. A native `:disabled`/`[disabled]` button needs none of that (the
 * native attribute already blocks activation and browsers don't dispatch hover/press states to a
 * disabled form control in the first place) -- `pointer-events:none` there only cost something:
 * it also blocks the native `title` tooltip from ever showing (e.g. chat's disabled #chatsend,
 * "AI parsing is off — set it up in Settings first"), the one piece of feedback a disabled control
 * still needs to give. Scoped to just the ARIA case now, the case that actually needs it. */
.btn[aria-disabled=true]{ pointer-events:none; }
/* compact button (mock .btn.sm, 17 uses across 10 boards) -- an open-property variant of the row
 * above (padding/min-height/font-size are all declared `open`, core/ui_pool.py). */
.btn--sm{padding:0 .6875rem;min-height:1.75rem;font-size:var(--fs-caption)}
/* ===== Segment item (owner 2026-10-24, "bare text with box and lens on it ... how can the proper text button be created properly"):
 * the ONE text-button recipe for every option inside a segmented track (.seg / .addseg / .viewsw / .qc-seg / nav.tabs / [role=tablist];
 * a radio-segmented .seg-lbl label is the item, its <span> is plain text). 26px flex-centred pill, 0 10px, 13px/500 muted; the selected
 * item is fg + strong over the sliding .seg-thumb (glass.css keeps those selected rules and the coarse-pointer 44px floor). The track is
 * 32px everywhere = 26 + 2 x 3px padding (glass.css). A module may position a track; it never sizes, pads or fonts an item --
 * scripts/button_probe.py R7 fails on any item that is not 26px / 10px. Games pages (.games-keep) are frozen and excluded. ===== */
:where(.seg,.addseg,.viewsw,.qc-seg,nav.tabs,[role=tablist]):where(:not(.games-keep *))>:is(button,a,label,.tab,.tab-btn,[role=tab]){
  box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-1);
  min-height:26px;height:26px;padding:0 var(--sp-3);margin:0;
  font:inherit;font-size:var(--fs-small);font-weight:500;line-height:1;white-space:nowrap;
  border:0;border-radius:var(--radius-pill);background:transparent;color:var(--muted);cursor:pointer;text-decoration:none;
}
.seg-lbl:where(:not(.games-keep *))>span{display:inline;padding:0;margin:0;font:inherit;line-height:1;border-radius:0;background:transparent}
@media (pointer:coarse){
  /* the coarse floor (glass.css) lifts a track OUTSIDE a toolbar to 44px items; the phone toolbar row (shell.css) pins 26px back */
  :where(.seg,.addseg,.viewsw,.qc-seg,nav.tabs,[role=tablist]):where(:not(.games-keep *))>:is(button,a,label,.tab,.tab-btn,[role=tab]){height:auto}
}
/* fix round (pool-gaps): three variants asked for independently by several module legs (Today's
 * widget size-cycle/remove controls, Balance's ‹/› pair, ledger's month prev/next, login/landing's
 * full-width CTAs) instead of inventing an undeclared shape each time -- every property each one
 * sets is already declared `open` on the base button row above (width/min-width/padding/
 * min-height/font-size/gap); only the DECLARATION was missing.
 * `.btn--icon` -- a round icon-only button (mock's circular S/M/L + remove controls): a square box
 * (border-radius is FIXED on the button row and already `--radius-pill` platform-wide, glass.css --
 * a square box at that radius already renders as a circle, no override needed here), no text
 * padding, centred glyph. Combine with `.btn--sm` for the smaller 28px step. */
.btn--icon{width:2rem;height:2rem;min-width:2rem;min-height:2rem;padding:0;
  display:inline-flex;align-items:center;justify-content:center;
  gap:0;font-size:1rem;line-height:1}
/* fix round 1 (pool, finding 12/E2): `.9rem` was off DESIGN.md's type ramp (the design hook's
 * design-system-font-size check) -- `--fs-caption`, the SAME step `.btn--sm` alone already uses
 * for its own font-size, keeps every "--sm" density-tier control on one consistent step rather
 * than a one-off value for the icon+sm combination. */
.btn--icon.btn--sm{width:1.75rem;height:1.75rem;min-width:1.75rem;min-height:1.75rem;font-size:var(--fs-caption)}
/* fix round (pool-gaps, finding 8): `.btn--icon.btn--sm` (two classes) out-specifies shell.css's
 * platform-wide coarse-pointer 44px floor (`main button:not(...)`, one class + two types) on
 * class-count alone regardless of source order, so it never reached the 44px WCAG target plain
 * `.btn--icon` alone (one class, correctly out-ranked by that floor) already does. Same
 * specificity as the base rule above, declared after it, so it wins the tie under coarse pointer
 * without needing to fight shell.css at all. */
@media (pointer:coarse){
  /* round 2 (G1 leftover 17.4): plain `.btn--icon` (32px) had no coarse floor of its OWN -- it
   * happened to still reach 44px on a real page via `main button:not(...)` (shell.css)'s own
   * platform-wide floor, since `.btn--icon` is always a real <button>, but that only works INSIDE
   * <main> (a real page's content) -- the admin pool page's own isolated sample preview (never
   * inside a <main>) showed the true, unfloored 32x32 base next to its already-correct 44x44
   * `.btn--sm` combo. Giving the base its own floor here (same width/height/min-* shape the combo
   * variant already had) makes it correct standalone, not just incidentally correct on some pages. */
  .btn--icon{width:2.75rem;height:2.75rem;min-width:2.75rem;min-height:2.75rem}
  .btn--icon.btn--sm{width:2.75rem;height:2.75rem;min-width:2.75rem;min-height:2.75rem}
}
/* `.btn--block` -- a full-width CTA (login's passkey/Google buttons, landing's hero sign-in). */
.btn--block{width:100%;min-width:0}
/* `.btn--lg` -- a taller, coarse-pointer-safe primary CTA (landing's hero sign-in, 44px). */
.btn--lg{min-height:var(--sp-8);padding:0 var(--sp-4);font-size:var(--fs-body)}
/* fix round (pool-gaps-2, finding 28): `:where(:not(:disabled))`, not bare `:not(:disabled)` --
 * `:hover` still matches a native `:disabled` button in both WebKit and Chromium (disabled blocks
 * activation/click, not hover-state matching), so a disabled button tinted under the pointer
 * exactly like a live one. A BARE `:not(:disabled)` would raise this rule's specificity from
 * (0,1,1) to (0,2,1) (`:not()` costs the specificity of its own argument, a pseudo-class = a
 * class), which would then OUT-RANK every other (0,1,1) button-hover override glass.css declares
 * LATER in the cascade and currently wins the tie against by SOURCE ORDER alone -- found live:
 * `.gswitch>button{background-color:var(--glass-fill-soft)}` (glass.css, no `:hover` of its own,
 * relying on this exact tie to keep its rest-state fill on hover too) started losing to this rule
 * instead, regrowing a hover tint on the games switcher `scripts/games_keep_ab.py --hover` was
 * built to catch. `:where()` contributes ZERO specificity regardless of its argument, restoring
 * the original (0,1,1) math every existing same-specificity rule was already tuned against. */
@media (hover:hover){ button:where(:not(:disabled)):hover, .btn:hover {background:var(--hover)} }
button.primary,.btn.primary{background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);border-color:transparent}
/* `.primary--danger` -- a primary that destroys (review 2 a6): the SAME fill/hover/press recipe with the accent role re-pointed at --down
   on the element itself, so glass.css's tint, hover and press steps and the text colour all follow. */
button.primary.primary--danger,.btn.primary.primary--danger{--accent:var(--down)}
/* `.primary--ok` -- a primary that starts something (the Capture bar's Begin, timelog L5): the same recipe with the accent role pointed at --up. */
button.primary.primary--ok,.btn.primary.primary--ok{--accent:var(--up)}
form button[type=submit],form button:not([type]){background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);border-color:transparent}
/* round 2 (G1 leftover 13): `filter:brightness()` hover steps for these four selectors removed --
 * every one of them ALSO carries `backdrop-filter` (glass.css's primary-button recipe, unconditional
 * for all four, `.primary` class or not), and this file's own Safari-first rule forbids combining
 * `filter` + `backdrop-filter` on one element (WebKit drops the frost mid-hover). The replacement
 * hover step (a background swap, no filter) lives in glass.css instead, right beside the button's
 * own accent-tint recipe -- the one place that already owns this exact hazard for the PRESS state.
 * `form button[type=submit]:hover` was missing here too (asymmetric with `:not([type])` despite
 * sharing the same rest rule) -- also folded into that one glass.css replacement rather than
 * re-introducing the same filter mistake for the previously-missing case. */
/* leg 6 / ADR 0004: press + focus-ring refinements */
button:active,.btn:active{transform:translateY(1px)}
button:focus-visible,.btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
@media (prefers-reduced-motion: no-preference){
  button,.btn{transition:background var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease),transform var(--dur-fast) var(--ease)}
}

/* G3 motion (2026-09-25, docs/plans/2026-09-25-plain-liquid-glass.md round 2, owner rulings 6-7):
 * pool buttons RELEASE (the transform springing back to rest once :active/.pressed lifts) with the
 * kit's own, more pronounced --bounce curve instead of the platform's shared --ease-spring -- the
 * button's actual release transition is glass.css's press-section rule (a different leg's file,
 * out of this leg's scope), but it already reads --dur-morph/--ease-spring through var(), and a
 * custom property cascades normally: a rule that targets the element DIRECTLY always beats an
 * INHERITED :root-level value, with total certainty, regardless of which stylesheet loads last --
 * so overriding the two custom properties here is enough to retune that release with no glass.css
 * edit. Scoped to actual `<button>`/`.btn` elements only (not `.rail-i`/`.tab`, which keep the
 * milder platform spring -- a nav row isn't "a button"); `.seg-thumb`/`.lens` are `<i>` elements
 * living inside a `.seg` `<div>`, never `.btn`, so they never inherit this and keep their own
 * timing (`.seg-thumb`'s CSS transition; the lens's own JS spring). */
button, .btn{ --dur-morph:420ms; --ease-spring:cubic-bezier(.34,1.56,.64,1); }

/* --- inputs --- */
/* textarea joined the group 2026-09-11 (text-box size pass): the note boxes that moved from a
   one-line <input> to a fixed-rows <textarea> (ledger/inventory/orders/journal notes, requests
   detail) must keep the exact font/padding/colour/focus ring the input had. */
input,select,textarea{font:inherit;padding:var(--field-pad-y) var(--field-pad-x);border:var(--field-bd-w) solid var(--bd);
  border-radius:var(--radius-sm);background-color:var(--bg);color:var(--fg)}
input:focus,select:focus,textarea:focus{outline:2px solid var(--accent);outline-offset:-1px;border-color:transparent}
/* leg 6 / ADR 0004: focus ring refinement (progressive enhancement via color-mix) */
input:focus-visible,select:focus-visible,textarea:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
@media (prefers-reduced-motion: no-preference){
  input,select,textarea{transition:box-shadow var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
}
/* --- shared <select> design (field-sizing addendum, 2026-09-04): identical height/padding/
   border/radius/background/focus-ring to the text input above (both draw from --field-* + the
   shared border/radius/bg/color) -- a select no longer paints the browser's shorter, rounder
   native pill next to a full-height text box. The chevron is a per-theme SVG (--select-chevron,
   tokens.css) since a mask-image ::after isn't possible on <select> and currentColor inside a
   background-image SVG doesn't reliably re-theme across engines. Dark mode + color-scheme keep
   the POPUP list native -- only the closed box is ours. Multi-select / size>1 excluded (their own
   native chrome is the only sane rendering for a list box). */
select{appearance:none;-webkit-appearance:none;-moz-appearance:none;
  padding-right:2.2em;background-image:var(--select-chevron);background-repeat:no-repeat;
  background-position:right .6em center;background-size:.8em;
  field-sizing:content;min-width:8ch}
select[multiple],select[size]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;
  padding-right:var(--field-pad-x);background-image:none;field-sizing:fixed;min-width:0}
/* round 2 (G1 leftover 13): select had NO hover state at all (base and head both -- a genuinely
 * missing affordance, not a regression). `background-color`, never the `background` shorthand --
 * this file's own hard-gated rule (`select{background-image:var(--select-chevron)}` further up)
 * would otherwise get silently wiped, the exact mistake `_check_select_rules` in tests/test_app.py
 * scans every platform CSS file for.
 * fix round (pool-gaps-2, finding 2): `:not(.games-keep *)` -- a games page's OWN content styles
 * MANY of its own selects with a per-module background (calendar's `.cal-patchsel`, rules-audit's
 * bare `select{}`, notifications' `.title select`/`.mcell select`, review's/sns's `.hl-row select`,
 * patch's `.langsel`, the shared `.hub-controls select`/`.roster-controls select`), each reading a
 * DIFFERENT token (`--bg2`/`--panel`/`--panel2`/`var(--surface,#fff)`) -- since this affordance is
 * "genuinely missing... not a regression" on games content specifically (base has NO select-hover
 * anywhere), the general games-keep counter-pin pattern (re-pin ONE frozen value in glass.css) does
 * not fit here: it would need to name and re-derive every one of those per-module values by hand,
 * an open-ended, easy-to-miss list (found piecemeal across three separate verify passes; see
 * scripts/games_keep_ab.py --hover's own history). Scoping the affordance itself out of games
 * content at the SOURCE instead -- `.games-keep` lives on <html>, so `.games-keep *` matches every
 * element on a games page, and `:not()` of that excludes all of them in one line, with no
 * per-module enumeration and no games-keep glass.css counterpart needed at all: whatever REST rule
 * already (correctly) governs a given select's background — the generic games-keep re-pin OR a
 * module's own more-specific one — simply keeps deciding on hover too, exactly matching base's own
 * "no hover-specific select fill anywhere" reality. */
@media (hover:hover){ select:where(:not(.select-quiet)):not(.games-keep *):hover { background-color:var(--hover); } }

/* Leg A2 owner review round 1 (A4): note boxes are fixed-size, never expandable -- each place sets
 * its own `rows` once (capture 4, journal note 3, mail body 12, ask composer 3, todo notes 3;
 * orders/inventory have no note textarea yet) and this is the ONE shared textarea recipe every one
 * of them inherits: no drag-resize handle, no JS auto-grow (removed from ask.js), long text
 * scrolls inside the fixed box instead of pushing the page around it. */
textarea{resize:none;overflow:auto;field-sizing:fixed}

/* --- pooled bare-field / ctl-quiet / suffix-field / select-quiet / switch (2026-09-19,
   docs/plans/2026-09-19-todo-pool-recipes.md, owner: "Same mistake on spacing, textbox&cursor, and
   button problems keep appeared again and again"): ONE recipe per kind instead of a per-ID/per-
   module patch each time a field/button needs to read as text-and-caret-only or a compact quiet
   control. Each is excluded from the ordinary input/select/textarea/button box rules above (and
   from glass.css's cell-fill/button-rim groups, see that file's own membership edits) by CLASS, so
   it never has to fight them with an ID or !important. */

/* .bare-field — a text-and-caret-only input/textarea. FIXED: border, border-radius, background,
   box-shadow, outline, padding (block+inline), margin-inline, appearance, caret-color, min-height.
   OPEN (a module/variant may tune): font-size, font-weight, line-height, color, text-align, width,
   min-width, max-width, max-height, flex, text-transform, letter-spacing. */
input.bare-field,textarea.bare-field{
  border:0;background:transparent;box-shadow:none;outline:none;border-radius:0;
  padding-block:0;padding-inline:2px;margin-inline:-2px;
  font:inherit;color:inherit;-webkit-appearance:none;appearance:none;
  caret-color:var(--accent);min-height:0;width:100%}
input.bare-field:focus, input.bare-field:focus-visible, textarea.bare-field:focus, textarea.bare-field:focus-visible {
  border:0;background:transparent;box-shadow:none;outline:none;border-radius:0}
@media (hover:hover){ input.bare-field:hover, textarea.bare-field:hover {
  border:0;background:transparent;box-shadow:none;outline:none;border-radius:0} }
input.bare-field[type=number]{-moz-appearance:textfield}
input.bare-field[type=number]::-webkit-inner-spin-button,
input.bare-field[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
textarea.bare-field{resize:none;field-sizing:content;overflow-y:auto}
/* variant: a note/description field — caption size, muted, taller line-height, clamps at 4 lines */
textarea.bare-field--note{font-size:var(--fs-caption);color:var(--muted);line-height:1.45;
  max-height:calc(4 * 1lh)}
/* variant: a right-aligned value living at the end of an info row (Add Tag, a trailing figure) */
input.bare-field--end{width:auto;flex:1 1 auto;text-align:right}
/* variant: a card/popover's own title field (e.g. To-Do's info-card #dtitle) — a headline size, not
   the page body size. Needed because `input.bare-field{font:inherit}` (0,1,1) otherwise beats a
   plain single-class module rule like `.det-title{font-size:...}` (0,1,0) regardless of source
   order (fix round 1, 2026-09-19: this is what silently dropped #dtitle from 16.8px to 14px — the
   module rule LOOKED like it should apply but never had enough specificity to win). */
input.bare-field--title{font-size:var(--fs-title);font-weight:var(--fw-strong);line-height:1.2}
/* variant: a value typed straight into a row that would otherwise read as a label (a metric reading, a fact value).
   Sets the ONE open property border-bottom: a 1px hairline at rest, the accent on hover/focus, nothing else changes.
   Tag in the selector for the same specificity-tie reason as --title (the base :hover/:focus rule is 0,2,1). */
input.bare-field--line,textarea.bare-field--line{border-bottom:1px solid var(--bd)}
input.bare-field--line:focus, input.bare-field--line:focus-visible, textarea.bare-field--line:focus, textarea.bare-field--line:focus-visible {border-bottom:1px solid var(--accent)}
@media (hover:hover){ input.bare-field--line:hover, textarea.bare-field--line:hover {border-bottom:1px solid var(--accent)} }

/* .ctl-quiet — a borderless text/icon button for use INSIDE a surface (card, bar, menu, row).
   FIXED: border, background, box-shadow (the "no chrome box" identity — this is the property set
   that kept silently reappearing as a full glass pill inside a compact surface). OPEN: border-
   radius, padding-block, padding-inline, min-width, min-height, color, font-size, font-weight,
   gap, width, height. */
button.ctl-quiet{border:0;background:none;box-shadow:none;
  border-radius:var(--radius-sm);padding:.2rem .3rem;font:inherit;color:var(--link);
  min-width:0;min-height:28px}
@media (hover:hover){ button.ctl-quiet:hover {background:var(--hover)} }
button.ctl-quiet:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
@media (pointer:coarse){ button.ctl-quiet{min-height:44px;min-width:44px} }
button.ctl-quiet--muted{color:var(--muted)}
button.ctl-quiet--icon{display:inline-flex;align-items:center;justify-content:center;
  border-radius:50%;padding-inline:0;padding-block:0;min-width:28px}
button.ctl-quiet--danger{color:var(--down)}
/* variant: an inline control sitting INSIDE a text line (a heading's own "…" menu, a caption row)
   that must not inflate that line's own height — the base's 28px is a HIT AREA, not a layout
   requirement, everywhere else; here the hit area moves off min-height (min-height:0, content-size
   layout box) onto an absolute-inset ::after instead, the same invisible-hit-area trick `.row .chk`/
   `.row .info` already use in To-Do's own CSS. Fix round 1 (2026-09-19): `.sec-more` inside `.sec-h`
   was stretching every section heading 20px -> 28px because nothing separated its hit area from its
   layout box. */
button.ctl-quiet--inline{min-height:0;position:relative}
/* an inline icon keeps its content width: its hit area lives on ::after, so the icon variant's 28px min-width must not widen the layout box */
button.ctl-quiet--icon.ctl-quiet--inline{min-width:0}
/* flat-buttons 2026-10-16: ONE icon-button size rule -- inside a row or heading the drawn box is 28px, the glyph 16px. An
   inline one keeps its LINE height by cancelling the 28px box's extra with a negative block margin (hit area on ::after as before). */
@media (pointer:fine){
  button.ctl-quiet--icon{width:28px;height:28px;min-width:28px;min-height:28px;padding:0;flex:none}
  button.ctl-quiet--icon svg{width:16px;height:16px}
  button.ctl-quiet--icon.ctl-quiet--inline{margin-block:-4px}
}
/* the hit area itself: absolutely positioned against the button, which the base positions itself (r7 R2: no consumer anchor needed). */
button.ctl-quiet--inline::after{content:'';position:absolute;inset:calc(-1*var(--sp-2))}

/* .suffix-field — a number input paired with a unit caption (Time Needed "45 min"), fixed BY
   CONSTRUCTION so the two can never touch: no fill, no pill, a real gap, one shared type size. */
.suffix-field{display:inline-flex;align-items:baseline;gap:var(--sp-1)}
.suffix-field .suffix-unit{font-size:inherit;line-height:inherit;color:var(--muted)}

/* .select-quiet — the select-in-row idiom (modelled on the info card's Early Reminder control):
   value-only look, right-aligned, clips rather than wrapping or spilling out of its row/card.
   fix round 2 (2026-09-19): the owner named this box the reference design ("selection box like
   Early Reminder") — the box itself (padding/margin-right/radius/hover fill/chevron inset) is BASE's
   own `.rem-detail .det-ctl select` geometry, ported in as this recipe's FIXED box instead of the
   flatter no-box look leg D shipped; the shrink-to-fit additions leg D made (min-width/max-width/
   overflow/text-overflow/white-space) stay — a 40-char value still stays inside its row.
   correction (2026-09-19, round 7 X2 addendum item b): `text-overflow:ellipsis` is set and DOES
   apply in Chromium (paints "…"), but WebKit 26.5 does not support `text-overflow` on a native
   `<select>` at all — it hard-clips with no ellipsis glyph regardless (tried
   `appearance:menulist-button` with the chevron hidden, `::-webkit-…` pseudo-targeting,
   `padding-inline-end:0`, `text-overflow:clip` — none paint it; WebKit screenshots in the round 7
   X2 dev-log). Kept as a clip: the owner's actual complaint was the value spilling out of its
   row/card, which this still prevents in both engines. DESIGN.md's own "ellipsizes" wording (its
   `select-quiet` line under Fields & Menus) describes the same thing and needs the identical
   correction — X1 owns that file.
   round 7 X2 item 2 (2026-09-19, "select-quiet overhang"): `margin-right:-.4rem` pulls the box's
   own edge .4rem past whatever it's laid out against. Where the select is the ONLY/LAST child of a
   `justify-content:flex-end` container that's .4rem past the CONTAINER's own edge — a lone
   flex-end item's margin box is always pinned to the container's content edge regardless of its
   own width, so the fix there is a matching .4rem reserved back on the CONTAINER (a consumer-side
   rule; see todo.css's `.det-ctl.sel`). Where the select is instead followed by a REAL sibling,
   that same -.4rem pulls the SIBLING .4rem closer instead — harmless where an existing flex `gap`
   already covers it, a real overlap where it doesn't (plain flow has none at all). That half is
   also a consumer-side rule (reserving the .4rem back on whatever follows, scoped to the specific
   containers that actually need it — todo.css) rather than a blanket sibling rule here: a
   `justify-content:flex-end` GROUP's raw width is anchored by its LAST member regardless of any
   member's own margin, so widening an EARLIER-followed sibling's gap inside such a group (e.g.
   `#dsendbtn`, already safe today) shifts the select's own position exactly as much as removing
   its margin would — the same regression a naive "just delete the margin" fix causes, just
   reached a different way. See todo.css's own comment for the measurements. */
select.select-quiet{appearance:none;-webkit-appearance:none;border:0;background-color:transparent;
  box-shadow:none;padding:.2rem 1.5em .2rem .6rem;margin-right:-.4rem;border-radius:var(--radius-sm);
  font:inherit;line-height:1.2;
  text-align:right;
  background-image:var(--select-chevron);background-position:right .4rem center;background-size:.8em;
  background-repeat:no-repeat;field-sizing:content;min-width:0;max-width:100%;
  text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
select.select-quiet:focus {background-color:var(--hover)}
@media (hover:hover){ select.select-quiet:hover {background-color:var(--hover)} }
select.select-quiet:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ===== select--pill (owner 2026-10-24 "selection box ... bare text with box"): a select that stands in a toolbar as a text button =
 * the segment track geometry: 32px pill, --seg-track fill, no border, 0 12px + the chevron slot, 13px/500, fg. The phone toolbar
 * applies it to every select it holds; a select elsewhere opts in with the class. ===== */
select.select--pill, [data-phone-toolbar] select{
  box-sizing:border-box;height:32px;min-height:32px;padding:0 2em 0 var(--sp-3);margin:0;
  border:0;border-radius:var(--radius-pill);background-color:var(--seg-track);color:var(--fg);box-shadow:none;
  font:inherit;font-size:var(--fs-small);font-weight:500;line-height:30px;
  background-position:right var(--sp-3) center;background-size:.75em;
}
select.select--pill:focus-visible, [data-phone-toolbar] select:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* .switch — moved here from modules/todo/static/css/todo.css (2026-09-19): the only toggle recipe
   on the platform, DESIGN.md already documents it as shared even though To-Do was its only
   consumer (grepped: no other module styles or uses `class=switch`). FIXED: appearance, border-
   radius, cursor, margin, the thumb's own shape/fill/shadow. OPEN: width, height (a smaller card
   variant scales both, `switch--sm`; the thumb's own size/offset/travel distance are unregulated —
   not fixed OR open, no base recipe cares which pixel values a variant's thumb uses as long as the
   track's box model stays untouched). */
/* P2 pool (2026-09-25): the thumb's own fill moves off `var(--bg)` onto `--switch-thumb-fill`
 * (tokens.css, `#fff` in both themes) -- `var(--bg)` rendered a DARK thumb in dark mode (the mock's
 * own bug list: "so the thumb is dark in dark mode, current is just a shadow"), which no track/
 * checked colour change was ever going to fix. Track fill/checked colour are left as they were
 * (`--bd` rest / `--accent` checked) -- the mock's own green vs the platform's accent is an
 * explicit owner-decision hazard (survey), not decided here. Games use `.switch` in 8 pages; the
 * old dark-mode-bug fill is re-pinned back under `.games-keep` (glass.css) so games stays
 * pixel-identical even though the fix is objectively correct everywhere else. */
/* R3 A1 (2026-09-27, owner #6, verbatim "Toggle button design [Image #2] edges aren't aligned"):
 * `input,select,textarea{padding;border}` (this file, above) reaches `.switch` too (it's a bare
 * `<input>`) and the UA stylesheet's own `box-sizing:border-box` for `[type=checkbox]` keeps the
 * track's declared width/height as its BORDER box -- the leaked 1px border + field padding then
 * shrink the PADDING box (the containing block for the `::after` knob) unevenly off the track's
 * visible edge (measured: 1px border + .15rem top offset = 3.4px inset top/left, but only
 * border(1px) + (.15rem track slack) = 1.4px bottom/right, since the padding ate into the slack
 * asymmetrically). `border:0;padding:0` removes both so the padding box equals the border box
 * again and the knob's four insets are symmetric by construction. */
.switch{appearance:none;width:2.4rem;height:1.4rem;border-radius:var(--radius-pill);border:0;padding:0;
  background:var(--bd);position:relative;cursor:pointer;margin:0;flex:none;transition:background .15s}
.switch::after{content:'';position:absolute;top:.15rem;left:.15rem;width:1.1rem;height:1.1rem;
  border-radius:50%;background:var(--switch-thumb-fill);box-shadow:0 1px 3px rgba(0,0,0,.25)}
@media (prefers-reduced-motion: no-preference){
  .switch::after{transition:transform .15s var(--ease)}
}
.switch:checked{background:var(--accent)}
.switch:checked::after{transform:translateX(1rem)}
.switch:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* variant: a smaller switch scoped to a compact surface (To-Do's info card) */
.switch--sm{width:2rem;height:1.2rem}
.switch--sm::after{width:.95rem;height:.95rem;top:.125rem;left:.125rem}
.switch--sm:checked::after{transform:translateX(.8rem)}

/* .colorcheck (2026-10-01, calendar density): a checkbox that IS its own colour swatch. FIXED: box
   size, radius, fill from --c, tick geometry, focus ring. OPEN: none. Filled + tick = on; filled
   without tick = off (the label greys, see the owner ruling in DESIGN.md). The whole <label> row is
   the hit area, not the 14px box. */
.colorcheck{appearance:none;-webkit-appearance:none;margin:0;width:14px;height:14px;flex:none;
  position:relative;border:0;border-radius:4px;background:var(--c,var(--accent));cursor:pointer;padding:0}
.colorcheck:checked::after{content:'';position:absolute;left:4.5px;top:1.5px;width:3.5px;height:7.5px;
  border:solid var(--bg-solid);border-width:0 2px 2px 0;transform:rotate(45deg)}
.colorcheck:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.colorcheck:disabled{cursor:default}

/* .swatchrow (2026-10-03, promoted out of todo.css for the Calendar category dialog): a row of
   round colour buttons, role=radiogroup, each button carries its own inline background and
   aria-checked. FIXED: button diameter (1.6rem; the coarse-pointer tap floor), radius, ring,
   hover scale, the checked ring + tick. OPEN: display/flex-wrap/gap on the row, position on a
   button (a module may anchor a child to it). */
.swatchrow{display:flex;flex-wrap:wrap;gap:.4rem}
.swatchrow button{width:1.6rem;height:1.6rem;border-radius:50%;border:2px solid transparent;
  padding:0;cursor:pointer}
/* each swatch sets its own inline background, which otherwise beats any hover rule outright */
@media (prefers-reduced-motion: no-preference){
  .swatchrow button{transition:transform var(--dur-fast) var(--ease)}
}
@media (hover:hover){ .swatchrow button:hover {transform:scale(1.12)} }
.swatchrow button[aria-checked=true]{border-color:var(--fg)}
/* the ✓ glyph inside the fixed-diameter swatch circle — em, not rem, same reasoning as .tile-ico */
.swatchrow button[aria-checked=true]::after{content:'✓';display:block;text-align:center;
  font-size:.7em;color:var(--accent-fg)}
@media (pointer:coarse){
  .swatchrow button{width:calc(var(--sp-7) + var(--sp-1));height:calc(var(--sp-7) + var(--sp-1));
    line-height:calc(var(--sp-7) + var(--sp-1))}
}

/* --- forms: roomy, wrap on mobile --- */
form{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center;margin:.75rem 0}
/* Global [hidden] guard. The UA sheet's [hidden]{display:none} is specificity (0,1,0) and loses to
 * ANY author display rule carrying a class or an id — form{display:flex} above, #capbar{display:flex},
 * .shellpanel a{display:flex}, .mod{display:block}, #evform label{display:flex} — so an element the
 * JS had hidden stayed on screen. Hit five times in this repo (leg6 .mod/.shellpanel, s13 .pl-found,
 * s13b #capbar + the calendar scope radios), so it is fixed ONCE here instead of per component.
 * !important is required: it is the only way to beat an id rule. Audited 2026-07-31 — no rule in
 * this repo deliberately styles a [hidden] element visible, and nothing uses hidden="until-found". */
[hidden]{display:none!important}

/* --- clear up/down --- */
/* A ▲/▼ glyph reinforces colour so price/return DIRECTION survives without hue (colorblind/low-vision),
 * which matters for the near-monochrome palettes in tokens.css. Modern screen readers DO announce
 * ::before content ("black up-pointing triangle"); that redundancy is acceptable because the value's
 * sign already states direction. The glyph is reserved for genuine numeric trend cells: word-bearing
 * badges (#explain/stance/precheck/F&G), bull/bear bullets and the alert ● marker opt OUT of it
 * (see app.css) because there .up/.down is an overloaded good/bad class, not a price uptrend. */
.up{color:var(--up);font-weight:600}.down{color:var(--down);font-weight:600}
.up::before{content:"\25B2 "}.down::before{content:"\25BC "}  /* ▲ / ▼ */
/* opt OUT where .up/.down is semantic (good/bad), not price direction — keeps the arrow truthful */
.badge.up::before,.badge.down::before,#suggestion li.up::before,#suggestion li.down::before{content:none}
/* --- badge: a small pill count/figure (a real base recipe, fix round 1, 2026-09-25, finding
 * 12b) --- `.badge` is one of the registered "chip" element's classes (core/ui_pool.py) sharing
 * its fixed border/border-radius/padding/background/font-size, but this file never actually gave
 * it any of them -- every module using `class=badge` had to hand-copy this exact box, or (ledger's
 * `#reccount`) rename away from `badge` entirely to dodge a restyle_violations() hit on a class
 * that was fixed-but-unimplemented. Real numbers match what those per-module copies already drew
 * (stocks' #explain/#feargreed/#analystviews `.badge`, ledger's now-deleted `.rc-count`) so this is
 * a like-for-like pool promotion, not a new look. Tone colour is NOT this recipe's job: combine
 * with the platform's own `.up`/`.down` utility classes above (`<span class="badge up">`, already
 * the registered sample) -- their `::before` arrow is opted out just above for exactly this pairing. */
.badge{display:inline-block;background:var(--panel);border:1px solid var(--bd2);border-radius:var(--radius-pill);
  padding:0 .5rem;font-size:var(--fs-caption);color:var(--muted)}
.hit{color:var(--up);font-weight:600}  /* fired-alert marker: the ● is the cue, no trend arrow */

/* round 2 (G1 leftover 2/20): `.chip`'s shared BOX MODEL -- pool gap noted by item 20 ("every
 * module still hand-copies it") and by this leg's own survey (pool.full.json). FIXED: border(-
 * radius)/padding/font-size, matching the two modules (requests, orders) that already agree on an
 * identical rule byte-for-byte; inventory's own copy differs only by one padding step (--sp-3 vs
 * --sp-2), close enough to converge on adoption, a G2 (chips leg) call, not decided here. OPEN:
 * `background`/`color` -- glass.css's pool paint (`.tagchip, .chip, .aliaschip{background:var(
 * --chip-fill);...}`) stays the FIXED default fill for a plain/unselected chip, but a STATUS-toned
 * chip needs its own fill+text pair (see the five `.chip--*` variants right below), so those two
 * properties move from fixed to open for this one declared sub-family -- exactly the "another layer
 * of variance... fixed base" shape CLAUDE.md's pooled-element convention describes, not a fork. */
.chip{border-radius:var(--radius-pill);padding:2px var(--sp-2);font-size:var(--fs-caption)}
/* Five STATUS TONE variants -- fixed, palette-independent bg/fg pairs (tokens.css; see that file's
 * own comment on `--chip-bad-bg` for why fixed hex beats deriving from --up/--down/--accent: their
 * OWN per-palette AA headroom is too thin for any added background tint, worst case classic light
 * --up 4.80:1 raw, already under 4.5 with a 6% tone tint). `--ok`/`--warn` reuse the existing toast
 * pair (already vetted the same way); `.chip--muted` is for an explicitly INACTIVE/neutral status
 * (cancelled, removed, ...), distinct from a plain unselected chip (which stays the pool's own
 * `--chip-fill` + inherited `--fg`, untouched by this rule). */
/* fix round 1 (pool, finding 9): each variant is worn ALONGSIDE the base (`class="chip chip--ok"`,
 * this file's own ui_pool.py sample), but was selected here as the variant class ALONE -- the same
 * (0,1,0) specificity as glass.css's `.tagchip, .chip, .aliaschip{background:var(--chip-fill)...}`,
 * which loads LAST on every page and so won every tie, painting every status tone on the plain grey
 * track regardless of which variant was asked for. Qualifying the base class alongside the variant
 * (`.chip.chip--ok`, (0,2,0)) matches how it is actually used and out-specifies glass.css's bare
 * `.chip` unconditionally, the ordinary CSS answer -- no `:where()` needed, since (unlike the
 * bare-field/ctl-quiet carve-outs elsewhere in this codebase) nothing needs the ORIGINAL (0,1,0)
 * math preserved for some other, lower-specificity module rule to keep winning. */
.chip.chip--ok{background:var(--toast-ok-bg);color:var(--toast-ok-fg)}
.chip.chip--warn{background:var(--toast-bg);color:var(--toast-fg)}
.chip.chip--bad{background:var(--chip-bad-bg);color:var(--chip-bad-fg)}
.chip.chip--info{background:var(--chip-info-bg);color:var(--chip-info-fg)}
.chip.chip--muted{background:var(--chip-muted-bg);color:var(--chip-muted-fg)}

/* --- per-ticker monogram badge (symIcon) --- */
/* em-sized so it scales with its context: small in tables/Quick View, large in the #quote headline.
 * Deterministic hue per symbol (--ico-h); low fixed lightness (30%) keeps white initials
 * above WCAG 4.5:1 across the whole hue wheel (worst case ~4.72:1 in the yellow arc), light + dark. */
.symico{position:relative;display:inline-flex;align-items:center;justify-content:center;width:auto;min-width:1.4em;height:1.4em;padding-inline:.12em;box-sizing:border-box;
  flex:none;border-radius:4px;margin-right:.4em;vertical-align:middle;
  background:hsl(var(--ico-h,0) 55% 30%);color:#fff;font-size:.62em;font-weight:700;
  line-height:1;letter-spacing:-.02em;overflow:hidden}
/* Company logo overlay (server-proxied /files/logo/<sym>): covers the monogram when it loads,
 * removes itself on error so the initials underneath stay the fallback. Transparent — served
 * favicons carry an alpha channel, so painting a background behind them would add a ground
 * they were never designed with. While the img is present, :has() blanks the monogram square
 * + initials underneath so they can't bleed through a transparent logo; on img error the img
 * removes itself, :has() stops matching, and the monogram falls back exactly as before. */
.symico img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;border-radius:inherit}
.symico:has(img){background:transparent;color:transparent}
/* Tone-adaptive chip: some transparent logos clash with one theme's ground (a near-white glyph
 * vanishes on light; RDW's red+black mark vanishes on dark). symIcoTone() tags the clashing side
 * only, so the chip paints just where it's needed — inherits the badge's border-radius. */
:root:not(.dark) .symico.lt:has(img){background:#39404d}
:root.dark .symico.dk:has(img){background:#e7eaef}

/* --- info tooltip icon (ℹ) --- used in multiple modules; promoted here so pages that
   load components.css get the affordance automatically. */
.info{display:inline-flex;align-items:center;justify-content:center;min-width:1.15em;height:1.15em;margin-left:.3rem;font-size:.82em;font-style:normal;font-weight:600;line-height:1;color:var(--muted);cursor:help;vertical-align:middle;user-select:none;opacity:.85}
.info:focus {opacity:1}
@media (hover:hover){ .info:hover {opacity:1} }
/* ui-review #29: the drawn glyph stays small; the hit target is an invisible >=24px box centred on it. */
.info,button.ctl-quiet--jump{position:relative}
.info::after,button.ctl-quiet--jump::after{content:'';position:absolute;left:50%;top:50%;
  width:max(100%,24px);height:max(100%,24px);transform:translate(-50%,-50%)}

/* --- design language v2 utilities (leg 6, ADR 0004) --- */
/* Skeleton = PULSE (owner 2026-10-01 "C"): a solid grey (--bd) bar that breathes in opacity, no sweeping
 * light. Reduced motion = a still bar. .skeleton-stack is the one block recipe for "content loads
 * here": 3-5 .skeleton lines (first shorter = heading, widths vary), role=status via shellSkeleton(). */
.skeleton{background:var(--bd);border-radius:var(--radius-sm);color:transparent;pointer-events:none}
@media (prefers-reduced-motion: no-preference){
  @keyframes skpulse{50%{opacity:.5}}
  .skeleton{animation:skpulse 1.2s ease-in-out infinite}
}
.skeleton--line{display:block;height:1rem;width:100%;min-width:2rem}      /* a bar inside a table cell / grid cell */
.skeleton--fill{position:absolute;inset:0;width:auto;height:auto;border-radius:inherit}  /* covers a position:relative host (a chart pane) */
.skeleton-stack{display:flex;flex-direction:column;gap:var(--sp-3);padding:var(--sp-2) 0}
.skeleton-stack>.skeleton{display:block;height:1rem;width:100%}
.skeleton-stack>.skeleton:first-child{width:38%;height:1.25rem}
.skeleton-stack>.skeleton:nth-child(3){width:86%}
.skeleton-stack>.skeleton:nth-child(4){width:64%}
.skeleton-stack>.skeleton:nth-child(5){width:78%}
/* First-entry loading screen (phone shell v2 leg 9d): shell.js puts #shell-loading first in <main> when the first data wave is
 * still pending 400 ms after DOMContentLoaded and nothing cached painted. Absolute inside <main> (never fixed), so the rail,
 * header and tab bar stay; page title in muted type over a 3-row pulse (1.6 s, still under reduced motion). */
html.shell-loading-on main{position:relative;min-height:60vh}
.shell-loading{position:absolute;inset:0;z-index:5;background:var(--bg-solid);padding:var(--sp-4) var(--sp-3)}
.shell-loading-title{margin:0 0 var(--sp-3);color:var(--muted);font-size:1.4rem;font-weight:600}
.shell-loading .skeleton-stack{padding:0}
.shell-loading .skeleton-stack>.skeleton{height:3.25rem;width:100%}
@media (prefers-reduced-motion: no-preference){ .shell-loading .skeleton{animation-duration:1.6s} }
/* centering uses left/right+margin-inline (not transform) so .fade-in's transform animation
 * (which ends at transform:none, fill-mode backwards) can't un-center the toast at rest */
/* z-index 65: above #shell (60, shell.css) so a toast is never painted under the sticky header,
 * below the modal/dialog chain (dropdowns 70, backdrop 80, panel 85, docktoast 95 — see the
 * z-ladder comment in shell.css) so an open menu/dialog still wins. top offset clears the
 * shell's own height (--shell-h, shell.css) plus a breathing gap, so the toast starts below the
 * header instead of overlapping it. --toast-top (shell.js, 2026-10-05) is the toast's own offset: the page's header row (and any
 * element marked data-toast-below) at or below --chrome-bottom. --chrome-bottom (shell.js, 2026-09-18 follow-up) is the
 * larger of the shell's own bottom edge and a page's second sticky bar's bottom edge (e.g.
 * stocks' own .topbar on phone, where it moves out of the shell) — a toast offsets off THAT when
 * shell.js has measured it, so it clears whichever bar of page chrome actually extends lower.
 * var(--shell-h) is the fallback before shell.js's first measurement runs, or on a page with no
 * second bar at all, so this never regresses to the pre-fix behaviour. */
.toast{position:fixed;top:calc(var(--toast-top, var(--chrome-bottom, var(--shell-h))) + var(--sp-3));left:0;right:0;margin-inline:auto;width:fit-content;max-width:34rem;
  background:var(--toast-bg);border:1px solid var(--toast-bd);color:var(--toast-fg);
  padding:var(--sp-2) var(--sp-4);border-radius:var(--radius-sm);box-shadow:var(--shadow-1);
  z-index:65;font-size:var(--fs-caption)}
/* success variant (e.g. hub command-center save confirmation) — same shape, green tokens */
.toast.ok{background:var(--toast-ok-bg);border-color:var(--toast-ok-bd);color:var(--toast-ok-fg)}
@keyframes fadein{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion: no-preference){
  /* fill-mode `backwards`, not `both`: a forwards fill keeps a (identity) transform applied to
     the animated element for ever, which makes it the containing block of every position:fixed
     descendant — the To-Do detail popover inside <main class="rem fade-in"> landed 224px (the
     rail width) right of where placeDetail() put it. The `to` keyframe equals the normal style,
     so dropping the fill after the run changes nothing visible. (todo popover leg 2026-09-05) */
  .fade-in{animation:fadein var(--dur) var(--ease) backwards}
}
/* M7 pane swap (shell.js shellSwapPane): the OUT pane is lifted out of flow so both panes overlap while they slide. */
.pane-sw{position:relative}
.pane-sw--out{position:absolute;inset:0 auto auto 0;width:100%}
/* M3 list motion (listmotion.js shellList): .lm-clip clips a row while it enters/leaves. Height is animated on purpose --
   a one-shot entrance/exit that must push siblings; it never runs on scroll or repeatedly. .lm-leaving blocks clicks mid-exit. */
.lm-clip{overflow:hidden}
.lm-leaving{pointer-events:none}
/* M8 undo toast shows its time: a 2px bar drains over the toast's own timeout; hover/focus pauses it (.toast--hold).
   Linear is deliberate -- it is a clock, not a movement. No bar under reduced motion (this block only). */
@keyframes toast-in{from{opacity:0;transform:translateY(-8px)}}
@keyframes toast-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
@media (prefers-reduced-motion: no-preference){
  .toast{overflow:hidden}
  .toast.shell-toast{animation:toast-in var(--dur) var(--ease-spring) both}
  .toast-timer{position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--accent);transform-origin:left;animation:toast-drain var(--t) linear both}
  .toast--hold .toast-timer{animation-play-state:paused}
}
/* page-fixed recipe, docs/plans/2026-09-05-todo-sections-review.md A.5 — a working surface (To-
   Do, Calendar, People) that fills the viewport exactly, with no page scroll: apply .page-fixed
   to the page's root (typically the same element as .fade-in) and .scroll-y to whichever
   descendant region should own the scrollbar instead. */
.page-fixed{height:calc(100dvh - var(--shell-h));min-height:0;overflow:hidden}   /* pre-shell.js fallback only; once html.page-fixed-doc is set, shell.css sizes it by flex (no viewport units) */
/* overflow-x:hidden (2026-10-23): a vertical scroller never scrolls sideways; iOS rubber-bands any
   scroller whose content is 1px wider (Calendar Month was) */
.scroll-y{min-height:0;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain}
[data-ph-scroll]{overflow-x:hidden;overscroll-behavior-x:none}

/* .search-field — ONE pooled search box (2026-09-25, plain-liquid-glass P2, docs/plans/…/survey/
 * pool.full.json: "a pooled search field replacing #tsearch/#jq/#psearch copies"). Before this, To-
 * Do's `#tsearch` (no icon), People's `#psearch` and Journal's `#jq` each hand-rolled the same
 * leading-magnifier layout with their own copy of the same five declarations. Markup contract:
 * <div class=search-field><svg aria-hidden=true>…</svg><input type=search class=…></div> — the
 * module keeps its own id (JS still reads it) and margin (this recipe positions nothing outside
 * itself). The input still goes through the normal input:where(:not(.bare-field)) fill/box-shadow/
 * radius/min-height recipe in glass.css; this only adds the icon gutter + focus/placeholder/cancel-
 * button treatment shared by every search field. */
.search-field{position:relative;flex:none}
.search-field svg{position:absolute;left:var(--sp-2);top:50%;translate:0 -50%;width:1rem;height:1rem;
  color:var(--muted);pointer-events:none}
.search-field input{width:100%;box-sizing:border-box;padding-left:calc(var(--sp-2) * 2 + 1rem)}
.search-field input::placeholder{color:var(--muted)}
.search-field input::-webkit-search-cancel-button{cursor:pointer}

/* named .tagchip (not .chip) — .chip is already owned by orders/todo/games modules with
 * different rest-state styling (no border); a shared name would silently restyle them.
 * P2 pool (2026-09-25, mock .chip): height ~26px (line-height, padding-block 0) + 10px horizontal
 * padding -- this row's own `background`/`border` below are already dead at runtime (glass.css's
 * chip-fill group, loaded last, repaints them); geometry is the only thing this file still
 * controls, so it's the only thing updated here. Not used by games (grepped clean; games uses
 * bare `.chip`, which is module-owned, not this class).
 * fix round 1 (finding 7): `color` was NOT actually dead the paragraph above claims -- glass.css's
 * chip-fill group repaints `background`/`border-color`/`box-shadow` but never `color`, so this
 * row's own value keeps deciding the text colour same as always. tokens.css's own --chip-fill
 * comment ("round 2 (G1 leftover 2)") already reasoned through this exact AA failure and settled
 * on `--fg` ("unselected seg labels and plain chip text read --fg, not --muted... --muted was the
 * actual problem, not the tint strength") -- `.chip` picked that up for free (nothing here or in
 * glass.css ever set its OWN color, so it already inherits --fg); `.tagchip` was the one place
 * that reasoning never actually reached, because its color came from THIS rule, believed dead. */
.tagchip{display:inline-block;background:var(--panel);border:1px solid var(--bd);color:var(--fg);
  border-radius:var(--radius-pill);padding:0 .625rem;line-height:26px;font-size:var(--fs-caption)}

/* .tag-chip / .tag-x — moved verbatim from modules/todo/static/css/todo.css (2026-09-21,
   docs/plans/2026-09-21-journal-tag-tokens.md): pooled so Journal's tag field (`.tokenfield`,
   below) can share the exact same chip To-Do's info card already draws. Only the `.tag-x`
   selectors changed, from `.rem-detail .tag-x`/`.rem-detail .tag-x:hover` to `.tag-chip .tag-x`/
   `.tag-chip .tag-x:hover` (same 0,2,0 / 0,3,0 specificity) — a chip can now sit outside
   `.rem-detail` too. */
/* addendum for X2 item 5a (UI tester leg F, 2026-09-19): gap was .1rem (1.6px) -- the tag text and
   its × sat closer than the probe's `siblings` check wants (>=4px). --sp-1 (4px) instead. */
/* 2026-10-02 DE-GLASS: games' review/sns pages have their own `.tag-chip` button (named long before this pooled class
   existed -- a name collision); the pooled recipe is scoped OFF games pages so those keep their module-owned look. */
.tag-chip:where(:not(.games-keep *)){display:inline-flex;align-items:center;gap:var(--sp-1);padding:.1rem .15rem .1rem .55rem;
  border-radius:var(--radius-pill);background:color-mix(in srgb,var(--accent) 14%,transparent);
  color:color-mix(in srgb,var(--accent) 72%,var(--fg));font-size:var(--fs-caption);font-weight:var(--fw-strong);white-space:nowrap;line-height:1.5}   /* review 2 c18: the ink steps toward --fg (darker in light, lighter in dark) so the 14% accent chip clears 4.5:1 */
.tag-chip .tag-x{border:0;background:none;box-shadow:none;color:inherit;padding:0 .35rem;cursor:pointer;
  font:inherit;line-height:1;opacity:.7;min-width:0;min-height:0;border-radius:var(--radius-pill)}
@media (hover:hover){ .tag-chip .tag-x:hover {opacity:1;background:color-mix(in srgb,var(--accent) 18%,transparent)} }

/* .tokenfield — a text-input-shaped box holding .tag-chip tokens + one input.bare-field (Journal's
   Tags field, 2026-09-21, owner: "when write tag, once space, show it as #tag with blue
   highlight"). FIXED: border, background, border-radius, outline, box-shadow (it must read as the
   sibling inputs' twin — values mirrored from the shared input,select,textarea rule above via the
   same tokens, not copied literals). OPEN: width, max-width, gap. The at-rest height of an empty
   .tokenfield equals a sibling <input>'s height: .bare-field zeroes its own padding, so the
   wrapper's own padding (below) is what drives the row's height, same as a plain input. */
.tokenfield{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-1);box-sizing:border-box;
  width:100%;cursor:text;
  padding:var(--field-pad-y) var(--field-pad-x);border:var(--field-bd-w) solid var(--bd);
  border-radius:var(--radius-sm);background-color:var(--bg);color:var(--fg)}
.tokenfield:focus-within{outline:2px solid var(--accent);outline-offset:-1px;border-color:transparent;
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
.tokenfield>input.bare-field{flex:1 1 6rem;min-width:4rem;width:auto}
/* a chip's own box (~22px) is taller than the input's 20.3px line -- without this the field grew
   1.6px the moment the first chip appeared (measured 34.45 -> 36.05, 2026-09-21). */
.tokenfield>.tag-chip{margin-block:-1px}
/* a long tag (up to 30 characters) wraps inside the field instead of running past its edge (Projects Details, phone) */
.tokenfield>.tag-chip{max-inline-size:100%;white-space:normal;overflow-wrap:anywhere}

/* fix round (pool-gaps, finding 10): radius was still the pre-glass literal (12px) -- `.stat`
 * joined the thin-glass card family (glass.css) in P2, so its radius should match that family's
 * own `var(--radius)` (16px) instead of a stale one-off value. background-color/border are dead at
 * runtime (glass.css's card-family rule, loaded last, already repaints both), left as-is. */
.stat{background-color:var(--bg);border:1px solid var(--bd);border-radius:var(--radius);padding:14px;text-align:center}
.stat b{display:block;font-size:1.8rem;line-height:1.1}
.stat>span{color:var(--muted);font-size:var(--fs-caption)}   /* the LABEL only: a span inside the figure <b> (Stocks' unrealized %) keeps the figure's own colour/size */
/* .stat--label: label-first, left-aligned (declared variant, fix round 1, 2026-09-25, finding
 * 12c) -- a dense multi-stat strip (Stocks' Quick View row) reads better than the base centred
 * figure-over-label; moved here from stocks' own module override so it stops being a per-module
 * restyle. `<b>` stays the figure element (JS reads `.querySelector('b')`), just reordered after
 * `<span>` in markup for the label-first reading order. */
.stat--label{display:flex;flex-direction:column;align-items:flex-start;gap:2px;text-align:left}
.stat--label>span{font-size:var(--fs-caption);font-weight:var(--fw-strong);letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted)}
.stat--label b{font-size:var(--fs-display);font-variant-numeric:tabular-nums}
.cat-h{font-size:var(--fs-caption);font-weight:var(--fw-strong);letter-spacing:.08em;
  text-transform:uppercase;color:var(--muted);margin:var(--sp-5) 0 var(--sp-2)}

/* --- .tbl: the shared hairline-table recipe (originated stocks-reorg 2026-09-05 Leg B; moved
   here verbatim by admin-console Leg D so any page can use it without loading the stocks
   stylesheet — stocks' own modules/stocks/static/css/app.css already loads this file first, so
   its rendering is unchanged). An uppercase caption-label header (DESIGN.md's Caption Label
   Rule) and --bd2 row hairlines, replacing a plain td,th{border:1px solid var(--bd)} rule so
   every data table sharing this class looks the same instead of each page reinventing its own
   borders. The header is NOT sticky-positioned — a .tbl is expected to sit inside its own
   overflow-x:auto wrapper with no vertical overflow, so a sticky <th> would never actually stick
   to anything (verified 2026-09-05). The <caption> stays in the DOM for assistive tech, visually
   hidden — every .tbl is expected to sit under an h2/h3 that already names it. */
.tbl{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}
.tbl th,.tbl td{border:0;border-bottom:1px solid var(--bd2);padding:var(--sp-2) var(--sp-3);text-align:right;white-space:nowrap;vertical-align:middle}
.tbl th{font-size:var(--fs-caption);font-weight:var(--fw-strong);letter-spacing:.06em;text-transform:uppercase;color:var(--muted);background:var(--bg)}
/* sortable heading (2026-10-05, Projects index): the th's own caption type drawn as a button; the active column carries aria-sort */
.tbl th>button.th-sort{all:unset;box-sizing:border-box;cursor:pointer;position:relative;font:inherit;letter-spacing:inherit;text-transform:inherit}
.tbl th>button.th-sort::after{content:'';position:absolute;inset:calc(-1 * var(--sp-2))}
.tbl th[aria-sort]>button.th-sort {color:var(--fg)}
@media (hover:hover){ .tbl th>button.th-sort:hover {color:var(--fg)} }
.tbl th>button.th-sort:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* the direction mark: its room (1.2ch) is always reserved, so sorting a column moves no neighbour */
.tbl th>button.th-sort>.th-dir{display:inline-block;inline-size:1.2ch;margin-inline-start:.3ch;text-align:center}
.tbl th[title]{cursor:help;text-decoration:underline dotted;text-decoration-thickness:1px}
.tbl th:first-child,.tbl td:first-child{text-align:left}
/* the header hairline is ONE line under every heading: an inset shadow, not a collapsed border (a following colspan band row painted over the border everywhere but the first column) */
.tbl thead th{border-bottom:0;box-shadow:inset 0 -1px 0 var(--bd2)}
.tbl tbody tr{min-height:44px}
@media (hover:hover){ .tbl tbody tr:hover {background:var(--hover)} }
.tbl tfoot td{font-weight:var(--fw-strong);border-bottom:0;background:var(--panel)}
.tbl tr.cash td{background:var(--panel)}
/* group-header row (a status band between row groups), late ink and the compact density step: OPEN on .tbl */
.tbl .tbl-grp th{padding-top:var(--sp-5);text-align:left}
.tbl .tbl-grp:first-child th{padding-top:var(--sp-2)}
.tbl td.tbl-late{color:var(--down)}
.tbl--sm td{font-size:var(--fs-small)}
/* tbl--wrap: long text cells wrap (the base never wraps) and read left-aligned */
.tbl--wrap th,.tbl--wrap td{white-space:normal;overflow-wrap:break-word;text-align:left}
.tbl caption{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
/* visually-hidden utility (same recipe as .tbl caption above) — for a heading a screen reader needs
 * but the page doesn't (stock-lens 2026-09-16, heading-order fix). */
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}

/* --- mobile --- */
@media(max-width:640px){
  .wrap{padding:1rem .75rem 3rem}
  h1{font-size:1.3rem}
  #q{width:100%}
  .topbar{gap:.5rem}
  .topbar h1{flex-basis:100%}
}

/* --- leg 6 / s10: global keyboard focus rings for non-button interactives --- */
a:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
.info:focus-visible{outline:2px solid var(--accent);outline-offset:2px;opacity:1}

/* --- leg 6 / s10: toast dismiss button --- */
/* fix round (pool-gaps, finding 10): neither of these two set min-height, so the base pool
 * `button,.btn{min-height:2rem}` (32px) stretched a glyph-only ×/text control (~15px wide) into an
 * oval hit box -- min-height:0 lets the toast's own line-height size it instead, same idiom as the
 * dense round/grid-cell exemption above.
 * round 2 (G1 leftover 9): `min-height:0` alone left the ACTUAL hit box at the glyph's own tiny
 * rendered size (16x18 for ×, 41x18 for "Undo") -- under the platform's usual tap-target floors
 * (28px desktop, 44px touch). `min-height`/`min-width` grow the invisible BOX only (no background/
 * border/font change, so the glyph still reads small); `inline-flex` centring keeps the glyph
 * centred inside that larger box instead of top/left-aligned in it. */
.toast .toast-x{border:0;background:none;color:inherit;cursor:pointer;padding:0 var(--sp-1);font-size:inherit;
  display:inline-flex;align-items:center;justify-content:center;min-height:28px;min-width:28px}
@media(pointer:coarse){ .toast .toast-x{min-height:44px;min-width:44px} }

/* Calendar Leg 1: shellToast's optional action button (e.g. "Undo") */
.toast .toast-action{border:0;background:none;color:inherit;cursor:pointer;padding:0 var(--sp-1);
  font-size:inherit;font-weight:var(--fw-strong);text-decoration:underline;
  display:inline-flex;align-items:center;justify-content:center;min-height:28px}
/* fix round 1 (pool, finding 19): min-width was missing here (only `.toast-x` got one) -- the
 * "Undo" label's own text+padding intrinsic width (40.8px) sat just under the 44px WCAG target
 * with nothing to floor it, unlike `.toast-x`'s icon-only box, which never had text to fall back
 * on and so always needed an explicit width floor from the start. */
@media(pointer:coarse){ .toast .toast-action{min-height:44px;min-width:44px} }

/* celebrate (micro-rewards, 2026-10-16): classes js/celebrate.js toggles for ~0.5-1s.
 * FIXED: the keyframes, the ease, the glow colour (accent). OPEN: nothing; the host only supplies the node.
 * `.cele-canvas` is the transient particle overlay (Playful/Bold only), removed with its last particle. */
.cele-canvas{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:var(--z-cele,150)}
@media (prefers-reduced-motion: no-preference){
  .cele-check{animation:cele-check var(--dur-morph) var(--ease-spring)}
  .cele-pop{display:inline-block;animation:cele-pop var(--dur-morph) var(--ease-spring)}
  .cele-in{animation:cele-in var(--dur-morph) var(--ease) backwards}
  .cele-glow{animation:cele-glow 1s var(--ease)}
}
@keyframes cele-check{0%{transform:scale(.82)}55%{transform:scale(1.1)}100%{transform:scale(1)}}
@keyframes cele-pop{0%{transform:translateY(40%) scale(.9);opacity:.4}100%{transform:none;opacity:1}}
@keyframes cele-in{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:none}}
@keyframes cele-glow{0%,100%{box-shadow:0 0 0 0 transparent}40%{box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 55%,transparent),0 0 24px 4px color-mix(in srgb,var(--accent) 30%,transparent)}}

/* --- leg 6 / s10: shared empty / error states --- */
.empty-state{text-align:center;color:var(--muted);padding:var(--sp-6) var(--sp-4)}
.empty-state .empty-ico{font-size:1.6rem;margin-bottom:var(--sp-2)}
/* 2026-10-20 empty states never sit in their own box: a cell-family surface whose ONLY content is an
 * empty state (directly, or in one wrapper <li>) drops its glass paint, so the drawing + line sit on
 * the page. Paint-only :has() (no layout, Safari 15.4+); the selector outranks glass.css's bare class rule.
 * Surfaces that hold other content keep their glass. */
.cell:has(>.empty-state:only-child),.cell:has(>li:only-child>.empty-state:only-child),
.card:has(>.empty-state:only-child),.cell--side:has(>.empty-state:only-child){background:none;box-shadow:none}
.empty-state .empty-title{margin:0;font-weight:var(--fw-strong);color:var(--fg)}
.empty-state .empty-hint{margin:var(--sp-1) 0 0;font-size:var(--fs-caption)}
/* --- empty-state spot illustrations (2026-10-16, lab 06; drawn by core/static/js/empty-art.js, which
   puts one .empty-art in .empty-ico). Line = --muted strokes + one --accent touch; clay = --clay-* tokens;
   pixel = --px-* tokens. Idle motion only under no-preference; the still frame is the base.
   Owner 2026-10-20: motion plays ~3 cycles then rests (forwards = holds its end pose, so no snap; the
   wing flaps ~18 s with its moth/bird; the leaf falls 3x then rests on the ground = base; pixel frames:
   counts set inline in empty-art.js scene() so all frames end together on frame 0). --- */
.empty-state .empty-ico.empty-art{width:168px;height:126px;margin:0 auto var(--sp-2);font-size:0}
.empty-art svg{width:100%;height:100%;display:block;overflow:visible}
.ea g,.ea path,.ea circle,.ea rect,.ea ellipse{transform-box:view-box}
.ea-a{fill:none;stroke:var(--muted);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
.ea-a *{vector-effect:non-scaling-stroke}
.ea-a .ea-ac{stroke:var(--accent)}
.ea-a .ea-acf{fill:var(--accent);stroke:none}
.ea-a .ea-acs{fill:color-mix(in srgb,var(--accent) 14%,transparent)}
.ea-a .ea-soft{opacity:.45}
.ea-a .ea-dash{stroke-dasharray:3 4}
.ea-a .ea-dot{stroke-dasharray:.5 5}
.ea-a .ea-bgf{fill:var(--bg-solid)}
.ea-a .ea-tx{fill:var(--muted);stroke:none;font:600 10px -apple-system,system-ui,sans-serif;text-anchor:middle;letter-spacing:.06em}
.ea-b{filter:var(--clay-filter)}
.ea-b .ea-cshadow{fill:var(--clay-shadow);transform-box:fill-box;transform-origin:center}
.ea-b .ea-tx{font:800 11px -apple-system,system-ui,sans-serif;text-anchor:middle;letter-spacing:.04em}
.ea-c{shape-rendering:crispEdges;image-rendering:pixelated}
.ea-c .ea-p-a{fill:var(--accent)}
.ea-c .ea-p-gr{fill:var(--muted);opacity:.35}
.ea-fr{opacity:0}
.ea-fr.ea-f0{opacity:1}
.ea-fall{transform:translate(46px,96px) rotate(30deg)}
.ea-puff{transform:translate(84px,22px) scale(1);opacity:.45}
.ea-dt{opacity:.8}
@media (prefers-reduced-motion:no-preference){
  .ea-sway{animation:ea-sway 3.6s ease-in-out 6 alternate forwards}
  .ea-flutter{animation:ea-flutter 5.5s ease-in-out 3 forwards}
  .ea-bob{animation:ea-bob 3s ease-in-out 6 alternate forwards}
  .ea-moth{animation:ea-moth 7s ease-in-out 3 forwards}
  .ea-wing{animation:ea-wing .28s ease-in-out 64 alternate}
  .ea-dt{animation:ea-dt 1.5s ease-in-out 3 forwards;opacity:.3}
  .ea-dt2{animation-delay:.25s}.ea-dt3{animation-delay:.5s}
  .ea-sweep{animation:ea-sweep 3.4s ease-in-out 6 alternate forwards}
  .ea-nudge{animation:ea-nudge 2.6s ease-in-out 6 alternate forwards}
  .ea-flick{animation:ea-flick 1.8s steps(1,end) 3 forwards}
  .ea-swing{animation:ea-swing 2.8s ease-in-out 6 alternate forwards}
  .ea-fall{animation:ea-fall 8s ease-in-out 3}
  .ea-stutter{animation:ea-stutter 3.2s cubic-bezier(.6,0,.4,1) 6 alternate forwards}
  .ea-puff{animation:ea-puff 3.6s ease-out 3 forwards}
  .ea-puff2{animation-delay:1.2s}.ea-puff3{animation-delay:2.4s}
  .ea-b .ea-cshadow{animation:ea-breathe 5s ease-in-out 6 alternate forwards}
  .ea-b .ea-float{animation:ea-float 5s ease-in-out 6 alternate forwards}
  .ea-b .ea-float2{animation:ea-float 4.2s ease-in-out -1.5s 6 alternate forwards}
  .ea-b .ea-wobble{animation:ea-wobble 2.8s ease-in-out 6 alternate forwards}
  .ea-fr{animation-timing-function:steps(1,end)}
  @keyframes ea-sway{from{transform:rotate(-5deg)}to{transform:rotate(5deg)}}
  @keyframes ea-flutter{0%,52%,100%{transform:none}68%{transform:scaleX(.8) skewY(-3deg)}84%{transform:scaleX(.95)}}
  @keyframes ea-bob{to{transform:translateY(-3px)}}
  @keyframes ea-moth{0%,100%{transform:translate(0,0)}25%{transform:translate(7px,-5px)}50%{transform:translate(1px,-9px)}75%{transform:translate(-7px,-3px)}}
  @keyframes ea-wing{to{transform:scaleX(.5)}}
  @keyframes ea-dt{0%,100%{opacity:.25}35%{opacity:1}}
  @keyframes ea-sweep{from{transform:translateX(-12px) rotate(-5deg)}to{transform:translateX(12px) rotate(5deg)}}
  @keyframes ea-nudge{to{transform:translateX(5px)}}
  @keyframes ea-flick{0%{opacity:1}35%{opacity:.1}45%{opacity:1}60%{opacity:.15}70%,100%{opacity:1}}
  @keyframes ea-swing{from{transform:rotate(-2deg)}to{transform:rotate(9deg)}}
  @keyframes ea-fall{0%{transform:translate(38px,8px) rotate(0);opacity:0}10%{opacity:1}35%{transform:translate(52px,40px) rotate(70deg)}60%{transform:translate(36px,70px) rotate(140deg)}85%{transform:translate(46px,96px) rotate(200deg);opacity:1}100%{transform:translate(46px,96px) rotate(200deg);opacity:0}}
  @keyframes ea-stutter{0%{transform:rotate(0)}55%{transform:rotate(32deg)}62%{transform:rotate(24deg)}100%{transform:rotate(26deg)}}
  @keyframes ea-puff{0%{transform:translate(80px,38px) scale(.4);opacity:0}20%{opacity:.7}100%{transform:translate(94px,4px) scale(1.5);opacity:0}}
  @keyframes ea-float{from{transform:translateY(0)}to{transform:translateY(-6px)}}
  @keyframes ea-breathe{from{transform:scale(1);opacity:1}to{transform:scale(.84);opacity:.6}}
  @keyframes ea-wobble{from{transform:rotate(-2.5deg)}to{transform:rotate(2.5deg)}}
  @keyframes ea-fr2{0%{opacity:1}50%{opacity:0}100%{opacity:0}}
  @keyframes ea-fr3{0%{opacity:1}33.333%{opacity:0}100%{opacity:0}}
  @keyframes ea-fr4{0%{opacity:1}25%{opacity:0}100%{opacity:0}}
}
.error-state{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--sp-3);color:var(--muted);padding:var(--sp-5) var(--sp-4)}
.error-state .error-msg{margin:0}

/* map-s1: shared "View on map" link, one primitive for every module's row */
.maplink{display:inline-block;margin-left:var(--sp-2);font-size:var(--fs-caption);color:var(--link);white-space:nowrap}
/* --- s08 capture / propcard.js shared styling (moved from modules/lifeos/static/css/app.css in
   s33 so every page that loads propcard.js gets these, not just app.css-loading pages; Leg 4 of
   docs/plans/2026-09-04-nav-settings-widgets.md made this page-independence the norm -- the
   platform dock's Capture panel lazy-mounts propcard.js + capture.js on ANY page) --- */
.cap-card{padding:var(--sp-4);margin-bottom:var(--sp-4)}
.cap-card h3{margin:0 0 var(--sp-2);font-size:1rem}
.cap-label{display:block;font-size:.88rem;color:var(--muted);margin-bottom:var(--sp-1)}
/* components.css styles input/select but NOT textarea -- theme it explicitly or it renders
   UA-default white inside a dark card (the s04 #evnote finding). Leg A2 (A4): drops its own
   resize:vertical override -- the shared textarea{resize:none} rule above now governs every
   note box, capture's included; capture.js sets rows=4. */
#capbox{width:100%;min-width:0;box-sizing:border-box;font:inherit;
  padding:.45rem .55rem;border:1px solid var(--bd);border-radius:var(--radius-sm);
  background:var(--bg);color:var(--fg)}
#capbox:focus{outline:2px solid var(--accent);outline-offset:-1px;border-color:transparent}
.cap-actions{display:flex;justify-content:flex-end;gap:var(--sp-2);margin-top:var(--sp-2)}   /* gap: Capture + Begin (timelog L5) sit side by side */
.cap-disclose{margin:var(--sp-2) 0 0;font-size:var(--fs-caption);color:var(--muted)}
#capprop{margin-top:var(--sp-3)}
.cap-row{padding:var(--sp-3);margin-bottom:var(--sp-3);background:var(--panel)}
.cap-head{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
.cap-head .cap-x{margin-left:auto}
.cap-icon{flex:none;color:var(--muted)}
.cap-conf{font-size:var(--fs-caption);color:var(--muted)}
.cap-src{font-size:var(--fs-caption);font-weight:600;letter-spacing:.04em;line-height:1;
  padding:.15em .45em;border:1px solid var(--bd);border-radius:var(--radius-sm);
  color:var(--accent);background:transparent;cursor:help}
.mb-ai .cap-src{margin-right:var(--sp-2);vertical-align:middle}  /* morning-pass narrative badge (lifeos app.js) */
.cap-why{margin:var(--sp-2) 0 var(--sp-2);font-size:var(--fs-caption);color:var(--muted)}
.cap-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--sp-2)}
.cap-fields label{display:flex;flex-direction:column;gap:var(--sp-1);
  font-size:var(--fs-caption);color:var(--muted);min-width:0}
/* fix round (pool-gaps round 2, finding 1): `width:100%` used to be unconditional here (every
 * `.cap-fields` input/select, direct child or nested inside an `.f-pair` pair alike). Two kinds
 * of `.w-date`/`.w-time` field live under `.cap-fields`: a LONE one (spend/note's "Date", a direct
 * `label>input` child) and a PAIRED one (event/todo's Start/End/Due, `label>.f-pair>input` --
 * `.f-pair` is the shared "single datum, two controls" recipe, FIELD SIZING section below, whose
 * own `.f-pair>*{flex:none}` already opts every pair's children OUT of stretching to fill the row,
 * same as `.composer .field>input:not(.w-date)...` already excludes `.f-pair`-nested fields from
 * ITS OWN width:100% for the identical reason -- `.cap-fields` never carried that exclusion.
 * `width:100%` now only reaches the direct-child case; a paired field keeps its own correctly-
 * sized `.w-date{width:12ch}`/`input.time24{width:6.5em}` (FIELD SIZING below / shell.css)
 * untouched, exactly like every other `.f-pair` on the platform. */
.cap-fields label>input,.cap-fields label>select{min-width:0;width:100%}
.cap-fields .f-pair{min-width:0}
/* round 2 (G1 leftover 7): the event kind's Start/End pair (date `.w-date`, 12ch, + time
 * `input.time24`, 6.5em, both `flex:none` -- never shrink) doesn't fit the grid's own
 * `minmax(11rem,1fr)` label column at desktop widths (1024-1440), so the time field ran under the
 * NEXT label's date field / the All-day checkbox -- an overlap, not just tightness. `.f-pair`'s
 * base recipe (`display:inline-flex`, no wrap, used platform-wide for quantity+unit/currency+
 * amount/etc, which DO fit their own composers) is left untouched; wrapping is scoped to THIS grid
 * only, letting a pair that doesn't fit stack its two controls on separate lines within the same
 * column instead of overflowing into a neighbor -- the field widths themselves (12ch/6.5em) stay
 * exactly what shell.js's adopters already set. */
.cap-fields .f-pair{flex-wrap:wrap}
/* fix round (pool-gaps, finding 6): the blanket `box-sizing:border-box` above used to also catch
 * the shell-adopted `.w-date`/`.w-time` fields -- glass.css's own field-min-height rule assumes
 * those two stay CONTENT-BOX (its calc() subtracts padding+border to land the total box on the
 * 36px floor; a `.w-*` field is deliberately excluded from the platform's OTHER field rule, the one
 * that pins border-box + the un-subtracted floor, exactly so it keeps that content-box pairing).
 * Forcing border-box here broke that pairing ONLY inside a capture card, landing them 4px short of
 * every sibling field (Title/Location/Note/the kind select) in the same row. Excluding them here
 * restores the content-box the rest of the platform's `.w-date`/`.w-time` fields already assume,
 * without touching their own `width:100%` (unaffected by box-sizing either way). */
.cap-fields input:not(.w-date):not(.w-time),.cap-fields select{box-sizing:border-box}
/* fix round (pool-gaps round 2, finding 1 continued): the exclusion above keeps the LONE `.w-date`
 * (spend/note's "Date") content-box, matching glass.css's field-material pairing -- but the
 * `width:100%` two rules up sets the CONTENT box only, so --field-pad-x/--field-bd-w then add on
 * TOP of it, overflowing the field's grid cell by exactly 2*padding + 2*border (~18px): at 1440
 * wide enough for Start date to overlap End date, at 390 wide enough to run past the card's right
 * edge (the `event` kind's Start/End are the PAIRED case above, not this one, but both kinds of
 * `.w-date` shared the one unqualified `box-sizing:border-box` exclusion, so both regressed
 * together). Giving the lone case a calc()'d width that PRE-SUBTRACTS the same padding+border the
 * content-box adds back lands it on the full 100% without touching box-sizing (would re-break its
 * height) or `.w-date{width:12ch}` elsewhere on the platform (this selector only reaches inside
 * `.cap-fields`). Scoped to `label>input` again on purpose: a plain `calc(100% - Npx)` behaves
 * DIFFERENTLY from a bare `width:100%` against an indefinite-width container in WebKit (it resolves
 * the percentage term against the next ANCESTOR with a definite size instead of falling back to
 * content size) -- reaching into `.f-pair` with the SAME calc() replaced an ~80px pre-existing
 * paired-field overflow with a ~300px one (measured: r3h_390.py, `.f-pair` children [359,671] on a
 * 312px-wide card at 390 vs main's own [359,440]) even though the paired case no longer matches
 * `width:100%` at all any more (previous paragraph) -- `.f-pair`'s own indefinite sizing is exactly
 * why it must never receive a calc()'d percentage width, only ever its own fixed ch/em one. */
.cap-fields label>input.w-date{width:calc(100% - 2*var(--field-pad-x) - 2*var(--field-bd-w))}
.cap-fields input[type=checkbox]{width:auto;align-self:flex-start}
#capbar{display:flex;gap:var(--sp-2);margin-top:var(--sp-2)}
/* Leg 20 / r5 S-CAP: the batch's project picker (pooled select + .cap-label, placement only) takes its own full-width row above the cards. */
#capprojectrow{display:flex;align-items:center;gap:var(--sp-2);min-width:0;margin-top:var(--sp-2)}
#capprojectrow[hidden]{display:none}
#capprojectrow .cap-label{margin:0;flex:none}
#capprojectrow select{flex:1 1 auto;min-width:0;max-width:100%}
#capstatus{margin-top:var(--sp-2);color:var(--muted);font-size:var(--fs-caption)}
#capstatus a{margin-left:var(--sp-2)}
/* map-s12 capture pin offer, moved here from modules/lifeos/static/css/app.css alongside the rest
   of .cap-* (Leg 4, 2026-09-04): capture.js runs on any page now, via the platform dock. */
.cap-pin{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);margin-top:var(--sp-2);font-size:var(--fs-caption)}
.cap-pin[hidden]{display:none}
.cap-pin-msg{color:var(--muted);min-width:0;overflow-wrap:anywhere}
.cap-pin-go,.cap-pin-chg{white-space:nowrap}
.cap-pin-chg{background:none;border:0;color:var(--link);padding:0;text-decoration:underline;cursor:pointer}

/* Email 2FA settings dialog (shell.js user menu) */
.otpdlg{background:var(--bg);color:var(--fg);border:1px solid var(--bd);border-radius:var(--radius);box-shadow:var(--shadow-1);padding:var(--sp-5);max-width:380px;width:calc(100% - 2*var(--sp-4))}
.otpdlg::backdrop{background:rgb(0 0 0/.35)}
.otpdlg p{margin:0 0 var(--sp-3);line-height:var(--lh-normal)}
.otpdlg input{width:100%;box-sizing:border-box;margin:0 0 var(--sp-3)}
.otpdlg .otpdlg-acts{display:flex;gap:var(--sp-2);justify-content:flex-end}
.otpdlg .err{color:var(--down);font-size:var(--fs-caption);min-height:1.2em;margin:0 0 var(--sp-2)}
/* .otpdlg .method* rules removed (Leg 2 verifier follow-up, 2026-09-04): Sign-in methods moved
   OFF this dialog onto the Settings page inline (core/static/css/settings.css's .authsec .method
   now owns that styling); .otpdlg itself stays -- shell.js's re-auth ("sudo mode") dialog still
   uses it. */
@media (max-width:480px){ .cap-fields{grid-template-columns:1fr} }

/* --- Capture sheet on a phone (phone shell A leg 6, 2026-10-23): the dock panel is already a bottom sheet <=640px. A big borderless
   text field, the detected kind as chips (read-only: they reflect the parse), the parsed facts as inset rows (a tap opens that
   proposal's full editable card, hidden until then), and the confirm button naming what it adds. #capkinds/#capsum are display:none
   above the sheet breakpoint (id selectors out-rank `.rowlist{display:grid}`), so the desktop panel does not move. --- */
#capkinds,#capsum,.cap-info{display:none}
@media (max-width:640px){
  .cap-label[for=capbox]{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
  #capbox{font-size:var(--fs-title);line-height:var(--lh-tight);font-weight:400;border:0;background:none;padding:0;min-height:0;overflow:hidden}   /* owner 2026-10-23: display size too big; --fs-title 16.8px stays >=16 so iOS does not zoom on focus */
  #capbox::placeholder{color:var(--muted);font-size:inherit;font-weight:inherit;opacity:1}
  #capbox:focus,#capbox:focus-visible{outline:none;box-shadow:none}
  .cap-actions{align-items:center;gap:var(--sp-2)}
  .cap-actions #capgo{flex:1}
  #capdisc:not(.cap-disc-open){display:none}
  .cap-info{display:inline-flex;align-items:center;justify-content:center;min-height:0;min-width:0;width:auto;padding:var(--sp-1);border:0;background:none;box-shadow:none}
  #capkinds:not([hidden]){display:flex;flex-wrap:wrap;gap:var(--sp-2);margin-top:var(--sp-3)}
  .cap-kchip{display:inline-flex;align-items:center;gap:var(--sp-1)}
  #capsum:not([hidden]){display:grid;margin-top:var(--sp-3)}
  .cap-sumrow{display:flex;align-items:center;gap:var(--sp-3);width:100%;text-align:left;border:0;border-radius:0;
    background:none;box-shadow:none;color:var(--fg);font:inherit;padding:0 var(--sp-3);cursor:pointer}
  .cap-sumico{flex:none;display:inline-flex;color:var(--muted)}
  .cap-sumtx{flex:1;min-width:0;overflow-wrap:anywhere;font-weight:500}
  .cap-sumchev{flex:none;color:var(--muted)}
  @media (prefers-reduced-motion:no-preference){ .cap-sumchev{transition:rotate var(--dur-fast) var(--ease)} }
  .cap-sumrow[aria-expanded=true] .cap-sumchev{rotate:90deg}
  #capprop .cap-row:not(.cap-open){display:none}
  #capconfirm{flex:1}
}

/* --- Capture BAR (docs/plans/2026-10-23-timelog.md L5, owner picked variant B): the dock panel is one box that does Search, Capture and
   Begin. Inline hits reuse the search overlay's .lsrow rows; Begin is the pooled primary with the --up accent (primary--ok); holding it
   (450 ms) draws a ring and fans out the recent activities. Motion is opacity/transform only; reduced motion = none (the kill-switch below
   plus the guards here). Layout of the bar itself (pill, panel, order) lives in shell.css under the dock. --- */
.cap-res{min-width:0}
.cap-hits{display:grid;padding:var(--sp-1) 0}
.cap-hits[hidden]{display:none}
.cap-hit-all{justify-self:end;color:var(--link)}
@property --hold{syntax:'<number>';inherits:false;initial-value:0}
.cap-begin{position:relative}
.cap-begin::before{content:"";position:absolute;inset:-3px;border-radius:inherit;padding:3px;pointer-events:none;opacity:0;
  background:conic-gradient(var(--up) calc(var(--hold) * 1%),transparent 0);
  -webkit-mask:linear-gradient(black 0 0) content-box,linear-gradient(black 0 0);-webkit-mask-composite:xor;mask-composite:exclude}
.cap-recent{position:absolute;right:0;bottom:calc(100% + var(--sp-2));z-index:7;min-width:14rem;max-width:min(22rem,86vw);max-height:min(50dvh,20rem);overflow:auto;
  padding:var(--sp-2);display:grid;gap:2px;background:var(--bg-solid);border:1px solid var(--bd);border-radius:var(--radius);box-shadow:var(--shadow-2)}
.cap-recent .cap-recent-i{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);width:100%;min-height:40px;text-align:left;font:inherit;color:var(--fg)}
.cap-recent-t{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cap-recent-d{flex:none;color:var(--muted);font-size:var(--fs-caption);font-variant-numeric:tabular-nums}
.cap-recent-empty{margin:0;padding:var(--sp-2) var(--sp-3);color:var(--muted)}
@media (prefers-reduced-motion:no-preference){
  .cap-hit,.cap-hit-all{animation:cap-hit-in var(--dur) var(--ease) both;animation-delay:calc(var(--i,0) * 24ms)}
  @keyframes cap-hit-in{from{opacity:0;transform:translateY(6px)}}
  .cap-begin.cap-holding::before{opacity:1;animation:cap-hold 450ms linear forwards}
  @keyframes cap-hold{to{--hold:100}}
  .cap-recent-i{animation:cap-rin 220ms var(--ease-spring) both;animation-delay:calc(var(--i,0) * 28ms)}
  @keyframes cap-rin{from{opacity:0;transform:translateY(10px) scale(.96)}}
}
@media (max-width:640px){
  .cap-recent{top:calc(100% + var(--sp-2));bottom:auto;left:0;right:0;max-width:none}
  @media (prefers-reduced-motion:no-preference){ @keyframes cap-rin{from{opacity:0;transform:translateY(-10px) scale(.96)}} }
}

/* leg 6 / ADR 0004: blanket reduce-motion kill-switch — neutralizes ALL animation/transition,
 * new and pre-existing (e.g. the body/button transitions above), per the OS accessibility setting. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.01ms!important;
    animation-iteration-count:1!important;
    transition-duration:.01ms!important;
    scroll-behavior:auto!important;
  }
}

/* ============================ SCROLLBARS ============================
 * Slim, theme-aware scrollbars for the whole platform (owner ask: the default ~15px system bars
 * were bland and ate layout width inside sidebars, panels and popovers). The thumb is drawn inset
 * by a transparent border with background-clip:content-box, so an 8px lane reads as a 4px hairline
 * pill at rest; it thickens to 6px and brightens while its scroll container is hovered, and takes
 * the palette accent when grabbed. Track/corner are transparent so panel backgrounds run edge to
 * edge. Colors derive from --fg/--accent, so all 8 palettes x light/dark follow automatically.
 * The games hub keeps its own copy of this block (games pages load games-hub.css only). */
:root{
  --sb-idle:color-mix(in srgb,var(--fg) 18%,transparent);
  --sb-live:color-mix(in srgb,var(--fg) 38%,transparent);
}
/* Chrome 121+/Safari 18.2+ IGNORE every ::-webkit-scrollbar-* rule as soon as the standard
 * scrollbar-width/scrollbar-color are set on the element, so the standard pair is gated to
 * engines without the pseudos (Firefox) and the pseudos own the styling everywhere else. */
@supports not selector(::-webkit-scrollbar){
  *{scrollbar-width:thin; scrollbar-color:var(--sb-idle) transparent}
}
/* A styled ::-webkit-scrollbar is a CLASSIC (non-overlay) bar: it takes its 8px from the viewport
 * whenever the document scrolls, so a max-width/margin:auto container re-centres 4px to the left on
 * every view that overflows and back on one that doesn't (ledger Overview -> Entries, 2026-09-12).
 * Reserve the lane permanently so switching views never shifts the page. Only a non-visible
 * overflow value gets a gutter (spec), hence the explicit auto. Page-fixed documents (shell.css
 * sets html.page-fixed-doc overflow:hidden, which still reserves the gutter) keep the SAME lane on
 * purpose: the topbar brand must sit at one x on every page, scrolling or not (owner 2026-09-12). */
html{overflow-y:auto;scrollbar-gutter:stable}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track,::-webkit-scrollbar-corner{background:transparent}
::-webkit-scrollbar-thumb{background-color:var(--sb-idle);background-clip:content-box;
  border:2px solid transparent;border-radius:var(--radius-pill)}
@media (hover:hover){ *:hover::-webkit-scrollbar-thumb {background-color:var(--sb-live);border-width:1px} }
::-webkit-scrollbar-thumb:active {
  background-color:color-mix(in srgb,var(--accent) 62%,var(--sb-live))}
@media (hover:hover){ ::-webkit-scrollbar-thumb:hover {
  background-color:color-mix(in srgb,var(--accent) 62%,var(--sb-live))} }

/* ============================ COMPOSER RECIPE ============================
 * Rule 3 (docs/plans/2026-09-04-module-ui-review.md §0, Leg A.4): one line per field in every
 * composer/form. Label left (fixed column), control right, full width, one field per row. The
 * only pairs allowed on one row are a single datum split into two controls: quantity+unit,
 * currency+amount, start+end time, home+work place. Below 48rem the label stacks above the
 * control. Modules use these classes — they do not re-invent their own grid layout.
 * Markup: <label class=field><span class=f-lbl>Name</span><input …></label>; a pair (a <div>, with
 * aria-labelledby on each input since one label can't own two controls):
 * <div class=field><span class=f-lbl>Quantity</span><span class=f-pair><input><input></span></div> */
/* box-inset-alignment 2026-09-17 (img3-token-form.png): the shared recipe carried no padding/
   radius of its own, so a bare .composer sitting directly in glass.css's filled/ringed box
   (background:var(--cell-solid) + a 1px ring) had its labels/controls/buttons flush against the
   box edge. Module-local overrides that already gave a composer its own padding (or deliberately
   zero it, e.g. todo's docked #todoform sharing a section row's edges) still win by specificity/
   cascade order — this only fixes the previously-unpadded default. */
.composer{display:grid;gap:var(--sp-3);padding:var(--sp-4);border-radius:var(--radius)}
/* fix round 1 (finding 1): every level of this nested grid/flex stack has the SAME gap -- a grid
 * item's automatic-minimum-size, when nothing overrides it, falls back to its own content
 * requirement, and `justify-items:stretch` (the default) can GROW an item past its track but never
 * SHRINKS it below that minimum. Three levels here each need their own `min-width:0` floor, or a
 * narrow composer (this pool recipe is used everywhere, incl. Ledger's Recurring-bills card, a
 * 20rem sidebar) forces the WHOLE row wider than the composer itself:
 *   1. `.field`/`.row-actions` are items of `.composer`'s own implicit single-column grid -- ONE
 *      field whose control can't shrink (a fixed ch-width, or WebKit's own UA-default preferred
 *      width for a plain `<input>`, see below) forces that ONE shared column past the container,
 *      and every sibling row -- including the last, `.row-actions` (`justify-content:flex-end`) --
 *      stretches to match and pokes its content out the right edge along with it. Reproduces
 *      identically on main (verified: byte-identical computed rects on port 8141), so this was
 *      always latent, just unmasked here by the recurring-bill form's mix of a plain-text Name
 *      field (next rule) and a Category field whose control is a whole wrapper (next-next rule) --
 *      every OTHER composer's fields already fit inside their container.
 *   2. `.field`'s OWN children (its label cell and its control cell, whatever the control is --
 *      plain input, select, or a plain wrapper span around something more elaborate like Category's
 *      input+chips) are items of `.field`'s OWN 2-column grid; without their own floor, the SAME
 *      forcing happens one level down, this time stretching `.field` itself past `.composer`'s now-
 *      correctly-shrunk column (a locally-contained overflow, at least -- it stops widening its
 *      siblings, but still visibly overflows its own row/card).
 *   3. A control that has an intrinsic/explicit size of its own (a plain `<input>`'s UA-default
 *      preferred width when it has no `size`; `.w-soft`'s explicit `width:26ch`, below) still won't
 *      shrink just because its ANCESTORS can now shrink -- it needs its own escape hatch too
 *      (width:100% for the plain input; max-width:100% for `.w-soft`, both paired with min-width:0
 *      so the browser's automatic-minimum algorithm doesn't override them with the unshrunk size). */
.composer .field,.composer .row-actions{min-width:0}
.composer .field{display:grid;grid-template-columns:minmax(6rem,8.5rem) 1fr;gap:var(--sp-3);align-items:center}
.composer .field>*{min-width:0}
.composer .field>.f-lbl{color:var(--muted);font-size:var(--fs-caption)}   /* label cell; ⓘ goes inside it */
/* .f-hint: one muted live line under a control, inside the field's control cell (Journal: "ends next day"). */
.f-hint{display:block;margin-top:var(--sp-1);color:var(--muted);font-size:var(--fs-caption)}
.f-hint:empty{display:none}
/* field-sizing plan (2026-09-04): a pair's own controls size to THEIR data (see .f-pair below,
   FIELD SIZING section), not to a 50/50 split of the row -- so the width:100% rule below only
   ever reaches a field's DIRECT input/select/textarea child, never one nested inside .f-pair.
   It also excludes a direct child carrying its own w-* sizing class (e.g. the date/time
   adopters' .w-date/.w-time, an explicit `width` so grid's default item-stretch never widens it)
   so THAT class's width wins instead of 100%. */
.composer .field>input:not(.w-date):not(.w-time):not(.w-num):not(.w-amount):not(.w-short):not(.w-soft):not(.w-ccy),
.composer .field>textarea{width:100%;box-sizing:border-box}
/* a composer <select> is never free text -- it stays content-fit (the shared select recipe below
   already sets field-sizing:content;min-width:8ch), so JPY/monthly boxes are as wide as their text
   instead of stretching to the field column's full width. */
.composer .row-actions{display:flex;justify-content:flex-end;gap:var(--sp-2);margin-top:var(--sp-2)}
/* .field.full: no label cell (the control already has its own aria-label) -- the control spans
   the whole row instead of sharing it with an empty label column. */
.composer .field.full{grid-template-columns:1fr}
@media (max-width:48rem){.composer .field{grid-template-columns:1fr;gap:var(--sp-1)}}
/* field--switch: declared variant of the composer row for a switch (label left, the pooled .switch right) -- it keeps its two
   columns at phone width instead of stacking the label over a 38px control. Sets only the open grid-template-columns and gap. */
@media (max-width:48rem){.composer .field.field--switch{grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-3)}}

/* ============================ FIELD SIZING (docs/plans/2026-09-04-field-sizing.md) ============
 * Owner: "box size should be proper size depending on the data to be filled on the box." A box is
 * as wide as its longest legal value, plus padding; only free text (title, URL, notes, webhook,
 * search) fills the row. `ch` units so widths scale with the font. The date/time adopters
 * (shell.js) apply .w-date/.w-time automatically; every other class is applied in page HTML. */
.w-date{width:12ch;flex:none}                    /* YYYY/MM/DD, set by shell.js's date adopter */
/* width lives ONLY in shell.css's input.time24{width:6.5em} (a class+element selector always
   beats this bare-class one on specificity, so a width:7ch here was dead weight); .w-time itself
   stays as the marker class shell.js's time adopter sets, still needed by the :not(.w-time)
   exclusions elsewhere (components.css/calendar.css/settings.css) and for its own flex:none. */
.w-time{flex:none}                                /* HH:MM, set by shell.js's time adopter */
.w-num{width:8ch;flex:none}                       /* counts, percents, days */
.w-amount{width:14ch;flex:none;text-align:right;font-variant-numeric:tabular-nums}  /* money */
.w-ccy{width:auto;min-width:6ch;field-sizing:content;flex:none}   /* currency <select>: JPY/THB */
.w-short{width:16ch;flex:none}                    /* symbols, tickers, codes */
/* fix round 1 (finding 1, continued): `.w-soft` is never used inside an `.f-pair` (its `flex:none`
 * is dead weight today, kept for a future pair) -- every current use sits alone in a plain wrapper
 * span (Ledger Category, Orders ETA/balance due), so `max-width:100%` is a plain block-context
 * percentage against that wrapper, no flex ambiguity. Ledger's Recurring-bills composer lives in a
 * 20rem sidebar column (262px content), narrower than 26ch (~234px) plus its own label -- capping
 * at 100% (+ min-width:0, same reasoning as `.composer .field`/`.row-actions` above) lets it shrink
 * to fit instead of forcing the row's own 2-column split past the card/aside. The field stays
 * content-box (unlike the platform's other sized fields, this class is deliberately "N characters
 * PLUS padding", per this same rule's own comment below), so the 100% cap has to subtract the
 * padding+border it will still add on top, or a clamped box would overflow its own wrapper by
 * exactly that amount (measured: 18px = 2 * (field-pad-x 8px + field-bd-w 1px), the residual gap
 * finding 1's own fix round left before this line was added). */
.w-soft{width:26ch;max-width:calc(100% - 2 * (var(--field-pad-x) + var(--field-bd-w)));min-width:0;flex:none}         /* soft dates ("2026/11 · Q4 2026 · TBA", orders ETA/balance due) and any ≤40-char label-like text: category, cash source, connection name (2026-09-11 text-box size pass) */
.w-full{width:100%;flex:1 1 auto}                 /* explicit opt-in for free text in a flex row */
/* .f-pair: the ONLY multi-control cell allowed on one row (qty+unit, currency+amount, start+end
   time, home+work place) -- reads as one datum, never stretches to fill the row. */
.f-pair{display:inline-flex;gap:var(--sp-2);align-items:center}
.f-pair>*{flex:none}
.f-pair>.w-full{flex:1 1 auto;min-width:0}   /* a search box in a pair fills the row, per plan */
/* field-sizing deep-verify (2026-09-04): .composer .field is display:grid with the default
   justify-items:stretch, so a select's own width:auto;field-sizing:content (set above) was still
   stretched to fill the 1fr track -- justify-self:start on the select itself is what actually
   lets its content-sized width show through the grid cell. */
.composer .field>select{justify-self:start}

/* ============================ CATEGORY/TAG CHIPS (ruled 2026-09-11/2026-09-04, promoted
 * platform-shared 2026-09-18, fix/quiet-controls-and-category-chips) ===========================
 * The decided design for a composer's Category (or any curated-option) field: a wrapping chips
 * row directly under the input, INSIDE THE SAME FIELD CELL (docs/plans/2026-09-04-module-ui-
 * review.md: "the #catchips row sits under the input inside the same cell") — click a chip fills
 * the input, `.active` marks whichever chip matches the field's current value, and the input's own
 * datalist stays so free text is always still allowed (docs/plans/2026-09-11-liquid-glass.md §6:
 * "Replace the bare DISTINCT datalist with a curated ... set shown as chips ... plus the
 * datalist"). Ledger (modules/ledger/static/js/app.js's _renderCatChips/_updateChipActive) is the
 * original implementation; this block moved out of its private app.css copy so a second module
 * (Journal) can share the exact recipe instead of re-deriving it — see DESIGN.md's Do's and
 * Don'ts for the dated ruling. A composer WITHOUT its own e-cat visual override (border/radius/
 * fill) just gets the platform's plain glass-lite <button> pill for free, which is enough. */
.catchips{display:flex;flex-wrap:wrap;gap:var(--sp-2);margin-top:var(--sp-2)}
.catchips .e-cat{cursor:pointer;font:inherit}
@media (hover:hover){ .catchips .e-cat:hover {background:var(--hover)} }
.catchips .e-cat.active{background:var(--accent);color:var(--accent-fg);border-color:transparent}

/* .ac — autocomplete dropdown (pooled; stocks symbol search + journal category, journal round 2
 * 2026-09-21 moved it here from modules/stocks/static/css/app.css so a second module could share
 * it byte-identical instead of re-deriving it). */
/* post-pass (2026-09-26, r2 final review finding "components.css .ac{...font-size:.9rem}", pre-
   existing on main): .9rem was an off-ramp literal (no token in tokens.css's own scale equals
   .9rem) -- every comparable dropdown/menu row (.more-menu/.colpick-menu/.gs-menu) sets no
   font-size of its own at all, reading the page's plain body text, so --fs-body (15px, the
   closest step to .9rem/14.4px besides) is the one that actually matches how every sibling menu
   already reads, not just the nearest number. */
.ac{position:absolute;background:var(--bg);border:1px solid var(--bd);border-radius:var(--radius-sm);z-index:50;max-height:14rem;overflow:auto;box-shadow:var(--shadow-sm);font-size:var(--fs-body);display:none}
.ac div{padding:.3rem .5rem;cursor:pointer;border-bottom:1px solid var(--bd2)}@media (hover:hover){ .ac div:hover {background:var(--hover)} }
/* keyboard 'active option' (aria-activedescendant) needs a STRONGER, visible focus indicator than the
 * subtle mouse :hover tint — accent fill + inset outline + weight, so sighted keyboard users can see it
 * and active vs hover are distinguishable (WCAG 2.4.7 focus visible / 1.4.11 non-text contrast). */
.ac div.acsel{background:var(--accent);color:var(--accent-fg);outline:2px solid var(--accent);outline-offset:-2px;font-weight:600}
.ac div.acsel small,.ac div.acsel b{color:inherit}
/* the "Create «q»" row (journal's category combobox, auto-create) -- a link-coloured row so it
 * reads as an action distinct from a plain match. */
.ac div.ac-new{color:var(--link)}

/* ============================ DATE PICKER POPOVER (Leg F, 2026-09-05) =========================
 * window.shellDatePicker (shell.js): one shared mini-calendar popover for every adopted YYYY/MM/DD
 * field, re-parented into the field's own open <dialog> (or <body> when there is none) and
 * positioned like the search combobox's .ac dropdown -- position:fixed so the SAME
 * getBoundingClientRect() math places it correctly inside a dialog's top layer too (verifier
 * round: appending to <body> alone left it inert/invisible under a showModal() dialog). Month-date
 * mode (a Mo-Su day grid) when the field holds a valid date; year-month mode (a 4x3 month grid)
 * when it's empty/invalid. z-index 50: above .rem-detail/#calpop (45) which can host an adopted
 * date field of their own, below .dock-backdrop (80). Tokens only. */
/* 2026-09-11 (owner: "date selection: use apple calendar ui?"): the Apple compact date picker
 * idiom -- "November 2026 ⌄" at the left with ‹ › at the right, single-letter muted weekday row,
 * PLAIN numbers (no per-cell circle: the platform's bare `button` glass-lite recipe used to leak a
 * rim + edge onto every day), the selected day the only filled circle, today in the accent colour,
 * month grid in year mode as plain text. Tokens only. */
.dpick{position:fixed;z-index:50;background:var(--bg);border:1px solid var(--bd);
  border-radius:var(--radius);box-shadow:var(--shadow-sm);padding:var(--sp-3);
  font-size:var(--fs-body);max-width:calc(100vw - 2rem)}
.dpick[hidden]{display:none}
.dpick-hd{display:flex;align-items:center;gap:var(--sp-1);margin-bottom:var(--sp-2)}
.dpick-title{flex:1;text-align:left;background:none;border:0;box-shadow:none;color:var(--fg);
  font:inherit;font-weight:var(--fw-strong);cursor:pointer;padding:.25rem .5rem;margin-left:-.5rem;
  border-radius:var(--radius-sm);font-variant-numeric:tabular-nums;
  display:flex;align-items:center;gap:.3rem;min-height:0;min-width:0}
.dpick-title::after{content:'';width:.45em;height:.45em;border-right:1.5px solid var(--accent);
  border-bottom:1.5px solid var(--accent);transform:translateY(-.15em) rotate(45deg);flex:none}
@media (hover:hover){ .dpick-title:hover {background:var(--hover)} }
.dpick-nav{background:none;border:0;box-shadow:none;color:var(--accent);font-size:var(--fs-title);
  font-weight:var(--fw-strong);line-height:1;padding:0;width:2rem;height:2rem;min-width:0;min-height:0;
  cursor:pointer;border-radius:var(--radius-pill);display:flex;align-items:center;justify-content:center}
@media (hover:hover){ .dpick-nav:hover {background:var(--hover)} }
.dpick-grid{display:grid;grid-template-columns:repeat(7,2.25rem);gap:0}
.dpick-grid.dpick-ym{grid-template-columns:repeat(3,1fr);gap:.25rem}
.dpick-dow{color:var(--muted);text-align:center;font-size:var(--fs-caption);font-weight:var(--fw-strong);
  text-transform:uppercase;line-height:1.6rem}
.dpick-cell{background:none;border:0;box-shadow:none;color:var(--fg);border-radius:var(--radius-pill);
  aspect-ratio:1;line-height:1;cursor:pointer;font:inherit;font-variant-numeric:tabular-nums;
  min-width:0;min-height:0;padding:0;display:flex;align-items:center;justify-content:center}
.dpick-grid.dpick-ym .dpick-cell{aspect-ratio:auto;padding:.5rem 0;border-radius:var(--radius-sm)}
@media (hover:hover){ .dpick-cell:hover {background:var(--hover)} }
.dpick-cell.dpick-today{color:var(--accent);font-weight:var(--fw-strong)}
.dpick-cell.dpick-sel {background:var(--accent);color:var(--accent-fg)}
@media (hover:hover){ .dpick-cell.dpick-sel:hover {background:var(--accent);color:var(--accent-fg)} }
.dpick-cell.dpick-disabled{color:var(--muted);opacity:.4;cursor:default}
.dpick-cell.dpick-focus{outline:2px solid var(--accent);outline-offset:-2px}
@media (pointer:coarse){
  .dpick-grid{grid-template-columns:repeat(7,2.75rem)}
  .dpick-cell{min-width:44px;min-height:44px}
}
}

/* ============================ ADD-ENTRY PLACEHOLDER (`.ghost-box`, 2026-09-18) ================
 * THE one recipe for a "dashed ghost of the list's next row, first in the list; click turns it
 * into the inline composer" add control (owner pick 2026-09-03, reconfirmed 2026-09-18: "old
 * design is better"). Before this pass the same idea was drawn five different ways (Journal lost
 * its dashes, Ledger's were invisible under glass.css's opaque-cell fill, Orders/Inventory/Stocks
 * each kept a private copy) -- modules add NO ghost CSS of their own beyond their own margins and
 * whatever sits in the trailing hint slot (an amount, a status chip, live stats). Markup contract:
 * <button class="ghost ghost-box" aria-expanded=false aria-controls=…><span class=ghost-lead
 * aria-hidden=true><svg plus/></span><span class=ghost-txt>Label</span><span
 * class=ghost-hint>…</span></button>, wrapped in <div class="entry-card row-add">…<form
 * class=composer hidden>…</form></div>. A ghost/add control carries NO glass rim/ring/edge
 * (box-shadow:none, enforced in glass.css's dashed-ghost group) -- the dashed border IS its own
 * chrome. Exemptions (native-idiom, not this recipe): To-Do's hover-revealed `.ghost-add-btn`
 * (Apple Reminders), Contacts' add-field row (Apple Contacts), Today's `.mod-ghost` module tile,
 * Map's `button.ghost` (an unrelated bordered button, not a `.row-add` add control). */
/* P2 pool (2026-09-25, mock .ghost): min-height 52->48px, padding goes padding-block:0 (was
 * --sp-3/10px, the horizontal --sp-4/14px is unchanged so it's still the same inset as the row it
 * sits above), lead shrinks 36px dashed circle -> 24px SOLID-ring circle. `--ghost-bd` (24% --fg)
 * is kept, not the mock's literal 12% --bd -- the dashes measurably vanished at that lower weight
 * before this recipe existed (2026-09-18 fix), and radius stays var(--radius)/16px (the mock's 14
 * is off this platform's ladder). Hover/focus unchanged. Not used anywhere in games (grepped
 * clean) -- no games-keep re-pin needed for this recipe. */
.row-add{margin:0 0 var(--sp-4)}
.ghost-box{display:flex;align-items:center;gap:var(--sp-4);width:100%;box-sizing:border-box;
  min-height:48px;padding:0 var(--sp-4);background:transparent;color:var(--muted);
  border:1.5px dashed var(--ghost-bd);border-radius:var(--radius);box-shadow:none;
  font:inherit;text-align:left;cursor:pointer}
.ghost-box:focus-visible {background:var(--hover);color:var(--fg);border-color:var(--muted)}
@media (hover:hover){ .ghost-box:hover {background:var(--hover);color:var(--fg);border-color:var(--muted)} }
.ghost-box:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.ghost-lead{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
  flex:none;border-radius:var(--radius-pill);border:0;box-shadow:inset 0 0 0 1.2px var(--bd);color:var(--muted)}
.ghost-box:focus-visible .ghost-lead {color:var(--fg)}
@media (hover:hover){ .ghost-box:hover .ghost-lead {color:var(--fg)} }
.ghost-txt{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Snippet line (ADR 0015, searchable notes): ONE muted line of a note with the matched words in <mark>.
   Fixed: type step, colour, one-line ellipsis, the mark paint. Open: width/placement (the host row). */
.snip{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fs-caption);color:var(--muted)}
.snip mark{background:color-mix(in srgb,var(--accent) 24%,transparent);color:var(--fg);border-radius:var(--radius-sm);padding:0 .1em}
/* Image caption (2026-10-15, games image descriptions): the text block under a media thumb. Fixed: type steps,
   colours, 3-line clamp on the body, the dl grid. Open: width/placement (the host card). */
.img-cap{display:block;min-width:0;padding:var(--sp-2) 10px 10px;font-size:var(--fs-caption);color:var(--muted);border-top:1px solid var(--bd)}
.img-cap-t{display:block;font-weight:600;color:var(--fg);overflow-wrap:anywhere}
.img-cap-x{margin:var(--sp-1) 0 0;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}
.img-cap-kv{display:grid;grid-template-columns:auto 1fr;gap:2px var(--sp-2);margin:var(--sp-1) 0 0}
.img-cap-kv dt{color:var(--muted)}
.img-cap-kv dd{margin:0;color:var(--fg);overflow-wrap:anywhere}
.img-cap-src{display:block;margin-top:var(--sp-1);color:var(--muted);font-style:italic}
/* Source mark (2026-10-15): small accent outline pill on a field whose value was read from an image. Fixed: look + type step. */
.src-mark{display:inline-block;margin-left:var(--sp-2);padding:0 var(--sp-2);border:1px solid var(--accent);border-radius:var(--radius-pill);color:var(--accent);font-size:var(--fs-caption);line-height:1.5;white-space:nowrap;text-decoration:none;vertical-align:baseline}
@media (hover:hover){ .src-mark:hover {background:color-mix(in srgb,var(--accent) 12%,transparent)} }
/* Prose block (ADR 0015 Leg 8): the user's own text on a proposal card -- caption size, muted, clamped to
   three lines; `.open` on the text shows all of it (its "Show all" button is the pooled .ctl-quiet). Fixed:
   type step, colour, wrapping, the 3-line clamp. Open: width/placement (the host card). */
.prose-block{display:block;min-width:0}
.prose-text{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font-size:var(--fs-caption);color:var(--muted);
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:var(--prose-lines,3);overflow:hidden}
.prose-text.open{display:block;-webkit-line-clamp:unset;overflow:visible}
/* prose-block--body: declared variant for text that IS the content of its section (a project's Brief), not a
   preview line on a card. Sets only the open properties: type step (--prose-fs, default body), ink
   (--prose-ink, default --fg) and the clamp (--prose-lines, set by shellProseBlock's `lines`, default 6). */
.prose-block--body{--prose-lines:6}
.prose-block--body>.prose-text{font-size:var(--prose-fs,var(--fs-body));color:var(--prose-ink,var(--fg))}
/* the "Show all" control sits on the text's own left edge: its label, not its hover box, aligns (the
   .ctl-quiet inline padding is cancelled by an equal negative margin); open: width/placement */
.prose-block>button.ctl-quiet{margin-inline-start:-.3rem}
.ghost-hint{margin-left:auto;flex:none;color:var(--muted);font-size:var(--fs-caption);font-variant-numeric:tabular-nums}
.row-add.open>.ghost-box{display:none}
.row-add>.composer{margin:0}
@media (prefers-reduced-motion:no-preference){
  .ghost-box{transition:background var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
}

/* .drop-into — drag-over nest/merge/set target outline (moved here byte-identical from
   modules/todo/static/css/todo.css, 2026-09-21, docs/plans/2026-09-21-shared-tags-journal-dnd.md
   Part C: a second module -- Journal's category/tag/people rows, mini-calendar day cells, Trash
   row -- now uses it too, so it belongs in the shared pool rather than owned
   by To-Do alone; core/ui_pool.py's `drop-into` row's `css` field points here now). */
.drop-into{outline:2px dashed var(--accent);outline-offset:-2px}

/* Live drag (lift and slide), 2026-10-16, docs/plans/2026-10-16-live-drag.md -- driven by sortable.js */
html.sort-active,html.sort-active *{cursor:grabbing!important;user-select:none!important;-webkit-user-select:none!important}
/* !important: the lifted row keeps pointer capture, so its own :hover / module row rules (.row:hover, .side-row) stay live and
   would paint it translucent (neighbours sliding under it show through) or flat -- the lift must always win (review 2026-10-20) */
.sort-lift{position:relative;z-index:30;background:var(--panel-solid)!important;box-shadow:var(--shadow)!important;border-radius:var(--radius-sm);will-change:transform;pointer-events:none}
.drag-ghost.sort-lift{position:fixed;z-index:61;box-shadow:var(--shadow-2)!important}
.sort-lift.sort-settle{box-shadow:none!important}
.sort-hole{visibility:hidden}
.sort-live .sort-shift{transition:transform 170ms cubic-bezier(.2,.7,.2,1)}
.sort-settle{transition:transform 180ms cubic-bezier(.2,.7,.2,1),box-shadow 180ms ease}
.sort-instant .sort-shift,.sort-instant .sort-settle,.sort-instant.sort-shift{transition:none!important}
@media (prefers-reduced-motion:reduce){.sort-live .sort-shift,.sort-settle{transition:none}}

/* .drag-ghost — hold-to-drag ghost card (journal press-and-hold drag, 2026-09-24, docs/plans/
   2026-09-24-journal-hold-to-drag.md): a plain clone of the dragged row/card, pinned under the
   pointer while a pointer-driven drag is active. Since 2026-10-16 it is the live-drag engine's overlay clone (sortable.js `overlay: true`). FIXED: it must never
   sit in the document flow, never intercept the pointer (elementFromPoint has to see straight
   through it so the drag loop's own hit-testing works), and always paint above an ordinary row (z-index 61) -- position/left/top/pointer-events/z-index/box-shadow/
   opacity. OPEN: `transform` (the consumer moves it every frame via `translate()`, never left/
   top, so following the pointer costs no layout -- a slight `scale()` composed into the same
   transform is the "lift" cue, reduced-motion callers simply omit it) and `width` (copied once
   from the source element so the clone doesn't reflow to its own natural width once detached from
   its original layout context). */
.drag-ghost{position:fixed;left:0;top:0;pointer-events:none;z-index:61;
  box-shadow:var(--shadow-2);border-radius:var(--radius);opacity:.92}

/* .seg-sm — compact pill segmented control, one size down from .addseg (stock-lens 2026-09-16) */
.seg-sm{display:inline-flex;align-items:center;background:var(--panel-solid);border:1px solid var(--bd2);border-radius:var(--radius-pill);padding:2px;gap:1px}
.seg-sm button{font:inherit;font-size:var(--fs-caption);line-height:1;padding:.32rem .55rem;min-height:24px;border:0;background:none;color:var(--muted);border-radius:var(--radius-pill);cursor:pointer}
.seg-sm button.on{background:var(--accent);color:var(--accent-fg);font-weight:var(--fw-strong)}
@media (hover:hover){ .seg-sm button:hover:not(.on) {background:var(--hover);color:var(--fg)} }
@media (pointer:coarse){.seg-sm button{min-height:36px;padding-inline:.7rem}}

/* Privacy mask (round 2): shell.js swaps every number inside .pv/.pv-block for "***" (text only,
   the currency symbol / % / labels stay). Masked figures draw in the neutral muted ink so a red/
   green P/L colour does not leak direction. */
.privacy .pv,.privacy .pv :is(.up,.down),.privacy .pv-block :is(.up,.down){color:var(--muted)!important}
.privacy input.pv{display:none}
.pv-alt{display:none}
.privacy .pv-alt{display:inline}
.privacy .meter.pv .meter-fill,.privacy .meter.pv .meter-tick{display:none}   /* a meter of the user's own money shows only its empty track */
/* the global .up/.down triangle (::before) would leak gain/loss direction: hide it on a cell holding a masked figure */
.privacy :is(.up,.down):has(.pv)::before,.privacy :is(.up,.down):has(.pv)::after{visibility:hidden}
.privacy :is(.up,.down).pv::before,.privacy :is(.up,.down).pv::after,.privacy .pv :is(.up,.down)::before,.privacy .pv :is(.up,.down)::after{content:none}   /* the marked element itself carries .up/.down (e.g. Performance stats) */
.cell>.skeleton-stack{padding:var(--sp-3)}

/* ===========================================================================================
   Routines + Projects pool (2026-10-02, docs/plans/2026-10-02-routines-projects.md §3.6; designed
   in docs/plans/2026-10-02-routines-projects-mock/, ported verbatim by Leg L0). Every recipe below
   follows the pool rules: tokens only, No-Edge Entry Rule (fill + optional colour rail, no border/
   outline/shadow at rest), Two-Weight Rule (400 / var(--fw-strong)), Caption Label Rule, Source
   Rail Rule (an origin colour is a 4px rounded rail or a small dot, text stays --fg). Each block
   names its FIXED and OPEN parts the way core/ui_pool.py's rows declare them.
   =========================================================================================== */

/* ===========================================================================================
   meter — a horizontal progress bar (role=progressbar; label + value live OUTSIDE the track).
   Markup:  <div class=meter role=progressbar aria-valuenow=4 aria-valuemax=7 style="--p:57">
              <span class=meter-fill></span><span class=meter-tick style="--t:70"></span></div>
   --p = fill percent (0..100, clamped), --t = an optional "expected / pace" marker percent.
   FIXED: track fill (--seg-track), pill radius, fill colour family, tick look, no edge/shadow.
   OPEN:  height (via --meter-h; the --sm variant), width / flex, fill hue (the --ok/--over
          variants), margin.
   Variants: meter--sm (4px, widgets / sidebar rows / list rows), meter--ok (goal met, --up),
             meter--over (past target / over budget, --down; fill pins at 100%),
             meter--done (target beaten, --up; fill pins at 100%).
   =========================================================================================== */
.meter{position:relative;display:block;height:var(--meter-h,8px);min-width:2rem;
  border-radius:var(--radius-pill);background:var(--seg-track)}
.meter-fill{position:absolute;left:0;top:0;bottom:0;display:block;
  width:clamp(0%, calc(var(--p,0) * 1%), 100%);
  border-radius:inherit;background:var(--meter-c,var(--accent))}
@media (prefers-reduced-motion:no-preference){ .meter-fill{transition:width var(--dur-slow) var(--ease-out)} }
/* the pace / expected marker: a 2px fg stroke that overshoots the track by 3px each side so it
   reads on a full fill too */
.meter-tick{position:absolute;top:-3px;bottom:-3px;width:2px;margin-left:-1px;
  left:clamp(0%, calc(var(--t,0) * 1%), 100%);background:var(--fg)}
.meter--sm{--meter-h:4px;min-width:1.5rem}
.meter--sm .meter-tick{top:-2px;bottom:-2px}
.meter--ok{--meter-c:var(--up)}
.meter--over{--meter-c:var(--down)}
.meter--over .meter-fill{width:100%}
.meter--done{--meter-c:var(--up)}
.meter--done .meter-fill{width:100%}
/* label row ABOVE a meter: caption-label left, figure right (the ledger .pace-head shape) */
.meter-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-2);
  margin-bottom:var(--sp-1);font-size:var(--fs-caption);color:var(--muted);min-width:0}
.meter-head>b{font-weight:var(--fw-strong);color:var(--fg);font-variant-numeric:tabular-nums}


/* ===========================================================================================
   heatgrid — a history grid of small cells, one per day or per period.
   Markup:  <div class=heatgrid style="--cols:24"><i class=heat-cell data-l=3 title="…"></i>…</div>
   data-l 0..4 = intensity (0 = scheduled but nothing logged = MISSED: the plain --seg-track
   fill, identical in both themes, no edge — the old --bd2 inset ring was ~1.1:1 on a white cell
   and read as blank space; 1..4 = --heat-c mixed 25/45/70/100% over --bg-solid, the ledger .days
   ramp); heat-cell--today adds an fg ring OUTSIDE the cell.
   Routine states the base needs (D3/D6): heat-cell--skip (a skipped period: freezes the score, so
   it must read neither done nor missed — the track fill + one muted diagonal), heat-cell--off (not
   scheduled / not yet started / future: a faint dot on NO fill, so a weekday rule's weekends
   don't look like misses), heat-cell--under (a quantity routine logged BELOW its target that day:
   intensity still encodes the AMOUNT, an inner --fg ring says "under target" — one encoding per
   meaning, critique A21; the legend shows it).
   FIXED: cell shape (square, --radius-xs), the level ramp, the today / under rings, the skip /
          off marks, the missed fill.
   OPEN:  cell size (--heat-size), column count (--cols / --weeks), the hue (--heat-c; a
          catalogue routine may pass its source hue).
   Variant heatgrid--weeks: 7 rows (Mon..Sun) x --weeks columns, column flow — the habit history.
   =========================================================================================== */
.heatgrid{display:grid;gap:var(--sp-1);justify-content:start;
  grid-template-columns:repeat(var(--cols,12), minmax(0, var(--heat-size,.9rem)))}
.heat-cell{display:block;aspect-ratio:1;border-radius:var(--radius-xs);min-width:0;
  background:var(--seg-track);box-shadow:none}
.heat-cell[data-l="1"]{background:color-mix(in srgb,var(--heat-c,var(--accent)) 25%,var(--bg-solid))}
.heat-cell[data-l="2"]{background:color-mix(in srgb,var(--heat-c,var(--accent)) 45%,var(--bg-solid))}
.heat-cell[data-l="3"]{background:color-mix(in srgb,var(--heat-c,var(--accent)) 70%,var(--bg-solid))}
.heat-cell[data-l="4"]{background:var(--heat-c,var(--accent))}
.heat-cell--skip{background:linear-gradient(to top right,transparent calc(50% - .75px),var(--muted) calc(50% - .75px),
  var(--muted) calc(50% + .75px),transparent calc(50% + .75px)),var(--seg-track)}
.heat-cell--off{background:radial-gradient(circle,var(--bd) 0 1.5px,transparent 2px)}
.heat-cell--under{box-shadow:inset 0 0 0 1.5px var(--fg)}
.heat-cell--today{outline:1.5px solid var(--fg);outline-offset:1px}
.heatgrid--weeks{grid-auto-flow:column;grid-template-rows:repeat(7,auto);
  grid-template-columns:repeat(var(--weeks,26), minmax(0, var(--heat-size,.9rem)))}
/* axis labels: a weekday column left of a --weeks grid, a month row above it (each month label
   spans its week count via grid-column), and the Less→More legend. All caption-micro muted. */
.heat-frame{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:var(--sp-2);row-gap:var(--sp-1);
  align-items:start;min-width:0}
.heat-days{grid-column:1;display:grid;grid-template-rows:repeat(7,1fr);gap:var(--sp-1);height:100%}
.heat-months{grid-column:2;display:grid;gap:var(--sp-1);justify-content:start;
  grid-template-columns:repeat(var(--weeks,26), minmax(0, var(--heat-size,.9rem)))}
.heat-months>span{white-space:nowrap;overflow:visible}
.heat-lbl,.heat-days>span,.heat-months>span{font-size:var(--fs-micro);line-height:1;color:var(--muted)}
.heat-days>span{display:flex;align-items:center}
.heat-legend{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--sp-1);font-size:var(--fs-micro);color:var(--muted)}
.heat-legend .heat-cell{width:.75rem}
.heat-legend>span{margin-inline:var(--sp-1)}

/* ===========================================================================================
   board — a kanban of columns (To-Do sections as columns, D21). The board itself is the ONE
   horizontal scroller; a column is a caption heading + cards, NO container fill (the To-Do column
   view ruling, Addendum C.3: "the box edge still exists in column view" — rejected), each card an
   opaque --panel-solid fill, no edge. (--panel-solid, not To-Do column view's --cell-solid: the
   Classic light ground is white since 2026-10-01, so a #fff card vanishes on it; dark's
   --cell-solid IS --panel-solid, so dark is unchanged.)
   Markup:  <div class=board><section class=board-col><header class=board-col-h><h3>Reading</h3>
              <span class=board-n>4</span>…</header><div class=board-rows>
              <article class=board-card>…</article></div></section>…</div>
   Drag & drop: the live-drag engine (sortable.js) with the pooled drop-into / drag-ghost.
   Width (critique A13): columns are minmax(--col-min, 1fr) grid tracks, so a board whose columns
   fit the page fills it with no scroll; one that does not scrolls sideways, and the right edge
   FADES (a mask over the last --sp-6) so off-screen columns are never invisible. The same
   --sp-6 is padding at the scroll end, so once scrolled fully right the last column sits clear of
   the fade. The page's toolbar also prints the column count ("5 sections").
   Add control (critique A14): the To-Do rule — NO ghost box at rest; a click on a column's blank
   area docks the one composer there, exactly like the list view's sections.
   FIXED: card fill/radius/no-edge, column heading as caption label, the scroller + overflow-only edge fade (.is-over),
          hover = the pooled --hover layer over the card's own fill.
   OPEN:  column floor (--col-min), gap, card padding step, column max-height (page-fixed pages
          give each column its own vertical scroll via .board-rows).
   =========================================================================================== */
.board{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(var(--col-min,14rem),1fr);
  align-items:start;gap:var(--sp-4);overflow-x:auto;min-width:0;scrollbar-width:thin;
  padding-bottom:var(--sp-2);scroll-snap-type:x proximity}
/* the right-edge fade exists only while the columns overflow (the page script toggles .is-over from a
   ResizeObserver); a fade over a board that fits would dim its last column for nothing. A mask clips even a
   fixed child, so the script also drops .is-over while a card menu is open. */
.board.is-over{padding-inline-end:var(--sp-6);
  -webkit-mask-image:linear-gradient(to left,transparent,black var(--sp-6));
          mask-image:linear-gradient(to left,transparent,black var(--sp-6))}
.board-col{min-width:0;display:flex;flex-direction:column;gap:var(--sp-2);
  scroll-snap-align:start}
.board-col-h{display:flex;align-items:center;gap:var(--sp-2);min-height:28px;padding-inline:var(--sp-1)}
.board-col-h h3,.board-col-h .board-ttl{margin:0;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:var(--fs-caption);font-weight:var(--fw-strong);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.board-n{font-size:var(--fs-caption);color:var(--muted);font-variant-numeric:tabular-nums}
.board-rows{display:flex;flex-direction:column;gap:var(--sp-2);min-height:0}
.board-card{display:flex;flex-direction:column;gap:var(--sp-1);min-width:0;
  padding:var(--sp-3);border:0;border-radius:var(--radius-sm);box-shadow:none;
  background:var(--panel-solid);color:var(--fg);font-size:var(--fs-small)}
/* hover: the pool state model — the --hover layer painted OVER the card's opaque fill (one token,
   both themes; no filter factors, no dark fork) */
@media (hover:hover){ .board-card:hover {background-image:linear-gradient(var(--hover),var(--hover))} }
.board-card>.board-sub{font-size:var(--fs-caption);color:var(--muted);display:flex;flex-wrap:wrap;gap:var(--sp-2);align-items:center}
.board-card .done{color:var(--muted);text-decoration:line-through}

/* ===========================================================================================
   timeline (tl) — weeks/months across, one row per item; bars, milestone marks and a today line.
   Positions are PERCENT of the track (0..100), computed by the page from dates:
     .tl-bar  --s (start) --e (end) [--p progress 0..100] [--src hue]
     .tl-ms   --at                    [--src hue]   (.tl-ms--open = not yet reached; .tl-ms--dot =
                                                     a dated single-day task)
     .tl-now  --at                    (one per .tl; spans every row)
   Markup:  <div class=tl style="--cols:12">
              <div class=tl-head><span class=tl-lbl></span><div class=tl-track><span>Oct</span>…</div></div>
              <div class=tl-row><span class=tl-lbl>Thesis</span><div class=tl-track>
                <span class=tl-bar style="--s:0;--e:60;--p:40;--src:var(--cal-project)">Thesis</span>
                <i class=tl-ms style="--at:30" title="Proposal approved · 2026/10/20"></i></div></div>
              <span class=tl-now style="--at:18"><span class=tl-now-lbl>Oct 2</span></span></div>
   The .tl is a 2-column grid whose rows are display:contents, so .tl-now can be absolutely placed
   across every track by one calc over the label gutter (--tl-lbl). Gridlines = one hairline per
   column, drawn as a repeating gradient in --bd2 (a token, not a literal).
   Today label (critique A23): the date rides the line itself, in its OWN row above the axis
   (.tl-now-lbl; the .tl reserves that row when it has one) — never on a week tick, which only
   ever names its own week.
   Stacking (critique B21): now line z 0 < bar z 1 < milestone z 2, so the line passes BEHIND bar
   labels instead of striking through them.
   Hue (critique B12): a project's bar and milestones take that project's OWN colour
   (--src:var(--lc-*)); dated to-dos are hollow --cal-todo dots. --cal-project never paints a bar.
   Phone (critique B5): wrap the .tl in .tl-scroll; below 48rem the timeline keeps a 3rem floor per
   column and scrolls sideways inside the wrapper instead of squeezing week labels to one letter.
   FIXED: bar = source-tinted fill + 3px rail + text (No-Edge), diamond milestone, now line in
          --down (the calendar .nowline colour), caption-micro head labels, the stacking order.
   OPEN:  --tl-lbl gutter width, --cols, row height (--tl-row), --src per bar.
   =========================================================================================== */
.tl{--tl-lbl:11rem;--tl-row:32px;position:relative;display:grid;
  grid-template-columns:var(--tl-lbl) minmax(0,1fr);min-width:0}
.tl-head,.tl-row{display:contents}
.tl-lbl{position:sticky;left:0;z-index:3;background:var(--bg-solid);display:flex;align-items:center;gap:var(--sp-2);min-width:0;padding-right:var(--sp-3);
  font-size:var(--fs-small);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;
  border-top:1px solid var(--bd2);min-height:var(--tl-row)}
.tl-lbl>span{min-width:0;overflow:hidden;text-overflow:ellipsis}
.tl-track{position:relative;min-width:0;min-height:var(--tl-row);border-top:1px solid var(--bd2);
  background-image:repeating-linear-gradient(to right,var(--bd2) 0 1px,transparent 1px calc(100% / var(--cols,12)))}
.tl-head .tl-lbl,.tl-head .tl-track{border-top:0;min-height:22px}
.tl-head .tl-track{display:grid;grid-template-columns:repeat(var(--cols,12),minmax(0,1fr));background-image:none;
  align-items:end;padding-bottom:var(--sp-1)}
.tl-head .tl-track>span{padding-left:var(--sp-1);font-size:var(--fs-micro);line-height:1;color:var(--muted);
  white-space:nowrap;overflow:hidden;text-overflow:clip}
/* the last axis label keeps its full width (it may run past the last column) instead of clipping its final letters */
.tl-head .tl-track>span:last-child{overflow:visible;min-width:max-content}
/* a group caption row inside the timeline (e.g. status group on the all-projects view). Review 2 (b12/b13): the heading sits ABOVE the
   today line (z 1, own ground) so the line never strikes through its letters, is only as wide as its text, and stays at the scroller's
   left edge while a phone timeline scrolls sideways. */
.tl-group{grid-column:1 / -1;justify-self:start;width:max-content;max-width:100%;position:sticky;left:0;z-index:1;background:var(--bg-solid);
  font-size:var(--fs-caption);font-weight:var(--fw-strong);letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted);padding:var(--sp-3) var(--sp-2) var(--sp-1) 0}
.tl-bar{position:absolute;z-index:1;top:var(--sp-1);bottom:var(--sp-1);min-width:6px;
  left:clamp(0%, calc(var(--s,0) * 1%), 100%);
  width:max(6px, calc((min(var(--e,0),100) - max(var(--s,0),0)) * 1%));
  display:flex;align-items:center;padding:0 var(--sp-2) 0 calc(3px + var(--sp-2));box-sizing:border-box;
  border-radius:var(--radius-xs);border:0;box-shadow:none;overflow:hidden;white-space:nowrap;
  font-size:var(--fs-caption);color:var(--fg);
  background:
    linear-gradient(var(--src,var(--accent)),var(--src,var(--accent))) left / 3px 100% no-repeat,
    linear-gradient(color-mix(in srgb,var(--src,var(--accent)) 30%,var(--bg-solid)),
                    color-mix(in srgb,var(--src,var(--accent)) 30%,var(--bg-solid))) left / clamp(0%, calc(var(--p,0) * 1%), 100%) 100% no-repeat,
    color-mix(in srgb,var(--src,var(--accent)) 14%,var(--bg-solid))}
.tl-bar>span{min-width:0;overflow:hidden;text-overflow:ellipsis}
/* a bar continuing past the visible window: the cut edge goes square (the cal-bar cont-* idea) */
.tl-bar--cont-prev{border-top-left-radius:0;border-bottom-left-radius:0}
.tl-bar--cont-next{border-top-right-radius:0;border-bottom-right-radius:0}
/* an undated area: a muted hatched bar, no progress fill (an area has no end to fill toward) */
.tl-bar--area{background:repeating-linear-gradient(135deg,var(--bd2) 0 2px,transparent 2px 7px),color-mix(in srgb,var(--muted) 8%,var(--bg-solid))}
/* a project bar in its health ink: the same tokens .status-dot uses (the page leaves --src unset so the class wins) */
.tl-bar--h-ok{--src:var(--up)}
.tl-bar--h-warn{--src:var(--warn-ink)}
.tl-bar--h-bad{--src:var(--down)}
.tl-ms{position:absolute;top:50%;left:clamp(0%, calc(var(--at,0) * 1%), 100%);z-index:2;
  width:10px;height:10px;margin:-5px 0 0 -5px;transform:rotate(45deg);border-radius:calc(var(--radius-xs) / 2);
  background:var(--src,var(--fg));box-sizing:border-box}
.tl-ms--open{background:var(--cell-solid);border:1.5px solid var(--src,var(--fg))}
.tl-ms--dot{transform:none;border-radius:50%;width:8px;height:8px;margin:-4px 0 0 -4px}
.tl-now{position:absolute;top:var(--tl-now-top,0);bottom:0;width:2px;margin-left:-1px;z-index:0;pointer-events:none;
  left:calc(var(--tl-lbl) + (100% - var(--tl-lbl)) * var(--at,0) / 100);background:var(--down)}
.tl-now::before{content:'';position:absolute;left:-3px;top:-1px;width:8px;height:8px;border-radius:50%;background:var(--down)}
/* the today label: its own row above the axis (the .tl reserves it), centred on the line */
.tl:has(.tl-now-lbl){--tl-now-top:calc(var(--fs-micro) + var(--sp-2));padding-top:var(--tl-now-top)}
.tl-now-lbl{position:absolute;bottom:calc(100% + var(--sp-1));left:50%;transform:translateX(-50%);
  font-size:var(--fs-micro);line-height:1;font-weight:var(--fw-strong);color:var(--down);white-space:nowrap}
.tl-scroll{min-width:0}
@media (max-width:48rem){
  .tl{--tl-lbl:7rem}
  .tl-scroll{overflow-x:auto;overscroll-behavior-x:contain}
  .tl-scroll>.tl{min-width:calc(var(--tl-lbl) + var(--cols,12) * 3rem)}
}

/* ===========================================================================================
   link-row — one linked item (Notion relation / backlink, item_refs D13): module icon · title ·
   muted caption (module · date · figure) · trailing `…` menu (Unlink, Set role), hover-revealed on pointer
   devices, always shown under hover:none. Optional source rail (--src) per the Source Rail Rule.
   Markup:  <div class="link-row link-row--src" style="--src:var(--cal-bill)">
              <span class=link-ico>svg</span>
              <a class=link-main href=…><span class=link-title>…</span><span class=link-cap>…</span></a>
              <button class="ctl-quiet ctl-quiet--icon ctl-quiet--muted link-act" aria-label=Unlink>…</button></div>
   FIXED: no edge, 40px row floor, icon colour (--muted; with a rail the icon stays muted, the
          rail alone carries origin), title --fg, caption --muted, hover --hover.
   OPEN:  row padding step, title weight (650 for a primary relation), width.
   Variant link-row--src: paints ONLY the 4px inset rounded rail (the .now-card::before recipe).
          The rail's room is in the BASE padding, so icons and titles share one x with or without
          a rail (critique B2: a --src-only pad shifted railed rows ~10px right of plain ones). A gone / unresolvable target adds .link-row--gone (struck caption,
          muted title) — the rowid-reuse guard D13 needs a visible state.
   =========================================================================================== */
.link-row{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;
  gap:var(--sp-3);min-height:40px;padding:var(--sp-1) var(--sp-2) var(--sp-1) calc(var(--sp-2) + var(--sp-3));
  border-radius:var(--radius-sm);border:0;box-shadow:none;background:transparent;color:var(--fg)}
.link-row:focus-within {background:var(--hover)}
@media (hover:hover){ .link-row:hover {background:var(--hover)} }
.link-ico{display:flex;align-items:center;justify-content:center;width:1.25rem;color:var(--muted)}
.link-ico svg{width:16px;height:16px}
.link-main{display:flex;flex-direction:column;min-width:0;color:inherit;text-decoration:none;gap:1px}
.link-main:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius-sm)}
.link-title{font-size:var(--fs-small);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.link-cap{font-size:var(--fs-caption);color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.link-act{opacity:0}
.link-row:focus-within .link-act {opacity:1}
@media (hover:hover){ .link-row:hover .link-act {opacity:1} }
@media (hover:none){ .link-act{opacity:1} }
.link-row--src::before{content:'';position:absolute;left:var(--sp-1);top:var(--sp-2);bottom:var(--sp-2);
  width:var(--sp-1);border-radius:var(--radius-xs);background:var(--src,var(--cal-other))}
.link-row--gone .link-title{color:var(--muted)}
.link-row--gone .link-cap{text-decoration:line-through}

/* ===========================================================================================
   stat parts (the existing `stat--label` variant carries the figure; stat--hero is retired):
     .stat-cap — one muted sentence-case caption under the figure; undoes .stat--label's
                 caption-LABEL treatment (650 caps) because it is a caption, not a label.
     <small> inside <b> — the unit ("days", "%"), body weight, muted.
     a .meter inside the stat stretches to the stat's width.
   .stat-row lays a set of them side by side (auto-fit, 9rem floor).
   =========================================================================================== */
.stat .stat-cap{font-size:var(--fs-caption);font-weight:400;letter-spacing:normal;text-transform:none;color:var(--muted)}
.stat b small{font-size:var(--fs-small);font-weight:400;color:var(--muted);margin-left:.25em}
.stat--label .meter{align-self:stretch;margin-top:var(--sp-1)}
/* Rolling digits (shellRoll, core/static/js/rolldigits.js): a changed digit's column holds ONLY old + new digit */
.roll-col{display:inline-block;height:1.2em;line-height:1.2em;overflow:hidden;vertical-align:bottom}
.roll-strip{display:flex;flex-direction:column}
.roll-strip>span{display:block;height:1.2em;line-height:1.2em}
@media (prefers-reduced-motion: no-preference){
  .roll-strip.roll-go{transition:transform var(--dur) var(--ease)}
}
.stat--soon b{color:var(--warn-ink)}   /* a figure a week from its deadline: warning ink, between plain and .stat--late */
.stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(9rem,1fr));gap:var(--sp-3)}
/* ===========================================================================================
   side-row — a generic sidebar navigation row (lead · name · trail), the shared base under
   To-Do's .listrow / Journal's .jr-row / Settings' .set-navi (Projects uses it first; those three
   are NOT migrated in this build). Current row = [aria-current] (page|true) → the --glass-tint
   band + 650: the platform's ONE selection language (rail rows, To-Do tiles, pick-row), never
   the --hover fill (critique B7).
   DEBT (not migrated in this build; each should adopt side-row when next touched): .listrow
   (todo.css), .jr-row (activity), .set-navi (settings) are three copies of this row not yet
   migrated onto side-row.
   A hover-revealed trailing action (.side-act, a ctl-quiet--icon) swaps in over the count.
   Markup:  <a class=side-row href=… aria-current=page><span class=side-lead>…</span>
              <span class=side-name>Thesis</span><span class=side-trail>62%</span></a>
   Lead options: an svg glyph (muted), a TINTED glyph (side-lead--tint + --c: a project's
   shellIcon glyph in its own colour — critique B1: a project's colour is never a filled circle,
   because circles mean HEALTH on every project surface), a pool .symico monogram, or .side-dot
   (a --c colour circle) for a To-Do LIST colour only (the Reminders list idiom).
   FIXED: grid, radius-sm, hover / current state model, caption-size trail in --muted.
   OPEN:  padding step, font-size step, min-height (the --sub variant), trail content (count / % / a status dot).
   =========================================================================================== */
.side-row{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;
  gap:var(--sp-2);min-height:32px;padding:var(--sp-1) var(--sp-2);border-radius:var(--radius-sm);
  border:0;box-shadow:none;background:transparent;color:var(--fg);text-decoration:none;
  font-size:var(--fs-small);text-align:left;cursor:pointer}
@media (hover:hover){ .side-row:hover {background:var(--hover)} }
.side-row[aria-current=page],.side-row[aria-current=true],.side-row[aria-pressed=true]{background:var(--glass-tint);font-weight:var(--fw-strong)}
.side-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
/* global `a:hover{text-decoration:underline}` (specificity 0,1,1) beats the base rule's none on an <a class=side-row> */
@media (hover:hover){ .side-row:is(:hover,:focus,:focus-visible,:active) {text-decoration:none} }
.side-lead{display:flex;align-items:center;justify-content:center;width:1.25rem;color:var(--muted)}
.side-lead svg{width:16px;height:16px}
.side-lead .symico{margin-right:0}
.side-lead--tint{color:var(--c,var(--muted))}
.side-dot{display:block;width:.6rem;height:.6rem;border-radius:50%;background:var(--c,var(--accent))}
.side-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.side-trail{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-caption);font-weight:400;
  color:var(--muted);font-variant-numeric:tabular-nums}
/* variant side-row--sub (2026-10-05, Projects clarity F2): a second caption line under the name, drawn under 48rem only (the phone
   list is the project index and shows just a name otherwise); at desktop width .side-sub is display:none, so the row is the base row */
.side-sub{display:none}
@media (max-width:48rem){
  .side-row--sub{min-height:44px;row-gap:0}
  .side-row--sub .side-lead{grid-column:1;grid-row:1 / span 2}
  .side-row--sub .side-name{grid-column:2;grid-row:1}
  .side-row--sub .side-trail{grid-column:3;grid-row:1 / span 2}
  .side-row--sub .side-sub{display:block;grid-column:2;grid-row:2;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    font-size:var(--fs-caption);font-weight:400;color:var(--muted)}
}
/* variant side-row--note (2026-10-16, Arknights sourced dates): the --sub caption line at every width */
.side-row--note{min-height:44px;row-gap:0}
.side-row--note .side-lead{grid-column:1;grid-row:1 / span 2}
.side-row--note .side-name{grid-column:2;grid-row:1}
.side-row--note .side-trail{grid-column:3;grid-row:1 / span 2}
.side-row--note .side-sub{display:block;grid-column:2;grid-row:2;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:var(--fs-caption);font-weight:400;color:var(--muted)}
.side-row[aria-current] .side-trail {color:var(--fg)}
@media (hover:hover){ .side-row:hover .side-trail {color:var(--fg)} }
.side-act{position:absolute;right:var(--sp-2);top:50%;transform:translateY(-50%);opacity:0}
/* calm circle: its own hover/focus fill is --press (stronger than the row's --hover it sits on) and the glyph goes muted -> fg */
button.side-act:focus-visible {background:var(--press);color:var(--fg)}
@media (hover:hover){ button.side-act:hover {background:var(--press);color:var(--fg)} }
.side-row:focus-within .side-act {opacity:1}
@media (hover:hover){ .side-row:hover .side-act {opacity:1} }
.side-row:focus-within .side-trail:has(+ .side-act) {visibility:hidden}
@media (hover:hover){ .side-row:hover .side-trail:has(+ .side-act) {visibility:hidden} }
@media (hover:none){ .side-act{display:none} }


/* ===========================================================================================
   prop-row — a Notion-style property row: icon · label · control, LEFT-aligned value column so a
   page-width property block scans as two straight columns. Controls are the pooled quiet ones:
   select-quiet, bare-field (incl. a shell-adopted date), switch switch--sm, chip, a plain value.
   Markup:  <div class=prop-row><span class=prop-ico>svg</span><span class=prop-lbl>Status</span>
              <span class=prop-ctl><select class=select-quiet>…</select></span></div>
   FIXED: 1rem icon column, muted label, 34px row floor, hover --hover, no edge.
   OPEN:  label column width (--prop-lbl), font-size step.
   (select-quiet's own .6rem leading pad is cancelled by a positioning margin so its text lines up
   with a bare-field / plain value in the same column — position only, never its box.)
   =========================================================================================== */
.prop-row{display:grid;grid-template-columns:1rem var(--prop-lbl,9rem) minmax(0,1fr);align-items:center;
  column-gap:var(--sp-3);min-height:34px;padding:0 var(--sp-2);border-radius:var(--radius-sm);
  font-size:var(--fs-small)}
.prop-row:focus-within {background:var(--hover)}
@media (hover:hover){ .prop-row:hover {background:var(--hover)} }
.prop-ico{display:flex;align-items:center;justify-content:center;color:var(--muted)}
.prop-ico svg{width:16px;height:16px}
.prop-lbl{color:var(--muted);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.prop-ctl{display:flex;align-items:center;gap:var(--sp-2);min-width:0;flex-wrap:wrap}
.prop-ctl>select.select-quiet:first-child{margin-left:-.6rem;margin-right:0}
.prop-ctl .bare-field{width:auto;min-width:0;font-size:inherit}
/* fixed by projects (2026-10-02): 9ch clipped "2026/09/01" to "2026/09/0" in WebKit (10 glyphs +
   the bare-field's 2px+2px inline pad); 10ch + 4px fits the shell-adopted yyyy/mm/dd exactly. */
.prop-ctl .bare-field.w-date{width:calc(10ch + 4px);font-variant-numeric:tabular-nums}
.prop-ctl .prop-sep{color:var(--muted)}
.prop-ctl .prop-empty{color:var(--muted)}
@media (max-width:48rem){ .prop-row{--prop-lbl:6rem;column-gap:var(--sp-2)} }


/* ===========================================================================================
   status-dot — a 8px state dot (project health: on track / at risk / off track / no recent
   update). ADDED beyond §3.6 (see open question): the same dot appears in the sidebar, the
   projects index, the Today `projects` widget and the update log; a chip there is too loud.
   Always paired with text or a title/aria-label — colour is never the only cue. Circles are
   HEALTH only on project surfaces: a project's own colour is its tinted glyph (side-lead--tint),
   never a dot. --warn uses its own --warn-ink token, not a --cal-* source hue.
   =========================================================================================== */
.status-dot{display:inline-block;flex:none;width:8px;height:8px;border-radius:50%;background:var(--muted);vertical-align:middle}
.status-dot--ok{background:var(--up)}
.status-dot--warn{background:var(--warn-ink)}
.status-dot--bad{background:var(--down)}
.status-dot--none{background:transparent;box-shadow:inset 0 0 0 1.5px var(--muted)}

/* --- bare-field--display: a DECLARED VARIANT of bare-field (sets only open props: font-size,
   font-weight, line-height). The page-title inline rename (a project name in the header row), so an
   editable title is the same size as every other page title (--fs-display); --title (the info-card
   title, --fs-title) reads one step too small for a page heading. */
input.bare-field--display{font-size:var(--fs-display);font-weight:var(--fw-strong);line-height:1.2}
/* --- spark — a tiny inline line chart of the last N values (a metric's log, a trend).
   Markup:  <svg class=spark viewBox="0 0 60 16" preserveAspectRatio=none aria-hidden=true>
              <polyline points="0,14 10,12 …"/><circle class=spark-dot cx=60 cy=3 r=1.75/></svg>
   The page computes points into the viewBox (y down; keep 2 units clear top/bottom). Always
   decorative: the figure it summarises is printed beside it as text.
   Consolidates dashboard .ws-spark / stocks .qvspark / settings .as-spark (not migrated here).
   FIXED: no fill, 1.5px non-scaling stroke, round joins, end dot, colour family.
   OPEN:  width / height (--spark-w / --spark-h), hue (--spark-c), vertical-align / margin. */
.spark{display:inline-block;flex:none;width:var(--spark-w,3.75rem);height:var(--spark-h,1rem);
  overflow:visible;vertical-align:middle;color:var(--spark-c,var(--accent))}
.spark polyline{fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;
  vector-effect:non-scaling-stroke}
.spark .spark-dot{fill:currentColor;stroke:none}

/* --- ms-row — a milestone row: diamond tick · title · date. Rows inside an .ms-list are joined by a
   1px --bd2 stepper line through the diamonds' centres (the orders .stepper idea turned vertical,
   so a project's phases read as one path). The diamond is the SAME mark as the timeline's .tl-ms
   (open = ahead, filled = reached), so a milestone looks identical on Overview and Timeline.
   Markup:  <ol class=ms-list><li class="ms-row ms-row--done">
              <button class=ms-tick aria-pressed=true aria-label="Mark not reached: Topic approved"></button>
              <span class=ms-title>Topic approved</span><span class=ms-date>Sep 12</span></li>…</ol>
   States: ms-row--done (filled diamond, muted struck title), ms-row--late (date in --down, the
   To-Do overdue colour rule), ms-row--next (the first open one IN ORDER, late or not: title 650,
   "you are here" — a "Next milestone" stat picks the same row, so an overdue milestone is never
   skipped for a later on-time one; critique A8).
   Hue: --ms-c is the project's OWN colour (--lc-*), the same value the timeline bar/diamonds use.
   FIXED: diamond size/shape/colours, connector, no edge, hover --hover, 36px floor.
   OPEN:  --ms-c (diamond hue = the project colour), padding step, date column. */
.ms-list{--ms-size:12px;--ms-row:36px;position:relative;list-style:none;margin:0;padding:0}
/* the connector runs from the first diamond's centre to the last one's: half a row in from each
   end, half a diamond in from the row's own inline pad */
.ms-list::before{content:'';position:absolute;left:calc(var(--sp-2) + var(--ms-size) / 2);
  top:calc(var(--ms-row) / 2);bottom:calc(var(--ms-row) / 2);width:1px;margin-left:-.5px;background:var(--bd2)}
.ms-row{position:relative;display:grid;grid-template-columns:var(--ms-size) minmax(0,1fr) auto auto;align-items:center;
  column-gap:var(--sp-3);min-height:var(--ms-row);padding:0 var(--sp-2);border-radius:var(--radius-sm);
  font-size:var(--fs-small)}
.ms-row:focus-within {background:var(--hover)}
@media (hover:hover){ .ms-row:hover {background:var(--hover)} }
/* a 4th (auto) track holds an optional trailing `…` menu; a row without one lets its date span both tracks.
   Title and date may be inline bare fields. */
.ms-row>.ms-date:last-child{grid-column:3 / -1;justify-self:end}
.ms-row>.ms-title.bare-field{font-size:inherit;width:100%}
.ms-date>.bare-field{font-size:inherit;color:inherit;width:auto}
.ms-date>small{font-size:inherit}
@media (max-width:48rem){.ms-row:not(.ms-row--late) .ms-date>small{display:none}}
button.ms-tick{all:unset;box-sizing:border-box;position:relative;z-index:1;display:block;width:var(--ms-size);height:var(--ms-size);
  transform:rotate(45deg);border-radius:calc(var(--radius-xs) / 2);cursor:pointer;
  background:var(--cell-solid);border:1.5px solid var(--ms-c,var(--fg))}
button.ms-tick::after{content:'';position:absolute;inset:calc(-1 * var(--sp-2))}
button.ms-tick:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.ms-row--done button.ms-tick{background:var(--ms-c,var(--fg))}
.ms-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ms-row--done .ms-title{color:var(--muted);text-decoration:line-through}
.ms-row--next .ms-title{font-weight:var(--fw-strong)}
.ms-date{font-size:var(--fs-caption);color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
.ms-row--late .ms-date{color:var(--down)}
.ms-date>.ms-date-txt{cursor:text}   /* the date at rest; click/Enter/Space opens the adopted field */
.ms-date>.ms-date-txt:focus-visible {text-decoration:underline dotted}
@media (hover:hover){ .ms-date>.ms-date-txt:hover {text-decoration:underline dotted} }
/* `.val-txt` -- the same caption-at-rest look for a value inside a property row (Projects facts: a number or date reads as text, click/Enter/Space opens the field) */
.val-txt{cursor:text}
.val-txt:focus-visible {text-decoration:underline dotted}
@media (hover:hover){ .val-txt:hover {text-decoration:underline dotted} }

/* --- log-row — one entry in a dated activity log (a project update, a metric value, a milestone
   reached, a linked journal entry). Lead (a status-dot or a 16px glyph) · body (title, optional
   text, muted caption) · trailing time. Hairline between siblings, no fill, no edge: the Journal
   .a-card shape without its card. A day group is a pool .cat-h above a run of rows.
   Markup:  <article class=log-row><span class=log-lead><i class="status-dot status-dot--ok"></i></span>
              <div class=log-body><p class=log-title>…</p><p class=log-text>…</p><p class=log-cap>…</p></div>
              <time class=log-time>18:40</time></article>
   FIXED: grid, --bd2 hairline between siblings, lead colour --muted, text sizes, no edge.
   OPEN:  padding step, text clamp (--log-lines), lead width. */
.log-row{display:grid;grid-template-columns:1.25rem minmax(0,1fr) auto;column-gap:var(--sp-3);
  align-items:start;padding:var(--sp-3) var(--sp-2);border:0;box-shadow:none}
.log-row+.log-row{border-top:1px solid var(--bd2)}
.log-lead{display:flex;align-items:center;justify-content:center;height:1.25rem;color:var(--muted)}
.log-lead svg{width:16px;height:16px}
.log-body{min-width:0;display:flex;flex-direction:column;gap:2px}
.log-body>p{margin:0}
.log-title{font-size:var(--fs-small);color:var(--fg);line-height:1.25rem;overflow-wrap:anywhere;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}   /* the first line never takes more than two lines */
.log-title--full{-webkit-line-clamp:unset;display:block;overflow:visible}   /* variant: a first line that would clip is shown whole */
.log-title b{font-weight:var(--fw-strong)}
/* log-row--system: a DECLARED VARIANT for an entry the app wrote (a status change, dates moved): the title reads as muted plain text, below what people wrote */
.log-row--system .log-title,.log-row--system .log-title b{font-weight:400;color:var(--muted)}
.log-title a{color:var(--link);text-decoration:none}
@media (hover:hover){ .log-title a:hover {text-decoration:underline} }
.log-text{font-size:var(--fs-small);color:var(--fg);line-height:var(--lh-normal);
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:var(--log-lines,4);overflow:hidden;
  overflow-wrap:anywhere;white-space:pre-line}
.log-cap{font-size:var(--fs-caption);color:var(--muted)}
.log-time{font-size:var(--fs-caption);color:var(--muted);font-variant-numeric:tabular-nums;line-height:1.25rem}
/* --- undo-box — an inline confirm panel under a log-row's title: what undoing will do, then Undo / Cancel.
   Markup: <div class=undo-box><p class=log-cap>Undo will:</p><p class="diff-line diff-line--chg">…</p><div class=undo-box-acts>buttons</div></div>
   FIXED: hairline border, radius, padding, column gap. OPEN: none (module CSS only places it). */
.undo-box{margin-top:var(--sp-2);padding:var(--sp-2) var(--sp-3);border:1px solid var(--bd2);border-radius:var(--radius-sm);display:flex;flex-direction:column;gap:var(--sp-2)}
.undo-box>p{margin:0}
.undo-box-acts{display:flex;gap:var(--sp-2)}
/* --- diff-line — one changed row of a History entry, git style: a sign, the table and row id, then the changed
   columns. Markup:  <p class="diff-line diff-line--add">+ todo_items #812  title: "Buy milk"</p>
   FIXED: monospace, caption size, wrapping, no margin. OPEN: colour. Variants add / chg / del set colour only. */
.diff-line{margin:0;font-family:var(--mono);font-size:var(--fs-caption);line-height:var(--lh-normal);color:var(--muted);
  white-space:pre-wrap;overflow-wrap:anywhere}
.diff-line--add{color:var(--up)}
.diff-line--chg{color:var(--fg)}
.diff-line--del{color:var(--down)}
/* --- pick-row — one option in a single-choice list of RICH options (a template, a plan): a radio
   whose label is icon · name · one-line description · muted "what it creates" caption. Selected =
   the platform's one selection language, the flat --glass-tint band (rail rows, To-Do tiles),
   never a ring. The real <input type=radio> stays in the DOM (visually hidden, .vh) so arrow
   keys, form submit and screen readers work natively.
   Markup:  <label class=pick-row><input type=radio name=tpl class=vh checked>
              <span class=pick-ico>svg</span><span class=pick-main><span class=pick-name>Research</span>
              <span class=pick-desc>…</span><span class=pick-cap>…</span></span></label>
   Icon (critique B20): --muted at rest, --accent only on the checked row — one accent, never a
   decorative hue per option (the old --pick-c open hue is removed).
   FIXED: grid, no edge, radius-sm, hover --hover, checked --glass-tint + accent icon, focus ring.
   OPEN:  padding step. */
.pick-list{display:flex;flex-direction:column;gap:2px}
.pick-row{display:grid;grid-template-columns:1.5rem minmax(0,1fr);column-gap:var(--sp-3);align-items:start;
  padding:var(--sp-2) var(--sp-3);border-radius:var(--radius-sm);cursor:pointer;border:0;box-shadow:none}
@media (hover:hover){ .pick-row:hover {background:var(--hover)} }
.pick-row:has(input:checked){background:var(--glass-tint)}
.pick-row:has(input:focus-visible){outline:2px solid var(--accent);outline-offset:-2px}
.pick-ico{display:flex;align-items:center;justify-content:center;height:1.5rem;color:var(--muted)}
.pick-row:has(input:checked) .pick-ico{color:var(--accent)}
.pick-ico svg{width:18px;height:18px}
.pick-main{display:flex;flex-direction:column;gap:1px;min-width:0}
.pick-name{font-size:var(--fs-small);font-weight:var(--fw-strong);color:var(--fg);line-height:1.5rem}
.pick-desc{font-size:var(--fs-small);color:var(--fg)}
.pick-cap{font-size:var(--fs-caption);color:var(--muted)}

/* ms-tick vs the platform's phone tap floor: shell.css's `main button:not(…, .ctl-quiet, .ms-tick)`
   44px floor exempts it; the HIT area grows on the ::after instead (28px desktop, 44px square on coarse pointers). */
/* the diamond is rotated 45deg and so is its ::after: counter-rotate the hit area so it is an axis-aligned 44px square
   (inset against the padding box, half the shortfall) */
@media (pointer:coarse){ button.ms-tick::after{inset:calc((100% - 44px) / 2);transform:rotate(-45deg)} }

/* --- symico--sm / symico--lg: DECLARED size variants of the pool `symico` (sets only its open
   property, font-size; the box is em-based, so one font-size step scales box + monogram
   together). Replaces the raw 1.45rem / .95rem font-sizes the Today widget wrapped around it.
   sm = a trailing figure-line mark; lg = a row lead the size of a check circle. */
.symico.symico--sm{font-size:var(--fs-body)}
.symico.symico--lg{font-size:var(--fs-display)}

/* --- chip--list: a DECLARED variant of `chip` — a To-Do list shown as its colour dot + name
   (critique A22: the list "Study" drew as a second grey chip beside the tier "Key", reading as a
   second tier). Sets only open props: display/gap (inline dot), background (none — the dot
   carries the identity), color, inline padding. --lc is the list's colour, the value .listrow's
   dot uses.  Markup: <span class="chip chip--list" style="--lc:var(--lc-indigo)">Study</span> */
.chip.chip--list{display:inline-flex;align-items:center;gap:var(--sp-1);background:transparent;color:var(--fg);padding-inline:0}
.chip.chip--list::before{content:'';flex:none;width:.5rem;height:.5rem;border-radius:50%;background:var(--lc,var(--accent))}

/* ===========================================================================================
   mrow -- a metric row: name . figure . meter . spark . log field, one line (routines-projects L6).
   FIXED: 40px floor, --radius-sm, hover/focus-within = the pooled --hover layer, tabular figure.
   OPEN:  --mrow-cols (the track list), width. Phone: the meter and log field drop under the name.
   A metric with no target keeps its column with an empty `.mrow-none` (no meter is drawn). The log field is a
   pooled `.suffix-field.mrow-add` ("Add reading" + the unit), not a bare field that read as a button.
   The figure column is capped at 8rem by projects.js; a longer figure wraps its caption.
   =========================================================================================== */
.mrow{display:grid;grid-template-columns:var(--mrow-cols,minmax(7rem,11rem) max-content minmax(4rem,1fr) auto 10rem auto);
  align-items:center;column-gap:var(--sp-3);min-height:40px;padding:0 var(--sp-2);border-radius:var(--radius-sm);
  font-size:var(--fs-small)}
.mrow:focus-within {background:var(--hover)}
@media (hover:hover){ .mrow:hover {background:var(--hover)} }
.mrow:focus-visible{outline:2px solid var(--accent);outline-offset:2px}   /* review 2 c17: the pooled ring, not the UA default (the row is focusable: tabIndex 0) */
.mrow>.mrow-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mrow>.mrow-fig{font-variant-numeric:tabular-nums;white-space:normal;min-width:0}
.mrow>.mrow-fig small{color:var(--muted);font-size:var(--fs-caption);white-space:nowrap}
.mrow>.mrow-add{font-size:var(--fs-caption);min-width:0}
.mrow>.mrow-add>.bare-field--end{min-width:0}
.mrow>.mrow-add>.suffix-unit,.mrd-add>.suffix-field>.suffix-unit{flex:none;max-width:4rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mrow>.mrow-add>.bare-field:placeholder-shown+.suffix-unit{display:none}   /* the unit shows once a number is typed: "Add reading" at rest, "42 km" while typing */
.mrow>.mrow-name.bare-field{font-size:inherit;width:100%}
@media (max-width:48rem){
  .mrow{grid-template-columns:minmax(0,1fr) auto auto;row-gap:var(--sp-1);padding-block:var(--sp-2)}
  .mrow>.meter{grid-column:1}
  .mrow>.spark{display:none}
  .mrow>.mrow-none{display:none}
  .mrow>.mrow-add{grid-column:2;grid-row:2;width:10rem}
  .mrow>.more{grid-column:3;grid-row:1}
}

/* ===========================================================================================
   mrd -- the Readings panel of a metric row (opens under it): the last readings as date . value . edit .
   remove, then one add line (a day + a value) and, when it applies, one pace caption.
   FIXED: hairline between rows, 32px floor, tabular figure, muted date, caption-size pace line.
   OPEN:  width, margin. Edit/remove are pooled `ctl-quiet--icon` buttons (28px).
   =========================================================================================== */
.mrd{display:grid;padding:var(--sp-1) var(--sp-2) var(--sp-2);margin-bottom:var(--sp-2);font-size:var(--fs-small)}
.mrd-row{display:grid;grid-template-columns:minmax(6rem,9rem) minmax(0,1fr) 28px 28px;align-items:center;
  column-gap:var(--sp-2);min-height:32px;border-top:1px solid var(--bd2)}
.mrd-date{color:var(--muted)}
.mrd-val{font-variant-numeric:tabular-nums;min-width:0;overflow-wrap:anywhere}
.mrd-row>.ctl-quiet{width:28px;height:28px}
.mrd-add{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3);min-height:32px;border-top:1px solid var(--bd2)}
.mrd-add>.suffix-field{flex:1 1 9rem;min-width:0;font-size:var(--fs-caption)}
.mrd-add .bare-field--end{min-width:0;text-align:left}
.mrd-add>.w-date{flex:none;width:8rem}
.mrd-pace{margin:var(--sp-1) 0 0;color:var(--muted);font-size:var(--fs-caption)}

/* more-menu--hints: a DECLARED VARIANT of the "... menu" (glass.css/module CSS own the panel). Sets only open props: the item's
   inner layout (two lines: the name, then a muted caption) and the caption step/ink. Item markup: <button role=menuitem>
   <span>Task</span><small>One thing to do</small></button>. */
.more-menu--hints button{display:flex;flex-direction:column;align-items:flex-start;gap:0;white-space:normal}
.more-menu--hints button>small{font-size:var(--fs-micro);color:var(--muted);line-height:1.3}
/* more-menu--check: a DECLARED VARIANT for a menu that picks one value. Item markup: <button role=menuitemradio aria-checked=true|false>.
   Every item reserves the same leading slot; the chosen one draws a check there (never a typed prefix in the label). */
.more-menu--check button::before{content:"";display:inline-block;width:1.2em;text-align:center}
.more-menu--check button[aria-checked=true]::before{content:"\2713"}
/* more-sep: a part of the "... menu" — a hairline between groups of items (Projects' Tasks menu: View / Sort / Show). Markup: <hr class=more-sep role=separator>. */
.more-sep{height:0;margin:var(--sp-1) var(--sp-2);border:0;border-top:1px solid var(--bd2);align-self:stretch}

/* ===========================================================================================
   pjrow -- a project row in a compact card (Today's Projects widget): health dot . name (+ health word /
   chip) . next . progress. One hairline between siblings, no card per row.
   FIXED: 44px floor, hairline, hover --hover, name ellipsis, caption sizes, % figure weight, late ink.
   OPEN:  grid-template-columns and the next/progress placement (a wide host moves "next" into its own
          column; a phone host drops it under the name), --pjrow-meter (the meter width).
   =========================================================================================== */
.pjrow-rows{display:grid}
.pjrow{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;
  column-gap:var(--sp-3);row-gap:2px;min-height:2.75rem;padding:var(--sp-2) var(--sp-3);
  border-top:1px solid var(--bd2);color:var(--fg);text-decoration:none}
.pjrow:first-child{border-top:0}
/* widget-gaps leg 2: the tick on a cross-card row / query list row (a link cannot hold a control, so the link is wrapped) */
.pjrow-tick{position:relative}
.pjrow-tick>.pjrow{border-top:0;padding-right:calc(var(--sp-3) + 2rem)}
.pjrow-tick:not(:first-child)>.pjrow{border-top:1px solid var(--bd2)}
.pjrow-tick>.pjrow-box{position:absolute;right:var(--sp-3);top:50%;transform:translateY(-50%)}
.pjrow-box{width:1.1rem;height:1.1rem;margin:0;cursor:pointer;accent-color:var(--accent)}
.link-row>.pjrow-box{grid-column:1}
.link-row:has(>.pjrow-box)>.link-main{grid-column:2}
.tick-done .pjrow-name,.tick-done .link-title{text-decoration:line-through;color:var(--muted)}
@media (hover:hover){ .pjrow:hover {background:var(--hover)} }
.pjrow:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.pjrow>.status-dot{grid-column:1;grid-row:1}
.pjrow-main{grid-column:2;grid-row:1;min-width:0;display:flex;align-items:center;gap:var(--sp-2)}
.pjrow-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pjrow-health{flex:none;font-size:var(--fs-caption);color:var(--muted)}
.pjrow-main .chip{flex:none}
.pjrow-next{grid-column:2;grid-row:2;min-width:0;font-size:var(--fs-caption);color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pjrow-next--late{color:var(--down)}
.pjrow-prog{grid-column:3;grid-row:1 / span 2;display:flex;align-items:center;gap:var(--sp-2);
  font-size:var(--fs-caption);color:var(--muted);font-variant-numeric:tabular-nums}
.pjrow-prog .meter{width:var(--pjrow-meter,5rem)}
.pjrow-prog b{min-width:3.5ch;text-align:right;font-weight:var(--fw-strong);color:var(--fg)}

/* ===========================================================================================
   crumb -- the breadcrumb line above a nested page's header (Projects: "Home > Kitchen reno"; ADR 0017). Drawn only when
   the page sits inside another: ancestors are quiet links, the page itself the plain last part, one "›" between parts.
   Markup: <nav class=crumb aria-label><a>Home</a><span class=crumb-sep aria-hidden>›</span><span class=crumb-here aria-current=page>Kitchen reno</span></nav>
   FIXED: caption type, muted ink, link hover (fg + underline), separator ink, one-line ellipsis, 44px hit on a coarse pointer.
   OPEN:  padding / margin (where the host puts the line).
   =========================================================================================== */
.crumb{display:flex;align-items:center;flex-wrap:wrap;column-gap:var(--sp-1);row-gap:0;min-width:0;
  padding:var(--page-gap-top) var(--sp-4) 0;font-size:var(--fs-caption);line-height:1.4;color:var(--muted)}
.crumb a{color:var(--muted);text-decoration:none;max-width:16rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media (hover:hover){ .crumb a:hover {color:var(--fg);text-decoration:underline} }
.crumb a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius-xs)}
.crumb-sep{color:var(--muted)}
.crumb-here{color:var(--fg);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media (pointer:coarse){ .crumb a{display:inline-flex;align-items:center;min-height:44px} }

/* media-strip — a row of media thumbs attached to a piece of content (Games post page "On X") */
.media-strip{margin:20px 0 0}
.media-strip-h{font-size:.85rem;font-weight:600;color:var(--muted);margin:0 0 8px;display:flex;align-items:center;gap:8px}
.media-strip-row{display:flex;gap:8px;overflow-x:auto;scrollbar-width:thin;padding-bottom:4px;margin-bottom:10px}
.media-strip-i{height:160px;width:auto;flex:none;border-radius:var(--radius-sm);border:1px solid var(--bd);background:var(--bg2);object-fit:cover;display:block}
video.media-strip-i{max-width:284px}

/* deeplink-hit -- one-shot highlight on the row/card a deep link (?entry/?id/#item) lands on; inset box-shadow (an outward ring is clipped by the overflow:hidden row lists of ledger/inventory/mail), not
   outline, so an absolutely positioned descendant (the order stepper line) can't notch it. shell.js shellFlash(). !important: it must win over a card's own .cell/.qrow shadow for its 3s life. */
.deeplink-hit.deeplink-hit{box-shadow:inset 0 0 0 2px var(--accent) !important;transition:box-shadow var(--dur-slow,400ms) var(--ease)}
.deeplink-hit.deeplink-out{box-shadow:inset 0 0 0 2px transparent !important}

/* segmented item label width (Projects S7, 2026-10-07): the selected item is bolder, so its width used to change and push its neighbours. An item that carries
   data-label="<its own text>" reserves the bold width through a hidden, zero-height bold copy, so selecting never moves anything. Items without the attribute draw as before. */
.addseg>button[data-label],.seg>button[data-label],.viewsw>button[data-label]{vertical-align:top}   /* the empty copy would otherwise move the button's baseline and grow the track */
.addseg>button[data-label]::after,.seg>button[data-label]::after,.viewsw>button[data-label]::after{content:attr(data-label);display:block;height:0;overflow:hidden;visibility:hidden;pointer-events:none;font-weight:var(--fw-strong)}
/* 2026-10-24 (appearance fix): the Segment item recipe above is inline-flex, so the hidden bold copy was a SECOND flex item beside the text and every item came out text + bold copy wide (Settings' Light item 88px for a 32px word). Stack the two (column, no gap) so the copy sits under the text at zero height and the item is exactly the bold width. */
.addseg>button[data-label],.seg>button[data-label],.viewsw>button[data-label]{flex-direction:column;gap:0}

/* Overlay tour (2026-10-09, docs/plans/2026-10-09-help-overlay-guide.md; engine js/tour.js). Three nodes appended to <body> for the
   life of a run: .tour-shield (a transparent click-catcher so the page under the dim cannot be pressed), .tour-ring (the target's
   cutout: the 9999px box-shadow IS the dim), .tour-pop (the step card; a bottom sheet on a phone). z 120-122 sits above the dock
   toast (95) and #navprog (100). The ring/pop are positioned by the engine; modules never restyle them. */
.tour-shield{position:fixed;inset:0;z-index:120;background:transparent}
.tour-ring{position:fixed;z-index:121;pointer-events:none;border-radius:var(--radius-sm);
  box-shadow:0 0 0 9999px rgba(0,0,0,.55);outline:2px solid var(--accent);outline-offset:2px}
.tour-ring[hidden]{display:block;visibility:hidden}
.tour-pop{position:fixed;z-index:122;box-sizing:border-box;width:min(21rem,calc(100vw - 1.5rem));
  background:var(--bg-solid);color:var(--fg);border:1px solid var(--accent);border-radius:var(--radius);box-shadow:var(--shadow);
  padding:var(--sp-3);display:flex;flex-direction:column;gap:var(--sp-2);min-width:0;outline:none}
.tour-kicker{margin:0;font-size:var(--fs-caption);font-weight:var(--fw-strong);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.tour-title{margin:0;font-size:1rem}
.tour-body{margin:0;font-size:var(--fs-caption);overflow-wrap:anywhere}
.tour-say{margin:0;font-size:var(--fs-caption);padding:var(--sp-2);border-radius:var(--radius-sm);background:var(--panel);overflow-wrap:anywhere}
.tour-nav{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2)}
.tour-nav{justify-content:flex-end}
.tour-dots{display:inline-flex;gap:var(--sp-1);margin-right:auto}
.tour-end .tour-nav>:first-child{margin-right:auto}
.tour-dot{width:6px;height:6px;border-radius:50%;background:var(--bd)}
.tour-dot[aria-current=true]{background:var(--accent)}
.tour-end{text-align:left}
@media (prefers-reduced-motion: no-preference){
  .tour-ring{transition:left .2s ease,top .2s ease,width .2s ease,height .2s ease}
  .tour-pop:not(.tour-sheet){transition:left .2s ease,top .2s ease}
}
@media (max-width:48rem){
  .tour-pop{left:0;right:0;bottom:0;top:auto;width:auto;border-radius:var(--radius) var(--radius) 0 0;border-bottom:0;
    padding-bottom:max(var(--sp-3),env(safe-area-inset-bottom))}
}

/* Notice strip (2026-10-09): one quiet line of "you are in a special mode" text with its way out, above a page's content. The Projects demo
   ("Demo project. Try anything. Nothing is saved." + Back to help / Leave demo) is the first user. Status-tone ink on the palette-independent
   info chip pair, so it reads in every palette. */
.notice-strip{flex:none;display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-1) var(--sp-3);margin:var(--sp-2) var(--sp-3) 0;
  padding:var(--sp-2) var(--sp-3);border-radius:var(--radius-sm);background:var(--chip-info-bg);color:var(--chip-info-fg);font-size:var(--fs-caption)}
.notice-strip b{font-weight:var(--fw-strong)}
.notice-strip-acts{margin-left:auto;display:inline-flex;gap:var(--sp-2)}
.notice-strip .ctl-quiet{color:inherit;font-weight:var(--fw-strong);text-decoration:underline}
/* --float (2026-10-09): fixed in the dock's corner, reserves no layout space (demo pages hide the dock; the strip takes its place).
   The base margin (right/left --sp-3) is kept, so right/left subtract it. */
.notice-strip--float{position:fixed;z-index:55;right:calc(max(calc(var(--sp-4) + var(--sp-2)),env(safe-area-inset-right)) - var(--sp-3));
  bottom:max(calc(var(--sp-4) + var(--sp-2)),env(safe-area-inset-bottom));max-width:min(36rem,calc(100vw - 2*var(--sp-4)));box-shadow:var(--shadow-2)}
@media (max-width:48rem){
  .notice-strip--float{left:calc(var(--sp-4) - var(--sp-3));right:calc(var(--sp-4) - var(--sp-3));max-width:none;
    bottom:calc(var(--phonebar-bottom) + env(safe-area-inset-bottom))}
}

/* Question row (2026-10-09, help overlay guide): one question as a full-width row, title on the left and the "ways" it can be answered as small
   chips on the right (the Help list: "Show me" in the info tone, "Say it" muted). A button, borderless, one hairline under it (rows are hairlines,
   never cards); the title turns accent on hover. Its chips are the pooled .chip variants, unchanged. */
button.hq-row{display:flex;align-items:center;gap:var(--sp-3);width:100%;text-align:left;border:0;border-bottom:1px solid var(--bd);border-radius:0;
  background:none;box-shadow:none;padding:var(--sp-2) var(--sp-1);font:inherit;color:var(--fg);min-height:2.6rem}
@media (hover:hover){ button.hq-row:hover {background:var(--hover)} }
@media (hover:hover){ button.hq-row:hover .hq-t {color:var(--accent)} }
button.hq-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.hq-t{flex:1;min-width:0;white-space:normal;overflow-wrap:anywhere}
.hq-ways{display:flex;gap:var(--sp-1);flex:none}

/* --- Row list (phone shell A, 2026-10-23): `.rowlist` = a bare vertical list of rows separated by hairlines (the grid reset
   `.rows` and every ul-of-rows already carried); `.rowlist--inset` = the phone's iOS "inset grouped" look: below 48rem the list
   is ONE rounded group with taller rows. Desktop is untouched, so a page can wear the variant everywhere. The fill is the `cell` paint
   (`--c-fill`/`--c-shadow`), whether or not the list also wears `cell`. One variant for every phone leg (Plan, Money, Life, Games). */
.rowlist{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:minmax(0,1fr)}
.rowlist>*+*{border-top:1px solid var(--bd2)}
@media (max-width:48rem){
  .rowlist--inset{border-radius:var(--radius-lg);overflow:hidden;background:var(--c-fill);box-shadow:var(--c-shadow)}   /* the `.cell` paint, so a list that is not a cell gets the group fill too */
  .rowlist--inset>*{min-height:3.25rem;box-sizing:border-box}   /* border-box: the 52px is the whole row, padding included */
}

/* --- Phone sheet form (owner 2026-10-24, "A. Inset rows"; docs/plans/2026-10-24-phone-quick-add.md leg A) ---------------------------------
   `.sheet` = a bottom-sheet surface (a page positions it: fixed bottom, or a full-height dialog); `.sheet-grab` = its visual grabber;
   `.sheet-head` = Cancel . title . action (flat 44px text buttons; `--sticky` pins it above a long scroll); `.value-row` = a label + 1-2
   compact controls row inside a `.rowlist--inset` (`--link` = the whole row is one accent button, "More options >"; `-ctl` holds the
   controls, `-chev` is the trailing chevron); `.value-pill` = the compact control itself (a real date/time input or select, 34px
   tall inside the >=44px row). `.seg--fill` = a segmented track whose options share the width evenly. A page sets `html.sheet-open`
   while a phone sheet is up (shell.css hides the tab bar then). Hex-free: tokens only. */
.sheet{box-sizing:border-box;width:100%;display:flex;flex-direction:column;gap:var(--sp-3);
  padding:var(--sp-2) var(--sp-4) calc(var(--sp-4) + env(safe-area-inset-bottom,0px));
  background:var(--bg-solid);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
  max-height:calc(100dvh - var(--kb-inset,0px) - env(safe-area-inset-top,0px));overflow-y:auto;overscroll-behavior:contain}
.sheet-grab{flex:none;align-self:center;width:36px;height:5px;margin:var(--sp-1) 0;border-radius:var(--radius-pill);
  background:color-mix(in srgb,var(--fg) 22%,transparent)}
.sheet-head{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;column-gap:var(--sp-2)}
.sheet-head>:first-child{justify-self:start}
.sheet-head>:last-child{justify-self:end}
.sheet-head-title{font-size:var(--fs-body);font-weight:var(--fw-strong);color:var(--fg);text-align:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sheet-head>button{box-sizing:border-box;min-height:44px;padding:0 var(--sp-3);margin:0;border:0;border-radius:var(--radius-sm);background:transparent;box-shadow:none;
  font:inherit;font-size:var(--fs-body);color:var(--muted);cursor:pointer}
.sheet-head>button:last-child{color:var(--accent);font-weight:var(--fw-strong)}
.sheet-head>button:disabled,.sheet-head>button[aria-disabled=true]{opacity:.4;pointer-events:none}
.sheet-head>button:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
@media (hover:hover){ .sheet-head>button:hover:not(:disabled) {background:var(--hover)} }
.sheet-head--sticky{position:sticky;top:0;z-index:1;background:var(--bg-solid)}

.value-row{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);
  min-height:44px;padding:0 var(--sp-4);color:var(--fg);font-size:var(--fs-body)}
.value-row-ctl{display:flex;align-items:center;justify-content:flex-end;gap:var(--sp-2);min-width:0}
.value-row-chev{flex:none;color:var(--muted);margin-left:var(--sp-1)}
.value-row-chev::before{content:'\203A'}
.value-row--link{width:100%;border:0;margin:0;background:transparent;box-shadow:none;font:inherit;font-size:var(--fs-body);text-align:left;color:var(--accent);cursor:pointer}
.value-row--link:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
@media (hover:hover){ .value-row--link:hover {background:var(--hover)} }

/* the pill sits on the real control: shell.js keeps the SAME element when it adopts type=date/time (it turns it into a text input
   carrying .date10/.w-date or .time24/.w-time), so `.value-pill` styles that element directly; `.value-pill.value-pill` out-ranks the
   adopted fields' own sizing (`.w-date`, `input.time24`). A <select> keeps its chevron (background-image) and only repaints the fill. */
.value-pill.value-pill{box-sizing:border-box;appearance:none;-webkit-appearance:none;height:34px;min-height:34px;min-width:0;margin:0;padding:0 var(--sp-3);
  border:0;border-radius:var(--radius-sm);background-color:var(--seg-track);color:var(--fg);box-shadow:none;
  font:inherit;font-size:16px;line-height:34px;text-align:center;font-variant-numeric:tabular-nums;field-sizing:fixed}
.value-pill.value-pill.w-date{width:7.5rem}
.value-pill.value-pill.w-time{width:4.5rem}
select.value-pill.value-pill{padding-right:2em;background-position:right var(--sp-2) center;background-size:.75em;text-align:left}
.value-pill.value-pill:focus-visible{outline:2px solid var(--accent);outline-offset:1px}

.seg--fill{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;width:100%}   /* equal columns; the absolutely placed .seg-thumb takes no cell */

/* --- Day strip, Progress ring, Segment bar (phone shell A leg 4, 2026-10-23) -------------------------------------------------------
   Day strip = seven day buttons in one row (weekday letters from shellWeek, Sunday first); the selected day is an accent tile, today
   an accent number, a dot under a day that holds something. Ring = the project progress figure drawn as a stroke (pathLength 100, so
   `--p` is the percent and the dash is `--p 100`); it eases on change like .meter-fill (meter-ease). Segment bar = one flat cell per
   step (project phases), `data-s` done / part / todo; names under it. All three: tokens only, flat (no shadow), no desktop rule. */
.daystrip{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:var(--sp-1)}
.daystrip-day{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:3.5rem;padding:var(--sp-2) 0;
  border:0;border-radius:var(--radius);background:none;box-shadow:none;color:var(--fg);font:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}
.daystrip-w{font-size:var(--fs-micro);font-weight:var(--fw-strong);color:var(--muted);text-transform:uppercase}
.daystrip-n{font-size:var(--fs-title);font-weight:var(--fw-strong);font-variant-numeric:tabular-nums}
.daystrip-dot{width:5px;height:5px;border-radius:50%;background:var(--c,var(--accent))}
.daystrip-dot:empty{display:block}
.daystrip-day--today .daystrip-n{color:var(--accent)}
.daystrip-day:active {background:var(--hover);box-shadow:none}
@media (hover:hover){ .daystrip-day:hover {background:var(--hover);box-shadow:none} }
.daystrip-day--on, .daystrip-day--on:active {background:var(--accent);color:var(--accent-fg)}
@media (hover:hover){ .daystrip-day--on:hover {background:var(--accent);color:var(--accent-fg)} }
.daystrip-day--on .daystrip-w,.daystrip-day--on .daystrip-n{color:var(--accent-fg)}
.daystrip-day--on .daystrip-dot{background:var(--accent-fg)}
.daystrip-day:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.pring{position:relative;display:inline-block;width:var(--pring-d,6.25rem);height:var(--pring-d,6.25rem);flex:none}
.pring-svg{display:block;width:100%;height:100%;transform:rotate(-90deg)}
.pring-track,.pring-fill{fill:none;stroke-width:10}
.pring-track{stroke:var(--seg-track)}
.pring-fill{stroke:var(--meter-c,var(--accent));stroke-linecap:round;stroke-dasharray:clamp(0,var(--p,0),100) 100}
@media (prefers-reduced-motion:no-preference){ .pring-fill{transition:stroke-dasharray var(--dur-slow) var(--ease-out)} }
.pring-n{position:absolute;inset:0;display:grid;place-items:center;font-size:var(--fs-display);font-weight:var(--fw-strong);font-variant-numeric:tabular-nums}
/* Round check (phone shell A leg 4): the circle check the To-Do rows and Today's Due list draw, as a pooled class for any native
   checkbox row (the Plan agenda). 24px, an inset ring at rest (`--chk-c`), accent fill + tick when checked; the input stays native. */
input.chk-round[type=checkbox]{appearance:none;-webkit-appearance:none;flex:none;width:24px;height:24px;margin:0;border-radius:var(--radius-pill);
  background:none;box-shadow:inset 0 0 0 2px var(--chk-c,var(--muted));cursor:pointer}
input.chk-round[type=checkbox]:checked{background-color:var(--accent);box-shadow:none;background-image:var(--check-mark);background-repeat:no-repeat;background-position:center;background-size:14px}
input.chk-round[type=checkbox]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* variant --compact (leg 8): above a phone the Today queue's circle stays the 20px / 1.5px --bd ring it always was; a phone keeps the 24px base */
@media (min-width:48.01rem){
  input.chk-round--compact[type=checkbox]{width:20px;height:20px;box-shadow:inset 0 0 0 1.5px var(--bd)}
  input.chk-round--compact[type=checkbox]:checked{box-shadow:none;background-size:12px}
}
.segbar{display:flex;gap:var(--sp-2)}
.segbar-cell{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:var(--sp-1)}
.segbar-seg{display:block;height:8px;border-radius:var(--radius-pill);background:var(--seg-track)}
.segbar-seg[data-s=done]{background:var(--meter-c,var(--accent))}
.segbar-seg[data-s=part]{background:color-mix(in srgb,var(--meter-c,var(--accent)) 40%,transparent)}
.segbar-name{font-size:var(--fs-micro);font-weight:var(--fw-strong);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.segbar-seg[data-s=todo]+.segbar-name{color:var(--muted)}

/* --- No iOS focus zoom (phone shell v2 leg 8, 2026-10-23): Safari zooms the whole layout on focus of any field under 16px and leaves it
   zoomed. On a phone every text field is at least 16px (max() keeps a larger pooled size where the parent already is). Sets only font-size. */
@media (max-width:48rem){
  html input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=button],[type=submit]),
  html textarea,html select,html .bare-field{font-size:max(16px,1em)}
}
