/*
 * LTFRB Online Appointment System
 *
 * Hand-written, served directly. No build step, no bundler, no CDN — which
 * also means script-src/style-src can stay locked to 'self'.
 *
 * PALETTE — sampled from the agency seal, which uses the Philippine national
 * flag colours. Contrast measured, not assumed:
 *   flag blue   on white  9.85:1  AAA
 *   white       on blue   9.85:1  AAA
 *   deep blue   on white 12.82:1  AAA
 *   flag yellow on blue   6.70:1  AA
 *   flag yellow on white  1.47:1  FAILS -> accent only, never text
 */

:root {
  --blue: #0038a8;
  --blue-deep: #002a7f;
  --blue-dark: #001c55;
  --blue-tint: #e8eefb;
  --gold: #fcd116;
  --gold-tint: #fff8dc;

  --canvas: #f5f7fb;
  --surface: #fff;
  --sunken: #eef2f9;
  --hairline: #d8e0ee;

  --ink: #14181f;
  --ink-muted: #4a5568;
  --ink-subtle: #6b7688;

  --success: #0f7b3d;  --success-tint: #e6f4ec;
  --warning: #8a5a00;  --warning-tint: #fdf3e0;
  --danger:  #b3261e;  --danger-deep: #8f1e18;  --danger-tint:  #fce9e8;

  --radius: 6px;
  --shadow: 0 1px 3px rgb(0 40 120 / 10%);
  --font: "Segoe UI", system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
}

*, *::before, *::after { box-sizing: border-box; }
body, h1, h2, h3, h4, p, figure, ul, ol, dl, dd { margin: 0; }
ul[class], ol[class] { list-style: none; padding: 0; }

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

body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
}

/* Headings are tightened and given a touch of negative tracking: at 24px the
   default spacing reads loose against the body copy. */
h1, h2, h3 { line-height: 1.25; letter-spacing: -.01em; color: var(--ink); }

/* Body links stay underlined for accessibility — this is a government service
   and links must be identifiable without relying on colour (WCAG 1.4.1). The
   offset stops the rule cutting through descenders. */
a { color: var(--blue); text-underline-offset: 2px; }
a:hover { color: var(--blue-deep); }

/*
 * A button that reads as a link, for actions that must not be GETs.
 *
 * Sign-out is the case: it has to be a POST, because a GET logout can be
 * fired by any image tag or link prefetch on another site. But it sits mid
 * sentence next to real links, so a button that looks like a button would
 * read as a different kind of thing than it is.
 *
 * Kept as a <button> rather than an anchor with a click handler so it works
 * with JavaScript off, and so the CSP's ban on inline handlers cannot break
 * it - which is exactly how the previous version failed.
 */
.link-button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--blue);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.link-button:hover { color: var(--blue-deep); }

/* Respect the OS setting — motion is never load-bearing here. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Focus is gold on light surfaces, white on blue: visible both ways.
   Never remove without a replacement. */
:focus-visible { outline: 3px solid var(--gold); outline-offset: 2px; border-radius: 2px; }
.on-brand :focus-visible { outline-color: #fff; }
@media (forced-colors: active) { :focus-visible { outline: 3px solid CanvasText; } }

/* Visually hidden but announced by screen readers - for headings that are
   obvious sighted but unlabelled otherwise (e.g. an actions column). */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px;
  background: var(--blue-deep); color: #fff;
  padding: .65rem 1rem; border-radius: 0 0 var(--radius) 0; z-index: 100;
}
.skip-link:focus { left: 0; top: 0; }

/* ---------- Layout ---------- */
.wrap { width: 100%; max-width: 1120px; margin-inline: auto; padding-inline: 20px; }
.main { flex: 1; padding-block: 40px; }
.stack > * + * { margin-top: 1rem; }
.row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.grid { display: grid; gap: 16px; }
@media (min-width: 640px)  { .grid-2, .grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 960px)  { .grid-3 { grid-template-columns: repeat(3, 1fr); } }

/* A row whose item count is not known when the template is written.

   The day screen shows one tile per SESSION plus a summary, and the number of
   sessions is configuration - it was two, it is three, and an office may open
   fewer than all of them. Any fixed column count orphans a tile at some count:
   grid-3 was exact at two sessions and left one card alone on a second row the
   moment there were three. auto-fit sizes to whatever arrives. */
.grid-auto { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

/* ---------- Masthead ---------- */
.masthead { background: var(--surface); }
.masthead .inner { display: flex; align-items: center; gap: 16px; padding-block: 14px; flex-wrap: nowrap; }
/* The brand is the flexible element: it may shrink so the nav never wraps.
   min-width:0 is required or a flex item refuses to shrink below its content. */
.masthead a.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; min-width: 0; flex: 0 1 auto; }
.seal { width: 46px; height: 46px; aspect-ratio: 1/1; object-fit: contain; flex: none; }
.agency { font-weight: 700; font-size: 15px; color: var(--blue); line-height: 1.25; min-width: 0; }
.agency small { display: block; font-weight: 400; font-size: 11.5px; color: var(--ink-muted); }

/* The agency name occupies ONE line, always.
   ------------------------------------------
   It used to be allowed to wrap (`overflow-wrap: anywhere`), on the reasoning
   that breaking was better than overflowing the viewport. In practice it broke
   on a laptop, not a phone: the name took two lines, pushed the strapline down,
   and made the masthead taller than the nav sitting beside it - so the bar
   changed height depending on which page you were on, which reads as a
   rendering fault rather than a design.

   Wrapping is not the only way to avoid overflow. Below, the full name is
   swapped for the abbreviation at the width where it stops fitting, so the
   masthead keeps a fixed height at every size and nothing is ever truncated
   mid-word. */
.agency-full,
.agency-short { display: block; white-space: nowrap; }

/* Wide screens show the full name; the abbreviation is not rendered at all,
   so it cannot be picked up by find-in-page or read out twice. */
.agency-short { display: none; }

/* The one signature device: blue bar broken by gold, echoing the seal. */
.brand-rule {
  height: 4px;
  background: linear-gradient(to right, var(--blue) 0 62%, var(--gold) 62% 100%);
}

/* The nav holds its line: flex:none stops it shrinking, nowrap stops items
   dropping to a second row. Pressure is absorbed by the brand above. */
.nav { margin-left: auto; display: flex; align-items: center; gap: 16px; flex-wrap: nowrap; flex: none; }
/* The sign-out form is a flex item; without this it adds margin and its own
   line box, which is what pushed the button onto a second row. */
.nav form { margin: 0; display: flex; align-items: center; }
.nav a { color: var(--ink-muted); text-decoration: none; font-size: 14px; white-space: nowrap; }
.nav a:hover { color: var(--blue); text-decoration: underline; }

/* A button inside the nav is a button, not a link.
   `.nav a` (0,1,1) outranks `.btn-primary` (0,1,0), so without this the
   button rendered muted grey on blue - about 2:1 contrast, unreadable - and
   underlined itself on hover. Matching specificity restores the button. */
.nav a.btn { text-decoration: none; }
/* These two now duplicate the base .btn rules, which state their colour in
 * every state. They were the original local fix for anchors picking up
 * `a:hover { color: var(--blue-deep) }` and turning their own labels
 * invisible; the same defect was live everywhere else on the site until the
 * base rules were corrected. Kept because they are harmless and identical -
 * safe to delete whenever someone is tidying, not worth a risk on its own. */
.nav a.btn-primary,
.nav a.btn-primary:hover { color: #fff; text-decoration: none; }
.nav a.btn-secondary,
.nav a.btn-secondary:hover { color: var(--blue); text-decoration: none; }
/* Six nav items, two buttons and the full agency name cannot share one line
   on a laptop. Rather than wrapping, shed weight in stages. */
@media (max-width: 1280px) {
  .nav { gap: 13px; }
  .nav a { font-size: 13.5px; }
  .agency { font-size: 14px; }
}

/* The width at which the full name can no longer share a line with the nav.
   Swapping to the abbreviation is what keeps it on one line; it is the first
   thing to give, because "LTFRB" is what citizens call the agency anyway and
   the full name is still in the logo's alt text and in the footer.

   1024px, not higher: consolidating "Verify appointment" and "Cancel a
   booking" into one "My booking" entry gave the brand back roughly 260px, so
   the full name comfortably holds its line well below the old 1280px squeeze
   point. Swapping earlier would hide the agency's own name while there was
   still room to show it.

   Below 900px the nav collapses behind the Menu button and the brand has the
   bar to itself, so no further step is needed. */
@media (max-width: 1024px) and (min-width: 901px) {
  .agency-full  { display: none; }
  .agency-short { display: block; font-size: 17px; letter-spacing: .02em; }
}

/* Phones: the nav is behind the toggle, but the full name still cannot share
   a line with the seal and the Menu button on a 360px screen. */
@media (max-width: 620px) {
  .agency-full  { display: none; }
  .agency-short { display: block; font-size: 17px; letter-spacing: .02em; }
}

@media (max-width: 1080px) {
  /* The strapline is the least load-bearing text in the masthead. */
  .agency small { display: none; }
  .nav .btn { padding: 8px 14px; font-size: 13px; }
}

/* Hidden on wide screens: the nav is already visible there.
 *
 * MUST STAY ABOVE THE 900px BLOCK BELOW, which overrides it.
 *
 * This rule used to sit AFTER that block. A media query adds no specificity,
 * so `.nav-toggle` here and `.nav-toggle` there both weigh (0,1,0) and the
 * later one simply wins - meaning `display: none` beat the `inline-flex` that
 * was meant to reveal the button on a phone.
 *
 * The nav itself is display:none below 900px and is only brought back by the
 * `.open` class the toggle sets. With the toggle hidden too, there was no way
 * to set it: every masthead link - Sign in, Create account, Verify appointment,
 * the account menu, Sign out - was unreachable on a phone, and the page looked
 * like it simply had no navigation.
 *
 * Nothing was wrong with the markup or the JavaScript; both worked. Ordering
 * was the whole defect, which is why it survived: the rules read correctly one
 * at a time.
 */
.nav-toggle {
  display: none; align-items: center; gap: 8px; font: inherit; font-size: 14px;
  background: transparent; border: 1px solid var(--hairline); border-radius: 4px;
  padding: 8px 12px; cursor: pointer; color: var(--ink);
}

/* Below this a horizontal nav cannot work at all; it becomes a stacked menu
   revealed by the toggle, so nothing is ever unreachable.
 *
 * IT SAID "STACKED" AND WAS NOT.
 *
 * The rule was `flex-wrap: wrap` with a 12px gap, which is a ROW that wraps -
 * so on a 375px phone all four links sat on one line and the menu opened as a
 * squeezed copy of the desktop bar rather than a menu. Measured on an iPhone
 * SE: "Offices" was a 41x21px target, "My booking" 72x21. The guidance
 * everyone converges on is 44x44 (WCAG 2.5.5, Apple HIG; Android says 48dp),
 * and these were less than half that in height with 12px of separation.
 *
 * That is a real failure on a government service, where a large share of
 * citizens book on a phone and some of them have limited dexterity or are
 * standing in a queue holding something. A mis-tap goes to the wrong page and
 * the whole menu has to be reopened.
 *
 * Now a genuine column: one full-width row per destination, separated by
 * hairlines, each at least 44px tall. The links span the masthead so the
 * target is the whole row rather than the width of the word in it - "Offices"
 * is no harder to hit than "How to book".
 */
@media (max-width: 900px) {
  .masthead .inner { flex-wrap: wrap; }
  .nav {
    display: none;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    /* Zero, because the rows are separated by their own hairlines. A gap here
       would float them apart and lose the "one continuous list" reading. */
    gap: 0;
    padding-bottom: 10px;
  }
  .nav.open { display: flex; }
  .nav-toggle { display: inline-flex; margin-left: auto; }

  /* The whole row is the target, not just the text. */
  .nav > a {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 11px 2px;
    border-top: 1px solid var(--hairline);
  }

  /* The call to action stays a button rather than becoming another list row:
     it is the one thing on this menu that DOES something rather than going
     somewhere, and it should not be findable only by reading. */
  .nav > a.btn {
    justify-content: center;
    border-top: 0;
    margin-top: 12px;
    padding: 12px 14px;
    font-size: 15px;
  }
}

/* ---------- Footer ---------- */
.footer { background: var(--blue-deep); color: #fff; margin-top: auto; }
.footer .inner { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; padding-block: 28px; }
.footer p { font-size: 13px; }
.footer .muted { color: #c6d4f2; font-size: 12px; }

/* ---------- Cards & panels ---------- */
.card {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: 22px;
}
/* Vertical spacing for STACKED cards only.
 *
 * Inside a grid or flex row the container's own `gap` already spaces items,
 * and this margin would apply to every card except the first - shifting cards
 * 2..n down and, because grid items stretch to fill their track, making them
 * shorter than card 1. Identical markup, visibly different cards.
 *
 * The two overrides below cancel it inside grid and flex containers, which is
 * more robust than a :not() chain - a new container type only needs one line
 * here rather than editing an exclusion list. */
.card + .card { margin-top: 16px; }
.grid > .card + .card,
.row > .card + .card { margin-top: 0; }

/* Cards used as grid items should fill their track, so a row of them is even
   regardless of how much text each contains. */
.grid > .card { display: flex; flex-direction: column; height: 100%; margin-top: 0; }
.card h2 { font-size: 17px; margin-bottom: 10px; }
.muted { color: var(--ink-muted); }
.subtle { color: var(--ink-subtle); font-size: 13px; }

/* ---------- Availability calendar ----------
   A month grid on /available-dates. Read-only: it shows which dates have
   seats. Choosing a date is still the native input on the booking form - see
   the note at the top of public/js/datepicker.js for why that is not a
   hand-built widget.

   `table-layout: fixed` so seven columns stay even whatever a cell says; a
   "Public holiday" note must not make Wednesday twice the width of Tuesday. */
.calendar { width: 100%; min-width: 560px; border-collapse: collapse; table-layout: fixed; }
.calendar th {
  font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-subtle); padding: 6px 8px; text-align: left;
}
.cal-cell {
  border: 1px solid var(--hairline); vertical-align: top;
  padding: 8px; height: 74px; width: 14.28%;
}
.cal-empty { border-color: transparent; background: transparent; }
.cal-date { display: block; font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.cal-note { display: block; font-size: 12px; line-height: 1.35; color: var(--ink-subtle); }

/* Open days read as open; shut days stay quiet.

   The distinction is NOT colour alone - an open day names its seat count and a
   shut one says why it is shut - so the grid still works printed in black and
   white, and for a reader who cannot separate the two grounds. Colour is the
   fast path, not the only one. */
.cal-open { background: var(--success-tint); }
.cal-open .cal-date { color: var(--ink); }
.cal-open .cal-link { font-weight: 600; color: var(--blue-deep); }
.cal-shut { background: var(--sunken); }
.cal-shut .cal-date { color: var(--ink-subtle); }
.eyebrow { font-size: 12px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-subtle); }

/* Lists on the how-to-book page.
 *
 * The reset at the top of this file strips list-style from any list carrying a
 * class, which is right for nav menus and card lists and wrong for these two:
 * they are prose. A list here that gets a class for any reason silently loses
 * its 1-2-3, leaving three bold labels that no longer read as a sequence.
 *
 * So the marker is restored explicitly, the way .bring-list does further down.
 * The padding lives here too rather than inline, so the indent and the marker
 * cannot drift apart. */
.steps-list { list-style: decimal; padding-left: 20px; }
.session-list { list-style: disc; padding-left: 20px; }

/* Action buttons in a table row, kept on ONE line.
 *
 * These cells used `.row`, which sets `flex-wrap: wrap` - correct for page
 * layout, wrong here. Four buttons exceeded the column width and the last one
 * wrapped underneath, so every row grew to double height and the table stopped
 * scanning as one row per person.
 *
 * nowrap on the flex container keeps the buttons in a line; nowrap on the
 * buttons themselves stops a two-word label like "Clear lockout" breaking
 * across lines and reintroducing the same height. The table scrolls
 * horizontally on narrow screens rather than wrapping - see .table-scroll. */
.table-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: flex-end;
  flex-wrap: nowrap;
}
.table-actions > * { flex: none; }
.table-actions .btn { white-space: nowrap; }

/* A secret shown once, meant to be read character by character and copied.
 *
 * Monospaced because a generated password mixes shapes people confuse by
 * sight - l/1/I, O/0 - and a proportional font makes that worse. The
 * user-select hint lets a double-click take the whole value rather than
 * stopping at a punctuation mark inside it. */
.credential {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .02em;
  padding: 2px 6px;
  border-radius: 4px;
  background: rgba(0, 0, 0, .06);
  user-select: all;
  word-break: break-all;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-block; font: inherit; font-weight: 600; font-size: 15px;
  padding: 11px 20px; border-radius: 4px; border: 0; cursor: pointer;
  text-decoration: none; text-align: center; white-space: nowrap;
}
/* Every variant states its colour in EVERY state, including :hover, :focus and
 * :visited. That looks redundant and is not.
 *
 * A button here is sometimes a <button> and sometimes an <a class="btn">, and
 * the two do not inherit alike. `a { color: var(--blue) }` and
 * `a:hover { color: var(--blue-deep) }` at the top of this file apply to
 * anything anchor-shaped, so a rule that changes only `background` on hover
 * leaves an anchor picking up the link colour underneath it.
 *
 * On .btn-primary that produced #002a7f text on a #002a7f background - a
 * contrast ratio of 1.00:1, which is to say the label vanished the moment the
 * pointer touched it. The admin transaction-types screen showed it plainly:
 * "New transaction type" became an empty blue rectangle. .btn-danger went to
 * 1.96:1 the same way. A <button> with identical classes was unaffected, which
 * is why this survived - the same control looked fine or broke depending on
 * markup nobody thinks about while styling.
 *
 * It HAD been found before, and patched under `.nav` only (see the nav block
 * above). Fixing it at the source makes that patch redundant rather than
 * load-bearing.
 *
 * :visited matters too. A visited anchor takes the visited colour on some
 * browsers, so a "New transaction type" link a user has followed once would
 * dim afterwards without it. */
.btn-primary,
.btn-primary:link,
.btn-primary:visited { background: var(--blue); color: #fff; border-bottom: 3px solid var(--gold); }
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active { background: var(--blue-deep); color: #fff; text-decoration: none; }

.btn-secondary,
.btn-secondary:link,
.btn-secondary:visited { background: transparent; color: var(--blue); box-shadow: inset 0 0 0 1px var(--blue); }
.btn-secondary:hover,
.btn-secondary:focus,
.btn-secondary:active { background: var(--blue-tint); color: var(--blue); text-decoration: none; }

.btn-danger,
.btn-danger:link,
.btn-danger:visited { background: var(--danger); color: #fff; }
.btn-danger:hover,
.btn-danger:focus,
.btn-danger:active { background: var(--danger-deep, #8f1e18); color: #fff; text-decoration: none; }

.btn-sm { padding: 7px 13px; font-size: 13.5px; }

/* Disabled buttons must not answer the pointer. Opacity alone still showed a
 * hover background and a text cursor on something that cannot be pressed. */
.btn[disabled],
.btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }
.btn[disabled]:hover,
.btn[aria-disabled="true"]:hover { background: inherit; }

/* A visible keyboard focus ring. Without this the only focus signal was the
 * hover background, which is invisible to anyone tabbing rather than pointing. */
.btn:focus-visible { outline: 3px solid var(--gold); outline-offset: 2px; }

/* ---------- Forms ---------- */
/* Two-column field rows.
 *
 * The problem: when one field in a row has a hint and its neighbour does not,
 * the hint pushes that field's input down and the two controls stop lining up.
 *
 * Two earlier attempts failed. `margin-top: auto` pinned controls to the
 * bottom of their cell, which overcorrected badly wherever cell heights
 * differed a lot (the barangay select floated halfway down its cell). Plain
 * start-alignment simply reinstated the original misalignment.
 *
 * The fix is to stop trying to align controls and instead make the HINT SLOT a
 * constant height. Every field reserves one hint line whether or not it has
 * one, so labels sit on one line, hints on the next, and controls on the
 * third - across the whole row, in every browser, with no :has() or subgrid
 * support required.
 *
 * Fields whose hint wraps to two lines are the one case this does not solve;
 * keep hints in two-column rows short. Single-column fields are unaffected. */
.grid > .field { display: flex; flex-direction: column; align-items: stretch; margin-bottom: 0; }
.grid > .field > :last-child { margin-bottom: 0; }

/* The reserved hint line. Height matches .hint's own line-height so a field
   with a hint and one without occupy identical vertical space. */
.grid > .field > .hint,
.grid > .field > .hint-spacer {
  min-height: 18px;
  margin-bottom: 5px;
}

/* Inserted by markup where a field has no hint but shares a row with one that
   does. Empty and hidden from assistive technology - it is pure spacing. */
.hint-spacer { display: block; }

.field { margin-bottom: 16px; }
.field label { display: block; font-weight: 600; font-size: 14px; margin-bottom: 5px; }
.field .hint { font-size: 12.5px; color: var(--ink-subtle); margin-bottom: 5px; }
.input, .select, .textarea {
  width: 100%; font: inherit; padding: 10px 12px;
  border: 1px solid var(--hairline); border-radius: 4px; background: #fff; color: var(--ink);
}
.select { appearance: none; padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='%234A5568' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center; background-size: 12px; }
/* A disabled dependent select must look inert, not merely empty. */
.select:disabled { background-color: var(--sunken); color: var(--ink-subtle); cursor: not-allowed; }
.input:focus, .select:focus, .textarea:focus { border-color: var(--blue); }
.input[aria-invalid="true"] { border-color: var(--danger); }
/* Errors are never colour-only: the message text carries the meaning. */
.error { color: var(--danger); font-size: 13px; margin-top: 5px; }
.required { color: var(--danger); }

/* A fixed country code sitting flush against its input, so "+63" reads as part
   of the field rather than as loose text above it.

   Both mobile fields referenced .input-prefix / .prefix but neither rule had
   ever been written, so the span and a width:100% input could not share a line
   and the prefix wrapped above the box.

   The wrapper carries the border and focus ring; the input inside is stripped
   of its own so the pair looks like one control. :focus-within is what moves
   the ring to the wrapper when the caret enters the input. */
.input-prefix { display: flex; align-items: stretch; width: 100%;
  border: 1px solid var(--hairline); border-radius: 4px; background: #fff; }
.input-prefix:focus-within { border-color: var(--blue); }
.input-prefix .prefix {
  display: flex; align-items: center; padding: 10px 10px 10px 12px;
  color: var(--ink-subtle); border-right: 1px solid var(--hairline);
  background: var(--sunken); border-radius: 4px 0 0 4px; user-select: none;
}
.input-prefix .input { border: 0; border-radius: 0 4px 4px 0; min-width: 0; }
.input-prefix .input:focus { border: 0; outline: 0; }
/* The invalid state belongs on the whole control, not the inner box whose
   border was just removed. */
.input-prefix:has(.input[aria-invalid="true"]) { border-color: var(--danger); }

/* ---------- Alerts ---------- */
.alert { border-radius: var(--radius); padding: 12px 16px; font-size: 14px; border-left: 4px solid; }
.alert-success { background: var(--success-tint); color: var(--success); border-color: var(--success); }
.alert-error   { background: var(--danger-tint);  color: var(--danger);  border-color: var(--danger); }
.alert-info    { background: var(--blue-tint);    color: var(--blue-deep); border-color: var(--blue); }
.alert-warning { background: var(--warning-tint); color: var(--warning); border-color: var(--warning); }

/* ---------- Status badges ----------
   Always rendered with their text label — status is never conveyed by colour
   alone (WCAG 1.4.1). AppointmentStatus::tone() supplies the token. */
/* nowrap because the pill is drawn round the text: a two-word label like
   "Checked In" or "Pending Verification" broken across two lines stretches the
   999px radius into a lumpy oval and knocks the row's baseline out. Status
   labels are short by construction, so holding them on one line costs a little
   column width and nothing else. */
.badge {
  display: inline-block; font-size: 12.5px; font-weight: 600;
  padding: 3px 10px; border-radius: 999px; box-shadow: inset 0 0 0 1px currentColor;
  white-space: nowrap;
}
.badge-neutral { background: var(--sunken);        color: var(--ink-muted); }
.badge-info    { background: var(--blue-tint);     color: var(--blue); }
.badge-success { background: var(--success-tint);  color: var(--success); }
.badge-warning { background: var(--warning-tint);  color: var(--warning); }
.badge-danger  { background: var(--danger-tint);   color: var(--danger); }

/* ---------- Tables ---------- */
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th, .table td { text-align: left; padding: 11px 12px; border-bottom: 1px solid var(--hairline); vertical-align: top; }
.table th { background: var(--sunken); color: var(--blue-deep); font-size: 12.5px; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
.table tbody tr:hover { background: var(--blue-tint); }
/* The last row's rule doubles up with the card border below it. */
.table tbody tr:last-child td { border-bottom: 0; }
/* Numeric columns read far better right-aligned and tabular. */
.table .num { text-align: right; font-variant-numeric: tabular-nums; }
/* An identifier is read as ONE token - compared against a printed slip, or read
   aloud at the window. Wrapped after a hyphen, LTFRB-2026-000002-Y9L3 becomes
   two things that look like two fields, and the check character ends up on a
   line of its own where it reads as noise rather than part of the reference. */
.table .cell-id { white-space: nowrap; }
/* Table links carry no underline until hover: a column of underlined
   references reads as noise. */
.table a { color: var(--blue-deep); text-decoration: none; font-weight: 600; }
.table a:hover { text-decoration: underline; }
/* Small screens: horizontal scroll beats squashing a 9-column table. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* A scrollable region must be reachable by keyboard, or its overflowing
   columns are simply unreadable without a mouse or touchscreen (WCAG 2.1.1).
   Markup pairs this with tabindex="0" and an accessible name; the outline
   confirms to the user that the region — not a link inside it — holds focus. */
.table-scroll:focus-visible { outline: 3px solid var(--gold); outline-offset: -3px; }

/* Digits that must line up in a column: reference numbers, counts, money.
   Was referenced by the appointment listings and never defined, so references
   rendered in the proportional body face and their columns did not align. */
.tabular { font-variant-numeric: tabular-nums; letter-spacing: .02em; }

/* ---------- Stacked tables ----------
 *
 * Opt-in, via `.table-stack` alongside `.table`.
 *
 * Horizontal scrolling is a reasonable fallback for a dense staff screen that
 * is only ever used at a desk. It is the wrong answer for the citizen-facing
 * listings: most people reach this service on a phone, and a six-column table
 * in a scroller hides the status and the action — the two things they came to
 * see — off the right-hand edge, behind a gesture nothing signals.
 *
 * Below 720px each row becomes its own bordered block and each cell is
 * labelled from `data-label`, so the reading order stays Reference, Date,
 * Office, ... exactly as the header declared it.
 *
 * The header is hidden with .sr-only rather than display:none so screen
 * readers keep the column semantics; only the visual grid is dropped.
 */
@media (max-width: 720px) {
  .table-stack thead {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }

  .table-stack, .table-stack tbody, .table-stack tr, .table-stack td { display: block; width: 100%; }

  .table-stack tr {
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    padding: 4px 14px;
    margin-bottom: 12px;
    background: var(--surface);
  }

  .table-stack tr:last-child { margin-bottom: 0; }

  /* The hover tint is a pointer affordance for a dense grid; on a stack of
     cards it just tints whichever card was last tapped. */
  .table-stack tbody tr:hover { background: var(--surface); }

  .table-stack td {
    padding: 9px 0;
    border-bottom: 1px solid var(--hairline);
    display: flex;
    gap: 14px;
    align-items: baseline;
    justify-content: space-between;
    text-align: right;
  }

  .table-stack tbody tr td:last-child,
  .table-stack tbody tr:last-child td:last-child { border-bottom: 0; }

  /* The label the hidden header would otherwise have supplied. */
  .table-stack td::before {
    content: attr(data-label);
    font-size: 12.5px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--ink-subtle);
    text-align: left;
    flex: 0 0 38%;
  }

  /* A cell with no label (the actions column) gives its control the full
     width instead of leaving a 38% gap where a label would have been. */
  .table-stack td:not([data-label])::before { content: none; }
  .table-stack td:not([data-label]) { justify-content: stretch; text-align: left; }
  .table-stack td:not([data-label]) .btn { width: 100%; }

  /* Secondary lines inside a cell are set right-aligned with the value they
     belong to, not left against the label. */
  .table-stack td .subtle { text-align: right; }

  /* The stack supplies its own borders, so the wrapping card must not draw a
     second one around them. */
  .table-stack-card { border: 0; box-shadow: none; background: transparent; }

  /* Nothing overflows once the table is a stack. */
  .table-stack-wrap { overflow-x: visible; }
}

/* Reference and queue numbers must align in a column, and 0/O must not be
   confusable when read aloud over the phone. */
.reference {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums; letter-spacing: .05em;
  font-weight: 700; color: var(--blue-deep);
  background: var(--blue-tint); border-left: 4px solid var(--gold);
  padding: 8px 12px; border-radius: 3px; display: inline-block;
}

/* ---------- Filter bars ----------
   Every listing screen carries one. Given its own component so filters read as
   a tool attached to the results rather than as another content card competing
   with them: flatter, sunken, no shadow. */
.filters {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: var(--radius); padding: 16px 18px; margin-bottom: 22px;
}
.filters .field { margin-bottom: 0; }
.filters .actions { display: flex; gap: 8px; align-items: center; margin-top: 14px; flex-wrap: wrap; }
/* Push a trailing action (e.g. "New office") to the far end of the row. */
.filters .actions .spacer { margin-left: auto; }

/* ---------- Back-office navigation ----------
   A second-level bar under the masthead, shown only inside /admin. Sunken
   rather than white so it reads as chrome attached to the masthead above,
   not as the first card of the page content. */
.admin-nav { background: var(--sunken); border-bottom: 1px solid var(--hairline); }
.admin-nav-inner { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding-block: 8px; }

.admin-nav-list { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; margin: 0; padding: 0; list-style: none; }

/* Group label. Small, quiet and non-interactive - it orients someone scanning
 * the bar without competing with the links for attention. aria-hidden in the
 * markup because each list already carries the same name as an aria-label, so
 * a screen reader would otherwise announce "Set up" twice.
 *
 * The separator is a border rather than a character, so it disappears cleanly
 * when the bar wraps onto a second line. */
.admin-nav-group {
    font-size: 11px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted, #5b6472);
    padding: 0 10px 0 0;
    margin-left: 10px;
    border-left: 1px solid var(--hairline);
    padding-left: 14px;
    white-space: nowrap;
}
/* No rule before the first group - it would hang off the left edge. */
.admin-nav-inner > .admin-nav-list:first-child .admin-nav-group {
    border-left: 0;
    margin-left: 0;
    padding-left: 0;
}

.admin-nav-list a {
  display: block; padding: 7px 12px; border-radius: 4px;
  font-size: 13.5px; font-weight: 600; color: var(--ink-muted); text-decoration: none;
  white-space: nowrap;
}
.admin-nav-list a:hover { background: var(--blue-tint); color: var(--blue-deep); }

/* The current section. Marked with aria-current in the markup, so the visual
   treatment here is reinforcement rather than the only signal — and the gold
   underline means it does not rely on the blue alone. */
.admin-nav-list a[aria-current="page"] {
  background: var(--surface); color: var(--blue-deep);
  box-shadow: inset 0 -3px 0 var(--gold);
}

@media print { .admin-nav { display: none !important; } }

/* ---------- Dropdown menu ----------
   Used by the masthead account menu. Deliberately a plain button plus a
   panel rather than a hover menu: hover menus are unusable on touch and
   hostile to anyone driving the page from a keyboard. */
.menu { position: relative; display: inline-flex; }

.menu-trigger { display: inline-flex; align-items: center; gap: 7px; }

/* Drawn rather than typed, so it inherits the button's colour and never
   renders as a stray character in a font that lacks the glyph. */
.menu-caret {
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
}

.menu-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 60;
  min-width: 232px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgb(0 40 120 / 16%);
}

/* [hidden] alone loses to the display rule above in some browsers. */
.menu-panel[hidden] { display: none; }

.menu-heading {
  padding: 8px 10px 10px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--hairline);
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ink);
  line-height: 1.3;
}
.menu-heading span {
  display: block;
  font-weight: 400;
  font-size: 12.5px;
  color: var(--ink-subtle);
  overflow-wrap: anywhere;
}

/* Items are links and one submit button; both must look and size identically,
   so they are styled together rather than relying on .btn. */
.menu-panel a,
.menu-panel button {
  display: block;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.menu-panel a:hover,
.menu-panel button:hover { background: var(--blue-tint); color: var(--blue-deep); }
.menu-panel form { margin: 4px 0 0; padding-top: 4px; border-top: 1px solid var(--hairline); }

/* The nav sets `.nav a { color: var(--ink-muted) }` at a specificity the menu
   items would otherwise lose to, greying them inside the white panel. */
.nav .menu-panel a { color: var(--ink); font-size: 14px; white-space: normal; }
.nav .menu-panel a:hover { color: var(--blue-deep); text-decoration: none; }

/* Below the nav's stacking breakpoint the masthead becomes a column, so an
   absolutely positioned panel would hang over the page. It becomes an inline
   block instead. */
@media (max-width: 900px) {
  .menu { display: block; width: 100%; }
  .menu-trigger { width: 100%; justify-content: center; }
  .menu-panel { position: static; min-width: 0; margin-top: 8px; box-shadow: none; }
}

@media print { .menu { display: none !important; } }

/* ---------- Session picker ----------
   Only ever two choices, so they get room rather than being shrunk into a
   grid of time buttons. Sized as full-width targets on a phone, which is
   where most citizens book. */
.session-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin-top: 10px;
}

.session-card {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 16px 18px;
  min-height: 92px;
  border: 2px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--surface);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.session-card:hover { border-color: var(--blue); background: var(--blue-tint); }
.session-card:focus-visible { outline: 3px solid var(--gold); outline-offset: 2px; }

/* Selection is carried by border, fill AND the check, not colour alone. */
.session-card.is-selected {
  border-color: var(--blue-deep);
  background: var(--blue-tint);
  box-shadow: inset 0 0 0 1px var(--blue-deep);
}
.session-card.is-selected .session-card-name::after {
  content: " ✓";
  color: var(--blue-deep);
}

.session-card-name  { font-size: 17px; font-weight: 700; color: var(--ink); }
.session-card-range { font-size: 14px; color: var(--ink-muted); }
.session-card-left  { font-size: 12.5px; color: var(--ink-subtle); margin-top: 2px; }

/* ---------- Directory cards ----------
   Used by the office and transaction listings. The whole card is the target,
   so the title carries no underline until hover - a grid of underlined blue
   headings reads as a link farm rather than a directory. */
.tile { display: flex; flex-direction: column; height: 100%; }
.tile h2, .tile h3 { font-size: 17px; margin: 4px 0 0; line-height: 1.35; }
.tile h2 a, .tile h3 a { color: var(--blue-deep); text-decoration: none; }
.tile h2 a:hover, .tile h3 a:hover { color: var(--blue); text-decoration: underline; }
/* The hairline that separates the heading from its metadata. */
.tile .meta { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--hairline); }
.tile .body { margin-top: 8px; color: var(--ink-muted); font-size: 14px; }
/* Pins the metadata to the bottom so a row of cards lines up regardless of
   how long each description runs. */
.tile .foot { margin-top: auto; }

/* Hovering the card lifts it slightly, signalling the whole surface is
   clickable without relying on the title alone. */
.tile:hover { border-color: var(--blue); box-shadow: 0 2px 8px rgb(0 40 120 / 12%); }

/* ---------- Stat tiles ---------- */
.stat { display: flex; flex-direction: column; gap: 2px; }
.stat .value {
  font-size: 28px; font-weight: 700; color: var(--blue-deep);
  font-variant-numeric: tabular-nums; line-height: 1.15;
}
.stat .value.sm { font-size: 22px; }

/* ---------- Toolbar ---------- */
.toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 18px; }
.toolbar .spacer { margin-left: auto; }

/* ---------- Inline table forms ----------
   Action controls sit inside table cells on the worklist and calendar. They
   must not inherit the full-width form styling or a select would blow the
   column out. */
.inline-form { display: flex; gap: 6px; align-items: center; flex-wrap: nowrap; }
.inline-form .input, .inline-form .select { width: auto; min-width: 0; padding: 6px 9px; font-size: 13.5px; }
.inline-form .select { padding-right: 28px; background-position: right 8px center; }

/* A back link above a page header. */
.backlink { display: inline-block; font-size: 13.5px; margin-bottom: 10px; color: var(--ink-muted); }

/* ---------- Date fields ----------
 * The native control is kept deliberately (see public/js/datepicker.js): it
 * brings keyboard support, screen-reader announcements, locale-aware months
 * and the OS date wheel on phones, none of which a hand-built widget would get
 * right for free.
 *
 * What it does NOT bring is a consistent look, so the browser's own icon is
 * replaced with one drawn here and the field is sized so mm/dd/yyyy never
 * sits cramped against the edge.
 */
.input[type="date"],
.input[type="month"] {
  /* Room for the icon, and a floor so the date segments are never clipped. */
  padding-right: 40px;
  min-width: 190px;
  /* Segments are digits: tabular figures stop the field jittering as they are
     typed. */
  font-variant-numeric: tabular-nums;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230038a8' stroke-width='2' stroke-linecap='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 17px;
  /* The whole field opens the picker, so it must read as clickable. */
  cursor: pointer;
}

/* The browser's own indicator is made invisible but left in place, sitting
   exactly over the icon drawn above. Two things follow:
   - the icon is ours, so every browser looks the same;
   - the native click target survives, which is the no-JavaScript fallback.

   It is deliberately NOT stretched across the whole field. Doing so does give
   click-anywhere behaviour without scripting, but it also swallows clicks on
   the mm/dd/yyyy segments - and typing a birth year is far faster than paging
   a calendar back forty years. Whole-field clicking is handled in JavaScript
   instead, which leaves the segments alone. */
.input[type="date"]::-webkit-calendar-picker-indicator,
.input[type="month"]::-webkit-calendar-picker-indicator {
  position: absolute;
  right: 0;
  top: 0;
  width: 40px;
  height: 100%;
  margin: 0;
  padding: 0;
  opacity: 0;
  cursor: pointer;
}

/* The indicator above is absolutely positioned, so the field must be its
   containing block or it would stretch to the nearest positioned ancestor. */
.input[type="date"], .input[type="month"] { position: relative; }

/* Firefox and Safari show no indicator at all; the JS handles those, and the
   icon drawn above still signals what the field is. */
.input[type="date"]:hover,
.input[type="month"]:hover { border-color: var(--blue); }

/* Compact variant for table cells. */
.inline-form .input[type="date"] { min-width: 150px; padding-right: 34px; background-position: right 9px center; background-size: 15px; }

/* Empty segments (mm/dd/yyyy) in a lighter ink, so an unset filter is
   distinguishable at a glance from one that carries a date. */
.input[type="date"]::-webkit-datetime-edit-text { color: var(--ink-subtle); padding: 0 1px; }
.input[type="date"]:invalid::-webkit-datetime-edit { color: var(--ink-subtle); }

/* ---------- Print (staff hand printed confirmations to customers) ---------- */
@media print {
  /* This rule was corrupted at some point: the body of the .sr-only rule had
     been pasted in here, so nav/footer/.no-print were clipped to 1px rather
     than removed. It happened to look right because a 1px clipped element is
     invisible - but the intent is display:none, and .sr-only must keep its
     normal definition. */
  .no-print, .nav, .footer, .skip-link, .nav-toggle { display: none !important; }

  body { background: #fff; }
  .card { box-shadow: none; border: 1px solid #999; }
  .brand-rule { height: 0; border-top: 3px solid #000; }

  /* Links print as plain text; a bare "click here" is useless on paper, and
     the reference number is what a citizen actually needs. */
  a { color: #000; text-decoration: none; }
  .table th { background: #eee !important; color: #000; }

  /* Browsers drop background colours when printing unless told otherwise, and
     a status badge stripped of its fill prints as bare text that reads like
     part of the sentence around it. The label always carries the meaning
     (status is never colour-alone), but the outline is what makes it scan as
     a discrete field on a printed slip a clerk checks at a counter. */
  .badge {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    border: 1px solid #000;
    box-shadow: none;
    color: #000 !important;
  }

  .reference {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    border: 1px solid #000;
    border-left: 4px solid #000;
    color: #000 !important;
  }

  /* A slip cut in half mid-appointment is a slip the counter cannot use. */
  .card, .table tr, .reference { break-inside: avoid; page-break-inside: avoid; }

  /* The stacked-table breakpoint keys off screen width; a printed page is
     wide, but print stylesheets are applied on top of the matched screen
     rules, so the grid is restored explicitly here. */
  .table-stack thead { position: static; width: auto; height: auto; clip: auto; overflow: visible; }
  .table-stack, .table-stack tbody { display: table; width: 100%; }
  .table-stack tr { display: table-row; border: 0; padding: 0; margin: 0; }
  .table-stack td { display: table-cell; text-align: left; }
  .table-stack td::before { content: none; }
}

/* ---------- Appointment pass ----------
   The confirmation screen is not a receipt, it is a CLAIM STUB: with mail
   switched off it is the only copy of the reference number the citizen ever
   gets, and it is the thing they hold up at the counter. So it is drawn as one,
   in two halves divided by a perforation - the credential (reference number and
   QR) above, the visit details below.

   The perforation is the one decorative flourish on the page and it earns its
   place by saying what the artefact is: people know on sight that a stub is
   something you keep and present. Everything else here is deliberately quiet.

   Palette is the existing flag tokens only. This is a government service and
   its identity is already settled; a new accent colour would be decoration
   competing with the seal. */
.pass {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  box-shadow: var(--shadow);
  overflow: hidden;
}

/* The credential half. Deep blue so the reference number and QR read as the
   official part of the page rather than as more content. */
.pass-credential {
  background: var(--blue-dark);
  color: #fff;
  padding: 22px 24px 26px;
}

.pass-credential .pass-label { color: rgb(255 255 255 / 72%); }

.pass-label {
  font-size: 11.5px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-subtle);
  margin: 0 0 4px;
}

/* Tabular figures and open tracking: this number gets read aloud at a window,
   copied onto a docket by hand, and compared character by character against a
   printed slip. Ambiguity here costs somebody a trip. */
.pass-reference {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: clamp(20px, 5.2vw, 30px);
  font-weight: 700;
  letter-spacing: .06em;
  line-height: 1.2;
  margin: 0;
  color: #fff;
  overflow-wrap: anywhere;
}

.pass-cred-grid {
  display: flex; gap: 24px; align-items: flex-start;
  justify-content: space-between; flex-wrap: wrap;
}
.pass-cred-main { flex: 1 1 240px; min-width: 0; }

/* The QR sits in a white well: a code printed on a dark field will not scan,
   and the quiet zone around it is part of the spec, not padding. */
.pass-qr {
  background: #fff;
  border-radius: 8px;
  padding: 10px;
  line-height: 0;
  flex: 0 0 auto;
}
.pass-qr img { display: block; width: 148px; height: 148px; }

/* Copy control. On a phone, selecting a 20-character reference by long-press is
   genuinely difficult, and getting it wrong is silent. */
.pass-copy {
  display: inline-flex; align-items: center; gap: 7px;
  margin-top: 12px;
  font: inherit; font-size: 13.5px; font-weight: 600;
  padding: 7px 13px; border-radius: 4px; cursor: pointer;
  background: rgb(255 255 255 / 12%);
  border: 1px solid rgb(255 255 255 / 38%);
  color: #fff;
}
.pass-copy:hover { background: rgb(255 255 255 / 20%); }
.pass-copy:focus-visible { outline: 3px solid var(--gold); outline-offset: 2px; }

/* The perforation. Two notches bitten out of the card edges with a dashed rule
   between them, which is what a torn stub actually looks like. */
.pass-perf {
  position: relative;
  height: 22px;
  background: var(--blue-dark);
  border-bottom: 3px solid var(--gold);
}
.pass-perf::before,
.pass-perf::after {
  content: "";
  position: absolute; top: 50%;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--canvas);
  transform: translateY(-50%);
}
.pass-perf::before { left: -11px; }
.pass-perf::after  { right: -11px; }

.pass-perf span {
  position: absolute; top: 50%; left: 16px; right: 16px;
  border-top: 2px dashed rgb(255 255 255 / 34%);
  transform: translateY(-50%);
}

.pass-details { padding: 20px 24px 22px; }

/* Label above value rather than beside it: the values here are of wildly
   different lengths (a session time against a full office name), and a two
   column layout either wraps badly or wastes half the width on a phone. */
.pass-facts {
  display: grid; gap: 16px 28px;
  grid-template-columns: 1fr;
  margin: 0;
}
@media (min-width: 560px) { .pass-facts { grid-template-columns: 1fr 1fr; } }

.pass-facts dt { margin: 0; }
.pass-facts dd { margin: 2px 0 0; font-weight: 600; }

/* Case number is set in the same face as the reference: it is a filing key that
   gets checked character by character, not prose. */
.pass-case {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: .04em;
}

/* Spans both columns so a long office name and a list of case numbers never
   collide. */
.pass-facts .pass-wide { grid-column: 1 / -1; }

/* A long case list runs in two columns with a rule between them.

   THE SPLIT IS DECIDED IN PHP, NOT BY THE BROWSER.

   This was `columns: 2` first, which reads better but hands the split to the
   multi-column balancer - and the balancer optimises for equal column HEIGHT,
   not for a predictable count. With eleven rows a 6/5 and a 5/6 split are
   equally balanced by that measure, so it picked 5/6 and the eleventh case
   hung below the left column with a gap beside it. On a filing document that
   looks like a fault rather than a layout.

   So each row is told which column AND which row it is in - see the template -
   and the grid simply places it. Stating the row matters as much as the
   column: grid auto-placement runs a cursor that only moves forward, so with
   the column alone the second column began below the entire first column
   rather than beside its top. Row 1 is the label, which spans both.

   The first column always takes the extra row on an odd count, which is the
   convention everywhere else a list is split.

   Column-major order is preserved: the citizen is checking these against a
   stack of papers, and a list that reads top-to-bottom down one column then
   the next matches how the stack is held. Row-major would have them zigzag.

   The rule is a border on the FIRST column's rows rather than a `column-rule`,
   which grid has no equivalent of. Rows carry padding instead of margin so the
   borders meet and the line reads as continuous, and the first column is the
   longer one, so the rule always spans the full height of the list. */
@media (min-width: 560px) {
  .pass-facts .pass-cases {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 0;
    align-content: start;
  }

  .pass-facts .pass-cases dt { grid-column: 1 / -1; }

  .pass-facts .pass-cases dd {
    grid-column: 1;
    margin: 0;
    padding: 3px 24px 3px 0;
    border-right: 1px solid var(--hairline);
  }

  .pass-facts .pass-cases dd.case-col-2 {
    grid-column: 2;
    padding: 3px 0 3px 24px;
    border-right: 0;
  }
}

/* Sticky action bar, phones only.

   With mail off, saving this page IS the flow - a citizen who closes the tab
   without doing it has lost their booking reference. On a phone the buttons
   otherwise sit below the fold, under the QR and the guidance, which is exactly
   where an action nobody must miss should not be. */
.pass-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }

/* Sits OUTSIDE .pass on purpose: .pass sets overflow:hidden to clip the
   perforation notches, and an overflow container is a scroll container, which
   silently stops position:sticky working for anything inside it. */
.pass-actions--primary { margin-top: 16px; }

@media (max-width: 560px) {
  .pass-actions--primary {
    position: sticky; bottom: 0; z-index: 40;
    margin: 16px -20px 0;
    padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
    background: rgb(255 255 255 / 95%);
    border-top: 1px solid var(--hairline);
  }
  .pass-actions--primary .btn { flex: 1 1 100%; }
}

/* ---------- Print: the pass becomes the slip ----------
   Backgrounds are forced because an unfilled QR well and a white-on-white
   reference number print as nothing at all, which would defeat the entire
   purpose of the button that triggered the print. */
@media print {
  .pass {
    border: 1px solid #000; box-shadow: none; border-radius: 0;
    break-inside: avoid; page-break-inside: avoid;
  }
  .pass-credential {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
    background: #fff !important; color: #000 !important;
    border-bottom: 2px solid #000;
  }
  .pass-reference { color: #000 !important; }
  .pass-credential .pass-label { color: #000 !important; }
  .pass-qr { padding: 0; }
  .pass-qr img { width: 168px; height: 168px; }
  .pass-perf { display: none; }
  .pass-details { border-top: 1px dashed #000; }

  /* The screen rule is a light hairline chosen against the card; on paper it
     prints too faint to read as a divider, so it is restated in black. The
     two columns themselves already apply - a printed page is wider than the
     560px the screen rule is gated on. */
  .pass-facts .pass-cases dd { border-right-color: #000; }
  .pass-facts .pass-cases dd.case-col-2 { border-right: 0; }
}

/* ---------- Bulk line-item actions ----------
   Sits above the transactions table on a visit carrying several rows. Quiet by
   design: these are shortcuts for a long visit, not the primary control, and
   the per-row dropdown below remains the way anything unusual is done. */
.bulk-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  padding: 12px 14px;
  background: var(--sunken);
  border-radius: var(--radius);
}
.bulk-actions form { margin: 0; }

/* ---------- Case numbers (one row per case) ----------
   A release may cover several cases, so this is a repeating row: an input and
   a Remove, with an Add beneath. The two controls are deliberately unequal in
   weight. Remove is destructive and belongs to ONE row, so it stays quiet and
   only colours on hover; Add is the safe, expected action and reads as a
   normal affordance. Making both look like buttons of the same weight - which
   is what an unstyled .btn pair did - put the loudest thing on the screen next
   to a field the citizen had not filled in yet. */

/* .btn sets display:inline-block, which BEATS the user-agent [hidden] rule,
   because an author stylesheet always wins over the UA one. So a button
   carrying the hidden attribute stayed visible - which is why the first row's
   Remove appeared even though the script had hidden it, and why the Add button
   flashed before JavaScript claimed it. The .menu-panel rule further up is the
   same bug, found separately; this covers every button. */
.btn[hidden] { display: none; }

.case-row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 8px;
}

.case-row .input { flex: 1; min-width: 0; }

/* Sized against the input beside it rather than against other buttons: these
   two sit on one line, and a control taller than the field it acts on reads as
   the more important of the pair. */
.btn-remove-case {
  flex: none;
  padding: 0 12px;
  align-self: stretch;
  font-size: 14px;
  font-weight: 600;
  background: transparent;
  color: var(--ink-subtle);
  box-shadow: inset 0 0 0 1px var(--hairline);
}
.btn-remove-case:hover,
.btn-remove-case:focus {
  background: var(--danger-tint);
  color: var(--danger);
  box-shadow: inset 0 0 0 1px var(--danger);
}

.btn-add-case {
  margin-top: 2px;
  padding: 9px 14px;
  font-size: 14px;
  background: transparent;
  color: var(--blue);
  box-shadow: inset 0 0 0 1px var(--hairline);
}
.btn-add-case:hover,
.btn-add-case:focus {
  background: var(--blue-tint);
  color: var(--blue-deep);
  box-shadow: inset 0 0 0 1px var(--blue);
}

/* The + is decoration; the word carries the meaning, so it is hidden from
   assistive tech rather than read out as "plus sign add another case number". */
.btn-add-case::before {
  content: "+";
  margin-right: 7px;
  font-weight: 700;
}

/* On a narrow screen the pair stacks rather than squeezing the input down to
   nothing. Remove goes full width under its own field, still clearly attached
   to it by the gap. */
@media (max-width: 460px) {
  .case-row { flex-wrap: wrap; }
  .case-row .input { flex: 1 0 100%; }
  .btn-remove-case { align-self: flex-start; padding: 8px 12px; }
}

/* ---------- Preset facts (collapsed step 1) ----------
   Stated, not asked. Same label/value treatment as the pass, so the two facts
   the citizen reads here are the two they see again on their confirmation. */
.preset-facts {
  display: grid; gap: 14px 28px;
  grid-template-columns: 1fr;
  margin: 0;
  padding: 14px 16px;
  background: var(--sunken);
  border-radius: var(--radius);
}
@media (min-width: 560px) { .preset-facts { grid-template-columns: 1fr 1fr; } }
.preset-facts dt { margin: 0; }
.preset-facts dd { margin: 2px 0 0; font-weight: 600; }

/* ---------- Claimant groups ----------
   Alternatives, not steps. Side by side on a wide screen so a citizen can
   compare and find themselves; stacked with a clear rule between on a phone,
   where side-by-side would compress each column into unreadable slivers.

   Each group is boxed because the boundary IS the information: it says these
   documents belong together and you need only this set. */
.claimant-groups {
  display: grid; gap: 14px;
  grid-template-columns: 1fr;
  margin-top: 12px;
}
@media (min-width: 860px) {
  .claimant-groups { grid-template-columns: repeat(3, 1fr); }
}

.claimant-group {
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  border-top: 3px solid var(--blue);
  padding: 14px 16px;
  background: var(--surface);
}

.claimant-group-title { font-size: 16px; margin: 0; }

@media print {
  /* Three columns of documents do not fit legibly across a printed page, and a
     group split across a page break is a checklist a citizen half-reads. */
  .claimant-groups { grid-template-columns: 1fr; }
  .claimant-group { break-inside: avoid; page-break-inside: avoid; }
}

/* ---------- "What to bring" list ----------
   Plain bullets, matching the accepted-ID list nested inside it.

   These were open squares, on the reasoning that a citizen ticks items off as
   they pack. But nothing here is interactive: on screen an empty square reads
   as an UNCHECKED checkbox, implying an action is still owed, and it clashed
   with the dotted ID list beneath it - two marker styles in one list, for a
   distinction most readers would never decode. One quiet bullet throughout is
   the clearer signal. */
.bring-list { list-style: disc; padding-left: 20px; margin: 12px 0 0; }

.bring-list > li { margin-bottom: 14px; }

/* ---------- Accepted documents ----------
   Nested one level under a requirement, so a hollow marker (circle) sets it
   below the requirement's solid bullet - the same relationship a browser's own
   default nested-list markers express, made explicit here because .bring-list
   resets list-style. Still "any ONE of", stated in the label above. */
.accepted-docs {
  list-style: circle;
  padding-left: 20px;
  margin: 0;
  font-size: 14px;
  line-height: 1.7;
}

.accepted-docs li { margin: 0; }

/* ---------- Accepted IDs, shown once for the page ----------
   Sits below the claimant groups rather than inside them, because it applies to
   all three equally. Wider layout than an in-group list: with the full column
   width available the ten entries fit in two columns instead of one long
   strip, which keeps the printed copy to a sensible length. */
.accepted-ids-note {
  margin-top: 14px;
  padding: 14px 16px;
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--gold);
  border-radius: var(--radius);
  background: var(--sunken);
}

/* TWO columns, at every width above the phone breakpoint - never three.

   There are ten accepted IDs, so three columns cannot come out even whatever
   the browser does. Worse, multi-column balances by HEIGHT rather than by item
   count, and one entry - UMID / GSIS e-card / SSS e-card - wraps to two lines
   at a third of this box's width. That made eleven line-boxes out of ten
   entries and the balancer, correctly minimising column height, dealt them
   3 / 3 / 4. It looked like a bug and was not one.

   Two columns divide the ten evenly, five and five, and give each entry twice
   the width - enough that the UMID line stops wrapping, which is what created
   the uneven split in the first place. Two lines taller on screen, and the
   list is scanned by someone looking for the ID in their own wallet, so an
   even edge is worth more here than a shorter box.

   This also matches the print rule below, which has always been two. */
.accepted-docs-wide { columns: 2; column-gap: 28px; }
@media (max-width: 560px) { .accepted-docs-wide { columns: 1; } }

/* A bullet split across a column break is unreadable. */
.accepted-docs-wide li { break-inside: avoid; page-break-inside: avoid; }

@media print {
  .accepted-ids-note { break-inside: avoid; page-break-inside: avoid; }
  .accepted-docs-wide { columns: 2; }
}
