/*
   WESTMERE PRIVATE HIRE — GLOBAL VISUAL THEME OVERRIDE
   ----------------------------------------------------
   Purpose:
   - Apply the Westmere logo-inspired visual language to ALL existing pages.
   - Keep your existing HTML, photos, booking logic and backend unchanged.
   - Remove cream/gold/dark filled UI styling.
   - Use clean white, deep navy, fine typography and subtle interaction states.

   HOW TO USE
   Add this file as the LAST stylesheet before </head> on every HTML page:

   <link rel="stylesheet" href="westmere-theme.css">

   It must come after styles.css and after any page-specific <style> blocks.
*/

/* Cormorant is THE face for the whole system. Roman + italic across the
   weights the readability layer needs: 400 for large display, 500/600 for
   body, UI and micro-labels — Cormorant is delicate, so small text is set
   heavier than you would with a normal serif. 53 rules use italic, so the
   italic axis is loaded too. */
@import url('https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&display=swap');

/* =========================================================
   1. MASTER DESIGN TOKENS
   ========================================================= */
:root {
  --westmere-navy: #102a43;
  /* GOLD, BACK BY DECISION. The accent was stripped to navy in an earlier
     re-skin; the owner found the result too faint to read at a glance and asked
     for blue, gold and white. Two values, because one gold cannot do both jobs:
     --westmere-gold is the ACCENT — rules, active underlines, rings, the edge of
     a chip — where it sits against white at large sizes. --westmere-gold-ink is
     the darker one for TYPE, which has to clear 4.5:1 on white; the accent does
     not and must never be used for small text.
     GUARDRAIL: server/tests/button-style.test.js */
  --westmere-gold: #C9A227;
  --westmere-gold-ink: #8A6A12;
  --westmere-navy-deep: #0c2237;
  --westmere-navy-soft: #173b5e;
  --westmere-text: #102a43;
  --westmere-muted: #657485;
  --westmere-line: #dfe5ea;
  --westmere-line-strong: #c8d1d9;
  --westmere-white: #ffffff;
  --westmere-press: rgba(16, 42, 67, 0.07);
  --westmere-hover: rgba(16, 42, 67, 0.025);
  /* The pre-redesign charcoal. Still the ink inside the booking-form picker
     popup, so it is named rather than left as a stray literal — but it keeps
     its exact value: this refactor changes no pixels. Re-point it at
     --westmere-navy when the owner wants that popup to match the rest. */
  --westmere-ink-legacy: #1b1b1a;
  /* SECONDARY TYPE THAT IS STILL TYPE. A date, a reference, the working under a
     figure — quieter than the heading above it, and still readable at ten
     pixels. Measured: 5.35:1 on white, 5.22:1 on the #F6F8FA card a settled job
     sits on, against the 4.5 that small text needs.

     It exists because the layers the owner reads money on — the driver page,
     the operator page, the send sheet — are appended to document.body, OUTSIDE
     #scr-app, so §15.2's blanket navy never reaches them and whatever ink the
     markup names is what renders. They named .45 and .5, which measure 2.8 and
     3.3. GUARDRAIL: server/tests/legibility.test.js */
  --westmere-ink-soft: rgba(27, 27, 26, .65);
  /* THE STATUS BLUE, dark enough to be read on the pale blue chip it is always
     printed on. #60A5FA was a dark-mode blue carried over from the old palette:
     on its own .06–.1 tint it measures 2.3–2.4:1, and it only ever looked
     all right because §15.2's blanket repainted it navy inside the apps. A
     colour that depends on being overridden is not a colour. 6.2:1. */
  --westmere-status-blue: #1D4ED8;
  --westmere-line-inverse: #ddd;   /* hairlines ON a dark photo band */

  /* ── STATE COLOURS ──────────────────────────────────────────────────────
     The only hues in the system that are not navy. Each is the value that was
     already in use; they are named here so a state can be restyled once. */
  --westmere-danger:       #9C2828;   /* cancel / destructive copy   */
  --westmere-danger-soft:  #c0392b;   /* inline form errors          */
  --westmere-danger-wash:  #fbf6f5;   /* danger button resting fill  */
  --westmere-success:      #2d6e47;   /* paid / confirmed copy       */
  --westmere-info:         #2563EB;   /* en-route / live states      */
  --westmere-scrim:        rgba(16, 42, 67, .42);  /* modal backdrops */

  /* ── FRAME SHAPE ────────────────────────────────────────────────────────
     THE ROUNDNESS OF EVERYTHING. This is the "shape of frames" dial: set the
     scale here and every card, button, pill, input and panel follows. The
     values are exactly what the system already used, so changing them is a
     deliberate act rather than a side effect of this refactor.
       sm  small chrome — chips, tags, small inputs
       md  the default — buttons, inputs, most panels
       lg  cards and larger panels
       xl  full sheets and modals
       pill / round  fully rounded (a pill button, a circular day cell) */
  --radius-sm:    8px;
  --radius-md:   10px;
  --radius-btn:  11px;   /* app buttons at phone widths */
  --radius-lg:   12px;
  --radius-xl:   14px;
  --radius-2xl:  16px;
  --radius-pill: 999px;
  --radius-round: 50%;
  --radius-none:  0;

  /* ── BORDERS / HAIRLINES ────────────────────────────────────────────────
     Widths only; the colours are the --westmere-line tokens above. */
  --border-hair:   1px;    /* the standard 1px rule                 */
  --border-strong: 1.5px;  /* a selected frame — picker, filter tab */
  --border-heavy:  2px;    /* an emphasised panel edge              */

  /* ── TYPOGRAPHY SCALE ───────────────────────────────────────────────────
     Cormorant is delicate, so the small tiers are set heavier than you would
     with a normal serif — see §16, the readability layer, which is what keeps
     these legible. Sizes are the ones already in use. */
  --text-2xs:  0.62rem;
  --text-xs:   0.68rem;
  --text-sm:   0.78rem;
  --text-base: 0.9rem;
  --text-md:   1rem;
  --text-lg:   1.15rem;
  --text-xl:   1.5rem;
  --text-2xl:  2rem;
  --text-3xl:  2.6rem;
  --weight-normal: 400;
  --weight-medium: 500;
  --weight-semi:   600;
  --weight-bold:   700;
  --tracking-wide:  .08em;   /* small-caps labels */
  --tracking-wider: .2em;    /* the wordmark      */

  /* ── SPACING SCALE ──────────────────────────────────────────────────────
     Layout rhythm. Used for padding/margin/gap so density is one edit. */
  --space-1: .25rem;
  --space-2: .5rem;
  --space-3: .75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  /* ── MOTION ─────────────────────────────────────────────────────────────*/
  --ease-ui: .18s cubic-bezier(.4, 0, .2, 1);
  --ease-panel: .26s cubic-bezier(.4, 0, .2, 1);
  --westmere-focus: rgba(16, 42, 67, 0.17);

  /* Override existing site variables without changing existing HTML. */
  --ink: var(--westmere-navy);
  --muted: var(--westmere-muted);
  --paper: var(--westmere-white);
  --line: var(--westmere-line);

  --dark: var(--westmere-navy);
  --navy: var(--westmere-navy);
  --navy2: var(--westmere-navy-soft);
  --text: var(--westmere-navy);
  --border: var(--westmere-line);

  /* ── ONE TYPEFACE, EVERYWHERE ────────────────────────────────────────────
     The owner chose CORMORANT. It sets ALL type — headings and body/UI alike —
     on the public site, My Account, the owner app, admin, the pay page and the
     emails. There is exactly one stack and it is declared HERE, once. Pages do
     not define --serif/--sans of their own; they inherit these, so changing the
     brand face is a single edit.

     Cormorant replaced a Didot-first stack deliberately. Didot is an Apple
     system font: it cannot be served, so it rendered as real Didot on the
     owner's iPhone and as a substitute everywhere else — the site literally
     looked different depending on the device. Cormorant is a Google web font,
     so EVERY visitor gets the same letterforms, the owner included. The system
     fonts that follow are only a safety net for a failed font fetch.

     --serif and --sans are kept as names because ~460 existing declarations
     reference them. They deliberately resolve to the SAME stack. */
  --westmere-type:'Cormorant','Cormorant Garamond','Didot','Bodoni MT',Georgia,'Times New Roman',serif;
  --serif: var(--westmere-type);
  --sans:  var(--westmere-type);

  color-scheme: only light;
}

/* =========================================================
   2. BASE — WHITE CANVAS + READABLE NAVY TYPE
   ========================================================= */
html {
  background: var(--westmere-white) !important;
  color-scheme: only light !important;
}

body {
  background: var(--westmere-white) !important;
  color: var(--westmere-text) !important;
  font-family: var(--sans) !important;
  font-weight: 300 !important;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body,
button,
input,
select,
textarea {
  font-synthesis: none;
}

p,
li,
small,
.muted,
.sec-sub,
.upd,
.legal {
  color: var(--westmere-muted) !important;
}

a {
  color: var(--westmere-navy);
}

/* =========================================================
   3. TYPOGRAPHY — THINNER, MORE LIKE THE LOGO
   ========================================================= */
.brand b,
.brand-name,
.serif,
h1,
h2,
h3,
h4,
.section-title,
.split h2,
.photo-card h3,
.coverage-card h3,
.cta-band h2,
.sec-title,
.ls-title,
.aj-driver,
.aj-meta-v,
.review-quote {
  font-family: var(--serif) !important;
  font-weight: 400 !important;
  color: var(--westmere-navy);
}

/* Main headings: elegant, not heavy or oversized. */
.hero h1,
.book-hero h1 {
  font-family: var(--serif) !important;
  font-weight: 400 !important;
  font-size: clamp(2.75rem, 5.4vw, 4.7rem) !important;
  letter-spacing: 0.09em !important;
  line-height: 1.02 !important;
}

.hero.left h1 {
  font-size: clamp(2.7rem, 5vw, 4.4rem) !important;
  letter-spacing: 0.035em !important;
}

.section-title,
.split h2 {
  font-size: clamp(2rem, 3.5vw, 3rem) !important;
  font-weight: 400 !important;
  letter-spacing: 0.025em;
  line-height: 1.12 !important;
}

main h1 {
  font-family: var(--serif) !important;
  font-size: clamp(2rem, 4vw, 3rem) !important;
  font-weight: 400 !important;
  letter-spacing: 0.08em !important;
  color: var(--westmere-navy) !important;
}

main h2 {
  font-family: var(--serif) !important;
  font-size: clamp(1.35rem, 2.2vw, 1.8rem) !important;
  font-weight: 400 !important;
  color: var(--westmere-navy) !important;
}

.sec-title {
  font-size: clamp(1.55rem, 3vw, 2rem) !important;
  font-weight: 400 !important;
}

.sec-title em,
h1 em,
h2 em,
em {
  color: inherit !important;
  font-family: var(--serif) !important;
  font-weight: 400 !important;
}

.eyebrow,
.ch,
.tb-sub,
.feature strong,
.field label,
.fl {
  color: var(--westmere-navy) !important;
  font-family: var(--sans) !important;
  font-weight: 400 !important;
  letter-spacing: 0.20em !important;
}

.brand b,
.brand-name {
  font-size: 1.3rem !important;
  letter-spacing: 0.28em !important;
  line-height: 1 !important;
}

.brand span {
  color: var(--westmere-navy) !important;
  letter-spacing: 0.36em !important;
}

/* =========================================================
   4. REMOVE CREAM / COLOURED PANELS
   ========================================================= */
.section.soft,
.book-form-col,
.quick-box,
.fare-estimate,
.fare-estimate.msg,
.card,
.photo-card,
.notice,
.vehicle-hero .copy,
.spec,
.ls-inner,
.sec,
.aj-card,
.tc-card,
.prof-card,
.topbar,
.bottom-nav,
.sug-drop,
select.fi option {
  background: var(--westmere-white) !important;
}

/* Cards should not look like blocks. Use spacing and fine rules. */
.card,
.sec,
.tc-card,
.prof-card,
.aj-card {
  box-shadow: none !important;
  border-radius: var(--radius-none) !important;
  border: 0 !important;
  border-top: 1px solid var(--westmere-line) !important;
}

.photo-card {
  box-shadow: none !important;
  border: 1px solid var(--westmere-line) !important;
  border-radius: var(--radius-lg) !important;
  overflow: hidden;
}

.quick-box,
.notice {
  box-shadow: none !important;
  border: 1px solid var(--westmere-line) !important;
  border-radius: var(--radius-xl) !important;
}

.fare-estimate {
  box-shadow: none !important;
  border: 1px solid var(--westmere-line) !important;
  border-radius: var(--radius-lg) !important;
}

/* Keep photo heroes, but tint overlays navy rather than black. */
.hero::after,
.hero.center::after {
  background: rgba(12, 34, 55, 0.24) !important;
}

.hero.left::after {
  background: linear-gradient(90deg, rgba(12,34,55,.34), rgba(12,34,55,.10)) !important;
}

.book-hero {
  /* Preserve the page's existing photo. Only the overlay colour changes. */
  background-blend-mode: multiply;
}

.cta-band::before {
  background: rgba(12, 34, 55, 0.30) !important;
}

/* =========================================================
   5. NAVIGATION — WHITE, FINE, QUIET
   ========================================================= */
nav,
.topbar {
  background: rgba(255,255,255,.97) !important;
  color: var(--westmere-navy) !important;
  border-bottom: 1px solid var(--westmere-line) !important;
  box-shadow: none !important;
}

.brand,
.brand b,
.brand span,
.brand-name,
.navlinks a,
.nav-links a,
.acct-nav,
.nav-book,
.tb-name,
.tb-sub {
  color: var(--westmere-navy) !important;
}

.navlinks a,
.nav-links a,
.acct-nav {
  font-family: var(--sans) !important;
  font-weight: 400 !important;
  letter-spacing: .10em !important;
}

.navlinks a.active,
.navlinks a:hover,
.nav-links a:hover,
.acct-nav:hover {
  color: var(--westmere-navy) !important;
  border-bottom-color: var(--westmere-navy) !important;
}

.burger,
.burger span {
  color: var(--westmere-navy) !important;
}

.burger span {
  background: var(--westmere-navy) !important;
}

.mobile-menu,
.mmenu {
  background: rgba(255,255,255,.99) !important;
}

.mobile-menu a,
.mmenu a {
  color: var(--westmere-navy) !important;
  font-family: var(--serif) !important;
  font-weight: 400 !important;
}

/* =========================================================
   6. BUTTONS — OUTLINE BY DEFAULT, BRIEF FILL ONLY ON PRESS
   ========================================================= */
.book-btn,
.btn,
.btn.dark,
.nav-book,
.ls-btn,
.btn-main,
.btn-ghost,
.btn-sm,
.aj-send-btn,
.aj-call-btn,
.modal-btn-primary,
.filter-btn,
.pick-btn {
  background: transparent !important;
  color: var(--westmere-navy) !important;
  border: 1px solid var(--westmere-navy) !important;
  border-radius: var(--radius-lg) !important;
  box-shadow: none !important;
  font-family: var(--sans) !important;
  font-weight: 400 !important;
  letter-spacing: .16em !important;
  text-transform: uppercase !important;
  transition:
    background-color .12s ease,
    color .12s ease,
    border-color .18s ease,
    box-shadow .18s ease,
    transform .08s ease !important;
}

.link-btn,
.ls-switch button {
  background: transparent !important;
  color: var(--westmere-navy) !important;
  border: 0 !important;
  box-shadow: none !important;
}

/* Hover is deliberately subtle: no permanent block of colour. */
@media (hover:hover) {
  .book-btn:hover,
  .btn:hover,
  .btn.dark:hover,
  .nav-book:hover,
  .ls-btn:hover,
  .btn-main:hover,
  .btn-ghost:hover,
  .btn-sm:hover,
  .aj-send-btn:hover,
  .aj-call-btn:hover,
  .modal-btn-primary:hover,
  .filter-btn:hover,
  .pick-btn:hover {
    background: rgba(16,42,67,.025) !important;
    color: var(--westmere-navy) !important;
    border-color: var(--westmere-navy-deep) !important;
  }
}

/* The only filled state: while the customer is physically pressing/tapping. */
.book-btn:active,
.btn:active,
.btn.dark:active,
.nav-book:active,
.ls-btn:active,
.btn-main:active,
.btn-ghost:active,
.btn-sm:active,
.aj-send-btn:active,
.aj-call-btn:active,
.modal-btn-primary:active,
.filter-btn:active,
.pick-btn:active {
  background: var(--westmere-navy) !important;
  color: var(--westmere-white) !important;
  transform: translateY(1px);
}

/* Active filters / tabs remain white; the outline/underline communicates state. */
.tabs button,
.filter-btn,
.pick-btn {
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
  border: 1px solid var(--westmere-line-strong) !important;
  border-radius: var(--radius-md) !important;
}

.tabs button.active,
.filter-btn.on,
.pick-btn.on,
/* …but NOT the time wheel's rows. They are listbox options with
   aria-selected="true", so this tab rule was drawing an inset underline
   beneath the selected hour and minute — a stray line through the middle of
   the wheel. The wheel draws its own centre frame. */
[aria-selected="true"]:not(.wmtw-opt) {
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
  border-color: var(--westmere-navy) !important;
  box-shadow: inset 0 -1px 0 var(--westmere-navy) !important;
}

/* =========================================================
   7. FORMS — WHITE, ROUNDED, THIN NAVY FOCUS
   ========================================================= */
.field input,
.field select,
.field textarea,
.fi,
.ls-input,
input,
select,
textarea {
  /* ONLY LIGHT, AND THE KEYWORD IS THE WHOLE BUG.
     `light` says "this CAN be shown light", which is exactly the invitation
     Android Chrome's Auto Dark Theme accepts — it then repaints the control
     itself, and the explicit white background on the next line does not stop
     it. §2 opted the document out with `only light !important`; this rule
     re-opened the hole for every form control on the site, and because it is
     !important no page could close it.

     Measured in Chrome with Auto Dark on: every field of the booking form —
     name, phone, flight number, passengers, luggage — rendered as a black box,
     and so did the date popup a customer could not read. One word, and they
     render white again. Commit 8e609d1 fixed the document and said element
     declarations were "a different job"; this is that job.
     GUARDRAIL: server/tests/booking-dark-mode.test.js */
  color-scheme: only light !important;
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
  font-family: var(--sans) !important;
  font-weight: 300 !important;
  border-color: var(--westmere-line-strong) !important;
  outline: none;
  box-shadow: none !important;
}

.field input,
.field select,
.field textarea {
  border: 1px solid var(--westmere-line-strong) !important;
  border-radius: var(--radius-lg) !important;
}

.fi,
.ls-input {
  border-bottom: 1px solid var(--westmere-line-strong) !important;
}

textarea.fi {
  border: 1px solid var(--westmere-line-strong) !important;
  border-radius: var(--radius-lg) !important;
}

.field input:focus,
.field select:focus,
.field textarea:focus,
.fi:focus,
.ls-input:focus,
textarea.fi:focus,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  border-color: var(--westmere-navy) !important;
  box-shadow: 0 0 0 3px var(--westmere-focus) !important;
}

input::placeholder,
textarea::placeholder,
.fi::placeholder,
.ls-input::placeholder {
  color: #8a97a4 !important;
  opacity: 1;
}

input[type="checkbox"],
input[type="radio"],
.book-form-col input[type="date"],
.book-form-col input[type="time"] {
  accent-color: var(--westmere-navy) !important;
}

.book-form-col input[type="date"]::-webkit-calendar-picker-indicator,
.book-form-col input[type="time"]::-webkit-calendar-picker-indicator {
  filter: none !important;
  opacity: .72 !important;
}

.ac-list,
.sug-drop {
  background: var(--westmere-white) !important;
  border: 1px solid var(--westmere-line) !important;
  box-shadow: 0 12px 30px rgba(16,42,67,.08) !important;
}

.ac-item,
.sug-item {
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
}

.ac-item:hover,
.sug-item:hover {
  background: rgba(16,42,67,.035) !important;
}

/* =========================================================
   8. VEHICLE PREFERENCE — SUBTLE, NOT A FLEET SHOWCASE
   These selectors are ready for the booking selector when used.
   ========================================================= */
.vehicle-choice,
.vehicle-option,
[data-vehicle-option],
[data-vehicle-choice] {
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
  border: 1px solid var(--westmere-line) !important;
  border-radius: var(--radius-xl) !important;
  box-shadow: none !important;
  transition: border-color .16s ease, box-shadow .16s ease, background-color .10s ease;
}

.vehicle-option:hover,
[data-vehicle-option]:hover {
  border-color: var(--westmere-line-strong) !important;
}

.vehicle-option.selected,
.vehicle-option.is-selected,
[data-vehicle-option][aria-selected="true"],
[data-vehicle-option].selected {
  background: var(--westmere-white) !important;
  border-color: var(--westmere-navy) !important;
  box-shadow: 0 0 0 1px var(--westmere-navy) !important;
}

.vehicle-option:active,
[data-vehicle-option]:active {
  background: var(--westmere-press) !important;
}

/* =========================================================
   9. DIVIDERS / FEATURES — USE LINES, NOT COLOURED BLOCKS
   ========================================================= */
.feature-strip,
.review-strip,
.form-divider,
.fares th,
.fares td,
.coverage-card,
.image-row figcaption,
.ch,
.topbar,
.bottom-nav {
  border-color: var(--westmere-line) !important;
}

.feature {
  background: var(--westmere-white) !important;
  border-color: var(--westmere-line) !important;
}

.feature p,
.coverage-card p,
.review-note,
.fare-estimate .fe-note,
.fare-estimate .fe-calc,
.quick-note {
  color: var(--westmere-muted) !important;
}

.icon,
.fare-estimate .fe-amount,
.google-g,
.stars {
  color: var(--westmere-navy) !important;
}

/* =========================================================
   10. FOOTER — WHITE, NOT BLACK / CREAM
   ========================================================= */
.footer,
footer.mini-foot {
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
  border-top: 1px solid var(--westmere-line) !important;
}

.footer .brand,
.footer .brand b,
.footer .brand span,
.footer-contact,
.footer-contact a,
footer.mini-foot,
.mini-legal,
.mini-legal a {
  color: var(--westmere-navy) !important;
}

.legal,
.legal a {
  color: var(--westmere-muted) !important;
}

/* =========================================================
   11. LEGAL PAGES — REMOVE THE OLD BLACK/GOLD THEME
   ========================================================= */
body main {
  background: var(--westmere-white) !important;
}

body main p,
body main li,
body main .upd {
  color: var(--westmere-muted) !important;
}

body main h1,
body main h2,
body main .eyebrow {
  color: var(--westmere-navy) !important;
}

body main a,
body main a[style] {
  color: var(--westmere-navy) !important;
}

body main ul li::before {
  color: var(--westmere-navy) !important;
}

table.cookies,
table.cookies th,
table.cookies td {
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
  border-color: var(--westmere-line) !important;
}

table.cookies th {
  color: var(--westmere-navy) !important;
}

/* =========================================================
   12. RIDER / ACCOUNT APP — SAME WESTMERE LANGUAGE
   ========================================================= */
body.mode-day,
.login-screen,
.app-shell,
.app-content {
  background: var(--westmere-white) !important;
}

.ls-inner {
  border: 1px solid var(--westmere-line) !important;
  border-radius: var(--radius-2xl) !important;
  box-shadow: 0 20px 55px rgba(16,42,67,.08) !important;
}

.ls-crest,
.tb-crest,
.aj-driver-avatar {
  color: var(--westmere-navy) !important;
  border-color: var(--westmere-line-strong) !important;
}

.ch,
.tb-sub,
.aj-eta,
.aj-meta-v.aj-fare,
.bn-item.on,
.aj-step.on span {
  color: var(--westmere-navy) !important;
}

.aj-bar,
.bn-item.on .bn-bar,
.bn-item.on::after,
.aj-step.on .aj-step-dot {
  background: var(--westmere-navy) !important;
}

/* =========================================================
   13. ACCESSIBILITY / INTERACTION LOGIC
   ========================================================= */
:focus-visible {
  outline: 2px solid var(--westmere-navy) !important;
  outline-offset: 3px;
}

button,
a,
input,
select,
textarea,
[role="button"],
[tabindex] {
  -webkit-tap-highlight-color: rgba(16,42,67,.08);
}

::selection {
  background: rgba(16,42,67,.14);
  color: var(--westmere-navy-deep);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* =========================================================
   14. MOBILE TUNING — KEEP THE LIGHT, EDITORIAL FEEL
   ========================================================= */
@media (max-width: 900px) {
  .hero h1,
  .hero.left h1,
  .book-hero h1 {
    font-size: clamp(2.35rem, 11vw, 3.55rem) !important;
    letter-spacing: .055em !important;
  }

  .section-title,
  .split h2 {
    font-size: clamp(1.8rem, 8vw, 2.45rem) !important;
  }

  .brand b,
  .brand-name {
    letter-spacing: .22em !important;
  }

  .book-btn,
  .btn,
  .btn.dark,
  .nav-book,
  .ls-btn,
  .btn-main {
    border-radius: var(--radius-btn) !important;
  }
}

/* =========================================================
   15. INTEGRATION FIXES  (added during integration, not by the designer)
   =========================================================
   Everything above is the supplied theme, unchanged. This section holds the
   small number of corrections found when it was applied to the real pages.
   Kept at the bottom, and clearly fenced, so a future re-supply of the theme
   is easy to diff against.
   --------------------------------------------------------- */

/* 15.1 — TEXT OVER THE HERO PHOTOGRAPH MUST STAY LIGHT.
   Section 3 sets an explicit navy on h1/h2/h3, which beats the colour the
   hero previously INHERITED from its own container (#fff). The result was the
   "Westmere" wordmark rendering navy-on-dark over the photograph — present on
   index, about, contact, services, airport-transfers and coverage.
   `inherit` restores the old behaviour exactly: over a photo the container is
   white so the heading is white; on a plain white hero the container is navy
   so the heading stays navy. It adapts rather than hard-coding either.

   The list covers the SUB-COPY too, not just the heading: §2 paints <p> the
   muted grey and §3 paints .eyebrow navy, and both landed on the booking
   page's dark hero photograph. Buttons are deliberately excluded — they carry
   their own outlined treatment from wm-buttons.css. */
.hero h1, .hero h2, .hero h3,
.hero p, .hero li,
.hero .sub, .hero .tag, .hero .eyebrow,
.hero-content h1, .hero-content h2, .hero-content p, .hero-content .eyebrow,
.book-hero h1, .book-hero h2,
.book-hero p, .book-hero .sub, .book-hero .tag, .book-hero .eyebrow {
  color: inherit !important;
}

/* 15.2 — STAFF APPS: NAVY TEXT, NOT PURE BLACK.
   The owner and admin apps each carry an older legibility patch that repaints
   the whole app white and forces EVERY element to #000 with !important:
       owner:  #scr-app, #scr-app *, #login-screen, #login-screen *   (1,0,1)
       admin:  body *                                                 (0,0,2)
   Both beat the theme's body colour, so the two staff apps rendered pure black
   text while the rest of the site rendered navy — the charcoal conflict.

   Re-pointed here at the theme's own ink. NOTE the selectors are scoped to each
   app's own root ids, never to a bare `body *` — this file is global, and a
   blanket rule here would repaint the white hero copy on the public pages navy.
   The owner selectors match the original exactly and win on load order; the
   admin ones are id-scoped so they outrank `body *` on specificity. */
#scr-app,
#scr-app *,
#login-screen,
#login-screen *,
#admin-login,
#admin-login *,
#admin-app,
#admin-app *,
#nav, #nav *,
#mmenu, #mmenu * {
  color: var(--westmere-text) !important;
  -webkit-text-fill-color: var(--westmere-text) !important;
}

/* …with one exception: a button that is FILLED navy needs its white label back,
   or the press state is navy-on-navy and unreadable. Each of these outranks the
   blanket above.

   NOTE the custom pickers are deliberately NOT in this list. Their selected
   state used to be a filled navy block and therefore needed white copy here.
   It is now an outlined FRAME — a navy ring on white — so the blanket navy
   above is exactly the colour it wants, and adding an exception back would
   paint the value white on white and hide it all over again. The direction of
   this bug reverses with the fill. GUARDRAIL: button-style.test.js. */
#scr-app .wm-btn:active, #scr-app .btn:active,
#scr-app .btn-main:active, #scr-app .ls-btn:active,
#scr-app .wm-btn.is-pressed, #scr-app .btn-main.is-pressed, #scr-app .ls-btn.is-pressed,
#admin-app .wm-btn:active, #admin-app .btn:active,
#admin-app .btn-main:active, #admin-app .btn-new:active,
#admin-app .wm-btn.is-pressed, #admin-app .btn-main.is-pressed,
#admin-login .ls-btn:active, #admin-login .btn:active, #admin-login .ls-btn.is-pressed,
#login-screen .ls-btn:active, #login-screen .btn:active, #login-screen .ls-btn.is-pressed,
/* THE TWO THINGS THAT REST ON A FILL (§20.1): the one primary action, and the
   chosen half of a segmented control. Both are navy slabs, so both need their
   label back — without this the invoice form showed an empty navy block where
   "Account customer" should have been, and the send sheet's chosen half went
   blank the moment it was selected. */
#scr-app .btn-primary, #admin-app .btn-primary,
#scr-app .wm-seg.on, #admin-app .wm-seg.on,
.wm-btn.is-pressed {
  color: var(--westmere-white) !important;
  -webkit-text-fill-color: var(--westmere-white) !important;
}

/* 15.3 — THE SIGN-IN HEADER IS NOT THE SAME COLOUR ON EVERY APP.
   My Account's `.ls-hd` is a near-black car photograph; the driver portal and
   the owner app reuse the SAME class over a white panel. A blanket rule here
   cannot be right for both — forcing light copy blanked the driver's wordmark
   to white-on-white. So the theme does not decide this: My Account declares its
   own light copy at #login-screen .ls-hd (1,2,0), which outranks 15.2's
   #login-screen * (1,0,1) on its own, and every other app keeps the navy the
   theme gives it. */

/* 15.4 — THE PAY PAGE'S HEADER IS NO LONGER A NAVY BLOCK.
   It used to be filled navy, so the wordmark inside it needed white copy or it
   went navy-on-navy and vanished. The header is transparent now ("no filled
   headers anywhere"), so that white copy would vanish the other way — white on
   white. The wordmark takes the same navy as every other header.
   Guardrail: button-style.test.js. */
.wm-pay-page .head,
.wm-pay-page .head * {
  color: var(--westmere-navy) !important;
  -webkit-text-fill-color: var(--westmere-navy) !important;
}

/* =========================================================
   16. READABILITY — CORMORANT AT SMALL SIZES
   =========================================================
   Cormorant is a delicate, low-contrast-stroke Garamond revival with a SMALL
   x-height. It is beautiful at display sizes and genuinely faint at UI sizes —
   more so than the Didot it replaced, because Cormorant's Regular is lighter
   than most serifs' Regular. Now that it sets ALL type, the small end has to be
   raised deliberately or the apps become tiring to read.

   Three levers, applied in order of how much they cost the design:
     1. WEIGHT — the big one for Cormorant. Body and UI run at 500, micro-labels
                 and navigation at 600. 400 is reserved for large display, where
                 the delicacy is the point. (The 400/500/600/700 roman + italic
                 weights are all loaded at the top of this file.)
     2. SIZE   — nothing below 12.5px; wide-tracked uppercase micro-labels may
                 sit at 12px because the tracking does the separating work.
                 Cormorant's small x-height means these floors sit a little
                 higher than they would for another face.
     3. TRACKING — a little more letter-spacing at the small end, which buys
                 legibility without adding more weight.
   ========================================================= */

/* The root and the form controls inherit too. Browsers give <button>, <input>,
   <select> and <textarea> their own UA font rather than inheriting, which is how
   a stray Arial survives an otherwise complete sweep. */
html { font-family: var(--westmere-type) !important; }
button, input, select, textarea, optgroup { font-family: inherit !important; }

/* Body copy is MEDIUM, not Regular. Cormorant 400 at 16px reads as grey wash;
   500 gives it a stem you can actually follow. Large display stays at 400 —
   §3 pins h1–h4 and the hero explicitly, so they are unaffected by this. */
body { font-weight: 500 !important; }

/* ── The micro-label tier: uppercase, widely tracked, 11.5px floor ───────── */
.brand span, .brand-sub, .brand-sub2, .eyebrow, .ch, .prof-ch, .tb-sub,
.fl, .field label, .tag, .upd, .mini-legal, .mini-legal a,
.tab, .tab-sub, .stag, .sl, .ptype, .ppark, .meta,
.contact-label, .driver-title, .as-lbl, .jd-key, .sub-k {
  font-size: max(0.75rem, 12px) !important;
  font-weight: 600 !important;
  letter-spacing: 0.14em !important;
}

/* ── Navigation and actions: 12.5px floor, medium weight ─────────────────── */
.navlinks a, .nav-links a, .acct-nav, .nav-book, .book-btn, .nav-wa-mobile,
.bn, .side-item, .tc-change, .tc-cancel, .tc-again, .filter-btn {
  font-size: max(0.8rem, 13px) !important;
  font-weight: 600 !important;
}

/* ── Body / UI text ──────────────────────────────────────────────────────
   NOTE: `font-size: max(a, b)` on two constants is a FIXED size, not a floor —
   CSS cannot express "raise this element's own declared size to at least X".
   So this list holds ONLY the classes measured below the floor. It must never
   grow to bare `p`/`li`/`td`: doing that clamps the hero sub-head and the
   section copy DOWN to 13px, which is how the homepage lost its body scale
   the first time this was written. Everything not listed keeps its own size. */
.note, .sub-v, .as-desc, .trip-meta, .jd-val, .aj-meta-k, .drow-tag {
  font-size: max(0.85rem, 13.5px) !important;
  font-weight: 500 !important;
}

/* Form controls: never below 16px on touch, or iOS zooms the page on focus. */
input, select, textarea {
  font-size: max(1rem, 16px) !important;
  font-weight: 500 !important;
}

/* Buttons carry the shared sizing from wm-buttons.css; only weight/floor here.
   Button labels are short, uppercase and tracked — at Cormorant 500 they read
   as faint, so they take 600 like the rest of the UI tier. */
.wm-btn, .btn, button { font-weight: 600 !important; }
.wm-btn-sm { font-size: max(0.72rem, 11.5px) !important; letter-spacing: 0.12em !important; }

/* ── DARK SURFACES KEEP LIGHT COPY ───────────────────────────────────────
   Navy ink is the default everywhere, but a handful of bands are genuinely
   dark: the site footer, the fleet spec panel, the coverage intro scrim.
   Navy on those measured 1.20:1 — invisible. They take white. */
/* NOTE: .footer is NOT in this list any more — it is white now, so the
   white-copy rule below would make it invisible. It was: the contact
   details and the licence line rendered white-on-white on every page. */
.right-panel, .right-panel *,
#intro, #intro *,
.cta-band, .cta-band * {
  color: var(--westmere-white) !important;
  -webkit-text-fill-color: var(--westmere-white) !important;
}
/* …but .spec blocks are WHITE cards sitting INSIDE that dark panel (§4 whitens
   them), so their copy stays navy. Measured: without this they render 1.00:1. */
.right-panel .spec,
.right-panel .spec *,
#intro .card, #intro .card * {
  color: var(--westmere-navy) !important;
  -webkit-text-fill-color: var(--westmere-navy) !important;
}

/* …and the coverage scrim is deepened so white clears AA over the photo. */
#intro { background: rgba(12, 34, 55, .82) !important; }

/* 16.1 — THE SELECTED FILTER PILL NEEDS ITS FILL BACK.
   Section 6 sets `background:transparent !important` across the button list so
   nothing sits filled at rest. This rule used to make the SELECTED tab the one
   legitimate FILLED state, because wm-buttons.css gives it a white label and
   section 6 had stripped the navy underneath it — so "Upcoming" rendered
   white-on-white and the filter row looked like an empty box (1.00:1).

   The owner has since rejected filled highlights: the tab is a FRAME now, like
   the pickers. That fixes the same invisibility from the other direction —
   navy copy on white — and it is why .filter-btn.on had to come OUT of the
   white-copy exception list in §15.2 above. Left there, the label would be
   white on white all over again. GUARDRAIL: button-style.test.js. */
.filter-btn.on,
.tabs button.active,
#scr-app .filter-btn.on,
#admin-app .filter-btn.on {
  background: var(--westmere-white) !important;
  border-color: var(--westmere-navy) !important;
  color: var(--westmere-navy) !important;
  -webkit-text-fill-color: var(--westmere-navy) !important;
  font-weight: 500 !important;
}

/* 16.2 — READABILITY FLOORS FOR THE APP SURFACES.
   Same rule as §16, applied to the class names the apps use for their micro
   copy. Measured: these were rendering between 8.3px and 11.9px in a hairline
   didone — the bottom-nav labels and the "SOON" chip were the worst. */
.sub-soon, .bn-label, .aj-label, .drow-tag, .aj-meta-k, .sub-item,
.pp-note, .st-chip, .tc-when, .cr-hint, .jc2-ab, .sbt, .ts-lbl {
  font-size: max(0.75rem, 12px) !important;
  font-weight: 600 !important;
  letter-spacing: 0.1em !important;
}
.pp-card, .pp-cash, .aj-meta-v, .tc-route, .drow-v {
  font-size: max(0.82rem, 13px) !important;
  font-weight: 600 !important;
}

/* 16.3 — READABILITY FLOORS FOR THE STAFF APPS.
   The owner and admin apps carry the densest micro-copy on the site — sidebar
   labels and the assistant panel were down at 8.3px, which a didone simply
   cannot hold. These are all chrome and labels, never table data, so raising
   them costs no rows. (The admin's own table cells measured 12.5px+ already and
   are left exactly as they are — see §16.4.) */
.sb-label, .sb-sub, .sb-version, .aa-hd-s, .aa-hint, .aa-clear, .aa-send,
.ct, .tag, .tb-back, .tb-mode-label, .btn-new, .adm-cal-job-mini,
.jc2-pin, .jc2-prog-dot, .drv-lbl, .sett-k {
  font-size: max(0.75rem, 12px) !important;
  font-weight: 600 !important;
  letter-spacing: 0.1em !important;
}

/* 16.4 — WHERE READABILITY BEATS FONT PURITY.
   Nothing is exempted today: the admin's journey table renders its cells at
   12.8px Didot and cleared both the size floor and AA contrast when measured,
   so the whole system is genuinely one typeface. If a future column is ever
   squeezed below the floor, widen the column or shorten the label — do NOT
   drop this surface back to a second font, because that is exactly the
   inconsistency the owner asked to remove. */

/* =========================================================
   17. MY ACCOUNT'S CANVAS IS WHITE, NOT A CREAM VEIL
   =========================================================
   My Account paints the Sussex coast photograph TWICE:

     body::before    fixed, behind everything — the scenery you see in the
                     margin around the rounded sheet. Deliberate, and guarded
                     by rider-cache.test.js ("day mode never strips the My
                     Account scenery"). Left exactly as it is.

     .screen::before inset 0 across the app shell itself, the same photo behind
                     a 90–95.5% white veil. Its own source comment calls it
                     "cream veil over the coast" — and that is measurably what
                     it produced: the canvas composited to #f3f2f0, hue 40°,
                     squarely inside the cream band this theme exists to remove.

   That second layer is why the two My Account screenshots looked different.
   Both views have it; the dashboard just covers it with opaque white .dpanel
   cards, while My Details has one short card and leaves it showing below.
   So the "difference" was really the cream canvas becoming visible.

   It is neutralised to flat white here rather than deleted in the page, so the
   scenery around the sheet — and the guardrail protecting it — stay intact.
   To bring the frosted-glass look back, delete this rule. */
.screen::before {
  background: var(--westmere-white) !important;
}

/* 16.5 — WHAT CORMORANT SURFACED THAT DIDOT HAD NOT.
   Swapping the face re-ran the measurements and exposed small text the previous
   pass never flagged, because Didot's heavier Regular carried sizes Cormorant
   cannot. Worst of it: "Request booking" — the primary submit on the booking
   form — computed to 9.6px at weight 400, and a "Leave a Review" button to 8px.

   NOTE the deliberate absence of a bare `strong` or `a` rule. Both appear
   inside larger copy, and a fixed size on them would SHRINK bold words in a
   16px paragraph — the same trap that clamped the homepage body text earlier.
   Only the specific small contexts are listed. */
.btn, .btn.dark, .btn.review-cta, a.btn, .link-btn {
  font-size: max(0.82rem, 13px) !important;
  font-weight: 600 !important;
}
.quick-note, .stars, .chip, .feature strong, .footer-contact a {
  font-size: max(0.82rem, 13px) !important;
  font-weight: 500 !important;
}
.field-help .muted, .field-help span {
  font-size: max(0.78rem, 12.5px) !important;
  font-weight: 500 !important;
}
/* Fine print stays fine print, but 9.9px in Cormorant is not readable. */
.legal a, .legal, .mini-legal a, .mini-legal {
  font-size: max(0.75rem, 12px) !important;
  font-weight: 500 !important;
}

/* 16.6 — WEIGHT FLOORS FOR THE APPS.
   Cormorant's Light/Regular is where it gets faint. These are all elements
   that declare their own 300/400 and sit under 20px — trip routes, the topbar
   date, section sub-heads, admin table cells. Weight is raised, not size: a
   heavier stem costs no layout, whereas a size floor would reflow the tables.
   (Measured at 14px/300 for the trip route before this.) */
.tb-date, .sec-sub, .dp-note, .drow-v, .drow-arrow,
.trip-route, .tc-route, .aj-meta-v, .sub-v, .jd-val,
.side-id-sub, .side-id, .btn-main, .drow-tag {
  font-weight: 500 !important;
}
/* The route line and the table values are the content people actually read. */
.drow-v, .tc-route, .trip-route { font-weight: 600 !important; }

/* 16.7 — the last few micro-labels the app scan turned up. */
.side-pill, .sb-pill { font-size: max(0.72rem, 11.5px) !important; font-weight: 600 !important; }
.jd-tbl th, table th   { font-size: max(0.72rem, 11.5px) !important; font-weight: 600 !important; }
.side-id-sub           { font-size: max(0.75rem, 12px) !important; }

/* 16.8 — the last of it, found by scanning the rendered apps.
   .aj-from / .aj-to are the pickup and drop-off on the "Next Journey" card —
   the single most-read line in My Account — and they were sitting at 14px/300.
   .dp-head and .aj-step are section eyebrows and the progress labels. */
.aj-from, .aj-to, .aj-from span, .aj-to span {
  font-weight: 600 !important;
}
.dp-head span, .aj-step span, .aj-step {
  font-size: max(0.75rem, 12px) !important;
  font-weight: 600 !important;
}

/* =========================================================
   18. PICKERS — ONE SELECTED-STATE FOR THE WHOLE SYSTEM
   =========================================================
   The owner's decision: a selected day / time / option is an OUTLINED FRAME —
   a navy ring on white, navy text, rounded — never a filled navy block. It
   applies to every picker on every surface, so it is declared once here rather
   than re-derived per app:

     public booking form  .wm-day.is-selected      .wm-time.is-selected
     My Account           .cal-day.cal-sel         .time-opt.t-sel  .pick-opt.p-sel
     owner month view     .cal-day.selected

   SELECTED vs TODAY must stay tellable apart. Both used to be a navy ring —
   1.5px vs 1px on the booking form, and an identical 2px border in the owner
   app, which is to say not distinguishable at all. So the two states now differ
   in KIND, not in weight: selected is a frame, today is a dot beneath the
   number. Each page also carries these values itself, so a surface still looks
   right if the theme fails to load; this block is what keeps them agreeing.
   GUARDRAIL: server/tests/button-style.test.js. */
/* THE SELECTED DAY IS WHITE, NOT TRANSPARENT. This said `transparent` because
   the rule it enforces is "selected is a frame, not a slab" — and on a flat
   page those are the same thing. A calendar grid is not a flat page: it draws
   its own gap lines by painting --westmere-line behind every cell and letting
   it show between them. Transparent therefore let that grey through, and the
   day the owner had just tapped came out as the one filled slab in the month —
   the exact opposite of what this block is for. White is the page's own
   colour, so it is still not a fill (see NOT_A_FILL in no-fills.test.js); the
   navy ring that marks the selection is §32's. */
.wm-day.is-selected,
.cal-day.cal-sel,
.cal-day.selected {
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
  -webkit-text-fill-color: var(--westmere-navy) !important;
  font-weight: 500 !important;
}
.wm-time.is-selected,
.time-opt.t-sel,
.pick-opt.p-sel {
  background: var(--westmere-white) !important;
  color: var(--westmere-navy) !important;
  -webkit-text-fill-color: var(--westmere-navy) !important;
  border-color: var(--westmere-navy) !important;
  font-weight: 500 !important;
}

/* =========================================================
   19. AT A GLANCE — DATE, TIME AND FARE ON A TRIP CARD
   =========================================================
   The owner reads his job list off a phone in a windscreen cradle, often with
   the car moving. Three fields carry that glance: WHEN the job is, WHAT TIME,
   and HOW MUCH. Everything else on the card — name, route, passengers, bags —
   he reads when he is stopped.

   Those three used to be buried in the card's third line, at --text-sm in 40%
   ink: the smallest and faintest thing on the card. They are now their own row
   at --text-xl in full-strength navy, so every card has an obvious first stop.

   THE SIZE IS A TOKEN, NEVER A LITERAL. Re-tuning the glance for the whole
   system — both trip lists in the owner app, the full-screen trip page, and
   the admin's job rows — is a one-line edit to --text-xl in §1, or to the tier
   named here. That is the entire point of the token layer.

   Lining tabular figures: Cormorant's default old-style numerals hang below
   the baseline and vary in height, which is precisely wrong for a number read
   in a fraction of a second. Tabular also stops the fare jittering as the
   digits change.

   The row wraps rather than shrinks — at this size a narrow phone would rather
   drop the fare to its own line than render all three small, which would undo
   the change. GUARDRAIL: server/tests/owner-glance.test.js */
.wm-glance {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2, .5rem);
  margin: var(--space-2, .5rem) 0 var(--space-1, .25rem);
}
.wm-glance-when {
  display: flex;
  align-items: baseline;
  gap: var(--space-3, .75rem);
  min-width: 0;
}
.wm-glance-date,
.wm-glance-time,
.wm-glance-fare {
  /* No font-family here on purpose: the theme’s `html { font-family: var(--westmere-type)
     !important }` already puts every surface on Cormorant, and a second
     declaration would be a second source of truth for the typeface — which is
     exactly what typography.test.js forbids. */
  font-size: var(--text-xl, 1.5rem);
  font-weight: var(--weight-semi, 600);
  line-height: 1.1;
  color: var(--westmere-navy, #102a43);
  -webkit-text-fill-color: var(--westmere-navy, #102a43);
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
}

/* =========================================================
   20. NOTHING HIGHLIGHTS BY FILLING
   =========================================================
   The owner's standing rule, and the last place it had not been applied.
   Pickers, filter tabs, headers and footers were converted in earlier passes
   (§15.4, §16.1, §18). This section finishes the job for the two remaining
   families: PRIMARY BUTTONS and STATUS CHIPS.

   THE RULE: at rest, nothing in the system signals importance with a solid
   background. Emphasis is carried by a frame — a navy border on white — and
   by weight. A fill is reserved for the MOMENT of a press, where it is
   feedback rather than decoration, and is gone as soon as you lift your
   finger.

   WHY THE OLD ONES SURVIVED SO LONG. §6 de-fills buttons through a list of
   CLASS NAMES. The offenders in the staff apps carry no class at all — they
   are hand-written `<button style="background:rgba(27,27,26,.55)">` inside
   generated HTML — so no selector in this file could ever reach them, and an
   inline background beats a stylesheet anyway. They have been fixed at source
   (the markup now declares a frame, not a fill); what is below is the safety
   net that catches the next one, and it is scoped to the app roots so it
   cannot repaint the public site.
   GUARDRAIL: server/tests/no-fills.test.js */

/* 20.1 — Every button in the staff apps and My Account is a frame at rest,
   whatever classes it does or does not carry. */
/* TWO THINGS MAY FILL, and they are named rather than guessed at:
     .btn-primary  the one deliberate primary action on a screen, which the
                   owner asked for filled;
     .wm-seg.on    the chosen half of a segmented control — "account / one-off",
                   "a driver / an operator". A segment that cannot fill cannot
                   show which one is chosen, and the apps were setting an inline
                   navy background that this rule then silently threw away: the
                   invoice form opened with neither mode looking selected.
   Everything else is still caught, which is what this net is for. */
#scr-app button:not(.btn-primary):not(.wm-seg.on),
#admin-app button:not(.btn-primary):not(.wm-seg.on),
#login-screen button:not(.btn-primary):not(.wm-seg.on),
#admin-login button:not(.btn-primary):not(.wm-seg.on) {
  background-color: transparent !important;
  background-image: none !important;
}

/* 20.1a — THE SEGMENTED CONTROL. One of two (or three) answers, chosen. The
   frame is the same as every other control; the fill is the answer. */
.wm-seg{
  font-family: var(--sans); letter-spacing: .08em; text-transform: uppercase;
  border: var(--border-hair, 1px) solid var(--westmere-line-strong, #c8d1d9);
  border-radius: 6px; background: transparent; color: var(--westmere-navy, #102a43);
  cursor: pointer;
}
.wm-seg.on{
  background-color: var(--westmere-navy, #102a43);
  border-color: var(--westmere-navy, #102a43);
  color: var(--westmere-white, #ffffff);
}
/* …and fills for the length of a press. This is the one legitimate fill: it
   is momentary, and on a phone there is no hover to fall back on. The white
   label is already handled by §15.2's pressed-button exception list. */
#scr-app button:active:not(:disabled),
#admin-app button:active:not(:disabled),
#login-screen button:active:not(:disabled),
#admin-login button:active:not(:disabled),
#scr-app button.is-pressed,
#admin-app button.is-pressed {
  background-color: var(--westmere-navy, #102a43) !important;
  border-color: var(--westmere-navy, #102a43) !important;
}

/* 20.2 — STATUS CHIPS. One look for every state, on every surface.
   Pending, estimate sent, awaiting payment, prepaid, cash, account, confirmed,
   active, completed, cancelled, "change requested" — all of them. A state is
   told apart by its WORDS, which is what the owner reads anyway; it was never
   told apart by hue, because the amber/green/red washes were all so pale that
   at a glance they read as one grey. */
.wm-chip,
#scr-app .tag, #admin-app .tag,
#scr-app [class*="tag-"], #admin-app [class*="tag-"],
/* The staff apps name a few status chips their own way — the calendar entry's
   state, an invoice's state. They are chips like any other; they were simply
   invisible to a selector list built from the class names of the day. */
.adm-cal-status, .inv-status,
.tc-badge, .st-chip, [class*="badge-"] {
  background: transparent !important;
  background-color: transparent !important;
  background-image: none !important;
  color: var(--westmere-navy, #102a43) !important;
  -webkit-text-fill-color: var(--westmere-navy, #102a43) !important;
}
/* The frame goes on the chips that are a chip — a bordered token beside the
   thing they describe. The rider's `.st-chip` family is deliberately left
   borderless: those are eyebrows set in the flow of the card, not tokens, and
   giving them a ring would add the visual noise this section removes. */
.wm-chip,
#scr-app .tag, #admin-app .tag,
#scr-app [class*="tag-"], #admin-app [class*="tag-"],
.adm-cal-status, .inv-status,
.tc-badge {
  border: var(--border-hair, 1px) solid var(--westmere-line-strong, #c8d1d9) !important;
  border-radius: var(--radius-sm, 8px) !important;
  padding: .15rem .5rem !important;
}
/* A chip the owner must ACT on gets the firmer navy edge — the same way a
   primary button is the firmer edge and not a filled slab. */
.wm-chip-alert,
#scr-app .tag-await, #admin-app .tag-await,
#scr-app .tag-cancel, #admin-app .tag-cancel {
  border-color: var(--westmere-navy, #102a43) !important;
  font-weight: var(--weight-semi, 600) !important;
}

/* =========================================================
   21. "PRE-BOOKINGS ONLY" — ONE NOTE, ONE PLACE TO EDIT
   =========================================================
   Westmere is a PRE-BOOKED service. It does not take on-demand work, and a
   customer rang expecting a car within the minute — so the site now says so,
   at the three moments where that expectation forms: the booking form, the
   homepage, and the contact page.

   Deliberately NOT a banner. A warning strip on a premium site reads as an
   apology, and the people who need this are not skim-reading a banner — they
   are filling in a date field or dialling the number. So it is set as quiet
   secondary copy in the muted tone at --text-xs, sitting exactly where the
   decision is being made, with only the opening clause carrying weight.

   TO CHANGE HOW LOUD IT IS: this one rule. Raise it to --text-sm to make it
   more assertive, or drop `.wm-advance b`'s colour to --westmere-muted to make
   it quieter still. To change the WORDS, they appear in four places — grep
   "Pre-bookings only". The phrase is the owner's, and it is deliberately
   general: no lead time is quoted, because quoting one invites an argument
   about whether the customer met it. GUARDRAIL: server/tests/advance-booking.test.js */
.wm-advance {
  display: block;
  /* Breathing room on BOTH sides. Without the lower margin the note collides
     with whatever field comes next — in My Account it sat hard against the
     Passengers label and read as that field's caption rather than as a note
     about the two fields above it. */
  margin: var(--space-2, .5rem) 0 var(--space-3, .75rem);
  /* --text-sm, not --text-xs. Cormorant is a delicate face and --text-xs
     renders at 10.9px, which in the muted tone is quiet to the point of not
     being read — and a note nobody reads is the same as no note. This is the
     same 12px floor §16 applies to every other small label in the system. */
  font-size: var(--text-sm, .78rem);
  font-weight: var(--weight-normal, 400);
  letter-spacing: .04em;
  line-height: 1.65;
  color: var(--westmere-muted, #657485);
  -webkit-text-fill-color: var(--westmere-muted, #657485);
}
/* The opening clause is the part someone catches at a glance. It earns weight
   and full-strength ink — never a fill, never a colour of its own (§20). */
.wm-advance b {
  font-weight: var(--weight-semi, 600);
  color: var(--westmere-navy, #102a43);
  -webkit-text-fill-color: var(--westmere-navy, #102a43);
}
/* Inside the two customer apps §15.2 paints every descendant navy, which would
   erase the muted/emphasis distinction that carries the whole effect. */
#scr-app .wm-advance {
  color: var(--westmere-muted, #657485) !important;
  -webkit-text-fill-color: var(--westmere-muted, #657485) !important;
}

/* =========================================================
   22. PRIMARY IS THE LABEL, NOT A FILL
   =========================================================
   The owner chose this from a sheet of eight alternatives: the main action on
   any screen is a BOLDER, LARGER WORD inside the same outlined frame as
   everything else. No fill, no second colour, not even a heavier border — the
   label alone. It is the only signal that still works at arm's length in a
   moving car, which is where these buttons get pressed.

   The canonical definition is .wm-btn-primary in wm-buttons.css. This section
   does two things that file cannot:

     a) applies the same treatment to the app class names the button system
        maps as primary — those carry !important, and §16's readability floors
        (which load after wm-buttons.css) would otherwise pin them back to
        12px/600 and quietly flatten the hierarchy;
     b) gives the hand-written buttons in the staff apps a class to opt in with,
        .wm-primary, since most of them carry no class at all.

   ONE DIAL. To widen or narrow the gap between primary and secondary, edit
   --wmb-label-primary / --wmb-weight-primary in wm-buttons.css. Nothing below
   restates a size.
   GUARDRAIL: server/tests/no-fills.test.js */
.wm-primary,
#scr-app .wm-primary, #admin-app .wm-primary,
/* The compound forms matter: the public site's own `.btn` and `.btn.dark`
   rules carry !important too, so a bare `.wm-primary` (0,1,0) loses the tie on
   specificity and the booking form's main action stayed at the secondary size. */
.btn.wm-primary, .wm-btn.wm-primary, button.wm-primary, a.wm-primary,
.btn.dark.wm-primary,
.btn-main, .ls-btn, .aj-send-btn, .modal-btn-primary, .pp-card,
.btn-navy, .ibtn-primary, .btn-new, .wm-pay-page .btn,
#scr-app .btn-main, #admin-app .btn-main {
  background: transparent !important;
  background-image: none !important;
  color: var(--westmere-navy, #102a43) !important;
  -webkit-text-fill-color: var(--westmere-navy, #102a43) !important;
  border: var(--border-hair, 1px) solid var(--westmere-navy, #102a43) !important;
  border-radius: var(--radius-lg, 12px) !important;
  font-size: var(--wmb-label-primary, .9rem) !important;
  font-weight: var(--wmb-weight-primary, 700) !important;
  letter-spacing: var(--wmb-track-primary, .13em) !important;
  text-transform: uppercase !important;
}
/* A press still fills, for the moment your finger is down. §15.2's exception
   list already turns the label white underneath it. */
.wm-primary:active:not(:disabled), .wm-primary.is-pressed,
.btn-main:active:not(:disabled), .ls-btn:active:not(:disabled),
.pp-card:active:not(:disabled), .btn-navy:active:not(:disabled),
.wm-pay-page .btn:active:not(:disabled) {
  background: var(--westmere-navy, #102a43) !important;
  border-color: var(--westmere-navy, #102a43) !important;
}
/* Secondary stays exactly as it was: the base label, the hairline frame. The
   hierarchy is the DIFFERENCE between the two, so nothing here touches them. */

/* =========================================================
   23. THE HERO PHOTOGRAPH ROTATES
   =========================================================
   A slow crossfade between hero photographs, in the same register as the
   reviews — 6s dwell, 1.4s fade, matched deliberately so the two moving things
   on a page move at the same speed rather than arguing with each other.

   THE STACK, bottom to top:
     .hero-photo    the photographs, crossfading   z-index 0
     .hero:after    the page's own scrim            z-index 1
     .hero-content  the copy and the buttons        z-index 2

   The scrim sits ABOVE the photographs on purpose. Each hero already carries a
   gradient chosen to hold its heading legible; putting the rotation underneath
   means the overlay a page had over one photograph is the overlay it has over
   every photograph, so a bright one cannot wash the copy out halfway through a
   fade. Nothing here touches the scrim, the copy or the buttons.

   The photographs are only ever added by JS, and only when there are two or
   more and the visitor has not asked for reduced motion — so with one photo
   this section does nothing at all and the inline background stands.
   GUARDRAIL: server/tests/hero-rotate.test.js */
.hero-photo {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  /* Matches .review-fade.is-out — the same easing and the same 1.4s. */
  transition: opacity 1.4s ease;
  z-index: 0;
  pointer-events: none;
}
.hero-photo.is-on { opacity: 1; }

/* The scrim and the copy stay above the rotation. .hero-content already
   declares z-index:2; the pseudo-element is given its layer explicitly rather
   than relying on paint order, which changes the moment a positioned child is
   inserted — which is exactly what the rotation does. */
.hero:after { z-index: 1; }

/* Belt and braces: if the media query is honoured but the script has already
   built its layers (a visitor turning reduced motion on mid-visit), the fade
   stops rather than snapping between photographs. */
@media (prefers-reduced-motion: reduce) {
  .hero-photo { transition: none; }
}

/* =========================================================
   24. "NO BOOKING, NO DETAILS" — THE LINE THAT LETS PEOPLE TYPE
   =========================================================
   People hesitate over the quick estimate because typing an address feels like
   the start of a commitment. It is not, so the widget says so before they
   begin — not after, and not in the small print underneath.

   Its own class rather than reusing .wm-advance: that note carries a policy
   ("Pre-bookings only"), this one carries permission, and they sit on the same
   screen. Keeping them separate means either can be re-tuned or moved without
   dragging the other with it. Same quiet register though — the tokens are the
   same, one tier down in weight, because a reassurance that shouts is not
   reassuring. GUARDRAIL: server/tests/price-first.test.js */
.wm-reassure {
  display: block;
  margin: 0 0 var(--space-3, .75rem);
  font-size: var(--text-sm, .78rem);
  font-weight: var(--weight-normal, 400);
  letter-spacing: .04em;
  line-height: 1.6;
  color: var(--westmere-muted, #657485);
  -webkit-text-fill-color: var(--westmere-muted, #657485);
}

/* ═══════════════════════════════════════════════════════════════════════
   §25  BOOK NOW UNDER THE HOMEPAGE ESTIMATE
   ═══════════════════════════════════════════════════════════════════════
   The one control the quick-estimate offers, and only after a price is on
   screen (booking-app.js reveals it; index.html ships it hidden).

   LOOK IS NOT DEFINED HERE. It carries .wm-primary, so it is the same
   bolder-label frame as every other primary action in the system and follows
   the same dial — §22 owns the size, the weight and the frame. This section
   sets POSITION ONLY: full width under the estimate, with the estimate's own
   rhythm above and below it.
   GUARDRAIL: server/tests/price-first.test.js */
.qe-book {
  display: block;
  width: 100%;
  margin: var(--space-4, 1rem) 0 0;
  padding: var(--space-3, .75rem) var(--space-4, 1rem);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
/* `hidden` is the reveal switch. Layout `display:block` above would otherwise
   out-specify the attribute's UA style and the button would show before there
   was ever a price — the one thing this must never do. */
.qe-book[hidden] { display: none; }

/* ═══════════════════════════════════════════════════════════════════════
   §26  THE ESTIMATE BOX SAYS WHAT IT IS
   ═══════════════════════════════════════════════════════════════════════
   Two address fields look like a booking form no matter what the small print
   says, and visitors were hesitating over the first keystroke because of it.
   The muted line under this heading answers the fear ("no booking"); it could
   not also do the labelling, because at --text-sm in the muted tone it is
   skimmed rather than read.

   So the box gets a HEADING — the size a label needs to be to arrive before
   the fields do. Deliberately --text-xl and NOT the section scale: the h2
   above it runs to 3.2rem, and a second heading at that weight would compete
   with the page rather than caption the box. This one is plainly subordinate
   to the section title and plainly dominant over the fields.

   No fill, no rule, no box (§20) — it is emphasised by SCALE, the same way a
   primary button is emphasised by its label. Cormorant comes from the global
   face; only the tier, the weight and the ink are set here.
   GUARDRAIL: server/tests/price-first.test.js */
.qe-title {
  display: block;
  margin: 0 0 var(--space-1, .25rem);
  font-size: var(--text-xl, 1.5rem);
  font-weight: var(--weight-normal, 400);
  line-height: 1.15;
  letter-spacing: .005em;
  color: var(--westmere-navy, #102a43);
  -webkit-text-fill-color: var(--westmere-navy, #102a43);
}
/* One italic word, as everywhere else a heading carries emphasis in this
   system. Italic, not bold: bold at this size reads as a banner. */
.qe-title em { font-style: italic; }

/* ═══════════════════════════════════════════════════════════════════════
   §27  FIND US — INSTAGRAM AND TRUSTPILOT
   ═══════════════════════════════════════════════════════════════════════
   Instagram used to be the fourth item in a row of .78rem footer text links,
   between an email address and a phone number, and nobody saw it. Trustpilot
   did not exist at all.

   Both are now framed icon buttons: a navy hairline on white with a 20px
   monochrome glyph and an uppercase label, at a real 44px tap target. They
   sit in the footer on every public page, and again directly under the
   reviews block on the homepage, where somebody who has just read the
   testimonials is deciding whether to believe them.

   MONOCHROME, DELIBERATELY. Instagram pink and Trustpilot green are the two
   most obvious places a brand colour sneaks back into this system, and §20
   says nothing highlights by filling. These read as Westmere's own controls,
   not as two borrowed logos — emphasis comes from the frame and the glyph.
   A press still fills navy, the same as every other control.
   GUARDRAIL: server/tests/social-links.test.js */
.wm-social {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-3, .75rem);
  margin: var(--space-4, 1rem) 0 0;
}
.wm-social-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2, .5rem);
  min-height: 44px;                     /* the tap target, not a decoration */
  padding: var(--space-2, .5rem) var(--space-4, 1rem);
  background: transparent;
  border: var(--border-hair, 1px) solid var(--westmere-navy, #102a43);
  border-radius: var(--radius-btn, 11px);
  color: var(--westmere-navy, #102a43);
  -webkit-text-fill-color: var(--westmere-navy, #102a43);
  text-decoration: none;
  font-size: var(--text-sm, .78rem);
  font-weight: var(--weight-semi, 600);
  letter-spacing: var(--tracking-wide, .08em);
  text-transform: uppercase;
  transition: background var(--ease-ui, .2s ease), color var(--ease-ui, .2s ease);
}
/* The glyph carries the recognition, so it is the largest thing in the button
   — bigger than the label, which is only there for anyone who does not read
   icons. currentColor keeps it in step with the frame. */
.wm-social-link svg {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  display: block;
}
/* A press fills, for the moment a finger is down — feedback, not decoration
   (§20's one exemption). */
.wm-social-link:active {
  background: var(--westmere-navy, #102a43);
  color: var(--westmere-white, #ffffff);
  -webkit-text-fill-color: var(--westmere-white, #ffffff);
}
.wm-social-lead {
  display: block;
  margin: 0;
  font-size: var(--text-sm, .78rem);
  font-weight: var(--weight-normal, 400);
  letter-spacing: var(--tracking-wide, .08em);
  text-transform: uppercase;
  color: var(--westmere-muted, #657485);
  -webkit-text-fill-color: var(--westmere-muted, #657485);
}

/* ── §27b  THE SAME PAIR, IN THE HEADER ─────────────────────────────────
   The owner wants them at the TOP as well as by the reviews, so they are read
   before anyone scrolls.

   Placed as a direct child of <nav>, immediately before the burger, NOT inside
   .navlinks — that container is display:none under 900px, which is precisely
   the width where the owner most wants these seen. The burger is never hidden,
   so sitting beside it means they survive every breakpoint.

   ICON ONLY up here. A label beside each glyph doubles the width and, on a
   390px screen, pushes the pair into the burger. The accessible name is on
   aria-label, so a screen reader still hears "Westmere Private Hire on
   Instagram" rather than a bare link.

   .navlinks gets margin-left:auto so the desktop header keeps its shape:
   brand hard left, everything else hard right, exactly as before this pair
   arrived. Without it, nav's space-between would spread three children and
   push the links into the middle of the bar.
   GUARDRAIL: server/tests/social-links.test.js */
nav > .navlinks { margin-left: auto; }
.wm-social-nav {
  /* --space-5, not --space-3. Measured at 1280px, a 12px gap left the BOOK NOW
     frame and the Instagram frame 12px apart — two bordered boxes that close
     together read as one broken control rather than two. 24px separates the
     nav's actions from its social pair. */
  margin: 0 0 0 var(--space-5, 1.5rem);
  gap: var(--space-2, .5rem);
  flex-wrap: nowrap;
}
.wm-social-nav .wm-social-link {
  width: 44px;                 /* square, and still a full tap target */
  padding: 0;
  gap: 0;
  justify-content: center;
}
/* On a phone the brand and the burger compete for the same bar, so the pair
   pulls in — but only its outer margin. The gap BETWEEN the two icons stays at
   --space-2: measured at 390px the pair still clears the burger by 37px, so
   squeezing them to 4px bought nothing and made two frames look like one. */
@media (max-width: 620px) {
  .wm-social-nav { margin-left: var(--space-2, .5rem); }
}

/* =========================================================
   24. TRACKED HERO TYPE IS CENTRED ON ITS INK
   =========================================================
   letter-spacing puts its gap AFTER every letter — the last one included. So a
   tracked line's box ends one tracking unit past the final glyph, and
   text-align:center centres the BOX. The visible ink lands half a tracking unit
   to the LEFT of true centre.

   It was visible because each line is tracked differently, so the stack did not
   line up with itself. Measured on the homepage before this:

       WESTMERE          -3.12px    (.09em on 69.12px)
       PRIVATE HIRE      -2.05px    (.32em on 12.8px)
       TRAVEL, REDEFINED -0.61px    (.10em on 12px)
       the rule           0.00px    (no tracking — the only honest one)

   Each is pulled back by ONE tracking unit on the right; the box grows by that
   much, the centred line moves half of it, and the ink lands on the true
   centre. In em, so it tracks every font-size and every breakpoint by itself.

   THE VALUES MUST MATCH THE TRACKING, and the tracking is set in three places:
     · h1        §3 above (.09em) and §14's mobile override (.055em)
     · .sub      styles.css .hero .sub (.32em) — not overridden here
     · .tag      §16.3's micro-label tier (.1em), which catches the hero's tag
   Change one and this must change with it. server/tests/hero-centring.test.js
   reads both files and fails if they drift apart.

   margin-right, not padding-left: padding narrows the line, and WESTMERE
   already fills 330px of a 343px measure on a phone — a few pixels less and it
   wraps. This widens instead, and the few px of overflow fall inside the 6vw of
   slack either side of .hero-content.

   CENTRED HEROES ONLY. On .hero.left the text is flush left and the trailing
   space sits at the far end where nobody can see it; shifting those would
   introduce the very fault this removes. */
.hero.center h1   { margin-right: -0.09em; }
.hero.center .sub { margin-right: -0.32em; }
.hero.center .tag { margin-right: -0.1em; }

@media (max-width: 900px) {
  .hero.center h1 { margin-right: -0.055em; }
}

/* ── GOLD, AS COMPONENTS ─────────────────────────────────────────────────────
   The accent appears in four places and nowhere else, so that "add a bit of
   gold" is never a decision taken per screen: a section label, the hairline
   under it, the ring around a list avatar, and the underline on the active tab.
   Labels take --westmere-gold-ink because they are TYPE; the rule, ring and
   underline take --westmere-gold because they are not.
   GUARDRAIL: server/tests/button-style.test.js */
.wm-lab{
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--westmere-gold-ink); margin: 20px 0 8px;
}
.wm-rule{ height: 1px; background: var(--westmere-gold); opacity: .55; margin: 0 0 2px; }
.wm-ring{
  border: 1.5px solid var(--westmere-gold); border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: var(--westmere-navy); flex: 0 0 auto;
}
.wm-tab{
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  padding: 9px 0 10px; border-bottom: 3px solid transparent;
  /* ON WHITE. These sit above a list on the page ground, not on the navy bar —
     written in the bar's ink the unselected tab was white on white, and the
     only reason the live screen reads is that the page re-states both colours
     inline. A class whose own rendering is invisible is a trap for the next
     place it is used. */
  color: rgba(27,27,26,.45); background: transparent;
}
.wm-tab.on{ color: var(--westmere-navy); border-bottom-color: var(--westmere-gold); }

/* The balance pill on a people list: gold when money is owed out, plain when it
   is not. Here rather than in the page because a colour typed into markup is how
   a palette grows a sixth gold. */
.wm-pill{
  padding: .22rem .55rem; border-radius: 20px; white-space: nowrap;
  font-family: var(--sans); font-size: .76rem;
  border: 1px solid var(--westmere-line-strong); color: var(--westmere-navy);
}
.wm-pill.owed{ border-color: var(--westmere-gold); color: var(--westmere-gold-ink); }

/* A registration is a code read off a car on a kerb, not prose, so it is spaced
   out to be read character by character. It is NOT set in a monospace face: this
   system has one typeface by the owner's own instruction, with a single
   documented exception for Google's own letterform, and a plate is not worth a
   second. The mockup showed mono; the house rule wins.
   GUARDRAIL: server/tests/button-style.test.js */
.wm-plate{ letter-spacing: .12em; font-variant-numeric: tabular-nums; }

/* A figure that is outstanding — money owed in or out. Gold ink, so it reads as
   the one number on the screen that is waiting on somebody. */
.wm-owing{ color: var(--westmere-gold-ink); }

/* ── THE TOP OF THE PHONE BELONGS TO THE PHONE ───────────────────────────
   A full-screen layer pinned to inset:0 starts at the physical top of the
   display, which on an iPhone is behind the clock and the notch. The back
   control is the first thing in it, so the first thing in it is the thing you
   cannot tap — reported on the driver page and on every preview.

   The page header got this right years ago (`--safe-top`); layers opened on
   top of it did not, because each one sets its own inline geometry and there
   was nothing to inherit. This is that padding as a class, so a new overlay
   gets it by saying so rather than by remembering a calc().

   `env(safe-area-inset-top)` is zero on a desktop and on a phone with no
   notch, so this costs nothing where it is not needed — but it is zero on an
   iPhone too unless the page asks for `viewport-fit=cover`, which the staff
   apps do. GUARDRAIL: server/tests/safe-area.test.js */
.wm-safe-top{ padding-top: env(safe-area-inset-top, 0px); }

/* A sheet that rises from the bottom must stop short of the notch rather than
   sliding under it when its content is tall. */
.wm-sheet-top-cap{ max-height: calc(100vh - env(safe-area-inset-top, 0px) - 10px); }

/* ═══════════════════════════════════════════════════════════════════════
   §28  THE COMPACT LIST — A SPREADSHEET YOU CAN TAP
   ═══════════════════════════════════════════════════════════════════════
   The owner's rule for history: "less information on each page, then click to
   expand." A list is a few short columns and one line per row; the full
   addresses, the phone number, what was paid and how, and the driver's
   commission all live on the detail page the row opens.

   Markup comes from /wm-compact.js, so both staff apps get the same table and
   a guard can assert the columns. Rows are real tab stops with role=button —
   the spreadsheet is navigable by keyboard, not only by thumb.

   NOT translucent, NOT inherited: explicit white paper and explicit ink, with
   `color-scheme: only light`, because these tables render inside overlays that
   Android's Auto Dark would otherwise repaint to black-on-black. */
.wm-ctab-wrap{
  color-scheme: only light;
  background: var(--westmere-white);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.wm-ctab{
  width: 100%;
  border-collapse: collapse;
  /* The columns are the ones declared in /wm-compact.js, not the ones the
     longest cell asks for. Auto sizing is what pushed the drop-off — half the
     journey — off the right-hand edge of a phone. */
  table-layout: fixed;
  background: var(--westmere-white);
  color: var(--westmere-text);
  font-size: .86rem;
  /* A spreadsheet does not wrap. A row that wraps is two rows, and the eye
     loses the column. */
  white-space: nowrap;
}
.wm-ctab-h{
  text-align: left;
  padding: .45rem .7rem;
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--westmere-ink-soft);
  background: var(--westmere-white);
  border-bottom: var(--border-hair) solid var(--westmere-line-strong);
  position: sticky;
  top: 0;
  z-index: 1;
}
.wm-ctab-colchev{ width: 1.6rem; }
.wm-ctab-c{
  padding: .62rem .7rem;
  border-bottom: 1px solid var(--westmere-line);
  color: var(--westmere-text);
  max-width: 11rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.wm-ctab-h.num, .wm-ctab-c.num{ text-align: right; font-variant-numeric: tabular-nums; }
/* The reference is the thing he reads off an email, so it stays in figures that
   line up down the column. */
.wm-ctab-c:first-child{ font-variant-numeric: tabular-nums; color: var(--westmere-ink-soft); }
/* The passenger is who the row is ABOUT. */
.wm-ctab-c:nth-child(3){ font-weight: 600; }
/* ── A CANCELLED TRIP, INSIDE THE ONE LIST ──────────────────────────────
   Trip History holds everything finished, so a journey that never ran has to
   be unmistakable at a glance and still readable — not hidden, and not a
   greyed-out row the eye skips. Three signals, none of them colour alone:
   the route is struck through, the row sits on a tint, and it carries the word
   "Cancelled" in its own column. */
.wm-ctab-r.is-cancelled > .wm-ctab-c{ background: rgba(16, 42, 67, .035); }
.wm-ctab-r.is-cancelled > .wm-ctab-c:nth-child(4),
.wm-ctab-r.is-cancelled > .wm-ctab-c:nth-child(5){
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  color: var(--westmere-ink-soft);
}
/* The word itself stays full-strength type: it is the answer to the question
   the owner opened the list with. */
.wm-ctab-r.is-cancelled > .wm-ctab-c:nth-child(6){
  color: var(--westmere-text);
  font-size: .74rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.wm-ctab-r{ cursor: pointer; }
.wm-ctab-r:hover > .wm-ctab-c{ background: var(--westmere-hover); }
.wm-ctab-r:active > .wm-ctab-c{ background: var(--westmere-press); }
.wm-ctab-r:focus-visible{ outline: 2px solid var(--westmere-gold); outline-offset: -2px; }
/* The affordance: every row says it opens something. */
.wm-ctab-chev{
  width: 1.4rem;
  padding-left: 0;
  padding-right: .55rem;
  text-align: right;
  color: var(--westmere-ink-soft);
  font-size: 1.05rem;
  line-height: 1;
}
/* A PHONE. Five columns of prose do not fit in 390px however small the type
   gets, so the two addresses stack into one cell-width and the reference
   column — which he almost never reads off the list — gives up its room. */
@media (max-width: 480px){
  .wm-ctab{ font-size: .82rem; }
  .wm-ctab-h, .wm-ctab-c{ padding-left: .5rem; padding-right: .5rem; }
  /* The column heading gives way before the data does — "PASSENGE|" was one
     letter short of its own label while the names underneath it fitted. */
  .wm-ctab-h{ font-size: .62rem; letter-spacing: .05em; }
  .wm-ctab-c{ max-width: none; }
  /* THE REFERENCE GIVES UP ITS COLUMN. Five columns of prose do not fit in
     390px however small the type gets, and of the five the reference is the
     one he reads off an email rather than off this list — where the drop-off
     is half the journey and was the half falling off the right-hand edge. It
     is on the detail page, with everything else. */
  /* WHAT THE REMAINING FOUR GET. Freeing the reference's share does not
     redistribute it — a declared width is a declared width — so the phone
     widths are stated, per kind of table. A history is two place names and
     needs them wide; a driver's trips are two short figures and do not. */
  .wm-ctab-history col:nth-child(2){ width: 19% !important; }
  .wm-ctab-history col:nth-child(3){ width: 25% !important; }
  .wm-ctab-history col:nth-child(4){ width: 27% !important; }
  .wm-ctab-history col:nth-child(5){ width: 29% !important; }
  /* …and the same four, tighter, when a sixth column has to fit because the
     month contains a cancelled trip. The status column gets the room it needs
     to print the whole word: "CANC…" is not a label, and a row the owner has
     to widen his phone to read is not marked at all.

     AND THE TABLE KEEPS A FLOOR. Six columns in 390 pixels left the two place
     names at about fifty pixels each — "Steyn…", "Southa…" — which is not a
     place, it is a hint. It slides sideways instead, the way the invoice sheet
     does and the way a spreadsheet is expected to. */
  .wm-ctab-history-mixed{ min-width: 27rem; }
  .wm-ctab-history-mixed col:nth-child(2){ width: 17% !important; }
  .wm-ctab-history-mixed col:nth-child(3){ width: 24% !important; }
  .wm-ctab-history-mixed col:nth-child(4){ width: 17% !important; }
  .wm-ctab-history-mixed col:nth-child(5){ width: 20% !important; }
  .wm-ctab-history-mixed col:nth-child(6){ width: 22% !important; }
  /* Six columns in 390px: the gutters give before the words do. The date and
     the passenger have to be readable whole — they are how he finds the trip —
     and the two place labels ellipsis, which is what the detail page is for. */
  .wm-ctab-history-mixed .wm-ctab-h,
  .wm-ctab-history-mixed .wm-ctab-c{ padding-left: .35rem; padding-right: .35rem; }
  .wm-ctab-history-mixed .wm-ctab-h:nth-child(6),
  .wm-ctab-history-mixed .wm-ctab-c:nth-child(6){ padding-left: .2rem; padding-right: .15rem; }
  .wm-ctab-r.is-cancelled > .wm-ctab-c:nth-child(6){ font-size: .58rem; letter-spacing: .01em; }
  .wm-ctab-trips col:nth-child(2){ width: 21% !important; }
  .wm-ctab-trips col:nth-child(3){ width: 35% !important; }
  .wm-ctab-trips col:nth-child(4){ width: 24% !important; }
  .wm-ctab-trips col:nth-child(5){ width: 20% !important; }
  /* The column is collapsed to nothing rather than removed: `display:none`
     takes the cell out of the row, every later cell shifts up one, and each of
     them inherits the width of the column before it — the date column
     disappeared and the passenger came out as "M…". Zero width with the
     content clipped keeps cell and column in step. The widths are declared
     inline from the column data, so this has to outrank them. */
  /* …AND ONLY ON THE TABLES WHOSE FIRST COLUMN IS THE REFERENCE. This was
     written as `.wm-ctab col:first-child`, which is every table there is — so
     the saved-customers list, whose first column is the NAME, came out with a
     blank column down the left and no way to tell one row from another. Named
     by kind, so a table whose first column matters keeps it. */
  .wm-ctab-history col:first-child,
  .wm-ctab-history-mixed col:first-child,
  .wm-ctab-trips col:first-child{ width: 0 !important; }
  .wm-ctab-history .wm-ctab-h:first-child, .wm-ctab-history .wm-ctab-c:first-child,
  .wm-ctab-history-mixed .wm-ctab-h:first-child, .wm-ctab-history-mixed .wm-ctab-c:first-child,
  .wm-ctab-trips .wm-ctab-h:first-child, .wm-ctab-trips .wm-ctab-c:first-child{
    width: 0; padding-left: 0; padding-right: 0;
    overflow: hidden; white-space: nowrap; text-indent: -999px;
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   §29  THE LINE-ITEM SHEET — ONE TRIP, ONE ROW
   ═══════════════════════════════════════════════════════════════════════
   The invoice editor asked for a journey in two halves: Date, From and To on
   one line, then a Card tick, Fare, Com., Toll and a × on another, every input
   a different width. Seven trips came out as fourteen ragged rows and nothing
   lined up down the page — the owner's word for it was "messy".

   It is a SPREADSHEET now: one grid, declared once, so every row sits on the
   same columns and the eye can run down Fare without hunting. The address
   cells are BUTTONS rather than inputs — there is no room for an address in a
   column, and cramming one in is what made the row wrap in the first place.
   Tapping one opens the pop-out editor (WMAsk.prompt with the booking lookup)
   and the answer comes back as a short label.
   GUARDRAIL: server/tests/invoice-sheet.test.js */
.wm-sheet{
  color-scheme: only light;
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 9px;
  overflow: hidden;
}
/* THE ONE DECLARATION EVERY ROW READS. Date · From · To · Fare · Com · Toll ·
   Card · remove. Changing a width here moves the heading and every row with
   it, which is the whole point of their sharing it. */
.wm-sheet-row{
  display: grid;
  grid-template-columns:
    6.2rem                /* date   */
    minmax(0, 1fr)        /* from   */
    minmax(0, 1fr)        /* to     */
    4.6rem                /* fare   */
    4.2rem                /* com.   */
    4.2rem                /* toll   */
    2.4rem                /* card   */
    1.9rem;               /* remove */
  align-items: center;
  gap: .3rem;
  padding: .3rem .45rem;
  border-top: 1px solid var(--westmere-line);
}
.wm-sheet-row:first-child{ border-top: 0; }
/* A GRID TRACK ONLY WINS IF ITS CHILD LETS IT. A grid item's default minimum
   is its content, and a native date input's intrinsic minimum is wide — so
   without this the date cell shoved every other column sideways and the two
   place cells were squeezed to a letter each. The declared widths are the
   widths. */
.wm-sheet-row > *{ min-width: 0; }
.wm-sheet-head{
  background: var(--westmere-white);
  border-bottom: var(--border-hair) solid var(--westmere-gold);
  border-top: 0;
  padding-top: .45rem;
  padding-bottom: .4rem;
}
.wm-sheet-head > span{
  font-family: var(--sans);
  font-size: .6rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  white-space: nowrap;
  overflow: hidden;
}
/* ── A CUSTOMER INVOICE IS NOT A JOB SHEET ────────────────────────────────
   Com. and Card belong to a settlement with another firm: what Westmere keeps
   on that trip, and whether their driver already took the fare at the kerb.
   On an invoice to a passenger they are nobody's business and were never
   shown — so the two columns collapse rather than standing empty, and the grid
   closes up behind them. One row builder, one heading, two shapes.
   GUARDRAIL: server/tests/invoice-sheet.test.js */
.wm-sheet:not(.wm-sheet-op) .wm-sheet-row{
  grid-template-columns: 6.2rem minmax(0,1fr) minmax(0,1fr) 4.6rem 4.2rem 1.9rem;
}
.wm-sheet:not(.wm-sheet-op) .wm-sheet-row > :nth-child(5),
.wm-sheet:not(.wm-sheet-op) .wm-sheet-row > :nth-child(7){ display: none; }

.wm-sheet-row > .num, .wm-sheet-head > .num{ text-align: right; }
.wm-sheet-row > .mid, .wm-sheet-head > .mid{ text-align: center; }
/* Every control in the grid is the same height and sits on the same baseline;
   ragged widths were half the mess. */
.wm-sheet input[type="date"], .wm-sheet input[type="number"], .wm-sheet .wm-cell{
  width: 100%;
  box-sizing: border-box;
  min-height: 30px;
  padding: .3rem .35rem;
  font-family: var(--sans);
  font-size: .8rem;
  color: var(--westmere-text);
  background: var(--westmere-white);
  border: 1px solid var(--westmere-line-strong);
  border-radius: 5px;
}
.wm-sheet input[type="number"]{ text-align: right; }
/* THE ADDRESS CELL. A button that reads like a cell until you want it, then
   opens the pop-out. Truncated rather than wrapped: a row that grows a second
   line is the thing this replaced. */
.wm-sheet .wm-cell{
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.wm-sheet .wm-cell:hover{ border-color: var(--westmere-navy); }
.wm-sheet .wm-cell:focus-visible{ outline: 2px solid var(--westmere-gold); outline-offset: -2px; }
.wm-sheet .wm-cell.empty{ color: var(--westmere-muted); }
.wm-sheet-com{
  font-family: var(--serif);
  font-size: .82rem;
  color: var(--westmere-ink-soft);
  text-align: right;
}
.wm-sheet-x{
  width: 100%;
  min-height: 30px;
  padding: 0;
  border: 1px solid var(--westmere-line);
  border-radius: 5px;
  background: var(--westmere-white);
  color: var(--westmere-muted);
  font-size: .9rem;
  line-height: 1;
  cursor: pointer;
}
.wm-sheet-x:hover{ color: #9b1c1c; border-color: #9b1c1c; }
/* A PHONE. The two place cells keep the room; the figures tighten rather than
   the row wrapping, which is the one thing it must never do again. */
@media (max-width: 560px){
  /* A SPREADSHEET SCROLLS; IT DOES NOT FOLD. Seven usable columns do not fit
     390 pixels, and the three ways out are all worse than this one: squeezing
     the places to a letter each (which is what the first attempt did), hiding
     a column he types into, or wrapping the row — the very thing he asked to
     be rid of. So the sheet keeps a workable minimum and slides sideways
     inside its wrapper, heading and rows together, because they are one grid.

     The places get a floor of their own: an address clipped to three
     characters is the same complaint in a narrower form. */
  .wm-sheet{ min-width: 30rem; }
  .wm-sheet-row{
    grid-template-columns: 6.4rem minmax(6rem,1fr) minmax(6rem,1fr) 3.6rem 3.4rem 3.2rem 1.7rem 1.5rem;
    gap: .22rem;
    padding: .28rem .3rem;
  }
  .wm-sheet:not(.wm-sheet-op) .wm-sheet-row{
    grid-template-columns: 6.4rem minmax(6rem,1fr) minmax(6rem,1fr) 3.8rem 3.4rem 1.5rem;
  }
  .wm-sheet:not(.wm-sheet-op){ min-width: 24rem; }
  .wm-sheet input[type="date"], .wm-sheet input[type="number"], .wm-sheet .wm-cell{ font-size: .74rem; padding: .26rem .28rem; }
  .wm-sheet-head > span{ font-size: .54rem; letter-spacing: .04em; }
}

/* ═══════════════════════════════════════════════════════════════════════
   §30  THE HOUSE TREATMENT, ON THE STRUCTURE BOTH APPS ALREADY USE
   ═══════════════════════════════════════════════════════════════════════
   The blue/gold/white pass reached the screens it was built for — drivers,
   operators, invoices, trip history — and stopped. Everything else kept the
   plain look, and the owner's word for it was "blunt".

   THE REASON IS WORTH STATING, because it decides the fix. The redesign was
   applied COMPONENT BY COMPONENT: a screen got the gold when somebody put a
   .wm-lab or a .wm-rule on it. Most screens are drawn by JavaScript builders
   and never had one added, so they stayed bare — and re-styling thirty
   builders by hand is thirty chances to break a feature.

   So the treatment goes on the STRUCTURE instead. Both apps already agree on
   a handful of structural classes — a page heading, a card, a card head, a
   stat, a field label — and styling those reaches every tab at once, without
   touching the code that fills them. A screen that gains a card gains the
   look with it.

   NOTHING HERE CHANGES BEHAVIOUR: type, rules, spacing and colour only, all
   from the token layer. Every ink is the navy or a measured grey on white.
   GUARDRAIL: server/tests/design-consistency.test.js */

/* ── SECTION HEADINGS ────────────────────────────────────────────────────
   The owner app's .sec-title/.sec-sub and the admin's .page-title/.page-sub
   are the same thing: the name of the screen you are on. Gold rule under it,
   gold-ink eyebrow beneath — the rhythm the redesigned screens set. */
.pg > .sec-title,
.view > .page-hd > .page-title{
  font-family: var(--serif);
  color: var(--westmere-navy);
  letter-spacing: .01em;
}
.pg > .sec-sub,
.view > .page-hd > .page-sub{
  font-family: var(--sans);
  font-size: .76rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
}
.pg > .sec-title{ padding-bottom: .35rem; border-bottom: var(--border-hair) solid var(--westmere-gold); }
.view > .page-hd{ padding-bottom: .45rem; border-bottom: var(--border-hair) solid var(--westmere-gold); margin-bottom: 1.1rem; }

/* ── CARDS ───────────────────────────────────────────────────────────────
   One surface: white paper, a hairline, the same corner, the same shadow.
   They were three different treatments across the two apps. */
.card{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(16, 42, 67, .04);
}
/* A card's head carries the gold rule the section heading does, one step down. */
.card > .ch{
  border-bottom: var(--border-hair) solid var(--westmere-line);
  padding: .85rem 1.1rem;
}
.card > .ch > .ct{
  font-family: var(--serif);
  font-size: 1.02rem;
  font-weight: 500;
  color: var(--westmere-navy);
}
.card > .cb{ padding: 1.1rem; }

/* ── THE FIGURES ON A DASHBOARD ─────────────────────────────────────────
   A stat is a label, a number and a note. The number is the thing; the label
   above it is the house eyebrow, and the note under it is quiet. */
.stat{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 12px;
  padding: 1rem 1.1rem;
}
.stat > .sv{ font-family: var(--serif); font-size: 1.9rem; line-height: 1.1; color: var(--westmere-navy); }
.stat > .sl{
  font-family: var(--sans);
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  margin-top: .35rem;
}
.stat > .ss{ font-family: var(--sans); font-size: .76rem; color: var(--westmere-ink-soft); margin-top: .15rem; }

/* ── FORMS ───────────────────────────────────────────────────────────────
   A field label is the house eyebrow; a field is white paper with a hairline
   and a gold focus ring. They were six different borders. */
.fg{ margin-bottom: .9rem; }
.fl{
  display: block;
  font-family: var(--sans);
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  margin-bottom: .3rem;
}
.fi{
  width: 100%;
  box-sizing: border-box;
  background: var(--westmere-white);
  color: var(--westmere-text);
  border: var(--border-hair) solid var(--westmere-line-strong);
  border-radius: 7px;
  font-family: var(--sans);
}
/* The focus ring is the accent as an OUTLINE, the way the compact table's rows
   already do it — not a third gold at a third opacity. Two tokens, no more. */
.fi:focus{ outline: 2px solid var(--westmere-gold); outline-offset: 1px; border-color: var(--westmere-navy); }
.fi::placeholder{ color: var(--westmere-muted); }

/* ── LISTS OF JOBS, WHEREVER THEY ARE DRAWN ─────────────────────────────
   The day timeline and the job rows the builders produce: one rule between
   rows, one padding, one hover — so Today, Confirmed, Awaiting and the rest
   read as one app rather than four. */
.tl-card, .jc2, .job-card{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 10px;
}
.tl-time{ font-family: var(--sans); font-size: .72rem; font-weight: 600; letter-spacing: .1em; color: var(--westmere-gold-ink); }
.tl-name{ font-family: var(--serif); font-size: 1rem; color: var(--westmere-navy); }
.tl-route{ font-family: var(--sans); font-size: .84rem; color: var(--westmere-ink-soft); }

/* ── THE CALENDAR ────────────────────────────────────────────────────────
   Only the cell edges here. The rest of the month — the weekday strip, the
   number, today, selected, and what is written inside a day — is §32, which
   is where it belongs and, more to the point, where the class names are the
   ones the two apps actually emit. This block first went out addressing
   .cal-head and .cal-dnum, which exist in neither app; the calendar was
   therefore the one screen §30 never reached, which is exactly how it came
   back from the owner still looking like the old app. Guarded now: every
   selector in §30 and §32 must name a class one of the apps writes. */
.cal-day, .adm-cal-day{ border-color: var(--westmere-line) !important; }

/* ═══════════════════════════════════════════════════════════════════════
   §31  A JOB IS A CARD
   ═══════════════════════════════════════════════════════════════════════
   Every job list in both apps drew its rows as a bare border-top: no edge, no
   paper, nothing to say where one job ended and the next began. Beside the
   redesigned screens it read exactly as the owner put it — blunt.

   The composition goes on the WRAPPER, not inside it. What a job card says,
   and in what order — when, then who, then where — is the owner's own
   decision from an earlier pass and is already right; this gives it an edge to
   sit on, a rhythm down the page, and the gold hairline under the glance row
   that the rest of the app uses to separate a heading from its detail.
   GUARDRAIL: server/tests/design-consistency.test.js */
.wm-job{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 11px;
  margin-bottom: .55rem;
  overflow: hidden;
  transition: border-color .12s ease, box-shadow .12s ease;
}
.wm-job:last-child{ margin-bottom: 0; }
.wm-job:hover{ border-color: var(--westmere-line-strong); box-shadow: 0 1px 3px rgba(16,42,67,.06); }
/* The tappable head of the card — the part that opens the trip. */
.wm-job-tap{ padding: .8rem 1rem; cursor: pointer; }
.wm-job-tap:active{ background: var(--westmere-press); }
/* THE GOLD HAIRLINE UNDER THE GLANCE. The same move the section headings and
   the sheet make: the big line is the heading, and a rule separates it from
   what explains it. */
.wm-job .wm-glance{
  margin-top: 0;
  padding-bottom: .4rem;
  border-bottom: var(--border-hair) solid var(--westmere-gold);
  margin-bottom: .5rem;
}
/* A cancelled job is a record, not a job — the same quieting the compact list
   gives it, so the two surfaces agree. */
.wm-job.is-cancelled{ background: var(--westmere-hover); }

/* ── THE DETAIL TABLE, wherever a page lists a job's facts ───────────────
   Label and value were the same grey at the same size, so nothing on the page
   led and the eye had nowhere to start. The label is the house eyebrow; the
   value is navy at reading size; a hairline between rows lets the eye run
   down them. Part of §30's structural pass.
   GUARDRAIL: server/tests/design-consistency.test.js */
.wm-detail td{ vertical-align: top; }
.wm-detail tr + tr td{ border-top: 1px solid var(--westmere-line); }
.wm-detail td:last-child{ color: var(--westmere-text); padding: .4rem 0; }

/* ═══════════════════════════════════════════════════════════════════════
   §32  THE CALENDARS
   ═══════════════════════════════════════════════════════════════════════
   Both calendars were built before the palette and kept their own: grey
   weekday headings, a grey wash for today, grey dots, a grey nav button. The
   grid itself is sound — seven columns, a cell per day — so this is ink and
   edges, not a rebuild. Nothing about which day is which, or what is in it,
   changes.

   TODAY IS MARKED IN GOLD, and that is the one real decision here. It was a
   grey fill in admin and a small grey dot in the owner app, and on a month of
   white cells neither read as "you are here". Gold is the accent this palette
   has for exactly that, and as a RING rather than a fill — the owner's
   standing rule that nothing highlights by filling (§20).
   GUARDRAIL: server/tests/design-consistency.test.js */

/* ── The month, and the arrows either side of it ─────────────────────── */
.cal-month, .adm-cal-month{
  font-family: var(--serif);
  font-weight: 400;
  color: var(--westmere-navy);
  letter-spacing: .02em;
}
.cal-nav, .adm-cal-nav{ padding-bottom: .55rem; border-bottom: var(--border-hair) solid var(--westmere-gold); margin-bottom: .7rem; }
.cal-nav-btn, .adm-cal-nav button{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line-strong);
  border-radius: 7px;
  color: var(--westmere-navy);
}
.cal-nav-btn:hover, .adm-cal-nav button:hover{ border-color: var(--westmere-navy); }

/* ── The weekday strip is a row of labels, so it is the house eyebrow ── */
.cal-hd, .adm-cal-hd{
  font-family: var(--sans);
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  background: var(--westmere-white);
  border-bottom: var(--border-hair) solid var(--westmere-gold);
}

/* ── The grid ─────────────────────────────────────────────────────────── */
.cal-grid, .adm-cal-grid{
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 10px;
  background: var(--westmere-line);
}
.cal-day, .adm-cal-day{ background: var(--westmere-white); }
/* The hover tint and the out-of-month cell are both TRANSLUCENT values, and the
   grid paints --westmere-line behind every cell to draw its own gap lines. A
   translucent background on a cell therefore shows the grid's grey, not the
   page's white: the last day of September came out a solid blue-grey slab, two
   shades darker than anything else on the screen and the heaviest thing in the
   month. Composite over white explicitly. */
.cal-day:hover, .adm-cal-day:hover{
  background-color: var(--westmere-white);
  background-image: linear-gradient(var(--westmere-hover), var(--westmere-hover));
}
.cal-num, .adm-cal-num{ font-family: var(--sans); font-size: .86rem; font-weight: 500; color: var(--westmere-text); }
/* A day from the month either side is the same paper — only its number is
   quieter. That is the whole signal, and it does not need a fill. */
.cal-day.other, .adm-cal-day.other{ background: var(--westmere-white); opacity: 1; }
.cal-day.other .cal-num, .adm-cal-day.other .adm-cal-num{ color: var(--westmere-muted); }

/* TODAY: a gold ring, not a grey wash. */
.cal-day.today, .adm-cal-day.today{ background: var(--westmere-white); box-shadow: inset 0 0 0 2px var(--westmere-gold); }
.cal-day.today::after{ display: none; }
.cal-today-label{ font-family: var(--sans); font-size: .58rem; font-weight: 600; letter-spacing: .1em; color: var(--westmere-gold-ink); }
/* SELECTED is the navy ring — the thing you chose, against the thing today is. */
.cal-day.selected, .adm-cal-day.selected{ background: var(--westmere-white); box-shadow: inset 0 0 0 2px var(--westmere-navy); }
.cal-day.today.selected, .adm-cal-day.today.selected{ box-shadow: inset 0 0 0 2px var(--westmere-navy), inset 0 0 0 4px var(--westmere-gold); }

/* What is IN a day. (.cal-job-count, .cal-dots and .cal-summary are NOT here:
   the owner app carries rules for all three and emits none of them — the month
   grid writes job lines, and the month's figures go in the .sec-sub §30 already
   dresses. Styling a class nothing writes is how the calendar missed §30.) */
.adm-cal-pill{
  font-family: var(--sans);
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--westmere-navy);
  background: var(--westmere-white);
  border: 1px solid var(--westmere-line-strong);
  border-radius: 999px;
  padding: 1px 7px;
}
/* A day with nobody on it, and a day somebody is off, keep their own meaning —
   but say it in this palette rather than in two reds nothing else uses. */
.adm-cal-pill.warn{ background: var(--westmere-white); border-color: var(--westmere-gold); color: var(--westmere-gold-ink); }
.adm-cal-pill.off{ background: var(--westmere-white); border-color: var(--westmere-danger); color: var(--westmere-danger); }

/* ── What is written INSIDE a day ─────────────────────────────────────────
   Each job in a day was a grey-blue filled pill with its own inline style,
   and a day that had any was washed grey behind them. Two fills, where the
   house marks things with a rule and an edge. So: the wash goes, and the job
   becomes a line of type stood against a gold rule — the same gesture the
   job cards use (§31), at the size a 50px cell allows. A day that holds
   something now says so in its own content and in the weight of its number,
   which is the quiet signal; nothing fills. */
/* EVERY PIXEL OF THE 52 IS SPOKEN FOR. Measured: the cell is 52px wide, its
   own padding takes 4, the gold rule 2 and the chip's indent 3 — which leaves
   43 for the type, and 43 is eight characters at this size. That is why the
   towns are budgeted at eight, and why this padding is as tight as it is. */
.cal-chip{
  font-family: var(--sans);
  background: var(--westmere-white);
  border-left: 2px solid var(--westmere-gold);
  padding: 1px 0 1px 3px;
  margin-top: 3px;
  min-width: 0;
}
.cal-day{ padding-left: .12rem; padding-right: .12rem; }
/* TWO LINES, BECAUSE THE WIDTH IS NOT THERE AND THE HEIGHT IS. A phone cell
   has about 50px across; "16:30 Weppons Farm→Gatwick £95" needs 180 and was
   cut to "16:30…". Stacked, the same cell says WHEN on one line and WHERE TO
   on the next, and neither is truncated on an ordinary job. */
.cal-chip-t{
  display: block;
  font-size: .66rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--westmere-navy);
  font-variant-numeric: tabular-nums;
}
.cal-chip-w{
  display: block;
  font-size: .58rem;
  line-height: 1.25;
  letter-spacing: .01em;
  color: var(--westmere-gold-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A calendar event that is not one of ours is quieter, and its rule is grey. */
.cal-chip.ext{ border-left-color: var(--westmere-line-strong); }
.cal-chip.ext .cal-chip-t{ color: var(--westmere-ink-soft); font-weight: 500; }
.cal-chip.ext .cal-chip-w{ color: var(--westmere-muted); }
.cal-more{
  font-family: var(--sans);
  font-size: .6rem;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--westmere-muted);
  margin-top: 2px;
}
/* The destination line. The arrow leads it, so the two towns read as a
   journey down the cell rather than as two unrelated places. */
/* NO ARROW ON THIS LINE, and that is a measurement rather than a preference.
   The town line has 43px. "Heathrow" needs all 43 of them; an arrow, however
   it is set — a character, a ::before, an absolute mark in a gutter — costs
   seven or eight, and the only thing it can take them from is the place name.
   The journey reads down the cell, which is the order a journey is in: the
   top line is where he starts and the bottom line is where he ends. The
   explicit "Wiston → Heathrow" is in the cell's tooltip and in the day.
   The two lines are told apart by weight and ink instead. */
.cal-chip-w.to{ color: var(--westmere-navy); font-weight: 600; letter-spacing: 0; }
/* The day's number carries the fact that the day is busy. */
.cal-day.has .cal-num{ color: var(--westmere-navy); font-weight: 600; }
/* Room for two stacked chips and the footer, which is what a day with a
   morning and an afternoon job actually needs. */
.cal-day{ min-height: 94px; }
.cal-day.other{ min-height: 0; }

/* The day panel the grid drills into. */
.cal-day-count{
  font-family: var(--sans);
  font-size: .64rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  margin-left: .55rem;
}
.cal-day-total{ font-family: var(--serif); font-size: 1.05rem; color: var(--westmere-navy); margin-left: .5rem; }

/* ── The admin month says the same things in the same way ───────────────
   Its day cells carried the identical two habits: a job line in plain grey
   with nothing holding it, and a personal event drawing its own navy with a
   literal. Same treatment as the owner's chips above, at the admin's size. */
/* TWO LINES HERE TOO, AND NOT BY CHOICE. Measured: the line is 157px wide,
   the time takes 42 of it, and "Haywards → Gatwick" needs 126 of the 115 that
   are left — so on one line the second town was always going to be cut. The
   obvious fix, smaller type, is not available: §16.3 holds this class at a
   12px floor because the house face is a didone and cannot hold less, and
   that floor was the owner's own legibility pass. So the time takes a line
   and the journey takes the one under it, which is what the phone does. */
.adm-cal-job-mini{
  display: block;
  font-family: var(--sans);
  line-height: 1.35;
  color: var(--westmere-navy);
  border-left: 2px solid var(--westmere-gold);
  padding-left: .32rem;
  margin-top: .3rem;
}
.adm-cal-job-mini b{ display: block; font-weight: 600; font-variant-numeric: tabular-nums; }
.adm-cal-where{
  display: block;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--westmere-ink-soft);
}
.adm-cal-day{ min-height: 108px; }
.adm-cal-job-mini.ext{ border-left-color: var(--westmere-line-strong); color: var(--westmere-ink-soft); }
.adm-cal-more{
  font-family: var(--sans);
  font-size: .72rem;
  letter-spacing: .02em;
  color: var(--westmere-muted);
  margin-top: .18rem;
}
.adm-cal-day.has .adm-cal-num{ color: var(--westmere-navy); font-weight: 600; }
/* THE RING LADDER, stated once so the combinations cannot argue. An admin day
   can be today AND selected AND flagged at the same time, and each of those
   wanted the same box-shadow. The order of precedence is: what you chose beats
   where you are, and both beat a day that needs a driver — which keeps a ring
   so it is still findable, from the token rather than a literal, and says the
   actual words in its warn pill. */
.adm-cal-day.flagged{ box-shadow: inset 0 0 0 2px var(--westmere-line-strong); }
.adm-cal-day.today, .adm-cal-day.today.flagged{ box-shadow: inset 0 0 0 2px var(--westmere-gold); }
.adm-cal-day.selected, .adm-cal-day.selected.flagged{ box-shadow: inset 0 0 0 2px var(--westmere-navy); }
.adm-cal-day.today.selected, .adm-cal-day.today.selected.flagged{
  box-shadow: inset 0 0 0 2px var(--westmere-navy), inset 0 0 0 4px var(--westmere-gold);
}
/* Time off stays striped — it is the one thing on this grid that is an absence
   rather than a job — but in the same red the Off pill uses. The white has to
   be a background-COLOUR under the gradient, not a stop inside it: the grid
   paints its gap lines by showing through, so a translucent red stop over that
   grey came out as grey bunting rather than a red stripe. */
.adm-cal-day.blocked{
  background-color: var(--westmere-white);
  background-image: repeating-linear-gradient(45deg,
    transparent, transparent 6px,
    rgba(156, 40, 40, .12) 6px, rgba(156, 40, 40, .12) 12px);   /* --westmere-danger at 12% */
}

/* ── The key under the grid ──────────────────────────────────────────────
   Three swatches that are supposed to show you what a cell looks like. They
   showed a white square, a #555 grey slab and a red hatch — only the last of
   which matched its cell. Each swatch is now the cell it names. */
.adm-cal-key{
  display: flex;
  flex-wrap: wrap;
  gap: 1.1rem;
  margin-top: .8rem;
  font-family: var(--sans);
  font-size: .78rem;
  color: var(--westmere-ink-soft);
}
.adm-cal-sw{
  display: inline-block;
  width: 12px;
  height: 12px;
  vertical-align: -1px;
  margin-right: .15rem;
  border-radius: 2px;
  background: var(--westmere-white);
  border: 1px solid var(--westmere-line);
}
.adm-cal-sw.flagged{ border: 2px solid var(--westmere-line-strong); }
.adm-cal-sw.off{
  border-color: var(--westmere-line);
  background-image: repeating-linear-gradient(45deg,
    transparent, transparent 3px,
    rgba(156, 40, 40, .35) 3px, rgba(156, 40, 40, .35) 6px);   /* --westmere-danger at 35% */
}

/* ═══════════════════════════════════════════════════════════════════════
   §33  THE FIGURES — WHERE THE NUMBERS ARE DRAWN
   ═══════════════════════════════════════════════════════════════════════
   The dashboards are the last screens still on the old palette, and they are
   the ones with the most colour in them: a revenue chart in #6a6a6a grey, a
   booking breakdown keyed in six unrelated hues (a tailwind blue, two
   different greens, a navy, a grey), a busiest-times heatmap in grey alpha,
   and an owner earnings trio in which the middle card is a navy GRADIENT.

   THE RULE FOR A CHART IN THIS SYSTEM: a chart that compares like with like —
   twelve weeks of takings, a grid of hours — has ONE hue and varies only in
   value. Colour means something here (gold is an ornament, red is an absence,
   navy is the ink) and a chart that spends five hues on five statuses is
   saying five things it does not mean. So: a navy ramp, defined once, read by
   the bar chart, the heatmap and the breakdown key alike.

   WHY A RAMP AND NOT ONE NAVY: the chart's job is to let the eye find the big
   week without reading the labels. Value does that; a single flat navy does
   not. Five steps, each a measured tint of the house navy on white, so the
   lightest is still visible against the card and the darkest is the ink
   itself.
   GUARDRAIL: server/tests/design-consistency.test.js */
:root{
  /* THE STEPS ARE WHERE THEY ARE FOR A REASON. A heatmap cell carries its
     count, so each step has to hold type. Navy reads on 1–3 (13.6, 11.0 and
     5.1 against the 4.5 small text needs); white reads on 4 and 5 (5.6 and
     14.6). The gap between .36 and .75 is wide because that is exactly where
     the ink has to flip, and a step in between would hold neither colour. */
  --wm-ramp-1: rgba(16, 42, 67, .10);
  --wm-ramp-2: rgba(16, 42, 67, .22);
  --wm-ramp-3: rgba(16, 42, 67, .36);
  --wm-ramp-4: rgba(16, 42, 67, .75);
  --wm-ramp-5: var(--westmere-navy);
  /* The step a bar that has no takings gets: present, so the week is still a
     column on the axis, and quiet enough not to be read as money. */
  --wm-ramp-0: rgba(16, 42, 67, .07);
}

/* ── THE OWNER'S EARNINGS TRIO ──────────────────────────────────────────
   Three cards: today, this week, this month. The middle one was filled with a
   navy gradient and then printed its figure in --navy on top of it, which is
   navy on navy; it only ever showed up because §15.2 repaints ink inside the
   app and happened to reach it. A card does not need to be filled to be the
   one that matters — it is the middle of three and the biggest number on the
   screen. It gets the house emphasis instead: the gold rule. */
.earn-stat-card{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 11px;
  padding: .8rem .65rem;
}
.earn-stat-card::before{ display: none; }      /* the diagonal hatch */
.earn-stat-card.esk-accent{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-top: 2px solid var(--westmere-gold);
}
.esk-label{
  font-family: var(--sans);
  font-size: .6rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  margin-bottom: .35rem;
}
.esk-amount, .earn-stat-card.esk-accent .esk-amount{
  font-family: var(--serif);
  font-weight: 400;
  color: var(--westmere-navy);
}
.esk-jobs, .earn-stat-card.esk-accent .esk-jobs{
  font-family: var(--sans);
  font-size: .68rem;
  letter-spacing: .02em;
  color: var(--westmere-ink-soft);
}

/* ── THE COMPARISON LINE ────────────────────────────────────────────────
   One quiet line under the stats, in both apps: what this week has taken and
   what last week took by the same point. It is a sentence, not a stat card —
   the owner asked to be able to glance at it, not to study it — so it is set
   as running type under a hairline, with only the two figures in navy. */
.wm-compare{
  font-family: var(--sans);
  font-size: .8rem;
  line-height: 1.5;
  color: var(--westmere-ink-soft);
  padding-top: .5rem;
  margin: -.2rem 0 1rem;
  border-top: var(--border-hair) solid var(--westmere-line);
}
.wm-compare b{ font-weight: 600; color: var(--westmere-navy); }
/* Up, down or level: said in words and weight, never in red and green. The
   only hues this system spends are gold for ornament and red for an absence,
   and a quieter week is neither. */
.wm-compare .wm-compare-d{ color: var(--westmere-text); font-weight: 600; }

/* ── THE REVENUE CHART ──────────────────────────────────────────────────── */
.wm-chart-bar{ transition: opacity .12s ease; }
.wm-chart-val{
  font-family: var(--sans);
  font-size: 9px;
  fill: var(--westmere-muted);
}
.wm-chart-lbl{
  font-family: var(--sans);
  font-size: .72rem;
  letter-spacing: .04em;
  color: var(--westmere-muted);
  flex: 1;
  text-align: center;
}

/* ── THE BREAKDOWN KEY AND THE HEATMAP ──────────────────────────────────── */
.wm-key-row{ display: flex; align-items: center; gap: .6rem; margin-bottom: .35rem; }
/* The swatch is keyed to the size of the share, so the small ones are nearly
   white — they need an edge or they vanish and the row loses its left margin. */
.wm-key-sw{ width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; border: 1px solid var(--westmere-line); }
.wm-key-name{ flex: 1; font-family: var(--sans); font-size: .86rem; color: var(--westmere-navy); text-transform: capitalize; }
.wm-key-n{ font-family: var(--sans); font-size: .86rem; font-weight: 600; color: var(--westmere-navy); font-variant-numeric: tabular-nums; }
.wm-key-pct{ font-family: var(--sans); font-size: .8rem; color: var(--westmere-muted); width: 2.4rem; text-align: right; font-variant-numeric: tabular-nums; }
.wm-key-money{ font-family: var(--sans); font-size: .86rem; color: var(--westmere-navy); font-variant-numeric: tabular-nums; min-width: 4.4rem; text-align: right; }
.wm-heat{ border-collapse: separate; border-spacing: 1px; font-family: var(--sans); font-size: .74rem; width: 100%; }
.wm-heat th{ font-weight: 600; font-size: .62rem; letter-spacing: .08em; text-transform: uppercase; color: var(--westmere-gold-ink); padding: .2rem .25rem; }
.wm-heat th.wm-heat-day{ text-align: left; color: var(--westmere-navy); letter-spacing: .04em; white-space: nowrap; }
.wm-heat td{ padding: .22rem; text-align: center; border-radius: 3px; font-variant-numeric: tabular-nums; }
/* The ink flips to white once the cell is dark enough to need it. */
.wm-heat td.lv4, .wm-heat td.lv5{ color: var(--westmere-white); }
/* An hour with nothing in it is graph paper, not a hole: at step 0 the grid
   still reads as a grid, which is what lets the dark cells read as a shape. */
.wm-heat td.lv0{ background: var(--wm-ramp-0); color: transparent; }
.wm-heat td.lv1{ background: var(--wm-ramp-1); color: var(--westmere-navy); }
.wm-heat td.lv2{ background: var(--wm-ramp-2); color: var(--westmere-navy); }
.wm-heat td.lv3{ background: var(--wm-ramp-3); color: var(--westmere-navy); }
.wm-heat td.lv4{ background: var(--wm-ramp-4); }
.wm-heat td.lv5{ background: var(--wm-ramp-5); }

/* ── A HEADING INSIDE A PAGE, AND A ONE-FIGURE ROW ──────────────────────
   Two shapes the earnings page invented inline: a small serif heading above a
   group ("Mileage"), and a strip with a label on the left and one figure on
   the right (the tax-year total). Both are the house's own moves written out
   by hand in #1a1a1a; named here so the next one of each is free. */
.wm-subhead{
  font-family: var(--serif);
  font-size: 1.02rem;
  font-weight: 400;
  color: var(--westmere-navy);
  padding-bottom: .3rem;
  margin-bottom: .55rem;
  border-bottom: var(--border-hair) solid var(--westmere-gold);
}
.wm-figline{
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .8rem;
  padding: .6rem .85rem;
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 9px;
}
.wm-figline-l{
  font-family: var(--sans);
  font-size: .64rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
}
.wm-figline-v{ font-family: var(--serif); font-size: 1.15rem; color: var(--westmere-navy); }

/* The dot beside a line of recent activity. It carried `style="background:
   var(--navy)"` inline, which is the one place a colour must not live; and a
   7px dot is ornament, which in this system is gold. */
.notif-dot{ background: var(--westmere-gold); }

/* ── THE WEEK'S FIGURE, on the owner's earnings page ────────────────────
   One number the owner looks at more than any other. It was set on white
   paper under a faint diagonal hatch, with two grey slabs beneath it holding
   the job count and the average. The hatch was decoration the rest of the app
   does not have, and the slabs are a fill (§20). What is left is the house:
   paper, a gold rule under the eyebrow, the figure in serif, and the two
   readings in a pair of cells separated by a hairline. */
.wm-weekcard{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-radius: 12px;
  padding: 1.3rem 1.4rem;
  margin-bottom: .85rem;
}
.wm-weekcard-k{
  font-family: var(--sans);
  font-size: .64rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  padding-bottom: .4rem;
  border-bottom: var(--border-hair) solid var(--westmere-gold);
  margin-bottom: .6rem;
}
.wm-weekcard-v{ font-family: var(--serif); font-size: 2.8rem; line-height: 1; color: var(--westmere-navy); }
.wm-weekcard-s{ font-family: var(--sans); font-size: .82rem; color: var(--westmere-ink-soft); margin-top: .35rem; }
.wm-weekcard-pair{
  display: grid;
  grid-template-columns: 1fr 1fr;
  margin-top: 1.1rem;
  padding-top: .9rem;
  border-top: var(--border-hair) solid var(--westmere-line);
}
.wm-weekcard-pair > * + *{ border-left: var(--border-hair) solid var(--westmere-line); padding-left: 1rem; }
.wm-weekcard-n{ font-family: var(--serif); font-size: 1.35rem; line-height: 1.1; color: var(--westmere-navy); }
.wm-weekcard-l{
  font-family: var(--sans);
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  margin-top: .2rem;
}

/* ═══════════════════════════════════════════════════════════════════════
   §34  THE ADMIN'S OWN FOUR SCREENS
   ═══════════════════════════════════════════════════════════════════════
   Settings, the Record Book, Time Off and the Fare Editor. They were the last
   of the gap list, and they turned out to be in better shape than the rest:
   all four already compose out of .card/.ch/.cb/.fg/.fl/.fi, so §30 had
   reached most of them the moment it shipped. What was left is the bespoke
   furniture each one invented for itself — two tables, a settings row, a
   loading state, a hint — plus the handful of places where a size or a colour
   was written into the markup instead of named here.

   THE RECORD BOOK IS A STATUTORY DOCUMENT. Lewes District Council can ask for
   it, and it gets printed. So its table is given the plainest possible
   treatment: no fills, no zebra, hairlines and type — which is also what the
   house wanted anyway.
   GUARDRAIL: server/tests/design-consistency.test.js */

/* ── The journey record, and the fare grid ──────────────────────────────
   Both had a grey slab behind their heading row. A column heading is a label,
   so it is the house eyebrow over a gold rule, and the slab goes (§20). */
.jtable th, .fare-table th{
  font-family: var(--sans);
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  background: var(--westmere-white);
  border-bottom: var(--border-hair) solid var(--westmere-gold);
  padding: .5rem .8rem;
}
.jtable td{
  font-family: var(--sans);
  font-size: .86rem;
  color: var(--westmere-text);
  border-bottom: var(--border-hair) solid var(--westmere-line);
  padding: .6rem .8rem;
}
.jtable tr:hover td{ background: var(--westmere-hover); }
.jtable .sub-v{ font-size: .76rem; color: var(--westmere-muted); }
.fare-table td{
  border: var(--border-hair) solid var(--westmere-line);
  padding: .4rem .5rem;
  font-family: var(--sans);
  font-size: .9rem;
  color: var(--westmere-text);
  font-variant-numeric: tabular-nums;
}
/* The first column names the town, so it is type rather than a figure. */
.fare-table tr td:first-child{ text-align: left; color: var(--westmere-navy); font-weight: 600; }

/* ── A settings row: a thing, what it does, and a control ──────────────── */
.setting-row{ border-bottom: var(--border-hair) solid var(--westmere-line); }
.sr-title{ font-family: var(--sans); font-size: .95rem; color: var(--westmere-navy); font-weight: 500; }
.sr-sub{ font-family: var(--sans); font-size: .8rem; color: var(--westmere-ink-soft); margin-top: .1rem; font-weight: 400; }

/* ── A row in a plain list: a line, a line under it, and an action ─────── */
.wm-listrow{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .9rem;
  padding: .8rem 1rem;
  border-bottom: var(--border-hair) solid var(--westmere-line);
}
.wm-listrow:last-child{ border-bottom: none; }
.wm-listrow-main{ min-width: 0; }
.wm-listrow-t{ font-family: var(--sans); font-size: .94rem; font-weight: 600; color: var(--westmere-navy); }
.wm-listrow-s{ font-family: var(--sans); font-size: .84rem; color: var(--westmere-ink-soft); margin-top: .15rem; }

/* ── Loading, empty and failed ──────────────────────────────────────────
   Three states written inline on every screen that has them, each with its
   own padding and its own grey — and the failed one in #a33, a red that
   appears nowhere else. One state, said once. */
.wm-state{
  font-family: var(--sans);
  font-size: .92rem;
  color: var(--westmere-ink-soft);
  text-align: center;
  padding: 2rem 1rem;
}
.wm-state.is-bad{ color: var(--westmere-danger); }

/* ── A hint, and a note beside a heading ────────────────────────────────── */
.wm-hint{ font-family: var(--sans); font-size: .84rem; line-height: 1.5; color: var(--westmere-ink-soft); }
.wm-hint strong{ color: var(--westmere-navy); font-weight: 600; }
.wm-subhead-note, .ch-note{
  font-family: var(--sans);
  font-size: .7rem;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--westmere-muted);
}
.wm-subhead-note{ margin-left: .45rem; }

/* ── The display sliders in Settings ────────────────────────────────────
   Left at the browser's own blue, which is the one colour in the system that
   belongs to nobody. Navy track, gold thumb — ornament, and the only moving
   part on the screen. */
input[type="range"]{
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 18px;              /* the hit area; the RULE is drawn by the track */
  background: transparent;
  outline: none;
  margin: .3rem 0;
}
/* The track is its own box in both engines. Painted on the input itself it
   came out invisible at 2px once the thumb was 16 — a gold dot floating on
   nothing. */
input[type="range"]::-webkit-slider-runnable-track{
  height: 3px;
  border-radius: 3px;
  background: var(--westmere-line-strong);
}
input[type="range"]::-moz-range-track{
  height: 3px;
  border-radius: 3px;
  background: var(--westmere-line-strong);
}
input[type="range"]::-webkit-slider-thumb{
  margin-top: -6.5px;        /* centre the 16px thumb on the 3px track */
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--westmere-white);
  border: 2px solid var(--westmere-gold);
  cursor: pointer;
}
input[type="range"]::-moz-range-thumb{
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--westmere-white);
  border: 2px solid var(--westmere-gold);
  cursor: pointer;
}
input[type="range"]:focus-visible{ outline: 2px solid var(--westmere-gold); outline-offset: 3px; }

/* ── THE RECORD BOOK MADE THE WHOLE PANEL SCROLL SIDEWAYS ───────────────
   MEASURED, not guessed: at a 1280px window the admin document came back
   1373px wide, and the content column 1312 — wider than the window, so the
   topbar's own New Job button was pushed off the right edge on every screen,
   not just this one.

   The cause is the statutory journey record: fourteen columns, several of
   them `white-space: nowrap`, inside a `overflow-x: auto` wrapper. The
   wrapper is the right idea and it was not working, because `.main` is a flex
   child and a flex child does not shrink below the intrinsic width of its
   content unless it is told it may. Without `min-width: 0` the table's
   intrinsic width propagates all the way out to the page, and the scrollbar
   that should have been on the table ends up on the window.

   This is the same bug as the invoice sheet's grid tracks (§29), in a
   different box model, and it is worth saying twice: an overflow container
   only works if every ancestor between it and the viewport is allowed to be
   narrower than its contents. */
.main{ min-width: 0; }
.content{ min-width: 0; }
.jtable{ width: 100%; }

/* ═══════════════════════════════════════════════════════════════════════
   §35  THE CALENDAR IS THE PAGE
   ═══════════════════════════════════════════════════════════════════════
   The owner opened the month and found a title, a summary sentence and a card
   edge above it: the grid itself began a third of the way down a phone
   screen, and then only six short rows of it fitted. A calendar is the one
   screen in this app that is ALL content — there is nothing to introduce and
   nothing to filter — so it gets the whole page.

   What is left above the grid is the month, its two arrows, and the month's
   own two figures under its name. The card went: a card is a way of saying
   "this is one thing among several on this page", and there is nothing else
   on this page.

   AND A DAY OPENS AS A PAGE. The old day panel rendered under the grid, which
   worked when the grid was short and stopped working the moment it filled the
   screen — the tap redrew something below the fold and nothing appeared to
   happen. A day now uses .trip-page, the same full-screen chrome the trip and
   customer details already use, so it inherits the back control, Escape, and
   the behaviour a home-screen app needs from the device Back button.
   GUARDRAIL: server/tests/calendar-page.test.js */
.pg.pg-cal{
  max-width: 760px;
  padding: .55rem .5rem 0;
  min-height: 100%;
  box-sizing: border-box;
}
.pg.pg-cal.on{ display: flex; flex-direction: column; }

/* THE GRID TAKES THE REST OF THE HEIGHT. The first row is the weekday strip
   and must stay the size of its own type, so it is named explicitly; every
   day row after it shares what is left, down to the floor a stacked chip and
   the day's figures need. On a short screen the floor wins and the page
   scrolls, which is the right way round. */
.pg-cal .cal-grid{
  flex: 1;
  grid-template-rows: auto;
  grid-auto-rows: minmax(94px, 1fr);
  margin-bottom: .5rem;
}
.pg-cal .cal-nav{ padding: .35rem 0 .5rem; margin-bottom: .55rem; }
.cal-head-c{ text-align: center; min-width: 0; }
.cal-month-sub{
  font-family: var(--sans);
  font-size: .62rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  margin-top: .1rem;
  white-space: nowrap;
}
.cal-month-sub strong{ font-weight: 700; }
/* The arrows are now a glyph each, so they are square rather than wide. */
.pg-cal .cal-nav-btn{ min-width: 44px; padding: .5rem .7rem; font-size: 1.05rem; }

/* ── The day, popped out ────────────────────────────────────────────────
   The page title carries the date, so the body leads with the two figures
   instead of repeating it. */
#cal-day-page .cal-day-detail{ margin-top: 0; }
#cal-day-page .ch{ display: flex; align-items: baseline; gap: .6rem; }
.cal-empty{ font-family: var(--sans); color: var(--westmere-muted); }

/* ── The same page, in the admin ────────────────────────────────────────
   Its grid has the window's height rather than a phone's, and its cells are
   170px wide rather than 50, so it needs no new ideas — only the same removal
   of everything above the month, and the same rule that a day opens over the
   grid instead of under it. */
.view.view-cal{ display: none; }
.view.view-cal.on{ display: flex; flex-direction: column; min-height: calc(100vh - 11rem); }
.view-cal .adm-cal-grid{ flex: 1; grid-template-rows: auto; grid-auto-rows: minmax(108px, 1fr); }
.view-cal .adm-cal-nav{ padding-bottom: .5rem; }
.modal.modal-wide{ max-width: 820px; }

/* The day page repeats nothing the page title already says: on a single day's
   page the date beside every job is the date in the heading. */
#cal-day-page .wm-glance-date{ display: none; }
#cal-day-page .wm-glance-when::before{ content: none; }

/* ── WHO IS DRIVING IT ───────────────────────────────────────────────────
   A job the owner has passed on looked exactly like one still on his own
   plate — every confirmed job carries his own driver_id from the moment it is
   confirmed, so "has a driver" never distinguished them. The name of whoever
   has it now rides on the card, beside the status chip.

   AN OUTLINE, NOT A SLAB (§20), and the label above the name is the house
   eyebrow — so it reads as a fact about the job rather than another status.
   A firm takes the navy edge; one of his own drivers takes the gold.
   GUARDRAIL: server/tests/passed-job-name.test.js */
.wm-who{
  display: inline-flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  /* IT GIVES WAY BEFORE THE STATUS CHIP DOES. At flex-shrink:0 a long firm
     name — "Harding Executive Travel" — pushed CONFIRMED clean off the right
     edge of a 390px card. The name is the new information; the chip is the one
     the owner has always read. So this shrinks and ellipsises, and the chip
     keeps its place. */
  flex-shrink: 1;
  min-width: 0;
  max-width: 8.2rem;
  padding: .18rem .5rem;
  border: var(--border-hair) solid var(--westmere-gold);
  border-radius: 7px;
  background: var(--westmere-white);
  line-height: 1.2;
}
.wm-who-k{
  font-family: var(--sans);
  font-size: .56rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
}
.wm-who-n{
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 600;
  color: var(--westmere-navy);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.wm-who.firm{ border-color: var(--westmere-line-strong); }
.wm-who.firm .wm-who-k{ color: var(--westmere-muted); }

/* ── TWO MARKS THAT SAY THE REMINDERS WENT ───────────────────────────────
   The sweeper tells the rider and the driver twelve hours out, and the only
   way to know either had happened was to look in the database. A tick when it
   has gone, a dash when it has not — an outline either way, because this is a
   fact about the job and not a state to be shouted (§20).
   GUARDRAIL: server/tests/reminder-indicator.test.js */
.wm-rem{
  display: inline-flex;
  align-items: center;
  gap: .28rem;
  margin: 0 .35rem .25rem 0;
  padding: .12rem .5rem;
  border: var(--border-hair) solid var(--westmere-line-strong);
  border-radius: 999px;
  background: var(--westmere-white);
  font-family: var(--sans);
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--westmere-muted);
  white-space: nowrap;
}
/* SENT takes the gold edge and the gold-ink tick — the house's way of saying
   a thing is done, without filling anything. */
.wm-rem.on{ border-color: var(--westmere-gold); color: var(--westmere-navy); }
.wm-rem-t{ font-size: .78rem; line-height: 1; color: var(--westmere-muted); }
.wm-rem.on .wm-rem-t{ color: var(--westmere-gold-ink); }

/* ── ADJUST, beside what the driver is paid ──────────────────────────────── */
.wm-adjust{
  display: inline-block;
  margin-left: .4rem;
  padding: .12rem .5rem;
  border: var(--border-hair) solid var(--westmere-line-strong);
  border-radius: 6px;
  background: var(--westmere-white);
  font-family: var(--sans);
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--westmere-navy);
  cursor: pointer;
  min-height: 0;
}
.wm-adjust:hover{ border-color: var(--westmere-navy); }
.adm-rem-row{ display: flex; flex-wrap: wrap; align-items: center; margin-top: .3rem; }
/* The two document actions sit under the driver's name rather than beside it:
   "View driver email" and "Resend to driver" are longer than the row has left
   once the name and the payout have had their share. */
.wm-drv-acts{ display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .4rem; }
.wm-drv-acts .wm-adjust{ margin-left: 0; }


/* ══ §36 · EARNINGS FROM OUTSIDE WESTMERE ════════════════════════════════
   A short form he fills in at the end of a day — amount, who it was for, and
   the day it was. On a phone the three fields stack and the button is full
   width; there is nothing here that needs to be side by side, and a row of
   three 100px inputs is how a form stops being fillable with a thumb.

   NAMED, NOT HIGHLIGHTED. An outside entry is not an exception or a warning,
   so it is not coloured: it carries a gold rule at its head like any other
   section (§20 — nothing highlights by filling), and the source name does the
   distinguishing. The TOTAL line is the one piece of emphasis, because it is
   the answer he came to the page for.
   GUARDRAIL: server/tests/external-earnings.test.js */
.ext-add{ display: grid; gap: .55rem; padding: .2rem 0 .9rem; }
.ext-f{ display: flex; flex-direction: column; gap: .25rem; }
.ext-l{
  font-family: var(--sans);
  font-size: .72rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
}
.ext-add .fi{ min-height: 44px; font-size: 1rem; }
.ext-add-btn{
  width: 100%;
  min-height: 46px;
  padding: .6rem 1rem;
  border-radius: 8px;
  font-family: var(--sans);
  font-size: .82rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  background: var(--westmere-white);
  color: var(--westmere-navy);
  border: var(--border-strong, 1.5px) solid var(--westmere-navy);
  cursor: pointer;
}
@media (min-width: 560px){
  .ext-add{ grid-template-columns: 7rem 1fr 9.5rem auto; align-items: end; }
  .ext-add-btn{ width: auto; }
}

/* One entry: what it was worth, who it was for, and the day — with a quiet
   way to take it off again, because the only correction for a typed figure is
   removing it and typing it once more. */
.ext-row{
  display: flex;
  align-items: baseline;
  gap: .6rem;
  padding: .55rem 0;
  border-top: var(--border-hair) solid var(--westmere-line);
}
.ext-row-amt{
  font-family: var(--serif);
  font-size: 1.02rem;
  color: var(--westmere-navy);
  white-space: nowrap;
}
.ext-row-who{ flex: 1; min-width: 0; font-size: .92rem; color: var(--westmere-navy);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ext-row-when{
  font-family: var(--sans);
  font-size: .74rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--westmere-muted);
  white-space: nowrap;
}
.ext-row-x{
  background: transparent;
  border: var(--border-hair) solid var(--westmere-line-strong);
  border-radius: 6px;
  padding: .1rem .45rem;
  font-family: var(--sans);
  font-size: .72rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--westmere-navy);
  cursor: pointer;
}
.ext-total{
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .6rem;
  margin-top: .7rem;
  padding-top: .6rem;
  border-top: var(--border-hair) solid var(--westmere-gold);
}
.ext-total-l{
  font-family: var(--sans);
  font-size: .74rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
}
.ext-total-v{ font-family: var(--serif); font-size: 1.18rem; color: var(--westmere-navy); }
.ext-none{ padding: .8rem 0 .2rem; font-size: .88rem; color: var(--westmere-ink-soft); line-height: 1.5; }


/* ══ §37 · CUSTOMER SPEND ════════════════════════════════════════════════
   THIS PAGE HAD NO DESIGN AT ALL. Not a dated one — none: .sp-totals,
   .sp-table, .sp-bar-row and the rest were written by the app and dressed by
   nothing, so the browser's defaults drew the whole report. The owner's words
   were "isn't well organised"; what he was looking at was unstyled HTML.

   Worth saying why it survived four design passes: the guard that checks this
   sort of thing (design-consistency) looks for selectors the THEME dresses and
   no app writes — a rule with nothing to style. The opposite — a class the app
   writes and the theme has never heard of — looked like every utility class in
   the system and so looked like nothing. §37 and its guard close that.

   The table itself is no longer here: it is the shared spreadsheet (§28), the
   same one Trip History and the journeys list are drawn from.
   GUARDRAIL: server/tests/customer-spend-table.test.js */

/* The three figures at the head of the report. A row of plain readings under a
   gold rule — no cards, no fills; the numbers are the thing. */
.sp-totals{
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: .6rem 2rem;
  padding: 0 0 1rem;
  margin-bottom: 1.1rem;
  border-bottom: var(--border-hair) solid var(--westmere-gold);
}
.sp-tot-k{
  font-family: var(--sans);
  font-size: .72rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
}
.sp-tot-v{
  font-family: var(--serif);
  font-size: 1.6rem;
  line-height: 1.2;
  color: var(--westmere-navy);
  margin-top: .15rem;
  font-variant-numeric: tabular-nums;
}

/* The top ten, as bars. The ramp is §33's — a chart comparing like with like
   is one colour at different strengths, because the eye is being asked to read
   a SIZE and a second hue only gets in the way of that. */
.sp-chart-wrap{ margin-bottom: 1.3rem; }
.sp-chart-hd{
  font-family: var(--sans);
  font-size: .72rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  margin-bottom: .6rem;
}
.sp-bar-row{
  display: grid;
  grid-template-columns: minmax(0, 11rem) 1fr minmax(0, 6rem);
  align-items: center;
  gap: .8rem;
  padding: .3rem 0;
}
.sp-bar-name{
  font-size: .88rem;
  color: var(--westmere-navy);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sp-bar-track{
  height: .55rem;
  background: var(--wm-ramp-0);
  border-radius: 2px;
  overflow: hidden;
}
.sp-bar-fill{
  height: 100%;
  background: var(--wm-ramp-4);
  border-radius: 2px;
}
.sp-bar-val{
  font-family: var(--serif);
  font-size: .96rem;
  color: var(--westmere-navy);
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
@media (max-width: 560px){
  .sp-bar-row{ grid-template-columns: minmax(0, 7rem) 1fr minmax(0, 4.5rem); gap: .5rem; }
}

.sp-empty{
  padding: 1.4rem 0;
  text-align: center;
  font-size: .9rem;
  color: var(--westmere-ink-soft);
}
/* The definition, which is the part people argue about. Set as a note, under a
   rule, in the soft ink — present when it is wanted and quiet when it is not. */
.sp-note{
  margin-top: 1rem;
  padding-top: .8rem;
  border-top: var(--border-hair) solid var(--westmere-line);
  font-size: .84rem;
  line-height: 1.6;
  color: var(--westmere-ink-soft);
}
.sp-note b{ color: var(--westmere-navy); font-weight: 600; }

/* WHICH COLUMN IS THE ROW ABOUT. §28 bolds the third column and sets the
   first in the soft ink, because in a history the third is the passenger and
   the first is a reference. Neither holds here: on the spend report the second
   column is the customer and the third is their email, and on the saved list
   the FIRST column is the name. Said explicitly rather than left to a position
   that means something different in each table. */
.wm-ctab-spend .wm-ctab-c:nth-child(2){ font-weight: 600; color: var(--westmere-navy); }
.wm-ctab-spend .wm-ctab-c:nth-child(3){ font-weight: 400; color: var(--westmere-ink-soft); }
.wm-ctab-customers .wm-ctab-c:first-child{
  font-weight: 600;
  color: var(--westmere-navy);
  font-variant-numeric: normal;
}
/* A customer with nothing settled shows an em dash, not a nought — and it
   reads as an absence rather than as a figure. */
.wm-ctab-spend .wm-ctab-c:nth-child(7){ color: var(--westmere-ink-soft); }

/* A REPORT ROW IS NOT A BUTTON. Spend is grouped by email and name rather than
   by a saved customer, so these rows have nowhere to go — no chevron, no tab
   stop, and the pointer must not promise one. */
.wm-ctab-r.is-report{ cursor: default; }
.wm-ctab-r.is-report:hover > .wm-ctab-c,
.wm-ctab-r.is-report:active > .wm-ctab-c{ background: var(--westmere-white); }

/* The saved-customers list on a phone: four columns, and the name gets the
   room because it is how he finds the person. */
@media (max-width: 480px){
  /* "TRIPS" came out as "TRIP" — the heading was a letter short of its own
     label while the figures under it had room to spare. §28 already gives the
     heading the first squeeze; this gives the column the width. */
  .wm-ctab-customers col:nth-child(1){ width: 40% !important; }
  .wm-ctab-customers col:nth-child(2){ width: 16% !important; }
  .wm-ctab-customers col:nth-child(3){ width: 23% !important; }
  .wm-ctab-customers col:nth-child(4){ width: 21% !important; }
  /* Eight columns of a spend report do not fit a phone and are not asked to:
     it slides sideways, the way the invoice sheet does. */
  .wm-ctab-spend{ min-width: 46rem; }
}


/* ══ §38 · THE BUSINESS ACCOUNT ══════════════════════════════════════════
   A company's account inside My Account. It borrows the admin's vocabulary —
   a gold-ruled card head, the stat tiles, the shared spreadsheet — because the
   person reading it is doing an operator's job: she books the cars, checks the
   statement and takes the paperwork. The only things declared here are the
   handful the customer app did not already have.

   Nothing fills to highlight (§20): the account tag is a gold rule round a
   label, and the privacy note is a gold edge beside the sentence it belongs to.
   GUARDRAIL: server/tests/business-account.test.js */

.biz-only{ display: none; }
body.is-business .biz-only{ display: flex; }
body.is-business .personal-only{ display: none !important; }

/* The three or four readings at the head of a business page. The same shape as
   the admin's stat tiles, at a width a phone can hold two of. */
.biz-stats{
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: .6rem;
  margin-bottom: .9rem;
}
.biz-stat{
  background: var(--westmere-white);
  border: var(--border-hair) solid var(--westmere-line);
  border-top: 2px solid var(--westmere-gold);
  padding: .8rem .9rem;
}
.biz-stat-v{
  font-family: var(--serif);
  font-size: 1.5rem;
  line-height: 1.1;
  color: var(--westmere-navy);
  font-variant-numeric: tabular-nums;
}
.biz-stat-k{
  font-family: var(--sans);
  font-size: .64rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--westmere-gold-ink);
  margin-top: .25rem;
}
.biz-stat-s{ font-size: .78rem; color: var(--westmere-ink-soft); margin-top: .1rem; line-height: 1.4; }

/* A card head, the admin's one, scaled for a phone. */
.biz-ch{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  padding: .75rem 1rem;
  border-bottom: var(--border-hair) solid var(--westmere-line);
  font-family: var(--sans);
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--westmere-navy);
}
.biz-ch-r{ color: var(--westmere-muted); font-weight: 400; letter-spacing: .06em; }
.biz-cb{ padding: .9rem 1rem; }

.biz-tag{
  display: inline-flex;
  align-items: center;
  font-size: .6rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .1rem .45rem;
  border: var(--border-hair) solid var(--westmere-line-strong);
  border-radius: 4px;
  color: var(--westmere-navy);
  white-space: nowrap;
}
.biz-tag.gold{ border-color: var(--westmere-gold); color: var(--westmere-gold-ink); }

/* THE RULE THE WHOLE ACCOUNT TURNS ON, said where it applies rather than in a
   help page: the driver rings her, not the passenger. */
.biz-privacy{
  border-left: 2px solid var(--westmere-gold);
  padding: .1rem 0 .1rem .75rem;
  margin-top: .6rem;
  font-size: .86rem;
  line-height: 1.55;
  color: var(--westmere-ink-soft);
}
.biz-privacy b{ color: var(--westmere-navy); font-weight: 600; }

/* A field she cannot edit because the answer is not hers to choose. */
.biz-locked{
  background: rgba(16, 42, 67, .03) !important;
  color: var(--westmere-ink-soft) !important;
  border-bottom-color: var(--westmere-line) !important;
}
.biz-opt{ letter-spacing: 0; text-transform: none; color: var(--westmere-muted); font-size: .8rem; }
.biz-r2{ display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; }
.biz-add{ width: 100%; margin-top: .3rem; }
.biz-note{ font-size: .84rem; color: var(--westmere-ink-soft); line-height: 1.55; margin-top: .7rem; }

/* One figure against its label, down a panel. */
.biz-fig{
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .8rem;
  padding: .45rem 0;
  border-top: var(--border-hair) solid var(--westmere-line);
}
.biz-fig:first-child{ border-top: 0; }
.biz-fig-l{ font-size: .88rem; color: var(--westmere-ink-soft); }
.biz-fig-v{ font-family: var(--serif); font-size: 1rem; color: var(--westmere-navy); font-variant-numeric: tabular-nums; white-space: nowrap; }
.biz-fig.total{ border-top: var(--border-hair) solid var(--westmere-gold); margin-top: .4rem; padding-top: .6rem; }
.biz-fig.total .biz-fig-l{ font-family: var(--sans); font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--westmere-gold-ink); }
.biz-fig.total .biz-fig-v{ font-size: 1.25rem; }

/* A download she can take without asking anybody. */
.biz-dl{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .8rem;
  padding: .7rem 0;
  border-top: var(--border-hair) solid var(--westmere-line);
}
.biz-dl:first-child{ border-top: 0; }
.biz-dl-t{ font-size: .92rem; color: var(--westmere-navy); }
.biz-dl-s{ font-size: .8rem; color: var(--westmere-ink-soft); line-height: 1.4; }
.biz-dl a{ white-space: nowrap; }
.biz-empty{ padding: 1.4rem 1rem; text-align: center; font-size: .9rem; color: var(--westmere-ink-soft); }
