/* ==========================================================================
   eCourts Tracker — design system
   Spec: docs/ui-spec.md §0. Dark is the primary (and only) screen theme;
   paper is a separate, deliberate light rendering in print.css.

   Position, stated once so it is not re-litigated in a later pass:

   * SHARP. No rounded corners anywhere (`--radius: 0`). Edges are cut, not
     softened — the softened-10px-everything look is what the last pass was
     rightly called out for.
   * THE CHROME HAS NO HUE. Navigation, buttons, links, focus rings and
     headings are all achromatic. That is not timidity: this product's own
     first rule is "colour means state, or it means nothing", and the only
     way to make that literally true is to spend the entire colour budget on
     the three hearing-date states and nothing else. Three hues exist in this
     file — amber, red, blue — and each one means exactly one thing about a
     case.
   * NO decorative shadow, no gradient over content, no translucency, no
     backdrop-filter (it recomposites every scroll frame, and this is a long
     list on old office GPUs), no purple, no "AI beige".
   * Depth is carried by three flat surface steps and hairlines, never by a
     blur.

   Token budget, held deliberately tight: 14 colours, 6 type sizes, 7 spacing
   steps. If something here needs a fifteenth colour, the answer is almost
   always that it should reuse a state token or none at all.
   ========================================================================== */

/* ---- typography ----------------------------------------------------------
   IBM Plex Sans, variable, latin subset, self-hosted (45.7 KB — the Inter it
   replaces was 352 KB). Never a CDN: this page gets opened in a courtroom
   corridor on a bad connection, and a blocking request to a font host in
   that exact moment is the one failure this product cannot afford.

   Chosen for one property above all: its figures are tabular BY DEFAULT —
   all ten digits are exactly 600/1000 em. Fonts that reach tabular through
   the `tnum` OpenType feature depend on that feature actually being applied;
   here the metric IS the default, so a column of DD-MM-YYYY dates cannot
   jitter even if `font-variant-numeric` is lost, overridden, or unsupported.
   On a screen that is nothing but dates and case numbers that is the
   difference between a scannable column and a transcription error.

   The fallback stack is the real safety net: if the woff2 has not arrived,
   the system font renders instantly (`font-display: swap`) and Plex swaps
   in. Never invisible text.

   Note: the latin subset has no arrow glyphs (U+2190/2192). Every arrow in
   this product is therefore an inline SVG icon or a guillemet (‹ ›), which
   the subset does carry — never a bare "→" that would silently fall back to
   a different typeface mid-sentence.
   ------------------------------------------------------------------------ */
@font-face {
  font-family: "IBM Plex Sans";
  src: url("/static/fonts/IBMPlexSans-latin-wght.woff2") format("woff2");
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
}

/* ---- tokens --------------------------------------------------------------
   Authored in oklch: perceptually uniform, so "one step darker" is one step
   darker to the eye at every hue, and the three state colours can be held at
   the same lightness without one of them shouting.

   Every colour is declared TWICE — sRGB hex first, oklch second. A browser
   that understands oklch takes the second declaration; one that does not
   ignores it and keeps the hex. That matters here specifically: these are
   old office PCs, oklch() is Chrome/Edge 111+, and an unparsed custom
   property does not degrade gracefully — it takes the whole page's colour
   with it. The hex values are generated from the oklch ones, not eyeballed.

   Contrast, measured on these exact values (see docs/ui-spec.md §0.1):
     ink on bg 15.9:1 · ink on surface 14.5:1 · ink on surface-raised 12.7:1
     ink-muted on surface 8.5:1 · awaiting 9.9:1 · stale 7.6:1 · moved 9.2:1
     control borders (line-strong) 3.4:1 — clears WCAG 1.4.11 non-text
   Neither pure #000 nor pure #fff is used: white-on-black haloes at arm's
   length, and it is the pairing that makes a saturated red bloom.
   ------------------------------------------------------------------------ */
:root {
  color-scheme: dark;

  /* --- surfaces: three flat steps, no blur, no translucency --- */
  --bg: #101316;
  --bg: oklch(0.185 0.008 255);
  --surface: #1A1D21;
  --surface: oklch(0.228 0.009 255);
  --surface-raised: #24272C;
  --surface-raised: oklch(0.272 0.010 255);

  /* --- lines: hairline for separation, strong for control edges --- */
  --line: #32363B;
  --line: oklch(0.330 0.011 255);
  --line-strong: #6B7178;
  --line-strong: oklch(0.545 0.014 255);

  /* --- text --- */
  --ink: #EBEDEF;
  --ink: oklch(0.945 0.004 255);
  --ink-muted: #B4B8BC;
  --ink-muted: oklch(0.780 0.008 255);
  --ink-faint: #909499;
  --ink-faint: oklch(0.665 0.009 255);

  /* --- chrome accent: achromatic on purpose (see the header note) --- */
  --accent: #E3E8F0;
  --accent: oklch(0.930 0.012 255);
  --on-accent: #101316;
  --on-accent: oklch(0.185 0.008 255);
  --accent-wash: #272B31;
  --accent-wash: oklch(0.288 0.013 255);

  /* --- state: the ENTIRE colour vocabulary of this product ---------------
     One hue per state, all three held near the same lightness so none of
     them outranks the others by accident. Nothing else in the app is
     allowed to borrow these. "Confirmed" is deliberately absent: at 500
     cases most rows are confirmed, and giving them a colour would drown the
     two that need a human. Disposed is absent for the same reason inverted
     — it is the one state meant to recede, so it reuses --ink-faint. */
  --state-await: #F0BE6D;   /* nothing has published a new date yet */
  --state-await: oklch(0.830 0.115 78);
  --state-stale: #FA9186;   /* nothing has confirmed this — verify by hand */
  --state-stale: oklch(0.765 0.128 27);
  --state-moved: #7FC8F3;   /* transferred: moved court, still live */
  --state-moved: oklch(0.800 0.095 236);

  /* --- type: exactly six sizes ------------------------------------------
     12 / 14 / 16 / 18 / 24 / 36. Body is 16px and never smaller — it is the
     iOS auto-zoom floor for inputs and the honest "read one-handed, standing
     up, in bad light" size. */
  --fs-1: 0.75rem;
  --fs-2: 0.875rem;
  --fs-3: 1rem;
  --fs-4: 1.125rem;
  --fs-5: 1.5rem;
  --fs-6: 2.25rem;

  /* --- space: exactly seven steps --- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;

  /* --- geometry --- */
  --radius: 0;              /* sharp. the token exists so it stays sharp. */
  --rail: 3px;              /* the state rail down the left edge of a row */
  --tap: 44px;              /* minimum touch target, everywhere */
  --sidenav-w: 232px;
  --track: 0.08em;          /* letter-spacing for the micro-label voice */
}

/* ==========================================================================
   reset / base
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html { font-size: 16px; }

body {
  font-family: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--fs-3);
  color: var(--ink);
  background: var(--bg);
  margin: 0;
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
  /* Belt and braces: Plex is already tabular by metric, but a fallback
     render (system-ui, before the woff2 lands) is not — this keeps the date
     column from reflowing when the real font swaps in. */
  font-variant-numeric: tabular-nums;
}

[hidden] { display: none !important; }

/* Headings sit on a single tight scale; weight and tracking do the work,
   not size jumps. */
h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.012em; line-height: 1.2; }
h1 { font-size: var(--fs-5); margin: var(--sp-5) 0 var(--sp-3); }
h2 { font-size: var(--fs-4); margin: var(--sp-5) 0 var(--sp-3); }
h3 { font-size: var(--fs-3); margin: var(--sp-4) 0 var(--sp-2); }
p { margin: var(--sp-2) 0; }

/* The micro-label voice: uppercase, tracked, small, quiet. Used for every
   overline, column header and section label in the product — it is the one
   typographic move that carries the "instrument, not website" register. */
.overline,
.eyebrow {
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-faint);
  margin: 0 0 var(--sp-2);
}

.page-lede { color: var(--ink-muted); margin: calc(-1 * var(--sp-2)) 0 var(--sp-4); max-width: 52ch; }

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

/* Focus is a first-class citizen: the clerk's desktop day is keyboard-driven
   and the ring has to be unmistakable on every one of the three surfaces. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 0;
}

/* ONE named exception, and it is named here so a second one has to argue with
   this comment first (ui-spec.md §2.2a).

   `.reveal-late` is the class on the three delayed lines of the search
   screen's loading state: the saved band's "Looking…" at 500ms, and the
   portal search's escalation copy at 5s and 12s. They are single-step opacity
   keyframes with no interpolation — CSS has no timer, so `animation` is being
   used as one. They move nothing and fade nothing.

   `prefers-reduced-motion` is a vestibular-safety preference, and the blanket
   reset below would use it to WITHHOLD TEXT from exactly the users most
   likely to be reading carefully. The genuinely moving element on that
   screen — the indeterminate progress bar — is NOT exempted and does stop
   (see `.inflight__bar` in the reduce block below). If a second exception is
   ever proposed, it is almost certainly real motion wearing this argument as
   a costume. */
@media (prefers-reduced-motion: reduce) {
  *:not(.reveal-late), *::before, *::after { transition: none !important; animation: none !important; }
  .reveal-late { transition: none !important; }
}

.icon { width: 1em; height: 1em; vertical-align: -0.125em; flex: none; }

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

.skip-link { position: absolute; left: -999px; top: 0; }
.skip-link:focus {
  left: var(--sp-2);
  top: var(--sp-2);
  background: var(--accent);
  color: var(--on-accent);
  padding: var(--sp-2) var(--sp-3);
  text-decoration: none;
  z-index: 40;
}

.muted { color: var(--ink-muted); font-size: var(--fs-2); overflow-wrap: anywhere; }
.nowrap { white-space: nowrap; }   /* a DD-MM-YYYY date never breaks at its hyphens */
.empty-state { color: var(--ink-muted); padding: var(--sp-5) 0; }

/* ==========================================================================
   The shell — three destinations (Calendar / Registry pull / Smart search).
   ≥1024px: fixed side nav + sticky top bar. Below: top bar + fixed bottom
   tab bar, thumb-reachable one-handed. Both are in the markup; CSS shows
   exactly one, so no client-side state decides the navigation.
   ========================================================================== */
.sidenav {
  display: none;
  position: fixed;
  inset: 0 auto 0 0;
  width: var(--sidenav-w);
  background: var(--surface);
  border-right: 1px solid var(--line);
  z-index: 30;
  flex-direction: column;
  padding: var(--sp-4) 0;
}
.sidenav__brand {
  display: block;
  text-decoration: none;
  color: var(--ink);
  font-weight: 600;
  font-size: var(--fs-3);
  letter-spacing: -0.01em;
  padding: var(--sp-2) var(--sp-4) var(--sp-5);
}
.sidenav__brand .sidenav__sub {
  display: block;
  color: var(--ink-faint);
  font-weight: 500;
  font-size: var(--fs-1);
  text-transform: uppercase;
  letter-spacing: var(--track);
  margin-top: var(--sp-1);
}
.sidenav__nav { display: flex; flex-direction: column; }
.sidenav__link {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--tap);
  padding: 0 var(--sp-4);
  border-left: var(--rail) solid transparent;
  text-decoration: none;
  color: var(--ink-muted);
  font-size: var(--fs-2);
  font-weight: 500;
  transition: background-color 120ms linear, color 120ms linear;
}
.sidenav__link .icon { width: 1.15em; height: 1.15em; }
.sidenav__link:hover { background: var(--surface-raised); color: var(--ink); }
.sidenav__link[aria-current="page"] {
  background: var(--accent-wash);
  border-left-color: var(--accent);
  color: var(--ink);
  font-weight: 600;
}
.sidenav__hint {
  /* `auto` top margin pushed this to the bottom of the column while it WAS
     the last child. Sign out is now below it, so the push moves here. */
  margin: auto var(--sp-4) 0;
  padding-top: var(--sp-3);
  color: var(--ink-faint);
  font-size: var(--fs-1);
  line-height: 1.5;
  border-top: 1px solid var(--line);
}

/* Sign out, at the foot of the nav (the top bar that used to hold it was
   removed 2026-08-09). Same reachability contract as before: always visible,
   never behind a menu, one tap.

   It is drawn as a REAL CONTROL -- a bordered button inset from the edge --
   and not as a third nav row. The first attempt made it a full-bleed strip in
   --ink-faint with a hairline above it, directly under the hint paragraph's
   own hairline: two rules stacked, and the only destructive-ish action in the
   shell reading as a footnote to a sentence about search. A control that ends
   the session on a shared office PC has to look like something you press. */
/* Settings, at the foot of the nav where Sign out used to be. Drawn as a
   nav row rather than a button: it IS a destination now (Sign out and Change
   the passcode moved inside it), and a button at the bottom of a list of
   links reads as an action you are about to take by accident. */
.sidenav__settings {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--tap);
  margin: var(--sp-3) 0 var(--sp-4);
  padding: 0 var(--sp-4);
  border-left: var(--rail) solid transparent;
  border-top: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: var(--fs-2);
  font-weight: 500;
  text-decoration: none;
  transition: background-color 120ms linear, color 120ms linear;
}
.sidenav__settings .icon { width: 1.15em; height: 1.15em; }
.sidenav__settings:hover { background: var(--surface-raised); color: var(--ink); }
.sidenav__settings[aria-current="page"] {
  background: var(--accent-wash);
  border-left-color: var(--accent);
  color: var(--ink);
  font-weight: 600;
}

/* THE ONLY ALARM CHROME IN THIS PRODUCT.
   Reuses --state-stale rather than introducing a hue: "a refresh ran and
   broke" is the same class of fact as "this date is unconfirmed" -- something
   a human has to act on. It is dark almost always; see the note in
   app/web/templating.py about what must happen if it stops being. */
.nav-alert {
  width: 8px;
  height: 8px;
  margin-left: auto;
  border-radius: 50%;
  background: var(--state-stale);
  flex: none;
}
.tabbar__link .nav-alert { position: absolute; top: 8px; right: calc(50% - 18px); margin: 0; }
.tabbar__link { position: relative; }

/* `.tabbar__signout` and its button rule are DELETED (2026-08-09). The phone
   tab bar carried a <form action="/logout"> until Sign out moved inside
   Settings; the form went with it and left three rules styling a class that
   appears nowhere. Dead CSS that documents a layout the markup no longer has
   is worse than no comment, because the next person measures against it.

   The bar is FOUR slots now (Today / Calendar / Search / Settings), all plain
   `.tabbar__link` anchors at `flex: 1 1 0`.

   MEASURED at 390px: 98 / 98 / 98 / 98, nothing clipped. The old three-slot
   bar came out UNEQUAL (126 / 126 / 118) and the comment here used to explain
   why that was correct -- `min-width: auto` floors every slot at its own
   min-content, and those three labels filled the bar exactly, so the floors
   were what you saw. Four shorter labels do not: their min-content floors are
   41 + 58 + 46 + 53 = 198px inside 390px, so every slot clears its floor with
   room to spare and `flex: 1 1 0` divides the bar evenly.

   Which means the asymmetry is gone, and it is worth knowing WHY it was there
   -- it was never a rule, it was a symptom of a full bar. Add a fifth item, or
   a longer label than "Calendar", and it comes straight back. */

@media (min-width: 1024px) {
  body.has-chrome { padding-left: var(--sidenav-w); }
  .sidenav { display: flex; }
}

.tabbar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 30;
  display: flex;
  align-items: stretch;
  background: var(--surface);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom, 0);
}
.tabbar__link {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: 56px;
  padding: var(--sp-1);
  text-decoration: none;
  color: var(--ink-muted);
  font-size: var(--fs-1);
  font-weight: 500;
  border-top: var(--rail) solid transparent;
  margin-top: -1px;
}
.tabbar__link .icon { width: 21px; height: 21px; }
.tabbar__link[aria-current="page"] { color: var(--ink); border-top-color: var(--accent); font-weight: 600; }
body.has-chrome { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
@media (min-width: 1024px) {
  .tabbar { display: none; }
  body.has-chrome { padding-bottom: 0; }
}

/* ---- page shell ---------------------------------------------------------- */
/* WIDER, AND NO LONGER CENTRED IN THE LEFTOVER SPACE (client, 2026-08-09:
   "why the huge side by side space -- let the UI fill properly").

   660px was a measure-of-text argument, and it is the right argument for
   prose. This is not prose: a day's list is case cards with two labelled
   fields side by side, and at 660px inside a 1568px window the result was a
   column of cards floating in a field of black with dead space on BOTH sides
   -- because `margin: 0 auto` centres within what is left after the 232px
   side nav, so the gutters are not even symmetrical with the window.

   1280px uses nearly all of a 1512px window once the 232px nav is taken out,
   and stops the page looking like a phone layout someone forgot to finish. It
   is still a cap and not `none`: a case card is a headline, a cause title and
   two labelled fields at opposite ends, and past about 1300px the two fields
   are far enough apart to stop reading as one band.
   CENTRED IN THE SPACE AFTER THE NAV, and `margin: 0 auto` is what does it.
   It was briefly `0 auto 0 0` -- anchored hard against the nav -- which is the
   opposite of what was asked for and looked it: a wide block jammed left with
   the whole right side empty. `body.has-chrome` already reserves the nav's
   232px, so `auto` centres within what remains rather than within the window,
   and the padding below is preserved at every width. */
.page, main.page {
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--sp-4) var(--sp-5) var(--sp-7);
}
/* The lookup screens mirror the portal's dense tabular layout and take the
   whole width they can get. */
.page.page--wide, main.page.page--wide { max-width: 1320px; }


/* ==========================================================================
   Section label — the one header treatment across the product.
   Court names, table captions and detail sections all use it. A filled bar
   would be a repeated slab of near-white on a dark page; a tracked
   micro-label over a rule reads as structure without shouting, and on paper
   it drains to bold black text over the same rule with nothing lost.
   ========================================================================== */
.section-bar {
  display: block;
  background: none;
  color: var(--ink);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  line-height: 1.45;
  padding: 0 0 var(--sp-2);
  margin: var(--sp-6) 0 var(--sp-3);
  border-bottom: 1px solid var(--line-strong);
  overflow-wrap: anywhere;
}
.section-bar .section-bar__count,
.section-bar .section-bar__hidden {
  color: var(--ink-faint);
  font-weight: 500;
  letter-spacing: var(--track);
  white-space: nowrap;
}
caption.section-bar {
  display: table-caption;
  text-align: left;
  margin: 0 0 var(--sp-2);
}
.court-block .section-bar { margin-bottom: var(--sp-2); }

/* ==========================================================================
   Calendar (`/`) — the day viewer.

   The product's actual mental model, in the client's words: "the application
   is literally opened to view what cases are there for that day, and after
   the completion of that day the next day." So the default state is one
   focused day, resolved server-side, and the header says WHICH day and WHY
   in plain words. Everything else — yesterday, an arbitrary date, the week —
   is a step away from it, not a tab to hunt through.
   ========================================================================== */
.dayhead {
  border-top: 2px solid var(--accent);
  padding-top: var(--sp-3);
  margin: var(--sp-2) 0 var(--sp-4);
}
.dayhead__label {
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-faint);
  margin: 0;
}
.dayhead__date {
  font-size: var(--fs-6);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.05;
  margin: var(--sp-1) 0 0;
  overflow-wrap: anywhere;
}
.dayhead__weekday { color: var(--ink-muted); font-weight: 400; }
.dayhead__count {
  margin: var(--sp-2) 0 0;
  color: var(--ink-muted);
  font-size: var(--fs-2);
}
/* Filtered, the count becomes TWO sentences with two nouns — never "N of M",
   which this client has twice read as "the Nth of M" (§1.6). The second line
   is the total, and it is never hidden. */
.dayhead__count--total { margin-top: var(--sp-1); color: var(--ink-faint); }
/* Why this day and not another — never silent. This line is what stops the
   automatic roll-forward from looking like the app losing today's list. */
.dayhead__note {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-2) var(--sp-3);
  border-left: var(--rail) solid var(--line-strong);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--fs-2);
}
.dayhead__note a { color: var(--ink); }
.dayhead--rest { border-top-color: var(--line-strong); }

/* the day stepper: always labelled with real dates, so a control can never
   claim to be "tomorrow" when a Sunday was skipped to get there */
.daynav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: stretch;
  margin: 0 0 var(--sp-5);
}
.daynav__step,
.daynav__jump {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-2);
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 120ms linear, border-color 120ms linear;
}
.daynav__step:hover,
.daynav__jump:hover { background: var(--surface-raised); border-color: var(--accent); }
.daynav__jump { font-weight: 600; }
.daynav__chev { color: var(--ink-faint); font-size: var(--fs-4); line-height: 1; }

/* The month link sits in the exact slot the native `Any date` input vacated
   (ui-spec.md 1.3), because it does that control's job and does it better: a
   date input cannot show you which dates have anything on them. It is the only
   control in this stepper that is NOT htmx — it leaves for a different
   document, and a swap would be a navigation in costume. */
.daynav__month {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-left: auto;
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--fs-2);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 120ms linear, border-color 120ms linear;
}
.daynav__month:hover { background: var(--surface-raised); border-color: var(--accent); }
@media (max-width: 560px) {
  /* One-handed: two controls to a row, and NEVER a squeezed step. A 40% basis
     wraps them in pairs; the label is nowrap and the flex basis is above its
     natural width, so "‹ Thu 13-08" cannot be ellipsised down to "‹ W..".
     A step that cannot show its own date breaks this stepper's whole rule --
     every control is labelled with the day it will show. */
  .daynav__step,
  .daynav__jump,
  .daynav__month { flex: 1 1 40%; min-width: 0; justify-content: center; }
  .daynav__month { margin-left: 0; }
}

/* ==========================================================================
   The day's filters (§1.6) — four selects over saved cases only, closed to a
   single 44px line until something is applied.

   Closed is the daily state, and it is the whole cost of this feature on the
   daily read: 44 pixels above the first case. Four stacked selects with labels
   would have cost ~140px on a 390px phone, on the one screen that must not
   have a fold problem. Open-ness is server-rendered from the URL, never from
   the DOM — this disclosure is not allowed to be the product's only piece of
   client-side state.
   ========================================================================== */
.filters {
  border: 1px solid var(--line);
  background: var(--surface);
  margin: 0 0 var(--sp-5);
}
.filters__summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  cursor: pointer;
  list-style: none;
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-muted);
}
.filters__summary::-webkit-details-marker { display: none; }
.filters__summary:hover { color: var(--ink); background: var(--surface-raised); }
.filters__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The marker is drawn from two 1px bars rather than shipped as an SVG: it is a
   control affordance, not a piece of state, and the latin subset carries no
   triangle for the UA ::marker either. */
.filters__marker { position: relative; flex: none; width: 12px; height: 12px; }
.filters__marker::before,
.filters__marker::after {
  content: "";
  position: absolute;
  inset: 5px 0 auto;
  height: 2px;
  background: var(--ink-faint);
}
.filters__marker::after { transform: rotate(90deg); }
.filters[open] .filters__marker::after { display: none; }
.filters__summary:hover .filters__marker::before,
.filters__summary:hover .filters__marker::after { background: var(--ink); }
.filters__set { color: var(--ink); overflow-wrap: anywhere; }
.filters__form { padding: var(--sp-3); border-top: 1px solid var(--line); }
.filters__grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.filters__field { min-width: 0; }
/* Court runs the full width at EVERY width, not only on the phone. In a
   half-column select (~300px at 660px) "CIVIL JUDGE AND JMFC, BELUR (2)" clips
   to "…BELUR (2" — a count cut off mid-bracket, on a screen whose counts are
   the whole point. Full width fits every real court name and its count on the
   desktop shell; at 390px the very longest names still clip their tail, which
   is the browser's own truncation of a label whose IDENTIFYING part comes
   first, and it is what §1.8's 390px drawing shows too. Case type and Stage
   are short and stay two-up. */
.filters__field--wide { grid-column: 1 / -1; }
.filters__field label { margin: 0 0 var(--sp-1); }
.filters__field select { max-width: 100%; }
.filters__ignored {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-2) var(--sp-3);
  border-left: var(--rail) solid var(--line-strong);
  background: var(--bg);
  color: var(--ink);
  font-size: var(--fs-2);
}
.filters__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}
.filters__clear {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: var(--ink-muted);
  font-size: var(--fs-2);
}
.filters__clear:hover { color: var(--ink); }

/* One line at the foot of the day, and the reason there is no time column
   anywhere above it (§1.1a). It prints. */
.daynote-times {
  margin: var(--sp-6) 0 0;
  color: var(--ink-faint);
  font-size: var(--fs-2);
  max-width: 68ch;
}

/* Force-included by print.css only. On screen these lines would repeat what
   the controls already say. */
.print-only { display: none; }

/* The foot of the day: one button, Print this list. It held a "This week" link
   beside it until 2026-08-10, when the client asked for the week view and its
   endpoint to go — so this is a flex row with a single item now, and it stays
   a flex row because that is what puts the rule above it and the spacing under
   the last case card. */
.dayfoot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
  margin-top: var(--sp-6);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}

/* `.week-heading` — the date rule between one day and the next inside the week
   sheet — was deleted here with the week view (2026-08-10). Nothing else used
   it: the day list's own section breaks are `.case-group__heading` (a court)
   and `.place-group` (a town), both of which are still live. */

/* ==========================================================================
   A TOWN, collapsible — the day list's outer level (_place_group.html).

   Louder than the court group nested inside it, and that inversion of the
   usual "outer heading is quieter" instinct is the point: "am I going to
   Belur today" is the coarser decision and comes first. A 2px accent rule
   under it, against the court's 1px hairline, is the whole distinction —
   no fill, no colour, nothing that would compete with a state rail.
   ========================================================================== */
.place-group { margin: 0 0 var(--sp-5); }
.place-group__summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: var(--sp-7) 0 var(--sp-2);
  cursor: pointer;
  list-style: none;
}
.place-group__summary::-webkit-details-marker { display: none; }
.place-group__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.place-group__heading {
  flex: 1 1 auto;
  margin: 0;
  padding: 0 0 var(--sp-2);
  border-bottom: 2px solid var(--accent);
  font-size: var(--fs-4);
  font-weight: 600;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}
.place-group__count {
  font-size: var(--fs-2);
  font-weight: 400;
  color: var(--ink-faint);
  letter-spacing: 0;
  white-space: nowrap;
}
.place-group__summary:hover .place-group__heading { border-bottom-color: var(--ink); }
.place-group__marker {
  position: relative;
  flex: none;
  width: 14px;
  height: 14px;
  margin-bottom: var(--sp-2);
}
.place-group__marker::before,
.place-group__marker::after {
  content: "";
  position: absolute;
  inset: 6px 0 auto;
  height: 2px;
  background: var(--ink-muted);
}
.place-group__marker::after { transform: rotate(90deg); }
.place-group[open] > .place-group__summary .place-group__marker::after { display: none; }
.place-group__summary:hover .place-group__marker::before,
.place-group__summary:hover .place-group__marker::after { background: var(--ink); }
/* The courts indent under their town. One step, 12px — enough to read as
   nesting at a glance, not enough to cost a phone a column of text. */
.place-group__body { padding-left: var(--sp-3); }
/* The first court inside a town does not need the 32px of air that separates
   one court from the next; its town heading is already directly above it. */
.place-group__body > .case-group:first-child > .case-group__summary { margin-top: var(--sp-3); }

/* ==========================================================================
   A court group, collapsible — nested inside a town.

   Native <details>, open by default, no script and no remembered state (see
   _case_group.html for why each of those is load-bearing). The summary is a
   44px-tall click target across the full width, because the thing being
   tapped is a court heading in a corridor, not a caret.
   ========================================================================== */
.case-group { margin: 0; }
/* THE SUMMARY OWNS THE SPACING, not the heading inside it. `.section-bar`
   normally carries `margin: --sp-6 0 --sp-3`; left there, the marker beside it
   would be centred against a box that includes 32px of margin and land above
   the court name. Moved out here, the flex row is exactly the heading's own
   height and `align-items: center` is then true. */
.case-group__summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: var(--sp-6) 0 var(--sp-3);
  cursor: pointer;
  /* the UA ::marker triangle differs per browser AND the latin font subset
     carries no triangle glyph — so we draw our own, as .filters and .sheet do */
  list-style: none;
}
.case-group__summary::-webkit-details-marker { display: none; }
.case-group__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.case-group__summary .case-group__heading { flex: 1 1 auto; margin: 0; }
.case-group__summary:hover .case-group__heading { border-bottom-color: var(--accent); }
/* Two 2px bars: a plus when shut, a minus when open. Same construction as
   `.filters__marker`. `margin-bottom` cancels the heading's own bottom padding
   (the gap between the court name and its rule), so the marker centres on the
   TEXT rather than on the text-plus-rule — which is what the eye pairs it
   with, and what stays true when a long court name wraps to two lines. */
.case-group__marker {
  position: relative;
  flex: none;
  width: 12px;
  height: 12px;
  margin-bottom: var(--sp-2);
}
.case-group__marker::before,
.case-group__marker::after {
  content: "";
  position: absolute;
  inset: 5px 0 auto;
  height: 2px;
  background: var(--ink-faint);
}
.case-group__marker::after { transform: rotate(90deg); }
.case-group[open] > .case-group__summary .case-group__marker::after { display: none; }
.case-group__summary:hover .case-group__marker::before,
.case-group__summary:hover .case-group__marker::after { background: var(--ink); }

/* ==========================================================================
   The month grid (`/month/<YYYY-MM>`) — §1.7.

   A PLANNING surface: how heavy is next week, what does the holiday do to the
   days around it, when is my next clear day. Three rules decide everything
   below.

   1. NO HUE, ANYWHERE ON THIS SCREEN. Colour means hearing-date state and
      nothing else, so a density ramp, a "busy day" tint or a weekend hue is
      out by the token rules before it is out on taste. A "contains unconfirmed
      cases" pip would be lit on nearly every working day of nearly every month
      (the refresher runs only when someone boots the office PC), and a grid
      that is 90% alarm is a grid with no alarm in it.
   2. DENSITY IS ONE LUMINANCE STEP AND THE NUMERAL. --surface for a day with
      listings, --bg for a day with none. Two levels — has / has not — and
      explicitly not a ramp: magnitude is the number's job.
   3. THE LATTICE IS HAIRLINES, NOT GUTTERS. At 390px: 374px of grid minus the
      rules, over seven columns, is ~52px per cell at min-height 56px — every
      cell comfortably over the 44px floor in both axes. An 8px gutter would
      have eaten that down to 44.3px, exactly at the floor with no margin. The
      8px-between-targets guideline is knowingly not met and the mitigation is
      that a mis-tap is recoverable in one tap: the destination announces
      itself in 36px type, the day's own stepper is ±1 day away, and Back
      returns here with the tapped cell still marked.
   ========================================================================== */
.monthhead {
  border-top: 2px solid var(--accent);
  padding-top: var(--sp-3);
  margin: var(--sp-2) 0 var(--sp-4);
}
.monthhead__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-3);
}
.monthhead__titles { min-width: 0; }
.monthhead__title {
  font-size: var(--fs-6);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.05;
  margin: var(--sp-1) 0 0;
}
.monthhead__count { margin: var(--sp-2) 0 0; color: var(--ink-muted); font-size: var(--fs-2); }
.monthnav { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
@media (max-width: 560px) {
  /* Two month steps sharing one row, and nothing else. There was a third
     control here -- "Today's list", which wrapped onto a line of its own at
     `flex: 1 1 100%` -- and it is gone (see _month_panel.html for why). Its
     rule went with it rather than being left to style an absent element. */
  .monthnav { width: 100%; }
  .monthnav .daynav__step { flex: 1 1 0; min-width: 0; justify-content: center; }
}

.month {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  margin: var(--sp-4) 0 var(--sp-5);
  /* kills the 300ms tap delay and double-tap-to-zoom, both actively harmful on
     a lattice of small adjacent links */
  touch-action: manipulation;

  /* THE CELL HAS TWO SIZES, AND THEY ARE CHOSEN BY HOW WIDE A COLUMN IS, NOT
     BY WHAT KIND OF DEVICE IT IS. §1.7 specifies them: full (a 24px date,
     `12 LISTED`, `5 COURTS`) needs ~95px of column; compact (an 18px date and
     the bare integer, centred) works down to 52px.

     They are declared as custom properties so the compact treatment exists
     ONCE and is switched on in the two places a column gets narrow: a phone,
     and a desktop that has given a third of its width to the day panel. The
     alternative was the same six declarations written out twice, which is how
     the phone's cell and the desktop's cell drift apart. */
  --mcell-minh: 84px;
  --mcell-pad: var(--sp-2);
  --mcell-gap: 2px;
  --mcell-align: stretch;
  --mcell-text: left;
  --mcell-date: var(--fs-5);
  --mcell-unit: inline;
  --mcell-l3: block;
  --mcell-todaymark: " · TODAY";
}
/* The two narrow-column cases, in ONE block and one media query list: a phone,
   and 1024–1239px where the day panel is on and the grid is left with 58–85px
   a column — under the ~95px the full cell needs, so it takes the phone's cell,
   which was measured for exactly this width. Above 1239 the column is back over
   90px and the full cell returns (measured, not guessed: `23 LISTED` is ~65px
   of text inside 16px of padding). */
@media (max-width: 560px), (min-width: 1024px) and (max-width: 1239px) {
  .month {
    --mcell-minh: 56px;
    --mcell-pad: var(--sp-1) 2px;
    --mcell-gap: 0;
    --mcell-align: center;
    --mcell-text: center;
    --mcell-date: var(--fs-4);
    /* the bare integer only — the unit is carried by the cell's accessible
       name, by the month header's own sentence, and by the day one tap away */
    --mcell-unit: none;
    --mcell-l3: none;
    --mcell-todaymark: "";
  }
}
/* Desktop has the vertical room a phone does not, and a 336px grid beside a
   600px panel reads as an afterthought. Height only; everything else compact. */
@media (min-width: 1024px) and (max-width: 1239px) {
  .month { --mcell-minh: 68px; }
}
.month__wd {
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-faint);
  text-align: left;
  padding: 0 var(--sp-2) var(--sp-2);
}
/* The SUN head is quiet, and that is the only thing on this grid that treats a
   Sunday differently. A Sunday CELL is decided by whether it has cases, exactly
   like any other day — on 09-08-2026 a Sunday really did carry two listings. */
.month__wd--sun { color: var(--ink-faint); }
.month__wd-short { display: none; }
/* `position: relative` is what the day panel's overlay anchor is measured
   against (.mcell-hit below) */
.month__cell { padding: 0; border: 1px solid var(--line); position: relative; }

/* the whole cell is the link, never just the numeral */
.mcell {
  display: flex;
  flex-direction: column;
  gap: var(--mcell-gap);
  align-items: var(--mcell-align);
  text-align: var(--mcell-text);
  min-height: var(--mcell-minh);
  padding: var(--mcell-pad);
  background: var(--bg);
  color: var(--ink);
  text-decoration: none;
  /* reserved on every cell so today's rule never shifts a cell's text */
  border-top: 2px solid transparent;
}
.mcell--listed { background: var(--surface); }
/* Hover is hung on the CELL, not on the link: from 1024px up a transparent
   anchor covers the cell (.mcell-hit), so `.mcell:hover` would never fire
   again on the one width where a mouse exists. */
.month__cell:hover .mcell { background: var(--surface-raised); }
.month__cell:focus-within .mcell { background: var(--surface-raised); }
.mcell__date { font-size: var(--mcell-date); font-weight: 500; line-height: 1; }
.mcell--empty .mcell__date { color: var(--ink-muted); }
/* adjacent-month days are real, counted and linked — a hearing on the 1st of
   next month is one tap from this grid with no navigation at all */
.mcell--out .mcell__date { color: var(--ink-faint); }
.mcell__count,
.mcell__l3 {
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  line-height: 1.3;
}
.mcell__count { color: var(--ink); }
.mcell__l3 { color: var(--ink-faint); display: var(--mcell-l3); }
.mcell__unit { display: var(--mcell-unit); }
/* "counted, none" must not look like "not computed" */
.mcell__n--zero { color: var(--ink-faint); }
/* One line, clipped rather than broken mid-word: "INDEPENDE / NCE DAY" across
   a 95px cell reads as a rendering fault. The full name is a sentence in the
   note block under the grid at every width — which is where §1.7 puts the
   authoritative version anyway ("a sentence, not a glyph"). */
.mcell__holiday {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* the micro-label's 0.08em tracking is what pushes INDEPENDENCE DAY over a
     138px cell; dropped here so the commonest holiday name fits whole */
  letter-spacing: 0;
}

/* TODAY. A 2px accent rule, full-weight ink, and the word.
   Achromatic throughout -- the product's established "this is the thing"
   device, not a fourth hue.

   The word used to appear only when today was ALSO the day you came from.
   That was right while the month was a secondary screen reached from a day
   you had already chosen. It stopped being right on 2026-08-09, when the
   client made Calendar OPEN the month: this is now the first thing anyone
   sees, and "which of these 42 cells is today" is the first question it has
   to answer. A 2px rule is a fine answer to "which one did I come from" and
   a poor one to "where am I", especially at arm's length on a busy grid. */
.mcell--today { border-top-color: var(--accent); }
.mcell--today .mcell__date { font-weight: 600; color: var(--ink); }
.mcell--today .mcell__todayline {
  display: block;
  color: var(--ink);
  font-weight: 600;
}
/* A day with listings already uses line 3 for its court count, so today's
   word goes BESIDE the date rather than pushing the cell taller -- every
   cell in the grid has to stay exactly the same height. */
.mcell--today .mcell__date::after {
  /* Empty in the compact cell: " · TODAY" beside an 18px date needs ~60px and
     the cell is 52px wide, so it would wrap and make one cell taller than the
     other forty-one. There it is carried by the accent rule and by the
     accessible name, which is prefixed "Today, " at every width (§1.7). */
  content: var(--mcell-todaymark, "");
  font-size: var(--fs-1);
  font-weight: 600;
  letter-spacing: var(--track);
  color: var(--ink-muted);
}
.mcell--today .mcell__todayline { display: none; }

/* THE SELECTED DAY — the one whose list is in the panel on the right.
   The primary-button inversion (--accent fill, --on-accent text, 15.2:1),
   already a declared variant in §0.4 and still achromatic: this is "which one
   am I looking at", not a fourth hue.

   It used to be `:target`, driven by a #d-NN fragment. That mechanism cannot
   survive the panel: `history.pushState` does not re-evaluate `:target`, so
   after an hx-push-url swap the marked cell would still be naming the day
   BEFORE the last click, next to a panel showing the day after it. The mark is
   now a server-rendered class computed from the same `?day=` the panel is —
   one fact, one source, and it survives Back, reload and a shared link. */
.mcell--selected { background: var(--accent); color: var(--on-accent); }
.mcell--selected .mcell__date,
.mcell--selected .mcell__count,
.mcell--selected .mcell__n--zero,
.mcell--selected .mcell__l3 { color: var(--on-accent); }
.mcell--selected .mcell__date::after { color: var(--on-accent); }
.month__cell:hover .mcell--selected { background: var(--accent); }
/* If today IS the selected day, the inversion swallows the accent rule, so the
   word takes over the third line. */
.mcell__todayline { display: none; }
.mcell--selected .mcell__todayline { display: var(--mcell-l3); }
.mcell--selected.mcell--today .mcell__l3:not(.mcell__todayline) { display: none; }

/* ---- the day panel's enhancement, and its only footprint on the grid ------
   A transparent anchor over the whole cell, carrying the SAME href as the cell
   link plus the hx-get that fills the panel. Shown only from 1024px up, where
   the panel exists; below that it is not rendered at all and the cell link is
   what gets tapped.

   It is CSS and not a script on purpose. htmx cancels an anchor's default
   action BEFORE it evaluates an hx-trigger filter (1.9.12: shouldCancel →
   preventDefault → maybeFilterEvent → bail), so putting `hx-get` +
   `hx-trigger="click[wide]"` on the cell link itself would leave that link
   dead on a phone. Here the media query is the entire gate: no width constant
   in any expression, and with JavaScript off this element is still an ordinary
   link to the same day. */
.mcell-hit { display: none; }

/* ==========================================================================
   Settings — ui-spec §5. Operator surface, not a daily one: read a few
   numbers, press a thing, leave.
   ========================================================================== */
.stat-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-5) var(--sp-7);
  margin: var(--sp-4) 0 var(--sp-5);
}
.stat { margin: 0; }
.stat dt {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  text-transform: uppercase;
  letter-spacing: var(--track);
}
.stat dd { margin: var(--sp-1) 0 0; color: var(--ink); font-size: var(--fs-3); }
.stat__n { font-size: var(--fs-6); font-weight: 600; line-height: 1.1; }
.stat__n--quiet { color: var(--ink-faint); font-weight: 500; }
/* The two "when" figures are prose, not headline numbers: the point is the
   COMPARISON between them, and equal-weight type is what makes a reader
   notice that one says 10 minutes and the other says 9 days. */
.stat-row--dates .stat dd { font-size: var(--fs-2); color: var(--ink-muted); }

.sync-state {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
  margin: var(--sp-4) 0;
}
.settings-action {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: var(--sp-4) 0;
}
.settings-action label {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  text-transform: uppercase;
  letter-spacing: var(--track);
}
.settings-sub { margin: var(--sp-6) 0 var(--sp-2); font-size: var(--fs-4); }
.settings-sub__lede, .settings-note { max-width: 62ch; }
.settings-note { margin-top: var(--sp-2); font-size: var(--fs-1); }

/* The ONE date input this product owns that a human types into (the other is
   §3.2's override). Chrome renders it in the BROWSER's locale, not the
   document's -- on this machine that is mm/dd/yyyy, and on a screen about
   re-checking "since 06/08" the difference between 6 August and 8 June is a
   fortnight of unverified hearing dates. `lang="en-IN"` on <html> does not
   fix it; nothing in CSS does either.
   So the control is kept (a real picker beats a text box people mistype) and
   the AMBIGUITY IS ANSWERED IN THE CONFIRM STEP, which echoes the chosen date
   back as DD-MM-YYYY in words before anything is queued. */
.settings-action input[type="date"] {
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-2);
}
.settings-action input[type="date"]::-webkit-calendar-picker-indicator {
  filter: invert(1);      /* the icon ships black; the theme is not */
  cursor: pointer;
}

.month-notes { margin-top: var(--sp-4); }
/* Named in a sentence, below the grid at every width: text prints, survives a
   photocopier, needs no legend and costs a 52px cell nothing. Inventing a
   warning glyph for the rarest cell in the grid was the alternative, and
   nobody would have known what it meant. */
.month-note { margin: var(--sp-2) 0; color: var(--ink-muted); font-size: var(--fs-2); }
.month-confess {
  margin: var(--sp-4) 0 0;
  color: var(--ink-faint);
  font-size: var(--fs-2);
  max-width: 68ch;
}
.month-confess a { color: var(--ink-muted); }
.month-confess a:hover { color: var(--ink); }

/* The two-letter weekday heads travel with the compact cell, for the same
   reason and at the same two widths. */
@media (max-width: 560px), (min-width: 1024px) and (max-width: 1239px) {
  .month__wd { text-align: center; padding: 0 0 var(--sp-1); }
  .month__wd-long { display: none; }
  .month__wd-short { display: inline; }
}
@media (max-width: 520px) {
  /* 8px each side → 374px of grid → ~52px per cell (§1.7) */
  .page--month, main.page.page--month { padding-left: var(--sp-2); padding-right: var(--sp-2); }
}

/* ==========================================================================
   1.7a — THE DAY PANEL, right of the grid.

   The client drew this: the month on the left, the selected day's list on the
   right, scrolling inside itself. §1.0a argued against it on four grounds and
   the client overruled it; three of the four are answered by construction and
   the fourth is why the panel does not exist below 1024px.

   * It is a PREVIEW. Everything clickable in it goes to `/?date=<day>`, so
     there is no second working surface to keep in step with the first.
   * It reuses `_case_card.html` unchanged, so a hearing-date state cannot mean
     one thing here and another on the day.
   * It scrolls INTERNALLY. A busy day is 15–25 listings; a panel that grew to
     fit would push the grid off screen, which would end the only thing this
     layout is for — the month and a day, together.
   * Below 1024px it is not rendered at all. A phone tapping a date goes
     straight to the full day, through the href every cell has always carried.
     One layout, one breakpoint, no second interaction model.
   ========================================================================== */
.daypanel { display: none; }

@media (min-width: 1024px) {
  /* 232px of nav + 32px of page padding leaves 760px at 1024: a 320px panel,
     24px of gap, and ~415px of grid at ~58px a column — over the 44px floor,
     and the compact cell is what fills it. From 1240px the column is back over
     90px and the full cell returns; from 1440px the panel takes 360px, which
     is where a case row stops having to break a court name across three
     lines. */
  .page--month, main.page.page--month { max-width: 1320px; }
  .month-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    gap: var(--sp-5);
    align-items: start;
  }
  .month-layout .month { margin-bottom: 0; }

  .daypanel {
    display: block;
    position: sticky;
    /* clears the page's own top padding, so the panel stops level with the
       grid rather than under the viewport edge */
    top: var(--sp-4);
    border: 1px solid var(--line);
    background: var(--surface);
  }

  /* The whole cell becomes the panel's trigger. Same href as the link under
     it, so a JavaScript-off desktop click is an ordinary navigation to the
     same day. */
  .mcell-hit {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 1;
  }
}
@media (min-width: 1440px) {
  .month-layout { grid-template-columns: minmax(0, 1fr) 360px; }
}

/* ---- the panel's header: a link, and it says where it goes --------------- */
.daypanel__head {
  display: block;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  border-top: 2px solid var(--accent);
  text-decoration: none;
  color: var(--ink);
}
.daypanel__head:hover { background: var(--surface-raised); }
.daypanel__label,
.daypanel__date,
.daypanel__count { display: block; }
/* The date and the open button share a row; the count stays under them.
   `wrap` is deliberate and is the only thing standing between a substituted
   font and a clipped control — see _day_panel.html for the measurements. */
.daypanel__headrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-1);
}
.daypanel__label {
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-faint);
}
.daypanel__date {
  font-size: var(--fs-5);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
  /* nowrap so the row's wrap decision is about the CHIP moving, never about
     "Thu 13-08-2026" breaking across two lines */
  white-space: nowrap;
}
.daypanel__weekday { color: var(--ink-muted); font-weight: 400; }
.daypanel__count { margin-top: var(--sp-1); color: var(--ink-muted); font-size: var(--fs-2); }

/* A button, drawn in the same vocabulary as `.daynav__step` — 1px
   `--line-strong`, square (`--radius: 0`), label then chevron — because it
   does the same kind of job and this product has one button. Smaller than
   `--tap` on purpose: the panel exists only at >=1024px (a mouse), and a 44px
   chip beside a 24px date would out-weigh the date it belongs to. */
.daypanel__open {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  flex: none;
  min-height: 32px;
  padding: 0 var(--sp-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  font-size: var(--fs-1);
  font-weight: 600;
  white-space: nowrap;
  color: var(--ink);
}
/* Hovering ANYWHERE on the header lights the chip — the whole block is the
   link, and the chip is the part that says where it goes. The header's own
   hover already raises its background, so the chip has to move further than
   that to still read as raised: border to `--accent`, fill to the wash. */
.daypanel__head:hover .daypanel__open {
  border-color: var(--accent);
  background: var(--accent-wash);
}

.daypanel__note,
.daypanel__badday {
  margin: 0;
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: var(--fs-2);
}

/* ---- the list: max-height + overflow, which is the whole point ----------- */
.daypanel__list {
  /* Tall enough that a 21-case day is a real scroll and not a keyhole; short
     enough that six grid rows stay on screen beside it. */
  max-height: min(58vh, 620px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sp-3);
}
.daypanel__empty,
.daypanel__prompt {
  margin: 0;
  padding: var(--sp-4);
  color: var(--ink-muted);
  font-size: var(--fs-2);
}
.daypanel__prompt { border-top: 2px solid var(--accent); }

/* A court heading that is also the one filter this panel has: it opens the
   full day already narrowed to that court (`?court=<est_code>`). */
.daypanel__court {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  min-height: var(--tap);
  margin: var(--sp-3) 0 var(--sp-2);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--line-strong);
  color: var(--ink);
  text-decoration: none;
}
.daypanel__list > .daypanel__court:first-child { margin-top: 0; }
.daypanel__court:hover { border-bottom-color: var(--accent); }
.daypanel__court-name {
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  line-height: 1.35;
  overflow-wrap: anywhere;   /* court names are long and will not be clipped */
}
.daypanel__court-count {
  flex: none;
  font-size: var(--fs-1);
  font-weight: 500;
  letter-spacing: var(--track);
  color: var(--ink-faint);
  white-space: nowrap;
}
.daypanel__court:hover .daypanel__court-count { color: var(--ink); }

/* Each row is a link, and its destination is the day — never the case. The
   card inside renders in `preview` mode precisely so there is no anchor in
   here to nest. */
.daypanel__row { display: block; text-decoration: none; color: inherit; }
.daypanel__row:hover .case-card { background: var(--surface-raised); border-color: var(--line-strong); }
.daypanel__row .case-card { margin-bottom: var(--sp-2); }

.daypanel__foot {
  margin: 0;
  padding: var(--sp-2) var(--sp-4) var(--sp-3);
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: var(--fs-1);
}

/* ==========================================================================
   Case row — the unit the whole product is made of.
   Sharp, flat, hairline-bounded, with a state rail down the left edge. The
   rail width is reserved on every row so a state change never shifts the
   text sideways.
   ========================================================================== */
.case-card {
  position: relative;   /* the overlay hit area below is positioned to this */
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: var(--rail) solid var(--line-strong);
  padding: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-2);
}
/* THE WHOLE CARD OPENS THE CASE — a transparent overlay anchor, the same
   mechanism as `.mcell-hit` on the month grid and for the same reason: the
   card already contains real links, and an anchor inside an anchor is not
   markup a browser keeps.

   `z-index: 0` on the overlay and `1` on everything interactive, so a click
   on a link is that link and a click on anywhere else is the card. Both need
   an explicit stacking context — a positioned element with `z-index: auto`
   would let source order decide, and the overlay is last in the markup. */
.case-card__hit { position: absolute; inset: 0; z-index: 0; }
.case-card__hit:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.case-card a:not(.case-card__hit),
.case-card button { position: relative; z-index: 1; }
.case-card:hover { border-color: var(--line-strong); }
/* The number is the card's own name for where it goes, so it is what lights
   up when the card is hovered anywhere — including over the overlay. */
.case-card:hover a.case-card__number { text-decoration: underline; text-decoration-thickness: 2px; }
.case-card--awaiting { border-left-color: var(--state-await); }
.case-card--stale { border-left-color: var(--state-stale); }
.case-card--moved { border-left-color: var(--state-moved); }
.case-card--closed { border-left-color: var(--line); background: var(--bg); }

.case-card__head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
  flex-wrap: wrap;
}
/* TYPE, NUMBER, YEAR — the client's first three facts, one line, one weight.
   `.case-card__type` and `.case-card__no` are separate elements so the two can
   be weighted apart later and so a test can assert each; today they render
   identically on purpose, because "OS 123/2025" is how the office says it and
   splitting it visually would invent a distinction they do not make. */
.case-card__number {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 0.4em;
  font-weight: 600;
  font-size: var(--fs-4);
  letter-spacing: -0.01em;
  text-decoration: none;
  color: var(--ink);
  padding: var(--sp-1) 0;
  margin: calc(-1 * var(--sp-1)) 0;
  overflow-wrap: anywhere;
}
/* `a.` on purpose: in the month's day panel the number is a <span> (the row
   itself is the link, ui-spec.md 1.7a), and an underline on hover there would
   promise a destination that element does not have. */
a.case-card__number:hover { text-decoration: underline; text-decoration-thickness: 2px; }
.case-card__title {
  overflow-wrap: anywhere;  /* transliterated names WILL outrun any column */
  margin: var(--sp-1) 0 var(--sp-2);
  line-height: 1.4;
  color: var(--ink);
  /* TWO LINES, THEN AN ELLIPSIS — the client's "a small sliver of parties
     names just to cross check and confirm".

     ⚠ A DEVIATION FROM §0.2 ("no line-clamp anywhere in this pass; the row
     grows instead"), and it is deliberate: one corporate respondent runs to
     three lines by itself and pushes the next date and the stage — the two
     fields the client ranks most important — out of the first glance. The
     full title is on `title=` for hover, and print.css lifts the cap, so the
     sheet carried into a courtroom still carries every name in full.

     `-webkit-` prefixes and `display: -webkit-box` are still the only thing
     every current engine implements; the unprefixed `line-clamp` is listed
     after it for the ones that have moved on. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* No italic anywhere: the variable font ships no true italic, and a
   synthesised oblique is exactly the kind of detail that reads as sloppy.
   The separator earns its place with case and colour instead. */
.case-card__title .vs {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  margin: 0 var(--sp-1);
}
.case-card__meta {
  color: var(--ink-muted);
  font-size: var(--fs-2);
  margin: var(--sp-1) 0;
  overflow-wrap: anywhere;
}
.case-card__dates {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-5);
  font-size: var(--fs-2);
  margin: var(--sp-2) 0;
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
}
.case-card__dates .label,
.case-card__foot .label {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  margin-right: var(--sp-1);
}

/* ---- the two fields the client ranks most important ----------------------
   Next hearing and Stage, in a ruled band, in the SAME place on every row.
   That constancy is the point: reading forty of these in a corridor, the
   value of a fixed layout is that the eye stops hunting.

   The next slot takes the whole line when it needs it — an "awaiting" or
   "stale" row puts a full sentence in there ("13-08-2026 — nothing has
   confirmed it for 5 days"), and pushing Stage onto its own line is the right
   trade: it stays a labelled field rather than being squeezed into an
   ellipsis. */
.case-card__key {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-2) var(--sp-5);
  margin: var(--sp-2) 0;
  padding: var(--sp-2) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.case-card__key-item {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
/* NEXT HEARING AND STAGE STAY ON ONE ROW (client, 2026-08-09, pointing at two
   adjacent cards where one had them side by side and the other had stacked
   them -- "why isn't the stage objection in line with the next hearing date
   like the card below it?").

   It was `flex: 1 1 14rem` on the next slot: 14rem is 224px, so in any column
   narrower than about 224 + the stage's width the two wrapped, and in a wider
   one they did not. Two cards a centimetre apart therefore had two different
   layouts, decided by nothing the reader could see -- which is exactly the
   fixed-position property the band exists to provide.

   `1 1 0` with `min-width: 0` lets the next slot shrink instead of wrapping,
   so the pair stays on one line down to the point where the text itself
   cannot fit. Below 380px they stack, together, on every card at once. */
.case-card__key-item--next { flex: 1 1 0; min-width: 0; }
.case-card__key-item--stage { flex: 0 0 auto; text-align: right; }
@media (max-width: 380px) {
  .case-card__key-item--next,
  .case-card__key-item--stage { flex: 1 1 100%; text-align: left; }
}
.case-card__key-label {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
}
/* Bigger than body text and bigger than the foot line — the typography is
   what says "these two are the answer" without a word of chrome. */
.case-card__key-value {
  font-size: var(--fs-3);
  font-weight: 600;
  color: var(--ink);
  overflow-wrap: anywhere;
}
/* The state partial's own value block already carries the state's rule and
   colour; inside the band it only needs its top margin dropped so it lines up
   with the Stage value beside it. */
.case-card__key-value .next-value { margin: 0; font-size: inherit; }
.case-card__key-value .next-value--awaiting,
.case-card__key-value .next-value--stale { font-size: var(--fs-2); font-weight: 500; }

/* THREE LINES IN THE DAY PANEL, not two. The cap is there to bound the row,
   not to hit a character count — and the panel's column is 320px against the
   day's ~700px, so two lines there was one party and an ellipsis, with the
   respondent gone entirely. A cause title that shows only one side cannot do
   the one job the client gave this line: cross-check and confirm. Three lines
   in 320px is about the same amount of name as two lines in 700px. */
.daypanel .case-card__title { -webkit-line-clamp: 3; line-clamp: 3; }

/* ---- the cherry, and the supporting facts -------------------------------- */
.case-card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--sp-2);
}
.case-card__foot-sep { color: var(--ink-faint); }
/* `Pending` is the quiet case and reads as the quiet case: same micro-label
   voice as the state markers beside it, no colour of its own. Disposed and
   Transferred keep their existing `.state--closed` / `.state--moved`
   treatment, so the two axes look like one line and read as two facts. */
.case-card__status {
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-faint);
  white-space: nowrap;
}
/* Rule 2: every row, no exceptions. The office worker only runs when someone
   boots the PC, so this stamp is frequently the most important line on the
   row — it is quiet, but it is never absent and never approximate. */
.case-card__asof {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  margin: var(--sp-2) 0 0;
}

/* ==========================================================================
   The three hearing-date states.
   Quiet by request — no pills, no filled alarm blocks, no global banner. A
   tracked micro-label plus a rule, in the state's own colour, plus a
   distinct icon SHAPE and a distinct border STYLE (none / dashed / solid),
   so the state survives colour-blindness and a black-and-white photocopy.
   ========================================================================== */
.state {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  padding: 3px 0;
  white-space: nowrap;
}
.state .icon { width: 1.15em; height: 1.15em; }

/* Confirmed: achromatic, and reduced to the smallest honest mark — since
   2026-08-09 that is a tick and no word at all on screen. It says "something
   confirmed this", which is exactly the distinction the client asked to keep;
   it does not celebrate, and it no longer takes a third of the foot line to
   say the unremarkable thing. The word stays in the markup for assistive tech
   and for paper (print.css puts it back). */
.state--confirmed { color: var(--ink-faint); }
.state--confirmed .state__word {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.state--confirmed .icon { width: 1.3em; height: 1.3em; }
.state--awaiting { color: var(--state-await); }
.state--stale { color: var(--state-stale); }
.state--moved { color: var(--state-moved); }
.state--closed { color: var(--ink-faint); }

/* The next-date value and its trust-state are never separated. */
.next-value {
  font-size: var(--fs-2);
  margin: var(--sp-1) 0 0;
  overflow-wrap: anywhere;
}
.next-value--awaiting {
  color: var(--state-await);
  border-left: 2px dashed var(--state-await);
  padding-left: var(--sp-2);
}
.next-value--stale {
  color: var(--state-stale);
  border-left: 2px solid var(--state-stale);
  padding-left: var(--sp-2);
  font-weight: 500;
}
.next-value--stale a { color: var(--state-stale); }

/* ---- transferred / disposed: a different axis, never a freshness state --- */
.banner {
  display: block;
  padding: var(--sp-2) var(--sp-3);
  margin: var(--sp-2) 0;
  font-size: var(--fs-2);
  border: 1px solid var(--line);
  border-left-width: var(--rail);
  background: var(--surface);
  overflow-wrap: anywhere;
}
.banner .icon { width: 1.1em; height: 1.1em; }
.banner-transferred {
  color: var(--state-moved);
  border-color: var(--line);
  border-left-color: var(--state-moved);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  font-size: var(--fs-1);
}
.banner-disposed {
  color: var(--ink-faint);
  border-left-color: var(--line-strong);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  font-size: var(--fs-1);
}
.banner-info { color: var(--ink-muted); border-left-color: var(--line-strong); }
.banner-error { color: var(--state-stale); border-left-color: var(--state-stale); }
.banner-success { color: var(--ink); border-left-color: var(--accent); }

/* ==========================================================================
   Controls — deliberately specified variants, not one button with overrides.
   ========================================================================== */
form label {
  display: block;
  margin: var(--sp-4) 0 var(--sp-1);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-muted);
}
/* one label + one control, as a unit -- the wrapper the search form's
   cascading fields are swapped by, so it owns the spacing rather than
   inheriting whatever the swapped-in fragment happened to have */
.form-field { margin: 0 0 var(--sp-3); }
.form-field label { margin-top: 0; }

input, select, textarea:not([hidden]) {
  min-height: var(--tap);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line-strong);   /* 3.4:1 — a control edge is not a hairline */
  border-radius: var(--radius);
  font-size: var(--fs-3);                 /* 16px floor: no iOS auto-zoom */
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--bg);
  appearance: none;
}
/* the native date picker's own indicator is black-on-black in a dark theme */
input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1); opacity: 0.7; cursor: pointer; }
input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
select {
  /* the chevron is drawn, not a glyph: the latin subset has no arrows */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 18px) calc(50% + 2px), calc(100% - 12px) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: var(--sp-6);
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 0;
  border-color: var(--accent);
}
input::placeholder { color: var(--ink-faint); }
select:disabled, input:disabled {
  background: var(--surface);
  color: var(--ink-faint);
  border-color: var(--line);
  cursor: not-allowed;
}

/* Radios only, in practice: the product's one checkbox was the login screen's
   "Keep me signed in", which is gone (docs/design-brief.md §3). The rule
   still names both because the scope toggle's radios need exactly the same
   reset, and a `.checkbox-row` layout class with nothing to lay out was
   removed with the feature rather than left waiting to be reused. */
input[type="radio"], input[type="checkbox"] {
  width: 1.15rem;
  height: 1.15rem;
  min-height: 0;
  padding: 0;
  margin: 0;
  appearance: auto;
  accent-color: var(--accent);
  flex: none;
}

.req { color: var(--state-stale); font-weight: 700; }
.label-hint { color: var(--ink-faint); font-weight: 400; text-transform: none; letter-spacing: normal; }

/* --- button: primary / secondary / ghost / destructive ------------------- */
button, .button-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  min-width: var(--tap);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius);
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--on-accent);
  font-family: inherit;
  font-size: var(--fs-2);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms linear, border-color 120ms linear, color 120ms linear;
}
button:hover, .button-link:hover { background: var(--ink); border-color: var(--ink); color: var(--on-accent); }
button:active { transform: none; }

/* `hx-disabled-elt` disables a control for the life of its request -- the
   search Search button, and now each result row's own two actions. A control
   that is disabled but looks identical to a live one is the same class of lie
   as a stale date that looks fresh, and the row actions spend ~2.5s of portal
   each, which is long enough to tap twice. Colour is not the only signal:
   the cursor changes and the paired `.htmx-indicator` says what is happening. */
button:disabled, button[disabled] {
  opacity: 0.45;
  cursor: default;
}
button:disabled:hover, button[disabled]:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
button.secondary:disabled:hover, button.secondary[disabled]:hover {
  background: transparent;
  color: var(--ink);
  border-color: var(--line-strong);
}
button.link-button:disabled:hover, button.link-button[disabled]:hover {
  background: none;
  color: var(--ink);
  border-color: transparent;
}

button.secondary, .button-link {
  background: transparent;
  color: var(--ink);
  border-color: var(--line-strong);
}
button.secondary:hover, .button-link:hover {
  background: var(--surface-raised);
  color: var(--ink);
  border-color: var(--accent);
}

button.ghost {
  background: none;
  border-color: transparent;
  color: var(--ink-muted);
  padding: var(--sp-2) var(--sp-3);
}
button.ghost:hover { background: var(--surface-raised); color: var(--ink); border-color: transparent; }

/* Destructive is outlined, never filled: a filled red block invites the tap
   it is meant to slow down. */
button.destructive {
  background: none;
  color: var(--state-stale);
  border-color: var(--state-stale);
}
button.destructive:hover { background: var(--surface-raised); color: var(--state-stale); border-color: var(--state-stale); }

button.link-button {
  background: none;
  border-color: transparent;
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  padding: var(--sp-2) var(--sp-2);
  min-width: auto;
}
button.link-button:hover { background: none; color: var(--ink); border-color: transparent; text-decoration-thickness: 2px; }

.copy-button {
  min-height: 32px;
  min-width: auto;
  padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  background: none;
  color: var(--ink-muted);
  border-color: var(--line-strong);
}
.copy-button:hover { background: var(--surface-raised); color: var(--ink); border-color: var(--accent); }

.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  text-decoration: none;
  font-size: var(--fs-2);
  font-weight: 500;
  color: var(--ink-muted);
}
.back-link:hover { color: var(--ink); text-decoration: underline; }

/* A CNR is 16 characters read off a physical file. Tabular figures plus
   tracking give it monospace-like rhythm without shipping a second font. */
.cnr-value {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.09em;
  overflow-wrap: anywhere;
}

/* ==========================================================================
   Card / panel variants
   ========================================================================== */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  padding: var(--sp-4);
  margin-bottom: var(--sp-4);
}
.panel--flush { padding: 0; }

/* a confirm step is a dialog in everything but element: it interrupts, it
   states its object, and it offers exactly two ways out */
.dialog {
  background: var(--surface-raised);
  border: 1px solid var(--line-strong);
  border-top: 2px solid var(--accent);
  padding: var(--sp-5) var(--sp-4);
  margin: var(--sp-4) 0;
}
.dialog h2 { margin-top: 0; }
.dialog--destructive { border-top-color: var(--state-stale); }
.dialog__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
  margin-top: var(--sp-5);
}
.dialog__actions form {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-1);
  margin: 0;
}
.track-form__identity { margin: 0; font-size: var(--fs-1); color: var(--ink-faint); }

/* .confirm-track lived here until 2026-08-09. Its card
   (partials/_confirm_track.html) is gone: the case-number flow tracks from
   the result row now, and the CNR flow's live preview is a whole case page,
   not a card. The rules are deleted rather than left orphaned, so nobody
   rebuilds the card after finding its stylesheet still here. */

/* Undo: a recovery path, never auto-dismissed. */
.undo-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  background: var(--surface-raised);
  border: 1px solid var(--line-strong);
  border-left: var(--rail) solid var(--accent);
  padding: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-4);
  font-size: var(--fs-2);
}
.undo-banner form { margin: 0; }

/* the inline "I checked — the next date is ___" override, now on the case
   page itself rather than on a separate worklist screen */
.override {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: var(--rail) solid var(--state-stale);
}
.override form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: end;
  margin: 0;
}
.override form label { flex-basis: 100%; margin: 0 0 var(--sp-1); }
/* capped, not full-bleed: a date field as wide as the panel reads as a text
   box and puts Save an arm's length from the value it commits */
.override form input[type="date"] { flex: 0 1 13em; width: auto; }
.override form button { flex: none; }

/* ==========================================================================
   Smart search (/find)
   ========================================================================== */
.find-form { margin: 0 0 var(--sp-5); }
.find-box {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  background: var(--bg);
  border: 1px solid var(--line-strong);
  padding: 0 var(--sp-3);
}
.find-box .icon { color: var(--ink-faint); width: 1.2em; height: 1.2em; }
.find-box input[type="search"] { border: none; min-height: 52px; padding: var(--sp-2) 0; background: none; }
.find-box input[type="search"]:focus-visible { outline: none; }
.find-box:focus-within { border-color: var(--accent); outline: 1px solid var(--accent); }
.find-form__actions { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
.find-count { color: var(--ink-muted); font-size: var(--fs-2); margin: var(--sp-4) 0; }
.find-miss {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: var(--rail) solid var(--line-strong);
  padding: var(--sp-4);
  margin-top: var(--sp-4);
}
.find-miss p:first-child { margin-top: 0; }
.find-miss__actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-4); }
/* The escape hatch, on the wall a person actually hits: party-name search
   works here and not at the court, so the route to an untracked case is its
   CNR (ui-spec.md §2.1b). */
.find-miss__hatch { margin-top: var(--sp-4); max-width: 62ch; }
.find-foot { margin-top: var(--sp-5); }

/* ==========================================================================
   Search (/search) — the merged screen: phase 1 above the button, phase 2
   below it, and the button is the boundary (ui-spec.md §2).
   ========================================================================== */
.mode-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  margin: var(--sp-3) 0 var(--sp-5);
  border-bottom: 1px solid var(--line-strong);
}
.mode-tab {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--sp-4);
  font-size: var(--fs-2);
  font-weight: 500;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  background: none;
  color: var(--ink-muted);
  text-decoration: none;
}
.mode-tab:hover { color: var(--ink); background: var(--surface); }
.mode-tab--active, .mode-tab--active:hover {
  color: var(--ink);
  border-bottom-color: var(--accent);
  font-weight: 600;
  background: none;
}

/* FULL WIDTH, not a 660px column (client, 2026-08-09: "can you make the
   search box UI span the space it has on the side instead of being left
   aligned").

   The 660px cap was a measure-of-text argument borrowed from prose, and this
   is not prose -- it is four fields and a list of results that the eye reads
   as one block. Capping it left two thirds of the screen empty beside a form
   whose own saved-case list was being squeezed into a narrow column and
   wrapping its two most important fields onto separate lines (see
   `.case-card__key` below). Widening the form fixes both at once. */
.search-form {
  background: var(--surface);
  border: 1px solid var(--line);
  padding: var(--sp-4) var(--sp-4) var(--sp-5);
  margin-bottom: var(--sp-5);
}
/* THE FIELDS SPAN THE FORM. There was a 620px cap here on a "readable
   measure" argument; the client rejected it, and they are right for the same
   reason they were right about the page shell -- a select and a number box
   are not prose, and a capped field inside an uncapped panel just leaves a
   hole in the panel. The container is the measure. */
.scope-toggle {
  border: none;
  padding: 0;
  margin: 0 0 var(--sp-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
}
.scope-toggle legend {
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-muted);
  padding: 0;
  margin-bottom: var(--sp-1);
}
.scope-radio {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  margin: 0;
  font-size: var(--fs-3);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  cursor: pointer;
}
/* the radio is a real either/or: exactly one court select is shown. Browsers
   without :has() show both rows; the server still honours only the one the
   radio designates, so the behaviour never depends on the CSS. */
.search-form:has(#scope-complex:checked) #establishment-field { display: none; }
.search-form:has(#scope-establishment:checked) #complex-field { display: none; }

/* align-items: flex-start, and it is load-bearing rather than tidy: the
   in-flight block sits in this row and GROWS as the 5s and 12s lines append.
   Centred, that growth would move the Search button up under the finger that
   just pressed it — the one thing this screen's action geometry may never do. */
.search-form__actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: flex-start; margin-top: var(--sp-5); }
.search-form__actions button[type="submit"] { flex: 1 1 auto; }
@media (min-width: 480px) { .search-form__actions button[type="submit"] { flex: none; min-width: 11em; } }
/* The button names its cost; this line prices it. The reference form shows a
   captcha box above its Search button, so its user can SEE what pressing it
   costs — we solve the captcha server-side, which removes the friction and
   the disclosure with it. This puts the disclosure back. */
.search-form__cost {
  flex-basis: 100%;
  margin: 0;
  color: var(--ink-faint);
  font-size: var(--fs-1);
}

/* ==========================================================================
   Phase 1 — the saved band (ui-spec.md §2.1a)

   A RECESS, not a raised panel: --bg inside the --surface form, full-bleed to
   the form's padding via negative inline margins. It has to be one for a
   concrete reason — `.case-card` is --surface and `.search-form` is
   --surface, so cards dropped straight into the form would be invisible
   against it. Dropping the band one surface step restores the contrast with
   a pattern that already exists (`.sheet__body`) and no new token.
   ========================================================================== */
.saved-band {
  display: block;
  background: var(--bg);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin: var(--sp-5) calc(var(--sp-4) * -1) 0;
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
}
.saved-band .section-bar { margin-top: 0; }
.saved-band__note {
  color: var(--ink-muted);
  font-size: var(--fs-2);
  margin: var(--sp-2) 0 0;
  max-width: 60ch;
}
.saved-band__note--next { color: var(--ink); }
/* The /find entry point (the top bar that used to carry it is gone). Quiet on
   purpose: it is an escape hatch for the case where the number is not what you
   have, not a competing search control. */
.saved-band__byname {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-1);
  color: var(--ink-faint);
}
.saved-band__byname a { color: var(--ink-muted); }
.saved-band__byname a:hover { color: var(--ink); }
.saved-band__more {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-2);
  color: var(--ink-muted);
}
.saved-band__more a { color: var(--ink); }
/* The cards inside the recess sit one step UP again, which is the whole
   point of the recess: --surface on --bg reads as a row, exactly as it does
   on the Calendar. */
.saved-band .case-card { margin-top: var(--sp-3); }
/* The band's one named action. A LINK, never a button: this lives inside the
   search form, and a submit in here would spend a captcha. */
.saved-band .case-card__asof {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-3);
}
.case-card__view {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  min-width: 5.5em;
  padding: var(--sp-1) var(--sp-4);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  font-size: var(--fs-2);
  font-weight: 600;
  text-decoration: none;
  /* the cell around it wraps long party names anywhere; a control's LABEL is
     never the thing that should wrap (the old "Vie / w") */
  white-space: nowrap;
  overflow-wrap: normal;
  word-break: keep-all;
}
.case-card__view:hover { background: var(--surface-raised); border-color: var(--accent); }

/* Phase 1 gets NO skeleton — 250ms + ~5ms would strobe next to the field
   being typed in. This line is the only loading affordance it has, and it is
   revealed by a timer at 500ms, so in the normal case it is never painted. */
.saved-band__looking {
  display: none;
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-2);
  color: var(--ink-faint);
}
.saved-band.htmx-request .saved-band__looking { display: block; }

/* The single-step reveal used as a timer, three times on this screen. No
   interpolated property: `step-end` plus a 1ms duration means the value flips
   once, when the delay expires, and nothing moves. */
@keyframes reveal-late { to { opacity: 1; } }
.reveal-late { opacity: 0; }
.saved-band.htmx-request .saved-band__looking.reveal-late {
  animation: reveal-late 1ms step-end 500ms forwards;
}

/* ==========================================================================
   Phase 2's loading state — the ghost screen (ui-spec.md §2.2a)
   ========================================================================== */

/* (a) the in-flight block, in the form's own actions row. This is the piece
   that must never be off-screen: on a 390px phone the Search button is at the
   bottom of the viewport when it is pressed. */
/* Beside the button on desktop, on its own line below it on a phone (the
   flex basis is wider than what is left over at 390px, so it wraps) — either
   way it is ON SCREEN at the moment of the press, which is the whole point:
   press a button, see nothing happen, and the screen is broken however well
   the region below the fold is rendering. */
.inflight { display: none; flex: 1 1 17em; }
.inflight.htmx-request { display: block; }
.inflight__label {
  margin: 0;
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-muted);
}
/* The ONE moving element on this screen, and the entire motion budget of it:
   2px, transform-only, compositor-only, no gradient. Indeterminate on
   purpose — we cannot know how far along the court's server is, and a bar
   claiming a fraction it invented is a confidently wrong statement about the
   one thing this screen is waiting on. It exists to distinguish WAITING from
   FROZEN over a wait that can run to ten seconds. */
.inflight__bar {
  height: 2px;
  margin: var(--sp-2) 0;
  background: var(--line);
  overflow: hidden;
  max-width: 34ch;
}
.inflight__bar span {
  display: block;
  height: 100%;
  width: 35%;
  background: var(--accent);
  transform: translateX(-100%);
  animation: inflight-slide 1.5s ease-in-out infinite;
}
@keyframes inflight-slide {
  from { transform: translateX(-100%); }
  to { transform: translateX(300%); }
}
.inflight__note {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-1);
  color: var(--ink-faint);
  max-width: 40ch;
}
/* Escalation, timed off the same class. Neither line replaces what is already
   there; each APPENDS — so they must take no room until they do, or the block
   opens with 120px of blank space inside the form panel and reads as a
   rendering fault for the first five seconds of every search. `max-height` is
   flipped by the same step-end keyframe as the opacity: one discrete change
   at the delay, nothing interpolated, nothing in motion. There is no
   client-side timeout of ours — a "giving up" at 8s would have abandoned the
   measured 10.4s run. */
.inflight__note--5s,
.inflight__note--12s { max-height: 0; overflow: hidden; margin-top: 0; }
@keyframes reveal-late-line { to { opacity: 1; max-height: 12em; margin-top: var(--sp-1); } }
.inflight.htmx-request .inflight__note--5s { animation: reveal-late-line 1ms step-end 5s forwards; }
.inflight.htmx-request .inflight__note--12s { animation: reveal-late-line 1ms step-end 12s forwards; }

@media (prefers-reduced-motion: reduce) {
  /* The bar is not exempt: it stops, and becomes a static rule. The words and
     the disabled button carry the meaning without it. */
  .inflight__bar span { transform: none; width: 100%; background: var(--line-strong); }
}

/* (b) the ghost results, inside #results. Built out of the REAL table markup
   (partials/_results_skeleton.html), so it inherits every responsive rule the
   answer has — including the ≤520px stack — and the two cannot drift apart.
   Static bars: no shimmer, no pulse, no gradient sweep. */
.skeleton { display: none; }
#results.htmx-request .skeleton { display: block; }
#results.htmx-request .results-answer { display: none; }
.skeleton__bar {
  display: block;
  height: 1em;
  background: var(--surface-raised);
  border-radius: var(--radius);
}
.skeleton__head { display: block; }
.skeleton__bar--court { height: 0.9em; width: 16em; max-width: 70%; }
.skeleton__bar--serial { width: 1.6em; }
.skeleton__bar--case { width: 9em; max-width: 100%; }
.skeleton__bar--party { width: 12em; max-width: 100%; }
.skeleton__bar--party-short { width: 7em; max-width: 70%; margin-top: var(--sp-2); }
.skeleton__bar--action { width: 100%; height: var(--tap); flex: 0 0 auto; }
/* Announced once, and counting nothing: the bars are all aria-hidden, so a
   screen-reader user is told a search is running and never that there are
   three results. */
.results-status {
  display: none;
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-faint);
}
#results.htmx-request .results-status { display: block; }
@media (max-width: 520px) {
  .skeleton__bar--action { width: 5em; }
}

.results-banner {
  color: var(--ink);
  font-size: var(--fs-2);
  background: var(--surface);
  border-left: var(--rail) solid var(--line-strong);
  padding: var(--sp-3) var(--sp-4);
  margin: var(--sp-4) 0;
}
/* Where these rows came from, and when — in the micro-label voice, so it
   pairs with the saved band's heading by VOICE rather than by container. Not
   a second .section-bar: the per-court headers below already are section
   bars, and a parent one would fork the heading hierarchy for no gain.
   This one stamp governs every row under it: portal rows have no
   `last_seen_at` of their own, so this is the honest answer to "how old is
   this?" at the level the fact actually exists at. */
.results-banner__source {
  display: block;
  margin-bottom: var(--sp-1);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-faint);
}
.banner__note { margin: var(--sp-2) 0 0; color: var(--ink); }
/* A retry is a button with its cost written on it. There is no auto-retry
   anywhere on this screen: a retry spends a captcha nobody asked to spend. */
.results-retry { margin: var(--sp-3) 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); }
.results-hatch { margin: var(--sp-3) 0 0; font-size: var(--fs-2); }
.results-hatch .muted { color: var(--ink-faint); }
/* The widening escape hatch: shown only when the search was narrowed to a
   case type, because that is the one thing most likely to be wrong when a
   case the clerk is holding does not appear. It re-runs the search, which
   costs a CAPTCHA -- said plainly next to the link rather than discovered. */
.results-widen {
  margin: calc(var(--sp-4) * -1 + var(--sp-2)) 0 var(--sp-4);
  font-size: var(--fs-2);
}
.results-widen .muted { color: var(--ink-faint); }

.court-block { margin-bottom: var(--sp-5); }
.results-table { background: var(--surface); border: 1px solid var(--line); }
.results-table .col-serial { width: 3.5em; }
.results-table .col-track { width: 9em; }
.results-table td.col-serial { color: var(--ink-faint); }
.results-table .case-number-cell { font-weight: 600; white-space: nowrap; overflow-wrap: normal; }
.results-table .parties-cell { overflow-wrap: anywhere; }
.results-table .versus {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
}

/* --- one result row = one <tbody> ----------------------------------------
   A row that has been expanded is two <tr>s, so the row's own hairline has
   to belong to the GROUP, not to each <tr> inside it -- otherwise a rule is
   drawn between a case and its own stage/date line. */
.results-table .result-row > tr > td { border-bottom: none; }
.results-table .result-row { border-bottom: 1px solid var(--line); }
.results-table .result-row:last-child { border-bottom: none; }

/* --- the action column's two fixed slots ----------------------------------
   Slot 1 is the write, or the OUTCOME of the write; slot 2 is always a read.
   The geometry is constant and only the contents change — which is what makes
   a fat-finger repeat safe: double-tap `+` and the second tap lands on an
   inert tick, because the instant slot 1 has written it stops being a control
   at all. They are sized and shaped differently (filled vs outlined), stacked
   with a gap on desktop and 24px apart on a phone, and both clear 44px. */
.row-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-3);
}
.row-actions form { margin: 0; }
/* `overflow-wrap: anywhere` on the table cells (needed for long party names)
   used to apply to the button in here too and broke its label across two
   lines -- the old "Vie / w". A control's label is never the thing that
   should wrap, so opt out explicitly rather than widening the column until
   it happens to fit. `.row-view` is in this list for the same reason: "View"
   is four characters and would break just as happily. */
.row-track,
.row-more,
.row-view {
  width: 100%;
  min-width: auto;
  padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-2);
  white-space: nowrap;
  overflow-wrap: normal;
  word-break: keep-all;
}
.row-track__glyph { font-size: var(--fs-5); line-height: 1; font-weight: 400; }
.row-busy { text-align: center; }

/* Slot 2 on a row we already hold: a link to our own record. Outlined, like
   the `see more` it replaces, because it is the same kind of thing — a read. */
.row-view {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
}
.row-view:hover { background: var(--surface-raised); border-color: var(--accent); }

/* Slot 1 after the write: the outcome, in the slot the control was in.
   Chrome, not state -- NO HUE. The three hues on this screen mean
   hearing-date state and nothing else, and a green "Saved!" would be the
   first time colour in this app meant "an action succeeded". */
.row-flag--saved {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-2);
}
.row-flag--saved .icon { width: 1.15em; height: 1.15em; }
/* It can hold focus (htmx restores it there by id after the swap) but it is
   not a control, so it must not look like one when it does. */
.row-flag--saved:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- the expanded line ----------------------------------------------------
   A raised panel ATTACHED to its row, with no rule between the two: the only
   hairline in this table separates one case from the next, so a row and its
   own stage/date line can never read as two cases. (They did, briefly, when
   both boundaries were the same 1px --line; caught in a screenshot.) */
.row-detail {
  background: var(--surface-raised);
  font-size: var(--fs-2);
}
.row-detail--error { border-left: var(--rail) solid var(--state-stale); }
.row-detail__error { margin: 0; color: var(--state-stale); }
.row-detail__line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
  margin: 0 0 var(--sp-2);
}
.row-detail__line:last-child { margin-bottom: 0; }
.row-detail__line--foot { margin-top: var(--sp-2); }
/* Undo sits at the far end of the line from `+`, never next to it. */
.row-detail__line--foot .row-action--undo { margin-left: auto; }
.row-detail__stage { color: var(--ink-muted); }
.row-detail__stage strong { color: var(--ink); font-weight: 600; }
.row-detail__cnr .label,
.row-detail__line .label {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
}
.row-detail__asof,
.row-detail__note { color: var(--ink-faint); font-size: var(--fs-1); }
.row-detail__link { font-size: var(--fs-1); }
.row-detail__line .next-value { margin: 0; }

/* "TRACKED" is chrome -- it says the record exists, not anything about the
   hearing date -- so it is achromatic and carries a border, never a fill. */
.row-flag {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  border: 1px solid var(--line-strong);
  padding: 1px var(--sp-2);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink);
}
.row-flag--off { color: var(--ink-muted); }

@media (max-width: 520px) {
  .results-table td.col-serial { display: none; }
  .results-table .case-number-cell { white-space: normal; }
  /* The stacked-card treatment applies to the ROW GROUP: the generic
     `.data-table tr` rule would otherwise draw a rule (and a second block of
     padding) between a case and its own expanded line. */
  .results-table .result-row { display: block; }
  .results-table .result-row > tr { border-bottom: none; }
  .results-table .result-row__detail { padding-top: 0; }
  .results-table td.col-track { width: auto; padding: var(--sp-3) 0 var(--sp-1); }
  .results-table td.row-detail { padding: var(--sp-3); }
  /* Side by side once the row is a stacked card: two controls of visibly
     different size and shape, held 24px apart, rather than two full-width
     bars a thumb cannot tell apart in a corridor. The gap is the point --
     `+` SAVES a case and `see more` does not, and the tap that hits the
     wrong one of those is the failure mode this screen has to design out. */
  .row-actions { flex-direction: row; align-items: center; gap: var(--sp-5); }
  .row-track { width: auto; min-width: 4.5em; }
  .row-more, .row-view { width: auto; min-width: 5em; }
  .row-flag--saved { justify-content: flex-start; }
}

/* ==========================================================================
   Tables — case history, orders, and the portal-shaped detail tables
   ========================================================================== */
.data-table, .detail-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-2);
  background: var(--surface);
  border: 1px solid var(--line);
}
.data-table th, .detail-table th {
  text-align: left;
  color: var(--ink-faint);
  font-weight: 600;
  font-size: var(--fs-1);
  text-transform: uppercase;
  letter-spacing: var(--track);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--line-strong);
  vertical-align: bottom;
}
.data-table td, .detail-table td {
  text-align: left;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
  vertical-align: top;
  overflow-wrap: anywhere;   /* no horizontal scroll, ever */
}
.detail-table th, .detail-table td { border-right: 1px solid var(--line); }
.detail-table th:last-child, .detail-table td:last-child { border-right: none; }
/* a date must never break across lines — "04-08-" / "2026" is a misreading
   waiting to happen. Text columns absorb the wrapping instead. */
.data-table td.date-cell, .detail-table td.date-cell { white-space: nowrap; overflow-wrap: normal; }

@media (max-width: 520px) {
  .data-table thead { display: none; }
  .data-table tr { display: block; padding: var(--sp-3); border-bottom: 1px solid var(--line); }
  .data-table td { display: block; border: none; padding: 1px 0; }
  .data-table td[data-label]::before {
    content: attr(data-label);
    display: block;
    color: var(--ink-faint);
    font-size: var(--fs-1);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--track);
  }
  .data-table td.date-cell { white-space: normal; }
}
@media (max-width: 700px) {
  .detail-table thead { display: none; }
  .detail-table tr { display: block; }
  .detail-table td { display: block; border: none; border-bottom: 1px solid var(--line); padding: var(--sp-2) var(--sp-3); }
  .detail-table tr td:last-child { border-bottom: none; }
  .detail-table td[data-label]::before {
    content: attr(data-label);
    display: block;
    color: var(--ink-faint);
    font-size: var(--fs-1);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--track);
  }
  .detail-table td.date-cell { white-space: normal; }
}

.change-log { list-style: none; padding: 0; margin: 0; font-size: var(--fs-2); }
.change-log li { padding: var(--sp-2) 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
.change-log__major { font-weight: 600; }

/* ==========================================================================
   Case detail
   ========================================================================== */
.court-heading {
  text-align: center;
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-muted);
  margin: var(--sp-5) 0 var(--sp-2);
  overflow-wrap: anywhere;
}
.case-number-line {
  text-align: center;
  font-size: var(--fs-5);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0 0 var(--sp-3);
  overflow-wrap: anywhere;
}
.cause-title {
  overflow-wrap: anywhere;
  font-size: var(--fs-3);
  font-weight: 400;
  line-height: 1.45;
  color: var(--ink-muted);
  margin: 0 auto var(--sp-6);
  max-width: 46ch;
  text-align: center;
}
.cause-title .vs {
  color: var(--ink-faint);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  margin: 0 var(--sp-1);
}
.asof-line { margin: var(--sp-2) 0; }
.table-footnote { margin: var(--sp-2) 0 0; }

.party-list { padding-left: var(--sp-5); margin: var(--sp-1) 0 var(--sp-5); }
.party-list li { margin-bottom: var(--sp-3); overflow-wrap: anywhere; }
.party-list .advocate { color: var(--ink-muted); font-size: var(--fs-2); }

section { margin-bottom: var(--sp-6); }

/* ---- the section jump strip (§3.1) ---------------------------------------
   Six plain in-page links, and what makes the Status/History-above-Details
   reorder safe: the page grew three sections and a per-row disclosure at
   once, and on a phone it is several screens long.

   DELIBERATELY NOT STICKY. A sticky bar repaints every scroll frame on the
   old integrated GPUs this runs on, and eats the vertical space it is trying
   to save. Hairline separators instead of gaps, so it reads as one
   instrument rather than six loose links; every link is a 44px target and
   the strip wraps to two lines at 390px. */
.jump-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin: 0 0 var(--sp-6);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.jump-strip a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  color: var(--ink-muted);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  text-decoration: none;
}
.jump-strip a + a { border-left: 1px solid var(--line); }
.jump-strip a:hover { color: var(--ink); background: var(--surface); }

/* ---- Case history: the section head and its one fetch control (§3.4) ----- */
.section-head { margin-bottom: var(--sp-3); }
.section-head__note { margin: 0 0 var(--sp-3); color: var(--ink-faint); }

/* Secondary, never primary: this page has no primary action, and a filled
   button here would compete with nothing and shout at everything. */
.fetch-sheets { margin: 0 0 var(--sp-4); }
.fetch-sheets button { text-align: left; }
.fetch-sheets__cost {
  margin: var(--sp-2) 0 0;
  color: var(--ink-faint);
  font-size: var(--fs-1);
  max-width: 46ch;
}
/* In-flight, in the section head only, and NAMING THE COST — this is four
   round trips, not one, and the human is standing in a corridor. */
.fetch-sheets__indicator { margin: var(--sp-2) 0 0; color: var(--ink-muted); }

/* ---- The order sheet's four states (§3.4) --------------------------------
   NO HUE IN THIS BLOCK, in any state. All three of the product's colours
   mean something about a HEARING DATE; a missing order sheet cannot miss a
   hearing. State is carried by WORDS and BORDER STYLE — none / dotted /
   solid — which survive a colour-blind reader and a photocopier alike. */

/* One <tbody> per sitting: the data row and its order sheet are one unit, so
   the rule that closes a sitting belongs to whichever row is last in it. */
.history-table .sitting > tr:not(:last-child) > td { border-bottom: none; }
.history-table .sitting__sheet > td { padding: 0 var(--sp-3) var(--sp-3); }

/* State 1 — a real disclosure, and the ONLY state that gets one. Its
   presence is therefore a scannable signal: a marker on the row means the
   court's own words are on file.

   NO left rule here, deliberately: that completes the none / dotted / solid
   set. Giving state 1 a solid rule too would have it wearing state 4's mark
   ("the portal carries none"), which is the one distinction in this section
   a reader has to be able to make at a glance. The capped measure applies to
   the whole disclosure so the `checked` stamp lands over the end of the
   paragraph it belongs to, not a foot away at the table's edge. */
.sheet { max-width: 68ch; }
.sheet__summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  cursor: pointer;
  color: var(--ink-muted);
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  /* the UA ::marker triangle differs per browser AND the latin font subset
     carries no triangle or arrow glyph — so we draw our own, below */
  list-style: none;
}
.sheet__summary::-webkit-details-marker { display: none; }
.sheet__summary:hover { color: var(--ink); background: var(--surface-raised); }
.sheet__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sheet__label { overflow-wrap: normal; }
.sheet__checked {
  margin-left: auto;
  color: var(--ink-faint);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}

/* The +/− marker, drawn in CSS from two bars (§0.5). Deliberately NOT part
   of the SVG icon vocabulary: that table is the STATE vocabulary, and
   nothing in it means "expandable". */
.sheet__marker { position: relative; flex: none; width: 12px; height: 12px; }
.sheet__marker::before,
.sheet__marker::after {
  content: "";
  position: absolute;
  left: 0;
  top: 5px;
  width: 12px;
  height: 2px;
  background: var(--ink-muted);
}
.sheet__marker::after { transform: rotate(90deg); }
details[open] > .sheet__summary .sheet__marker::after { display: none; }
.sheet__summary:hover .sheet__marker::before,
.sheet__summary:hover .sheet__marker::after { background: var(--ink); }

/* The court's own words: set on --bg INSIDE a --surface table, i.e. one
   surface step DOWN. A recess, not a raise — it is a quotation from
   elsewhere, not another panel of ours. Larger than the 14px table around
   it, because this is the field he reads standing up in a corridor. */
.sheet__body {
  margin: 0;
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  background: var(--bg);
  border-top: 1px solid var(--line);
  color: var(--ink);
  font-size: var(--fs-3);
  line-height: 1.5;
  white-space: pre-wrap;      /* newlines are content, and stay content */
  overflow-wrap: anywhere;
}

/* States 2, 3 and 4 — a plain line, never a disclosure. A <details> that
   opens onto "there is nothing here" claims a record that does not exist. */
.sheet-note {
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-2);
  overflow-wrap: anywhere;
}
/* 2 and 3: OUR gap. They differ in cause but not in what they ask of a
   human, so they share a treatment and differ in words. */
.sheet-note--gap { color: var(--ink-faint); border-left: 2px dotted var(--line-strong); }
/* 4: NOT our gap, and the only one of the four the reader can act on — ring
   the court, or read the physical file. Hence the heavier rule. */
.sheet-note--none { color: var(--ink-muted); border-left: 2px solid var(--line-strong); }

/* ---- the pointer line under Case Status (§3.4) --------------------------- */
.pointer-line a { color: var(--ink); }

/* ---- Acts (§3.3): a definition list, nested inside Case Details ---------- */
.section-bar--nested { margin-top: var(--sp-5); }
.acts-list { margin: var(--sp-3) 0 0; }
.acts-list dt { color: var(--ink); font-weight: 600; overflow-wrap: anywhere; }
.acts-list dd {
  margin: var(--sp-1) 0 var(--sp-3) var(--sp-4);
  color: var(--ink-muted);
  font-size: var(--fs-2);
  overflow-wrap: anywhere;
}
.acts-list__none { color: var(--ink-faint); }
.empty-state--tight { padding: var(--sp-3) 0; }

/* ---- Process details (§3.5) ---------------------------------------------
   The ×N IS the information the dropped `Process id` was carrying — that
   there are N of these — expressed as a count instead of N opaque keys. */
.process-count { color: var(--ink-faint); white-space: nowrap; }

/* ---- the em-dash legend (§3.8) ------------------------------------------- */
.legend-line { color: var(--ink-faint); margin: var(--sp-5) 0 0; }

@media (max-width: 520px) {
  /* the diary restacks: one labelled block per sitting, its order sheet
     directly beneath, and one rule closing the pair */
  .history-table .sitting { display: block; }
  .history-table .sitting > tr:not(:last-child) { border-bottom: none; padding-bottom: 0; }
  .history-table .sitting__sheet { padding: var(--sp-2) var(--sp-3) var(--sp-3); }
  .history-table .sitting__sheet > td { padding: 0; }
  .sheet__body { padding: var(--sp-3); }
}

/* Destructive actions are visually separated from everything else: a rule
   above, right-aligned, outlined not filled. */
.destructive-action { text-align: right; margin-top: var(--sp-6); padding-top: var(--sp-4); border-top: 1px solid var(--line); }

/* ==========================================================================
   Print helpers (screen side) — print.css does the real work
   ========================================================================== */
.print-footer { display: none; }
.print-stale-note { display: none; }

/* ==========================================================================
   Login — the gate, not a product surface
   ========================================================================== */
body.body--login { background: var(--bg); min-height: 100vh; }
.page--login, main.page.page--login { max-width: 25rem; padding-top: clamp(24px, 9vh, 88px); }
.login-card {
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-top: 2px solid var(--accent);
  padding: var(--sp-6) var(--sp-5) var(--sp-5);
}
.login-card__name { margin: 0; font-size: var(--fs-5); letter-spacing: -0.015em; }
.login-card__purpose { margin: var(--sp-2) 0 var(--sp-5); color: var(--ink-muted); font-size: var(--fs-2); }
/* the one bare pre-auth number: a count and nothing else */
.login-count {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-5);
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.login-count__n { font-size: var(--fs-6); font-weight: 600; line-height: 1; letter-spacing: -0.02em; color: var(--ink); }
.login-count__label { color: var(--ink-faint); font-size: var(--fs-1); font-weight: 600; text-transform: uppercase; letter-spacing: var(--track); }
.login-form label[for="passcode"] { margin-top: 0; }
.login-form__submit { width: 100%; margin-top: var(--sp-5); }
.login-card__foot { margin: var(--sp-5) 0 0; padding-top: var(--sp-4); border-top: 1px solid var(--line); }

/* ==========================================================================
   htmx
   ========================================================================== */
.htmx-indicator { display: none; color: var(--ink-muted); font-size: var(--fs-2); }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { display: inline-block; }

/* ==========================================================================
   Settings — the cases the sync cannot read (client, 2026-08-09).

   Deliberately a plain list and not a table: it is usually empty, occasionally
   two rows, and a table's header row is a permanent structure for something
   that is normally absent. Each row is one sentence's worth of facts, with the
   case number as the only link — the fix is nearly always on the case page.
   ========================================================================== */
.failing-list { list-style: none; margin: 0 0 var(--sp-5); padding: 0; }
.failing-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--sp-3);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-2);
}
.failing-row__case { font-weight: 600; color: var(--ink); }
.failing-row__court { color: var(--ink-muted); }
/* The state hue, because this row IS a stale-data problem — the one licensed
   meaning of colour in this product. */
.failing-row__why { color: var(--state-stale); font-weight: 500; }
.failing-row__asof { color: var(--ink-faint); font-size: var(--fs-1); }
/* The portal's own words, wrapped and full-width: an error truncated to fit a
   row is an error nobody can act on. */
.failing-row__detail {
  flex: 1 1 100%;
  color: var(--ink-faint);
  font-size: var(--fs-1);
  overflow-wrap: anywhere;
}
.settings-hint {
  margin: 0 0 var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-left: 2px solid var(--accent);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--fs-2);
}

/* ==========================================================================
   Search results — the row's own destination (client, 2026-08-09).

   Three changes, one idea: everything you can press is in the actions column,
   and a saved row's whole line is the press.
   ========================================================================== */

/* `Open full case` moved out of a sentence at the foot of the detail strip and
   into the actions column, where every other control on this row already is.
   Same button vocabulary as `.row-view` was — this replaces it in the slot. */
.row-open {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: none;
  color: var(--ink);
  font-size: var(--fs-2);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.row-open:hover { background: var(--surface-raised); border-color: var(--accent); }

/* A SAVED ROW'S WHOLE LINE IS A LINK to its case page. The overlay is
   positioned against the <tr>; `position: relative` on a table row is
   supported by every engine this app targets (office Chrome), and the
   alternative — wrapping the row — is not legal markup around a <form>.

   Stacking, and both halves are required: the overlay takes `z-index: 0` and
   everything interactive in the row takes `1`. Without the explicit `0` the
   overlay would be a positioned element with `z-index: auto`, and source
   order would decide — it is inside the FIRST cell, so the forms in the last
   cell would win by accident today and lose the day someone reorders the
   cells. */
.result-row__main { position: relative; }
.row-hit { position: absolute; inset: 0; z-index: 0; }
.result-row__main a:not(.row-hit),
.result-row__main button,
.result-row__main form { position: relative; z-index: 1; }
.result-row:hover .case-number-cell { text-decoration: underline; text-decoration-thickness: 2px; }

/* The ambiguous-case-type prompt (`m` is M.C. and M.V.C.). Shown rather than
   guessed: narrowing on a guess hides rows, and rows hidden by a filter
   nobody chose are indistinguishable from rows that do not exist. */
.field-choices {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-2);
  color: var(--ink-muted);
}
.field-choice {
  margin: 0 var(--sp-1) var(--sp-1) 0;
  padding: 2px var(--sp-2);
  min-height: 28px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: none;
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.field-choice:hover { background: var(--surface-raised); border-color: var(--accent); }

/* ==========================================================================
   The case-type combobox — a text field that LOOKS like the select above it.

   It cannot BE a select (it must accept `os` and mean `O.S.`, which no native
   type-to-jump can do) and it must not look like anything else, so every
   dimension here is copied from `.form-field select` rather than chosen.
   ========================================================================== */
.combo { position: relative; }
.combo > input { width: 100%; padding-right: calc(var(--sp-5) + 8px); }
/* The same caret the select draws, so the two controls read as one kind. */
.combo__caret {
  position: absolute;
  top: 50%;
  right: var(--sp-3);
  width: 0; height: 0;
  margin-top: -2px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--ink-muted);
  pointer-events: none;
}
.combo__list {
  position: absolute;
  z-index: 40;
  top: calc(100% - 1px);
  left: 0;
  right: 0;
  display: none;            /* the script opens it; see .combo--open */
  max-height: min(46vh, 340px);
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
}
.combo--open .combo__list { display: block; }
.combo--open.combo--empty .combo__list { display: none; }
.combo__option {
  padding: var(--sp-2) var(--sp-3);
  min-height: 34px;
  font-size: var(--fs-2);
  color: var(--ink);
  cursor: pointer;
}
.combo__option:hover { background: var(--surface-raised); }
/* Keyboard highlight and hover are the SAME treatment on purpose: arrowing to
   a row and pointing at it are the same act, and two different highlights on
   one list is how you lose track of which one Enter will take. */
.combo__option[aria-selected="true"] {
  background: var(--accent-wash);
  box-shadow: inset 3px 0 0 var(--accent);
}

/* NO SCRIPT: the list is a plain, always-visible block. `.combo--open` is only
   ever added by the script, so this is what a scriptless browser renders --
   longer, and correct. */
.no-js .combo__list { display: block; position: static; max-height: 220px; }

/* The unresolvable-case-type message. Same block as the ambiguous prompt, in
   the state hue, because this one is a refusal rather than a choice: the
   search did not run. */
.field-choices--bad { color: var(--state-stale); }

/* ==========================================================================
   The Database screen (§E9) — every saved case, filtered, one scrolling list.
   ========================================================================== */
.db-filters {
  background: var(--surface);
  border: 1px solid var(--line);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  margin: 0 0 var(--sp-5);
}
.db-filters__search { margin-bottom: var(--sp-3); }
.db-filters__search input { width: 100%; }
/* `auto-fit` and not a fixed column count: six selects reflow from one column
   on a phone to six across on a wide monitor without a breakpoint per step,
   and the minimum is what keeps a court name readable. */
.db-filters__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--sp-3);
}
.db-filters__grid .form-field { margin: 0; }
.db-filters__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-3);
  margin-top: var(--sp-3);
}
.db-filters__foot p { margin: 0; font-size: var(--fs-1); }
.db-filters__clear { margin-left: auto; font-size: var(--fs-2); }
.db-count { margin: 0 0 var(--sp-3); color: var(--ink-muted); font-size: var(--fs-2); }

/* The scroll sentinel. Visible and worded: an invisible one means a reader on
   a slow connection sees an ending that is not one. */
.db-more {
  padding: var(--sp-4) 0;
  text-align: center;
  color: var(--ink-faint);
  font-size: var(--fs-2);
}
.db-end {
  padding: var(--sp-4) 0 0;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: var(--fs-2);
}

/* A court whose every row we already hold. The court still appears -- deleting
   the block would make a court vanish from a result list it is genuinely in --
   and this line is what stops the empty section reading as a failed fetch. */
.court-block__all-saved {
  margin: 0 0 var(--sp-4);
  padding: var(--sp-2) var(--sp-3);
  border-left: 2px solid var(--line-strong);
  color: var(--ink-muted);
  font-size: var(--fs-2);
}

/* ==========================================================================
   Portal results as CASE CARDS (docs/search-rework.md E5/E6/E8).

   There is almost nothing here, and that is the point. The result card is
   `.case-card` — the same class, the same rail, the same key band, the same
   foot as the Calendar's row and the saved band's row — so the two halves of
   the Search screen share every rule above rather than agreeing by hand. What
   follows is only what a PORTAL card has and a saved one does not: an action
   strip, and a hearing-date band we have not paid to fill in.
   ========================================================================== */

/* The court disclosure carries `.court-block` as well as `.case-group` (one
   box, two names — see _search_results.html). `.court-block`'s own 32px
   bottom margin is dropped here: on the day list the SUMMARY owns the spacing
   between courts, and inheriting two spacing systems at once is how the two
   screens start drifting apart a pixel at a time. */
.case-group.court-block { margin: 0; }

/* --- the action strip ------------------------------------------------------
   `.row-actions` in the table was a COLUMN in a 9em-wide cell. On a card it is
   a row along the bottom edge, right-aligned: `+` (the only control here that
   writes), then the read beside it. It keeps the class name because the rule
   attached to that name is a safety rule and not a layout one — slot 1 writes,
   everything after it reads — and because print.css already drops the strip
   from paper by that name. Undo is deliberately NOT in here; see the as-of
   line below. */
.row-actions--card {
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
  margin-top: var(--sp-3);
}
/* The controls sized themselves to a table column (`width: 100%`); on a card
   they size to their own labels. `+` stays square and stays 44px, because it
   is the one control on this card that writes and it is pressed one-handed. */
.row-actions--card .row-track {
  width: auto;
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0 var(--sp-3);
}
.row-actions--card .row-flag--saved { min-width: auto; padding: 0; }

/* The muted foot line carries the as-of stamp and, on a card `+` has just
   written, UNDO — a row above the action strip and at the opposite end of it,
   so the tap that saves and the tap that unsaves are never in the same place.
   Distance is what makes a mis-tap unlikely; sequence is not. */
.case-card--portal .case-card__asof {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1) var(--sp-3);
}
.case-card--portal .case-card__asof .row-action--undo { margin-left: auto; }
.case-card--portal .case-card__asof form { margin: 0; }

/* --- E6: the locked hearing-date band --------------------------------------
   An unsaved portal card knows the case's identity and nothing else, because
   the search response carries no dates at all. Stage and next date cost one
   `get_cnr_details` per case — captcha-free, but a real request and ~2.5s of
   somebody's wait — and that request used to hide behind a `see more` button
   that looked exactly as free as its neighbour.

   So the band is drawn in its real position, at its real size, and blurred,
   with the price written on the button over it. The blur is the honest
   rendering of "we could know this, and have not asked".

   ⚠ WHAT IS BLURRED IS EMPTY. `.case-card__ghost` is a bar with no text
   content — never a placeholder date, never a stale value, never the previous
   card's. A blur is a rendering effect and rendering effects come off (a
   screenshot at 400%, a browser that ignores filters, a print). A wrong date
   under 6px of blur is still a wrong date, and that is the one failure this
   product cannot have. */
.case-card__key--locked {
  position: relative;
  /* Room for a 44px button and its cost line without the band jumping size
     when the real values land: the revealed band is two labelled values, this
     one is the same two plus the overlay. Kept close on purpose — the card
     must not resize under the finger when the answer arrives. */
  min-height: 92px;
  align-items: stretch;
}
.case-card__lock-ghosts {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-2) var(--sp-5);
  width: 100%;
  /* `filter` and not `opacity`: a faded label reads as "disabled", a blurred
     one reads as "not yet legible", which is the true statement. The labels go
     with it — half a band in focus and half out reads as a rendering bug. */
  filter: blur(5px);
  opacity: 0.6;
  user-select: none;
}
.case-card__lock-ghosts .case-card__key-item--next { flex: 1 1 0; min-width: 0; }
.case-card__lock-ghosts .case-card__key-item--stage { flex: 0 0 auto; }
/* The bars stand where the values would stand and are sized like values, so
   the band's geometry is honest even though its contents are not there. */
.case-card__ghost {
  display: block;
  width: 8.5em;
  max-width: 100%;
  height: 0.9em;
  margin-top: 2px;
  background: var(--ink-faint);
  border-radius: var(--radius);
}
.case-card__ghost--short { width: 5.5em; }

/* The overlay. It covers the band rather than sitting beside it, because the
   thing being said is "this space is not filled in yet" and a button parked
   next to a blur says something weaker. */
.case-card__lock {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  margin: 0;
}
.case-card__lock-button {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--tap);
  padding: var(--sp-1) var(--sp-4);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-2);
  font-weight: 600;
  line-height: 1.25;
  cursor: pointer;
  text-align: center;
  /* Above the card's whole-card overlay anchor, along with every other
     control — though a locked card has no overlay anchor at all, since it is
     not ours to open yet. Explicit anyway: the day this card becomes clickable
     is the day this line stops being redundant. */
  position: relative;
  z-index: 1;
}
.case-card__lock-button:hover { background: var(--surface-raised); border-color: var(--accent); }
.case-card__lock-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* THE COST, ON THE CONTROL. Every control in this app that reaches the courts'
   servers says so next to itself; this is that rule, on the one press E6 was
   written to make visible. Quiet, because it is a fact and not a warning. */
.case-card__lock-cost {
  font-size: var(--fs-1);
  font-weight: 400;
  color: var(--ink-faint);
  letter-spacing: var(--track);
}
.case-card__lock .row-busy { font-size: var(--fs-1); }

/* A locked card claims no hearing-date state, so it takes no rail HUE — but it
   is not the same as a confirmed one either, and somebody working down six
   near-identical rows should be able to see which they have already asked
   about. A DOTTED rail: unmistakably not one of the three states, carried by
   texture rather than colour, so it survives a black-and-white photocopy and
   a colour-blind reader alike. It is a modifier class and not `:has()`,
   because the office runs whatever Chrome the machine shipped with. */
.case-card--locked { border-left-style: dotted; }

@media (max-width: 380px) {
  /* Same breakpoint as the key band's own stack, so the ghosts and the real
     values change shape at the same width — the locked and revealed cards
     must never be laid out differently at any size. */
  .case-card__lock-ghosts .case-card__key-item--next,
  .case-card__lock-ghosts .case-card__key-item--stage { flex: 1 1 100%; }
  .case-card__key--locked { min-height: 116px; }
}

/* ==========================================================================
   The Database screen's filters, second pass (docs/search-rework.md §F).
   The grid above now carries SEVEN controls, not six -- town and court where
   there was one court select, and "Listed in" + "Case year" where there were
   month and year. `auto-fit` needs no edit for that, which is the whole
   reason it was written that way; the note above it is one filter behind.
   ========================================================================== */

/* `<input type="month">` gets the same treatment as `<input type="date">`
   twelve hundred lines up, for the same reason and it is not cosmetic: the
   native picker indicator is drawn in black, which on this theme's dark field
   is a control that is present, focusable, clickable and INVISIBLE. Anyone who
   cannot see it concludes the field is type-only and never opens the month
   wheel that is the point of choosing this control. */
input[type="month"]::-webkit-calendar-picker-indicator {
  filter: invert(1);
  opacity: 0.7;
  cursor: pointer;
}
input[type="month"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }

/* Firefox has no month input and falls back to a text box, as does the bare-
   year case the template switches to `type="text"` deliberately. Both then
   show the `YYYY-MM` placeholder, and a placeholder is a hint rather than a
   value: it must not be mistaken for one at arm's length in a corridor. This
   is the same faint the rest of the product uses for placeholder text, stated
   here only because the fallback field is the one place a reader meets a
   format hint where every neighbouring control shows a chosen value. */
.db-filters__grid input::placeholder {
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   The portal card, second pass (client, 2026-08-10). Three moves, and each
   one is a control leaving a strip of its own for the line it belongs to.

   Nothing here restyles a control. `+` is still `.row-track` at 44px, `View`
   is still the outlined 44px link the saved band uses, and the card is still
   `.case-card`. What follows is only WHERE they sit.
   ========================================================================== */

/* 1. `+` MOVES UP, ONTO THE HEADLINE ROW, AT THE RIGHT END.
   The top line of the card is its identity -- type, number, year -- and `+`
   answers the question a reader has the instant they have decided this is
   their case. It used to be at the bottom-right of the card, which is a
   distance that varies with the length of a cause title: on a two-line title
   the control was 24px lower than on the card above it, and a tap target that
   moves between adjacent rows is one people aim at rather than reach for.

   Scoped to `--portal`: the Calendar's and the saved band's cards have a
   single child in this row, and `space-between` on a lone flex item does
   nothing -- but scoping says out loud that this is the portal card's layout
   and not a new rule for every case card in the product. */
.case-card--portal .case-card__head {
  justify-content: space-between;
  /* `baseline` was right when this row held only text. It is not right for a
     44px control next to a 24px headline -- baseline-aligning those puts the
     button's own text on the number's baseline and hangs 20px of button below
     the line. */
  align-items: center;
}
/* `margin-left: auto` as well as `space-between`, and the pair is not
   redundant. `.case-card__head` wraps (long transliterated numbers exist), and
   on a wrapped line `space-between` puts a lone item at the START. The auto
   margin is what keeps the control at the right end in that case too. */
.case-card--portal .case-card__head > .row-actions {
  margin: 0 0 0 auto;
  flex: none;
}

/* 2. `View` (was `Open full case`) MOVES INTO THE FOOT LINE, AT ITS RIGHT END.
   The foot is what we know -- tick, status, PREV -- so the way to see more of
   it belongs at the end of that sentence rather than in a strip below it.

   It carries `.case-card__view` as well as `.row-open`, so it is sized, and
   dropped from paper, by the same rules as the saved band's `View`. This rule
   only positions it. `align-self` rather than changing the foot's own
   `align-items: baseline`: every other item on that line is text and should go
   on keeping its baseline; the one 44px control centres itself against them. */
.case-card--portal .case-card__foot .row-open {
  margin-left: auto;
  align-self: center;
}

/* 3. THE GHOST SCREEN IS TWO GHOST CARDS (partials/_results_skeleton.html).
   The bars below are the ones the card shape needs and the table shape did
   not. `--serial`, `--action` and `--party-short` are left where they are:
   they belong to the four-column ghost this replaced, and `--action` in
   particular is `width: 100%` sized to a table cell, which inside a headline
   row would be a full-width block where a 44px square goes. */

/* The `+`, at its real size. Square and 44px, because that is what it is
   standing in for and the ghost's job is to be the right shape. */
.skeleton__bar--plus {
  width: var(--tap);
  height: var(--tap);
  flex: 0 0 auto;
}
/* One bar per key slot, sized like a VALUE (`--fs-3`, 600) rather than like
   body text, so the band is the height the answer's band will be and the card
   does not resize under the finger when the rows land. `max-width: 100%`
   because the next slot is `flex: 1 1 0; min-width: 0` and will be narrower
   than 8.5em on a 390px phone. */
.skeleton__bar--key { width: 8.5em; max-width: 100%; height: 1.1em; }
.skeleton__bar--key-short { width: 5.5em; }
/* The foot line: shorter and thinner than a value, like the muted line it
   stands in for. */
.skeleton__bar--foot { width: 14em; max-width: 100%; height: 0.8em; }

/* 4. UNDO SITS BESIDE `SAVED`, TO ITS LEFT (client, 2026-08-10).
   It was on the as-of line below, at the far end -- so on screen the two
   stacked, Undo above and the flag under it. They are one row now.

   `order: -1` rather than reordering the markup, and this is the load-bearing
   line of the whole change. In the DOM the strip is still [outcome-of-the-
   write, undo]: htmx restores focus to the flag by id after the swap and a
   screen reader then reads "Saved. Undo.", which is the right sentence in
   that order; and `tests/test_confirm_gate_contract.py` reads slot 1 as the
   FIRST child of this strip, which is the only way a markup test can assert
   "the outcome of the write is in the write's position". On screen the client
   wants Undo first. Nothing anyone OPERATES depends on which of the two
   orders they get: the flag is `tabindex="-1"`, so exactly one of the pair is
   in the tab order.

   WHY LEFT AND NEVER RIGHT: the right end is where `+` was. Press it and that
   exact position becomes the inert flag, so a double-tap in a corridor lands
   on something that cannot act. A removal in that position would mean the
   tap that saves and the tap that unsaves are the same pixel -- ui-spec.md
   2.2's fat-finger failure, and ui-spec.md 5's "no destructive control where
   a finger expects a read", both at once. */
.case-card--portal .row-actions--card .row-action--undo {
  order: -1;
  margin: 0;
  flex: none;
}
/* THE TWO TARGETS MAY NOT TOUCH. The strip's own column gap is --sp-3 (12px),
   already more than the --sp-2 floor; these two lines are what stop a future
   padding change from eating it, and what keeps both controls at 44px:
   `.link-button` drops `min-width` and `.row-actions--card .row-flag--saved`
   drops the flag's padding, so each is restated here at the size a thumb
   needs. They stay visibly different KINDS of thing -- an underlined
   link-button against a bordered, uppercase flag -- which is the other half
   of not confusing one for the other at arm's length. */
.case-card--portal .row-actions--card { column-gap: var(--sp-3); }
.case-card--portal .row-actions--card .row-action--undo .link-button {
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
}
.case-card--portal .row-actions--card .row-flag--saved {
  min-height: var(--tap);
  padding: 0 var(--sp-2);
}

/* The town disclosure inside the month's day panel. The panel is a 320px
   column, so the heading drops to the body size the panel uses elsewhere and
   the indent is halved — a full 12px step there is a twelfth of the column. */
.place-group--panel { margin: 0; }
.place-group--panel .place-group__summary { margin: var(--sp-3) var(--sp-3) var(--sp-2); }
.place-group--panel .place-group__heading { font-size: var(--fs-2); }
.place-group--panel .place-group__body { padding-left: var(--sp-2); }

/* ==========================================================================
   2026-08-10 — the client's flow batch. Appended; nothing above is edited.
   ========================================================================== */

/* ---- §C: one container, form then answer ---------------------------------
   The portal's rows now render INSIDE the form's container, directly beneath
   the Search button (client, 2026-08-10). `.search-panel` wraps the <form>
   and `#results` and takes over the panel chrome; the form keeps its class
   (the `:has()` scope toggle above is written against it) and loses its box,
   so nothing is drawn twice and `.saved-band`'s full-bleed negative margins
   land on the same edge they always did — the padding simply moved out one
   element.

   `#results` may NEVER become a child of the <form>: every portal card is
   three forms, and nested forms are dropped by the parser with their controls
   reparented onto the OUTER form, which on this screen spends a captcha. The
   container is shared; the form element is not.

   @media screen, and that is deliberate rather than tidy. print.css already
   flattens `form.search-form` for paper and knows nothing about this wrapper;
   scoping the new chrome to the screen means the printed sheet is byte-for-
   byte what it was before this change, with no override to keep in step. */
@media screen {
  .search-panel {
    background: var(--surface);
    border: 1px solid var(--line);
    padding: var(--sp-4) var(--sp-4) var(--sp-5);
    margin-bottom: var(--sp-5);
  }
  .search-panel > .search-form {
    background: none;
    border: 0;
    padding: 0;
    margin-bottom: 0;
  }

  /* The answer is a RECESS in the panel, the same one step down `.saved-band`
     takes — and for the same concrete reason: `.case-card` is --surface and so
     is the panel, so cards dropped straight in would be invisible against it.
     Full-bleed to the panel's padding on three sides, so it reads as the
     drawer at the bottom of the container rather than as a box inside a box.

     ONLY WHEN THERE IS SOMETHING IN IT. Before a search `#results` holds an
     empty `.results-answer` and a hidden ghost; a recess around that would be
     a grey strip under the button, permanently, saying nothing. `:has()` is
     already load-bearing on this screen (the scope toggle above); where it is
     missing the results still render, just without the inset — the degraded
     state is "two blocks", which is where this screen started. */
  .search-panel > #results.htmx-request,
  .search-panel > #results:has(.results-answer > *) {
    display: block;
    background: var(--bg);
    border-top: 1px solid var(--line);
    margin: var(--sp-5) calc(var(--sp-4) * -1) calc(var(--sp-5) * -1);
    padding: var(--sp-4);
  }
}

/* ---- §C2: the saved list folds, and the fold is a summary -----------------
   Shut, this is one line that still says what is up there — "Already saved
   here — 2 cases numbered 401/2024 in Belur — show". Folding it to nothing
   would hide the fact that we hold the case at all, which is the single most
   useful thing this screen knows.

   The whole summary row is the target, not the triangle: this is read and
   tapped one-handed at arm's length, so the row carries the 44px minimum. */
.saved-fold { display: block; }
.saved-fold__summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  cursor: pointer;
  list-style: none;
}
.saved-fold__summary::-webkit-details-marker { display: none; }
.saved-fold__summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* The same triangle the day list's court groups use, drawn the same way, so
   the two disclosures are one vocabulary rather than two. */
.saved-fold__marker {
  flex: 0 0 auto;
  width: 0;
  height: 0;
  border-left: 5px solid var(--ink-faint);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 0.12s ease;
}
.saved-fold[open] > .saved-fold__summary > .saved-fold__marker { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
  .saved-fold__marker { transition: none; }
}
.saved-fold__heading { flex: 1 1 auto; min-width: 0; margin: 0; }
/* "show" / "hide" — the word for the direction it will go. One of the two is
   rendered at a time; both ship, because a fold that has to ask the server
   which word to draw is a fold that cannot be operated offline. */
.saved-fold__toggle {
  flex: 0 0 auto;
  font-size: var(--fs-1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-muted);
}
.saved-fold__hide { display: none; }
.saved-fold[open] > .saved-fold__summary > .saved-fold__toggle > .saved-fold__show { display: none; }
.saved-fold[open] > .saved-fold__summary > .saved-fold__toggle > .saved-fold__hide { display: inline; }
.saved-fold__body { padding-top: var(--sp-2); }
/* The band's town/court hierarchy is the day list's, so it inherits every
   rule those two partials already have. It only needs to stop shouting: this
   is a strip inside a form, not the page's own heading structure. */
.saved-band .place-group__heading { font-size: var(--fs-3); }
.saved-band .place-group__body { margin-left: 0; }
.saved-band .case-group { margin-top: var(--sp-3); }

/* ---- §3.4: the order-sheet control sits on its heading's line -------------
   It used to sit under the heading, pushing the first row of the diary ~90px
   down on every tracked case whether or not anybody was going to press it.
   Wraps back underneath below 640px, because a right-hand control on a 390px
   phone is a control squeezed into a third of the width. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
}
.section-head > .section-bar { flex: 1 1 18em; min-width: 0; }
.section-head > .fetch-sheets { flex: 0 1 auto; text-align: right; }
.section-head > .fetch-sheets .fetch-sheets__cost { margin-left: auto; }
@media (max-width: 640px) {
  .section-head > .fetch-sheets { flex: 1 1 100%; text-align: left; }
  .section-head > .fetch-sheets .fetch-sheets__cost { margin-left: 0; }
}

/* ---- the case page's header row, and the `+` in it ------------------------
   Where you came from on the left, the one write on the right (client,
   2026-08-10). The button is a second SUBMIT for the single `#track-form` at
   the foot of the page — not a second form — so the two cannot disagree about
   whether the case is saved; see partials/_case_body.html. */
.case-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
}
.case-head__back { margin: 0; }
.case-head__track {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  white-space: nowrap;
}
.case-head__track-glyph { font-size: var(--fs-4); line-height: 1; }

/* ---- the case page's ghost (client, 2026-08-10) ---------------------------
   Bars where the real blocks go, so the page does not jump when the answer
   lands. `.skeleton__bar` is reused literally — the bar is the bar — but the
   ghost's ROOT is deliberately not `.skeleton`, which is display:none except
   inside a busy `#results`. */
.case-ghost__wait { margin-top: var(--sp-3); }
.case-ghost__bars { margin-top: var(--sp-5); }
.case-ghost__bars > .skeleton__bar { margin-bottom: var(--sp-3); }
.case-ghost__bar--court { height: 1.3em; width: 24em; max-width: 80%; }
.case-ghost__bar--number { width: 12em; max-width: 60%; }
.case-ghost__bar--title { height: 1.2em; width: 32em; max-width: 95%; }
.case-ghost__section { margin-top: var(--sp-5); }
.case-ghost__head { padding: var(--sp-2) 0; }
.case-ghost__bar--head { height: 0.9em; width: 10em; max-width: 50%; }
.case-ghost__bar--short { width: 60%; }
@media print {
  /* A ghost on paper is the worst artefact this product could produce: rows
     of grey bars a reader could take for redacted or pending facts. print.css
     says exactly this about the search skeleton; the same rule, for the same
     reason, one screen over. The status line above them PRINTS, so a sheet
     pulled at that moment says why it is empty rather than being empty. */
  .case-ghost__bars { display: none !important; }
}

/* ==========================================================================
   Settings — the sync-failure alarm (client, 2026-08-10).

   Almost nothing new: it is `.banner.banner-error`, the vocabulary this page
   already uses for a fault, and the reason it can be that is that the banner
   it replaces (the quieter one that used to sit inside "The sync") is gone
   rather than duplicated. What is added here is the weight it needs at the
   top of a page — space beneath it, and a phone number that is a real tap
   target rather than a link in a sentence.

   The hue is --state-stale and not a new red. This product spends its entire
   colour budget on three case states; an alarm about the sync failing IS the
   stale state, stated at the level of the whole docket rather than one row.
   ========================================================================== */
.sync-alarm { margin: 0 0 var(--sp-5); }
.sync-alarm__head { margin: 0; font-size: var(--fs-3); }
.sync-alarm__contact { margin: var(--sp-2) 0 0; font-size: var(--fs-3); }
/* Read at arm's length and dialled one-handed in a corridor: the full 44px
   target, and the negative margin keeps the enlarged hit area from indenting
   the number away from the sentence it belongs to. `inline-flex` rather than
   `block` so it stays inside the line of prose on desktop. */
.sync-alarm__phone {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--sp-2);
  margin: 0 calc(var(--sp-2) * -1);
  color: inherit;              /* the banner's own state hue — 7.6:1 on surface */
  font-weight: 600;
  white-space: nowrap;         /* a phone number broken across two lines is not one */
}

/* The "left behind" list under Re-check from a date. A MODIFIER on the list
   that already exists, not a second row style: it states the same kind of
   fact as "Cases the sync cannot read" and must not look like a different
   kind of thing. The one difference is which fact leads — there, it is what
   broke; here, it is how long it has been — so the age steps up from a
   footnote to body text and stops being --ink-faint. */
.failing-list--behind .failing-row__asof {
  color: var(--ink-muted);
  font-size: var(--fs-2);
}

/* The day stepper's two chevrons (client, 2026-08-10: "make the buttons right
   and left chevron, nothing else"). Square, so the pair reads as one control
   rather than two buttons that happen to be adjacent, and still 44px because
   this is the control the office presses most on a phone. */
.daynav__step--icon {
  width: var(--tap);
  min-width: var(--tap);
  padding: 0;
  justify-content: center;
}
.daynav__step--icon .daynav__chev { font-size: var(--fs-4); color: var(--ink); }
@media (max-width: 560px) {
  /* Overrides the `flex: 1 1 40%` the wrapped stepper gives its steps: two
     40% chevrons on a phone would be two enormous empty boxes. */
  .daynav__step--icon { flex: 0 0 var(--tap); }
}

/* ==========================================================================
   The day's filters, 2026-08-10 — OUT OF THE DISCLOSURE (§1.6).

   Client: *"in the today's page let's have the filter like in the DB page,
   instead of that dropdown that opens to something-something options"*. So the
   day screen now wears the Database screen's control: the box is `.db-filters`,
   the grid is `.db-filters__grid`, each field is a `.form-field` and the foot is
   `.db-filters__foot`. Only what genuinely differs is restated below.

   ⚠ THE RULES ABOVE AT ~639–715 ARE NOW PART DEAD. `.filters__summary`,
   `.filters__marker` and `.filters__set` styled a <summary> that no longer
   exists; this file is append-only while several agents are editing it, so they
   are named here for whoever next has the file to themselves. `.filters`,
   `.filters__form`, `.filters__field--wide`, `.filters__ignored` and
   `.filters__clear` are all still live.

   WHAT THE DISCLOSURE WAS BUYING, AND HOW THIS PAYS FOR IT
   All measured in headless Chrome at 390x844 against the dev database's
   busiest day, because a fold claim is worth nothing unstated:

     this grid, unfiltered   174px + 24px margin = 198px
     this grid, filtered     230px + 24px        = 254px
     the old <details>, closed  46px + 24px      =  70px
     the old <details>, OPEN   353px + 24px      = 377px

   The old comment here estimated four stacked selects at ~140px. It was 2.5x
   low. So this costs 128px more than the closed disclosure and 123px LESS than
   the open one — and open is what a filtered day showed, since open-ness was
   derived from the URL.

   Unfiltered, the list starts at y=461 and the first town cover is fully on
   screen; filtered, the first case card's top is at y=648, 196px above the
   fold. That is the whole budget for this region: three selects, two rows, no
   permanently-visible Apply button. A fourth select is ~66px of it and a
   visible Apply button ~60px.
   ========================================================================== */

/* The form IS the panel now, so it takes the Database box whole. Stated at
   0,2,0 rather than left to source order: `.filters__form` above still carries
   the disclosure-era `padding: var(--sp-3)` and a `border-top`, which would
   otherwise draw a stray rule across the top of the grid. */
.filters.db-filters {
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  border: 1px solid var(--line);
}

/* TWO COLUMNS, NOT `auto-fit`, below 1240px — and this is the one place the two
   screens' grids legitimately differ.

   `auto-fit, minmax(190px, 1fr)` collapses to ONE column at 390px — two tracks
   would need 392px and the panel's interior measures 293px — which would stack
   all three fields: a 222px grid instead of the 144px two rows give.

   The Court field still spans both, exactly as it did in the disclosure: in a
   140.5px cell "CIVIL JUDGE AND JMFC, BELUR (4)" clips to "…BELUR (4", and a
   count cut off mid-bracket is a wrong number on a screen whose counts are the
   point. Case type and Stage are short and stay two-up. */
.filters__grid.db-filters__grid { grid-template-columns: 1fr 1fr; }
/* `1fr` is `minmax(auto, 1fr)`, and a <select>'s min-content width is set by
   its LONGEST OPTION — a court name would push the track wider than the panel
   and hand the whole day a horizontal scrollbar. */
.filters__grid .form-field { min-width: 0; }

/* One row of three, but only once each column can hold a court name. Measured
   at exactly 1240px: the side nav has taken 232px, the panel's interior is
   911px and the three columns come out at 295.7px each — the width at which the
   longest real establishment name and its count both fit. Below that, two rows
   read better than three clipped labels. */
@media (min-width: 1240px) {
  .filters__grid.db-filters__grid { grid-template-columns: repeat(3, 1fr); }
  .filters__grid .filters__field--wide { grid-column: auto; }
}

/* The foot carries its own spacing so that an EMPTY foot costs nothing. With
   htmx on and nothing filtered its only child is the <noscript> submit, which
   is display:none — and 12px of dead margin under the grid on the screen with
   the tightest vertical budget in the product is not nothing. */
.filters .db-filters__foot { margin-top: 0; }
.filters .db-filters__foot > * { margin-top: var(--sp-3); }

/* ---- "Not being tracked", with the way back (2026-08-10) ----------------
   The case page's own restore control. Until this existed, the ONLY button
   posting to /case/<cnr>/restore lived in the Dashboard's undo banner, which
   survives one screen -- so a soft delete the product calls reversible was one
   navigation from permanent, and an auto-archived disposed case never had the
   button at all.

   Same `.banner` shell as its neighbours, so it reads as one of the page's
   status lines rather than as an interruption; the rail is `--state-stale`
   because "this stopped being maintained" is the same class of fact as an
   unconfirmed date, and the office already reads that colour that way.
   Row layout, wrapping to two lines on a phone with the button beneath the
   sentence that explains it. */
.untracked-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-3);
  border-left-color: var(--state-stale);
  background: var(--surface-raised);
}
.untracked-banner__text { flex: 1 1 22rem; }
.untracked-banner form { margin: 0; }
.untracked-banner__resume { white-space: nowrap; }

/* ON PAPER it is forced, for the same reason as the stale note (§3.9): a
   printed sheet carried into court must not present a case as current when
   nothing has refreshed it since the day it left the rotation. The button is
   not a fact, so it goes. */
@media print {
  .untracked-banner form { display: none; }
}

/* ==========================================================================
   The day's filters, 2026-08-10 (second pass) — BACK INTO A DISCLOSURE, and
   it is the Database screen's WHOLE block now (§1.6).

   Client: *"i want this whole thing on the today page ... let's have the whole
   thing there inside a dropdown with the dropdown default to closed, and if
   required they will click on it and have options"*. "This whole thing" is the
   Database screen's filter block entire — a search box, six selects and the
   Case year footnote.

   ⚠ THIS REVERSES THE BLOCK ABOVE (~3748), WHICH IS LEFT IN PLACE BECAUSE ITS
   MEASUREMENTS ARE REAL AND ARE WHAT THIS IS MEASURED AGAINST. That block took
   the disclosure OUT, at the same client's word, earlier the same day. Both
   instructions are consistent: the first was about THREE selects, where a
   wrapper is furniture around something small; this one is about SEVEN
   controls and a footnote, where the wrapper is what makes the block
   affordable at all on the screen that must not have a fold problem.

   The note there that `.filters__summary`, `.filters__marker` and
   `.filters__set` are "part dead" IS SUPERSEDED — all three are live again,
   and they are why the revival cost so little markup. `.filters`,
   `.filters__form`, `.filters__grid`, `.filters__field--wide`,
   `.filters__ignored` and `.filters__clear` were live throughout.

   MEASURED, headless Chrome at 390 x 844, inside a 390px IFRAME — a
   `--window-size=390` window clamps to ~500px on Windows and would have
   flattered every figure. Dev database, 11-08-2026 (23 hearings, 8 courts).
   The full table is in the header comment of
   `templates/partials/_day_filters.html`; the three numbers that decide the
   shape are:

     this block, closed (the daily read)   46px + 24px margin =  70px
     this block, open                     501px + 24px        = 525px
     the always-open grid it replaces     174px + 24px        = 198px

   So the daily read is 128px CHEAPER than the always-open grid, and the tall
   state is reached only deliberately or when a filter really is applied. At
   ≥1240px the six selects go three across and the open block is 327px.
   ========================================================================== */

/* The FORM is the box and the DISCLOSURE is its only child, so the box's own
   padding has to go: a closed panel must be a flush 44px line inside the box's
   own 1px border — 46px total — not a 44px line inside 28px of padding.
   Stated at 0,2,0 and after the earlier `.filters.db-filters` rule, which set
   the always-open grid's padding. */
.filters.db-filters { padding: 0; }
.filters__disclosure { margin: 0; }

/* The summary is the whole control when the block is shut, so it is a full-
   width 44px tap target — and it WRAPS, because what it carries is not
   "Filters" but "Filters · 2 applied — Town: Belur, Stage: Evidence". A
   confession that overflows its line is a confession nobody reads. */
.filters__summary {
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
}
.filters__title { flex: none; }
/* The applied filters, in their own words: NOT uppercased (a typed search term
   shouted back is not what was typed) and normal weight, so the count and the
   values read as content against the micro-label voice around them. */
.filters__set {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  font-size: var(--fs-2);
}
/* The `open` bit moved off `.filters` — the form carries that class now and the
   <details> is inside it. Without this the marker keeps its `+` while the panel
   is open. */
.filters__disclosure[open] .filters__marker::after { display: none; }

/* The panel's own padding, so it costs nothing while shut, and the rule that
   separates it from the summary. */
.filters__panel {
  border-top: 1px solid var(--line);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
}
/* `form label`'s 16px top margin would push the search box away from the rule
   above it; every other field in here is a `.form-field`, which already zeroes
   it. Scoped to the day block so the Database screen's own spacing is
   untouched. */
.filters .db-filters__search label { margin-top: 0; }

/* TOWN AND COURT EACH SPAN BOTH COLUMNS below 1240px (`.filters__field--wide`,
   declared above). Court for the reason it always had — in a 140.5px half cell
   "CIVIL JUDGE AND JMFC, BELUR (4)" clips to "…BELUR (4", and a count cut off
   mid-bracket is a wrong number on a screen whose counts are the whole point.
   Town for a different one: six fields where one spans is seven cells in a
   two-column grid, i.e. four rows WITH A HOLE in them, and the hole lands
   between the town and the court it narrows — the two controls that have to
   read as one question. Two spans is seven-plus-one cells, four rows, no hole,
   and the hierarchy reads straight down. Same row count either way.
   At ≥1240px the media query above drops both spans and the six go three
   across in two rows. */

/* ⚠ THE HALF CELL HAS TO HOLD "All statuses (23)", AND AT 12px GAP / 32px
   CHEVRON RESERVE IT DID NOT — caught by screenshotting the open panel at
   390px, not by reading it. Measured against the real IBM Plex metrics: the
   cell was 148px, the text budget 102px after 12px of left padding and 32px
   reserved for the drawn chevron, and the string is 111px. It rendered as
   "All statuses (2" with the 3 under the arrow: a count cut off mid-bracket,
   which on a screen whose counts are the whole point is a WRONG NUMBER, not a
   cosmetic clip. It is the same failure `FILTER_ALL_WORDS` already shortened
   "All case types" to "All types" for.

   Two changes buy the 12px, and neither costs a row:

     * an 8px COLUMN gap (rows keep 12px — the vertical rhythm is what makes
       label/control pairs read as pairs, and nothing is tight there): 150px
       cells;
     * a 24px chevron reserve instead of 32px. The glyph is drawn from
       `calc(100% - 18px)` and is 12px wide, so it ends 6px from the edge; 24px
       leaves a 6px gap before it rather than 14px.

   Budget after: 114px against a 111px longest string. `text-overflow` is the
   belt-and-braces half — if the webfont ever fails to load and the fallback
   runs wide, the reader gets an ellipsis, which reads as "truncated" rather
   than as a number. Applied to the whole grid, so long portal STAGE strings
   truncate visibly too (a stage clipping its tail is accepted — the
   identifying part comes first — but it must not look like a complete word).
   Not applied to the Database screen's own grid, whose cells are 190px+ and
   which has no two-up row at 390px at all. */
.filters__grid.db-filters__grid { column-gap: var(--sp-2); }
.filters__grid select {
  padding-right: 24px;
  background-position: calc(100% - 16px) calc(50% + 2px), calc(100% - 10px) calc(50% + 2px);
  text-overflow: ellipsis;
}

/* ...and 8px more of it on the phone, where the two-up row is. With the fix
   above, "All statuses (23)" measured 111px against a 112px budget: correct,
   and one pixel is not a margin anyone should have to trust. A 12px inset
   instead of 16px below 560px gives 116px, so the longest zero-option clears
   the drawn chevron by 5px instead of 1px — and every wide field, including
   the court names, gets the same 8px. Above 560px the grid is not two-up and
   the Database screen's own 16px inset stands. */
@media (max-width: 560px) {
  .filters__panel { padding-left: var(--sp-3); padding-right: var(--sp-3); }
}

/* ==========================================================================
   Settings — the health panel and the backup nudge (client, 2026-08-11).

   "lets have a health page ... shows the health of web app db and worker ...
   and show the disk space too".

   No new colours and no new components. The four state hues this product owns
   are already the right vocabulary: a check that is DOWN is the same class of
   fact as a hearing date nothing has confirmed (--state-stale), a check that
   is merely filling up is the same class of fact as a case awaiting a date
   (--state-await), and OK is achromatic on purpose — at four rows, three of
   which are almost always fine, giving "fine" a colour would spend the
   reader's attention on the rows that do not need it. That is the same
   argument the case-state palette makes in this file's header, one screen
   over.
   ========================================================================== */
.health-list { list-style: none; margin: var(--sp-4) 0 var(--sp-2); padding: 0; }

/* A GRID, NOT A TABLE. Four fixed rows whose cells are label / state / since
   / detail — the label column is sized to the longest of four known strings
   ("This application"), so the state words line up down the page and a reader
   scanning for the odd one out is scanning a column rather than prose.
   Below 720px it stacks: at 390px "Sync worker  Stopped  since 09-08-2026,
   03:12" cannot share a line with anything and truncating a since-date is
   losing the one fact the row exists for. */
.health-row {
  display: grid;
  grid-template-columns: 10px minmax(9rem, auto) 1fr;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-3);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--line);
}
.health-row:last-child { border-bottom: 0; }

/* The dot is DECORATIVE (aria-hidden in the markup) and the state word beside
   it carries the meaning — the same rule the nav badge follows, for the same
   reason: this is read at arm's length, on office machines, and a hue alone
   says something is wrong without saying what. */
.health-row__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--line-strong);
  align-self: center;
}
.health-row__label { color: var(--ink-muted); font-size: var(--fs-2); }
.health-row__state { color: var(--ink); font-size: var(--fs-2); font-weight: 600; }
/* The two trailing facts start in the state column and wrap under it, so the
   label column stays a clean edge no matter how long the numbers get. */
.health-row__since,
.health-row__detail {
  grid-column: 3;
  color: var(--ink-muted);
  font-size: var(--fs-1);
}
/* The since-date is the sentence the phone call is made of, so it is the one
   secondary line that is not faint. */
.health-row__since { color: var(--state-stale); font-weight: 600; }

.health-row--ok .health-row__dot { background: var(--line-strong); }
.health-row--warn .health-row__dot { background: var(--state-await); }
.health-row--warn .health-row__state { color: var(--state-await); }
.health-row--down .health-row__dot { background: var(--state-stale); }
.health-row--down .health-row__state { color: var(--state-stale); }
/* "Unknown" is a third thing and must not read as either — a worker that has
   never reported in on a fresh install is not broken, and colouring it like a
   fault is exactly the day-one scream services/ops.py refuses to make. */
.health-row--unknown .health-row__dot { background: transparent; border: 1px solid var(--line-strong); }
.health-row--unknown .health-row__state { color: var(--ink-muted); font-weight: 500; }

@media (max-width: 720px) {
  .health-row { grid-template-columns: 10px 1fr; }
  .health-row__since,
  .health-row__detail { grid-column: 2; }
}

/* The backup nudge. `.banner.banner-info` doing the work, plus a row that
   keeps the two controls beside the sentence on a desktop and stacks them
   under it on a phone. It is not an alarm and must not grow into one. */
.backup-nudge {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  margin: var(--sp-4) 0 var(--sp-5);
}
/* `22rem` rather than `100%`: the sentence takes the whole line only when
   there is not room for the buttons beside it. */
.backup-nudge__text { flex: 1 1 22rem; margin: 0; }
.backup-nudge__go { white-space: nowrap; }
.backup-nudge__dismiss { white-space: nowrap; }

/* ==========================================================================
   A PAST DAY — the two dates that bracket it, and what the court wrote
   (client, 2026-08-11; services/dashboard.past_day_facts)

   Rendered only when the day on screen is before today. Every other screen
   that uses `.case-card` — today, a future day, /find, the Database rows, the
   saved band — is untouched, because the markup below is not emitted at all.
   ========================================================================== */

/* ONE RULED BLOCK, NOT TWO. This band sits directly above `.case-card__key`,
   and if both kept their own top rule the card would carry three hairlines in
   14mm — the kind of stripe that reads as a rendering fault rather than as
   structure. So the past band takes the rule and the key band gives its top
   one up whenever it is preceded by one; what the eye sees is a single ruled
   block holding all four fields in the client's own order:

       PREV  NEXT ALLOTTED
       UPCOMING            STAGE

   Quieter than the key band on purpose (--fs-2 against --fs-3, --ink-muted
   against --ink). These two dates are CONTEXT for the day being looked at;
   the key band is still the answer to "where does this case stand now", which
   is the question that can cost a hearing. */
.case-card__past {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-1) var(--sp-5);
  margin: var(--sp-2) 0 0;
  padding: var(--sp-2) 0 0;
  border-top: 1px solid var(--line);
}
.case-card__past + .case-card__key {
  margin-top: var(--sp-1);
  padding-top: var(--sp-1);
  border-top: 0;
}
.case-card__past-item {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.case-card__past-value {
  font-size: var(--fs-2);
  font-weight: 500;
  color: var(--ink-muted);
}

/* THE ORDER SHEET — a quotation, and it has to LOOK like one.
   A left rule and the muted voice say "these are not our words"; the label
   above says whose they are. `pre-wrap` because newlines are content: "Case
   called. / For issues by 23.09.2026." is two lines in the original and stays
   two. No clamp — a clipped order sheet cannot be checked against the
   physical file, and this row has already spent its one deviation from
   ui-spec.md §0.2 on the parties line. `overflow-wrap` so a long unbroken
   token cannot widen the card and reintroduce horizontal scrolling. */
.case-card__sheet {
  margin: var(--sp-2) 0 0;
  padding: 0 0 0 var(--sp-3);
  border-left: 2px solid var(--line);
  color: var(--ink-muted);
  font-size: var(--fs-2);
  overflow-wrap: anywhere;
}
.case-card__sheet .case-card__key-label { display: block; }
.case-card__sheet-text { display: block; white-space: pre-wrap; }
