/* ==========================================================================
   Warranty & Service Management — design system
   Hand-written, no build step, no external fonts (the CSP is self-only).

   Direction: the subject is a moulded water tank on an Indian rooftop, and the
   premise is a single code read aloud between a customer and an agent. Three
   consequences drive everything below:

   1. The IDENTIFIER is the display face. Serial numbers, reference numbers and
      ticket numbers are set in tracked monospace at display sizes, because they
      are the most important text on any page. The manufacturing date is moulded
      into the tank itself, so codes here are typeset as if stamped.

   2. Colour is the brand, and status is NOT colour alone. Maroon carries the
      identity; green carries "covered", which is also the eco-friendly signal —
      so the environmental claim lives in the most-used state in the product
      rather than a badge in the footer. Deliberately NOT maroon: the danger hue.
      A brand that is red cannot also say "error" in red.

   3. Simulated against deuteranopia, green / amber / danger collapse to the same
      olive (luminance separation 1.03–1.09 — effectively identical). So every
      status carries a TEXT label, and the cover bar carries a distinct FILL:
      solid = covered, hatched = expiring, hollow = expired. Colour confirms; it
      never informs on its own.

   Signature: `.cover` — the warranty window as a band of time, with today marked
   on it. It is the one fact this entire system exists to answer, so it is drawn
   rather than written, and it appears wherever a unit does.
   ========================================================================== */

/* --- Tokens ------------------------------------------------------------- */
:root {
  /* Warm near-black, carrying a trace of the brand maroon so the page reads as
     one material rather than black text sitting on a coloured chrome. */
  --ink: #241a1c;
  --ink-soft: #4a3b3e;
  --ink-faint: #75656a;
  --ink-invert: #ffffff;
  /* Legible on the dark topbar without the glare of pure white. */
  --on-dark: #ead9dc;
  --on-dark-faint: #b39aa0;

  --surface: #faf7f7;
  --paper: #ffffff;
  --rule: #e7dedf;
  --rule-strong: #cdbfc2;

  /* Brand. Primary actions, chrome, identifiers. */
  --maroon: #7b1e2b;
  --maroon-deep: #5c141f;
  --maroon-wash: #f7eaec;

  /* "Covered" and, by design, the eco-friendly half of the identity. */
  --green: #1e7a4c;
  --green-wash: #e6f2eb;

  /* Expiring soon. Darkened to #8c5200 so it passes AA as body text (6.3:1);
     the brighter amber it replaced sat at 4.2:1 and failed. */
  --amber: #8c5200;
  --amber-wash: #fdf2e0;

  /* Error, expired, destructive. Kept orange-red so it can never be mistaken
     for the maroon chrome. */
  --danger: #c2410c;
  --danger-deep: #9a3409;
  --danger-wash: #fdece4;

  /* WORK IN HAND, and WORK BEING JUDGED.
     ------------------------------------------------------------------
     Added because the badge map in services/ticket_states.py sent five different
     statuses to one colour: "Being reviewed", "Ready to assign", "Engineer assigned"
     and "Visit in progress" all rendered as maroon --info. The comment above that map
     says the badges exist "so the queue reads at a glance", and four indistinguishable
     maroon pills is the one thing that cannot do.

     Blue is the queue moving under its own steam — accepted, assigned, on the way.
     Purple is a human deciding — in review, waiting on a judgement.

     Deliberately NOT --mat-mid, which is the same family of blue but is a MATERIAL: the
     colour of the middle layer of a cut tank wall. Nothing in the material scale ever
     means good or bad, and a status must. Two tokens, one hue, different jobs.

     Measured on #ffffff / on their own wash: 8.4:1 and 7.31:1 (blue), 9.16:1 and
     7.78:1 (purple). */
  --blue: #1f5fa8;
  --blue-wash: #e8f0fa;
  --blue-on-surface: #1a4e8a;

  --purple: #6b3fa0;
  --purple-wash: #f0eaf8;
  --purple-on-surface: #5b3488;

  /* ACCENT. Peach, and only ever on a dark brand panel — the step icons in the
     registration hero, a rule under a sidebar heading. It has no light-surface
     counterpart on purpose: on white it drops to 1.9:1 and would read as disabled
     text. If something on a light surface needs emphasis, that is what --maroon is.
     Measured 5.49:1 on the light chrome and 7.12:1 on the dark one. */
  --accent: #e3a87c;
  --accent-faint: #c98a5d;

  --focus: #7b1e2b;
  /* Focus rings inside the top bar. The page focus colour IS the bar's own colour now, so a
     maroon ring on maroon measured 1.11:1 — a keyboard user could not see where they were.
     White works on any chrome the bar is ever given, which is the point of naming a token by
     where it lands rather than by what colour it happens to be. */
  --focus-on-chrome: #ffffff;

  /* STATUS TEXT. Named by where they land, not by how dark they are.
     ------------------------------------------------------------------
     These carry the same values as the `-deep` tokens below, so light mode is unchanged to
     the byte. They exist because `-deep` names a DIRECTION — darken — which is right against
     white and wrong against near-black. When the washes correctly inverted for the dark
     theme, the `-deep` text that pairs with them stayed dark, and the badges and the cover
     bar fell to between 1.68:1 and 4.15:1. One of those is the warranty verdict, which is the
     single fact this system exists to state.

     Rule for anything added later: status TEXT uses `--*-on-surface`; status FILLS keep
     `--green` / `--amber` / `--danger` / `--maroon`. */
  --green-on-surface: #155c39;
  --amber-on-surface: #8c5200;
  --danger-on-surface: #9a3409;

  /* Two roles that are the same colour in light and cannot be in dark. --maroon is a FILL,
     read behind white text; --maroon-on-surface is the brand used AS text or a border,
     read against the page. On near-black the first must stay saturated enough to look like
     an action and the second must lift far enough to be legible, and one value cannot do
     both. Identical here, so light mode is unchanged. */
  --maroon-on-surface: #7b1e2b;

  /* The top bar. The brand maroon rather than the near-black it used to be: a black slab
     above a warm off-white page reads as an unfinished admin tool, and P4's identity is
     maroon and white. Deliberately NOT --ink, which is a text colour and flips with theme. */
  --chrome: #6f1a26;

  /* The serial plate, which stays near-black in both themes — see .plate. */
  --plate: #241a1c;

  /* Materials, not states. These are the colours of an actual cut tank wall — the sample
     sections P4 puts on a dealer's counter — so they belong to the product, not to the
     status scale above. Nothing here ever means "good" or "bad". */
  --mat-shell: #2f3a3d;
  --mat-foam: #d9d2b8;
  --mat-mid: #1f5fa8;
  --mat-barrier: #16181a;
  --mat-inner: #f2f4f3;
  --mat-silver: #b9c4c9;
  --mat-copper: #a85a2b;

  /* Identifiers are load-bearing here, so monospace leads the stack. */
  --font-id: ui-monospace, "Cascadia Mono", "Consolas", "DejaVu Sans Mono", monospace;
  /* Noto Sans is in the fallback because customer names will carry Bengali and
     Devanagari glyphs even though the interface itself is English-only. */
  --font-ui: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "Noto Sans",
    sans-serif;
  /* Headlines only. Segoe UI Variable ships THREE optical sizes and the interface has been
     using the Text one everywhere — the cut drawn for 11-18px, with the loose spacing and
     open counters that keep small type readable. At 44px that reads as a paragraph set
     large rather than as a headline. Display is the same typeface with tighter spacing and
     finer strokes, which is the whole difference between the two panels. */
  --font-display: "Segoe UI Variable Display", "Segoe UI Semibold", "Segoe UI", system-ui,
    -apple-system, "Noto Sans", sans-serif;

  --step--1: 0.8125rem; /* 13px */
  --step-0: 0.9375rem; /* 15px */
  --step-1: 1.0625rem; /* 17px */
  --step-2: 1.3125rem; /* 21px */
  --step-3: 1.6875rem; /* 27px */
  --step-4: 2.125rem; /* 34px */
  /* The hero headline, and nothing else. The scale stopped at 34px, which is a section
     heading — on a half-page brand panel it looked like body copy that had been bolded. */
  --step-5: 2.75rem; /* 44px */

  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.5rem;
  --sp-6: 2rem;
  --sp-7: 3rem;

  /* THREE RADII, BY WHAT THE THING IS.
     ------------------------------------------------------------------
     --radius stays 3px and keeps its original argument: moulded plastic has a tight
     edge, and the identifiers, plates and material sections in this app are drawn to
     look like the product. Rounding those would be the "soft SaaS pill" that comment
     was written to refuse.

     --radius-card and --radius-control are new, from the P4 reference screens, and are
     opted INTO per component rather than applied by renaming --radius. That is the
     difference between a design decision and a find-and-replace: a plate and a stat card
     are not the same kind of object and must not share an edge treatment.

     Reverting the softer look is two values here, and nothing else. */
  --radius: 3px;
  --radius-card: 14px;
  --radius-control: 10px;
  --radius-pill: 999px;

  /* Elevation, low and soft. Two levels only: a card sits on the page, and a thing that
     floats over the page sits above it. A third level would be a decision nobody could
     make consistently. Warm-tinted rather than neutral black, for the same reason --ink
     carries a trace of maroon: a grey shadow under a warm card reads as dirt. */
  --shadow-card: 0 1px 2px rgb(64 26 32 / 4%), 0 2px 8px rgb(64 26 32 / 5%);
  --shadow-raised: 0 2px 4px rgb(64 26 32 / 6%), 0 8px 24px rgb(64 26 32 / 10%);

  /* The edge on a PHOTOGRAPH, and the one token in this group that is deliberately NOT
     warm-tinted. The shadows above are tinted because they fall on the page and a grey
     shadow under a warm card reads as dirt. This one falls on the photo itself, where a
     maroon-tinted hairline reads as a colour cast on somebody's tank — so it stays
     neutral black, and its dark counterpart neutral white. Content, not chrome. */
  --edge-image: rgb(0 0 0 / 10%);

  --shell: 1180px;
  /* The admin sidebar. Named once because the content column, the sticky utility bar and
     the mobile drawer transform all measure against it. */
  --sidebar-w: 248px;

  /* The top bar's height, named once. Two other things have to sit against the bottom of
     it — the progress line and the alert strip — and both used to carry their own copy of
     "52px", which the coarse-pointer block near the end of this file silently raises to
     56px. So both were wrong on every touch device, which is every phone this app is
     actually used on. */
  --topbar-h: 52px;

  /* One opacity for "not available to you yet". There were four in service — 0.5, 0.45,
     0.4, and an inline 0.55 on retired parts — which reads as four degrees of unavailable
     when there is only ever one. */
  --disabled-opacity: 0.45;

  /* --- Motion ------------------------------------------------------------
     Named by ROLE, not by length — the same argument `--*-on-surface` and
     `--focus-on-chrome` above make about colour. "160ms" tells the next reader nothing
     about the moment it belongs to; "a state crossing under a pointer that is already
     there" tells them whether their new rule is that moment or a different one.

     Deliberately NO composite `--transition-*` token. A shorthand hides WHICH property is
     moving, and animating the wrong property — a box-model one, behind a two-hundred-row
     queue an admin holds open for hours — is precisely the bug these tokens exist to make
     visible. Whatever is animated has to be spelled out at the point of use. */

  /* The finger is still on the control. Past roughly 100ms it stops reading as
     "caused by me" and starts reading as the page doing something of its own. */
  --dur-press: 90ms;
  /* A state crossing under a pointer that is already there: hover, a dim, a note landing. */
  --dur-state: 160ms;
  /* Something arriving that was not on screen a moment ago. */
  --dur-enter: 260ms;
  /* The cover bar, and nothing else. There the duration IS the content: the bar is drawing
     a fifteen-year window, and drawing it instantly would state the answer without ever
     showing it was a span of time. */
  --dur-draw: 500ms;

  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  /* The progress line only. Quick off the mark, then almost still — a request that takes
     eight seconds must still be visibly moving in its eighth second. */
  --ease-creep: cubic-bezier(0, 0.8, 0.15, 1);

  /* Between items in a sequence. */
  --stagger: 55ms;
  /* How long the progress line takes to reach its ceiling. Long, on purpose: this is a
     rural 2G budget, not a localhost one. */
  --dur-creep: 8s;
  /* Nothing in the app announces itself as busy sooner than this. A request that finishes
     first never flashes anything, which is most of them. */
  --delay-busy: 180ms;

  /* Tell the UA which way round the page is. Without it, dark mode still renders every
     surface the UA owns in light: the date picker, the <select> popup behind the
     792-product category list, scrollbars, Chrome's autofill wash on the sign-in form,
     and any checkbox or radio we have not styled ourselves. */
  color-scheme: light;
  /* One line that brand-colours the permission grid, the dry-run checkbox and the product
     radio list, none of which we draw by hand. */
  accent-color: var(--maroon);
}

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

html {
  -webkit-text-size-adjust: 100%;
  /* Both of these are no-ops everywhere except macOS, which is the point. macOS is the
     one platform that renders text HEAVIER than it was drawn, so the same maroon heading
     that is 600 here is optically ~700 on a Mac, and the --ink-faint greys the rest of
     this file tunes against WCAG come out thicker and darker than they were measured at.
     This puts a Mac on the same footing as the Windows desks and Android handsets the app
     is actually used on, rather than making anything lighter for its own sake. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--step-0);
  line-height: 1.55;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

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

/* The ring's GEOMETRY is never animated, anywhere in this file. A keyboard user moving
   quickly through a form would otherwise leave a trail of half-drawn rectangles behind
   them, and the ring's whole job is to answer "where am I" in the frame it appears in.
   `outline-color` may crossfade; width, offset and style may not. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* --- Skip link ----------------------------------------------------------- */
/* Off-screen rather than display:none, because a hidden element cannot be focused and
   a skip link that cannot be focused is not a skip link. It rides above the sticky
   topbar so it is not covered the moment it appears. */
.skip-link {
  position: absolute;
  left: var(--sp-3);
  top: var(--sp-3);
  z-index: 100;
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--maroon-on-surface);
  border: 1px solid var(--rule-strong);
  text-decoration: none;
  font-weight: 600;
  transform: translateY(calc(-100% - var(--sp-4)));
}
/* No transition, deliberately. This exists to be instant: a skip link that slides into
   place is a skip link somebody has already pressed Tab past. */
.skip-link:focus {
  transform: none;
}

/* --- Typography --------------------------------------------------------- */
h1,
h2,
h3 {
  margin: 0;
  line-height: 1.2;
  font-weight: 600;
  letter-spacing: -0.01em;
  /* HOW A HEADING BREAKS.
     ------------------------------------------------------------------
     `balance` evens the lines rather than filling the first and orphaning the last. This
     app has no control over its own headings — product names come from a 792-row catalogue
     and customer names carry Bengali and Devanagari — so the wrap is decided by data, and
     the only two-line heading anyone had checked was the one on the reference screen.
     "Warranty ended 6 / months ago" on a 375px phone is what that looked like in practice.

     A progressive nicety: browsers that do not support it wrap exactly as before, and the
     ones that do cap the effort at a few lines, which is why it goes on headings and short
     labels rather than on paragraphs. */
  text-wrap: balance;
}

h1 {
  font-size: var(--step-3);
}
h2 {
  font-size: var(--step-2);
}
h3 {
  font-size: var(--step-1);
}

p {
  margin: 0 0 var(--sp-4);
  /* `pretty`, not `balance`: paragraphs want an even measure with no orphaned last word,
     which is a different job from evening out four words across two lines. */
  text-wrap: pretty;
}

/* The other two places this app puts a SENTENCE, both missed when `p` got the rule above.
   A caption sits under a work photo in a three-across grid, so its measure is about 110px
   and a single orphaned word is most of a line. A list item is where this app explains a
   consequence — "Stops this registration. Nothing is deleted..." on the void screen, the
   binding notes on bind-card — which is prose in everything but the tag it is wrapped in.
   Not on `td`: a table cell is data, and evening its last line moves numbers around. */
figcaption,
li {
  text-wrap: pretty;
}

a {
  color: var(--maroon-on-surface);
  text-underline-offset: 2px;
}

/* The identifier face. Anywhere a code appears, it is set like this. */
.id {
  font-family: var(--font-id);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-variant-ligatures: none;
}

.num {
  font-variant-numeric: tabular-nums;
}

/* The code face for text that must be shown EXACTLY as it is: email addresses, API keys.
   `.id` above is for codes read off a tank, and it uppercases and spreads them — which
   turns "Anita.Sharma@Example.com" into something nobody typed, and makes a pasted key
   impossible to check against the one on the provider's dashboard. */
.literal {
  font-family: var(--font-id);
  font-variant-ligatures: none;
  overflow-wrap: anywhere;
}

.eyebrow {
  font-family: var(--font-id);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 var(--sp-2);
}

.lede {
  color: var(--ink-soft);
  max-width: 62ch;
}

.muted {
  color: var(--ink-faint);
}

/* --- Shell -------------------------------------------------------------- */
/* Every focusable thing on the bar: the theme buttons, the brand link, "Sign out". */
.topbar :focus-visible {
  outline-color: var(--focus-on-chrome);
}

.topbar {
  background: var(--chrome);
  color: var(--on-dark);
  position: sticky;
  top: 0;
  z-index: 20;
}

.topbar__inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 var(--sp-5);
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  color: var(--ink-invert);
  text-decoration: none;
  font-weight: 600;
}

/* The brand logo. Sized by height so swapping in a wider real logo needs no CSS change. */
.brand__logo {
  height: 28px;
  width: auto;
  border-radius: 4px;
  flex: 0 0 auto;
}

.topbar__spacer {
  flex: 1;
}

.topbar__role {
  font-family: var(--font-id);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 2px 6px;
  /* Translucent rather than a fixed maroon: the bar is now maroon itself, and a maroon border
     on it is invisible. White at low alpha reads as a hairline on whatever the bar becomes. */
  border: 1px solid rgb(255 255 255 / 28%);
  border-radius: var(--radius);
  color: var(--on-dark);
}

.main {
  flex: 1;
  max-width: var(--shell);
  width: 100%;
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-5) var(--sp-7);
}

.page-head {
  margin-bottom: var(--sp-5);
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--rule);
}

/* The one link in the footer was 17px tall — the smallest target anywhere in the app, and
   it appears on every page. Padded to clear the 24px pointer minimum without moving the
   surrounding sentence, which is why the padding is vertical only. */
.foot a {
  display: inline-block;
  padding: var(--sp-1) 0;
}

.foot {
  border-top: 1px solid var(--rule);
  padding: var(--sp-4) var(--sp-5);
  font-size: var(--step--1);
  color: var(--ink-faint);
  text-align: center;
}

/* Constrain forms for readability; tables stay full width.

   CENTRED, not flush-left. The shell is 1180px and this column is 44rem, so a
   left-aligned form sat in the top-left corner of a 1280px screen with most of the page
   empty to the right of it — which reads as a layout that failed to load rather than as a
   deliberate measure. Centring costs nothing on a phone, where the column is the viewport. */
.col-form {
  max-width: 44rem;
  margin-inline: auto;
}

/* One task, one column. Narrower than --col-form because these pages ask for a phone
   number or a password and nothing else, and a 44rem line length over a single input reads
   as a form that lost its other half. */
.col-auth {
  max-width: 26rem;
  margin: var(--sp-6) auto var(--sp-7);
}

/* --- Signature: the serial plate ---------------------------------------- */
/* A unit's identity, rendered as its moulded nameplate. The strata bars on the
   left encode layer count, which is the product tier and therefore the warranty
   term — information, not ornament. */
.plate {
  display: inline-flex;
  align-items: stretch;
  /* Its own token, and near-black in both themes on purpose: this is a moulded nameplate, and
     the strata bars inside it are maroon and amber. It briefly shared --chrome with the top
     bar, which meant giving the bar the brand maroon would have painted the plate maroon too
     and sunk its own bars into it. */
  background: var(--plate);
  color: var(--ink-invert);
  border-radius: var(--radius);
  overflow: hidden;
  max-width: 100%;
  /* A plate is frequently an anchor, and a moulded nameplate does not have an underline
     drawn through its registration number. Stated here rather than as an inline style on
     each caller: `style="text-decoration: none"` was repeated at three call sites and
     silently missing from a fourth. */
  text-decoration: none;
}

.plate__strata {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 5px;
  background: var(--maroon-deep);
  justify-content: center;
  min-width: 16px;
}

.plate__strata i {
  display: block;
  width: 10px;
  height: 2px;
  background: var(--maroon);
  border-radius: 1px;
}

/* Outer walls of a sandwich tank are the visible colour; inner plies are foam. */
.plate__strata i:first-child,
.plate__strata i:last-child {
  background: var(--amber);
}

.plate__body {
  padding: var(--sp-2) var(--sp-4);
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}

.plate__code {
  /* Set explicitly, not inherited. An inline `color: inherit` on a .plate anchor once
     beat the class and rendered the serial in dark ink on the dark plate — leaving two
     units of the same model looking like one registration listed twice. */
  color: var(--ink-invert);
  font-family: var(--font-id);
  font-size: var(--step-1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.plate__meta {
  font-size: var(--step--1);
  color: var(--on-dark-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.plate--lg .plate__code {
  font-size: var(--step-3);
}

/* --- Signature: the cover bar ------------------------------------------- */
/* The warranty window as a band of time. This answers the one question the whole
   system exists for — is this unit covered, and for how much longer — so it is
   drawn rather than written, and it appears wherever a unit appears.

   The FILL carries the state, not the colour. Deuteranopia collapses green, amber
   and danger to the same olive, so a colour-only bar would tell a red-green
   colourblind engineer nothing at all:

     covered   solid fill
     expiring  diagonal hatch
     expired   hollow, hatched track, no fill
     lifetime  fill runs to the edge and fades — it has no end to draw

   Every bar is also labelled in words. The drawing is the fast path, never the
   only path. */
.cover {
  --pct: 0%;
  /* The same figure unitless, for `scaleX` — see `.cover__fill`. Both are set together by
     the `cover` macro; the default here is what a bar with no inline style would draw. */
  --pct-n: 0;
  margin: var(--sp-4) 0;
  /* A FIFTEEN-YEAR WINDOW HAS TO FIT IN ONE LOOK.
     Left uncapped the bar took whatever width its container gave it, which on the
     customer's product card was the full 1180px shell. At that width `.cover__now` — the
     hairline marking today — moves by about a pixel a month, so the drawing stops saying
     anything the two dates underneath do not already say. Capped, it reads as a band of
     time again. */
  max-inline-size: 28rem;
}

.cover__track {
  position: relative;
  height: 12px;
  border-radius: 2px;
  background: var(--rule);
  border: 1px solid var(--rule-strong);
  overflow: hidden;
}

/* SCALED, NOT RESIZED.
   ------------------------------------------------------------------
   This drew itself by transitioning `width`, which is a layout property: every frame of
   the 500ms draw re-ran layout and paint for the bar and everything after it. One bar is
   survivable; the customer's product list renders one per registration, so an account with
   five tanks ran five simultaneous half-second layout animations on first paint, on the
   phone hardware this app is built for.

   `transform` composites instead — same picture, no layout. The fill is a solid block
   inside `overflow: hidden`, so there is nothing in it for a horizontal scale to distort.
   The one exception is `--lifetime`, whose fill is a gradient; it is pinned to scaleX(1)
   below and never animates anyway. `.cover__now` keeps `left: var(--pct)` — it is a static
   position, not a transition, and percentages are what a position wants. */
.cover__fill {
  height: 100%;
  width: 100%;
  transform: scaleX(var(--pct-n));
  transform-origin: left center;
  background: var(--green);
  /* The one place in the app where the duration is the content rather than a cost. */
  transition: transform var(--dur-draw) var(--ease-out);
}

/* Where today sits on the band. A hairline, not a dot: it marks an instant. */
.cover__now {
  position: absolute;
  top: -3px;
  bottom: -3px;
  left: var(--pct);
  width: 2px;
  background: var(--ink);
  transform: translateX(-1px);
}

.cover__ends {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-3);
  margin-top: var(--sp-2);
  font-size: var(--step--1);
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* NOT transitioned, and neither are `.cover__now`, `.badge`, `.plate__code` or
   `.stat__value`. Every one of them states the verdict — covered, expiring, expired, the
   serial, the count. A verdict that arrives gradually is a verdict somebody reads
   mid-transition, and reading "expired" as "expiring" is a warranty claim in the post. */
.cover__state {
  font-weight: 600;
  color: var(--green-on-surface);
  text-align: center;
  flex: 1;
  /* The verdict, squeezed between two dates on a phone. Left to wrap naturally it broke as
     "Warranty ended 6 / months ago" — the number stranded on the line above the unit it
     counts, which is the one place in this sentence a break costs meaning. */
  text-wrap: balance;
}

/* Expiring: the remaining stretch is hatched, so "running out" is visible as
   texture at arm's length in bright sun. */
.cover--expiring .cover__fill {
  background: var(--amber);
}
.cover--expiring .cover__track {
  background: repeating-linear-gradient(
    45deg,
    var(--amber-wash),
    var(--amber-wash) 4px,
    var(--paper) 4px,
    var(--paper) 8px
  );
}
.cover--expiring .cover__state {
  color: var(--amber-on-surface);
}

/* Expired: nothing is filled. The track itself is struck through. */
.cover--expired .cover__fill {
  background: none;
}
.cover--expired .cover__track {
  background: repeating-linear-gradient(
    45deg,
    var(--danger-wash),
    var(--danger-wash) 4px,
    var(--paper) 4px,
    var(--paper) 8px
  );
  border-color: var(--danger);
}
.cover--expired .cover__state {
  color: var(--danger-on-surface);
}
.cover--expired .cover__now {
  background: var(--danger);
}

/* Lifetime has no right-hand edge to draw, so the fill runs off the end. Pinned to
   scaleX(1) explicitly: this is the one fill that is a gradient rather than a flat colour,
   and a horizontal scale would squeeze the fade instead of shortening the bar. */
.cover--lifetime .cover__fill {
  transform: scaleX(1);
  background: linear-gradient(90deg, var(--green) 0%, var(--green) 55%, var(--green-wash) 100%);
}
.cover--lifetime .cover__now {
  left: 55%;
}

/* No cover at all: a flat, empty band. Honest rather than alarming — an
   accessory that never carried a warranty has not failed at anything. */
.cover--none .cover__track {
  background: var(--surface);
}
.cover--none .cover__fill {
  background: none;
}
.cover--none .cover__now,
.cover--none .cover__ends .cover__end {
  display: none;
}
.cover--none .cover__state {
  color: var(--ink-faint);
  font-weight: 400;
}

/* Compact form for list rows, where the bar is a glance and not the subject. */
.cover--sm .cover__track {
  height: 6px;
}
.cover--sm .cover__ends {
  font-size: var(--step--1);
}

/* On a phone the verdict gets its own line, under the two dates rather than squeezed
   between them. Three items sharing 340px left the middle one — the sentence that is the
   whole point of the bar — with about a third of the width and two lines to say it in.
   `text-wrap: balance` above stops that breaking in the wrong place; this stops it having
   to break at all. */
@media (max-width: 480px) {
  .cover__ends {
    flex-wrap: wrap;
  }

  .cover__state {
    order: 3;
    flex-basis: 100%;
    margin-top: var(--sp-1);
  }
}

/* The reduced-motion opt-out for `.cover__fill` used to live here. It is now stated once,
   with everything else that moves, in the motion policy at the end of this file. */

/* --- Cards & surfaces --------------------------------------------------- */
.card {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: var(--sp-5);
}

.card + .card {
  margin-top: var(--sp-4);
}

.card__title {
  font-size: var(--step-1);
  font-weight: 600;
  margin: 0 0 var(--sp-1);
}

.grid {
  display: grid;
  gap: var(--sp-4);
}

/* Fluid, not stepped. The previous rules gave two columns below 640px and three
   or four above 720px — which left 641-719px on ONE column, so a large phone in
   landscape was worse off than the same phone in portrait. Worse, the intake photo
   grid (grid--3) squeezed three upload cards into ~150px each on a 375px screen.

   auto-fit + minmax lets the column count follow the available width with no
   breakpoints and no dead zone. `min(100%, ...)` is what stops the track from
   overflowing when the viewport is narrower than the minimum itself. */
.grid--2 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}
.grid--3 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
}
.grid--4 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}

/* auto-fit keeps adding columns as the viewport grows, so on a wide monitor a
   `.grid--2` became three columns and `.grid--4` became six. The class names mean
   "at most N", so the count is capped once N columns definitely fit. Capping only
   at the top is safe: everything below stays fluid, so no dead zone can return. */
@media (min-width: 60rem) {
  .grid--2 {
    grid-template-columns: repeat(2, 1fr);
  }
  .grid--3 {
    grid-template-columns: repeat(3, 1fr);
  }
  .grid--4 {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* A STAT CARD: a label chip, a figure, and an optional note about the figure.
   ------------------------------------------------------------------
   From the P4 reference screens. The card is a plain .card; these three parts are what
   goes inside it. The label is a chip rather than a heading because it sits ABOVE the
   number the eye lands on first, and a chip reads as a tag on the figure rather than as
   a section title competing with it.

   .stat__note is the "+2 today" / "needs action now" line. It takes a status family, so
   the note carries the reading rather than the number having to: 10 is neither good nor
   bad, and "needs action now" is what makes it urgent. */
.stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-3);
}

.stat__tag {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  background: var(--surface);
  border-radius: var(--radius-pill);
  padding: 3px 10px;
}

/* The dot that follows the label in the reference. Decorative — the note below carries the
   same reading in words, so this is aria-hidden at the point of use. */
.stat__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentcolor;
  flex: 0 0 auto;
}

.stat__note {
  font-size: var(--step--1);
  font-weight: 600;
  border-radius: var(--radius-pill);
  padding: 3px 10px;
  background: var(--surface);
  color: var(--ink-soft);
}

.stat--ok .stat__value,
.stat--ok .stat__dot {
  color: var(--green-on-surface);
}
.stat--ok .stat__note {
  background: var(--green-wash);
  color: var(--green-on-surface);
}

.stat--warn .stat__value,
.stat--warn .stat__dot {
  color: var(--amber-on-surface);
}
.stat--warn .stat__note {
  background: var(--amber-wash);
  color: var(--amber-on-surface);
}

.stat--bad .stat__value,
.stat--bad .stat__dot {
  color: var(--danger-on-surface);
}
.stat--bad .stat__note {
  background: var(--danger-wash);
  color: var(--danger-on-surface);
}

/* A figure with its label, for counts on the admin dashboard. */
.stat__value {
  font-family: var(--font-id);
  font-size: var(--step-4);
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.stat__label {
  font-size: var(--step--1);
  color: var(--ink-faint);
  margin-top: var(--sp-2);
}

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

.field > label {
  display: block;
  font-size: var(--step--1);
  font-weight: 600;
  margin-bottom: var(--sp-2);
}

.field__hint {
  font-size: var(--step--1);
  color: var(--ink-faint);
  margin-top: var(--sp-2);
}

input[type="text"],
input[type="tel"],
input[type="email"],
input[type="password"],
input[type="date"],
input[type="number"],
input[type="search"],
select,
textarea {
  /* Softer than --radius, matching the reference screens. Deliberately narrower in scope
     than a global rename: the plate, the material sections and the identifier chips below
     keep their tight 3px edge, because those are drawn to look like the moulded product. */
  width: 100%;
  font: inherit;
  color: inherit;
  padding: 0.55rem 0.7rem;
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--maroon-on-surface);
}

/* The ring in the focus colour every other control already uses, on every surface. It was
   --maroon-wash, about 1.1:1 on white: present in the stylesheet and invisible on screen,
   which matters most in the back office, worked by keyboard all day. Fields always match
   :focus-visible, so a mouse click still shows it — as it should, on a text box. */
:is(input, select, textarea):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* The photo buttons are labels for an invisible file input, so the input takes the focus
   and nothing on screen showed it. The ring goes on the button that can be seen. */
.shot form:has(input:focus-visible) .shot__action {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

textarea {
  min-height: 7rem;
  resize: vertical;
}

/* Codes are typed and read back character by character.
   font-family is repeated here on purpose: the `input[type=...]` selector above has the
   same specificity as `.id` and its `font: inherit` shorthand would otherwise win. */
input.id {
  font-family: var(--font-id);
  font-size: var(--step-2);
  letter-spacing: 0.16em;
  padding: 0.65rem 0.7rem;
}

/* Search-and-pick list, used where a <select> would be unusable — the product list is
   792 entries with names up to 163 characters. */
.picker {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  max-height: 16rem;
  overflow-y: auto;
  margin-top: var(--sp-2);
  background: var(--paper);
}

.picker__row {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  font-size: var(--step--1);
  transition:
    background-color var(--dur-state) var(--ease-out),
    border-color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.picker__row:last-child {
  border-bottom: none;
}

.picker__row:focus-within {
  background: var(--maroon-wash);
}

/* Hover is gated on a device that has one. A phone browser synthesises :hover on tap and
   then LEAVES IT STUCK on the last thing touched — so a picked row stayed tinted while the
   reader scrolled on, claiming to be under a pointer that does not exist. */
@media (hover: hover) {
  .picker__row:hover {
    background: var(--maroon-wash);
  }
}

/* On the engineer's phone :hover never fires and :focus-visible only lands after the tap
   has committed, so :active is the ENTIRE feedback budget — it has to be the strongest of
   the three, not the weakest. Colour first, movement second: the motion policy at the end
   of this file strips the transform, and what is left must still read as a press. */
.picker__row:active {
  background: var(--maroon-wash);
  border-bottom-color: var(--maroon-on-surface);
  transform: scale(0.99);
}

.picker__row input {
  width: auto;
  margin-top: 3px;
  flex: none;
}

/* The chosen row. There was no checked state at all: the picked row was pixel-identical
   to the other nine apart from the radio dot, on a list whose names run to 163 characters.
   Measured before the fix — the selected row and an unselected one both computed to
   `rgba(0, 0, 0, 0)`.

   Border AND background, not colour alone: the row is already tinted by :hover and
   :active, so a tint by itself would say "the pointer is here" rather than "this is the
   one". */
.picker__row:has(input:checked) {
  background: var(--maroon-wash);
  box-shadow: inset 3px 0 0 var(--maroon-on-surface);
}

/* The confirmation line above the list, naming what is chosen even when a later search has
   scrolled it out of the results. */
.picker__chosen {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin: var(--sp-2) 0 0;
  font-size: var(--step--1);
}

.picker__name {
  display: block;
  font-weight: 500;
  color: var(--ink);
}

.picker__meta {
  display: block;
  color: var(--ink-faint);
  margin-top: 2px;
}

/* A download link for an import sample. Sits beside the file input, because "what should
   my file look like?" is asked before the upload, not after the rejection report. */
.sample-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--step--1);
  color: var(--maroon-on-surface);
  text-decoration: none;
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: 1px;
}

.sample-link:hover {
  border-bottom-color: var(--maroon-on-surface);
}

.sample-link svg {
  flex: 0 0 auto;
}

/* File inputs. Photographing the invoice is the customer's main action on a phone,
   and the browser default is a ~21px-tall control with a small unstyled button —
   the smallest tap target in the whole flow, on the step that matters most. */
input[type="file"] {
  width: 100%;
  padding: var(--sp-3);
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: var(--step--1);
  color: var(--ink-soft);
  cursor: pointer;
}

input[type="file"]:hover {
  border-color: var(--maroon-on-surface);
}

input[type="file"]::file-selector-button {
  font: inherit;
  font-weight: 600;
  margin-right: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  min-height: 40px;
  border: 1px solid var(--maroon-on-surface);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--maroon-on-surface);
  cursor: pointer;
}

input[type="file"]::file-selector-button:hover {
  background: var(--maroon-wash);
}

/* The role permission grid. Checkboxes grouped into fieldsets so the list reads as a
   set of jobs, and each row is a full-width label so the whole line is tappable. */
.permset {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--sp-4);
  margin: 0;
  min-width: 0;
}

.permset__legend {
  margin: 0;
  padding: 0 var(--sp-2);
}

.permset__row {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  font-weight: 400;
  padding: var(--sp-2) 0;
  cursor: pointer;
}

.permset__row input {
  margin-top: 0.3rem;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
}

.permset__note {
  font-size: var(--step--1);
}

@media (pointer: coarse), (max-width: 640px) {
  /* A 44px row, so the whole line is a comfortable target rather than the box alone. */
  .permset__row {
    min-height: 44px;
    align-items: center;
  }
  .permset__row input {
    width: 22px;
    height: 22px;
    margin-top: 0;
  }
}

/* --- Buttons ------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  font: inherit;
  font-weight: 600;
  font-size: var(--step-0);
  padding: 0.55rem 1.1rem;
  /* 44, not 40. At 40 the computed height came out at 43px — one pixel under the 44px
     guidance, on the control a customer taps on a phone in the sun. Cheaper to fix here than
     to argue about it for every button in the app. */
  min-height: 44px;
  border-radius: var(--radius-control);
  border: 1px solid transparent;
  background: var(--maroon);
  color: var(--ink-invert);
  cursor: pointer;
  text-decoration: none;
  /* Colour and transform only. Padding, height and border-width are all left alone on
     purpose: the primary desktop user keeps a two-hundred-row queue open for hours, and a
     box-model property on a hovered control relays out the table under the cursor. */
  transition:
    background-color var(--dur-state) var(--ease-out),
    border-color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

/* Gated: a phone browser synthesises :hover on tap and leaves it stuck, so a submitted
   form's button stays in its hover colour until something else is touched. */
@media (hover: hover) {
  .btn:hover {
    background: var(--maroon-deep);
  }
}

/* The strongest of the three channels, because for the engineer on a roof it is the only
   one: :hover never fires on his phone and :focus-visible lands only after the tap has
   already committed. Colour AND movement — the motion policy at the end of this file
   removes the transform, so the colour is what has to survive on its own. */
.btn:active {
  background: var(--maroon-deep);
  border-color: var(--maroon-deep);
  transform: translateY(1px) scale(0.985);
}

/* TRANSPARENT, NOT WHITE.
   ------------------------------------------------------------------
   This was `background: var(--paper)`, which is the colour of a CARD, not of the page. So
   the same button had two appearances depending on what it was standing on: inside a card
   it disappeared into the surface behind it, and on the bare page it was a slab of pure
   white sitting a shade brighter than the warm off-white around it — which reads as a
   disabled control rather than a secondary one. Dark mode had the same fault inverted,
   --paper being lighter than --surface there too.

   Transparent means it matches whatever it is placed on, in both themes, always. The
   border and the full-contrast label are what make it a button. */
.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--rule-strong);
}

/* Hover moves the BORDER rather than the fill, for the same reason: a background tint has
   to be chosen against a known surface, and this button does not have one. */
@media (hover: hover) {
  .btn--ghost:hover {
    border-color: var(--maroon-on-surface);
    color: var(--maroon-on-surface);
  }
}

/* Past the hover tint on purpose: a ghost button's hover is a 1.5% step off --paper, and
   a press has to be unmistakable at arm's length in sun. */
.btn--ghost:active {
  background: var(--maroon-wash);
  border-color: var(--maroon-on-surface);
  color: var(--maroon-on-surface);
}

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

@media (hover: hover) {
  .btn--danger:hover {
    background: var(--danger-deep);
  }
}

.btn--danger:active {
  background: var(--danger-deep);
  border-color: var(--danger-deep);
}

.btn--lg {
  font-size: var(--step-1);
  padding: 0.7rem 1.4rem;
  min-height: 48px;
}

.btn-row {
  display: flex;
  gap: var(--sp-3);
  flex-wrap: wrap;
  align-items: center;
}

/* A button that has to sit inline in prose or a header — used where an action must be
   a POST for CSRF reasons but reads as a link to the user. */
.inline-form {
  display: inline;
}

.linkbtn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.topbar .linkbtn {
  /* Full contrast, not the faint variant. This rule overrides the inherited colour, so it is
     the one that decides whether "Sign out" is legible — and on the maroon bar the faint
     value measured 4.35:1, under AA for something a person has to find and click. */
  color: var(--on-dark);
  /* And big enough to hit. It was a 20px-tall word in the chrome: the same height as body
     text, on the one control that ends a session. */
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 var(--sp-1);
}

.topbar .linkbtn:hover {
  color: var(--ink-invert);
}

/* --- Badges ------------------------------------------------------------- */
/* 13px (--step--1), not the 11px this and six other rules hardcoded. 11px sits below the
   scale's own floor, and the reader is an engineer or a customer on a cheap phone outdoors —
   the two places in this app where small type is least affordable.

   No hover, no active, no transition — no interactive state at all. A badge is a LABEL
   sitting inside a row that is itself the target, and giving a label a hover teaches the
   reader it is something to press. It also states a verdict, and a verdict must not arrive
   gradually: read mid-crossfade, "expired" and "expiring" are the same word. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-id);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--ink-soft);
  border: 1px solid var(--rule);
  white-space: nowrap;
}

.badge--ok {
  background: var(--green-wash);
  color: var(--green-on-surface);
  border-color: var(--green);
}

.badge--warn {
  background: var(--amber-wash);
  color: var(--amber-on-surface);
  border-color: var(--amber);
}

.badge--bad {
  background: var(--danger-wash);
  color: var(--danger-on-surface);
  border-color: var(--danger);
}

.badge--info {
  background: var(--maroon-wash);
  color: var(--maroon-on-surface);
  border-color: var(--maroon-on-surface);
}

/* The queue moving under its own steam: accepted, assigned, on the way. */
.badge--ready {
  background: var(--blue-wash);
  color: var(--blue-on-surface);
  border-color: var(--blue);
}

/* A human deciding. */
.badge--review {
  background: var(--purple-wash);
  color: var(--purple-on-surface);
  border-color: var(--purple);
}

/* --- Tables ------------------------------------------------------------- */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
}

th {
  text-align: left;
  font-family: var(--font-id);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 600;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
  background: var(--surface);
}

td {
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--rule);
  vertical-align: middle;
}

tbody tr:last-child td {
  border-bottom: none;
}

/* The row under the cursor, on the busiest screen in the app. This was `--surface` on
   `--paper`: a 1.5% luminance step, which is no highlight at all on a table somebody reads
   across for a whole shift. Neutral on purpose — it says "the line you are on", not "a
   thing you can click"; that claim belongs to [data-row-href] below.
   Gated on a real pointer, because a phone leaves a synthesised :hover stuck on the last
   row touched, so a tapped row stays tinted after the reader navigates back to the list. */
@media (hover: hover) {
  tbody tr:hover {
    background: var(--rule);
  }
}

/* A row that IS a link. The whole row is the target — the ticket number inside it is a
   link too, for keyboard and for opening in a new tab, but a finger aims at the row. */
[data-row-href] {
  cursor: pointer;
  transition:
    background-color var(--dur-state) var(--ease-out),
    box-shadow var(--dur-press) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

@media (hover: hover) {
  [data-row-href]:hover {
    background: var(--maroon-wash);
  }
}

/* The press. */
[data-row-href]:active {
  background: var(--maroon-wash);
  transform: translateY(1px);
}

/* The rule down the leading edge is what a reduced-motion reader gets in place of the
   nudge — without it a press on a row would look exactly like a hover on one. It is a
   paint property rather than a border because 3px of border on a row relays out every
   column in the table, and it hangs off the first CELL rather than the row: box-shadow on
   a `<tr>` is not reliably painted once `border-collapse: collapse` is in play, which it
   is on every table here. */
[data-row-href]:active > td:first-child {
  box-shadow: inset 3px 0 0 var(--maroon-on-surface);
}

td.right,
th.right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* --- Tables on a phone -------------------------------------------------- */
/* A six-column table with inline selects and buttons — the warranty-terms and
   parts screens — cannot be read on a 375px screen, and horizontal scrolling a
   table you are also editing is worse than useless.

   `.table--stack` turns each row into a card below 720px: every cell becomes a
   labelled line, using the `data-label` the template supplies. The header row is
   hidden because each cell now carries its own label. Tables that are genuinely
   just numbers keep scrolling instead — stacking a 200-row report helps nobody. */
@media (max-width: 720px) {
  .table--stack thead {
    /* Not display:none — that removes it from the accessibility tree too. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

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

  .table--stack tr {
    padding: var(--sp-4);
    border-bottom: 1px solid var(--rule);
    /* SKIP THE ROWS NOBODY IS LOOKING AT.
       ------------------------------------------------------------------
       Once stacked, each row is a block card about 180px tall, so a 200-row queue is
       roughly thirty-six thousand pixels of layout and paint on a phone — most of it for
       rows the reader will never scroll to. `auto` lets the browser skip rendering the
       off-screen ones and pick them up as they approach the viewport.

       `contain-intrinsic-size` is what makes that safe rather than jumpy: it is the height
       to assume for a row that has not been rendered yet, so the scrollbar is roughly
       right from the start instead of shrinking as the reader scrolls into real content.

       Deliberately NOT applied to the desktop table. There the rows are still table rows,
       and a skipped row contributes nothing to column widths — so the columns would
       re-measure and jump every time one scrolled into view. Stacked rows are `display:
       block` and have no columns to disturb, which is the whole reason this is safe here
       and not there. */
    content-visibility: auto;
    /* 220px is the measured median of a stacked ticket row at 375px, not a round guess —
       the first estimate of 180px had the page growing by 200px as the reader scrolled,
       because every unrendered row was under-counted. The `auto` keyword is what stops
       that being permanent: once a row has been rendered its real height is remembered,
       so the error only ever applies to rows nobody has reached yet. */
    contain-intrinsic-size: auto 220px;
  }

  .table--stack tr:last-child {
    border-bottom: none;
  }

  .table--stack td {
    padding: var(--sp-2) 0;
    border: none;
    display: grid;
    grid-template-columns: minmax(6.5rem, 34%) 1fr;
    gap: var(--sp-3);
    align-items: baseline;
  }

  .table--stack td::before {
    content: attr(data-label);
    font-family: var(--font-id);
    font-size: var(--step--1);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }

  /* A cell holding a form or buttons gets the full width — a 34% label column
     beside a select and a number input leaves neither of them usable. */
  .table--stack td[data-span] {
    grid-template-columns: 1fr;
    gap: var(--sp-2);
    padding-top: var(--sp-3);
  }

  .table--stack td.right {
    text-align: left;
  }

  .table--stack tbody tr:hover {
    background: none;
  }
}

/* --- The lifetime record ------------------------------------------------ */
/* A stratified column: successive layers laid down on one unit over fifteen years.

   Named `.record`, and worth keeping named that. It once shared the class name `.strata`
   with a tank-wall cross-section drawn elsewhere in this file, and both definitions
   matched both consumers: the audit trail on serial_history.html picked up the
   cross-section's `background: var(--mat-barrier)` and painted a fifteen-year history on a
   near-black slab, with `overflow: hidden` clipping the markers' rings off. Two unrelated
   meanings, one name, and the later definition silently won.

   The cross-section itself has since been removed — it labelled what each layer of the
   tank did, and nothing substantiated those labels — so the collision cannot recur. The
   lesson outlives it: a name describing a SHAPE rather than a meaning will be reached for
   twice. */
.record {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
}

.record::before {
  content: "";
  position: absolute;
  left: 7px;
  top: 6px;
  bottom: 6px;
  width: 2px;
  background: var(--rule);
}

.record__item {
  display: flex;
  gap: var(--sp-4);
  padding-bottom: var(--sp-5);
  position: relative;
}

.record__marker {
  flex: none;
  width: 16px;
  height: 16px;
  margin-top: 4px;
  border-radius: 3px;
  background: var(--maroon);
  border: 3px solid var(--surface);
  box-shadow: 0 0 0 1px var(--maroon-on-surface);
  z-index: 1;
}

/* A replacement is the one event that breaks the column, so it is marked differently. */
.record__marker--swap {
  background: var(--amber);
  box-shadow: 0 0 0 1px var(--amber);
}

.record__body {
  flex: 1;
  min-width: 0;
}

/* --- Engineer, on a phone, on a roof ------------------------------------ */
/* Whole-card tap target rather than a link inside a card: gloves, sunlight, one hand. */
.job-card {
  display: block;
  text-decoration: none;
  color: inherit;
  transition:
    border-color var(--dur-state) var(--ease-out),
    background-color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

/* :focus-visible stays outside the hover gate — a keyboard user has no pointer at all,
   and this is the only cue that says which job is selected. */
.job-card:focus-visible {
  border-color: var(--maroon-on-surface);
}

@media (hover: hover) {
  .job-card:hover {
    border-color: var(--maroon-on-surface);
  }
}

/* The whole reason this card is a link rather than a link inside a card: gloves, sunlight,
   one hand. So the press has to be visible on the whole card, not on a word inside it. */
.job-card:active {
  border-color: var(--maroon-on-surface);
  background: var(--maroon-wash);
  transform: scale(0.99);
}

@media (max-width: 640px) {
  /* Larger base type and taller controls on small screens — this is read outdoors. */
  body {
    font-size: var(--step-1);
  }

  .btn {
    min-height: 48px;
  }

  /* email and password belong in this list as much as any of the others: without them
     every sign-in field in the app rendered visibly shorter than the fields beside it,
     and iOS zoomed the page the moment a password was tapped. */
  input[type="text"],
  input[type="tel"],
  input[type="email"],
  input[type="password"],
  input[type="number"],
  input[type="search"],
  input[type="date"],
  select,
  textarea {
    /* 16px minimum, or iOS zooms the whole page on focus. */
    font-size: 1rem;
    padding: 0.7rem;
    min-height: 48px;
  }

  input[type="file"] {
    font-size: 1rem;
    padding: var(--sp-4);
  }

  input[type="file"]::file-selector-button {
    min-height: 44px;
  }

}

/* --- Empty state: an invitation, not a shrug ---------------------------- */
.empty {
  text-align: center;
  padding: var(--sp-7) var(--sp-5);
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
}

.empty h2,
.empty h3 {
  /* An empty state sits under the page's h1 with nothing between, so its heading is an
     h2 by structure. It is still the quietest heading on the page, so it keeps h3's size. */
  font-size: var(--step-1);
  margin-bottom: var(--sp-2);
}

/* --- Messages ----------------------------------------------------------- */
.note {
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius);
  border: 1px solid var(--rule);
  background: var(--paper);
  border-left: 3px solid var(--ink-faint);
  margin-bottom: var(--sp-4);
  font-size: var(--step--1);
  /* The entrance lives on the CONTENT, not on the swap. htmx's own swapping and settling
     classes are configured here at 0ms and 20ms, so anything hung off them is over before
     a frame is drawn; this runs identically whether the note arrived with a full page load
     or landed in a fragment. */
  animation: note-in var(--dur-state) var(--ease-out) both;
}

/* Starts at 0.55, not at 0, on purpose. A quarter of these are `note--error` on a
   re-rendered form — the single thing the reader is waiting to read — so it has to be
   legible in the first frame. The animation is an arrival, not a reveal. */
@keyframes note-in {
  from {
    opacity: 0.55;
    transform: translateY(-3px);
  }
}

/* Same arrival with the travel taken out; selected by the motion policy at the end of
   this file. Kept as its own keyframes because a keyframe list cannot be conditioned. */
@keyframes note-in-flat {
  from {
    opacity: 0.55;
  }
}

.note--error {
  background: var(--danger-wash);
  border-color: var(--danger);
  border-left-color: var(--danger);
  color: var(--danger-on-surface);
}

.note--ok {
  background: var(--green-wash);
  border-color: var(--green);
  border-left-color: var(--green);
  color: var(--green-on-surface);
}

.note--warn {
  background: var(--amber-wash);
  border-color: var(--amber);
  border-left-color: var(--amber);
  color: var(--amber-on-surface);
}

/* --- HTMX feedback ------------------------------------------------------ */
/* A 2px progress line along the bottom edge of the top bar. The only ambient motion in
   the app, and the only thing that says "still working" on a connection where a request
   can take eight seconds.

   Positioned against the bar itself rather than at a fixed `top: 52px`. That number was a
   hand-copy of `.topbar__inner { height: 52px }`, and the coarse-pointer block raises the
   bar to 56px — so on EVERY touch device the line was drawn 4px INSIDE the maroon chrome,
   amber on maroon at 1.9:1. The one indicator in the app, invisible on the devices the app
   is used on. `.topbar` is `position: sticky`, which is a positioned element, so absolute
   children resolve against it and follow it down the page for free. */
#hx-progress {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--amber);
  /* Rests at full width and invisible, so the busy state animates OUT to a finished line
     rather than snapping away mid-draw. */
  transform: scaleX(1);
  transform-origin: left;
  opacity: 0;
  z-index: 1;
  pointer-events: none;
  /* The completion, and the only place it is expressed: when `hx-busy` comes off, the
     animation stops applying and the bar transitions from wherever it had crept to full,
     then fades. The opacity delay is what keeps the finish visible instead of dissolving
     the bar while it is still travelling. */
  transition:
    transform var(--dur-state) var(--ease-out),
    opacity var(--dur-state) linear var(--dur-state);
}

body.hx-busy #hx-progress {
  opacity: 1;
  /* Both delayed by --delay-busy: a request that comes back inside 180ms — which on a
     desk in the office is most of them — never draws anything at all. */
  animation: hx-advance var(--dur-creep) var(--ease-creep) var(--delay-busy) both;
  transition: opacity var(--dur-state) linear var(--delay-busy);
}

/* Eight seconds, not 1.6. The old curve reached 97% in under two and then sat there for
   the remaining six seconds of a rural request — a bar that has stopped moving is a bar
   that says the page has hung, which is the opposite of what it is for. --ease-creep is
   shaped so the line is still visibly advancing at the far end. */
@keyframes hx-advance {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(0.97);
  }
}

/* `.htmx-swapping` and `.htmx-added` were deleted from here. Both were dead: the vendored
   htmx is configured `defaultSwapDelay: 0` / `defaultSettleDelay: 20`, so the swapping
   class went on and came off inside one frame and the 120ms fade was truncated to 20ms.
   Neither had ever rendered. The entrance now lives on `.note` and the pending state on
   `[data-swap]`, both of which are real elements with real durations. */

/* Dim and hold. A list that is being replaced stays exactly where it is and goes quiet,
   rather than collapsing to nothing and springing back a different height — the reader
   keeps their place in a two-hundred-row queue. `pointer-events` is the other half and is
   NOT delayed: from the first millisecond a click can no longer land on a row that is
   about to be replaced by a different one. */
[data-swap] {
  transition: opacity var(--dur-state) linear;
}

/* Two selectors because htmx marks the element that MADE the request, not the one it
   fills. On all three of these screens the search form is a sibling of the list it
   replaces, which is what the second form matches; the first is what would match if any
   of them ever gained an `hx-indicator` pointing at its own target. */
[data-swap].htmx-request,
.htmx-request ~ [data-swap] {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity var(--dur-state) linear var(--delay-busy);
}

/* --- The alert strip ----------------------------------------------------- */
/* Script-raised messages — a failed upload, geolocation refused — pinned where the reader
   is rather than at the top of a page they have already scrolled past. */
#app-alert {
  position: sticky;
  top: var(--topbar-h);
  /* One below the topbar's 20. Where the two meet the bar wins: an alert painted over the
     brand and the sign-out link is a worse problem than one briefly tucked behind them. */
  z-index: 19;
}

/* The region has to stay in the DOM to be announced — one created at the moment of the
   message often is not — so it is the emptiness that is hidden, not the element. */
#app-alert:empty {
  display: none;
}

/* `.alert` is a modifier on `.note`, never a second box: the markup is
   `class="note note--error alert"`, so the border, wash, colour and the `note-in` entrance
   all arrive from `.note` and are deliberately not restated here. All this adds is room
   for the dismiss button and a layout for the two or three paragraphs inside. */
.alert {
  position: relative;
  padding-right: 52px;
}

/* The reset gives every <p> a bottom margin, which inside a three-paragraph alert leaves a
   16px gap under the last line and a strip taller than its own content. */
.alert p {
  margin: 0;
}

.alert p + p {
  margin-top: var(--sp-2);
}

/* The reference is the only handle on a 500 that the reader can quote down a phone line,
   so it is set in the identifier face like every other code in this app. */
.alert__ref {
  font-family: var(--font-id);
  letter-spacing: 0.06em;
}

/* 44px square, and absolutely positioned rather than a flex item: the message wraps to
   three lines on a 320px screen and the button has to stay in the corner it started in.
   This is a real control taken with a thumb, and the only one inside the strip. */
.alert__close {
  position: absolute;
  top: 0;
  right: 0;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  font-size: var(--step-2);
  line-height: 1;
  cursor: pointer;
  transition:
    background-color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

/* A neutral rule colour rather than the alert's own hue: it has to sit legibly on the
   danger, amber and green washes alike, and it flips with the theme for free. */
.alert__close:active {
  background: var(--rule-strong);
  transform: scale(0.92);
}

/* --- A form that is already submitting ----------------------------------- */
/* Presentation only. NEVER `disabled`: the registration form carries two submit buttons
   whose name/value says WHICH INVOICE DATE the warranty runs from, and a disabled
   submitter is dropped from the form entry list in some engines. Disabling it here would
   silently start warranties on the wrong day, which is a fifteen-year mistake made to
   prevent a double tap. `pointer-events` stops the second tap and touches nothing that is
   submitted. */
form[aria-busy="true"] button[type="submit"],
form.htmx-request button[type="submit"] {
  pointer-events: none;
  opacity: var(--disabled-opacity);
}

/* --- Error pages -------------------------------------------------------- */
.errpage {
  max-width: 34rem;
  margin: var(--sp-7) auto;
  text-align: center;
}

.errpage__code {
  font-family: var(--font-id);
  font-size: 4rem;
  font-weight: 600;
  color: var(--rule-strong);
  line-height: 1;
  letter-spacing: 0.08em;
}

.errpage__ref {
  font-family: var(--font-id);
  font-size: var(--step--1);
  color: var(--ink-faint);
  margin-top: var(--sp-5);
}

/* --- Utilities ---------------------------------------------------------- */
.stack > * + * {
  margin-top: var(--sp-4);
}

/* A label/value list for a handful of facts about one record. Collapses to stacked
   rows on a phone, where two columns would leave nothing but wrapped fragments. */
.kv {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-2) var(--sp-4);
  margin: var(--sp-4) 0;
}
.kv dt {
  font-size: var(--step--1);
  color: var(--ink-faint);
}
.kv dd {
  margin: 0;
}
@media (max-width: 480px) {
  .kv {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .kv dd {
    margin-bottom: var(--sp-3);
  }
}
.row {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  flex-wrap: wrap;
}
/* A row of fields. `center` lines up a button with an input, but with fields of different
   heights (one carries a hint, one does not) it floats the shorter one down and their labels
   stop lining up. Top alignment keeps every label on one line. */
.row--top {
  align-items: flex-start;
}
.between {
  display: flex;
  gap: var(--sp-4);
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
}
/* `!important` on the box, and only here: a hiding utility has to beat every component
   rule it lands on. `input[type="file"]` outranks a bare class, and its `width: 100%` and
   padding — resolved against the viewport, since the input is absolutely positioned — made
   each hidden photo input 1280px wide and gave a 1280px page a 2064px scroll width. */
.visually-hidden {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  border: 0 !important;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Touch targets ------------------------------------------------------ */
/* Keyed on pointer type, not viewport width. A tablet at 768px and a touch laptop
   are both operated with a finger while being wider than any phone breakpoint —
   sizing by width alone left every control on them at mouse dimensions. The
   width clause stays for narrow viewports whose browser misreports the pointer.

   Inline text links inside a sentence are deliberately excluded: WCAG 2.2 SC 2.5.8
   exempts them, and padding them out would wreck the running text. */
@media (pointer: coarse), (max-width: 640px) {
  .sample-link,
  a.btn,
  .linkbtn,
  .brand,
  .foot a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .btn {
    min-height: 48px;
  }

  /* Sign out is a real control, not a caption. At 48x20 it was the smallest hit
     area in the app and sat next to nothing else tappable. */
  .topbar .linkbtn {
    min-height: 44px;
    padding-inline: var(--sp-2);
  }

  /* The bar gets taller here, so everything that hangs off its bottom edge has to know.
     Redeclared rather than hardcoded a second time: the progress line and the alert strip
     both used to carry their own "52px" and both were wrong on every touch device. */
  :root {
    --topbar-h: 56px;
  }

  .topbar__inner {
    height: auto;
    min-height: var(--topbar-h);
    padding-block: var(--sp-2);
  }

  input[type="text"],
  input[type="tel"],
  input[type="email"],
  input[type="password"],
  input[type="number"],
  input[type="search"],
  input[type="date"],
  select,
  textarea {
    min-height: 48px;
  }

  input[type="file"]::file-selector-button {
    min-height: 44px;
  }

  /* A checkbox renders at 13px by default. Its label is a target too, so this is not a
     2.5.8 failure — but on the import screens the box is what a thumb aims at, and 13px
     is smaller than the printed dot on the form it is transcribed from. */
  input[type="checkbox"],
  input[type="radio"] {
    width: 20px;
    height: 20px;
  }

  /* The identifier IS the action: a ticket number, a warranty card number, a phone number
     to tap from a rooftop. These are a row's primary target rather than inline links in a
     sentence, so SC 2.5.8's exemption does not cover them — and at 20px tall they were the
     smallest real hit areas left in the app, on its busiest screen. */
  td.id a,
  a.id {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
}

/* --- Small-screen refinements ------------------------------------------- */
/* Form controls sized for a mouse, then relaxed for a thumb. Declared as classes
   rather than inline styles specifically so the media query below can win — an
   inline width beats any stylesheet rule and would pin these narrow on a phone. */
.ctl-type {
  width: auto;
  min-width: 8.5rem;
}

.ctl-months {
  width: 6.5rem;
}

.ctl-search {
  max-width: 24rem;
}

@media (max-width: 720px) {
  .ctl-type,
  .ctl-months,
  .ctl-search {
    width: 100%;
    max-width: none;
    min-width: 0;
  }

  /* A form inside a stacked table cell becomes a column: side-by-side selects in
     a 340px-wide card leave neither of them tappable. */
  .table--stack td[data-span] .row {
    flex-direction: column;
    align-items: stretch;
  }

  .table--stack td[data-span] .btn {
    width: 100%;
  }
}

@media (max-width: 640px) {
  /* `.between` puts a title and its action on one line. Below this width that
     squeezes both, so the action moves under the title at full width. */
  .between {
    align-items: stretch;
    flex-direction: column;
  }

  .between .btn-row {
    width: 100%;
  }

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

  /* A destructive action should never sit a thumb-width from a safe one. */
  .btn-row .btn {
    flex: 1 1 auto;
  }

  .col-form,
  .card {
    max-width: 100%;
  }

  .plate {
    width: 100%;
  }
}

/* --- Responsive --------------------------------------------------------- */
@media (max-width: 640px) {
  .topbar__inner,
  .main {
    padding-left: var(--sp-4);
    padding-right: var(--sp-4);
  }
  .main {
    padding-top: var(--sp-5);
  }
  h1 {
    font-size: var(--step-2);
  }
  /* NOTHING IN THE BAR WRAPS.
     ------------------------------------------------------------------
     At 375px the bar held a logo, "Warranty & Service", a role chip and "Sign out", and
     the last two both broke across two lines: "Sign / out" as a stacked pair, which is
     unreadable as a control and halves the width of a target that WCAG already forced to
     44px tall. The bar also grew to 100px to hold the extra lines.

     The brand is the only elastic thing here, so it is the one that gives way — truncated
     with an ellipsis rather than wrapped. Everything to the right of it is a control and
     keeps its own line. */
  .brand {
    min-width: 0;
  }

  .brand > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .topbar .linkbtn,
  .topbar__role {
    white-space: nowrap;
  }
}

/* With the account menu the signed-in bar needs 44px on the right instead of ~160. Below
   430px the brand's words still would not fit whole — a 390px phone drew "Warranty & Ser…",
   a stub that names nothing — so there the logo says P4 on its own. */
@media (max-width: 430px) {
  .topbar__inner:has(.account__btn) .brand > span {
    display: none;
  }
}

/* Very small phones — 320px is still in service across rural India. Nothing may
   overflow here; the shell padding tightens rather than the content clipping.

   This block sits BELOW the 640px one on purpose. It used to sit above it, with the same
   three selectors and the same specificity, so the wider query simply overwrote it and
   the tightening its own comment promises never once happened: a 320px screen got --sp-4
   padding like every other phone. Source order is the whole fix. */
@media (max-width: 360px) {
  .topbar__inner,
  .main {
    padding-left: var(--sp-3);
    padding-right: var(--sp-3);
  }

  .topbar__role {
    display: none;
  }
}

/* The blanket `animation-duration: 0.01ms !important` on `*` that used to sit here is
   gone. See the motion policy at the very end of this file for what replaced it and why
   it could not stay where it was. */

/* --- Pagination --------------------------------------------------------- */
/* One row, one shape, on every list. Previous and Next keep their footprint when
   they are unavailable, so the row does not resize at the first and last page and
   move the Go button out from under the cursor. */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}

.pager__count {
  margin: 0;
  font-size: var(--step--1);
  color: var(--ink-faint);
  /* "Showing 1-25 of 1,438" is a live region — it is rewritten in place on every page
     step and every filter change, and the numbers change WIDTH as they do it (9 -> 10,
     99 -> 100). In a proportional face that shifts the whole line under the reader's eye
     at the exact moment they are trying to read it. Every other number in this file that
     changes already has this; the one that changes most often did not. */
  font-variant-numeric: tabular-nums;
}

.pager__controls {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.pager__jump {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--step--1);
  color: var(--ink-faint);
  margin: 0;
}

.pager__jump input[type="number"] {
  width: 4.5rem;
  text-align: center;
  font-family: var(--font-id);
  /* Matches the button height exactly, so the row reads as one control group. */
  padding: var(--sp-1) var(--sp-2);
}

.pager__of {
  white-space: nowrap;
}

.btn.is-disabled {
  opacity: var(--disabled-opacity);
  pointer-events: none;
}

/* Previous and Next on six list screens are <span aria-disabled="true">, not buttons — a
   real button would still take focus and still be pressed. Nothing styled them, so at the
   first and last page the app said "unavailable" to assistive technology and looked
   perfectly available to everybody else. */
[aria-disabled="true"] {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
  pointer-events: none;
}

/* --- Card foot ----------------------------------------------------------- */
/* A strip of related facts under a card's main content — the live service request on a
   product card. Wraps rather than truncates: on a narrow phone the ticket number and its
   state both have to survive. */
.card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--rule);
  font-size: var(--step--1);
}

/* --- The customer's product card ---------------------------------------- */
/*
   ONE UNIT, AS ITS OWNER SEES IT.

   The staff screens and this one show the same registration and want opposite things from
   it. On a queue the serial is the subject — it is what is being read down a phone line,
   and the plate is sized accordingly. Here the owner is standing next to the tank. They do
   not need to be told which one it is; they need to know what it is, whether it is still
   covered, and whether anyone is coming.

   So this card inverts the order, and the top edge is a hairline of the brand rather than
   nothing: on a page of white boxes on cream, one warm line per card is what makes the
   page read as a single material instead of a stack of receipts. Both colours are tokens,
   so the dark theme follows without a second rule.
*/
.product-card {
  border-top: 2px solid var(--maroon-wash);
  display: flex;
  flex-direction: column;
}

/* The product name is the card's title AND its tap target, so it is a heading rather than
   the styled paragraph it was. `--step-1`, not the h2 default: it must lead the card
   without competing with the page's own h1 two inches above it. */
.product-card__name {
  font-size: var(--step-1);
  margin: 0;
}

/* Pinned to the bottom of the card, so a two-line product name in one column does not
   leave the button in the next column floating halfway up. */
.product-card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: auto;
  padding-top: var(--sp-4);
}

/* On a phone the plate takes the full width and the action sits under it. Deliberately in
   that order: the button used to be the first thing in the card, so when the row collapsed
   it arrived above the product it applied to — an answer offered before the question. */
@media (max-width: 480px) {
  .product-card__foot {
    flex-direction: column;
    align-items: stretch;
  }

  .product-card__foot .btn {
    justify-content: center;
  }
}

/* The plate as a reference line rather than a nameplate: same object, quieter, because
   here it is the answer to "which one is this" and not the subject of the card. */
.plate--sm .plate__code {
  font-size: var(--step-0);
}

.plate--sm .plate__body {
  padding: var(--sp-1) var(--sp-3);
}

/* --- The rail: where a request has got to -------------------------------- */
/*
   Four steps, from `ticket_states.journey`. A badge alone answered this in the office's
   own words — "Ready to assign" — which tells a customer nothing about whether anybody is
   coming, and invites the phone call the badge was supposed to prevent.

   Colour is never the only channel here either: each step carries its own word, the
   current one is marked `aria-current="step"`, and the marks differ in FILL as well as
   hue — done is solid, now is a ring, still-to-come is hollow and faint.
*/
.journey {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
  gap: var(--sp-2);
}

.journey__step {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--step--1);
  color: var(--ink-faint);
  /* The connector, drawn as a border on the step rather than as an extra element: it has
     to stop at the last step, and `:last-child` can say that where a separate node cannot. */
  position: relative;
}

.journey__step::before {
  content: "";
  position: absolute;
  top: 6px;
  left: 50%;
  right: -50%;
  height: 2px;
  background: var(--rule);
}

.journey__step:last-child::before {
  display: none;
}

.journey__mark {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid var(--rule-strong);
  background: var(--paper);
  position: relative;
  z-index: 1;
}

.journey__label {
  text-align: center;
  text-wrap: balance;
}

/* Behind us. */
.journey__step.is-done {
  color: var(--ink-soft);
}
.journey__step.is-done .journey__mark {
  background: var(--green);
  border-color: var(--green);
}
.journey__step.is-done::before {
  background: var(--green);
}

/* In hand. The one step that is louder than the rest, because it is the answer. */
.journey__step.is-now {
  color: var(--maroon-on-surface);
  font-weight: 600;
}
.journey__step.is-now .journey__mark {
  border-color: var(--maroon-on-surface);
  box-shadow: 0 0 0 3px var(--maroon-wash);
}

/* Stopped: cancelled or refused. Struck through rather than left pending, so the rail does
   not read as though somebody is still working on it. */
.journey__step.is-stopped {
  color: var(--ink-faint);
  text-decoration: line-through;
}

/* The rail arrives one step at a time, left to right — the only place `--stagger` is used
   outside the layer stack, and the only sequence in the customer's half of the app.
   `--i` is the step's index, set inline by the template.

   An opt-IN rather than something the reduced-motion policy has to undo: where the
   movement IS the effect, declaring it inside `no-preference` is the shape that cannot be
   got wrong later. */
@media (prefers-reduced-motion: no-preference) {
  .journey__step {
    animation: journey-in var(--dur-enter) var(--ease-out) both;
    animation-delay: calc(var(--i, 0) * var(--stagger));
  }
}

@keyframes journey-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --- Intake photos ------------------------------------------------------- */
/* The customer's three claim photos. Built as a fixed-ratio frame per shot so the grid
   keeps its shape whether a photo is there or not — a layout that reflows as each upload
   lands makes the remaining tap targets move under the thumb. */
.steps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  list-style: none;
  margin: 0 0 var(--sp-4);
  padding: 0;
}

.steps__item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--step--1);
  color: var(--ink-faint);
}

.steps__item.is-done {
  color: var(--green-on-surface);
}

.steps__dot {
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  border: 1px solid var(--rule-strong);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  flex: none;
}

.steps__item.is-done .steps__dot {
  background: var(--green);
  border-color: var(--green);
  color: var(--ink-invert);
}

.shots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: var(--sp-4);
}

.shot {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
}

.shot--done {
  border-color: var(--green);
}

.shot__head {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
}

.shot__no {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  border: 1px solid var(--rule-strong);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  flex: none;
}

.shot--done .shot__no {
  background: var(--green);
  border-color: var(--green);
  color: var(--ink-invert);
}

.shot__title {
  margin: 0;
  font-weight: 600;
}

.shot__hint {
  margin: var(--sp-1) 0 0;
  font-size: var(--step--1);
  color: var(--ink-faint);
}

/* 4:3 whatever the photo is, so nine cards on a page do not each pick their own height. */
.shot__frame {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  border: 1px dashed var(--rule-strong);
}

.shot--done .shot__frame {
  border-style: solid;
  border-color: var(--rule);
}

.shot__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.shot__empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: var(--sp-2);
  color: var(--ink-faint);
  font-size: var(--step--1);
}

/* The label IS the button: a browser's own file control cannot be styled. */
.shot__action {
  display: block;
  width: 100%;
  text-align: center;
  cursor: pointer;
  margin-top: auto;
}

/* It carries `.btn--ghost` too, so it already presses. This adds to that rather than
   competing with it: pressing this one opens the camera, outdoors, with the tank in the
   other hand, and a label has no `:active` of its own to fall back on if the tap is
   swallowed by a scroll. */
.shot__action:active {
  border-color: var(--maroon-on-surface);
  box-shadow: inset 0 0 0 1px var(--maroon-on-surface);
}

.shot__busy {
  display: block;
  margin-top: var(--sp-2);
  font-size: var(--step--1);
  color: var(--ink-faint);
}

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

/* .visually-hidden already exists above — not redeclared. */

/* The only disabled rule for buttons. There used to be a `.btn:disabled` up in the button
   block as well — identical specificity, earlier in the file, so it never applied to
   anything and its 0.5 was simply a number nobody could see. */
.btn[disabled] {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
}

/* --- Two doors to one account -------------------------------------------- */
/* The customer sign-in offers a phone code and an email password. Both panels are
   visible until the script hides one, so a browser with no JavaScript gets a working
   page with two forms rather than one dead tab. */
.doors {
  margin-top: var(--sp-5);
}

.doors__tabs {
  display: flex;
  gap: var(--sp-1);
  border-bottom: 1px solid var(--rule);
}

.doors__tab {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: var(--sp-3) var(--sp-4);
  font: inherit;
  font-weight: 600;
  color: var(--ink-faint);
  cursor: pointer;
  /* Reserves the weight the selected state adds, so the row does not shift on switch. */
  margin-bottom: -1px;
  transition:
    color var(--dur-state) var(--ease-out),
    border-color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

/* Left ungated deliberately, along with the other five colour-only hovers on chrome: a
   stuck colour on a tab is not a stuck claim about a target, and rewriting twelve rules
   where six needed it is how a polish pass becomes a rewrite. */
.doors__tab:hover {
  color: var(--ink);
}

/* Choosing between a phone code and an email password is the first thing a customer does
   on this screen, and on their phone the press is the only confirmation they get that the
   tap landed on the tab rather than beside it. */
.doors__tab:active {
  color: var(--maroon-on-surface);
  border-bottom-color: var(--rule-strong);
  transform: translateY(1px);
}

.doors__tab.is-on {
  color: var(--maroon-on-surface);
  border-bottom-color: var(--maroon-on-surface);
}

/* The panel had no top padding at all, so the lede sat welded to the tab rule — the one
   line explaining which door you are standing in front of, printed as if it were part of
   the border above it. --sp-5, matching the gap between the heading and the tabs, so the
   three bands of this screen are evenly spaced rather than two of them touching. */
.doors__panel {
  padding-top: var(--sp-5);
}

/* Only for the no-JS case, where both doors are shown at once. Without it the second
   panel's lede ran straight into the first panel's form and the page read as one long form
   with a stray sentence in the middle of it.

   `:not(.doors__panel--off) +` is what keeps it there. `display: none` does not remove an
   element from sibling selectors, so a bare `+` still matched once the script hid the
   first panel — and the email door then carried 64px of separator from a neighbour that
   was not on screen, giving the two tabs visibly different spacing. */
.doors__panel:not(.doors__panel--off) + .doors__panel {
  margin-top: var(--sp-6);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--rule);
}

/* The lede carries `mt-0` in the markup to kill the paragraph margin; the panel's own
   padding is what spaces it now. Its bottom margin is what separates it from the form. */
.doors__panel > .lede {
  margin-bottom: var(--sp-4);
}

.doors__panel--off {
  display: none;
}

/* ==========================================================================
   Dark theme
   ==========================================================================
   Only colour tokens are redefined; every rule in this stylesheet is written
   against them, so nothing below the token block needed touching.

   Two states, set by theme.js before first paint: light, the default everywhere, and
   data-theme="dark" when the reader has chosen it. There is no "follow the operating
   system" case any more — the product defaults to light whatever the phone prefers — so
   the prefers-color-scheme copy of this block that used to sit here was removed: it could
   never apply once theme.js always set the attribute.

   Roles that do NOT flip are as important as the ones that do — see the notes
   on --ink-invert and --chrome. Inverting a token used as a background is how a
   dark theme hides content, and this file had three such backgrounds. */

:root[data-theme="dark"] {
  /* Tell the UA as well as this stylesheet, or the date picker, <select> popups,
     scrollbars and autofill wash all still render light. */
  color-scheme: dark;

  /* Text. --ink-invert and --on-dark are NOT flipped: they are the colours of text on a
     filled or deliberately dark thing (a maroon button, the top bar, the serial plate), and
     those stay dark in both themes. */
  --ink: #f2e9ea;
  --ink-soft: #cfbdc1;
  --ink-faint: #a38f94;

  /* Warm, not neutral: the same trace of maroon the light theme carries, so the page still
     reads as one material. Paper sits ABOVE surface so a card lifts off the page, which is
     the opposite direction from light mode and the reason cards need their own token. */
  --surface: #171113;
  --paper: #1f1719;
  --rule: #342a2c;
  --rule-strong: #4d3d41;
  /* Deepened rather than brightened: the same maroon that carries a light page glares above
     a near-black one, and a top bar is furniture, not a focal point. */
  --chrome: #4e121b;
  /* Below --surface, so the plate still reads as a plate and not as a card. */
  --plate: #0e0a0b;

  /* Brand, in its two roles. The fill keeps enough saturation to look like an action and
     carries white text at ~7:1; the surface-reading value lifts to ~7:1 against --surface,
     which #7b1e2b cannot do at 1.9:1. */
  --maroon: #93293a;
  --maroon-deep: #7d2131;
  --maroon-on-surface: #e79aa6;
  --maroon-wash: #2b1a1e;

  /* Lifted from #2f9160, which measured 4.47:1 on --paper and so missed AA by a hair. */
  --green: #3aa873;
  --green-wash: #16281f;

  /* Lifted well past the light theme's #8c5200, which is invisible on near-black. */
  --amber: #d59a3c;
  --amber-wash: #2b2010;

  --danger: #d95a2a;
  --danger-deep: #b8481f;
  --danger-wash: #2e1710;

  --focus: #e79aa6;
  --focus-on-chrome: #ffffff;

  /* Lifted, not deepened — the whole reason these tokens exist. Each clears AA against both
     its own wash and --paper. */
  --green-on-surface: #5cc98f;
  --amber-on-surface: #e0ad57;
  --danger-on-surface: #f0845a;

  /* Work in hand, and work being judged. Same treatment as the three above: the FILL keeps
     enough saturation to read as a filled thing and carries white text (4.93:1 and 5.74:1),
     while the surface-reading value is LIFTED, not deepened — the light theme's #1a4e8a and
     #5b3488 are all but invisible on near-black. 8.32:1 and 8.16:1 on --paper. */
  --blue: #2f72bd;
  --blue-wash: #131c28;
  --blue-on-surface: #8ab6e8;

  --purple: #7d4fb5;
  --purple-wash: #1e1628;
  --purple-on-surface: #c4a3e8;

  /* Unchanged between themes. It only ever sits on the brand chrome, which is dark in both,
     so flipping it would be inverting a token against a background that never inverted —
     the mistake the note at the top of this section is about. 7.12:1 on the dark chrome. */
  --accent: #e3a87c;
  --accent-faint: #c98a5d;

  /* Deeper and wider than the light pair: the same spread that reads as a soft lift on cream
     is invisible on near-black, and lightening the shadow instead would make it a glow. */
  --shadow-card: 0 1px 2px rgb(0 0 0 / 28%), 0 2px 10px rgb(0 0 0 / 24%);
  --shadow-raised: 0 2px 6px rgb(0 0 0 / 34%), 0 10px 30px rgb(0 0 0 / 40%);

  /* Flipped to white alpha: a black hairline on a dark card is the card, and the edge
     disappears exactly where a photograph needs it most. Still neutral — see the light
     value for why this one token does not take the warm tint the shadows do. */
  --edge-image: rgb(255 255 255 / 12%);

  /* Materials are the colours of an actual cut tank wall, so they do NOT change with the
     theme — foam is the colour foam is. Only the two near-blacks are nudged so the section
     does not disappear into the page behind it. */
  --mat-barrier: #202325;
}

/* --- Theme control ------------------------------------------------------- */
/* Three segments in the top bar: light, system, dark. Sits on the chrome, so its colours
   come from the --on-dark pair rather than from --ink, and it looks the same in both
   themes because the bar it sits on does. */
.theme {
  display: inline-flex;
  gap: 1px;
  padding: 2px;
  /* CONCENTRIC. The track was --radius (3px) around 2px segments with 2px of padding
     between them, which is the concentric relationship off by one — an outer radius has
     to be the inner radius PLUS the padding for the two curves to stay parallel, and
     3 != 2 + 2. At this size the error shows as a hairline of track that pinches at each
     corner. --radius-control (10px) over 8px segments states the same relationship
     correctly, using a token the file already owns rather than a fourth literal. */
  border-radius: var(--radius-control);
  background: rgb(255 255 255 / 8%);
  margin-right: var(--sp-4);
  /* Never shrunk. As an ordinary flex item it was the first thing the signed-in bar
     squeezed when the role chip and Sign out arrived: 109px down to 66px on a 375px phone,
     three 20px buttons narrower than their own icons, which drew as an empty pill. The
     brand text is the elastic part of this bar — see the 640px block. */
  flex: none;
}

.theme__btn {
  appearance: none;
  border: 0;
  background: none;
  color: var(--on-dark-faint);
  padding: 5px 10px;
  border-radius: 8px;
  cursor: pointer;
  display: grid;
  place-items: center;
  /* WHY THIS IS 44 AND NOT 28.
     ------------------------------------------------------------------
     It was 28px, on the argument that this is chrome "used once and then never again"
     and so could yield space to the customer's own name. The first half is true; the
     second half never was. The bar is --topbar-h — 52px, and 56px on a coarse pointer —
     so the 16px this control gave back vertically was given back to nothing. It was a
     small target sitting in the middle of a large empty strip.

     Horizontal is the real constraint, and it is why this is 44 tall rather than 44
     square: three segments cannot each be 44px wide on a 375px bar without pushing the
     sign-out block off the end, and expanding them with a pseudo-element would leave
     three OVERLAPPING hit areas, which is worse than a small one — a press would land on
     whichever of two segments won the stacking order. So the height is taken in full, the
     width as far as the bar measures out, and `:active` below keeps doing the work the
     width still cannot. */
  min-height: 44px;
  transition:
    color var(--dur-state) var(--ease-out),
    background-color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.theme__btn:hover {
  color: var(--on-dark);
  background: rgb(255 255 255 / 10%);
}

/* Full height now, but still narrow, so the press keeps doing the confirming that the
   target WIDTH cannot: at this size a thumb often cannot tell which of the three segments
   it hit, and the answer has to be visible without moving the thumb to look. */
.theme__btn:active {
  color: var(--ink-invert);
  background: rgb(255 255 255 / 22%);
  transform: scale(0.9);
}

.theme__btn[aria-pressed="true"] {
  background: rgb(255 255 255 / 16%);
  color: var(--ink-invert);
}

/* The theme control used to be hidden below 34rem, so that "the customer's name and the
   sign-out link win the space on a phone". That reasoning outlived its own premise. The
   name is hidden from 640px by `.topbar__who strong` above, so by the width this rule
   fired at — 544px — the thing it was yielding space to had already been gone for 96px,
   and the control was being hidden to make room for nothing.

   Measured at 375px with it shown: brand 62px, control 54px, sign-out block 163px, 279 of
   375 used and no overflow. A phone is also where the dark theme is worth most, being
   where people read in the dark. */

/* --- Card alignment inside gap-based layouts ----------------------------- */
/* `.card + .card` adds a top margin so cards STACKED in normal flow are spaced. Inside a
   container that already spaces its children with `gap`, that margin is not just redundant —
   it pushes every card after the first down by 16px AND shortens it by 16px, because a grid
   item's stretched height is the row height minus its own margins.
   
   That is what made a row of three cards read as misaligned and unequal: the first was
   157px tall and the other two were 141px, sitting 16px lower. */
.grid > .card + .card,
.row > .card + .card,
.between > .card + .card,
.shots > .card + .card {
  margin-top: 0;
}

/* Cards in a grid are laid out as columns so their insides line up too, not merely their
   outlines: a title sits at the top of every card and anything marked `margin-top: auto`
   (a button, a footnote) sits on a common baseline at the bottom. */
.grid > .card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

.footnote {
  margin-top: var(--sp-5);
  font-size: var(--step--1);
  max-width: 58ch;
}

/* ==========================================================================
   Spacing and size utilities
   ==========================================================================
   Deliberately last in the file, and deliberately small.

   These exist to absorb 215 inline `style=` attributes spread across 46 templates,
   of which 136 were margins. That was never 136 decisions — it was one missing
   utility, copied by hand until "margin-top: var(--sp-4)" appeared 25 times and
   nobody could change the rhythm of the app without a find-and-replace.

   Two rules govern what belongs here:

   1. Only values that were ALREADY being written inline, and written more than
      once. A utility per unique value would be a worse system than the inline
      styles it replaced. Genuinely one-off geometry — `flex: 1 1 12rem`,
      `margin: var(--sp-6) auto` — stays inline, where its specificity is honest.

   2. `.flush` is declared FIRST so `class="flush mt-3"` reproduces
      `margin: var(--sp-3) 0 0` exactly. That composition is load-bearing: the
      reset gives every <p> a bottom margin (`p { margin: 0 0 var(--sp-4) }`), so
      `margin-top` alone is NOT the same declaration and would add trailing space
      to sixty paragraphs.

   Position matters as much as content. Inline styles beat every selector; a single
   class does not. Sitting at the end of the file is what lets `.flush` win against
   the component rules it is replacing an inline override of. */

.flush {
  margin: 0;
}

.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--sp-1); }
.mt-2 { margin-top: var(--sp-2); }
.mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }
.mt-6 { margin-top: var(--sp-6); }
.mt-7 { margin-top: var(--sp-7); }

.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--sp-1); }
.mb-4 { margin-bottom: var(--sp-4); }
.mb-5 { margin-bottom: var(--sp-5); }

/* The one type utility here. 36 inline copies of it, always for the same reason:
   secondary text beside something more important. */
.text-sm {
  font-size: var(--step--1);
}

.w-full {
  width: 100%;
}

/* Photos carry width/height attributes so the browser reserves the right box before the
   image arrives — the page no longer jumps as each one lands. Those attributes also set an
   intrinsic height, which `width: 100%` would otherwise fight, so the ratio is restored
   here rather than by dropping the attributes. */
img.w-full {
  height: auto;
}

/* --- A card whose title is a tap target ---------------------------------- */
/* On the customer's product card the title link was 20px tall on a desktop, because an
   inline link is exactly as tall as its text. It stretched to full width on a phone and so
   already passed there — which is why this was a desktop-only failure and worth fixing
   without disturbing the mobile layout that was already fine. */
.card__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: inherit;
  transition:
    color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

/* The title of the customer's own product card, so this is the tap that opens their tank.
   Colour rather than a background tint: the target is a line of text inside a card, and
   tinting it would look like a selection the reader then has to clear. */
.card__link:active {
  color: var(--maroon-on-surface);
  transform: scale(0.98);
}

/* ==========================================================================
   Between pages
   ==========================================================================
   `hx-boost` is used exactly nowhere in this app, so every move from one screen to the
   next is a full document load — and two consecutive screens here are ninety per cent the
   same chrome. What the reader sees between them is a white flash, which reads as the page
   having been thrown away and rebuilt, because it has.

   Naming the persistent furniture is the whole difference between "a page transition" and
   "an app": the bar, the section tabs and the footer are carried across instead of being
   destroyed and recreated, and only the content crossfades.

   Everything here degrades to nothing where it is not supported — an unknown at-rule and
   unknown pseudo-elements are dropped at parse time, so the browsers that do not have it
   get exactly the behaviour they have today. */
@view-transition {
  navigation: auto;
}

/* The UA default is around 250ms, which is sludge on a queue an admin opens forty tickets
   from and comes back to forty times. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-state);
}

/* The furniture, and ONLY the furniture. Rows, cards, plates and identifiers are
   deliberately left unnamed for two reasons. A name must be unique per document and a
   duplicate aborts the ENTIRE transition — serial_history.html already prints the same
   ticket number twice on one page. And naming them would put the app's most important
   text, the identifier itself, into motion, which is the one thing that must sit still. */
.topbar {
  view-transition-name: topbar;
}

/* NOT `.sidebar` unconditionally, and that is the whole point of the media query.

   Below 60rem the sidebar is not a column at all: it is a fixed, full-height, off-canvas
   drawer over the page, and on a phone it is also the ONLY way to reach Sign out. So the
   snapshot taken when staff signed out was the drawer they had just opened — a full-height
   slab of `--chrome` — and it is a view-transition group, which paints in the TOP LAYER,
   above the new document. The page it transitions to has no sidebar at all, so the group
   is exit-only and that slab is what covered the sign-in form. Intermittently, because it
   only resolves itself if the transition runs to completion: a second tap, a slow reply or
   the 4s capture timeout leaves the last painted frame on screen, which is a black screen.

   Above 60rem the sidebar is a static 248px column in the grid, so an exit fade of it is
   just the column fading, which is the effect this was written for. */
@media (min-width: 60.0625rem) {
  .sidebar {
    view-transition-name: nav;
  }
}

.foot {
  view-transition-name: foot;
}

/* `.main` is deliberately unnamed, so it crossfades as part of root. */

/* ==========================================================================
   Motion policy: reduced motion
   ==========================================================================
   This replaces a blanket `animation-duration: 0.01ms !important` on `*`, which was the
   wrong tool twice over. It killed opacity and colour crossfades, which the guidance
   explicitly permits and which are the only press feedback left once transforms are gone.
   And by flattening animations rather than removing them it landed `hx-advance` on its
   final keyframe instantly, parking the progress line at 97% for the whole of a request —
   the one element whose entire job is to say "still working" telling every reduced-motion
   user that the app had hung.

   The policy is: suppress MOVEMENT — transforms, travel, sequences — and leave changes of
   colour and opacity alone. A press still reads, a note still arrives, the queue still
   dims; nothing crosses the screen.

   It sits last in the file because it must come after every rule it neutralises, and the
   press states it undoes are declared with their components, where they belong. Nothing
   here uses !important; source order is doing the work, the way it is everywhere else in
   this file. */
@media (prefers-reduced-motion: reduce) {
  /* Every press keeps its colour and loses its travel. This is exactly why each :active
     rule above pairs a transform with a colour change: what is left here has to still be
     a legible press on the engineer's phone, where :active is the only channel there is. */
  .btn:active,
  .btn--ghost:active,
  .btn--danger:active,
  [data-row-href]:active,
  .card__link:active,
  .job-card:active,
  .picker__row:active,
  .doors__tab:active,
  .theme__btn:active,
  .alert__close:active {
    transform: none;
  }

  /* The live photo stops drifting. The FILTER stays on: since the `<animate>` came out of
     `causticRipple` the ripple is a still distortion, and a still distortion is a picture,
     not movement. It used to be dropped here as well because CSS cannot pause SMIL and the
     shimmer had to go somehow; there is no longer any shimmer to stop.

     Only the slow zoom goes, which is the whole of what this setting is asking for. */
  .split__water {
    animation: none;
  }

  /* The nav icons stop turning. The active item keeps its filled block and its accent
     edge, and a hovered one keeps its tint, so both states are still stated — the spin was
     always decoration on top of a signal that does not need it. */
  .sidebar__item svg,
  .sidebar__item:hover svg {
    transition: none;
    transform: none;
  }

  .sidebar__item[aria-current="page"] svg {
    animation: none;
  }

  /* A bar growing across a fifteen-year window is travel, not a crossfade. This used to be
     stated twice — once here and once in a scoped block beside `.cover` itself — because
     the blanket rule made the scoped one look redundant. It is stated once now. */
  .cover__fill {
    transition: none;
  }

  /* The layer-stack stagger is NOT undone here: it is declared inside a
     `@media (prefers-reduced-motion: no-preference)` opt-in at its own site, which is the
     better shape wherever the movement is the entire effect. Nothing to switch off. */

  /* The same arrival, without the 3px drop. The opacity half is kept: it is what makes a
     re-rendered form's error read as new rather than as having always been there. */
  .note {
    animation-name: note-in-flat;
  }

  /* A discrete indicator in place of the creep: the line is either there or it is not,
     it appears after --delay-busy and it fades on completion. Emphatically NOT a
     near-zero duration, which is what left the old policy showing a bar frozen at 97%. */
  body.hx-busy #hx-progress {
    animation: none;
  }

  /* The page transition above is deliberately NOT disabled here. Its default is an opacity
     crossfade, which is a change rather than a movement, and it is the thing that stops
     two near-identical pages flashing white between them.
     Any future CUSTOM ::view-transition-* animation — a slide, a scale, anything with a
     transform in it — must add its own branch here. */
}

/* ==========================================================================
   The back-office shell: a sidebar, not a tab strip
   ==========================================================================
   From P4 reference screen 2. The horizontal underline nav it replaces held up to twelve
   items for an admin — My jobs, Service requests, Register, Invoice checks, Products,
   Warranty terms, Parts, Card numbers, Staff, Roles, Outbox, Reports — in a strip that
   scrolled sideways. Items past the seventh were reachable only by dragging a scrollbar
   most people never saw, which is a poor place to keep Reports.

   Staff only. Customers and the public keep the top bar: they have no nav_items at all, and
   a 248px column of empty chrome beside a registration form is worse than no column at all.
   base.html branches on nav_items for exactly this reason. */

.appshell {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
}

/* minmax(0, 1fr) above rather than 1fr: a grid item's default min-width is auto, so one
   wide table would push the whole column past the viewport and take the sidebar with it. */
.appshell__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.sidebar {
  background: var(--chrome);
  color: var(--on-dark);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  /* The nav can outgrow a laptop screen; the brand and the profile block must not scroll
     away with it, so only the middle section scrolls. */
  overflow: hidden;
}

.sidebar :focus-visible {
  outline-color: var(--focus-on-chrome);
}

.sidebar__brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-4);
  color: var(--ink-invert);
  text-decoration: none;
  flex: 0 0 auto;
}

.sidebar__brand img {
  height: 34px;
  width: auto;
  border-radius: var(--radius-control);
}

.sidebar__brandtext {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}

.sidebar__brandtext strong {
  font-weight: 700;
  font-size: var(--step-1);
}

.sidebar__brandtext span {
  font-size: var(--step--1);
  color: var(--on-dark-faint);
}

.sidebar__nav {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--sp-2) var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sidebar__item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* 44px, the same floor every other target in this app is held to. */
  min-height: 44px;
  padding: 0 var(--sp-3);
  border-radius: var(--radius-control);
  color: var(--on-dark);
  text-decoration: none;
  font-size: var(--step-0);
  font-weight: 500;
  white-space: nowrap;
  /* Colour only. This column is fixed beside a queue an admin holds open for hours, and a
     box-model transition here would relay the table out on every pointer crossing. */
  transition: background-color var(--dur-state) var(--ease-out);
}

.sidebar__item svg {
  flex: 0 0 auto;
  /* Transform only. This column sits beside a queue an admin holds open for hours, and the
     icon is the one thing here that can move without relaying anything out: a transform is
     composited, so a spin costs no layout and cannot shift the label beside it. */
  transition: transform var(--dur-enter) var(--ease-out);
  /* The peach accent, which exists only on the dark chrome — see the token note. It is the
     one thing in the reference screens that makes an icon column read as brand rather than
     as decoration. */
  color: var(--accent);
}

@media (hover: hover) {
  .sidebar__item:hover {
    background: rgb(255 255 255 / 10%);
  }

  /* A full turn, and back again on the way out. Gated behind (hover: hover) with the tint
     above for the same reason: a phone browser synthesises :hover on tap and leaves it
     stuck, which would park one icon mid-spin until something else was touched. */
  .sidebar__item:hover svg {
    transform: rotate(360deg);
  }
}

.sidebar__item:active {
  background: rgb(255 255 255 / 16%);
}

/* The active item. A filled block plus a solid left edge, not colour alone: on the maroon
   chrome the difference between "current" and "hovered" is a few percent of alpha, and the
   edge is what survives a cheap phone screen in sunlight. */
.sidebar__item[aria-current="page"] {
  background: rgb(255 255 255 / 14%);
  color: var(--ink-invert);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent);
}

.sidebar__item[aria-current="page"] svg {
  color: var(--ink-invert);
  /* The arrival, not a held pose: a permanent rotate(360deg) renders identically to no
     rotation at all, so the current item settles into place once when the page loads and
     then sits still. Half a turn rather than a full one — this is a landing, and a whole
     spin on every navigation would be the page showing off. */
  animation: nav-settle var(--dur-enter) var(--ease-out);
}

@keyframes nav-settle {
  from {
    transform: rotate(-180deg);
  }
  to {
    transform: none;
  }
}

/* The profile block at the foot of the column, per the reference. */
.sidebar__profile {
  flex: 0 0 auto;
  padding: var(--sp-4);
  /* The peach rule from the reference, not the white hairline this used to carry. It is
     the one horizontal line in the column, so it reads as the brand closing the panel
     rather than as another divider between nav items. --accent is legible here for the
     same reason the item icons use it: it exists only on the maroon chrome. */
  border-top: 1px solid var(--accent);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.sidebar__avatar {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgb(255 255 255 / 16%);
  color: var(--ink-invert);
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: var(--step--1);
  letter-spacing: 0.04em;
}

.sidebar__whoami {
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}

.sidebar__name {
  color: var(--ink-invert);
  font-weight: 600;
  font-size: var(--step-0);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- The utility bar ---------------------------------------------------- */
/* Sticky, so the theme control and the breadcrumb stay reachable down a long queue. */
.utilitybar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-5);
  min-height: var(--topbar-h);
}

.utilitybar__where {
  font-size: var(--step--1);
  color: var(--ink-faint);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.utilitybar__where strong {
  color: var(--ink);
  font-weight: 600;
}

.utilitybar__spacer {
  flex: 1;
}

/* The theme control is drawn for the maroon chrome. In the utility bar it sits on paper, so
   the segments need the page's own ink instead — otherwise it is --on-dark on white. */
.utilitybar .theme {
  background: var(--surface);
  border-color: var(--rule);
}

.utilitybar .theme__btn {
  color: var(--ink-soft);
}

.utilitybar .theme__btn[aria-pressed="true"] {
  background: var(--paper);
  color: var(--ink);
}

/* The role chip, restyled for paper.
   ------------------------------------------------------------------
   .topbar__role is drawn for the maroon chrome: --on-dark text inside a translucent WHITE
   border. Moving it into this bar put both on a white surface, where the text measured
   1.36:1 and the border was not there at all — "ADMIN" was legible only as a faint pink
   smudge. It reads correctly in dark mode, which is what kept it hidden.

   --maroon-on-surface is the token for exactly this: the brand used AS text against the
   page rather than as a fill behind white. 10.18:1 on paper in light, 8.01:1 in dark. */
.utilitybar .topbar__role {
  color: var(--maroon-on-surface);
  background: var(--maroon-wash);
  border-color: color-mix(in srgb, var(--maroon-on-surface) 28%, transparent);
}

/* The drawer trigger. Hidden until the sidebar stops being a column. */
.drawer-toggle {
  display: none;
}

/* --- The sidebar on a phone --------------------------------------------- */
/* 60rem, the same width the grid caps switch at, so the whole layout changes character at
   one place rather than at three. */
@media (max-width: 60rem) {
  .appshell {
    grid-template-columns: minmax(0, 1fr);
  }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    width: min(88vw, var(--sidebar-w));
    z-index: 40;
    box-shadow: var(--shadow-raised);
    /* Off-canvas by transform, not by display, so opening it can be animated without
       relaying the page out. The [hidden] rule below is what keeps it out of the tab
       order while closed — a drawer that is merely translated off-screen still takes
       focus, so a keyboard user tabs through twelve invisible links. */
    transform: translateX(-100%);
    transition: transform var(--dur-enter) var(--ease-out);
  }

  /* The one `will-change` in the file, and it is scoped to the open state on purpose.
     A permanent `will-change: transform` is a permanent promotion to its own compositor
     layer — real memory, held for a 248px column that is off-screen on every page load and
     opened on almost none of them. Attached to the open attribute, the hint arrives with
     the class that starts the slide and leaves with the one that ends it. */
  .sidebar[data-open="true"] {
    transform: translateX(0);
    will-change: transform;
  }

  .sidebar[hidden] {
    display: none;
  }

  .drawer-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    flex: 0 0 auto;
    border: 1px solid var(--rule);
    border-radius: var(--radius-control);
    background: var(--paper);
    color: var(--ink);
    cursor: pointer;
  }

  .drawer-scrim {
    position: fixed;
    inset: 0;
    z-index: 39;
    background: rgb(36 26 28 / 48%);
    border: 0;
    cursor: pointer;
  }
}

@media (min-width: 60.0625rem) {
  /* The drawer markup is inert on a desktop: the column is always visible, so the [hidden]
     the toggle manages must not be allowed to remove it. */
  .sidebar[hidden] {
    display: flex;
  }

  .drawer-scrim {
    display: none;
  }
}

/* In the sidebar layout the content column is already bounded by the grid, so the shell
   max-width would centre it a second time and leave the queue floating. */
.appshell .main {
  max-width: none;
}

/* ==========================================================================
   The split hero: a dark brand panel beside the thing you came to do
   ==========================================================================
   From P4 reference screen 1, and used on the pages a stranger arrives at with one task —
   registration and the two sign-in doors. One column below 60rem, and the brand panel goes
   SECOND there: on a phone the form is what the visitor came for, and making them scroll
   past a headline and three trust badges to reach it is how a mobile funnel leaks. */

.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}

@media (min-width: 60rem) {
  .split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 0;
    /* One card split down the middle, rather than two cards side by side. */
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-card);
    overflow: hidden;
    border: 1px solid var(--rule);
  }

  .split > .split__brand,
  .split > .split__work {
    border-radius: 0;
    border: 0;
    box-shadow: none;
    height: 100%;
  }
}

.split__brand {
  /* THE GRADIENT, from the reference.
     ------------------------------------------------------------------
     Three layers: a warm highlight up and left, a deep pool down and right, and the brand
     ramp underneath carrying them. Not a photograph — the caustics come from the ::before
     below, which is generated rather than fetched, so there is no second request and
     nothing to go missing on a slow connection.

     Every stop was measured against the text that sits on it before being chosen. The
     lightest, #9c2839, carries white at 7.56:1 and --on-dark at 5.57:1; the darkest is
     16.65:1. That is the difference between a gradient and a photograph: a gradient has a
     lightest point, and it can be checked once and trusted. */
  background:
    radial-gradient(115% 85% at 16% 20%, rgb(156 40 57 / 92%) 0%, rgb(156 40 57 / 0%) 62%),
    radial-gradient(95% 105% at 88% 92%, rgb(45 9 16 / 88%) 0%, rgb(45 9 16 / 0%) 68%),
    linear-gradient(152deg, #8b2233 0%, #6e1b2e 46%, #4a0f1c 100%);
  color: var(--on-dark);
  border-radius: var(--radius-card);
  /* --sp-7 down the sides as well as top and bottom. The panel is the one place in this
     app with nothing to fit into a queue, and the reference gives it room accordingly:
     tight padding is most of what made it read as a form with a coloured background
     rather than as a cover. */
  padding: var(--sp-7) var(--sp-6);
  display: flex;
  flex-direction: column;
  /* Not one gap for everything. The mark sits close to the headline it introduces, the
     steps stand apart from the copy above them, and the trust row is pinned to the floor
     by its own margin. Even spacing is what makes a stack of unrelated blocks look
     unconsidered. */
  gap: var(--sp-5);
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* The headline is sized against THIS box, not the viewport — see the clamp below. */
  container-type: inline-size;
}

/* The mark belongs to the headline beneath it, not to the panel edge above it. */
.split__brand .split__brandmark {
  margin-bottom: calc(var(--sp-2) - var(--sp-5));
}

/* The steps are a different kind of thing from the sentence above them, and want air. */
.split__brand .steps {
  margin-top: var(--sp-4);
}

/* THE LIVE PHOTO.
   ------------------------------------------------------------------
   A still frame of water, rippled by a fixed SVG displacement map and drifting under a slow
   zoom — no video, no canvas, no script. The generated turbulence pattern this replaces was
   a stand-in for exactly this.

   The displacement map used to animate its own frequency, and that one attribute cost more
   than every other moving thing in this file put together: see the note on `causticRipple`
   in macros/ui.html. The zoom below is what keeps the picture alive now, and a transform is
   the compositor's job rather than the CPU's.

   Sits at z-index 0 with the scrim over it and the content above that. Decorative, so the
   <img> carries an empty alt: it says nothing the headline beside it does not. */
.split__water {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  filter: url(#causticRipple);
  /* Slightly over 1 at every point of the loop: the displacement map pushes pixels beyond
     the element's own box, and at scale(1) the panel edge shows the gap it leaves. */
  animation: hero-drift 14.4s ease-in-out infinite;
}

@keyframes hero-drift {
  0%,
  100% {
    transform: scale(1.03);
  }
  50% {
    transform: scale(1.07);
  }
}

/* THE SCRIM — a window, not a wash.
   ------------------------------------------------------------------
   The photo alone cannot carry text: measured across the frame, its brightest pixels give
   white 1.49:1. The first version answered that with an even wash at 86%, which was legible
   and buried the photograph — the thing was in the page and could not be seen.

   A single radial does both. It is thin (22%) over the upper right, where nothing is
   written, and 86% everywhere else, where everything is. The falloff is the ellipse.

   Verified block by block against the real image, at each block's actual position — worst
   case --on-dark: brandmark 7.55, headline 6.33, paragraph 7.88, steps 7.59, trust row
   8.17. All clear AA, and 78% of the photo shows through the window.

   Move any text into the upper right and this guarantee is void. */
.split__brand::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
    46% 40% at 86% 20%,
    rgb(85 18 34 / 22%) 0%,
    rgb(85 18 34 / 62%) 55%,
    rgb(85 18 34 / 86%) 100%
  );
}

/* Everything the reader is here for sits above both the photo and the scrim.

   :not(.split__water) is load-bearing, not tidiness. The photo is also a direct child, so
   a bare `> *` matched it at equal specificity to .split__water and won on source order —
   which put the photo at z-index 2, ON TOP of the scrim, and quietly undid the whole
   contrast guarantee the scrim exists to provide. The panel still looked plausible; the
   worst-case white text behind it was 1.49:1. */
.split__brand > :not(.split__water) {
  position: relative;
  z-index: 2;
}

.split__brand h1 {
  color: var(--ink-invert);
  font-family: var(--font-display);
  /* Sized against the panel, not the window. At a flat 44px "Register your P4 product"
     measures ~530px against 501px of usable panel, so it broke to three short lines where
     the reference has two — and a headline that wraps one word early reads as an accident.
     cqi tracks the panel, so the break lands in the same place whatever the column does.
     The clamp keeps it from shrinking past a heading on a phone or growing past --step-5
     on a wide monitor. */
  font-size: clamp(var(--step-3), 7.6cqi, var(--step-5));
  /* 1.05, not the 1.2 every other heading uses. A two-line headline at 44px with 1.2
     leading has a 9px gulf between the lines and reads as two separate statements; closed
     up, it reads as one. */
  line-height: 1.05;
  letter-spacing: -0.025em;
  font-weight: 700;
  margin: 0;
  /* A ceiling, not the mechanism — the clamp above does the fitting. This only stops a very
     wide panel running the headline out past comfortable reading length. */
  max-width: 22ch;
}

/* :not(.split__brandmark) for the same reason the stacking rule needs its own exclusion:
   the mark is a <p> too, so `.split__brand p` (0,1,1) outranked `.split__brandmark` (0,1,0)
   and silently held the wordmark at body size. Element selectors in a component scope beat
   the component's own class names, which is a trap worth naming twice. */
.split__brand p:not(.split__brandmark) {
  color: var(--on-dark);
  margin: 0;
  font-size: var(--step-1);
  /* 1.6, against the 1.5 body default. This paragraph sits on a photograph rather than on
     paper, and open leading is what stops the texture reading through the block of text. */
  line-height: 1.6;
  max-width: 42ch;
}

.split__work {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: var(--sp-6) var(--sp-5);
}

/* Stacked below 60rem, brand panel first — as drawn in the reference's mobile view.

   An earlier version put the form first here, reasoning that the visitor came to type a
   code. That is the right call for a form somebody navigated to on purpose; it is the
   wrong one for the front door, where the panel is what says whose site this is and what
   the code is for. It also matches the document order, so the <h1> is no longer read out
   after the form it introduces. */
@media (max-width: 59.9375rem) {
  .split {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    /* stretch, NOT the `align-items: start` inherited from the grid rule above.
       ------------------------------------------------------------------
       In the grid that property means "do not stretch vertically", which is what it was
       written for. In this flex COLUMN the cross axis is horizontal, so the same value
       means "do not stretch horizontally" — and the panel then sizes to its contents.

       That was survivable until the panel became a container query. `container-type:
       inline-size` implies `contain: inline-size`, which forbids sizing from contents, so
       the two together collapsed the whole brand panel to 32px wide (its padding) and
       1719px tall, with the headline and the photo stretched down a sliver. */
    align-items: stretch;
  }
}

/* --- The three steps ---------------------------------------------------- */
/* SCOPED to the hero. The service-request panel has its own `ol.steps` progress strip
   (styled near .shots), and these rules — defined later, at equal specificity — used
   to win there too: its labels drew in --ink-invert, white on a light page, with the
   hero's dotted connectors running between them. */
/* Phone OTP, Invoice, Address, connected by a dotted rule in the reference. */
.split__brand .steps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

.split__brand .steps__item {
  /* 5rem, not 7. At 7 the three steps wrapped 2 + 1 on a 375px phone — "Address" dropped
     to its own line and the dotted connector between the first two pointed at nothing.
     Three at 5rem plus two gaps fit inside the panel down to a 320px screen, which is the
     narrowest this app is used on. */
  flex: 1 1 5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-2);
  position: relative;
}

.split__brand .steps__icon {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  /* Two stops rather than one flat wash: on moving water a single translucent disc looks
     like a smudge, and the inner ring is what gives it an edge to sit on. */
  background: rgb(255 255 255 / 10%);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 12%);
  color: var(--accent);
  display: grid;
  place-items: center;
  margin-bottom: var(--sp-1);
}

.split__brand .steps__label {
  color: var(--ink-invert);
  font-weight: 700;
  font-size: var(--step-0);
  letter-spacing: -0.005em;
}

.split__brand .steps__hint {
  /* --on-dark, not the faint variant. The faint token is for the sidebar's company line,
     and on a maroon panel it measures 2.9:1 — under AA at ANY scrim strength, and under it
     on the flat colour this panel used to be, too. These three hints are the only
     explanation a customer gets of what they are about to be asked for. */
  color: var(--on-dark);
  font-size: var(--step--1);
}

/* The connector, drawn from each item rather than as a line between them, so a wrapped row
   simply has no rule rather than one pointing at nothing.

   20rem, not 30: with the narrower basis above the three steps stay on one row on every
   phone, and the reference's own mobile view shows the connectors there. */
@media (min-width: 20rem) {
  .split__brand .steps__item:not(:last-child)::after {
    content: "";
    position: absolute;
    top: 27px;
    left: calc(50% + 30px);
    right: calc(-50% + 30px);
    border-top: 2px dotted rgb(255 255 255 / 30%);
  }
}

/* --- The trust row ------------------------------------------------------ */
/* One bordered container holding three claims, per the reference: the claims are a single
   statement about the company, not three separate tags. */
.trustrow {
  display: flex;
  /* nowrap, and the items shrink instead. Wrapping put each claim on its own line and
     turned one statement about the company into a stacked list — the three belong on one
     rule, which is what the container is for. Below, each item is allowed to wrap its own
     label, so "Trusted by 50k+ customers" takes two lines inside its cell rather than
     pushing the row to two. */
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--sp-3);
  /* The golden edge. --accent at low alpha rather than white: on this panel a white
     hairline reads as a UI border, and the warm one reads as part of the brand. */
  border: 1px solid rgb(227 168 124 / 42%);
  border-radius: var(--radius-pill);
  background: rgb(255 255 255 / 6%);
  /* A tight inner lift, so the edge does not sit flat on the photograph. The wide gold
     halos that used to ring it are gone: a zero-offset coloured glow is decoration, and
     the warm border above already carries the gold on its own. */
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 10%);
  padding: var(--sp-4) var(--sp-5);
  /* Pinned to the bottom of the brand panel, so the panel's height is set by the form
     beside it rather than by this row. */
  margin-top: auto;
}

.trustrow__item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--ink-invert);
  font-size: var(--step--1);
  font-weight: 600;
  line-height: 1.2;
  /* Sized to its own words, then shrinkable. `flex: 1 1 0` would divide the row into three
     equal cells, which is why "15-year warranty" broke across two lines while a third of
     the row sat empty beside it — the claims are different lengths and the reference lets
     them be. min-width: 0 is what allows shrinking at all: without it a flex item refuses
     to go narrower than its longest word, which is what forced the row to wrap. */
  flex: 0 1 auto;
  min-width: 0;
}

/* The lead claim holds one line. It is the promise the whole page is selling, the reference
   sets it that way, and a two-word break across "15-year / warranty" reads as the row
   having run out of room rather than as a hierarchy. The claims after it wrap instead —
   they are qualifications, and qualifications can take two lines.

   Not below 30rem: at 375px the row has about 311px for everything, and holding this one
   open would push the other two to three lines each to pay for it. */
@media (min-width: 30rem) {
  .trustrow__item:first-child {
    white-space: nowrap;
    flex-shrink: 0;
  }
}

/* A hairline between claims, drawn on the item rather than as a separate element so it
   cannot survive a change in the number of claims. */
.trustrow__item + .trustrow__item {
  border-left: 1px solid rgb(227 168 124 / 24%);
  padding-left: var(--sp-3);
}

.trustrow__item svg {
  color: var(--accent);
  flex: 0 0 auto;

}

/* Tighter on a phone, where the three claims share about 295px between them. The radius
   stays a pill: the row no longer wraps, so there are no corners to cut off. */
@media (max-width: 30rem) {
  .trustrow {
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
  }

  .trustrow__item + .trustrow__item {
    padding-left: var(--sp-2);
  }
}

/* --- Filter chips ------------------------------------------------------- */
/* The quick filters beside the status dropdown in reference screen 2. Links, not buttons:
   each is a URL the operator can bookmark or send to a colleague, and the select beside
   them already covers the general case. */
.chiprow {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  /* 32px rather than 44: four of these sit in a row beside a select, and at 44 they push
     the table below the fold on a phone. Still clears the 24px pointer minimum. */
  min-height: 32px;
  padding: 0 var(--sp-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink-soft);
  font-size: var(--step--1);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur-state) var(--ease-out);
}

@media (hover: hover) {
  .chip:hover {
    background: var(--surface);
    color: var(--ink);
  }
}

.chip__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentcolor;
  flex: 0 0 auto;
}

/* Tinted per status family, matching the pills in the table below, so a chip and the rows
   it selects are visibly the same colour. */
.chip--bad {
  background: var(--danger-wash);
  color: var(--danger-on-surface);
  border-color: var(--danger-wash);
}

.chip--warn {
  background: var(--amber-wash);
  color: var(--amber-on-surface);
  border-color: var(--amber-wash);
}

.chip--ready {
  background: var(--blue-wash);
  color: var(--blue-on-surface);
  border-color: var(--blue-wash);
}

.chip--review {
  background: var(--purple-wash);
  color: var(--purple-on-surface);
  border-color: var(--purple-wash);
}

/* The chip standing for the filter in force. A ring, because the tint is already spent on
   saying which family the chip belongs to. */
.chip[aria-current="true"] {
  outline: 2px solid currentcolor;
  outline-offset: 1px;
}

/* --- An input with a button inside it ----------------------------------- */
/* The warranty-card field in reference screen 1, and the product picker's clear button.
   The button sits inside the input's border rather than beside it, so the pair reads as
   one control.

   The wrapper is what makes that true. The first version hung the button off `.field`
   itself, which also contains the label and any hint below — so `height: 100%` made a
   44px-wide invisible button that covered the label, and clicking the label pressed it.
   The wrapper holds the input and the button and nothing else. */
.control {
  position: relative;
  display: block;
}

.control input {
  padding-right: 3rem;
}

.control__action {
  position: absolute;
  top: 0;
  right: 0;
  /* The input is the only other thing in here, so 100% is exactly its height and the two
     edges line up at any font size. */
  height: 100%;
  width: 2.75rem;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
  border-radius: 0 var(--radius-control) var(--radius-control) 0;
}

@media (hover: hover) {
  .control__action:hover {
    color: var(--ink);
  }
}

.control__action[data-copied="true"] {
  color: var(--green-on-surface);
}

/* --- Two things that were inline on thirty templates -------------------- */
/* A retired row. The token block above says there is ONE opacity for "not available to
   you", and names the four that were in service before it existed — 0.5, 0.45, 0.4, and
   "an inline 0.55 on retired parts". That last one was still there, in parts.html, which
   is how a consolidated token ends up with a fifth value nobody can see in the stylesheet.

   The three remaining 0.55s in this file are deliberately NOT this: two are the start of
   an entrance animation and one is the busy dim on a list being replaced. Transient
   states, not a permanent verdict about availability. */
.is-retired {
  opacity: var(--disabled-opacity);
}

/* A photo thumbnail. The same declaration was written inline in five templates, all of
   them an <img> in a grid of evidence — intake photos, visit photos, the signoff sheet.
   --radius-control rather than the tight --radius: at 72px beside a 14px card, a 3px
   corner reads as an unstyled image rather than a deliberate one. */
.thumb {
  border-radius: var(--radius-control);
  /* An edge, because these are PHOTOGRAPHS on a white card and the photographs are of
     water tanks: pale plastic against a bright wall, shot on a phone in daylight. The
     top-right corner of that image and the card behind it are the same value, so the
     picture has no boundary and reads as a bleed rather than a thing. `outline` with a
     negative offset rather than `border`, so it draws INSIDE the box and cannot change
     the layout the width/height attributes have already reserved. */
  outline: 1px solid var(--edge-image);
  outline-offset: -1px;
}

/* A supporting document that is not a picture: a download, never drawn inline. Sits in the
   same 4:3 frame a photo would, or in a thumbnail grid, so it reads as one of the set. */
.filecard {
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: var(--sp-3);
  max-width: 100%;
  text-align: center;
  color: var(--ink);
  text-decoration: none;
}

.filecard__kind {
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* --maroon, not --accent: the accent is a chrome colour, about 2:1 on a white card. */
  color: var(--maroon);
}

.filecard__name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-decoration: underline;
}

.filecard__size {
  font-size: var(--step--1);
  color: var(--ink-faint);
}

/* --- Front-door pieces -------------------------------------------------- */
/* The logo lockup at the top of the brand panel. */
.split__brandmark {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin: 0;
  color: var(--ink-invert);
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 700;
  letter-spacing: -0.015em;
}

.split__brandmark img {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  /* A ring rather than a plate: the mark is drawn on maroon already, and boxing it would
     put a second edge inside a panel that already has one. The outer glow is what lifts it
     off a photograph — on flat colour a hairline is enough, on moving water it is not. */
  border: 1px solid rgb(255 255 255 / 34%);
  box-shadow: 0 0 0 6px rgb(255 255 255 / 6%), 0 2px 12px rgb(0 0 0 / 22%);
  padding: 7px;
}

/* "Already registered?" — a label sitting on the rule rather than above it, so the branch
   reads as one object instead of a heading with a line under it. */
.split__divider {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: var(--sp-5) 0 var(--sp-3);
  color: var(--ink-faint);
  font-size: var(--step--1);
}

.split__divider::before,
.split__divider::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--rule);
}

/* A hint with an icon in front of it. Grid rather than flex, so the second and third lines
   of a wrapped sentence align under the first rather than under the icon. */
.hint--icon {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-2);
  align-items: start;
}

.hint--icon svg {
  margin-top: 2px;
  color: var(--maroon-on-surface);
}

/* The panel keeps its own padding on a phone, where --sp-6 either side of a 320px screen
   leaves very little room for a 60-character headline. */
@media (max-width: 30rem) {
  .split__brand,
  .split__work {
    padding: var(--sp-5) var(--sp-4);
  }

  .split__brand h1 {
    font-size: var(--step-3);
  }

  /* The last thing keeping the three steps from fitting a 320px screen. At --sp-4 the two
     gaps cost 32px of the 264px available and "Address" dropped to a second row; at --sp-2
     they cost 16px and all three stay in the row the connectors are drawn across. */
  .split__brand .steps {
    gap: var(--sp-2);
  }
}

/* An <svg> that exists only to hold a <filter> definition. Not `display: none` — Firefox
   and Safari have both, at points, declined to apply a filter defined inside a hidden
   subtree, so it is taken out of flow at zero size instead. */
.svg-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}


/* --- Withheld contact details ------------------------------------------- */
/*
   The blur is a LABEL, not a lock. What sits inside `.pii` while a reader is locked out
   is a row of bullets that the server sent — the real phone number was never in the
   response — so nothing here is what stops it being read. Blurring bullets is what makes
   them say "withheld" instead of "we don't hold one", which a plain row of dots does not.

   `user-select: none` for the same reason: dragging across it copies bullets, and the
   pointer cursor of a text selection would suggest there is something under there to get
   at. There is not.
*/
.pii {
  filter: blur(4px);
  user-select: none;
  color: var(--ink-faint);
  letter-spacing: 0.12em;
  /* Blur bleeds past the glyphs, so it needs a little room or it clips against the cell
     border and reads as a rendering fault. */
  padding: 0 var(--sp-1);
  /* Kept legible-as-a-shape in a high-contrast or reduced-transparency setting, where a
     blur may not render at all: the colour and spacing still separate it from real data. */
  forced-color-adjust: none;
}

/* Deliberately NO :hover or :focus rule lifting the blur. A reveal-on-hover would make
   the whole gate decorative, and there is nothing under the blur to reveal anyway. */

/* The password box that lifts it. Set apart from the surrounding cards with the warm
   accent rather than a warning colour: asking for a password is routine here, and a red
   panel would teach people to click past it. */
.pii-gate {
  border-left: 3px solid var(--maroon-on-surface);
}


/* --- Password reveal ----------------------------------------------------- */
/*
   The button sits inside the field's own box rather than beside it, so the control reads
   as part of the input and the field keeps its full width on a 320px screen — a separate
   button in a .row would have taken a line of its own there.
*/
.reveal {
  position: relative;
  display: block;
}

/* Room for the button, so a long password does not run underneath it. */
.reveal input {
  padding-right: 2.9rem;
}

.reveal__btn {
  position: absolute;
  right: var(--sp-1);
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  /* 44px: a thumb target, not an icon-sized one. */
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
}

.reveal__btn:hover {
  color: var(--ink);
}

/* Exactly one of the two icons is drawn, chosen by the button's own state — so the
   markup carries no duplicate "which icon" logic for the script to keep in step. */
.reveal__btn .reveal__eye-off,
.reveal__btn[aria-pressed="true"] .reveal__eye {
  display: none;
}

.reveal__btn[aria-pressed="true"] .reveal__eye-off {
  display: block;
}

/* === The customer surface ================================================= */
/* Refinements to shared components for the customer side only, scoped under the
   `surface-customer` class base.html puts on every page without the staff sidebar. The back
   office keeps its denser settings: an agent working a queue all day reads a 13px note
   differently from a customer on a rooftop reading one sentence about their own tank.

   Nothing here adds a colour, a face or a component. It is the same world, set for the
   person it is actually for. */

/* --- Type ------------------------------------------------------------------ */
/* 16px floor. The shared body is 15px on a desktop, which is a queue's density. */
.surface-customer .main {
  font-size: max(1rem, var(--step-0));
}

/* A note carries the error on a refused form and the time of an engineer's visit — the two
   sentences a customer most needs to read — and was the smallest text on the page. */
.surface-customer .note {
  font-size: inherit;
}

/* One step above body, with weight, so a card's title is a title and not a bold sentence:
   at 17px it matched the phone's body size exactly. */
.surface-customer .card__title {
  font-size: var(--step-2);
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: 1.25;
}

/* Words in the word face. The code face is for codes — card numbers, references — and a
   status like "Waiting for photos" or a role like "Customer" set in it, spaced and shouted
   in capitals, is the typewriter as costume. */
.surface-customer .badge,
.surface-customer .topbar__role,
.surface-customer th {
  font-family: var(--font-ui);
  text-transform: none;
  letter-spacing: 0.01em;
}

.surface-customer th {
  font-size: inherit;
  color: var(--ink-soft);
  white-space: normal;
}

/* A page heading keeps its lead over a card title on a phone; the shared rule dropped it to
   --step-2, the same size as the cards beneath it. The hero sizes its own headline. */
@media (max-width: 640px) {
  .surface-customer .col-form > h1,
  .surface-customer .page-head h1 {
    font-size: var(--step-3);
  }
}

/* "Service request" said in words, the number beside it in the code face it belongs to. */
.ticket-title {
  display: grid;
  gap: var(--sp-1);
  margin: 0;
}

.ticket-title__kind {
  font-size: var(--step-1);
  font-weight: 600;
  color: var(--ink-soft);
  letter-spacing: 0;
}

.ticket-title .id {
  font-size: var(--step-4);
  line-height: 1.05;
}

@media (max-width: 640px) {
  .ticket-title .id {
    font-size: var(--step-3);
  }
}

.surface-customer .foot {
  line-height: 1.55;
}

/* --- The front door: bolder -------------------------------------------------- */
/* The form half of the front door read as a generic form beside a hero that commits: a
   17px heading with its subtitle jammed underneath. It now speaks in the same display face
   at a step below the hero's headline — the same brand, sure of itself on both halves — and
   the subtitle gets the room a lede needs. The hero still leads. */
.surface-customer .split__work > h2 {
  font-family: var(--font-display);
  font-size: clamp(var(--step-3), 1.2rem + 1.2vw, var(--step-4));
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin-bottom: var(--sp-2);
}

.surface-customer .split__work > h2 + .lede {
  margin-top: 0;
  margin-bottom: var(--sp-5);
}

/* --- Layout -------------------------------------------------------------------- */
/* The three intake photos sit three across when there is room, instead of two and an
   orphan; the two optional ones pair up beneath. */
.surface-customer .shots {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}

/* No cards inside the card. The registration form's sections were bordered, shadowed
   boxes inside a bordered, shadowed box; a rule and a heading say "new section" without
   drawing three frames around it. */
.surface-customer .split__work .card {
  background: none;
  border: 0;
  border-top: 1px solid var(--rule);
  border-radius: 0;
  box-shadow: none;
  padding: var(--sp-5) 0 0;
  margin-top: var(--sp-6);
}

/* On a phone the hero filled the whole first screen, so the one field a customer came to
   type into began below the fold. The hero still comes first — it says whose site this is
   and what the code is for — but it says it in less height: the step hints and the single
   trust claim wait for a wider screen. */
@media (max-width: 40rem) {
  .surface-customer .split__brand {
    padding: var(--sp-5) var(--sp-4);
    gap: var(--sp-4);
  }

  .surface-customer .split__brand .steps {
    margin-top: 0;
  }

  .surface-customer .split__brand .steps__hint,
  .surface-customer .split__brand .trustrow {
    display: none;
  }

  .surface-customer .split__brand .steps__icon {
    width: 44px;
    height: 44px;
  }

  .surface-customer .split__brand .steps__item:not(:last-child)::after {
    top: 22px;
  }
}

/* --- Browser surfaces ---------------------------------------------------------- */
.surface-customer {
  caret-color: var(--maroon-on-surface);
}

.surface-customer ::selection {
  background: var(--maroon-wash);
  color: var(--ink);
}

/* Focus rings are no longer here: they apply on every surface — see the form controls. */

/* The invoice photo field was the browser's own "Choose file" control. */
.surface-customer input[type="file"]::file-selector-button {
  font: inherit;
  font-weight: 600;
  padding: 0.5rem 1rem;
  margin-right: var(--sp-3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
}

/* --- Motion: the one authored moment --------------------------------------------- */
/* The warranty band draws to where today sits, once, on arrival. It is the one place in the
   app where the movement IS the content: how much of the cover has been used, shown as a
   distance travelled. The transition was always here; without a starting style it never
   ran. Reduced motion lands on the final state, still and exact. */
@media (prefers-reduced-motion: no-preference) {
  @starting-style {
    .surface-customer .cover__fill {
      transform: scaleX(0);
    }
  }

  .surface-customer .cover__fill {
    transition-duration: 700ms;
    transition-delay: 120ms;
    transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
  }
}

/* A section heading owns more space below it than a label does, so "Your details" reads as
   the title of the fields beneath it rather than as another label in the stack. */
.surface-customer .split__work .card > .card__title {
  margin-bottom: var(--sp-4);
}


/* === The account menu ====================================================== */
/* One round initial on the top bar, opening who is signed in, Help and Sign out. It
   replaced a role chip, the name and a Sign out link, which took most of a phone's bar.
   The drawing matches the theme buttons beside it: the same translucent white on the
   maroon chrome, the same 44px target, the same press. */
.account__btn {
  appearance: none;
  flex: none;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  /* 44px to a finger without drawing a 44px circle: the margin is part of the target. */
  margin: 4px;
  border: 0;
  border-radius: 50%;
  background: rgb(255 255 255 / 16%);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 22%);
  color: var(--ink-invert);
  font: inherit;
  font-weight: 700;
  font-size: var(--step-0);
  line-height: 1;
  cursor: pointer;
  transition:
    background-color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.account__btn:hover {
  background: rgb(255 255 255 / 24%);
}

.account__btn:active {
  transform: scale(0.92);
}

.account__btn:focus-visible {
  outline: 2px solid var(--ink-invert);
  outline-offset: 2px;
}

/* The panel. Fixed under the bar's right edge rather than anchored to the button: anchor
   positioning is not yet in every browser this app is used in, and the button never moves
   from that corner. `inset: auto` clears the popover default of a centred dialog. */
.account {
  position: fixed;
  inset: auto;
  top: calc(var(--topbar-h) + var(--sp-2));
  right: max(var(--sp-3), calc((100vw - var(--shell)) / 2 + var(--sp-4)));
  width: min(20rem, calc(100vw - 2 * var(--sp-3)));
  margin: 0;
  padding: var(--sp-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  background: var(--paper);
  color: var(--ink);
  box-shadow: 0 12px 32px rgb(64 26 32 / 16%), 0 2px 6px rgb(64 26 32 / 8%);
  font-size: max(1rem, var(--step-0));
}

/* Browsers without the Popover API get app.js's fallback, which toggles `.is-open`.
   Combined with :is() so a browser that does not know :popover-open keeps the rule. */
.account:not(:is(:popover-open, .is-open)) {
  display: none;
}

.account__who {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--sp-1);
}

.account__avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--maroon);
  color: var(--ink-invert);
  font-weight: 700;
  font-size: var(--step-1);
}

.account__ids {
  min-width: 0;
}

.account__ids p {
  margin: 0;
  overflow-wrap: anywhere;
}

.account__name {
  font-weight: 650;
}

.account__email,
.account__phone,
.account__role {
  font-size: var(--step--1);
  color: var(--ink-soft);
}

/* One row style for Help, the help page and Sign out, whether it is a link, a <summary>
   or the form's button — the element differs, the row does not. */
.account__item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 44px;
  padding: 0 var(--sp-3);
  border: 0;
  border-radius: var(--radius-control);
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  list-style: none;
}

.account__item::-webkit-details-marker {
  display: none;
}

.account__item:hover {
  background: var(--surface);
}

/* The disclosure mark on Help, drawn rather than the UA triangle, turning as it opens. */
.account__help > summary::after {
  content: "";
  margin-left: auto;
  width: 7px;
  height: 7px;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  transition: transform var(--dur-state) var(--ease-out);
}

.account__help[open] > summary::after {
  transform: rotate(225deg) translateY(-2px);
}

.account__call {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 44px;
  margin: var(--sp-1) var(--sp-3) 0;
  padding: 0 var(--sp-3);
  border-radius: var(--radius-control);
  background: var(--maroon-wash);
  color: var(--maroon-on-surface);
  text-decoration: none;
}

.account__sub {
  display: block;
  padding: var(--sp-2) var(--sp-3) var(--sp-3) var(--sp-5);
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.account__signout {
  margin: var(--sp-1) 0 0;
  padding-top: var(--sp-1);
  border-top: 1px solid var(--rule);
}

/* Opening: a short drop from under the bar, so the panel reads as coming FROM the
   button's corner. Reduced motion keeps the fade and loses the travel. */
@media (prefers-reduced-motion: no-preference) {
  .account:popover-open {
    animation: account-in var(--dur-enter) cubic-bezier(0.16, 1, 0.3, 1);
  }
}

@keyframes account-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
}

/* The card's own words for its numbers, set small above the code on the dark plate. */
.plate__label {
  font-size: var(--step--1);
  color: var(--on-dark-faint);
  letter-spacing: 0.02em;
}

/* "Service request SR-2608-000017" is one label: it may wrap as a whole, never between the
   words and the number they name. */
.ticket-ref {
  white-space: nowrap;
}

/* The three changes are the account's own business; Help and Sign out are about leaving it.
   A rule between the two groups says so without another heading. */
.account__changes {
  display: grid;
  padding-bottom: var(--sp-1);
  margin-bottom: var(--sp-1);
  border-bottom: 1px solid var(--rule);
}

/* The account-change pages: the heading owns its space, and Cancel sits apart from the card
   it cancels rather than touching its edge. */
.account-change > h1 {
  margin-bottom: var(--sp-5);
}

.account-change > .card + .btn-row,
.account-change > .note + .btn-row {
  margin-top: var(--sp-4);
}
