/* ==========================================================================
   CIBL Obsidian — shared theme layer (portal + staff console)
   Dark glass on a near-black ground, ember-orange accents.
   Tokens first; components below only consume tokens.

   This file is the single source of the token layer and every component
   used by more than one app. App-specific rules stay in the app's own
   stylesheet (apps/portal/static/portal/portal.css,
   apps/console/static/console/console.css) loaded AFTER this one.
   ========================================================================== */

/* THEME TOKENS. This first :root block is the dark theme and the complete
   list of everything a theme may change; the two light blocks right after
   it restate every one of these (apps/accounts/tests/test_theme.py holds
   the three sets equal). Type, motion and geometry live in the second
   :root block below and never change with the theme. */
:root {
  color-scheme: dark;

  /* ground + surfaces */
  --ground: #0B0A09;
  --ground-raise: #121110;
  --ground-hover: #171513;
  --terminal-top: #110F0D;
  /* the RGB triplets every translucency is mixed from: --overlay-rgb is the
     colour of glass, hairlines and inset highlights (white on obsidian, ink
     on paper); --ground-rgb tints scrims and backdrops; --shadow-rgb and
     --shadow-k (an alpha multiplier) shape every drop shadow. */
  --overlay-rgb: 255, 255, 255;
  --ground-rgb: 11, 10, 9;
  --shadow-rgb: 0, 0, 0;
  --shadow-k: 1;
  --glass: rgba(var(--overlay-rgb), 0.035);
  --glass-strong: rgba(var(--overlay-rgb), 0.06);
  --hairline: rgba(var(--overlay-rgb), 0.055);
  --hairline-strong: rgba(var(--overlay-rgb), 0.12);
  /* the atmospheric glow behind everything (body::before) */
  --ground-glow:
    radial-gradient(1100px 520px at 85% -10%, rgba(255, 107, 0, 0.11), transparent 62%),
    radial-gradient(900px 640px at -10% 110%, rgba(255, 163, 51, 0.075), transparent 58%);
  /* which glyph the theme toggle shows: the theme that IS, not the next one */
  --glyph-sun: none;
  --glyph-moon: block;
  /* fields: the raised translucent well every input sits in, and its lit
     (hover / focus) state */
  --field-grad: linear-gradient(180deg, rgba(var(--overlay-rgb), 0.055), rgba(var(--overlay-rgb), 0.03));
  --field-grad-hi: linear-gradient(180deg, rgba(var(--overlay-rgb), 0.07), rgba(var(--overlay-rgb), 0.04));
  --field-border: rgba(var(--overlay-rgb), 0.35);
  /* the select chevron is drawn inside a data URI, which cannot read a
     custom property — so the whole image is the token. Ember on obsidian;
     the deep ember on paper, where #FFA333 was all but invisible. */
  /* the date/time picker glyph is the browser's own black bitmap; the theme
     only tints it. Both chains MUST start with an invert: sepia/saturate/
     hue-rotate are linear matrices with no constant term and brightness is
     multiplicative, so pure black is a fixed point of everything else and a
     tint-only chain leaves the platform's black glyph untouched. */
  --picker-filter: invert(0.82) sepia(0.6) saturate(4) hue-rotate(-10deg);
  --select-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23FFA333' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  /* the films. --film-blend / --film-filter composite one: screened onto
     obsidian, luminance-inverted (hue kept) and multiplied into the paper.
     The login HERO always renders — deleting it left light with half an empty
     screen. --film-display is only the AMBIENT clips' switch: they sit behind
     a whole page of figures, and on paper even a composited plate costs the
     text over it more than the atmosphere is worth. */
  --film-display: block;
  --film-blend: screen;
  --film-filter: brightness(1.45) contrast(1.04) saturate(1.06);
  /* the frosted fill of every glass card and toast */
  --glass-grad: linear-gradient(180deg, rgba(var(--overlay-rgb), 0.055), rgba(var(--overlay-rgb), 0.025));
  /* the rail's own tint: a breath lighter than the ground on obsidian, none
     on paper (its sticky box would otherwise print as a column) */
  --rail-bg: rgba(var(--overlay-rgb), 0.015);

  /* ink */
  --ink: #F4EFE9;
  --ink-dim: rgba(244, 239, 233, 0.78);
  /* 0.52 alpha: >= 5:1 on --ground / --ground-raise / glass cards (WCAG AA
     for the 10-11px mono eyebrows, table headers and ccy codes it paints —
     small text needs 4.5:1; the old 0.38 measured 3.2:1). */
  --ink-faint: rgba(244, 239, 233, 0.64);

  /* ember accent; --on-ember is the ink that sits on an ember fill (buttons,
     counts, ticks) — dark in both themes because the fill keeps its flame */
  --on-ember: #14100A;
  --ember-hi: #FFA333;
  --ember-lo: #FF6B00;
  --ember-grad: linear-gradient(135deg, var(--ember-hi), var(--ember-lo));
  --ember-soft: rgba(255, 138, 26, 0.14);
  --ember-ring: rgba(255, 138, 26, 0.45);

  /* signals. Mint is the one green in the product: it means settled, OK,
     complete. --ok is an alias so every older rule reads the same green. */
  --mint: #3ECF8E;
  --mint-soft: rgba(62, 207, 142, 0.14);
  --mint-ring: rgba(62, 207, 142, 0.45);
  --ok: var(--mint);
  --ok-soft: var(--mint-soft);
  --bad: #FF6B6B;
  --bad-soft: rgba(255, 107, 107, 0.12);
  --bad-ring: rgba(255, 107, 107, 0.4);

  /* the 1px gradient edge every glass card wears: ember at the corners,
     a faint white highlight along the top run */
  --edge-grad: linear-gradient(135deg,
    rgba(255, 163, 51, 0.55) 0%, rgba(var(--overlay-rgb), 0.14) 30%,
    rgba(var(--overlay-rgb), 0.05) 55%, rgba(255, 107, 0, 0.4) 100%);

}

/* ---- light: the same bank in daylight -----------------------------------
   Warm paper instead of obsidian. Ink text, ember darkened to clear WCAG AA
   on paper (the gradient FILL keeps its full flame — dark ink sits on it),
   glass turned into frosted white over a soft warm shadow, hairlines and
   inset highlights mixed from ink instead of white. The set is stated for
   the explicit toggle (html[data-theme="light"]) and again for the system
   preference unless the toggle forced dark; a media query cannot share a
   selector list, so the test holds the two blocks identical. */
:root[data-theme="light"] {
  color-scheme: light;
  --ground: #FAF7F2;
  --ground-raise: #FFFFFF;
  --ground-hover: #F3EEE6;
  --terminal-top: #FFFFFF;
  --overlay-rgb: 26, 21, 17;
  --ground-rgb: 250, 247, 242;
  --shadow-rgb: 74, 48, 20;
  --shadow-k: 0.32;
  --glass: rgba(255, 255, 255, 0.64);
  --glass-strong: rgba(255, 255, 255, 0.88);
  --hairline: rgba(26, 21, 17, 0.09);
  --hairline-strong: rgba(26, 21, 17, 0.18);
  --ground-glow:
    radial-gradient(1100px 520px at 85% -10%, rgba(255, 107, 0, 0.10), transparent 62%),
    radial-gradient(900px 640px at -10% 110%, rgba(255, 163, 51, 0.09), transparent 58%);
  --glyph-sun: block;
  --glyph-moon: none;
  --field-grad: linear-gradient(180deg, #FFFFFF, #FDFBF8);
  --field-grad-hi: linear-gradient(180deg, #FFFFFF, #FFFFFF);
  --field-border: rgba(var(--overlay-rgb), 0.5);
  --select-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23AD4507' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  --picker-filter: invert(31%) sepia(96%) saturate(1400%) hue-rotate(2deg) brightness(0.92);
  --film-display: none;
  --film-blend: multiply;
  --film-filter: invert(1) hue-rotate(180deg) saturate(1.1) brightness(1.05);
  --glass-grad: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.62));
  --rail-bg: transparent;
  --ink: #1A1511;
  --ink-dim: rgba(26, 21, 17, 0.82);
  --ink-faint: rgba(26, 21, 17, 0.70);
  --ember-hi: #AD4507;
  --ember-lo: #E85D00;
  --on-ember: #14100A;
  --ember-grad: linear-gradient(135deg, #FFA333, #FF6B00);
  --ember-soft: rgba(255, 138, 26, 0.12);
  --ember-ring: rgba(173, 69, 7, 0.55);
  --mint: #0D6E47;
  --mint-soft: rgba(13, 110, 71, 0.09);
  --mint-ring: rgba(13, 110, 71, 0.45);
  --ok: var(--mint);
  --ok-soft: var(--mint-soft);
  --bad: #C4302B;
  --bad-soft: rgba(196, 48, 43, 0.08);
  --bad-ring: rgba(196, 48, 43, 0.5);
  --edge-grad: linear-gradient(135deg,
    rgba(173, 69, 7, 0.45) 0%, rgba(26, 21, 17, 0.14) 30%,
    rgba(26, 21, 17, 0.06) 55%, rgba(232, 93, 0, 0.35) 100%);
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;
    --ground: #FAF7F2;
    --ground-raise: #FFFFFF;
    --ground-hover: #F3EEE6;
    --terminal-top: #FFFFFF;
    --overlay-rgb: 26, 21, 17;
    --ground-rgb: 250, 247, 242;
    --shadow-rgb: 74, 48, 20;
    --shadow-k: 0.32;
    --glass: rgba(255, 255, 255, 0.64);
    --glass-strong: rgba(255, 255, 255, 0.88);
    --hairline: rgba(26, 21, 17, 0.09);
    --hairline-strong: rgba(26, 21, 17, 0.18);
    --ground-glow:
      radial-gradient(1100px 520px at 85% -10%, rgba(255, 107, 0, 0.10), transparent 62%),
      radial-gradient(900px 640px at -10% 110%, rgba(255, 163, 51, 0.09), transparent 58%);
    --glyph-sun: block;
    --glyph-moon: none;
    --field-grad: linear-gradient(180deg, #FFFFFF, #FDFBF8);
    --field-grad-hi: linear-gradient(180deg, #FFFFFF, #FFFFFF);
    --field-border: rgba(var(--overlay-rgb), 0.5);
    --select-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23AD4507' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
    --picker-filter: invert(31%) sepia(96%) saturate(1400%) hue-rotate(2deg) brightness(0.92);
    --film-display: none;
    --film-blend: multiply;
    --film-filter: invert(1) hue-rotate(180deg) saturate(1.1) brightness(1.05);
    --glass-grad: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.62));
    --rail-bg: transparent;
    --ink: #1A1511;
    --ink-dim: rgba(26, 21, 17, 0.82);
    --ink-faint: rgba(26, 21, 17, 0.70);
    --ember-hi: #AD4507;
    --ember-lo: #E85D00;
    --on-ember: #14100A;
    --ember-grad: linear-gradient(135deg, #FFA333, #FF6B00);
    --ember-soft: rgba(255, 138, 26, 0.12);
    --ember-ring: rgba(173, 69, 7, 0.55);
    --mint: #0D6E47;
    --mint-soft: rgba(13, 110, 71, 0.09);
    --mint-ring: rgba(13, 110, 71, 0.45);
    --ok: var(--mint);
    --ok-soft: var(--mint-soft);
    --bad: #C4302B;
    --bad-soft: rgba(196, 48, 43, 0.08);
    --bad-ring: rgba(196, 48, 43, 0.5);
    --edge-grad: linear-gradient(135deg,
      rgba(173, 69, 7, 0.45) 0%, rgba(26, 21, 17, 0.14) 30%,
      rgba(26, 21, 17, 0.06) 55%, rgba(232, 93, 0, 0.35) 100%);
  }
}

/* invariants: type, motion, geometry — the same in every theme */
:root {
  /* motion curves (obsidian-motion.css). ease-out is the decisive settle
     every entrance uses; spring only for pops (ticks, dots, controls). */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* type */
  --font-display: "Manrope", system-ui, -apple-system, sans-serif;
  --font-body: "Manrope", system-ui, -apple-system, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* geometry */
  --radius: 14px;
  --radius-md: 12px;
  --radius-sm: 9px;
  --rail-w: 236px;
  --gap: 20px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* the atmospheric glow: a fixed layer behind everything, so a long page
   never scrolls it away into flat black. A pseudo-element rather than
   background-attachment: fixed, which Chrome re-rasters on every scroll
   frame; this is composited once. Negative z-index sits it under the
   in-flow content but over the canvas ground. */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: var(--ground-glow);
}

a { color: var(--ember-hi); text-decoration: none; }
a:hover { text-decoration: underline; }

/* a11y: one ember focus ring everywhere. The SOLID accent, not --ember-ring:
   a 45%/55%-alpha outline measures 2.3-2.5:1 on its own surface in both themes,
   under the 3:1 a focus indicator needs (WCAG 2.2 SC 1.4.11 / 2.4.11). */
:focus-visible {
  outline: 2px solid var(--ember-hi);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ---- layout: fixed rail + content ------------------------------------- */

.shell { display: flex; min-height: 100vh; }

.rail {
  width: var(--rail-w);
  flex: 0 0 var(--rail-w);
  border-right: 1px solid var(--hairline);
  background: var(--rail-bg);
  padding: 22px 14px;
  display: flex;
  flex-direction: column;
  gap: 26px;
}

.brand { display: flex; align-items: center; gap: 10px; padding: 0 8px; }
.brand-mark {
  width: 30px; height: 30px; border-radius: 8px;
  background: var(--ember-grad);
  box-shadow: 0 0 18px rgba(255, 107, 0, 0.35);
  display: grid; place-items: center;
  color: var(--on-ember); font-family: var(--font-body); font-weight: 800; font-size: 15px;
}
.brand-name {
  font-family: var(--font-body);
  font-weight: 700; font-size: 16px; letter-spacing: 0.01em;
}
/* the wordmark's descriptor ("CLIENT PORTAL", the bank's own name) is a LABEL,
   not a reference: SIMPLE doctrine keeps the body face here, with the tracking */
.brand-sub {
  font-family: var(--font-body); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-faint);
}

.nav { display: flex; flex-direction: column; gap: 4px; }
.nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: var(--radius-sm);
  color: var(--ink-dim); font-size: 14px; font-weight: 500;
  border: 1px solid transparent;
}
.nav a:hover { color: var(--ink); background: var(--glass); text-decoration: none; }
.nav a.active {
  color: var(--ink);
  background: var(--ember-soft);
  border-color: rgba(255, 138, 26, 0.25);
}
.nav a.active .nav-dot { background: var(--ember-grad); }
.nav-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--hairline-strong); flex: 0 0 6px;
}

.rail-foot { margin-top: auto; padding: 0 8px; }
.rail-user {
  font-family: var(--font-mono); font-size: 11px;
  color: var(--ink-faint); margin-bottom: 8px; word-break: break-all;
}
/* the theme toggle (templates/obsidian/_theme_toggle.html): a glass pill
   with the current theme's glyph and a three-letter state. aria-pressed
   is true while a choice overrides the system. */
.theme-toggle {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 32px; padding: 5px 10px 5px 8px; margin-bottom: 10px;
  background: var(--glass); color: var(--ink-dim);
  border: 1px solid var(--hairline-strong); border-radius: 999px;
  font: inherit; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  cursor: pointer;
  transition: border-color 180ms var(--ease-out), color 180ms var(--ease-out), background-color 180ms var(--ease-out);
}
.theme-toggle:hover { color: var(--ink); border-color: var(--ember-ring); background: var(--glass-strong); }
.theme-toggle[aria-pressed="true"] { color: var(--ember-hi); border-color: var(--ember-ring); }
.theme-toggle:focus-visible { outline: 2px solid var(--ember-hi); outline-offset: 2px; }
.theme-glyph { width: 16px; height: 16px; flex: none; }
.theme-sun { display: var(--glyph-sun); }
.theme-moon { display: var(--glyph-moon); }
.login-panel .theme-toggle { position: absolute; top: 20px; right: 20px; z-index: 2; margin: 0; }
.tabbar-sheet .theme-toggle { margin: 6px 0 0; }

.btn-ghost {
  display: inline-block; width: 100%;
  padding: 8px 12px; border-radius: var(--radius-sm);
  background: transparent; border: 1px solid var(--hairline-strong);
  color: var(--ink-dim); font: inherit; font-size: 13px; cursor: pointer;
  text-align: center;
}
.btn-ghost:hover { color: var(--ink); border-color: var(--ember-ring); }

.main { flex: 1; min-width: 0; padding: 34px 40px 60px; }
.main-inner { max-width: 1040px; margin: 0 auto; }

/* ---- page chrome -------------------------------------------------------- */

.page-head { margin-bottom: 26px; }
.kicker {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ember-hi); margin: 0 0 6px;
}
h1.page-title {
  font-family: var(--font-body);
  /* The name of the company, or of the desk, is the largest thing on the page:
     one confident line at a real display size, in a medium weight rather than
     bold, tracked in slightly and set at full ink.  Scale and restraint are
     what read as expensive here — not a second typeface. */
  font-size: clamp(30px, 2.6vw, 38px); font-weight: 500; letter-spacing: -0.02em;
  color: var(--ink);
  margin: 0;
}
.page-sub { color: var(--ink-dim); margin: 6px 0 0; font-size: 14px; }

/* k-label: mono uppercase eyebrow used above facts/values everywhere. */
.k-label {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-faint);
}

/* note: explanatory copy — a sentence, not a label.
   .k-label is an eyebrow of two or three words. Set a whole sentence in it
   and you get 10px uppercase mono at 0.14em tracking, which is genuinely
   hard to read: the identity step's privacy promise was 34 words of it, and
   that promise is the one thing on the page the applicant should be able to
   read without effort. Prose uses this. */
.note {
  font-family: var(--font-body); font-size: 12.5px; line-height: 1.55;
  letter-spacing: 0; text-transform: none; color: var(--ink-dim);
}

/* ---- glass cards -------------------------------------------------------- */

.card {
  background: var(--glass);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 20px 22px;
  backdrop-filter: blur(10px);
  transition: border-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
}
/* interactive cards lift 2px with a warming border */
a.card:hover, .card-lift:hover {
  transform: translateY(-2px);
  border-color: rgba(255, 163, 51, 0.28);
  box-shadow: 0 12px 30px rgba(var(--shadow-rgb), calc(0.35 * var(--shadow-k)));
}
.card + .card, .section { margin-top: var(--gap); }
/* a grid spaces its own cards with gap; the sibling rule above must not drop the second one */
.grid > .card + .card { margin-top: 0; }
.card-title {
  font-family: var(--font-body);
  font-size: 15px; font-weight: 600; margin: 0 0 14px;
}

.grid { display: grid; gap: var(--gap); }
/* the cards stretch to the row height: a currency with nothing committed has a
   shorter body, and `align-items: start` tore a hole in the row instead */
.grid-totals { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

.total-card { position: relative; overflow: hidden; }
.total-card::after {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: var(--ember-grad); opacity: 0.85;
}

/* the composition under the headline: what is committed, and the sum. The
   headline figure is what the client can move; these two say why it is not
   the whole balance. */
.total-split {
  margin: 16px 0 0; padding-top: 13px;
  border-top: 1px solid var(--hairline);
  display: grid; gap: 7px;
}
.total-split-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
}
.total-split dt {
  font-family: var(--font-mono); font-size: 10px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint);
}
.total-split dd { margin: 0; font-size: 13.5px; color: var(--ink-dim); }
.total-split-sum dt { color: var(--ink-dim); }
.total-split-sum dd { color: var(--ink); font-weight: 600; }
/* On a phone the label and the figure do not fit on one line; left as a
   flex row the figure wraps under its label but keeps the row's gap as a
   stray indent. Stack them deliberately instead. */
@media (max-width: 560px) {
  .total-split-row { flex-direction: column; align-items: flex-start; gap: 2px; }
}

/* ---- money -------------------------------------------------------------- */

.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* A figure is the most valuable thing on a banking page, so it is set in the
   interface face at a confident weight with the digits locked to one width —
   not in the mono face, which made every balance look like a log line. */
.money {
  font-family: var(--font-body);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  letter-spacing: -0.01em;
}
.money-lg { font-size: 30px; font-weight: 600; letter-spacing: -0.02em; }
.money-md { font-size: 19px; font-weight: 600; letter-spacing: -0.015em; }
.ccy {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em;
  color: var(--ink-faint);
}
.amount-in { color: var(--ok); }
.amount-out { color: var(--ink); }

/* ---- tables ------------------------------------------------------------- */

/* At 375 a ledger's last columns (ACCOUNT, AMOUNT) sit off the right edge and
   nothing says so — the row looks like it simply ends. Fade the last 28px so
   it reads as cut off, and thin the scrollbar so the track is not a grey slab
   across the card (macOS hides overlay scrollbars until you scroll, so the
   fade, not the bar, is the hint).

   The fade has to disappear once the table is scrolled to its end, or it dims
   the very column it was advertising, and it must never appear on a table that
   already fits. Both fall out of driving it from the scroller's own position:
   scroll() is INACTIVE when there is nothing to scroll, so --table-fade keeps
   its 0px initial value and the mask is a no-op. Browsers with no scroll-driven
   animations land on that same 0px — no fade, exactly as before. */
@property --table-fade {
  syntax: "<length>"; inherits: false; initial-value: 0px;
}
.table-wrap {
  overflow-x: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--hairline-strong) transparent;
  mask-image: linear-gradient(90deg, #000 calc(100% - var(--table-fade)), transparent);
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - var(--table-fade)), transparent);
  /* the shorthand resets animation-timeline, so the timeline comes after it */
  animation: table-edge-fade linear both;
  animation-timeline: scroll(self inline);
  animation-range: 86% 100%;
}
@keyframes table-edge-fade {
  from { --table-fade: 28px; }
  to { --table-fade: 0px; }
}
table.ledger { width: 100%; border-collapse: collapse; font-size: 13.5px; }
table.ledger th {
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint);
  text-align: left; padding: 8px 12px;
  border-bottom: 1px solid var(--hairline-strong);
}
table.ledger td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--hairline);
  vertical-align: top;
}
table.ledger tr:last-child td { border-bottom: none; }
table.ledger td.num, table.ledger th.num { text-align: right; }
table.ledger tbody tr { transition: background-color 120ms ease; }
table.ledger tbody tr:hover { background: rgba(255, 163, 51, 0.035); }
table.ledger tbody tr:hover:has(td.empty-cell) { background: transparent; }
/* a reference is one token: 'TD-' on one line and '0005' on the next reads
   as two tags (tables scroll inside .table-wrap, so nothing widens the page) */
.ref, .nowrap { white-space: nowrap; }
.ref { font-family: var(--font-mono); font-size: 12px; color: var(--ink-dim); }
.deal-ref {
  font-family: var(--font-mono); font-size: 11px;
  color: var(--ember-hi); background: var(--ember-soft);
  border-radius: 5px; padding: 1px 6px; white-space: nowrap;
}

/* ---- state chips --------------------------------------------------------- */

.chip {
  display: inline-block;
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px;
  background: var(--glass-strong);
  border: 1px solid var(--hairline-strong);
  color: var(--ink-dim);
  white-space: nowrap;
}
.chip-ok { background: var(--ok-soft); border-color: var(--mint-ring); color: var(--ok); }
.chip-bad { background: var(--bad-soft); border-color: var(--bad-ring); color: var(--bad); }
.chip-ember { background: var(--ember-soft); border-color: var(--ember-ring); color: var(--ember-hi); }

/* ---- buttons (actions) --------------------------------------------------- */

.btn-ember {
  width: 100%; padding: 11px 16px; margin-top: 6px;
  border: none; border-radius: var(--radius-sm);
  background: var(--ember-grad); color: var(--on-ember);
  font: inherit; font-weight: 700; cursor: pointer;
  transition: filter 150ms ease, box-shadow 200ms ease, transform 90ms ease;
}
.btn-ember:hover {
  filter: brightness(1.06);
  box-shadow: 0 6px 22px rgba(255, 107, 0, 0.32);
}
.btn-ember:active { transform: translateY(1px); box-shadow: 0 2px 10px rgba(255, 107, 0, 0.25); }

/* .btn-ember on its own is the full-width primary at the foot of a stacked
   form. Written as `.btn .btn-ember` the author is asking for an inline
   primary sitting in a row of other controls, so it must size to its label
   like every other .btn: its width:100% was eating whole rows — on the
   marketplace board it squeezed the message field to 26px beside it. */
.btn.btn-ember { width: auto; margin-top: 0; }

/* A row of actions. This was console-only for a while, but four client-portal
   templates use it and never load console.css — so "Negotiate / Accept —
   create deal / Decline" on an incoming match request fell back to display:
   block and stacked, with the two <form> children touching. Shared layer. */
.btn-row { display: flex; gap: 6px; flex-wrap: wrap; }

/* compact inline action buttons (console rows, htmx actions) */
.btn {
  display: inline-block;
  padding: 6px 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--hairline-strong);
  background: var(--glass-strong);
  color: var(--ink); font: inherit; font-size: 13px; font-weight: 500;
  cursor: pointer; white-space: nowrap;
}
.btn { transition: border-color 150ms ease, box-shadow 200ms ease, filter 150ms ease, transform 90ms ease; }
.btn:hover { border-color: var(--ember-ring); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn-primary { background: var(--ember-grad); border-color: transparent; color: var(--on-ember); font-weight: 700; }
.btn-primary:hover {
  filter: brightness(1.06);
  box-shadow: 0 5px 18px rgba(255, 107, 0, 0.3);
}
.btn-danger { color: var(--bad); border-color: var(--bad-ring); background: var(--bad-soft); }
/* Disabled. Opacity alone was enough on obsidian — a faded button sank into
   the ground. On paper it fades TOWARDS the ground, so a disabled ember
   primary kept a live-looking orange fill while its dark label washed out to
   an unreadable grey.  grayscale() fixed paper and broke obsidian: greyscaling
   an ember gradient on a dark ground produces a LIGHT fill, so the one control
   the operator may NOT press became the brightest, most button-like thing in
   the row, with its own label at about 3.5:1 (OPS-06).  Take the fill away
   instead of draining its colour — a flat glass fill and dim ink read as dead
   on either ground, and neither theme needs a filter. */
.btn:disabled, .btn[disabled], .btn-ember:disabled, .btn-ember[disabled] {
  opacity: 0.45; cursor: not-allowed;
  background: var(--glass); background-image: none;
  color: var(--ink-dim); border-color: var(--hairline);
  transform: none; box-shadow: none;
}

/* ---- form fields ---------------------------------------------------------- */

.field { margin-bottom: 16px; }
.field label {
  display: block; margin-bottom: 6px;
  font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-dim);
}
/* ---- form controls: ONE look for every input, select and textarea -------
   Applies site-wide (portal, console, wizard, marketplace) so no page has
   to restyle a control. Glass field, inner top highlight, ember focus
   ring, custom chevron on selects, dark option lists, visible date and
   number affordances, ghost file buttons. Page stylesheets may still add
   layout (width, alignment) — they should not repaint these. */

:where(.field) :where(input, select, textarea),
:where(form, .card, .glass-card, .panel, .stack) :where(
  input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):not([type="submit"]):not([type="button"]):not([type="hidden"]),
  select, textarea
) {
  width: 100%; min-height: 44px; padding: 10px 14px;
  background: var(--field-grad);
  border: 1px solid var(--field-border);
  border-radius: 10px;
  color: var(--ink); font: inherit; line-height: 1.35;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), inset 0 1px 2px rgba(var(--shadow-rgb), calc(0.25 * var(--shadow-k)));
  transition: border-color 200ms var(--ease-out), box-shadow 200ms var(--ease-out), background-color 200ms var(--ease-out);
  -webkit-appearance: none; appearance: none;
}
:where(.field) :where(input, select, textarea)::placeholder,
:where(form, .card, .glass-card, .panel, .stack) :where(input, textarea)::placeholder { color: var(--ink-faint); }
:where(.field) :where(input, select, textarea):hover,
:where(form, .card, .glass-card, .panel, .stack) :where(input, select, textarea):hover { border-color: rgba(var(--overlay-rgb), 0.2); }
:where(.field) :where(input, select, textarea):focus,
:where(form, .card, .glass-card, .panel, .stack) :where(input, select, textarea):focus,
:where(.field) :where(input, select, textarea):focus-visible,
:where(form, .card, .glass-card, .panel, .stack) :where(input, select, textarea):focus-visible {
  outline: none; border-color: var(--ember-hi);
  background: var(--field-grad-hi);
  box-shadow: 0 0 0 3px var(--ember-soft), inset 0 1px 0 rgba(var(--overlay-rgb), 0.06);
}
/* select: our own chevron, ember, never the platform's */
:where(.field, form, .card, .glass-card, .panel, .stack) select {
  padding-right: 42px; cursor: pointer;
  background-image: var(--select-chevron),
    var(--field-grad);
  background-repeat: no-repeat, no-repeat;
  background-position: right 14px center, 0 0;
  background-size: 14px 14px, 100% 100%;
}
:where(.field, form, .card, .glass-card, .panel, .stack) select:focus {
  /* The shared focus rule above uses the `background` SHORTHAND, which resets
     repeat, size and position to their defaults.  Restating only the image left
     the chevron tiling across the whole field at its natural size the moment a
     select took focus.  These three lines put the layer geometry back. */
  background-image: var(--select-chevron),
    var(--field-grad-hi);
  background-repeat: no-repeat, no-repeat;
  background-position: right 14px center, 0 0;
  background-size: 14px 14px, 100% 100%;
}
:where(.field, form, .card, .glass-card, .panel, .stack) select[multiple] { background-image: none; padding-right: 14px; min-height: 120px; }
select option, select optgroup { background: var(--ground-raise); color: var(--ink); }
/* textarea */
:where(.field, form, .card, .glass-card, .panel, .stack) textarea { min-height: 104px; resize: vertical; line-height: 1.5; }
/* date / time pickers: the native icon, but legible on obsidian */
:where(.field, form, .card, .glass-card, .panel, .stack) :where(input[type="date"], input[type="datetime-local"], input[type="time"], input[type="month"]) { font-family: var(--font-mono); font-size: 14px; letter-spacing: 0.01em; }
input::-webkit-calendar-picker-indicator { filter: var(--picker-filter); opacity: 0.75; cursor: pointer; }
input::-webkit-calendar-picker-indicator:hover { opacity: 1; }
/* numbers: mono, tabular, no spinner clutter */
:where(.field, form, .card, .glass-card, .panel, .stack) input[type="number"] { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
/* search: no platform cancel glyph */
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
/* checkbox / radio: native, tinted ember, sized for a thumb */
input[type="checkbox"], input[type="radio"] { width: 18px; height: 18px; accent-color: var(--ember-hi); cursor: pointer; vertical-align: -3px; }
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible { outline: 2px solid var(--ember-hi); outline-offset: 2px; border-radius: 4px; }
/* file: a ghost button, not a grey platform widget */
input[type="file"] { color: var(--ink-dim); font: inherit; font-size: 13px; }
input[type="file"]::file-selector-button {
  margin-right: 12px; padding: 8px 14px; cursor: pointer;
  background: var(--glass-strong); color: var(--ink);
  border: 1px solid var(--hairline-strong); border-radius: 8px; font: inherit; font-size: 13px;
  transition: border-color 200ms var(--ease-out), background-color 200ms var(--ease-out);
}
input[type="file"]::file-selector-button:hover { border-color: var(--ember-ring); background: var(--ember-soft); }
/* disabled, read-only, invalid */
:where(.field, form, .card, .glass-card, .panel, .stack) :where(input, select, textarea):disabled { opacity: 0.5; cursor: not-allowed; }
:where(.field, form, .card, .glass-card, .panel, .stack) :where(input, textarea)[readonly] { background: rgba(var(--overlay-rgb), 0.02); color: var(--ink-dim); }
:where(.field, form, .card, .glass-card, .panel, .stack) :where(input, select, textarea):user-invalid,
:where(.field, form, .card, .glass-card, .panel, .stack) :where(input, select, textarea)[aria-invalid="true"],
.field-invalid :where(input, select, textarea), .has-error :where(input, select, textarea) {
  border-color: var(--bad); box-shadow: 0 0 0 3px var(--bad-soft);
}
/* browser autofill: keep the obsidian field, not a yellow one */
input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--ground-raise) inset; -webkit-text-fill-color: var(--ink); caret-color: var(--ink);
  border-color: var(--hairline-strong); transition: background-color 9999s ease-out 0s;
}
/* helper text under a field, and a two-up row of fields */
.field-hint, .field .hint { display: block; margin-top: 6px; font-size: 12.5px; line-height: 1.45; color: var(--ink-faint); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 640px) { .field-row { grid-template-columns: 1fr; } }
/* the mono eyebrow label everywhere, not only under .field */
:where(form) label:not(.chip):not(.btn):not(.toggle):not(.dropzone) { display: block; margin-bottom: 6px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-dim); }
label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) { display: inline-flex; align-items: center; gap: 10px; text-transform: none; letter-spacing: 0; font-family: var(--font-body); font-size: 14px; color: var(--ink); }
.form-error {
  background: var(--bad-soft);
  border: 1px solid var(--bad-ring);
  color: var(--bad);
  border-radius: var(--radius-sm);
  padding: 10px 12px; font-size: 13px; margin-bottom: 16px;
}

/* ---- toasts (Django messages, htmx-refreshed) ----------------------------- */

.toasts {
  position: fixed; right: 20px; bottom: 20px; z-index: 100;
  display: flex; flex-direction: column; gap: 8px;
  max-width: 380px;
}
.toast {
  background: var(--ground-raise);
  border: 1px solid var(--hairline-strong);
  border-left: 3px solid var(--ember-hi);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  font-size: 13px; color: var(--ink);
  box-shadow: 0 10px 30px rgba(var(--shadow-rgb), calc(0.55 * var(--shadow-k)));
}
.toast-error { border-left-color: var(--bad); }
.toast-success { border-left-color: var(--ok); }
.toast-warning { border-left-color: var(--ember-hi); }
@media (prefers-reduced-motion: no-preference) {
  .toast { animation: toast-in 240ms cubic-bezier(0.22, 1, 0.36, 1) both; }
  .toast.toast-hide { animation: toast-out 320ms ease both; }
}
.toast.toast-hide { opacity: 0; }
@keyframes toast-in {
  from { opacity: 0; transform: translateX(18px); }
  to { opacity: 1; transform: none; }
}
@keyframes toast-out {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateY(8px); }
}

/* ---- meter bars (coverage, match strength) -------------------------------- */

.bar {
  height: 6px; border-radius: 999px; overflow: hidden;
  background: var(--glass-strong);
  border: 1px solid var(--hairline);
}
.bar > span {
  display: block; height: 100%;
  background: var(--ember-grad);
}
.bar-ok > span { background: var(--ok); }
.bar-bad > span { background: var(--bad); }

/* ---- empty states / placeholders ---------------------------------------- */

.empty {
  text-align: center; padding: 56px 20px;
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--radius);
  color: var(--ink-dim);
}
.empty h2 { font-family: var(--font-body); color: var(--ink); margin: 0 0 8px; }

/* faint CSS glyph that heads every designed empty state */
.empty-glyph {
  display: block; width: 40px; height: 40px; margin: 0 auto 14px;
  border: 1px dashed var(--hairline-strong); border-radius: 50%;
  position: relative;
}
.empty-glyph::after {
  content: ""; position: absolute; inset: 11px; border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, rgba(255, 163, 51, 0.35), rgba(255, 107, 0, 0.08));
}
.empty > .empty-glyph { width: 48px; height: 48px; }

/* designed empty state inside a table (single full-width cell) */
table.ledger td.empty-cell {
  text-align: center; padding: 34px 16px 30px;
  color: var(--ink-faint); font-size: 13px;
}
td.empty-cell .empty-glyph { width: 32px; height: 32px; margin-bottom: 10px; }
td.empty-cell .empty-glyph::after { inset: 9px; }
td.empty-cell a { font-weight: 500; }

/* ---- login -------------------------------------------------------------- */

.login-body { display: grid; place-items: center; min-height: 100vh; padding: 24px; }

/* layered background: static gradient ground -> slow ember drift (CSS
   fallback) -> optional muted looping video, all behind the glass card. */
.login-bg { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.login-ember-drift {
  position: absolute; inset: -25%;
  background:
    radial-gradient(700px 480px at 78% 18%, rgba(255, 107, 0, 0.13), transparent 62%),
    radial-gradient(620px 520px at 16% 82%, rgba(255, 163, 51, 0.09), transparent 58%),
    radial-gradient(420px 380px at 55% 55%, rgba(255, 138, 26, 0.05), transparent 65%);
}
.login-video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0.55;
}
/* dark veil so the glass card always reads, video or not */
.login-bg::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(1200px 800px at 50% 45%, transparent, rgba(var(--ground-rgb), 0.72));
}
@media (prefers-reduced-motion: no-preference) {
  .login-ember-drift { animation: login-drift 28s ease-in-out infinite alternate; }
}
@media (prefers-reduced-motion: reduce) {
  /* reduced motion: the static gradient only */
  .login-video { display: none; }
}
@keyframes login-drift {
  from { transform: translate3d(-3%, -2%, 0) scale(1); }
  50% { transform: translate3d(2%, 3%, 0) scale(1.06); }
  to { transform: translate3d(4%, -3%, 0) scale(1.02); }
}

.login-card {
  position: relative; z-index: 1;
  width: 100%; max-width: 400px;
  background: var(--glass);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 34px 32px 30px;
  backdrop-filter: blur(12px);
  box-shadow: 0 30px 80px rgba(var(--shadow-rgb), calc(0.5 * var(--shadow-k)));
}
.login-card .brand { padding: 0; margin-bottom: 22px; }
.login-title { font-family: var(--font-body); font-size: 22px; margin: 0 0 4px; }
.login-sub { color: var(--ink-dim); font-size: 13px; margin: 0 0 22px; }

/* ---- motion: orchestrated entrance, staggers, live glows ------------------
   One entrance per page load — main-inner children rise in sequence.
   Everything in this layer runs ONLY when the user allows motion.        */

@keyframes rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
  .main-inner > * { animation: rise-in 340ms cubic-bezier(0.22, 1, 0.36, 1) both; }
  .main-inner > *:nth-child(2) { animation-delay: 70ms; }
  .main-inner > *:nth-child(3) { animation-delay: 140ms; }
  .main-inner > *:nth-child(4) { animation-delay: 210ms; }
  .main-inner > *:nth-child(5) { animation-delay: 280ms; }
  .main-inner > *:nth-child(n+6) { animation-delay: 340ms; }

  /* staggered grid cards inside an entering section */
  .grid-stagger > * { animation: rise-in 320ms cubic-bezier(0.22, 1, 0.36, 1) both; }
  .grid-stagger > *:nth-child(2) { animation-delay: 60ms; }
  .grid-stagger > *:nth-child(3) { animation-delay: 120ms; }
  .grid-stagger > *:nth-child(n+4) { animation-delay: 180ms; }

  /* staggered table rows (deals list, accounts) */
  .rows-stagger tbody tr { animation: rise-in 280ms ease-out both; }
  .rows-stagger tbody tr:nth-child(1) { animation-delay: 40ms; }
  .rows-stagger tbody tr:nth-child(2) { animation-delay: 80ms; }
  .rows-stagger tbody tr:nth-child(3) { animation-delay: 120ms; }
  .rows-stagger tbody tr:nth-child(4) { animation-delay: 160ms; }
  .rows-stagger tbody tr:nth-child(5) { animation-delay: 200ms; }
  .rows-stagger tbody tr:nth-child(6) { animation-delay: 240ms; }
  .rows-stagger tbody tr:nth-child(n+7) { animation-delay: 280ms; }

  /* healthy live derived figures breathe a quiet green */
  .pulse-ok { animation: breathe-ok 3.4s ease-in-out infinite; }
  /* value held live in escrow breathes ember */
  .pulse-ember { animation: breathe-ember 3.4s ease-in-out infinite; }

  /* htmx swap: outgoing content fades, incoming settles in */
  .htmx-swapping { opacity: 0; transform: translateY(4px); }
  .htmx-added { animation: rise-in 240ms ease-out both; }
}
.htmx-swapping { transition: opacity 120ms ease, transform 120ms ease; }

@keyframes breathe-ok {
  0%, 100% { text-shadow: 0 0 10px rgba(85, 201, 138, 0.12); }
  50% { text-shadow: 0 0 22px rgba(85, 201, 138, 0.4); }
}
@keyframes breathe-ember {
  0%, 100% { text-shadow: 0 0 10px rgba(255, 138, 26, 0.12); }
  50% { text-shadow: 0 0 22px rgba(255, 138, 26, 0.38); }
}

/* quiet settled seal (deal that has reached RELEASED / SETTLED) */
.seal-settled {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: 10px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ok);
  border: 1px solid var(--mint-ring);
  background: var(--ok-soft);
  border-radius: 999px; padding: 3px 10px;
}
.seal-settled::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ok); box-shadow: 0 0 8px rgba(85, 201, 138, 0.6);
}

/* ---- responsive --------------------------------------------------------- */
/* Contract: 1280 is the reference composition; >=1600 the same composition
   scales up centered; ~1080 tightens gutters; <=860 the rail collapses to a
   top bar; <=780 single column; <=480 thumb-sized targets. Tables always
   scroll inside .table-wrap — the page itself never scrolls horizontally. */

@media (min-width: 1600px) {
  .main-inner { max-width: 1440px; }
}

@media (max-width: 1080px) {
  .main { padding: 28px 26px 54px; }
  :root { --gap: 16px; }
}

@media (max-width: 860px) {
  .shell { flex-direction: column; }
  .rail {
    width: auto; flex: none;
    flex-direction: row; align-items: center;
    border-right: none; border-bottom: 1px solid var(--hairline);
    padding: 12px 16px; gap: 14px;
  }
  /* one scrolling row of links under the wordmark, fading at the right edge
     so the overflow reads as more, not as an end. The portal only shows this
     between 781 and 860px (the tab bar takes over below); the console opts
     back into a WRAPPING list in console-polish.css — a founder decision
     pinned by apps/console/tests/test_polish_markers.py: all twelve sections
     stay visible and tappable on a phone, at the cost of the rows they take. */
  .nav {
    flex-direction: row; flex-wrap: nowrap; overflow-x: auto; flex: 1 1 auto; min-width: 0;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
  }
  .nav::-webkit-scrollbar { display: none; }
  .nav a { flex: 0 0 auto; white-space: nowrap; }
  .rail-foot { margin-top: 0; margin-left: auto; }
  .rail-user { display: none; }
  .main { padding: 22px 16px 48px; }
}

@media (max-width: 780px) {
  .brand-sub { display: none; }
  .nav a { padding: 7px 10px; font-size: 13px; }
  .grid-totals, .grid { grid-template-columns: 1fr; }
  h1.page-title { font-size: 24px; }
  .money-lg { font-size: 22px; }
  .card { padding: 16px 16px; }
  .toasts { left: 16px; right: 16px; max-width: none; }
}

@media (max-width: 480px) {
  .main { padding: 18px 12px 44px; }
  .rail { padding: 10px 12px; gap: 10px; }
  /* touch targets: at least 44px tall on every tappable control */
  .nav a { min-height: 44px; padding: 10px 12px; align-items: center; }
  .btn { padding: 11px 14px; }
  .btn-ghost { padding: 12px; }
  .btn-ember { padding: 13px 16px; }
  .field input { padding: 12px; }
  h1.page-title { font-size: 21px; }
  .money-lg { font-size: 20px; }
  .card { padding: 14px 14px; }
  table.ledger { font-size: 12.5px; }
  table.ledger th, table.ledger td { padding: 8px 8px; }
  .login-card { padding: 26px 20px 24px; }
}


/* ---- deal progress rail (.step-rail on the seven deal states) --------------
   Shared by the portal and the console so both sides of the bank see the
   same picture. DISPUTED / REFUNDED are alert branches: the active dot and
   the end of the fill burn red. SETTLED is terminal: the active dot settles
   mint. Markup: .step-rail.deal-rail[.is-alert] with li.step.is-alert /
   .is-terminal on the node the state sits on. */
.deal-rail-card { padding: 22px 22px 12px; }
.deal-rail.is-alert .step-fill { background: linear-gradient(90deg, var(--ember-lo), var(--bad)); box-shadow: 0 0 12px rgba(255, 107, 107, 0.4); }
.deal-rail .step.is-alert .step-dot { border-color: var(--bad); box-shadow: 0 0 0 5px rgba(255, 107, 107, 0.14), 0 0 18px rgba(255, 107, 107, 0.45); }
.deal-rail .step.is-alert .step-label { color: var(--bad); }
.deal-rail .step.is-terminal .step-dot { background: var(--mint-soft); border-color: var(--mint); color: var(--mint); box-shadow: 0 0 14px rgba(62, 207, 142, 0.45); }
.deal-rail .step.is-terminal .step-label { color: var(--mint); }
.deal-rail .step.is-alert .step-dot::after, .deal-rail .step.is-terminal .step-dot::after { content: none; }
.deal-rail .step-label { font-size: 9.5px; letter-spacing: 0.08em; }
/* the lockbox hosts an ambient film: every child paints above it */
.lockbox-card > * { position: relative; }
.doc-flow { margin-top: 5px; }

/* ---- reduced motion: keep the shared layer perfectly still ---------------- */

@media (prefers-reduced-motion: reduce) {
  .card, a.card:hover, .card-lift:hover,
  .btn, .btn:active:not(:disabled), .btn-ember, .btn-ember:active {
    transition: none; transform: none;
  }
  .toast, .main-inner > *, .grid-stagger > *, .rows-stagger tbody tr,
  .pulse-ok, .pulse-ember, .login-ember-drift, .htmx-added {
    animation: none !important;
  }
  .htmx-swapping { opacity: 1; transform: none; transition: none; }
}

/* Real CIBL wordmark (static/obsidian/brand/cibl-logo.png) replaces the
   stand-in "C" tile: no box, no glow — the orange serif mark carries itself. */
.brand-mark.brand-logo {
  width: auto; height: 26px; border-radius: 0;
  background: none; box-shadow: none; display: block; object-fit: contain;
}
.login-card .brand-mark.brand-logo,
.otp-card .brand-mark.brand-logo { height: 34px; }

/* ==========================================================================
   APP PACK — notification bell, notification lists, channel preferences,
   the install prompt, the mobile tab bar and htmx skeletons.

   Shared layer on purpose: the bell and the notification list render
   identically in the client portal and the staff console, and the mobile
   shell tokens (safe-area insets, 44px targets) belong with the rest of
   the responsive system rather than in one app's stylesheet.
   ========================================================================== */

/* Alpine cloaks elements until it has initialised; without this the panel
   flashes open on every page load. */
[x-cloak] { display: none !important; }

/* The browser's own `[hidden] { display: none }` is a UA rule, so ANY
   author rule that sets `display` on the same element beats it — the
   install prompt (display:flex) and the iOS step list (display:grid)
   rendered in full while the DOM said hidden.  Restate it as an author
   rule so `el.hidden = true` means hidden, everywhere. */
[hidden] { display: none !important; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.inline-form { display: inline; margin: 0; }
.btn-tiny { font-size: 11.5px; padding: 5px 9px; }

/* ---- sticky rail ----------------------------------------------------------
   The rail's foot (bell, user, sign out) used to sit at the bottom of the
   DOCUMENT, so on a long console page the notification bell was a thousand
   pixels below the fold — useless.  On the two-column layout the rail now
   pins to the viewport, which is also what makes the app feel like an app
   rather than a scrolling web page.  Below 860px the rail is already a top
   bar and needs none of this. */

@media (min-width: 861px) {
  .rail {
    position: sticky; top: 0;
    height: 100vh; align-self: flex-start;
    /* `position: sticky` makes the rail its own stacking context, which
       traps the dropdown's z-index inside it — without a z-index here the
       panel paints UNDER every positioned card in the main column. */
    z-index: 50;
  }
  /* The NAV scrolls, not the rail: an `overflow` on the rail itself would
     clip the bell's dropdown to a 236px column. */
  .rail .nav { overflow-y: auto; min-height: 0; overscroll-behavior: contain; }
}

/* ADM-13 — the rail fits the laptop it is read on.
   At 1440x900 the nav's scroll box was 613px against a 727px list, so Chain
   was clipped mid-item and Governance and Mandates were not visible at all:
   an administrator could not see the pages they own, and the 375 layout
   (which wraps the nav into chips) showed MORE of the console than the
   laptop did.  The rail still scrolls — this just spends less of it on air,
   so every item is on screen at the standard desktop height.  Applied by
   viewport height, not by item count, so it holds as the console grows. */
@media (min-width: 861px) and (max-height: 1000px) {
  .rail { padding: 16px 14px; gap: 16px; }
  .rail .nav { gap: 2px; }
  .rail .nav a { padding: 6px 12px; }
}

/* ---- bell ---------------------------------------------------------------- */

.bell-wrap { position: relative; }
.bell-slot { display: block; }
/* Room for the count badge, which pokes 5px above the button. */
.rail-foot .bell-wrap { margin-bottom: 12px; padding-top: 5px; }
.bell-btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; padding: 0;
  background: var(--glass); color: var(--ink-dim);
  border: 1px solid var(--hairline); border-radius: 11px;
  cursor: pointer;
  transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
}
.bell-btn:hover, .bell-btn:focus-visible { color: var(--ink); border-color: var(--ember-ring); }
.bell-glyph { width: 20px; height: 20px; display: block; }
.bell-btn-unread { color: var(--ember-hi); border-color: rgba(255, 138, 26, 0.35); }
.bell-count {
  position: absolute; top: -5px; right: -5px;
  min-width: 18px; height: 18px; padding: 0 5px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--ember-grad); color: var(--on-ember);
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;
  border-radius: 9px; border: 2px solid var(--ground);
}
/* The bell used to nudge every five seconds for as long as anything was
   unread — a loop that carried no information after the first swing. It
   now wiggles ONCE, when a new unread arrives (obsidian-motion.js adds
   .bell-unread when the polled count rises; obsidian-motion.css animates). */

.bell-panel {
  position: absolute; z-index: 90;
  bottom: calc(100% + 10px); left: 0;
  width: 330px; max-height: 60vh; overflow-y: auto;
  background: var(--ground-raise);
  border: 1px solid var(--hairline-strong); border-radius: var(--radius);
  box-shadow: 0 20px 50px rgba(var(--shadow-rgb), calc(0.6 * var(--shadow-k)));
}
.bell-panel:empty { display: none; }
.bell-panel-head, .bell-panel-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 14px;
}
.bell-panel-head { border-bottom: 1px solid var(--hairline); }
.bell-panel-foot { border-top: 1px solid var(--hairline); }
.notif-all-link { font-size: 12.5px; color: var(--ember-hi); }

/* ---- notification list --------------------------------------------------- */

.notif-list { list-style: none; margin: 0; padding: 0; }
.notif-item {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 14px; border-bottom: 1px solid var(--hairline);
}
.notif-item:last-child { border-bottom: none; }
.notif-link { display: flex; gap: 10px; width: 100%; color: var(--ink); text-decoration: none; }
.notif-link:hover { text-decoration: none; }
.notif-dot {
  flex: 0 0 auto; width: 7px; height: 7px; margin-top: 6px;
  border-radius: 50%; background: var(--hairline-strong);
}
.notif-unread .notif-dot { background: var(--ember-grad); box-shadow: 0 0 8px rgba(255, 138, 26, 0.45); }
.notif-alert .notif-dot { background: var(--bad); box-shadow: none; }
.notif-action.notif-unread .notif-dot { background: var(--ember-grad); }
.notif-copy { display: grid; gap: 3px; min-width: 0; flex: 1 1 auto; }
.notif-title {
  font-size: 13.5px; font-weight: 500; color: var(--ink-dim);
  text-decoration: none; overflow-wrap: anywhere;
}
a.notif-title:hover { color: var(--ember-hi); text-decoration: none; }
.notif-unread .notif-title { color: var(--ink); font-weight: 600; }
.notif-body { font-size: 12.5px; color: var(--ink-dim); overflow-wrap: anywhere; }
.notif-meta {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline;
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint);
}
.notif-actions { flex: 0 0 auto; }
.notif-list-wrap { display: block; }
.notif-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 12px;
}
.notif-filters { display: flex; gap: 8px; }
.notif-filters .chip { cursor: pointer; text-decoration: none; }
.notif-filters .chip:hover { text-decoration: none; border-color: var(--ember-ring); }
.notif-list-compact .notif-item { padding: 10px 14px; }

/* ---- channel preferences -------------------------------------------------- */

.prefs { display: grid; gap: 0; }
.pref-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--hairline);
}
.pref-row:last-of-type { border-bottom: none; }
.pref-row-sub { padding-left: 16px; border-left: 2px solid var(--hairline); }
.pref-copy { display: grid; gap: 3px; min-width: 0; }
.pref-label { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.pref-note { font-size: 12.5px; color: var(--ink-dim); }
.pref-control { flex: 0 0 auto; text-align: right; }
.pref-ios { font-size: 12px; color: var(--ink-dim); margin: 8px 0 0; max-width: 34ch; }

/* CLI-06 — per-kind switches, folded away.  Twenty rows open by default
   would bury the wholesale channel switch above them, so the list is a
   <details>; its own rows carry no rule of their own because the group
   already sits inside one. */
.pref-kinds summary {
  cursor: pointer; font-size: 12.5px; color: var(--ink-dim);
  list-style: none; padding: 2px 0;
}
.pref-kinds summary::-webkit-details-marker { display: none; }
.pref-kinds summary::before { content: "→ "; color: var(--ink-faint); }
.pref-kinds[open] summary::before { content: "↓ "; }
.pref-kinds summary:hover { color: var(--ink); }
.pref-kinds[open] summary { margin-bottom: 6px; }
.pref-row.pref-kind { padding: 8px 0; border-bottom: none; }
.pref-row.pref-kind .pref-label { font-weight: 500; }

/* ---- install prompt ------------------------------------------------------- */

.install-prompt {
  position: fixed; z-index: 95;
  left: 20px; right: auto; bottom: 20px; width: min(360px, calc(100vw - 40px));
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px 16px 14px;
  padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  background: var(--ground-raise);
  border: 1px solid var(--hairline-strong);
  border-left: 3px solid var(--ember-hi);
  border-radius: var(--radius);
  box-shadow: 0 18px 44px rgba(var(--shadow-rgb), calc(0.6 * var(--shadow-k)));
}
.install-title { font-family: var(--font-body); font-size: 15px; margin: 0 0 4px; color: var(--ink); }
.install-body { font-size: 12.5px; color: var(--ink-dim); margin: 0; }
.install-ios { font-size: 12.5px; color: var(--ink-dim); margin: 8px 0 0; padding-left: 18px; display: grid; gap: 4px; }
.install-actions { display: flex; gap: 8px; align-items: center; }
@media (prefers-reduced-motion: no-preference) {
  .install-prompt.install-in { animation: toast-in 260ms cubic-bezier(0.22, 1, 0.36, 1) both; }
}

/* ---- offline page --------------------------------------------------------- */

.offline-card { text-align: left; }
.offline-list {
  list-style: none; margin: 0 0 20px; padding: 0;
  display: grid; gap: 8px; font-size: 13px; color: var(--ink-dim);
}
.offline-list li { padding-left: 16px; position: relative; }
.offline-list li::before {
  content: ""; position: absolute; left: 0; top: 7px;
  width: 6px; height: 6px; border-radius: 50%; background: var(--ember-hi);
}
.offline-retry { width: 100%; }

/* ---- htmx skeletons ------------------------------------------------------
   Any element htmx is currently loading into gets a shimmer instead of a
   dead gap.  Purely additive: no existing template had to be touched. */

.skeleton {
  background: linear-gradient(90deg, var(--glass) 25%, var(--glass-strong) 37%, var(--glass) 63%);
  background-size: 400% 100%;
  border-radius: var(--radius-sm);
}
.skeleton-line { height: 12px; margin: 8px 0; }
[hx-get].htmx-request, [hx-post].htmx-request,
.htmx-request > .skeleton { position: relative; }
@media (prefers-reduced-motion: no-preference) {
  /* the sheen is the REQUEST, not the placeholder: htmx carries .htmx-request
     on the element it is loading into and drops it when the response lands —
     including a 500, which swaps nothing. Keyed on it, a failed fetch leaves a
     bar at rest instead of a shimmer looping over nothing in flight. */
  .htmx-request .skeleton, .skeleton.htmx-request { animation: skeleton-sheen 1.4s ease-in-out infinite; }
  @keyframes skeleton-sheen {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
  }
  .htmx-request .notif-item, .htmx-request.card { opacity: 0.55; transition: opacity 140ms ease; }
}

/* ---- mobile app shell: bottom tab bar ------------------------------------
   Below 780px the portal stops being a website with a sidebar and starts
   being an app: primary destinations move to a thumb-reachable bar pinned
   to the bottom, padded for the home indicator. */

.tabbar { display: none; }
.tabbar-sheet { display: none; }

@media (max-width: 860px) {
  /* Top-bar layout: the bell hangs on the right of the row, so its panel
     drops DOWN from the bar instead of rising off the top of the screen.
     This block MUST precede the 780px one below, which switches the panel
     to fixed positioning — a `top: calc(100% + 10px)` left in place after
     that switch would resolve against the viewport and land off-screen. */
  .bell-panel { top: calc(100% + 10px); bottom: auto; right: 0; left: auto; }
  .rail-foot { display: flex; align-items: center; gap: 10px; }
  .rail-foot .theme-toggle { margin-bottom: 0; }
}

@media (max-width: 780px) {
  .tabbar {
    position: fixed; z-index: 80; left: 0; right: 0; bottom: 0;
    display: flex; align-items: stretch;
    background: rgba(var(--ground-rgb), 0.94);
    border-top: 1px solid var(--hairline);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    backdrop-filter: blur(14px);
  }
  .tabbar-item {
    flex: 1 1 0; min-width: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; padding: 8px 4px 7px; min-height: 52px;
    color: var(--ink-faint); font-size: 10.5px; letter-spacing: 0.02em;
    background: none; border: 0; cursor: pointer; text-decoration: none;
  }
  .tabbar-item:hover { text-decoration: none; color: var(--ink-dim); }
  .tabbar-item.active { color: var(--ember-hi); }
  .tabbar-glyph { width: 21px; height: 21px; display: block; }
  .tabbar-label { display: block; }
  .tabbar-badge {
    position: absolute; transform: translate(12px, -6px);
    min-width: 8px; height: 8px; border-radius: 50%; background: var(--ember-grad);
  }
  .tabbar-sheet {
    position: fixed; z-index: 81; left: 0; right: 0;
    bottom: calc(52px + env(safe-area-inset-bottom, 0px));
    display: grid; gap: 2px; padding: 10px 12px 14px;
    background: var(--ground-raise);
    border-top: 1px solid var(--hairline-strong);
    box-shadow: 0 -18px 44px rgba(var(--shadow-rgb), calc(0.6 * var(--shadow-k)));
  }
  .tabbar-sheet a, .tabbar-sheet button {
    display: block; width: 100%; text-align: left;
    padding: 13px 12px; min-height: 44px;
    color: var(--ink-dim); font-size: 14px;
    background: none; border: 0; border-radius: var(--radius-sm); cursor: pointer;
  }
  .tabbar-sheet a:hover, .tabbar-sheet button:hover {
    background: var(--glass); color: var(--ink); text-decoration: none;
  }
  /* The bar floats over the page; give the content room to scroll clear. */
  body.has-tabbar .main { padding-bottom: calc(78px + env(safe-area-inset-bottom, 0px)); }
  body.has-tabbar .toasts { bottom: calc(70px + env(safe-area-inset-bottom, 0px)); }
  body.has-tabbar .install-prompt { bottom: calc(70px + env(safe-area-inset-bottom, 0px)); }
  /* The sidebar nav is redundant once the tab bar exists. */
  body.has-tabbar .rail .nav { display: none; }

  /* Sticky primary action: on a phone the confirm button must stay under
     the thumb instead of scrolling away up the page.  `.sticky-action` is
     the opt-in class; `.actions-card` (the portal's deal action panel) gets
     it for free because that is the one place where a mis-tap costs money
     and a scroll-to-find costs confidence.  Sticky, not fixed: the panel
     pins only while its own section is on screen. */
  .sticky-action, .actions-card {
    position: sticky; z-index: 40;
    bottom: calc(60px + env(safe-area-inset-bottom, 0px));
  }

  /* Phone: the dropdown becomes a full-width sheet under the top bar
     rather than a 330px card hanging off one corner. */
  .bell-panel {
    position: fixed; left: 12px; right: 12px; width: auto;
    top: 64px; bottom: auto; max-height: 70vh;
  }
  .install-prompt { left: 12px; right: 12px; width: auto; }
}

@media (max-width: 480px) {
  .bell-btn { width: 44px; height: 44px; }
  .notif-item { padding: 12px 12px; }
  .pref-row { flex-direction: column; align-items: flex-start; gap: 10px; }
  .pref-control { text-align: left; width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  /* the bell never moves under reduced motion, whichever layer animates
     it (the once-only wiggle lives in obsidian-motion.css) */
  .bell-btn-unread .bell-glyph, .bell-unread .bell-glyph,
  .skeleton, .install-prompt.install-in {
    animation: none !important;
  }
  .htmx-request .notif-item, .htmx-request.card { opacity: 1; transition: none; }
}

/* ==========================================================================
   PRIMITIVES — the shared vocabulary every page is built from.

   This is the STILL layer: every rule below is the settled state, the
   final frame. Motion for the same classes lives in obsidian-motion.css,
   gated behind prefers-reduced-motion; with that file missing, or motion
   reduced, a page still renders complete. Naming note: `.rail` is the app
   sidebar (above), so the wizard rail is `.step-rail`.
   Usage comment above each primitive; the gallery at /dev/obsidian/
   (DEBUG only) shows every one with bank content.
   ========================================================================== */

/* ---- .eyebrow-pill ---------------------------------------------------------
   <span class="eyebrow-pill">Gateway online</span>
   A dot + two or three mono words, never a sentence. Tone classes colour
   the dot: .is-ok (mint), .is-bad (red), .is-off (no glow). Works as <a>. */
.eyebrow-pill {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 4px 11px 4px 9px; border-radius: 999px;
  border: 1px solid var(--hairline-strong); background: var(--glass-strong);
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-dim);
  white-space: nowrap; vertical-align: middle; line-height: 1.4;
}
.eyebrow-pill::before {
  content: ""; flex: 0 0 6px; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ember-hi);
  box-shadow: 0 0 0 2px rgba(255, 163, 51, 0.18), 0 0 10px 1px rgba(255, 163, 51, 0.65);
}
.eyebrow-pill.is-ok::before {
  background: var(--mint);
  box-shadow: 0 0 0 2px rgba(62, 207, 142, 0.18), 0 0 10px 1px rgba(62, 207, 142, 0.6);
}
.eyebrow-pill.is-bad::before {
  background: var(--bad);
  box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.18), 0 0 10px 1px rgba(255, 107, 107, 0.6);
}
.eyebrow-pill.is-off::before { background: var(--hairline-strong); box-shadow: none; }
a.eyebrow-pill:hover { text-decoration: none; border-color: var(--ember-ring); color: var(--ink); }

/* ---- .grad-text ------------------------------------------------------------
   <h1>Every settlement <span class="grad-text">carries its evidence</span></h1>
   Ember gradient on ONE key phrase of a headline. One per page. */
.grad-text {
  background: linear-gradient(92deg, var(--ember-hi) 0%, var(--ember-lo) 100%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
}

/* ---- .glass-card -----------------------------------------------------------
   <section class="glass-card">…</section>   (also .card.glass-card)
   Floating glass: a 1px gradient edge (masked pseudo), an inner top
   highlight, a soft drop. Hover lift + edge warming in the motion layer.
   Add data-tilt for the hero cards that may tilt toward the pointer.
   Do not combine with .total-card (it owns ::after). */
.glass-card {
  position: relative;
  background: var(--glass-grad);
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 20px 22px;
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  box-shadow: inset 0 1px 0 rgba(var(--overlay-rgb), 0.07), 0 14px 34px rgba(var(--shadow-rgb), calc(0.28 * var(--shadow-k)));
}
.glass-card::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1px;
  background: var(--edge-grad); opacity: 0.75;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
.glass-card.is-mint::before {
  background: linear-gradient(135deg, rgba(62, 207, 142, 0.6), rgba(var(--overlay-rgb), 0.1) 40%, rgba(62, 207, 142, 0.35));
}
/* a glass ROW: the same edge on a list item / table-row stand-in */
.glass-row { padding: 12px 16px; border-radius: var(--radius-sm); }
.glass-row + .glass-row { margin-top: 8px; }

/* ---- .stat-strip / .stat ---------------------------------------------------
   <div class="stat-strip">
     <a class="stat" href="…"><span class="stat-label">Escrow held</span>
       <span class="stat-value money">1,305,000.00</span>
       <span class="stat-delta up">+2 this week</span></a>
   </div>
   Mono eyebrow, display number (money keeps the mono .money face), delta
   chip .up / .down / .warn. Put .glow-ring on the ONE stat that needs a
   human. A .sparkline may sit last inside a stat. */
/* The gaps used to be the strip's own (darker) background showing through.
   With eight tiles in an auto-fit grid the last row leaves empty cells, and
   those cells then read as a hole cut in the panel — worst on the console
   dashboard, where the tall escrow tile makes the hole half the panel.  So
   the strip is painted in the TILE colour and the rules hang off the tiles:
   an empty cell is now just unused surface, and overflow:hidden clips the
   rules that fall outside the panel. */
.stat-strip {
  /* 240 not 180: the console dashboard carries eight tiles, and at 180 they
     laid out six-then-two, leaving half the panel empty.  Four across fills
     both rows exactly.  The ground is the card surface, NOT --hairline: when
     the last row is short the leftover area is the container itself, and a
     hairline ground printed it as a grey slab on paper.  The 1px gaps are
     drawn by the cells' own rings below. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1px;
  background: var(--ground-raise); border: 1px solid var(--hairline-strong);
  border-radius: var(--radius); overflow: hidden;
}
.stat {
  position: relative; display: grid; gap: 6px; align-content: start;
  padding: 16px 18px 15px; min-width: 0;
  background: var(--ground-raise); color: var(--ink);
  box-shadow: 0 0 0 1px var(--hairline);  /* fills the 1px gap; clipped at the edges */
}
a.stat:hover { text-decoration: none; background: var(--ground-hover); }
.stat-label {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-faint);
}
.stat-value {
  font-family: var(--font-body); font-size: 26px; font-weight: 700;
  letter-spacing: -0.02em; line-height: 1.1; font-variant-numeric: tabular-nums;
}
.stat-value.money { font-weight: 600; font-size: 26px; letter-spacing: -0.02em; }
.stat-delta {
  display: inline-flex; align-items: center; gap: 4px; width: max-content;
  font-family: var(--font-mono); font-size: 10.5px; padding: 1px 7px; border-radius: 999px;
  border: 1px solid var(--hairline-strong); background: var(--glass-strong); color: var(--ink-dim);
}
.stat-delta.up { color: var(--mint); border-color: var(--mint-ring); background: var(--mint-soft); }
.stat-delta.down { color: var(--bad); border-color: var(--bad-ring); background: var(--bad-soft); }
.stat-delta.warn { color: var(--ember-hi); border-color: var(--ember-ring); background: var(--ember-soft); }
.stat .sparkline { margin-top: 4px; }

/* ---- .ticker ---------------------------------------------------------------
   <div class="ticker" data-ticker><div class="ticker-track">
     <span class="ticker-chip"><b>TD-0005</b> Bill of lading · <span class="money">42,000.00</span></span>…
   </div></div>
   MOTION DOCTRINE, and the board's only definition: a marquee that never
   stops is decoration repeating in the corner of a working screen, and a
   treasurer reading the third chip should not have to wait for it to come
   round. So the newest posting RESTS at the left edge, the strip arrives once
   with the page cascade it sits in, and it moves again only when the reader
   scrolls it (trackpad, shift-wheel, touch, arrow keys) or a new posting
   really arrives with the next render. No animation, no JS, no clone: the
   two products used to patch a loop away in two different stylesheets. */
.ticker {
  position: relative; display: flex; padding: 8px 0;
  overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none;
  -webkit-mask: linear-gradient(90deg, #000 0, #000 88%, transparent);
  mask: linear-gradient(90deg, #000 0, #000 88%, transparent);
}
.ticker::-webkit-scrollbar { display: none; }
/* it is a scrollable region, so it takes focus and the arrow keys */
.ticker:focus-visible { outline: 2px solid var(--ember-ring); outline-offset: 2px; border-radius: 8px; }
.ticker-track { flex: 0 0 auto; display: flex; gap: 10px; padding-right: 10px; min-width: 100%; }
.ticker-chip {
  display: inline-flex; align-items: center; gap: 8px; padding: 5px 12px; border-radius: 999px;
  border: 1px solid var(--hairline); background: var(--glass);
  font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-dim); white-space: nowrap;
}
.ticker-chip b { color: var(--ink-faint); font-weight: 500; font-size: 0.92em; }
.ticker-chip .money { color: var(--ink); }
.ticker-chip.is-ok { border-color: var(--mint-ring); }
/* The reference is how you find the posting again, not the news. It keeps the
   mono face so the numbers line up, but it steps back to the quiet ink so the
   money beside it is the first thing the eye lands on. A green reference was
   competing with the figure it belongs to. */
.ticker-chip.is-ok b { color: var(--ink-faint); }
a.ticker-chip:hover { text-decoration: none; border-color: var(--ember-ring); }

/* ---- .progress-rail --------------------------------------------------------
   <nav class="progress-rail" data-rail aria-label="On this page">
     <a href="#people" data-label="People"></a>…
   </nav>
   Side dots that light per section (obsidian-motion.js spies the targets).
   Desktop only — it hides below 1180px where there is no margin for it. */
.progress-rail {
  position: fixed; right: 18px; top: 50%; transform: translateY(-50%); z-index: 40;
  display: none; flex-direction: column; gap: 14px; margin: 0; padding: 0; list-style: none;
}
@media (min-width: 1180px) { .progress-rail { display: flex; } }
.progress-rail a {
  position: relative; display: block; width: 10px; height: 10px; border-radius: 50%;
  border: 1px solid var(--hairline-strong); background: var(--ground-raise);
}
.progress-rail a::after {
  content: ""; position: absolute; inset: 2px; border-radius: 50%;
  background: var(--ember-grad); transform: scale(0);
}
.progress-rail a.on { border-color: var(--ember-hi); box-shadow: 0 0 10px rgba(255, 138, 26, 0.5); }
.progress-rail a.on::after { transform: scale(1); }
.progress-rail a::before {
  content: attr(data-label); position: absolute; right: 18px; top: 50%; transform: translateY(-50%);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-dim); white-space: nowrap; opacity: 0; pointer-events: none;
}
.progress-rail a:hover::before, .progress-rail a:focus-visible::before { opacity: 1; }

/* ---- .terminal-card --------------------------------------------------------
   <div class="terminal-card" data-typed="140" data-fx-resync>
     <div class="terminal-bar"><span class="terminal-dots"><i></i><i></i><i></i></span>
       <span class="terminal-title">chain outbox · core-1</span>
       <span class="eyebrow-pill is-ok terminal-chip">Gateway online</span></div>
     <div class="terminal-body">
       <div class="t-line"><span class="t-p">›</span><span class="t-cmd">anchor TD-0005 …</span><span class="t-out ok">SENT</span></div>
       …<span class="t-caret" aria-hidden="true"></span>
     </div>
   </div>
   Mono log lines are REAL rows from the view; data-typed="ms" reveals them
   one at a time (obsidian-motion.js) with a caret. .t-out.ok mint, .bad
   red, .dim faint. Subtle scanlines; a slow scan band in the motion layer. */
.terminal-card {
  position: relative; overflow: hidden;
  font-family: var(--font-mono); font-size: 12.5px; line-height: 1.9;
  background: linear-gradient(172deg, var(--terminal-top) 0%, var(--ground) 100%);
  border: 1px solid var(--hairline-strong); border-radius: var(--radius);
  box-shadow: inset 0 1px 0 rgba(var(--overlay-rgb), 0.05), 0 30px 60px rgba(var(--shadow-rgb), calc(0.45 * var(--shadow-k)));
}
/* the moving scan band (motion layer animates it; hidden at rest) */
.terminal-card::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 34%; z-index: 0;
  background: linear-gradient(180deg, transparent, rgba(255, 163, 51, 0.035), transparent);
  opacity: 0; pointer-events: none;
}
/* static scanlines */
.terminal-card::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background: repeating-linear-gradient(0deg, transparent 0 3px, rgba(var(--overlay-rgb), 0.012) 3px 4px);
}
.terminal-bar, .terminal-body { position: relative; z-index: 1; }
.terminal-bar {
  display: flex; align-items: center; gap: 12px; padding: 10px 16px;
  border-bottom: 1px solid var(--hairline); background: rgba(var(--overlay-rgb), 0.02);
}
.terminal-dots { display: flex; gap: 6px; }
.terminal-dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--hairline-strong); }
.terminal-dots i:nth-child(1) { background: rgba(255, 107, 107, 0.7); }
.terminal-dots i:nth-child(2) { background: rgba(255, 163, 51, 0.7); }
.terminal-dots i:nth-child(3) { background: rgba(62, 207, 142, 0.7); }
.terminal-title { font-size: 11px; color: var(--ink-faint); letter-spacing: 0.04em; flex: 1 1 auto; min-width: 0; }
/* on a phone the title, a button and the chip share the bar only if it wraps */
@media (max-width: 560px) { .terminal-bar { flex-wrap: wrap; gap: 8px 12px; } .terminal-chip { margin-left: 0; } }
.terminal-chip { margin-left: auto; }
.terminal-body { padding: 14px 18px 12px; min-height: 120px; }
.t-line { display: flex; align-items: baseline; gap: 10px; white-space: nowrap; min-width: 0; }
.t-p { flex: 0 0 auto; color: var(--ember-hi); }
.t-cmd { color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.t-cmd .dim { color: var(--ink-faint); }
.t-out { margin-left: auto; padding-left: 12px; font-weight: 500; color: var(--ink); }
.t-out.ok { color: var(--mint); }
.t-out.bad { color: var(--bad); }
.t-out.dim { color: var(--ink-faint); font-weight: 400; }
.t-out.ember { color: var(--ember-hi); }
.t-caret {
  display: inline-block; width: 7px; height: 1.05em; margin: 2px 0 0 2px;
  background: var(--ember-hi); vertical-align: text-bottom; opacity: 0;
}
.terminal-foot { padding: 0 18px 14px; font-family: var(--font-body); font-size: 12.5px; color: var(--ink-dim); position: relative; z-index: 1; }

/* ---- .glow-ring ------------------------------------------------------------
   <section class="glass-card glow-ring">…</section>
   A soft ember ring for the ONE tile on the page that needs a human
   (pending approval, unverified upload, coverage under 100%). One per
   page; the motion layer pulses it slowly. Works on .stat too. */
.glow-ring { position: relative; border-color: rgba(255, 163, 51, 0.42); }
.glow-ring::after {
  content: ""; position: absolute; inset: -1px; border-radius: inherit; pointer-events: none;
  box-shadow: 0 0 0 1px rgba(255, 163, 51, 0.38), 0 0 28px 2px rgba(255, 138, 26, 0.22),
              inset 0 0 26px rgba(255, 107, 0, 0.07);
}
.glass-card.glow-ring::before {
  opacity: 1;
  background: linear-gradient(135deg, rgba(255, 163, 51, 0.9), rgba(255, 107, 0, 0.35) 45%,
                                       rgba(255, 163, 51, 0.3) 70%, rgba(255, 107, 0, 0.85));
}
.stat.glow-ring { background: linear-gradient(180deg, rgba(255, 138, 26, 0.06), transparent 60%), var(--ground-raise); }

/* ---- .sparkline ------------------------------------------------------------
   <svg class="sparkline" viewBox="0 0 100 36" aria-hidden="true">
     <path class="spark-fill" d="M0 30 L20 22 … L100 12 V36 H0 Z"/>
     <path class="spark-line" pathLength="1" d="M0 30 L20 22 … L100 12"/>
     <circle class="spark-dot" cx="100" cy="12" r="2.4"/>
   </svg>
   Inline SVG helper for a balance over the last N journal dates. With
   pathLength="1" the line draws itself on reveal (motion layer). .is-ok
   turns it mint. Keep the viewBox 100x36 so the dot stays round. */
.sparkline { display: block; width: 100%; height: 36px; overflow: visible; }
.sparkline .spark-line {
  fill: none; stroke: var(--ember-hi); stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke;
}
.sparkline .spark-fill { fill: rgba(255, 163, 51, 0.10); stroke: none; }
.sparkline .spark-dot { fill: var(--ember-hi); stroke: var(--ground-raise); stroke-width: 1.5; }
.sparkline.is-ok .spark-line { stroke: var(--mint); }
.sparkline.is-ok .spark-dot { fill: var(--mint); }
.sparkline.is-ok .spark-fill { fill: rgba(62, 207, 142, 0.1); }

/* ---- .section-opener -------------------------------------------------------
   <header class="section-opener"><span class="eyebrow-pill">Signing rules</span>
     <h2 class="section-title">Who may <span class="grad-text">release</span></h2>
     <p class="note">One sentence.</p></header>
   Centred opener between the blocks of a long page. */
.section-opener {
  text-align: center; max-width: 60ch; margin: 44px auto 24px;
  display: grid; gap: 10px; justify-items: center;
}
.section-opener:first-child { margin-top: 0; }
.section-opener .section-title {
  font-family: var(--font-body); font-size: 26px; font-weight: 700;
  letter-spacing: -0.02em; margin: 0; line-height: 1.15;
}
.section-opener .note { margin: 0; max-width: 52ch; }

/* ---- .step-rail (wizard rail) ----------------------------------------------
   <div class="step-rail" data-rail style="--n:5; --fill:.5">
     <div class="step-track" aria-hidden="true"><i class="step-fill"></i><b class="step-spark"></b></div>
     <ol class="step-list">
       <li class="step is-done"><span class="step-dot"><span class="step-num">1</span>
         <svg class="step-tick" viewBox="0 0 12 12"><path d="M2.5 6.5l2.5 2.5 4.5-5.5"/></svg></span>
         <span class="step-label">Company</span></li>
       <li class="step is-active">…</li><li class="step">…</li>
     </ol>
   </div>
   --n = number of steps, --fill = done ÷ (n − 1) — both from the view's
   real step number. Horizontal by default; add .step-rail-v for the side
   rail of a desktop wizard. Motion: travelling fill, spark, pulsing dot. */
.step-rail { --n: 5; --fill: 0; position: relative; margin: 0 0 28px; }
.step-track {
  position: absolute; top: 14px; height: 3px; border-radius: 999px;
  left: calc(100% / (var(--n) * 2)); right: calc(100% / (var(--n) * 2));
  background: var(--glass-strong);
}
.step-fill {
  position: absolute; inset: 0; border-radius: inherit; overflow: hidden;
  transform-origin: left center; transform: scaleX(var(--fill));
  background: var(--ember-grad); box-shadow: 0 0 12px rgba(255, 138, 26, 0.5);
}
.step-fill::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 200%; opacity: 0;
  background: linear-gradient(90deg, var(--ember-lo) 0%, var(--ember-hi) 25%,
    rgba(255, 255, 255, 0.55) 40%, var(--ember-hi) 55%, var(--ember-lo) 100%);
}
/* the spark is exactly as long as the filled run and slides in from the
   track's left edge, so it can never reach past the fill (a full-width
   element translated by the fill overflowed the page by ~300px). */
.step-spark {
  position: absolute; left: 0; top: 50%; width: calc(var(--fill) * 100%); height: 12px; margin-top: -6px;
  pointer-events: none; opacity: 0;
  background: radial-gradient(circle at calc(100% - 6px) 50%, #FFF 0, #FFF 2px, rgba(255, 163, 51, 0.85) 3px, transparent 6.5px);
}
.step-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(var(--n), 1fr); }
.step-rail .step { display: flex; flex-direction: column; align-items: center; gap: 9px; text-align: center; min-width: 0; }
.step-dot {
  position: relative; z-index: 1; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--ground-raise); border: 1.5px solid var(--hairline-strong);
  font-family: var(--font-body); font-size: 12px; font-weight: 700; color: var(--ink-faint);
}
.step-dot .step-tick { display: none; width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* a step name is a label, never a reference: SIMPLE doctrine, body face */
.step-label { font-family: var(--font-body); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); }
.step.is-active .step-dot {
  border-color: var(--ember-hi); color: var(--ink);
  box-shadow: 0 0 0 5px rgba(255, 163, 51, 0.14), 0 0 18px rgba(255, 138, 26, 0.45);
}
.step.is-active .step-label { color: var(--ink); }
.step.is-done .step-dot { background: var(--mint-soft); border-color: var(--mint); color: var(--mint); }
.step.is-done .step-dot .step-num { display: none; }
.step.is-done .step-dot .step-tick { display: block; }
.step.is-done .step-label { color: var(--ink-dim); }
@media (max-width: 640px) { .step-rail .step-label { display: none; } .step-rail { margin-bottom: 20px; } }
/* vertical side rail */
.step-rail-v { margin: 0; }
.step-rail-v .step-track {
  top: calc(100% / (var(--n) * 2)); bottom: calc(100% / (var(--n) * 2));
  left: 14px; right: auto; width: 3px; height: auto;
}
.step-rail-v .step-fill { transform-origin: center top; transform: scaleY(var(--fill)); }
.step-rail-v .step-fill::after {
  top: 0; left: 0; right: 0; width: auto; height: 200%;
  background: linear-gradient(180deg, var(--ember-lo) 0%, var(--ember-hi) 25%,
    rgba(255, 255, 255, 0.55) 40%, var(--ember-hi) 55%, var(--ember-lo) 100%);
}
.step-rail-v .step-spark {
  left: 50%; top: 0; width: 12px; height: calc(var(--fill) * 100%); margin: 0 0 0 -6px;
  background: radial-gradient(circle at 50% calc(100% - 6px), #FFF 0, #FFF 2px, rgba(255, 163, 51, 0.85) 3px, transparent 6.5px);
}
.step-rail-v .step-list { grid-template-columns: 1fr; grid-template-rows: repeat(var(--n), 1fr); min-height: calc(var(--n) * 64px); }
.step-rail-v .step { flex-direction: row; text-align: left; gap: 12px; }
.step-rail-v .step-label { display: block; }

/* ---- .ring-progress --------------------------------------------------------
   <div class="ring-progress" data-progress="60" style="--p:.6">
     <svg viewBox="0 0 120 120" aria-hidden="true">
       <circle class="ring-bg" cx="60" cy="60" r="52"/>
       <circle class="ring-fg" cx="60" cy="60" r="52" pathLength="100"/></svg>
     <div class="ring-label"><strong>3 of 5</strong><span>ready</span></div>
   </div>
   Settle-safe: the label is server text and never animates; only the
   stroke fills, from data-progress (obsidian-motion.js sets --p; an inline
   --p is the no-JS path). .is-ok mint; .ring-sm for a 44px inline ring. */
.ring-progress { --p: 0; --size: 132px; position: relative; width: var(--size); height: var(--size); display: grid; place-items: center; flex: 0 0 auto; }
.ring-progress svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); overflow: visible; }
.ring-progress .ring-bg { fill: none; stroke: var(--glass-strong); stroke-width: 7; }
.ring-progress .ring-fg {
  fill: none; stroke: var(--ember-hi); stroke-width: 7; stroke-linecap: round;
  stroke-dasharray: 100; stroke-dashoffset: calc(100 - var(--p) * 100);
  filter: drop-shadow(0 0 6px rgba(255, 138, 26, 0.5));
}
.ring-progress.is-ok .ring-fg { stroke: var(--mint); filter: drop-shadow(0 0 6px rgba(62, 207, 142, 0.45)); }
.ring-label { position: relative; text-align: center; display: grid; gap: 3px; }
.ring-label strong { font-family: var(--font-body); font-size: 22px; font-weight: 700; letter-spacing: -0.02em; line-height: 1; }
.ring-label span { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); }
.ring-progress.ring-sm { --size: 44px; }
.ring-sm .ring-bg, .ring-sm .ring-fg { stroke-width: 10; }
.ring-sm .ring-label strong { font-family: var(--font-mono); font-size: 11px; font-weight: 600; }
.ring-sm .ring-label span { display: none; }

/* ---- .proc-steps -----------------------------------------------------------
   <ol class="proc-steps">
     <li class="done"><span class="ps-dot"><svg viewBox="0 0 12 12"><path d="M2.5 6.5l2.5 2.5 4.5-5.5"/></svg></span>Documents read<span class="ps-meta">5 of 5</span></li>
     <li class="doing">…</li><li class="todo">…</li>
   </ol>
   Each li is .todo / .doing / .done from REAL records. Add .proc-strip for
   the horizontal process strip (review page, case file). */
.proc-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.proc-steps > li { display: flex; align-items: center; gap: 12px; font-size: 14px; color: var(--ink-faint); }
.ps-dot {
  position: relative; flex: 0 0 24px; width: 24px; height: 24px; border-radius: 50%;
  display: grid; place-items: center; border: 1.5px solid var(--hairline-strong); color: var(--mint);
}
.ps-dot svg { width: 12px; height: 12px; opacity: 0; transform: scale(0.4); fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.proc-steps > li.doing { color: var(--ink); }
.proc-steps > li.doing .ps-dot { border-color: var(--ember-hi); box-shadow: 0 0 0 4px rgba(255, 163, 51, 0.14); }
.proc-steps > li.doing .ps-dot::after { content: ""; position: absolute; inset: 6px; border-radius: 50%; background: var(--ember-hi); }
.proc-steps > li.done { color: var(--ink-dim); }
.proc-steps > li.done .ps-dot { background: var(--mint-soft); border-color: var(--mint); }
.proc-steps > li.done .ps-dot svg { opacity: 1; transform: none; }
.ps-meta { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--ink-faint); white-space: nowrap; }
.proc-steps.proc-strip { display: flex; gap: 10px; align-items: flex-start; }
.proc-steps.proc-strip > li { flex: 1 1 0; flex-direction: column; text-align: center; gap: 8px; font-size: 12.5px; min-width: 0; }
.proc-steps.proc-strip .ps-meta { margin-left: 0; white-space: normal; }
@media (max-width: 560px) { .proc-steps.proc-strip { flex-wrap: wrap; } .proc-steps.proc-strip > li { flex-basis: calc(50% - 5px); } }

/* ---- .panel-slide[data-dir=next|back] --------------------------------------
   <section class="panel-slide" data-dir="{{ dir }}">…</section>
   A wizard panel slides in from the right (next) or the left (back) — the
   view passes the direction it was reached by. In-page swaps use
   window.obsidianSlide(el, dir); add .is-leaving to play the exit. */
.panel-slide { position: relative; }

/* ---- .shake / .err-in / .dot-pop / .flash / .burst --------------------------
   .shake        one shake on a field wrapper or a card (self-removes).
   .shake-on-error  a card that shakes when it CONTAINS a .form-error.
   .form-error   already drops in (errIn); .err-in for any other element.
   .dot-pop      pops once; `input:checked + .dot-pop` pops a custom tick.
   .flash        mint edge that fades: only when something REAL completed.
                 window.obsidianFlash(el, "ember") for the ember variant.
   .burst        particle container; window.obsidianBurst(el) fills it. */
.flash, .flash-ember { position: relative; }
.flash::after, .flash-ember::after {
  content: ""; position: absolute; inset: -1px; border-radius: inherit; pointer-events: none; z-index: 2;
  box-shadow: 0 0 0 2px var(--mint), 0 0 34px rgba(62, 207, 142, 0.35);
  background: rgba(62, 207, 142, 0.08); opacity: 0;
}
.flash-ember::after {
  box-shadow: 0 0 0 2px var(--ember-hi), 0 0 34px rgba(255, 138, 26, 0.35);
  background: rgba(255, 138, 26, 0.08);
}
.burst { position: absolute; left: 50%; top: 50%; width: 0; height: 0; pointer-events: none; z-index: 5; }
.burst i { position: absolute; left: -3px; top: -3px; width: 7px; height: 7px; border-radius: 2px; opacity: 0; }

/* ---- .nav-indicator --------------------------------------------------------
   <nav class="nav" data-nav-indicator>…</nav>
   obsidian-motion.js appends the sliding marker and parks it on the
   .active link; hovering another link slides it there and back. Vertical
   rail only. Also fine on a tab strip that uses .nav markup. */
.nav { position: relative; }
.nav-indicator {
  position: absolute; left: 0; width: 3px; border-radius: 0 3px 3px 0;
  background: var(--ember-grad); box-shadow: 0 0 12px rgba(255, 138, 26, 0.7);
  opacity: 0; pointer-events: none;
}
.nav-indicator.show { opacity: 1; }

/* ---- .press / .field-lift --------------------------------------------------
   .press on any button: scale .97 while pressed.
   .field-lift on a .field: lifts 2px and warms its label while focused.
   Both are transitions in the motion layer; nothing here at rest. */

/* ---- form submit pill ------------------------------------------------------
   <form data-submit-pill><button class="btn-ember" data-submitting-label="Verifying…">Sign in</button></form>
   On submit the button becomes a pill with a sweep and its label swaps —
   tied to the REAL request: it lasts exactly until the response replaces
   the page. Text-only buttons. */
.btn-primary, .btn-ember { position: relative; overflow: hidden; }
.is-submitting { border-radius: 999px !important; pointer-events: none; }
.btn-ember.is-submitting, .btn-primary.is-submitting { filter: brightness(1.04); }

/* ---- dropzone breathe ------------------------------------------------------
   <label class="dropzone" data-dropzone>…</label>
   obsidian-motion.js adds .is-over while a file hovers; the wizard's own
   Alpine `dragover` class is honoured too. */
.dropzone.is-over { border-color: var(--ember-hi); background: var(--ember-soft); }

/* ---- the file control the bank drew, not the operating system -------------
   <label class="dropzone dropzone-file" data-dropzone>
     <input type="file" …><span class="dropzone-glyph">…</span>
     <span class="dropzone-copy"><span class="dropzone-name">Choose a file</span>
       <span class="dropzone-hint">or drop it here</span></span></label>
   A naked <input type="file"> puts the OS "Choose file / No file chosen" next
   to a fully styled form — on the console screen where the bank's money
   arrives, and on the portal screen where a client tells us their money moved.
   The input is still the control: same id, same name, same accept, same
   required flag, still focusable and still operable by keyboard; only its
   rendering is ours.  obsidian-motion.js writes the chosen filename and its
   size into .dropzone-name / .dropzone-hint — ONE control and one byte format
   for both products, which used to be two components whose formatters
   disagreed about the same file.  Without JS the label still reads correctly
   and the input still picks a file. */
.dropzone.dropzone-file {
  position: relative; display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; min-height: 52px;
  border: 1px dashed var(--hairline-strong); border-radius: var(--radius-sm);
  background: var(--glass); cursor: pointer;
  transition: border-color 160ms, background 160ms;
}
.dropzone.dropzone-file:hover { border-color: var(--ember-ring); background: var(--ground-hover); }
.dropzone.dropzone-file:focus-within { border-color: var(--ember-hi); box-shadow: 0 0 0 2px var(--ember-soft); }
/* visually hidden, still focusable and still operable by keyboard */
.dropzone.dropzone-file input[type="file"] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer;
}
.dropzone-glyph {
  flex: 0 0 auto; display: grid; place-items: center; width: 28px; height: 28px;
  border-radius: 8px; background: var(--ember-soft); color: var(--ember-hi);
}
.dropzone-glyph svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.dropzone-copy { display: grid; gap: 2px; min-width: 0; }
.dropzone-name {
  font-family: var(--font-body); font-size: 13.5px; font-weight: 600; color: var(--ink);
  text-transform: none; letter-spacing: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dropzone-hint { font-family: var(--font-body); font-size: 11.5px; color: var(--ink-dim); text-transform: none; letter-spacing: 0; }
.dropzone.dropzone-file.has-file { border-style: solid; border-color: var(--ember-ring); }
.dropzone.dropzone-file.has-file .dropzone-hint { font-variant-numeric: tabular-nums; }


/* ---- .scene ----------------------------------------------------------------
   <div class="scene-row" data-fx-resync>
     <div class="scene is-done"><div class="scene-art"><svg viewBox="0 0 120 84">…</svg></div>
       <p class="scene-cap"><strong>Funded</strong><span>12 Sep</span></p></div>
     <div class="scene is-current">…</div><div class="scene">…</div>
   </div>
   Storyboard base. Defaults are the FINAL FRAME: .is-done settles on it,
   .is-current plays (motion layer), the rest dim. SVG actors use the
   vocabulary .fx-draw (pathLength="1"), .fx-pop, .fx-drop, .fx-spin,
   .fx-glint, .fx-rise; transient actors (.fx-drop, .fx-glint) are hidden
   at rest. Shape classes: .sc-doc .sc-line .sc-ember .sc-mint .sc-ink
   .sc-tick. Keep scenes SMALL: 3–8 shapes, viewBox 120x84. */
.scene-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; }
@media (max-width: 1100px) { .scene-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 560px) { .scene-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.scene {
  position: relative; border: 1px solid var(--hairline); border-radius: var(--radius);
  background: var(--glass); padding: 10px 8px 12px; text-align: center; min-width: 0;
}
.scene-art svg { display: block; width: 100%; max-width: 130px; height: auto; margin: 0 auto; overflow: visible; }
.scene-cap { margin: 4px 0 0; line-height: 1.35; }
/* a scene's caption is a label ("ESCROW HELD"), never a reference: body face */
.scene-cap strong { display: block; font-family: var(--font-body); font-size: 10px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-dim); }
.scene-cap span { font-size: 12px; color: var(--ink-faint); }
.scene.is-done { border-color: var(--mint-ring); box-shadow: 0 0 0 1px rgba(62, 207, 142, 0.1), 0 8px 22px rgba(62, 207, 142, 0.08); }
.scene.is-done .scene-cap strong { color: var(--mint); }
.scene.is-current {
  z-index: 1; transform: scale(1.04);
  border-color: rgba(255, 163, 51, 0.5); box-shadow: 0 10px 30px rgba(255, 107, 0, 0.16);
}
.scene.is-current::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: radial-gradient(90% 70% at 50% 0%, rgba(255, 163, 51, 0.14), transparent 70%);
}
.scene.is-current .scene-cap strong { color: var(--ember-hi); }
.scene.is-alert { border-color: var(--bad-ring); }
.scene.is-alert .scene-cap strong { color: var(--bad); }
.scene:not(.is-done):not(.is-current) .scene-art { opacity: 0.5; }
.scene:not(.is-done):not(.is-current) .scene-cap strong { color: var(--ink-faint); }
.sc-doc { fill: var(--ground-raise); stroke: var(--hairline-strong); stroke-width: 1.5; }
.sc-line { fill: var(--hairline-strong); }
.sc-ember { fill: var(--ember-soft); stroke: var(--ember-hi); stroke-width: 1.6; }
.sc-ember-solid { fill: var(--ember-hi); }
.sc-mint { fill: var(--mint-soft); stroke: var(--mint); stroke-width: 1.5; }
.sc-ink { fill: none; stroke: var(--ink-dim); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.sc-tick { fill: none; stroke: var(--mint); stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.scene .fx-draw { stroke-dasharray: 1; stroke-dashoffset: 0; }
.scene .fx-drop, .scene .fx-glint { opacity: 0; }
.scene [class*="fx-"] { transform-box: fill-box; transform-origin: center; }

/* ---- skeletons that match the primitives ------------------------------------
   <div class="skeleton skeleton-stat"></div>  while an htmx stat loads
   .skeleton-card / .skeleton-row / .skeleton-line.w-40|w-60|w-80 / .skeleton-circle */
.skeleton-stat { height: 76px; border-radius: 0; }
.skeleton-card { min-height: 120px; border-radius: var(--radius); }
.skeleton-row { height: 42px; border-radius: var(--radius-sm); margin: 6px 0; }
.skeleton-line.w-40 { width: 40%; }
.skeleton-line.w-60 { width: 60%; }
.skeleton-line.w-80 { width: 80%; }
.skeleton-circle { width: 44px; height: 44px; border-radius: 50%; }

/* ---- toasts on glass ---------------------------------------------------- */
.toast {
  background: var(--glass-grad), var(--ground-raise);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  box-shadow: inset 0 1px 0 rgba(var(--overlay-rgb), 0.06), 0 14px 40px rgba(var(--shadow-rgb), calc(0.55 * var(--shadow-k)));
}

/* ---- focus rings on the new interactive shapes ------------------------- */
.btn:focus-visible, .btn-ember:focus-visible, .btn-ghost:focus-visible { outline-offset: 3px; }
.eyebrow-pill:focus-visible, .ticker-chip:focus-visible, .stat:focus-visible,
.progress-rail a:focus-visible, .scene:focus-visible, .step-dot:focus-visible { outline-offset: 3px; border-radius: 999px; }
.stat:focus-visible, .scene:focus-visible { border-radius: var(--radius-sm); }

/* ---- shell refinements ---------------------------------------------------
   The rail's hairline becomes a gradient (ember at the top where the
   wordmark sits), the active item carries a faint glow behind the sliding
   .nav-indicator, and the footer (user, sign out) speaks more quietly. */
@media (min-width: 861px) {
  .rail { border-right-color: transparent; }
  .rail::after {
    content: ""; position: absolute; top: 0; bottom: 0; right: 0; width: 1px; pointer-events: none;
    background: linear-gradient(180deg, transparent 0%, rgba(255, 163, 51, 0.4) 14%,
      rgba(var(--overlay-rgb), 0.09) 50%, rgba(255, 107, 0, 0.22) 84%, transparent 100%);
  }
}
.nav a.active { box-shadow: inset 0 0 0 1px rgba(255, 163, 51, 0.08), 0 0 22px rgba(255, 107, 0, 0.1); }
.rail-user { font-size: 10.5px; }
.btn-ghost { border-color: var(--hairline); color: var(--ink-faint); }
.btn-ghost:hover { color: var(--ink); border-color: var(--ember-ring); }

/* ---- SIMPLE: one sans everywhere -------------------------------------------
   Founder decision, 24 Sep 2026: labels in the body face with light tracking,
   money and stat figures in the body face with tabular figures; the mono
   face stays only on references and hashes, where alignment matters. */
.k-label, .stat-label, .eyebrow-pill, .chip, th, .queue-meta .k-label { font-family: var(--font-body); letter-spacing: 0.05em; font-weight: 600; }
.k-label, .stat-label, th { text-transform: uppercase; font-size: 11px; }
.money, .stat-value, .amount, .conf-value, .ccy { font-family: var(--font-body); font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
/* Dates, counts and basis points now sit in table cells and fact values in the body face
   (the FX and pool pages dropped .mono there): tabular figures keep those columns aligned. */
table.ledger td, dl.facts dd { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* The four SIMPLE selectors above are single-class and LOSE on specificity to
   rules written earlier in this file, when the product had two display faces:
   bare `th` loses to `table.ledger th`, `.k-label` loses to the form-label
   rule's :not() chain, and .stat-delta / .ps-meta were never named at all.
   So until 28 Sep every table header, form label, stat delta and process-step
   meta still rendered in the mono face and the founder's decision only half
   landed.  These three rules re-state it at the specificity that wins.
   `:not(.mono)` leaves the mono face where a span asks for it by name — an
   import's sha on the progress strip, a bank reference. */
table.ledger th { font-family: var(--font-body); letter-spacing: 0.05em; font-weight: 600; }
:where(form) label:not(.chip):not(.btn):not(.toggle):not(.dropzone) { font-family: var(--font-body); letter-spacing: 0.05em; }
.stat-delta:not(.mono), .ps-meta:not(.mono) { font-family: var(--font-body); letter-spacing: 0.04em; }
/* A small ring's strong is a count ("2/5 proofs", "3 of 3"), not a reference, so it
   follows the doctrine too; the large ring's strong is already in the display face. */
.ring-sm .ring-label strong { font-family: var(--font-body); font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
/* A column header is a label, never a reference, so the doctrine's bare `th` above meant
   to carry it — but `table.ledger th` outranks it and kept every header in mono, a mono
   header sitting straight above body-face figures. Same selector, so the doctrine wins. */
table.ledger th:not(.mono) { font-family: var(--font-body); font-size: 11px; font-weight: 600; letter-spacing: 0.05em; }
/* The doctrine for the figure components, stated once here rather than in
   console.css and portal.css both: a proc-step's meta ("3 on file"), the stat
   pill, a money stat value (.stat-value.money outranks the block above by
   specificity), the postings ticker and notification times. Moved out of the
   two app stylesheets, which left a page on an obsidian.css-only shell
   (accounts/security.html, offline.html, verify/page.html) in the mono face. */
.ps-meta:not(.mono), .stat-delta, .stat-value.money, .ticker-chip, .notif-meta { font-family: var(--font-body); font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
.stat-delta { font-weight: 600; letter-spacing: 0.02em; }
/* A reference inside a ticker chip stays mono — but the marketplace board puts a
   word there ("Buy" / "Sell"), so the mono face is opt-out, not blanket. */
.ticker-chip b:not(.side) { font-family: var(--font-mono); }
