/* ============================================
   PersonalDash — overrides on top of configapa_mono.css.
   Currently scoped to the leaderboard table only.
   ============================================ */


/* --------------------------------------------
   Type scale: every visible font-size in the
   leaderboard view bumped by +10 px from the
   configapa_mono.css baseline.
   -------------------------------------------- */

:root {
    --size-body:        19px;   /* was 15 */
    --size-h1:          36px;   /* was 32 */
    --size-h2:          28px;   /* was 24 */
    --size-h3:          22px;   /* was 18 */
    --size-secondary:   20px;   /* was 16 */
    --size-stat-number: 40px;   /* was 36 */
    --size-footnote:    16px;   /* was 12 */

    /* Figure/telemetry monospace. --font-figure resolves to Open Sans (proportional),
       so the prior `--font-figure` declarations with a trailing monospace fallback never
       fired — figures rendered proportional. This is the real tabular stack; one mono family. */
    --font-mono: "Spline Sans Mono", ui-monospace, monospace;

    /* Shared hero metrics — the Biometric hero (header.hero) and the PhD
       Dashboard banner (.cal-hero) BOTH read these, so their title/subtitle
       sizes can never drift apart. Colors are likewise shared via
       configapa's --bg-hero / --color-hero-text / --color-inverse-text. */
    --hero-title-size:    var(--size-h1);   /* was 24px */
    --hero-subtitle-size: var(--size-h3);   /* was 16px */
}

/* --------------------------------------------
   Connection status banner.
   Shown only when the PostgREST read API is unreachable (js/connection-
   banner.js toggles [hidden]). Inverted ink strip — same dark-on-paper
   treatment as the hero (--bg-hero: var(--fg)) — so it reads as a deliberate
   system bar rather than decoration. Sticky so it stays visible on scroll.
   -------------------------------------------- */
#connection-banner {
    position: sticky;
    top: 0;
    z-index: 200;   /* above the admin panel (100) and info-pop (80) so an
                       offline state is never painted over */
    box-sizing: border-box;
    width: 100%;
    padding: 9px 16px;
    background: var(--fg);
    color: var(--bg);
    font-family: var(--font-body);
    font-size: var(--size-secondary);
    font-weight: 600;
    letter-spacing: 0.5px;
    text-align: center;
    border-bottom: 2px solid var(--bg);
}
#connection-banner[hidden] { display: none; }
/* Degraded (some streams unhealthy, transport still up): inverted so it reads
   as a warning distinct from a full outage, without leaving the mono system. */
#connection-banner[data-variant="warn"] {
    background: var(--bg);
    color: var(--fg);
    border-bottom: 2px solid var(--fg);
}
/* Snapshot (hosted deploy reading a published payload, not PostgREST): same
   inverted treatment as warn but lighter weight — a dateline, not an alarm. */
#connection-banner[data-variant="info"] {
    background: var(--bg);
    color: var(--fg);
    font-weight: 400;
    border-bottom: 1px solid var(--fg);
}

/* Hard-coded sizes inside configapa_mono.css that don't flow through tokens */
thead th               { font-size: 16px; }   /* was 12 */
footer                 { font-size: 16px; }   /* was 12 */
header.hero h1         { font-size: var(--hero-title-size); }    /* was 20 */
header.hero .hero-subtitle { font-size: var(--hero-subtitle-size); }  /* was 12 */
header.hero .hero-stamp    { font-size: 15px; }  /* was 11 */
header.hero .hero-arrow    { font-size: 14px; }  /* was 10 */
header.hero p              { font-size: 17px; }  /* was 13 */
.admin-panel .admin-title  { font-size: 15px; }  /* was 11 */
.admin-panel .admin-list   { font-size: 16px; }  /* was 12 */


/* --------------------------------------------
   CSS Grid canvas — 16 columns wide, auto rows.
   Each cell is a square sized at 1/N of the viewport width
   (currently 1/20). Cards drop in as children of .grid with
   grid-column: span <n> / grid-row: span <n> to occupy
   multiple cells. Cells are slot markers, not data containers.
   -------------------------------------------- */
:root                                 { --cell-size: calc(95vw / 16); --card-buffer: 12px; }

/* Strip wrap padding so the grid sits flush with the viewport. */
.wrap                                 { padding: 0; max-width: 100vw; }
.grid-wrap                            { width: 100vw; overflow: visible; }

.grid {
    position: relative;
    display: grid;
    grid-template-columns: repeat(16, var(--cell-size));
    grid-auto-rows: var(--cell-size);
    width: calc(var(--cell-size) * 16);
    margin: 0 auto;   /* center the 95vw grid in the 100vw wrap, 2.5vw gutters */
}

/* 6 spare rows held open at the bottom of the card canvas (below the last
   flowed row, accordion-expansion included). Padding rather than a spacer
   element so it needs no explicit row pin; the lattice overlay (inset: 0)
   draws across it. Card-grid only — the calendar grid (#today-mount) keeps
   its natural height. */
#card-grid { padding-bottom: calc(6 * var(--cell-size)); }

.cell {
    width:  var(--cell-size);
    height: var(--cell-size);
    box-sizing: border-box;
    /* center any cell-level content; cards override as needed */
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--size-secondary);
    color: var(--color-body);
    overflow: hidden;
}

/* --------------------------------------------
   Dashed grid overlay — a single non-interactive layer painted
   above the cells that draws a dashed vertical line at every
   --cell-size step and a dashed horizontal line at every step.
   Sits above cells, below cards (cards get z-index: 1 to clear it).
   -------------------------------------------- */
.grid::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 0;
    display: none;
    background-image:
        /* vertical dashes: a 1px column repeated every cell */
        repeating-linear-gradient(
            to right,
            transparent 0,
            transparent calc(var(--cell-size) - 1px),
            var(--fg) calc(var(--cell-size) - 1px),
            var(--fg) var(--cell-size)
        ),
        /* horizontal dashes: a 1px row repeated every cell */
        repeating-linear-gradient(
            to bottom,
            transparent 0,
            transparent calc(var(--cell-size) - 1px),
            var(--fg) calc(var(--cell-size) - 1px),
            var(--fg) var(--cell-size)
        );
    /* Cut each 1px line into a dashed sequence by masking with a
       repeating gradient that alternates 6px opaque / 4px clear. */
    mask-image:
        repeating-linear-gradient(to bottom, #000 0 6px, transparent 6px 10px),
        repeating-linear-gradient(to right,  #000 0 6px, transparent 6px 10px);
    -webkit-mask-image:
        repeating-linear-gradient(to bottom, #000 0 6px, transparent 6px 10px),
        repeating-linear-gradient(to right,  #000 0 6px, transparent 6px 10px);
    mask-composite: source-over;
    -webkit-mask-composite: source-over;
}

/* Outer dashed frame for the grid itself. Toggle with `html.show-grid`. */
html.show-grid .grid::before { display: block; }
html.show-grid .grid {
    outline: 1px dashed var(--fg);
    outline-offset: -1px;
}

/* --------------------------------------------
   Cards — drop-in children of .grid. Default footprint
   is 1×1 (single cell); override per-card via inline style
   or modifier classes (.card-w2, .card-h2, etc.).
   -------------------------------------------- */
.card {
    position: relative;
    z-index: 1;     /* sit above the dashed overlay */
    grid-column: span 1;
    grid-row:    span 1;
    background: var(--bg);
    border: 1px solid var(--fg);
    padding: 8px 10px;
    margin: var(--card-buffer);    /* admin panel sets this; cards shrink within their grid cells */
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    font-size: var(--size-secondary);
    color: var(--color-body);
    cursor: grab;
    container-type: inline-size;   /* enables cqw/cqh units in descendants */
}
.card:active                          { cursor: grabbing; }
.card[hidden]                         { display: none; }
.card.card-w2 { grid-column: span 2; }
.card.card-w3 { grid-column: span 3; }
.card.card-w4 { grid-column: span 4; }
.card.card-h2 { grid-row:    span 2; }
.card.card-h3 { grid-row:    span 3; }
.card.card-h4 { grid-row:    span 4; }
.card.card-giant { grid-column: 1 / -1; grid-row: span 3; }   /* full width × 3 rows */

/* HRV CV chart — full-width card (pinned in the registry) holding the HRV delay-
   embedding hero, which fills the whole card. The coordinate/magnitude/regime/
   forecast tables were extracted to the separate .hrv-cv-tables-card below
   (cards/hrv-cv-tables.js). */
.hrv-cv-matrix-card .hrv-cv-grid {
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    gap: 8px;
}
.hrv-cv-matrix-card .hrv-cv-cell {
    position: relative;
    min-height: 0;
    overflow: hidden;
    border: 1px solid var(--ink-15);
    cursor: grab;
}
.hrv-cv-matrix-card .hrv-cv-cell:active { cursor: grabbing; }
.hrv-cv-matrix-card .hrv-cv-cell canvas { display: block; }

/* HRV CV tables — the extracted lag-2 coordinate/magnitude/regime/forecast readouts
   (cards/hrv-cv-tables.js): five scrollable cells across the full-width card. */
.hrv-cv-tables-card .hrv-tables-grid {
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 8px;
}
.hrv-cv-tables-card .hrv-tables-cell {
    position: relative;
    min-height: 0;
    overflow: auto;
    border: 1px solid var(--ink-15);
}
/* Tables match the gray `.sleep-stamp-inline` "running 10-day" stamp exactly —
   weight 400, footnote size, --ink-40, tabular-nums, 0.2px tracking — across
   header (caption), column heads (th), and cell values (td). Colour stays reserved
   for the sin²θ bias cells (inline) and the active-regime/-direction highlight. */
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table {
    width: 100%;
    border: none;
    border-collapse: collapse;
    font: 400 var(--size-footnote)/1.25 var(--font-body), sans-serif;
    color: var(--ink-40);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.2px;
    text-transform: lowercase;
}
/* Override the global APA table rules (configapa_mono.css: table / thead th /
   tbody td / tbody tr) that otherwise leak 12px UPPERCASE bold heads, dark cell
   colour, a 32px row height and a 2px border. Every cell now matches the gray
   `.sleep-stamp-inline` stamp: 400 / footnote / --ink-40 / 0.2px / lowercase. */
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table caption {
    caption-side: top;
    text-align: left;
    color: var(--ink-40);
    font-weight: 400;
    text-transform: lowercase;
    padding: 2px 4px 1px;
}
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table th {
    position: static;
    background: transparent;
    color: var(--ink-40);
    font-size: var(--size-footnote);
    font-weight: 400;
    letter-spacing: 0.2px;
    text-transform: lowercase;
    text-align: right;
    border-bottom: 1px solid var(--ink-25);
    padding: 1px 4px;
}
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table td {
    color: var(--ink-40);
    font-weight: 400;
    text-transform: lowercase;
    text-align: right;
    padding: 1px 4px;
    border-bottom: 1px solid var(--ink-08);
}
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table tbody tr { height: auto; }
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table th:first-child,
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table td:first-child {
    text-align: left;
    color: var(--ink-40);
}
/* x̄ = magnitude/level column — faint rule sets it apart from the n/n+1/n+2 triple. */
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table .mean-col {
    border-left: 1px solid var(--ink-15);
}
/* Magnitude table — mono level/band channel (no bias ramp). The band glyph centred. */
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-coord-table .band {
    text-align: center;
    color: var(--ink-55);
}
/* Shape-regime table (slot 4) — AR(2) spectral-radius regime vs the ρ=1 stability
   threshold. Mono by design: colour stays reserved for the embedding's bias ramp,
   so the active regime carries via weight + a faint ink fill, not hue. */
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-regime-table .fc-mark {
    text-align: center;
    color: var(--fg);
}
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-regime-table .fc-active td {
    font-weight: 700;
    color: var(--fg);
    background: var(--ink-08);
}
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-regime-table .fc-desc td {
    color: var(--ink-40);
    font-weight: 600;
    border-bottom: 1px solid var(--ink-08);
    padding-bottom: 2px;
}
:is(.hrv-cv-tables-card, .gabs-memory-card) .hrv-regime-table .fc-active.fc-desc td {
    color: var(--ink-55);
}

/* GABS memory card (cards/gabs-memory.js, 2026-09-28) — the season AR(1) table,
   one row per domain, beneath Layer1. Shares the tables card's mono table rules
   via the :is(.hrv-cv-tables-card, .gabs-memory-card) selectors above. */
.gabs-memory-card .gabs-memory-body {
    flex: 1;
    min-height: 0;
    overflow: auto;
    margin-top: 4px;
}
.gabs-memory-card .gabs-memory-table th:not(:first-child),
.gabs-memory-card .gabs-memory-table td:not(:first-child) { white-space: nowrap; }
.gabs-memory-card .gabs-memory-table caption { white-space: normal; }

/* Placeholder shells (cards/placeholders.js): empty cards reserving grid
   space for future sources. They inherit the full .card contract (border,
   paper background, container-type) — these rules only flex-center the value
   and give it the standard responsive size. The em-dash renders at full body
   ink like every other card's empty state; grey is reserved for the
   `.sleep-stamp-inline` title stamp, never the value. Monochrome, alpha not
   hue. Delete a card's framing when it's wired to real data. */
/* Subtitle: muted secondary text on its OWN line directly below the title
   (not an inline stamp). Same --ink-40 / 12px / 400 treatment as the inline
   stamp, but block-level so it stacks under the strong. */
.card .placeholder-sub {
    display: block;
    font-weight: 400;
    font-size: var(--size-footnote);
    color: var(--ink-40);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.2px;
}
/* Flex-center only the value wrapper — exclude the title stamp and the
   below-title subtitle so neither gets stretched. */
.card.placeholder-card > span:not(.sleep-stamp-inline):not(.placeholder-sub) {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
}
.card.placeholder-card [data-field="val"] {
    font-size: min(36px, 36cqw);   /* same responsive cap as real value cards */
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* Card stack (cards/card-stack.js): twelve 1×1 cells stacked in one SVG
   viewBox. The SVG fills the card; cells are hairline --fg outlines (mono,
   non-scaling-stroke so the line weight stays 1px as the viewBox scales). */
.card.card-stack-card .card-stack-wrap {
    flex: 1;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.card.card-stack-card .card-stack-svg { width: 100%; height: 100%; display: block; }
.card.card-stack-card .cs-cell {
    fill: none;
    stroke: var(--fg);
    stroke-width: 1;
    stroke-dasharray: 3 2;          /* dashed cells */
    vector-effect: non-scaling-stroke;
}
/* In-cell course label (HTML inside <foreignObject>): one line, ellipsis on
   overflow, vertically centered. font-size/line-height are in viewBox user
   units (scaled with the SVG). */
.card.card-stack-card .cs-label {
    height: 100%;
    box-sizing: border-box;
    padding: 0.5px 1.5px;
    display: flex;
    align-items: center;            /* vertically center the wrapped block */
    font-size: 2.6px;               /* sized so a wrapped title fits a 10-row cell */
    line-height: 1.1;               /* multi-line: each course wraps + stacks vertically */
    color: var(--color-body);
    overflow: hidden;
}

/* Administrative Tasks (cards/placeholders.js): a row of five numbered
   checkboxes — the digit sits inside a bordered square. Unchecked = ink-55
   outline; checked (.is-checked) inverts to a filled ink slab with paper
   digit. State via alpha/fill, never hue. */
.card.admin-tasks-card .admin-boxes {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: clamp(4px, 4cqw, 12px);
}
.card.admin-tasks-card .admin-box {
    width: min(34px, 16cqw);
    aspect-ratio: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--ink-55);
    color: var(--ink-55);
    font-weight: 700;
    font-size: min(18px, 9cqw);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.card.admin-tasks-card .admin-box.is-checked {
    background: var(--fg);
    border-color: var(--fg);
    color: var(--bg);
}

/* Accordion toggle stub (cards/sleep-accordion.js): a super-small control
   card — standard 1-col width, HALF the cell height (it still occupies one
   grid row; the lower half of the cell stays empty). Click target, so it
   trades the grab cursor for a pointer. State signalled by the chevron and
   an ink hover tint — alpha, never hue. */
.card.acc-toggle-card {
    height: calc(var(--cell-size) / 2 - 2 * var(--card-buffer));
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    cursor: pointer;
    user-select: none;
}
.card.acc-toggle-card:active          { cursor: pointer; }
.card.acc-toggle-card:hover           { background: var(--ink-04); }
.card.acc-toggle-card .acc-chev       { color: var(--ink-55); font-size: var(--size-footnote); line-height: 1; }

/* PhD workspace cards (cards/phd.js) — Grants, APPIC Top 10, and the three
   checklists. List rows at 12px; state by glyph + alpha, never hue. */
.phd-card .phd-list {
    flex: 1;
    min-height: 0;
    margin: 6px 0 0;
    padding: 0;
    list-style: none;
    font-size: var(--size-footnote);
    line-height: 1.45;   /* tight — the 16-item doctoral checklist and the
                            15-row APPIC list must fit a 3-row card */
    overflow: hidden;
}
.phd-card .phd-list li        { display: flex; align-items: baseline; gap: 6px; }
.phd-card .phd-box            { color: var(--ink-55); }
.phd-card li.is-done          { color: var(--ink-40); text-decoration: line-through; }
.phd-card li.is-done .phd-box { text-decoration: none; }
.phd-card .phd-rank {
    /* Flush left — the rank digit starts on the same vertical as the card
       title (no fixed right-aligned column inset). */
    text-align: left;
    color: var(--ink-40);
    font-variant-numeric: tabular-nums;
    flex-shrink: 0;
}
.phd-card .phd-grant-name     { flex: 1; }
.phd-card .phd-meta           { color: var(--ink-40); font-variant-numeric: tabular-nums; flex-shrink: 0; }
/* Dense variant — the 19-item APPIC checklist in a 3-row card. */
.phd-card .phd-list.phd-dense { font-size: var(--size-footnote); line-height: 1.35; }

/* Ranked lists: long site names truncate with an ellipsis (full text in title). */
.phd-card .phd-site           { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Manuscripts: the Peer-Reviewed block is anchored 3/4 of the way down the
   card (absolute against the card box), separate from the in-prep list that
   flows from the top. Manuscript numbers are stable ids, so 05 lives here. */
[data-card="manuscripts"] .manuscripts-peer {
    position: absolute;
    top: 75%;
    left: 10px;   /* match .card horizontal padding */
    right: 10px;
}
[data-card="manuscripts"] .phd-section-label {
    font-size: var(--size-footnote);
    font-weight: 700;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--ink-55);
    margin-bottom: 2px;
}
[data-card="manuscripts"] .manuscripts-peer .phd-list { margin-top: 0; }

/* Bottom-left ▼ on the big composite cards (Stress, Readiness, Movement). */
.card .card-arrow {
    position: absolute;
    left: 8px;
    bottom: 6px;
    font-size: var(--size-footnote);
    line-height: 1;
    color: var(--ink-40);
}
/* All three ▼ are live toggles (arrow-accordions — Stress → ANS/Recovery,
   Readiness → Sleep/RHR/HRV, Movement → Steps/Active). */
.card .card-arrow          { cursor: pointer; }
.card .card-arrow:hover    { color: var(--fg); }

/* Bottom-right ● toggle (hero cards → calc-cell). Outline when off; solid when on. */
.card .card-dot {
    position: absolute;
    right: 8px;
    bottom: 8px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 1px solid var(--ink-40);
    background: transparent;
    cursor: pointer;
    z-index: 3;
}
.card .card-dot:hover     { border-color: var(--fg); }
.card .card-dot.is-filled { background: var(--fg); border-color: var(--fg); }

/* Calc card revealed by the ● — 2 cols × 1 row on R3, page-colour fill. */
.calc-cell {
    flex: 1;
    background: var(--bg);
    min-height: 0;
    overflow: hidden;              /* never scrolls */
    display: block;                /* text flows edge to edge */
    /* Sean 2026-08-16: explicitly below the 16px floor — the note must fit
       a 2×1 cell without scrolling. */
    font-size: 11px;
    line-height: 1.2;
    color: var(--ink-40);
    font-variant-numeric: tabular-nums;
    white-space: normal;
    text-align: left;
}
.calc-cell p         { margin: 0; }
.calc-cell .calc-cite {
    margin-top: 2.4em;             /* two lines below the note */
    padding-left: 1.5em;
    text-indent: -1.5em;           /* APA hanging indent */
    overflow-wrap: anywhere;       /* long DOIs wrap, never scroll */
}
.calc-cell i { font-style: italic; }
.calc-cell b { font-weight: 600; color: var(--ink-55); }


/* --------------------------------------------
   Column address labels in the header row.
   Light-ink tag so the address ("C4 · prog") sits
   quietly beneath any real column title we add later.
   -------------------------------------------- */

.col-addr {
    display: block;
    color: var(--ink-40);
    font-weight: 400;
    font-size: var(--size-secondary);   /* was 10 */
    letter-spacing: 0.3px;
    text-transform: none;
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
}


/* --------------------------------------------
   Cell-address overlay (toggleable via admin panel).
   When `html.show-addr` is set, every data cell gets a
   floating "R<n>·<col>" tag in its top-right corner so
   the visual position of each addressable cell is
   obvious at a glance. Tag is light-ink so it sits
   under any real cell content.
   -------------------------------------------- */

html.show-addr tbody { counter-reset: row-counter; }
html.show-addr tbody tr { counter-increment: row-counter; }
html.show-addr tbody td { position: relative; }

html.show-addr tbody td::before {
    position: absolute;
    top: 2px;
    right: 6px;
    font-size: var(--size-footnote);   /* was 9 */
    color: var(--ink-40);
    font-weight: 400;
    font-style: normal;
    letter-spacing: 0.2px;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
    pointer-events: none;
    text-transform: none;
    white-space: nowrap;
}

html.show-addr tbody tr td:nth-child(1)::before  { content: "R" counter(row-counter) "·rank"; }
html.show-addr tbody tr td:nth-child(2)::before  { content: "R" counter(row-counter) "·C2"; }
html.show-addr tbody tr td:nth-child(3)::before  { content: "R" counter(row-counter) "·C3"; }
html.show-addr tbody tr td:nth-child(4)::before  { content: "R" counter(row-counter) "·prog"; }
html.show-addr tbody tr td:nth-child(5)::before  { content: "R" counter(row-counter) "·set"; }
html.show-addr tbody tr td:nth-child(6)::before  { content: "R" counter(row-counter) "·loc"; }
html.show-addr tbody tr td:nth-child(7)::before  { content: "R" counter(row-counter) "·C7"; }
html.show-addr tbody tr td:nth-child(8)::before  { content: "R" counter(row-counter) "·C8"; }
html.show-addr tbody tr td:nth-child(9)::before  { content: "R" counter(row-counter) "·C9"; }
html.show-addr tbody tr td:nth-child(10)::before { content: "R" counter(row-counter) "·C10"; }
html.show-addr tbody tr td:nth-child(11)::before { content: "R" counter(row-counter) "·C11"; }
html.show-addr tbody tr td:nth-child(12)::before { content: "R" counter(row-counter) "·C12"; }
html.show-addr tbody tr td:nth-child(13)::before { content: "R" counter(row-counter) "·C13"; }
html.show-addr tbody tr td:nth-child(14)::before { content: "R" counter(row-counter) "·hrs"; }
html.show-addr tbody tr td:nth-child(15)::before { content: "R" counter(row-counter) "·C15"; }
html.show-addr tbody tr td:nth-child(16)::before { content: "R" counter(row-counter) "·orient"; }

/* Rows hold their declared 16 px height — strip the base cell padding
   from configapa_mono.css and the show-addr top-padding override. */
tbody td                   { padding: 0; }
html.show-addr tbody tr    { height: 64px; }
html.show-addr tbody td    { padding: 0; }


/* Compact stat cards — tabular numerics across the board. */
[data-card] b                         { font-weight: 700; font-variant-numeric: tabular-nums; }

/* Sleep + Weather body text: 2px smaller than the 14px card baseline.
   Title (strong) + gray stamp (.sleep-stamp-inline) are unaffected. */
.card[data-card="sleep"] > span,
.card[data-card="weather"] > span     { font-size: var(--size-footnote); }

/* Mini detail cards (rows 4-5): body text 1px smaller than card baseline
   (14 → 13). Title and gray stamp unaffected. */
.card[data-card="steps"] > span,
.card[data-card="hr"] > span,
.card[data-card="active"] > span,
.card[data-card="sedentary"] > span,
.card[data-card="skintemp"] > span,
.card[data-card="readiness"] > span,
.card[data-card="spo2"] > span,
.card[data-card="hrv-entropy"] > span,
.card[data-card="hrv-nrhr"] > span,
.card[data-card^="hr-zone-"] > span   { font-size: var(--size-footnote); }
.sleep-stamp-inline {
    margin-left: 6px;
    font-weight: 400;
    font-size: var(--size-footnote);
    color: var(--ink-40);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.2px;
}

/* Today card — vertical day-view grid (5 AM → 8 PM). */
.today-card                           { gap: 4px; padding: 8px 10px 10px; }
.today-list                           {
    position: relative;
    flex: 1;            /* fill the card below the title */
    min-height: 0;
    /* Equal-height 1fr rows: the hour grid always stretches to fill the
       card's full height (2×5 footprint), never compressing to label
       height. Events + now-line are absolutely positioned and unaffected. */
    display: grid;
    grid-auto-rows: 1fr;
    /* 16px top + bottom buffers (> the labels' 7px upward offset, so the
       5 AM label clears the title row); overflow must stay visible or the
       list box clips that overhang. Event blocks are %-clamped to [0,100] so
       nothing else can escape. */
    margin-top: 16px;
    margin-bottom: 16px;
    padding-left: 38px;
    overflow: visible;
}
.today-hour                           {
    border-top: 1px dashed var(--ink-15);
    position: relative;
    min-height: 0;
}
.today-hour:last-child                { border-bottom: 1px dashed var(--ink-15); }
.today-hour-label                     {
    position: absolute;
    left: -38px;
    top: -7px;
    width: 34px;
    text-align: right;
    font-size: var(--size-footnote);
    color: var(--ink-40);
    font-variant-numeric: tabular-nums;
    background: var(--bg);
    padding: 0 3px 0 0;
    line-height: 1;
}
.today-event                          {
    position: absolute;
    left: 38px;
    right: 2px;
    background: var(--ink-08);
    border-left: 2px solid var(--fg);
    padding: 1px 6px;
    font-size: var(--size-footnote);
    line-height: 1.2;
    overflow: hidden;
    border-radius: 2px;
    cursor: default;
}
.today-event-time                     {
    color: var(--ink-55);
    margin-right: 5px;
    font-variant-numeric: tabular-nums;
}
.today-event-title                    { color: var(--fg); }
.today-event.is-current               { background: var(--ink-25); border-left-width: 3px; }
.today-event.is-current .today-event-title { font-weight: 700; }
.today-event.is-past                  { opacity: 0.5; }
.today-event.is-past .today-event-title { text-decoration: line-through; text-decoration-thickness: 1px; }

.today-now                            {
    position: absolute;
    left: 0;
    right: 0;
    height: 0;
    border-top: 1.5px solid var(--fg);
    pointer-events: none;
    z-index: 5;
}
.today-now::before                    {
    content: "";
    position: absolute;
    left: -3px;
    top: -4px;
    width: 6px;
    height: 6px;
    background: var(--fg);
    border-radius: 50%;
}

/* Weather card — animated FX behind the text. JS sets data-condition on
   .weather-fx and populates it with drops / flakes / clouds / sun. */
.card[data-card="weather"] > .weather-fx {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 0;
}
.card[data-card="weather"] > strong,
.card[data-card="weather"] > span,
.card[data-card="weather"] > .weather-hourly {
    position: relative;
    z-index: 1;
}

/* Hourly forecast strip — mirrors .trends-grid format with 12 cells
   (one per hour, next 12 hours starting from the current hour). */
.weather-hourly {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;   /* all five rows evenly distributed top→bottom */
    margin-top: 10px;
    min-height: 0;
}
/* Three discrete icon rows (rain / sun / wind-icon). Each cell centers
   a single glyph — no stacking, no grid-area overlap. */
.weather-hourly-row-rain .weather-hourly-cells > div,
.weather-hourly-row-sun .weather-hourly-cells > div,
.weather-hourly-row-wind-icon .weather-hourly-cells > div {
    display: grid;
    place-items: center;
    height: 100%;
    color: var(--ink-40);
}
.hourly-icon {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    display: block;
}
.hourly-icon-wind {
    /* Pinwheel inherits .hourly-icon sizing; only adds rotation. */
    animation: weather-turbine-spin linear infinite;
    /* animation-duration is set inline per cell based on windSpinDur tier */
}

/* Hourly rain row: per-cell falling-drops stream. Mirrors the sleep card's
   .zzz-stream pattern — absolutely-positioned spans falling top → bottom
   on a continuous linear loop. */
.hourly-rain-fx {
    position: relative;
    width: 28px;
    height: 28px;
    color: var(--ink-40);
    overflow: hidden;
}
.hourly-rain-fx > span {
    position: absolute;
    top: -25%;
    width: 1.5px;
    height: 6px;
    background: currentColor;
    border-radius: 1px;
    animation: hourly-rain-fall linear infinite;
    /* left, animation-duration, animation-delay set inline per drop */
}
@keyframes hourly-rain-fall {
    from { top: -25%; }
    to   { top: 125%; }
}
.weather-hourly-row-rain,
.weather-hourly-row-sun,
.weather-hourly-row-wind-icon {
    flex: 0 0 34px;   /* just enough for the 28px icon + small breathing room */
}
.weather-unit {
    font-weight: 400;
    font-size: var(--size-footnote);
    color: var(--ink-40);
    letter-spacing: 0.3px;
    margin-left: 4px;
}
.weather-hourly-row {
    display: grid;
    grid-template-columns: 80px 1fr;
    align-items: center;
    gap: 14px;
    min-height: 0;
}
.weather-hourly-label {
    font-weight: 700;
    text-transform: uppercase;
    font-size: var(--size-footnote);
    color: var(--ink-55);
    letter-spacing: 0.5px;
}
.weather-hourly-cells {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: 8px;
    align-items: center;
    justify-items: center;
    height: 100%;
}
.weather-hourly-cells > div {
    font-size: var(--size-footnote);
    color: var(--color-body);
    font-variant-numeric: tabular-nums;
    text-align: center;
    line-height: 1.2;
}
.weather-hourly-row-header .weather-hourly-cells > div {
    font-size: var(--size-footnote);
    color: var(--ink-55);
}

/* Sun: rotating disk + rays in the top-right corner. */
.weather-sun {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 48px;
    height: 48px;
    animation: weather-sun-rotate 24s linear infinite;
}
.weather-sun line   { stroke: var(--ink-40); stroke-width: 3; stroke-linecap: round; }
.weather-sun circle { fill: var(--ink-40); }
@keyframes weather-sun-rotate { to { transform: rotate(360deg); } }

/* Rain: short vertical lines falling. */
.weather-drop {
    position: absolute;
    top: -15%;
    width: 2px;
    height: 12px;
    background: var(--ink-40);
    animation: weather-rain-fall linear infinite;
}
@keyframes weather-rain-fall {
    from { top: -15%; }
    to   { top: 115%; }
}

/* Snow: asterisks drifting down with a small horizontal sway. */
.weather-flake {
    position: absolute;
    top: -10%;
    color: var(--ink-40);
    font-weight: 700;
    user-select: none;
    animation: weather-snow-drift linear infinite;
}
@keyframes weather-snow-drift {
    from { top: -10%;  transform: translateX(0); }
    50%  {            transform: translateX(10px); }
    to   { top: 110%;  transform: translateX(0); }
}

/* Overcast: soft cloud-shaped pills drifting left to right. */
.weather-cloud {
    position: absolute;
    left: -25%;
    width: 60px;
    height: 18px;
    background: var(--ink-15);
    border-radius: 18px;
    animation: weather-cloud-drift linear infinite;
}
@keyframes weather-cloud-drift {
    from { left: -25%; }
    to   { left: 125%; }
}

/* Shared spin keyframe — used by .hourly-icon-wind. Uses the `rotate`
   property (not `transform: rotate`) so it composes with the hourly
   pinwheel's `translate` positioning without overwriting it. */
@keyframes weather-turbine-spin {
    to { rotate: 360deg; }
}

/* Sleep Quality score card — drifting Z's behind the score text. JS populates
   .zzz-stream with N spans on mount; each Z gets randomized top/size/dur/delay.
   Keyed to the .sleepq-score-card class (sleepCard's extraClass) so it styles
   BOTH the row-1 'sleep' card and the detail-panel 'sp-score' copy. */
.sleepq-score-card > .zzz-stream {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 0;
}
.sleepq-score-card > strong {
    position: relative;
    z-index: 1;
    text-align: left;   /* title ("Sleep Quality" + "7 signal") left-justified;
                           the score below stays centered via .sleepq-wrap */
}
.zzz-stream > span {
    position: absolute;
    top: -10%;
    font-weight: 700;
    color: var(--ink-25);
    user-select: none;
    animation: zzz-fall linear infinite;
}
@keyframes zzz-fall {
    from { top: -10%; }
    to   { top: 110%; }
}

/* Sleep Quality score card: the composite score centered over the drifting
   Z's. Title + stamp + score all centered; score sizing matches the stress/
   readiness overlays (responsive cap, stays readable in a w2 cell). */
.sleepq-score-card                { text-align: center; }
.sleepq-score-card > .sleepq-wrap {
    position: relative;
    z-index: 1;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
}
/* Sleep Quality disk — same size/inks as .stress-ball, tier mapping inverted
   (high score = palest), and NO pulse animation. */
.sleepq-ball {
    position: absolute;
    width: min(80px, 60cqw);
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--sleepq-tier-color, transparent);
}
.sleepq-score {
    position: relative;
    z-index: 1;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    font-size: min(36px, 36cqw);
    color: var(--fg);
    line-height: 1;
}

/* Stress card — pulsing ball, period driven by --pulse-period set in JS.
   Period maps to a breathing-rate analog: slow = calm, fast = panic.
   Ball is a low-opacity disk in --fg; score number is overlaid centered. */
.stress-ball-wrap {
    position: relative;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
}
.stress-ball {
    width: min(80px, 60cqw);
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--stress-tier-color, var(--ink-25));
    transform-origin: center;
    animation: stress-pulse var(--pulse-period, 3.5s) ease-in-out infinite;
    will-change: transform;
}
.stress-score-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
.stress-score-overlay b {
    font: inherit;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    font-size: min(36px, 36cqw);
    color: var(--fg);
    line-height: 1;
}
@keyframes stress-pulse {
    0%, 100% { transform: scale(0.55); }
    50%      { transform: scale(1.0);  }
}

/* Readiness card — battery icon with fill driven by score.
   Wrap mirrors the stress card layout; score number overlays centered. */
.readiness-battery-wrap {
    position: relative;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
}
.readiness-battery {
    display: flex;
    align-items: center;
    gap: 2px;
}
.battery-body {
    width: min(100px, 73cqw);
    aspect-ratio: 100 / 50;
    border: 2px solid var(--fg);
    border-radius: 4px;
    position: relative;
    overflow: hidden;
}
.battery-fill {
    position: absolute;
    inset: 2px 0 2px 2px;
    /* Tiered like the Sleep Quality disk (inverse of Stress): high readiness =
       palest ink, low = darkest. --readiness-tier-color set in readiness.js. */
    background: var(--readiness-tier-color, var(--ink-25));
    width: 0%;
    transition: width 0.6s ease-out;
}
.battery-terminal {
    width: min(9px, 7cqw);
    height: min(28px, 20cqw);
    background: transparent;
    border: 2px solid var(--fg);
    border-left: none;
    border-radius: 0 3px 3px 0;
    flex-shrink: 0;
}
.readiness-score-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
.readiness-score-overlay b {
    font: inherit;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    font-size: min(36px, 36cqw);
    color: var(--fg);
    line-height: 1;
}

/* Movement card — 3 mountains, each = 10k steps; the grey fill rises with the
   day's step count (same allocation + look as the Last-10-days movement row).
   The SVG is rebuilt every render in js/cards/movement.js. */
.movement-wrap {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
    margin-top: 4px;
}
/* scale(0.75) = a true 25%-smaller render; the SVG's `meet` fit is height-
   constrained here, so a width/height % would not reliably resize it. */
.movement-mountains            { width: 100%; height: 100%; display: block; transform: scale(0.75); }
.movement-mountains .m-outline { fill: none; stroke: var(--fg); stroke-width: 1; vector-effect: non-scaling-stroke; }
.movement-mountains .m-fill    { fill: var(--ink-40); }

/* Steps + Active — wide (4-col) combined card: two prominent numbers (steps,
   active zone mins) side by side, each with a grey unit label below. */
[data-card="steps-active"] .sa-row {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: space-around;
    gap: 12px;
}
[data-card="steps-active"] .sa-stat {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
[data-card="steps-active"] .sa-stat b {
    font-size: min(40px, 13cqw);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
[data-card="steps-active"] .sa-label {
    font-size: var(--size-footnote);
    color: var(--ink-40);
    letter-spacing: 0.2px;
}

/* Bandwidth card — sine wave whose amplitude (and stroke weight) reflects
   schedule-busyness tier. Wave path is built in JS from the tier amplitude. */
.bandwidth-wrap {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
}
.bandwidth-svg {
    width: 100%;
    height: min(56px, 30cqw);
}
.bandwidth-wave {
    fill: none;
    stroke: var(--fg);
    stroke-width: 1.5;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
    /* No CSS transition on `d` — rAF rebuilds the path every frame. */
}

/* Hypnogram — full-width sleep-stage timeline strip. */
.hypnogram-card                       { padding: 8px 12px 10px; gap: 6px; }
.hypnogram-card .svg-wrap             { flex: 1; min-height: 0; display: flex; }
.hypnogram-card svg                   { width: 100%; height: 100%; display: block; }
.hypnogram-card .stage-label          { font-size: var(--size-footnote); fill: var(--ink-55); font-variant-numeric: tabular-nums; }
.hypnogram-card .time-label           { font-size: var(--size-footnote); fill: var(--ink-55); font-variant-numeric: tabular-nums; }
.hypnogram-card .lane-bg              { fill: var(--ink-04); }
.hypnogram-card .seg-wake             { fill: var(--ink-25); }
.hypnogram-card .seg-rem              { fill: var(--ink-55); }
.hypnogram-card .seg-light            { fill: var(--ink-40); }
.hypnogram-card .seg-deep             { fill: var(--fg); }

/* 10-day trends giant card — three labeled rows, 10 mini cells each. */
.card[data-card="trends-10d"]         { padding: 10px 14px 12px; gap: 8px; }
.trends-grid {
    flex: 1;
    display: grid;
    grid-template-rows: auto 1fr 1fr 1fr;
    gap: 10px;
    min-height: 0;
}
.trends-row-header                    { align-self: end; }
/* Sleep row overlaps upward into the day-labels row for a layered effect.
   Z's spill over the labels but the labels still read clearly because
   they sit on the card background. Bottom margin left at 0 so the
   Sleep↔Stress gap matches Stress↔Movement (the grid's 10px).
   transform shifts contents down 6px without changing the layout — using
   margin gets eaten by `align-items: center` on .trends-row. */
.trends-row-sleep                     {
    margin-top: -16px;
    transform: translateY(6px);
    position: relative;
    z-index: 1;
}
/* Stress label nudged down 6px so it sits visually centered against the
   ball cells. transform (not margin) for the same align-items reason above. */
.trends-row-stress .trends-label      { transform: translateY(6px); }
.trends-day-labels > div {
    font-size: var(--size-footnote);
    color: var(--ink-55);
    font-variant-numeric: tabular-nums;
    text-align: center;
    line-height: 1.2;
    width: 100%;
    height: auto;
    display: block;
}
.trends-row {
    display: grid;
    grid-template-columns: 110px 1fr;
    align-items: center;
    gap: 14px;
    min-height: 0;
}
.trends-label {
    font-weight: 700;
    text-transform: uppercase;
    font-size: var(--size-footnote);
    color: var(--ink-55);
    letter-spacing: 0.5px;
}
.trends-cells {
    display: grid;
    grid-template-columns: repeat(10, 1fr);
    gap: 8px;
    height: 100%;
    align-items: center;
    justify-items: center;
}
.trends-cells > * {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}
.trend-stress-ball {
    width: 60%;
    aspect-ratio: 1;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    container-type: inline-size;   /* lets the score scale with each ball */
}
/* Stress score, centered + knockout (paper-colored, reads as transparent on
   the tinted ball). Sized relative to the ball so it scales per tier. */
.trend-stress-score {
    color: var(--bg);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    font-size: 40cqw;
    line-height: 1;
}
.trend-stress-ball.is-empty {
    border: 1px dashed var(--ink-25);
    background: transparent !important;
}
.trend-sleep.is-empty {
    color: var(--ink-25);
    font-weight: 400;
}
.trend-mountains {
    width: 100%;
    height: auto;
    aspect-ratio: 100 / 32;
}
.trend-mountains .m-outline { fill: none; stroke: var(--fg); stroke-width: 1; vector-effect: non-scaling-stroke; }
.trend-mountains .m-fill    { fill: var(--ink-40); }
.trend-sleep {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1px;
    color: var(--ink-40);
    font-weight: 700;
    line-height: 1;
}

/* Heart rate chart — same layout shell as hypnogram. */
.hr-chart-card                        { padding: 8px 12px 10px; gap: 6px; }
.hr-chart-card .svg-wrap              { flex: 1; min-height: 0; display: flex; }
.hr-chart-card svg                    { width: 100%; height: 100%; display: block; }
.hr-chart-card .bpm-label             { font-size: var(--size-footnote); fill: var(--ink-55); font-variant-numeric: tabular-nums; }
.hr-chart-card .time-label            { font-size: var(--size-footnote); fill: var(--ink-55); font-variant-numeric: tabular-nums; }
.hr-chart-card .hr-line               { fill: none; stroke: var(--fg); stroke-width: 1.2; }
.hr-chart-card .hr-rest               { stroke: var(--ink-40); stroke-dasharray: 4 4; }

/* 14-day HRV trend — giant card (js/cards/hrv-14d.js): daily HRV polyline +
   dots with a dashed target line. Same layout shell as the HR chart. */
.hrv-chart-card                       { padding: 8px 12px 10px; gap: 6px; }
.hrv-chart-card .svg-wrap             { flex: 1; min-height: 0; display: flex; }
.hrv-chart-card svg                   { width: 100%; height: 100%; display: block; }
.hrv-chart-card .axis-label           { font-size: var(--size-footnote); fill: var(--ink-55); font-variant-numeric: tabular-nums; }
.hrv-chart-card .hrv-line             { fill: none; stroke: var(--fg); stroke-width: 1.2; }
.hrv-chart-card .hrv-dot              { fill: var(--fg); }
.hrv-chart-card .hrv-target           { stroke: var(--ink-40); stroke-dasharray: 4 4; }
.hrv-chart-card .hrv-target-label     { font-size: var(--size-footnote); fill: var(--ink-40); font-variant-numeric: tabular-nums; }
/* events — parallel x-axis: faint dashed guide line + italic label (no marker) */
.hrv-chart-card .hrv-event-line       { stroke: var(--ink-25); stroke-width: 1; stroke-dasharray: 3 4; }
.hrv-chart-card .hrv-event-label      { font-size: var(--size-footnote); fill: var(--ink-55); font-style: italic; }


/* --------------------------------------------
   STANDARD card-value size — 36 px, bold, tabular-nums,
   responsive cap `min(36px, 36cqw)` so values stay readable
   down to ~100 px wide cards. Applies to every prominent
   primary value on the dashboard.

   To opt a new card in: add its `[data-card="x"] [data-field="y"]`
   selector to the rule below (and to the centering rule above it
   if the value should also be flex-centered in its span).

   Currently applied to:
     - row-1 score overlays  (stress, readiness)
     - row-2 input signals   (stress-*, readiness-*  → val)
     - row-4 detail stats    (active, steps, hr, hrv-entropy)
     - row-7 HR zones        (hr-zone-* → min, today's time-in-zone)
   -------------------------------------------- */

[data-card="active"]      > span:not(.sleep-stamp-inline),
[data-card="steps"]       > span:not(.sleep-stamp-inline),
[data-card="hr"]          > span:not(.sleep-stamp-inline),
[data-card="hrv-entropy"] > span:not(.sleep-stamp-inline),
[data-card^="stress-"]    > span:not(.sleep-stamp-inline),
[data-card^="readiness-"] > span:not(.sleep-stamp-inline),
[data-card^="sq-"]        > span:not(.sleep-stamp-inline),
[data-card^="hr-zone-"]   > span:not(.sleep-stamp-inline) {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
}

[data-card="active"]      [data-field="azm"],
[data-card="steps"]       [data-field="steps"],
[data-card="hr"]          [data-field="now"],
[data-card="hrv-entropy"] [data-field="val"],
[data-card^="stress-"]    [data-field="val"],
[data-card^="readiness-"] [data-field="val"],
[data-card^="sq-"]        [data-field="val"],
[data-card^="sleepq-"]    [data-field="val"],
[data-card^="hr-zone-"]   [data-field="min"] {
    font-size: min(36px, 36cqw);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* HR card + HR-zone cards: a .sleep-stamp-inline that sits on its own line
   (not nested inside <strong>) — the "rest <b>—</b>" stamp on HR, the band
   stamp ("109–126") on each zone card — would otherwise inherit the default
   6px left margin and hang right of the title. Reset to the title's edge. */
[data-card="hr"]        > .sleep-stamp-inline,
[data-card^="hr-zone-"] > .sleep-stamp-inline {
    margin-left: 0;
}

/* Small detail/signal cards: drop the grey unit stamp (nested inside the title
   <strong>) onto its own second line, flush to the title's left edge, instead
   of trailing the label inline. Excludes the larger readiness-score card (shares
   the readiness- prefix). The hr / hr-zone stamps are siblings of <strong>
   (already on their own line via the rule above), so they're untouched. */
[data-card="active"]                                          > strong > .sleep-stamp-inline,
[data-card^="stress-"]                                        > strong > .sleep-stamp-inline,
[data-card^="readiness-"]:not([data-card="readiness-score"]) > strong > .sleep-stamp-inline,
[data-card^="sq-"]                                            > strong > .sleep-stamp-inline,
[data-card^="sleepq-"]                                        > strong > .sleep-stamp-inline {
    display: block;
    margin-left: 0;
}

/* --------------------------------------------
   Sleep Quality — large composite card (col 5–12, rows 2–3).
   Big score on the left, per-signal bar breakdown below.
   -------------------------------------------- */
.sleep-quality-card                          { padding: 12px 16px 14px; gap: 8px; }
.sleep-quality-card > strong                 { text-align: left; align-self: stretch; }
.sleep-quality-card .sq-body                 { flex: 1; display: flex; flex-direction: column; gap: 10px; min-height: 0; }
.sleep-quality-card .sq-headline             { display: flex; align-items: baseline; gap: 14px; }
.sleep-quality-card .sq-score                { line-height: 1; }
.sleep-quality-card [data-field="score"]     { font-size: min(56px, 18cqw); font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }
.sleep-quality-card .sq-basis                { font-size: var(--size-footnote); color: var(--ink-55); font-variant-numeric: tabular-nums; }
.sleep-quality-card .sq-rows                 { display: flex; flex-direction: column; gap: 4px; flex: 1; justify-content: center; }
.sleep-quality-card .sq-row                  { display: grid; grid-template-columns: 110px 1fr 36px; align-items: center; gap: 10px; font-size: var(--size-footnote); font-variant-numeric: tabular-nums; }
.sleep-quality-card .sq-row-sub              { font-size: var(--size-footnote); color: var(--ink-55); }
.sleep-quality-card .sq-label                { color: var(--ink-55); }
.sleep-quality-card .sq-row-top .sq-label    { color: var(--fg); }
.sleep-quality-card .sq-bar                  { position: relative; height: 8px; background: var(--ink-04); border-radius: 2px; overflow: hidden; }
.sleep-quality-card .sq-fill                 { display: block; height: 100%; background: var(--fg); transition: width 0.4s ease; }
.sleep-quality-card .sq-fill-sub             { background: var(--ink-40); }
.sleep-quality-card .sq-val                  { text-align: right; }
.sleep-quality-card .sq-val b                { font-weight: 600; }

/* --------------------------------------------
   Balance — 6×6 synthesis card. Four daily composites in a 2×2 ledger:
   LEFT column = LOAD (Stress / Movement), RIGHT = RESERVE (Readiness / Sleep).
   A heavy spine splits the columns; a hairline rule splits the rows. Each
   quadrant is a vessel filling bottom-up to its score, capped by a level-line.
   Strict monochrome: fills + lines are --ink tints, the figure voice carries
   the numerals. No hue. (cards/balance.js)
   -------------------------------------------- */
.balance-card                       { padding: 14px 16px 16px; gap: 10px; }
.balance-card > strong              { align-self: stretch; text-align: left; }

/* Quadrant toggles — top-right of the card. Same font/format as the orb
   HRV/RHR/SlpQ/SlpHR toggles, but fill stays transparent (outline only). */
.balance-card .bal-quad-toggles {
    position: absolute;
    top: 14px;
    right: 16px;
    z-index: 3;
    display: flex;
    gap: 4px;
}
.balance-card .bal-quad-toggle {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.04em;
    line-height: 1.3;
    padding: 2px 6px;
    cursor: pointer;
    background: transparent;
    color: var(--ink-40);
    border: 1px solid var(--ink-25);
}
.balance-card .bal-quad-toggle.on {
    background: transparent;        /* fill stays transparent when active */
    color: var(--fg);
    border-color: var(--fg);
}

/* --------------------------------------------
   Composite drill-down grid — the 2×2 of stress-orb scenes (one per quadrant).
   For now every quadrant is a duplicate stress orb. Non-stress views hide the
   cells and show a centered placeholder stub. Each .bal-orb-cell mirrors the
   stress-quadrant control layout (mode/size, toggles, palette, caption foot).
   -------------------------------------------- */
.balance-card .bal-grid-orbs {
    flex: 1;
    min-height: 0;
    position: relative;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 10px;
}
.balance-card .bal-grid-orbs[data-view="stress"] .bal-view-stub { display: none; }
.balance-card .bal-grid-orbs:not([data-view="stress"]) .bal-orb-cell { display: none; }
.balance-card .bal-view-stub {
    grid-column: 1 / -1;
    grid-row: 1 / -1;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    color: var(--ink-40);
}

.balance-card .bal-orb-cell {
    position: relative;
    overflow: hidden;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px 10px;
    border: 1px solid var(--ink-15);
}
.balance-card .bal-orb-cell .stress-pc-3d {
    flex: 1;
    min-height: 0;
    width: 100%;
    position: relative;
    cursor: grab;
}
.balance-card .bal-orb-cell .stress-pc-3d:active { cursor: grabbing; }
.balance-card .bal-orb-cell .stress-pc-3d canvas { display: block; }
.balance-card .bal-orb-cell .stress-pc-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.balance-card .bal-orb-cell .stress-pc-cap { align-self: flex-start; }
.balance-card .bal-orb-cell .stress-pc-toggles {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
    display: flex;
    gap: 4px;
}
.balance-card .bal-orb-cell .stress-pc-toggle {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.04em;
    line-height: 1.3;
    padding: 2px 6px;
    cursor: pointer;
    color: var(--ink-40);
    background: var(--bg);
    border: 1px solid var(--ink-25);
}
.balance-card .bal-orb-cell .stress-pc-toggle.on {
    color: var(--bg);
    background: var(--fg);
    border-color: var(--fg);
}
.balance-card .bal-orb-cell .stress-pc-modes {
    position: absolute;
    top: 6px;
    left: 6px;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
}
.balance-card .bal-orb-cell .stress-pc-mode {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.04em;
    line-height: 1.3;
    min-width: 3.4em;
    white-space: nowrap;
    padding: 2px 6px;
    cursor: pointer;
    color: var(--bg);
    background: var(--fg);
    border: 1px solid var(--fg);
}
.balance-card .bal-orb-cell .stress-pc-palette {
    flex: none;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.02em;
    padding: 1px 4px;
    cursor: pointer;
    color: var(--fg);
    background: var(--bg);
    border: 1px solid var(--ink-25);
}

.balance-card .bal-heads {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 14px;
}
.balance-card .bal-head {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    font-weight: 500;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink-55);
    text-align: center;
}

.balance-card .bal-grid {
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    column-gap: 14px;     /* the gap the spine sits inside */
    row-gap: 0;
    position: relative;
}
/* The spine — one heavy vertical rule down the column gutter. */
.balance-card .bal-grid::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 2px;
    transform: translateX(-50%);
    background: var(--fg);
    z-index: 2;
    pointer-events: none;
}

.balance-card .bal-cell {
    position: relative;
    overflow: hidden;
    display: flex;
    background: var(--ink-04);   /* faint vessel ground */
}
/* Hairline cross-rule between the top and bottom rows. */
.balance-card .bal-stress,
.balance-card .bal-readiness        { border-bottom: 1px solid var(--ink-15); }

/* The fill: rises from the base to the score's percent height, topped by a
   crisp level-line. Height is set inline by the renderer. */
.balance-card .bal-fill {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 0%;
    background: var(--ink-08);
    border-top: 1.5px solid var(--ink-40);
    z-index: 0;
    transition: height 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.balance-card .bal-content {
    position: relative;
    z-index: 1;
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 10px 12px;
    min-width: 0;
}
.balance-card .bal-eyebrow {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-55);
    line-height: 1;
}
.balance-card .bal-num {
    line-height: 1;
}
.balance-card .bal-num b {
    font-family: var(--font-mono);
    font-size: clamp(30px, 11cqw, 56px);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--fg);
}
.balance-card .bal-cap {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.02em;
    color: var(--ink-40);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* Stress quadrant — a real 3D (Three.js/WebGL) PC1/PC2/PC3 orb scene replaces
   the vessel fill. The canvas fills the cell body between the eyebrow and the
   β₃D caption; orb radius carries each component's explained variance. Drag to
   rotate (OrbitControls); it idles with a slow auto-spin. */
.balance-card .bal-stress-pc .bal-content {
    padding: 8px 10px;
    gap: 4px;
    justify-content: flex-start;
}
.balance-card .bal-stress-pc .stress-pc-3d {
    flex: 1;
    min-height: 0;
    width: 100%;
    position: relative;
    cursor: grab;
}
.balance-card .bal-stress-pc .stress-pc-3d:active { cursor: grabbing; }
.balance-card .bal-stress-pc .stress-pc-3d canvas { display: block; }
.balance-card .bal-stress-pc .stress-pc-cap { align-self: flex-start; }

/* PC layer toggles — small buttons over the top-right of the 3D canvas. */
.balance-card .bal-stress-pc .stress-pc-toggles {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
    display: flex;
    gap: 4px;
}
.balance-card .bal-stress-pc .stress-pc-toggle {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.04em;
    line-height: 1.3;
    padding: 2px 6px;
    cursor: pointer;
    color: var(--ink-40);
    background: var(--bg);
    border: 1px solid var(--ink-25);
}
.balance-card .bal-stress-pc .stress-pc-toggle.on {
    color: var(--bg);
    background: var(--fg);
    border-color: var(--fg);
}

/* Bottom row — the HRV/RHR/SlpQ/SlpHR legend (left) shares its line with the
   palette picker (right). */
.balance-card .bal-stress-pc .stress-pc-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
/* Palette picker — native select (= drop arrow), aligned with the legend. */
.balance-card .bal-stress-pc .stress-pc-palette {
    flex: none;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.02em;
    padding: 1px 4px;
    cursor: pointer;
    color: var(--fg);
    background: var(--bg);
    border: 1px solid var(--ink-25);
}

/* Mode switch (β ↔ score) — top-left of the 3D canvas. */
.balance-card .bal-stress-pc .stress-pc-modes {
    position: absolute;
    top: 6px;
    left: 6px;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
}
.balance-card .bal-stress-pc .stress-pc-mode {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.04em;
    line-height: 1.3;
    min-width: 3.4em;
    white-space: nowrap;
    padding: 2px 6px;
    cursor: pointer;
    color: var(--bg);
    background: var(--fg);
    border: 1px solid var(--fg);
}

@media (prefers-reduced-motion: reduce) {
    .balance-card .bal-fill { transition: none; }
}

/* --------------------------------------------
   Custom Visual — a solid dark-slab card. Carries its own hero background
   (per Documentation.md "Non-Card Grid Elements": a standalone element must
   paint the slab itself; hero text tokens are paper-colored). A large ▼ glyph
   matches the header's hero arrow. Placeholder for a future visualization.
   (cards/custom-visual.js)
   -------------------------------------------- */
.custom-visual-card {
    background: var(--bg-hero);
    color: var(--color-hero-text);
    border-color: var(--fg);
    padding: 12px 14px;
}
.custom-visual-card > strong {
    align-self: flex-start;
    color: var(--color-hero-text);
    font-family: var(--font-heading, inherit);
    font-weight: 600;
    letter-spacing: 0.01em;
}
.custom-visual-card .cv-arrow {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-hero-text);
    font-size: clamp(40px, 22cqw, 120px);
    line-height: 1;
}

/* --------------------------------------------
   Idiogram — solid all-green card. Green slab + accent from the Copic palette
   (G28 Ocean Green / G14 Apple Green); white text for legibility on the slab.
   Placeholder for a future visualization. (cards/idiogram.js)
   -------------------------------------------- */
.idiogram-card {
    background: transparent;        /* default: outline card; fill via admin */
    color: var(--color-body);       /* theme text (legible on transparent) */
    border-color: var(--fg);        /* match other cards; tracks the theme */
}
.idiogram-card > strong {
    align-self: flex-start;
    color: var(--fg);
    font-family: var(--font-body);   /* match the Stress card title */
}
/* Subheader: "Global Psychophysiological State" drops onto its own line
   directly beneath the Idiograph label, flush to its left edge, in the grey
   stamp ink the composite cards use. (2026-08-13.) */
.idiogram-card > strong > .sleep-stamp-inline {
    display: block;
    margin-left: 0;
    line-height: 1.2;
}
/* Down-arrow row directly below the title (theme ink, matches the hero ▼). */
.idiogram-card .idio-arrows {
    display: flex;
    gap: 18px;
    align-items: center;
    margin: 4px 0 6px;
    color: var(--fg);
    font-size: var(--size-secondary);
    line-height: 1;
}
.idiogram-card .idio-arrow {
    cursor: pointer;
    transition: transform 0.15s ease;
}
.idiogram-card .idio-arrow.on { transform: rotate(180deg); }   /* open state */
.idiogram-card .idio-arrow:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.idiogram-card .idio-viz {
    flex: 1;
    min-height: 0;
    position: relative;          /* anchor for the condition overlays */
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
/* Per-condition overlay slots: four vertical bands over the viz, each toggled
   by its arrow. The balanceCellSvg inside is itself position:absolute/inset:0. */
.idiogram-card .idio-overlay { position: absolute; inset: 0; pointer-events: none; }
.idiogram-card .idio-slot { position: absolute; top: 0; bottom: 0; width: 25%; display: none; }
.idiogram-card .idio-slot[data-slot="0"] { left: 0; right: 0; width: auto; }   /* 2nd dropdown spans full viz box, matching the legend slot */
/* ...and its vessel fill matches the legend slab color (--bg-hero). */
.idiogram-card .idio-slot[data-slot="0"] .bal-overlay rect:first-of-type { fill: var(--bg-hero) !important; }
/* Fill the UPPER row; lower row stays transparent (flip the bottom-up vessel). */
.idiogram-card .idio-slot[data-slot="0"] .bal-overlay { transform: scaleY(-1); }
/* The duplicated Readiness subcards (Sleep / RHR / HRV), in the lower row. */
.idiogram-card .idio-slot[data-slot="0"] .idio-subcards {
    position: absolute;
    left: 0; right: 0; bottom: 0; top: 50%;   /* lower (transparent) row */
    display: flex;
    gap: 10px;
    padding: 12px;
    align-items: stretch;
}
.idiogram-card .idio-subcard {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    background: var(--bg-hero);
    color: var(--color-hero-text);
    border: 1px solid var(--fg);
    padding: 10px 12px;
}
.idiogram-card .idio-subcard strong { font-size: 0.6em; font-weight: 600; }
.idiogram-card .idio-subcard .sleep-stamp-inline { opacity: 0.72; }
.idiogram-card .idio-subcard b { font-size: var(--size-h2); }
.idiogram-card .idio-slot[data-slot="1"] { left: 25%; }
.idiogram-card .idio-slot[data-slot="2"] { left: 50%; }
.idiogram-card .idio-slot[data-slot="3"] { left: 75%; }
.idiogram-card .idio-slot.on { display: block; }

/* Key & Criteria panel (leftmost dropdown). Large, h2-scaled, on the hero
   banner dark slab (--bg-hero) with paper text. Spans the viz; scrolls if tall. */
.idiogram-card .idio-legend-slot {
    position: absolute;
    top: 0; left: 0; right: 0;
    max-height: 100%;
    display: none;
    z-index: 1;                 /* above the condition bands */
}
.idiogram-card .idio-legend-slot.on { display: block; }
.idiogram-card .idio-legend {
    background: var(--bg-hero);            /* hero banner color */
    color: var(--color-hero-text);        /* paper text, legible on the slab */
    border: 1px solid var(--fg);
    padding: 16px 20px;
    font-size: var(--size-h2);            /* 28px */
    line-height: 1.18;
    max-height: 100%;
    overflow: auto;
    display: flex;                        /* two sections share the x-axis */
    gap: 24px;
    align-items: flex-start;
}
.idiogram-card .il-sec { flex: 1 1 0; min-width: 0; }
.idiogram-card .il-sec + .il-sec { margin-top: 0; }
.idiogram-card .il-head {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 0.6em;
    color: var(--color-hero-text);
    opacity: 0.72;
    margin-bottom: 8px;
}
.idiogram-card .il-row { display: flex; gap: 14px; align-items: flex-start; margin: 8px 0; }
.idiogram-card .il-glyph { flex: none; width: 1.2em; text-align: center; color: var(--color-hero-text); }
.idiogram-card .il-status { flex: none; width: 2em; font-weight: 700; font-size: 0.7em; color: var(--color-hero-text); }
.idiogram-card .il-text { display: flex; flex-direction: column; min-width: 0; }
.idiogram-card .il-text b { font-weight: 600; }
.idiogram-card .il-desc { font-size: 0.6em; opacity: 0.72; }
/* Grayscale the colored Poincaré render so it reads in the mono system. */
.idiogram-card .idio-viz img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    filter: grayscale(1) contrast(1.03);
}

/* --------------------------------------------
   Stress orb scene duplicated into the Idiograph card (idio-stress-orb).
   Idiograph-scoped MIRROR of the .balance-card .bal-stress-pc control styles so
   the visual is identical — the Temporal Balance card's own rules are untouched.
   Anchored to the card, not the viz: when open the chart fills the entire
   card interior (border to border), sitting over the title/arrows/viz; the
   title and arrow rows are z-raised below so the toggle stays clickable.
   -------------------------------------------- */
.idiogram-card > strong,
.idiogram-card .idio-arrows {
    position: relative;
    z-index: 3;                  /* stay visible/clickable above the orb scene */
}
.idiogram-card .idio-stress-orb {
    position: absolute;
    inset: 0;                    /* full card interior, inside the border */
    z-index: 2;
    display: none;
    flex-direction: column;
    padding: 0;                  /* 2026-08-16: canvas runs border to border */
    gap: 0;
    background: var(--bg);   /* opaque overlay over the viz */
}
/* Footer (DFA caption + palette select) floats over the bottom edge of the
   scene instead of reserving its own row, so the plate gets the full height. */
.idiogram-card .idio-stress-orb .stress-pc-foot {
    position: absolute;
    left: 10px;
    right: 10px;
    bottom: 6px;
    z-index: 2;
    pointer-events: none;
}
.idiogram-card .idio-stress-orb .stress-pc-foot > * { pointer-events: auto; }
.idiogram-card .idio-stress-orb.on { display: flex; }
.idiogram-card .idio-stress-orb .stress-pc-3d {
    flex: 1;
    min-height: 0;
    width: 100%;
    position: relative;
    cursor: grab;
}
.idiogram-card .idio-stress-orb .stress-pc-3d:active { cursor: grabbing; }
.idiogram-card .idio-stress-orb .stress-pc-3d canvas { display: block; }
.idiogram-card .idio-stress-orb .stress-pc-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
/* DFA caption: 15 metrics × "name α=… (R²=…)" is long — keep it a compact,
   scrollable footer strip instead of letting card-scale text flood the scene. */
.idiogram-card .idio-stress-orb .stress-pc-cap {
    align-self: flex-start;
    font-size: 10px;
    line-height: 1.35;
    max-height: 4.2em;
    overflow-y: auto;
    min-width: 0;
}
.idiogram-card .idio-stress-orb .stress-pc-toggles {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
    display: flex;
    gap: 4px;
}
.idiogram-card .idio-stress-orb .stress-pc-toggle {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.04em;
    line-height: 1.3;
    padding: 2px 6px;
    cursor: pointer;
    color: var(--ink-40);
    background: var(--bg);
    border: 1px solid var(--ink-25);
}
.idiogram-card .idio-stress-orb .stress-pc-toggle.on {
    color: var(--bg);
    background: var(--fg);
    border-color: var(--fg);
}
.idiogram-card .idio-stress-orb .stress-pc-modes {
    position: absolute;
    top: 6px;
    left: 6px;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
}
.idiogram-card .idio-stress-orb .stress-pc-mode {
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.04em;
    line-height: 1.3;
    min-width: 3.4em;
    white-space: nowrap;
    padding: 2px 6px;
    cursor: pointer;
    color: var(--bg);
    background: var(--fg);
    border: 1px solid var(--fg);
}
.idiogram-card .idio-stress-orb .stress-pc-palette {
    flex: none;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.02em;
    padding: 1px 4px;
    cursor: pointer;
    color: var(--fg);
    background: var(--bg);
    border: 1px solid var(--ink-25);
}

/* --------------------------------------------
   Sleep Quality detail panel — second hero dropdown (mirrors .admin-panel).
   Its ▼ arrow sits just left of the admin arrow. The score card spans the
   full top row; the seven sub-field cards flow beneath in a 3-column grid.
   Cards carry the standard --card-buffer margin (the "+ buffer").
   -------------------------------------------- */
header.hero .hero-arrow-sleep         { right: 40px; }   /* left of the admin ▼ (right:12px) */
header.hero .hero-arrow-sleep3        { right: 68px; }   /* third arrow (Stress), left of the sleep ▼ */
header.hero .hero-arrow-readiness     { right: 96px; }   /* fourth arrow (Readiness), left of the stress ▼ */

/* Sleep Quality dropdown — the standalone panel (pd_sleepquality_dropdown.js),
   driven by the #sleep-arrow hero ▼ and appended into header.hero. The script
   ships its own namespaced .pd-sq-panel styling; here we only anchor it under
   the hero (drop from the bottom-right) like the admin panel. The legacy Alpine
   .sleep-panel / .sleep-panel-grid / SLEEP_PANEL_CARDS chrome was removed
   2026-06-09 in favor of that self-contained file. */
header.hero .pd-sq-panel,
header.hero .pd-st-panel,
header.hero .pd-rd-panel  { top: calc(100% - 1px); right: 8px; text-align: left; }

/* --------------------------------------------
   Hero sync stamp — top-left telemetry readout.
   The .hero-stamp container is positioned by configapa_mono.css (absolute,
   top:8px left:12px, color:var(--bg) on the dark hero band). Here it becomes a
   two-line stack: the wordmark recedes; a live "last synced" line — fed by
   js/sync-stamp.js from today_payload().synced_at — leads.
   Monochrome-honest: state is signalled by alpha + motion, never hue.
   -------------------------------------------- */
header.hero .hero-stamp {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    line-height: 1;
    text-align: left;
    opacity: 1;                       /* override configapa's container dim; children carry their own */
}
.sync-readout {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: var(--size-footnote);
    font-weight: 400;
    letter-spacing: 0.2px;
    font-variant-numeric: tabular-nums;
    opacity: 0.95;
    cursor: default;
}
.sync-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: currentColor;
    flex-shrink: 0;
    /* Alpha + scale pulse — never a color shift; honors the mono system's
       "cycle by alpha, not hue" rule. This base pulse IS the "fresh" state;
       the stale/unknown rules below opt out of it. Disabled globally under
       prefers-reduced-motion (configapa_mono.css). */
    animation: sync-pulse 2.4s ease-in-out infinite;
}
.sync-label  { font-weight: 700; letter-spacing: 0.6px; }
.sync-clock  { opacity: 0.6; }
.sync-sep    { opacity: 0.4; }

/* Stale (>30 min): pulse stops, the line recedes. The "Xm ago" already tells
   the story; the dimming makes it felt at a glance. */
.sync-readout[data-state="stale"]            { opacity: 0.55; }
.sync-readout[data-state="stale"] .sync-dot  { animation: none; opacity: 0.5; }

/* No watermark yet (cold start / offline with empty cache): quietest state. */
.sync-readout[data-state="unknown"]           { opacity: 0.45; }
.sync-readout[data-state="unknown"] .sync-dot { animation: none; opacity: 0.35; }
.sync-readout[data-state="unknown"] .sync-sep,
.sync-readout[data-state="unknown"] .sync-clock { display: none; }

@keyframes sync-pulse {
    0%, 100% { opacity: 0.35; transform: scale(0.82); }
    50%      { opacity: 1;    transform: scale(1.12); }
}

/* Narrow viewports: collapse to one compact line so the stamp clears the
   centered hero title. The absolute clock drops; the live "● SYNCED Xm ago"
   — the part that earns its place — stays. */
@media (max-width: 700px) {
    .sync-sep,
    .sync-clock { display: none; }
}

/* --------------------------------------------
   Calendar section masthead — "PhD Dashboard"
   An inverted ink slab (mirrors the page hero: --fg ground, paper text)
   that announces the schedule grid below it. Pinned to the same
   16-cell canvas width as .grid, so its left/right edges meet the
   calendar. Ops-surface character without invented copy: faint paper
   hairlines on the --cell-size step, corner registration brackets, and
   editorial rules flanking the title. One staggered reveal on load;
   under reduced-motion it renders as a static slab (final state is the
   default; motion is layered on only when the user allows it).
   -------------------------------------------- */
.cal-hero {
    position: relative;
    width: calc(var(--cell-size) * 16);
    max-width: 100vw;
    margin: 0 auto 14px;
    padding: 30px 24px;
    /* Same tokens as header.hero (configapa_mono.css) — the two banners
       always share a color. */
    background: var(--bg-hero);
    color: var(--color-hero-text);
    overflow: hidden;
    isolation: isolate;
}

.cal-hero-inner {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}

/* Rules + title share one centered row; the credentials sit on the next. */
.cal-hero-titlerow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: clamp(12px, 3vw, 40px);
    align-self: stretch;
}

/* Title metrics LINKED to the page hero (header.hero h1) via the shared
   --hero-title-size / --color-hero-text tokens — always the same size and
   color by construction. */
.cal-hero-title {
    margin: 0;
    font-family: var(--font-body);
    font-weight: 700;
    font-size: var(--hero-title-size);
    letter-spacing: 0.4px;
    line-height: 1.1;
    white-space: nowrap;
    color: var(--color-hero-text);
}

/* Name + credentials, matched to the hero subtitle (header.hero .hero-subtitle):
   16px, 400, 0.2px tracking, paper at 0.9. */
.cal-hero-subtitle {
    margin: 0;
    font-family: var(--font-body);
    font-weight: 400;
    font-size: var(--hero-subtitle-size);   /* linked to header.hero .hero-subtitle */
    letter-spacing: 0.2px;
    color: var(--color-inverse-text);       /* same token as .hero-subtitle */
    opacity: 0.9;
}

/* Hero Banner — sits directly after the .cal-hero content title. A slim,
   centered strip that reuses the hero color tokens so it always tracks the
   two mastheads. Full banner width inside the .cal-hero-inner column. */
.phd-hero-banner {
    align-self: stretch;
    padding: 18px 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    text-align: center;
    /* Standalone grid row (not nested in the dark masthead), so it carries its
       own hero slab — same tokens as .cal-hero — or its hero-white text would
       be invisible on the paper page background. */
    background: var(--bg-hero);
    color: var(--color-hero-text);
}
/* Title + subtitle linked to the same shared hero tokens as the masthead
   (.cal-hero-title / .cal-hero-subtitle), so the two banners stay identical
   in size and color by construction. */
.phd-hero-banner-title {
    margin: 0;
    font-family: var(--font-body);
    font-weight: 700;
    font-size: var(--hero-title-size);
    letter-spacing: 0.4px;
    line-height: 1.1;
    white-space: nowrap;
    color: var(--color-hero-text);
}
.phd-hero-banner-subtitle {
    margin: 0;
    font-family: var(--font-body);
    font-weight: 400;
    font-size: var(--hero-subtitle-size);
    letter-spacing: 0.2px;
    color: var(--color-inverse-text);
    opacity: 0.9;
}

@media (prefers-reduced-motion: no-preference) {
    .cal-hero          { animation: calhero-slab 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
    .cal-hero-title    { animation: calhero-rise 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) 0.18s both; }
    .cal-hero-subtitle { animation: calhero-rise 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) 0.30s both; }
    .phd-hero-banner   { animation: calhero-rise 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) 0.42s both; }
}

@keyframes calhero-slab {
    from { opacity: 0; clip-path: inset(0 50% 0 50%); }
    to   { opacity: 1; clip-path: inset(0 0 0 0); }
}
@keyframes calhero-rise {
    from { opacity: 0; transform: translateY(0.5em); }
    to   { opacity: 1; transform: none; }
}


/* ============================================================
   MathVisual theme layer — ADDITIVE, REVERSIBLE.
   Ported from the MathVisual landing template. Brings the
   instrument-readout identity to the dashboard without touching
   card JS, the grid framework, or the base token files:
     · Fraunces display on the three mastheads
     · Spline Sans Mono on the live sync telemetry
     · a blue→red physiological state axis (blue = recovered,
       red = strained) on the .c-blue / .c-red utilities
     · magenta (--mv-live) reserved for the live/interactive accent
   Delete this block (and the <style id="mathvisual-theme"> in
   index.html) to revert to the pure monochrome surface.
   ============================================================ */

/* Display face on the three mastheads. They hardcode var(--font-body),
   so set the family explicitly here; weight relaxes from 700 → 440 because
   Fraunces carries authority at its own weight and 700 reads heavy. */
header.hero h1,
.cal-hero-title,
.phd-hero-banner-title {
    font-family: "Fraunces", Georgia, serif;
    font-weight: 440;
    letter-spacing: -0.01em;
}

/* Telemetry stamp → mono. The "last synced" line is a readout; the mono face
   makes it read as instrument output rather than prose. */
header.hero .hero-stamp,
.sync-readout {
    font-family: var(--font-mono);
    letter-spacing: 0.02em;
}

/* The live sync accent. "Synced" (the fresh, pulsing state) is the one truly
   live signal on the page → it earns the magenta. Stale/unknown keep receding
   by alpha (rules above) and override this back toward ink. */
.sync-readout[data-state="fresh"],
.sync-readout:not([data-state="stale"]):not([data-state="unknown"]) {
    color: var(--mv-live);
}

/* Semantic state axis. The mono system rendered .c-blue / .c-red as ink
   opacities (cycle-by-alpha). With a real physiological meaning available,
   give them real hue: blue = recovered / in-range, red = strained / elevated. */
.c-blue { color: var(--mv-complex);   fill: var(--mv-complex);   stroke: var(--mv-complex); }
.c-red  { color: var(--mv-collapsed); fill: var(--mv-collapsed); stroke: var(--mv-collapsed); }

/* Drill-down hero arrows: ink by default, magenta on hover/focus to signal
   they are the interactive controls. */
header.hero .hero-arrow:hover,
header.hero .hero-arrow:focus-visible { color: var(--mv-live); }


/* --------------------------------------------
   Composite score cards (Stress · Readiness · Movement · Sleep Quality):
   center the graphic on the CARD BORDER BOX, not on the leftover space under
   the title. Each wrap is pulled out of the flex column and pinned to inset:0
   — which ignores the card's 8px/10px padding — so its flex centering resolves
   against the true geometric center of the bordered box. The title and the ▼
   float above it on z-index 1.
   (2026-08-13, alongside the 2-row growth of these four cards.)
   -------------------------------------------- */

[data-card="stress"]          > .stress-ball-wrap,
[data-card="readiness-score"] > .readiness-battery-wrap,
[data-card="movement"]        > .movement-wrap,
[data-card="sleep"]           > .sleepq-wrap {
    position: absolute;
    inset: 0;
    flex: none;
    margin: 0;
    z-index: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Keep the title legible if a graphic grows tall enough to reach it. */
[data-card="stress"]          > strong,
[data-card="readiness-score"] > strong,
[data-card="movement"]        > strong,
[data-card="sleep"]           > strong {
    position: relative;
    z-index: 1;
}
[data-card="stress"]          > .card-arrow,
[data-card="readiness-score"] > .card-arrow,
[data-card="movement"]        > .card-arrow,
[data-card="sleep"]           > .card-arrow { z-index: 2; }

/* Sleep Quality score disc — now the tiered .sleepq-ball element (see the
   .sleepq-ball rule above; colour = inverse of the Stress tiers, no pulse).
   The fixed --ink-25 ::before disc that lived here (2026-08-13) was removed
   2026-08-16 — it sat under the tiered disk and masked its colour. */
[data-card="sleep"] .sleepq-score {
    position: relative;
    z-index: 1;
}

/* Composite cards (Stress · Readiness · Movement · Sleep Quality): drop the
   grey stamp nested inside the title <strong> onto its own line, flush to the
   title's left edge, instead of trailing the label inline. Same treatment the
   small signal cards already get above — extended to the four row-1 composites
   2026-08-13. */
[data-card="stress"]          > strong > .sleep-stamp-inline,
[data-card="readiness-score"] > strong > .sleep-stamp-inline,
[data-card="movement"]        > strong > .sleep-stamp-inline,
[data-card="sleep"]           > strong > .sleep-stamp-inline {
    display: block;
    margin-left: 0;
    line-height: 1.2;
}

/* --------------------------------------------
   1×1 cards: shrink the text chrome by 4px so the smallest cells stop feeling
   crowded. A 1×1 is tagged [data-w="1"][data-h="1"] by mount.js — the card-w{N}
   classes only cover widths 2–4, so class sniffing would also catch the 8-wide
   Balance and Idiograph cards. Label drops 20 → 16px; the grey
   sublabel / signal stamp drops 16 → 12px. Values (b, .*-score) are untouched.
   (2026-08-13.)
   -------------------------------------------- */
.card[data-w="1"][data-h="1"] > strong {
    font-size: calc(var(--size-secondary) - 4px);
}
.card[data-w="1"][data-h="1"] .sleep-stamp-inline,
.card[data-w="1"][data-h="1"] .placeholder-sub {
    font-size: calc(var(--size-footnote) - 4px);
}

/* 1×1 cards: the value sits centered along the BOTTOM edge. `margin-top: auto`
   consumes the free space in the card's flex column, pushing the value span to
   the floor; the inner flex centers the glyph horizontally. Excludes the grey
   stamp spans (.sleep-stamp-inline on hr / hr-zone cards, .placeholder-sub)
   and the accordion stubs' .acc-chev, which are siblings of <strong> and keep
   their own placement. Overrides the earlier
   .placeholder-card centering by specificity. (2026-08-13.) */
.card[data-w="1"][data-h="1"] > span:not(.sleep-stamp-inline):not(.placeholder-sub):not(.acc-chev) {
    margin-top: auto;
    width: 100%;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    text-align: center;
}

/* ============================== GABS layers ==============================
   The four full-width GABS idiograph-layer cards (rows 11–34,
   cards/giant-placeholders.js). Each hosts a literal copy of the Idiograph's
   matte-plate orb scene (stress-pc-3d-plate.js) — a GABS-scoped mirror of the
   .idiogram-card .idio-stress-orb rules so the visual is identical; the
   Idiograph's own rules are untouched. Unlike the Idiograph overlay, the
   plate here is always on and lives in normal flow beneath the card title. */
.gabs-card .gabs-plate {
    flex: 1;
    min-height: 0;
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
}
.gabs-card .gabs-plate .stress-pc-3d {
    flex: 1;
    min-height: 0;
    width: 100%;
    position: relative;
    cursor: grab;
}
.gabs-card .gabs-plate .stress-pc-3d:active { cursor: grabbing; }
.gabs-card .gabs-plate .stress-pc-3d canvas { display: block; }
.gabs-card .gabs-plate .stress-pc-foot {
    position: absolute;
    left: 10px;
    right: 10px;
    bottom: 6px;
    z-index: 2;
    pointer-events: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.gabs-card .gabs-plate .stress-pc-foot > * { pointer-events: auto; }
.gabs-card .gabs-plate .stress-pc-cap {
    align-self: flex-start;
    font-size: 10px;
    line-height: 1.35;
    max-height: 4.2em;
    overflow-y: auto;
    min-width: 0;
}
/* GABS Layer1 floor (cards/gabs-floor.js): the spherobot grid floor, always
   on, header toggle to switch it off. Mirrors the .gabs-plate footprint. */
.gabs-card .gabs-floor {
    flex: 1;
    min-height: 0;
    width: 100%;
    position: relative;
    overflow: hidden;
    cursor: grab;
}
.gabs-card .gabs-floor:active { cursor: grabbing; }
.gabs-card .gabs-floor canvas { display: block; width: 100%; height: 100%; }
.gabs-card .gabs-floor.is-off,
.balance-card .balance-floor.is-off { cursor: default; }
.gabs-card .gabs-floor.is-off canvas,
.balance-card .balance-floor.is-off canvas { visibility: hidden; }
.gabs-card .gabs-floor .gabs-floor-off,
.balance-card .balance-floor .gabs-floor-off {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.02em;
    color: var(--ink-40);
}
.gabs-card .gabs-floor .gabs-floor-off[hidden],
.balance-card .balance-floor .gabs-floor-off[hidden] { display: none; }
/* The floor toggles are shared by the GABS Layer1 card and the Temporal
   Balance snapshot, so the selector covers both scopes. */
.gabs-card .gabs-floor-toggle,
.balance-card .gabs-floor-toggle {
    margin-left: 10px;
    padding: 1px 6px;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.02em;
    line-height: 1.2;
    color: var(--ink-55);
    background: transparent;
    border: 1px solid var(--ink-25);
    border-radius: 2px;
    cursor: pointer;
    vertical-align: middle;
}
.gabs-card .gabs-floor-toggle:hover,
.balance-card .gabs-floor-toggle:hover { color: var(--ink-85); border-color: var(--ink-55); }
.gabs-card .gabs-floor-toggle[aria-pressed="false"],
.balance-card .gabs-floor-toggle[aria-pressed="false"] { opacity: 0.55; }

/* Toggle row — nine buttons on Layer1, six on Temporal Balance, too many to
   trail the title inline. Its own wrapping row under the title; the inline
   left margin the buttons carry beside a title is dropped here, the row's own
   gap spaces them. */
.gabs-toggles {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 6px;
    margin: 2px 0 6px;
}
.gabs-toggles .gabs-floor-toggle { margin-left: 0; }
/* Layer1 window drop-down (2026-09-19): a native select wearing the toggle's
   own type, outline and padding, first in the row. */
/* Heat select (2026-09-28): the Layer1 heat quantity, same dress. */
/* Map select (2026-10-12): heat / temperature / communality, same dress. */
.gabs-toggles .gabs-window-select,
.gabs-toggles .gabs-heat-select,
.gabs-toggles .gabs-map-select {
    appearance: auto;
    padding: 0 2px;
    color: var(--ink-85);
    border-color: var(--ink-55);
    background: var(--bg);
    font-weight: 600;
}

/* AR(1) lane note (φ · t½ · σ) appended to a lane label by gabs-axes.js (2026-09-28). */
.gabs-factor-note { opacity: 0.7; }

/* Floor colour legends (cards/gabs-legend.js, 2026-09-19): the legend strip
   above a floor host (between the toggle rows and the floor), shared by Layer1 and the snapshot cards. Classic heat-map
   legend boxes — contiguous swatches with their values beneath. Two rows, side
   by side on a wide card, stacked on a narrow one — heat and temp — each shown
   only while its own toggle is on. Fixed height; the floor (flex: 1) gives up
   the room. The heat swatches carry the wrap surface's opacity inline, over
   the card ground, so they match the translucent surface on the floor. */
.gabs-legend {
    flex: none;
    display: flex;
    flex-wrap: wrap;
    gap: 4px 24px;
    /* pushed down off the toggle rows, against the visual; clicks pass through */
    margin: 12px 0 2px;
    position: relative;
    z-index: 2;
    pointer-events: none;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.02em;
    line-height: 1.2;
    color: var(--ink-55);
}
/* Layer1 and the Idiograph leave an empty band at the top of the floor host
   (the wave sits lower in the frame), so there the legend is laid over it;
   the 7-day floor fills its host to the top and gets no overlap.
   SUPERSEDED 2026-10-12 (Sean): the legend now shares the map select's row
   (.gabs-head-row below), so the overlap is off; the rule is kept here.
[data-card="giant-1"] .gabs-legend,
[data-card="idiogram"] .gabs-legend { margin-bottom: -30px; }
*/
/* 2026-10-12 (Sean): one header row on every floor card — the map select at
   the left, the active legend row(s) to its right, vertically centred on the
   select; the hidden controls sit in the same .gabs-toggles and cost no
   width (display: none). The legend wraps under the select only when the
   card is too narrow for a legend row beside it. */
.gabs-head-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 18px;
    margin: 2px 0 6px;
}
.gabs-head-row .gabs-toggles { flex: none; margin: 0; }
/* min-content keeps a legend row whole: beside the select where the row fits
   (Layer1), under it where it does not (the 8-column cards: a row is ≈ 650 px,
   the select ≈ 175 px). */
.gabs-head-row .gabs-legend {
    flex: 0 1 auto;
    min-width: min-content;
    margin: 0;
    pointer-events: none;
}
.gabs-head-row .gabs-legend-row { flex: 0 0 auto; min-width: min-content; }
/* 8-column cards (Sean, 2026-10-12): the legend sits BESIDE the map select on
   one row. The fixed text columns and 28 px boxes (sized for two stacked rows)
   are released here — only one legend row shows at a time now — and the boxes
   drop to 23 px with 9 px tick values so the row fits a 702 px host. */
.balance-card .gabs-head-row .gabs-legend { flex: 1 1 0; min-width: 0; }
.balance-card .gabs-head-row .gabs-legend-row { flex: 1 1 0; min-width: 0; gap: 6px; }
.balance-card .gabs-head-row .gabs-legend-label { min-width: 0; font-size: 10px; }
.balance-card .gabs-head-row .gabs-legend-end { font-size: 10px; }
.balance-card .gabs-head-row .gabs-legend-label + .gabs-legend-end { min-width: 0; }
.balance-card .gabs-head-row .gabs-legend-boxes { flex: 1 1 auto; min-width: 0; max-width: 280px; }
.balance-card .gabs-head-row .gabs-legend-box { flex: 1 1 0; min-width: 0; width: auto; }
.balance-card .gabs-head-row .gabs-legend-tickval { font-size: 9px; overflow: hidden; }
.gabs-legend-row { flex: 1 1 440px; min-width: 0; display: flex; align-items: flex-start; gap: 8px; }
.gabs-legend-row[hidden] { display: none; }
.gabs-legend-label, .gabs-legend-end { white-space: nowrap; line-height: 14px; }
/* fixed text columns so the two box runs line up, stacked or side by side */
.gabs-legend-label { color: var(--ink-70); min-width: 182px; }
.gabs-legend-label + .gabs-legend-end { min-width: 80px; text-align: right; }
.gabs-legend-value { color: var(--ink-85); font-weight: 600; }
.gabs-legend-boxes { display: flex; flex: none; }
.gabs-legend-box {
    flex: none;
    width: 28px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
}
.gabs-legend-swatch {
    position: relative;
    height: 14px;
    box-sizing: border-box;
    border: 1px solid var(--ink-25);
    margin-left: -1px;            /* contiguous boxes share their edges */
    background: var(--bg);
}
.gabs-legend-box:first-child .gabs-legend-swatch { margin-left: 0; }
.gabs-legend-swatch > span { position: absolute; inset: 0; }
.gabs-legend-box.is-current .gabs-legend-swatch {
    border: 2px solid var(--fg);
    z-index: 1;
}
.gabs-legend-tickval {
    height: 11px;
    margin-top: 2px;
    font-size: 10px;
    line-height: 11px;
    letter-spacing: 0;
    text-align: center;
    white-space: nowrap;
}

/* Temporal Balance 14-day snapshot (cards/balance.js): the same floor host as
   the GABS Layer1 card, live since 2026-09-16 (frame loop, ball, two control rows).
   The old .balance-card .bal-* rules above are unused while the card's previous
   content is commented out; they stay so restoring it is one edit. */
.balance-card .balance-floor {
    flex: 1;
    min-height: 0;
    width: 100%;
    position: relative;
    overflow: hidden;
    cursor: grab;
}
.balance-card .balance-floor:active { cursor: grabbing; }
.balance-card .balance-floor canvas { display: block; width: 100%; height: 100%; }

/* 2026-09-16: small table under the Temporal Balance floor (balance.js
   `table` option) — styled like a resting floor toggle: mono footnote type,
   ink-55, the light-grey 1px outline, 0.55 opacity. Cells empty for now. */
.balance-card .gabs-mini-table {
    flex: none;
    width: 100%;
    margin-top: 6px;
    border-collapse: collapse;
    table-layout: fixed;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0.02em;
    line-height: 1.2;
    color: var(--ink-55);
    opacity: 0.55;
}
.balance-card .gabs-mini-table th.gabs-mini-label {
    width: 12em;   /* 9em → 12em (Sean, 2026-10-12): the full constituent name, never an ellipsis */
    padding: 1px 6px;
    border: 1px solid var(--ink-25);
    border-radius: 2px;
    font-weight: 400;
    text-align: left;
    color: var(--ink-70);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.balance-card .gabs-mini-table td {
    height: 1.4em;
    padding: 1px 6px;
    border: 1px solid var(--ink-25);
    border-radius: 2px;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.balance-card .gabs-mini-table .gabs-mini-head { color: var(--ink-70); }
.balance-card .gabs-mini-table td.is-warm { color: #cc0000; }
.balance-card .gabs-mini-table td.is-cool { color: #0033cc; }

/* 2026-09-16: radio button pinned to the card's bottom-right corner
   (balance.js `radio` option; checked by default). */
.balance-card { position: relative; }
.balance-card .gabs-corner-radio {
    position: absolute;
    right: 10px;
    bottom: 8px;
    margin: 0;
    line-height: 0;
    z-index: 3;
}
.balance-card .gabs-corner-radio input {
    margin: 0;
    width: 13px;
    height: 13px;
    accent-color: var(--ink-55);
    cursor: pointer;
}

/* 2026-10-12 (Sean): play button pinned to the top-right corner of each floor
   card (giant-placeholders.js, balance.js `gabs-play`). ▶ at rest — the flat
   heat map, amplitude 0, motion off — ❚❚ while the wave plays. Same mono type,
   ink and outline family as .gabs-floor-toggle, but its own class so the
   toggles' [aria-pressed="false"] dimming does not fade the card's one visible
   action; disabled while the floor is off. */
.gabs-card, .balance-card { position: relative; }
.gabs-play {
    position: absolute;
    top: 10px;
    right: 12px;
    z-index: 3;
    min-width: 30px;
    padding: 1px 7px;
    font-family: var(--font-mono);
    font-size: var(--size-footnote);
    letter-spacing: 0;
    line-height: 1.3;
    text-align: center;
    color: var(--ink-55);
    background: var(--bg);
    border: 1px solid var(--ink-25);
    border-radius: 2px;
    cursor: pointer;
}
.gabs-play:hover { color: var(--ink-85); border-color: var(--ink-55); }
.gabs-play[aria-pressed="true"] { color: var(--ink-85); border-color: var(--ink-55); }
.gabs-play:disabled { opacity: 0.35; cursor: default; }

/* GABS edge pan arrows (gabs-floor.js, 2026-09-16): ‹ › appear when the pointer
   nears the floor host's left/right edge; a click pans the view that way. */
.gabs-pan {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 28px;
    height: 56px;
    display: grid;
    place-items: center;
    padding: 0;
    border: 1px solid var(--ink-40);
    border-radius: 6px;
    background: var(--bg);
    color: var(--ink-55);
    font-family: var(--font-mono);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    opacity: 0.85;
    z-index: 2;
}
.gabs-pan:hover { color: var(--ink-85); border-color: var(--ink-55); opacity: 1; }
.gabs-pan-left  { left: 6px; }
.gabs-pan-right { right: 6px; }
.gabs-pan[hidden] { display: none; }

/* GABS axes overlay (cards/gabs-axes.js): HTML labels projected over the floor
   canvas — ring day-ages, constituent headings, the 0.3 m edge rule. Absolutely
   positioned inside the floor host (.gabs-floor / .balance-floor are already
   position:relative; overflow:hidden), never hit-testable. */
.gabs-axis-label {
    position: absolute;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.02em;
    color: var(--ink-55);
    pointer-events: none;
    transform: translate(-50%, -50%);
    white-space: nowrap;
}
.gabs-axis-label[hidden] { display: none; }
.gabs-z-label.is-warm, .gabs-factor-value.is-warm { color: #cc0000; }
.gabs-z-label.is-cool, .gabs-factor-value.is-cool { color: #0033cc; }
.gabs-amp-label { color: var(--ink-70); }

/* Idiograph HRV delay-embedding overlay (cards/idiogram.js + hrv-embedding.js).
   Re-enabled 2026-09-12: the HRV ▼ toggles it. Anchored to .idio-viz, hidden
   until .on, same treatment as the stress-orb overlay. */
.idiogram-card .idio-hrv-embed {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: none;
    background: var(--bg);
}
.idiogram-card .idio-hrv-embed.on { display: block; }
.idiogram-card .idio-hrv-embed .idio-hrv-cell {
    width: 100%;
    height: 100%;
    position: relative;
    cursor: grab;
}
.idiogram-card .idio-hrv-embed .idio-hrv-cell:active { cursor: grabbing; }
.idiogram-card .idio-hrv-embed .idio-hrv-cell canvas { display: block; }

/* 2026-09-12: axis-overlay label sizes. z-score labels were unreadable at 10px;
   they carry the numbers the user reads, so they get the footnote size in
   semibold with a paper halo; amplitude labels follow at 12px. */
.gabs-axis-label { font-size: 11px; }
.gabs-z-label {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.01em;
    text-shadow: 0 0 3px var(--bg), 0 0 3px var(--bg), 0 0 6px var(--bg);
}
.gabs-amp-label { font-size: 12px; font-weight: 500; text-shadow: 0 0 3px var(--bg), 0 0 6px var(--bg); }
/* 2026-09-13: fixed constituent table (gabs-axes.js) — one row per constituent,
   name · z · amp, anchored in the floor host's lower-left corner. */
.gabs-axis-table {
    position: absolute;
    left: 12px;
    bottom: 36px;
    display: grid;
    grid-template-columns: auto auto auto;
    gap: 2px 14px;
    align-items: baseline;
    pointer-events: none;
    z-index: 2;
}
.gabs-axis-table .gabs-axis-label { position: static; transform: none; }
.gabs-axis-table .gabs-table-name { color: var(--ink-70); }

/* 2026-09-12: labels sized in scene units — --gabs-label-px is set per frame by
   gabs-axes.js to the projected height of a 4 cm drive wheel (clamped 12–48px).
   z labels = one tire; amplitude labels 0.75 tire; ring/spoke labels 0.6 tire. */
.gabs-axis-label { font-size: calc(var(--gabs-label-px, 14px) * 0.6); }
.gabs-amp-label  { font-size: calc(var(--gabs-label-px, 14px) * 0.75); }
.gabs-z-label    { font-size: var(--gabs-label-px, 14px); }
/* 2026-09-16: factor labels (gabs-axes.js) — constituent name · live z, one per
   lane at the age rim (linear) or on the heading (polar). Pinned into frame. */
.gabs-factor-label {
    font-size: 14px;        /* fixed: does not scale with the floor zoom */
    font-weight: 500;
    color: var(--ink-55);   /* the name; .gabs-factor-value carries the warm/cool colour */
}
.gabs-factor-label.is-right { transform: translate(-100%, -50%); }   /* text ends at the anchor, floor to its right */
/* 2026-10-12 (Sean): lane labels beside the anchor-day (right) edge — the
   name only, left-aligned outward from the rim, attached to the band by a
   1 px leader dash (gabs-axes.js labelSide 'anchor'; 'rim' + labelValue is
   the previous rendering, retained). The leader is a positioned span whose
   left/top/width gabs-axes.js sets per frame from the projected rim point. */
.gabs-factor-label.is-left { transform: translate(0, -50%); }   /* text starts at the anchor, floor to its left */
.gabs-lane-leader {
    position: absolute;
    height: 1px;
    background: var(--ink-55);
    pointer-events: none;
    transform: translateY(-50%);
}
.gabs-lane-leader[hidden] { display: none; }
/* 2026-10-12 (Sean): the day (time-axis) tick labels at the lane labels'
   size, bold; gabs-axes.js drops any that would overlap at this size. */
.gabs-time-label {
    font-size: 14px;
    font-weight: 700;
    color: var(--ink-70);
}
/* 2026-10-12 (Sean): cell data labels (gabs-axes.js cell layer,
   gabs-cell-labels.js) — one per lane per day, centred in its lattice cell,
   the quantity the heat paints, formatted like the mini table. Small mono
   (the legend tick size), dark ink with a faint paper halo so it reads over
   saturated red and blue, no colour coding (the cell colour carries the sign).
   Shown only while a cell projects ≥ 36 × 14 px (CELL_MIN_W_PX / CELL_MIN_H_PX). */
.gabs-cell-label {
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0;
    color: var(--ink-85);
    text-shadow: 0 0 2px var(--bg), 0 0 2px var(--bg), 0 0 4px var(--bg);
    z-index: 1;
}

/* Idiograph Orb + Rollertruck anatomy panel (cards/idiogram.js + idio-anatomy.js).
   2026-09-13: third ▼. Card-anchored like .idio-stress-orb; the module builds
   the stage rail, hint and tooltip inside .idio-anatomy-host. */
.idiogram-card .idio-anatomy {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: none;
    background: var(--bg);
}
.idiogram-card .idio-anatomy.on { display: block; }
.idiogram-card .idio-anatomy-host {
    width: 100%;
    height: 100%;
    position: relative;
    overflow: hidden;
    cursor: grab;
}
.idiogram-card .idio-anatomy-host:active { cursor: grabbing; }
.idiogram-card .idio-anatomy-host canvas { display: block; width: 100%; height: 100%; }

.idiogram-card .idio-anat-rail {
    position: absolute;
    left: 12px;
    top: 50%;
    transform: translateY(-50%);
    width: 176px;
    padding: 8px;
    z-index: 2;
    border: 1px solid var(--ink-30, rgba(0,0,0,.3));
    background: var(--bg);
    font-family: var(--font-heading);
}
.idiogram-card .idio-anat-rail ol { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
.idiogram-card .idio-anat-stage {
    display: grid;
    grid-template-columns: 23px minmax(0, 1fr);
    align-items: center;
    width: 100%;
    min-height: 31px;
    padding: 0 7px;
    border: 0;
    border-left: 2px solid transparent;
    background: transparent;
    color: var(--ink-70);
    font: inherit;
    font-size: 10px;
    letter-spacing: 0.06em;
    text-align: left;
    cursor: pointer;
}
.idiogram-card .idio-anat-stage span { font-family: var(--font-figure); opacity: 0.65; }
.idiogram-card .idio-anat-stage:hover { color: var(--fg); background: var(--ink-05, rgba(0,0,0,.05)); }
.idiogram-card .idio-anat-stage.is-active { border-left-color: var(--mv-live); color: var(--fg); }
.idiogram-card .idio-anat-stage.is-active span { color: var(--mv-live); opacity: 1; }
.idiogram-card .idio-anat-stage:focus-visible { outline: 2px solid var(--mv-live); outline-offset: 2px; }
.idiogram-card .idio-anat-context {
    display: grid;
    gap: 3px;
    margin-top: 7px;
    padding: 8px 7px 1px;
    border-top: 1px dashed var(--ink-30, rgba(0,0,0,.3));
}
.idiogram-card .idio-anat-context strong { color: var(--fg); font-size: 10px; letter-spacing: 0.06em; }
.idiogram-card .idio-anat-context p { margin: 1px 0 0; color: var(--ink-70); font-size: 8.5px; line-height: 1.4; }

.idiogram-card .idio-anat-hint {
    position: absolute;
    left: 0; right: 0; bottom: 6px;
    margin: 0;
    text-align: center;
    font-family: var(--font-figure);
    font-size: 8.5px;
    letter-spacing: 0.08em;
    color: var(--ink-70);
    pointer-events: none;
    z-index: 2;
}

.idiogram-card .idio-anat-tip {
    position: absolute;
    z-index: 4;
    width: 230px;
    padding: 8px 10px;
    border: 1px solid var(--fg);
    background: var(--bg);
    font-family: var(--font-heading);
    pointer-events: none;
}
.idiogram-card .idio-anat-tip-title { font-family: var(--font-figure); font-size: 8px; letter-spacing: 0.1em; color: var(--mv-live); }
.idiogram-card .idio-anat-tip span { display: block; margin-top: 4px; font-size: 8px; letter-spacing: 0.06em; color: var(--ink-70); }
.idiogram-card .idio-anat-tip strong { display: block; font-size: 11px; color: var(--fg); }
.idiogram-card .idio-anat-tip p { margin: 3px 0 0; font-size: 9px; line-height: 1.4; color: var(--fg); }
.idiogram-card .idio-anat-tip small { display: block; margin-top: 4px; font-family: var(--font-figure); font-size: 8px; color: var(--ink-70); }
.idiogram-card .idio-anat-adds { color: var(--mv-live); }

/* --------------------------------------------
   Snapshot gate (hosted deploy only — js/snapshot-gate.js). Full-screen
   passcode prompt in the mono system; hidden once the payload decrypts.
   -------------------------------------------- */
#snapshot-gate {
    position: fixed;
    inset: 0;
    z-index: 300;                      /* above the connection banner (200) */
    display: grid;
    place-items: center;
    padding: 16px;
    background: var(--bg);
    color: var(--fg);
}
#snapshot-gate[hidden] { display: none; }
.snapshot-gate__form { display: grid; gap: 12px; width: min(320px, 100%); }
.snapshot-gate__form h1 {
    margin: 0 0 4px;
    font-family: var(--font-heading);
    font-size: var(--size-secondary);
    font-weight: 600;
    letter-spacing: 0.5px;
    text-transform: uppercase;
}
.snapshot-gate__form input,
.snapshot-gate__form button {
    font-family: var(--font-body);
    font-size: var(--size-secondary);
    padding: 10px 12px;
    border: 2px solid var(--fg);
    border-radius: 0;
    background: var(--bg);
    color: var(--fg);
}
.snapshot-gate__form button { cursor: pointer; font-weight: 600; }
.snapshot-gate__form button:hover { background: var(--fg); color: var(--bg); }
.snapshot-gate__form input:disabled { opacity: 0.5; }
.snapshot-gate__err { margin: 0; font-size: var(--size-secondary); }
