/* ==========================================================================
   FileTrack v3 — design system
   --------------------------------------------------------------------------
   Hand-written CSS. No preprocessor, no build step, no framework.

   Carries forward the visual decisions in design.md — institutional green,
   flat surfaces, system fonts, pill badges, cards as the single content
   container — and rebuilds the execution on design tokens, a real type scale,
   AA-checked contrast, and larger touch targets.

   Light only, deliberately. A `prefers-color-scheme: dark` variant used to
   ship, on the reasoning that records rooms are badly lit and phones are
   often in dark mode. In practice it meant the app looked black on a phone
   set to dark and green-on-white on the desk beside it, with no way to
   choose — the owner asked for one appearance, green and white, everywhere.
   A toggle would need JavaScript and persistent state, which Rules.md #9
   rules out for core UI, so the honest answer is a single palette.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
:root {
  /* Brand — carried over from v2 */
  --green-900: #0b3d33;
  --green-800: #0f5041;
  --green-700: #16604e;
  --green-600: #1d6f5c;
  --green-500: #2b8a72;
  --green-100: #dff0ea;
  --green-050: #eef7f4;

  /* Neutrals */
  --grey-950: #14161a;
  --grey-900: #1f2329;
  --grey-800: #2d333b;
  --grey-700: #454d57;
  --grey-600: #5c6672;
  --grey-500: #78838f;
  --grey-400: #9aa4ae;
  --grey-300: #c4ccd3;
  --grey-200: #dfe4e8;
  --grey-100: #eef1f3;
  --grey-050: #f6f8f9;

  /* Semantic */
  --red-700: #8f1d16;
  --red-600: #b3261e;
  --red-100: #fbe6e4;
  --amber-700: #6d4700;
  --amber-600: #8a5a00;
  --amber-100: #fbf0d9;
  --blue-600: #1c5c8f;
  --blue-100: #e2eefa;

  /* Roles — light */
  --bg: #f4f5f3;
  --surface: #ffffff;
  --surface-sunken: var(--grey-050);
  --surface-raised: #ffffff;
  --border: #e1e3df;
  --border-strong: var(--grey-300);
  --text: var(--grey-900);
  --text-muted: var(--grey-600);
  --text-faint: var(--grey-500);
  --text-inverse: #ffffff;
  --accent: var(--green-600);
  --accent-hover: var(--green-700);
  --accent-soft: var(--green-050);
  --accent-text: var(--green-800);
  --danger: var(--red-600);
  --danger-soft: var(--red-100);
  --danger-text: var(--red-700);
  --warning: var(--amber-600);
  --warning-soft: var(--amber-100);
  --warning-text: var(--amber-700);
  --info: var(--blue-600);
  --info-soft: var(--blue-100);
  --focus-ring: color-mix(in srgb, var(--green-600) 45%, transparent);

  /* Type scale — 1.200 minor third from 15px */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;
  --text-xs: 0.75rem;
  --text-sm: 0.8125rem;
  --text-base: 0.9375rem;
  --text-md: 1.0625rem;
  --text-lg: 1.25rem;
  --text-xl: 1.5rem;
  --text-2xl: 1.875rem;
  --text-3xl: 2.25rem;

  /* Spacing — 4px base */
  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.25rem;
  --sp-6: 1.5rem;
  --sp-8: 2rem;
  --sp-10: 2.5rem;
  --sp-12: 3rem;

  --radius-sm: 5px;
  --radius: 8px;
  --radius-lg: 12px;
  --radius-pill: 999px;

  --shadow-sm: 0 1px 2px rgba(16, 24, 32, 0.06);
  --shadow: 0 2px 8px rgba(16, 24, 32, 0.08);

  /* Minimum comfortable touch target */
  --tap: 44px;

  --sidebar-width: 244px;
  --content-max: 1240px;
}

/* --------------------------------------------------------------------------
   2. Reset
   -------------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 var(--sp-3);
  line-height: 1.25;
  font-weight: 600;
  letter-spacing: -0.011em;
}

h1 { font-size: var(--text-xl); }
h2 { font-size: var(--text-lg); }
h3 { font-size: var(--text-md); }
h4 { font-size: var(--text-base); }

p { margin: 0 0 var(--sp-4); }
p:last-child { margin-bottom: 0; }

a {
  color: var(--accent-text);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
a:hover { color: var(--accent-hover); }

img, svg { max-width: 100%; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--sp-4);
}

.stack > * + * { margin-top: var(--sp-4); }
.stack-sm > * + * { margin-top: var(--sp-2); }
.stack-lg > * + * { margin-top: var(--sp-6); }

.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
}
.row-between { justify-content: space-between; }
.row-end { justify-content: flex-end; }

.grid { display: grid; gap: var(--sp-4); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

/* --------------------------------------------------------------------------
   4. Card
   -------------------------------------------------------------------------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.card-header {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--border);
}

.card-header h2,
.card-header h3 { margin: 0; }

.card-body { padding: var(--sp-5); }
.card-body-flush { padding: 0; }

.card-footer {
  padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--border);
  background: var(--surface-sunken);
  border-radius: 0 0 var(--radius) var(--radius);
}

/* --------------------------------------------------------------------------
   5. Buttons
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-5);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--text-inverse);
  font: inherit;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.12s ease, border-color 0.12s ease;
  /* A button's label is short and is never improved by being broken in half.
     Without this, "Label" in a narrow table column wrapped to "Lab / el". */
  white-space: nowrap;
}
.btn:hover { background: var(--accent-hover); color: var(--text-inverse); }
.btn:disabled,
.btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; }

.btn-secondary {
  background: var(--surface);
  border-color: var(--border-strong);
  color: var(--text);
}
.btn-secondary:hover { background: var(--surface-sunken); color: var(--text); }

.btn-danger { background: var(--danger); }
.btn-danger:hover { background: var(--red-700); }

.btn-ghost {
  background: transparent;
  color: var(--accent-text);
  border-color: transparent;
}
.btn-ghost:hover { background: var(--accent-soft); color: var(--accent-text); }

.btn-sm {
  min-height: 34px;
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--text-sm);
}

/* --------------------------------------------------------------------------
   Touch targets
   --------------------------------------------------------------------------
   34px is comfortable with a mouse and too small with a thumb. Apple asks for
   44pt and Google for 48dp, and a records clerk uses this standing at a shelf
   holding a file in the other hand.

   Keyed on `pointer: coarse` rather than a width breakpoint. The thing that
   matters is what is doing the pointing, not how wide the screen is — a small
   laptop window still has a mouse, and a large tablet still has a thumb.

   Only the compact variants are touched; the full-size .btn is already 44px.
   -------------------------------------------------------------------------- */
@media (pointer: coarse) {
  .btn-sm,
  summary.btn-sm {
    min-height: var(--tap);
    padding-inline: var(--sp-4);
  }

  /* A bare <summary> is a disclosure toggle — "Permissions", "Offices" — and
     was 23px, the line box of its own text. Padding rather than a min-height,
     because changing its display would take the marker with it. */
  details > summary:not(.btn):not(.btn-sm) {
    padding-block: var(--sp-3);
  }

  /* Table action cells get taller rows, so the buttons in them do not end up
     touching each other. */
  .table td {
    padding-block: var(--sp-3);
  }
}

.btn-block { width: 100%; }

/* --------------------------------------------------------------------------
   6. Forms
   -------------------------------------------------------------------------- */
.field { display: block; margin-bottom: var(--sp-4); }

.label {
  display: block;
  margin-bottom: var(--sp-1);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-muted);
}

.input,
.select,
.textarea {
  display: block;
  width: 100%;
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  /* 16px on mobile prevents iOS Safari zooming the viewport on focus, which
     otherwise leaves the user scrolled sideways mid-form. */
  font-size: max(16px, var(--text-base));
}

.textarea { min-height: 96px; resize: vertical; }

.input:focus,
.select:focus,
.textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus-ring);
}

.input[aria-invalid="true"] { border-color: var(--danger); }

.hint {
  margin-top: var(--sp-1);
  font-size: var(--text-sm);
  color: var(--text-faint);
}

/* --------------------------------------------------------------------------
   7. Badges
   -------------------------------------------------------------------------- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px var(--sp-3);
  border-radius: var(--radius-pill);
  background: var(--grey-100);
  color: var(--grey-800);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}

.badge-success { background: var(--accent-soft); color: var(--accent-text); }
.badge-danger  { background: var(--danger-soft); color: var(--danger-text); }
.badge-warning { background: var(--warning-soft); color: var(--warning-text); }
.badge-info    { background: var(--info-soft); color: var(--info); }

/* --------------------------------------------------------------------------
   8. Alerts
   -------------------------------------------------------------------------- */
.alert {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--radius);
  background: var(--surface);
  font-size: var(--text-base);
}
.alert-success { border-left-color: var(--accent); background: var(--accent-soft); color: var(--accent-text); }
.alert-danger  { border-left-color: var(--danger); background: var(--danger-soft); color: var(--danger-text); }
.alert-warning { border-left-color: var(--warning); background: var(--warning-soft); color: var(--warning-text); }
.alert-info    { border-left-color: var(--info); background: var(--info-soft); color: var(--info); }

/* --------------------------------------------------------------------------
   9. Tables
   -------------------------------------------------------------------------- */
.table-wrap {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.table th,
.table td {
  padding: var(--sp-3) var(--sp-4);
  text-align: left;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.table thead th {
  position: sticky;
  top: 0;
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

.table tbody tr:hover { background: var(--surface-sunken); }

/* --------------------------------------------------------------------------
   Whole-row links
   --------------------------------------------------------------------------
   Pressing anywhere on a record's line opens it. Reaching for the file number
   specifically is a small target, and on a phone it is a genuinely fiddly one.

   No JavaScript. A click handler on the row would be the obvious way and is
   what most tables do, but Rules #9 rules that out for core navigation, and
   BUG-002 was JS-dependent UI failing on the owner's phones. Instead the
   existing anchor grows an empty absolutely-positioned pseudo-element that
   covers its whole row — the "stretched link" pattern. The row is still one
   link to a screen reader, keyboard focus still lands on it, middle-click and
   "open in new tab" still work, because it is still just an <a href>.

   Degrades safely: if a browser will not honour `position: relative` on a
   table row, the overlay sizes to the cell instead and the number stays
   clickable exactly as it does today. Nothing is lost, only unstretched.
   -------------------------------------------------------------------------- */
.table-linked tbody tr {
  position: relative;
}

.table-linked tbody tr:hover {
  background: var(--accent-soft);
  cursor: pointer;
}

.row-link::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

/* Anything else in the row that can be clicked has to sit above the overlay,
   or the row link swallows it — printing a label from the list would
   silently open the file instead. */
.table-linked tbody td a:not(.row-link),
.table-linked tbody td button,
.table-linked tbody td label,
.table-linked tbody td input,
.table-linked tbody td select {
  position: relative;
  z-index: 1;
}

/* The overlay makes the row a click target, which stops text selection inside
   it. The number itself stays selectable, since that is the value anyone
   would actually want to copy. */
.table-linked tbody td.mono {
  user-select: text;
}
.table tbody tr:last-child td { border-bottom: none; }

.table .num { text-align: right; font-variant-numeric: tabular-nums; }
.table .mono { font-family: var(--font-mono); }

.empty-state {
  padding: var(--sp-10) var(--sp-5);
  text-align: center;
  color: var(--text-faint);
}

/* --------------------------------------------------------------------------
   10. Definition list — used heavily on detail pages
   -------------------------------------------------------------------------- */
.datalist {
  display: grid;
  grid-template-columns: minmax(110px, max-content) 1fr;
  gap: var(--sp-2) var(--sp-5);
  margin: 0;
  font-size: var(--text-base);
}

.datalist dt {
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

.datalist dd { margin: 0; }

/* --------------------------------------------------------------------------
   11. Public QR scan pages
   --------------------------------------------------------------------------
   Reached from a phone camera while standing at a shelf. Optimised for one
   hand, poor light, and no JavaScript whatsoever.
   -------------------------------------------------------------------------- */
.scan-page {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

.scan-header {
  padding: var(--sp-4);
  background: var(--green-800);
  color: #fff;
  text-align: center;
}

.scan-header .brand {
  font-size: var(--text-md);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.scan-header .sub {
  font-size: var(--text-sm);
  opacity: 0.85;
}

.scan-main {
  flex: 1;
  width: 100%;
  max-width: 640px;
  margin-inline: auto;
  padding: var(--sp-5) var(--sp-4) var(--sp-10);
}

.scan-footer {
  padding: var(--sp-4);
  text-align: center;
  color: var(--text-faint);
  font-size: var(--text-xs);
}

/* The headline identity of the scanned item — big enough to read at arm's
   length while holding a physical file in the other hand. */
.scan-identity {
  text-align: center;
  padding: var(--sp-6) var(--sp-4);
}

.scan-identity .number {
  font-size: var(--text-3xl);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  word-break: break-word;
}

.scan-identity .kind {
  margin-top: var(--sp-2);
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* 4-digit code entry */
.code-form { max-width: 320px; margin-inline: auto; text-align: center; }

.code-input {
  width: 100%;
  padding: var(--sp-3) var(--sp-2);
  border: 2px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: 0.4em;
  text-align: center;
  text-indent: 0.4em; /* re-centres text that letter-spacing pushes left */
}

/* Four faint guide marks, so an empty box reads as "four digits go here"
   rather than as a large blank field. Pure CSS — no input masking, which
   would need JavaScript. */
.code-input::placeholder {
  color: var(--grey-300);
  font-weight: 400;
}

.code-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 4px var(--focus-ring);
}

.record-item {
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--border);
}
.record-item:last-child { border-bottom: none; }

.record-item .head {
  font-weight: 600;
  font-size: var(--text-md);
}

.record-item .meta {
  margin-top: var(--sp-1);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* --------------------------------------------------------------------------
   12. Print
   -------------------------------------------------------------------------- */
@media print {
  .no-print { display: none !important; }

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

  .card {
    border: 1px solid #999;
    box-shadow: none;
    break-inside: avoid;
  }

  .table thead th { position: static; background: #eee; color: #000; }

  a[href]::after { content: ""; }
}

/* --------------------------------------------------------------------------
   13. Motion
   -------------------------------------------------------------------------- */
@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;
  }
}

/* ==========================================================================
   14. Application shell
   --------------------------------------------------------------------------
   Sidebar on desktop, off-canvas drawer on mobile.

   The drawer is driven by a hidden checkbox and `:checked ~` sibling
   selectors — NO JavaScript. This is Rules.md #9, and it is not a stylistic
   preference: a JS toggle was tried, and it silently failed on the owner's
   iPhones in both Chrome and Safari (tracker.md BUG-002). CSS state cannot be
   broken by a script error, a content blocker, or an engine quirk.

   Do not replace this with a JS toggle or a framework component.
   ========================================================================== */

.app-shell {
  display: flex;
  min-height: 100dvh;
}

/* The checkbox itself is never seen; it only holds state. */
.menu-toggle-cb {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}

/* --- Sidebar ---
   Green, with white on it. The green is `--green-800` rather than the old
   `--green-900`: at #0b3d33 the panel read as black rather than as a colour,
   which was the complaint. Two shades up it is unmistakably green and still
   carries white text at well over 7:1.

   `position: sticky` with a viewport height is the other half. Without it the
   sidebar is simply a tall flex child that grows to match the page content,
   so on a 325-row list the Log out button sits a thousand pixels below the
   fold and you have to scroll the whole table to sign out. Now the nav
   scrolls inside itself and the footer stays put. */
.sidebar {
  flex: none;
  width: var(--sidebar-width);
  background: var(--green-800);
  color: rgba(255, 255, 255, 0.9);
  display: flex;
  flex-direction: column;

  /* dvh, not vh: on iOS Safari `100vh` is taller than the visible area while
     the browser chrome is showing, which would push the footer off-screen —
     the exact bug this is fixing. */
  position: sticky;
  top: 0;
  height: 100dvh;
}

.sidebar-brand {
  padding: var(--sp-5) var(--sp-5) var(--sp-4);
  font-size: var(--text-md);
  font-weight: 700;
  letter-spacing: 0.01em;
  color: #fff;
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}

.sidebar-nav {
  flex: 1;
  padding: var(--sp-3) 0 var(--sp-6);
  overflow-y: auto;
  /* Keeps the scroll gesture inside the nav instead of chaining to the page
     once it reaches the end. */
  overscroll-behavior: contain;
}

.nav-section {
  padding: var(--sp-4) var(--sp-5) var(--sp-1);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
}

.nav-link {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-5);
  color: rgba(255, 255, 255, 0.88);
  font-size: var(--text-base);
  text-decoration: none;
  border-left: 3px solid transparent;
}

.nav-link:hover {
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
}

.nav-link[aria-current="page"] {
  background: rgba(255, 255, 255, 0.16);
  border-left-color: #8fd9c3;
  color: #fff;
  font-weight: 600;
}

.sidebar-footer {
  padding: var(--sp-4) var(--sp-5);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  background: var(--green-900);
  font-size: var(--text-sm);
}

.sidebar-footer .who {
  color: #fff;
  font-weight: 600;
}

.sidebar-footer .role {
  color: rgba(255, 255, 255, 0.6);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.logout-form { margin-top: var(--sp-3); }

/* 44px, not 38: signing out is a real tap target on a phone, and it sits
   right above the home indicator where a small button is easy to miss. */
.btn-logout {
  width: 100%;
  min-height: var(--tap);
  padding: var(--sp-1) var(--sp-3);
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: var(--radius);
  color: #fff;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.btn-logout:hover {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.5);
}

/* --- Main column --- */
.main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.content {
  flex: 1;
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  padding: var(--sp-6) var(--sp-5) var(--sp-12);
}

.page-header {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: var(--sp-5);
}

.page-header h1 { margin: 0; }

.page-header .lede {
  margin: var(--sp-1) 0 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* --- Mobile top bar --- */
.mobile-topbar { display: none; }

.sidebar-overlay { display: none; }

@media (max-width: 860px) {
  .app-shell { display: block; }

  .mobile-topbar {
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--green-800);
    color: #fff;
  }

  .mobile-menu-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    border-radius: var(--radius);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    user-select: none;
  }

  .mobile-menu-toggle:hover { background: rgba(255, 255, 255, 0.12); }

  .mobile-topbar-brand {
    font-weight: 700;
    font-size: var(--text-md);
  }

  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    /* Explicit left/width rather than `inset` or a transform: older WebKit
       handles this reliably, and BUG-002 was an older-WebKit problem. */
    left: -280px;
    width: 272px;
    z-index: 50;
    transition: left 0.2s ease;
  }

  .sidebar-overlay {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 40;
    background: rgba(0, 0, 0, 0.45);
    cursor: pointer;
  }

  /* The whole mechanism, in two rules. */
  .menu-toggle-cb:checked ~ .sidebar { left: 0; }
  .menu-toggle-cb:checked ~ .sidebar-overlay { display: block; }

  .content { padding: var(--sp-4) var(--sp-3) var(--sp-10); }
}

/* --------------------------------------------------------------------------
   15. Login / change-password (standalone, no shell)
   -------------------------------------------------------------------------- */
.login-wrap {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-5);
  background: var(--green-900);
}

.login-box {
  width: 100%;
  max-width: 400px;
  padding: var(--sp-8) var(--sp-6);
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}

.login-box h1 {
  margin-bottom: var(--sp-1);
  font-size: var(--text-xl);
  text-align: center;
}

.login-box .sub {
  margin-bottom: var(--sp-6);
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-align: center;
}

.login-box .foot {
  margin-top: var(--sp-5);
  font-size: var(--text-sm);
  text-align: center;
}

/* --------------------------------------------------------------------------
   16. Flash messages
   -------------------------------------------------------------------------- */
.flashes { margin-bottom: var(--sp-5); }
.flashes > * + * { margin-top: var(--sp-2); }

/* --------------------------------------------------------------------------
   17. Stat cards
   -------------------------------------------------------------------------- */
.stat {
  padding: var(--sp-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.stat .value {
  font-size: var(--text-2xl);
  font-weight: 700;
  line-height: 1.1;
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}

.stat .label {
  margin-top: var(--sp-1);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

@media print {
  .sidebar, .mobile-topbar, .sidebar-overlay { display: none !important; }
  .content { max-width: none; padding: 0; }
}

/* --------------------------------------------------------------------------
   18. Disclosure panels (<details>)
   --------------------------------------------------------------------------
   Every collapsible form in the app is a <details>/<summary> pair. That is a
   deliberate choice, not a stylistic one: it collapses and expands with zero
   JavaScript (Rules.md #9), so the container screens keep working on the
   phones that broke in BUG-002.
   -------------------------------------------------------------------------- */

/* Remove the native marker so a <summary> can be styled as a button. */
summary { list-style: none; }
summary::-webkit-details-marker { display: none; }
summary::marker { content: ""; }

summary.btn,
summary.btn-sm {
  display: inline-flex;
  user-select: none;
}

/* A card whose header is the toggle. */
details.card > summary { list-style: none; }
details.card > summary::after {
  content: "+";
  margin-left: auto;
  font-size: var(--text-lg);
  line-height: 1;
  color: var(--text-muted);
}
details.card[open] > summary::after { content: "\2212"; } /* minus */

/* --- Confirmation tucked inside a table row ---
   A destructive action in a list needs its confirmation next to the thing
   being destroyed, not in a modal that has to be told which row it is about.
   `<details>` gives that with no JavaScript; the panel is absolutely
   positioned so opening it does not shove the table rows around. */
.inline-confirm {
  position: relative;
  display: inline-block;
}

.inline-confirm > :not(summary) {
  position: absolute;
  right: 0;
  z-index: 20;
  width: 15rem;
  margin-top: var(--sp-2);
  padding: var(--sp-3);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: left;
  white-space: normal;
}

/* A control that exists to explain why it cannot be used. Shown rather than
   hidden, so the "in use" count beside it reads as the reason. */
[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Row of per-record actions.
   The tricky part: the summaries sit side by side like a button bar, but an
   OPEN panel must break to its own full-width row — otherwise its form is
   squeezed into one button's width, which is exactly what it looked like
   before this rule existed. */
.record-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: flex-start;
}

.record-actions > details,
.record-actions > form { flex: none; }

.record-actions > details[open] {
  flex-basis: 100%;
  padding: var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}

/* --------------------------------------------------------------------------
   19. Table refinements
   -------------------------------------------------------------------------- */

/* Container numbers are identifiers — never break them across lines.
   "FIL-\n0001" is unreadable at a glance, which is the only job the column
   has. */
.table .mono {
  white-space: nowrap;
}

/* Wrap long cell text at spaces, not mid-word. `break-word` turned "Mahmood"
   into "Mahmo\nod" once a column got narrow on a phone; `overflow-wrap` only
   breaks a token that genuinely has no space to break at. Identifiers never
   break. */
.table td { word-break: normal; overflow-wrap: break-word; }
.table td.mono, .table .mono { word-break: normal; overflow-wrap: normal; }

/* --------------------------------------------------------------------------
   19b. Stacked-card tables on phones
   --------------------------------------------------------------------------
   A five-column table cannot fit a phone: the browser either crushes columns
   (breaking words mid-syllable) or clips the right-hand ones off-screen, and a
   horizontal scrollbar is undiscoverable under a thumb. Tables that a person
   actually browses on a phone — search results, file lists, a checked-out
   list, the shelf-scan page — carry `.table-cards`, and on a narrow screen each
   row becomes one labelled card. Every field is visible, nothing is clipped,
   and there is no sideways scroll. Pure CSS (Rules.md #9): the labels come from
   each cell's `data-label`, rendered with `::before`.

   Admin and report tables are wide by nature and rarely opened on a phone; they
   keep the scrolling `.table-wrap` and simply no longer break words mid-token.

   The breakpoint is 640px — phones in portrait. Tablets keep the real table. */
@media (max-width: 640px) {
  .table-cards thead {
    /* Hidden visually; the per-cell data-labels carry the column names now. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }

  .table-cards,
  .table-cards tbody,
  .table-cards tr,
  .table-cards td { display: block; width: auto; }

  .table-cards tbody tr {
    position: relative;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    padding: var(--sp-2) var(--sp-4);
    margin-bottom: var(--sp-3);
  }
  .table-cards tbody tr:last-child { margin-bottom: 0; }
  .table-cards tbody tr:hover { background: var(--surface); }

  .table-cards td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--sp-4);
    padding: var(--sp-2) 0;
    border: 0;
    border-bottom: 1px solid var(--border);
    text-align: right;
  }
  .table-cards td:last-child { border-bottom: 0; }

  .table-cards td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    text-align: left;
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  /* The title cell (the record name or file number) heads the card rather than
     reading as a label:value pair. It keeps its own line, and any stretched
     row-link inside it still covers the whole card, so tapping anywhere on the
     card opens the record. */
  .table-cards td.card-title {
    justify-content: flex-start;
    font-size: var(--text-md);
    font-weight: 600;
    padding-top: var(--sp-1);
  }
  .table-cards td.card-title::before { content: none; }

  /* A cell with no value to show (data-label but empty) still reads as a row. */
  .table-cards td:empty { display: none; }
}

/* --------------------------------------------------------------------------
   20. Trend bars
   --------------------------------------------------------------------------
   A twelve-month bar chart in pure CSS. No charting library and no
   JavaScript: it is twelve numbers, and Rules.md #9 means the dashboard has
   to read correctly with scripts disabled.

   Heights come from a `--bar` custom property set inline per bar, which is
   the one thing that genuinely varies per render.
   -------------------------------------------------------------------------- */
.trend {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-2);
  height: 140px;
  padding-top: var(--sp-4);
}

.trend-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  height: 100%;
  min-width: 0;
}

.trend-bar {
  width: 100%;
  max-width: 34px;
  min-height: 2px;
  height: calc(var(--pct, 0) * 1%);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: var(--accent);
}

/* A month with no records still gets a visible tick, so the gap reads as
   "nothing happened" rather than as a rendering fault. */
.trend-col[data-empty="true"] .trend-bar {
  background: var(--border-strong);
}

.trend-value {
  margin-bottom: var(--sp-1);
  font-size: var(--text-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.trend-label {
  margin-top: var(--sp-2);
  font-size: var(--text-xs);
  color: var(--text-faint);
  white-space: nowrap;
}

/* Horizontal proportion bars, for the top-categories list. */
.meter {
  height: 6px;
  margin-top: var(--sp-1);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  overflow: hidden;
}

.meter > span {
  display: block;
  height: 100%;
  width: calc(var(--pct, 0) * 1%);
  border-radius: var(--radius-pill);
  background: var(--accent);
}

/* ==========================================================================
   Utilities
   ==========================================================================

   These exist because the Content-Security-Policy is `style-src 'self'` —
   no `unsafe-inline` — which means the browser silently ignores every
   inline `style=` attribute. Silently is the operative word: the page still
   renders, just without the spacing or the bar height, so the failure looks
   like a design choice rather than a bug. Every layout tweak therefore has
   to be a class in this file.

   `tests/test_no_inline_styles.py` enforces it. */

.m-0  { margin: 0; }
.mt-1 { margin-top: var(--sp-1); }
.mt-2 { margin-top: var(--sp-2); }
.mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); }
.mb-2 { margin-bottom: var(--sp-2); }
.mb-3 { margin-bottom: var(--sp-3); }
.mb-4 { margin-bottom: var(--sp-4); }
.mb-5 { margin-bottom: var(--sp-5); }

.span-full { grid-column: 1 / -1; }
.grow      { flex: 1; min-width: 240px; }
.gap-1     { gap: var(--sp-1); }
.max-w-sm  { max-width: 320px; }
.clickable { cursor: pointer; }

/* --------------------------------------------------------------------------
   The print dialog
   --------------------------------------------------------------------------
   An overlay inside the application, not a tab and not a second browser
   window. It holds the label in an <iframe> and a Print button.

   Opened by `:target` — the trigger is a link to `#print-dialog` and the close
   control is a link back — so it works with JavaScript switched off, which is
   the whole point of Rules #9 and the reason BUG-002 happened. `<dialog>` and
   `showModal()` would be the modern answer and would need a script to open at
   all.

   The iframe matters as much as the overlay. Printing an iframe prints *its*
   document and *its* @page rules, so the surrounding application never reaches
   the paper — no print stylesheet has to hide the sidebar, and nothing in
   app.css can accidentally change the physical size of a label.
   -------------------------------------------------------------------------- */
.print-dialog {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(15, 22, 20, 0.55);
  padding: var(--sp-4);
  overflow-y: auto;
}

/* `:target` for the link-opened case; `.is-open` for the one that arrives
   already open — a POST response has no fragment to target, and adding one
   with a script would put the whole dialog behind JavaScript. */
.print-dialog:target,
.print-dialog.is-open {
  display: flex;
  align-items: flex-start;
  justify-content: center;
}

.print-dialog-panel {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  width: 100%;
  max-width: 30rem;
  margin: auto;
  display: flex;
  flex-direction: column;
  max-height: calc(100dvh - var(--sp-8));
}

.print-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--border);
}

.print-dialog-head h2 {
  margin: 0;
  font-size: var(--text-md);
}

.print-dialog-body {
  padding: var(--sp-5);
  overflow-y: auto;
}

/* A light ground behind the label, so a white sticker on a white panel still
   reads as an object with edges. */
.print-preview {
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-4);
  display: flex;
  justify-content: center;
}

/* Sized in millimetres: this is a true-size preview, so what is on screen is
   the physical thing. Plus a little room so a multi-label run can scroll. */
.print-frame {
  border: 0;
  width: 46mm;
  height: 46mm;
  background: #fff;
}

/* The frame is a true-size window onto the label, so its height follows the
   label's — 6mm of slack for the frame's own border and a little breathing
   room, matching .print-frame's 46mm for a 40mm square. */
.print-frame-wide { height: 26mm; }
.print-frame-strip { height: 21mm; }
.print-frame-sheet { width: 100%; height: 40vh; }

.print-dialog-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--border);
}

.print-dialog-foot .spacer { margin-right: auto; }
.uppercase { text-transform: uppercase; }
.block     { display: block; }
.inline    { display: inline; }
.min-w-0   { min-width: 0; }
.items-end   { align-items: end; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }

/* A field inside a filter grid: the grid already provides the spacing, so the
   field's own bottom margin would push its neighbours out of alignment. */
.field-flush { margin: 0; }
.text-center { text-align: center; }

/* A field that sits directly under something else in a stack, so it needs a
   top gap and no bottom one. */
.field-tight { margin: var(--sp-3) 0 0; }

/* The QR token, shown so it can be checked against a printed label. It is a
   long unbroken string, so it has to be allowed to break anywhere. */
.token-text {
  word-break: break-all;
  font-family: var(--font-mono);
}

/* The whole cell is the tap target on the bulk-print screen, so selecting a
   label on a phone does not depend on hitting a 13px checkbox. */
.pick-target {
  display: flex;
  align-items: center;
  min-width: var(--tap);
  min-height: var(--tap);
}

.error-body { text-align: center; padding: var(--sp-10) var(--sp-5); }
.error-code {
  font-size: var(--text-3xl);
  font-weight: 700;
  color: var(--text-faint);
}
.muted     { color: var(--text-muted); }
.text-sm   { font-size: var(--text-sm); }

/* A radio or checkbox and its wording, on one line, with a full-size tap
   target. Used wherever a form asks a this-or-that question. */
.choice {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
}

/* Column widths for the report tables, which are shared between the screen
   and the PDF. */
.col-10 { width: 10%; }
.col-12 { width: 12%; }
.col-20 { width: 20%; }
.col-26 { width: 26%; }

/* --------------------------------------------------------------------------
   Percentages, without inline styles

   A bar's height and a meter's width are per-row data, so they cannot be a
   fixed class — and `style="--pct: 87"` is exactly what the CSP throws away.
   The alternatives were a nonce'd <style> block or `unsafe-inline`; both
   loosen a policy the audit deliberately tightened, to draw a bar chart.

   So the value travels as an attribute and lands on a custom property here.
   101 rules, about 2KB, no JavaScript, and it works in every browser that
   understands custom properties.
   -------------------------------------------------------------------------- */

[data-pct="0"] { --pct: 0; }
[data-pct="1"] { --pct: 1; }
[data-pct="2"] { --pct: 2; }
[data-pct="3"] { --pct: 3; }
[data-pct="4"] { --pct: 4; }
[data-pct="5"] { --pct: 5; }
[data-pct="6"] { --pct: 6; }
[data-pct="7"] { --pct: 7; }
[data-pct="8"] { --pct: 8; }
[data-pct="9"] { --pct: 9; }
[data-pct="10"] { --pct: 10; }
[data-pct="11"] { --pct: 11; }
[data-pct="12"] { --pct: 12; }
[data-pct="13"] { --pct: 13; }
[data-pct="14"] { --pct: 14; }
[data-pct="15"] { --pct: 15; }
[data-pct="16"] { --pct: 16; }
[data-pct="17"] { --pct: 17; }
[data-pct="18"] { --pct: 18; }
[data-pct="19"] { --pct: 19; }
[data-pct="20"] { --pct: 20; }
[data-pct="21"] { --pct: 21; }
[data-pct="22"] { --pct: 22; }
[data-pct="23"] { --pct: 23; }
[data-pct="24"] { --pct: 24; }
[data-pct="25"] { --pct: 25; }
[data-pct="26"] { --pct: 26; }
[data-pct="27"] { --pct: 27; }
[data-pct="28"] { --pct: 28; }
[data-pct="29"] { --pct: 29; }
[data-pct="30"] { --pct: 30; }
[data-pct="31"] { --pct: 31; }
[data-pct="32"] { --pct: 32; }
[data-pct="33"] { --pct: 33; }
[data-pct="34"] { --pct: 34; }
[data-pct="35"] { --pct: 35; }
[data-pct="36"] { --pct: 36; }
[data-pct="37"] { --pct: 37; }
[data-pct="38"] { --pct: 38; }
[data-pct="39"] { --pct: 39; }
[data-pct="40"] { --pct: 40; }
[data-pct="41"] { --pct: 41; }
[data-pct="42"] { --pct: 42; }
[data-pct="43"] { --pct: 43; }
[data-pct="44"] { --pct: 44; }
[data-pct="45"] { --pct: 45; }
[data-pct="46"] { --pct: 46; }
[data-pct="47"] { --pct: 47; }
[data-pct="48"] { --pct: 48; }
[data-pct="49"] { --pct: 49; }
[data-pct="50"] { --pct: 50; }
[data-pct="51"] { --pct: 51; }
[data-pct="52"] { --pct: 52; }
[data-pct="53"] { --pct: 53; }
[data-pct="54"] { --pct: 54; }
[data-pct="55"] { --pct: 55; }
[data-pct="56"] { --pct: 56; }
[data-pct="57"] { --pct: 57; }
[data-pct="58"] { --pct: 58; }
[data-pct="59"] { --pct: 59; }
[data-pct="60"] { --pct: 60; }
[data-pct="61"] { --pct: 61; }
[data-pct="62"] { --pct: 62; }
[data-pct="63"] { --pct: 63; }
[data-pct="64"] { --pct: 64; }
[data-pct="65"] { --pct: 65; }
[data-pct="66"] { --pct: 66; }
[data-pct="67"] { --pct: 67; }
[data-pct="68"] { --pct: 68; }
[data-pct="69"] { --pct: 69; }
[data-pct="70"] { --pct: 70; }
[data-pct="71"] { --pct: 71; }
[data-pct="72"] { --pct: 72; }
[data-pct="73"] { --pct: 73; }
[data-pct="74"] { --pct: 74; }
[data-pct="75"] { --pct: 75; }
[data-pct="76"] { --pct: 76; }
[data-pct="77"] { --pct: 77; }
[data-pct="78"] { --pct: 78; }
[data-pct="79"] { --pct: 79; }
[data-pct="80"] { --pct: 80; }
[data-pct="81"] { --pct: 81; }
[data-pct="82"] { --pct: 82; }
[data-pct="83"] { --pct: 83; }
[data-pct="84"] { --pct: 84; }
[data-pct="85"] { --pct: 85; }
[data-pct="86"] { --pct: 86; }
[data-pct="87"] { --pct: 87; }
[data-pct="88"] { --pct: 88; }
[data-pct="89"] { --pct: 89; }
[data-pct="90"] { --pct: 90; }
[data-pct="91"] { --pct: 91; }
[data-pct="92"] { --pct: 92; }
[data-pct="93"] { --pct: 93; }
[data-pct="94"] { --pct: 94; }
[data-pct="95"] { --pct: 95; }
[data-pct="96"] { --pct: 96; }
[data-pct="97"] { --pct: 97; }
[data-pct="98"] { --pct: 98; }
[data-pct="99"] { --pct: 99; }
[data-pct="100"] { --pct: 100; }
