/* THE DASHBOARD COMPONENTS
   ========================
   KPI cards, the achievement bar, the picker cards and their detail blocks,
   the inventory tiles, the order board, the analysis tiles, the Today band
   and the panel tab strip.

   ⚠ WHY THIS FILE EXISTS, AND THE BUG IT FIXES (v2.8.0)
   -----------------------------------------------------
   Every rule here was inside a 1,055-line `<style nonce>` block in
   templates/dashboard.html. That block was emitted by base.html's
   `{% block extra_style %}`, which comes AFTER every linked stylesheet — and
   **a media query adds no specificity**. So a `.kpi-card` rule inside
   `@media (max-width: 767px)` in layout/mobile.css lost to the `.kpi-card`
   rule in the template at EVERY viewport width, phones included.

   Seventeen phone rules had therefore never once applied:

       .kpi-card .kpi-icon-box .kpi-title .kpi-value .kpi-delta
       .kpi-link-hint .machine-target-row .machine-target-title
       .progress-labels .order-card .order-meta .breakdown-list
       .breakdown-item .achievement-pill .filter-panel
       .toggle-switch-container .toggle-option

   which is why the dashboard rendered 24px KPI padding, desktop type sizes
   and the "View sales →" hint on a 360px phone. v2.5.0 found the same defect
   in four `.inv-*` rules and DELETED them, recording the rule "a component
   styled in a page's <style> block keeps its responsive rules in that same
   block". That rule was the right call for a component stuck in a template.
   The better fix is for the component not to be stuck in a template.

   The three other things that came with the move:

     * these components are reachable from every page now, not only from
       /dashboard;
     * ~34 KB of CSS stopped being re-parsed on every dashboard view and
       re-sent inside every panel fragment, and started riding in
       app.bundle.css behind the APP_VERSION cache-buster;
     * the CSP nonce is no longer load-bearing for the dashboard's styling.

   WHERE THE RESPONSIVE RULES WENT. layout/mobile.css, at the end, in the
   override layer that is loaded last and is the only file in this app
   allowed to carry a `max-width` query. The `(hover: hover)`,
   `(min-width: …)`, `(prefers-reduced-motion)` and `print` blocks stay here:
   mobile.css's own test forbids them, and they belong with the component.
   ================================================================== */

/* KPI cards. No longer glassmorphism — the class comment said
   "Glassmorphism" but the blur had already been removed for scroll
   performance, and the brand refresh drops the remaining soft-shadow
   styling in favour of a flat surface with a hairline border. */
.kpi-card {
    /* Mesh + parchment, like .panel. A KPI card sits directly on the page
       rather than inside a panel, so it is a top-level surface and takes
       the top-level treatment. */
    background: var(--card-mesh), var(--surface);
    padding: 24px;
    border-radius: var(--radius-lg);
    border: 1px solid var(--card-border);
    display: flex;
    flex-direction: column;
    position: relative;
    overflow: hidden;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: var(--shadow-sm);
}
/* backdrop-filter removed: KPI cards sit on a static background, so the
   blur was invisible but still forced a full repaint of everything
   behind them on every scroll frame — with 4 KPI cards plus 6 panels
   that was the dominant cost of scrolling this page. */
@media (hover: hover) and (pointer: fine) {
    /* The lift was translateY(-4px). On a shop-floor tablet with a
       mouse a 4px jump under the cursor makes a row of cards feel
       unstable as you sweep across them; a border colour change reads
       as clearly and moves nothing. */
    .kpi-card:hover { border-color: var(--brand); box-shadow: var(--elev-2); }
}

/* KPI cards and inventory tiles are now <a> elements (drill-down), so
   they must not inherit link underline/colour. */
a.kpi-card, a.inv-item { text-decoration: none; color: inherit; }
a.kpi-card:hover, a.inv-item:hover { text-decoration: none; }
a.kpi-card:focus-visible, a.inv-item:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/* The "View sales →" affordance. Hidden until hover/focus on pointer
   devices so the card stays clean, always visible on touch where there
   is no hover to reveal it. */
.kpi-link-hint {
    display: inline-flex; align-items: center; gap: 6px;
    margin-top: 10px; font-size: 12px; font-weight: 600;
    color: var(--brand-ink);
}
.kpi-arrow { transform: rotate(180deg); }
@media (hover: hover) and (pointer: fine) {
    .kpi-link-hint { opacity: 0; transition: opacity var(--dur-fast, 120ms) ease; }
    .kpi-card:hover .kpi-link-hint,
    .kpi-card:focus-visible .kpi-link-hint { opacity: 1; }
}

.kpi-icon-box {
    width: 48px; height: 48px; border-radius: var(--radius-md);
    display: flex; align-items: center; justify-content: center;
    font-size: 20px; margin-bottom: 12px;
}
/* Modifier classes rather than four inline `style="background:
   rgba(var(--accent-green-rgb), 0.12)"` attributes. Those literals were left over from
   a Tailwind palette that no longer exists anywhere else in the app, so
   they did not follow the theme and did not change with the rebrand.
   The tint is now derived from the same -rgb triplet the token defines. */
.kpi-icon-box--brand    { background: var(--brand-wash-strong);            color: var(--brand-ink); }
.kpi-icon-box--positive { background: rgba(var(--accent-green-rgb), 0.12); color: var(--positive-ink); }
.kpi-icon-box--negative { background: rgba(var(--accent-red-rgb), 0.12);   color: var(--negative-ink); }
.kpi-icon-box--caution  { background: rgba(var(--accent-orange-rgb), 0.16);color: var(--caution-ink); }

.kpi-title { color: var(--text-muted); font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }

/* Period-over-period change. Direction is carried by BOTH the arrow
   glyph and the colour, so it survives greyscale printing and doesn't
   rely on colour perception. */
.kpi-delta {
    display: inline-flex; align-items: baseline; gap: 5px;
    margin-top: 6px; font-size: 12px; font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.kpi-delta.up   { color: var(--positive-ink); }
.kpi-delta.down { color: var(--negative-ink); }
.kpi-delta.flat,
.kpi-delta.none { color: var(--text-muted); font-weight: 600; }
.kpi-delta-label { font-weight: 500; color: var(--text-muted); }
.kpi-value { font-size: clamp(1.5rem, 1.3rem + 1vw, 1.75rem); font-weight: 700; color: var(--text-main); line-height: 1.2; font-variant-numeric: tabular-nums; }

/* Five rules stood here and are GONE (v2.7.0), not renamed: kpi-dual,
   kpi-dual-row, kpi-unit, kpi-value--sm and kpi-baseline. (Named without
   their leading dots on purpose — test_dashboard_v270 scans this page
   for a line beginning with any of those selectors, and a tombstone that
   trips the test guarding the removal it is describing is worse than no
   tombstone.)
   They styled the Total In and Total Out headline pair, and both totals
   came off the panel — see the long note in dashboard/_today_band.html
   for why. Deleting them is the same call v2.6.0 made about
   .kpi-card--dual: an orphan rule costs nothing at runtime and quite a
   lot the next time somebody edits it and cannot work out why the page
   does not change, because the class is on no element. `.kpi-delta` is
   NOT among them — the Revenue card beside these two still uses it. */

/* ---- The itemised In / Out panels (v2.6.0) -------------------------
   v2.5.0 put the five product lines behind these tiles in a `title`
   tooltip. A tooltip needs a mouse, so on the handsets this is mostly
   read on the five figures did not exist at all. They are on the face of
   the panel now, and the OUT side carries what each line earned.

   These rules are here and not in components/cards.css because a KPI card
   is not a card: it is a figure, a delta and a drill-down link that
   happen to sit on a card-shaped surface. Its phone overrides are in
   layout/mobile.css with every other max-width rule in the app — which
   is the v2.8.0 correction to the v2.5.0 note this comment used to
   carry. See the header of this file. */
.kpi-card--itemised { display: flex; flex-direction: column; }
.kpi-card--itemised .kpi-value {
    font-size: clamp(1.3rem, 1.15rem + 0.7vw, 1.55rem);
}
.kpi-card--itemised .kpi-link-hint { margin-top: auto; padding-top: 10px; }

.kpi-lines {
    margin: 12px 0 0; padding: 12px 0 0;
    border-top: 1px solid var(--border);
    display: flex; flex-direction: column; gap: 6px;
}
/* Two columns on the IN side, three on the OUT side where a money
   column joins. Grid rather than flex so the figures line up in a
   column down the panel instead of each row centring on its own
   label — five ragged right edges is what makes a list of numbers
   unreadable. */
.kpi-line {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: baseline;
    gap: 4px 10px;
}
.kpi-lines--money .kpi-line { grid-template-columns: 1fr auto auto; }
.kpi-line dt {
    font-size: 12.5px; font-weight: 500; color: var(--text-muted);
    /* The three treated lines have long names. Wrapping is fine; what is
       not fine is a name pushing the figure out of its column. */
    min-width: 0;
}
.kpi-line dd {
    margin: 0;
    /* A step up from v2.6.0's 12.5px. These five figures are no longer
       a footnote under a headline pair — with Total In and Total Out
       gone they ARE the panel, and they are sized as its content
       rather than as its supporting detail. */
    font-size: 13.5px; font-weight: 700; color: var(--text-main);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.kpi-line__unit {
    font-size: 10.5px; font-weight: 600; color: var(--text-muted);
}
/* Fixed minimum so five rupee figures of different magnitudes still form
   a straight right edge. */
.kpi-line__money { min-width: 6.5rem; text-align: right; }
.kpi-line--total {
    margin-top: 4px; padding-top: 8px;
    border-top: 1px dashed var(--border);
}
.kpi-line--total dt { color: var(--text-main); font-weight: 700; }

/* ---- The per-line comparisons (v2.7.0) -----------------------------
   A second grid ROW inside each .kpi-line rather than a third and fourth
   column. Two chips beside a wrapping product name and a rupee figure
   would leave five columns fighting over a 380px card, and the first
   thing to give would be the label — which is the one part of the row
   that must not be abbreviated.

   `grid-column: 1 / -1` spans whatever track count the parent has, so
   the same rule serves the two-column IN side and the three-column OUT
   side without either being named. */
.kpi-line__vs {
    grid-column: 1 / -1;
    display: flex; flex-wrap: wrap; gap: 4px 6px;
    margin-top: 2px;
}
.kpi-vs {
    display: inline-flex; align-items: baseline; gap: 5px;
    padding: 1px 7px 2px;
    border-radius: var(--radius-sm);
    background: var(--brand-wash);
    white-space: nowrap;
}
.kpi-vs__label {
    font-size: 10px; font-weight: 600; letter-spacing: 0.02em;
    color: var(--text-muted);
    /* NOT tabular. The label is a month name, and a numeric grid pushes
       the digits of "Jan-Jul 2026" apart from its letters. */
    font-variant-numeric: normal;
}
.kpi-vs__delta {
    font-size: 11px; font-weight: 700;
    font-variant-numeric: tabular-nums;
}
/* The same three colours the .kpi-delta chip on the Revenue card uses,
   so up/down/flat means one thing on this band. Read from the semantic
   INK tokens, not the fills: these are small text on a tinted wash, and
   --positive / --negative are sized for white text ON a fill. */
.kpi-vs__delta--up    { color: var(--positive-ink); }
.kpi-vs__delta--down  { color: var(--negative-ink); }
.kpi-vs__delta--flat  { color: var(--text-muted); }
/* No baseline to compare against. Muted and lighter than "flat", because
   "we did the same as last year" and "there is no last year" are
   different answers and the second must not read as the first. */
.kpi-vs__delta--none  { color: var(--text-muted); opacity: .7; font-weight: 600; }

/* A DOWN arrow on the OUT panel is not automatically bad news and an UP
   arrow on the IN panel is not automatically good, which is why these
   carry no ✓/✗ iconography and no green/red BACKGROUND — only coloured
   text. The colour says which way the number went; the reader supplies
   whether that is what they wanted. */

/* The sentence that stops two different revenue figures on one screen
   reading as an arithmetic error. */
.kpi-footnote {
    margin: 10px 0 0; font-size: 10.5px; line-height: 1.45;
    color: var(--text-muted);
}

/* Toggle Switch */
.toggle-switch-container {
    display: inline-flex; align-items: center; gap: 4px;
    background: var(--input-bg); padding: 4px; border-radius: var(--radius-md);
    border: 1px solid var(--input-border);
}
.toggle-option { font-size: 13px; font-weight: 600; color: var(--text-muted); cursor: pointer; padding: 6px 14px; border-radius: var(--radius-sm); transition: 0.3s; display: inline-block; }
.toggle-option:has(input:checked) { background: var(--brand); color: var(--on-brand); }

/* Filter Panel */
.filter-panel { background: var(--card-mesh), var(--surface); padding: 10px 16px; border-radius: var(--radius-lg); border: 1px solid var(--border); box-shadow: var(--elev-1); }

/* Horizontal Machine Target Box */
.machine-target-row {
    display: grid; grid-template-columns: 1fr; gap: 15px;
    background: var(--brand-wash); padding: 16px 20px; border-radius: var(--radius-lg); margin-bottom: 12px;
    border: 1px solid var(--border);
}
html.dark-theme .machine-target-row { background: var(--brand-wash); border-color: var(--border); }
@media(min-width: 768px) { .machine-target-row { grid-template-columns: 140px 1fr 1fr; align-items: center; } }
.machine-target-title { font-weight: 700; display: flex; align-items: center; gap: 8px; font-size: 15px; }
.progress-wrap { display: flex; flex-direction: column; width: 100%; }
.progress-labels { display: flex; justify-content: space-between; font-size: 12px; font-weight: 600; margin-bottom: 6px; color: var(--text-main); }
.progress-bar-bg { width: 100%; height: 8px; background: rgba(0,0,0,0.1); border-radius: var(--radius-full); overflow: hidden; }
html.dark-theme .progress-bar-bg { background: rgba(255,255,255,0.1); }
/* height: 100%, so this MUST be a <div>. As a <span> it is inline, the
   height resolves to nothing and the bar renders as an empty grey track at
   every percentage including 100% — with the class, the width and the
   number all correct. See CLAUDE.md.

   --radius-full, not --radius-md: the track is 8px tall, so any radius at
   or above 4px is a full pill. Naming it "full" says that on purpose
   instead of relying on the browser clamping a 12px radius down. */
.progress-bar-fill {
    height: 100%; background: var(--brand); border-radius: var(--radius-full);
    transition: width var(--dur-celebrate) var(--ease-spring);
}
.progress-bar-fill.success { background: var(--positive); }

/* ---- OVER-FULFILMENT (v2.4.0) --------------------------------------
   A fixed-width track cannot draw 147% by being 147% wide, so past 100%
   it changes what the track MEANS: it stops being "0 to target" and
   becomes "0 to what was actually achieved", the fill runs the whole
   width, and a PLAN LINE is drawn where the target now falls. The eye
   reads "finished, and this far past the line", which is what happened.

   `overflow: hidden` has to come off, or the plan line's border is
   clipped by the track's own pill radius at the exact percentages where
   the line is nearest the end — which is to say, the ones that matter
   most. The fill keeps its own radius, so nothing spills.

   WHY THE LINE IS A BORDER ON A SPACER. It has to sit at an arbitrary
   percentage, and this app's CSP carries a per-request nonce with no
   'unsafe-inline' for style-src: a `style="left: 68%"` would be dropped
   by the parser with nothing in the server log. A transparent block
   sized by one of the 101 .w-pct-* utilities, drawing its right border,
   puts a marker anywhere without a single inline style. */
.progress-bar-bg.is-over { overflow: visible; position: relative; }
.progress-plan-line {
    position: absolute;
    top: -3px; bottom: -3px; left: 0;
    border-right: 2px solid var(--text-main);
    opacity: 0.55;
    pointer-events: none;
}
/* No plan line, no denominator: the whole track is hatched instead, so
   "there was no target here" is legible as a texture before anyone reads
   the words next to it. A flat full bar would say "100%", which is the
   claim this state exists to avoid making. */
.progress-bar-fill.is-untargeted {
    background-image: repeating-linear-gradient(
        135deg,
        rgba(255, 255, 255, 0.34) 0 5px,
        rgba(255, 255, 255, 0) 5px 10px);
}

/* ---- The figures above the bar ------------------------------------- */
.ach-figures { display: inline-flex; align-items: baseline; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.ach-figure { font-variant-numeric: tabular-nums; }
/* Chips, not coloured text. "+53.5 surplus" next to "108.3%" in the same
   weight and size is two numbers competing; in a chip it is a number and
   a label about it. Both variants are a tint carrying its own INK token
   rather than white on a fill — they sit inline in a 12px line and a
   solid green block that size reads as an error state. */
.ach-chip {
    display: inline-flex; align-items: center;
    padding: 1px 7px; border-radius: var(--radius-full);
    font-size: 11px; font-weight: 700; line-height: 1.6;
    font-variant-numeric: tabular-nums; white-space: nowrap;
}
.ach-chip--surplus {
    background: rgba(var(--accent-green-rgb), 0.14);
    color: var(--positive-ink);
}
.ach-chip--untargeted {
    background: var(--brand-wash-strong);
    color: var(--brand-ink);
    font-weight: 600;
    text-transform: none;
}
/* The exact-figures pills under the achievement chart take the same two
   accents, one step quieter — they are a dense list, and a row of tinted
   pills at full strength is a stripe. */
.achievement-pill.is-surplus  { border-color: var(--positive); }
.achievement-pill.is-untargeted { border-style: dashed; }

/* The STATIC card (`.pick-card--static` / `.pick-grid--static`) stood here
   from v2.2.0 (as `.treatment-card`) to v2.9.0. Its one caller was the
   `period_card` macro on the Treatment Plant panel, and both went when that
   panel came off the Production tab — the Treatment Achievement panel below
   it reports the same figures bucket by bucket and rolls them up on its own
   picker cards.

   The rules are gone rather than kept "in case": a modifier with no caller is
   the thing the next person reaches for instead of the component, which is
   precisely how `.treatment-card` and `.pick-card` came to be two names for
   one card before v2.8.0 merged them. The base `.pick-card` / `.pick-grid`
   below is unchanged and is what both picker strips still use. */

/* ====================================================================
   THE PICKER: SELECTABLE CARDS OVER A DETAIL REGION (v2.5.0)
   ====================================================================
   Two panels use this — Weekly Machine Achievement (one card per
   machine) and the Logs / Sawn Timber panels (Production vs Sales) —
   so it is ONE component with two callers rather than two components
   that look alike until somebody restyles one of them.

   THE PROGRESSIVE-ENHANCEMENT CONTRACT. The server renders every detail
   block VISIBLE. `main.dashboard.js` adds `.picker--ready` to the strip
   and `.picker-panel` gains its hidden state only through the
   `[hidden]` attribute that script sets. So with JavaScript off, or on
   a printout, the cards are in-page links and every block is on the
   page — which is why print.css needs no rule for any of this.

   The cards are <a> elements. They keep card-shaped padding, so the
   usual link colour/underline has to be turned off explicitly, exactly
   as a.kpi-card and a.inv-item do above. */
.pick-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: var(--space-4);
    margin-bottom: var(--space-5);
}
.pick-card {
    display: flex; flex-direction: column; gap: 10px;
    background: var(--surface-raised, var(--input-bg));
    border: 1px solid var(--input-border);
    border-left: 3px solid var(--text-muted);
    border-radius: var(--radius-md);
    padding: 14px 16px;
    color: inherit; text-decoration: none;
    cursor: pointer;
    /* The selected card grows a 2px inner ring. Reserving the space up
       front means selecting one does not shove the row sideways by a
       pixel — the commonest way a card strip feels "jumpy". */
    box-shadow: inset 0 0 0 0 var(--brand);
    transition: border-color var(--dur-fast, 120ms) ease,
                box-shadow var(--dur-fast, 120ms) ease,
                background-color var(--dur-fast, 120ms) ease;
}
.pick-card:hover, .pick-card:focus-visible { text-decoration: none; }
.pick-card:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
    .pick-card:hover { border-color: var(--brand); }
}
/* Same left-edge convention as .treatment-card: the state band survives
   a greyscale printout and does not rely on colour perception. */
.pick-card.is-met { border-left-color: var(--positive); }
.pick-card.is-untargeted { border-left-color: var(--brand); }
/* SELECTED. Carried by three things at once — the ring, the tinted
   surface and the heavier title — because on a shop-floor tablet in
   daylight a single 1px border change is not a signal anyone sees. */
.pick-card.is-active {
    border-color: var(--brand);
    background: var(--brand-wash);
    box-shadow: inset 0 0 0 2px var(--brand);
}
.pick-card.is-active .pick-card-title { color: var(--brand-ink); }
.pick-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.pick-card-title {
    display: inline-flex; align-items: center; gap: 8px;
    font-size: 13px; font-weight: 700; color: var(--text-main);
}
.pick-card-figure {
    display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
    font-variant-numeric: tabular-nums; font-size: 12px;
}
.pick-card-figure strong { font-size: 20px; font-weight: 800; color: var(--text-main); line-height: 1.1; }
.pick-card-foot { font-size: 11px; color: var(--text-muted); line-height: 1.4; }
/* `.pick-card-foot--empty` was here until v2.9.0 — reserved space so a row
   of period cards stayed the same height when only some carried a
   carry-forward note. Only `period_card` emitted it, and that macro went with
   the Treatment Plant panel. The picker cards below always render a foot. */

/* ---- The detail region the cards select ---------------------------
   Separated from the strip by a rule rather than by a gap: the two are
   one control and one readout, and a bare gap reads as two unrelated
   blocks stacked. */
.detail-block, .picker-panel {
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px dashed var(--card-border);
}
.detail-title {
    display: flex; align-items: center; gap: 8px;
    margin: 0 0 14px; font-size: 14px; font-weight: 700; color: var(--text-main);
}
.detail-rows { display: flex; flex-direction: column; gap: 14px; }
.detail-row {
    display: flex; flex-direction: column; gap: 6px;
    padding: 12px 14px;
    background: var(--input-bg);
    border: 1px solid var(--input-border);
    border-radius: var(--radius-md);
}
.detail-row.is-met { border-color: rgba(var(--accent-green-rgb), 0.45); }
.detail-row-head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 10px; flex-wrap: wrap; font-size: 12px; font-weight: 600;
}
.detail-bucket { color: var(--text-main); font-weight: 700; }
.detail-note { font-size: 11px; color: var(--text-muted); }

/* Two cards, not a wrapping auto-fit grid: Production and Sales are a
   PAIR, and letting them stretch to 480px each on a wide panel makes two
   small figures look like two half-empty panels. */
.pick-grid--pair { grid-template-columns: repeat(auto-fit, minmax(220px, 320px)); }

/* The detail region the Logs / Sawn Timber cards select. Its blocks hold
   a <details> each, so unlike .detail-block there is nothing to rule
   off — the collapsed summary is its own visual boundary. */
.picker-views { margin-top: var(--space-4); }
.picker-view { margin: 0; }
/* Belt and braces. `hidden` is honoured by every browser's UA stylesheet
   UNTIL a rule sets `display` on the same element, and a later tidy-up
   adding `display: flex` to .picker-view or .detail-block would leave
   every panel on screen at once with no error anywhere. */
.picker-view[hidden], .picker-panel[hidden], .detail-block[hidden] { display: none; }

/* ---- Three-month sales outlook (v2.2.0) ---------------------------
   Sits under the Sales Trends chart, not inside it. The chart's dashed
   tail is fitted to whatever the toggle is showing — WEEKS in monthly
   mode — and these three figures are fitted to calendar months in both
   modes. Different questions, so they are visually a separate block
   rather than a caption on the chart. */
.outlook {
    margin-top: 18px; padding-top: 16px;
    border-top: 1px dashed var(--card-border);
}
.outlook-head {
    display: flex; align-items: baseline; justify-content: space-between;
    flex-wrap: wrap; gap: 6px 14px; margin-bottom: 12px;
}
.outlook-title {
    display: inline-flex; align-items: center; gap: 8px;
    font-size: 13px; font-weight: 700; color: var(--text-main);
}
.outlook-meta { font-size: 11px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.outlook-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); }
/* Dashed, and never solid. Every recorded figure on this page sits in a
   solid-bordered box; a predicted one must not be able to be mistaken for
   one at a glance, and the dash does that before the reader has read a
   word of the label. */
.outlook-card {
    display: flex; flex-direction: column; gap: 3px;
    padding: 12px 14px; border-radius: var(--radius-md);
    /* --surface-raised, not --surface. This tile sits INSIDE a panel, and
       since v2.4.0 the panel is parchment too — a --surface tile on a
       --surface card is a rectangle you can only see the border of. The
       raised token exists for exactly this nesting. */
    background: var(--surface-raised);
    border: 1px dashed var(--card-border);
}
.outlook-month {
    font-size: 11px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.05em; color: var(--text-muted);
}
.outlook-value {
    font-size: 18px; font-weight: 800; color: var(--text-main);
    font-variant-numeric: tabular-nums; line-height: 1.2;
}
.outlook-range { font-size: 11px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.outlook-foot {
    display: flex; align-items: baseline; justify-content: space-between;
    flex-wrap: wrap; gap: 6px 14px; margin-top: 12px;
    font-size: 12px; color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* ====================================================================
   INVENTORY — remodelled v2.5.0
   ====================================================================
   The old grid was minmax(200px) tiles with 16px of padding and a 16px
   gap, all twelve in one run. Three things were wrong with it and each
   fix below is one of them:

     SPACE      240px tiles, 20px of padding, a 22px gap, and 28px
                between one group and the next. The panel is not short of
                room — it was spending it on twelve equal boxes.
     GROUPING   .inv-group wraps each of the four families, with a
                heading that is deliberately QUIET (12px, uppercase,
                muted): it is a signpost, and a signpost that competes
                with the figures is worse than none.
     HIERARCHY  four descending steps — 11px eyebrow, 13px name, 28px
                figure, 11px threshold. The old tile had 13 / 22 / 11
                with the name in the same weight as the value, so
                nothing was first.

   Every colour here is a semantic token, and every one that carries text
   is an -ink partner rather than a fill: --caution as text is 2.3:1 on
   parchment, and in dark mode the fills are lighter than the page, so a
   literal would flip to unreadable on a theme toggle. See CLAUDE.md. */
.inv-group { margin-bottom: 28px; }
.inv-group:last-child { margin-bottom: 0; }
.inv-group-head {
    display: flex; align-items: baseline; gap: 6px 12px; flex-wrap: wrap;
    margin-bottom: 12px; padding-bottom: 8px;
    border-bottom: 1px solid var(--card-border);
}
.inv-group-title {
    margin: 0; font-size: 12px; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.07em;
    color: var(--text-muted);
}
.inv-group-note { font-size: 11px; color: var(--text-muted); opacity: 0.85; }

.inv-grid {
    display: grid;
    /* auto-FIT, not auto-fill. The groups hold one, four, four and three
       tiles, and auto-fill keeps the empty tracks — so "Yard intake",
       which is a single tile, rendered at a third of the row with two
       track-widths of nothing beside it. auto-fit collapses the empty
       tracks and lets the tiles that exist share the row. */
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 22px;
}
.inv-item {
    position: relative; overflow: hidden;
    display: block;
    /* The left padding clears the inset status stripe. */
    padding: 20px 20px 18px 26px;
    background: var(--surface-raised);
    border: 1px solid var(--card-border);
    border-radius: var(--radius-lg);
    transition: border-color var(--dur-fast, 120ms) ease,
                box-shadow var(--dur-fast, 120ms) ease;
}
@media (hover: hover) and (pointer: fine) {
    .inv-item:hover { border-color: var(--brand); box-shadow: var(--elev-1); }
}
/* INSET AND ROUNDED, not a hard 4px edge butted into the corner radius.
   A full-height square stripe on a 12px-radius card leaves two visible
   notches at top and bottom; a 6px inset bar with its own radius reads
   as a deliberate marker instead of a rendering artefact. */
.inv-item-status {
    position: absolute; left: 10px; top: 20px; bottom: 18px;
    width: 4px; border-radius: var(--radius-full);
    background: var(--text-muted);
}
.inv-item.critical .inv-item-status { background: var(--negative); }
.inv-item.warning .inv-item-status { background: var(--caution); }
.inv-item.healthy .inv-item-status { background: var(--positive); }
.inv-item.untracked .inv-item-status { background: var(--text-muted); }

.inv-item-body { display: flex; flex-direction: column; gap: 6px; }

/* THE EYEBROW. Status in words, not only in colour — the stripe on its
   own is a colour-only signal, and this is the cheapest line on the tile
   to read and the most expensive to get wrong. */
.inv-eyebrow {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 11px; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--text-muted);
}
.inv-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.inv-item.critical .inv-eyebrow { color: var(--negative-ink); }
.inv-item.warning .inv-eyebrow  { color: var(--caution-ink); }
.inv-item.healthy .inv-eyebrow  { color: var(--positive-ink); }

/* The NAME. Normal weight and full text colour: it is the label, and
   making it bold put it in competition with the figure below it. */
.inv-title {
    font-size: 13px; font-weight: 600; color: var(--text-main);
    line-height: 1.35;
}

/* THE FIGURE — the thing the tile exists for, and now unambiguously the
   first thing on it. The unit is a separate span at a third of the size,
   so "1,240.5 m³" reads as one number with a unit rather than as a name
   with a bracket in it. */
.inv-figure { display: flex; align-items: baseline; gap: 6px; margin-top: 2px; }
.inv-value {
    font-size: 28px; font-weight: 800; color: var(--text-main);
    line-height: 1.1; font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
}
.inv-unit { font-size: 12px; font-weight: 600; color: var(--text-muted); }

/* The threshold rail. Taller than the old 4px hairline (6px) and set
   BELOW its label rather than beside it, so a long minimum does not
   squeeze the bar to nothing on a narrow tile. */
.inv-meta { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.inv-threshold {
    font-size: 11px; color: var(--text-muted); white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.inv-rail {
    display: block; height: 6px; border-radius: var(--radius-full);
    background: rgba(0,0,0,0.07); overflow: hidden;
}
html.dark-theme .inv-rail { background: rgba(255,255,255,0.10); }
.inv-rail-fill {
    display: block; height: 100%; border-radius: var(--radius-full);
    background: var(--text-muted);
}
.inv-rail-fill.critical { background: var(--negative); }
.inv-rail-fill.warning  { background: var(--caution); }
.inv-rail-fill.healthy  { background: var(--positive); }
/* Reserved space, not a hidden element: without it an untracked tile is
   shorter than its neighbours and the row of figures stops lining up —
   the same argument as .treatment-note--empty above. */
.inv-meta--empty { min-height: 22px; justify-content: flex-end; }

/* Order Kanban style */
.order-board { display: flex; flex-direction: column; gap: 12px; }
.order-card {
    display: flex; justify-content: space-between; align-items: center;
    background: var(--input-bg); border: 1px solid var(--input-border);
    padding: 14px 16px; border-radius: var(--radius-md); transition: 0.2s;
}
.order-card:hover { border-color: var(--brand); background: var(--brand-wash); }
.order-info { display: flex; flex-direction: column; gap: 4px; }
.order-item-name { font-weight: 700; font-size: 14px; }
.order-meta { font-size: 12px; color: var(--text-muted); display: flex; gap: 12px; }
/* `.status-badge` + `.status-pending` / `-progress` / `-done` were here.
   They were `.badge badge--upper` plus the three wash/ink pairings
   base/utilities.css already held — a fourth badge system on a platform
   that had three too many. The order board's markup names the shared
   classes now; see templates/dashboard/_inventory.html. */

/* Breakdown Timeline */
.breakdown-list { display: flex; flex-direction: column; gap: 16px; padding-left: 10px; }
.breakdown-item {
    display: flex; flex-direction: column; gap: 6px;
    padding-left: 24px; position: relative;
    border-left: 2px solid var(--card-border);
}
.breakdown-item::before {
    content: ''; position: absolute; left: -7px; top: 0;
    width: 12px; height: 12px; border-radius: 50%; background: var(--negative);
    border: 2px solid var(--surface);
}
.breakdown-item.positive::before { background: var(--brand); }
.achievement-pill { background: var(--input-bg); border: 1px solid var(--input-border); padding: 2px 8px; border-radius: var(--radius-sm); font-size: 12px; }

/* ---- Analysis panel (v1.18.0) --------------------------------------
   Recovery, availability, cover, destinations and fulfilment. Every
   colour below is a SEMANTIC token and every one that sits on a tint is
   the -ink partner, not the fill: --caution as text is 2.3:1, and in
   dark mode the fills are lighter than the page, so a literal would flip
   to unreadable on a theme toggle. See CLAUDE.md, "Colour". */

/* A short explanatory strip: what this figure cannot know yet, and where
   to fix it. Deliberately not styled as an error — nothing is broken, a
   setting is simply not filled in. */
.analysis-note {
    display: flex; align-items: flex-start; gap: 10px;
    padding: 10px 14px; border-radius: var(--radius-md);
    background: var(--input-bg); border: 1px solid var(--input-border);
    font-size: 12px; line-height: 1.5; color: var(--text-muted);
}
.analysis-note strong { color: var(--text-main); }
.analysis-note .icon { flex: 0 0 auto; margin-top: 2px; }

/* Summary tiles, colour-banded by status. The band is carried by the
   left edge AND by the pill's text, so it survives greyscale printing
   and does not rely on colour perception alone. */
.stat-tile {
    display: flex; flex-direction: column;
    background: var(--input-bg); border: 1px solid var(--input-border);
    border-left: 3px solid var(--text-muted);
    border-radius: var(--radius-md); padding: 12px 14px;
}
.stat-tile--good  { border-left-color: var(--positive); }
.stat-tile--watch { border-left-color: var(--caution); }
.stat-tile--poor  { border-left-color: var(--negative); }

.stat-pill {
    font-size: 12px; font-weight: 700; white-space: nowrap;
    font-variant-numeric: tabular-nums;
    padding: 2px 8px; border-radius: var(--radius-full);
}
.stat-pill--good  { background: rgba(var(--accent-green-rgb), 0.12);  color: var(--positive-ink); }
.stat-pill--watch { background: rgba(var(--accent-orange-rgb), 0.16); color: var(--caution-ink); }
.stat-pill--poor  { background: rgba(var(--accent-red-rgb), 0.12);    color: var(--negative-ink); }
.stat-pill--none  { background: var(--input-bg); color: var(--text-muted); }

/* The availability list: a label, a bar, and three small facts. */
.analysis-rows { display: flex; flex-direction: column; gap: 16px; }
.analysis-row { display: flex; flex-direction: column; gap: 7px; }
.analysis-row-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.analysis-row-meta {
    display: flex; flex-wrap: wrap; gap: 6px 14px;
    font-size: 12px; color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
/* Reuses .progress-bar-fill's geometry, overriding only the fill. */
.progress-bar-fill.avail-good  { background: var(--positive); }
.progress-bar-fill.avail-watch { background: var(--caution); }
.progress-bar-fill.avail-poor  { background: var(--negative); }
.progress-bar-fill.avail-none  { background: var(--text-muted); }

/* Days-of-cover status, in a table cell where a left border cannot go. */
.cover-dot {
    display: inline-block; width: 8px; height: 8px; margin-right: 8px;
    border-radius: 50%; background: var(--text-muted); vertical-align: middle;
}
.cover-dot--critical { background: var(--negative); }
.cover-dot--warning  { background: var(--caution); }
.cover-dot--healthy  { background: var(--positive); }

.ageing-bucket {
    padding: 10px 0;
    border-bottom: 1px solid var(--card-border);
}
.ageing-bucket:last-child { border-bottom: none; }
.ageing-orders { display: flex; flex-direction: column; gap: 4px; margin-top: 7px; }
.ageing-order {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; padding: 6px 10px; border-radius: var(--radius-sm);
    background: var(--input-bg); border: 1px solid var(--input-border);
    font-size: 12px; text-decoration: none; color: var(--text-main);
}
.ageing-order:hover { border-color: var(--brand); text-decoration: none; }
.ageing-order:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ---- Today band ----------------------------------------------------
   The four figures that answer "are we on target?", plus the counts that
   used to cost a scroll each. The chips are deliberately lighter than the
   cards above them: they are states to notice, not figures to read, and
   giving them card weight would dilute the four that matter. */
.today-chips {
    display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px;
}
.today-chip {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 6px 12px; border-radius: var(--radius-full);
    /* Raised, for the same reason as .outlook-card above. */
    background: var(--surface-raised); border: 1px solid var(--card-border);
    font-size: 12px; font-weight: 600; color: var(--text-main);
    font-variant-numeric: tabular-nums;
    text-decoration: none;
}
a.today-chip:hover { border-color: var(--brand); text-decoration: none; }
a.today-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* -ink partners, not the fills: these are TEXT colours on a pale
   background, and --caution as text is 2.3:1. See variables.css. */
.today-chip--alert { color: var(--negative-ink); border-color: var(--negative); }
.today-chip--ok    { color: var(--positive-ink); }
.today-chip--muted { color: var(--text-muted); }

/* ---- The recording streak (v2.3.0) ---------------------------------
   A live streak is the ONE chip that gets the brand fill. That is
   deliberate and it is the only reason the chip earns its place: the
   four cards above it are figures about the mill, and this is the one
   element on the screen that says whether those figures are current.
   A live streak should be visible from the doorway; an ended one is a
   muted chip like any other, because at that point it is a note, not an
   achievement.

   position: relative because the celebration burst is appended inside it
   and positioned at its centre. Without it the sparks would be laid out
   against the nearest positioned ancestor, which is the page. */
.streak-chip { position: relative; }
.streak-chip--live {
    background: var(--brand);
    border-color: var(--brand);
    color: var(--on-brand);
}
/* currentColor, so the flame is white on the live fill and muted grey on
   an ended streak without a second rule for each state. */
.streak-flame { display: inline-flex; color: currentColor; }

/* ---- Tab strip -----------------------------------------------------
   Four links styled as tabs. They are real <a>s with real hrefs — the
   fetch-and-swap in main.dashboard.js is layered on top — so everything
   here has to work for a link as well as for a tab. */
.panel-tabs {
    display: flex; gap: 4px; margin-bottom: 20px;
    padding: 4px; border-radius: var(--radius-lg);
    background: var(--input-bg); border: 1px solid var(--card-border);
    overflow-x: auto; scrollbar-width: none;
}
.panel-tabs::-webkit-scrollbar { display: none; }
.panel-tab {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 9px 18px; border-radius: var(--radius-md); white-space: nowrap;
    font-size: 13px; font-weight: 600; color: var(--text-muted);
    text-decoration: none; border: none; background: transparent;
    cursor: pointer; transition: background var(--dur-fast, 120ms) ease,
                                 color var(--dur-fast, 120ms) ease;
}
.panel-tab:hover { color: var(--text-main); text-decoration: none; }
/* --on-brand, never #fff: in dark mode --brand is LIGHTER than the page,
   so a hardcoded white label drops to 2.4:1. And the colour is set
   explicitly because typography.css gives every <a> --brand-ink, which
   was picked for contrast against the page and not against a fill. */
.panel-tab.active {
    background: var(--brand); color: var(--on-brand);
}
.panel-tab.active:hover { color: var(--on-brand); }
.panel-tab:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The swap itself. Opacity only — no height animation, because the
   panels are different heights and animating that makes the page jump
   under a thumb that is already reaching for the next tab. */
#dashboard-panel { outline: none; }
#dashboard-panel.panel-swapping { opacity: 0.45; transition: opacity 120ms ease; }
@media (prefers-reduced-motion: reduce) {
    #dashboard-panel.panel-swapping { opacity: 1; transition: none; }
}

/* Printing takes the panel that is on screen. A printed dashboard is a
   filed record of one thing, and the tab strip is navigation, not
   content. */
@media print {
    .panel-tabs { display: none; }
    #dashboard-panel.panel-swapping { opacity: 1; }
}
