/*
Font brand, 2026-08-02: one serif family everywhere -- Georgia, the same
face the primary logo lockup (brand/logo/primary-lockup.svg) actually
renders in. The logo's CSS also names Cinzel for the "TedRants"/"TRC"
display text, but Cinzel isn't installed on this machine (verified via
System.Drawing.Text.InstalledFontCollection) or loaded as a webfont
anywhere, so it was silently falling back to Georgia there too -- meaning
Georgia was already the one font actually being seen, everywhere except
this stylesheet's own body rule (a separate, never-loaded sans stack:
Inter/Lato aren't installed either, so body text was quietly rendering in
Segoe UI against Georgia headings). One declared family, set once on body
and inherited, is the fix -- see the `font-family: inherit` rule below for
form controls, which don't inherit a page's font by default in any browser.

Tufte-guided otherwise: maximize data-ink, minimize chartjunk. Tiles use a
hairline rule instead of a boxed card; charts are direct-labeled bars with
no axis, gridlines, or legend; numeric columns are right-aligned and use
tabular digits so figures compare cleanly down a column even when labels
wrap. See memory: every visual in this project should follow
edwardtufte.com.
*/

:root {
    --onyx: #0B0B0D;
    --charcoal: #17181B;
    --silver: #C9CED3;
    --bright-silver: #E8EAED;
    /* Reading-comfort fix, 2026-08-03: body text on --onyx was 16.3:1
       contrast -- more than double WCAG AAA's 7:1 minimum, well into the
       range that causes halation (light text visibly glowing/vibrating
       against near-black) on long reads. --body-text on --charcoal lands
       at 9.8:1 -- still comfortably high-contrast, not washed out, just
       under the threshold where extended reading gets fatiguing.
       --bright-silver stays reserved for things that should genuinely
       pop: headlines, key stat numbers, hover/active states. */
    --body-text: #BCC1C6;
    --steel-gray: #6B7278;
    --hairline: #2A2C30;
    --danger: #C9524B;
    --ok: #6FAE8C;
    --font-brand: Georgia, "Times New Roman", serif;
}

/* Theme presets (design handoff 2026-08-13): each block redefines the
   SAME custom properties -- nothing below this file's token layer knows
   themes exist. data-theme is server-rendered on <html> (no FOUC, no
   JS); auth pages never carry it, so the front door stays Codex Onyx.
   Palette table: design handoff IMPLEMENTATION.md section 2. */
[data-theme="parchment"] {
    --charcoal: #F4EFE6;
    --onyx: #FBF8F2;
    --body-text: #3A3630;
    --bright-silver: #1F1C18;
    --silver: #8A7F6D;
    --steel-gray: #98917F;
    --hairline: #DFD8C9;
    /* Dark-theme ok/danger wash out on paper -- deepened for contrast. */
    --ok: #4E7D5B;
    --danger: #A63D36;
}
[data-theme="indigo"] {
    --charcoal: #161722;
    --onyx: #0C0D16;
    --body-text: #B9BCD1;
    --bright-silver: #E7E8F4;
    --silver: #A6AACB;
    --steel-gray: #6A6E8C;
    --hairline: #2A2C40;
}
[data-theme="hoard"] {
    --charcoal: #1C1714;
    --onyx: #100D0B;
    --body-text: #C6BBB0;
    --bright-silver: #F0E8DE;
    --silver: #C9A96A;
    --steel-gray: #7D6F5F;
    --hairline: #332B24;
}

/* Logos always sit on a fixed DARK chip -- the lockups and the channel
   marks are light-on-transparent artwork that vanishes on Parchment.
   #0B0B0D literal on purpose: the chip must NOT follow the theme. */
.logo-chip {
    background: #0B0B0D;
    border-radius: 6px;
    padding: 2px 10px;
    display: inline-flex;
    align-items: center;
}

/* ...except the NAV logo on a dark theme, where the tile is all cost
   and no benefit (#245, Ted: "black on grey ... does not look good").
   The prototype does chip the nav logo, and at ITS 44px the tile reads
   as a thin edge -- but production renders the lockup at 64px
   deliberately (see .brand-mark: the card page gets shown on stream),
   and the same 2px/10px padding around a mark 45% taller is a large
   near-black rectangle sitting on #17181B. The artwork is light-fill,
   so on onyx / indigo / hoard it needs no backing at all.
   Parchment is the ONLY light theme (--charcoal #F4EFE6), and there
   the tile is what keeps the wordmark from vanishing -- so it stays,
   scoped. Channel chips (/dashboard, /whatnot), the Appearance preview
   whose own copy promises "a fixed dark tile", and the printed
   manifest all keep theirs. */
nav .logo-chip {
    background: transparent;
    padding: 0;
}
[data-theme="parchment"] nav .logo-chip {
    background: #0B0B0D;
    padding: 2px 10px;
}

* { box-sizing: border-box; }

body {
    background: var(--charcoal);
    color: var(--body-text);
    font-family: var(--font-brand);
    text-rendering: optimizeLegibility;
    margin: 0;
    padding: 0;
}

/* Form controls don't inherit the page font by default in any browser's
   UA stylesheet -- without this, every button/input/select on the site
   renders in the OS default UI font regardless of what body declares. */
input, button, select, textarea {
    font-family: inherit;
}

h1, h2, h3 {
    color: var(--bright-silver);
    letter-spacing: 0.02em;
}

h2 {
    font-size: 1.05rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--silver);
    border-bottom: 1px solid var(--hairline);
    padding-bottom: 0.4rem;
    margin-top: 2.5rem;
}

nav {
    background: var(--charcoal);
    padding: 1rem 2rem;
    display: flex;
    align-items: center;
    gap: 1.5rem;
    border-bottom: 1px solid var(--steel-gray);
}

nav .brand {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
}

/* Full lockup, sized up deliberately -- branding matters for this app
   specifically because the card drill-down page gets shown on stream;
   the nav is the one place it's worth spending real visual weight on. */
.brand-mark {
    height: 64px;
    width: auto;
    flex-shrink: 0;
}

nav a {
    color: var(--silver);
    text-decoration: none;
    font-size: 0.9rem;
}

nav a:hover { color: var(--bright-silver); }

nav .server-status {
    margin-left: auto;
    font-size: 0.68rem;
    line-height: 1.5;
    max-width: 21rem;
    text-align: right;
    color: var(--steel-gray);
    font-variant-numeric: tabular-nums;
}

main {
    max-width: 1100px;
    margin: 0 auto;
    padding: 2rem;
    text-align: center;
}

/* --- Stat tiles: hairline rule, not a boxed card. Label area has a fixed
   min-height so a wrapped 2-line label doesn't push that tile's value out
   of alignment with its neighbors' single-line labels. Flexbox, not grid --
   grid's auto-fit tracks are shared across every row, so a lone tile
   wrapping to its own row (e.g. 4 tiles, 3 fit per row) lands in the
   FIRST track, flush left, instead of centered on its own row. Flexbox
   wraps and centers each row independently. --- */
.tiles {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 1.25rem 1.5rem;
    margin-bottom: 2rem;
}

.tile {
    flex: 1 1 180px;
    max-width: 240px;
    border-bottom: 1px solid var(--hairline);
    padding-bottom: 0.6rem;
}

.tile .label {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    /* A flex container drops whitespace-only text nodes between children,
       so the space before a trailing <span class="derived"> qualifier
       disappeared -- "EST. PROFIT(AFTER 60% COGS)". gap restores it in
       the way flex actually understands. */
    gap: 0.35em;
    /* Wrap instead of overflowing the tile: a label plus a qualifier
       ("TCGplayer Rev / Week (wk of Jul 27)") is wider than 240px, and a
       non-wrapping flex row pushed the qualifier outside the tile's
       hairline. */
    flex-wrap: wrap;
    min-height: 2.2em;
    line-height: 1.1;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--steel-gray);
}

.tile .value {
    text-align: center;
    font-size: 1.6rem;
    color: var(--bright-silver);
    margin-top: 0.3rem;
    font-variant-numeric: tabular-nums;
}

.warning {
    background: rgba(201, 82, 75, 0.15);
    border: 1px solid var(--danger);
    color: var(--bright-silver);
    padding: 0.75rem 1rem;
    border-radius: 6px;
    margin-bottom: 1.5rem;
    /* One bad value must not set a SHARED surface's width (QA #155): a
       5000-character card name in the unmatched-SKU list stretched this
       box -- and with it html/body/main -- to 40,428px, making /export
       unusable on a phone for all 55 other cards until that one row was
       removed. The API cap (routes_intake._overlong_field) stops such a
       value being created; this makes the page survive one that already
       exists, which is the half that holds for values we do not control
       (a marketplace name, a pasted title). `anywhere` rather than
       `break-word` so it also shrinks the box's min-content width. */
    overflow-wrap: anywhere;
}

/* NO global overflow-wrap on th/td (#275). One lived here for a day
   (#155) and its own comment conceded it "is not what saves the page"
   -- the .warning rule above and the API's 200-char cap are. What the
   global rule DID do: `anywhere` collapses a cell's MIN-content to ~one
   character, and auto table layout negotiates columns from min-content,
   so every width-constrained table became free to crush all its columns
   -- the intake match table (a flex:1 1 320px panel by design) rendered
   its header as "MATC/H" and collector number 342 as "34/2" on Ted's
   screen the same day. A cell's min-content floor is load-bearing: it
   is what keeps words and numbers whole while long text columns wrap at
   spaces. Hostile-value defence for tables stays where #155 measured it
   working: .table-scroller for unconstrained tables, the API cap for
   new values. If a specific constrained cell ever needs to break a
   hostile value, scope the rule to that cell -- never to th/td. */

/* Match-result panel (#310, Ted: "NOTHING should have a scroll bar at
   the bottom, it needs to auto size the font to match the display").
   The prototype draws this as a LIST of rows -- name with the set dim
   beside it, the percentage nowrap at the right, one hairline per row,
   no column header -- not a 5-column table. Production keeps the table
   MARKUP (the builder and the #178 tie logic are fine) and reflows each
   row into the mock's composition, cardlist-style: the text column is
   the only flexible track, so a row can never be wider than the panel;
   the type scales with the panel via container units, which is what
   "auto size the font" means in CSS. #275's scroll fallback is gone --
   Ted's call, and the right one for a reading panel. Applies at every
   width: the mock is rows at every width. */
#intake-results { container-type: inline-size; }
#intake-results table {
    display: block;
    width: 100%;
    font-size: clamp(0.74rem, 0.42rem + 1.6cqi, 0.95rem);
}
#intake-results thead { display: none; }
#intake-results tbody { display: block; }
#intake-results tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto auto;
    grid-template-areas: "name num pct pick" "set num pct pick";
    column-gap: 0.6em;
    align-items: center;
    padding: 0.45em 0.25em;
    border-bottom: 1px solid var(--hairline);
}
#intake-results td { display: block; padding: 0; border: none; }
#intake-results td:nth-child(1) { grid-area: name; color: var(--body-text); overflow-wrap: break-word; }
#intake-results td:nth-child(2) { grid-area: set; color: var(--steel-gray); font-size: 0.9em; overflow-wrap: break-word; }
#intake-results td:nth-child(3) { grid-area: num; color: var(--steel-gray); font-variant-numeric: tabular-nums; white-space: nowrap; }
#intake-results td:nth-child(4) {
    grid-area: pct;
    color: var(--bright-silver);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    text-align: right;
}
#intake-results td:nth-child(5) { grid-area: pick; }
/* The pick button takes the scaled size too; its mobile 44px tap floor
   still comes from the mobile block (lower specificity on min-height is
   deliberate -- this rule sets none). */
#intake-results td:nth-child(5) button { font-size: inherit; padding: 0.35em 0.7em; white-space: nowrap; }

.ok-banner {
    background: rgba(111, 174, 140, 0.15);
    border: 1px solid var(--ok);
    padding: 0.75rem 1rem;
    border-radius: 6px;
    margin-bottom: 1.5rem;
}

/* --- Tables: hairlines only, no full grid. Numeric columns right-align
   and use tabular digits so the numbers themselves stay scannable in a
   column, even though the page layout around them is centered. --- */
table {
    width: 100%;
    border-collapse: collapse;
    margin: 0 auto 2rem;
}

th, td {
    text-align: left;
    padding: 0.45rem 0.75rem;
    border-bottom: 1px solid var(--hairline);
    font-size: 0.9rem;
}

/* Same size/role as .tile .label above (a small-caps eyebrow) -- same
   letter-spacing, so the two don't read as two different conventions. */
th { color: var(--silver); font-weight: normal; text-transform: uppercase; font-size: 0.72rem; letter-spacing: 0.04em; }

/* Sortable table headers: cursor + hover are the only affordance until a
   column is actually active -- the arrow only appears on the one column
   currently sorted, not as a static hint on every header. */
table.sortable th.sortable-col { cursor: pointer; user-select: none; }
table.sortable th.sortable-col:hover { color: var(--bright-silver); }
table.sortable th.sort-asc::after { content: " \25B4"; color: var(--steel-gray); }
table.sortable th.sort-desc::after { content: " \25BE"; color: var(--steel-gray); }

/* Filter row: one compact input per column, deliberately smaller/quieter
   than a real form field -- it's a fine-grained tool sitting under the
   headers, not something that should out-weigh the data itself. */
tr.filter-row th { padding: 0.25rem 0.75rem; }
input.col-filter {
    width: 100%;
    box-sizing: border-box;
    padding: 0.25rem 0.4rem;
    background: var(--onyx);
    border: 1px solid var(--hairline);
    color: var(--body-text);
    border-radius: 3px;
    font-size: 0.72rem;
    font-weight: normal;
    text-transform: none;
    letter-spacing: normal;
}
input.col-filter:focus { border-color: var(--steel-gray); outline: none; }
p.table-count-note { margin-top: -1.25rem; margin-bottom: 2rem; }

td.num, th.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

tr:hover td { background: rgba(255, 255, 255, 0.02); }

a.row-link {
    color: inherit;
    text-decoration: none;
}
a.row-link:hover { color: var(--bright-silver); text-decoration: underline; }

/* --- Bar list (Top Sets by Value, Rarity by Value): the bar length IS the
   data. No axis line, no gridlines, no legend -- label and value are set
   directly beside each bar. --- */
.barlist {
    text-align: left;
    max-width: 900px;
    margin: 0 auto 2rem;
}

.barlist-row {
    display: grid;
    /* minmax, not a fixed 18rem: fixed label + 6rem value + gaps set a
       ~403px floor, which overflowed phone viewports by exactly the
       33px QA measured on /dashboard (#46). The label column yields
       down to 8rem and the label wraps (mock behavior); desktop still
       gets the full 18rem. */
    grid-template-columns: minmax(8rem, 18rem) 1fr 6rem;
    align-items: center;
    gap: 0.6rem;
    padding: 0.28rem 0;
}

/* Value column wider than money (#104, QA's /training 47.0/28.2px):
   the strata table reuses this component with "12/300 val (45 scanned)
   need 255" in the value slot -- 3-4x the "$4,812.50" the 6rem column
   was sized for, and its nowrap made the excess OVERFLOW the track
   rather than wrap. `auto` sizes the track to the row's own value; the
   1fr bar track absorbs the difference, and right-aligned .num keeps
   the digits on one shared edge. Money barlists keep the aligned 6rem
   column -- this modifier exists so one over-long consumer does not
   reshape every dashboard. */
.barlist--wide-value .barlist-row {
    grid-template-columns: minmax(8rem, 18rem) 1fr auto;
}
/* The component owns the value's line behavior. It was an inline
   `white-space:nowrap` on training's bare span -- which no rule could
   reach, so the phone shape below could never relax it (#104's 21px
   escape). Desktop: one line, `auto` sizes the track to it. */
.barlist--wide-value .barlist-value { white-space: nowrap; }

.barlist-label {
    font-size: 0.85rem;
    color: var(--body-text);
    text-decoration: none;
    /* Full names matter more than single-line neatness -- wrap instead of
       truncating, so two differently-numbered sets sharing a long prefix
       (e.g. two "March of the Machine" products) don't render identically. */
    overflow-wrap: break-word;
    line-height: 1.2;
}
a.barlist-label:hover { color: var(--silver); text-decoration: underline; }

.barlist-track {
    background: var(--hairline);
    height: 0.7rem;
    border-radius: 1px;
}

.barlist-fill {
    display: block;
    height: 100%;
    background: var(--silver);
    border-radius: 1px;
}

.barlist-value {
    font-size: 0.85rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--steel-gray);
}

/* .panel, not form.panel: the review page carries the same panel look
   on DIVs, which the form-scoped selector silently skipped -- so those
   panels never got the auto side margins and hugged the content-left
   edge under centered headings (#100, leftgap 32 vs rightgap 348). */
/* First-run block (#332): the setup list a fresh install boots to. A
   plain ordered list inside the existing .panel -- mark, label, one
   sentence, one action; done rows dim rather than disappear so the
   order stays readable until the whole block leaves. */
.first-run { max-width: 760px; margin: 0 auto 2rem; text-align: left; }
.first-run h2 { margin-top: 0; }
.first-run-list { list-style: none; margin: 0; padding: 0; }
.first-run-list li {
    display: grid;
    grid-template-columns: 1.6rem 1fr;
    gap: 0.4rem;
    padding: 0.5rem 0;
    border-top: 1px solid var(--hairline);
    line-height: 1.35;
}
.first-run-list li:first-child { border-top: 0; }
.first-run-mark { color: var(--silver); text-align: center; }
.first-run-list li.is-done { color: var(--steel-gray); }
.first-run-list li.is-done .first-run-mark { color: var(--ok); }
.first-run-body a.button {
    margin: 0.45rem 0 0;
    padding: 0.35rem 0.9rem;
    font-size: 0.85rem;
    font-weight: normal;
    white-space: nowrap;
}

.panel {
    background: var(--charcoal);
    border: 1px solid var(--steel-gray);
    border-radius: 6px;
    padding: 1.5rem;
    max-width: 640px;
    margin: 0 auto 2rem;
    text-align: left;
}

label {
    display: block;
    font-size: 0.85rem;
    color: var(--silver);
    margin-top: 0.75rem;
}

input[type=text], input[type=number], input[type=file] {
    width: 100%;
    padding: 0.5rem;
    background: var(--onyx);
    border: 1px solid var(--steel-gray);
    color: var(--body-text);
    border-radius: 4px;
    margin-top: 0.25rem;
}

/* a.button: an anchor that IS an action (the first-run block's
   "Import" / "Scan a card", #332) renders as the house button and
   inherits the 44px mobile floor below -- a 19px text link is not a
   phone target. Same rules, one more selector; nothing else changes. */
button, a.button {
    margin-top: 1.25rem;
    background: var(--silver);
    color: var(--onyx);
    border: none;
    padding: 0.6rem 1.25rem;
    border-radius: 4px;
    font-weight: bold;
    cursor: pointer;
}

a.button { display: inline-block; text-decoration: none; line-height: 1.2; }

button.secondary, a.button.secondary {
    background: transparent;
    border: 1px solid var(--silver);
    color: var(--silver);
}

button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.autocomplete-wrap {
    position: relative;
}

.autocomplete-dropdown {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    margin-top: 0.25rem;
    background: var(--onyx);
    border: 1px solid var(--steel-gray);
    border-radius: 4px;
    max-height: 320px;
    overflow-y: auto;
    z-index: 10;
}

.autocomplete-item {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.5rem 0.75rem;
    cursor: pointer;
}

.autocomplete-item:not(:last-child) {
    border-bottom: 1px solid var(--hairline);
}

.autocomplete-item:hover,
.autocomplete-item.active {
    background: rgba(255, 255, 255, 0.05);
}

.autocomplete-name {
    color: var(--body-text);
    font-size: 0.88rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.autocomplete-set {
    color: var(--steel-gray);
    font-size: 0.78rem;
    flex-shrink: 0;
}

.derived {
    color: var(--steel-gray);
    font-size: 0.85rem;
}

/* Status elements JS fills in later: out of the layout until they have
   something to say (#200). An empty element is invisible but NOT free --
   it keeps its own margin, and in a flex row it keeps the gap before it,
   so a centred row silently centres on content the eye cannot see:
   /intake's Auto-capture button sat 6px left of every other row (a 0-wide
   span with margin-left:0.75rem) and the destination buttons 8px left (a
   0-wide <p> after gap:1rem). Both measured at 1280 AND 1920 -- the shift
   is fixed-px, so it does not care about viewport.
   :empty rather than a `hidden` the JS toggles, because it keys off the
   real content state: no write site can forget it (the auto-capture status
   alone has four), and it survives the wholesale `style.cssText =` rewrite
   the lot line does. Deliberately NOT applied to #label-save-status /
   #model-recommendation: those reserve vertical space on purpose so a
   save's result does not make the page jump. */
.js-status:empty { display: none; }

/* Stage-3 strip verdict on the scan page (#110). The redirect is the
   veto signal and must read as a warning; the confirmation is quiet. */
.stage3 { margin: 0 0 0.5rem; }
.stage3-redirect {
    padding: 0.5rem 0.75rem;
    border-left: 3px solid #e0a040;
    background: rgba(224, 160, 64, 0.08);
}
.stage3-redirect button { margin-left: 0.5rem; }
/* #178: tied candidate rows share one visual weight -- the list order is
   the embedding's, not a verdict. Same tint on every tied row so the top
   one never reads as "the answer". */
/* On the ROW, not its cells: the match table renders as grid rows
   (#310), so a per-cell tint would checker and a first-cell rail would
   cover only the name line. */
table.has-tie tr.tied { background: rgba(224, 160, 64, 0.07); border-left: 3px solid #e0a040; }

.trend {
    font-size: 1rem;
    font-weight: bold;
    margin-left: 0.4rem;
}

/* Colored by whether the change is good or bad for that specific metric, not
   raw direction -- e.g. losing listings going DOWN is shown in trend-good
   green even though the arrow points down. */
.trend-good { color: var(--ok); }
.trend-bad { color: var(--danger); }
.trend-flat { color: var(--steel-gray); }

/* --- Centered prose/figure blocks (2026-08-04) ---------------------------
   `main` is text-align:center, but a BLOCK element given a max-width and
   no horizontal auto-margin still sits flush against the container's left
   edge -- its text centers inside a box that is itself off-centre. That's
   what Ted saw as "the details below the totals are not aligned centred":
   several notes carried an inline max-width with no margin. Use .note for
   any constrained paragraph and .chart for any figure, instead of a
   one-off inline max-width. --- */
.note {
    max-width: 760px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}

.chart {
    max-width: 900px;
    margin: 0 auto 1.5rem;
    text-align: center;
}

/* An SVG is inline-level, so it also needs the centring its container
   provides -- but block display + auto margins survives a parent that
   later stops centring. */
.chart svg {
    display: block;
    margin: 0 auto;
}

/* --- Body links (2026-08-04) -------------------------------------------
   There was no general `a` rule at all, so any link outside nav/tables
   rendered in browser-default blue-and-purple -- jarring against the
   Onyx/silver palette, and it made visited links a different colour than
   unvisited ones for no reason. Scoped to `main` so nav keeps its own
   treatment. --- */
main a {
    color: var(--silver);
    text-decoration: none;
    border-bottom: 1px solid var(--steel-gray);
}

main a:hover,
main a:focus-visible {
    color: var(--bright-silver);
    border-bottom-color: var(--bright-silver);
}

/* Links that already carry their own styling shouldn't get the underline. */
main a.row-link,
main a.barlist-label,
main a.tile,
main a:has(> img) {
    border-bottom: none;
}


/* --- Nav "More" menu (2026-08-05) -------------------------------------
   details/summary needs no JS to open/close and stays keyboard
   accessible; the panel is absolutely positioned so opening it never
   reflows the bar. --- */
.navmenu { position: relative; display: inline-block; }
.navmenu > summary {
    cursor: pointer;
    list-style: none;
    color: var(--body-text, #C9CED3);
    padding: 0.25rem 0.5rem;
}
.navmenu > summary::-webkit-details-marker { display: none; }
.navmenu > summary:hover { color: var(--bright-silver); }
.navmenu-panel {
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 11rem;
    background: var(--charcoal, #17181B);
    border: 1px solid var(--steel-gray, #6B7278);
    border-radius: 6px;
    padding: 0.4rem 0;
    z-index: 50;
    display: flex;
    flex-direction: column;
    text-align: left;
}
.navmenu-panel a {
    padding: 0.4rem 1rem;
    border-bottom: none;
}
.navmenu-panel a:hover { background: var(--onyx, #0B0B0D); }

/* --- Drill-down tables (2026-08-05) -----------------------------------
   Visual first, numbers on demand: every long table sits inside a
   <details class="drill">. The summary reads as a quiet control, not a
   heading -- the chart above it is the real content. --- */
details.drill { margin: 0.75rem auto 2rem; max-width: 900px; }
details.drill > summary {
    cursor: pointer;
    list-style: none;
    display: inline-block;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--steel-gray, #6B7278);
    border: 1px solid var(--hairline, #2A2C30);
    border-radius: 4px;
    padding: 0.35rem 0.9rem;
}
details.drill > summary::-webkit-details-marker { display: none; }
details.drill > summary:hover { color: var(--bright-silver); border-color: var(--steel-gray); }
details.drill[open] > summary { color: var(--bright-silver); }
details.drill > summary::after { content: " \25BE"; }
details.drill[open] > summary::after { content: " \25B4"; }

/* Sign-out button in the nav menu, styled to sit among the links.
   Only rendered when AUTH_ENABLED (dormant today). */
.navmenu-panel .linklike {
    background: none;
    border: none;
    border-bottom: 1px solid var(--hairline, #2A2C30);
    color: var(--silver, #C9CED3);
    cursor: pointer;
    display: block;
    font: inherit;
    padding: 0.5rem 0.9rem;
    text-align: left;
    width: 100%;
}
.navmenu-panel .linklike:hover { background: var(--onyx, #0B0B0D); }

/* Corner-marking canvas on the merged intake page: block when shown (so
   margin:auto centers it), properly gone when the hidden attribute is on. */
#intake-canvas:not([hidden]) { display: block; }

/* Export-table review toggle: a real button with a real pressed state
   (Ted, 2026-08-07: still-scanning pre-show polish). Unreviewed = hollow
   outline, reviewed = filled --ok green; the fill IS the state, readable
   at a glance while paging through 30+ rows. */
.mark-btn {
    min-width: 5.2rem;
    padding: 0.35rem 0.55rem;
    border: 1px solid var(--steel-gray);
    border-radius: 6px;
    background: transparent;
    color: var(--body-text);
    font-weight: 700;
    cursor: pointer;
}
.mark-btn:hover { border-color: var(--silver); color: var(--bright-silver); }
.mark-btn.reviewed {
    background: var(--ok);
    border-color: var(--ok);
    color: var(--onyx);
}

/* Tufte table typography pass (Ted, 2026-08-07 + the standing ALWAYS-
   TUFTE order; see the expanded table rules in memory). One type size
   per table -- muted color, not smaller text, carries the hierarchy.
   Controls inside cells: uniform height, no prose margins, so scanning
   a column reads data rather than layout noise. */
td { vertical-align: middle; }
td button, td input[type="number"], td input[type="text"], td select {
    margin: 0;
    height: 2.15rem;
    box-sizing: border-box;
    padding: 0 0.7rem;
    font-size: 0.85rem;
    line-height: 1;
}

/* Inline dropdown edits stay quiet until touched -- the data reads as
   data, the affordance appears on hover/focus (Tufte: structure gets
   minimal ink). */
select.inline-field {
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    color: var(--body-text);
}
select.inline-field:hover, select.inline-field:focus {
    border-color: var(--steel-gray);
    background: var(--onyx);
}
select.inline-field option { background: var(--charcoal); }
td.derived, td .derived { font-size: 0.9rem; }
/* Warning-box capture thumbnails (QA #129): the td scoping left the
   cross-platform provenance images unconstrained -- one capture filled
   the viewport. Same visual size as table thumbs, inline so they sit
   beside their row's text for side-by-side comparison. */
.warning img.thumb {
    height: 2.6rem;
    width: auto;
    display: inline-block;
    vertical-align: middle;
    margin-left: 0.35rem;
}

td img.thumb {
    height: 2.6rem;
    width: auto;
    display: block;
    border-radius: 3px;
}

/* Interaction feedback, not decoration: a row pulses once when an
   in-place action lands (Ted, 2026-08-07: "I just did not see things
   changing as I was clicking buttons"). */
@keyframes row-updated {
    0%   { background: rgba(111, 174, 140, 0.28); }
    100% { background: transparent; }
}
tr.row-updated td { animation: row-updated 1.2s ease-out 1; }

/* Warning-box row links (#50) must LAND somewhere visible: the jumped-to
   row holds a steady tint until the next click takes :target away --
   same green family as the update pulse, weaker because it lingers. */
tr:target td { background: rgba(111, 174, 140, 0.16); }

/* The pending table carries 13 columns of working controls -- genuinely
   wider than main's 1100px reading column. Before this, the table pinned
   to main's left edge and overflowed right (Ted, 2026-08-07: "the
   columns drift right"). The scroller breaks out of main and centers the
   table on the VIEWPORT; the table sizes to its content (Tufte: columns
   sized to data, never stretched); narrow windows scroll inside the
   scroller instead of skewing the page. */
.table-scroller {
    width: 100vw;
    margin-left: calc(50% - 50vw);
    overflow-x: auto;
    padding-bottom: 0.5rem;
}
.table-scroller table {
    width: -webkit-max-content;
    width: max-content;
    max-width: none;
    margin: 0 auto 2rem;
}
.table-scroller th, .table-scroller td { padding: 0.45rem 0.55rem; }

/* Inline set fix shares the quiet inline-edit look: data until touched. */
input.inline-set {
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    color: var(--body-text);
}
input.inline-set:hover, input.inline-set:focus {
    border-color: var(--steel-gray);
    background: var(--onyx);
    outline: none;
}

/* Column titles centered over their columns (Ted, 2026-08-07). */
.table-scroller th { text-align: center; }

/* Wide analytical tables that FIT the reading column on desktop but
   crush at phone widths (design handoff section 4: stale cards, show
   detail, price distribution): the box scrolls, the columns keep their
   integrity. The content floor is the mock's -- far under the 900px
   drill column, so desktop never notices. Unlike .table-scroller,
   no viewport breakout. */
.scrollbox {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
.scrollbox > table { min-width: 520px; }

/* ---- Auth surface (login / 2FA / enrollment), 2026-08-12 ----------------
   The branded front door: full-bleed Onyx stage, the hexagon seal, one
   centered Charcoal card. Uses only the two hero colors per the brand
   guide (silver on onyx; bright-silver reserved for the title and
   focus states). These pages extend auth_base.html, not base.html --
   no nav exists before a session is proven. */

.auth-stage {
    background: var(--onyx);
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

.auth-column {
    width: min(420px, 92vw);
    padding: 2.5rem 0;
    text-align: center;
}

.auth-seal { width: 96px; height: 96px; }

.auth-title {
    color: var(--bright-silver);
    font-size: 2.1rem;
    font-weight: bold;
    letter-spacing: 0.04em;
    margin: 0.75rem 0 0.35rem;
}

/* The SHOP brand's slot on the front door: below the sign-in card,
   under a hairline -- product identity leads, the client's mark follows
   (design handoff two-brand rule). */
.auth-shop {
    border-top: 1px solid var(--hairline);
    margin-top: 1.75rem;
    padding-top: 1.5rem;
}
.auth-shop-logo { max-height: 64px; width: auto; }

.auth-footer {
    color: var(--steel-gray);
    font-size: 0.68rem;
    letter-spacing: 0.14em;
    margin-top: 1.5rem;
}

.auth-tagline {
    color: var(--steel-gray);
    font-size: 0.72rem;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    margin: 0 0 1.75rem;
}

.auth-card {
    background: var(--charcoal);
    border: 1px solid var(--hairline);
    border-radius: 6px;
    padding: 1.5rem 1.5rem 1.25rem;
    text-align: left;
}

.auth-card label {
    display: block;
    color: var(--body-text);
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.auth-card input {
    display: block;
    width: 100%;
    margin-top: 0.35rem;
    padding: 0.55rem 0.7rem;
    background: var(--onyx);
    color: var(--bright-silver);
    border: 1px solid var(--hairline);
    border-radius: 4px;
    font-size: 1rem;
}

.auth-card input:focus {
    outline: none;
    border-color: var(--silver);
}

.auth-card button {
    display: block;
    width: 100%;
    padding: 0.6rem;
    margin-top: 0.25rem;
    background: transparent;
    color: var(--bright-silver);
    border: 1px solid var(--silver);
    border-radius: 4px;
    font-size: 1rem;
    letter-spacing: 0.03em;
    cursor: pointer;
}

.auth-card button:hover {
    background: var(--silver);
    color: var(--onyx);
}

.auth-error {
    color: var(--danger);
    font-size: 0.9rem;
    margin: 0 0 1rem;
}

.auth-note {
    color: var(--steel-gray);
    font-size: 0.82rem;
    line-height: 1.45;
}

/* The QR is black-on-white by construction; it sits on a white tile so
   scanners get clean quiet-zone contrast against the dark card. */
.auth-qr {
    display: block;
    width: 200px;
    margin: 0.25rem auto 0.75rem;
    padding: 10px;
    background: #fff;
    border-radius: 6px;
}

.auth-secret {
    display: block;
    text-align: center;
    color: var(--silver);
    letter-spacing: 0.08em;
    word-break: break-all;
    margin: 0.25rem 0 1rem;
}

.auth-codes {
    background: var(--onyx);
    border: 1px solid var(--hairline);
    border-radius: 4px;
    padding: 0.9rem 1.1rem;
    color: var(--bright-silver);
    letter-spacing: 0.06em;
    line-height: 1.7;
    user-select: all;
}

.auth-continue {
    display: block;
    text-align: center;
    color: var(--bright-silver);
    margin-top: 1rem;
}

/* ------------------------------------------------------------------ */
/* Mobile (merged 2026-08-13: design-handoff section 4 tab bar + issue
   #46's safety floor from master -- complementary halves of one story).
   One breakpoint, 768px. Desktop untouched. */
.tabbar { display: none; }

@media (max-width: 768px) {
    /* Safety floor (#46 step 1): wide tables scroll inside their own
       box instead of stretching the page; media and controls can never
       overflow the viewport. width:max-content (not the base 100%):
       a block-ified table's VISIBLE rows are an anonymous inner table
       that left-aligns inside the block, so a full-width block under a
       centered heading reads as a left-hugging table with dead space
       right (#100, Ted's card-page screenshot). fit-content, NOT
       max-content: it clamps at the available width, so a wide table
       (the stats weeklies' percentage columns) still lays out at
       container width and wraps/scrolls exactly as the 100% floor did,
       instead of sizing to its content and inflating the viewport --
       measured 418px on the harness before the clamp. Content-sized,
       the existing auto side margins center the box. */
    table {
        display: block;
        width: -webkit-fit-content;
        width: fit-content;
        overflow-x: auto;
        max-width: 100%;
        -webkit-overflow-scrolling: touch;
    }
    img, svg, video, canvas { max-width: 100%; height: auto; }
    input, select, textarea, button { max-width: 100%; }
    pre { overflow-x: auto; }

    /* Design handoff: bottom tab bar replaces the top link row. */
    main { padding: 1.25rem; }
    nav > a:not(.brand), nav details.navmenu { display: none; }
    nav .server-status { display: none; }

    .tabbar {
        display: flex;
        position: fixed;
        left: 0; right: 0; bottom: 0;
        z-index: 40;
        background: var(--onyx);
        border-top: 1px solid var(--hairline);
        padding-bottom: env(safe-area-inset-bottom);
    }
    .tabbar a, .tabbar summary {
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 44px;
        color: var(--body-text);
        text-decoration: none;
        font-size: 0.85rem;
        letter-spacing: 0.04em;
        cursor: pointer;
        list-style: none;
    }
    .tabbar summary::-webkit-details-marker { display: none; }
    .tabbar a:active, .tabbar a.active, .tabbar summary:active {
        color: var(--bright-silver);
    }
    /* The More sheet: the same links the desktop menu carries, rising
       from the tab bar. */
    .tabbar details { flex: 1; position: relative; }
    .tabbar details[open] .tabbar-sheet {
        position: fixed;
        left: 0; right: 0;
        bottom: calc(44px + env(safe-area-inset-bottom));
        background: var(--onyx);
        border-top: 1px solid var(--hairline);
        max-height: 70vh;
        overflow-y: auto;
        padding: 0.5rem 0;
    }
    .tabbar-sheet a, .tabbar-sheet button.linklike {
        display: block;
        width: 100%;
        text-align: center;
        min-height: 44px;
        line-height: 44px;
        color: var(--body-text);
        text-decoration: none;
        font-size: 0.95rem;
    }
    /* Content must clear the fixed bar. */
    body { padding-bottom: calc(52px + env(safe-area-inset-bottom)); }

    /* Thumb-sized controls everywhere (handoff: 44px minimum). Checkbox
       and radio EXCLUDED (#114): the blanket rule was inflating them
       into 44px boxes -- a checkbox should look like a checkbox. Their
       target size comes from the LABEL below instead. textarea ADDED
       (#114): the one control type the rule missed; the app's only
       textarea already clears 44px at rows=2, but the rule should say
       so rather than rely on that being true forever. */
    input:not([type="checkbox"]):not([type="radio"]), select, button, a.button, textarea { min-height: 44px; }
    .mark-btn, td button, td input, td select { min-height: 36px; }

    /* Checkbox tap target lives on the LABEL, not the control (#114):
       an explicit class, not a blanket label:has(> input[checkbox])
       selector -- the Appearance theme-radio label also wraps its
       control directly, but it's a whole padded card (name + swatch +
       note stacked vertically); inline-flex + align-items:center would
       flatten that stack into a row. Class-scoped stays safe as the
       app grows more label-wrapped controls. */
    label.check-label {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }

    /* Cards page (handoff section 4): the 8-column table reflows into
       stacked list rows -- name + set·cond·qty left, price + market
       right, ONE hairline per row, the whole row a 44px tap target to
       the card page. Rarity and Days stay desktop-only (the mock drops
       them on mobile); sort/filter UI lives in the hidden thead -- a
       desktop workflow. Same DOM either way: these rules only reflow,
       so sort-table.js keeps working (it resets row display to ""). */
    table.cardlist thead { display: none; }
    /* The reflowed list keeps the full column: its rows are grids with
       a 1fr track, so max-content sizing (the floor's centering fix)
       would shrink-wrap them instead of spanning the reading width. */
    table.cardlist { width: 100%; }
    table.cardlist tbody { display: block; }
    table.cardlist tr {
        display: grid;
        grid-template-columns: auto auto auto 1fr auto;
        column-gap: 0.3rem;
        align-items: baseline;
        position: relative;
        min-height: 44px;
        padding: 0.6rem 0.25rem;
        border-bottom: 1px solid var(--hairline);
    }
    table.cardlist td {
        padding: 0;
        border-bottom: none;
        font-size: 0.78rem;
        color: var(--steel-gray);
    }
    table.cardlist .card-name {
        grid-column: 1 / span 3;
        grid-row: 1;
        font-size: 0.95rem;
        color: var(--body-text);
    }
    table.cardlist .card-price {
        grid-column: 5;
        grid-row: 1;
        font-size: 1rem;
        color: var(--bright-silver);
        text-align: right;
    }
    table.cardlist .card-set  { grid-column: 1; grid-row: 2; }
    table.cardlist .card-cond { grid-column: 2; grid-row: 2; }
    table.cardlist .card-qty  { grid-column: 3; grid-row: 2; text-align: left; }
    table.cardlist .card-market {
        grid-column: 5;
        grid-row: 2;
        text-align: right;
    }
    table.cardlist .card-set::after,
    table.cardlist .card-cond::after { content: " \00B7"; }
    table.cardlist .card-qty::before { content: "qty "; }
    table.cardlist .card-market::before { content: "mkt "; }
    table.cardlist .card-rarity, table.cardlist .card-days { display: none; }
    /* The name's link covers the whole row (mock: the row IS the tap
       target); no other interactive element lives in a card row. */
    table.cardlist .card-name a.row-link::after {
        content: "";
        position: absolute;
        inset: 0;
    }
}

/* Version stamp in the page footer (#89) -- deliberately quiet: it is
   reference information Ted glances at to confirm a restart picked up
   a release, not part of the reading flow. Matches .auth-footer's
   muted treatment so the two footers are visibly the same thing.
   Bottom padding clears the mobile tab bar, which is fixed. */
.app-footer {
    color: var(--steel-gray);
    font-size: 0.68rem;
    letter-spacing: 0.14em;
    text-align: center;
    margin: 3rem 0 1rem;
}
/* Launcher drift (QA #199): the ONE loud thing the quiet footer is
   allowed to say -- the running code is not today's. Danger token so it
   reads on every theme; no letter-spacing so the sentence stays a
   sentence. */
.app-footer .launcher-stale {
    color: var(--danger);
    letter-spacing: 0;
    margin-left: 0.6em;
}
@media (max-width: 760px) {
    .app-footer { margin-bottom: 4.5rem; }

    /* #104, phone half -- the whole component, not one consumer. At
       390px `minmax(8rem,18rem) 1fr 6rem` gives the label column its
       growth room BEFORE the flexible bar gets any: measured 234.8px
       label / 0px bar / 96px value on /, /dashboard, /customers,
       /usage, and 128px / 0px / ~194px on /training. #46's phone fix
       removed the overflow and left no bars on every page; the
       prototype's rows are the same grid with no mobile override, so
       they have the same math. Below the breakpoint the barlist changes
       SHAPE: label + value share one line, the bar takes the full width
       beneath (the bar length is the data -- 350px of it beats 90px),
       and a long value wraps instead of escaping. Selector specificity
       matches the wide-value desktop rules (0,2,0) so it wins for both
       modes; desktop is untouched. */
    .barlist .barlist-row {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "label value" "track track";
        row-gap: 0.25rem;
    }
    .barlist .barlist-label { grid-area: label; }
    .barlist .barlist-track { grid-area: track; }
    .barlist .barlist-value { grid-area: value; white-space: normal; }
}

/* Corner-drag labeling must beat the browser's own touch gestures
   (#170). intake-capture.js drives the 4-corner labeling with pointer
   events, which DO fire on a phone -- but without touch-action the
   browser claims the gesture for panning/scrolling first, so the
   corner never moves and the page slides instead. Same class as the
   CSP inline-handler bugs: the control exists, looks right, and the
   platform never lets it run. `none` here because every gesture on
   this canvas is a label edit; the page still scrolls everywhere
   else. */
#intake-canvas {
    touch-action: none;
}

/* Phone intake pass (Ted, 2026-08-22, from his iPhone: "the screen
   keeps trying to copy and paste when I am moving the green dots").
   A long-press on a canvas is, to iOS, a request for the selection /
   callout UI. Every press on this canvas is a corner edit, so the
   callout and text selection are switched off on the capture stage --
   and ONLY there; the rest of the page still selects and copies. */
/* The stage's width lives in CSS, not inline, so the phone override below
   can take it edge-to-edge (an inline margin beat the media rule). */
#intake-stage { max-width: 1100px; margin: 0 auto; }

#intake-canvas, #intake-video-wrap, #intake-loupe {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-user-drag: none;
}

/* The loupe: a round magnified view of the pixels under the finger,
   drawn while a corner is being dragged, placed on the OPPOSITE side of
   the screen from the touch so the thumb never covers it (Ted: "I can
   not see under my thumb"). Fixed so it stays put while the page is
   scrolled mid-drag. pointer-events:none -- it must never steal the
   drag it is illustrating. */
#intake-loupe {
    position: fixed;
    width: 132px;
    height: 132px;
    border-radius: 50%;
    border: 3px solid #6FAE8C;
    box-shadow: 0 6px 24px rgba(0,0,0,0.55);
    background: #000;
    pointer-events: none;
    z-index: 60;
}
#intake-loupe[hidden] { display: none; }

/* The model's verdict, ON the capture stage -- measured 2026-08-22: the
   results panel renders at y=4031 on a 390x844 phone, five screens below
   the camera, so a phone user never saw what the model concluded. This
   one line sits directly under the frame and says it plainly. */
#intake-verdict {
    margin: 0 auto 0.5rem;
    max-width: 520px;
    text-align: center;
    font-size: 1rem;
    line-height: 1.35;
}
#intake-verdict[hidden] { display: none; }
#intake-verdict strong { color: var(--body-text); }

@media (max-width: 768px) {
    /* The camera is the page on a phone: the stage goes edge-to-edge
       (main's 20px + the panel's 24px of side padding made a 300px
       canvas out of a 390px screen), the intro prose gets out of the
       way (it cost 105px above the fold), and the frame is never taller
       than the screen minus the action bars, so the whole card is
       reachable without the tab bar covering its bottom corners
       (measured: canvas 419-952 under a tab bar at 799-844). */
    #intake-stage { padding-left: 0; padding-right: 0; margin-left: -1.25rem; margin-right: -1.25rem; border-radius: 0; }
    #intake-stage > #intake-video-wrap, #intake-stage > #intake-canvas { max-width: 100%; border-radius: 0; }
    #intake-video-wrap video, #intake-canvas { max-height: calc(100vh - 13rem); object-fit: contain; }
    .intake-intro { display: none; }
    main h1 { font-size: 1.35rem; margin-bottom: 0.25rem; }

    /* Corner mode: the accept / clear / retake controls become a fixed
       action bar just above the tab bar. "Looks Right" is the primary,
       full-height target; Clear and Retake are smaller and on the far
       side so a thumb reaching for accept cannot wipe the corners
       (they used to be the same size, side by side). */
    body.intake-corner-mode #intake-corner-controls {
        position: fixed; left: 0; right: 0;
        bottom: calc(45px + env(safe-area-inset-bottom));
        z-index: 41;
        margin: 0; padding: 0.5rem 0.75rem calc(0.5rem + 2px);
        background: var(--panel-bg, #1b1d21);
        border-top: 1px solid var(--steel-gray, #6B7278);
        display: grid; grid-template-columns: 1fr auto auto; gap: 0.5rem; align-items: center;
    }
    body.intake-corner-mode #intake-corner-controls p { grid-column: 1 / -1; margin: 0 0 0.25rem; font-size: 0.85rem; }
    body.intake-corner-mode #intake-corner-accept-btn { min-height: 52px; font-size: 1.05rem; width: 100%; margin: 0; }
    body.intake-corner-mode #intake-corner-accept-btn[hidden] { display: none; }
    body.intake-corner-mode #intake-corner-clear-btn,
    body.intake-corner-mode #intake-corner-retake-btn { margin: 0; min-height: 44px; padding: 0 0.6rem; font-size: 0.9rem; }
    /* and the page gets bottom room so nothing sits under the bar */
    body.intake-corner-mode main { padding-bottom: 9rem; }
}
