/* =====================================================================
   Beaufort Life Manager — PWA styles
   Ported from the Beaufort cockpit (beaufort-intelligence/cockpit/pwa/
   styles.css) so the two surfaces share one visual language. Token names
   mirror website/assets/site.css :root, per that file's note that mirroring
   is what stops palette drift.

   The four things doing the work, in order of impact:
     1. Spectral (serif) body type — not a system sans
     2. 4px radii — sharp, not the 10-14px rounding of a default component kit
     3. Translucent cards with backdrop blur, floating over the star field
     4. Mono/uppercase/letter-spaced kickers and buttons
   ===================================================================== */

/* =====================================================================
   BF-10b — "Dusk". The palette below is the approved preview's, taken
   verbatim from cockpit/prototypes/design-preview-dusk-home.html.

   It is expressed as the Dusk names FIRST and the old cockpit names as
   aliases pointing at them, deliberately. ~1500 lines of rules downstream
   reference --ice/--steel/--card-bg/--gold-bright, and re-pointing the
   aliases repaints every one of them at once. Redefining each rule instead
   would have left whichever screens this session did not reach still
   rendering the old steel-and-Spectral look, which is the exact complaint
   BF-10b exists to answer.
   ===================================================================== */
:root {
  /* Dusk tokens */
  --night: #131629;
  --dusk-hi: #3a2c4e;
  --dawn: #c9856b;
  --day-hi: #4a5a82;
  --ink: #f6f0e2;
  --ink-dim: rgba(246, 240, 226, .82);
  --ink-faint: rgba(246, 240, 226, .64);
  --gold: #efc074;
  --gold-soft: rgba(239, 192, 116, .18);
  --dusk-card: rgba(20, 18, 34, .72);
  --card-line: rgba(246, 240, 226, .14);
  /* Per-feature embers. One hue per feature, used as a 2px top seam on a tile
     and as its badge fill, so a feature is recognisable before it is read. */
  --ember-astro: #a897e0;
  --ember-kitchen: #e6a869;
  --ember-quest: #efc074;
  --ember-file: #8fc4b9;
  --ember-health: #d79cad;
  --ember-task: #8aa6d8;
  /* The calendar's second colour, and the whole of its language: gold means it
     wants Jack, this calm blue means the system has it. */
  --ember-cal: #8fa3cf;
  /* BF-10b §12. The constraint amber, from the approved matter preview. It is NOT
     a warning colour and it is deliberately not red: a constraint is a standing
     fact about the case ("do not involve the landlord"), not an alarm, and rule 10
     has no alarms in it. Measured 8.2:1 on the sheet's own background at the
     lightest sky, so the pinned line reads in daylight. */
  --amber: #e2a86b;
  --amber-soft: rgba(226, 168, 107, .08);
  --amber-line: rgba(226, 168, 107, .35);

  --display-font: 'Fraunces', Georgia, 'Times New Roman', serif;
  --ui-font: 'Instrument Sans', system-ui, -apple-system, sans-serif;

  /* Aliases: the old names, re-pointed. Nothing below this line in the file
     needed changing to pick the new palette up. */
  --bg: var(--night);
  --bg-deep: #0d1020;
  --ice: var(--ink);
  /* Was #7D8695 steel. Now the preview's faint ink, which measures 7.1:1 on a
     card and (with the day/dawn scrim below) 4.9:1 at its worst directly on the
     sky. The scrim exists because of that measurement: see .sky.day/.sky.dawn. */
  --steel: var(--ink-faint);
  --parchment: var(--ink);
  --parchment-dim: var(--ink-dim);
  --cyan-glow: var(--ember-astro);
  --antique-gold: #d9a95f;
  --gold-bright: var(--gold);
  --green-live: #7fd3a4;
  --red-warn: #e0846d;

  /* Cockpit extensions */
  --card-bg: rgba(20, 18, 34, .62);
  --card-bg-strong: var(--dusk-card);
  --card-border: var(--card-line);
  --card-border-strong: rgba(246, 240, 226, .26);
  --card-blur: 14px;
  --surface-radius: 18px;
  --kicker-font: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace;

  /* Aliases kept so existing markup/JS class names keep working */
  --panel: var(--card-bg);
  --border: var(--card-border);
  /* BF-10a U-7: --card was referenced by the answer/capture inputs at
     style.css:549 / :629 but never defined. Result: `background: var(--card)`
     computed to the initial value (transparent), so the inputs disappeared
     against the wallpaper. Aliased to --card-bg-strong for opacity contrast. */
  --card: var(--card-bg-strong);
  --text: var(--ice);
  --muted: var(--steel);
  --accent: var(--gold-bright);
  --accent-press: var(--antique-gold);
  --ok: var(--green-live);
  --warn: var(--gold-bright);
  --danger: var(--red-warn);
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }

html { font-size: 16px; -webkit-text-size-adjust: 100%; }

/* Zoom behaviour (iPad). BF-10b removed the pinch-zoom lock from the viewport
   meta; what remains here are the two that are not accessibility regressions:
   1. iOS force-zooms on focus of any input/textarea/select under 16px — this is
      the one that bites, and no viewport meta overrides it. Hence the 16px floor.
   2. Double-tap-to-zoom — killed by touch-action: manipulation.
   Pinch-zoom is now allowed. Deliberately: magnifying the screen is the first
   thing a household member with poor near vision reaches for, and blocking it
   fails WCAG 1.4.4. */
* { touch-action: manipulation; }
input, textarea, select, button { font-size: 16px; }

body {
  background: var(--night);
  color: var(--ink);
  font-family: var(--ui-font);
  font-size: 17px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: contain;
  min-height: 100dvh;
  position: relative;
  overflow-x: hidden;
}

/* Vignette. Kept, but taken from 0.55 to 0.22: at full strength it crushed the
   warm glow the dusk and dawn skies put along the bottom edge, which is the one
   thing the palette is for. It still stops the star field reading as a
   screensaver. Sits above the sky and stars, below the content. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  background:
    radial-gradient(ellipse 140% 100% at 50% 40%, transparent 45%, rgba(0,0,0,0.22) 100%);
}

/* ---------- the sky is the clock (BF-10b) ----------
   Four stacked full-bleed gradients; app.js gives exactly one the .on class from
   the real hour and re-checks every minute. Crossfade is 2.4s, which is slow
   enough that the change is noticed rather than seen. */
.sky {
  position: fixed; inset: 0; z-index: -2;
  transition: opacity 2.4s ease; opacity: 0;
  pointer-events: none;
}
.sky.on { opacity: 1; }
.sky.night {
  background:
    radial-gradient(120% 70% at 50% 108%, #2b2342 0%, transparent 60%),
    linear-gradient(180deg, #0d1020 0%, #131629 55%, #1d1a33 100%);
}
.sky.dusk {
  background:
    radial-gradient(130% 75% at 50% 110%, #7a4a52 0%, rgba(122,74,82,0) 58%),
    radial-gradient(90% 50% at 50% 115%, #c9856b 0%, rgba(201,133,107,0) 55%),
    linear-gradient(180deg, #171531 0%, #2c2347 60%, #3a2c4e 100%);
}
/* ⚠️ The flat rgba(11,12,24,.38) plate on dawn and day is NOT decoration and must
   not be tidied away. The preview measured its contrast on a card; the app puts
   plenty of quiet text (--steel, i.e. --ink-faint) directly on the sky. Measured
   against the raw day gradient's lightest stop (#4a5a82) that text is 3.5:1,
   under AA. The plate takes the same stop to an effective #323c5a, where it
   measures 4.9:1. Dawn goes 3.9:1 -> 5.5:1. Dusk and night are already dark
   enough and deliberately carry no plate. */
.sky.dawn {
  background:
    linear-gradient(0deg, rgba(11,12,24,.38), rgba(11,12,24,.38)),
    radial-gradient(120% 70% at 50% 112%, #d99a5b 0%, rgba(217,154,91,0) 55%),
    radial-gradient(140% 80% at 50% 110%, #a45f6b 0%, rgba(164,95,107,0) 62%),
    linear-gradient(180deg, #1a1c38 0%, #33305c 55%, #58466a 100%);
}
.sky.day {
  background:
    linear-gradient(0deg, rgba(11,12,24,.38), rgba(11,12,24,.38)),
    radial-gradient(120% 80% at 50% 115%, #5e6f9c 0%, rgba(94,111,156,0) 60%),
    linear-gradient(180deg, #232946 0%, #34406b 60%, #4a5a82 100%);
}
.stars {
  position: fixed; inset: 0; z-index: -1;
  pointer-events: none; transition: opacity 2.4s ease;
}
.stars circle { fill: #f2ead9; }
body[data-sky="day"]   .stars { opacity: 0; }
body[data-sky="dawn"]  .stars { opacity: .25; }
body[data-sky="dusk"]  .stars { opacity: .55; }
body[data-sky="night"] .stars { opacity: .9; }

/* The whole point of the sky is that it moves. Honour the system setting. */
@media (prefers-reduced-motion: reduce) {
  .sky, .stars { transition: none; }
}

/* Content rides above both the field and the vignette. Cards are deliberately
   translucent here (unlike the first pass): the glass-over-starfield is the
   cockpit look, and --card-bg is dark enough to hold ice text at contrast. */
/* `dialog` is deliberately NOT in this rule. A dialog opened with showModal() is
   promoted to the browser's top layer, where z-index does nothing — and giving it
   `position: relative` puts it back into normal document flow. In WebKit that renders
   it *after* #app's 100dvh-tall content, i.e. off the bottom of the screen, so tapping
   "Account" appeared to do nothing at all (Jack, installed PWA on iPad, 2026-07-29).
   Chromium tolerated it, which is why a desktop check missed it. */
/* BF-33 §1: `.capture-bar` came OUT of this selector with the tray it named. The
   dock is `position: fixed` with its own stacking context (z-index 140), so
   `position: relative; z-index: 2` here was already dead weight overridden further
   down the file — and leaving a stale name in a shared selector is how a rename
   half-lands. */
#app, #login, header { position: relative; z-index: 2; }

#app {
  max-width: 920px; margin: 0 auto;
  /* Bottom padding clears the fixed nav. It has to grow with the same inset the nav
     pads by, or the last card sits under the bar on a device with a home indicator. */
  padding: 16px 20px calc(92px + env(safe-area-inset-bottom, 0px));
}

/* ---------- header ---------- */
header {
  display: flex; justify-content: space-between; align-items: center;
  /* wrap: at 375px the three header controls overflowed by 23px and the whole page
     scrolled sideways. Found at S5 while checking the digest card, not by looking at
     the header. Jack's iPad mini is wider than that in portrait, but a split-view or a
     narrow window is not, and a page that slides horizontally feels broken. */
  flex-wrap: wrap;
  gap: 12px; padding: 10px 0 22px;
  padding-top: calc(10px + env(safe-area-inset-top, 0px));
}
h1 {
  font-family: var(--display-font); font-weight: 420;
  font-size: 20px; color: var(--ink);
  letter-spacing: .01em; display: flex; align-items: center; gap: 12px;
}
/* Serpent mark. Same asset + technique as beaufort-intelligence.com: an alpha
   silhouette used as a CSS mask, so the colour follows --parchment rather than
   being baked into the file. */
h1 .mark {
  width: 22px; height: 28px; display: block; flex: none;
  background: var(--parchment);
  -webkit-mask: url("icons/beaufort-mark.png") center/contain no-repeat;
          mask: url("icons/beaufort-mark.png") center/contain no-repeat;
}
.head-right { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }

/* ---------- kickers (section headings) ---------- */
h2 {
  font-family: var(--kicker-font);
  font-size: .66rem; font-weight: 500; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-faint);
  margin: 30px 0 12px;
}
/* ---- the worker light (Jack, 2026-08-24) ----------------------------------
   THREE states, and the middle one is the whole point.

   The old `.heartbeat` chip had four (`—`, `.ok`, `.stale`, `.blocked`) and its
   text said things like "sweep 4200m ago" in warning gold. That is a true number
   attached to a false implication: the workers have been deliberately unloaded
   since 2026-08-08 (CLAUDE.md rule 11, and the `_workers_paused` sentinel row the
   Lambda already reports as `paused`), so the app was showing a fault light for a
   decision. Rule 10 — nothing surfaced to Jack may read as an alarm it is not.

     green  a heartbeat arrived when one was due. The promise kept.
     grey   deliberately paused. Calm, unlit, no pulse — it must read as OFF, not
            as broken, so it is the same --steel every other quiet label uses.
     red    a heartbeat was EXPECTED and did not come. Only the server decides
            this (`stale`), and the server does not set it while paused.

   Amber survives for `blocked`, which is a fourth FACT and not a fourth state of
   this dot: the sweeps are fine and the AI step is not, so it keeps its own
   banner and the dot goes amber to point at it. */
.worker-dot {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--kicker-font);
  font-size: 10px; letter-spacing: 1px; text-transform: uppercase;
  color: var(--steel);
}
.worker-dot-light {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--steel);
  /* No glow in the resting state. A grey dot that glows is a grey dot that is
     asking to be looked at, which is the opposite of what paused means. */
  box-shadow: none;
}
.worker-dot.ok .worker-dot-light {
  background: var(--green-live);
  box-shadow: 0 0 0 3px rgba(126, 200, 148, .16);
}
.worker-dot.blocked { color: var(--gold-bright); }
.worker-dot.blocked .worker-dot-light {
  background: var(--gold-bright);
  box-shadow: 0 0 0 3px rgba(233, 206, 122, .18);
}
.worker-dot.missed { color: var(--red-warn); }
.worker-dot.missed .worker-dot-light {
  background: var(--red-warn);
  box-shadow: 0 0 0 3px rgba(224, 122, 122, .18);
}
/* The paused dot never pulses; the live one breathes slowly so "green" is a
   liveness claim you can watch rather than a colour you have to trust. */
@media (prefers-reduced-motion: no-preference) {
  .worker-dot.ok .worker-dot-light { animation: worker-breathe 3.2s ease-in-out infinite; }
}
@keyframes worker-breathe {
  0%, 100% { box-shadow: 0 0 0 3px rgba(126, 200, 148, .16); }
  50%      { box-shadow: 0 0 0 5px rgba(126, 200, 148, .05); }
}

/* ---------- surfaces ---------- */
.card {
  background: var(--card-bg);
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
  border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
  padding: clamp(14px, 3.5vw, 22px);
  display: flex; flex-direction: column; gap: 12px;
}
.banner {
  background: var(--card-bg-strong);
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
  border: 1px solid var(--antique-gold);
  border-left: 2px solid var(--gold-bright);
  border-radius: var(--surface-radius);
  padding: 12px 16px; font-size: 15px; margin-bottom: 16px;
}
/* AI-blocked is a different fact from a stale sweep and reads differently: red
   edge, and the banner text carries the fix rather than just the alarm. */
.banner-alert { border-color: var(--red-warn); border-left-color: var(--red-warn); }
.banner-alert code {
  font-family: var(--kicker-font); font-size: 0.9em;
  background: rgba(197, 106, 85, 0.16); padding: 1px 5px; border-radius: 3px;
}
/* D-159 · the view-as bar. Gold rather than red on purpose: this is a state Jack
   chose and can leave in one tap, not a problem to be alarmed about (rule 10). Fixed
   to the top of the VIEWPORT so it survives every screen change, and full width so
   the tap target is the whole thing. A modal sheet paints over it, which is correct:
   while a sheet is open the sheet is what you are looking at, and every sheet closes
   back to a screen that carries this. */
.actas-bar {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  display: block; width: 100%; border: 0; border-bottom: 1px solid var(--gold-bright);
  background: var(--gold-soft);
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
  color: var(--ink); font: inherit; font-size: 14px; letter-spacing: .2px;
  padding: 10px 16px; text-align: center; cursor: pointer;
}
.actas-bar:hover, .actas-bar:focus-visible { background: rgba(239, 192, 116, .28); }
/* The bar is fixed, so it would otherwise sit ON the header. Pushing the whole app
   down by its height is a body class rather than a measurement: one number, set in
   one place, and nothing to recompute when the viewport changes. */
body.acting-as #app { padding-top: 40px; }

/* Queue depth is information, not an alarm — kicker weight, no border, no box. */
.queue-note {
  font-family: var(--kicker-font);
  font-size: 10px; letter-spacing: 1px; text-transform: uppercase;
  color: var(--steel); margin-bottom: 12px;
}
/* ---------- account & security screen ---------- */
/* ⚠️ `gap: 0` MEANT TWO BLOCKS TOUCHED AND THE REST DID NOT (measured 2026-08-24).
   Every gap on this sheet came from `.acct h2`'s own margins, so the rhythm existed
   only where there happened to be a heading between two blocks. Where there was not,
   the gap was ZERO and the cards butted together:
     `.acct-top` → the identity card      0px  (a card jammed under the sticky bar)
     Passkey card → `#acct-actas-card`    0px  (View as wraps its OWN h2, so the
                                                sheet-level selector never saw one)
   Everywhere else: 22px. The fix is a real `gap` on the column, so spacing is a
   property of the STACK rather than of whichever child remembered to carry a margin —
   and the heading margin drops to the difference, so the 22px above a heading is
   unchanged and nothing else has to move. */
.acct { display: flex; flex-direction: column; gap: 14px; }
.acct h2 { margin: 8px 0 0; }
/* Negative SIDE margins pull this to the dialog's own edges through its 22px padding, so
   the sticky bar spans the full width and nothing scrolls out from behind it.
   No negative TOP margin, and this is load-bearing: `position: sticky; top: 0` pins to
   the scrollport edge, which sits 22px below where a -22px top margin puts the bar. The
   two fought, sticky won, and the bar painted 22px over whatever followed it — measured
   in both dialogs that use this class. The dialogs drop their own top padding instead
   (see #capture-dialog / #acct-dialog below), so the bar is still flush to the top. */
.acct-top {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 0 -22px; padding: 12px 22px;
  background: var(--card-bg-strong);
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
  border-bottom: 1px solid var(--card-border);
}
/* ⚠️ A SHEET TITLE, NOT A KICKER (found by BF-10b §7's harness, 2026-08-13). The
   global `h2` rule above turns every h2 in the app into a 0.66rem uppercase mono
   section label, and `.acct-top h2` only ever reset its margin — so EVERY dialog
   title in the app has been shipping as "ASK THE SKY" in tiny mono since §2, while
   the tab screens next to them wear `.screen-title` in Fraunces at 1.5rem. The
   shared-chrome rule the whole Dusk set is built on says the drag handle, the
   Fraunces title and the one 44px Close pill are IDENTICAL on every sheet; a dialog
   is a sheet. Nothing had ever measured a dialog's title font, so it never said so.
   Same values as `.screen-title`, deliberately, because they are the same thing. */
.acct-top h2 {
  margin: 0;
  font-family: var(--display-font); font-weight: 420;
  font-size: 1.5rem; letter-spacing: 0; text-transform: none;
  color: var(--ink);
}
.acct-small { font-size: 14px; line-height: 1.5; }
.acct-block { display: flex; width: 100%; margin: 10px 0; }
.acct-foot { margin-top: 22px; }
.acct-state { font-size: 15px; }
.acct-state.ok { color: var(--green-live); }
.acct-state.warn { color: var(--gold-bright); }
.acct-msg { font-size: 14px; color: var(--steel); }
.acct-msg.ok { color: var(--green-live); }
.acct-msg.warn { color: var(--red-warn); }
/* BF-17 Part B: the registered passkeys, one line each. Read-only — this is a list
   of facts about the account, not a set of controls to tend, so it borrows the
   access log's shape rather than the settings-row shape. */
.acct-pk-list { list-style: none; margin: 10px 0 0; padding: 0; }
.acct-pk-list li {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  padding: 10px 0; border-top: 1px solid var(--card-border);
}
.acct-pk-list li:first-child { border-top: none; }
.acct-pk-name { font-size: 15px; }
.acct-pk-when {
  font-family: var(--kicker-font); font-size: 12px; letter-spacing: 1px;
  color: var(--steel); white-space: nowrap;
}

/* The TOTP secret: monospace and letter-spaced because it gets typed by hand onto
   another device, and a misread character is a support call with yourself. */
.acct-secret {
  display: block; font-family: var(--kicker-font); font-size: 15px;
  letter-spacing: 2px; word-break: break-all; line-height: 1.6;
  background: rgba(168, 200, 224, 0.08); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius); padding: 10px 12px; margin-bottom: 4px;
}
#acct-dialog { max-width: 560px; }
/* Matches .task-actions input, but full width and spaced out: this field takes six
   digits copied off another device, so it wants to be big and unambiguous. 16px is
   the floor that stops iOS force-zooming on focus (see the zoom-lock note above). */
#acct-2fa-code {
  width: 100%; margin-top: 12px; min-height: 44px;
  background: var(--bg-deep); border: 1px solid var(--card-border);
  color: var(--ice); border-radius: var(--surface-radius);
  padding: 12px; font-size: 18px; font-family: var(--kicker-font);
  letter-spacing: 6px; text-align: center;
}
#acct-2fa-code:focus { outline: none; border-color: var(--card-border-strong); }
#acct-2fa-code::placeholder { letter-spacing: 1px; font-size: 14px; }

.muted { color: var(--steel); }
.stack { display: flex; flex-direction: column; gap: 8px; }

/* ---------- buttons ---------- */
.ghost, .task-actions .primary, .scan-btn, .capture-dock button {
  -webkit-appearance: none; appearance: none;
  font-family: var(--kicker-font);
  font-weight: 500; letter-spacing: 2px; text-transform: uppercase;
  border-radius: var(--surface-radius);
  cursor: pointer; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  transition: background .15s, border-color .15s, color .15s;
  -webkit-tap-highlight-color: transparent;
}
.ghost {
  background: transparent; border: 1px solid var(--card-border-strong);
  color: var(--ice); font-size: 11px; padding: 11px 18px;
}
.ghost:active { border-color: var(--gold-bright); color: var(--gold-bright); }
.task-actions .primary {
  background: var(--gold-bright); border: 1px solid var(--gold-bright);
  color: var(--bg); font-size: 11px; padding: 11px 18px;
}
.task-actions .primary:active { background: var(--antique-gold); border-color: var(--antique-gold); }

.scan-btn {
  width: 100%; padding: 24px; font-size: 13px;
  background: var(--gold-bright); border: 1px solid var(--gold-bright);
  color: var(--bg); gap: 12px;
}
.scan-btn:active { background: var(--antique-gold); border-color: var(--antique-gold); }
.scan-icon { font-size: 22px; line-height: 1; letter-spacing: 0; }
.progress { margin-top: 10px; font-size: 14px; color: var(--steel); }

/* ---------- lists ---------- */
.doc-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.doc-list li {
  background: var(--card-bg);
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
  border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
  padding: 13px 15px; display: flex; justify-content: space-between;
  gap: 10px; align-items: center; cursor: pointer;
}
.doc-list li:active { border-color: var(--card-border-strong); }
/* ⚠️ This was `white-space: nowrap` with an ellipsis that never appeared, because a
   flex child defaults to min-width:auto and simply overflowed instead. Filenames were
   short enough that nobody noticed; real TITLES are not, and "Debt letter from Lowell,
   12 Jun" lost its year. A title cut where the reader cannot see it was cut is the
   same defect as S17 §5's caps in a different coat. It wraps now, to two lines, which
   fits every title doc_title() can produce. */
.doc-name { min-width: 0; flex: 1; }
.doc-list .doc-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.doc-meta { font-size: 13px; color: var(--steel); }

/* BF-10b §4 · the unfiled row's own filing controls. The row is a column here, not
   the default space-between row: the guess and its two buttons belong UNDER the title,
   not beside it. */
.doc-list li.unfiled { flex-direction: column; align-items: stretch; }
.doc-guess {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px;
}
.doc-guess .guess-q { font-size: 0.85rem; color: var(--ink-dim); }
.doc-guess .guess-q b { color: var(--gold); font-weight: 600; }
.doc-guess button {
  min-height: 44px; padding: 8px 16px; border-radius: 999px; cursor: pointer;
  font-family: inherit; font-size: 0.85rem;
}
.doc-guess .guess-yes {
  background: var(--gold-soft); border: 1px solid rgba(239, 192, 116, .4);
  color: var(--ink); font-weight: 500;
}
.doc-guess .guess-yes:active { background: rgba(239, 192, 116, .3); }
.doc-guess .guess-yes:disabled { opacity: .55; }
.doc-guess .guess-other {
  background: none; border: 1px solid var(--card-line); color: var(--ink-dim);
}
.doc-guess .guess-other:active { border-color: var(--gold); color: var(--gold); }
/* An honest failure, at the control that failed, carrying the way forward. */
.doc-guess .guess-said {
  flex-basis: 100%; font-size: 0.82rem; color: var(--ember-quest); line-height: 1.5;
}

.chip {
  font-family: var(--kicker-font);
  font-size: 9px; letter-spacing: 1.5px; text-transform: uppercase;
  padding: 4px 8px; border-radius: var(--surface-radius); white-space: nowrap;
  border: 1px solid var(--card-border); color: var(--steel);
}
.chip.uploaded { border-color: var(--cyan-glow); color: var(--cyan-glow); }
.chip.filed { border-color: var(--green-live); color: var(--green-live); }
.chip.pending_upload { border-color: var(--antique-gold); color: var(--gold-bright); }
.chip.action_needed { border-color: var(--red-warn); color: var(--red-warn); }

/* ---------- login ---------- */
#login { display: flex; min-height: 100dvh; align-items: center; justify-content: center; }
.login-box {
  text-align: center; display: flex; flex-direction: column;
  gap: 18px; width: min(340px, 84vw); align-items: center;
}
.login-box h1 { justify-content: center; }

/* ---------- dialog ---------- */
dialog {
  background: var(--card-bg-strong);
  -webkit-backdrop-filter: blur(calc(var(--card-blur) * 1.5));
          backdrop-filter: blur(calc(var(--card-blur) * 1.5));
  color: var(--ice);
  border: 1px solid var(--card-border-strong);
  border-radius: var(--surface-radius);
  padding: 22px; width: min(640px, 92vw); margin: auto;
}
/* Pin and centre explicitly rather than relying on the UA's modal placement, and cap
   the height so long content scrolls inside the dialog instead of running off-screen.
   dvh, not vh: Safari's vh ignores the dynamic toolbars.

   ── Jack, 2026-08-24: A FIXED SHEET, not a tall box that runs off the screen ──
   Three things were wrong and they had three different causes, so they get three
   different lines rather than one bigger number:

   1. THE CAP IGNORED THE SAFE AREA. `calc(100% - 40px)` is 20px of clearance top
      and bottom on a device whose home indicator alone wants more than that, so
      the last control could sit under it and simply not respond to a tap.
   2. THE CAPTURE PILL. `.capture-dock` is `position: fixed` at
      `max(26px, safe-area + 10px)` and the pill inside it is 50px tall, so the
      bottom ~86px of the screen is spoken for. A modal dialog is in the TOP LAYER,
      so it paints over the pill rather than under it — but the pill is still
      visible through the .72 backdrop, and a sheet whose last button lands in that
      band reads as one control overlapping another. `--sheet-dock` is that band,
      derived from the same expression `.capture-dock` uses so the two cannot drift.
   3. NOTHING SCROLLED "INSIDE" as far as the eye was concerned, because the sheet
      ran past the bottom of the screen and the content simply carried on. The cap
      is what makes the internal scroll visible as an internal scroll; `.acct-top`
      was already `position: sticky`, so the title and its Close now genuinely stay
      put while the body moves under them.

   ⚠️ The page behind is scroll-locked in app.js, NOT here: `overscroll-behavior`
   stops a scroll CHAIN, and does nothing about a touch that begins on the backdrop.
   See `lockBodyScrollWithDialogs()`. */
dialog[open] {
  position: fixed; inset: 0;
  /* The band along the bottom that the floating capture pill owns: its own offset,
     plus its 50px height, plus 14px so the sheet is clear of it rather than on it.
     Derived from the same expression `.capture-dock` uses so the two cannot drift. */
  --sheet-dock: calc(max(26px, env(safe-area-inset-bottom, 0px) + 10px) + 50px + 14px);
  --sheet-top-gap: calc(env(safe-area-inset-top, 0px) + 20px);
  /* ⚠️ BOTTOM-ANCHORED, NOT CENTRED, and this is the correction that made the
     clearance actually true. `margin: auto` centres, which means it splits the free
     space EVENLY — so reserving the pill's band in `max-height` gave the bottom only
     half of it, and the sheet's edge still cut across the top of the pill. Measured:
     at 1112px tall the sheet's bottom landed at 1028 and the pill's top at 1026. Two
     pixels, on the real device, which is exactly the kind of overlap that is invisible
     in a screenshot and obvious in the hand.
     `margin-top: auto` with a fixed `margin-bottom` pins the bottom edge above the
     pill and lets the sheet grow upward, which is also the app's own sheet language:
     `#screen-peek` exists to show Home above a sheet rather than behind a centred box.
     The `max-height` below is what keeps the top edge from going off-screen, so the
     two are one calculation and must be changed together. */
  margin: auto auto var(--sheet-dock) auto;
  max-height: min(85dvh, calc(100dvh - var(--sheet-dock) - var(--sheet-top-gap)));
  overflow: auto; overscroll-behavior: contain;
  /* WebKit momentum inside the sheet, so the internal scroll feels like the page. */
  -webkit-overflow-scrolling: touch;
  /* Breathing room at the end of the scroll, so the last control is never flush
     against the sheet's own edge. On the dialog rather than on an inner wrapper
     because these dialogs are flat: the header and every content row are siblings,
     and there is no body element to hang it on that all twenty share. */
  padding-bottom: calc(26px + env(safe-area-inset-bottom, 0px));
}
dialog::backdrop { background: rgb(0 0 0 / .72); }
/* The other half of `lockBodyScrollWithDialogs()` in app.js. `overflow: hidden`
   alone does not hold iOS still; `position: fixed` does, and the JS carries the
   scroll offset across so the page is exactly where it was on the way back out. */
body.sheet-open {
  position: fixed; left: 0; right: 0; width: 100%;
  overflow: hidden;
}

/* No pinch-zoom, no double-tap zoom (Jack, 2026-08-24). `pan-x pan-y` keeps every
   scroll and every swipe — the task cards and the document pager both depend on
   those — and refuses only the scaling gesture. See app.js for why the viewport
   meta tag is not enough on its own, and index.html for the WCAG 1.4.4 cost Jack
   accepted for his own device. */
html { touch-action: pan-x pan-y; }
/* BF-26 §1: the `#doc-detail iframe` pair that used to sit here went with the markup.
   A canvas deliberately does NOT inherit this rule — `.pdf-canvas` carries its own
   sizing, and a 14px top margin here would push every page away from its own
   "Page 2 of 9" label. */
#doc-detail img { max-width: 100%; border-radius: var(--surface-radius); margin-top: 14px; }
#doc-detail dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; margin-top: 12px; font-size: 15px; }
#doc-detail dt { font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
                 text-transform: uppercase; color: var(--steel); align-self: center; }
dialog .ghost { margin-top: 18px; }
/* ...but never the close control in a sticky dialog header. There the 18px inflates the
   bar to 87px while it only contributes 65px to the flow, so it paints 22px OVER the
   first row of content beneath it. Measured at 375x812: it clipped the top line of the
   capture placeholder and the account dialog's "signed in" row. Jack saw the capture one.
   Fixing the margin fixes both, because both use .acct-top. */
dialog .acct-top .ghost { margin-top: 0; }
/* ...and never inside a .task-actions row either. That row is `display: flex;
   align-items: center`, so an 18px top margin on only SOME of its buttons pushes those
   ~9px below the primary and the row stops looking like a row. Measured at 390px in the
   approval dialog on 2026-07-30: "Approve" sat at y=919 and "Ask for a change" at y=928.
   Same rule, same cause as the .acct-top case above: a margin meant for stacked ghost
   buttons leaking into a horizontal group. */
dialog .task-actions .ghost { margin-top: 0; }

/* The two dialogs whose first child is a sticky .acct-top give up their own top padding,
   so the bar sits flush at the top with no negative margin to fight the sticky offset.
   ⚠️ `padding-top`, never the `padding` shorthand: the shorthand would also reset the
   bottom padding that clears the capture pill, and it would do it silently. */
/* ⚠️ STRUCTURAL, NOT A LIST OF IDS (2026-08-24). This was two hand-maintained id
   lists — one here and one beside the approval rules 400 lines down — and they had
   drifted from each other AND from the markup: `#approval-dialog` was in the second
   and not the first, and `#finance-line-dialog`, `#recipe-add-dialog` and
   `#recipe-dialog` were in neither, so all three painted their sticky bar 22px below
   the top of the sheet with content scrolling up into the gap behind it. Every dialog
   that adds a sticky header from now on gets this for free, which is the only version
   of this rule that cannot go stale.
   `:has()` is Safari 15.4+ / Chrome 105+, and the iPad is far past both. Where it is
   not supported the rule simply does not apply and every sheet keeps the 22px it has
   today — the old behaviour, not a worse one. */
dialog:has(> .acct-top) { padding-top: 0; }

/* Capture sheet spacing. The Save button used to sit at exactly 0px from the bottom of
   the matter field (measured, not estimated), which read as one control. A destructive-
   looking gap is not the point: the point is that Save is a separate decision. */
#capture-dialog #capture-text { margin-top: 14px; }
#capture-dialog .task-actions { margin-top: 20px; }
#capture-dialog #capture-said:not([hidden]) { margin-top: 12px; }

/* ---------- the capture dock (BF-33 §1: the tray is gone) ----------------------
   ⚠️ THIS USED TO BE `.capture-bar`: a full-width fixed glass tray with a
   `border-top`, `background: var(--card-bg-strong)` and a blur, holding one pill.
   Jack, 2026-08-17: "The bottom nav bar isn't supposed to be a bar. It's supposed
   to be a sticky pill-shaped component." The tray spanning the width is most of
   what made it read as a bar, and the visible text labels were the rest.

   So this element keeps NOTHING of the tray. It is a positioning host: full width
   so the pill can centre without a `translateX(-50%)` (which would fight the
   expand transition on `width`), and `pointer-events: none` so a strip of dead
   glass is not sitting over the bottom 68px of every screen swallowing taps. Only
   its children take pointer events.

   The GLASS did not disappear, it moved: `.nav-pill` below now carries the misty
   dark card material the tray used to provide, which is also what keeps the gold
   glyphs at their measured contrast now that there is no guaranteed surface
   behind them. */
.capture-dock {
  position: fixed; z-index: 140;
  left: 0; right: 0;
  /* The approved Dusk home preview floats its one piece of chrome at `bottom: 26px`
     (`design-preview-dusk-home.html`, `.add`). max(), not `26px + inset`, for the
     reason the old bar's padding documented and got wrong twice: adding a fixed
     offset ON TOP of the home-indicator inset double-counts the gap, and a calc()
     with no fallback drops the whole declaration where env() is undefined. */
  bottom: max(26px, calc(env(safe-area-inset-bottom, 0px) + 10px));
  display: flex; justify-content: center;
  padding: 0; border: none; background: none;
  pointer-events: none;
}
.capture-dock > * { pointer-events: auto; }

/* ---------- sub-navigation (To-do: Tasks | Matters, Files: Documents | Vault) ----
   Two sections under one tab, full width, because these are peers rather than two
   views of one thing. (It inherits the look of the List | Calendar toggle that used
   to sit inside Tasks; that control and its .view-toggle styles are deleted, because
   the calendar it switched to is now its own tab.) */
.sub-nav { display: flex; gap: 0; margin-bottom: 16px; }
.sub-nav button {
  flex: 1; font-family: var(--kicker-font); font-size: 11px; letter-spacing: 1px;
  text-transform: uppercase; padding: 9px 10px; cursor: pointer;
  background: transparent; color: var(--steel);
  border: 1px solid var(--card-border);
}
.sub-nav button + button { border-left: none; }
.sub-nav button.active {
  color: var(--gold-bright); border-color: var(--antique-gold);
  background: rgba(233, 206, 122, 0.06);
}

/* ---------- the two-segment toggle on a sheet (BF-10b §3) ----------
   What .sub-nav becomes on a Dusk sheet. Two differences from the thing above, and
   both are the reason it is a new class rather than a restyle: the segments are 44px
   (U-11 retired 34px controls, and BF-10b puts a floor under every target), and the
   selected one is FILLED rather than only recoloured, so the selection survives being
   read by somebody who cannot separate gold from grey.

   ⚠️ It hangs off aria-selected, which wireSubNav already sets alongside .active. Do
   not restyle it onto .active only: aria-selected is the thing a screen reader uses,
   and pinning the visible state to the same attribute is what stops the two drifting. */
.segs { display: flex; gap: 8px; margin-bottom: 18px; }
.segs button {
  flex: 1; min-height: 44px;
  font-family: var(--kicker-font); font-size: 11px; letter-spacing: 1.4px;
  text-transform: uppercase; cursor: pointer;
  background: none; color: var(--ink-dim);
  border: 1px solid var(--card-line); border-radius: 12px;
}
.segs button[aria-selected="true"] {
  background: var(--gold-soft); border-color: rgba(239, 192, 116, .45);
  color: var(--gold); font-weight: 500;
}

/* The quiet mono label over a section of a sheet. */
.sec-label {
  font-family: var(--kicker-font); font-size: 0.66rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--ink-faint); margin: 20px 0 10px;
}
.sec-label:first-child { margin-top: 4px; }

/* ---------- tasks / matters ---------- */
.task-card { display: flex; flex-direction: column; gap: 10px; }
.task-title { font-weight: 600; color: var(--ice); }
.task-meta { font-size: 13px; color: var(--steel); }

/* BF-10b §3: the first move, in gold, above everything but the title.
   ⚠️ The GOLD is on the move itself, not on the words "First move", because the bold
   half is the bit he has to read to act. And `.held` is a COLOUR SWAP rather than a
   dimming: rule SR6 Job 2 §1 — state is never signalled by opacity alone on body
   text, or it fails AA the moment the sky goes light. */
.first-move { font-size: 0.9rem; color: var(--ink-dim); line-height: 1.5; }
.first-move b { color: var(--gold); font-weight: 600; }
.first-move.held { color: var(--ember-task); }
.first-move .fm-why { display: block; color: var(--ink-faint); font-size: 0.82rem; }

/* One primary and one "Not right now", the preview's pair. Both 44px. */
.task-row { display: flex; gap: 8px; flex-wrap: wrap; }
.task-row button {
  min-height: 44px; padding: 8px 18px; border-radius: 999px;
  font-family: inherit; font-size: 0.9rem; cursor: pointer;
}
.task-row .primary {
  background: var(--gold-soft); border: 1px solid rgba(239, 192, 116, .4);
  color: var(--ink); font-weight: 500;
}
.task-row .primary:active { background: rgba(239, 192, 116, .3); }
.task-row .ghost {
  background: none; border: 1px solid var(--card-line); color: var(--ink-dim);
}
.task-row .ghost:active { border-color: var(--gold); color: var(--gold); }
/* The comment box, demoted under the decision it explains. */
.task-say { opacity: 1; }
.task-say input::placeholder { color: var(--ink-faint); }

.task-actions { display: flex; gap: 8px; align-items: center; }
.task-actions input {
  flex: 1; background: var(--bg-deep); border: 1px solid var(--card-border);
  color: var(--ice); border-radius: var(--surface-radius);
  padding: 10px 12px; font-size: 16px; min-width: 0; font-family: inherit;
}
.task-actions input:focus { outline: none; border-color: var(--card-border-strong); }
/* ---------- the "..." card menu (S7 §1b) ----------
   Deliberately in-flow rather than an absolutely-positioned popup. The account dialog
   taught this the hard way: WebKit put a `position: relative` dialog off-screen while
   Chromium tolerated it, so a desktop check passed and the iPad got nothing. An
   expanding block inside the card cannot be clipped, cannot land off-screen, and needs
   no measurement. */
.card-head { display: flex; gap: 10px; align-items: flex-start; }
.card-head .task-title { flex: 1; min-width: 0; }
.dots {
  background: none; border: none; color: var(--steel);
  font-size: 20px; line-height: 1; letter-spacing: 1px;
  /* 44px is Apple's minimum comfortable touch target; a "..." glyph is small. */
  min-width: 44px; min-height: 44px; margin: -10px -8px 0 0;
  border-radius: var(--surface-radius);
}
.dots:active { color: var(--gold-bright); background: rgba(233, 206, 122, 0.08); }
.card-menu {
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px; border-radius: var(--surface-radius);
  background: var(--bg-deep); border: 1px solid var(--card-border);
}
/* The shared button rule centres with justify-content, so text-align alone does
   nothing here. A menu reads better left-aligned: the eye scans one edge. */
.card-menu > .ghost { justify-content: flex-start; }
.menu-row { display: flex; gap: 8px; align-items: center; }
.menu-row input, .menu-row select, .edit-panel input {
  background: var(--bg-deep); border: 1px solid var(--card-border);
  color: var(--ice); border-radius: var(--surface-radius);
  /* 16px: anything smaller and iOS Safari zooms the whole page on focus. */
  padding: 10px 12px; font-size: 16px; font-family: inherit; min-width: 0; flex: 1;
}
.edit-panel { display: flex; flex-direction: column; gap: 8px; }

/* The learned list: what "don't show me this" taught the app. */
.learned { margin-top: 14px; font-size: 14px; color: var(--steel); }
.learned summary { cursor: pointer; padding: 8px 0; color: var(--steel); }
.learned-row {
  display: flex; gap: 10px; align-items: center; justify-content: space-between;
  padding: 10px 0; border-top: 1px solid var(--card-border); color: var(--ice);
}

/* Confirmation, with the undo in it. Fixed above the nav so it never covers it. */
.toast {
  position: fixed; left: 12px; right: 12px;
  bottom: calc(84px + max(6px, env(safe-area-inset-bottom, 0px)));
  z-index: 40; display: flex; gap: 12px; align-items: center;
  justify-content: space-between;
  padding: 12px 14px; border-radius: var(--surface-radius);
  background: rgba(12, 16, 24, 0.96); border: 1px solid var(--card-border-strong);
  color: var(--ice); font-size: 14px;
}
.toast .ghost { padding: 8px 14px; white-space: nowrap; }

.approval-text {
  background: var(--bg-deep); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
  padding: 12px; font-size: 16px; white-space: pre-wrap; width: 100%;
  color: var(--ice); font-family: inherit; min-height: 88px; line-height: 1.5;
}
.approval-text:focus { outline: none; border-color: var(--card-border-strong); }

.matter-card { cursor: pointer; }
.matter-counts { font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
                 text-transform: uppercase; color: var(--steel); margin-top: 4px; }
/* BF-10b §3: whose move it is. The chip is a COLOUR SWAP and a WORD CHANGE together,
   never a dot and never a tint on otherwise identical text, so it survives both a
   light sky and a reader who does not separate teal from gold. */
.matter-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.matter-state {
  font-family: var(--kicker-font); font-size: 0.6rem; letter-spacing: 1px;
  border-radius: 999px; padding: 3px 9px; flex: none; white-space: nowrap;
}
.matter-state.theirs { color: var(--ember-file); border: 1px solid rgba(143, 196, 185, .4); }
.matter-state.yours { color: var(--gold); border: 1px solid rgba(239, 192, 116, .45); }
.matter-line { font-size: 0.85rem; color: var(--ink-dim); margin: 5px 0 0; line-height: 1.5; }
/* Matter screen: the research button, and the questions that hold a draft (S4 §7-§8).
   The questions sit ABOVE the timeline on purpose. They are the only thing on this
   screen that is waiting on Jack, and a question he has to scroll to find is a question
   that does not get answered. No red, no badge, no count that grows: this is a request,
   not a debt. */
.matter-actions { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 8px; }
.btn {
  background: transparent; border: 1px solid var(--card-border-strong);
  color: var(--ice); font-size: 11px; padding: 11px 18px;
}
.btn:active { border-color: var(--gold-bright); color: var(--gold-bright); }
.btn:disabled { opacity: .5; }
.questions { margin-top: 20px; }
.questions h3 {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 2px;
  text-transform: uppercase; color: var(--steel); margin: 0 0 10px;
}
.question {
  border: 1px solid var(--card-border); border-radius: var(--surface-radius);
  padding: 14px; margin-bottom: 10px; font-size: 15px; line-height: 1.5;
}
.question .muted { font-size: 13px; margin-top: 6px; }
.q-answer { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.q-answer input {
  flex: 1 1 200px; min-width: 0; min-height: 44px; padding: 10px 12px;
  background: var(--card); border: 1px solid var(--card-border-strong);
  border-radius: var(--surface-radius); color: var(--ice);
  font-family: inherit; font-size: 16px;   /* 16px or iOS zooms the page on focus */
}
.q-answer input:focus { outline: none; border-color: var(--gold-bright); }

/* BF-10a U-9: the direct add-task control at the top of To-do's Tasks pane.
   Shape matches .q-answer (same input treatment for the same iOS focus-zoom
   avoidance at 16px), sits above the list rather than in a card so it never
   competes with a task for attention. The message slot fires only on failure. */
.todo-add { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; align-items: center; }
.todo-add input {
  flex: 1 1 200px; min-width: 0; min-height: 48px; padding: 10px 14px;
  background: rgba(246, 240, 226, .06); border: 1px solid var(--card-line);
  border-radius: 12px; color: var(--ice);
  font-family: inherit; font-size: 16px;
}
.todo-add input:focus { outline: none; border-color: var(--gold-bright); }
.todo-add input::placeholder { color: var(--ink-faint); }
.todo-add #todo-add-msg { flex-basis: 100%; margin: 0; }
/* BF-10b §3: the preview's filled Add, 48px so the box and the button are one shape.
   It is the only filled control on the sheet, which is the point: adding a task is
   the one thing this screen wants you to be able to do without looking. */
.todo-add .primary {
  min-height: 48px; padding: 0 22px; border: none; border-radius: 12px;
  background: linear-gradient(160deg, #f0c37e, #d99a5b); color: #241c33;
  font-family: inherit; font-size: 0.95rem; font-weight: 600; cursor: pointer;
}
.todo-add .primary:active { background: linear-gradient(160deg, #d99a5b, #c1854a); }
.todo-add .primary:disabled { opacity: .55; }

.timeline { list-style: none; margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
.timeline li { font-size: 14px; display: flex; gap: 10px; align-items: baseline; }
.timeline .tl-when {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  color: var(--steel); white-space: nowrap;
}

/* Respect the OS setting — the star field module already does, and the blur
   is expensive on older hardware. */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}

/* ---------- the morning brief (S5, DESIGN §5.9.1) ----------
   Five blocks, same order every day. The headline is the only thing allowed to change
   weight, and even then it warms rather than alarms: no red, no counts, no badges. */
.digest { display: flex; flex-direction: column; gap: 14px; }
.digest-kicker {
  flex: 1; font-family: var(--kicker-font); font-size: 10px; letter-spacing: 2px;
  text-transform: uppercase; color: var(--steel);
}
.digest-headline { font-size: 20px; line-height: 1.35; color: var(--ice); }
.digest-headline.warn { color: var(--gold-bright); }
.digest-block { display: flex; flex-direction: column; gap: 6px; }
.digest-h {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 2px;
  text-transform: uppercase; color: var(--steel);
}
.digest-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.digest-list li { font-size: 15px; color: var(--ice); line-height: 1.45; }
.digest-step { font-size: 13px; margin-top: 2px; }

/* ---- S7: decision cards, closure, capture, lighter mode -------------------- */

/* Swipe. The card moves under the finger and a single word says what letting go
   will do. No colour-only signal: the word is the affordance, so it still reads
   with reduced motion, in bright sun, and for anyone who does not see the tint. */
.task-card { position: relative; touch-action: pan-y; }
.task-card.swiping { transition: none; user-select: none; }
.task-card:not(.swiping) { transition: transform .18s ease; }
.swipe-hint {
  position: absolute; top: 10px; pointer-events: none;
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 2px;
  text-transform: uppercase; color: var(--steel); opacity: 0;
  transition: opacity .12s ease, color .12s ease;
}
.swipe-hint.right { left: 14px; opacity: 1; }
.swipe-hint.left { right: 14px; opacity: 1; }
.swipe-hint.armed { color: var(--gold-bright); }

/* Closure. Once, quietly, and it does not carry over: a count that survives to
   tomorrow is a streak, and a streak is a debt with a friendly face. */
.closure { text-align: center; padding: 40px 20px; }
.closure-count {
  font-family: var(--kicker-font); font-size: 11px; letter-spacing: 3px;
  text-transform: uppercase; color: var(--gold-bright); margin-bottom: 10px;
}
.closure-line { font-size: 20px; line-height: 1.5; color: var(--ice); }

/* ⚠️ .capture-fab is GONE, not hidden. The floating action button was deleted when
   "+" moved into the centre of the nav (FEEDBACK.md §3); Jack called it ugly, and a
   second way in would have meant two controls to keep in step. If you are looking for
   it because something still references it, that reference is the bug. */
.capture-pending { position: fixed; right: 16px; z-index: 40;
  /* 86px, up from 74px (BF-33 §1). The pill no longer sits in a tray flush to the
     bottom edge: it floats 26px up, so its top edge is at 76px and a chip at 80px
     was clearing it by four pixels. Measured against the dock's own offsets rather
     than nudged until it looked right. */
  bottom: calc(86px + max(6px, env(safe-area-inset-bottom, 0px))); }
/* S17 §5. A blocked capture needs to be readable, not alarming: no red, no badge,
   no guilt (CLAUDE.md rule 10). It is wider than the sync chip because it carries a
   sentence rather than a count, and it wraps rather than truncating, which would be
   this whole section's defect appearing in the message about the defect. */
.capture-pending.capture-blocked {
  left: 12px; right: 12px; max-width: none; white-space: normal; line-height: 1.35;
  border-color: var(--antique-gold); }
#capture-matter {
  width: 100%; min-height: 44px; padding: 10px 12px; margin-top: 6px;
  background: var(--card); border: 1px solid var(--card-border-strong);
  border-radius: var(--surface-radius); color: var(--ice);
  font-family: inherit; font-size: 16px;   /* 16px or iOS zooms the page on focus */
}
#capture-matter:focus { outline: none; border-color: var(--gold-bright); }

/* ---- S14: the assist layer ------------------------------------------------
   The strip is the card's answer to "how much of this is not my problem?", and it has
   to read at a glance without opening anything. Quiet by default: this sits on every
   task card, so anything loud here would make the whole list shout. */
.plan-strip {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  margin-top: 10px;
}
.plan-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: none; padding: 6px 0; min-height: 44px;
  color: var(--gold-bright); font-family: var(--kicker-font);
  font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase;
}
.plan-chevron { display: inline-block; transition: transform .15s ease; font-size: 14px; }
.plan-toggle.open .plan-chevron { transform: rotate(90deg); }
/* The button that turns a list of instructions into a list of offers. Outlined, not
   filled: the filled primary on a task card is Done, and nothing should compete with it. */
.do-it {
  padding: 9px 14px; border-color: var(--antique-gold); color: var(--gold-bright);
}
.do-it[disabled] { opacity: .5; }
/* Stated, never spun. The work happens in a skill on the Mac, and the Mac may be
   asleep: a spinner here would be a lie about what is happening. */
.assist-state { font-size: 13px; color: var(--steel); line-height: 1.4; }

.plan { margin-top: 10px; border-top: 1px solid var(--card-border); padding-top: 10px; }
.plan-summary { font-size: 15px; color: var(--ice); line-height: 1.5; margin-bottom: 10px; }
.plan-note { font-size: 13px; color: var(--steel); line-height: 1.5; margin-top: 10px; }
.plan-steps { list-style: none; display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; }
/* A FIXED first column, not `auto`. Each step is its own grid, so an auto column is
   sized per row: "Your yes" is 7px wider than "Mine", which pushed that one row's text
   7px right and made the whole column zigzag. Measured, then fixed at the width of the
   longest label. Scanning straight down this column is the entire point of the panel. */
.step {
  display: grid; grid-template-columns: 72px 1fr; gap: 4px 10px; align-items: baseline;
}
.step-actor {
  grid-column: 1; justify-self: stretch; text-align: center;
  font-family: var(--kicker-font); font-size: 9px; letter-spacing: 1.2px;
  text-transform: uppercase; padding: 3px 7px; border-radius: var(--surface-radius);
  border: 1px solid var(--card-border); color: var(--steel);
}
.step-system .step-actor { color: var(--gold-bright); border-color: var(--antique-gold); }
.step-approval .step-actor { color: var(--ice); border-color: var(--card-border-strong); }
.step-text { grid-column: 2; font-size: 15px; color: var(--ice); line-height: 1.45; }
.step-why, .step-result, .step-link { grid-column: 2; font-size: 13px; line-height: 1.45; }
.step-why { color: var(--steel); }
.step-result { color: var(--green-live); }
.step-link { color: var(--gold-bright); justify-self: start; }
/* Done is a strikethrough on the text only, never on the whole row: the actor label has
   to stay readable so the column still scans. No colour-only signal anywhere here. */
.step-done .step-text { text-decoration: line-through; color: var(--steel); }

/* Asks. Deliberately shaped like an answer and not like a task: no swipe, no "..." menu,
   one way out, and it leaves for good when Jack is done with it. */
.ask-answer { font-size: 15px; color: var(--ice); line-height: 1.5; margin-top: 8px; white-space: pre-wrap; }
.ask-sources { font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
               text-transform: uppercase; color: var(--steel); margin-top: 8px; }
/* Approvals (S15). The card is a summary; reading is a deliberate act. */
.approval-lede {
  margin-top: 10px; font-size: 15px; line-height: 1.5; color: var(--steel);
  font-style: italic;
}
/* The second copy of the padding-top list lived here and is deleted: see the
   `dialog:has(> .acct-top)` rule above, which now covers every sheet including the
   three this list never mentioned. */
#approval-dialog #approval-title { margin: 16px 0 14px; }
#approval-dialog label { display: block; margin: 14px 0 6px; }
.approval-full { min-height: 46vh; font-size: 15px; line-height: 1.55; }
/* The sources block is monospace and scrolls in its own box: it is a machine-written
   audit trail, not prose, and pretending otherwise makes it harder to scan. */
.approval-annotation {
  margin: 10px 0 0; padding: 12px; max-height: 34vh; overflow: auto;
  background: var(--bg-deep); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius); color: var(--steel);
  font-family: var(--kicker-font); font-size: 12px; line-height: 1.6;
  white-space: pre-wrap; word-break: break-word;
}
.approval-actions { margin-top: 18px; flex-wrap: wrap; }

/* ── BF-10b §8: the Dusk approval sheet ───────────────────────────────────────
   Three surfaces, and each one is an argument about attention.

   THE LETTER reads as a letter. It used to be the value of a textarea, which is
   what a form field looks like, and this is not a form: it is the thing that
   goes out in his name. Serif, 1.75 line height, generous measure. The edit box
   is still one tap below it (`#approval-edit-toggle`), because the point is the
   default, not the removal.

   WHERE IT GOES sits at the top, above the letter, because it is the first thing
   that has to be right and the last thing anybody re-reads.

   THE TRUTH BLOCK sits directly above the button in the app's own gold. It is a
   statement of fact from `send_gate`, not a warning, so it is never red and
   never carries an alarm colour: "this one posts by hand" is not a problem. */
.letter {
  background: rgba(246, 240, 226, .045);
  border: 1px solid var(--card-line);
  border-radius: var(--surface-radius);
  padding: 20px 22px;
  font-family: var(--display-font); font-weight: 340;
  font-size: 1rem; line-height: 1.75; color: var(--ink);
  white-space: pre-wrap; word-break: break-word;
}
#approval-dialog #approval-edit-toggle { margin-top: 10px; }

/* ── the terminal state (Jack, 2026-08-24) ───────────────────────────────────
   A card, not a banner: it is what the sheet is now ABOUT, so it carries the
   sheet's own surface rather than the thin `.acct-msg` strip the old success
   message used. The mark is a filled dot rather than a tick — a tick claims
   completion, and the honest claim on the send path is "on its way". */
.approval-done {
  margin-top: 18px; padding: 16px 18px;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-left: 3px solid var(--green-live);
  border-radius: var(--surface-radius);
  display: flex; flex-direction: column; gap: 6px;
}
.approval-done.warn { border-left-color: var(--gold-bright); }
.approval-done-head {
  display: flex; align-items: center; gap: 9px;
  font-family: var(--display-font, var(--kicker-font));
  font-size: 17px; color: var(--ice);
}
.approval-done-mark {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--green-live);
  box-shadow: 0 0 0 4px rgba(126, 200, 148, .14);
}
.approval-done.warn .approval-done-mark {
  background: var(--gold-bright); box-shadow: 0 0 0 4px rgba(233, 206, 122, .16);
}
.approval-done-detail { color: var(--steel); font-size: 15px; line-height: 1.5; }
/* `.task-actions` already lays the row out; it only needs the space above it. */
.approval-done .task-actions { margin-top: 10px; }
.approval-to {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: var(--card-bg); border: 1px solid var(--card-line);
  border-radius: var(--surface-radius); padding: 12px 16px; margin-bottom: 10px;
}
.approval-to .addr { font-size: 0.9rem; color: var(--ink-dim); flex: 1 1 60%; min-width: 0;
                     word-break: break-word; }
.approval-to .addr b { color: var(--ink); font-weight: 600; }
/* ⚠️ `min-height`, not `height`: the pill has to survive a long address wrapping
   the line above it, and every target on a Dusk screen is 44px (the §2 rule). */
#approval-dialog .approval-to .btn { min-height: 44px; margin-top: 0; flex: 0 0 auto; }

/* One citation, one line, with the day it was last verified on the right in the
   filing ember. A citation OUTSIDE the freshness window says so in words rather
   than by being a different shade of the same colour: this screen is read once,
   quickly, and "verified" vs "verified, out of date" must not depend on
   remembering which teal meant which. */
.cite {
  display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
  background: var(--card-bg); border: 1px solid var(--card-line);
  border-radius: var(--surface-radius); padding: 10px 16px; margin-bottom: 8px;
}
.cite .c { font-size: 0.85rem; color: var(--ink-dim); flex: 1 1 60%; min-width: 0; }
.cite .v {
  font-family: var(--kicker-font); font-size: 0.6rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ember-file); white-space: nowrap;
}
.cite .v.old { color: var(--gold-bright); white-space: normal; }
.approval-truth {
  background: var(--gold-soft); border: 1px solid rgba(239, 192, 116, .4);
  border-radius: var(--surface-radius); padding: 14px 16px; margin: 18px 0 0;
  font-size: 0.92rem; line-height: 1.55; color: var(--ink);
}
.approval-truth b { font-weight: 600; }
@media (max-width: 420px) {
  .approval-to { flex-direction: column; align-items: stretch; }
  #approval-dialog .approval-to .btn { width: 100%; }
  .cite { flex-direction: column; }
}

/* BF-20 part 2. Attachments and address candidates sit above the letter in their own
   boxes: both are things to LOOK at before approving, not prose to read past. On a
   phone the rows stack, because a tappable choice squeezed onto one line at 375px is
   how you approve the wrong address. */
.approval-block {
  margin: 4px 0 0; padding: 12px;
  background: var(--bg-deep); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
}
.approval-block .kicker { margin-bottom: 8px; }
.approval-row {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline;
  padding: 8px 0; border-top: 1px solid var(--card-border);
}
.approval-row:first-of-type { border-top: 0; }
.approval-row .grow { flex: 1 1 60%; min-width: 0; word-break: break-word; }
.approval-row .btn { flex: 0 0 auto; }
.approval-row .muted { font-size: 13px; }
/* Locked is a STATE, not an alarm: it says what to do next, so it takes the same
   gold as anything else waiting on a tap, never red. */
.att-locked { color: var(--gold-bright); }
/* BF-26 §2.3. The sealed note is a footnote under the state line and is deliberately
   NOT gold: gold on this screen means "waiting on a tap", and there is no tap here.
   It is the app saying what it cannot draw, which is the honest half of rule 13. */
.att-sealed { margin-top: 3px; }
@media (max-width: 420px) {
  .approval-row { flex-direction: column; align-items: stretch; }
  .approval-row .btn { width: 100%; }
}

/* The matter thread: the conversation, oldest first, his side aligned differently so a
   long history can be skimmed for "what did I say" without reading the replies. */
.thread { margin-top: 14px; display: flex; flex-direction: column; gap: 10px; }
.thread-entry {
  padding: 10px 12px; border-radius: var(--surface-radius);
  background: var(--bg-deep); border: 1px solid var(--card-border);
  font-size: 15px; line-height: 1.5; color: var(--ice); white-space: pre-wrap;
}
.thread-entry.mine { border-left: 2px solid var(--gold-bright); }
.thread-who {
  display: block; font-family: var(--kicker-font); font-size: 9px; letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--steel); margin-bottom: 5px;
}
.matter-ask { margin: 18px 0 4px; padding-top: 14px; border-top: 1px solid var(--card-border); }
.matter-ask .task-actions { margin: 8px 0; }
.matter-ask .acct-msg:not([hidden]) { margin-top: 8px; }
.capture-actions { margin-top: 20px; }
.capture-hint { margin-top: 10px; color: var(--steel); }

/* `.lighter` / `.lighter.on` were here and went with the control (Jack,
   2026-08-24). TRAPS: a rule that matches nothing is indistinguishable from one
   that matters, so it is deleted rather than left. */
/* Bottom padding for the nav is set on #app at :118 (calc(92px + safe-area-inset)).
   The deleted FAB had a 150px override here — a card sitting under it, unreachable
   through the "..." menu. Removed with the FAB (BF-10a U-10 / SR-6). */
.question .q-matter {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--steel); margin-bottom: 8px;
}

/* ---------- the question card (BF-10b §11) ----------
   design-preview-dusk-questions.html. The card leads with what answering unblocks,
   asks exactly one thing, and puts under it whatever control the SERVER said the
   question can take. Two lines of type do the work: the lead is small and in filing's
   teal ember so it reads as context rather than as the question, and the question
   itself is Fraunces, because it is the sentence being asked. */
.q-why {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  margin-bottom: 8px;
}
/* Sentence case and 13px, NOT the preview's 10px uppercase mono: the preview's
   sample why is six words and the real ones are full clauses. Eighty characters of
   uppercase mono is unreadable, and the shared chrome rule says mono is for kickers
   and labels, never for content. The colour and the position are the preview's. */
.q-why .q-why-text { font-size: 13px; line-height: 1.5; color: var(--ember-file); }
.q-why .q-matter { margin-bottom: 0; }
.question .q-text {
  font-family: var(--display-font); font-weight: 420; font-size: 1.05rem;
  line-height: 1.45; letter-spacing: 0; text-transform: none; color: var(--ink);
  margin: 0;
}
/* Two big targets, side by side, each one a whole answer. They wrap to one per row
   on a narrow screen rather than shrinking below the thumb. */
.q-choices { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.q-choice {
  flex: 1 1 46%; min-height: 48px; padding: 10px 16px; border-radius: 999px;
  font-family: var(--ui-font); font-size: 0.95rem; font-weight: 600;
  letter-spacing: 0; text-transform: none; cursor: pointer;
  background: none; border: 1px solid var(--card-line); color: var(--ink-dim);
}
.q-choice-first {
  background: var(--gold-soft, rgba(239, 192, 116, .18));
  border-color: rgba(239, 192, 116, .45); color: var(--ink);
}
.q-choice:disabled { opacity: .55; cursor: default; }
.q-skip { margin-top: 10px; }
.q-skip-btn {
  min-height: 44px; padding: 8px 4px; background: none; border: none;
  font-family: var(--ui-font); font-size: 0.82rem; letter-spacing: 0;
  text-transform: none; color: var(--ink-faint, var(--steel));
  text-decoration: underline; cursor: pointer;
}
/* The failure state, and it is the app's standard: what happened, in the server's
   own words, and what is still safe. Never an opacity change, never a colour alone. */
.savefail {
  margin-top: 10px; font-size: 0.85rem; line-height: 1.5;
  color: #f0b8a8; background: rgba(240, 184, 168, .08);
  border: 1px solid rgba(240, 184, 168, .3); border-radius: 12px; padding: 10px 14px;
}
/* Answered today. One quiet line each, so the pile visibly shrinks toward zero. */
#tab-questions .sec-label {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 2px;
  text-transform: uppercase; color: var(--steel); margin: 24px 0 10px;
}
.done-list {
  background: var(--card); border: 1px solid var(--card-line);
  border-radius: var(--surface-radius);
}
.donerow {
  display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
  padding: 11px 16px; border-top: 1px solid rgba(246, 240, 226, .08);
}
.donerow:first-child { border-top: none; }
.donerow-q { font-size: 0.88rem; color: var(--ink-dim); }
.donerow-chip {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--ember-file); white-space: nowrap;
  border: 1px solid rgba(143, 196, 185, .4); border-radius: 999px; padding: 3px 9px;
}

@media (prefers-reduced-motion: reduce) {
  .task-card:not(.swiping) { transition: none; }
  .swipe-hint { transition: none; }
}

/* ---------- vault (S9) ----------
   Deliberately quieter than the rest of the app. Everything here is rare-use and
   consequential, so nothing in it competes with Today for attention: no colour
   accents on the cards, no counts, and the only bright thing on the screen is the
   value itself once Jack has asked for it. */
.vault-subject {
  margin: 18px 0 6px; font-family: var(--kicker-font); font-size: 11px;
  letter-spacing: 1.5px; text-transform: uppercase; color: var(--antique-gold);
}
.vault-field {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 12px 0; border-top: 1px solid var(--card-border);
}
.vault-field:first-of-type { border-top: none; }
.vault-field-label { flex: 1 1 auto; min-width: 40%; font-size: 15px; color: var(--ice); }
/* BF-10b §9: label, value and button on ONE row, the way the preview draws them.
   This used to be `flex: 1 1 100%`, which pushed the value onto its own line and
   left the sealed marker floating under its own label. It goes full width only
   while it is holding the re-auth prompt, which is a paragraph and a button and
   genuinely needs the row (`.vault-field-wide`, set by vault.js). */
.vault-field-value { flex: 0 1 auto; }
.vault-field-value.vault-field-wide { flex: 1 1 100%; }
.vault-value {
  display: inline-block; font-family: var(--kicker-font); font-size: 17px;
  letter-spacing: 1px; color: var(--gold-bright);
  background: rgba(233, 206, 122, 0.08); border-radius: var(--surface-radius);
  padding: 6px 10px; user-select: all;
}
/* Says out loud that the read was recorded. A silent audit trail is worth the same
   to Jack as no audit trail: he has to be able to see that looking left a mark. */
.vault-logged { font-size: 12px; letter-spacing: 0.5px; }
.vault-logged::before { content: "· "; }
.vault-locked { flex: 1 1 100%; }
.vault-locked p { margin: 0 0 8px; }
.vault-ask { border-left: 2px solid var(--antique-gold); }
.vault-ask-list { margin: 8px 0 0; padding-left: 20px; font-size: 15px; color: var(--ice); }
.vault-ask-msg:empty { display: none; }
.vault-pick {
  display: block; margin: 12px 0 4px; font-family: var(--kicker-font);
  font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--steel);
}
/* 16px minimum, and it is not a taste call: anything smaller makes iOS zoom the whole
   page on focus, which on the vault screen means the field Jack was reading scrolls
   off. Same trap the capture field hit. */
#vault-subject, #vault-doc-type {
  width: 100%; font-size: 16px; padding: 10px;
  background: var(--bg-deep); border: 1px solid var(--card-border);
  color: var(--ice); border-radius: var(--surface-radius);
}
/* =====================================================================
   BF-10b §4 — THE VAULT AS A SECTION (design-preview-dusk-files.html).
   Replaces the `Documents | Vault` sub-nav. It is all storage, so it is one
   scroll, and the vault is told apart by TEMPERATURE rather than by a toggle:
   a violet seam, a violet lock line, and a card that is visibly a different
   material from the document rows above it.

   ⚠️ MEASURED, all four sky modes, worst stop of the gradient over the worst
   stop of each sky (day carries its .38 plate, see .sky.day above):
     the violet lock line   5.65:1 (day) .. 6.57:1 (night)
     the title, --ink      12.79:1 .. 14.88:1
     the note, --ink-dim    9.08:1 .. 10.32:1
   Nothing in here signals state with opacity: the seal is either drawn or the
   records are, and the button says which.
   ===================================================================== */
.vault-sec { margin-top: 26px; }
.vault-seal {
  position: relative; overflow: hidden;
  background: linear-gradient(180deg, rgba(38, 30, 58, .75), rgba(26, 22, 44, .75));
  border: 1px solid rgba(168, 151, 224, .3);
  border-radius: var(--surface-radius);
  padding: 16px; margin-bottom: 10px;
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
}
.vault-seal::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--ember-astro), transparent);
  opacity: .7;
}
.vault-lock {
  font-family: var(--kicker-font); font-size: 0.68rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ember-astro);
}
.vault-seal-title {
  font-family: var(--display-font); font-weight: 420; font-size: 1.05rem;
  color: var(--ice); margin: 6px 0 0;
}
.vault-seal-note {
  font-size: 0.88rem; color: var(--ink-dim); margin: 5px 0 0; line-height: 1.5;
}
.vault-seal-note:empty { display: none; }
.vault-seal-row { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
/* 44px is the floor for every target in the Dusk set, and these two are the only
   way into the vault, so they are not the place to shave it. */
.vault-btn {
  min-height: 44px; padding: 8px 16px; border-radius: 999px; cursor: pointer;
  font-family: inherit; font-size: 0.9rem; background: none;
  border: 1px solid rgba(168, 151, 224, .4); color: var(--ink-dim);
}
.vault-btn[aria-expanded="true"] { color: var(--ember-astro); border-color: var(--ember-astro); }
#vault-inside { margin-bottom: 10px; }

/* =====================================================================
   BF-10b §9 — THE ACCOUNT SHEET AND THE VAULT DETAIL
   (design-preview-dusk-account.html, approved D-26).

   Two arguments, both about ordering rather than colour:

   1. The account sheet states FACTS on one calm card (who, two-step, this
      session) and keeps the machinery below it. The 2FA line used to sit
      inside the enrolment card, where a settled fact wore the costume of an
      outstanding job.
   2. A vault record is a different MATERIAL from a document row — the same
      violet gradient the seal wears in §4, so opening the seal does not
      change temperature — and the re-auth sentence stands ABOVE the records,
      before anything refuses.

   ⚠️ MEASURED, all four sky modes, worst gradient stop over the worst sky
   stop, composited (the §4 method, which these numbers reproduce exactly):
     record card    ink 12.79 (day) .. 14.13 (night)
                    ink-dim  9.08 .. 9.89      ember 5.65 .. 6.25
     account card   ink 16.27 .. 16.40         ink-dim 11.11 .. 11.18
                    ink-faint 7.13 .. 7.16     gold 11.95 .. 12.04
   Nothing here signals state with opacity: sealed and shown differ by the
   glyphs (dots vs the value), by colour (ink vs gold), and by what the
   button says.
   ===================================================================== */
/* ⚠️ `flex-direction: row` IS LOAD-BEARING AND WAS MISSING (measured 2026-08-24).
   `.card` sets `flex-direction: column`, and this rule never reset it — so BF-10b §9's
   identity card, whose three children are written for a wrapping ROW, had been laying
   out as a column since it shipped, and every one of those flex values was doing
   something other than what it says:
     `.acct-id-who   { flex: 1 1 200px }`  became a 200px MINIMUM HEIGHT on a block
                                           holding three short lines. Measured at 834px
                                           wide: 200px tall for ~60px of text.
     `.acct-id-out   { flex: 0 0 auto }`   sat CENTRED under the name instead of at the
                                           right-hand end of the row, because
                                           `align-items: center` centres across the
                                           cross axis and in a column that is the
                                           horizontal one.
     `.acct-id-note  { flex: 1 1 100% }`   the "own line" it asks for was already every
                                           line, so it said nothing.
   The whole card measured 360px tall for about 110px of content. This is TRAPS' "a CSS
   rule scoped to one container is not a rule for that element" from the other side: a
   property inherited from the container that the specific rule forgot to take back. */
.acct-id {
  display: flex; flex-direction: row; flex-wrap: wrap;
  align-items: center; gap: 14px;
}
.acct-id-who { flex: 1 1 200px; min-width: 200px; }
.acct-id-name {
  font-family: var(--display-font); font-weight: 420; font-size: 1.05rem;
  color: var(--ink); word-break: break-word;
}
.acct-id-who .acct-state { margin-top: 4px; }
.acct-id-who .acct-small { margin-top: 3px; }
/* Full width on its own line, under both, so the sentence explaining how far the
   sign-out reaches is never orphaned from the button on a narrow iPad. */
.acct-id-note { flex: 1 1 100%; margin: 0; }
/* The preview's neutral pill, not `.vault-btn`: violet is the vault's temperature
   and this button has nothing to do with the vault. Its own rule rather than
   `.ghost` because `.ghost` is 11px uppercase mono, and this one is a word the
   preview sets in the UI face. */
.acct-id-out {
  flex: 0 0 auto; min-height: 44px; padding: 8px 18px; border-radius: 999px;
  cursor: pointer; font-family: inherit; font-size: 0.85rem; background: none;
  border: 1px solid var(--card-line); color: var(--ink-dim);
}
.acct-id-out:active { border-color: var(--gold-bright); color: var(--gold-bright); }

/* The record. Same gradient, same seam and same lock kicker as `.vault-seal`,
   deliberately: crossing the seal must not feel like arriving somewhere else. */
.vault-rec {
  position: relative; overflow: hidden;
  background: linear-gradient(180deg, rgba(38, 30, 58, .75), rgba(26, 22, 44, .75));
  border: 1px solid rgba(168, 151, 224, .3);
  border-radius: var(--surface-radius);
  padding: 16px; margin-bottom: 10px;
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
}
.vault-rec::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--ember-astro), transparent);
  opacity: .7;
}
.vault-rec-title {
  font-family: var(--display-font); font-weight: 420; font-size: 1.05rem;
  color: var(--ink); margin: 6px 0 0; letter-spacing: 0; text-transform: none;
}
.vault-rec-sub { font-size: 0.85rem; color: var(--ink-faint); margin-top: 3px; }
.vault-rec .vault-field:first-of-type { margin-top: 10px; }
.vault-rec .vault-field { border-top-color: rgba(168, 151, 224, .15); }
/* The standing explanation, above every record. Its whole job is to arrive before
   the refusal does, so it is quiet but it is not hidden. */
.vault-note {
  font-size: 0.85rem; color: var(--ink-dim); line-height: 1.55;
  background: rgba(168, 151, 224, .1);
  border: 1px solid rgba(168, 151, 224, .25);
  border-radius: 12px; padding: 12px 14px; margin-bottom: 12px;
}
.vault-note p { margin: 0; }
.vault-note p + p { margin-top: 8px; }
/* Eight dots, fixed. Ink rather than a faded value: sealed is a different THING
   from shown, not a dimmer version of it. */
.vault-sealed {
  font-family: var(--kicker-font); font-size: 0.85rem; letter-spacing: 0.22em;
  color: var(--ink-dim);
}

/* The activity log. Each row is a `<details>` that starts collapsed and opens on
   tap (Jack, 2026-08-24) — see vault.js for why the platform element rather than a
   hand-rolled disclosure. The row still LOOKS like the flat list it replaced; the
   only added ink is the chevron, because a thing that opens has to say it does. */
.vault-log { display: flex; flex-direction: column; }
.vault-log-row {
  border-top: 1px solid rgba(246, 240, 226, .08);
}
.vault-log-row:first-child { border-top: none; }
.vault-log-sum {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  padding: 11px 0;
  cursor: pointer; list-style: none;
  /* 44px of thumb, kept without padding the row out: the summary's own line box is
     about 20px, so the rest comes from the padding above plus this floor. */
  min-height: 44px; align-content: center;
}
/* Safari draws its own triangle through ::-webkit-details-marker, and `list-style:
   none` alone does not remove it. Both are needed, and dropping either leaves a
   stray disclosure triangle beside ours on exactly one engine. */
.vault-log-sum::-webkit-details-marker { display: none; }
.vault-log-sum::marker { content: ""; }
.vault-log-sum:focus-visible { outline: 1px solid var(--antique-gold); outline-offset: 2px; }
.vault-log-what { font-size: 0.9rem; color: var(--ink-dim); flex: 1 1 auto; min-width: 0; }
.vault-log-when {
  font-family: var(--kicker-font); font-size: 0.75rem; letter-spacing: 0.08em;
  color: var(--ink-faint); white-space: nowrap; flex: 0 0 auto;
  display: flex; align-items: center; gap: 8px;
}
/* The chevron, drawn in CSS so there is no icon to go stale. */
.vault-log-when::after {
  content: ""; width: 7px; height: 7px; flex: none;
  border-right: 1.4px solid var(--steel); border-bottom: 1.4px solid var(--steel);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .18s ease;
}
.vault-log-row[open] .vault-log-when::after {
  transform: rotate(225deg) translate(-2px, -2px);
}
.vault-log-detail {
  display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px;
  margin: 0 0 12px; padding: 12px 14px;
  background: var(--card-bg); border-radius: var(--surface-radius);
  font-size: 14px;
}
/* Each fact is its own row of the grid, so the labels line up down the column.
   `display: contents` is what lets the wrapper exist for readability in the
   template while the dt/dd still take part in the parent grid — an `auto` column
   sized per row is the .step trap in TRAPS, one screen along. */
.vault-log-fact { display: contents; }
.vault-log-detail dt {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--steel); align-self: center;
}
.vault-log-detail dd { margin: 0; color: var(--ink-dim); }
.vault-log-foot { margin-top: 12px; }

/* ===== S7 §0: the unified stream + calendar ============================ */
/* Kind labels replace the old section headers: one column, each card says
   what it is. Same voice as the digest kicker, so the stream reads as one
   instrument. */
.kicker {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 2px;
  text-transform: uppercase; color: var(--steel); margin-bottom: 8px;
}
.question .kicker { margin-bottom: 4px; }
.question .q-matter { color: var(--antique-gold); }

/* ---------- the month calendar (FEEDBACK.md §3) ----------
   Replaces the agenda view that lived behind a List | Calendar toggle inside Tasks.
   The toggle and its styles are deleted with it: two calendars drift apart. */
.cal-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 12px;
}
.cal-head h2 {
  margin: 0; font-family: var(--display-font); font-weight: 420; font-size: 1.1rem;
}
/* 44px both ways. The month arrows were 34px tall, which is one of the U-11 targets
   BF-10b retires: a control you have to aim at is a control you stop using. */
.cal-head .ghost {
  margin-top: 0; min-width: 44px; min-height: 44px; padding: 0 14px; font-size: 18px;
  display: inline-flex; align-items: center; justify-content: center;
}

.cal-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px;
}
.cal-dow-head {
  font-family: var(--kicker-font); font-size: 9px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--steel);
  text-align: center; padding-bottom: 4px;
}
.cal-cell {
  /* A square-ish tap target. 44px is the floor Apple's own guidance sets and the
     floor a shaky morning needs; at 375px seven of these plus gaps fits. */
  min-height: 48px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 3px;
  background: transparent; border: 1px solid transparent;
  border-radius: 12px;
  color: var(--ink-dim); font-family: inherit; font-size: .85rem; padding: 6px 0 0;
}
.cal-cell.blank { pointer-events: none; }
.cal-cell.has { color: var(--ink); }
.cal-cell.today { border-color: var(--gold); color: var(--gold); font-weight: 600; }
.cal-cell.picked {
  background: var(--gold-soft); border-color: rgba(239, 192, 116, 0.4);
  color: var(--ink);
}
.cal-num { line-height: 1; }
.cal-dots { display: flex; gap: 3px; height: 6px; align-items: center; }
/* TWO COLOURS AND NO MORE. Gold: this day wants Jack. Calm blue: the system is
   holding it. A third dot colour (there used to be a cyan one for matter dates)
   makes the grid a legend to memorise instead of an answer to read. */
.cal-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--gold);
}
.cal-dot.watched { width: 5px; height: 5px; background: var(--ember-cal); }

.cal-today-btn { margin-top: 12px; min-height: 44px; }
.cal-day-name {
  font-family: var(--kicker-font); font-size: .66rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-faint);
  margin: 22px 0 10px; padding-bottom: 0; border-bottom: none;
}

/* ---------- the day's rows (BF-10b §2) ----------
   ⚠️ STATUS IS A COLOUR SWAP AND A NAMED CHIP, NEVER OPACITY ON BODY TEXT. The two
   rules this replaces were `.cal-row.watched { opacity: .62 }` and
   `.cal-row.presence { opacity: .85 }`, and both fail the AA-in-all-four-sky-modes
   requirement the moment the day sky is behind them: dimmed ink on a light gradient
   is unreadable, and "unreadable" is not a status anybody chose to signal. */
.ev {
  display: flex; align-items: baseline; gap: 14px;
  padding: 13px 14px; border-radius: 12px; border: 1px solid transparent;
}
.ev .t {
  font-family: var(--kicker-font); font-size: .7rem; color: var(--ink-faint);
  width: 4.2em; flex: none;
}
.ev .what { font-size: .95rem; color: var(--ink); }
.ev .sub { display: block; font-size: .8rem; color: var(--ink-dim); margin-top: 2px; }
.ev.handled .what { color: var(--ink-faint); }
.ev.handled .sub { color: rgba(246, 240, 226, 0.5); }
.ev .chip {
  font-family: var(--kicker-font); font-size: .6rem; letter-spacing: .1em;
  border-radius: 999px; padding: 3px 9px; margin-left: auto; flex: none;
}
.ev .chip.watch { color: var(--ember-file); border: 1px solid rgba(143, 196, 185, 0.4); }
.ev .chip.who { color: var(--ember-task); border: 1px solid rgba(138, 166, 216, 0.4); }
.ev.wants { background: var(--dusk-card); border-color: var(--card-line); }
.ev.wants .what b { color: var(--gold); font-weight: 600; }
/* "Open →" is on rows with a real action and on no others, which is the preview's
   own rule: an arrow that leads nowhere teaches him to stop tapping arrows. */
.ev .go {
  margin-left: auto; flex: none; font-size: .85rem; color: var(--gold);
  cursor: pointer; min-height: 44px; min-width: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; font-family: inherit;
}

/* S19: "who did the family task". One line each, and the styling is deliberately
   the quietest thing on the page. No colour, no badge, no count: the moment this
   reads as a scoreboard it has stopped being the nice thing Jack asked for. */
.done-recently { margin-top: 14px; }
.done-row {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 14px; line-height: 1.6; color: var(--steel);
}
/* min-width: 0 on the growing child, because a flex child defaults to min-width:auto
   and a long task title would push the name off the row instead of wrapping. This is
   the ellipsis-that-never-appeared bug from the eleventh run, in its other form. */
.done-row > span:nth-child(2) { flex: 1; min-width: 0; }
.done-tick { flex: none; }
.done-by { flex: none; font-size: 13px; opacity: 0.8; }
.cal-day { font-size: 22px; color: var(--ice); line-height: 1.1; }
.cal-edit { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.cal-edit .ghost { min-height: 44px; }
/* 16px floor: any smaller input makes iOS zoom the page on focus. */
.cal-edit input[type="date"] {
  font-size: 16px; background: rgba(8, 9, 12, 0.6); color: var(--ice);
  border: 1px solid var(--card-border); border-radius: 4px; padding: 6px 8px;
}

/* ---------- the "+" sheet ----------
   Three options with icons, and the tier is chosen by which one Jack taps. Big rows
   rather than a menu: this is the control the whole redesign hangs on, and it has to
   be usable one-handed without aiming. */
.sheet .add-options { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; }
.add-option {
  display: grid; grid-template-columns: 42px 1fr; grid-template-rows: auto auto;
  align-items: center; gap: 0 12px; text-align: left; width: 100%;
  padding: 14px 16px; cursor: pointer;
  background: var(--card-bg); color: var(--ice); font-family: inherit; font-size: 16px;
  border: 1px solid var(--card-border); border-radius: var(--surface-radius);
}
.add-option:active { border-color: var(--antique-gold); background: var(--card-bg-strong); }
.add-icon {
  grid-row: 1 / 3; font-size: 24px; color: var(--gold-bright); text-align: center;
  font-family: var(--kicker-font);
}
.add-label { font-weight: 600; color: var(--ice); }
.add-sub { font-size: 13px; color: var(--steel); }

/* ---------- document filter chips (FEEDBACK.md §3) ----------
   Horizontally scrolling, with the search icon pinned at the end. The chips derive
   from the matter each document is filed to, so the row maintains itself. */
.chip-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
/* Since BF-10b §4 NOBODY has a Documents | Vault sub-nav above this (the vault is a
   section further down the same scroll, and a child has no vault at all), so the chip
   bar is the first thing on the page for everyone and needs the breathing room the
   sub-nav used to provide. */
#files-sec-docs .chip-bar { margin-top: 6px; }
/* The assist strip calls a route a child is refused on (the AI is rationed to Jack's
   account), and a button whose only outcome is a refusal is worse than no button. */
.no-assist .plan-strip { display: none; }
.chips {
  display: flex; gap: 6px; overflow-x: auto; flex: 1; min-width: 0;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  padding-bottom: 2px;
}
.chips::-webkit-scrollbar { display: none; }
.doc-chip {
  flex: none; white-space: nowrap; cursor: pointer;
  font-family: var(--kicker-font); font-size: 11px; letter-spacing: 0.5px;
  min-height: 44px; padding: 6px 14px; background: transparent; color: var(--steel);
  border: 1px solid var(--card-border); border-radius: 999px;
}
.doc-chip.on {
  color: var(--gold-bright); border-color: var(--antique-gold);
  background: rgba(233, 206, 122, 0.08);
}
.chip-search { flex: none; margin-top: 0; min-height: 44px; padding: 6px 14px; font-size: 16px; }
/* 16px floor, or iOS zooms the page the moment it takes focus. */
.doc-search {
  width: 100%; font-size: 16px; margin-bottom: 12px;
  background: rgba(8, 9, 12, 0.6); color: var(--ice);
  border: 1px solid var(--card-border); border-radius: 4px; padding: 8px 10px;
}
.doc-list .doc-title { color: var(--ice); font-weight: 600; line-height: 1.35; }

/* ---------- one document ----------
   ⚠️ The SCAN is the document. It leads, at full width; everything the app worked
   out about it is a footnote underneath, and the transcript is behind a tap. */
.doc-detail-title { margin: 0; font-size: 19px; }
.doc-detail-meta { font-size: 13px; color: var(--steel); margin: 4px 0 14px; }
.doc-scan img, .pdf-canvas {
  width: 100%; border: 1px solid var(--card-border); border-radius: var(--surface-radius);
  background: rgba(8, 9, 12, 0.6);
}

/* BF-26 §1 — the PDF strip. There is no `iframe` rule here any more and its absence is
   deliberate: the two `.doc-scan iframe` rules were deleted with the markup that used
   them, because a loaded example left in the stylesheet is what the next screen copies
   (the D-28 `.upcoming-row` pin, arriving again). Pages are one vertical scroll, same
   gesture as the multi-page review strip, with the page number stated on every page —
   "which page am I on" must never be a thing to work out. */
.doc-pdf { display: block; }
/* ── the document pager (Jack, 2026-08-24) ───────────────────────────────────
   Pages go LEFT AND RIGHT, one at a time, instead of down a single tall column.

   ⚠️ Done with scroll-snap and no JavaScript pointer handling, on purpose. This
   sheet already contains `makeSwipeable` cards elsewhere in the app and a second,
   hand-rolled horizontal drag inside a vertically-scrolling dialog is the class of
   thing that fights the sheet's own scroll on a real iPad. Native scroll-snap keeps
   the platform's momentum, its rubber-banding and its accessibility behaviour, and
   `scroll-snap-stop: always` is what stops one flick throwing four pages past the
   one he wanted.

   `overscroll-behavior-x: contain` matters here specifically: without it, swiping
   past the last page chains outward, and in a PWA the horizontal chain at the edge
   of the screen is the iPad's own back-navigation gesture. */
.pdf-strip {
  display: flex; flex-direction: row; gap: 0;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  /* The strip owns the horizontal axis; the sheet keeps the vertical one. */
  touch-action: pan-x;
  /* No visible scrollbar: the page counter below is the position indicator, and a
     bar over the foot of a letter is ink on somebody's document. */
  scrollbar-width: none;
}
.pdf-strip::-webkit-scrollbar { display: none; }
.pdf-page {
  display: block; flex: 0 0 100%; width: 100%; min-width: 0;
  scroll-snap-align: center; scroll-snap-stop: always;
}
.pdf-page-label {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--ink-dim); margin-bottom: 6px;
}
.pdf-canvas { display: block; height: auto; }
/* The position indicator. It replaces the per-page label as the thing you read to
   know where you are: with one page on screen at a time, a label that scrolls away
   with its own page cannot answer "where am I" while you are between two. */
.pdf-pager {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 0 0 8px;
}
.pdf-pager-dots { display: flex; gap: 5px; flex-wrap: wrap; justify-content: center; }
.pdf-pager-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--card-border-strong); flex: none;
  transition: background .18s ease;
}
.pdf-pager-dot.on { background: var(--gold-bright); }
.pdf-pager-count {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--steel);
}
/* Past a dozen pages a dot row stops being scannable and becomes a texture. */
.pdf-pager.many .pdf-pager-dots { display: none; }
.pdf-loading { padding: 18px 0; }
.pdf-failed, .pdf-page-failed, .pdf-more, .doc-other {
  padding: 14px; background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
}
.pdf-more { margin-top: 14px; }
.pdf-failed-line { margin: 0; line-height: 1.5; }
.pdf-failed-detail { margin: 8px 0 0; }
.pdf-failed-actions { margin-top: 12px; }
/* The third leak of `dialog .ghost { margin-top: 18px }` cost a session (D-51); this
   is a fourth group of ghost-shaped controls inside a <dialog>, so it gets its own
   exclusion in the same commit as the markup that introduces it. */
.pdf-failed-actions .ghost { margin-top: 0; display: inline-block; text-decoration: none; }
.doc-said {
  margin-top: 10px; padding: 12px; white-space: pre-wrap; line-height: 1.5;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
}
.doc-detail-file { margin-top: 14px; }

/* BF-27 §1.2 · the confirm step for what a letter says is owed. It sits under the
   filing control, on the document, because the sentence it was read out of has to
   be readable in the same glance as the figure it produced. Quiet by design: no
   red, no count that survives to tomorrow (rule 10) — a proposal, and two taps. */
.doc-obligations { margin-top: 16px; }
.obl-card { margin-top: 10px; }
.obl-quote { margin-top: 8px; font-style: italic; }
.obl-date { display: block; margin-top: 6px; width: 100%; }
.obl-card .obl-msg { margin-top: 8px; }

/* BF-10a §5: the filing control lives inside the document sheet, next to the scan
   it decides for. Compact by design — the scan is still the page, this is a footer. */
.doc-filing {
  margin-top: 12px; padding: 10px 12px; border: 1px solid var(--card-border);
  border-radius: var(--surface-radius); background: var(--card-bg);
}
.doc-filing-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.doc-filing-label { font-size: 13px; color: var(--steel); }
.doc-filing-chip {
  display: inline-block; padding: 2px 8px; font-size: 13px;
  background: rgba(212, 175, 90, 0.14); color: var(--antique-gold);
  border-radius: 999px;
}
.doc-filing-picker { margin-top: 10px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.doc-filing-picker label { font-size: 13px; color: var(--steel); }
.doc-filing-picker select { flex: 1 1 180px; min-height: 44px; }
.doc-filing-picker .acct-msg { flex: 1 0 100%; }

/* "Not right now" says what it chose, or says plainly that it cannot move. Quiet:
   this is a confirmation, not a warning, and nothing here is red. */
.defer-said { font-size: 13px; color: var(--steel); padding: 2px 0 6px; }
.defer-said.warn { color: var(--gold-bright); }
/* What the Write router did as well as answering. */
.ask-did { font-size: 13px; color: var(--antique-gold); }

/* ===== S7 §4 polish ==================================================== */
.tl-glyph {
  font-family: var(--kicker-font); color: var(--antique-gold);
  display: inline-block; width: 16px;
}
.chip.upload-failed {
  border-color: var(--red-warn); color: var(--red-warn);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.progress { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/* D-28 DELETED `.upcoming-row` WITH THE SECOND "Coming up" RAIL. Home's `.rail-row`
   is the surviving one and it says more: it marks the handled rows and closes with
   the arithmetic. Deleted rather than left behind on purpose — the rule that went
   with it was `.upcoming-row.watched { opacity: .62 }`, which is precisely the
   opacity-as-state pattern SR6 Job 2 §1 retired and §5 wrote a DO-NOT pin about, so
   leaving it in the stylesheet is leaving a loaded example for the next screen to
   copy. `.tl-when` is untouched: it is defined above, on the timeline it belongs to,
   and the deleted rail was only ever borrowing it. */

/* =====================================================================
   BF-10b §5 — KITCHEN AS CARDS (design-preview-dusk-kitchen.html).
   Replaces the S20 sub-nav-and-shop-mode block. Two .kcard sections in one
   scroll, both wearing the kitchen ember as a 2px top seam, the same way a
   Home tile does — the seam is what says "this is the kitchen" before a
   word is read.

   ⚠️ WCAG AA on the sky is a hard requirement Jack stated himself in the
   S20 brief and BF-10b restates for all four sky modes. Body text is --ice
   on --card-bg. NOTHING here signals state with opacity or a strike-through
   on body text: a ticked row swaps the box to the filing teal and puts a
   tick in it, and the label moves to --ink-dim, which still measures AA.
   The rows are aisle-sized ALWAYS (56px, 17px text) because shop mode is
   gone and there is no longer a "make it big" tap to find first.
   ===================================================================== */
.kcard {
  position: relative; overflow: hidden;
  background: var(--card-bg-strong); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
  padding: 16px; margin-bottom: 14px;
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
}
.kcard::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--ember-kitchen), transparent);
  opacity: .7;
}
.kcard-title {
  font-family: var(--display-font); font-weight: 420; font-size: 1.15rem;
  color: var(--ice); margin: 0 0 2px;
}
.kcard-sub { font-size: 0.86rem; color: var(--ink-dim); margin: 0 0 14px; }
.kcard-label {
  font-family: var(--kicker-font); font-size: 0.66rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--steel); margin: 16px 0 6px;
}
.kcard-empty { font-size: 0.92rem; color: var(--ink-dim); padding: 10px 2px; }

.shop-add { display: flex; gap: 8px; align-items: stretch; margin-bottom: 12px; }
.shop-add input {
  flex: 1; min-width: 0;
  background: rgba(246, 240, 226, .06); border: 1px solid var(--card-border);
  color: var(--ice); border-radius: 12px;
  padding: 12px 14px; min-height: 48px; font-size: 17px; font-family: inherit;
}
.shop-add input::placeholder { color: var(--steel); }
.shop-add input:focus { outline: none; border-color: var(--gold-bright); }
.shop-add button { min-height: 48px; padding: 0 22px; min-width: 84px; }

.shop-list {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-direction: column; gap: 4px;
}
.shop-item {
  display: flex; align-items: center; gap: 14px;
  padding: 8px 10px; min-height: 56px;
  border-radius: 12px; cursor: pointer; font-size: 17px;
}
.shop-item:hover { background: rgba(246, 240, 226, .05); }
/* The box carries the state. Empty outline = still to get; teal fill and a
   tick = in the trolley. */
.shop-item .shop-box {
  width: 24px; height: 24px; flex: none;
  border-radius: 8px; border: 1.5px solid var(--steel);
}
.shop-item.bought .shop-box {
  border-color: var(--ember-file); background: rgba(143, 196, 185, .18);
  display: flex; align-items: center; justify-content: center;
}
.shop-item.bought .shop-box::after {
  content: "\2713"; color: var(--ember-file); font-size: 0.9rem; font-weight: 600;
}
.shop-item .shop-text { flex: 1; min-width: 0; color: var(--ice); }
.shop-item.bought .shop-text { color: var(--ink-dim); font-size: 16px; }
.shop-item .shop-note {
  display: block; font-size: 13px; color: var(--steel); margin-top: 2px;
}
.shop-item .shop-by {
  font-family: var(--kicker-font); font-size: 11px;
  color: var(--steel); letter-spacing: 1px;
}
.shop-item .shop-rm {
  background: none; border: none; color: var(--steel);
  padding: 4px 10px; font-size: 20px; line-height: 1;
  min-height: 44px; min-width: 44px; cursor: pointer;
}
.shop-item .shop-rm:active { color: var(--red-warn); }
/* Undo says the word rather than drawing an icon, because it is read in an
   aisle by somebody who has just mis-tapped and wants certainty, not a glyph. */
.shop-item .shop-undo {
  margin-left: auto; flex: none;
  background: none; border: none; cursor: pointer; font: inherit;
  font-size: 0.85rem; color: var(--ember-file);
  min-height: 44px; padding: 0 10px;
}

/* BF-10a 4a, restyled: the "Just bought" band. The clear control is QUIET on
   purpose (BF-10b §5) — the server's six-hour auto-clear does the real work,
   and this is the reassurance that the band is not permanent. */
.shop-bought-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 18px 0 4px; flex-wrap: wrap;
}
.shop-bought-head .kcard-label { margin: 0; }
.quiet-act {
  background: none; border: none; cursor: pointer; font: inherit;
  font-size: 0.8rem; color: var(--steel);
  min-height: 44px; padding: 0 6px; text-decoration: underline;
  text-underline-offset: 3px;
}
.quiet-act:hover, .quiet-act:focus-visible { color: var(--gold-bright); outline: none; }

/* BF-10b D-34: the quick-add sheet. The chips used to be inline on the card
   (BF-10a 4c) and are behind one door now, because twelve of them cost twelve
   rows of chrome above the list and the list is what the aisle needs. The door
   borrows `.add-recipe`, which is already the app's word for "a quiet invitation
   that opens something". */
#shop-regulars-open { margin: 0 0 14px; display: block; }
.reg-grid { display: flex; flex-wrap: wrap; gap: 10px; margin: 4px 0 8px; }
/* Aisle-sized like the list rows, not label-sized like a filter chip: this is a
   thing you tap with a trolley in the other hand. */
.reg-chip {
  min-height: 52px; padding: 12px 18px;
  border-radius: 999px;
  background: none; border: 1px solid var(--card-border);
  color: var(--ice); font: inherit; font-size: 17px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 9px;
}
.reg-chip:hover, .reg-chip:focus-visible {
  border-color: var(--gold-bright); outline: none;
}
/* ⚠️ ON is a COLOUR SWAP AND A TICK, exactly as a ticked shopping row is, and for
   the same reason (SR6 Job 2 §1): never `opacity`, never a strike-through. The
   teal is `--ember-file`, the same token the row's box uses, so the two surfaces
   agree about what "it is on the list" looks like. The label stays at full
   opacity and moves to --ink-dim, which measures AA against every sky. */
.reg-chip.on {
  border-color: var(--ember-file); background: rgba(143, 196, 185, .18);
  color: var(--ink-dim);
}
.reg-chip.on::before {
  content: "\2713"; color: var(--ember-file); font-size: 1rem; font-weight: 600;
}
.reg-chip:disabled { cursor: default; }
#shop-regulars-dialog .shop-add { margin-bottom: 16px; }

/* Cookbook: a recipe is a row, and "Send to shopping" is ON it. */
.recipe-rows { display: flex; flex-direction: column; }
.recipe-row {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 8px; border-radius: 12px; cursor: pointer;
  border-bottom: 1px solid var(--card-border);
}
.recipe-row:last-of-type { border-bottom: none; }
.recipe-row:hover { background: rgba(246, 240, 226, .05); }
.recipe-main { flex: 1; min-width: 0; }
.recipe-name { font-family: var(--display-font); font-size: 1.02rem; color: var(--ice); }
.recipe-meta {
  font-family: var(--kicker-font); font-size: 0.66rem; letter-spacing: 0.1em;
  color: var(--steel); margin-top: 3px;
}
.recipe-send {
  margin-left: auto; flex: none;
  min-height: 44px; padding: 8px 14px; border-radius: 999px;
  background: var(--gold-soft); border: 1px solid rgba(239, 192, 116, .4);
  color: var(--ice); font: inherit; font-size: 0.82rem; font-weight: 500;
  cursor: pointer;
}
.recipe-send:disabled { cursor: progress; }
.recipe-row .recipe-tags {
  display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px;
}
.recipe-row .recipe-tags .chip {
  font-size: 11px; padding: 2px 8px; color: var(--steel);
  border: 1px solid var(--card-border);
}
.add-recipe {
  margin-top: 12px; min-height: 44px; padding: 8px 16px; border-radius: 999px;
  background: none; border: 1px solid var(--card-border); color: var(--ink-dim);
  font: inherit; font-size: 0.88rem; cursor: pointer;
}
.add-recipe:hover, .add-recipe:focus-visible {
  color: var(--gold-bright); border-color: var(--gold-bright); outline: none;
}

/* BF-10b §5: the recipe read, sized for a worktop. Jack asked for a detail
   view that is "readable while cooking: large type, no hover, does not need a
   second tap to see the next step" — so every step is rendered at once and the
   type is a size up from the app's body text. */
.recipe-read #recipe-view-title { font-family: var(--display-font); }
.recipe-ingredients {
  list-style: none; padding: 0; margin: 0 0 4px;
  display: flex; flex-direction: column; gap: 8px;
  color: var(--ice); font-size: 18px; line-height: 1.5;
}
.recipe-ingredients li { padding: 4px 0; }
.recipe-method {
  margin: 0 0 12px 22px; padding: 0;
  display: flex; flex-direction: column; gap: 12px;
  color: var(--ice); font-size: 18px; line-height: 1.55;
}
.recipe-method li { padding-left: 4px; }
/* The dialogs are already styled for other add sheets; only the extra label
   spacing is worth naming, because the recipe add sheet has more fields. */
#recipe-add-dialog textarea { min-height: 120px; }
#recipe-add-dialog #recipe-method { min-height: 140px; }

/* =====================================================================
   BF-10b §6 — TONIGHT'S READING (design-preview-dusk-reading.html), with
   the person chip bar Jack asked for on 2026-08-10 (spec: BF-16's
   amendment). Violet is astrology's ember, so the household card is the
   same material as the vault seal: a 2px ember seam over a violet-tinted
   plate. Two different temperatures, one visual grammar.

   ⚠️ MEASURED, all four sky modes, worst gradient stop over the worst stop
   of each sky (day carries its .38 plate, see .sky.day above):
     the lead card's title, --ice       12.79:1 (day) .. 14.88:1 (night)
     the lead card's body, --ink-dim     9.08:1 .. 10.32:1
     the name chip / selected chip,
       --ember-astro on the card plate   5.65:1 .. 6.57:1
     an unselected chip, --ink-dim       9.08:1 .. 10.32:1
   NOTHING in here signals state with opacity. A selected chip swaps to the
   violet AND gains a filled plate AND a heavier border, which is the
   colour-swap pattern the whole Dusk set uses (SR6 Job 2 §1) — an
   unselected chip is still full-contrast body text, because a filter whose
   unchosen options are faint is a filter you cannot read in daylight.
   ===================================================================== */
.astro-lead-card {
  position: relative; overflow: hidden;
  background: linear-gradient(180deg, rgba(38, 30, 58, .75), rgba(26, 22, 44, .75));
  border: 1px solid rgba(168, 151, 224, .3);
  border-radius: var(--surface-radius);
  padding: 18px; margin-bottom: 4px;
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
}
.astro-lead-card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--ember-astro), transparent);
  opacity: .7;
}
.astro-lead-title {
  font-family: var(--display-font); font-weight: 420; font-size: 1.15rem;
  color: var(--ice); margin: 0 0 8px; padding-right: 84px;
}
.astro-constellation { position: absolute; right: 14px; top: 14px; opacity: .8; }

.astro-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }
/* 44px, like every other target in the Dusk set. These are the whole of the
   navigation on this sheet, so they are not the place to shave it. */
.astro-chip {
  min-height: 44px; padding: 8px 16px; border-radius: 999px; cursor: pointer;
  font-family: inherit; font-size: 0.9rem; background: none;
  border: 1px solid var(--card-border); color: var(--ink-dim);
}
.astro-chip[aria-selected="true"] {
  color: var(--ember-astro); border-color: var(--ember-astro);
  background: rgba(168, 151, 224, .16);
}

.astro-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
}
.astro-head .task-title { min-width: 0; }
.astro-name-chip {
  flex: none;
  font-family: var(--kicker-font); font-size: 0.62rem; letter-spacing: 0.12em;
  color: var(--ember-astro); border: 1px solid rgba(168, 151, 224, .4);
  border-radius: 999px; padding: 3px 10px; white-space: nowrap;
}

/* The invitation at the foot. One sentence and one 44px control, and it only
   renders when there is a reading behind it to ask about. */
.astro-invite {
  display: flex; gap: 10px; align-items: center; margin-top: 22px;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius); padding: 14px 16px;
}
.astro-invite-q { flex: 1; font-size: 0.9rem; color: var(--ink-dim); line-height: 1.5; }
.astro-invite-go {
  flex: none; min-height: 44px; padding: 8px 18px; border-radius: 999px;
  cursor: pointer; font-family: inherit; font-size: 0.85rem; font-weight: 500;
  background: var(--gold-soft); border: 1px solid rgba(239, 192, 116, .4);
  color: var(--ice);
}

/* BF-10a U-1: astrology reading dialog. Uses the app's existing card and
   stack tokens; per-section headline uses task-title so the body is one
   visual language with everything else. The `.astro-open` and `.astro-body`
   are the two prose seams the writer already fills. */
.astro-reading-card { display: flex; flex-direction: column; gap: 8px; }
.astro-open { color: var(--gold-bright); font-size: 15px; line-height: 1.45; }
.astro-body {
  color: var(--ice); font-size: 15px; line-height: 1.6;
  white-space: pre-wrap;
}
/* One quiet line, as the preview draws it: who wrote it and when, in the mono
   kicker, under whatever the chip bar has on screen. */
.astro-prov {
  font-family: var(--kicker-font); font-size: 0.62rem; letter-spacing: 0.1em;
  color: var(--ink-faint); margin: 14px 2px 0;
}
.astro-ask-card .astro-body {
  font-size: 14px; color: var(--ice); margin-top: 4px;
}

/* ── BF-10b §7: the ask sheet ("Ask the sky") ─────────────────────────────────
   The approved preview's argument is that asking is a who row and one box, and
   that what you already asked lives on the same sheet: Waiting, then Answered.
   Those two rails are why the sheet is worth opening a second time. */

/* The who row. Same 44px pill as §6's chips and deliberately the same shape, but
   `aria-pressed` rather than `aria-selected`, because this is a choice being made
   and not a view being filtered — a screen reader should hear the difference. */
.astro-who { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 18px; }
.astro-who-chip {
  min-height: 44px; padding: 8px 16px; border-radius: 999px; cursor: pointer;
  font-family: inherit; font-size: 0.9rem; background: none;
  border: 1px solid var(--card-border); color: var(--ink-dim);
}
/* A colour swap and a filled plate, never a dimming (SR6 Job 2 §1): an unchosen
   name has to stay readable in daylight or the row cannot be used outdoors. */
.astro-who-chip[aria-pressed="true"] {
  color: var(--ember-astro); border-color: var(--ember-astro);
  background: rgba(168, 151, 224, .16); font-weight: 500;
}

/* The box and its control, side by side as the preview draws them. The button
   sits at the BOTTOM of the box so it stays put as the textarea is dragged taller. */
.astro-askbox { display: flex; gap: 10px; align-items: stretch; }
.astro-askbox textarea { flex: 1; min-height: 88px; resize: vertical; }
.astro-askbox button { flex: none; align-self: flex-end; min-height: 44px; }
.astro-hint {
  font-size: 0.78rem; color: var(--ink-faint); margin: 10px 2px 0; line-height: 1.5;
}
/* Event context, demoted rather than deleted (D-36). Closed by default so the
   sheet reads as one box, one tap from open so the capability is not hidden. */
.astro-more { margin-top: 18px; }
.astro-more > summary {
  min-height: 44px; display: flex; align-items: center; cursor: pointer;
  font-size: 0.85rem; color: var(--ink-dim);
}
.astro-more[open] > summary { color: var(--gold-bright); }
.astro-ctx-note {
  font-size: 0.78rem; color: var(--ink-faint); margin: 8px 2px 0; line-height: 1.55;
}

/* One card shape for both rails, so a question does not change appearance when it
   is answered — it gains the answer and swaps its chip. The ember seam is the same
   material as the household reading's, because both are astrology speaking. */
.astro-qa {
  position: relative; overflow: hidden;
  background: linear-gradient(180deg, rgba(38, 30, 58, .75), rgba(26, 22, 44, .75));
  border: 1px solid rgba(168, 151, 224, .3);
  border-radius: var(--surface-radius); padding: 16px;
}
.astro-qa::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--ember-astro), transparent);
  opacity: .7;
}
.astro-qa-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
}
.astro-qa-q {
  font-family: var(--display-font); font-weight: 420; font-size: 1rem;
  color: var(--ice); min-width: 0;
}
.astro-qa-a {
  font-size: 0.92rem; color: var(--ink-dim); line-height: 1.65; margin-top: 10px;
  white-space: pre-wrap;
}
.astro-qa-meta {
  font-family: var(--kicker-font); font-size: 0.62rem; letter-spacing: 0.1em;
  color: var(--ink-faint); margin-top: 10px;
}
/* The waiting chip is GOLD, not red and not a count: it says "this is yours and it
   is in hand", which is the whole of what waiting means here. It disappears when
   the answer lands rather than accumulating into anything. */
.astro-wait-chip {
  flex: none;
  font-family: var(--kicker-font); font-size: 0.62rem; letter-spacing: 0.12em;
  color: var(--gold-bright); border: 1px solid rgba(239, 192, 116, .4);
  border-radius: 999px; padding: 3px 10px; white-space: nowrap;
}

/* BF-10a U-6: capture-pending sheet. The blocked rows get a warm border so
   the reader's eye lands there before the waiting rows below. */
.capture-row-blocked { border-color: var(--antique-gold); }
.capture-row-text { word-break: break-word; }

/* ═══════════════════════════════════════════════════════════════════════════
   HOME (S20, nineteenth run 2026-08-08). Ported from
   cockpit/prototypes/home-dashboard.html, which Jack saw and approved on
   2026-08-06. That prototype duplicated these tokens and was deleted when this
   landed, exactly as its own header instructed.

   Home ships as a SECOND screen BESIDE Today, not instead of it. S20 and Jack
   are both explicit: he sees both before either goes. Today is untouched.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── The next-seven-days rail ──────────────────────────────────────────────
   FRAMING A, chosen by Jack 2026-08-08: show everything, and mark what is
   already handled. B (life only, admin lives in matters) was rejected. The
   reason is worth keeping: B re-homes the heavy items, and its failure mode is
   something important going quiet, which is the exact thing rule 10 exists to
   prevent. A is honest and the heavy items stay in front of him; what changes
   is that the week reads as mostly handled instead of mostly threatening. */
/* BF-10b: the rail is no longer inside a card. Each row is its own surface, and
   only the rows that WANT Jack get one; the handled ones are bare. That is the
   preview's whole argument in one rule — the week reads as mostly quiet, and the
   two things owed stand up off the page without anything going red. */
.dash-rail { display: flex; flex-direction: column; gap: 2px; }
.rail-row {
  display: flex; gap: 14px; align-items: baseline;
  padding: 11px 14px; border-radius: 12px;
  font-size: 15px;
}
.rail-row.wants {
  background: var(--dusk-card);
  border: 1px solid var(--card-line);
}
.rail-when {
  font-family: var(--kicker-font); font-size: 11px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--ink-faint);
  flex: none; width: 74px;
  font-variant-numeric: tabular-nums;
}
.rail-row.handled .rail-when { color: rgba(246, 240, 226, .5); }
.rail-what { flex: 1; min-width: 0; }
/* "handled" is a STATE, not a strikethrough. The item is still legible and
   still there; it just stops shouting. A struck-through council deadline reads
   as "done", which it is not - it is watched. */
.rail-row.handled .rail-what { color: var(--steel); }
.rail-caption { font-size: 13px; color: var(--steel); line-height: 1.5; margin: 10px 0 0; }

/* ── Feature tiles ─────────────────────────────────────────────────────────
   Jack's insight and the whole of S20: the pleasant features are the delivery
   mechanism for the boring ones. Astrology and the cookbook are what get him to
   open the app; the questions and deadlines are what he does once he is there.
   So they are peers on one grid, sized to be tapped and scanned, not read. */
.tiles {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.tile {
  position: relative; overflow: hidden;
  background: var(--dusk-card);
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
  border: 1px solid var(--card-line);
  border-radius: var(--surface-radius);
  padding: 18px 16px 16px; min-height: 118px;
  display: flex; flex-direction: column; gap: 6px;
  cursor: pointer; text-align: left;
  color: inherit; font: inherit;
  transition: transform .18s ease, border-color .18s ease;
}
/* The ember seam. One hue per feature across the top edge: it is what makes a
   tile recognisable before it is read, on a grid where every tile is otherwise
   the same rectangle. --e defaults to gold for anything without its own hue. */
.tile { --e: var(--gold); }
.tile::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--e), transparent);
  opacity: .7;
}
.tile.astro   { --e: var(--ember-astro); }
.tile.kitchen { --e: var(--ember-kitchen); }
.tile.quest   { --e: var(--ember-quest); }
.tile.file    { --e: var(--ember-file); }
.tile.health  { --e: var(--ember-health); }
.tile.task    { --e: var(--ember-task); }
.tile.cal     { --e: var(--day-hi); }
.tile.money   { --e: var(--ember-kitchen); }
/* D-28. GOLD, and stated rather than left to the default, because gold means one
   thing everywhere else in this app: it wants Jack. The calendar's gold dot, the
   matter sheet's "Wants you" chip and every card's first move are all this hue, and
   a letter that will not go out until he has read it is the purest case of it. */
.tile.approve { --e: var(--gold); }
/* The wide tile is the one the family opens the app for, so it gets the full
   width of the grid and does not have to be square. */
.tile.wide { grid-column: 1 / -1; min-height: 0; }
/* BF-33 §3 · the grid never leaves a hole. GENERALISED FROM `.tile.wide` rather
   than invented alongside it (§3.2): the same mechanism, with the span computed
   per paint instead of fixed at "all of them". app.js measures the rendered
   columns and the rendered tile count, picks the first tile of the last row and
   sets `--fill-span` to 1 + the number of empty cells beside it.

   ⚠️ DECLARED AFTER `.tile.wide` ON PURPOSE. The two selectors have identical
   specificity, so source order is the tie-break — and a tile that somehow carried
   both classes must span the row rather than a fraction of it. app.js never adds
   `fill` to a `.wide` tile; this is the belt.

   §3.4: a widened tile gets a layout that suits the width rather than a stretched
   one. The only thing width changes for a tile is how its body wraps, and
   `.tile-body`'s 28ch cap would leave the extra width as an empty gutter, so the
   cap is raised in proportion. Nothing is scaled and nothing is centred: it is the
   same card with more room for the same sentence. */
.tile.fill { grid-column: span var(--fill-span, 1); }
.tile.fill .tile-body { max-width: 52ch; }
.tile:hover { transform: translateY(-2px); border-color: rgba(242, 234, 217, .2); }
.tile:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.tile:active { border-color: var(--gold); }
.tile-head { display: flex; align-items: center; gap: 9px; }
.tile-title {
  font-family: var(--display-font); font-weight: 420;
  font-size: 1.06rem; line-height: 1.2; letter-spacing: -.005em;
  color: var(--ink); flex: 1; min-width: 0;
}

/* ── Tile icons ────────────────────────────────────────────────────────────
   Jack, 2026-08-06: "can we have some kind of icons / visual indicator of each?
   Easier to navigate than just all the text." Which is the brief's own
   complaint applied one level down, so it is the right call.

   NOT emoji. Emoji would be the only cartoon in an app built on Spectral, mono
   kickers, 4px radii and a hand-masked serpent mark, and they render as a
   different typeface on every platform. These are stroked line marks on a 24px
   grid, 1.4 stroke, round caps: the same instrument register as the rest.

   COLOUR TRACKS WHETHER THE TILE WANTS YOU, and is not decoration:
     gold-bright  = something to do, and it clears to zero (the badged pair)
     antique-gold = a standing capability, there when he wants it
     cyan-glow    = astrology, which already owns cyan on its own tile
     steel        = nothing here yet (health), matching its honest empty state
   Shape carries the differentiation; colour carries state. A six-colour set
   would have been prettier and would have meant nothing. */
/* BF-10b: colour no longer carries STATE here, the ember carries IDENTITY. The
   four-colour state scheme above was written when the tiles were monochrome and
   the icon was the only coloured thing on them; with a per-feature seam and a
   filled badge, an icon on a different scheme was a second, contradictory
   language on the same 118px card. */
.tile-icon {
  flex: none; width: 20px; height: 20px; display: block;
  color: var(--e);
}
.tile.empty .tile-icon { color: var(--ink-faint); }
.tile-body {
  font-size: .88rem; line-height: 1.45; color: var(--ink-dim);
  flex: 1; text-wrap: pretty; max-width: 28ch;
}
.tile-body .lead { color: var(--ink); font-weight: 500; }
.tile-foot {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--ink-faint);
}

/* Badge. CLAUDE.md rule 10 as amended by Jack 2026-08-01: a count is allowed
   ONLY when doing the obvious thing takes it to zero. Gold, never red; no
   streaks; no number that survives to tomorrow bigger than it was. The two here
   (questions to answer, documents waiting to be filed) both clear in seconds.
   app.js hides the badge entirely at zero rather than rendering a "0", because
   a zero is still a number on a screen asking to be cleared. */
.badge {
  position: absolute; top: 14px; right: 14px;
  font-family: var(--kicker-font); font-size: 11px; font-weight: 500;
  min-width: 22px; height: 22px; padding: 0 6px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px;
  /* Filled with the tile's own ember and set in the night colour. A hollow gold
     pill read as decoration next to the gold seam; a filled one reads as a
     count, which is what it is. #16132a is the preview's badge ink and measures
     11:1 on every ember hue in the set. */
  color: #16132a;
  background: var(--e);
  border: none;
  font-variant-numeric: tabular-nums;
}
.tile.has-badge { border-color: rgba(239, 192, 116, 0.3); }

/* The astrology tile is the one place a little atmosphere is warranted: it is
   the pleasant thing, and it is what gets him in the door. One quiet gradient,
   no animation beyond the star field the shell already draws. */
.tile.astro {
  background:
    radial-gradient(ellipse 80% 120% at 85% 0%, rgba(168, 200, 224, 0.10) 0%, transparent 60%),
    var(--card-bg);
}
.tile.astro .tile-body .lead { color: var(--cyan-glow); }

/* ── The astrology question surface (S13 6a2) ──────────────────────────────
   SURFACE B, chosen by Jack 2026-08-08: a "?" in the card header opening a
   small form. A (a permanent comment box with three intents) stays available
   and is the only one that gives CONTEXT and FEEDBACK a home, which is the one
   real argument for it. B adds no permanent furniture and maps 1:1 onto
   POST /api/astro/questions {question, person, event_context?}. */
.astro-ask-btn {
  flex: none; width: 44px; height: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--surface-radius);
  background: transparent; color: var(--cyan-glow); cursor: pointer;
  border: 1px solid var(--card-border);
  font-family: var(--kicker-font); font-size: 15px; line-height: 1;
}
.astro-ask-btn:hover, .astro-ask-btn:focus-visible {
  border-color: var(--cyan-glow); outline: none;
}
/* The asymmetry has to be VISIBLE, not just correct. The daily reading refuses
   event context (S13 3c) and the question path accepts it (6a2), on purpose.
   Unexplained, the two paths look inconsistent rather than designed. */
.astro-ctx-note { font-size: 12px; color: var(--steel); line-height: 1.45; margin: 4px 0 0; }
/* Provenance. How Jack can tell the answer LOOKED rather than guessed: the same
   instinct as the sweep heartbeat. Counted against ASTRO_ASK_TOOL_CALL_CEILING. */
/* BF-10b: Home's greeting is now the page's one display line. It was 22px sans
   and quiet on purpose in BF-10a; the approved preview makes it the thing you
   land on, in Fraunces, with the name as the warm word. */
.dash-clockline {
  font-family: var(--kicker-font); font-size: .66rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-faint);
}
.dash-greeting {
  font-family: var(--display-font); font-weight: 420;
  font-size: 2.15rem; line-height: 1.12; letter-spacing: -.01em;
  color: var(--ink); margin: 6px 0 0;
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
.dash-greeting .warm { color: var(--gold); }
.dash-greeting .dash-swept {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--ink-faint); margin-left: auto;
  align-self: center;
}
.dash-h {
  font-family: var(--kicker-font); font-size: .66rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-faint);
  font-weight: 500; margin: 30px 0 12px;
}
/* Home's condensed digest, now the preview's lead sentence rather than a card.
   Same voice, same words: block 1 verbatim, then the way into the full brief. */
.dash-lead { margin: 14px 0 4px; }
.dash-lead .lead-line {
  font-family: var(--display-font); font-weight: 340;
  font-size: 1.22rem; line-height: 1.45; color: var(--ink-dim);
  max-width: 34ch;
}
.dash-lead .lead-line.warn { color: var(--ink); }
.dash-lead .digest-more {
  font-family: var(--kicker-font); font-size: 10px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--gold);
  background: none; border: none; cursor: pointer; padding: 8px 0 0; min-height: 44px;
}

/* D-28. The answers rail, moved onto Home off the deleted Today screen. Spacing
   only: the cards themselves are `.ask-card` and are unchanged, because the point of
   the move is that Jack sees the SAME card in a better place, not a redesigned one.
   It sits between the lead line and the win pills, which is where it sat in Today's
   stream relative to everything else — above the work, below the summary. */
.dash-asks { margin: 16px 0 4px; }

/* "Two minutes each". A pill, not a card: it is a thing to tap on the way past,
   and it disappears the moment there is nothing small to do. */
.wins { display: flex; gap: 10px; flex-wrap: wrap; }

/* ═══ THE WIDE-SCREEN DASHBOARD · D-164 kept and rebuilt as D-165, 2026-08-25 ═══
   Jack's reaction to D-164, in full: "obvs I would prefer widescreen
   compatibility, it's just if you're unable to do it I guess." So the band stays.
   What follows is the version that survives its own empty states, which the
   proposal did not.

   ⚠️ WHAT D-164 GOT WRONG, MEASURED RATHER THAN REASONED. It paired the "Two
   minutes each" wins with the week rail. But the wins section HIDES ITSELF when
   there is nothing small to do (`winSec.hidden = wins.length === 0`, app.js), and
   that is the ordinary day rather than the exception — so on the ordinary day the
   left column was empty and the rail sat in the right-hand 457px of a 1194px
   screen with 423px of nothing beside it. Driven and measured before it was
   changed, not guessed. A pairing whose left half is usually absent is not a
   two-column layout, it is a right-aligned one.

   WHAT THIS DOES INSTEAD. The week rail becomes a persistent right-hand column,
   and everything else — the brief, the answers, the wins — stacks down the left,
   with the tiles full width underneath. That is D-164's own argument (act on the
   left, being-handled on the right, rule 10's "waiting is never a task for Jack")
   applied to the whole of Home rather than to one band, and it is STABLE because
   `#dash-rail` is the one block on Home that never hides: an empty week says
   "Nothing is landing this week" rather than disappearing (app.js). The column
   that is always there is the only one that can be a column.

   WHAT IT BUYS, STATED HONESTLY. D-164's comment claimed it brought "The house"
   back above the fold. Measured on the harness fixture at 1194x834 it does not
   and could not: the answer cards alone are 442px tall. What it actually does is
   lift the tiles by the whole height of the rail — ~390px on that fixture, most
   of a screen of scrolling. That is worth having, it is what the harness asserts,
   and it is the claim that goes in the file rather than the flattering one.

   WHY 900px. An iPad in PORTRAIT is 834px, so the layout Jack uses most often is
   untouched; this is landscape (1194px) and desktop only. Nothing moves in the
   markup, so reading order, tab order and screen-reader order are identical in
   both layouts, and turning the whole thing off is still deleting these blocks.

   ⚠️ Grid PLACEMENT of existing components only. No tile, rail row or win pill is
   restyled here. */
@media (min-width: 900px) {
  /* ⚠️ TWO COLUMNS ONLY WHEN THERE IS A LEFT-HAND COLUMN TO HAVE, and this guard is
     the fix rather than a refinement of it. The rail is permanent; everything that
     can sit beside it is not. The answers hide when there is nothing to answer and
     the wins hide when there is nothing small to do, and on the quiet evening when
     BOTH are away the left column would be the brief (101px) above 400px of
     nothing. Home is genuinely one column on that evening, so it says so.
     `:not([hidden])` rather than a class, because `hidden` is already what app.js
     sets and a second flag is a second thing to keep in step; `:has()` has been in
     every engine this app runs on since 2023, and `#ask-list` is REMOVED outright
     for a person without the ask capability, which reads correctly as absent. */
  .dash:has(#ask-list:not([hidden])),
  .dash:has(#dash-wins-sec:not([hidden])) {
    display: grid;
    /* The rail is a sidebar, so it takes a SHARE rather than a fraction: 299px
       inside an 880px container and 360px inside 1060px, which keeps a day label
       and a matter title on one line at both ends without ever costing half the
       screen. `minmax(0, …)` on the main column, or a long rail title refuses to
       shrink and the grid overflows — the min-width:auto default, the same trap
       as TRAPS' flex-ellipsis note one axis over. */
    grid-template-columns: minmax(0, 1fr) minmax(240px, 34%);
    column-gap: 26px;
    align-items: start;
    /* Named rather than numbered, so adding a block to Home is a line here rather
       than a silent re-flow of everything after it. `week` spans two rows on
       purpose: EITHER of them can collapse to nothing — the answers hide when
       there is nothing to answer, the wins when there is nothing small to do —
       and the sidebar is unmoved by both. That is the whole fix. */
    grid-template-areas:
      "clock  clock"
      "greet  greet"
      "lead   weekh"
      "asks   week"
      "wins   week"
      "househ househ"
      "house  house";
  }
  #dash-clockline { grid-area: clock; }
  #dash-greeting  { grid-area: greet; }
  #dash-digest    { grid-area: lead; }
  #ask-list       { grid-area: asks; }
  #dash-wins-sec  { grid-area: wins; }
  #dash-h-week    { grid-area: weekh; }
  #dash-rail      { grid-area: week; }
  #dash-h-house   { grid-area: househ; }
  #dash-tiles     { grid-area: house; }
  /* ⚠️ THE RAIL HEADING IS BOTTOM-ALIGNED, AND THAT IS NOT A TASTE CALL. It shares
     a grid row with the lead line, which is three times its height, so `start`
     left "Your week, mostly handled" floating 89px above the list it names —
     measured, on the fixture, at 1194px. A heading has to touch its own content or
     it reads as belonging to whatever is nearest, and what was nearest was the
     brief in the other column. `end` puts it back on the rail.
     Safe when the brief is hidden: a grid row always sizes to its own content, so
     the row cannot collapse under the heading and the heading cannot escape it. */
  /* Scoped to the same condition as the grid itself: unscoped, this would strip
     `.dash-h`'s 30px top margin off the heading in the ONE-column fallback above,
     where it is the only thing separating it from the brief. The `grid-area` lines
     above need no such scoping — they are inert on a child of a block container. */
  .dash:has(#ask-list:not([hidden])) > #dash-h-week,
  .dash:has(#dash-wins-sec:not([hidden])) > #dash-h-week {
    align-self: end; margin: 0 0 12px;
  }
  /* Reachable only when the wins are visible, which is one of the two ways the grid
     forms, so it needs no guard of its own. */
  #dash-wins-sec > .dash-h { margin-top: 22px; }
}

/* Above 1400px the reading column stops being the constraint and starts being the
   design: a 920px app centred on a 1512px laptop leaves ~300px of background on
   each side, which is a considered choice on a text site and a waste in an app
   whose Home is a grid of doors.

   1100px is a measured step and not "as wide as it goes". The prose on Home is
   capped at 34ch by `.lead-line` whatever the container does, so nothing here
   lengthens a line of text; what it actually buys is the tiles' FOURTH column,
   which `.tiles` picks up on its own from `repeat(auto-fit, minmax(210px, 1fr))`
   at this width. BF-33's hole-filling is computed per paint from the measured
   column count (app.js), so it follows the extra column without being told. */
@media (min-width: 1400px) {
  #app { max-width: 1100px; }
}
.win {
  background: var(--gold-soft);
  border: 1px solid rgba(228, 177, 99, .32);
  color: var(--ink); border-radius: 999px;
  padding: 12px 18px; min-height: 44px;
  font-family: inherit; font-size: .92rem; font-weight: 500;
  cursor: pointer; text-align: left;
  transition: transform .18s ease, background .18s ease;
}
.win:hover { transform: translateY(-1px); background: rgba(228, 177, 99, .24); }
.win:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.win small { color: var(--ink-dim); font-weight: 400; margin-left: 6px; font-size: .82rem; }

/* The "handled" marker on the week rail. Two words, right-aligned, quiet: it
   names the state without repeating a sentence on every row. See app.js for why
   this replaced the per-row prose. */
.rail-mark {
  flex: none; align-self: center;
  font-family: var(--kicker-font); font-size: .6rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ember-file);
  border: 1px solid rgba(143, 196, 185, .35); border-radius: 999px;
  padding: 3px 8px;
}

/* Selects inside sheets. ⚠️ This was NOT only the new astrology one: #upload-subject
   and #vault-doc-type have rendered as native white OS dropdowns in a black app
   since they shipped, because the only select rule in this file was scoped to
   `.menu-row select`. Fixed for all three at once rather than for mine alone.
   16px minimum: anything smaller and iOS Safari zooms the page on focus. */
dialog select {
  -webkit-appearance: none; appearance: none;
  width: 100%; box-sizing: border-box;
  background: var(--bg-deep); border: 1px solid var(--card-border);
  color: var(--ice); border-radius: var(--surface-radius);
  padding: 10px 12px; font-size: 16px; font-family: inherit;
  min-height: 44px; margin-bottom: 4px;
  /* The native arrow goes with the native appearance, so it is drawn back on. */
  background-image:
    linear-gradient(45deg, transparent 50%, var(--steel) 50%),
    linear-gradient(135deg, var(--steel) 50%, transparent 50%);
  background-position: right 18px center, right 13px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
dialog select:focus-visible { border-color: var(--antique-gold); outline: none; }

/* ---------- the sheet, and the chrome every sheet shares (BF-10b §2-§5) ----------
   Five screens, one set of chrome, drawn once here and built once in app.js. The
   previews argue this and the argument is about the way OUT: a Close pill that is
   44px, in the same corner, on every screen, is muscle memory by the second day; a
   control that moves between screens is one the user has to look for every time.

   The panel itself is what makes a screen read as sitting OVER Home rather than
   replacing it: rounded at the top only, blurred, and with Home's own greeting
   visible above it. */
.screen-sheet {
  background: linear-gradient(180deg, rgba(22, 20, 38, .92), rgba(16, 15, 30, .96));
  border: 1px solid var(--card-line);
  border-bottom: none;
  border-radius: calc(var(--surface-radius) + 6px) calc(var(--surface-radius) + 6px) 0 0;
  backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
  box-shadow: 0 -12px 50px rgba(0, 0, 0, .45);
  padding: 10px 16px 60px;
  margin-top: 10px;
  min-height: calc(100vh - 260px);
}
.grab {
  width: 44px; height: 5px; border-radius: 99px;
  background: rgba(246, 240, 226, .28);
  margin: 2px auto 10px;
}
.screen-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 0 0 18px;
}
.screen-back {
  -webkit-appearance: none; appearance: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; min-width: 88px; padding: 8px 16px;
  font-family: var(--ui-font); font-size: 0.9rem; font-weight: 500;
  letter-spacing: 0; text-transform: none; cursor: pointer;
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--card-line);
  border-radius: 999px;
}
.screen-back:active { border-color: var(--gold); color: var(--gold); }
.screen-title {
  font-family: var(--display-font); font-weight: 420;
  font-size: 1.5rem; letter-spacing: 0; text-transform: none;
  color: var(--ink); margin: 0;
}

/* Home, peeking above the sheet. Two lines, no controls: it orients, it does not
   navigate. A second way home here would compete with the Close pill, and the whole
   point of the pill is that there is only one. */
.home-peek { padding: 6px 4px 0; }
.home-peek .kicker { font-size: 0.66rem; letter-spacing: 0.18em; }
.home-peek .peek-greet {
  font-family: var(--display-font); font-weight: 420; font-size: 1.35rem;
  color: var(--ink); margin-top: 4px;
}
.home-peek .peek-greet .warm { color: var(--gold); }
.home-peek .peek-greet .dash-swept { display: none; }

/* The ≤560px nav media query lived here. It existed to stop six tab labels wrapping
   into each other on a 375px phone, and it is deleted with the tabs it was shrinking
   (BF-10b's deletion gate names it explicitly). Nothing replaces it: three capture
   segments have room at any width the app supports. */

/* BF-14 · the three-segment pill (Scan / Write / Ask), BF-33 §1-§2 · floating,
   iconic, and with a fourth segment when there is somewhere to come back from.

   ⚠️ IT IS A PILL NOW, `border-radius: 999px`, and that overturns a note the old
   `.nav-add` rule carried ("14px, not a pill: the app is built on 4px radii and a
   fully round button would be the only circle in it"). Two things changed under
   that note: the app's own `--surface-radius` is 18px rather than 4px, and Jack
   asked for this shape by name — "a sticky pill-shaped component". The approved
   Dusk home preview draws its one piece of chrome fully round too. It is also no
   longer "the only circle": every `.who` chip, every `.badge` and the win pills
   are 999px.

   ⚠️ THE GLASS IS HERE, not on a tray behind it. `--card-bg-strong` under a gold
   wash is the same material `.capture-bar` was made of; moving it onto the pill is
   what lets the tray go without the gold glyphs losing the dark surface they were
   measured against. The shadow pair is the preview's, and is what makes it read as
   floating over the content rather than pinned to an edge. */
.nav-pill {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 2px; align-self: center;
  /* 50px, not the 42px it shipped at: 3px of padding either side left each segment
     36px tall, and BF-10b puts a 44px floor under every target including these. */
  padding: 3px; height: 50px;
  background:
    linear-gradient(180deg, rgba(233, 206, 122, .15), rgba(233, 206, 122, .09)),
    var(--card-bg-strong);
  -webkit-backdrop-filter: blur(var(--card-blur));
          backdrop-filter: blur(var(--card-blur));
  border: 1px solid rgba(233, 206, 122, 0.45);
  border-radius: 999px;
  box-shadow: 0 6px 30px rgba(228, 177, 99, .20), 0 2px 10px rgba(0, 0, 0, .45);
}
.nav-pill-btn {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  min-width: 48px; padding: 0 10px; height: 100%; min-height: 44px;
  font-family: inherit; font-size: 11px; line-height: 1; letter-spacing: 0.4px;
  color: var(--gold-bright);
  background: transparent;
  border: none; border-radius: 999px;
  cursor: pointer;
}
.nav-pill-btn:active {
  background: rgba(233, 206, 122, 0.28);
}
.nav-pill-btn[aria-selected="true"] {
  background: rgba(233, 206, 122, 0.22);
}
/* BF-33 §1.2. 22px on a 24px grid: the same drawing register as `.tile-icon`, one
   size up because this is a thumb target rather than a card ornament. */
.nav-pill-ico { width: 22px; height: 22px; display: block; flex: none; }

/* BF-33 §1.3 · the labels stay, they just stop being visible. The old design put
   "SCAN WRITE ASK" on screen in kicker caps, which is the "little text nav box"
   Jack was describing. Screen readers still get them (and the buttons' aria-labels
   say more: "Scan a document"), so nothing was lost but the ink. */
.nav-pill-lbl { font-family: var(--kicker-font); text-transform: uppercase; }
/* The one visually-hidden utility in this stylesheet. Not `display: none` and not
   `visibility: hidden` — both take the text out of the accessibility tree, which
   is the opposite of what §1.3 asks for. */
.vh {
  position: absolute !important;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* ── BF-33 §2 · the expanded state ──────────────────────────────────────────
   ONE component in two states, not two components swapping (§2.2): the back
   segment is always in the pill and its WIDTH is what changes, so the pill is seen
   to grow a fourth affordance. A fixed 48px rather than `auto` because `width`
   cannot transition to or from `auto`, and an expand that snaps is the one thing
   §2.4 asks us not to ship.

   The separator is a hairline, not a gap: without it the back chevron reads as a
   fourth capture action rather than as the way out. */
.nav-pill-sep {
  flex: 0 0 auto; width: 0; height: 26px; margin: 0;
  border-left: 1px solid rgba(233, 206, 122, 0.35);
  opacity: 0;
  transition: width .22s ease, margin .22s ease, opacity .16s ease;
}
.nav-pill-back {
  width: 0; min-width: 0; padding: 0; overflow: hidden;
  opacity: 0;
  transition: width .22s ease, min-width .22s ease, padding .22s ease,
              opacity .16s ease;
}
.nav-pill.expanded .nav-pill-sep {
  width: 1px; margin: 0 3px; opacity: 1;
}
.nav-pill.expanded .nav-pill-back {
  /* 48 wide x 44 tall: the same floor every other segment keeps (BF-33 §1.3, and
     BF-10b's 44px rule that the harness measures). */
  width: 48px; min-width: 48px; padding: 0 10px; opacity: 1;
}

/* BF-14 · Write/Ask tabs INSIDE the capture-dialog. Same look as the sub-nav
   two-halves toggle, but scoped so it does not affect the sub-nav. */
dialog .pill-tabs.capture-modes {
  display: flex; gap: 0; margin: 12px 0 8px;
}
dialog .pill-tabs.capture-modes button {
  flex: 1; font-family: var(--kicker-font); font-size: 11px; letter-spacing: 1px;
  text-transform: uppercase; padding: 9px 10px; cursor: pointer;
  background: transparent; color: var(--steel);
  border: 1px solid var(--card-border);
}
dialog .pill-tabs.capture-modes button + button { border-left: none; }
dialog .pill-tabs.capture-modes button[aria-selected="true"] {
  color: var(--gold-bright); border-color: var(--antique-gold);
  background: rgba(233, 206, 122, 0.08);
}

/* BF-14 · the upload-dropdown (rare path: file already on the device). Closed
   by default so the common Write/Ask sheet is one text box and nothing else. */
dialog .upload-dropdown {
  margin-top: 16px; padding-top: 12px;
  border-top: 1px dashed var(--card-border);
}
dialog .upload-dropdown > summary {
  cursor: pointer; font-family: var(--kicker-font);
  font-size: 11px; letter-spacing: 1px; text-transform: uppercase;
  color: var(--steel); padding: 6px 0;
}
dialog .upload-dropdown[open] > summary { color: var(--gold-bright); }
dialog .upload-tier-choice {
  display: flex; flex-direction: column; gap: 10px; margin-top: 10px;
}

/* BF-10b §10 · the tier choice as two labelled CARDS, which is what the approved
   preview draws. It was two ghost buttons carrying a bold line and a 12px grey one:
   the same words, in the shape of "two more controls". The choice is load-bearing
   (an identity document in the ordinary tier puts extracted values on screens with
   no re-auth) so it gets a card each, a Fraunces title, and a full sentence.

   ⚠️ The vault card is THE SAME MATERIAL as the vault seal in Files and the record
   cards in §9: violet gradient, violet border, violet title. That is deliberate and
   it is pinned — crossing into the vault must never change temperature, so a reader
   recognises where they are before they read a word. */
.tier {
  display: block; width: 100%; text-align: left; cursor: pointer;
  min-height: 44px; padding: 16px;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
  font-family: inherit; color: var(--ice);
}
.tier:active { border-color: var(--gold-bright); }
.tier .tier-title {
  display: block;
  font-family: var(--display-font); font-weight: 420;
  font-size: 1rem; letter-spacing: 0; text-transform: none;
  color: var(--ice);
}
.tier .tier-sub {
  display: block; margin-top: 6px;
  font-size: 14px; line-height: 1.5; color: var(--ink-dim, var(--steel));
}
.tier.tier-vault {
  background: linear-gradient(180deg, rgba(38, 30, 58, .75), rgba(26, 22, 44, .75));
  border-color: rgba(168, 151, 224, .35);
}
.tier.tier-vault .tier-title { color: var(--ember-astro, #a897e0); }

/* BF-10b §10 · the review step's tier switch and the sentence under it. Off the
   hurry path by design: Scan is still one tap to the camera, and the two decisions
   that need reading live here. */
.scan-subject {
  display: block; margin: 16px 0 6px;
  font-family: var(--kicker-font); font-size: 11px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--steel);
}
.vault-switch { margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--card-border); }
.vault-switch-row {
  display: flex; align-items: flex-start; gap: 12px;
  min-height: 44px; padding: 6px 0; cursor: pointer;
}
.vault-switch-row input[type="checkbox"] {
  width: 24px; height: 24px; flex: none; margin-top: 2px;
  accent-color: var(--ember-astro, #a897e0);
}
.vault-switch-text { font-size: 15px; line-height: 1.5; color: var(--ice); }
.vault-switch-note {
  font-size: 14px; line-height: 1.5; color: var(--steel); margin-top: 6px;
}

/* BF-36 §2.4 · the big-scan toggle. It borrows the vault switch's LAYOUT (the
   44px row, the 24px box, the note underneath) because it is the same shape of
   decision on the same sheet, and deliberately NOT its violet: violet in this app
   means sealed, and a document held before Pass B is not sealed — it is stored in
   the open exactly like any other letter, with only the judgement deferred.
   Borrowing the vault's temperature here would be the first place in the app
   where the colour stopped meaning one thing. */
.scan-backfill {
  margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--card-border);
}
.scan-backfill input[type="checkbox"] {
  accent-color: var(--gold-bright);
}

/* The standing banner on the "+" sheet. Gold, not red: nothing is wrong, and a
   red strip on the sheet Jack opens to jot a thought is a guilt mechanic (rule
   10). It carries its own way out, so it is never a statement he can only read
   (rule 13). */
.capture-backfill {
  margin: 0 0 14px; padding: 14px 16px;
  background: rgba(217, 169, 95, .09);
  border: 1px solid rgba(217, 169, 95, .32);
  border-radius: var(--surface-radius);
}
.capture-backfill p {
  margin: 0 0 10px; font-size: 14px; line-height: 1.55; color: var(--ink-dim);
}
.capture-backfill button { min-height: 44px; }

/* BF-10b §10 · the offline queue, said on the sheet the reader is typing into.
   No red, no badge: a waiting count drains itself and a refused one has its own
   three ways out, so neither is a debt that survives to tomorrow (rule 10). */
.capture-sync {
  margin-top: 18px; padding-top: 14px;
  border-top: 1px solid var(--card-border);
}
.capture-sync-blocked {
  margin-top: 10px; padding: 14px 16px;
  background: rgba(240, 184, 168, .07);
  border: 1px solid rgba(240, 184, 168, .3);
  border-radius: var(--surface-radius);
}
.capture-sync-why { font-size: 14px; line-height: 1.5; color: #f0b8a8; }
.capture-sync-kept {
  font-size: 14px; line-height: 1.5; color: var(--steel); margin-top: 6px;
}

/* BF-14 · the scan review strip. Thumbnails of every page taken so far, in
   order, with Retake and Remove per page. Every page is a clear, unambiguous
   entry — a lost or mis-ordered page in a court letter is exactly the class of
   quiet drift SR-1 §P-2 was written to prevent. */
/* ⚠️ REBUILT 2026-08-14 (D-50) — Jack, after the first real upload: "the 'remove'
   under each photo that I uploaded was crammed / badly designed." Two things were
   wrong and only one of them was visible in the CSS:

     1. A 108px card minus 12px of padding is a 96px content box. `flex: 1` is
        `flex-basis: 0` with `min-width: auto`, so each button floored at its
        min-content width instead: "RETAKE" at 11px with 2px of letter-spacing
        measures 62px, so 62 + 4 + 62 = 128px of buttons in a 96px box and the
        second one hung out over the card's own edge. Measured in the harness
        against the pre-fix ref: `escaped: 1`. The controls are STACKED now, one
        per row, full card width — a two-word column beats a two-column word.
     2. `dialog .ghost { margin-top: 18px }` was landing on both of them (TRAPS
        has this leak twice already, for .acct-top and .task-actions; this is the
        third). It pushed the controls 18px off the page label and made the card
        taller than anything in the CSS said it was. Excluded below.

   The strip is an auto-fill grid rather than a wrapping flex row so the cards
   share the sheet's width evenly instead of leaving a ragged tail — a 3-page
   letter and a 12-page one both read as a sequence. */
.scan-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px;
  margin: 14px 0 6px;
}
.scan-thumb {
  padding: 10px;
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--surface-radius);
  display: flex; flex-direction: column; gap: 10px;
}
.scan-thumb img, .scan-thumb-file {
  width: 100%; height: 168px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.35);
}
.scan-thumb img { object-fit: cover; }
/* A page that came from Choose File rather than the camera. A PDF in an <img> is
   a broken-image glyph, which reads as "this page failed" rather than "this page
   is a document"; it says what it is and what it is called instead. */
.scan-thumb-file {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 8px;
  padding: 10px; box-sizing: border-box;
  border: 1px dashed var(--card-border-strong);
}
.scan-thumb-glyph {
  font-family: var(--kicker-font); font-size: 11px; letter-spacing: 2px;
  color: var(--gold, var(--steel));
}
.scan-thumb-name {
  font-size: 12px; line-height: 1.4; color: var(--ice);
  text-align: center; word-break: break-word;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden;
}
.scan-thumb-num {
  font-family: var(--kicker-font); font-size: 11px; letter-spacing: 1px;
  text-transform: uppercase; color: var(--steel); text-align: center;
}
.scan-thumb-controls {
  display: flex; flex-direction: column; gap: 8px;
}
.scan-thumb-controls button {
  width: 100%; padding: 11px 10px; font-size: 11px;
}
/* The third instance of the leak. Without this each control carries an 18px top
   margin it was never meant to have, which is most of what "crammed" was. */
dialog .scan-thumb-controls .ghost { margin-top: 0; }
.scan-review-actions { margin-top: 12px; }


/* ── BF-10b §12 · the matter sheet ──────────────────────────────────────────────
   Four blocks, four questions, and each one has its own material so a reader
   scrolling fast can tell them apart without reading a heading:

     the chip + lead   who is holding this, and what happens next
     the amber lines   what must never be done here
     the timeline      what has happened, quiet, newest first
     the citations     what it stands on, each wearing its status

   The chip takes its colour from the SAME two tokens the matter CARD's chip uses
   (`--ember-file` for their move, `--gold` for yours), because the card and the
   sheet are the same claim about the same matter one tap apart, and two colours
   for one fact is how a reader learns to distrust both. */
.movechip {
  display: inline-block; font-family: var(--kicker-font); font-size: 0.62rem;
  letter-spacing: 0.12em; text-transform: uppercase;
  border-radius: 999px; padding: 5px 12px; margin: 4px 0 14px;
  color: var(--ember-file); border: 1px solid rgba(143, 196, 185, .4);
}
.movechip.yours { color: var(--gold); border-color: rgba(239, 192, 116, .45); }
.matter-lead {
  background: var(--card-bg); border: 1px solid var(--card-line);
  border-radius: var(--surface-radius); padding: 16px;
  font-size: 0.92rem; color: var(--ink-dim); line-height: 1.6;
}
.keydates { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
.kd {
  background: var(--card-bg); border: 1px solid var(--card-line);
  border-radius: var(--surface-radius); padding: 12px 16px; min-width: 150px;
  flex: 1 1 150px;
}
.kd .d { font-family: var(--kicker-font); font-size: 0.66rem; letter-spacing: 0.1em;
         color: var(--gold); }
.kd .w { font-size: 0.85rem; color: var(--ink-dim); margin-top: 4px; line-height: 1.45; }

/* A constraint is PINNED: a left rule in amber, its own tint, and full-strength
   ink for the sentence itself. It is the only block on this sheet whose body text
   is `--ink` rather than `--ink-dim`, because it is the one thing here a reader
   must not skim. */
.constraint {
  background: var(--amber-soft); border: 1px solid var(--amber-line);
  border-left: 3px solid var(--amber);
  border-radius: var(--surface-radius); padding: 12px 16px; margin-bottom: 8px;
}
.constraint .t {
  font-family: var(--kicker-font); font-size: 0.6rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--amber);
}
.constraint p { font-size: 0.88rem; color: var(--ink); line-height: 1.55; margin: 5px 0 0; }
.constraint-note { font-size: 0.78rem; color: var(--ink-faint); line-height: 1.5;
                   margin: 6px 0 0; }

/* The timeline is one card of quiet rows, not a stack of cards. Three sentences
   about a case file are a paragraph with dates down the side; three CARDS are a
   backlog, and this screen is not a backlog. */
.tl {
  background: var(--card-bg); border: 1px solid var(--card-line);
  border-radius: var(--surface-radius); padding: 6px 16px;
}
.tl .row { display: flex; gap: 14px; border-top: 1px solid rgba(246, 240, 226, .08);
           padding: 12px 0; }
.tl .row:first-child { border-top: none; }
.tl .t { font-family: var(--kicker-font); font-size: 0.62rem; letter-spacing: 0.1em;
         color: var(--ink-faint); white-space: nowrap; padding-top: 2px; flex: none; }
.tl .w { font-size: 0.88rem; color: var(--ink-dim); line-height: 1.5; }
.tl .empty { padding: 12px 0; font-size: 0.88rem; color: var(--ink-faint); }
/* A cut citation keeps its reason on the row. `.cite .v.cut` is the amber, and it
   wraps, because "declined: it points at the landlord" is a sentence and not a
   chip. */
.cite .v.cut { color: var(--amber); white-space: normal; }
.mdoc {
  display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
  background: var(--card-bg); border: 1px solid var(--card-line);
  border-radius: var(--surface-radius); padding: 12px 16px; margin-bottom: 8px;
}
.mdoc .n { font-family: var(--display-font); font-weight: 420; font-size: 0.95rem;
           color: var(--ink); }
.mdoc .m { font-family: var(--kicker-font); font-size: 0.6rem; letter-spacing: 0.1em;
           text-transform: uppercase; color: var(--ink-faint); white-space: nowrap; }
/* BF-26 §2.2 — "Drafted by Life Manager from your records, 13 Aug". One rule, three
   screens (the Files list, the matter's filed-here rows, the document sheet), because
   there is one writer (`originWords`) and a second rule is how two of them start
   disagreeing about what the sentence says.

   `--ink-dim`, not `--ink-faint`: this is the line that tells him a machine wrote what
   he is about to approve, and it measures 7.1:1 at the worst sky where faint does not.
   `.mdoc` wraps for it — the row is `space-between` with two children, so a third
   would have squeezed itself between the name and the date. */
.doc-origin {
  font-size: 0.78rem; line-height: 1.45; color: var(--ink-dim); margin-top: 4px;
}
.mdoc { flex-wrap: wrap; }
.mdoc .doc-origin { flex-basis: 100%; }
/* On the sheet it sits directly under the title and above the scan, which is the one
   place it must be read BEFORE the document rather than after it. */
.doc-origin-lead { margin: -8px 0 14px; }
#matter-dialog .matter-ask { margin-top: 0; }
#matter-dialog .matter-ask .btn { min-height: 44px; }
#matter-dialog .thread { margin-top: 0; }
@media (max-width: 420px) {
  .mdoc { flex-direction: column; }
}

/* =====================================================================
   BF-25 §6 · the Health sheet.
   Ported from cockpit/prototypes/design-preview-dusk-health.html, which Jack
   approved on 2026-08-10. Two deliberate departures from the drawing, both so the
   sheet belongs to THIS app rather than to the prototype:

     1. The seal colour is `--ember-health`, not the preview's one-off `--seal`
        purple. The Health tile on Home already wears that ember as its seam, and
        a feature whose door is pink and whose room is purple reads as two
        features. One hue per feature is the whole point of the ember tokens.
     2. The chrome (`.grab`, `.acct-top`, `.ghost` close) is the app's, shared with
        every other sheet, rather than the preview's private copy of it. The
        preview's own note asks for exactly that: "identical chrome to every other
        sheet, on purpose".
   ===================================================================== */
.who-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.who {
  min-height: 44px; display: inline-flex; align-items: center;
  padding: 8px 16px; border-radius: 999px;
  border: 1px solid var(--card-line); background: none; color: var(--ink-dim);
  font-family: var(--ui-font); font-size: 0.88rem; cursor: pointer;
}
.who[aria-pressed="true"] {
  background: var(--gold-soft); border-color: rgba(239, 192, 116, .4);
  color: var(--ink);
}
.who-note {
  font-size: 0.78rem; color: var(--ink-faint); line-height: 1.6;
  margin: 0 0 20px;
}
.health-timeline { display: block; }
/* The month heading. A chronology needs somewhere for the eye to land; a flat
   list of forty dated rows does not read as a timeline. */
.health-month {
  font-family: var(--kicker-font); font-size: 0.66rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--ink-faint); margin: 24px 0 10px;
}
.health-entry {
  position: relative; padding: 14px 14px 14px 18px; border-radius: 12px;
  border: 1px solid transparent;
  border-left: 2px solid rgba(215, 156, 173, .45);
}
.health-entry:hover {
  background: var(--card-bg); border-color: var(--card-line);
  border-left-color: var(--ember-health);
}
/* ⚠️ "Dated" and "Filed" are DIFFERENT STATEMENTS and they are set differently.
   Gold means the letter says so; faint ink means it is only when it arrived.
   Painting both gold would invent a chronology out of an arrival order. */
.health-when {
  font-family: var(--kicker-font); font-size: 0.72rem; color: var(--gold);
  letter-spacing: 0.06em;
}
.health-when.filed { color: var(--ink-faint); }
.health-label { display: block; font-size: 1rem; color: var(--ink); margin-top: 4px; }
.health-one-line {
  display: block; font-size: 0.86rem; color: var(--ink-dim);
  margin-top: 3px; line-height: 1.55;
}
.health-meta {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 9px;
}
.health-chip {
  font-family: var(--kicker-font); font-size: 0.62rem; letter-spacing: 0.1em;
  border-radius: 999px; padding: 4px 9px; flex: none;
}
.health-chip.seal { color: var(--ember-health); border: 1px solid rgba(215, 156, 173, .45); }
.health-chip.who-chip { color: var(--ember-task); border: 1px solid rgba(138, 166, 216, .4); }
.health-chip.est { color: var(--ink-faint); border: 1px solid var(--card-line); }
.health-open {
  margin-left: auto; flex: none; min-height: 44px;
  display: inline-flex; align-items: center; padding: 8px 4px;
  background: none; border: none; font-family: var(--ui-font);
  font-size: 0.88rem; color: var(--gold); cursor: pointer;
}
.health-because {
  margin-top: 10px; padding: 11px 13px; border-radius: 10px;
  background: rgba(215, 156, 173, .09); border: 1px solid rgba(215, 156, 173, .22);
  font-size: 0.82rem; color: var(--ink-dim); line-height: 1.6;
}
.health-because b { color: var(--ink); font-weight: 600; }
.health-undo {
  margin-top: 9px; min-height: 44px; display: inline-flex; align-items: center;
  padding: 8px 16px; border-radius: 999px; border: 1px solid var(--card-line);
  background: none; color: var(--ink-dim);
  font-family: var(--ui-font); font-size: 0.86rem; cursor: pointer;
}
.health-undo:hover, .health-undo:focus-visible {
  border-color: var(--gold); color: var(--gold); outline: none;
}
.health-locked {
  margin-top: 22px; padding: 13px 15px; border-radius: 12px;
  border: 1px dashed var(--card-line);
  font-size: 0.82rem; color: var(--ink-faint); line-height: 1.6;
}
.health-locked b { color: var(--ink-dim); font-weight: 600; }
/* The letter itself. Read at arm's length like a recipe is, and pre-wrapped:
   a transcript's own line breaks are part of what it says. */
.health-letter {
  white-space: pre-wrap; font-size: 1rem; line-height: 1.7; color: var(--ink-dim);
  background: var(--card-bg); border: 1px solid var(--card-line);
  border-radius: var(--surface-radius); padding: 16px;
}

/* BF-10b. The sky, the tile lift and the win pills all move; a household member
   who has asked the system to stop animating things gets a still app. Scoped to
   transitions and transforms, not to `all`, so nothing that relies on a
   transition to become visible ends up invisible. */
@media (prefers-reduced-motion: reduce) {
  .sky, .stars, .tile, .win { transition: none; }
  .tile:hover, .win:hover { transform: none; }
  /* BF-33 §2.4. The pill's expand is a width transition on two children; with this
     set they change width in one frame instead. The STATE still changes — the back
     segment still appears and still works — only the animation of getting there is
     dropped, which is what `reduce` asks for and not "no motion, no feature". */
  .nav-pill-back, .nav-pill-sep { transition: none; }
}

/* BF-43 §2.1. The row that is DIFFERENT, never the row that FAILED.
   ⚠️ NOT red, no --danger, no alarm icon (CLAUDE.md rule 10: no guilt mechanics,
   nothing that reads as a telling-off). It is the ordinary .doc-guess with a
   slightly recessed ground and the same weight of text, which says "this one is
   different" and stops there.
   ⚠️ min-width: 0 AND NO CLAMP AT ALL, and the harness is why. A 4-line clamp was
   written here first, on the assumption that these run to "two or three lines at
   375px". Measured, the too_large sentence runs to FIVE, and the clamp cut it off
   exactly at "...or split it into two documents" — removing the way out, which is
   the half that matters. Every one of these strings is bounded and server-authored
   and ENDS in what to do, so it wraps in full or the row is a dead end. */
.doc-guess.bad {
  background: var(--surface-2, rgba(127, 127, 127, 0.08));
  border-radius: 10px;
  padding: 8px 10px;
}
.doc-guess.bad .guess-q {
  min-width: 0;
}
