/* ============================================================================
   The Nijsters Hub: shared CSS foundation
   One source. No drift.

   Load order on every page: this file, then the page's own <style>.
   During reconciliation, page-local rules may still override; legacy token
   and class names are aliased here so existing component, chart and auth
   code keeps working untouched while the palette, type, spacing and chrome
   become identical across every app.
   ========================================================================= */

/* --- Editorial voice (Law 1). Functional voice is Hanken, loaded per page. */
@font-face { font-family:'Migra'; src:url('../fonts/Migra-Extrabold.woff2') format('woff2'); font-weight:800; font-style:normal; font-display:swap; }
@font-face { font-family:'Migra'; src:url('../fonts/Migra-Extralight.woff2') format('woff2'); font-weight:200; font-style:normal; font-display:swap; }
@font-face { font-family:'Migra'; src:url('../fonts/MigraItalic-ExtraboldItalic.woff2') format('woff2'); font-weight:800; font-style:italic; font-display:swap; }
@font-face { font-family:'Migra'; src:url('../fonts/MigraItalic-ExtralightItalic.woff2') format('woff2'); font-weight:200; font-style:italic; font-display:swap; }

:root {
  /* ---- Primitive anchors (locked, from the brand) -------------------- */
  --ground:    oklch(0.18 0.018 305);
  --ground-up: oklch(0.22 0.018 305);
  --bone:      oklch(0.94 0.018 80);
  --accent:    oklch(0.72 0.16 38);
  --accent-d:  oklch(0.62 0.18 38);
  --warn:      oklch(0.62 0.18 22);
  --error:     oklch(0.70 0.16 28);

  /* ---- Semantic roles (components consume these) -------------------- */
  --surface:        var(--ground);
  --surface-raised: var(--ground-up);
  --text:           var(--bone);
  --text-secondary: oklch(0.94 0.018 80 / 0.78);
  --text-muted:     oklch(0.94 0.018 80 / 0.55);
  --text-faint:     oklch(0.94 0.018 80 / 0.32);
  --line:           oklch(0.94 0.018 80 / 0.16);
  --line-soft:      oklch(0.94 0.018 80 / 0.08);
  --veil:           oklch(0.94 0.018 80 / 0.10);
  --signal:         var(--accent);
  --signal-strong:  var(--accent-d);
  --danger:         var(--warn);
  --field-error:    var(--error);

  /* ---- Data-visualisation ramp (Law 2: bone L-steps, no hue palette) */
  --series-1:          oklch(0.52 0.022 75);
  --series-2:          oklch(0.70 0.015 70);
  --series-3:          oklch(0.92 0.020 80);
  --series-ref:        oklch(0.72 0.16 38 / 0.55);
  --series-ref-strong: oklch(0.78 0.18 38);

  /* The legacy alias layer (--ink/--rule/--stack-*/--temp*) is GONE.
     It was strangler scaffolding while pages migrated; every page +
     the two chart-JS colour maps now consume canonical tokens
     directly. Nothing aliases anything. */

  /* Inverted section (canonical) — public landing's .section-2 only */
  --inv-ground: var(--bone);
  --inv-ink:    var(--ground);
  --inv-soft:   oklch(0.18 0.018 305 / 0.55);
  --inv-mute:   oklch(0.18 0.018 305 / 0.35);
  --inv-faint:  oklch(0.18 0.018 305 / 0.22);

  /* ---- Space — 4pt rhythm + the shared page frame ------------------- */
  --space-2xs:4px;  --space-xs:8px;   --space-sm:12px;  --space-md:16px;
  --space-lg:24px;  --space-xl:32px;  --space-2xl:48px; --space-3xl:64px;
  --space-4xl:96px;
  --frame-x: clamp(24px, 5vw, 96px);
  --frame-y: var(--space-md);
  /* Density rhythm (one rule for the whole Hub, DESIGN_SYSTEM §5.3):
     a ruled band sits --rhythm-tight off its content; major blocks are
     --rhythm-block apart. Nothing in the chrome uses a larger gap. */
  --rhythm-tight: var(--space-xs);
  --rhythm-block: var(--space-lg);

  /* ---- Type scale (fixed rem, app UI) ------------------------------ */
  --t-eyebrow:0.5625rem;  --t-meta:0.6875rem;  --t-body:0.875rem;
  --t-lead:1rem;          --t-h3:1.5rem;       --t-h2:2.25rem;
  --t-h1:4rem;
  /* --t-display is the LANDING voice only (index.html). Authenticated
     Hub apps use --t-app-title in the ribbon head. Density doctrine,
     ratified 2026-05-16. */
  --t-display:   clamp(3rem, 9vw, 6rem);
  --t-grace:     clamp(1.5rem, 3vw, 2.5rem);
  --t-app-title: var(--t-h3);

  /* ---- Motion (Law 4) ---------------------------------------------- */
  --dur-fast:150ms; --dur-mid:250ms; --dur-slow:450ms;
  --ease-out: cubic-bezier(0.25, 1, 0.5, 1);
  --ease-in:  cubic-bezier(0.55, 0, 0.6, 0.2);

  /* ---- Depth (Law 3: none) + scrim + z ----------------------------- */
  --radius: 0;
  --scrim:  oklch(0.12 0.018 305 / 0.6);
  --z-base:0; --z-sticky:200; --z-dialog:400; --z-notify:500; --z-cursor:9999;
}

/* --- Body baseline (page <style> may restate; harmless) -------------- */
body {
  font-family:'Hanken Grotesk', sans-serif;
  background: var(--surface);
  color: var(--text);
}

/* --- Custom cursor (Law 4) — one canonical size, was 32/34 -> 34 ----- */
.cursor {
  position:fixed; width:8px; height:8px; background:var(--text);
  border-radius:50%; pointer-events:none; z-index:var(--z-cursor);
  transform:translate(-50%,-50%);
}
.cursor-ring {
  position:fixed; width:34px; height:34px;
  border:1px solid var(--text-faint); border-radius:50%;
  pointer-events:none; z-index:calc(var(--z-cursor) - 1);
  transform:translate(-50%,-50%);
  transition:width var(--dur-mid) var(--ease-out),
             height var(--dur-mid) var(--ease-out);
}
@media (pointer:coarse){ .cursor, .cursor-ring { display:none; } }

/* ============================================================================
   App-Shell (§5) — the four-region frame every Hub app inhabits.
   Scoped to .hub-shell so the public landing's own masthead is untouched.
   ========================================================================= */
.hub-shell {
  display:flex; justify-content:space-between; align-items:center;
  padding: var(--frame-y) var(--frame-x);
}
.hub-shell .brand { display:flex; gap:var(--space-lg); align-items:baseline; }
/* Right-side shell actions. A module with a settings destination slots
   its cog here, before Sign out; Settings is a destination, not a data
   lens, so it lives at shell level and never in the view-switch. */
.hub-shell .shell-actions { display:flex; align-items:center; gap:var(--space-lg); }

.wordmark, .locator, .signout, .shell-cog {
  font-family:'Hanken Grotesk', sans-serif;
  font-size:var(--t-eyebrow); text-transform:uppercase;
  color:var(--text-muted); font-weight:500;
  text-decoration:none; background:none; border:0; padding:0;
  transition:color var(--dur-mid) var(--ease-out);
}
.wordmark { letter-spacing:0.5em;  color:var(--text-secondary); }
.locator  { letter-spacing:0.35em; }
.signout  { letter-spacing:0.3em; }
/* Glyph, not tracked text: a quiet inside-baseline affordance. */
.shell-cog { font-size:var(--t-meta); line-height:1; }
.shell-cog[aria-current="true"] { color:var(--signal); }
.wordmark:hover, .locator:hover, .signout:hover, .shell-cog:hover { color:var(--text); }
@media (pointer:fine){ .wordmark, .locator, .signout, .shell-cog { cursor:none; } }

/* --- Family Hub jump menu (js/hub-header.js mountJumpMenu) -----------------
   A hover-revealed contents list of the live Hub apps. Reads like a
   table of contents under a masthead, not an OS dropdown: Law 3 holds
   (no radius, no shadow) — a solid raised ground + a hairline border is
   what lifts it off the page. Desktop only; the menu rules live entirely
   inside the hover/fine query so touch never renders it. */
.hub-jump { position:relative; display:inline-flex; align-items:baseline; }
.hub-jump__menu { display:none; }

@media (hover:hover) and (pointer:fine) {
  .hub-jump__menu {
    display:block; position:absolute; top:100%; left:0;
    z-index:var(--z-sticky); min-width:max-content;
    padding:var(--space-xs) 0;
    background:var(--surface-raised);
    border:1px solid var(--line);
    opacity:0; visibility:hidden; transform:translateY(-4px);
    transition:opacity var(--dur-mid) var(--ease-out),
               transform var(--dur-mid) var(--ease-out),
               visibility 0s linear var(--dur-mid);
  }
  .hub-jump[data-open="true"] .hub-jump__menu {
    opacity:1; visibility:visible; transform:translateY(0);
    transition:opacity var(--dur-mid) var(--ease-out),
               transform var(--dur-mid) var(--ease-out),
               visibility 0s;
  }

  .hub-jump__row {
    position:relative; display:flex; align-items:baseline;
    gap:var(--space-md);
    /* left padding doubles as the marker gutter — no layout shift */
    padding:var(--space-2xs) var(--space-lg);
    font-family:'Hanken Grotesk', sans-serif;
    text-decoration:none; white-space:nowrap;
  }
  .hub-jump__row .hj-name {
    font-size:var(--t-body); letter-spacing:0.02em;
    color:var(--text-secondary);
    transition:color var(--dur-fast) var(--ease-out);
  }
  .hub-jump__row .hj-tag {
    margin-left:auto; font-size:var(--t-meta);
    text-transform:lowercase; letter-spacing:0.06em;
    color:var(--text-faint);
  }
  /* The one scarce accent: an ochre tick in the gutter meaning
     "where you are, or where you're about to go." */
  .hub-jump__row::before {
    content:""; position:absolute;
    left:calc(var(--space-lg) - var(--space-sm));
    top:50%; width:5px; height:5px; margin-top:-2.5px;
    background:var(--signal);
    opacity:0; transform:translateX(-3px);
    transition:opacity var(--dur-fast) var(--ease-out),
               transform var(--dur-fast) var(--ease-out);
  }
  a.hub-jump__row:hover .hj-name,
  a.hub-jump__row:focus-visible .hj-name { color:var(--text); }
  a.hub-jump__row:hover::before,
  a.hub-jump__row:focus-visible::before { opacity:1; transform:translateX(0); }

  .hub-jump__row.is-current { cursor:default; }
  .hub-jump__row.is-current .hj-name { color:var(--text-muted); }
  .hub-jump__row.is-current::before { opacity:1; transform:none; }

  a.hub-jump__row { cursor:none; }
}

@media (prefers-reduced-motion:reduce) {
  .hub-jump__menu,
  .hub-jump__row .hj-name,
  .hub-jump__row::before { transition-duration:0.01ms; }
  @media (hover:hover) and (pointer:fine) {
    .hub-jump__menu { transform:none; }
    .hub-jump[data-open="true"] .hub-jump__menu { transform:none; }
  }
}

/* ============================================================================
   Content well (§5.3, density doctrine, ratified 2026-05-16)
   Hub apps are dense instruments: data must be above the fold and use the
   screen width. .hub-main is the full-frame, left-aligned well for the
   Ledger / Dashboard / Library / Registry archetypes. .flow-main is the
   centred narrow column reserved ONLY for the Flow archetype (auth).
   ========================================================================= */
.hub-main {
  flex: 1;
  padding: var(--rhythm-tight) var(--frame-x) var(--space-2xl);
  display: flex; flex-direction: column; gap: var(--rhythm-block);
}
.flow-main {
  flex: 1;
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-2xl) var(--frame-x) var(--space-3xl);
}
.flow-main > * { width: 100%; max-width: 460px; }
/* Flow archetype (§7.E) head: a compact centred column. NOT the landing
   display voice; an arrival/task screen at app scale. A lede IS allowed
   here (it guides the task) — the no-lede rule is for data apps only. */
.flow-col { display: flex; flex-direction: column; gap: var(--space-lg); }
.flow-col h1 {
  font-family: 'Migra', serif; font-weight: 800;
  font-size: var(--t-h2); line-height: 1.05; letter-spacing: -0.02em;
  color: var(--text);
}
.flow-col h1 .it { font-weight: 200; font-style: italic; }
.lede { font-size: var(--t-body); line-height: 1.7; color: var(--text-muted); max-width: 46ch; }

/* ============================================================================
   The page framework (DESIGN_SYSTEM.md §1) — the ONE header every Hub
   app inherits. Rendered by js/hub-header.js from a per-app config;
   no app hand-writes this markup, so it cannot drift. .hub-head holds
   the three lower zones (ribbon, strip, control row) between the shell
   row and the content well, aligned to the same frame inset.
   ========================================================================= */
.hub-head {
  display:flex; flex-direction:column; gap:var(--rhythm-tight);
  padding:0 var(--frame-x);
}

/* Zone 4 control row: optional contextual line (legend / summary) on
   the left, the tab bar pinned hard right, a single hairline UNDER the
   row so the tabs sit ABOVE the line and that line closes the header.
   margin-left:auto pins the tabs right whether or not the left slot
   has content, so it cannot regress left (that bug shipped once). */
.control-row {
  display:flex; align-items:baseline; gap:var(--space-lg);
  padding-bottom:var(--rhythm-tight);
  border-bottom:1px solid var(--line);
}
.control-row .cr-left      { flex:1; }
.control-row .view-switch  { margin-left:auto; }

/* Module actions slot — the SHARED disclosure for app-level commands
   (Import CSV, Build export, Verify invoice, ...). Sits to the right
   of the tab bar; hidden until the app calls hub.setActions(items).
   One slot, one pattern, every app. The popover floats over content
   so it does not push the page on toggle. */
.hub-actions { position:relative; margin-left:var(--space-lg); }
.hub-actions-toggle {
  background:none; border:none; padding:0; font:inherit;
  font-size:var(--t-eyebrow); letter-spacing:0.3em; text-transform:uppercase;
  color:var(--text-muted);
  display:inline-flex; align-items:center; gap:var(--space-xs);
  transition:color var(--dur-mid) var(--ease-out);
}
.hub-actions-toggle:hover { color:var(--text); }
.hub-actions-toggle[aria-expanded="true"] { color:var(--accent); }
.hub-actions-toggle:focus { outline:none; }
.hub-actions-toggle:focus-visible { outline:1px solid var(--text-secondary); outline-offset:4px; }
@media (pointer:fine){ .hub-actions-toggle { cursor:none; } }
.hai-chev {
  display:inline-block; width:0.45em; height:0.45em;
  border-right:1px solid currentColor; border-bottom:1px solid currentColor;
  transform:translateY(-0.15em) rotate(45deg);
  transition:transform var(--dur-mid) var(--ease-out);
}
.hub-actions-toggle[aria-expanded="true"] .hai-chev {
  transform:translateY(0.05em) rotate(-135deg);
}
.hub-actions-pop {
  position:absolute; top:calc(100% + var(--space-sm)); right:0;
  min-width:18rem; max-width:24rem;
  background:var(--surface);
  border:1px solid var(--line);
  padding:var(--space-xs) 0;
  /* Must paint above sticky table heads (also at --z-sticky) — without
     this they bleed through the popover and the meta lines vanish. */
  z-index:var(--z-dialog);
  opacity:0; transform:translateY(-4px); pointer-events:none;
  transition:opacity var(--dur-mid) var(--ease-out),
             transform var(--dur-mid) var(--ease-out);
}
.hub-actions[data-open="true"] .hub-actions-pop {
  opacity:1; transform:translateY(0); pointer-events:auto;
}
.hub-action-item {
  display:flex; flex-direction:column; gap:var(--space-2xs);
  width:100%; text-align:left;
  background:none; border:none;
  padding:var(--space-sm) var(--space-md);
  font:inherit; color:var(--text-secondary);
  transition:background var(--dur-mid) var(--ease-out),
             color var(--dur-mid) var(--ease-out);
}
.hub-action-item:hover, .hub-action-item:focus-visible {
  outline:none; background:var(--veil); color:var(--text);
}
@media (pointer:fine){ .hub-action-item { cursor:none; } }
.hai-label {
  font-size:var(--t-body); font-weight:600; letter-spacing:0.01em;
}
.hai-meta {
  font-size:var(--t-meta); color:var(--text-faint);
}

/* ============================================================================
   Page-head (§6.2) — the RIBBON. One identity assertion per view, on a
   single hairline-ruled band, ~40px tall. Eyebrow-locator, then the
   app-scale Migra title, then the italic grace note, baseline-aligned on
   one line. No --t-display here (that voice is the public landing only).
   No lede: the content below is self-evident.
   ========================================================================= */
.eyebrow {
  font-family:'Hanken Grotesk', sans-serif;
  font-size:var(--t-eyebrow); letter-spacing:0.35em; text-transform:uppercase;
  color:var(--text-muted); font-weight:500;
}
.page-head {
  display:flex; align-items:baseline; gap:var(--space-md);
  flex-wrap:wrap;
  padding-bottom:var(--rhythm-tight);
  border-bottom:1px solid var(--line);
}
.page-head .eyebrow { margin-right:var(--space-md); }
.page-head .title {
  font-family:'Migra', serif; font-weight:800;
  font-size:var(--t-app-title); line-height:1; letter-spacing:-0.01em;
  color:var(--text);
}
.page-head .grace {
  font-family:'Migra', serif; font-weight:200; font-style:italic;
  font-size:var(--t-lead); color:var(--text-muted);
}
/* A page-head may carry trailing controls (view-switch, stepper) pushed
   to the far end of the ribbon. */
.page-head .head-actions { margin-left:auto; display:flex; align-items:baseline; gap:var(--space-lg); }

/* ============================================================================
   Core primitives (§6) — the fixed vocabulary. During reconciliation a
   page that still defines its own version overrides these (later <style>);
   pages that don't, get the canonical one. Phase 2 reclasses internals
   onto these and deletes the local copies.
   ========================================================================= */

/* §6.3 Action hierarchy ------------------------------------------------ */
.btn {
  font-family:'Hanken Grotesk', sans-serif; font:inherit;
  font-size:var(--t-eyebrow); letter-spacing:0.3em; text-transform:uppercase;
  background:none; border:1px solid var(--line); color:var(--text-secondary);
  padding:var(--space-xs) var(--space-md); border-radius:var(--radius);
  transition:color var(--dur-mid) var(--ease-out),
             border-color var(--dur-mid) var(--ease-out);
}
.btn:hover:not(:disabled){ color:var(--text); border-color:var(--text-secondary); }
.btn:disabled{ color:var(--text-faint); border-color:var(--line-soft); }
.btn--primary:hover:not(:disabled){ border-color:var(--signal); color:var(--text); }
.btn--ghost{ border-color:transparent; padding-left:0; padding-right:0; }
.btn--danger{ border-color:var(--danger); color:var(--danger); }
.btn--danger:hover:not(:disabled){ background:var(--danger); color:var(--surface); }
.link {
  color:var(--text-muted); text-decoration:none;
  transition:color var(--dur-mid) var(--ease-out);
}
.link:hover{ color:var(--text); }
@media (pointer:fine){ .btn, .link { cursor:none; } }

/* §6.4 View-switch ----------------------------------------------------- */
.view-switch { display:flex; gap:var(--space-lg); align-items:baseline; }
.view-switch button {
  font:inherit; font-size:var(--t-eyebrow); letter-spacing:0.3em;
  text-transform:uppercase; background:none; border:0; padding:0;
  color:var(--text-muted);
  transition:color var(--dur-mid) var(--ease-out);
}
.view-switch button[aria-current="true"]{ color:var(--signal); }
.view-switch button:hover{ color:var(--text); }
@media (pointer:fine){ .view-switch button { cursor:none; } }

/* Zone 3 stat strip (DESIGN_SYSTEM.md §1) — THE single canonical strip.
   App-wide high-level figures, re-computed for the active tab; never
   the page body. One bold hero left, then a variable number of smaller
   secondaries right (as many as fit, NOT a fixed slot grid). This is
   the utilities .ttm contract, generalised: .strip-stats is auto-fit
   so the secondary count is free. No other strip style exists. */
.hub-strip {
  display:grid;
  grid-template-columns:minmax(260px,1fr) minmax(0,2fr);
  gap:var(--space-2xl); align-items:end;
  /* No border-top: the ribbon's border-bottom already rules this
     junction. A strip border here would be a redundant double line. */
  padding-top:var(--rhythm-tight);
}
.strip-hero { display:flex; flex-direction:column; gap:var(--space-2xs); }
.strip-hero .hero-value {
  font-size:var(--t-h2); font-weight:600; color:var(--text);
  font-variant-numeric:tabular-nums; letter-spacing:-0.01em; line-height:1;
}
.strip-hero .hero-label {
  font-size:var(--t-eyebrow); letter-spacing:0.35em; text-transform:uppercase;
  color:var(--text-muted);
}
.strip-hero .hero-delta {
  font-size:var(--t-body); color:var(--text-secondary);
  font-variant-numeric:tabular-nums;
}
.strip-hero .hero-delta .glyph { color:var(--text); margin-right:var(--space-2xs); }
.strip-stats {
  display:grid; grid-template-columns:repeat(auto-fit,minmax(0,1fr));
  gap:var(--space-lg); text-align:right;
}
.strip-stat { display:flex; flex-direction:column; gap:var(--space-2xs); }
.strip-stat .ss-label {
  font-size:var(--t-eyebrow); letter-spacing:0.35em; text-transform:uppercase;
  color:var(--text-muted); font-weight:500;
}
.strip-stat .ss-value {
  font-size:var(--t-h3); font-variant-numeric:tabular-nums;
  color:var(--text); font-weight:500;
}
.strip-stat .ss-context {
  font-family:'Migra', serif; font-style:italic; font-weight:200;
  font-size:var(--t-meta); color:var(--text-muted);
}
@media (max-width:720px){
  .hub-strip { grid-template-columns:1fr; gap:var(--space-lg); }
  .strip-stats {
    text-align:left; gap:var(--space-sm);
    grid-template-columns:repeat(auto-fit,minmax(120px,1fr));
  }
}

/* Data table: THE locked standard for every table in every Hub app.
   These exact values are the contract. Derived
   from the utilities "All bills" table, which is the approved reference.
   Do not override size/padding/colour per page; use the cell roles.

   Cell roles:
     .label   first/identity column — upright, weight 500, --t-lead
     .num     numeric column — tabular, right-aligned
     .strong  emphasis column (totals) — brighter, heavier
   Highlight: a row gets .is-cued (or :hover) → --veil wash + text lifts. */
.data-table { width:100%; border-collapse:collapse; }
.data-table thead th {
  font-size:var(--t-body); letter-spacing:0.3em; text-transform:uppercase;
  color:var(--text-muted); font-weight:500; text-align:left;
  padding:var(--space-xs) var(--space-md);
  border-bottom:1px solid var(--line);
  position:sticky; top:0; background:var(--surface); z-index:var(--z-sticky);
}
.data-table tbody td {
  font-size:var(--t-body); color:var(--text-secondary);
  padding:var(--space-2xs) var(--space-md);
  border-bottom:1px solid var(--line-soft);
  vertical-align:baseline;
  transition:color var(--dur-mid) var(--ease-out),
             background var(--dur-mid) var(--ease-out);
}
.data-table th:first-child, .data-table td:first-child { padding-left:0; }
.data-table th:last-child,  .data-table td:last-child  { padding-right:0; }
.data-table tbody tr:last-child td { border-bottom:1px solid var(--line); }
.data-table tbody tr:hover td,
.data-table tbody tr.is-cued td { background:var(--veil); color:var(--text); }
.data-table .num    { text-align:right; font-variant-numeric:tabular-nums; }
.data-table .label  {
  color:var(--text); white-space:nowrap;
}
.data-table .strong { color:var(--text); font-weight:600; }
.data-table .muted  { color:var(--text-muted); }
/* Totals footer — a single grand-total row. Set apart by line + type,
   no fill: a single heavier rule (--line, the same weight as the head
   rule) above it, the label dropped to a muted tracked eyebrow (rhymes
   with the sticky head so head and foot bookend the data), and the
   figures bumped one step to --t-lead so the sum carries grand-total
   typographic weight, like a KPI at the foot of the column. The
   border-top collapses with the heavier tbody:last-child border into
   one clean rule (border-collapse). One definition, every Hub table
   with a true column sum inherits it (NOT running ledgers). */
.data-table tfoot td {
  /* Breathing room ABOVE (separates the total from the data), and the
     SAME bottom treatment as a normal last row: --space-2xs inset AND
     the closing hairline (border-bottom). Without the rule the eye
     reads the table as ending at the Total row's ink rather than at a
     clean line, and the larger --t-lead line-box pushes that visual
     end ~14px lower than a tbody:last-child + border ends, making the
     next section-head's shared --rhythm-block look airier. The border
     restores parity. */
  padding:var(--space-sm) var(--space-md) var(--space-2xs);
  border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);
  font-size:var(--t-lead);
  color:var(--text); font-weight:600;
  font-variant-numeric:tabular-nums;
}
/* Scroll container for long tables: sticky head stays, body scrolls. */
.data-table-scroll {
  max-height:60vh; overflow-y:auto;
  scrollbar-width:thin; scrollbar-color:var(--text-faint) transparent;
}
.data-table-scroll::-webkit-scrollbar { width:6px; }
.data-table-scroll::-webkit-scrollbar-thumb { background:var(--text-faint); border-radius:3px; }

/* Expandable row — a parent data row that discloses a detail row beneath
   it. Click anywhere on the parent OR Enter/Space toggles; there is NO
   hover-expand (a list that reflows under the pointer reads as jitter).
   The chevron in the first cell is the only affordance: muted at rest,
   it rotates and lifts to accent when open (a sanctioned scarce use,
   like the active tab). The detail row is one td[colspan]; its inner
   wrapper animates grid-template-rows 0fr->1fr so height moves with no
   measurement and the collapsed state has zero height and no border. */
.data-table tr.row-parent { cursor:pointer; }
.data-table tr.row-parent:focus-visible {
  outline:1px solid var(--text-secondary); outline-offset:-1px;
}
@media (pointer:fine){ .data-table tr.row-parent { cursor:none; } }
.row-chevron {
  display:inline-block; width:0.45em; height:0.45em;
  margin-right:var(--space-sm); vertical-align:middle;
  border-right:1px solid var(--text-muted);
  border-bottom:1px solid var(--text-muted);
  transform:rotate(-45deg);
  transition:transform var(--dur-mid) var(--ease-out),
             border-color var(--dur-mid) var(--ease-out);
}
.data-table tr.row-parent[aria-expanded="true"] .row-chevron {
  transform:rotate(45deg); border-color:var(--accent);
}
.data-table tr.row-detail > td { padding:0; border:0; overflow:hidden; }
.data-table tr.row-detail:hover > td { background:none; }
/* The 0fr->1fr grid-rows trick does NOT collapse inside a <td>: table
   cells size to their content's intrinsic height, which defeats it and
   the first detail line bleeds out while "collapsed". A max-height +
   overflow:hidden accordion is the technique that is reliable inside
   table cells. The ceiling only has to exceed real detail content (a
   fee ledger, never tall); content is never clipped open. */
.row-detail__inner {
  max-height:0; overflow:hidden;
  transition:max-height var(--dur-slow) var(--ease-out);
  /* Detail content is real reading matter (a fee ledger, notes), not a
     caption: it gets body size, never --t-meta. Apps must not shrink it. */
  font-size:var(--t-body); line-height:1.5; color:var(--text-secondary);
}
.data-table tr.row-detail[data-open="true"] .row-detail__inner {
  max-height:40rem;
}
@media (prefers-reduced-motion:reduce){
  .row-detail__inner { transition:none; }
}

/* Section head — the shared chrome above any table or panelled block:
   a tracked-uppercase title, an optional muted meta count, optional
   trailing actions, closed by one hairline. Promoted verbatim from the
   airbnb local copy so every app's section chrome is identical. */
.section-head {
  display:flex; align-items:baseline; justify-content:space-between;
  gap:var(--space-md);
  /* One rhythm: a doubled block gap above (2x --rhythm-block) so the
     break between sections reads as a real pause, a tight gap to its
     own table head below. No oscillation, no per-app spacing. */
  margin:var(--space-2xl) 0 var(--rhythm-tight);
  padding-bottom:var(--rhythm-tight);
  border-bottom:1px solid var(--line);
}
.section-head .title {
  font-family:'Hanken Grotesk', sans-serif;
  font-size:var(--t-lead); letter-spacing:0.2em; text-transform:uppercase;
  color:var(--text-secondary); font-weight:600;
}
.section-head .meta {
  font-size:var(--t-meta); color:var(--text-faint);
  margin-right:auto; margin-left:var(--space-md);
}
.section-head .actions { display:flex; gap:var(--space-md); }
.section-head .action {
  background:none; border:none; padding:0; font:inherit;
  font-size:var(--t-eyebrow); letter-spacing:0.3em; text-transform:uppercase;
  color:var(--text-muted);
  transition:color var(--dur-mid) var(--ease-out);
}
.section-head .action:hover { color:var(--text); }
.section-head .action[aria-expanded="true"] { color:var(--accent); }
@media (pointer:fine){ .section-head .action { cursor:none; } }

/* Block head — a chart/KPI heading in the DENSE app scale. NOT the
   public landing's Migra display: that voice is the landing's only and
   app UI uses no fluid type (AGENT.md #14). A tabular Hanken figure at
   the stat-strip scale + a tracked eyebrow caption, so a chart heading
   sits in the same family and proportion as the strip and table head. */
.block-head { display:flex; flex-direction:column; gap:var(--space-2xs); }
.block-head .bh-value {
  font-family:'Hanken Grotesk', sans-serif; font-weight:600;
  font-size:var(--t-h3); line-height:1; letter-spacing:-0.01em;
  color:var(--text); font-variant-numeric:tabular-nums;
}
.block-head .bh-cap {
  font-size:var(--t-eyebrow); letter-spacing:0.35em; text-transform:uppercase;
  color:var(--text-muted);
}

/* Card grid — THE canonical media/image grid (promoted from the photos
   library). Auto-fill 180px tracks, 4:5 frames, hairline-tight gap.
   This primitive is geometry + the image treatment only; per-app
   overlays (flags, counts, delete) stay app content over the frame. */
.card-grid {
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));
  gap:var(--space-2xs);
}
.card-grid__item {
  position:relative; aspect-ratio:4 / 5; overflow:hidden;
  background:var(--veil);
}
.card-grid__item img {
  width:100%; height:100%; object-fit:cover;
  filter:brightness(0.92) saturate(0.9);
  transition:filter var(--dur-mid) var(--ease-out),
             transform var(--dur-slow) var(--ease-out);
}
.card-grid__item:hover img {
  filter:brightness(1.02) saturate(1.02); transform:scale(1.03);
}

/* §6.8 Empty state ----------------------------------------------------- */
.empty-state {
  display:flex; align-items:center; justify-content:center;
  padding:var(--space-3xl) var(--space-lg);
  font-size:var(--t-lead); color:var(--text-muted); text-align:center;
}

/* §6.9 Form field ------------------------------------------------------ */
.field { display:flex; flex-direction:column; gap:var(--space-2xs); }
.field label {
  font-size:var(--t-eyebrow); letter-spacing:0.25em; text-transform:uppercase;
  color:var(--text-muted);
}
.field input, .field select, .field textarea {
  font-family:'Hanken Grotesk', sans-serif; font-size:var(--t-body);
  color:var(--text); background:none;
  border:1px solid var(--line); border-radius:var(--radius);
  padding:var(--space-sm); width:100%;
  transition:border-color var(--dur-mid) var(--ease-out);
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline:none; border-color:var(--text-secondary);
}
.field[data-error] input, .field[data-error] select { border-color:var(--field-error); }
.field .field-error { font-size:var(--t-meta); color:var(--field-error); }

/* §6.10 Badge / flag --------------------------------------------------- */
.badge {
  display:inline-flex; align-items:center; gap:var(--space-2xs);
  font-size:var(--t-eyebrow); letter-spacing:0.2em; text-transform:uppercase;
  color:var(--text-muted); padding:var(--space-2xs) var(--space-xs);
  border:1px solid var(--line); border-radius:var(--radius);
}
.badge--danger { color:var(--danger); border-color:var(--danger); }

/* §6.11 Tile — the Hub home grid, the system's growth surface. Hairline
   separators, Migra-italic label, no icons (Law 5). Status is a quiet
   state line, never accent: a module list is not "the one way forward"
   (Law 2). */
.hub-grid {
  display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr));
  gap:var(--space-xl) var(--space-2xl);
}
.hub-tile {
  border-top:1px solid var(--line); padding-top:var(--space-md);
  display:flex; flex-direction:column; gap:var(--space-xs);
  min-height:150px; text-decoration:none; color:inherit;
  transition:border-color var(--dur-mid) var(--ease-out);
}
a.hub-tile:hover { border-top-color:var(--text-secondary); }
.hub-tile h3 {
  font-family:'Migra', serif; font-style:italic; font-weight:200;
  font-size:var(--t-h3); color:var(--text);
}
.hub-tile p { font-size:var(--t-meta); color:var(--text-muted); line-height:1.6; }
.hub-tile .status {
  font-size:var(--t-eyebrow); letter-spacing:0.35em; text-transform:uppercase;
  color:var(--text-faint); margin-top:auto; padding-top:var(--space-sm);
}
.hub-tile .status.view {
  font-family:'Migra', serif; font-style:italic; font-weight:200;
  font-size:var(--t-meta); letter-spacing:0.02em; text-transform:lowercase;
  color:var(--text-muted);
}
.hub-tile.disabled { opacity:0.55; }
.hub-tile.disabled p { color:var(--text-faint); }
@media (pointer:fine){ a.hub-tile { cursor:none; } }

/* Chart standard: THE locked look for every chart in every Hub app.
   Plot/SVG geometry (height, margins, scales)
   stays in each module's JS, but the *visual* standard is enforced here
   on the emitted SVG so every chart reads as one instrument:
     axis + tick text  → --t-meta, --text-muted, Hanken (Law 1)
     gridlines / domain → --line-soft hairlines
     caption / legend   → --t-eyebrow tracked, --text-muted
     series colours     → the §4.1 bone ramp; ochre = reference only
   A chart sits in .chart-frame; its caption is .chart-caption. */
.chart-frame { display:flex; flex-direction:column; gap:var(--space-sm); min-width:0; }
.chart-caption {
  font-size:var(--t-eyebrow); letter-spacing:0.3em; text-transform:uppercase;
  color:var(--text-muted); font-weight:500;
}
/* Covers every chart container in the Hub: utilities (.chart-host,
   .strip-host) and airbnb (.trend-chart). One instrument. */
.chart-host svg, .strip-host svg, .trend-chart svg {
  display:block; max-width:100%; height:auto;
}
/* Law 1: never Migra inside a figure — Hanken on ALL chart text. This
   is safe to apply broadly (family only, not colour). */
.chart-host svg text, .strip-host svg text, .trend-chart svg text {
  font-family:'Hanken Grotesk', sans-serif !important;
}
/* Axis tick + axis label text is the muted meta tier. Scoped to axis
   groups ONLY — data-label marks (e.g. the ochre occupancy % labels)
   keep their own fill. The old blanket `svg text` recolour was wrong. */
.chart-host svg [aria-label$="-axis tick"] text,
.chart-host svg [aria-label$="-axis label"] text,
.strip-host svg [aria-label$="-axis tick"] text,
.strip-host svg [aria-label$="-axis label"] text,
.trend-chart svg [aria-label$="-axis tick"] text,
.trend-chart svg [aria-label$="-axis label"] text {
  font-size:var(--t-meta) !important; fill:var(--text-muted) !important;
}
/* Hairline gridlines + domain across every chart. */
.chart-host svg [stroke="currentColor"],
.strip-host svg [stroke="currentColor"],
.trend-chart svg [stroke="currentColor"] { stroke:var(--line-soft); }
.chart-host svg [aria-label$="grid"] line,
.strip-host svg [aria-label$="grid"] line,
.trend-chart svg [aria-label$="grid"] line { stroke:var(--line-soft) !important; }

/* §9.1 Synchronized chart guide + readout — the ONE shared chart hover
   instrument, driven by js/chart-sync.js. Every effect is a DOM overlay
   OUTSIDE the SVG (guide line) or an inline fill on existing rects:
   never a node add/remove inside a figure on move (mistake #12). The
   guide line is appended to the chart container, so containers are the
   positioning context. */
.chart-host, .strip-host, .trend-chart { position:relative; }
.month-guide {
  position:absolute; left:0; top:0; width:2px;
  background:var(--series-ref-strong); opacity:0; pointer-events:none;
  z-index:2; transition:opacity var(--dur-fast) var(--ease-out);
}
.month-guide.show { opacity:0.7; }
.chart-host svg g[aria-label="bar"] rect,
.trend-chart svg g[aria-label="bar"] rect {
  transition:fill var(--dur-fast) var(--ease-out);
}
/* The hovered category's detail, a flat hairline-bound box in the top
   layer (position:fixed escapes chart overflow). Not a modal, no
   pointer capture; it follows the active band. */
.chart-readout {
  position:fixed; z-index:var(--z-notify); pointer-events:none;
  background:var(--surface); border:1px solid var(--line);
  padding:var(--space-xs) var(--space-sm); max-width:300px;
  font-size:var(--t-meta); color:var(--text-secondary);
  font-variant-numeric:tabular-nums; line-height:1.5;
}
.chart-readout[hidden] { display:none; }
.chart-readout .cr-key {
  display:block; font-size:var(--t-eyebrow); letter-spacing:0.3em;
  text-transform:uppercase; color:var(--text); margin-bottom:var(--space-2xs);
}
.chart-readout .cr-row {
  display:flex; justify-content:space-between; gap:var(--space-lg);
}
.chart-readout .cr-row .cr-v { color:var(--text); }
@media (prefers-reduced-motion:reduce){
  .month-guide,
  .chart-host svg g[aria-label="bar"] rect,
  .trend-chart svg g[aria-label="bar"] rect { transition:none; }
}

/* §6.13 Notification stack (mounted by js/hub-notify.js) --------------- */
/* The ONE shared, non-blocking message surface for every Hub app. Flat,
   hairline-bound, bottom-anchored, never a modal. Severity is carried by
   the eyebrow WORD plus a single mark colour from the scarce token set,
   never a coloured fill: the brand rejects the alert-banner look. No app
   hand-writes this markup. */
.hub-notify-stack {
  position:fixed; z-index:var(--z-notify);
  right:max(var(--frame-x), env(safe-area-inset-right));
  bottom:max(var(--frame-y), env(safe-area-inset-bottom));
  width:min(380px, calc(100vw - 2 * var(--frame-x)));
  display:flex; flex-direction:column; gap:var(--space-sm);
  pointer-events:none;            /* stack ignores clicks; notes opt back in */
}

/* Each note sits in a collapsing grid row so the stack reflows smoothly
   (transform/opacity on the note, grid-rows on the slot) with no layout
   animation. */
.hub-note-slot {
  display:grid; grid-template-rows:1fr;
  transition:grid-template-rows var(--dur-mid) var(--ease-out);
}
.hub-note-slot.is-leaving { grid-template-rows:0fr; }

.hub-note {
  pointer-events:auto; overflow:hidden; min-block-size:0;
  display:grid; grid-template-columns:auto 1fr auto;
  gap:var(--space-sm); align-items:start;
  background:var(--surface-raised);
  border:1px solid var(--line); border-radius:var(--radius);
  padding:var(--space-md);
  opacity:0; transform:translateY(8px);
  transition:opacity var(--dur-mid) var(--ease-out),
             transform var(--dur-mid) var(--ease-out);
}
.hub-note.is-in { opacity:1; transform:none; }
.hub-note.is-leaving {
  opacity:0; transform:translateY(4px);
  transition:opacity var(--dur-fast) var(--ease-in),
             transform var(--dur-fast) var(--ease-in);
}
.hub-note[data-tone="error"] { border-color:var(--danger); }

/* Severity mark: a 7px square. A square reads as authored within the
   radius-0 system where a generic dot reads as template. */
.hub-note-mark {
  width:7px; height:7px; margin-top:0.3em; flex:none;
  background:var(--text-muted);          /* info: no accent spend */
}
.hub-note[data-tone="success"] .hub-note-mark { background:var(--signal); }
.hub-note[data-tone="warn"]    .hub-note-mark,
.hub-note[data-tone="error"]   .hub-note-mark { background:var(--danger); }

.hub-note-body { min-width:0; display:flex; flex-direction:column; gap:var(--space-2xs); }
.hub-note-eyebrow {
  font-size:var(--t-eyebrow); letter-spacing:0.2em; text-transform:uppercase;
  color:var(--text-muted); font-weight:600;
}
.hub-note[data-tone="success"] .hub-note-eyebrow { color:var(--signal); }
.hub-note[data-tone="warn"]    .hub-note-eyebrow,
.hub-note[data-tone="error"]   .hub-note-eyebrow { color:var(--danger); }
.hub-note-msg {
  font-size:var(--t-body); line-height:1.55; color:var(--text);
  overflow-wrap:anywhere;
}
.hub-note-actions { margin-top:var(--space-xs); display:flex; }

/* Dismiss: 14px glyph, 44px tap target via the inset pseudo (spatial law). */
.hub-note-x {
  position:relative; align-self:start; background:none; border:0; padding:0;
  font:inherit; font-size:14px; line-height:1; color:var(--text-faint);
  transition:color var(--dur-mid) var(--ease-out);
}
.hub-note-x::before { content:""; position:absolute; inset:-15px; }
.hub-note-x:hover { color:var(--text); }
.hub-note-x:focus-visible,
.hub-note .btn:focus-visible { outline:2px solid var(--signal); outline-offset:2px; }
@media (pointer:fine){ .hub-note-x { cursor:none; } }

/* Optional auto-dismiss timer: a hairline draining left, transform only.
   Decorative; the JS timeout is the authority. Pauses on hover/focus. */
.hub-note-timer {
  grid-column:1 / -1; height:1px; margin-top:var(--space-sm);
  background:var(--line); transform-origin:left center;
  animation:hub-note-drain linear var(--note-timer,6000ms) forwards;
}
.hub-note:hover .hub-note-timer,
.hub-note:focus-within .hub-note-timer { animation-play-state:paused; }
@keyframes hub-note-drain { from{transform:scaleX(1)} to{transform:scaleX(0)} }
@media (prefers-reduced-motion:reduce){ .hub-note { transform:none; } }

/* Reduced motion: honour the system preference (Law 4). */
@media (prefers-reduced-motion:reduce){
  *, *::before, *::after { transition-duration:0.01ms !important; animation-duration:0.01ms !important; }
}
