
/* Light theme only. The surface scale below is hardcoded to warm off-whites, so
   [data-theme=dark] currently renders with light surfaces — dark mode is not
   maintained. If dark is revived, move the surface + primary tokens into
   per-theme blocks. */
:root {
    /* ── Brand — two coppers, split by role. See docs/design-system.html. ──
       --copper is bright, for fills/borders/marks. --copper-ink is the deep
       readable variant for copper *text* on light (bright copper as text fails
       contrast). --copper-soft is the tint used for hovers and badge grounds. */
    --copper: #C28570;
    --copper-ink: #9C5E48;
    --copper-soft: rgba(194, 133, 112, 0.12);
    --copper-light: rgba(194, 133, 112, 0.52);
    --accent: var(--copper);
    --gradient: linear-gradient(90deg, rgba(194, 133, 112, 0.15), rgba(194, 133, 112, 0.05));
    --gradient-strong: linear-gradient(90deg, rgba(194, 133, 112, 0.65), rgba(194, 133, 112, 0.55));

    /* ── Ink / inverse surface family — one family, three uses ──
       Secondary buttons, avatars, and toasts all draw from this. */
    --ink: #26201B;
    --ink-content: #FAF9F6;

    /* ── Surfaces. We override DaisyUI's own base tokens (rather than keep a
       parallel set) so every DaisyUI component — cards, inputs, badges, and the
       login page's bg-base-* utilities — adopts this palette. --surface/--bg/
       --bg-2/--border are semantic aliases pointing at the same values. */
    --color-base-100: #FAF9F6;                    /* surfaces: cards, inputs */
    --color-base-200: oklch(96.33% 0.008 98.88);  /* page background */
    --color-base-300: oklch(90.5% 0.012 92);      /* borders, dividers */
    --color-base-content: var(--ink);             /* body text (warm near-black) */
    --surface: var(--color-base-100);
    --bg: var(--color-base-200);
    --bg-2: var(--color-base-300);
    --muted: #6B625A;                             /* secondary text (AA ~5.5:1) */
    /* DaisyUI paints the ROOT element with --root-bg (default: base-100) and
       derives the scroll/backdrop colour from it. Stock DaisyUI treats base-100
       as the page ground, but here base-100 is the lighter card surface — so
       without this, anything past the body box (overscroll, or scrolling beyond
       content) shows the lighter colour. Point it at the page ground instead. */
    --root-bg: var(--bg);
    /* NOTE: do not define --border here. DaisyUI owns it as a border *width*
       (1px) and feeds it into border-width and into calc() for badge padding;
       giving it a color value breaks buttons, badges and tabs. Use
       --color-base-300 / --bg-2 for border colors instead. */

    /* ── DaisyUI semantic roles mapped to the system. These drive the .btn,
       .badge, .alert and text-/bg- color utilities. Primary is copper with
       white text (~3.0:1 — acceptable for short bold button labels; deepen the
       fill to ~#8F5541 if strict AA is required). Secondary/neutral are ink. */
    --color-primary: var(--copper);
    --color-primary-content: #ffffff;
    --color-secondary: var(--ink);
    --color-secondary-content: var(--ink-content);
    --color-accent: var(--copper);
    --color-accent-content: var(--ink);
    --color-neutral: var(--ink);
    --color-neutral-content: var(--ink-content);

    /* ── Status — warm-harmonized, all text-safe on --surface. */
    --color-success: #2A7A52;
    --color-warning: #8A5A12;
    --color-error:   #B23A2E;
    --color-info:    #2C6E8F;
    /* The -content pairs must be set alongside: DaisyUI's defaults are tuned for
       its own lighter status hues and render dark-on-dark against these. All four
       above clear 4.8:1 with white. */
    --color-success-content: #ffffff;
    --color-warning-content: #ffffff;
    --color-error-content:   #ffffff;
    --color-info-content:    #ffffff;
    --success: var(--color-success);
    --warning: var(--color-warning);
    --danger:  var(--color-error);
    --error:   var(--color-error);
    --info:    var(--color-info);

    /* ── Geometry. Override DaisyUI's radius tokens rather than the component
       rules: .btn-circle / .btn-square override border-radius themselves, so
       setting it on .btn directly would flatten them. DaisyUI ships 4px fields,
       which reads as square at our sizes; the system radius is 9px.
       --radius-selector is left alone — it also shapes checkboxes and radios. */
    --radius-field: 9px;   /* buttons, inputs, tabs */
    --radius-box: 9px;     /* cards, modals */

    --gap: 12px;
    --pad: 16px;
    --shadow: 0 6px 16px rgba(38, 32, 27, 0.08);
    --shadow-sm: 0 2px 6px rgba(38, 32, 27, 0.06);
    --rounded: 9px;
    --glass: rgba(255, 255, 255, 0.6);
    --max-width: 1800px;

    /* Base UI font. Setting DaisyUI's --default-font-family propagates Inter Tight
       to everything the framework styles (html, plus form controls via font:inherit). */
    --default-font-family: 'Inter Tight', ui-sans-serif, system-ui, sans-serif;

    /* Type faces (design-system.html): serif is reserved for headlines, mono is
       the data face for tabular figures, codes and measurements. */
    --serif: "Instrument Serif", Georgia, serif;
    --mono: "JetBrains Mono", ui-monospace, monospace;
    /* Tailwind's font-mono utility (and code/pre) read this; its own default is
       the system monospace stack. */
    --font-mono: var(--mono);
    --content: var(--ink);
    font-family: var(--default-font-family);
}

/* Copper as text must use the deep variant to stay readable on light surfaces.
   DaisyUI points these utilities at --color-primary/--color-accent (bright
   copper); redirect them to --copper-ink. (Links are handled in the `a` rule.) */
.text-primary,
.text-accent {
    color: var(--copper-ink);
}

/* Reset & base */
* {
    box-sizing: border-box
}

/* --- Touch responsiveness ------------------------------------------------
   On touch devices the browser withholds the :active state (and used to add a
   ~300ms click delay) while it decides whether a tap is the start of a scroll.
   That is why DaisyUI's press animations "never fire" on mobile/tablet and why
   presses feel laggy. `touch-action: manipulation` opts these elements out of
   double-tap-zoom/pan detection, so the browser paints :active immediately.

   We also define an explicit press state so feedback never depends on
   DaisyUI internals: a tiny scale + dim that fires the instant the finger
   lands. Gated to coarse pointers so mouse hover behaviour is untouched. */
a, button, .btn, [role="button"], summary, label, input[type="submit"],
input[type="button"], .nav-icon, .data-item, .clickable {
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

@media (pointer: coarse) {
    a:active, button:active, .btn:active, [role="button"]:active,
    summary:active, input[type="submit"]:active, input[type="button"]:active {
        transform: scale(0.97);
        opacity: 0.9;
        transition: transform 0.06s ease, opacity 0.06s ease;
    }
}

@media (prefers-reduced-motion: reduce) {
    a:active, button:active, .btn:active, [role="button"]:active {
        transform: none;
    }
}

html, body, #app {
    height: 100%
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--color-base-content);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

footer {
    background-color: var(--color-base-200);
    margin-top: 30px;
}

a {
    text-decoration: none;
}

/* Link color is the deep readable copper — but not on anchors styled as buttons:
   DaisyUI's .btn color rules are layered, so an unlayered `a` color would beat
   them and tint every <a class="btn"> (washing out primary buttons). Excluding
   .btn lets each button variant keep its own text color. */
a:not(.btn) {
    color: var(--copper-ink);
}

/* Layout */
.container {
    max-width: var(--max-width);
    /*margin: 28px auto;*/
    margin: 0 auto; /* todo check mobile header */
    padding: 20px;
}

.layout {
    display: grid;
    grid-template-columns:260px 1fr;
    gap: 20px;
    align-items: start;
}

/* Sidebar */
.sidebar {
    background: var(--surface);
    border-radius: var(--rounded);
    padding: 18px;
    box-shadow: var(--shadow);
    height: calc(100vh - 96px);
    position: sticky;
    top: 38px;
    overflow: auto;
}

.sidebar button {
    margin-top: 8px;
}


.sidebar p {
    font-family: "Courier New", Courier, monospace;
    font-size: 13px;
    color: var(--muted);
}

.sidebar hr {
    color: var(--glass);
}

.card hr,
.legacy-card hr {
    color: var(--glass);
}

.brand {
    justify-content: center;
    text-align: center;
    display: flex;
    gap: 12px;
    align-items: center;
    margin-bottom: 18px;
}

.brand h1 {
    color: var(--color-base-content);
}

.brand img {
    margin-top: 25px;
    max-width: 200px;
}

.subtitle {
    font-size: 0.8em;
    font-weight: 200;
}

nav ul {
    list-style: none;
    padding: 0;
    margin: 0
}

nav li {
    margin: 8px 0
}

nav a {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px;
    border-radius: var(--rounded);
    color: var(--muted);
    cursor: pointer;
    position: relative;
    /* Fast transitions so hover/press feel instant, not laggy. Explicit
       properties (not `all`) keep it cheap and avoid animating layout. */
    transition: background 0.15s ease, color 0.15s ease, transform 0.08s ease;
}

/* Hover feedback only where a real pointer exists (desktop, or a tablet with a
   mouse/trackpad). Gating on `hover: hover` stops the state sticking after a
   tap on touch-only devices. */
@media (hover: hover) {
    nav a:hover {
        background: linear-gradient(90deg, rgba(194, 133, 112, 0.10), rgba(194, 133, 112, 0.03));
        color: var(--color-base-content);
        transform: translateX(2px);
    }
}

/* Immediate press feedback on every device (touch already gets the global
   coarse-pointer :active scale; this adds it for mouse too). */
nav a:active {
    transform: scale(0.98);
    background: linear-gradient(90deg, rgba(194, 133, 112, 0.18), rgba(194, 133, 112, 0.08));
}

nav a.active {
    background: linear-gradient(90deg, rgba(194, 133, 112, 0.16), rgba(194, 133, 112, 0.06));
    color: var(--color-base-content);
    font-weight: 600;
}

/* Accent bar marking the current page. */
nav a.active::before {
    content: "";
    position: absolute;
    left: 2px;
    top: 8px;
    bottom: 8px;
    width: 3px;
    border-radius: 3px;
    background: var(--accent);
}

/* Tabs styling. Inter Tight (not serif) — serif is reserved for headlines. */
nav.tabs {
    display: flex;
    border-bottom: 1px solid var(--color-base-300);
    margin-bottom: 12px;
    justify-content: center;
}

.tab-content {
    display: none;
    padding-top: 4px
}

.tab-content.active {
    display: block
}

a.tab-link.active {
    color: var(--copper-ink);
    font-weight: 600;
    background-image: none;
    border-radius: 0;
    border-bottom: 3px solid var(--accent);
    border-left: none;
}

/* Top bar */
.topbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 18px
}

.search {
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--surface);
    padding: 10px;
    border-radius: 999px;
    box-shadow: var(--shadow);
    min-width: 300px
}

/* Closes the search sheet; hidden unless the bar is open below 900px. */
.search-cancel {
    display: none;
    flex-shrink: 0;
    margin-left: 4px;
    padding: 4px 2px 4px 8px;
    border: none;
    background: none;
    font-size: 14px;
    font-weight: 500;
    color: var(--copper-ink);
    cursor: pointer;
}

.profile {
    display: flex;
    align-items: center;
    gap: 12px
}

.topbar .avatar {
    width: 40px;
    height: 40px;
    border-radius: 999px;
    background: var(--ink);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--copper);
    font-weight: 700
}

/* Main content */
.main {
    min-height: 80vh;
    /* As a grid column (.layout) the default `min-width: auto` lets wide content
       (stat rows, tables, long UUIDs) inflate the track past the viewport, which
       pushed the section-header actions off-screen. `min-width: 0` lets the
       column shrink to the viewport; the `overflow-x: clip` below then contains
       any genuinely wide inner content instead of stretching the whole page. */
    min-width: 0;
    /* Guard against wide dropdown menus pushing the page into a horizontal
       scroll. `clip` (unlike `hidden`) does not create a scroll container, so
       the sticky section header keeps working. The clip edge sits at the box
       edge, which cut off inner cards' box-shadows on the left/right. We pad
       the sides by the shadow's blur radius so those shadows fall inside the
       padding, clear of the clip edge. (`overflow-clip-margin` did this in
       Chrome/Firefox but Safari/WebKit ignores it for block boxes, so the
       shadow was still clipped there — padding works cross-browser.) */
    overflow-x: clip;
    padding-inline: 16px;
}

.grid-cards {
    display: grid;
    grid-template-columns:repeat(auto-fit, minmax(400px, 1fr));
    gap: 16px;
    margin-bottom: 18px
}

.flex-cards {
    display: flex;
    flex-wrap: wrap;
    grid-template-columns:repeat(auto-fit, minmax(1000px, 2fr));
    gap: 16px;
    margin-bottom: 18px;
    justify-content: center;
}

/* The pre-design-system panel, for bare cards with no .card-body: KPI tiles, table
   wrappers, plain containers. Kept as-is until each page moves to the design-system
   patterns (ADR 0048); new markup should not use it. */
.legacy-card {
    background: var(--surface);
    padding: 16px;
    border-radius: var(--rounded);
    position: relative;
    display: flex;
    flex-direction: column;
    /*align-items: center;*/
    justify-content: center;
    text-align: center;
    transition: transform 0.2s;
    box-shadow: var(--shadow);
    max-width: 100%;
}

/* daisyUI's card with the system's surface and shadow. Inside utilities.app, so it
   ranks above daisyUI's own rules but a Tailwind utility on the card still wins
   (ADR 0048): `bg-*` and `p-*` on a card or its body apply. */
@layer utilities.app {
    .card {
        background: var(--surface);
        box-shadow: var(--shadow);
    }

    /* daisyUI's 1.5rem card-body padding is the biggest viewport-eater on mobile. */
    @media (max-width: 900px) {
        .card > .card-body {
            padding: 16px;
        }
    }

    /* Phones: tighter bodies, and table bodies nearly edge-to-edge so the table
       gets the width instead of the gutters. */
    @media (max-width: 640px) {
        .card > .card-body {
            padding: 12px;
        }

        .card > .card-body:has(.table) {
            padding-left: 8px;
            padding-right: 8px;
        }
    }
}

.kpi {
    font-size: 20px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.kpi-small {
    color: var(--muted);
    font-size: 13px
}

/* ── Design-system patterns (docs/design-system.html §05) ──────────────────
   Layered per ADR 0048: above daisyUI, below Tailwind utilities. Hairlines use
   --color-base-300 because daisyUI owns --border as a width. */
@layer utilities.app {
    /* Section card: one card type for every titled block on a page. */
    .section {
        background: var(--surface);
        border: 1px solid var(--color-base-300);
        border-radius: var(--rounded);
        box-shadow: var(--shadow-sm);
    }

    /* Wraps, so actions that don't fit drop beneath the title as one row instead
       of stacking button over button (ADR 0049). The 8px column gap matches the
       buttons' own; it only matters where title and actions barely fit. */
    .section-head {
        padding: 16px 18px;
        display: flex;
        flex-wrap: wrap;
        align-items: flex-start;
        justify-content: space-between;
        gap: 12px 8px;
    }

    .section-meta {
        font-size: 13px;
        color: var(--muted);
        margin-top: 2px;
    }

    /* Record list (§05): one row per central record. The facts sit in fixed-width
       columns so they line up across rows, and drop under the text when narrow. */
    .record-list {
        list-style: none;
        margin: 0;
        padding: 0;
        container-type: inline-size;
    }

    .record {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto auto;
        align-items: center;
        column-gap: 16px;
        row-gap: 10px;
        padding: 14px 18px;
        border-top: 1px solid var(--color-base-300);
    }

    .record-name {
        font-size: 16px;
        font-weight: 600;
        line-height: 1.25;
    }

    .record-sub {
        font-size: 13px;
        color: var(--muted);
        margin-top: 2px;
    }

    .record .badges {
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
        margin-top: 6px;
    }

    .record-desc {
        font-size: 13px;
        color: var(--muted);
        margin-top: 6px;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }

    .record-facts {
        display: grid;
        grid-auto-flow: column;
        grid-auto-columns: 96px;
    }

    .record-facts .k {
        font-size: 12px;
        color: var(--muted);
    }

    .record-facts .v {
        font-family: var(--mono);
        font-size: 14px;
        font-variant-numeric: tabular-nums;
        margin-top: 2px;
    }

    .record-facts .v.bad {
        color: var(--color-error);
    }

    /* Lifecycle track (§05): a segment per step on a batch's route, equal widths; only the
       current step fills partway (--fill). Release is a tick between steps. */
    .track {
        display: flex;
        align-items: center;
        gap: 3px;
        margin-top: 8px;
        max-width: 320px;
    }

    .track .seg {
        flex: 1 1 0;
        height: 6px;
        border-radius: 999px;
        background: var(--color-base-300);
        overflow: hidden;
    }

    .track .seg::before {
        content: "";
        display: block;
        height: 100%;
        width: 0;
        background: var(--copper);
        border-radius: 999px;
    }

    .track .seg.done::before {
        width: 100%;
    }

    .track .seg.now::before {
        width: var(--fill, 0%);
    }

    .track .seg.over::before {
        width: 100%;
        background: var(--color-warning);
    }

    /* The current step with nothing to measure: hatched, never a guessed fraction. */
    .track .seg.open {
        background: repeating-linear-gradient(135deg, var(--copper-light) 0 4px, var(--color-base-300) 4px 8px);
    }

    .track .tick {
        flex: 0 0 2px;
        height: 12px;
        border-radius: 2px;
        background: var(--ink);
    }

    .track .tick.expected {
        background: transparent;
        outline: 1px dashed var(--muted);
        outline-offset: -1px;
    }

    .track-labels {
        display: flex;
        gap: 3px;
        max-width: 320px;
        margin-top: 4px;
        font-size: 12px;
        color: var(--muted);
    }

    .track-labels span {
        flex: 1 1 0;
        min-width: 0;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* The current step's label never truncates; on a narrow head the others give way. */
    .track-labels .now {
        flex: 0 0 auto;
        max-width: 100%;
        color: var(--ink);
        font-weight: 600;
    }

    .track-labels .tick-gap {
        flex: 0 0 2px;
    }

    .track.lg,
    .track-labels.lg {
        max-width: 520px;
    }

    .track.lg .seg {
        height: 8px;
    }

    .record-actions {
        display: flex;
        gap: 2px;
    }

    @container (max-width: 640px) {
        .record {
            grid-template-columns: auto minmax(0, 1fr) auto;
        }

        .record-facts {
            grid-column: 2 / -1;
            grid-row: 2;
            grid-auto-columns: minmax(0, 96px);
        }
    }

    /* Filter chips (§05): toggles that include a kind of row; aria-pressed marks the
       state, and the ✓ keeps it from resting on colour alone. */
    .filters {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px;
        margin-top: 8px;
    }

    .filters .k {
        font-size: 13px;
        color: var(--muted);
        margin-right: 2px;
    }

    .filter-chip {
        font: inherit;
        font-size: 13px;
        padding: 3px 11px;
        border: 1px solid var(--color-base-300);
        border-radius: 999px;
        background: var(--surface);
        color: var(--muted);
        cursor: pointer;
    }

    .filter-chip:hover {
        border-color: var(--copper);
        color: var(--content);
    }

    .filter-chip[aria-pressed="true"] {
        background: var(--copper-soft);
        border-color: var(--copper);
        color: var(--content);
    }

    .filter-chip[aria-pressed="true"]::before {
        content: "✓";
        margin-right: 5px;
        color: var(--copper-ink);
    }

    .filter-chip:focus-visible {
        outline: 2px solid var(--copper-ink);
        outline-offset: 1px;
    }

    /* Phones: a row of chips scrolls sideways on one line, as the tab strip does,
       rather than stacking lines above the list. The head's text column may then
       shrink to the head's width, which is what lets the row scroll. */
    @media (max-width: 640px) {
        .section-head > :first-child {
            min-width: 0;
            max-width: 100%;
        }

        .filters {
            flex-wrap: nowrap;
            overflow-x: auto;
            scrollbar-width: none;
        }

        .filters::-webkit-scrollbar {
            display: none;
        }

        .filters > * {
            flex: none;
        }
    }

    /* Period stepper (§05): ‹ month › in a section head, for a register read one
       period at a time. An arrow with nowhere to go carries aria-disabled. */
    .period {
        display: inline-flex;
        align-items: center;
        gap: 2px;
    }

    .period .step {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 30px;
        height: 30px;
        border-radius: 7px;
        font-size: 18px;
        line-height: 1;
    }

    .period .step:hover {
        background: var(--copper-soft);
    }

    .period .step[aria-disabled="true"] {
        color: var(--color-base-300);
        pointer-events: none;
    }

    .period select {
        padding: 5px 8px;
        font: inherit;
        font-size: 13px;
        color: var(--color-base-content);
        background: var(--surface);
        border: 1px solid var(--color-base-300);
        border-radius: 7px;
    }

    /* Phones: the stepper takes the head's action row to itself, the menu between
       the arrows widened to fill it; the labelled actions follow on the next row. */
    @media (max-width: 640px) {
        .section-actions .period {
            flex: 1 1 100%;
        }

        .period select {
            flex: 1;
        }
    }

    /* Register search (§05): the field a lookup register is for, in its section head. */
    .register-search {
        display: flex;
        align-items: center;
        gap: 8px;
        width: min(100%, 26rem);
        margin-top: 10px;
        padding: 7px 12px;
        border: 1px solid var(--color-base-300);
        border-radius: var(--rounded);
        background: var(--surface);
        cursor: text;
    }

    .register-search:focus-within {
        border-color: var(--copper);
    }

    .register-search svg {
        width: 15px;
        height: 15px;
        flex: none;
        color: var(--muted);
    }

    .register-search input {
        flex: 1;
        min-width: 0;
        padding: 0;
        border: 0;
        outline: 0;
        background: none;
        font: inherit;
        font-size: 14px;
        color: var(--color-base-content);
    }

    /* Scope chip (§05): narrows a register to one record; its ✕ widens it again. */
    .scope-chip {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 3px 4px 3px 10px;
        font-size: 13px;
        border: 1px solid var(--color-base-300);
        border-radius: 999px;
        background: var(--surface);
    }

    .scope-chip .k {
        color: var(--muted);
    }

    .scope-chip .x {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 20px;
        height: 20px;
        border-radius: 999px;
        font-size: 12px;
    }

    .scope-chip .x:hover {
        background: var(--copper-soft);
    }

    /* `margin-left: auto` keeps the group right-aligned once it wraps to its own row. */
    .section-actions {
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-end;
        gap: 8px;
        margin-left: auto;
    }

    /* Labelled section actions get the hairline the header actions use. */
    .section-actions .btn-ghost:not(.btn-circle, .btn-square) {
        border-color: var(--color-base-300);
        background-color: var(--surface);
    }

    .section-actions .btn-ghost:not(.btn-circle, .btn-square):hover,
    .section-actions .btn-ghost[aria-expanded="true"] {
        background-color: var(--copper-soft);
        border-color: var(--copper);
    }

    .section-body {
        padding: 0 18px 18px;
    }

    /* A form or summary closing the section, below a hairline. */
    .section-foot {
        border-top: 1px solid var(--color-base-300);
        padding-top: 16px;
    }

    /* Splits a section body: the Label type spec (12px, 600, uppercase, muted). */
    .subhead {
        font-size: 12px;
        font-weight: 600;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--muted);
        padding: 14px 18px 6px;
    }

    /* Key/value rows: one hairline per row, the accent bar on one headline figure. */
    .rows .row {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: 8px;
        padding: 11px 18px;
        border-top: 1px solid var(--color-base-300);
        font-size: 14px;
    }

    .rows .lbl {
        color: var(--muted);
    }

    .rows .val {
        font-family: var(--mono);
        font-variant-numeric: tabular-nums;
        text-align: right;
        min-width: 0;
    }

    .rows .row.accent {
        border-left: 3px solid var(--copper);
        padding-left: 15px;
    }

    .rows:last-child .row:last-child {
        border-bottom-left-radius: calc(var(--rounded) - 1px);
    }

    /* Data table: full-bleed in a section; sans text, mono for figures and codes. */
    .tbl-wrap {
        overflow-x: auto;
        border-top: 1px solid var(--color-base-300);
        container-type: inline-size;
    }

    table.data {
        width: 100%;
        border-collapse: collapse;
        font-size: 14px;
    }

    table.data :where(th, td) {
        text-align: left;
        padding: 9px 14px;
        border-bottom: 1px solid var(--color-base-300);
        vertical-align: top;
    }

    table.data :where(th, td):first-child {
        padding-left: 18px;
    }

    table.data :where(th, td):last-child {
        padding-right: 18px;
    }

    table.data th {
        font-size: 12px;
        font-weight: 600;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--muted);
        background: var(--bg);
        white-space: nowrap;
        position: sticky;
        top: 0;
        z-index: 1;
    }

    /* An inline figure outside a num cell: mono with tabular numerals. */
    .num {
        font-family: var(--mono);
        font-variant-numeric: tabular-nums;
    }

    .num:not(td, th) {
        white-space: nowrap;
    }

    table.data :where(td.num, td.code) {
        font-family: var(--mono);
        font-size: 13px;
        font-variant-numeric: tabular-nums;
    }

    table.data :where(td.num, th.num) {
        text-align: right;
    }

    table.data td.actions {
        text-align: right;
        white-space: nowrap;
        width: 1%;
        padding-top: 5px;
        padding-bottom: 5px;
    }

    /* Several row actions sit in a row, and stack when the table is narrow, as the
       order page's do, so the last one is not pushed past the table's edge. */
    .row-actions {
        display: flex;
        justify-content: flex-end;
        gap: 2px;
    }

    @container (max-width: 820px) {
        .row-actions {
            flex-direction: column;
            align-items: flex-end;
            gap: 4px;
        }
    }

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

    /* Totals (§05): one row per unit for quantities, one row for litres and euro. */
    table.data tfoot td {
        font-weight: 600;
        background: var(--bg);
        border-top: 1px solid var(--color-base-300);
        border-bottom: none;
    }

    table.data tr.empty td {
        text-align: center;
        color: var(--muted);
        padding-top: 18px;
        padding-bottom: 18px;
    }

    @media (max-width: 900px) {
        table.data :where(th, td) {
            padding: 7px 10px;
        }

        table.data {
            font-size: 13px;
        }
    }

    /* A table in a narrow section (a third of the page) tightens its inner padding;
       the outer 18px edges stay aligned with the section head. */
    @container (max-width: 480px) {
        table.data :where(th, td) {
            padding: 7px 8px;
        }
    }

    /* A muted second line in a text cell: a code or count qualifying the first. */
    table.data .cell-sub {
        font-family: var(--default-font-family);
        font-size: 12px;
        color: var(--muted);
        margin-top: 2px;
    }

    /* Direction (§05): which way stock moved, as the ledger's in/out tones on the
       type cell and the signed figure alike. Not a state, so no badge. */
    table.data .flow {
        white-space: nowrap;
    }

    table.data .flow.in {
        color: var(--color-success);
    }

    table.data .flow.out {
        color: var(--color-error);
    }

    /* Reference chips (§05): a ledger row's links to what caused it, one tone per
       kind of record. The text colour is set unlayered below (A30). */
    .ref-chips {
        display: flex;
        flex-wrap: wrap;
        gap: 4px;
        margin-top: 4px;
    }

    .ref-chip {
        display: inline-flex;
        align-items: center;
        gap: 4px;
        padding: 1px 8px;
        border-radius: 999px;
        font-family: var(--default-font-family);
        font-size: 12px;
        line-height: 18px;
        white-space: nowrap;
        background: color-mix(in oklab, var(--chip) 12%, var(--surface));
    }

    .ref-chip:hover {
        background: color-mix(in oklab, var(--chip) 22%, var(--surface));
    }

    .ref-chip svg {
        width: 12px;
        height: 12px;
        flex: none;
    }

    .ref-chip.batch {
        --chip: var(--color-info);
    }

    .ref-chip.transfer {
        --chip: var(--ink);
    }

    .ref-chip.order {
        --chip: var(--copper-ink);
    }

    /* Grouped rows (§05): a tbody per group, whose member rows show while the
       head's toggle is expanded. Script flips aria-expanded; the style keys off it. */
    .group-toggle {
        display: inline-flex;
        align-items: baseline;
        gap: 6px;
        padding: 0;
        border: 0;
        background: none;
        color: inherit;
        font: inherit;
        text-align: left;
        cursor: pointer;
    }

    .group-toggle::before {
        content: "›";
        display: inline-block;
        width: 14px;
        color: var(--muted);
        transition: transform 0.15s;
    }

    .group-toggle[aria-expanded="true"]::before {
        transform: rotate(90deg);
    }

    .group-toggle:hover {
        color: var(--copper-ink);
    }

    table.data tr.group-head .cell-sub {
        padding-left: 20px;
    }

    table.data tbody.group:has(.group-toggle[aria-expanded="false"]) tr.member {
        display: none;
    }

    table.data tr.member td {
        background: var(--bg);
        font-size: 13px;
    }

    table.data tr.member td:first-child {
        padding-left: 38px;
    }

    /* Line forms (§05 Section card): a formset's repeating lines in a section,
       one hairline each, like rows. */
    .line-form {
        padding: 12px 18px 10px;
        border-top: 1px solid var(--color-base-300);
    }

    /* A line's head numbers it and carries its delete, so each line reads as one.
       A hidden (removed) line doesn't count, so the numbers stay consecutive. */
    .line-forms {
        counter-reset: line-form;
    }

    .line-forms > .line-form {
        counter-increment: line-form;
    }

    /* The head is a full-bleed band on the page ground, as a table header is, so a
       tall line's start stands out without striping (§05 forbids zebra). */
    .line-form-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin: -12px -18px 10px;
        padding: 4px 18px;
        background: var(--bg);
        border-bottom: 1px solid var(--color-base-300);
    }

    .line-form-no {
        font-size: 12px;
        font-weight: 600;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--muted);
    }

    .line-form-no::after {
        content: " " counter(line-form);
    }

    /* Settings rows (§05): one setting per hairline, its name and help on the
       left and the field on the right; the field drops beneath when narrow. */
    .settings {
        container-type: inline-size;
    }

    .setting {
        display: grid;
        grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
        gap: 6px 24px;
        align-items: start;
        padding: 14px 18px;
        border-top: 1px solid var(--color-base-300);
    }

    /* The name lines up with the text inside its field. */
    .setting > :first-child {
        padding-top: 9px;
    }

    .setting-name {
        font-size: 14px;
        font-weight: 500;
    }

    .setting-help {
        font-size: 13px;
        color: var(--muted);
        margin-top: 2px;
    }

    .setting-field .input,
    .setting-field .select {
        width: 100%;
        max-width: 26rem;
    }

    .setting-state {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 4px 12px;
        font-size: 13px;
        color: var(--muted);
        margin-top: 6px;
    }

    .setting-state label {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        cursor: pointer;
    }

    .setting-error {
        font-size: 13px;
        color: var(--color-error);
        margin-top: 4px;
    }

    @container (max-width: 560px) {
        .setting {
            grid-template-columns: minmax(0, 1fr);
        }

        .setting > :first-child {
            padding-top: 0;
        }
    }

    /* A group's last row keeps its hairline when another group follows, and the
       table's last collapsed head drops it like any last row. */
    table.data tbody.group:not(:last-child) tr:last-child td {
        border-bottom: 1px solid var(--color-base-300);
    }

    table.data tbody.group:last-child tr.group-head:has(.group-toggle[aria-expanded="false"]) td {
        border-bottom: none;
    }

    /* Priority columns (§05): .opt cells drop in a narrow table, and a .fold
       repeats one dropped figure under a kept one. Keyed on the table's width. */
    table.data .fold {
        display: none;
    }

    @container (max-width: 560px) {
        table.data .opt {
            display: none;
        }

        table.data .fold {
            display: block;
            margin-top: 2px;
            font-size: 12px;
            color: var(--muted);
        }
    }

    /* Row delete: the delete icon in the error tone. The icon files bake copper
       into their strokes, so this paints the shape through a mask instead. */
    .row-delete::before {
        content: "";
        width: 16px;
        height: 16px;
        background-color: var(--color-error);
        -webkit-mask: url("img/delete-svgrepo-com.svg") center / contain no-repeat;
        mask: url("img/delete-svgrepo-com.svg") center / contain no-repeat;
    }

    /* The ignore toggle's eye is drawn inline, so it takes copper from currentColor. */
    .row-ignore {
        color: var(--copper);
    }

    .row-ignore svg {
        width: 16px;
        height: 16px;
    }

    /* An excluded row (§05 Data table): muted and struck through, never faded, so it still reads. */
    table.data tr.ignored td {
        color: var(--muted);
    }

    table.data tr.ignored td.num {
        text-decoration: line-through;
    }

    /* Inline edit (§05 Data table): the row's figures become right-aligned fields in place. */
    table.data tr.editing input {
        width: 5rem;
        font-family: var(--mono);
        font-variant-numeric: tabular-nums;
        text-align: right;
    }

    /* A code (a permit) reads left to right like its column, and needs the room. */
    table.data tr.editing td.code input {
        width: 8.5rem;
        text-align: left;
    }

    /* Empty states: a title, one terse line, a button only when immediately usable. */
    .empty {
        padding: 20px 18px 22px;
        text-align: center;
    }

    .empty-title {
        font-weight: 600;
        font-size: 15px;
    }

    .empty-line {
        color: var(--muted);
        font-size: 14px;
        margin-top: 2px;
    }

    .empty .btn {
        margin-top: 12px;
    }

    /* The whole view has nothing yet: a frame on the page ground, the title as the page's serif headline. */
    .empty.page {
        background: var(--bg);
        border: 1px solid var(--color-base-300);
        border-radius: var(--rounded);
        padding: 40px 24px;
    }

    .empty.page .empty-title {
        font-family: var(--serif);
        font-weight: 400;
        font-size: 30px;
    }

    .nil {
        color: var(--muted);
    }

    /* Status badges: soft tint ground, text in the full tone (12% like --copper-soft). */
    .badge-soft {
        background-color: color-mix(in oklab, var(--badge-color, var(--color-base-content)) 12%, var(--color-base-100));
        border-color: transparent;
    }

    .badge-soft.badge-primary {
        color: var(--copper-ink);
    }

    .badge-soft.badge-neutral {
        color: var(--muted);
    }

    /* Initials avatar (§04): ink ground, copper letters. daisyUI owns .avatar. */
    .initials {
        width: 44px;
        height: 44px;
        flex: 0 0 auto;
        border-radius: 999px;
        background: var(--ink);
        color: var(--copper);
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-weight: 600;
        font-size: 16px;
        overflow: hidden;
    }

    .initials.lg {
        width: 60px;
        height: 60px;
        font-size: 22px;
    }

    .initials img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }

    /* A glyph avatar: the icon file (--glyph) is a mask painted copper, since the files
       come in black and copper and black vanishes on the ink ground. */
    .initials.glyph-avatar::before {
        content: "";
        width: 22px;
        height: 22px;
        background-color: var(--copper);
        -webkit-mask: var(--glyph) center / contain no-repeat;
        mask: var(--glyph) center / contain no-repeat;
    }

    .initials.lg.glyph-avatar::before {
        width: 30px;
        height: 30px;
    }

    /* Page head (§05): the code beside the title, the identity line, the badges. */
    .header-card .code {
        font-family: var(--mono);
        font-size: 18px;
        color: var(--muted);
    }

    .header-card .sub {
        font-size: 14px;
        color: var(--muted);
        margin-top: 2px;
    }

    .header-card .badges {
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
        margin-top: 8px;
    }

    /* The page's Actions menu (ADR 0049): a popover, so it renders in the top
       layer above the sticky head and the drawer. Items keep daisyUI's .menu.
       The look also serves a section's daisyUI dropdown (a [popover]-less menu),
       so the popover-only rules below are scoped to [popover]. */
    .actions-menu {
        width: 16rem;
        padding: 8px;
        color: var(--color-base-content);
        background: var(--surface);
        border: 1px solid var(--color-base-300);
        border-radius: var(--rounded);
        box-shadow: var(--shadow);
    }

    /* The browser hides a closed popover with a UA rule, which daisyUI's
       `.menu { display: flex }` overrides like any author rule; restate it. */
    .actions-menu[popover]:not(:popover-open) {
        display: none;
    }

    /* Desktop: a dropdown hung off the head's trigger. Without anchor positioning
       app.js places it. */
    @supports (anchor-name: --a) {
        .page-actions-trigger {
            anchor-name: --page-actions;
        }

        .actions-menu[popover] {
            position-anchor: --page-actions;
            inset: auto;
            top: anchor(bottom);
            right: anchor(right);
            margin: 6px 0 0;
            position-try-fallbacks: flip-block;
        }
    }

    /* Combobox (§05): a field that picks one record from many by typing. Its options
       are a popover hung off the field, so a dialog's scroll box can't clip them. */
    .combobox-options {
        max-height: 20rem;
        overflow-y: auto;
        margin: 4px 0 0;
        padding: 6px;
        color: var(--color-base-content);
        background: var(--surface);
        border: 1px solid var(--color-base-300);
        border-radius: var(--rounded);
        box-shadow: var(--shadow);
    }

    /* Under the field, as wide as it. Without anchor positioning app.js places it. */
    @supports (anchor-name: --a) {
        .combobox-options[popover] {
            position-anchor: --combobox;
            inset: auto;
            top: anchor(bottom);
            left: anchor(left);
            width: anchor-size(width);
            position-try-fallbacks: flip-block;
        }
    }

    .combobox-options [role="option"] {
        padding: 7px 10px;
        border-radius: 6px;
        cursor: pointer;
    }

    .combobox-options [role="option"]:hover,
    .combobox-options [role="option"][aria-selected="true"] {
        background: var(--copper-soft);
    }

    .combobox-options .option-name {
        font-size: 14px;
        font-weight: 500;
    }

    .combobox-options .option-sub {
        font-size: 12px;
        color: var(--muted);
    }

    /* What the field's rule left out, counted once at the foot (§05): never an option. */
    .combobox-note {
        padding: 8px 10px 4px;
        font-size: 13px;
        color: var(--muted);
    }

    [role="listbox"] + .combobox-note {
        margin-top: 6px;
        border-top: 1px solid var(--color-base-300);
    }

    /* Below 900px the head's trigger hides and the menu is moved into the
       drawer (mobile_nav.html), where it is a panel like .nav-items: in flow,
       collapsed, and opened by #navigator.acting as ≡ opens .expanded. It is
       never shown as a popover there, so the UA's [popover] box (fixed, inset 0,
       margin auto) and the closed-state rule above are reset. */
    /* The head's primary is repeated as the menu's first item, shown only in the
       drawer: below 900px it leaves the head and sits nearest the thumb. */
    @media (min-width: 901px) {
        .actions-menu > .page-primary-item {
            display: none;
        }
    }

    @media (max-width: 900px) {
        .page-actions-trigger,
        .header-actions > .page-primary {
            display: none;
        }

        /* Rendered bottom-up: a menu reads outward from its trigger, so the
           destructive item ends farthest from the thumb (ADR 0049). */
        #navigator .actions-menu {
            position: static;
            display: flex;
            flex-flow: column-reverse nowrap;
            width: auto;
            height: auto;
            max-height: 0;
            margin: 0;
            padding: 0;
            overflow: hidden;
            font-size: 16px;
            background: var(--color-base-200);
            border: 0;
            border-radius: 20px;
            box-shadow: none;
            transition: max-height 0.4s ease;
        }

        #navigator.acting .actions-menu {
            max-height: min(500px, 60dvh);
            overflow-y: auto;
        }

        #navigator .actions-menu > li > a {
            gap: 14px;
            padding: 14px 22px;
            border-radius: 0;
            border-top: 1px solid var(--color-base-300);
        }

        /* A group break: a gap between the rows' hairlines rather than a rule. */
        #navigator .actions-menu > li:empty {
            height: 8px;
            margin: 0;
            background: none;
        }

        /* ⋯ turns into ✕ while the actions are open, as ≡ does. */
        .drawer-do .icon-close,
        #navigator.acting .drawer-do .icon-more {
            display: none;
        }

        #navigator.acting .drawer-do .icon-close {
            display: block;
        }
    }

    /* The drawer's "find" zone: the location, styled as a field so it reads as
       tappable. Opens the omnisearch sheet (search_bar.html). */
    .drawer-find {
        display: inline-flex;
        align-items: center;
        gap: 7px;
        max-width: 100%;
        padding: 7px 14px;
        border: 1px solid var(--color-base-300);
        border-radius: 999px;
        background: var(--bg);
        color: var(--color-base-content);
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
    }

    .drawer-find svg {
        flex-shrink: 0;
        color: var(--muted);
    }

    /* The drawer's "do" slot shows only on pages that have an Actions menu. */
    body:not(:has(#page-actions)) .drawer-do {
        display: none;
    }

    /* KPI strip: figures in tabular numerals, colour only from the semantic set.
       .stat is scoped under .stat-row because daisyUI ships a global .stat. */
    .stat-row {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
        gap: 1px;
        background: var(--color-base-300);
        border: 1px solid var(--color-base-300);
        border-radius: var(--rounded);
        overflow: hidden;
    }

    /* The gap is the hairline; daisyUI's global .stat adds a dashed divider of its own. */
    .stat-row .stat {
        background: var(--surface);
        padding: 16px 18px;
        border: 0;
    }

    .stat-row .stat .k {
        font-size: 12px;
        font-weight: 600;
        letter-spacing: 0.05em;
        text-transform: uppercase;
        color: var(--muted);
    }

    .stat-row .stat .v {
        font-size: 27px;
        font-weight: 600;
        font-variant-numeric: tabular-nums;
        margin-top: 6px;
        letter-spacing: -0.01em;
    }

    /* Phones: two figures a row rather than one, so the strip doesn't fill the
       first screen. An odd last figure spans the row; the gap shows the hairline
       colour, so an empty cell would read as a grey block. */
    @media (max-width: 640px) {
        .stat-row {
            grid-template-columns: repeat(2, minmax(0, 1fr));
        }

        .stat-row .stat {
            padding: 12px 14px;
        }

        .stat-row .stat .v {
            font-size: 22px;
        }

        .stat-row .stat:last-child:nth-child(odd) {
            grid-column: 1 / -1;
        }
    }

    .stat-row .stat .v small {
        font-size: 14px;
        font-weight: 500;
        color: var(--muted);
    }

    .stat-row .stat .d {
        font-size: 13px;
        color: var(--muted);
        margin-top: 4px;
    }

    /* Scoped figure (§05, ADR 0053): the cell states its scope — what it includes —
       and proves its own completeness. Layers carry the state badges; the gaps
       toggle opens the input list beneath the strip. */
    .stat-row .stat.figure .layers {
        display: flex;
        flex-wrap: wrap;
        gap: 4px 12px;
        margin-top: 8px;
    }

    .stat-row .stat.figure .layer {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        font-size: 12px;
        color: var(--muted);
    }

    .stat-row .stat.figure .gaps {
        font: inherit;
        font-size: 12px;
        font-weight: 600;
        color: var(--color-warning);
        display: inline-flex;
        align-items: center;
        gap: 5px;
        margin-top: 8px;
        padding: 0;
        border: 0;
        background: none;
        cursor: pointer;
    }

    .stat-row .stat.figure .gaps.bad {
        color: var(--color-error);
    }

    .stat-row .stat.figure .gaps::after {
        content: "›";
        display: inline-block;
        transition: transform 0.15s;
    }

    .stat-row .stat.figure .gaps[aria-expanded="true"]::after {
        transform: rotate(90deg);
    }

    .stat-row .stat.figure .d .badge {
        margin-left: 4px;
    }

    /* The completeness half: one row per incomplete input — a link to the record
       the fix acts on, its state badge, a muted reason. */
    .figure-inputs {
        background: var(--surface);
        border: 1px solid var(--color-base-300);
        border-top: 0;
        border-radius: 0 0 var(--rounded) var(--rounded);
    }

    .figure-input {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        gap: 6px 12px;
        padding: 10px 18px;
        border-top: 1px solid var(--color-base-300);
        font-size: 14px;
    }

    .figure-input:first-child {
        border-top: 0;
    }

    .figure-input .name {
        color: var(--copper-ink);
        font-weight: 500;
        text-decoration: none;
    }

    .figure-input .name:hover {
        text-decoration: underline;
    }

    .figure-input .why {
        color: var(--muted);
        font-size: 13px;
        margin-left: auto;
    }

    @media (max-width: 640px) {
        .figure-input .why {
            flex-basis: 100%;
            margin-left: 0;
        }
    }
}

/* Unlayered on purpose: the global `h2 { font-size }` rule below is unlayered and
   would beat a layered class. Moves into the layer once the element rules do. */
.section-title {
    font-size: 22px;
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.2;
}

/* Tabs (§05): links under the page head, the active one marked by aria-current.
   Unlayered for the same reason, against the global `nav a` rules, whose hover
   and press effects it also resets. */
.page-tabs {
    display: flex;
    gap: 2px;
    border-bottom: 1px solid var(--color-base-300);
    overflow-x: auto;
    scrollbar-width: none;
}

.page-tabs::-webkit-scrollbar {
    display: none;
}

.page-tabs a {
    display: block;
    flex: 0 0 auto;
    padding: 10px 14px 9px;
    margin-bottom: -1px;
    border-radius: 0;
    border-bottom: 2px solid transparent;
    font-size: 15px;
    font-weight: 500;
    white-space: nowrap;
    color: var(--muted);
}

.page-tabs a:hover,
.page-tabs a:active {
    color: var(--content);
    background: none;
    transform: none;
}

.page-tabs a[aria-current="page"] {
    color: var(--content);
    border-bottom-color: var(--copper);
}

.page-tabs a:focus-visible {
    outline: 2px solid var(--copper-ink);
    outline-offset: -2px;
    border-radius: 6px;
}

.page-tabs .count {
    font-family: var(--mono);
    font-size: 12px;
    color: var(--muted);
    margin-left: 6px;
}

/* A record's name reads as a title, in ink. Unlayered against the global
   `a:not(.btn)` copper, like .page-tabs (backlog A30). */
.record-name a {
    color: var(--content);
}

/* The scope chip's ✕ is a link to the wider register; muted, not copper (A30). */
.scope-chip a.x {
    color: var(--muted);
}

.scope-chip a.x:hover {
    color: var(--content);
}

.period a.step {
    color: var(--content);
}

a.ref-chip {
    color: var(--chip);
}

.record-name a:hover {
    color: var(--copper-ink);
    text-decoration: underline;
}

.table {
    font-family: "JetBrains Mono", monospace;
}

/* Tables go mono above, but badges inherit it — so a chip inside a table
   renders in JetBrains Mono while the same chip outside renders in Inter
   Tight. Pin chips back to the UI font wherever they sit. */
.table .badge,
.table [class*="badge-"] {
    font-family: var(--default-font-family);
}

/* DaisyUI badges fix their height but leave `white-space: normal`, so longer
   chip labels (e.g. "Wholesale #1") wrap onto a second line and spill out of
   the fixed-height pill. Keep chips on a single line and let them size to their
   content — wide chips are fine now that tables scroll horizontally. The
   attribute selector out-specifies DaisyUI's own `.badge` rule, which loads
   after app.css. */
.badge,
[class*="badge-"] {
    white-space: nowrap;
}

/*Generic mobile treatment for every DaisyUI `.table`, replacing the ad-hoc
per-template `is_mobile` `table-xs` / `overflow-x-auto` wrappers.

`display: block` turns the <table> itself into a horizontal scroll container,
so a wide table scrolls within its own box instead of pushing the whole page
sideways. The rows still form an anonymous table internally, so columns stay
aligned; a narrow table just fills the width and doesn't scroll. Cells are
compacted so more columns fit before scrolling is needed. */
@media (max-width: 900px) {
    .legacy-card {
        padding: 12px;
    }

    .table {
        display: block;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }

    .table :where(th, td) {
        padding: 0.4rem 0.6rem;
        font-size: 0.78rem;
    }
}

/* On phones the side padding of a table card wastes a big fraction of a ~375px
   viewport, so let the table stretch close to the card edges. */
@media (max-width: 640px) {
    /* The page gutter is 20px each side (40px total); halve it on phones to
       give content back a meaningful slice of a ~375px viewport. */
    .container {
        padding: 12px;
    }

    .legacy-card:has(.table) {
        padding-left: 8px;
        padding-right: 8px;
    }
}

.subrow {
    background-color: var(--surface);
    /* visibility: hidden; */
    display: none;
    margin-left: 10px;
}

.subrow td {
    font-size: 12px;
    padding: 5px;
}

.subsubrow {
    background-color: var(--color-base-300);
    /* visibility: hidden; */
    display: none;
}

.subsubrow td {
    padding: 0;
}

.grid-2 {
    width: 95%;
    font-family: "JetBrains Mono", "Courier New", Courier, monospace;
    font-size: 15px;
    display: grid;
    grid-template-columns: 35% 65%; /* two equal columns */
    gap: 8px; /* spacing between cells */
    align-items: stretch; /* make cells same height in a row */
    margin-bottom: 16px;

    div {
        text-align: left;
        border-bottom: 2px dotted var(--bg-2);
        vertical-align: center;
    }

}

.grid-2 > div:nth-child(odd) {
    font-weight: bold;
}

.grid-2 h1 {
    text-align: center;
    width: 100%;

}

/* styling for demo */

.data-table {
    font-family: "JetBrains Mono", "Courier New", Courier, monospace;

    text-align: left;
    vertical-align: bottom;
    font-size: 13px;
    width: 90%;
    padding: 8px;
    margin: auto auto 40px;
    background-color: var(--surface);
}

.data-table th {
    border-bottom: 1px dotted var(--color-base-content);
}

.data-table td {
    vertical-align: bottom;
    border-bottom: 1px dotted var(--color-base-content);
}

.data-table .left {
    border-collapse: collapse;
    width: 20%;
    vertical-align: bottom;
}

.data-table .right {
    width: 70%;
    text-align: center;
}

/* Forms */
.form-row {
    display: flex;
    gap: 12px;
    flex-wrap: wrap
}

.form-label {
    font-weight: bold;
}

.form-error {
    color: var(--danger);
}

.input, .select, .textarea {
    border-radius: 8px;
    border: 1px solid var(--color-base-300);
    background: var(--color-base-100);
    color: var(--color-base-content);
    min-width: 0
}

/* Horizontal padding only. DaisyUI gives .input and .select a fixed `height`,
   and with border-box sizing vertical padding is subtracted from it rather than
   added to it: at `select-sm` (2rem) a 10px top/bottom pad leaves 10px of content
   box for 12px text. Chrome centres a <select>'s value text anyway, WebKit clips
   it — which is why this only ever showed up in Safari. */
.input {
    padding-block: 0;
    padding-inline: 10px;
}

/* A select also needs its inline-end kept clear of the chevron DaisyUI paints
   there (it reserves 1.75rem); a symmetric pad would let long option text run
   under the arrow. */
.select {
    padding-block: 0;
    padding-inline: 10px 28px;
}

/* A textarea has no fixed height, so it keeps real padding on both axes. */
.textarea {
    padding: 10px;
}


.btn {
    font-weight: 500; /* medium */
}

/* The templates use two button conventions: DaisyUI's own .btn-primary /
   .btn-secondary / .btn-ghost (driven by the --color-* tokens above) and a
   custom `class="btn primary"` / `btn ghost`. These bridge the custom modifiers
   onto DaisyUI's own --btn-* mechanism so both render identically, including
   hover/active states. */
.btn.primary {
    --btn-color: var(--color-primary);
    --btn-fg: var(--color-primary-content);
}

.btn.secondary {
    --btn-color: var(--color-secondary);
    --btn-fg: var(--color-secondary-content);
}

/* Ghost: quiet, borderless, copper-tint on hover. Written self-contained so it
   doesn't depend on DaisyUI's ghost internals. */
.btn.ghost {
    background-color: transparent;
    border-color: transparent;
    box-shadow: none;
    color: var(--color-base-content);
}

.btn.ghost:hover {
    background-color: var(--copper-soft);
}

/* Neutral .btn — the templates use a bare `class="btn"` widely. DaisyUI fills it
   with base-200, which in THIS palette is the page background, so those buttons
   read as flat grey boxes (Logout, "New order", filter chips). Give them the
   surface colour and a hairline so they look like real buttons. The :not() list
   keeps every colour variant and custom modifier out of the way. */
.btn:not(.btn-primary, .btn-secondary, .btn-accent, .btn-neutral, .btn-info,
         .btn-success, .btn-warning, .btn-error, .btn-ghost, .btn-link,
         .btn-outline, .btn-soft, .btn-dash, .btn-active,
         .primary, .secondary, .ghost, .warn) {
    background-color: var(--surface);
    border-color: var(--color-base-300);
    color: var(--color-base-content);
}

.btn:not(.btn-primary, .btn-secondary, .btn-accent, .btn-neutral, .btn-info,
         .btn-success, .btn-warning, .btn-error, .btn-ghost, .btn-link,
         .btn-outline, .btn-soft, .btn-dash, .btn-active,
         .primary, .secondary, .ghost, .warn):hover {
    background-color: var(--copper-soft);
    border-color: var(--copper);
}

/* Outline: copper border + deep-copper text. Status outlines (error, success…)
   keep their own colour. */
.btn-outline:not(.btn-primary, .btn-secondary, .btn-accent, .btn-neutral,
                 .btn-info, .btn-success, .btn-warning, .btn-error) {
    background-color: transparent;
    border-color: var(--copper);
    color: var(--copper-ink);
}

.btn-outline:not(.btn-primary, .btn-secondary, .btn-accent, .btn-neutral,
                 .btn-info, .btn-success, .btn-warning, .btn-error):hover {
    background-color: var(--copper-soft);
    border-color: var(--copper);
    color: var(--copper-ink);
}

/* A disabled button reads as disabled. The unlayered outline rules above beat
   daisyUI's layered disabled styles, which left a disabled button looking live. */
.btn:is(:disabled, [disabled], .btn-disabled) {
    background-color: transparent;
    border-color: var(--color-base-300);
    color: var(--muted);
    cursor: not-allowed;
}

/* Action rows mix btn-outline and btn-ghost, which reads as mismatched bordering
   when they sit side by side. Inside a section header, give the quiet variants
   the same hairline so the row is uniform. Icon-only buttons stay borderless. */
.header-actions .btn-ghost:not(.btn-circle, .btn-square) {
    border-color: var(--color-base-300);
    background-color: var(--surface);
}

.header-actions .btn-ghost:not(.btn-circle, .btn-square):hover {
    background-color: var(--copper-soft);
    border-color: var(--copper);
}

/* The icon files have copper baked in (stroke="#C28570"), and an <img> can't
   inherit currentColor — so on a filled copper/ink/status ground the icon
   disappears. brightness(0) invert(1) repaints any source color to white,
   matching the content color of every filled variant. Outline/ghost buttons and
   ghost badges sit on a light ground, so their icons stay copper as an accent. */
.btn-primary .icon,
.btn.primary .icon,
.btn-secondary .icon,
.btn.secondary .icon,
.btn-neutral .icon,
.btn-error .icon,
.btn.warn .icon,
.badge-primary .icon,
.badge-secondary .icon,
.badge-neutral .icon,
.badge-success .icon,
.badge-warning .icon,
.badge-error .icon,
.badge-info .icon {
    filter: brightness(0) invert(1);
}

/* Small utilities */
.muted {
    color: var(--muted)
}

.pill {
    display: inline-block;
    padding: 6px 10px;
    border-radius: 999px;
    background: rgba(6, 10, 15, 0.04);
    font-size: 13px
}

label {
    font-weight: 600;
    color: inherit;
}

.list {
    list-style: none;
    padding: 0;
    margin: 0;
}

.header-card {
    /* Slightly translucent so content shows through while it floats over the
       page as a sticky "glass" bar. Falls back to the opaque surface where
       backdrop-filter is unsupported. */
    background: color-mix(in srgb, var(--surface) 90%, transparent);
    backdrop-filter: blur(1px);
    -webkit-backdrop-filter: blur(14px);
    color: var(--accent);
    padding: 20px;
    border-radius: var(--rounded);
    box-shadow: var(--shadow);
    margin-bottom: 20px;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    max-width: 100%;
    position: sticky;
    top: 20px;
    z-index: 999;
}

/* Title block: allowed to shrink (min-width:0) so a long title never pushes the
   action group off the card; the actions wrap to their own line instead. */
.header-card > :first-child {
    flex: 1 1 auto;
    min-width: 0;
}

/* The page title: the design system's H1 (Instrument Serif 400, 34px), in ink
   rather than the header card's copper. */
.header-card h1 {
    color: var(--content);
    font-size: 34px;
    font-family: var(--serif);
    font-weight: 400;
    line-height: 1.1;
    letter-spacing: -0.01em;
}

.header-card h2 {
    font-size: 18px;
    font-weight: 300;
    color: var(--muted);
}

/* Action buttons that live on the right side of a sticky section header.
   `margin-left:auto` + `justify-content:flex-end` keep the group pinned to the
   right and right-aligned even after it wraps to its own line. `flex-wrap`
   lets >2 buttons wrap within the group instead of overflowing the card. */
.header-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    justify-content: flex-end;
    margin-left: auto;
}


/* Responsive */
/* Small screens (phones + tablet portrait): hide the desktop sidebar, show the
   fixed bottom drawer, and give content one full-width column. This single
   breakpoint is the source of truth for the shell — see app.html (both shells
   are always rendered). */
@media (max-width: 900px) {
    .layout {
        grid-template-columns: 1fr;
    }

    .sidebar {
        display: none;
    }

    #navigator {
        display: block;
    }

    /* Clear the fixed drawer (pill sits ~20px from the bottom, ~60px tall). */
    .main {
        padding-bottom: 90px;
    }

    /* The head's actions move to the drawer (ADR 0049). An emptied group would
       still wrap to its own line and add a gap, so drop it. Unlayered, as is the
       `.header-actions` rule it has to beat. */
    .header-actions:has(> .page-actions-trigger):not(:has(> :not(.page-actions-trigger, .page-primary))) {
        display: none;
    }

    /* Search is reached from the drawer's location label on small screens
       (ADR 0049), which opens this same bar as a full-screen sheet. The input
       sits at the top, clear of the keyboard. */
    .topbar-search {
        display: none;
    }

    .topbar-search.is-open {
        position: fixed;
        inset: 0;
        z-index: 10000;
        display: flex;
        flex-direction: column;
        max-width: none;
        padding: calc(16px + env(safe-area-inset-top)) 16px 16px;
        background: var(--bg);
    }

    .topbar-search.is-open .search {
        min-width: 0;
    }

    .topbar-search.is-open .search-cancel {
        display: inline-flex;
    }

    .topbar-search.is-open #search-results2 {
        position: static;
        flex: 1;
        min-height: 0;
        margin-top: 12px;
        overflow-y: auto;
    }

    body:has(.topbar-search.is-open) {
        overflow: hidden;
    }

    /* Compact header: trim the serif company name, drop the "Welcome" line, and
       shrink the avatar so the topbar doesn't eat vertical space on phones. */
    .topbar {
        margin-bottom: 10px;
    }

    .topbar h1 {
        font-size: 1.3rem;
    }

    .profile .muted {
        display: none;
    }

    .topbar .avatar {
        width: 32px;
        height: 32px;
        font-size: 0.85rem;
    }
}

/* Own additions */
h1 {
    font-family: var(--serif);
    font-size: 2rem;
    font-weight: bold;
}

h2 {
    font-size: 1.5rem;
}

.nav-icon {
    display: inline-block;
    max-width: 25px;
}

.logo {
    max-width: 80px;
    display: block;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(-6px)
    }
    to {
        opacity: 1;
        transform: translateY(0)
    }
}


.hero-section a {
    color: var(--copper-ink);
}


.kpi-img {
    float: right;
}

.glyph {
    position: absolute;
    top: -10px;
    right: -10px;
    max-width: 60px;
    background-color: var(--surface);
    border-radius: 20%;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    padding: 5px;
}

/* Flash messages */
.flash-container {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 1000;
    display: flex;
    flex-direction: column;
    gap: 10px;
    animation: slideIn 0.3s ease;
}

.flash-message {
    padding: 14px 18px;
    border-radius: var(--rounded);
    box-shadow: var(--shadow);
    font-weight: 500;
}

.flash-success {
    background: var(--success);
    color: white
}

.flash-error {
    background: var(--error);
    color: white
}

.flash-close {
    position: absolute;
    top: 10px;
    right: 10px;
    font-weight: bold;
    background: none;
    border: none;
    font-size: 20px;
    cursor: pointer;
    color: var(--bg)
}


@keyframes slideIn {
    from {
        opacity: 0;
        transform: translateY(-10px)
    }
    to {
        opacity: 1;
        transform: translateY(0)
    }
}

.hero-notice {
    border: 1px solid rgba(220, 38, 38, 0.3);
    border-radius: var(--rounded);
    padding: 18px 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20px;
    box-shadow: var(--shadow);

    img {
        max-width: 30px;
    }
}

.hero-notice .warn {
    color: var(--danger)
}

.hero-notice .info {
    color: var(--info)
}

/* Dropdown trigger — a <summary> styled as a button (`class="btn drop"`). It
   used to force `color: white` and `border: none`, which on this light palette
   rendered white text on a near-white ground (an invisible button). It now takes
   the neutral button treatment like any other .btn; only the cursor is special. */
.btn.drop {
    cursor: pointer;
}

.btn.warn {
    --btn-color: var(--color-error);
    --btn-fg: #ffffff;
    background-color: var(--danger);
    color: #ffffff;
}

.btn.mini {
    padding: 4px 8px;
}

.icon {
    /* Definite width, not max-width: these are <img> elements sourcing SVGs
       with no intrinsic dimensions. With only max-width, WebKit/Safari fails
       to resolve their size inside a flex button and nondeterministically
       inflates the icon (and its button) to many times its intended size.
       An explicit width gives the image a definite size and stops that. */
    width: 25px;
    height: auto;
    border: none;
    box-shadow: none;
}

.data-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 46px;
    padding: 6px;
}

.data-item .value {
    font-weight: 700;
    font-size: 0.95rem;
    color: var(--color-base-content);
}

.data-item .label {
    font-size: 0.75rem;
    color: var(--muted);
    opacity: 0.7;
    margin-top: 4px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

/* Scrollable tabs for mobile */
.tabs-scrollable {
    overflow-x: auto;
    overflow-y: hidden;
    white-space: nowrap;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none; /* Hide scrollbar for Firefox */
    -ms-overflow-style: none; /* Hide scrollbar for IE and Edge */
}

.tabs-scrollable::-webkit-scrollbar {
    display: none; /* Hide scrollbar for Chrome, Safari and Opera */
}

.tabs-scrollable .tabs {
    flex-wrap: nowrap;
    min-width: max-content;
}

/* DaisyUI overrides */
details > summary {
    list-style: none;
}

details > summary::-webkit-details-marker {
    display: none;
}

details > summary:hover {
    cursor: pointer;
}


.modal-box {
    max-width: 900px !important;
    background-color: var(--surface);
    /* Tall forms (e.g. the inventory transfer) must scroll within the dialog
       instead of overflowing off-screen. DaisyUI sets this by default, but the
       markup used to carry `overflow-visible`, which disabled it. `dvh` keeps
       the cap correct on mobile browsers whose viewport shrinks with the URL
       bar. Native <select> popups aren't clipped by this, so dropdowns are fine. */
    max-height: calc(100dvh - 4em);
    overflow-y: auto;
}

.fab.left {
    inset-inline-start: 10px !important;
    inset-inline-end: unset;
    text-align: left;
    margin-left: 0;
    display: flex;
    align-items: center;
    justify-content: center;

}

.fab {
    bottom: 10px;
}

/* Navigator container. Visible by default; hidden on large screens by the
   min-width rule below (kept separate so source order can't let a later base
   rule override the small-screen reveal). The sidebar is the inverse. */
#navigator {
    z-index: 9999;
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    width: 90%;

    background: color-mix(in srgb, var(--surface) 90%, transparent);
    backdrop-filter: blur(1px);
    border-radius: 20px;
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
    overflow: hidden;
    transition: height 0.4s ease;
}

/* Desktop / tablet landscape: the sidebar is the shell, so hide the drawer. */
@media (min-width: 901px) {
    #navigator {
        display: none;
    }
}

/* Navigation items (initially hidden) */
.nav-items {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.4s ease;
    background: var(--color-base-200);
    border-radius: 20px;
}

/* Left-aligned icon + label row, mirroring the desktop sidebar so the icons
   form a single column and every label starts at the same x. */
.nav-items a {
    display: flex;
    align-items: center;
    gap: 14px;
    text-decoration: none;
    color: var(--color-base-content);
    padding: 14px 22px;
    border-top: 1px solid var(--color-base-300);
    transition: background 0.2s ease;
}

.nav-items a .nav-icon {
    width: 22px;
    flex-shrink: 0;
}

.nav-items a:hover {
    background: var(--color-base-300);
}

.nav-items a.active {
    background: var(--gradient);
    color: var(--color-base-content);
    font-weight: 600;
}

/* Bottom row: three tracks with equal 1fr sides so the section title stays
   truly centred regardless of the side controls' widths. */
.bottom-row {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
}

#hamburger {
    justify-self: start;
}

.bottom-row-end {
    justify-self: end;
}

.hamburger {
    width: 22px;
    height: 16px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    pointer-events: none; /* let clicks fall through to the #hamburger button */
}

.hamburger span {
    display: block;
    height: 2.5px;
    width: 100%;
    background: var(--color-base-content);
    border-radius: 2px;
    transition: transform 0.3s ease, opacity 0.3s ease;
}

.right-button {
    background: var(--ink);
    color: var(--ink-content);
    padding: 6px 12px;
    border: none;
    border-radius: 12px;
    cursor: pointer;
    font-size: 14px;
}

/* Page-level progress bar — shows during any htmx request (boosted nav, partial
   updates, form submissions). JS in app.html drives width/opacity directly so
   the bar animates to ~90% during the request then snaps to 100% on settle. */
#page-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 3px;
    width: 0%;
    background: var(--accent, #c28570);
    z-index: 99999;
    pointer-events: none;
    opacity: 0;
    border-radius: 0 2px 2px 0;
    will-change: width, opacity;
}

/* Expanded state */
#navigator.expanded .nav-items {
    max-height: 500px; /* adjust as needed */
}

/* Hamburger morphs into an X when the drawer is open. */
#navigator.expanded .hamburger span:nth-child(1) {
    transform: translateY(6.5px) rotate(45deg);
}

#navigator.expanded .hamburger span:nth-child(2) {
    opacity: 0;
}

#navigator.expanded .hamburger span:nth-child(3) {
    transform: translateY(-6.5px) rotate(-45deg);
}

/* optional: stack to one column on small screens */
/* MOBILE */
@media (max-width: 600px) {
    .grid-2 {
        grid-template-columns: 1fr;
        font-size: 0.7rem;
    }

    .glyph {
        max-width: 40px;
    }

    .report-hero {
        h1 {
            font-size: 1.4rem;
        }

        h2 {

            font-size: 1.1rem;
        }
    }

    .batch_card {
        max-width: 100%;
    }

    /* Keep the title and action buttons on one row so the actions don't drop to
       a separate line and eat vertical space. Title block shrinks (min-width:0
       from the base rule) and actions wrap within their own group if needed.
       Tighter padding/margins/gap shrink the card's footprint on phones. */
    .header-card {
        padding: 12px 14px;
        margin-bottom: 12px;
        gap: 10px;
        top: 10px;
    }

    /* Header actions stay right-aligned even when the header stacks (see
       `.header-actions` margin-left:auto), so DaisyUI's `dropdown-end` already
       opens the menu leftward from the trigger and keeps it on-screen. Only cap
       the width so a very wide menu can't run off the left edge on a narrow
       phone. (An earlier version forced left-alignment here, which — once the
       triggers moved to the right — pushed menus off the RIGHT edge instead.) */
    .header-actions .dropdown-end .dropdown-content {
        max-width: calc(100vw - 2rem);
    }

    .legacy-card {
        align-items: unset;
    }

    .header-card {
        h1 {
            font-size: 1.4rem;
        }

        h2 {
            font-size: 0.8rem;
        }
    }
}

#hx-modal {
    h2 {
        font-size: 2.2rem;
        font-family: var(--serif);
    }

    input, textarea, select {
        background-color: var(--surface);
    }
}


/* Batch analysis page (batch/batch_timeline.html). Scoped with :where() so the
   rules keep their original specificity but no longer reach other pages. */
:where(.batch-analysis) {
  /* Wider than the system's 1040px wrap: the DAG needs the room. */
  .wrap {
    max-width: 1880px;
    margin: 0 auto;
    padding: 56px 24px 96px;
  }

  /* ── Masthead ────────────────────────────────────────── */
  .masthead .eyebrow {
    font-size: 12px; font-weight: 600; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--copper-ink); margin: 0 0 10px;
  }
  .masthead h1 {
    font-family: var(--serif); font-weight: 400;
    font-size: clamp(40px, 7vw, 68px); line-height: 1.02;
    margin: 0; text-wrap: balance; letter-spacing: -0.01em;
  }
  .masthead p {
    max-width: 60ch; color: var(--muted); margin: 14px 0 0; font-size: 16px;
  }

  /* ── Stat row ────────────────────────────────────────── */
  .stat-row {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1px;
    background: var(--color-base-300); border: 1px solid var(--color-base-300);
    border-radius: var(--rounded); overflow: hidden; margin-top: 32px;
  }
  .stat { background: var(--surface); padding: 16px 18px; }
  .stat .k { font-size: 12px; font-weight: 600; letter-spacing: 0.05em;
             text-transform: uppercase; color: var(--muted); }
  .stat .v { font-size: 24px; font-weight: 600; font-variant-numeric: tabular-nums;
             margin-top: 6px; letter-spacing: -0.01em; }
  .stat .v small { font-size: 13px; font-weight: 500; color: var(--muted); }

  /* ── Section scaffolding ─────────────────────────────── */
  section { margin-top: 64px; }
  .sec-head {
    display: flex; align-items: baseline; gap: 14px;
    border-bottom: 1px solid var(--color-base-300); padding-bottom: 12px; margin-bottom: 28px;
  }
  .sec-head h2 { font-family: var(--serif); font-weight: 400; font-size: 30px;
                 margin: 0; letter-spacing: -0.01em; }
  .sec-head .num { font-family: var(--mono); font-size: 13px; color: var(--copper-ink); font-weight: 500; }
  .sec-head .note { margin-left: auto; font-size: 13px; color: var(--muted); }

  .lede { color: var(--muted); font-size: 15px; max-width: 68ch; margin: -12px 0 24px; }

  /* ── Graph frame ─────────────────────────────────────── */
  .graph-frame {
    background: var(--surface);
    border: 1px solid var(--color-base-300);
    border-radius: var(--rounded);
    overflow-x: auto;
    box-shadow: var(--shadow);
  }
  .graph-frame svg { display: block; margin: 0 auto; }

  .stage-label {
    font-size: 11px; font-weight: 600;
    letter-spacing: 0.08em; text-transform: uppercase;
    fill: var(--muted);
  }

  .s-label {
    font-size: 12px; font-weight: 600;
    fill: var(--content);
    text-anchor: middle;
  }

  /* Node cards live inside foreignObject so they get real HTML typography. */
  .node { transition: opacity 160ms ease; }
  .node.dimmed { opacity: 0.16; }
  .node-card {
    width: 100%; height: 100%;
    background: var(--surface);
    border: 1px solid var(--color-base-300);
    border-left: 3px solid var(--nc);
    border-radius: var(--rounded);
    padding: 9px 11px 8px;
    display: flex; flex-direction: column; gap: 5px;
    cursor: default;
    box-shadow: var(--shadow-sm);
    transition: box-shadow 160ms ease, border-color 160ms ease;
  }
  .node:hover .node-card, .node.active .node-card {
    border-color: var(--nc);
    box-shadow: 0 0 0 3px var(--copper-soft), var(--shadow);
  }
  .node-type {
    display: flex; align-items: center; gap: 6px;
    font-size: 10px; font-weight: 600;
    letter-spacing: 0.07em; text-transform: uppercase;
    color: var(--nc-text);
  }
  .node-type::before {
    content: "";
    width: 5px; height: 5px; border-radius: 50%;
    background: var(--nc);
  }
  .node-title {
    font-size: 12.5px; font-weight: 600; line-height: 1.25;
    letter-spacing: -0.005em;
  }
  .node-meta {
    font-size: 11px; line-height: 1.45;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
  }
  .node-meta .mono {
    font-family: var(--mono);
    font-size: 10px;
  }
  .node-foot {
    display: flex; justify-content: space-between; gap: 8px;
    font-size: 10.5px; color: var(--muted);
    border-top: 1px solid var(--color-base-300);
    padding-top: 5px;
    font-variant-numeric: tabular-nums;
  }
  .node-foot .chip {
    font-weight: 600;
    color: var(--nc-text);
  }

  .link { transition: opacity 160ms ease; }
  .link.dimmed { opacity: 0.08; }
  .link-label {
    font-family: var(--mono);
    font-size: 10px; font-weight: 500;
    fill: var(--muted);
    font-variant-numeric: tabular-nums;
    paint-order: stroke;
    stroke: var(--surface);
    stroke-width: 3px;
  }
  .link-label.dimmed { opacity: 0.08; }

  .footer-note {
    margin-top: 72px; padding-top: 20px; border-top: 1px solid var(--color-base-300);
    font-size: 13px; color: var(--muted); max-width: 70ch;
    display: flex; flex-wrap: wrap; gap: 8px 24px;
  }
  .legend { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; }
  /* A legend directly under the timeline graph frame (not in a footer-note). */
  .graph-frame + .legend { margin-top: 14px; }
  .legend .key {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 11px; font-weight: 500; color: var(--muted);
  }
  .legend .key::before {
    content: "";
    width: 8px; height: 8px; border-radius: 2.5px;
    background: var(--k);
  }

  /* ── Batch log (timeline page, section 02) ────────────── */
  .log {
    border: 1px solid var(--color-base-300);
    border-radius: var(--rounded);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
  }
  .log-row {
    display: grid;
    grid-template-columns: 96px 104px 1fr auto;
    gap: 4px 16px;
    align-items: baseline;
    padding: 10px 16px;
    border-top: 1px solid var(--color-base-300);
    font-size: 13px;
  }
  .log-row:first-child { border-top: none; }
  .log-date {
    font-family: var(--mono); font-size: 11px; color: var(--muted);
    font-variant-numeric: tabular-nums; white-space: nowrap;
  }
  .log-chip {
    font-size: 10px; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase;
    padding: 1px 8px; border-radius: 999px; white-space: nowrap; justify-self: start;
  }
  .chip-measurement { background: color-mix(in srgb, var(--info) 12%, transparent); color: var(--info); }
  .chip-event { background: var(--copper-soft); color: var(--copper-ink); }
  .chip-note { background: color-mix(in srgb, var(--muted) 12%, transparent); color: var(--muted); }
  .chip-batch, .chip-fill, .chip-lot { background: color-mix(in srgb, var(--ink) 8%, transparent); color: var(--ink); }
  .log-text { display: flex; flex-direction: column; min-width: 0; }
  .log-title { font-weight: 600; }
  .log-detail { color: var(--muted); }
  .log-value {
    font-family: var(--mono); font-size: 12px; white-space: nowrap;
    font-variant-numeric: tabular-nums;
  }
}

/* flatpickr: restyle the vendored default theme with daisyUI tokens so the
   calendar matches the app. Loaded after flatpickr.min.css; the
   .flatpickr-calendar prefix keeps these rules above the theme's specificity. */
.flatpickr-calendar {
    background: var(--color-base-100);
    color: var(--color-base-content);
    border: 1px solid var(--color-base-300);
    border-radius: var(--radius-box);
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}

.flatpickr-calendar .flatpickr-months .flatpickr-month,
.flatpickr-calendar .flatpickr-current-month,
.flatpickr-calendar .flatpickr-weekday {
    color: var(--color-base-content);
    fill: var(--color-base-content);
}

.flatpickr-calendar .flatpickr-prev-month svg,
.flatpickr-calendar .flatpickr-next-month svg {
    fill: var(--color-base-content);
}

.flatpickr-calendar .flatpickr-prev-month:hover svg,
.flatpickr-calendar .flatpickr-next-month:hover svg {
    fill: var(--color-primary);
}

.flatpickr-calendar .flatpickr-day {
    color: var(--color-base-content);
}

.flatpickr-calendar .flatpickr-day:hover,
.flatpickr-calendar .flatpickr-day:focus {
    background: var(--color-base-200);
    border-color: var(--color-base-200);
}

.flatpickr-calendar .flatpickr-day.today {
    border-color: var(--color-primary);
}

.flatpickr-calendar .flatpickr-day.selected,
.flatpickr-calendar .flatpickr-day.selected:hover,
.flatpickr-calendar .flatpickr-day.selected:focus {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: var(--color-primary-content);
}

.flatpickr-calendar .flatpickr-day.prevMonthDay,
.flatpickr-calendar .flatpickr-day.nextMonthDay,
.flatpickr-calendar .flatpickr-day.disabled {
    color: var(--color-base-content);
    opacity: 0.4;
}
