/* AUTO-GENERATED by tools/build_css.py — do not edit.
   Edit the source files under static/css/ and re-run the script. */

/* ===== base/variables.css ===== */
/* ========================================================
   DESIGN TOKENS — SINGLE SOURCE OF TRUTH
   ========================================================
   This file previously defined a --color-* palette while layout/main.css
   defined a parallel --accent-* / --card-* one. They described the SAME
   colours under different names, but only the main.css set had dark-theme
   overrides — so anything styled with --color-* (components/cards.css,
   components/forms.css, the link colour in base/typography.css) silently
   stayed in light-mode colours when dark mode was on.

   There is now ONE layer:

     RAW PALETTE   literal colour values, defined here. Never referenced by
                   a component — exists so a hue can be retuned in one place.
     SEMANTIC      what a colour MEANS (--surface, --brand, --negative).
                   Defined and themed in layout/main.css, next to the
                   dark-theme block that flips them, so light and dark
                   values sit side by side and cannot drift apart.
     LEGACY ALIAS  the old --color-* names, re-pointed at the semantic
                   tokens (bottom of this file). Every existing rule keeps
                   working; delete the aliases once nothing references them.

   IMPORTANT — FILL vs INK
   Several accents are legible as a background fill but fail WCAG AA as
   text (--caution #D9A404 with white on it is 2.3:1 against a 4.5:1
   requirement). Each has an -ink partner for text use:
       --positive        fills, bars, status dots, chart series
       --positive-ink    the same meaning, safe as text on a surface

   NOTE ON VALUES: the spacing and radius numbers below are deliberately
   UNCHANGED from what the app already used. The BRAND REFRESH (blue ->
   State Timber Corporation orange) changed colour values only, for the same
   reason: retuning colour and measurement at once would make any visual
   regression impossible to attribute. Every value below was checked against
   WCAG AA — the ratios are recorded next to the tokens in layout/main.css.
   ======================================================== */

:root {
  /* ---------- RAW PALETTE ----------
     Light-theme hues. Dark-theme equivalents live in layout/main.css.

     BRAND SOURCE: the State Timber Corporation mark — a white elephant-and-
     log line drawing on a burnt-orange plate. Every warm hue here is a tint
     or shade of that one sampled colour, which is why the UI reads as one
     thing with the logo rather than as a blue app with an orange badge
     stuck on it.

     WHY --timber-500 IS NOT THE PLATE COLOUR
     The plate sampled off the artwork is #BC6203 (--timber-logo below).
     White text on it is 4.3:1 — just under the 4.5:1 WCAG AA floor. Since
     the brand fill carries white button labels, white nav text and white
     badge text, it is one step darker at #A85703 (5.2:1). The two are
     indistinguishable side by side; the difference is that one of them is
     legible for someone reading a screen in a mill yard at midday.

     --timber-logo is used only where nothing sits on top of it: the plate
     behind the mark itself, the browser theme-color, the PWA/Android splash.

     The previous palette was blue (#3A7DFF) with cool greys. Nothing about a
     timber mill is blue, and the logo has no blue in it at all. The neutrals
     below are deliberately WARM (a touch of red in the grey) so white cards
     sit on the page without the bluish cast that cool greys give. */

  /* Brand orange — the logo plate and its usable range. */
  --timber-50:  #FDF8F2;   /* faintest wash: table zebra stripe, hover rows */
  --timber-100: #F7EBDB;   /* chips, subtle fills */
  --timber-200: #EDD8BE;
  --timber-300: #E4D8C6;   /* warm hairline — the border colour */
  --timber-400: #E3913B;   /* dark-theme brand fill — 7.1:1 on the dark surface */
  --timber-500: #A85703;   /* light-theme brand fill — 5.2:1 with white on it */
  --timber-700: #8A4703;   /* hover, and the text-safe orange (7.0:1 on white) */
  --timber-logo: #BC6203;  /* SAMPLED from the artwork. OS chrome only. */

  /* Bark — the dark warm browns. Sidebar, deep surfaces, headings. */
  --bark-800:  #2B1D14;    /* sidebar */
  --bark-900:  #241C16;    /* body text */

  /* Stone — warm neutrals. Replaces the cool --slate-* set. */
  --stone-500: #6F6156;    /* muted text (5.97:1 on white) */
  --stone-900: #241C16;

  /* Status. Deliberately NOT tints of the brand: an operator must be able to
     tell "on target" from "brand-coloured" at a glance on a small chip. */
  --green-500: #2E7D32;
  --green-700: #1B5E20;
  --red-500:   #C62828;
  --red-700:   #A31515;
  --amber-500: #D9A404;
  --amber-700: #8A6A00;

  /* ---------- LEGACY RAW ALIASES ----------
     The --blue-, --slate- and --grey-300 names are still referenced by a few
     rules that have not been renamed yet. They now resolve to the timber
     equivalents so nothing renders in the old brand colour. Do not add new
     uses; delete these once the last reference is gone. */
  --blue-400:  var(--timber-400);
  --blue-500:  var(--timber-500);
  --blue-700:  var(--timber-700);
  --slate-500: var(--stone-500);
  --slate-900: var(--stone-900);
  --grey-300:  var(--timber-300);

  /* ---------- SPACING ----------
     4 / 8 / 16 / 24 / 32 / 48 / 64. Values 1-5 are unchanged from the
     original file; 6 and 7 are new steps for page-level rhythm.
     There is intentionally no 12px step — components that want one use
     --space-2 (8) or --space-3 (16) rather than reintroducing eyeballed
     in-between values. */
  --space-1: .25rem;   /*  4px */
  --space-2: .5rem;    /*  8px */
  --space-3: 1rem;     /* 16px */
  --space-4: 1.5rem;   /* 24px */
  --space-5: 2rem;     /* 32px */
  --space-6: 3rem;     /* 48px */
  --space-7: 4rem;     /* 64px */

  /* ---------- RADIUS ----------
     Each step has a ROLE, so the choice is never made by eye:
       sm    badges, chips, table controls
       md    inputs, buttons
       lg    cards, panels, tiles
       xl    modals, drawers, the login card
       full  pills, avatars */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-full: 9999px;
  --radius: 10px;              /* legacy default used by buttons.css */

  /* ---------- ELEVATION ----------
     elev-1 resting cards · elev-2 hover · elev-3 modals and drawers.
     Replaces three separate shadow definitions that had drifted apart.

     The tint is warm (brown-black, not neutral black) to match the warm
     surfaces — a neutral shadow on a bone-white card reads faintly blue.
     Values are deliberately shallow: the redesign leans on hairline borders
     for separation, with shadow reserved for things that genuinely float. */
  --elev-0: none;
  --elev-1: 0 1px 2px rgba(43, 29, 20, 0.06);
  --elev-2: 0 4px 12px rgba(43, 29, 20, 0.09);
  --elev-3: 0 12px 28px rgba(43, 29, 20, 0.16);

  /* ---------- MOTION ----------
     Previously a mix of 0.2s, 0.25s, 0.3s and 1s with three different
     easing curves. */
  --dur-fast: 120ms;                        /* hover, focus, colour */
  --dur-base: 200ms;                        /* toggles, tabs, toasts */
  --dur-slow: 320ms;                        /* drawers, modals */
  --ease-out: cubic-bezier(.2, .8, .2, 1);  /* things entering */
  --ease-in-out: cubic-bezier(.4, 0, .2, 1);/* things moving */

  /* ---------- TYPE ---------- */
  --base: 16px;
  --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto,
               'Helvetica Neue', Arial, sans-serif;

  /* ---------- LAYERS ----------
     Named so a new overlay never has to guess a z-index. Ordered by how
     they actually stack: the skip link must clear the sidebar, and a toast
     must clear everything. */
  --z-bottom-nav:    900;
  --z-mobile-header: 950;
  --z-overlay:       999;
  --z-sidebar:      1000;
  --z-modal:        1050;
  /* The notification panel sits above the sidebar drawer (it can be opened
     from inside it) and above a modal, but BELOW the toasts — a toast
     confirming an action must never be hidden by the panel listing it. */
  --z-notif-overlay:1060;
  --z-notif-panel:  1070;
  --z-skip-link:    1100;
  --z-toast:        1200;
}

/* ========================================================
   LEGACY ALIASES
   ========================================================
   The old names, re-pointed at the semantic tokens defined in
   layout/main.css. CSS custom properties resolve at use time, not at
   definition time, so referencing --surface here before main.css declares
   it is fine — and it means these aliases follow the dark theme correctly,
   which was the whole bug.

   Do not add new rules that use these. They exist so the migration didn't
   need to touch every stylesheet at once.
   ======================================================== */
:root {
  --color-bg:            var(--surface-sunken);
  --color-surface:       var(--surface);
  --color-primary:       var(--brand);
  --color-primary-hover: var(--brand-hover);
  --color-on-primary:    #ffffff;
  --color-muted:         var(--text-muted);
  --color-success:       var(--positive);
  --color-warning:       var(--caution);
  --color-error:         var(--negative);

  /* Old shadow names -> the elevation scale. */
  --shadow-sm: var(--elev-1);
  --shadow-md: var(--elev-2);
}

html {
  font-family: var(--font-sans);
  font-size: var(--base);
  background: var(--surface-sunken);
  color: var(--text);
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;  /* stop iOS inflating text on rotation */
}

/* ===== base/utilities.css ===== */
/* ========================================================
   UTILITY CLASSES — APPEARANCE LAYER
   ========================================================
   OWNERSHIP (see IMPROVEMENT_PROPOSAL.md §1.2):
     grid.css        owns LAYOUT   — display, flex, grid, gap, width/height,
                                     position, overflow, spans, containers.
     utilities.css   owns APPEARANCE — colours, borders, radius, badges,
                                     cursor, plus the margin/padding steps
                                     that grid.css does not define.
     typography.css  owns TEXT     — size, weight, muted/primary/error colours.

   Anything previously duplicated here AND in grid.css has been removed:
   utilities.css is linked BEFORE grid.css in base.html, so grid.css was
   already winning every one of those declarations. Deleting the losing
   copies is a no-op at render time and removes the ambiguity.

   Three declarations below intentionally keep `!important` — they are
   load-bearing. `.hidden` in particular must beat `.form-group{display:flex}`
   from components/forms.css, which is linked LATER than grid.css. Without
   the `!important`, every conditional "Other (Specify)" field in the data
   entry forms would be permanently visible.
   ======================================================== */

/* ========================================================
   ACCESSIBILITY HELPERS
   ======================================================== */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0;
}
.sr-only-focusable:focus {
  position: static; width: auto; height: auto;
  padding: inherit; margin: inherit; overflow: visible;
  clip: auto; white-space: normal;
}

/* The skip link needs to render ON TOP of the fixed sidebar and mobile
   header when focused, otherwise it appears underneath them and the
   keyboard user sees nothing happen. */
.skip-link:focus {
  position: fixed;
  top: var(--space-2, 8px);
  left: var(--space-2, 8px);
  z-index: var(--z-skip-link, 1100);   /* above the sidebar and mobile header */
  padding: 12px 20px;
  background: var(--brand);
  color: var(--on-brand);
  border-radius: var(--radius-sm);
  box-shadow: var(--elev-3);
  text-decoration: none;
  font-weight: 600;
}

/* ========================================================
   TOUCH TARGETS
   ========================================================
   Previously this was a bare `.tap-target, button, a, input, .btn { ... }`
   rule, which applied `padding: 0.5rem 1rem` to EVERY anchor and input in
   the application — inline links, sidebar nav items, DataTables row
   checkboxes, the dashboard month picker. It also set `min-height:16px`,
   which is far below any touch-target guideline despite the comment above
   it claiming otherwise.

   Now scoped to genuinely interactive controls, at the WCAG 2.5.5 /
   iOS HIG minimum of 44px. Padding is left to the component that owns
   the element (buttons.css, main.css) rather than imposed globally.
   ======================================================== */
.tap-target,
button,
.btn,
[role="button"] {
  min-height: 44px;
  min-width: 44px;
  touch-action: manipulation;
}

/* Checkboxes and radios must never be inflated by touch-target sizing —
   they are sized explicitly by the component that renders them. */
input[type="checkbox"],
input[type="radio"] {
  min-height: 0;
  min-width: 0;
  padding: 0;
  touch-action: manipulation;
}

/* Common action classes get a slightly larger target than the 44px floor. */
.btn-action, .btn-danger, .btn-edit, .btn-submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0.5rem 1rem;
}

/* ========================================================
   TEXT ALIGNMENT
   (not defined in grid.css, so it lives here)
   ======================================================== */
.text-center { text-align: center; }
.text-left   { text-align: left; }
.text-right  { text-align: right; }

/* ========================================================
   SPACING STEPS NOT PROVIDED BY grid.css
   grid.css defines .m-0..4, .p-0..6, .px-*, .py-*, .mx-*, .my-*.
   The directional single-axis steps below are used throughout the
   templates and exist only here.
   ======================================================== */
.m-0 { margin: 0 !important; }   /* load-bearing: beats heading margins */
.p-0 { padding: 0 !important; }  /* load-bearing: beats component padding */

.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-auto { margin-top: auto; }

.mb-1 { margin-bottom: 0.25rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-5 { margin-bottom: 1.25rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }

.ml-0 { margin-left: 0; }
.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
.ml-auto { margin-left: auto; }   /* push an item to the end of a flex row */
.mr-2 { margin-right: 0.5rem; }

.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.pt-4 { padding-top: 1rem; }
.pb-2 { padding-bottom: 0.5rem; }
.pb-3 { padding-bottom: 0.75rem; }
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }

.font-normal { font-weight: 400; }
.whitespace-nowrap, .nowrap { white-space: nowrap; }
.table-auto { table-layout: auto; }

/* ========================================================
   DISPLAY
   ======================================================== */
/* Load-bearing `!important`: must beat components/forms.css
   `.form-group { display: flex }`, which is linked after grid.css. */
.hidden { display: none !important; }

/* ========================================================
   SIZING (fixed steps used by chart/panel wrappers)
   ======================================================== */
.h-300 { height: 300px; }
.h-fit { height: fit-content; }
.max-w-600 { max-width: 600px; }
.max-h-300 { max-height: 300px; }

/* ========================================================
   BORDERS & LISTS
   ======================================================== */
.border-none { border: none; }
.list-none { list-style: none; }
.uppercase { text-transform: uppercase; letter-spacing: 0.04em; }

/* ========================================================
   COLOURS
   `bg-success` / `bg-warning` are applied by static/js/main.js when it
   renders the sales status badge, so they are referenced from JS rather
   than from any template — do not remove as "unused".
   ======================================================== */
.bg-transparent  { background: transparent; }
.bg-light-shade  { background: rgba(0, 0, 0, 0.05); }
.bg-success      { background: var(--accent-green); }
.bg-warning      { background: var(--accent-orange); }
.bg-danger       { background: var(--accent-red); }

.text-orange { color: var(--accent-orange-ink, var(--accent-orange)); }
.text-red    { color: var(--accent-red-ink, var(--accent-red)); }
/* .text-blue is a legacy name kept because it is applied in several
   templates. It now paints the BRAND colour, which is orange — renaming it
   across the templates would put unrelated churn in a colour-only change.
   Use .text-brand in new markup. */
.text-blue   { color: var(--brand-ink); }
.text-brand  { color: var(--brand-ink); }
.text-green  { color: var(--accent-green-ink, var(--accent-green)); }
.text-main   { color: var(--text-main); }
.text-white  { color: #ffffff; }

/* ========================================================
   BADGE
   ======================================================== */
.badge {
  display: inline-flex;
  align-items: center;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* ========================================================
   SMALL LAYOUT PATTERNS
   These class names were already used in the templates but had never been
   defined anywhere, so they were inert.
   ======================================================== */

/* The heading block at the top of every page. */
.page-title h1,
.page-title h2 { margin: 0 0 var(--space-1) 0; }
.page-title p  { margin: 0; color: var(--text-muted); }

/* Button row at the foot of a form. Wraps rather than overflowing on
   narrow screens, and right-aligns from tablet up. */
.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}
@media (min-width: 640px) {
  .form-actions { justify-content: flex-end; }
}

/* ===== base/typography.css ===== */
/* ========================================================
   TYPOGRAPHY SYSTEM - REM-BASED SCALE & VERTICAL RHYTHM
   ======================================================== */

/* ========================================================
   INTER — SELF-HOSTED
   ========================================================
   These three files replace the Google Fonts <link> that base.html used to
   carry. That link was two DNS lookups (fonts.googleapis.com and
   fonts.gstatic.com) plus a RENDER-BLOCKING stylesheet request, on every
   page load, before anything could paint — and on a mill network with poor
   or filtered external connectivity it was a third party the app did not
   need to depend on. 72 KB of woff2 served from our own /static/ removes
   all of it.

   The files are the latin subset of Inter 400/600/700, taken from the
   @fontsource/inter npm package (SIL Open Font License 1.1 — the licence
   text ships beside them in static/fonts/LICENSE.txt, which the OFL
   requires). Same three weights the Google link requested, so nothing about
   how the app renders changes.

   ABSOLUTE paths, deliberately. A relative url() resolves against the
   stylesheet's own URL, and this file is served from two different
   locations: /static/css/base/typography.css unbundled, and inlined into
   /static/css/app.bundle.css when the bundle is fresh. A relative path
   would therefore be correct in one and broken in the other.

   font-display: swap keeps text visible while the font loads, exactly as
   the &display=swap parameter on the old Google URL did.

   unicode-range is what makes the latin subset safe: a character outside
   this range (Sinhala, for instance) never waits on these files at all —
   the browser goes straight to the next font in --font-sans. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/inter-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/inter-600.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/static/fonts/inter-700.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

/* BASE CONFIGURATION */
:root {
  /* Type scale modular (1.125 ratio) */
  --fs-xs: 0.75rem;      /* 12px */
  --fs-sm: 0.875rem;     /* 14px */
  --fs-base: 1rem;       /* 16px (body) */
  --fs-lg: 1.125rem;     /* 18px */
  --fs-xl: 1.25rem;      /* 20px */
  --fs-2xl: 1.5rem;      /* 24px */
  --fs-3xl: 1.875rem;    /* 30px */
  --fs-4xl: 2.25rem;     /* 36px */
  --fs-5xl: 2.813rem;    /* 45px */
  
  /* Line height / Vertical rhythm (1.5rem baseline) */
  --lh-tight: 1.2;
  --lh-normal: 1.5;
  --lh-relaxed: 1.75;
  
  /* Font weights */
  --fw-light: 300;
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;
  
  /* Letter spacing */
  --ls-tight: -0.02em;
  --ls-normal: 0;
  --ls-wide: 0.05em;
  --ls-wider: 0.1em;
}

/* ========================================================
   GLOBAL TEXT STYLES
   ======================================================== */

html {
  font-size: 16px; /* Base = 1rem */
}

body {
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: var(--lh-normal);
  font-weight: var(--fw-regular);
  color: var(--text-main);
  letter-spacing: var(--ls-normal);
}

/* ========================================================
   HEADINGS (H1-H6)
   ======================================================== */

/* Headings scale FLUIDLY with clamp() rather than stepping down at two
   breakpoints. This replaces the `@media (max-width:767px)` and
   `@media (max-width:639px)` heading blocks further down this file — those
   produced a visible jump at each breakpoint and, worse, the 639px block
   also shrank the ROOT font size to 15px, which dragged body copy below a
   comfortable reading size on exactly the devices that need it most.

   Line height tightens as size grows: 1.2 is right at 36px but noticeably
   loose on a two-line 20px heading. */
h1, h2, h3, h4, h5, h6 {
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  margin-top: 0;
  margin-bottom: 1rem; /* 1 vertical rhythm unit */
  text-wrap: balance;  /* avoid a lone trailing word; ignored where unsupported */
}

h1 {
  font-size: clamp(1.5rem, 1.1rem + 2vw, 2.25rem);   /* 24px -> 36px */
  line-height: 1.15;
  font-weight: var(--fw-bold);
  margin-bottom: 1.5rem;
  letter-spacing: var(--ls-tight);
}

h2 {
  font-size: clamp(1.25rem, 1rem + 1.4vw, 1.875rem); /* 20px -> 30px */
  line-height: 1.18;
  font-weight: var(--fw-bold);
  margin-bottom: 1.25rem;
}

h3 {
  font-size: clamp(1.125rem, 1rem + 0.7vw, 1.5rem);  /* 18px -> 24px */
  line-height: 1.25;
  font-weight: var(--fw-semibold);
  margin-bottom: 1rem;
}

h4 {
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.25rem);  /* 16px -> 20px */
  line-height: 1.3;
  font-weight: var(--fw-semibold);
  margin-bottom: 1rem;
}

h5 {
  font-size: var(--fs-lg);        /* 18px */
  font-weight: var(--fw-semibold);
  margin-bottom: 0.75rem;
}

h6 {
  font-size: var(--fs-base);      /* 16px */
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  margin-bottom: 0.75rem;
}

/* ========================================================
   PARAGRAPH & BODY TEXT
   ======================================================== */

p {
  margin-top: 0;
  margin-bottom: 1.5rem; /* Full vertical rhythm unit */
  line-height: var(--lh-normal);
}

/* Remove bottom margin from last p in containers */
p:last-child {
  margin-bottom: 0;
}

/* ========================================================
   TEXT UTILITIES & SEMANTIC CLASSES
   ======================================================== */

.text-xs {
  font-size: var(--fs-xs);
  line-height: 1.2;
}

.text-sm {
  font-size: var(--fs-sm);
  line-height: 1.4;
}

.text-base {
  font-size: var(--fs-base);
  line-height: var(--lh-normal);
}

.text-lg {
  font-size: var(--fs-lg);
  line-height: 1.5;
}

.text-xl {
  font-size: var(--fs-xl);
  line-height: 1.5;
}

.text-2xl {
  font-size: var(--fs-2xl);
  line-height: 1.3;
}

.text-3xl {
  font-size: var(--fs-3xl);
  line-height: 1.2;
}

.text-4xl {
  font-size: var(--fs-4xl);
  line-height: 1.1;
}

/* Font weight utilities */
.font-light {
  font-weight: var(--fw-light);
}

.font-normal {
  font-weight: var(--fw-regular);
}

.font-medium {
  font-weight: var(--fw-medium);
}

.font-semibold {
  font-weight: var(--fw-semibold);
}

.font-bold {
  font-weight: var(--fw-bold);
}

/* Line height utilities */
.leading-tight {
  line-height: var(--lh-tight);
}

.leading-normal {
  line-height: var(--lh-normal);
}

.leading-relaxed {
  line-height: var(--lh-relaxed);
}

/* Letter spacing utilities */
.tracking-tight {
  letter-spacing: var(--ls-tight);
}

.tracking-normal {
  letter-spacing: var(--ls-normal);
}

.tracking-wide {
  letter-spacing: var(--ls-wide);
}

.tracking-wider {
  letter-spacing: var(--ls-wider);
}

/* Text color utilities */
.text-muted {
  color: var(--text-muted);
}

.text-error {
  color: var(--color-error);
}

.text-success {
  color: var(--accent-green);
}

.text-primary {
  color: var(--color-primary);
}

/* ========================================================
   SEMANTIC TEXT ELEMENTS
   ======================================================== */

strong, .font-bold {
  font-weight: var(--fw-bold);
}

em, .italic {
  font-style: italic;
}

small, .text-small {
  font-size: var(--fs-sm);
  line-height: 1.4;
}

mark {
  background-color: rgba(255, 193, 7, 0.2);
  padding: 0.125rem 0.25rem;
  border-radius: 2px;
}

code, pre {
  font-family: 'Fira Code', 'Courier New', monospace;
  font-size: 0.875rem;
  line-height: 1.5;
}

code {
  background: var(--color-muted);
  color: white;
  padding: 0.125rem 0.375rem;
  border-radius: 4px;
}

pre {
  background: var(--color-muted);
  color: white;
  padding: 1rem;
  border-radius: 6px;
  overflow-x: auto;
  margin-bottom: 1.5rem;
}

pre code {
  background: none;
  color: inherit;
  padding: 0;
}

/* ========================================================
   LIST STYLES
   ======================================================== */

ul, ol {
  margin-top: 0;
  margin-bottom: 1.5rem; /* Full vertical rhythm */
  padding-left: 1.5rem;
  line-height: var(--lh-normal);
}

li {
  margin-bottom: 0.5rem;
}

li > ul, li > ol {
  margin-bottom: 0.5rem;
  margin-top: 0.5rem;
}

/* ========================================================
   BLOCKQUOTE & CITATION
   ========================================================== */

blockquote {
  margin: 1.5rem 0;
  padding: 1rem 1.5rem;
  border-left: 4px solid var(--color-primary);
  background: rgba(11, 95, 255, 0.05);
  font-style: italic;
  line-height: var(--lh-relaxed);
  color: var(--text-muted);
}

blockquote p {
  margin-bottom: 0;
}

blockquote footer {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  margin-top: 0.5rem;
}

/* ========================================================
   LINKS
   ========================================================== */

/* Uses --brand-ink, not --brand. The brand orange is a FILL colour: at
   5.0:1 on white it clears AA for normal text by a hair and fails for the
   12px links in table cells once a browser's subpixel rendering thins the
   strokes. --brand-ink is the same hue two steps darker (7.2:1) and is the
   token that exists for exactly this. Both themes define it, which is why
   the `html.dark-theme a { ... }` patch further down this file is now
   redundant rather than load-bearing. */
a {
  color: var(--brand-ink);
  text-decoration: none;
  transition: color 0.2s ease;
  cursor: pointer;
}

a:hover {
  color: var(--brand-hover);
  text-decoration: underline;
}

/* :focus-visible rather than :focus so the ring appears for keyboard
   users but not on every mouse click. Matches the convention already
   used in layout/main.css and components/buttons.css. */
a:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* NOTE: the `a:visited { color: #7c3aed }` rule that used to live here has
   been removed. In an internal MIS every link is visited within a day, so
   it turned the whole navigation, every table edit link and all pagination
   controls purple and off-brand within one session. Visited styling is a
   web-document convention that does not apply to an application UI. */

/* ========================================================
   TABULAR NUMERALS
   ========================================================
   Inter's default figures are PROPORTIONAL — a '1' is narrower than a '8'.
   In a column of numbers that means the digits don't line up vertically,
   and a KPI value visibly jitters sideways when it updates.

   Every element that displays a quantity, currency amount, percentage or
   timestamp opts into tabular (fixed-width) figures. This is a small change
   with an outsized effect on how precise the interface looks.
   ======================================================== */
.kpi-value,
.inv-value,
.progress-labels,
.achievement-pill,
.recent-meta,
.error-code,
table.dataTable tbody td,
.dataTables_wrapper .dataTables_info,
.status-badge,
td, th,
time,
.tabular {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ========================================================
   RESPONSIVE TYPOGRAPHY
   ========================================================
   The two heading-shrink blocks that used to live here are gone — headings
   now scale continuously via clamp() (see the HEADINGS section above),
   which removes the visible jump at 767px and 639px.

   The old 639px block also set `html { font-size: 15px }`, shrinking EVERY
   dimension expressed in rem — body copy, form labels, table text — on the
   smallest screens. That is backwards: a phone held at arm's length on a
   factory floor needs body text no smaller than on desktop. The root size
   now stays at 16px everywhere.
   ======================================================== */
@media (max-width: 639px) {
  body {
    font-size: var(--fs-base);
    line-height: 1.6;   /* slightly looser for small-screen reading */
  }

  p {
    margin-bottom: 1rem; /* Reduce vertical rhythm on mobile */
  }

  ul, ol {
    margin-bottom: 1rem;
    padding-left: 1.25rem;
  }
}

/* ========================================================
   DARK THEME TYPOGRAPHY
   ======================================================== */

html.dark-theme body {
  color: var(--text-main);
}

/* REMOVED in v1.16.0: `html.dark-theme a { color: var(--brand-ink) }` and its
   :hover twin.

   They were already redundant — both themes define --brand-ink, so the plain
   `a` rule near the top of this file resolves correctly on dark, and the
   comment above them said as much. What was not obvious is that they were
   also harmful. `html.dark-theme a` is specificity (0,1,2); a component rule
   like `a.btn-submit` is (0,1,1). So the dark-theme rule WON, and every link
   styled as a button — the Admin Panel's backup downloads, and anything
   similar added later — rendered its label in --brand-ink ON the --brand
   fill in dark mode: pale orange on orange, measured at 1.2:1.

   That is the FILL-vs-INK failure from CLAUDE.md arriving through a route the
   rule there does not cover: not a hardcoded #fff, but the default anchor
   colour outranking the component that meant to override it. Deleting these
   two lines removes the landmine rather than working around it one component
   at a time. Do not reinstate them. */

html.dark-theme code {
  background: rgba(255, 255, 255, 0.1);
  color: var(--text);
}

html.dark-theme pre {
  background: rgba(0, 0, 0, 0.3);
  color: var(--text);
}

html.dark-theme blockquote {
  background: var(--brand-wash);
  color: var(--text-muted);
}

/* ========================================================
   PRINT STYLES
   ======================================================== */

@media print {
  body {
    font-size: 12pt;
    line-height: 1.5;
  }

  h1, h2, h3 {
    page-break-after: avoid;
  }

  p, ul, ol {
    page-break-inside: avoid;
  }
}

/* ===== layout/grid.css ===== */
/* ========================================================
   RESPONSIVE GRID & LAYOUT SYSTEM
   Mobile-first approach with CSS Grid, Flexbox, and Container Queries
   ======================================================== */

/* ========================================================
   BREAKPOINT VARIABLES
   ======================================================== */

:root {
  /* Breakpoints (mobile-first) */
  --bp-sm: 640px;      /* Tablet: 640px+ */
  --bp-md: 768px;      /* Desktop: 768px+ */
  --bp-lg: 1024px;     /* Wide desktop: 1024px+ */
  --bp-xl: 1280px;     /* Extra wide: 1280px+ */

  /* Container sizes */
  --container-sm: 540px;
  --container-md: 720px;
  --container-lg: 960px;
  --container-xl: 1140px;
  --container-2xl: 1320px;

  /* Gap/spacing utilities */
  --gap-0: 0;
  --gap-1: 0.5rem;
  --gap-2: 1rem;
  --gap-3: 1.5rem;
  --gap-4: 2rem;
  --gap-5: 2.5rem;
  --gap-6: 3rem;
}

/* ========================================================
   CONTAINER COMPONENT
   ======================================================== */

.container {
  width: 100%;
  margin-left: auto;
  margin-right: auto;
  padding-left: var(--space-3);
  padding-right: var(--space-3);
}

/* Mobile (default, no max-width) */
.container {
  max-width: 100%;
}

/* Tablet (640px+) */
@media (min-width: 640px) {
  .container {
    max-width: var(--container-sm);
  }
}

/* Desktop (768px+) */
@media (min-width: 768px) {
  .container {
    max-width: var(--container-lg);
  }
}

/* Wide Desktop (1024px+) */
@media (min-width: 1024px) {
  .container {
    max-width: var(--container-xl);
  }
}

/* Extra Wide (1280px+) */
@media (min-width: 1280px) {
  .container {
    max-width: var(--container-2xl);
  }
}

/* Fluid container (no max-width) */
.container-fluid {
  width: 100%;
  padding-left: var(--space-3);
  padding-right: var(--space-3);
  margin-left: auto;
  margin-right: auto;
}

/* ========================================================
   GRID SYSTEM
   ======================================================== */

.grid {
  display: grid;
  gap: var(--gap-3);
  width: 100%;
}

/* Mobile: 1 column (default) */
.grid {
  grid-template-columns: 1fr;
}

/* Tablet breakpoint (640px+) */
@media (min-width: 640px) {
  /* Default 2-column on tablet */
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .grid-cols-1-sm { grid-template-columns: 1fr; }
  .grid-cols-2-sm { grid-template-columns: repeat(2, 1fr); }
  .grid-cols-3-sm { grid-template-columns: repeat(3, 1fr); }
  .grid-cols-4-sm { grid-template-columns: repeat(4, 1fr); }
  .grid-cols-6-sm { grid-template-columns: repeat(6, 1fr); }
}

/* Desktop breakpoint (768px+) */
@media (min-width: 768px) {
  /* Default 3-column on desktop */
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }

  .grid-cols-1-md { grid-template-columns: 1fr; }
  .grid-cols-2-md { grid-template-columns: repeat(2, 1fr); }
  .grid-cols-3-md { grid-template-columns: repeat(3, 1fr); }
  .grid-cols-4-md { grid-template-columns: repeat(4, 1fr); }
  .grid-cols-6-md { grid-template-columns: repeat(6, 1fr); }
  .grid-cols-12-md { grid-template-columns: repeat(12, 1fr); }
}

/* Wide Desktop (1024px+) */
@media (min-width: 1024px) {
  .grid-cols-1-lg { grid-template-columns: 1fr; }
  .grid-cols-2-lg { grid-template-columns: repeat(2, 1fr); }
  .grid-cols-3-lg { grid-template-columns: repeat(3, 1fr); }
  .grid-cols-4-lg { grid-template-columns: repeat(4, 1fr); }
  .grid-cols-6-lg { grid-template-columns: repeat(6, 1fr); }
  .grid-cols-12-lg { grid-template-columns: repeat(12, 1fr); }
}

/* Extra Wide (1280px+)
   ---------------------------------------------------------------------
   These were MISSING. templates/entry_form.html has always been written as
   `grid grid-cols-1-lg grid-cols-3-xl` with a `col-span-2-xl` form pane
   and a recent-records sidebar — but with no -xl classes defined, the
   page silently collapsed to a single column at every width, and the
   "Recent Records" panel dropped below the form instead of sitting beside
   it. Defining them makes the page render as it was authored. */
@media (min-width: 1280px) {
  .grid-cols-1-xl { grid-template-columns: 1fr; }
  .grid-cols-2-xl { grid-template-columns: repeat(2, 1fr); }
  .grid-cols-3-xl { grid-template-columns: repeat(3, 1fr); }
  .grid-cols-4-xl { grid-template-columns: repeat(4, 1fr); }
  .grid-cols-6-xl { grid-template-columns: repeat(6, 1fr); }
  .grid-cols-12-xl { grid-template-columns: repeat(12, 1fr); }

  .col-span-1-xl { grid-column: span 1; }
  .col-span-2-xl { grid-column: span 2; }
  .col-span-3-xl { grid-column: span 3; }
  .col-span-4-xl { grid-column: span 4; }
  .col-span-6-xl { grid-column: span 6; }
  .col-span-8-xl { grid-column: span 8; }
  .col-span-12-xl { grid-column: span 12; }
}

/* Explicit, unscoped column counts. Defined after the breakpoint blocks above
   so that an explicit .grid-cols-N always wins over the default per-breakpoint
   column count set on .grid, regardless of viewport width. */
.grid-cols-2 {
  grid-template-columns: repeat(2, 1fr);
}

.grid-cols-3 {
  grid-template-columns: repeat(3, 1fr);
}

.grid-cols-4 {
  grid-template-columns: repeat(4, 1fr);
}

.grid-cols-6 {
  grid-template-columns: repeat(6, 1fr);
}

.grid-cols-12 {
  grid-template-columns: repeat(12, 1fr);
}

/* ========================================================
   GAP UTILITIES
   ======================================================== */

.gap-0 { gap: var(--gap-0); }
.gap-1 { gap: var(--gap-1); }
.gap-2 { gap: var(--gap-2); }
.gap-3 { gap: var(--gap-3); }
.gap-4 { gap: var(--gap-4); }
.gap-5 { gap: var(--gap-5); }
.gap-6 { gap: var(--gap-6); }

/* Column gap only */
.gap-x-0 { column-gap: var(--gap-0); }
.gap-x-1 { column-gap: var(--gap-1); }
.gap-x-2 { column-gap: var(--gap-2); }
.gap-x-3 { column-gap: var(--gap-3); }
.gap-x-4 { column-gap: var(--gap-4); }
.gap-x-5 { column-gap: var(--gap-5); }
.gap-x-6 { column-gap: var(--gap-6); }

/* Row gap only */
.gap-y-0 { row-gap: var(--gap-0); }
.gap-y-1 { row-gap: var(--gap-1); }
.gap-y-2 { row-gap: var(--gap-2); }
.gap-y-3 { row-gap: var(--gap-3); }
.gap-y-4 { row-gap: var(--gap-4); }
.gap-y-5 { row-gap: var(--gap-5); }
.gap-y-6 { row-gap: var(--gap-6); }

/* ========================================================
   FLEXBOX LAYOUT
   ======================================================== */

.flex {
  display: flex;
}

.flex-col {
  flex-direction: column;
}

.flex-row {
  flex-direction: row;
}

.flex-wrap {
  flex-wrap: wrap;
}

.flex-nowrap {
  flex-wrap: nowrap;
}

/* Responsive flex-direction / alignment (matches the -sm/-md/-lg suffix
   convention used elsewhere in this file; this codebase does not use
   Tailwind's colon-prefixed variant syntax like `md:flex-row`). */
@media (min-width: 768px) {
  .flex-row-md { flex-direction: row; }
  .items-center-md { align-items: center; }
}

/* Justify content */
.justify-start { justify-content: flex-start; }
.justify-center { justify-content: center; }
.justify-end { justify-content: flex-end; }
.justify-between { justify-content: space-between; }
.justify-around { justify-content: space-around; }
.justify-evenly { justify-content: space-evenly; }

/* Align items */
.items-start { align-items: flex-start; }
.items-center { align-items: center; }
.items-end { align-items: flex-end; }
.items-stretch { align-items: stretch; }
.items-baseline { align-items: baseline; }

/* Align self */
.self-start { align-self: flex-start; }
.self-center { align-self: center; }
.self-end { align-self: flex-end; }
.self-stretch { align-self: stretch; }

/* Flex grow/shrink */
.flex-1 { flex: 1 1 0%; }
.flex-auto { flex: 1 1 auto; }
.flex-none { flex: none; }

/* ========================================================
   COLUMN UTILITIES
   ======================================================== */

.col-span-1 { grid-column: span 1; }
.col-span-2 { grid-column: span 2; }
.col-span-3 { grid-column: span 3; }
.col-span-4 { grid-column: span 4; }
.col-span-6 { grid-column: span 6; }
.col-span-12 { grid-column: span 12; }

/* Responsive column spans */
@media (min-width: 768px) {
  .col-span-1-md { grid-column: span 1; }
  .col-span-2-md { grid-column: span 2; }
  .col-span-3-md { grid-column: span 3; }
  .col-span-4-md { grid-column: span 4; }
  .col-span-6-md { grid-column: span 6; }
  .col-span-8-md { grid-column: span 8; }
  .col-span-12-md { grid-column: span 12; }
}

/* ========================================================
   PADDING & MARGIN UTILITIES
   ======================================================== */

/* Padding */
.p-0 { padding: 0; }
.p-1 { padding: var(--space-1); }
.p-2 { padding: var(--space-2); }
.p-3 { padding: var(--space-3); }
.p-4 { padding: var(--space-4); }
.p-5 { padding: var(--space-5); }
.p-6 { padding: 3rem; }

/* Padding X (horizontal) */
.px-0 { padding-left: 0; padding-right: 0; }
.px-1 { padding-left: var(--space-1); padding-right: var(--space-1); }
.px-2 { padding-left: var(--space-2); padding-right: var(--space-2); }
.px-3 { padding-left: var(--space-3); padding-right: var(--space-3); }
.px-4 { padding-left: var(--space-4); padding-right: var(--space-4); }

/* Padding Y (vertical) */
.py-0 { padding-top: 0; padding-bottom: 0; }
.py-1 { padding-top: var(--space-1); padding-bottom: var(--space-1); }
.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.py-3 { padding-top: var(--space-3); padding-bottom: var(--space-3); }
.py-4 { padding-top: var(--space-4); padding-bottom: var(--space-4); }

/* Margin
   NOTE: `.m-0` is intentionally NOT defined here. It lives in
   base/utilities.css with `!important`, which is load-bearing (it has to
   beat the heading margins set in base/typography.css). Two competing
   definitions made which-one-wins depend on <link> order. */
.m-1 { margin: var(--space-1); }
.m-2 { margin: var(--space-2); }
.m-3 { margin: var(--space-3); }
.m-4 { margin: var(--space-4); }

/* Margin X (horizontal) */
.mx-0 { margin-left: 0; margin-right: 0; }
.mx-1 { margin-left: var(--space-1); margin-right: var(--space-1); }
.mx-2 { margin-left: var(--space-2); margin-right: var(--space-2); }
.mx-3 { margin-left: var(--space-3); margin-right: var(--space-3); }
.mx-auto { margin-left: auto; margin-right: auto; }

/* Margin Y (vertical) */
.my-0 { margin-top: 0; margin-bottom: 0; }
.my-1 { margin-top: var(--space-1); margin-bottom: var(--space-1); }
.my-2 { margin-top: var(--space-2); margin-bottom: var(--space-2); }
.my-3 { margin-top: var(--space-3); margin-bottom: var(--space-3); }
.my-4 { margin-top: var(--space-4); margin-bottom: var(--space-4); }

/* ========================================================
   WIDTH & HEIGHT UTILITIES
   ======================================================== */

.w-full { width: 100%; }
.w-screen { width: 100vw; }
.w-auto { width: auto; }
.w-1/2 { width: 50%; }
.w-1/3 { width: 33.333%; }
.w-2/3 { width: 66.666%; }
.w-1/4 { width: 25%; }
.w-3/4 { width: 75%; }

.h-full { height: 100%; }
.h-screen { height: 100vh; }
.h-auto { height: auto; }
.min-h-full { min-height: 100%; }
.min-h-screen { min-height: 100vh; }

/* ========================================================
   POSITION & DISPLAY UTILITIES
   ======================================================== */

.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.sticky { position: sticky; }
.static { position: static; }

.block { display: block; }
.inline { display: inline; }
.inline-block { display: inline-block; }
/* NOTE: `.hidden` is intentionally NOT defined here. It lives in
   base/utilities.css with `!important`, which is load-bearing: it must beat
   `.form-group { display: flex }` from components/forms.css, which is linked
   after this file. A non-important `.hidden` here would have left every
   conditional "Other (Specify)" field in the entry forms permanently
   visible if utilities.css were ever reordered. */

/* ========================================================
   VISIBILITY UTILITIES
   ======================================================== */

/* Hide on mobile, show on tablet+ */
.hidden-mobile {
  display: none;
}

@media (min-width: 640px) {
  .hidden-mobile {
    display: block;
  }
}

/* Show on mobile, hide on tablet+ */
.hidden-desktop {
  display: block;
}

@media (min-width: 768px) {
  .hidden-desktop {
    display: none;
  }
}

/* Hide on mobile, show on desktop+ */
.hidden-tablet {
  display: none;
}

@media (min-width: 768px) {
  .hidden-tablet {
    display: block;
  }
}

/* ========================================================
   OVERFLOW UTILITIES
   ======================================================== */

.overflow-auto { overflow: auto; }
.overflow-hidden { overflow: hidden; }
.overflow-x-auto { overflow-x: auto; }
.overflow-y-auto { overflow-y: auto; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ========================================================
   RESPONSIVE WRAPPER (For responsive tables/content)
   ======================================================== */

.responsive-wrapper {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.responsive-wrapper table {
  width: 100%;
  min-width: 500px;
}

/* ========================================================
   SIDEBAR + MAIN LAYOUT
   ======================================================== */

.layout-with-sidebar {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--gap-3);
  width: 100%;
  min-height: 100vh;
}

/* Stack sidebar on top on mobile */
@media (max-width: 1024px) {
  .layout-with-sidebar {
    grid-template-columns: 1fr;
  }
}

/* ========================================================
   FEATURE-FLAG: CONTAINER QUERIES
   (For modern browsers that support @container)
   ======================================================== */

@supports (container-type: inline-size) {
  /* Container query example for cards */
  .card-container {
    container-type: inline-size;
  }

  /* Responsive card layout based on container width, not viewport */
  .card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: var(--gap-3);
  }

  @container (max-width: 500px) {
    .card-grid {
      grid-template-columns: 1fr;
    }
  }

  @container (min-width: 500px) and (max-width: 800px) {
    .card-grid {
      grid-template-columns: repeat(2, 1fr);
    }
  }

  @container (min-width: 800px) {
    .card-grid {
      grid-template-columns: repeat(3, 1fr);
    }
  }
}

/* ========================================================
   ASPECT RATIO UTILITIES
   ======================================================== */

.aspect-square { aspect-ratio: 1; }
.aspect-video { aspect-ratio: 16 / 9; }
.aspect-4-3 { aspect-ratio: 4 / 3; }
.aspect-3-2 { aspect-ratio: 3 / 2; }
.aspect-16-10 { aspect-ratio: 16 / 10; }

/* ========================================================
   BORDER RADIUS UTILITIES
   ======================================================== */

.rounded-none { border-radius: 0; }
.rounded-sm { border-radius: 4px; }
.rounded { border-radius: 8px; }
.rounded-md { border-radius: 8px; }
.rounded-lg { border-radius: 12px; }
.rounded-xl { border-radius: 16px; }
.rounded-full { border-radius: 9999px; }

/* ========================================================
   SHADOW UTILITIES
   ======================================================== */

.shadow-none { box-shadow: none; }
.shadow-sm { box-shadow: var(--shadow-sm); }
.shadow-md { box-shadow: var(--shadow-md); }
.shadow-lg { box-shadow: 0 10px 30px rgba(16, 24, 40, 0.12); }
.shadow-xl { box-shadow: 0 20px 40px rgba(16, 24, 40, 0.15); }

/* ========================================================
   BORDER UTILITIES
   ======================================================== */

.border-0 { border: none; }
.border { border: 1px solid var(--card-border); }
.border-2 { border: 2px solid var(--card-border); }
.border-t { border-top: 1px solid var(--card-border); }
.border-b { border-bottom: 1px solid var(--card-border); }
.border-l { border-left: 1px solid var(--card-border); }
.border-r { border-right: 1px solid var(--card-border); }

/* ========================================================
   OPACITY UTILITIES
   ======================================================== */

.opacity-0 { opacity: 0; }
.opacity-25 { opacity: 0.25; }
.opacity-50 { opacity: 0.5; }
.opacity-75 { opacity: 0.75; }
.opacity-100 { opacity: 1; }

/* ========================================================
   CURSOR UTILITIES
   ======================================================== */

.cursor-auto { cursor: auto; }
.cursor-default { cursor: default; }
.cursor-pointer { cursor: pointer; }
.cursor-not-allowed { cursor: not-allowed; }
.cursor-move { cursor: move; }
.cursor-text { cursor: text; }

/* ========================================================
   TRANSITION UTILITIES
   ======================================================== */

.transition-none { transition: none; }
.transition-all { transition: all 0.2s ease; }
.transition-colors { transition: color, background-color 0.2s ease; }
.transition-opacity { transition: opacity 0.2s ease; }
.transition-transform { transition: transform 0.2s ease; }

/* ===== components/buttons.css ===== */
/* Base Button Class */
.btn, .btn-primary, .btn-secondary, .btn-submit, .btn-danger, .btn-action, .btn-edit, .btn-import, .btn-export {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px; /* Standardize icon spacing */
    border-radius: var(--radius);
    font-weight: 600;
    border: none;
    cursor: pointer;
    padding: 10px 16px;
    min-height: 44px;
    font-size: 14px;
    transition: background var(--dur-fast, 120ms) var(--ease-out),
                color var(--dur-fast, 120ms) var(--ease-out),
                border-color var(--dur-fast, 120ms) var(--ease-out),
                box-shadow var(--dur-fast, 120ms) var(--ease-out);
    text-decoration: none;
}

.btn:focus-visible, .btn-primary:focus-visible, .btn-secondary:focus-visible, .btn-submit:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/* Primary / Submit
   `color` is --on-brand, NOT #ffffff. In dark mode --brand is the lighter
   orange (#E3913B) and white text on it is 2.4:1; --on-brand flips to the
   near-black page ink and keeps it at 6.7:1. See the token block in
   layout/main.css. */
.btn-primary, .btn-submit, .btn--primary {
    background: var(--brand);
    color: var(--on-brand);
    box-shadow: var(--elev-1);
}
.btn-primary:hover, .btn-submit:hover, .btn--primary:hover {
    background: var(--brand-hover);
    box-shadow: var(--elev-2);
}
/* The hover used to be `transform: translateY(-2px)` plus a coloured glow.
   Both are dropped: a button that moves out from under a gloved thumb on a
   shop-floor tablet is a mis-tap, and the glow was the last piece of the
   soft-shadow style the redesign replaces with flat fills. */

/* Secondary */
.btn-secondary, .btn--secondary {
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text-muted);
}
.btn-secondary:hover, .btn--secondary:hover {
    background: var(--brand-wash);
    border-color: var(--brand);
    color: var(--brand-ink);
}
html.dark-theme .btn-secondary, html.dark-theme .btn--secondary {
    background: var(--surface-raised);
}
html.dark-theme .btn-secondary:hover, html.dark-theme .btn--secondary:hover {
    background: var(--brand-wash-strong);
}

/* Danger / Delete */
.btn-danger {
    background: rgba(var(--accent-red-rgb), 0.12);
    color: var(--negative-ink);
    border: 1px solid rgba(var(--accent-red-rgb), 0.28);
}
.btn-danger:hover {
    background: var(--negative);
    color: var(--on-negative);
    border-color: var(--negative);
}

/* Small Buttons (Edit, Action) */
.btn-sm, .btn-edit {
    min-height: 36px;
    min-width: 36px;
    padding: 6px 12px;
}
.btn-edit {
    background: var(--brand-wash-strong);
    color: var(--brand-ink);
}
.btn-edit:hover {
    background: var(--brand);
    color: var(--on-brand);
}

/* Login specific */
.btn-login { width: 100%; padding: 12px 20px; }

/* ===== components/cards.css ===== */
/* Card surfaces.
   The border was `rgba(15,23,42,.06)` — a cool navy at 6%, which on the new
   warm bone page background read as a faint blue edge. It is now the shared
   --border token, so a card edge matches a table rule and an input outline
   instead of being a third, slightly different hairline. */
.card{background:var(--surface);padding:var(--space-3);border-radius:var(--radius-lg,16px);box-shadow:var(--elev-1);border:1px solid var(--border);overflow:hidden}
.card--grid{display:grid;grid-template-columns:1fr;gap:var(--space-3)}
@media (min-width:768px){.card--grid{grid-template-columns:repeat(3,1fr)}}

/* --- Access-role badge -------------------------------------------------
   Lives here rather than in admin.html's <style> block because the badge is
   now rendered in the sidebar on EVERY page, not just the Admin Panel. The
   admin-only copy meant the sidebar badge rendered as unstyled text
   everywhere else. One rule per role, keyed on the canonical role name so a
   legacy 'admin' row still picks up the Super Admin colour after
   normalisation.

   SOLID fills with white text, not the previous 12%-tint-plus-coloured-text.
   The badge renders in two very different places — a white admin table and
   the dark sidebar — and a translucent tint only works against one of them:
   .role-viewer used --text-muted on a 14% grey, which on the dark sidebar
   was dark-grey text on near-black. A solid fill carries its own contrast
   (5.0-9.3:1 for white on each) and therefore reads on any surface. */
.role-badge{display:inline-block;padding:3px 8px;border-radius:var(--radius-sm,8px);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:#fff}
.role-super_admin{background:#6B4E9E}   /* 6.5:1 */
.role-manager{background:var(--timber-500)}  /* 5.0:1 — brand orange */
.role-operator{background:var(--green-500)}  /* 5.1:1 */
.role-viewer{background:#7E6B5A}        /* 5.1:1 — warm stone */
/* Retained so an un-migrated database still renders a coloured badge. */
.role-admin{background:#6B4E9E;color:#fff}

/* --- Web-app expiry banner ---------------------------------------------
   Rendered at the top of <main> by base.html, and only ever for an account
   holding MANAGE_SYSTEM. A free PythonAnywhere web app expires monthly and
   stops serving with no warning of its own; this is the warning.

   A banner, not a toast: a toast dismisses itself after a few seconds, and
   the entire value of this element is that it will not go away until
   somebody presses renew on the PythonAnywhere Web tab.

   FILL + ON-FILL, per the colour rule in CLAUDE.md. --caution and --negative
   are background fills, so the text on them takes --on-caution /
   --on-negative and never a hardcoded #fff — in dark mode both fills are
   LIGHTER than the page, and white text on them drops to about 2.4:1. */
.expiry-banner{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 var(--space-3,16px);padding:12px 16px;border-radius:var(--radius-sm,8px);font-size:14px;font-weight:600;background:var(--caution);color:var(--on-caution)}
.expiry-banner.urgent{background:var(--negative);color:var(--on-negative)}
/* The link inherits the on-fill colour rather than taking --brand-ink, which
   is a text token picked for the page background and has no contrast
   guarantee against an amber or red fill. Underlined so it is still
   identifiable as a link once it is the same colour as the sentence. */
.expiry-banner a{color:inherit;text-decoration:underline;margin-left:auto;white-space:nowrap}
.expiry-banner a:hover{color:inherit}
@media (max-width:767px){.expiry-banner a{margin-left:0}}

/* ===== components/forms.css ===== */
.form-group{display:flex;flex-direction:column;margin-bottom:var(--space-3)}
label{font-size:.875rem;margin-bottom:.5rem;color:var(--color-muted)}
/* The border was rgba(15,23,42,.08) — a cool navy tint that read as a faint
   blue edge against the warm surfaces, and that had no dark-theme value at
   all. It is now the shared --border token, so an input outline matches a
   card edge and a table rule. */
input[type="text"],input[type="email"],select,textarea{padding:.75rem;border:1px solid var(--border);border-radius:var(--radius-md,10px);font-size:1rem;background:var(--input-bg);color:var(--text)}

/* ========================================================
   INLINE FIELD ERRORS
   ========================================================
   The .validation-msg span already existed under every field but was never
   populated — a validation failure showed up only as a generic toast, so
   the operator had to guess which of a dozen fields was wrong. The server
   now returns per-field messages and static/js/main.js fills these in.
   ======================================================== */
.input-error,
input.input-error,
select.input-error,
textarea.input-error {
  border-color: var(--negative) !important;
  box-shadow: 0 0 0 3px rgba(var(--accent-red-rgb), .18) !important;
}

.validation-msg {
  display: block;
  font-size: 12px;
  line-height: 1.4;
  color: var(--negative-ink);
  margin-top: 4px;
  min-height: 14px;   /* reserve the space so filling it doesn't shift layout */
}

/* Only show the warning glyph when there is actually a message. */
.validation-msg:not(:empty)::before {
  content: "⚠ ";
  font-size: 11px;
}

/* ===== components/icons.css ===== */
/* ========================================================
   INLINE SVG ICONS
   ========================================================
   Replaces Font Awesome. Glyph geometry lives in the sprite
   (templates/partials/_icon_sprite.html); everything about how an icon
   LOOKS is set here, so restyling the whole icon set is a one-file change.

   Sized in `em` so an icon inherits the font-size of whatever contains it —
   exactly how the Font Awesome icons behaved, which means existing
   `font-size` rules on icon containers keep working unchanged.
   ======================================================== */
.icon {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: -0.125em;   /* optical centring against the text baseline */
  flex-shrink: 0;             /* never squash inside a flex row */

  /* Lucide icons are stroked, not filled. These live on the class rather
     than on each <symbol> so one change restyles all of them. */
  fill: none;
  stroke: currentColor;       /* inherits colour from the text around it */
  stroke-width: 1.75;         /* slightly lighter than Lucide's default 2,
                                 which is drawn for 24px and reads heavy at
                                 the 13-16px sizes used through this UI */
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Solid-ish emphasis for status glyphs that need to read at a glance
   (toast icons, inventory status). Thicker stroke, not a fill, so the
   icon stays legible at 12-14px. */
.icon-bold { stroke-width: 2.25; }

/* Explicit size steps. Prefer these over ad-hoc font-size on the parent. */
.icon-sm { width: 0.875em; height: 0.875em; }
.icon-lg { width: 1.25em; height: 1.25em; }
.icon-xl { width: 1.5em; height: 1.5em; }

/* ========================================================
   SPIN (replaces Font Awesome's fa-spin)
   ======================================================== */
.icon-spin {
  animation: iconSpin 0.9s linear infinite;
  transform-origin: center;
}

@keyframes iconSpin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  /* Keep a slow spin rather than freezing entirely — a motionless spinner
     next to a disabled "Processing..." button reads as a hung form. */
  .icon-spin { animation-duration: 2.4s; }
}

/* ========================================================
   CONTEXT TWEAKS
   ======================================================== */
/* Icons inside buttons sit next to a text label; the gap is owned by the
   button's flex layout, so the icon just needs to not add its own. */
.btn .icon, .btn-action .icon, .nav-logout .icon { margin: 0; }

/* The large decorative icons on empty states and error pages. */
.empty-state .icon { width: 48px; height: 48px; stroke-width: 1.25; opacity: 0.55; }
.error-icon.icon   { width: 56px; height: 56px; stroke-width: 1.25; }

/* KPI icon boxes size their glyph via font-size on .kpi-icon-box. */
.kpi-icon-box .icon { width: 1.1em; height: 1.1em; }

/* ===== components/notifications.css ===== */
/* ========================================================
   NOTIFICATION PANEL
   ========================================================
   A toast is gone in four seconds. That is the right lifetime for a
   confirmation and the wrong one for everything else: an operator who looks
   up from a docket half a second late has no way to find out what the app
   just told them, and a low-stock warning raised on the dashboard is
   invisible from every other screen.

   This is the record of those messages — every toast the session has
   raised, plus the system alerts and version updates that never got one —
   reachable from the bell in the mobile header and from the sidebar on
   desktop.

   WHERE THE DATA LIVES: entirely in the browser (localStorage), written by
   static/js/main.js. There is no notifications table and no new route. On
   the PythonAnywhere free tier that is not a shortcut, it is the design:
   the single web worker loses all in-memory state on every reload, and a
   database-backed notification feed would add a table, a migration, a poll
   endpoint and a query to every page render in exchange for something the
   client already knows.

   Structure, so the media queries below make sense:
     .notif-bell     the trigger. Rendered twice (mobile header + sidebar),
                     class-based rather than id-based for exactly that reason.
     .notif-overlay  the scrim behind the panel.
     .notif-panel    the panel itself — a right-hand drawer on desktop, a
                     full-height sheet on a phone.
   ======================================================== */


/* ---- Trigger ------------------------------------------------------- */

.notif-bell {
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    background: none;
    border: none;
    color: inherit;
    font-family: inherit;
    font-size: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.notif-bell:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: 8px;
}

/* Inside the sidebar the trigger is one nav item among several, so it has
   to match them rather than the header's bare icon button. */
.nav-links .notif-bell {
    width: 100%;
    justify-content: flex-start;
    padding: 10px 16px;
    border-radius: 10px;
    color: rgba(255, 255, 255, 0.7);
    font-weight: 500;
    transition: all 0.2s;
}
.nav-links .notif-bell:hover { background: rgba(255, 255, 255, 0.08); color: var(--sidebar-text, #fff); }
.nav-links .notif-bell:focus-visible { outline: 2px solid #fff; outline-offset: -2px; }
.nav-links .notif-bell .icon { width: 20px; flex-shrink: 0; }

/* The unread count. Absolutely positioned so it never changes the size of
   the button underneath it — a badge that appears and disappears must not
   shift the rest of the header sideways. */
.notif-badge {
    position: absolute;
    top: 2px;
    left: 22px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: var(--radius-full);
    background: var(--negative);
    color: var(--on-negative);
    font-size: 10px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
}
/* In the sidebar the item is full width, so the badge belongs at the end of
   the row rather than stuck to the icon. */
.nav-links .notif-bell .notif-badge {
    position: static;
    margin-left: auto;
}


/* ---- Overlay ------------------------------------------------------- */

.notif-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-notif-overlay, 1060);
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    transition: opacity var(--dur-base, 200ms) var(--ease-out, ease);
}
.notif-overlay.active { opacity: 1; }


/* ---- Panel --------------------------------------------------------- */

.notif-panel {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: var(--z-notif-panel, 1070);
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    width: min(380px, 100vw);
    background: var(--surface);
    border-left: 1px solid var(--border);
    box-shadow: var(--elev-3);
    /* Off-screen by default and translated in, rather than toggled with
       display: the panel keeps its scroll position and the transition has
       something to animate. */
    transform: translateX(100%);
    transition: transform var(--dur-slow, 320ms) var(--ease-in-out, ease);
    will-change: transform;
}
.notif-panel.active { transform: translateX(0); }

/* `hidden` on the panel/overlay is what keeps them out of the tab order and
   the accessibility tree while closed. It has to be beaten explicitly,
   because [hidden] is display:none and a transform cannot animate from it —
   main.js removes the attribute one frame BEFORE adding .active. */
.notif-panel[hidden],
.notif-overlay[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
    .notif-panel, .notif-overlay { transition: none; }
}

.notif-header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
    padding: var(--space-3);
    border-bottom: 1px solid var(--border);
    /* Clears the notch in the installed app, where the panel is full height
       and there is no browser chrome above it. */
    padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
}
.notif-header h2 {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    font-size: 16px;
    font-weight: 700;
    color: var(--text-main);
}
.notif-header .icon { color: var(--brand); }

.notif-close {
    margin-left: auto;
    min-width: 44px;
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: 8px;
    color: var(--text-muted);
    cursor: pointer;
}
.notif-close:hover { background: var(--input-bg); color: var(--text-main); }
.notif-close:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }


/* ---- Category filters ---------------------------------------------- */

.notif-filters {
    display: flex;
    gap: var(--space-1);
    flex-shrink: 0;
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
.notif-chip {
    flex: 1 0 auto;
    min-height: 34px;
    padding: 0 12px;
    border: 1px solid var(--input-border);
    border-radius: var(--radius-full);
    background: transparent;
    color: var(--text-muted);
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
}
.notif-chip:hover { border-color: var(--brand); color: var(--text-main); }
.notif-chip.active {
    background: var(--brand);
    border-color: var(--brand);
    /* --on-brand, not #fff: on the dark theme --brand is the lighter
       orange and white on it is 2.4:1. */
    color: var(--on-brand);
}
.notif-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }


/* ---- The list ------------------------------------------------------ */

.notif-list {
    flex: 1;
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;   /* don't scroll the page behind the panel */
}

.notif-item {
    border-bottom: 1px solid var(--border);
    /* The colour band is how a warning is told from a confirmation at a
       glance; the per-type colours are set on the modifier classes below. */
    border-left: 3px solid transparent;
}
.notif-item.is-unread { background: var(--brand-wash); }

/* The row inside the <li>.
   The padding lives HERE and not on the .notif-item, because a server
   notification renders this element as an <a> covering the whole row — an
   operator tapping anywhere on "Sale · Awaiting approval" should land on
   Records. Padding on the li would leave a dead 12px gutter around a target
   that is already being aimed at with a thumb. */
.notif-row {
    display: flex;
    gap: var(--space-2);
    padding: var(--space-3);
}

/* A linked row. `color: inherit` so an alert is not rendered in link blue —
   the severity band on the left is the colour signal here, and a blue
   message would compete with it. */
.notif-link {
    color: inherit;
    text-decoration: none;
}
.notif-link:hover { background: var(--input-bg); }
/* Inset, because an outline drawn outside the row is clipped by the
   scrolling list and only half of it is visible on the first and last item. */
.notif-link:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
}
/* The affordance that the row goes somewhere. Only on linked rows, so a
   local "Saved" entry does not look tappable when it is not. */
.notif-link .notif-message::after {
    content: ' ›';
    color: var(--brand-ink);
    font-weight: 700;
}

.notif-item .notif-icon { flex-shrink: 0; width: 18px; height: 18px; margin-top: 2px; }
.notif-item.type-success { border-left-color: var(--positive); }
.notif-item.type-success .notif-icon { color: var(--positive-ink); }
.notif-item.type-error   { border-left-color: var(--negative); }
.notif-item.type-error   .notif-icon { color: var(--negative-ink); }
.notif-item.type-warning { border-left-color: var(--caution-ink); }
.notif-item.type-warning .notif-icon { color: var(--caution-ink); }
.notif-item.type-info    { border-left-color: var(--brand); }
.notif-item.type-info    .notif-icon { color: var(--brand-ink); }

.notif-body { flex: 1; min-width: 0; }
.notif-message {
    margin: 0;
    color: var(--text-main);
    font-size: 13px;
    line-height: 1.45;
    /* Species names, batch IDs and destinations arrive here verbatim; one
       long unbroken string must wrap rather than widen the panel. */
    overflow-wrap: anywhere;
}
.notif-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-top: 4px;
    color: var(--text-muted);
    font-size: 11px;
}
/* The category label. Deliberately quiet: it is there for scanning, not
   reading. */
.notif-cat {
    padding: 1px 6px;
    border: 1px solid var(--input-border);
    border-radius: var(--radius-full);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.notif-empty {
    margin: 0;
    padding: var(--space-5) var(--space-3);
    color: var(--text-muted);
    font-size: 13px;
    text-align: center;
}
.notif-empty .icon { display: block; width: 28px; height: 28px; margin: 0 auto var(--space-2); opacity: 0.5; }

/* ---- Device-notification opt-in ------------------------------------- */
/* Sits between the list and the footer. flex-shrink: 0 for the same reason
   the footer has it — the list above is the flexible child and must be the
   one that gives up height when the panel is short, otherwise this block
   collapses and the button becomes unreadable on a small phone in landscape. */
.notif-os {
    flex-shrink: 0;
    padding: var(--space-3);
    border-top: 1px solid var(--border);
    background: var(--input-bg);
}
.notif-os-btn { width: 100%; }
.notif-os-status {
    margin: var(--space-2) 0 0;
    color: var(--text-muted);
    font-size: 11px;
    line-height: 1.5;
}
/* When the button is hidden — unsupported browser, or permission already
   refused — the status line is the whole content of the block and should not
   carry the margin that separated it from a button that is not there. */
.notif-os-btn[hidden] + .notif-os-status { margin-top: 0; }

.notif-footer {
    display: flex;
    gap: var(--space-2);
    flex-shrink: 0;
    padding: var(--space-3);
    border-top: 1px solid var(--border);
    /* Clear of the home indicator in the installed app. */
    padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
}
.notif-footer .btn { flex: 1; }

/* ===== layout/main.css ===== */
/* ========================================================
   SEMANTIC COLOUR TOKENS (LIGHT & DARK)
   ========================================================
   The meaning-level layer. Components reference THESE, never the raw hex
   palette in base/variables.css.

   Both themes are declared in this file, one block after the other, so a
   value can never be added to light without someone noticing dark is
   missing — which is exactly how the old --color-* set ended up frozen in
   light-mode colours.

   Naming:
     --surface / --surface-sunken / --surface-raised   backgrounds
     --text / --text-muted                             foreground
     --border                                          hairlines
     --brand / --positive / --negative / --caution     accents (FILLS)
     --*-ink                                           same, safe as TEXT
   ======================================================== */
:root {
    /* ---- Semantic: surfaces ----
       Warm bone rather than the old pale blue. Cards are pure white and sit
       on it, so the card edge is visible from the surface change alone and
       does not need a shadow to be found. */
    --surface:        #ffffff;
    --surface-sunken: #FAF6F0;   /* page background, behind cards */
    --surface-raised: #ffffff;   /* cards sitting on the sunken surface */
    --border:         var(--timber-300);

    /* ---- Semantic: text ----
       text       16.8:1 on white   text-muted  6.0:1 on white */
    --text:       var(--bark-900);
    --text-muted: var(--stone-500);

    /* ---- Semantic: accents (FILLS — white text on them) ----
       brand    5.0:1   positive 5.1:1   negative 5.6:1   (all AA)
       caution  2.3:1 — a fill ONLY. Never put text on it; use --caution-ink. */
    --brand:       var(--timber-500);
    --brand-hover: var(--timber-700);
    --positive:    var(--green-500);
    --negative:    var(--red-500);
    --caution:     var(--amber-500);

    /* ---- Semantic: accents (safe as TEXT on a light surface) ----
       brand-ink 7.2:1  positive-ink 7.9:1  negative-ink 7.9:1  caution-ink 5.1:1 */
    --brand-ink:    var(--timber-700);
    --positive-ink: var(--green-700);
    --negative-ink: var(--red-700);
    --caution-ink:  var(--amber-700);

    /* ---- Semantic: brand washes ----
       Tinted backgrounds for selected rows, active chips and hover states.
       Named separately so a component never has to invent an rgba() of the
       brand and get the opacity subtly different from its neighbour. */
    --brand-wash:        var(--timber-50);
    --brand-wash-strong: var(--timber-100);

    /* ---- Semantic: INK ON A FILL ----
       What colour text/icons take when they sit ON --brand, --positive etc.
       This pair is the reason the dark theme does not break: in light mode
       the fills are dark enough for white text (5.0-5.6:1), in dark mode the
       same fills are LIGHTER than the page, so white on them collapses to
       2.4:1 and the ink has to flip to near-black. Hardcoding `color: #fff`
       next to `background: var(--brand)` is the bug this token prevents. */
    --on-brand:    #ffffff;   /* 5.0:1 */
    --on-positive: #ffffff;   /* 5.1:1 */
    --on-negative: #ffffff;   /* 5.6:1 */
    --on-caution:  #241C16;   /* 7.4:1 — caution is too light for white */

    /* ---- CHART SERIES ----
       Eight categorical colours, ordered so the first four are the ones a
       reader already knows the meaning of (brand, good, bad, watch) and the
       rest are neutral fillers. Previously the dashboard read the first four
       from tokens and then hardcoded a sky blue, a violet and two others that
       belonged to no palette at all. All eight are >= 3:1 on the surface,
       which is the WCAG threshold for a graphical object. */
    --chart-1: #A85703;   /* timber orange — brand */
    --chart-2: #2E7D32;   /* forest */
    --chart-3: #1F6F8B;   /* deep teal */
    --chart-4: #C62828;   /* brick */
    --chart-5: #A67C00;   /* bronze */
    --chart-6: #5D4037;   /* bark */
    --chart-7: #6A7B8C;   /* slate */
    --chart-8: #7E57A6;   /* plum */

    /* ---- Component-level tokens ----
       MINIMAL GRADIENTS. The page background, the sidebar and the H1 were
       each a gradient before; all three are now flat. A gradient behind
       translucent cards forced a backdrop-filter blur on every card to keep
       text legible, which is the single most expensive thing this UI did per
       scroll frame on a mid-range Android handset. Flat surfaces let the
       cards be opaque. */
    --bg-gradient: var(--surface-sunken);
    --card-bg: rgba(255, 255, 255, 0.94);   /* near-opaque, drawer only */
    --text-gradient: none;                  /* kept: rules still reference it */

    --sidebar-width: 260px;
    --sidebar-bg: var(--bark-800);
    --sidebar-text: #ffffff;                /* 16.3:1 on --bark-800 */
    --sidebar-muted: rgba(255, 255, 255, 0.66);
    /* The sidebar is dark in BOTH themes, so its active state needs its own
       pair rather than reusing --brand + white. In dark mode --brand is the
       lighter orange, and white on that is 2.4:1 — the active nav item was
       the one place the theme flip could break contrast. */
    --sidebar-active-bg:   var(--timber-500);
    --sidebar-active-text: #ffffff;         /* 5.0:1 */

    /* ---- LEGACY ALIASES ----
       The names the existing rules already use, now pointing at the
       semantic tokens above rather than carrying their own copies of the
       hex values. One place to change a colour; both themes follow
       automatically.

       The -rgb triplets stay literal because they are consumed inside
       rgba(...) expressions that need the raw channel components — CSS has
       no way to decompose a colour value back into its channels. */
    --bg-color:       var(--surface-sunken);
    --card-bg-solid:  var(--surface);
    --card-border:    var(--border);
    --text-main:      var(--text);
    --input-bg:       var(--surface);
    --input-border:   var(--border);

    --accent-blue:        var(--brand);
    --accent-blue-hover:  var(--brand-hover);
    --accent-green:       var(--positive);
    --accent-red:         var(--negative);
    --accent-orange:      var(--caution);
    --accent-green-ink:   var(--positive-ink);
    --accent-red-ink:     var(--negative-ink);
    --accent-orange-ink:  var(--caution-ink);

    --accent-blue-rgb:   168, 87, 3;
    --accent-green-rgb:  46, 125, 50;
    --accent-red-rgb:    198, 40, 40;
    --accent-orange-rgb: 217, 164, 4;
}

/* Dark Theme Overrides — warm graphite with the logo orange lifted to a
   lighter tint. The old dark theme was a different brand from the light one:
   electric blue, neon cyan and a lime "positive" that appeared nowhere in
   light mode. Both themes are now the same palette at two lightnesses. */
html.dark-theme {
    /* ---- Semantic: surfaces ----
       Warm graphite (a trace of red), not neutral #1E1E1E. Next to the
       orange brand a neutral dark reads slightly blue. */
    --surface:        #1C1714;
    --surface-sunken: #131010;
    --surface-raised: #262019;
    --border:         rgba(235, 217, 194, 0.14);

    /* ---- Semantic: text ----
       text 15.6:1 on surface   text-muted 7.4:1 */
    --text:       #F5EFE7;
    --text-muted: #B3A597;

    /* ---- Semantic: accents (FILLS — DARK text on them on this theme) ----
       brand 6.7:1  positive 7.5:1  negative 5.1:1  caution 9.7:1
       against --surface used as the ink colour. */
    --brand:       var(--timber-400);
    --brand-hover: #F0A055;
    --positive:    #66BB6A;
    --negative:    #EF5350;
    --caution:     #E8B93B;

    /* ---- Semantic: accents (safe as text) ----
       On a dark surface the readable variant is LIGHTER, not darker.
       brand-ink 8.5:1  positive-ink 10.3:1  negative-ink 7.8:1  caution-ink 11.9:1 */
    --brand-ink:    #EDA366;
    --positive-ink: #97D49A;
    --negative-ink: #FF8A80;
    --caution-ink:  #F2D07A;

    --brand-wash:        rgba(227, 145, 59, 0.10);
    --brand-wash-strong: rgba(227, 145, 59, 0.18);

    /* All four flip to the page ink: every fill is lighter than the surface
       on this theme. brand 6.7:1  positive 7.5:1  negative 5.1:1  caution 9.7:1 */
    --on-brand:    #1C1714;
    --on-positive: #1C1714;
    --on-negative: #1C1714;
    --on-caution:  #1C1714;

    /* Chart series, lifted for a dark background. Same hues, same order, so
       a chart means the same thing after someone flips the theme. */
    --chart-1: #E8944A;
    --chart-2: #7CC47F;
    --chart-3: #5AAFC7;
    --chart-4: #F0736E;
    --chart-5: #D6B04A;
    --chart-6: #B08968;
    --chart-7: #9AABBC;
    --chart-8: #B18FD4;

    /* Elevation reads as a lighter surface on dark, not a heavier shadow —
       a black shadow on a near-black background is invisible. */
    --elev-1: 0 1px 2px rgba(0, 0, 0, 0.4);
    --elev-2: 0 4px 12px rgba(0, 0, 0, 0.5);
    --elev-3: 0 12px 28px rgba(0, 0, 0, 0.6);

    /* ---- Component-level tokens ---- */
    --bg-gradient: var(--surface-sunken);
    --card-bg: rgba(38, 32, 25, 0.94);
    --sidebar-bg: #14100D;
    --sidebar-muted: rgba(245, 239, 231, 0.62);
    --sidebar-active-bg:   var(--timber-400);
    --sidebar-active-text: #1C1714;         /* 6.7:1 — dark ink on light orange */

    /* ---- LEGACY ALIASES ----
       Only the ones whose dark value differs from the light alias need
       restating; the rest inherit through the semantic tokens above.
       --input-bg is deliberately NOT var(--surface) here: inputs use a
       recessed, slightly transparent well on dark so they read as
       editable against the raised card surface. */
    --input-bg:     rgba(19, 16, 16, 0.6);
    --input-border: #3A2F26;

    --accent-blue-rgb:   227, 145, 59;
    --accent-green-rgb:  102, 187, 106;
    --accent-red-rgb:    239, 83, 80;
    --accent-orange-rgb: 232, 185, 59;
}

body {
    margin: 0;
    padding: 0;
    font-family: 'Inter', sans-serif;
    background: var(--bg-color);
    color: var(--text-main);
    display: flex;
    min-height: 100vh;
    transition: background 0.3s ease, color 0.3s ease;
}

/* The page gradient, as a fixed pseudo-element instead of
   `background-attachment: fixed` on <body>.

   `background-attachment: fixed` forces the browser to repaint the whole
   viewport background on every scroll frame, and combined with the
   backdrop-filter blurs on cards it was the main cause of the dashboard
   feeling slightly sticky to scroll. A fixed-position layer paints once and
   is composited thereafter — visually identical, no per-frame repaint. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    background: var(--bg-gradient);
    pointer-events: none;
}

/* ========================================================
   LAYOUT: SIDEBAR, MOBILE HEADER & MAIN CONTENT
   ======================================================== */
.mobile-header {
    display: none;
    position: fixed;
    top: 0; left: 0; right: 0;
    height: calc(60px + env(safe-area-inset-top, 0px));
    padding-top: env(safe-area-inset-top, 0px);   /* clear the notch */
    background: var(--card-bg-solid);
    border-bottom: 1px solid var(--card-border);
    z-index: var(--z-mobile-header, 950);
    align-items: center;
    padding-left: 16px;
    padding-right: 16px;
    gap: 12px;
}
.mobile-header h2 { margin: 0; font-size: 16px; font-weight: 700; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The gap between the menu button and the title is the flex `gap: 15px` on
   .mobile-header. With the mark now between them that reads as too much, so
   the mark closes up against its own title and keeps the 15px to the button. */
.mobile-header .brand-lockup { margin-right: -7px; }
.mobile-header .brand-mark { width: 30px; height: 30px; border-radius: 7px; }
#mobile-menu-btn { background: none; border: none; font-size: 20px; color: var(--text-main); cursor: pointer; min-width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; }
#mobile-menu-btn:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: 6px;
}

.sidebar {
    width: var(--sidebar-width);
    background: var(--sidebar-bg);
    color: var(--sidebar-text);
    position: fixed;
    height: 100vh;
    left: 0; top: 0;
    display: flex;
    flex-direction: column;
    z-index: var(--z-sidebar, 1000);
    transition: transform var(--dur-slow, 320ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
    box-shadow: 4px 0 15px rgba(0,0,0,0.1);
    will-change: transform;
}

.sidebar-overlay {
    display: none;
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.5);
    z-index: var(--z-overlay, 999);
    opacity: 0;
    transition: opacity 0.3s;
}

.sidebar-header { padding: 22px 18px; text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.12); }
.sidebar-header h2 { margin: 0; font-size: 18px; font-weight: 700; letter-spacing: .5px; }

/* ========================================================
   BRAND LOCKUP (logo + wordmark)
   ========================================================
   One set of rules for every place the State Timber Corporation mark
   appears — sidebar, mobile header, login card, error pages — so the mark
   is never resized by eye in a page-local <style> block. Markup lives in
   partials/_brand.html; change the file, not these numbers.

   The mark is a WHITE line drawing on the orange plate. On the dark
   sidebar the plate would disappear into the background, so .brand-mark
   keeps its own orange plate and rounded corners rather than being made
   transparent. */
.brand-lockup {
    display: flex;
    align-items: center;
    gap: 12px;
    justify-content: center;
    text-decoration: none;
    color: inherit;
}
.brand-mark {
    flex: none;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-sm);
    /* --timber-logo, not --brand: this is the plate the artwork itself sits
       on, so it must be the sampled colour rather than the AA-adjusted fill,
       or a transparent-background version of the logo would show a hairline
       of the wrong orange around the mark. Nothing is drawn on top of it. */
    background: var(--timber-logo);
    /* The source artwork is a square plate with the mark already on it, so
       it fills the box edge to edge. object-fit guards against a
       replacement file with a different aspect ratio silently stretching. */
    object-fit: contain;
    display: block;
}
/* Shown when static/brand/ has no logo file — see partials/_brand.html. */
.brand-mark--fallback {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #fff;
}
.brand-mark--fallback .icon { width: 55%; height: 55%; }

.brand-text { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.15; min-width: 0; }
.brand-name {
    font-size: 15px;
    font-weight: 700;
    letter-spacing: .3px;
    white-space: nowrap;
}
.brand-sub {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    opacity: .72;
    white-space: nowrap;
}
/* Sidebar variant: left-aligned, on the dark plate. */
.sidebar-header .brand-lockup { justify-content: flex-start; }
.sidebar-header .brand-sub { color: var(--sidebar-muted); opacity: 1; }
/* The "Logged in as ..." line. It used the .text-muted utility, which paints
   --text-muted — a colour tuned for dark text on a light card. On the dark
   sidebar that was 2.0:1. The sidebar has its own muted token for the same
   reason it has its own active-state pair: it is dark in both themes. */
.sidebar-meta { color: var(--sidebar-muted); text-align: left; line-height: 1.6; margin: 14px 0 0; }
.sidebar-meta strong { color: var(--sidebar-text); font-weight: 600; }

/* `display: block` here made the `flex-direction` and `gap` declarations
   inert — the nav items were only being separated by whitespace in the
   markup, and multi-word labels ("Records & Export", "Data Change Log")
   wrapped underneath the icon instead of indenting to align with the
   first line. Switched to flex so both actually apply. */
.nav-links { list-style: none; padding: 10px 0; margin: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
.nav-links li { padding: 0 15px; }
.nav-links a {
    display: flex; align-items: center; gap: 12px; padding: 10px 16px;
    color: var(--sidebar-muted); text-decoration: none;
    border-radius: 10px; font-weight: 500; transition: all 0.2s;
    font-size: 14px;
}
/* Fixed-width icon column so every label starts on the same x-position.
   Without this the labels sit ragged, because the glyphs have different
   advance widths. */
.nav-links a i,
.nav-links a > .icon { width: 20px; height: 20px; text-align: center; flex-shrink: 0; }

/* ---- Low-stock badge ----
   Count of inventory lines below their reorder threshold. Sits at the end
   of the Dashboard nav item so the condition is visible from any page. */
.nav-badge {
    margin-left: auto;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    border-radius: var(--radius-full);
    background: var(--negative);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    line-height: 1;
}

/* On the bottom nav the item is a vertical stack, so the badge is pinned
   to the top-right of the icon instead of pushed to the end of a row. */
.bottom-nav-badge {
    position: absolute;
    top: 4px;
    left: 50%;
    margin-left: 4px;
    height: 16px;
    min-width: 16px;
    font-size: 10px;
    padding: 0 4px;
}
.nav-links a:hover  { background: rgba(255, 255, 255, 0.08); color: var(--sidebar-text); }
.nav-links a.active { background: var(--sidebar-active-bg); color: var(--sidebar-active-text); font-weight: 600; }
.nav-links a:focus-visible {
    outline: 2px solid #fff;
    outline-offset: -2px;
}

/* Logout submit button, styled to be indistinguishable from the nav <a>
   items above it (it is a form button so the route can be POST-only). */
.nav-logout {
    display: flex; align-items: center; gap: 12px; padding: 10px 16px;
    width: 100%; background: transparent; border: none; cursor: pointer;
    border-radius: 10px; font-weight: 500; font-size: 14px;
    font-family: inherit; text-align: left;
    color: var(--accent-red); transition: all 0.2s;
}
.nav-logout i { width: 20px; text-align: center; flex-shrink: 0; }
.nav-logout:hover { background: var(--negative); color: #fff; }
.nav-logout:focus-visible { outline: 2px solid #fff; outline-offset: -2px; }

.theme-btn {
    width: 100%; display: flex; align-items: center; gap: 15px; padding: 12px 20px;
    background: transparent; border: none; color: rgba(255, 255, 255, 0.7);
    border-radius: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s;
    font-family: inherit; font-size: 16px;
}
.theme-btn:hover { background: rgba(255,255,255,0.1); color: #fff; }
.theme-btn:focus-visible {
    outline: 2px solid #fff;
    outline-offset: -2px;
}

.main-content {
    margin-left: var(--sidebar-width);
    padding: 40px;
    flex: 1;
    width: calc(100% - var(--sidebar-width));
    box-sizing: border-box;
}

/* ========================================================
   CARDS & PANELS (GLASSMORPHISM)
   ======================================================== */
.card, .panel {
    background: var(--card-bg-solid);
    border-radius: var(--radius-lg, 16px);
    padding: 28px;
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--card-border);
    margin-bottom: 30px;
    transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s ease, border 0.25s ease;
}

/* backdrop-filter is now limited to elements that genuinely OVERLAY page
   content — the mobile header and the sidebar drawer. Cards sit on a static
   background, so blurring behind them produced no visible glass effect while
   still forcing an expensive per-frame repaint of everything underneath.
   Dropping it from .card/.panel/.kpi-card is the single biggest scroll
   performance win on the dashboard, which can carry 20+ blurred surfaces. */
@media (min-width: 1024px) {
    @supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
        .mobile-header {
            background: var(--card-bg);
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
        }
        .sidebar {
            backdrop-filter: blur(15px);
            -webkit-backdrop-filter: blur(15px);
        }
    }
}

/* Hover lift only where the card is actually interactive, and only on
   devices with a real pointer. On touch screens a :hover state STICKS after
   a tap, leaving the panel visibly raised until you tap elsewhere. */
@media (hover: hover) and (pointer: fine) {
    .card:hover, .panel:hover {
        box-shadow: var(--shadow-md);
    }
}

/* ========================================================
   FORMS, INPUTS & PASSWORD TOGGLE
   ======================================================== */
.form-group { margin-bottom: 20px; }
/* Uppercase text always needs extra tracking — the letterforms were not
   designed to sit at their default spacing when set in caps, and at 13px
   without it the words read as a solid block. */
.form-group label {
    display: block; font-size: 13px; font-weight: 600; color: var(--text-muted);
    margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.04em;
    cursor: pointer;   /* now that every label is bound to its field */
}

/* Added 'input[type="search"]' for DataTables Search Box, and 'textarea' so multi-line
   fields (e.g. remarks/notes) pick up the same styling without needing .form-control */
.form-control, select, textarea, input[type="text"], input[type="number"], input[type="date"], input[type="password"], input[type="search"] {
    width: 100%; padding: 12px 15px;
    border: 1px solid var(--input-border);
    border-radius: var(--radius-md, 10px);
    background: var(--input-bg);
    color: var(--text-main);
    font-size: 16px; font-family: inherit;
    box-sizing: border-box; transition: all 0.2s;
    min-height: 44px;
}
@media (min-width: 1024px) {
    .form-control, select, textarea, input[type="text"], input[type="number"], input[type="date"], input[type="password"], input[type="search"] {
        font-size: 14px;
    }
}
.form-control:focus, select:focus, textarea:focus, input:focus {
    border-color: var(--brand); outline: none;
    box-shadow: 0 0 0 3px rgba(var(--accent-blue-rgb), 0.18);
}

/* FORCE NATIVE SELECT OPTIONS TO BE READABLE IN DARK MODE */
html.dark-theme select option {
    /* Literal, not var(--surface-raised): several Android WebView builds
       resolve a custom property to nothing here and fall back to white text
       on white. A hex is the only value that is reliably honoured. Keep it
       in step with --surface-raised (#262019) and --text (#F5EFE7). */
    background-color: #262019;
    color: #F5EFE7;
}

.password-wrapper { position: relative; display: flex; align-items: center; }
.password-wrapper input { padding-right: 45px; }
.password-wrapper .toggle-eye {
    position: absolute;
    top: 50%;
    right: 15px;
    transform: translateY(-50%);
    color: var(--text-muted); cursor: pointer;
    font-size: 16px; transition: color 0.2s;
}
.password-wrapper .toggle-eye:hover { color: var(--brand-ink); }

/* Was a blue-to-navy gradient clipped to the glyphs. Gradient text cannot be
   contrast-checked (every pixel is a different colour), it renders as a solid
   block in high-contrast mode, and it printed as nothing at all until
   print.css added an override for it. It is now the flat brand ink, which is
   7.2:1 on white and needs no exceptions anywhere.

   The class name is kept: it is applied to the revenue KPI in dashboard.html
   and to headings, and renaming it across the templates would put unrelated
   churn in a colour-only change. */
.text-gradient {
    color: var(--brand-ink);
    -webkit-text-fill-color: currentColor;  /* undo any inherited clipping */
    display: inline-block;
}

.kpi-icon {
    font-size: 28px;
    margin-bottom: 15px;
    color: var(--brand);
    -webkit-text-fill-color: currentColor;
}

/* BUTTONS CONSOLIDATED IN components_buttons.css */

/* ========================================================
   SKELETONS & EMPTY STATES
   ======================================================== */
.skeleton {
    background: linear-gradient(90deg, rgba(228, 218, 203, 0.35) 25%, rgba(228, 218, 203, 0.75) 50%, rgba(228, 218, 203, 0.35) 75%);
    background-size: 200% 100%;
    animation: loadingSkeleton 1.5s infinite;
    border-radius: 8px;
}
html.dark-theme .skeleton {
    background: linear-gradient(90deg, rgba(90, 76, 62, 0.3) 25%, rgba(90, 76, 62, 0.6) 50%, rgba(90, 76, 62, 0.3) 75%);
    background-size: 200% 100%;
}
@keyframes loadingSkeleton {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 40px 20px;
    text-align: center;
    color: var(--text-muted);
}
.empty-state i {
    font-size: 48px;
    margin-bottom: 15px;
    opacity: 0.5;
}

/* ========================================================
   FIELDSETS & INLINE VALIDATION
   ======================================================== */
.fieldset-group {
    border: 1px solid var(--card-border);
    border-radius: 12px;
    padding: 20px;
    margin-bottom: 25px;
    background: rgba(0,0,0,0.02);
}
html.dark-theme .fieldset-group {
    background: rgba(255,255,255,0.02);
}
.fieldset-group legend {
    font-size: 14px;
    font-weight: 700;
    color: var(--text-main);
    padding: 0 10px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.validation-msg {
    display: block;
    font-size: 12px;
    color: var(--accent-red);
    margin-top: 5px;
    min-height: 14px;
}

/* ========================================================
   TOAST / FLASH NOTIFICATIONS (FIXED STACKING & PADDING)
   ======================================================== */
.toast-container {
    position: fixed;
    bottom: 30px;
    right: 30px;
    z-index: var(--z-toast, 1200);
    display: flex;
    flex-direction: column;
    gap: 15px;
    /* Never intercept clicks on the page behind an empty container. */
    pointer-events: none;
}
.toast-container > * { pointer-events: auto; }

/* ---- Phones ----
   Two separate faults lived in the previous version of this block, and both
   are worth naming because they are easy to reintroduce.

   1. `.toast { width: 100%; }` on an element that also carries
      `padding: 16px 20px`. This stylesheet has NO global border-box reset
      (the same trap is documented in layout/mobile.css §6 for .filter-panel),
      so the toast measured its container's full width PLUS 40px. It hung off
      the right-hand edge of the screen — taking the dismiss button with it —
      and made the whole page scroll sideways for as long as it was visible.

   2. The stack was pinned to the TOP, at the same offset as the sticky
      "All forms" bar on the Data Entry screen (mobile.css §5, also
      `60px + safe-area-inset-top`). The toast wins on z-index, so every
      successful save covered the operator's only way back out of the form
      for four seconds.

   Fixed by sizing with border-box and moving the stack to the bottom, above
   the bottom nav and clear of the home indicator. Nothing else is pinned
   there, the default slide-up animation now matches the direction it enters
   from, and the dismiss button lands in the thumb's natural reach. */
@media (max-width: 767px) {
    .toast-container {
        /* max() rather than a flat gutter: in the installed app the page
           really does run edge to edge on a curved or notched screen. */
        left: max(var(--space-2), env(safe-area-inset-left, 0px));
        right: max(var(--space-2), env(safe-area-inset-right, 0px));
        top: auto;
        /* Login page — no bottom nav to clear, just the home indicator. */
        bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
        gap: var(--space-2);
        align-items: stretch;
    }

    /* Signed in, so partials/_bottom_nav.html is on the page: clear its
       56px row as well. Keyed off the attribute base.html already sets
       rather than a new class, and reading it this way means the two can
       never disagree about whether the nav is there. */
    body:not([data-user-role=""]) .toast-container {
        bottom: calc(56px + env(safe-area-inset-bottom, 0px) + var(--space-2));
    }

    .toast {
        min-width: 0;
        /* The desktop 420px cap would leave the toast short and left-aligned
           on a 600px-wide handset; here the container's own gutters do the
           limiting. */
        max-width: none;
        width: 100%;
        padding: var(--space-3);
        font-size: 13px;
    }

    /* WCAG 2.5.5. 24px is a comfortable mouse target and an impossible
       gloved-thumb one. */
    .toast-close { min-width: 44px; min-height: 44px; }
}
.toast {
    /* Load-bearing, see the phone block above: .toast sets its own padding
       AND is given a width on small screens, and there is no global reset. */
    box-sizing: border-box;
    max-width: 420px;
    min-width: 280px;
    padding: 16px 20px;
    border-radius: 12px;
    color: white;
    font-weight: 500;
    font-size: 14px;
    box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
    opacity: 0;
    transform: translateY(20px);
    animation: slideUp 0.3s forwards;
    display: flex;
    align-items: center;
    gap: 12px;
}
/* Toast surfaces are deliberately the DARKEST step of each status hue
   (--*-700), not the --positive / --negative fills. The fills are tuned to
   carry white text at ~5:1; a toast is a floating opaque overlay that has to
   stay legible over any page content underneath, so it takes the 7-8:1 step.

   They reference the RAW palette rather than the semantic tokens on purpose:
   the semantic --positive flips lighter in dark mode, and a toast must read
   the same in both themes — it is its own surface, not part of the page. */
.toast.success { background-color: var(--green-700);  color: #ffffff; }  /* 7.9:1 */
.toast.error   { background-color: var(--red-700);    color: #ffffff; }  /* 7.9:1 */
.toast.warning { background-color: var(--amber-700);  color: #ffffff; }  /* 5.1:1 */
/* Info was a navy that existed nowhere else in the app. It is now the brand
   ink, so an informational toast reads as coming from this product. */
.toast.info    { background-color: var(--timber-700); color: #ffffff; }  /* 7.2:1 */

.toast > span { flex: 1; }

/* Dismiss control — WCAG 2.2.1 requires timed content to be dismissible. */
.toast-close {
    background: transparent;
    border: none;
    color: inherit;
    opacity: 0.7;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    padding: 4px;
    min-height: 24px;
    min-width: 24px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
    flex-shrink: 0;
}
.toast-close:hover { opacity: 1; background: rgba(255, 255, 255, 0.15); }
.toast-close:focus-visible { outline: 2px solid #fff; outline-offset: 1px; opacity: 1; }

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

/* ========================================================
   DATATABLES OVERRIDES
   ======================================================== */
table.dataTable { border-collapse: collapse !important; width: 100% !important; margin-top: 20px !important; }
table.dataTable thead th {
    border-bottom: 2px solid var(--card-border) !important;
    color: var(--text-muted); font-size: 12px; text-transform: uppercase; padding: 15px !important;
}
table.dataTable tbody td {
    border-bottom: 1px solid var(--card-border);
    padding: 12px 15px !important; font-size: 14px; color: var(--text-main); vertical-align: middle;
}
table.dataTable tbody tr:nth-child(even) {
    background-color: rgba(0, 0, 0, 0.02) !important;
}
html.dark-theme table.dataTable tbody tr:nth-child(even) {
    background-color: rgba(255, 255, 255, 0.02) !important;
}

/* ---- DataTables toolbar: length selector + search box ----
   These rules used to live in a <style> block inside records.html, so only
   that one page got them. Every other DataTable — Order Status, Login
   Activity — fell back to the plugin's own defaults, where the search box is
   a float:right, text-align:right label with an unconstrained input: the
   "Search:" caption wrapped onto its own line and the input ran past the
   right edge of the panel and got clipped by .responsive-wrapper's
   overflow-x. Shared here so all tables lay their toolbar out identically. */
.dataTables_wrapper .dataTables_length {
    margin-bottom: 5px;
}
.dataTables_wrapper .dataTables_filter {
    /* flex, not the plugin's float — the label and input stay on one line at
       any panel width, which is what stopped the caption wrapping. */
    display: flex;
    align-items: center;
    justify-content: flex-end;
    margin-bottom: 10px;
    /* The wrapper is a block child of .responsive-wrapper, so it is exactly
       as wide as the panel while the table beside it overflows and scrolls.
       max-width belts-and-braces that: no toolbar element can be pushed
       outside the visible box by a wide table. */
    max-width: 100%;
}
.dataTables_wrapper .dataTables_filter input {
    padding: 6px 12px !important;
    border-radius: 6px;
    border: 1px solid var(--input-border);
    background-color: var(--input-bg);
    color: var(--text-main);
    font-size: 13px;
    margin-left: 8px;
    /* A fixed width is what keeps it inside the panel. The plugin ships no
       width at all, leaving it at the browser's default ~270px, which is
       wider than the space left over after the "Search:" caption. */
    width: 200px;
    max-width: 100%;
}
@media (max-width: 600px) {
    .dataTables_wrapper .dataTables_filter input { width: 150px; }
}

/* Make DataTables select dropdowns visually fit our theme */
.dataTables_wrapper select {
    border: 1px solid var(--input-border);
    border-radius: 4px;
    background: var(--input-bg);
    color: var(--text-main);
    padding: 1px 4px !important;
    margin: 0 4px;
    height: auto !important;
    line-height: 1.2;
    /* components/forms.css styles every bare <select> as a full-size field.
       Inside the "Show [10] entries" sentence that made the control taller
       than its own line and pushed the caption apart, so it is pinned back
       to an inline control here. */
    font-size: 13px;
    width: auto !important;
    display: inline-block;
}

/* Advanced Dark Mode DataTables Fixes */
html.dark-theme table.dataTable tbody tr { background-color: transparent !important; }
html.dark-theme table.dataTable.display tbody tr.odd > .sorting_1,
html.dark-theme table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 { background-color: rgba(255,255,255,0.02) !important; }
html.dark-theme table.dataTable.display tbody tr.even > .sorting_1,
html.dark-theme table.dataTable.order-column.stripe tbody tr.even > .sorting_1 { background-color: transparent !important; }

html.dark-theme .dataTables_wrapper .dataTables_info,
html.dark-theme .dataTables_wrapper .dataTables_length,
html.dark-theme .dataTables_wrapper .dataTables_filter { color: var(--text-main) !important; }

html.dark-theme .dataTables_wrapper .dataTables_paginate .paginate_button:not(.current) { color: var(--text-muted) !important; }
html.dark-theme .dataTables_wrapper .dataTables_paginate .paginate_button:not(.current):hover { background: rgba(255,255,255,0.1) !important; border-color: transparent !important; color: white !important; }

.dataTables_wrapper .dataTables_paginate .paginate_button.current {
    background: var(--brand) !important; color: var(--on-brand) !important; border: none !important; border-radius: var(--radius-sm);
}

/* DataTables Skeletons & Empty States */
.dataTables_processing {
    background: linear-gradient(90deg, rgba(228, 218, 203, 0.35) 25%, rgba(228, 218, 203, 0.75) 50%, rgba(228, 218, 203, 0.35) 75%) !important;
    background-size: 200% 100% !important;
    animation: loadingSkeleton 1.5s infinite !important;
    border-radius: 8px;
    height: 100px !important;
    color: transparent !important;
    box-shadow: none !important;
    border: none !important;
}
html.dark-theme .dataTables_processing {
    background: linear-gradient(90deg, rgba(90, 76, 62, 0.3) 25%, rgba(90, 76, 62, 0.6) 50%, rgba(90, 76, 62, 0.3) 75%) !important;
    background-size: 200% 100% !important;
}
.dataTables_empty {
    padding: 40px 20px !important;
    color: var(--text-muted) !important;
    font-size: 15px !important;
}

/* ========================================================
   RESPONSIVE DESIGN (MOBILE OVERRIDES)
   ======================================================== */
@media (max-width: 1024px) {
    .mobile-header { display: flex; }
    .sidebar { transform: translateX(-100%); }
    .sidebar.active { transform: translateX(0); }
    .sidebar-overlay.active { display: block; opacity: 1; }
    .main-content { margin-left: 0; width: 100%; padding: 80px 20px 20px 20px; }
}

/* ========================================================
   CSV IMPORT PREVIEW
   ======================================================== */
.import-report {
    margin-top: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--input-bg);
    font-size: 13px;
    max-height: 260px;
    overflow-y: auto;
}

.import-summary {
    display: flex; flex-wrap: wrap; gap: var(--space-3);
    margin-bottom: var(--space-2);
}
.import-stat {
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}
.import-stat strong { color: var(--text); font-size: 15px; }
.import-stat.ok strong { color: var(--positive-ink); }
.import-stat.bad strong { color: var(--negative-ink); }

.import-errors {
    list-style: none;
    margin: var(--space-2) 0 0 0;
    padding: 0;
    border-top: 1px solid var(--border);
}
.import-errors li {
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
    color: var(--text-muted);
    line-height: 1.45;
}
.import-errors li:last-child { border-bottom: none; }
.import-errors strong { color: var(--negative-ink); font-variant-numeric: tabular-nums; }

.import-fatal {
    margin: 0;
    color: var(--negative-ink);
    font-weight: 600;
    line-height: 1.5;
}
.import-hint {
    margin: var(--space-2) 0 0 0;
    color: var(--text-muted);
    font-size: 12px;
    line-height: 1.5;
}
.import-hint code {
    background: rgba(0, 0, 0, 0.05);
    padding: 1px 5px;
    border-radius: var(--radius-sm);
    word-break: break-word;
}
html.dark-theme .import-hint code { background: rgba(255, 255, 255, 0.08); }

/* ========================================================
   RECORDS DATE FILTER
   ======================================================== */
.date-filter-panel { padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); }

.date-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
}

.date-filter-field { display: flex; flex-direction: column; min-width: 150px; flex: 1 1 150px; }
.date-filter-field label {
    font-size: 12px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--text-muted); margin-bottom: 4px;
    cursor: pointer;
}

.date-filter-actions { display: flex; gap: var(--space-2); flex: 0 0 auto; }

/* Presets wrap to their own row on narrow screens. */
.date-filter-presets {
    display: flex; flex-wrap: wrap; gap: var(--space-2);
    flex: 1 1 100%; padding-top: var(--space-1);
}
@media (min-width: 900px) {
    .date-filter-presets { flex: 0 1 auto; padding-top: 0; margin-left: auto; }
}

.preset-btn {
    background: var(--input-bg);
    border: 1px solid var(--input-border);
    color: var(--text-muted);
    border-radius: var(--radius-full);
    padding: 6px 14px;
    min-height: 36px;
    font-size: 12px;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    transition: all var(--dur-fast, 120ms) ease;
    white-space: nowrap;
}
.preset-btn:hover { border-color: var(--brand); color: var(--brand); }
.preset-btn.active { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.preset-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.date-filter-status:empty { display: none; }

/* ========================================================
   CONFIRM DIALOG
   ========================================================
   Replaces window.confirm() for destructive and duplicate-warning actions.
   A native confirm() cannot show the structured detail that makes a
   duplicate warning useful ("here is the record you'd be duplicating"), and
   on mobile it renders detached from the page.
   ======================================================== */
.confirm-backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal, 1050);
    background: rgba(0, 0, 0, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-3);
    animation: confirmFade var(--dur-fast, 120ms) var(--ease-out) both;
}

.confirm-dialog {
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    box-shadow: var(--elev-3);
    padding: var(--space-4);
    width: 100%;
    max-width: 460px;
    animation: confirmRise var(--dur-base, 200ms) var(--ease-out) both;
}

.confirm-title {
    margin: 0 0 var(--space-2) 0;
    font-size: 1.125rem;
    line-height: 1.3;
}

.confirm-body {
    margin: 0 0 var(--space-3) 0;
    color: var(--text-muted);
    font-size: 0.9375rem;
    line-height: 1.55;
}

/* The existing record being echoed back. Monospaced-ish and boxed so it
   reads as data rather than prose. */
.confirm-detail {
    background: var(--input-bg);
    border: 1px solid var(--input-border);
    border-left: 3px solid var(--caution);
    border-radius: var(--radius-md);
    padding: var(--space-3);
    margin-bottom: var(--space-4);
    font-size: 0.875rem;
    font-variant-numeric: tabular-nums;
    word-break: break-word;
}

.confirm-actions {
    display: flex;
    gap: var(--space-3);
    justify-content: flex-end;
    flex-wrap: wrap;
}
.confirm-actions .btn { flex: 1 1 auto; }
@media (min-width: 480px) {
    .confirm-actions .btn { flex: 0 0 auto; min-width: 120px; }
}

@keyframes confirmFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes confirmRise {
    from { opacity: 0; transform: translateY(12px) scale(0.98); }
    to   { opacity: 1; transform: none; }
}

/* Entry form button row: primary action first, "add another" beside it.
   Stacks full-width on narrow screens so both stay easy to hit. */
.entry-actions { margin-top: var(--space-4); }
.entry-actions .btn { flex: 1 1 100%; }
@media (min-width: 560px) {
    .entry-actions { justify-content: flex-start; }
    .entry-actions .btn { flex: 0 1 auto; }
}

/* ========================================================
   TASK-FIRST DATA ENTRY  (§5.2, v1.19.0)
   ========================================================
   The phone has drilled in through a card picker since v1.13.0 and it works:
   every record type visible at once, then one form filling the screen. The
   desktop kept the seven-tab strip with all seven forms stacked behind it, so
   an operator who only ever logs conversions still landed on Raw Receipt and
   had to know that the second tab was theirs.

   This makes the two screens the same shape at every width. Nothing about the
   tab machinery changes — the cards call the same openTab() the tab buttons
   call, and the URL hash, the linked Recent Records pane and the AJAX submit
   handlers are all untouched. `is-focused` on the scope remains the ONLY
   state; the rules below are the desktop half of what layout/mobile.css §5
   already does for phones.

   THE TAB STRIP SURVIVES, and only inside a form. On a phone it is replaced
   entirely, because seven tabs do not fit; on a desk PC it is the fastest way
   to move between forms once you are in one, and there is room for it. So:
   picker on the landing screen, strip once you have chosen.
   ======================================================== */
@media (min-width: 768px) {

    /* ---- Step 1: the landing screen ---- */
    .tab-group-scope:not(.is-focused) .entry-picker {
        display: grid;
        /* Four across at desktop width, dropping to three and two as the
           column narrows. auto-fit rather than a fixed count so the tablet in
           landscape — the most common non-phone device here — is not left
           with one card on a second row. */
        grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
        gap: var(--space-3);
        margin-bottom: 0;
    }

    .tab-group-scope:not(.is-focused) .entry-card {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-2);
        min-height: 120px;
        padding: var(--space-4);
        text-align: left;
        background: var(--input-bg);
        border: 1px solid var(--input-border);
        border-radius: var(--radius-md);
        color: var(--text-main);
        font-family: inherit;
        cursor: pointer;
        transition: border-color var(--dur-fast, 120ms) ease,
                    background var(--dur-fast, 120ms) ease,
                    box-shadow var(--dur-fast, 120ms) ease;
    }
    .tab-group-scope:not(.is-focused) .entry-card:hover {
        border-color: var(--brand);
        box-shadow: var(--elev-1);
    }
    .tab-group-scope:not(.is-focused) .entry-card:focus-visible {
        outline: 2px solid var(--brand);
        outline-offset: 2px;
    }
    .tab-group-scope:not(.is-focused) .entry-card .icon {
        width: 26px; height: 26px;
        color: var(--brand);
        flex-shrink: 0;
    }
    .tab-group-scope:not(.is-focused) .entry-card-name {
        font-size: 16px; font-weight: 700; line-height: 1.25;
    }
    .tab-group-scope:not(.is-focused) .entry-card-hint {
        font-size: 13px; font-weight: 500;
        color: var(--text-muted); line-height: 1.35;
    }

    /* No strip, no form and no Recent Records pane until something is chosen.
       `.tab-content.active` carries display:block, so this needs the same
       specificity weight to win — hence the compound selector rather than
       !important, exactly as the mobile rules do. */
    .tab-group-scope:not(.is-focused) .tab-container { display: none; }
    .tab-group-scope:not(.is-focused) .tab-content.active { display: none; }
    .tab-group-scope:not(.is-focused) .recent-panel { display: none; }

    /* The panel carries `h-full` so that, inside a form, it lines up with the
       Recent Records column beside it. On the picker screen that column is
       hidden and there is no form, so the same rule stretches a card grid
       four rows tall into an empty box the height of the viewport. */
    .tab-group-scope:not(.is-focused) .panel.h-full { height: auto; }

    /* ---- Step 2: inside a form ---- */
    .tab-group-scope.is-focused .entry-picker { display: none; }

    .tab-group-scope.is-focused .entry-back {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        min-height: 44px;
        margin: 0 0 var(--space-3);
        padding: 0;
        background: none;
        border: none;
        color: var(--brand);
        font-family: inherit;
        font-size: 14px;
        font-weight: 700;
        cursor: pointer;
    }
    .tab-group-scope.is-focused .entry-back:hover { text-decoration: underline; }
    .tab-group-scope.is-focused .entry-back:focus-visible {
        outline: 2px solid var(--brand);
        outline-offset: 2px;
        border-radius: var(--radius);
    }
    /* The form name is already the highlighted tab two lines below, so
       repeating it here is noise on a screen that has the room to show both. */
    .tab-group-scope.is-focused .entry-back-current { display: none; }
}

/* ---- Repeat last entry, and the soft anomaly warning (§5.2) ---- */

.entry-repeat { margin-bottom: var(--space-4); }

/* The warning that a figure is unlike the usual one. Amber, NOT red: red is
   what --negative-ink means everywhere else in this app, and it is reserved
   for a value the server refused. This is advice about a value that is
   perfectly legal and probably fine.

   Hidden until it has something to say — an empty span with margin would push
   every field apart by a line for a warning that is not there. */
.entry-warning {
    display: none;
    margin-top: 4px;
    font-size: 12.5px;
    line-height: 1.4;
    font-weight: 600;
    color: var(--caution-ink);
}
.entry-warning.is-shown { display: block; }

/* ========================================================
   MOBILE BOTTOM NAVIGATION
   ========================================================
   Only below 768px. Above that the sidebar is always visible and a bottom
   bar would be redundant.
   ======================================================== */
.bottom-nav { display: none; }

@media (max-width: 767px) {
    .bottom-nav {
        display: flex;
        position: fixed;
        inset: auto 0 0 0;
        z-index: var(--z-bottom-nav, 900);
        background: var(--surface);
        border-top: 1px solid var(--border);

        /* env(safe-area-inset-bottom) keeps the bar clear of the iPhone
           home indicator. Without it the bottom row of controls sits under
           the system gesture area and taps get swallowed. */
        padding-bottom: env(safe-area-inset-bottom, 0px);
        box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.06);
    }

    .bottom-nav-item {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 3px;
        min-height: 56px;
        padding: 6px 4px;
        background: transparent;
        border: none;
        font-family: inherit;
        font-size: 11px;
        font-weight: 600;
        color: var(--text-muted);
        text-decoration: none;
        cursor: pointer;
        transition: color var(--dur-fast, 120ms) ease;
        -webkit-tap-highlight-color: transparent;
    }

    .bottom-nav-item .icon { width: 20px; height: 20px; }

    .bottom-nav-item.active { color: var(--brand); }

    /* A short bar above the active item — clearer at a glance than colour
       alone, and it does not rely on colour perception. */
    .bottom-nav-item.active::before {
        content: '';
        position: absolute;
        top: 0;
        width: 28px;
        height: 3px;
        border-radius: 0 0 3px 3px;
        background: var(--brand);
    }
    .bottom-nav-item { position: relative; }

    .bottom-nav-item:active { background: rgba(var(--accent-blue-rgb), 0.08); }

    .bottom-nav-item:focus-visible {
        outline: 2px solid var(--brand);
        outline-offset: -3px;
        border-radius: var(--radius-sm);
    }

    /* Reserve room so the last row of page content is never trapped
       underneath the fixed bar. */
    .main-content {
        padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
    }
}

/* ========================================================
   REDUCED MOTION
   ========================================================
   The app animates card hover lifts, the sidebar slide, toast slide-up,
   tab fade-in, progress-bar fills and an infinite skeleton shimmer. For
   users who have asked their OS to minimise motion — including people
   with vestibular disorders — all of that should stop.

   Kept as the LAST rule in the last-linked stylesheet so it wins without
   needing to out-specify every individual animation.
   ======================================================== */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    /* Hover lifts translate content under the cursor; drop them entirely
       rather than just making them instant. */
    .card:hover, .panel:hover, .kpi-card:hover,
    .inv-item:hover, .order-card:hover, .recent-item:hover,
    .btn-primary:hover, .btn-submit:hover {
        transform: none !important;
    }
}


/* ---------------------------------------------------------------------------
   Print-only blocks
   ---------------------------------------------------------------------------
   A printed dashboard carries a header naming the period and when it was
   produced, so a filed copy is self-describing. On screen that information is
   already in the page furniture, so the block is hidden.

   This rule has to be HERE and not in layout/print.css, which is where it
   used to be and where it did nothing: base.html links print.css with
   media="print", so every rule in that file — inside the @media block or not
   — applies only when printing. The header was therefore visible on screen on
   every dashboard view. */
.print-header { display: none; }

/* ===== layout/mobile.css ===== */
/* ========================================================
   MOBILE COMPATIBILITY LAYER
   ========================================================
   THE ONE RULE FOR THIS FILE: every declaration that changes an existing
   look must sit inside a `max-width` media query. This stylesheet is
   linked LAST — after layout/main.css — so anything unscoped here would
   silently win on desktop too, which is exactly what this file must never
   do. The only unscoped rules below define BRAND-NEW classes (.tab-nav,
   .entry-picker, .chart-frame sizes) that no desktop rule touches.

   Breakpoints, and why these three:
     <= 1024px   the sidebar has already collapsed to a drawer (main.css),
                 so the page is a single column and gets tablet padding.
     <=  767px   the bottom nav appears (main.css) and the viewport is a
                 phone held one-handed. This is where the real work is.
     <=  400px   small/older handsets — a 320-360px viewport where a
                 two-up grid stops fitting.

   WHY THIS FILE EXISTS AT ALL
   The app was authored desktop-first with mobile patches sprinkled through
   six stylesheets and five <style> blocks. Collecting the mobile rules in
   one place means a future "why is this squashed on a phone?" has exactly
   one file to read, and a mistake here can never leak onto the desktop
   layout that the office actually uses for reporting.
   ======================================================== */


/* ========================================================
   1. NEW COMPONENTS (shared by desktop and mobile)
   ========================================================
   These classes did not exist before, so defining them unscoped changes
   nothing that was already rendering. */

/* ---- Chart frames ----
   Chart.js needs a parent with a RESOLVED height to fill; a responsive
   canvas in an auto-height box grows without bound on every redraw. The
   dashboard used to carry `style="height: 340px; position: relative;"`
   inline on each wrapper, which meant a mobile override could only win
   with !important. The heights are now classes, so the phone can simply
   restate them. */
.chart-frame { position: relative; width: 100%; height: 340px; }
.chart-frame--sm { height: 300px; }
.chart-frame--md { height: 320px; }

/* The grey shimmer shown while a chart is being built. Must match the
   frame it replaces or the page jumps when the chart appears. */
.chart-skeleton { height: 340px; }

/* ---- Tab navigation wrapper ----
   `.tab-nav` holds BOTH representations of one tab group: the horizontal
   button strip (desktop) and a native <select> (phone). Only one is ever
   displayed. They are kept in sync by static/js/main.js, and both drive
   the same openTab() function, so the URL hash, the DataTables redraw and
   the linked "recent records" panes behave identically either way.

   A native <select> was chosen over a custom dropdown deliberately: it
   opens the phone's own full-height picker, which is operable with gloves,
   readable in sunlight, and needs no focus-trap or outside-click code. */
.tab-select-wrap { display: none; }

/* ---- Entry form card picker ----
   Hidden entirely on desktop, where the seven tabs fit on one line. */
.entry-picker { display: none; }
.entry-back { display: none; }


/* ========================================================
   2. TABLET AND BELOW  (<= 1024px)
   ========================================================
   The sidebar is a drawer from here down, so the content column is the
   full width of the screen. */
@media (max-width: 1024px) {

    /* main.css already sets `padding: 80px 20px 20px`. Only the horizontal
       value is retuned here, via the spacing scale rather than a literal,
       and the top value is derived from the actual header height instead
       of the hardcoded 80 (which was 20px of guesswork on a notched
       phone, where the header is 60px PLUS the safe-area inset). */
    .main-content {
        padding-left: var(--space-3);
        padding-right: var(--space-3);
        padding-top: calc(60px + env(safe-area-inset-top, 0px) + var(--space-3));
    }

    /* A grid or flex ITEM defaults to min-width:auto, meaning it refuses to
       shrink below its content. One long unbroken string — a batch ID, a
       species name, a "Rs. 1,234,567.00" — then pushes the whole row wider
       than the screen and the entire page scrolls sideways. This single
       declaration is the fix for most horizontal-scroll bugs on this app. */
    .grid > *,
    .flex > *,
    .panel,
    .card { min-width: 0; }

    /* Panels lose the desktop's 28px inset. At 28px a side, a 360px screen
       spent 32% of its width on padding before any content was drawn. */
    .panel, .card {
        padding: var(--space-3);
        margin-bottom: var(--space-4);
    }
}


/* ========================================================
   3. PHONE  (<= 767px)
   ======================================================== */
@media (max-width: 767px) {

    /* ---- 3.1 The twelve-column grid ----
       THE SINGLE WORST MOBILE BUG IN THE APP, and the reason the dashboard
       scrolled sideways. `.grid-cols-12` is defined UNSCOPED in grid.css,
       so a phone got `repeat(12, 1fr)` with `gap-5` (2.5rem). Eleven gaps
       at 40px is 440px of pure gutter — already wider than a 360px phone
       before a single pixel of content. Everything spanning those columns
       inherited that overflow.

       Below 768px there is only ever room for one column, so the track
       list collapses and every span becomes automatic. */
    .grid-cols-12,
    .grid-cols-6,
    .grid-cols-4 { grid-template-columns: 1fr; }

    [class*="col-span-"] { grid-column: auto; }

    /* Gaps scale down with the screen. `gap-5` (2.5rem) between stacked
       full-width panels is a third of a phone screen of empty space. */
    .gap-4, .gap-5, .gap-6 { gap: var(--space-3); }

    /* ---- 3.2 Page chrome ---- */
    .main-content {
        padding-left: var(--space-2);
        padding-right: var(--space-2);
    }

    .page-title { margin-bottom: var(--space-4); }

    .panel, .card {
        border-radius: var(--radius-md);
        padding: var(--space-3);
        margin-bottom: var(--space-3);
    }

    /* ---- 3.3 Typography ----
       Page headings drop a step. `clamp()` is not used because the sizes
       here are set by utility classes (.text-3xl, .text-2xl) that the
       templates apply directly. */
    h1, .text-3xl { font-size: 1.375rem; line-height: 1.25; }
    h2, .text-2xl { font-size: 1.25rem; }
    h3 { font-size: 1rem; }

    /* A heading with a right-aligned meta label ("Weekly", "Monthly YTD")
       must be allowed to wrap rather than squeeze the label off-screen. */
    .panel h3 { flex-wrap: wrap; }
    .panel h3 .ml-auto { margin-left: 0; width: 100%; }

    /* ---- 3.4 Forms ----
       Inputs stay at 16px (set in main.css) on purpose: iOS Safari zooms
       the whole page in when a field smaller than 16px receives focus, and
       it never zooms back out. */
    .fieldset-group {
        padding: var(--space-3) var(--space-2);
        margin-bottom: var(--space-3);
        border-radius: var(--radius-md);
    }
    .fieldset-group legend {
        font-size: 13px;
        padding: 0 var(--space-1);
    }
    .form-group { margin-bottom: var(--space-3); }

    /* Every action button at the foot of a form goes full width. A 44px
       target in the middle of a row is reachable; a full-width one is
       unmissable, which matters when the other hand is holding a docket. */
    .form-actions .btn,
    .form-actions .btn-action { flex: 1 1 100%; }
    .form-actions { gap: var(--space-2); }

    /* ---- 3.5 Toolbars (Records) ----
       `justify-content: space-between` on a two-item toolbar put Delete on
       the far left and Import/Export on the far right with a canyon
       between them. Stacked and full-width instead. */
    .toolbar {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
    }
    .toolbar-right {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--space-2);
    }
    .toolbar .btn-action { width: 100%; font-size: 12px; padding: 0 var(--space-2); }

    /* ---- 3.6 Tables ----
       Two different kinds of table live in this app and they need opposite
       treatment.

       (a) DataTables with the Responsive extension COLLAPSE their surplus
           columns into an expandable child row — but only if they are
           allowed to measure themselves against the real screen width.
           `.responsive-wrapper table { min-width: 500px }` from grid.css
           lied to them: the plugin saw 500px of room on a 360px phone and
           kept every column, so the table overflowed AND the collapse
           feature silently never triggered. Releasing the min-width is
           what makes the responsive plugin actually work. */
    .responsive-wrapper table.dataTable { min-width: 0; }

    /*   (b) Plain tables (audit log, user list, waste breakdown) have no
           such plugin, so they keep a min-width and scroll horizontally
           inside their own box — which is correct: the surrounding page
           must not scroll, only the table. */
    .responsive-wrapper {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        /* A hairline on the right edge tells the reader there is more
           table out of view; without it a cut-off column looks like a
           rendering fault. */
        background:
            linear-gradient(to right, var(--surface) 30%, rgba(0,0,0,0)),
            linear-gradient(to right, rgba(0,0,0,0), var(--surface) 70%) 100% 0,
            radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.12), rgba(0,0,0,0)),
            radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.12), rgba(0,0,0,0)) 100% 0;
        background-repeat: no-repeat;
        background-size: 32px 100%, 32px 100%, 12px 100%, 12px 100%;
        background-attachment: local, local, scroll, scroll;
    }

    table.dataTable thead th { padding: 10px 8px !important; font-size: 11px; }
    table.dataTable tbody td { padding: 10px 8px !important; font-size: 13px; }

    /* DataTables' own controls are laid out with floats, which on a narrow
       screen overlap each other. Stacked and full width. */
    .dataTables_wrapper .dataTables_length,
    .dataTables_wrapper .dataTables_filter {
        float: none;
        text-align: left;
        justify-content: flex-start;
        width: 100%;
        margin-bottom: var(--space-2);
    }
    .dataTables_wrapper .dataTables_filter { display: block; }
    .dataTables_wrapper .dataTables_filter label { display: flex; align-items: center; width: 100%; }
    .dataTables_wrapper .dataTables_filter input {
        width: 100% !important;
        flex: 1;
        min-height: 40px;
    }
    .dataTables_wrapper .dataTables_info,
    .dataTables_wrapper .dataTables_paginate {
        float: none;
        text-align: center;
        width: 100%;
        padding-top: var(--space-2);
    }
    .dataTables_wrapper .dataTables_paginate .paginate_button {
        padding: 6px 10px !important;
        min-width: 36px;
    }

    /* ---- 3.7 Modals become bottom sheets ----
       A centred dialog on a phone sits under the keyboard the moment a
       field is focused. Anchoring to the bottom keeps the first field and
       the submit button in the visible half of the screen. */
    .modal { align-items: flex-end; padding: 0; }
    .modal-content {
        margin: 0;
        max-width: 100%;
        max-height: 88vh;
        overflow-y: auto;
        border-radius: var(--radius-xl) var(--radius-xl) 0 0;
        padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    }
    .confirm-dialog { max-height: 85vh; overflow-y: auto; }

    /* ---- 3.8 Date filter (Records) ---- */
    .date-filter-field { flex: 1 1 100%; min-width: 0; }
    .date-filter-actions { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); width: 100%; }
    .date-filter-actions .btn { width: 100%; }
    .date-filter-presets { gap: var(--space-1); }
    .preset-btn { flex: 1 1 calc(50% - var(--space-1)); }
}


/* ========================================================
   4. TAB GROUPS -> NATIVE DROPDOWN  (<= 767px)
   ========================================================
   Applies to Records and the Admin Panel. The horizontal strip is
   replaced, not merely made scrollable: a scrolling strip still hides its
   last option behind a gesture nobody discovers, and on Records the last
   option is Maintenance, which operators open every day.
   ======================================================== */
@media (max-width: 767px) {
    .tab-nav .tab-container { display: none; }

    .tab-select-wrap {
        display: block;
        margin-bottom: var(--space-3);
    }

    .tab-select-label {
        display: block;
        font-size: 11px;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.06em;
        color: var(--text-muted);
        margin-bottom: var(--space-1);
    }

    /* Styled as a prominent control rather than a plain field: it is the
       primary navigation for the page it sits on. */
    .tab-select {
        width: 100%;
        min-height: 48px;
        font-size: 16px;         /* 16px or iOS zooms on focus */
        font-weight: 600;
        color: var(--text-main);
        background: var(--input-bg);
        border: 1px solid var(--brand);
        border-radius: var(--radius-md);
        padding: 10px var(--space-3);
    }
    .tab-select:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
}


/* ========================================================
   5. DATA ENTRY: CARD PICKER -> FOCUSED FORM  (<= 767px)
   ========================================================
   Seven forms behind seven tabs is roughly 700px of tab strip on a 360px
   screen. Scrolled horizontally, four of the seven are invisible and the
   operator has no way to know they exist.

   The replacement is a two-step drill-in:

     Step 1  a 2-up grid of large cards, one per record type. Everything
             the page can do is visible without scrolling sideways.
     Step 2  tap a card and that ONE form takes the whole screen, with a
             sticky "All forms" bar to come back.

   Nothing about the underlying tab machinery changes: the cards call the
   same openTab() the desktop buttons call, so the URL hash, the linked
   "Recent Records" pane and the AJAX submit handlers are untouched. On a
   desktop none of this renders at all.
   ======================================================== */
@media (max-width: 767px) {

    /* The desktop tab strip is gone; the picker takes its place. */
    .tab-group-scope .tab-container { display: none; }

    .entry-picker {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--space-2);
        margin-bottom: var(--space-2);
    }

    .entry-card {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        justify-content: flex-start;
        gap: 6px;
        min-height: 96px;
        padding: var(--space-3) var(--space-2);
        text-align: left;
        background: var(--input-bg);
        border: 1px solid var(--input-border);
        border-radius: var(--radius-md);
        color: var(--text-main);
        font-family: inherit;
        cursor: pointer;
        transition: border-color var(--dur-fast, 120ms) ease,
                    background var(--dur-fast, 120ms) ease;
        -webkit-tap-highlight-color: transparent;
    }
    .entry-card:active { background: rgba(var(--accent-blue-rgb), 0.08); border-color: var(--brand); }
    .entry-card:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

    .entry-card .icon {
        width: 22px; height: 22px;
        color: var(--brand);
        flex-shrink: 0;
    }
    .entry-card-name { font-size: 14px; font-weight: 700; line-height: 1.25; }
    .entry-card-hint {
        font-size: 11px;
        font-weight: 500;
        color: var(--text-muted);
        line-height: 1.3;
    }

    /* ---- Step 1: picker showing ----
       No form is rendered at all, whatever `.active` says. `.tab-content`
       carries `display:block` when active, so this needs the same
       specificity weight to win — hence the compound selector rather than
       !important. */
    .tab-group-scope:not(.is-focused) .tab-content.active { display: none; }
    .tab-group-scope:not(.is-focused) .recent-panel { display: none; }

    /* ---- Step 2: one form showing ---- */
    .tab-group-scope.is-focused .entry-picker { display: none; }

    /* Sticky so the way back is always one thumb-tap away, however long
       the form is. Sits directly under the fixed mobile header. */
    .tab-group-scope.is-focused .entry-back {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        position: sticky;
        top: calc(60px + env(safe-area-inset-top, 0px));
        z-index: 5;
        min-height: 48px;
        /* Pulled out to the panel's edges (top -16px, sides -8px) so the bar
           reads as chrome attached to the panel rather than as a control
           floating inside it. The width compensates for the two negative
           side margins: 2 x 8px = one --space-3. */
        margin: calc(var(--space-3) * -1) calc(var(--space-2) * -1) var(--space-3);
        width: calc(100% + var(--space-3));
        padding: 0 var(--space-3);
        background: var(--surface);
        border: none;
        border-bottom: 1px solid var(--border);
        border-radius: var(--radius-md) var(--radius-md) 0 0;
        color: var(--brand);
        font-family: inherit;
        font-size: 14px;
        font-weight: 700;
        cursor: pointer;
    }
    .entry-back .icon { width: 18px; height: 18px; }
    .entry-back-current {
        margin-left: auto;
        color: var(--text-muted);
        font-weight: 600;
        font-size: 12px;
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }

    /* ---- Sticky save bar (§5.2, v1.19.0) ----
       The primary action, always reachable, without scrolling to the bottom
       of a form that is fourteen fields long on a 667px screen. The Raw
       Receipt form is the worst of them: an operator who has filled in
       everything has to scroll past four more fieldsets to find Save, and on
       a phone held in one hand at a yard gate that is where entries get
       abandoned.

       `position: sticky`, not `fixed`: the row keeps its place in the
       document, so it does not overlap the last field, it does not need a
       spacer element underneath it, and it stops sticking naturally when the
       form ends rather than hovering over the Recent Records pane below.

       Only when a form is open. On the picker screen there is no form and
       nothing to save. */
    .tab-group-scope.is-focused .entry-actions {
        position: sticky;
        /* Clear of the fixed 56px bottom navigation, which would otherwise
           sit on top of the save button — the one control this rule exists to
           keep reachable. Keyed off the attribute base.html already sets,
           exactly as the toast container is, so the two can never disagree
           about whether the nav is on the page. Data Entry is behind a login,
           so in practice it always is. */
        bottom: 0;
        z-index: 4;
        gap: var(--space-2);
        /* Pulled to the panel's edges and re-padded, so the bar reads as a
           bar rather than as two buttons floating over the content that
           scrolls under them. */
        margin: var(--space-4) calc(var(--space-2) * -1) 0;
        padding: var(--space-3) var(--space-3)
                 calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
        background: var(--surface);
        border-top: 1px solid var(--border);
    }
    body:not([data-user-role=""]) .tab-group-scope.is-focused .entry-actions {
        bottom: calc(56px + env(safe-area-inset-bottom, 0px));
        padding-bottom: var(--space-3);
    }

    /* ---- Recent Records ----
       Demoted to a collapsed <details> below the form. It is reference
       material; on a 667px-tall screen it should never push the form the
       operator came here to fill in below the fold. */
    .recent-panel {
        margin-top: var(--space-3);
        padding: 0;
        border: 1px solid var(--card-border);
        border-radius: var(--radius-md);
        background: var(--card-bg-solid);
    }
    .recent-panel > summary {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        min-height: 48px;
        padding: 0 var(--space-3);
        font-weight: 700;
        font-size: 14px;
        cursor: pointer;
        list-style: none;
    }
    .recent-panel > summary::-webkit-details-marker { display: none; }
    .recent-panel > summary::after {
        content: '▾';
        margin-left: auto;
        color: var(--text-muted);
        transition: transform var(--dur-fast, 120ms) ease;
    }
    .recent-panel[open] > summary::after { transform: rotate(180deg); }
    .recent-panel-body { padding: 0 var(--space-3) var(--space-3); }
    .recent-item { padding: var(--space-2); margin-bottom: var(--space-2); }
}

/* On desktop the <details> is permanently open and its summary is just the
   panel heading — the disclosure affordance would be a lie, since the
   element cannot be closed there (main.js re-opens it on `toggle`). So the
   marker triangle goes, and so does the pointer cursor that would invite a
   click. The heading must look and read exactly as the <h3> it replaced. */
@media (min-width: 768px) {
    .recent-panel > summary {
        display: block;
        list-style: none;
        cursor: default;
        font-size: 1.17em;
        font-weight: bold;
    }
    .recent-panel > summary::-webkit-details-marker { display: none; }
    .recent-panel > summary::marker { content: ''; }
}


/* ========================================================
   6. DASHBOARD  (<= 767px)
   ======================================================== */
@media (max-width: 767px) {

    /* ---- Header + period filter ----
       Three controls (mode toggle, month picker, print) in one flex row
       overflowed. Each becomes full width and stacks. */
    /* box-sizing is deliberate: this stylesheet has no global border-box
       reset, so `width: 100%` plus padding and a 1px border made the panel
       ~34px wider than the KPI cards below it — it sat proud of the grid on
       the right-hand edge. Same arithmetic applied to the toggle inside it.
       Set here rather than globally: a site-wide reset would resize every
       fixed-width element in the app at once. */
    .filter-panel {
        box-sizing: border-box;
        width: 100%;
        padding: var(--space-3);
        border-radius: var(--radius-md);
        gap: var(--space-2);
    }
    .toggle-switch-container { box-sizing: border-box; width: 100%; }
    /* 10px made the toggle visibly taller than the month picker sitting
       directly under it; 8px lines the two rows up. */
    .toggle-option { flex: 1; text-align: center; padding: 8px var(--space-2); }
    .filter-panel > .flex { flex: 1; min-width: 0; }
    .filter-panel input[type="month"] { width: 100%; min-width: 0; }

    /* Printing from a phone is not a thing anyone does on a factory floor,
       and the button was stealing a row of width from the controls that
       are. Hidden rather than deleted — it is still there on any screen
       big enough to be attached to a printer. */
    .filter-panel .btn-secondary[onclick*="print"] { display: none; }

    /* ---- KPI cards ----
       Two up rather than one: four full-width cards is four screens of
       scrolling before the first chart. */
    .kpi-card { padding: var(--space-3); border-radius: var(--radius-md); }
    .kpi-icon-box { width: 36px; height: 36px; border-radius: 10px; font-size: 16px; margin-bottom: 8px; }
    .kpi-title { font-size: 11px; }
    .kpi-value { font-size: 1.125rem; }
    .kpi-delta { font-size: 11px; }
    /* The "View sales →" hint duplicates what tapping the card already
       does, and costs a line of height on every card. */
    .kpi-link-hint { display: none; }

    /* ---- Charts ----
       Shorter frames. A 340px chart plus its heading is more than half a
       phone screen, and the dashboard has five of them. */
    .chart-frame,
    .chart-frame--sm,
    .chart-frame--md { height: 240px; }
    .chart-skeleton { height: 240px; }

    /* ---- Target achievement rows ---- */
    .machine-target-row {
        padding: var(--space-3);
        border-radius: var(--radius-md);
        gap: var(--space-3);
    }
    .progress-labels { font-size: 11px; gap: var(--space-2); }
    /* The label and the "12 / 30 (40%)" figure must be free to sit on two
       lines rather than colliding. */
    .progress-labels > span:last-child { text-align: right; white-space: nowrap; }

    /* ---- Inventory tiles ----
       minmax(200px, 1fr) produced ONE column on a 360px screen. 140px
       gives two, which is what the grid was designed to look like. */
    .inv-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--space-2); }
    .inv-item { padding: var(--space-2) var(--space-2) var(--space-2) 10px; border-radius: var(--radius-md); }
    .inv-title { font-size: 11px; }
    .inv-value { font-size: 18px; }

    /* ---- Order cards ----
       Item name and status badge side by side truncated the name. Stacked,
       with the badge on its own line. */
    .order-card {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-2);
        padding: var(--space-3);
    }
    .order-meta { flex-wrap: wrap; gap: var(--space-2); font-size: 11px; }

    /* ---- Downtime / achievement pills ---- */
    .breakdown-list { padding-left: 0; gap: var(--space-3); }
    .breakdown-item { padding-left: var(--space-3); }
    .achievement-pill { font-size: 11px; }

    /* ---- Maintenance counters ---- */
    .machine-target-title { font-size: 14px; }
}

/* ========================================================
   6b. NOTIFICATION PANEL  (<= 767px)
   ========================================================
   A 380px drawer on a 360px screen is a drawer with a 20px strip of page
   showing down one side — enough to look like a mistake, not enough to be
   useful. On a phone the panel is the whole screen instead, which is what
   every other full-screen surface in this app already does (see the entry
   picker in §5 and the bottom-sheet modals in §3.7).
   ======================================================== */
@media (max-width: 767px) {
    .notif-panel {
        width: 100%;
        border-left: none;
    }

    /* The list is the only thing that should scroll; the header, the filter
       chips and the footer stay put so "Clear all" is reachable without
       scrolling to the end of a long list. */
    .notif-list { overscroll-behavior: contain; }

    /* Two lines of 11px metadata under every message is a lot of a phone
       screen spent on chrome. The absolute time is dropped, leaving the
       relative one ("12 min ago"), which is what anyone actually reads. */
    .notif-exact { display: none; }

    .notif-item { padding: var(--space-3) var(--space-2); }
}


/* ========================================================
   7. SMALL HANDSETS  (<= 400px)
   ========================================================
   320-360px viewports. Anything two-up here has about 150px per column,
   which is below the point where a card is readable. */
@media (max-width: 400px) {
    .entry-picker { grid-template-columns: 1fr; }
    .entry-card { min-height: 0; flex-direction: row; align-items: center; }
    .entry-card-hint { display: none; }

    .toolbar-right { grid-template-columns: 1fr; }
    .date-filter-actions { grid-template-columns: 1fr; }

    /* KPI cards go one-up; two 150px cards cannot hold "Rs. 4,250,000.00". */
    .grid-cols-2-sm { grid-template-columns: 1fr; }
    .inv-grid { grid-template-columns: 1fr; }
}


/* ========================================================
   8. LANDSCAPE PHONES
   ========================================================
   A phone turned sideways is ~360px TALL. A 240px chart plus the fixed
   header and the bottom nav leaves nothing. Charts shrink further; the
   sticky back bar stops being sticky, because a sticky bar on a 360px-tall
   viewport eats an eighth of it. */
@media (max-width: 900px) and (orientation: landscape) and (max-height: 500px) {
    .chart-frame,
    .chart-frame--sm,
    .chart-frame--md,
    .chart-skeleton { height: 200px; }

    .tab-group-scope.is-focused .entry-back { position: static; }
    .entry-picker { grid-template-columns: repeat(3, 1fr); }
}


/* ========================================================
   9. SAFE AREAS (notches, home indicators, punch-holes)
   ========================================================
   Applies in the installed Android app / PWA, where the page really does
   run edge to edge and there is no browser chrome to absorb the inset.
   ======================================================== */
@media (max-width: 767px) {
    /* main.css already reserves 72px for the bottom nav. Anything that
       positions itself against the bottom of the viewport must clear BOTH
       the nav and the home indicator. */
    .modal-content { margin-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }

    .main-content {
        padding-left: max(var(--space-2), env(safe-area-inset-left, 0px));
        padding-right: max(var(--space-2), env(safe-area-inset-right, 0px));
    }
}


/* ========================================================
   10. TOUCH DEVICES OF ANY SIZE
   ========================================================
   Keyed on the INPUT METHOD, not the screen width, so a touchscreen
   laptop or a wall-mounted panel in the mill gets these too. */
@media (hover: none) {
    /* :hover sticks after a tap on a touchscreen, leaving rows and cards
       visibly lifted until something else is tapped. main.css already
       guards .card/.panel/.kpi-card; these three were missed. */
    .order-card:hover,
    .recent-item:hover,
    .inv-item:hover { transform: none; box-shadow: none; }

    /* Anything that opens or submits gets the WCAG 2.5.5 target size. */
    .preset-btn,
    .tab-btn,
    .btn-sm,
    .btn-edit { min-height: 44px; }
}
