/* Reginald — clean, minimal design system (polish unlocked after Phase 4, DESIGN.md §8). */
:root,
:root[data-theme="light"] {
    color-scheme: light;
    /* Warm & Fresh (default): buttery-paper neutrals, a clean teal action colour, a gold Reginald
       mark. The teal never collides with the red/amber/green states, which are warmed to match.
       The original blue-on-white palette is kept, selectable, as "Classic" further down. */
    --ink: #29251e;
    --ink-soft: #776e5e;
    --line: #e7decd;
    --surface: #fffdf8;
    --surface-alt: #f3efe5;
    --bg: #f7f3ea;
    --accent: #0b7d71;        /* teal — darkened from the pick (#0e8c7f, which fails AA at 4.14:1) so white-on-accent (5.0:1) AND accent-as-text on cards/page (4.9 / 4.5:1) all clear WCAG AA */
    --accent-ink: #0b6f63;
    --on-accent: #ffffff;     /* text / icons that sit on an --accent fill */
    --brand: #b5851f;         /* the Reginald wordmark — gold */

    --overdue-bg: #f6ded7;  --overdue-ink: #a5381f;  --overdue-line: #d67c64;
    --duesoon-bg: #f7e9c4;  --duesoon-ink: #85610f;  --duesoon-line: #d6b248;
    --stocked-bg: #deead0;  --stocked-ink: #46662c;  --stocked-line: #8fb068;
    --unknown-bg: #e8eaed;  --unknown-ink: #4a525b;  --unknown-line: #b6bdc6;
    --plan-bg: #e9e6fb;     --plan-ink: #5b3fbf;     --plan-line: #a99af0;

    --radius: 8px;
    --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.05);

    /* Width of the left nav rail. Shared by both themes (dark doesn't redefine it) and by the fixed
       tour panel, which offsets itself past the rail. */
    --sidebar-w: 15rem;

    /* Report chart series — fixed slot order (the ordering IS the colorblind-safety mechanism:
       it maximizes adjacent-pair CVD distance; validated against --surface in both modes).
       Series are assigned 1..8 in rank order and never cycled; pooled series wear --chart-pooled. */
    --chart-1: #2a78d6;
    --chart-2: #1baf7a;
    --chart-3: #eda100;
    --chart-4: #008300;
    --chart-5: #4a3aa7;
    --chart-6: #e34948;
    --chart-7: #e87ba4;
    --chart-8: #eb6834;
    --chart-pooled: #8a939e;
}

/* Dark palette. The theme script (wwwroot/js/theme.js) stamps data-theme=light|dark on <html>
   before first paint — resolving "auto" against the OS — so the dark tokens live in ONE place,
   keyed on data-theme, rather than being split across a prefers-color-scheme media query. */
:root[data-theme="dark"] {
    color-scheme: dark;
    --ink: #ece5d6;
    --ink-soft: #a69b86;
    --line: #342f22;
    --surface: #232019;
    --surface-alt: #29241a;
    --bg: #191712;
    --accent: #20a596;
    --accent-ink: #48c1b4;
    --on-accent: #06231f;     /* dark text on the lighter dark-mode teal (white would fail contrast) */
    --brand: #d9a93b;
    --danger-dark: #b3392c;       /* solid dark red for the danger BUTTON in dark mode — the --overdue-ink token is a light salmon here (tuned for pill text), so white on it is illegible */
    --danger-dark-hover: #9a3025;

    --overdue-bg: #38201b;  --overdue-ink: #f0a79a;  --overdue-line: #a9564a;
    --duesoon-bg: #332d12;  --duesoon-ink: #e6c77e;  --duesoon-line: #8f7529;
    --stocked-bg: #16311f;  --stocked-ink: #83d69e;  --stocked-line: #3d8055;
    --unknown-bg: #2a2f36;  --unknown-ink: #aab2bb;  --unknown-line: #586675;
    --plan-bg: #262046;     --plan-ink: #c7bafa;     --plan-line: #6f5fc4;

    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3);

    /* Same eight hues re-stepped for the dark surface (validated separately) — not a hue flip. */
    --chart-1: #3987e5;
    --chart-2: #199e70;
    --chart-3: #c98500;
    --chart-4: #008300;
    --chart-5: #9085e9;
    --chart-6: #e66767;
    --chart-7: #d55181;
    --chart-8: #d95926;
    --chart-pooled: #6d7783;
}

/* ---- Named palettes (themes) --------------------------------------------------------------------
   Two orthogonal axes, both stamped on <html> by wwwroot/js/theme.js BEFORE first paint:
     • data-theme    = light | dark   (resolved from the OS under "auto" — the two blocks above)
     • data-apptheme = which palette   (the DEFAULT palette lives in the bare :root above so a
                        JS-off / pre-stamp render still gets it; every ALTERNATIVE palette is an
                        override block down here)
   Every component reads the --ink / --bg / --accent tokens and never a palette directly, so adding a
   theme is one more override block and nothing else changes. Per-theme (redefined in each palette):
   the core neutrals, accent, brand, on-accent, and the warm-tuned overdue/duesoon/stocked chips.
   SHARED (defined once in :root, the same for every palette): the unknown/plan status colours, the
   CVD-safe chart hues, --shadow, --radius and --sidebar-w — so the charts stay colourblind-safe on
   every ground and the secondary states stay consistent.

   Classic — the original blue-on-white palette (verbatim), kept selectable so anyone can switch
   straight back. The [data-theme="light"] variant makes it out-specify the default light block for a
   Classic+light visitor regardless of source order; the bare [data-apptheme="classic"] covers the
   pre-stamp fallback. */
:root[data-apptheme="classic"],
:root[data-apptheme="classic"][data-theme="light"] {
    color-scheme: light;
    --ink: #1f2328;
    --ink-soft: #59636e;
    --line: #e2e5e9;
    --surface: #ffffff;
    --surface-alt: #f0f2f5;
    --bg: #f6f7f9;
    --accent: #2563eb;
    --accent-ink: #1d4ed8;
    --on-accent: #ffffff;
    --brand: var(--ink);      /* Classic keeps its plain-ink wordmark — the gold mark is a Warm thing */

    --overdue-bg: #fde7e9;  --overdue-ink: #b42318;  --overdue-line: #e8625a;
    --duesoon-bg: #fef4d6;  --duesoon-ink: #8a6100;  --duesoon-line: #e3b341;
    --stocked-bg: #dcf3e6;  --stocked-ink: #116b3c;  --stocked-line: #58b487;
}

:root[data-apptheme="classic"][data-theme="dark"] {
    color-scheme: dark;
    --ink: #e6e8eb;
    --ink-soft: #9aa4af;
    --line: #2b3138;
    --surface: #1b1f24;
    --surface-alt: #20252b;
    --bg: #131619;
    --accent: #4b86ff;
    --accent-ink: #7aa6ff;
    --on-accent: #ffffff;
    --brand: var(--ink);

    --overdue-bg: #3a1d1f;  --overdue-ink: #f4a6a0;  --overdue-line: #b4524a;
    --duesoon-bg: #38300f;  --duesoon-ink: #eccf85;  --duesoon-line: #9a7d2c;
    --stocked-bg: #143427;  --stocked-ink: #84dfac;  --stocked-line: #3f8e64;
}

body {
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    margin: 0;
    color: var(--ink);
    background: var(--bg);
    line-height: 1.45;
}

/* ---- App chrome: a persistent left nav rail beside the content column ---- */
/* On narrow viewports the rail collapses to an off-canvas drawer (see the max-width:1100px block) opened
   from a slim top bar. Above that breakpoint the rail is always visible, which is what kills the old
   header overflow — the 13 links no longer share one horizontal row. */
.app-shell {
    display: grid;
    grid-template-columns: var(--sidebar-w) 1fr;
    min-height: 100vh;
}

/* The content column. min-width:0 lets it shrink below its content's intrinsic width, so a wide table
   (which scrolls inside its own .table-scroll box) can't blow the grid out into a page-wide scrollbar.
   Kept a plain block so .site-main stays a normal block child that honours its own max-width — a flex
   column would make main a flex item whose min-width:auto (its content size) overrides that cap. */
.content {
    grid-column: 2;
    min-width: 0;
}

/* The rail: sticky + full-height so it stays in view while the content column scrolls; the link list
   scrolls inside it if the viewport is too short to hold every link plus the account footer. */
.sidebar {
    grid-column: 1;
    position: sticky;
    top: 0;
    align-self: start;
    height: 100dvh;
    /* border-box so the 100dvh height INCLUDES the padding (content-box would add it on top, pushing
       the footer ~27px below the fold). min-height:0 so that height actually caps the item — its
       default min-height:auto is the content height, which would otherwise win and stop the nav from
       scrolling internally when links + footer are taller than the viewport. */
    box-sizing: border-box;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 0.85rem 0.75rem;
    background: var(--surface);
    border-right: 1px solid var(--line);
    overflow: hidden;
}

.sidebar-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.25rem 0.35rem 0.5rem;
}

/* Shared by the rail head and the mobile top bar. (AccountLayout's .auth-brand .brand sizes its own.) */
.brand {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--brand);
}

/* The dapper-blob mascot, reused as a small brand badge and as the friendly face on the empty and
   not-found states. A rounded square cut from the same art as the app icon, so it reads the same in
   light and dark; sizes are set per use, the rounding scales with them. */
.mascot {
    display: block;
    border-radius: 22%;
    flex-shrink: 0;
}

.brand-mascot {
    width: 26px;
    height: 26px;
    cursor: pointer; /* a gentle "click me" — the dancing-Reginald easter egg (js/reginald-dance.js) */
}

/* Easter egg: the mascot's dance clip (js/reginald-dance.js), popped out and centred over the header
   badge, then removed when it ends. Transient + non-interactive, so it floats above the chrome without
   blocking anything. Position/size are set inline by the script from the mascot's live rect.
   ⚠️ z-index ABOVE the mobile drawer (1100) + its backdrop (1050) — the drawer head is Reginald's most
   tappable spot on a phone, so a dance triggered there must render over the open drawer, not vanish under
   it. Kept under .skip-link. pointer-events:none, so being on top never blocks anything (item 36's rule). */
.reginald-dance-clip {
    position: fixed;
    z-index: 1200;
    pointer-events: none;
    border-radius: 22%;
    object-fit: contain;
    filter: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.28));
}

.empty-state {
    display: flex;
    align-items: center;
    gap: 1.1rem;
    padding: 0.5rem 0;
}

.empty-mascot {
    width: 88px;
    height: 88px;
}

@media (max-width: 480px) {
    .empty-state {
        flex-direction: column;
        text-align: center;
        gap: 0.75rem;
    }
}

/* The link list fills the space between the brand and the account footer, scrolling on its own if it
   can't all fit a short viewport. */
.sidebar nav {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

.sidebar nav a {
    display: block;
    padding: 0.5rem 0.6rem;
    border-radius: 6px;
    color: var(--ink-soft);
    text-decoration: none;
    font-weight: 500;
    /* A two-word label must wrap as a unit, never split down the middle into two apparent links. */
    white-space: nowrap;
}

.sidebar nav a:hover {
    background: var(--surface-alt);
    color: var(--accent-ink);
}

/* Active page: tinted, bolder, with a left accent bar drawn as an inset shadow so it adds no width
   (a border would shift the label left↔right on every navigation). */
.sidebar nav a.active {
    background: var(--surface-alt);
    color: var(--accent-ink);
    font-weight: 600;
    box-shadow: inset 3px 0 0 var(--accent);
}

/* Account email + Sign out + theme switcher, pinned to the bottom of the rail. */
.sidebar-foot {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--line);
}

/* ☰ opens the drawer (in the mobile top bar); ✕ closes it (in the rail head). Both are hidden on
   desktop: the top bar is display:none and the close button is display:none until the drawer exists. */
.nav-toggle,
.nav-close {
    background: none;
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--ink);
    font-size: 1.1rem;
    line-height: 1;
    padding: 0.3rem 0.55rem;
    cursor: pointer;
}

/* Shown only once the rail becomes a drawer (the max-width:1100px block flips these on). */
.nav-close,
.mobile-bar,
.sidebar-backdrop {
    display: none;
}

/* Keyboard users tab past the whole rail on every page; this jumps them straight to the content.
   Off-screen until focused. */
.skip-link {
    position: fixed;
    top: -4rem;
    left: 0.5rem;
    z-index: 2000;
    padding: 0.5rem 0.75rem;
    background: var(--surface);
    color: var(--accent-ink);
    border: 1px solid var(--accent);
    border-radius: 6px;
    text-decoration: none;
}

.skip-link:focus {
    top: 0.5rem;
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: no-preference) {
    .skip-link {
        transition: top 0.15s ease;
    }
}

/* Below this width the rail collapses to an off-canvas drawer opened from the slim top bar. The
   breakpoint is chosen so the rail only appears once it can be carved off WITHOUT squeezing the
   content: every content page but Products fits in ≤765px, and 1100 − 15rem rail − scrollbar leaves
   ~845px, a comfortable margin. Below it the content spans the whole width, exactly as before this
   rework, so the original ~768–1400px header overflow is gone (a drawer has no horizontal link row). */
@media (max-width: 1100px) {
    .app-shell {
        display: block;
    }

    .mobile-bar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 0.75rem;
        padding: 0.6rem 1rem;
        background: var(--surface);
        border-bottom: 1px solid var(--line);
        position: sticky;
        top: 0;
        z-index: 30;
    }

    /* The drawer sits above the bottom-fixed furniture (voice assistant z-60), and its backdrop above
       that but below the drawer itself. */
    .sidebar {
        position: fixed;
        top: 0;
        left: 0;
        height: 100dvh;
        width: min(80vw, 17rem);
        transform: translateX(-100%);
        /* Off-canvas isn't enough on its own: a translated element stays in the tab order, so the
           closed drawer's links would still be keyboard-focusable (and read by AT) off-screen.
           visibility:hidden takes it out of both; the transition below delays the hide until the
           slide-out has finished so the animation still plays. */
        visibility: hidden;
        z-index: 1100;
        border-right: 1px solid var(--line);
        box-shadow: 0 0 24px rgba(0, 0, 0, 0.25);
    }

    .app-shell.nav-open .sidebar {
        transform: translateX(0);
        visibility: visible;
    }

    .nav-close {
        display: inline-flex;
    }

    .sidebar-backdrop {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.45);
        z-index: 1050;
    }

    .app-shell.nav-open .sidebar-backdrop {
        display: block;
    }

    /* Lock background scroll while the drawer is open, so a drag on the backdrop doesn't scroll the page
       behind it. The rail carries its own overflow-y for the link list, so it still scrolls internally. */
    body:has(.app-shell.nav-open) {
        overflow: hidden;
    }
}

/* The drawer slides rather than snaps — but only where motion is welcome. On close, the visibility
   change is held back until the slide-out finishes (0s change, 0.2s delay); on open it flips
   immediately (the open rule doesn't transition visibility) so the drawer is visible as it slides in. */
@media (max-width: 1100px) and (prefers-reduced-motion: no-preference) {
    .sidebar {
        transition: transform 0.2s ease, visibility 0s linear 0.2s;
    }
    .app-shell.nav-open .sidebar {
        transition: transform 0.2s ease;
    }
}

.site-main {
    padding: 1.5rem;
    max-width: 56rem;
    margin: 0 auto;
}

/* A page holding a wide data grid opts out of the 56rem reading cap (which is meant for prose/forms)
   and uses a fuller column, so the grid fits without a horizontal scroll on a desktop window. Bounded
   so it doesn't sprawl on an ultra-wide monitor; it still scrolls inside its own box below this width.
   Opt-in via `table-scroll--wide` on the grid (see Products.razor) — only that page is affected. */
.site-main:has(.table-scroll--wide) {
    max-width: 72rem;
}

/* The receipt-review grid (Upload.razor) is the WIDEST table in the app — ~11 input columns: item, brand,
   size, variety, expiry, qty, category, tags, product match… — so it earns a wider column still than the
   general wide-grid tier above. At 56rem it measured ~1857px crammed into an ~857px box, scrolling ~1000px
   of itself out of view on a 1920px monitor; `--xwide` lets the whole grid show on a large screen and fill
   the content column on a laptop, with any remainder still scrolling inside the box. Bounded at roughly the
   grid's own natural width so an ultra-wide monitor gets a grid-sized panel, not an edge-to-edge sprawl.
   Only RAISES the cap, so viewports already narrower than it (mobile / the ≤1100px drawer) are unaffected.
   The census review grid (PantryPhoto.razor) is a sibling but has only 7 columns — the same scale as the
   Products grid — so it opts into `--wide` (72rem) above, not this, to avoid a wide empty band beside it. */
.site-main:has(.table-scroll--xwide) {
    max-width: 120rem;
}

h1 {
    font-size: 1.6rem;
    letter-spacing: -0.02em;
    margin: 0;
}

h2 {
    font-size: 1.05rem;
}

h1:focus {
    outline: none;
}

/* Content links use the chart blue (--accent) so they match the price-history line charts.
   The nav rail (.sidebar nav a) has its own muted/active treatment and overrides this. */
a {
    color: var(--accent);
}

/* ---- Tables (Products, Upload, "everything else") ---- */
table {
    border-collapse: collapse;
    margin: 1rem 0;
    width: 100%;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

/* Wide grids scroll horizontally inside their own box instead of overflowing the page.
   The wrapper is the SINGLE scroll container: keep the inner table a normal table so the
   narrow-screen `table { display:block; overflow-x:auto }` rule (below) can't stack a SECOND
   horizontal scrollbar inside .table-scroll — that nesting is the "two scrollbars" bug. */
.table-scroll {
    overflow-x: auto;
    margin: 1rem 0;
    /* Positioned so an absolutely-positioned descendant — e.g. a table's `.visually-hidden` <caption> —
       has THIS box as its containing block, and so is anchored and clipped by it. Otherwise the caption
       escapes to the page and its off-screen position extends documentElement's scroll width (a phantom
       horizontal scrollbar). Harmless to the sticky header in .table-scroll--capped. */
    position: relative;
}

.table-scroll table {
    margin: 0;
    display: table;
    overflow-x: visible;
    white-space: normal;
}

/* The wide editable review grid also caps its height to the viewport and pins its header, so
   its horizontal scrollbar stays reachable instead of buried at the bottom of a tall receipt
   (you no longer scroll the whole page down just to find it). Scoped to the review grid — the
   report tables fit horizontally, so they don't need a scroll box. */
.table-scroll--capped {
    max-height: min(75vh, 42rem);
    overflow: auto;
}

.table-scroll--capped thead th {
    position: sticky;
    top: 0;
    z-index: 1;
}

th, td {
    border-bottom: 1px solid var(--line);
    padding: 0.5rem 0.75rem;
    text-align: left;
}

th {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--ink-soft);
    background: var(--surface-alt);
}

/* A sortable column header (SortableHeader): a plain-looking button that inherits the th styling, with a
   direction arrow when it's the active sort. Whole cell is the click target. */
.th-sort {
    display: inline-flex;
    align-items: center;
    gap: 0.3em;
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    color: inherit;
    cursor: pointer;
    text-align: left;
}
.th-sort:hover { color: var(--accent-ink); }
.th-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sort-ind { font-size: 0.85em; color: var(--accent); }
th.sorted { color: var(--accent-ink); }

tbody tr:last-child td {
    border-bottom: none;
}

/* ---- Buttons & inputs ---- */
button {
    font: inherit;
    padding: 0.4rem 0.8rem;
    border: 1px solid var(--accent);
    background: var(--accent);
    color: var(--on-accent);
    border-radius: 6px;
    cursor: pointer;
}

button:hover:not(:disabled) {
    background: var(--accent-ink);
    border-color: var(--accent-ink);
}

button:disabled {
    opacity: 0.5;
    cursor: default;
}

button.secondary {
    background: var(--surface);
    color: var(--ink);
    border-color: var(--line);
}

button.secondary:hover:not(:disabled) {
    background: var(--surface-alt);
    border-color: var(--ink-soft);
}

/* Destructive action — reuses the "overdue" red so delete reads as a real warning. */
button.danger {
    background: var(--overdue-ink);
    border-color: var(--overdue-ink);
    color: #fff;
}

button.danger:hover:not(:disabled) {
    background: var(--overdue-line);
    border-color: var(--overdue-line);
}

/* In dark mode --overdue-ink is a LIGHT salmon (tuned for pill text), so white on the danger BUTTON is
   illegible at rest — and its hover (--overdue-line, a mid-tone) can't satisfy both white and dark text.
   So the dark-mode danger button gets a solid dark red instead, keeping the white label legible at rest
   AND on hover, in both palettes. Light mode's fill is already a dark red, so it's left as-is (Classic's
   light danger button stays verbatim). */
:root[data-theme="dark"] button.danger {
    background: var(--danger-dark);
    border-color: var(--danger-dark);
}

:root[data-theme="dark"] button.danger:hover:not(:disabled) {
    background: var(--danger-dark-hover);
    border-color: var(--danger-dark-hover);
}

/* Subscription & billing (Settings): hosted-redirect links styled as buttons. A redirect is a NAVIGATION,
   so these are anchors (they work without the circuit), mirroring the button look via the same theme vars. */
.billing-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0.5rem 0;
}
.billing-actions a {
    display: inline-block;
    padding: 0.4rem 0.8rem;
    border: 1px solid var(--line);
    background: var(--surface);
    color: var(--ink);
    border-radius: 6px;
    text-decoration: none;
}
.billing-actions a:hover {
    background: var(--surface-alt);
    border-color: var(--ink-soft);
}
.billing-actions a.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
}
.billing-actions a.primary:hover {
    background: var(--accent-ink);
    border-color: var(--accent-ink);
}
/* "save 22%" beside the annual option — green, like the positive callout, so it reads as a win not a warning. */
.save-badge {
    font-size: 0.8rem;
    color: var(--stocked-ink);
    background: var(--stocked-bg);
    border: 1px solid var(--stocked-line);
    border-radius: 999px;
    padding: 0.05rem 0.4rem;
    margin-left: 0.35rem;
}

.danger-zone { border-color: var(--overdue-line); }

.danger-confirm {
    margin-top: 0.75rem;
    padding: 0.85rem 1rem;
    background: var(--overdue-bg);
    border: 1px solid var(--overdue-line);
    border-radius: 8px;
}

.danger-confirm label { display: block; margin: 0.6rem 0 0; font-weight: 600; }
.danger-confirm input { display: block; margin-top: 0.3rem; }

/* Keyboard-shortcut hint (e.g. the grocery-list Download shortcut). */
.kbd-hint { align-self: center; }
kbd {
    font: inherit;
    font-size: 0.85em;
    padding: 0.05rem 0.4rem;
    border: 1px solid var(--line);
    border-bottom-width: 2px;
    border-radius: 4px;
    background: var(--surface-alt);
    color: var(--ink);
}

/* ---- Split button (primary action + caret menu) ---- */
.split-btn {
    position: relative;
    display: inline-flex;
}

.split-btn .split-main {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    border-right: none;
}

.split-btn .split-caret {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    padding-left: 0.4rem;
    padding-right: 0.5rem;
    display: inline-flex;
    align-items: center;
}

.split-menu {
    position: absolute;
    top: calc(100% + 0.25rem);
    right: 0;
    z-index: 20;
    display: flex;
    flex-direction: column;
    min-width: 100%;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 6px;
    box-shadow: var(--shadow);
    overflow: hidden;
}

.split-menu button {
    border: none;
    border-radius: 0;
    white-space: nowrap;
    text-align: left;
    width: 100%;
}

.split-backdrop {
    position: fixed;
    inset: 0;
    z-index: 10;
}

input, select {
    font: inherit;
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
}

input:focus, select:focus {
    outline: 2px solid var(--accent);
    outline-offset: -1px;
}

/* ---- Panels ---- */
.panel {
    margin: 1rem 0;
    padding: 1rem 1.15rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow);
}

.panel h2 {
    margin-top: 0;
}

.panel form {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    flex-wrap: wrap;
}

/* A panel form that is a stacked DOCUMENT rather than a one-line action row (the bug report).
   Without this, the row default above laid its .field blocks side by side, shrink-wrapped — the
   half of "the form looks bad" that stacking labels inside each field couldn't fix. */
.panel form.stacked {
    flex-direction: column;
    align-items: stretch;
    gap: 0.6rem;
}

.panel.temp {
    border-style: dashed;
    box-shadow: none;
    background: transparent;
}

/* ---- Dashboard: header + summary ---- */
.page-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

.summary {
    display: flex;
    gap: 0.4rem;
    align-items: center;
}

.summary .all-good {
    color: var(--stocked-ink);
    font-weight: 600;
}

.empty-note {
    margin-top: 0.25rem;
}

/* ---- Dashboard: running-low cards ---- */
.cards {
    list-style: none;
    padding: 0;
    margin: 1rem 0;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.85rem 1rem 0.85rem 1.15rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-left: 4px solid var(--unknown-line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.card.accent-overdue { border-left-color: var(--overdue-line); }
.card.accent-duesoon { border-left-color: var(--duesoon-line); }

/* Compact name + note rows — shared by "You keep running out of these" (the burn-rate-≪-rebuy habit
   insight promoted from Product Detail) and "Coming up this week" (the near-term run-out heads-up). One
   definition so the two dashboard lists can't drift. */
.habit-list {
    list-style: none;
    padding: 0;
    margin: 0.5rem 0 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.habit-list li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem;
}
.habit-name {
    font-weight: 600;
    color: var(--accent);
    text-decoration: none;
}
.habit-name:hover { text-decoration: underline; }
.habit-note { color: var(--ink-soft); font-size: 0.9rem; }

/* First-run "getting started" banner (keyless / empty-catalog onboarding). */
.onboarding { position: relative; border-left: 4px solid var(--accent); }
.onboarding h2 { margin-top: 0; }
.onboarding-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; }
.onboarding-x {
    position: absolute;
    top: 0.4rem;
    right: 0.5rem;
    background: none;
    border: none;
    font-size: 1.35rem;
    line-height: 1;
    color: var(--ink-soft);
    cursor: pointer;
    padding: 0.15rem 0.4rem;
}
.onboarding-x:hover { color: var(--accent-ink); }

/* Founder-tier thank-you badge (Settings AI-usage panel). Uses the positive/"stocked" status palette
   so it reads as a warm callout, theme-aware in both light and dark for free. */
.founder-badge {
    background: var(--stocked-bg);
    color: var(--stocked-ink);
    border: 1px solid var(--stocked-line);
    border-radius: var(--radius);
    padding: 0.6rem 0.85rem;
    margin: 0 0 0.75rem;
}

/* The public credit price list (Settings billing panel). Collapsed by default because it answers a
   question most visits don't ask, but it is PUBLISHED rather than hidden — an abstract unit with a secret
   exchange rate is a casino chip. Narrow: it is two short columns, and a full-width table of eleven rows
   reads as a spreadsheet rather than as a price list. */
.price-list {
    margin: 0.75rem 0 0;
    max-width: 24rem;
}

.price-list > summary {
    cursor: pointer;
    font-weight: 600;
}

.price-list td:last-child,
.price-list th:last-child {
    text-align: right;
}

/* ---- Guided walkthrough (Components/Layout/GuidedTour.razor) ----
   Docked bottom-LEFT because the voice assistant owns bottom-right; the two are the only fixed-position
   furniture in the app and must not stack. It's a fixed element, so it doesn't know about the nav rail's
   grid column — clear it explicitly (see left, below). Non-modal on purpose: no backdrop, nothing
   blocked — the page under it stays usable, so a visitor can follow a step by actually doing it. */
.tour-panel {
    position: fixed;
    /* Sit just past the left rail on desktop; the max-width:1100px override drops it back to the edge
       when the rail becomes an off-canvas drawer and no longer occupies that space. */
    left: calc(var(--sidebar-w) + 1rem);
    bottom: 1rem;
    /* Above ordinary page content, deliberately BELOW the modal layer (.picker-backdrop, 60): a
       walkthrough is transient furniture, and floating it over a modal leaves the modal not modal. */
    z-index: 55;
    width: min(360px, calc(100vw - 2rem));
    margin: 0;
    border-left: 4px solid var(--accent);
    max-height: calc(100vh - 2rem);
    overflow-y: auto;
}

/* Drawer widths: no rail occupies the left edge, so the panel returns to it. The narrower max-width:560px
   rule below (full-width bottom sheet) still wins at phone sizes by coming later in source order. */
@media (max-width: 1100px) {
    .tour-panel {
        left: 1rem;
    }
}

.tour-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.tour-progress {
    margin: 0;
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.tour-title {
    margin: 0.35rem 0 0.4rem;
    font-size: 1.05rem;
}

/* The heading takes focus on every step change (tabindex=-1). Keep the focus ring for keyboard users
   but spare pointer users a ring on something they didn't click into. */
.tour-title:focus:not(:focus-visible) { outline: none; }

.tour-body {
    margin: 0;
    color: var(--ink-soft);
    font-size: 0.92rem;
    line-height: 1.5;
}

.tour-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.9rem;
}

.tour-skip { margin-left: auto; }

/* The ring around whatever the current step is about. Applied by wwwroot/js/tour.js, which may find
   nothing to ring — the step still reads, so this styling is never load-bearing for comprehension. */
.tour-spot {
    outline: 3px solid var(--accent);
    outline-offset: 4px;
    border-radius: 4px;
}

@media (prefers-reduced-motion: no-preference) {
    .tour-spot { animation: tour-spot-in 0.9s ease-out; }
}

/* ⚠️ Animates the OFFSET, never the colour. An animation is not guaranteed to run — a background tab,
   a throttled compositor, a device under load — and it holds its first keyframe when it stalls. A first
   keyframe of "outline-color: transparent" therefore leaves the ring INVISIBLE in exactly the case the
   visitor most needs it, which is how the first version of this shipped. Every frame here is a ring. */
@keyframes tour-spot-in {
    from { outline-offset: 12px; }
    to   { outline-offset: 4px; }
}

/* Narrow screens: the panel becomes a bottom sheet spanning the width, which would sit right on top of
   the voice assistant's FAB. The walkthrough is transient and the assistant isn't going anywhere, so the
   FAB stands down until the tour is closed (the tour's own copy never claims it's on screen). */
@media (max-width: 560px) {
    .tour-panel {
        left: 0;
        right: 0;
        bottom: 0;
        width: auto;
        border-radius: var(--radius) var(--radius) 0 0;
        border-left: none;
        border-top: 4px solid var(--accent);
        max-height: 60vh;
    }

    body.tour-active .voice-agent { display: none; }
}

.card-top {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.card-name {
    font-weight: 600;
    font-size: 1.05rem;
    color: var(--accent);
    text-decoration: none;
}

.card-name:hover {
    color: var(--accent-ink);
    text-decoration: underline;
}

.rhythm {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.25rem 1rem;
    margin: 0;
    max-width: 28rem;
}

.rhythm dt {
    color: var(--ink-soft);
}

.rhythm dd {
    margin: 0;
    font-weight: 600;
}

/* The two-stream "gap" insight — the actionable line on Product Detail (out-before-restock, or ahead). */
.rhythm-gap {
    margin: 0.75rem 0 0;
    padding: 0.5rem 0.75rem;
    background: var(--surface-alt);
    border-left: 3px solid var(--accent);
    border-radius: 6px;
    font-size: 0.9rem;
}

/* ---- Product detail: brands-bought breakdown ---- */
.brands {
    list-style: none;
    padding: 0;
    margin: 0.5rem 0 0;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.brands li {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
}

.brand-name {
    font-weight: 600;
}

/* ---- Trends (spend insight) ---- */
.portfolio {
    display: flex;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin: 1rem 0 1.5rem;
}

.stat {
    flex: 1 1 9rem;
    padding: 0.75rem 1rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.stat-label {
    display: block;
    font-size: 0.78rem;
    color: var(--ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.stat-value {
    font-size: 1.4rem;
    font-weight: 700;
}

.stat.forecast {
    border-color: var(--accent);
}

.stat.forecast .stat-value {
    color: var(--accent);
}

/* A "watch this" warning tile (amber) — distinct from pass (green all-clear) and fail (red). */
.stat.warn {
    border-color: var(--duesoon-line);
}

.stat.warn .stat-value {
    color: var(--duesoon-ink);
}

.stat.pass { border-color: var(--stocked-line); }
.stat.pass .stat-value { color: var(--stocked-ink); }
.stat.fail { border-color: var(--overdue-line); }
.stat.fail .stat-value { color: var(--overdue-ink); }

pre {
    background: var(--surface-alt);
    padding: 0.5rem 0.75rem;
    border-radius: 6px;
    overflow-x: auto;
    border: 1px solid var(--line);
}

.change-up {
    color: var(--overdue-ink);
    font-weight: 600;
}

.change-down {
    color: var(--stocked-ink);
    font-weight: 600;
}

.linechart {
    display: block;
    vertical-align: middle;
}

.card-urgency {
    font-weight: 600;
    margin-top: 0.15rem;
}

.card.accent-overdue .card-urgency { color: var(--overdue-ink); }
.card.accent-duesoon .card-urgency { color: var(--duesoon-ink); }

.card-note {
    font-size: 0.85rem;
    color: var(--ink-soft);
    margin-top: 0.1rem;
}

.card-basis {
    margin-top: 0.1rem;
}

.card-actions {
    display: flex;
    gap: 0.4rem;
    flex-shrink: 0;
}

.quick-update input {
    flex: 1 1 18rem;
}

/* ---- Voice / push-to-talk ---- */
.voice {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.voice-divider {
    display: flex;
    align-items: center;
    text-align: center;
    color: var(--ink-soft);
    font-size: 0.82rem;
    margin: 0.85rem 0 0.15rem;
}

.voice-divider::before,
.voice-divider::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--line);
}

.voice-divider span {
    padding: 0 0.6rem;
}

button.mic {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    align-self: flex-start;
    padding: 0.5rem 1rem;
    border-radius: 999px;
    background: var(--surface);
    color: var(--accent-ink);
    border: 1px solid var(--accent);
    /* Let press-and-hold work without the browser treating it as a scroll/long-press gesture. */
    touch-action: none;
    user-select: none;
}

button.mic:hover:not(:disabled) {
    background: var(--surface-alt);
    border-color: var(--accent-ink);
}

button.mic .mic-glyph {
    font-size: 1.05rem;
    line-height: 1;
}

button.mic-recording,
button.mic-recording:hover:not(:disabled) {
    background: var(--overdue-bg);
    color: var(--overdue-ink);
    border-color: var(--overdue-line);
    animation: mic-pulse 1.2s ease-in-out infinite;
}

button.mic-thinking,
button.mic-speaking {
    color: var(--ink-soft);
    border-color: var(--line);
}

@keyframes mic-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(180, 35, 24, 0.35); }
    50% { box-shadow: 0 0 0 6px rgba(180, 35, 24, 0); }
}

@media (prefers-reduced-motion: reduce) {
    button.mic-recording { animation: none; }
}

.voice-status {
    min-height: 1.2em;
}

.voice-heard {
    margin: 0.1rem 0;
    color: var(--ink-soft);
    font-style: italic;
}

/* ---- Voice conversation (v2.1 multi-turn) ---- */
.voice-convo {
    margin-top: 0.6rem;
}

.convo-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.4rem;
}

.convo-log {
    display: grid;
    gap: 0.35rem;
    margin-bottom: 0.6rem;
    max-height: 16rem;
    overflow-y: auto;
}

.convo-user,
.convo-bot {
    margin: 0;
    padding: 0.3rem 0.5rem;
    border-radius: 8px;
}

.convo-user {
    background: var(--surface-alt);
}

.convo-bot {
    background: var(--stocked-bg);
    color: var(--stocked-ink);
}

.convo-user .who,
.convo-bot .who {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    opacity: 0.7;
    margin-right: 0.4rem;
}

.convo-phase {
    margin: 0.35rem 0 0;
    font-size: 0.85rem;
    font-weight: 600;
}

.convo-phase.listening {
    color: var(--stocked-ink);
}

.convo-phase.speaking {
    color: var(--accent-ink);
}

@media (prefers-reduced-motion: no-preference) {
    .convo-phase.listening::before {
        content: "";
        display: inline-block;
        width: 0.55rem;
        height: 0.55rem;
        margin-right: 0.4rem;
        border-radius: 50%;
        background: var(--stocked-line);
        vertical-align: middle;
        animation: convo-listen-pulse 1.2s ease-in-out infinite;
    }
}

@keyframes convo-listen-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.35; transform: scale(0.75); }
}

/* ---- Persistent voice assistant (floating, hosted in MainLayout) ---- */
.voice-agent {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 60;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.4rem;
    max-width: min(360px, calc(100vw - 2rem));
}

.voice-agent-fab {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.6rem 1rem;
    border-radius: 999px;
    background: var(--accent);
    color: var(--on-accent);
    border: 1px solid var(--accent-ink);
    box-shadow: var(--shadow);
    font-weight: 600;
    cursor: pointer;
}

.voice-agent-fab:hover:not(:disabled) {
    background: var(--accent-ink);
}

.voice-agent-fab:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.voice-agent-fab > span[aria-hidden] {
    font-size: 1.1rem;
    line-height: 1;
}

.voice-agent-note {
    margin: 0;
    font-size: 0.82rem;
    background: var(--surface);
    padding: 0.3rem 0.5rem;
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.voice-agent-panel {
    width: min(360px, calc(100vw - 2rem));
    max-height: min(70vh, 30rem);
    overflow-y: auto;
    margin: 0;
    box-shadow: 0 6px 24px rgba(16, 24, 40, 0.18);
}

:root[data-theme="dark"] .voice-agent-panel {
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
}

/* Reader / cook-along header actions (Back to assistant + close) */
.ra-head-actions {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

/* Recipes "?uses" filter banner */
.filter-banner {
    margin: 0 0 0.6rem;
    padding: 0.4rem 0.6rem;
    border-radius: var(--radius);
    background: var(--surface-alt);
    border-left: 3px solid var(--accent);
    font-size: 0.9rem;
}

.quick-update-hint {
    margin: 0.4rem 0 0;
}

/* ---- Narrow screens ---- */
@media (max-width: 560px) {
    .site-main {
        padding: 1rem;
    }

    .card {
        flex-direction: column;
        align-items: stretch;
    }

    .card-actions {
        justify-content: flex-start;
    }

    /* Keep wide management tables from overflowing the viewport. */
    table {
        display: block;
        overflow-x: auto;
        white-space: nowrap;
    }
}

/* ---- Status chips ---- */
.chip {
    display: inline-block;
    padding: 0.1rem 0.55rem;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    white-space: nowrap;
}

.chip-overdue { background: var(--overdue-bg); color: var(--overdue-ink); }
.chip-duesoon { background: var(--duesoon-bg); color: var(--duesoon-ink); }

/* A soft, friendly heads-up box — amber, not alarm (the Upload done-panel's pack-misread nudge).
   Reuses the theme-aware duesoon tokens so it reads correctly in light and dark. */
.callout {
    background: var(--duesoon-bg);
    color: var(--duesoon-ink);
    border: 1px solid var(--duesoon-line);
    border-radius: var(--radius);
    padding: 0.75rem 1rem;
    margin: 0.75rem 0;
}
.callout > p:first-child { margin-top: 0; }
.callout > p:last-child { margin-bottom: 0; }
.callout .muted { color: inherit; opacity: 0.85; } /* keep the amber ink; don't fall back to grey on amber */
/* Positive confirmation (e.g. the /about "you're on the list" notices) — green, not caution amber. */
.callout.ok {
    background: var(--stocked-bg);
    color: var(--stocked-ink);
    border-color: var(--stocked-line);
}
.chip-stocked { background: var(--stocked-bg); color: var(--stocked-ink); }
.chip-unknown { background: var(--unknown-bg); color: var(--unknown-ink); }
.chip-plan { background: var(--plan-bg); color: var(--plan-ink); }

/* Eggs's lookalike nudge (GroceryList) — friendly, never a warning. The mood lives in his face and one
   line; only a thin left accent shifts as a pair lingers (brand teal → gentle amber), and the copy stays
   a soft "want to roll them into one?" the whole way. */
.nudge {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    border-left: 4px solid var(--accent);
}
.nudge.nudge-nagging,
.nudge.nudge-frazzled { border-left-color: var(--duesoon-line); }
.nudge .eggs-mascot { flex: 0 0 auto; }
.nudge-body { flex: 1 1 auto; min-width: 0; }
.nudge-line { margin: 0 0 0.15rem; font-weight: 600; }
.nudge-pair { margin: 0 0 0.6rem; }
/* A cluster card's member list: a compact inline run of product links, not a bulleted shelf. */
.nudge-members { list-style: none; margin: 0 0 0.6rem; padding: 0; display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; }
.nudge-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.nudge-done { display: flex; align-items: center; gap: 0.75rem; justify-content: space-between; }

/* The "you told Eggs these are separate" list on a product's detail page (ProductDetail). */
.dismissed-lookalikes .dismissed-list { list-style: none; margin: 0.4rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.dismissed-lookalikes .dismissed-list li { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }

/* A grocery row the meal plan put on the list (GroceryList.razor). Tinted so plan items read as
   plan-driven at a glance; the "Plan" chip + "for <meal>" note carry the meaning for screen readers
   (color is never the only signal). */
.grocery-src-plan { background: color-mix(in srgb, var(--plan-bg) 45%, transparent); }
.plan-for { font-size: 0.85em; color: var(--plan-ink); white-space: nowrap; }

/* ---- Everything-else disclosure ---- */
.everything-else summary {
    cursor: pointer;
    font-weight: 600;
    color: var(--ink-soft);
    margin: 1.25rem 0 0;
}

/* ---- Receipts page: one card per receipt, the disclosure reveals its line items ---- */
.receipt-card summary {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    cursor: pointer;
    list-style-position: inside; /* keep the disclosure marker inline with the flex row */
}

.receipt-card .receipt-date {
    font-weight: 600;
}

.receipt-card .receipt-total {
    margin-left: auto;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.receipt-card table {
    margin-top: 0.75rem;
}

/* The money figures printed on the receipt (subtotal/tax/savings/total) — a compact right-aligned
   summary beneath the line-item table, distinct from that table's computed line-item sum. */
.receipt-totals {
    width: fit-content;
    min-width: 13rem;
    margin: 0.85rem 0 0 auto;
    font-variant-numeric: tabular-nums;
}

.receipt-totals .rt-caption {
    color: var(--ink-soft);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    text-align: right;
    margin-bottom: 0.3rem;
}

.receipt-totals .rt-row {
    display: flex;
    justify-content: space-between;
    gap: 1.5rem;
    padding: 0.12rem 0;
}

.receipt-totals .rt-row > span:first-child {
    color: var(--ink-soft);
}

.receipt-totals .rt-savings > span {
    color: var(--stocked-ink);
    font-weight: 600;
}

.receipt-totals .rt-final {
    margin-top: 0.25rem;
    padding-top: 0.4rem;
    border-top: 1px solid var(--line);
    font-weight: 700;
}

/* Running "amount saved" line under the page intro, and its figure. */
.receipt-running {
    margin-top: -0.35rem;
}

.receipt-running .receipt-saved {
    color: var(--stocked-ink);
    font-weight: 700;
}

.chip-saved {
    background: var(--stocked-bg);
    color: var(--stocked-ink);
}

.receipt-eval-actions,
.receipt-remove-actions,
.receipt-download-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-top: 0.75rem;
}

/* The download link is a .linkish anchor, whose inline-after-text left margin would indent it from the
   card edge — drop it here where the link stands alone in its row. */
.receipt-download-actions .linkish {
    margin-left: 0;
}

/* ---- Review opt-in + accuracy self-check run row ---- */
.verify-for-eval label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 600;
}

.verify-for-eval .muted {
    display: block;
    margin-top: 0.25rem;
}

.selfeval-run {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-top: 0.75rem;
}

/* ---- Text helpers ---- */
.muted {
    color: var(--ink-soft);
    font-size: 0.9rem;
}

.chat-reply {
    color: var(--stocked-ink);
    font-weight: 600;
}

.error {
    color: var(--overdue-ink);
}

/* Small-text modifier for the helpers above — pairs with .muted and .error, and stands alone
   (the cook-along reply). Equal specificity with .muted, so it must stay AFTER it in this
   sheet or .muted's own font-size wins the cascade. */
.small {
    font-size: 0.8rem;
}

/* Header above the kept "Get ideas" batch: what it was for, when, and the clear action. */
.suggestions-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
}

.suggestions-head p {
    margin: 0.5rem 0 0;
}

/* "Why is this red?" — the covering product is just predicted run-out (recipe ingredient rows). */
.maybe-have {
    color: var(--ink-soft);
    font-size: 0.85rem;
    font-style: italic;
    margin: 0 0.35rem;
}

/* Duplicate-product prompt under the Add form: a caution, not an error. */
.dup-check {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    flex-wrap: wrap;
    margin: 0.6rem 0 0;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--line);
    border-left: 3px solid var(--duesoon-ink);
    border-radius: var(--radius);
    background: var(--duesoon-bg);
}

/* ---- Upload review table cues ---- */
.low-confidence {
    background: var(--duesoon-bg);
}

.spinner {
    font-style: italic;
}

td.raw {
    font-family: ui-monospace, monospace;
    font-size: 0.85rem;
    color: var(--ink-soft);
    vertical-align: top;
}

td.raw .chip {
    margin-top: 0.3rem;
    font-family: system-ui, sans-serif;
}

.review-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.5rem;
}

/* ---- Accessibility ---- */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Screen-reader-only text (visually hidden, still announced). */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ---- Products filter bar ---- */
.filters {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    flex-wrap: wrap;
}

.filters .count {
    margin-left: auto;
    color: var(--ink-soft);
    font-size: 0.9rem;
}

.empty-row td {
    color: var(--ink-soft);
    text-align: center;
    padding: 1rem;
}

/* Size + "usual brand" hints under the product (item) name in the Products grid. */
.item-meta {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    margin-top: 0.2rem;
}

.usual-brand {
    font-size: 0.78rem;
    color: var(--ink-soft);
}

.usual-variety {
    font-size: 0.78rem;
    color: var(--accent);
    font-style: italic;
}

/* Tap-for-detail brand/variety breakdown (BrandVarietyHint). Expands inline — the grids sit in
   overflow-scroll containers that would clip a floating popover. */
.hint-pop {
    display: inline-block;
}

.hint-pop summary {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
    list-style: none;
}

.hint-pop summary::-webkit-details-marker {
    display: none;
}

.hint-pop summary::after {
    content: "▾";
    font-size: 0.65rem;
    color: var(--muted);
}

.hint-pop[open] summary::after {
    content: "▴";
}

.hint-pop-body {
    margin-top: 0.25rem;
    padding: 0.3rem 0.55rem;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface-alt);
    font-size: 0.75rem;
    color: var(--ink-soft);
}

.hint-pop-body .hint-label {
    font-weight: 600;
    margin-right: 0.3rem;
}

/* Neutral pill for package size (part of the product's identity). */
.size-chip {
    display: inline-block;
    padding: 0.05rem 0.45rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--ink-soft);
    background: var(--surface-alt);
    white-space: nowrap;
    vertical-align: middle;
}

/* ── Tags: the descriptive second category layer (cloud, chips, review editor) ── */
.tag-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    align-items: center;
}

.tag-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.08rem 0.5rem;
    border: 1px solid var(--accent);
    border-radius: 999px;
    font-size: 0.74rem;
    font-weight: 600;
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    text-decoration: none;
    line-height: 1.5;
}

a.tag-chip:hover,
button.tag-chip:hover {
    background: color-mix(in srgb, var(--accent) 20%, transparent);
}

button.tag-chip {
    cursor: pointer;
}

.tag-chip .tag-x {
    border: none;
    background: none;
    color: inherit;
    cursor: pointer;
    font-size: 0.85rem;
    line-height: 1;
    padding: 0;
}

.tag-chip-mini {
    font-size: 0.68rem;
    padding: 0.02rem 0.4rem;
    font-weight: 500;
}

.detail-tags {
    margin: -0.2rem 0 0.6rem;
}

/* The browsable tag cloud on the Products page. */
.tag-cloud {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
    margin: 0.4rem 0 0.9rem;
}

.tag-cloud-chip {
    font-size: 0.8rem;
}

.tag-cloud-chip.active {
    color: var(--surface);
    background: var(--accent);
}

.tag-count {
    font-size: 0.66rem;
    opacity: 0.8;
}

.tag-clear {
    border: none;
    background: none;
    color: var(--ink-soft);
    cursor: pointer;
    font-size: 0.78rem;
    text-decoration: underline;
}

/* Per-line tag editor on the receipt review screen. */
.tags-cell {
    min-width: 11rem;
}

.tags-cell .tag-add {
    display: flex;
    gap: 0.25rem;
    margin-top: 0.3rem;
}

.tags-cell .tag-add input {
    width: 6rem;
}

/* The refusal shown beside a receipt row's tag box ("that tag is too long"). Matches .tag-warning's
   size and weight rather than a full-size <p>: it sits inside a compact grid row, right next to that
   styled box, and an unstyled paragraph there reads as a layout fault rather than a note. */
.tag-note {
    margin-top: 0.3rem;
    font-size: 0.74rem;
    color: var(--ink-soft);
}

.tag-warning {
    margin-top: 0.3rem;
    font-size: 0.74rem;
    color: var(--ink-soft);
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    align-items: center;
}

/* Products grid row actions: a tiny "Out" button (always-available mark-out) + delete. */
/* Per-row action cell (Products grid, Grocery List). Must stay a real table-cell — display:flex on a
   <td> pulls it out of table layout, so the cell stops stretching to the row height and its bottom
   border floats mid-row instead of joining the grid line. Space buttons with margins instead. */
.row-actions {
    white-space: nowrap;
}

.row-actions > * {
    vertical-align: middle;
}

.row-actions > * + * {
    margin-left: 0.3rem;
}

.mark-out {
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--overdue-ink);
    background: transparent;
    border: 1px solid var(--overdue-line);
    border-radius: 6px;
    padding: 0.05rem 0.45rem;
    cursor: pointer;
    line-height: 1.5;
}

.mark-out:hover {
    background: var(--overdue-bg);
}

/* Quiet neutral chip: borderless with a soft fill (an outline in --line was too faint on dark and read
   as a broken border), not a green "I'm stocked" statement. Shared by the grocery list's "don't want it
   for a while" Untrack and the recipe row's "I'm out" correction — one definition, two consumers. */
.untrack-btn,
.imout-btn {
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--ink-soft);
    background: var(--surface-alt);
    border: none;
    border-radius: 6px;
    padding: 0.15rem 0.5rem;
    cursor: pointer;
    line-height: 1.5;
    white-space: nowrap;
}

.untrack-btn:hover:not(:disabled) {
    background: var(--line);
    color: var(--ink);
}

/* "I'm out" hints toward the run-out red on hover — it's a correction that marks the item out. */
.imout-btn:hover:not(:disabled) {
    background: var(--line);
    color: var(--overdue-ink);
}

/* "I have it" — the green row actions, unlike Untrack's neutral chip beside them: Restocked (a fresh
   supply → re-anchors the due date) and Still in stock (the reminder was early → the engine snoozes it).
   Both are status-only signals that never feed the cadence (§6 two-stream model); one definition, two
   selectors so the tests can tell the buttons apart. */
.restock-btn,
.stillstock-btn {
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--stocked-ink);
    background: var(--stocked-bg);
    border: none;
    border-radius: 6px;
    padding: 0.15rem 0.5rem;
    cursor: pointer;
    line-height: 1.5;
    white-space: nowrap;
}

.restock-btn:hover:not(:disabled),
.stillstock-btn:hover:not(:disabled) {
    box-shadow: inset 0 0 0 1px var(--stocked-line);
}

/* Item name → product detail (grocery list + products grid). */
.item-name {
    font-weight: 600;
    color: var(--accent);
    text-decoration: none;
}

/* Product-detail rename: the h1 swaps to this input, so size it to feel like the title it replaces. */
.rename-input {
    font-size: 1.3rem;
    font-weight: 700;
    padding: 0.2rem 0.5rem;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
    min-width: 16rem;
}

/* Merge-into panel: the two controls sit side by side and wrap on narrow screens. */
.merge-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin-top: 0.6rem;
}

.merge-controls label {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.85rem;
    color: var(--muted);
}

.item-name:hover {
    text-decoration: underline;
}

/* ── Recipes ── */
.recipe-disclaimer {
    background: var(--duesoon-bg);
    color: var(--duesoon-ink);
    border: 1px solid var(--duesoon-line);
    border-radius: var(--radius);
    padding: 0.6rem 0.9rem;
    font-size: 0.85rem;
}

.recipe-suggestions {
    display: grid;
    gap: 0.8rem;
    margin-top: 0.9rem;
}

.recipe-card {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.8rem 1rem;
    background: var(--surface-alt);
}

.recipe-card-top {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.6rem;
}

.recipe-card-top h3 {
    margin: 0;
}

.ingredient-list {
    list-style: none;
    padding: 0;
    margin: 0.5rem 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 1rem;
}

.ingredient-list li.grab {
    color: var(--overdue-ink);
    font-weight: 600;
}

.ingredient-list li.have {
    color: var(--stocked-ink);
}

/* A ✓ backed only by the buying rhythm, not a fresh count: honest but LESS certain, so it reads in the
   normal ink rather than the confident "stocked" green, with a "likely" chip to say why. This is the
   visible half of the fix for "the recipe said I had it but I didn't". */
.ingredient-list li.have.likely {
    color: var(--ink);
}

.likely-note {
    color: var(--ink-soft);
    font-size: 0.8rem;
    font-style: italic;
}

/* A main covered ONLY by a declared stand-in (chuck roast for steak): the recipe's steps are written for
   the original and may cook differently, so this points at Adapt. Amber accent to tie it to the "Makeable
   with a swap" chip, but the TEXT stays in readable ink — amber text on the page background fails contrast. */
.swap-note {
    color: var(--ink-soft);
    font-size: 0.8rem;
    border-left: 3px solid var(--duesoon-line);
    padding-left: 0.35rem;
}

/* ---- Meal plan ---- */
.mealplan-setup {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem 1.2rem;
    align-items: flex-end;
    margin-bottom: 1rem;
}
.mealplan-slots {
    display: flex;
    flex-wrap: wrap;
    gap: 0.2rem 0.8rem;
    border: none;
    padding: 0;
    margin: 0;
}
.mealplan-slots legend {
    font-size: 0.85rem;
    color: var(--ink-soft);
    padding: 0;
}
.mealplan-invent {
    flex-basis: 100%;
}
.mealplan-foodgroups {
    flex-basis: 100%;
    max-width: 34rem;
}
/* The per-meal line-up: one row per meal (slot · calories · effort · remove), repeated each day. */
.mealplan-meals {
    margin-bottom: 1rem;
}
.mealplan-meals-head {
    font-size: 0.85rem;
    color: var(--ink-soft);
    margin-bottom: 0.5rem;
}
.mealplan-mealrow {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    margin-bottom: 0.5rem;
}
.mealplan-mealrow select,
.mealplan-mealrow input {
    width: auto;
    min-width: 7rem;
}
.mealplan-mealrow input[type="number"] {
    max-width: 9rem;
}
.mealplan-addmeal {
    margin-top: 0.2rem;
}
.mealplan-day {
    margin-top: 1.2rem;
}
.mealplan-day h3 {
    margin-bottom: 0.5rem;
}
.mealplan-meal {
    margin-bottom: 0.8rem;
}

/* Month-grid calendar (MealPlanPage): weekday header + week rows of 7 day cells, each showing its meal
   titles; click a day to expand its recipes below. Cells shrink to fit (minmax 0) so it never overflows. */
.mealcal {
    margin-bottom: 1.2rem;
}
.mealcal-weekhead,
.mealcal-week {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 4px;
}
.mealcal-weekhead {
    margin-bottom: 4px;
    font-size: 0.75rem;
    color: var(--ink-soft);
    text-align: center;
}
.mealcal-week {
    margin-bottom: 4px;
}
.mealcal-day {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-height: 4.5rem;
    padding: 4px;
    text-align: left;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 6px;
    cursor: pointer;
    font: inherit;
    color: inherit;
    overflow: hidden;
}
.mealcal-day:hover:not(.mealcal-pad) { border-color: var(--accent); }
.mealcal-day.today { border-color: var(--accent); }
.mealcal-day.selected {
    outline: 2px solid var(--accent);
    outline-offset: -1px;
    background: var(--surface-alt);
}
.mealcal-day.mealcal-pad {
    background: transparent;
    border-color: transparent;
    cursor: default;
    min-height: 0;
}
.mealcal-daynum {
    font-size: 0.75rem;
    color: var(--ink-soft);
    font-weight: 600;
}
.mealcal-day.today .mealcal-daynum { color: var(--accent-ink); }
/* A meal title chip in a day cell — wraps to show the full name (the whole point of the at-a-glance
   summary); a coloured left edge marks the slot. Cells grow to fit and the week row aligns to the tallest. */
.mealcal-meal {
    font-size: 0.72rem;
    line-height: 1.2;
    padding: 2px 4px;
    border-radius: 4px;
    background: var(--surface-alt);
    border-left: 3px solid var(--unknown-line);
    white-space: normal;
    overflow-wrap: break-word;
}
.mealcal-meal.slot-breakfast { border-left-color: var(--duesoon-line); }
.mealcal-meal.slot-lunch { border-left-color: var(--stocked-line); }
.mealcal-meal.slot-dinner { border-left-color: var(--accent); }
.mealcal-meal.slot-snack { border-left-color: var(--plan-line); }
.mealcal-detail {
    border-top: 1px solid var(--line);
    padding-top: 0.8rem;
}
.mealcal-detail > h3 { margin-top: 0; }
/* Meal card header: title on the left, the per-slot reroll on the right. */
.mealcal-cardtop {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 0.5rem;
}
.mealcal-reroll {
    flex-shrink: 0;
    font-size: 0.8rem;
    padding: 0.25rem 0.6rem;
    white-space: nowrap;
}
/* Right side of the meal-card header: the serving box stacked directly above the reroll button. */
.mealcal-cardactions {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.4rem;
    flex-shrink: 0;
}
/* Serving / batch stepper — scales the ingredient amounts live (a pure re-render, no AI call). */
.serving-box {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.15rem 0.35rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface-alt);
    font-size: 0.8rem;
    white-space: nowrap;
}
.serving-label { color: var(--ink-soft); font-size: 0.75rem; }
.serving-box .serving-step {
    min-width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    font-size: 1rem;
    line-height: 1;
}
.serving-count {
    width: 3rem;
    text-align: center;
    padding: 0.15rem 0.2rem;
    font-size: 0.85rem;
    -moz-appearance: textfield;
    appearance: textfield;
}
.serving-count::-webkit-outer-spin-button,
.serving-count::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Ingredient row with its swap control + bubble cloud. */
.ingredient-list li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.3rem;
}

/* When a swap cloud is open, the ingredient takes the full row so the bubbles sit under it. */
.ingredient-list li:has(.alt-cloud) {
    flex-basis: 100%;
}

.alt-swap {
    border: none;
    background: transparent;
    color: var(--accent);
    cursor: pointer;
    font-size: 0.95rem;
    line-height: 1;
    padding: 0 0.2rem;
}

.alt-swap:hover {
    color: var(--accent-ink);
}

.alt-cloud {
    flex-basis: 100%;
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0.25rem 0 0.15rem 1.2rem;
}

.alt-bubble {
    border-radius: 999px;
    padding: 0.15rem 0.6rem;
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    border: 1px solid var(--line);
    background: var(--surface);
}

.alt-bubble.have {
    color: var(--stocked-ink);
    border-color: var(--stocked-line);
    background: var(--stocked-bg);
}

.alt-bubble.grab {
    color: var(--overdue-ink);
    border-color: var(--overdue-line);
    background: var(--overdue-bg);
}

.alt-bubble:disabled {
    opacity: 0.6;
    cursor: default;
}

.recipe-grab {
    font-size: 0.88rem;
    margin: 0.2rem 0 0;
}

.recipe-grab.all-good {
    color: var(--stocked-ink);
}

.recipe-cal {
    margin: 0.15rem 0;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--ink-soft);
}

/* ---- Settings + pending imports ---- */
.setting-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font-weight: 600;
    margin: 0.6rem 0 0.15rem;
    cursor: pointer;
}

.import-mode {
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 0.6rem 0.9rem 0.8rem;
    margin: 0.6rem 0 0.4rem;
    display: grid;
    gap: 0.55rem;
}

.import-mode legend {
    font-weight: 600;
    padding: 0 0.35rem;
}

.import-mode label {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.5rem;
    align-items: baseline;
    cursor: pointer;
    font-size: 0.92rem;
}

.pending-receipts {
    list-style: none;
    padding: 0;
    margin: 0.4rem 0 0;
    display: grid;
    gap: 0.5rem;
}

.pending-receipts li {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    border-bottom: 1px solid var(--line);
    padding-bottom: 0.5rem;
}

.pending-actions {
    display: inline-flex;
    gap: 0.4rem;
}

/* ---- Multi-receipt upload: the "one receipt per image" batch ---- */
.combine-check {
    display: block;
    margin: 0.4rem 0 0.6rem;
    color: var(--ink-soft);
}

.combine-check input {
    margin-right: 0.35rem;
}

.batch-progress {
    list-style: none;
    padding: 0;
    margin: 0.75rem 0 0;
    display: grid;
    gap: 0.4rem;
}

.batch-progress li {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    flex-wrap: wrap;
    border-bottom: 1px solid var(--line);
    padding-bottom: 0.4rem;
}

.batch-progress .batch-name {
    font-weight: 600;
    overflow-wrap: anywhere;
}

.recipe-steps {
    margin-top: 0.45rem;
    font-size: 0.88rem;
}

.recipe-steps summary {
    cursor: pointer;
    color: var(--accent-ink);
    font-weight: 600;
}

.recipe-steps ol {
    margin: 0.4rem 0 0;
    padding-left: 1.3rem;
    display: grid;
    gap: 0.25rem;
}

.recipe-links {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.9rem;
}

/* ---- Recipe read-aloud (Phase 3) ---- */
.read-aloud {
    border: 1px solid var(--accent);
    background: var(--surface-alt);
}

.ra-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
}

.ra-head h3 {
    margin: 0;
}

.ra-steps {
    margin: 0.6rem 0;
    padding-left: 1.4rem;
    display: grid;
    gap: 0.3rem;
}

.ra-steps li {
    padding: 0.15rem 0.4rem;
    border-radius: 6px;
}

.ra-steps li.current {
    background: var(--duesoon-bg);
    color: var(--duesoon-ink);
    font-weight: 600;
}

.ra-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.4rem;
}

/* ---- Built-in hands-free reader: what it's doing with the microphone ---- */
.ra-listen {
    margin-top: 0.7rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--line);
}

.ra-listen p {
    margin: 0.25rem 0;
}

.listen-state {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    font-weight: 600;
}

.listen-dot {
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 50%;
    background: var(--ink-soft);
    flex: none;
}

/* Listening is the only state the cook needs to recognise from across a kitchen, so it's the only one
   that moves. The rest are colour alone — a room full of blinking dots reads as an alarm. */
.listen-listening {
    background: var(--accent);
    animation: listen-pulse 1.4s ease-in-out infinite;
}

.listen-thinking,
.listen-answering {
    background: var(--accent);
}

.listen-held {
    background: var(--duesoon-line);
}

@keyframes listen-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.45; transform: scale(1.35); }
}

@media (prefers-reduced-motion: reduce) {
    .listen-listening { animation: none; }
}

/* ---- Listening calibration (Settings) ---- */
.calibrate-stage {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.6rem 0;
}

.listen-values {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.25rem 0.8rem;
    margin: 0.6rem 0;
}

.listen-values dt {
    color: var(--ink-soft);
}

.listen-values dd {
    margin: 0;
}

/* What the default WOULD have been — a calibrated value you can't tell apart from a guess hasn't
   reported anything. */
.listen-delta {
    color: var(--ink-soft);
    font-size: 0.85em;
    margin-left: 0.4rem;
    white-space: nowrap;
}

@media (max-width: 30rem) {
    .listen-values {
        grid-template-columns: 1fr;
        gap: 0 0.8rem;
    }

    .listen-values dd {
        margin-bottom: 0.4rem;
    }
}

/* ---- Hands-free cook-along (Phase 4b) ---- */
.cookalong {
    border: 1px solid var(--accent);
    background: var(--surface-alt);
}

.cookalong-status {
    font-weight: 600;
    margin: 0.3rem 0;
}

.cookalong-status.listening {
    color: var(--stocked-ink);
}

.cookalong-status.speaking {
    color: var(--accent-ink);
}

.saved-recipes {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    gap: 0.6rem;
}

.saved-recipes li {
    border-bottom: 1px solid var(--line);
    padding-bottom: 0.5rem;
}

/* An adapted variant, grouped under its original. */
.saved-recipes li.recipe-variant {
    margin-left: 1.25rem;
    padding-left: 0.75rem;
    border-left: 3px solid var(--accent);
    border-bottom: none;
}

.variant-chip {
    background: var(--surface-alt);
    color: var(--accent-ink);
}

/* A parent shown only as context for a matching variant under the ?uses filter — muted, no actions. */
.saved-recipes li.recipe-reference {
    opacity: 0.65;
    padding-bottom: 0.5rem;
    border-bottom: 1px dashed var(--line);
}

.saved-top {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.saved-top {
    gap: 0.5rem;
    flex-wrap: wrap;
    justify-content: flex-start;
}

.saved-actions {
    display: inline-flex;
    gap: 0.3rem;
    align-items: center;
    margin-left: auto;
}

.picked-banner {
    background: var(--stocked-bg);
    color: var(--stocked-ink);
    border: 1px solid var(--stocked-line);
    border-radius: var(--radius);
    padding: 0.5rem 0.8rem;
}

/* ─────────────────────────────── Cookbook (recipe carousel) ─────────────────────────────── */
.cookbook-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: flex-end;
    margin-bottom: 0.8rem;
}

/* The My-recipes / Meal-plan-recipes source toggle — a two-button segmented control. The active side
   reads as pressed (accent fill); reuses the button .secondary base. */
.cookbook-source {
    display: inline-flex;
    gap: 0;
    margin-bottom: 0.8rem;
}
.cookbook-source button:first-child { border-radius: var(--radius) 0 0 var(--radius); }
.cookbook-source button:last-child { border-radius: 0 var(--radius) var(--radius) 0; margin-left: -1px; }
.cookbook-source button.active {
    background: var(--accent);
    color: var(--on-accent);
    border-color: var(--accent);
    position: relative; /* sit above the sibling's shared border */
}

.cookbook-product-filter {
    margin: 0;
}

/* Which recipe is centred — announced above the shelf (a live region). */
.cookbook-position {
    margin: 0 0 0.6rem;
    text-align: center;
    color: var(--ink-soft);
}

.cookbook-position strong {
    color: var(--ink);
}

/* The preview shelf: a horizontal scroll-snap track of recipe previews. Touch + trackpad swipe come free
   from scroll-snap; mouse click-drag and centre-reporting come from /js/cookbook-carousel.js. The centred
   preview is emphasised; its neighbours peek at a smaller scale + softer opacity. */
.cookbook-shelf-wrap {
    position: relative;
    margin-bottom: 1rem;
}

.cookbook-shelf {
    --card-w: min(20rem, 74vw);
    display: flex;
    gap: 1rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    /* Half the shelf minus half a card, so the first and last previews can reach the centre and their
       neighbours peek in from the edge. */
    padding: 1rem calc(50% - var(--card-w) / 2);
    /* Fade the previews out at both edges rather than hard-clipping them. */
    -webkit-mask-image: linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
    mask-image: linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
    scrollbar-width: none;
}

.cookbook-shelf::-webkit-scrollbar {
    display: none;
}

/* Focusable (the arrow keys page it) — the ring shows it's focused. */
.cookbook-shelf:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: var(--radius);
}

/* While a mouse drag is in progress: suspend snap so the drag moves freely (it re-engages on release and
   snaps to the nearest preview), and suppress text selection + show a grabbing cursor. */
.cookbook-shelf.is-dragging {
    scroll-snap-type: none;
    cursor: grabbing;
    user-select: none;
}

.cookbook-preview {
    scroll-snap-align: center;
    flex: 0 0 var(--card-w);
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    text-align: left;
    padding: 0.8rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    color: var(--ink);
    font: inherit;
    cursor: pointer;
    /* Neighbours peek: smaller + softer. The centred preview overrides both below. */
    opacity: 0.5;
    transform: scale(0.92);
}

@media (prefers-reduced-motion: no-preference) {
    .cookbook-preview {
        transition: opacity 0.2s ease, transform 0.2s ease;
    }
}

.cookbook-preview.is-current {
    opacity: 1;
    transform: scale(1);
    border-color: var(--accent);
    cursor: default;
}

.cookbook-preview-photo {
    width: 100%;
    aspect-ratio: 3 / 2;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-alt);
    display: grid;
    place-items: center;
}

.cookbook-preview-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.cookbook-preview-photo.is-placeholder span {
    font-size: 2.4rem;
    opacity: 0.5;
}

.cookbook-preview-name {
    font-weight: 600;
    font-size: 1.05rem;
}

.cookbook-preview-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    align-items: center;
}

.cookbook-preview-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
}

.cookbook-badges {
    display: inline-flex;
    gap: 0.3rem;
    flex-wrap: wrap;
    align-items: center;
}

.cookbook-subhead {
    margin: 0.6rem 0 0.2rem;
    font-size: 1rem;
}

.cookbook-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.8rem;
}

/* Per-recipe tag chips + the add/suggest controls. */
.cookbook-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
    margin: 0.4rem 0;
}

.cookbook-tag-add {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    align-items: center;
}

.cookbook-tag-add input {
    width: 9rem;
}

.cookbook-photo {
    display: block;
    max-width: 100%;
    max-height: 22rem;
    width: auto;
    border-radius: var(--radius);
    margin: 0.2rem 0 0.6rem;
    object-fit: cover;
}

.cookbook-head-actions {
    display: inline-flex;
    gap: 0.4rem;
    flex-wrap: wrap;
}

/* Recipe import (the /cookbook/import review flow) */
.import-paste {
    width: 100%;
    max-width: 40rem;
    font: inherit;
}

.import-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 1rem;
    align-items: center;
}

.import-ingredient-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
    margin-bottom: 0.35rem;
}

.import-qty {
    width: 7rem;
}

.import-name {
    flex: 1 1 12rem;
    min-width: 10rem;
}

.import-main {
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.import-step-row {
    display: flex;
    gap: 0.4rem;
    align-items: flex-start;
    margin-bottom: 0.35rem;
}

.import-step-row textarea {
    flex: 1 1 auto;
    font: inherit;
}

.import-step-no {
    padding-top: 0.4rem;
    color: var(--ink-soft);
    min-width: 1.5rem;
}

/* The print-only surfaces never show on screen; @media print reveals the chosen one. */
.cookbook-print {
    display: none;
}

/* A gentle fade as the detail panel mounts (it's @key'd per recipe, so changing the centred recipe
   remounts it and replays this). Motion-safe only — reduced-motion gets an instant cut, no animation. */
@media (prefers-reduced-motion: no-preference) {
    .cookbook-detail {
        animation: cookbook-detail-in 0.25s ease-out;
    }
}

@keyframes cookbook-detail-in {
    from {
        opacity: 0;
        transform: translateY(0.5rem);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Keep the "Next buy" date + relative on one line (the popover keeps normal wrapping). */
.nextbuy {
    white-space: nowrap;
}

.nextbuy .popover {
    white-space: normal;
}

/* Integrated in-header filter row. */
.filter-row th {
    padding: 0.3rem 0.5rem;
    background: var(--surface-alt);
    font-weight: 400;
    border-bottom: 2px solid var(--line);
}

.filter-row input,
.filter-row select {
    width: 100%;
    box-sizing: border-box;
}

/* Small icon-only button (e.g. delete). */
.icon-btn {
    padding: 0.25rem 0.45rem;
    background: var(--surface);
    color: var(--ink-soft);
    border: 1px solid var(--line);
    line-height: 1;
    font-size: 1rem;
}

.icon-btn:hover:not(:disabled) {
    background: var(--surface-alt);
    border-color: var(--overdue-line);
    color: var(--overdue-ink);
}

/* Buttons that read as inline row-level actions ("Used one") — link-styled, not button-styled.
   The class predates this rule; undefined, these fell back to the full blue button styling. */
.linkish {
    background: none;
    border: none;
    padding: 0;
    margin-left: 0.4rem;
    color: var(--accent-ink);
    cursor: pointer;
    font: inherit;
    font-size: 0.85rem;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.linkish:hover:not(:disabled) {
    color: var(--accent);
}

.linkish:disabled {
    opacity: 0.5;
    cursor: default;
}

/* A label that proxies a visually-hidden file input — the 📷 photo buttons on the Cookbook and
   Recipe-import pages (the receipt and census pages dropped theirs). Styled as the primary button;
   the focus ring is mirrored from the INPUT's focus (a label isn't focusable, and the real input is
   visually hidden, so its own ring would draw around a 1px clip). The input must precede the label
   in the DOM for these selectors. */
.file-button {
    display: inline-block;
    font: inherit;
    padding: 0.4rem 0.8rem;
    border: 1px solid var(--accent);
    background: var(--accent);
    color: var(--on-accent);
    border-radius: 6px;
    cursor: pointer;
    margin-left: 0.5rem;
    user-select: none;
}

input.visually-hidden:not(:disabled) + .file-button:hover {
    background: var(--accent-ink);
    border-color: var(--accent-ink);
}

input.visually-hidden:focus-visible + .file-button {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

input.visually-hidden:disabled + .file-button {
    opacity: 0.5;
    cursor: default;
}

/* The photos/files staged for an upload — compact rows, each with its ✕. */
.staged-files {
    list-style: none;
    margin: 0.6rem 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.staged-files li {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    max-width: 28rem;
}

.staged-files li span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---- "Which did you use?" picker (Ate it, DESIGN.md 13.3) ---- */
.picker-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 60;
}

.picker-modal {
    max-width: 26rem;
    width: calc(100% - 2rem);
    max-height: 70vh;
    overflow-y: auto;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}

/* ---- Grocery List report ---- */
tfoot td {
    border-top: 2px solid var(--line);
    background: var(--surface-alt);
}

.subtotal-label {
    text-align: right;
    color: var(--ink-soft);
}

.subtotal {
    text-align: right;
    font-weight: 700;
}

.grand-total {
    margin: 1rem 0;
    font-size: 1.05rem;
}

/* ---- Accessible detail popover (Products grid) ---- */
.info-btn {
    padding: 0 0.3rem;
    margin-left: 0.25rem;
    background: transparent;
    border: none;
    color: var(--accent);
    cursor: pointer;
    font-size: 0.95rem;
}

.popover {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.75rem 1rem;
    background: var(--surface);
    color: var(--ink);
    box-shadow: var(--shadow);
    max-width: 20rem;
}

.popover dl {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.15rem 0.75rem;
    margin: 0.5rem 0 0;
}

.popover dt {
    color: var(--ink-soft);
}

.popover dd {
    margin: 0;
}

/* ---- Grocery List actions, review date, print ---- */
.list-actions {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin: 0.5rem 0 1rem;
}

.review-date {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    flex-wrap: wrap;
}

/* ---- Reports ---- */
.report-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

.report-presets {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin: 0.25rem 0 1.25rem;
}

.report-preset-pill {
    padding: 0.4rem 0.9rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    color: var(--ink);
    text-decoration: none;
    font-size: 0.9rem;
}

.report-preset-pill:hover { border-color: var(--accent); }

.report-preset-pill.active {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
}

.report-section {
    background: var(--surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1rem 1.25rem 1.25rem;
    margin-bottom: 1.25rem;
}

.report-section h2 { margin-top: 0.25rem; }
.report-section h3 { margin-bottom: 0.25rem; }

.report-chart {
    display: block;
    width: 100%;
    height: auto;
    max-width: 860px;
}

.report-chart-tick {
    font-size: 11px;
    fill: var(--ink-soft);
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}

.chart-legend {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1rem;
    padding: 0;
    margin: 0 0 0.5rem;
    font-size: 0.85rem;
    color: var(--ink-soft);
}

.chart-legend li {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.chart-legend a {
    color: inherit;
    text-decoration-style: dotted;
    text-underline-offset: 2px;
}

.chart-legend a:hover { color: var(--accent); }

/* The Products cleanup chips (Untagged / Other aisle) sit apart from real tags. */
.untagged-chip { border-style: dashed; }

.active-aisle-filter {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    color: var(--ink-soft);
    font-size: 0.9rem;
}

/* A linked chart segment behaves like the link it is. */
.report-chart a { cursor: pointer; }
.report-chart a:hover rect,
.report-chart a:focus-visible rect { opacity: 0.8; }
.report-chart a:focus-visible rect { stroke: var(--accent); stroke-width: 2; }

.chart-legend-swatch {
    width: 12px;
    height: 12px;
    border-radius: 3px;
    flex: none;
    /* Browsers strip HTML background colors when printing (the SVG chart keeps its fills, so a
       printed page had a colored chart beside a legend of blank squares). exact opts the swatch
       out; the hairline border is the grayscale-printer fallback so a square still shows. */
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    border: 1px solid var(--line);
}

.report-data table { font-size: 0.85rem; }

/* Numeric table cells: right-aligned, digits that line up down the column. */
.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.rank-bar-col { width: 40%; }

.rank-bar {
    display: inline-block;
    height: 10px;
    border-radius: 3px;
    background: var(--chart-1);
    min-width: 2px;
    vertical-align: middle;
    /* Same printed-background stripping as the legend swatches — a share bar that vanishes on
       paper turns the top-items table into an empty column. */
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
}

.report-empty { padding: 1.25rem; }

.builder-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 0.75rem 1rem;
    margin-bottom: 0.75rem;
}

.builder-grid label {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.85rem;
    color: var(--ink-soft);
}

.builder-grid select,
.builder-grid input {
    font: inherit;
    color: var(--ink);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 0.35rem 0.5rem;
}

.builder-grid .builder-check {
    flex-direction: row;
    align-items: center;
    gap: 0.5rem;
    align-self: end;
}

.builder-problems {
    color: var(--overdue-ink);
    background: var(--overdue-bg);
    border-radius: 6px;
    padding: 0.5rem 0.75rem 0.5rem 1.75rem;
    font-size: 0.85rem;
    margin: 0 0 0.75rem;
}

.builder-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.builder-save-name {
    font: inherit;
    color: var(--ink);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 0.35rem 0.5rem;
}

.saved-reports {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.saved-report {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
}

.saved-report-delete {
    background: none;
    border: none;
    color: var(--ink-soft);
    cursor: pointer;
    padding: 0.15rem 0.3rem;
    border-radius: 4px;
}

.saved-report-delete:hover {
    color: var(--overdue-ink);
    background: var(--overdue-bg);
}

/* ---- Site footer (problem reporting entry points) ---- */
.site-footer {
    display: flex;
    gap: 1rem;
    justify-content: center;
    padding: 1rem;
    margin-top: 1.5rem;
    border-top: 1px solid var(--line);
    font-size: 0.9rem;
}

.site-footer a {
    color: var(--ink-soft);
    text-decoration: none;
}

.site-footer a:hover {
    color: var(--accent-ink);
    text-decoration: underline;
}

/* The admin page's expandable exception text — long, monospace, and must scroll inside its own
   box rather than widen the page (the no-horizontal-page-scroll rule). */
.error-detail {
    max-height: 20rem;
    overflow: auto;
    font-size: 0.78rem;
    background: var(--surface-alt);
    border: 1px solid var(--line);
    padding: 0.5rem;
    white-space: pre-wrap;
    word-break: break-word;
}

/* The printed page introduces itself; on screen the app chrome already does. */
.report-print-header { display: none; }

@media print {
    .mobile-bar,
    .sidebar,
    .sidebar-backdrop,
    .skip-link,
    .list-actions,
    details.everything-else,
    .info-btn,
    .untrack-btn,
    .restock-btn,
    .stillstock-btn,
    .report-presets,
    .report-print-btn,
    .report-builder,
    .voice-agent,
    .tour-panel,
    .reginald-dance-clip,
    .site-footer {
        /* .sidebar / .mobile-bar: the nav rail (and its phone top bar) are app chrome, not document
           content — hidden so a printed grocery list or report is just the document.
           .voice-agent: the assistant is a floating control on every page — screen furniture,
           never document content. Global, not report-only: no printout wants it.
           .tour-panel: same class of thing, and the walkthrough deliberately VISITS the two pages
           built to be printed — Reports and the grocery list — so it would otherwise land in the
           printout of a page it just invited you to print.
           .site-footer: app chrome on every page — "Report a bug" doesn't belong in a printed
           grocery list. */
        display: none !important;
    }

    /* With the rail hidden, drop the two-column grid so the content prints full width rather than
       squeezed into the (now empty) sidebar track. */
    .app-shell {
        display: block;
    }

    body {
        background: #fff;
        color: #000;
    }

    table {
        box-shadow: none;
    }

    .report-print-header {
        display: flex;
        justify-content: space-between;
        gap: 1rem;
        border-bottom: 1px solid #000;
        padding-bottom: 0.35rem;
        margin-bottom: 1rem;
        font-size: 0.9rem;
    }

    .report-section {
        box-shadow: none;
        padding: 0 0 0.5rem;
        margin-bottom: 1rem;
        break-inside: avoid;
    }

    .report-chart { max-width: 100%; }

    /* ── Cookbook: print ONLY the chosen print-only block (the recipe card, or its products list).
       Scoped to .cookbook-root so hiding the page-head/disclaimer here can't hide another printable
       page's (these @media print rules are global CSS, applied on whatever page prints). ── */
    .cookbook-root .page-head,
    .cookbook-root .recipe-disclaimer,
    .cookbook-root .panel {
        display: none !important;
    }

    .cookbook-print {
        display: block !important;
    }

    .cookbook-print.show-recipe .print-products,
    .cookbook-print.show-products .print-recipe {
        display: none !important;
    }

    .cookbook-print h1 {
        font-size: 1.6rem;
        margin: 0 0 0.4rem;
    }

    .cookbook-print h2 {
        font-size: 1.1rem;
        margin: 1rem 0 0.3rem;
        border-bottom: 1px solid #000;
        padding-bottom: 0.15rem;
    }

    .cookbook-print ul,
    .cookbook-print ol {
        margin: 0.3rem 0 0.6rem 1.4rem;
    }

    .cookbook-print li {
        margin: 0.15rem 0;
    }

    .cookbook-print .print-products-list {
        list-style: none;
        margin-left: 0;
    }
}

/* ---- Blazor form validation + error boundary defaults ---- */
.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

/* ---- BYOK provider & keys (Settings) ---- */
.ai-settings {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    max-width: 34rem;
}

/* A labelled form control, label stacked ABOVE the box. Unscoped on purpose: it was
   `.ai-settings .field`, which left every OTHER `class="field"` (the bug-report form) completely
   unstyled — label and box side by side, the `.linkish` failure shape again. */
.field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    max-width: 34rem;
}

.field > span {
    font-weight: 600;
    font-size: 0.9rem;
}

/* The stacked label needs its control to fill the line, or the 34rem cap above constrains
   nothing: a bare <select> sizes to its widest option and a textarea to its default cols. */
.field input,
.field select,
.field textarea {
    width: 100%;
    box-sizing: border-box;
}

.ai-settings input,
.ai-settings select {
    width: 100%;
    box-sizing: border-box;
}

.ai-settings .model-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.7rem;
}

.ai-settings h3 {
    margin: 0.4rem 0 0;
}

/* Unscoped like .field above, and for the same reason: checkbox rows exist outside the BYOK
   panel too (the invite single-use and expiration toggles), and a box-above-its-caption layout
   is what class="field" would give them. */
.checkbox-field {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.5rem;
    align-items: start;
    cursor: pointer;
    font-size: 0.92rem;
}

.checkbox-field input {
    width: auto;
    margin-top: 0.2rem;
}

/* Bug-report "details to attach" — the diagnostic snapshot the reporter reviews and can trim per
   section before sending (Bugs.razor). Deliberately plain: it's a form aside, not a feature panel. */
.bug-attach {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.75rem 1rem 1rem;
    margin: 0.25rem 0;
    max-width: 34rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.bug-attach legend {
    font-weight: 600;
    font-size: 0.9rem;
    padding: 0 0.35rem;
}

.attach-item {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.attach-diag {
    margin: 0;
    display: grid;
    gap: 0.15rem 0.75rem;
    font-size: 0.85rem;
}

.attach-diag > div {
    display: grid;
    grid-template-columns: 6rem 1fr;
    gap: 0.5rem;
}

.attach-diag dt {
    color: var(--ink-soft);
}

.attach-diag dd {
    margin: 0;
    word-break: break-word;
}

.attach-errors {
    margin: 0;
    padding-left: 1rem;
    list-style: disc;
}

.attach-content {
    max-height: 12rem;
    overflow: auto;
    background: var(--surface-alt);
    border-radius: var(--radius);
    padding: 0.5rem 0.65rem;
    margin: 0.35rem 0 0;
    font-size: 0.8rem;
    white-space: pre-wrap;
    word-break: break-word;
}

/* An unchecked section stays visible (so you can see what you're deciding about) but dims, to show it
   won't be sent. */
.attach-off {
    opacity: 0.5;
}

/* The same snapshot rendered for the admin (BugSnapshotView) — collapsed by default so a report row
   stays compact until the admin wants the detail. */
.admin-attach {
    margin-top: 0.35rem;
    font-size: 0.85rem;
}

.admin-attach summary {
    cursor: pointer;
    color: var(--ink-soft);
}

@media (max-width: 560px) {
    .ai-settings .model-fields {
        grid-template-columns: 1fr;
    }
}

/* ---- Accounts: rail sign-out + the static Account pages ---- */
/* In the rail footer the email + Sign out stack; the email truncates to the rail width. */
.account-box {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
}

.account-email {
    color: var(--ink-soft);
    text-decoration: none;
    font-size: 0.9rem;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.account-email:hover {
    color: var(--accent-ink);
}

.account-signout {
    background: none;
    border: none;
    padding: 0;
    color: var(--ink-soft);
    font: inherit;
    font-size: 0.9rem;
    cursor: pointer;
    text-decoration: underline;
}

.account-signout:hover {
    color: var(--accent-ink);
}

/* Theme switcher in the rail footer: palette (Warm & Fresh / Classic) over light/dark (Auto/Light/Dark).
   Drives wwwroot/js/theme.js, which stamps data-apptheme + data-theme on <html>; the selects are themed
   by the tokens like everything else. Stacked so both fit the narrow rail. */
.theme-switch {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.theme-switch select {
    /* font, background, and border come from the base `input, select` rule — only the overrides here. */
    width: 100%;
    font-size: 0.9rem;
    color: var(--ink-soft);
    border-radius: var(--radius);
    padding: 0.2rem 0.4rem;
    cursor: pointer;
}

.theme-switch select:hover {
    border-color: var(--ink-soft);
    color: var(--ink);
}

.auth-shell {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 3rem 1rem;
    gap: 1.25rem;
}

.auth-brand .brand {
    font-weight: 700;
    font-size: 1.3rem;
    letter-spacing: -0.01em;
}

.auth-card {
    width: 100%;
    max-width: 26rem;
    padding: 1.5rem 1.75rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow);
}

.auth-card h1 {
    margin-top: 0;
    font-size: 1.3rem;
}

.auth-card form {
    display: flex;
    flex-direction: column;
    gap: 0.8rem;
}

.auth-field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.auth-field label {
    font-weight: 600;
    font-size: 0.9rem;
}

.auth-field input {
    width: 100%;
    box-sizing: border-box;
}

.auth-remember {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.92rem;
}

.auth-household {
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    padding: 0.75rem 1rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.8rem;
}

.auth-household legend {
    font-weight: 600;
    font-size: 0.9rem;
    padding: 0 0.35rem;
}

.auth-submit {
    align-self: flex-start;
    padding: 0.5rem 1.1rem;
    border: 1px solid var(--accent);
    border-radius: 6px;
    background: var(--accent);
    color: var(--on-accent);
    font-weight: 600;
    cursor: pointer;
}

.auth-submit:hover {
    filter: brightness(1.08);
}

.auth-alt {
    margin-bottom: 0;
}

.auth-external {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.auth-external-btn {
    width: 100%;
    padding: 0.5rem 1.1rem;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
    font-weight: 600;
    cursor: pointer;
}

.auth-external-btn:hover {
    border-color: var(--accent);
}

.auth-status {
    padding: 0.6rem 0.9rem;
    border-radius: 6px;
    margin: 0.5rem 0 0.9rem;
    font-size: 0.95rem;
}

.auth-status-ok {
    background: color-mix(in srgb, var(--stocked-ink) 12%, transparent);
    color: var(--stocked-ink);
}

.auth-status-error {
    background: color-mix(in srgb, #e50000 10%, transparent);
    color: #c62828;
}

/* ---- Household panel (Settings) ---- */
.invite-row {
    align-items: center;
}

.invite-code {
    font-size: 1.05rem;
    letter-spacing: 0.12em;
    padding: 0.35rem 0.7rem;
    border: 1px dashed var(--line);
    border-radius: 6px;
    background: var(--surface-alt, transparent);
    user-select: all;
}

.member-list {
    margin: 0.25rem 0 0.75rem;
    padding-left: 1.25rem;
}

.member-list li {
    margin: 0.15rem 0;
}

/* ---- Public /about page (its own AboutLayout, outside the app shell) ---- */
.about-shell {
    max-width: 52rem;
    margin: 0 auto;
    padding: 1.5rem 1.25rem 3rem;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.about-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--line);
}

.about-head .brand {
    font-weight: 700;
    font-size: 1.15rem;
    color: var(--ink);
    text-decoration: none;
}

.about-nav a,
.about-foot a,
.about-selfhost a,
.about-reserve a {
    color: var(--accent-ink);
    text-decoration: none;
}

.about-main {
    flex: 1;
    padding: 1.5rem 0;
}

.about-foot {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    padding-top: 1.25rem;
    border-top: 1px solid var(--line);
    color: var(--ink-soft);
    font-size: 0.9rem;
}

.about > section {
    margin: 2.25rem 0;
}

.about-hero {
    margin-top: 0.5rem;
}

.about-hero h1 {
    font-size: 2rem;
    margin: 0 0 0.25rem;
}

.about-tagline {
    font-size: 1.15rem;
    color: var(--ink-soft);
    max-width: 40rem;
}

.about h2 {
    font-size: 1.3rem;
    margin: 0 0 0.6rem;
}

.about-media {
    display: grid;
    gap: 1rem;
}

.about-media figure {
    margin: 0;
}

.about-media figcaption {
    font-weight: 600;
    margin-bottom: 0.3rem;
}

.about-media audio {
    width: 100%;
}

.about-video video {
    width: 100%;
    max-width: 40rem;
    border-radius: var(--radius);
    background: #000;
}

.about-snippet {
    background: var(--surface-alt);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.85rem 1rem;
    overflow-x: auto;
    font-size: 0.85rem;
    line-height: 1.5;
}

/* the reserve / wishlist */
.about-tiers {
    border: none;
    padding: 0;
    margin: 0 0 1rem;
}

.about-tiers legend {
    font-weight: 600;
    margin-bottom: 0.5rem;
    padding: 0;
}

.about-tier {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.15rem 0.6rem;
    align-items: start;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.7rem 0.9rem;
    margin-bottom: 0.5rem;
    cursor: pointer;
}

.about-tier:hover {
    border-color: var(--accent);
}

.about-tier input {
    grid-row: 1 / span 2;
    margin-top: 0.2rem;
}

.about-tier-head {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: baseline;
}

.about-tier-name {
    font-weight: 600;
}

.about-tier-price {
    color: var(--ink-soft);
    font-size: 0.9rem;
}

.about-tier-blurb {
    grid-column: 2;
    color: var(--ink-soft);
    font-size: 0.9rem;
}

.about-field {
    margin: 0.75rem 0;
    max-width: 26rem;
}

.about-field label {
    display: block;
    margin-bottom: 0.25rem;
}

.about-field input {
    width: 100%;
}

.about-submit {
    margin-top: 0.25rem;
}

.about-fineprint {
    font-size: 0.85rem;
    margin-top: 0.5rem;
}

/* Honeypot: in the DOM (so a bot parsing HTML fills it) but off-screen, aria-hidden and untabbable,
   so a person never sees or reaches it. A filled one is dropped server-side. */
.about-hp {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.about-support {
    margin-top: 1.5rem;
    padding: 1rem 1.15rem;
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    background: var(--surface-alt);
    border-radius: var(--radius);
}

.about-support h3 {
    margin: 0 0 0.4rem;
}

.about-support-btn {
    display: inline-block;
    margin-top: 0.5rem;
    padding: 0.55rem 1rem;
    background: var(--accent);
    color: var(--on-accent);
    border-radius: var(--radius);
    text-decoration: none;
    font-weight: 600;
}

@media (min-width: 40rem) {
    .about-media {
        grid-template-columns: 1fr 1fr;
    }

    .about-video {
        grid-column: 1 / -1;
    }
}
