/* ============================================================================
   The portal's stylesheet.

   One hand-written file. No npm, no build step - the same ethos as the desktop
   UI, for the same reason: a site nobody can restyle without a toolchain is a
   site that stops being restyled.

   IT IS THE SAME DESIGN SYSTEM AS THE DESKTOP APP, deliberately. The tokens
   below are lifted from wwwroot/styles.css: the same five surface steps, the
   same two line colours, the same brand blue, the same pass/fail/warn/skip
   with their washes, the same space, radius and type scales. A customer who
   uses the bench app and then opens this site should not have to work out that
   they are the same product.

   What was here before was six colours and a radius, and every gap, size and
   shadow elsewhere was a hand-typed literal - which is the same complaint the
   desktop stylesheet opens with, and it had the same result: nothing quite
   lined up, and nothing looked like the app it belongs to.

   TWO DIFFERENCES FROM THE DESKTOP, both intentional:

     1. It has to work at 380px. Nobody reads a report on a phone at a bench,
        but a manager checking a serial from a warehouse floor does.

     2. Nothing else. Both halves now carry BOTH palettes and the same switch:
        the desktop was dark only, on the reasoning that a bench lives under
        workshop lighting, and a bench by a window disagreed.

   Root stays at 15px, matching the desktop, because every px below was
   authored against it.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   LIGHT IS THE BASE, AND DARK IS THE OVERRIDE.

   It was the other way round, keyed on prefers-color-scheme, which made the
   theme the operating system's decision and nobody else's. It is now the
   PERSON'S decision, stored against their portal.Users row, so choosing dark at
   a bench is still dark on this site and on the next PC they sign in to. See
   Pages/Account/Theme.cshtml.cs for the write and _Layout.cshtml for the read.

   The base block is unqualified on purpose. Every failure that can lose the
   attribute - a cookie that has not arrived, a claim from before this shipped,
   an error page rendered outside the layout, a value nobody recognises - then
   lands on light, which is the stated default and a complete palette. Putting
   the default inside [data-theme="light"] would make all of those render as
   tokenless: the browser's own colours behind rules that assume ours.

   A TOKEN ADDED HERE MUST BE ANSWERED BELOW if it is a colour, and must NOT be
   repeated below if it is not. The old light block redefined six of them and
   left the statuses and shadows at their dark values, which is how #22c55e came
   to sit on white at about 1.9:1 and every card carried a shadow tuned for a
   black canvas. Every colour token now appears in exactly two places.
   ------------------------------------------------------------------------ */
:root {
  /* Not `dark light`: this is a declared theme, not a negotiated one, and the
     value has to follow the attribute or form controls and scrollbars paint
     themselves for the opposite scheme. */
  color-scheme: light;

  /* ---- surfaces: five genuine steps, none of them equal to a line ----
     THE 2026-10-13 REBRAND: clean white on a cool grey canvas, one deep blue for everything you
     can press, colour otherwise kept for status. The owner's choice of three, "like Stripe or
     Shopify admin", for the reason given with it: calm, and easiest to read for somebody who is
     not confident with computers. */
  --bg-0: #f2f5fa;      /* page canvas, behind everything - a hair of the brand's blue in it */
  --bg-1: #ffffff;      /* header, rail, action bar */
  --surface: #ffffff;   /* cards */
  --surface-2: #f5f7fb; /* raised, hover, table heads */
  --surface-3: #e7ecf5; /* pressed, selected */

  /* ---- lines: deliberately distinct from every surface above ---- */
  --line: #e1e6ee;
  --line-2: #c8d0dd;

  /* ---- text: darker than before, because the person reading may need every bit of it ---- */
  --text: #0f172a;
  --text-2: #334155;
  --muted: #5b6a80;

  /* ---- brand + status, each with a wash for backgrounds ----
     The statuses are the 600-weight equivalents, not the dark palette's: at
     15px on white, #22c55e is about 1.9:1 and unreadable as text. These clear
     4.5:1. The washes are correspondingly lighter. */
  /* Deep blue: 6.7:1 on white as text, and the colour of every primary button. */
  --brand: #1d4ed8;
  --brand-strong: #1e3a8a;
  --brand-soft: rgba(29, 78, 216, .08);
  --on-brand: #ffffff;

  --pass: #15803d;  --pass-soft: rgba(21, 128, 61, .10);
  --fail: #b91c1c;  --fail-soft: rgba(185, 28, 28, .09);
  --warn: #a16207;  --warn-soft: rgba(161, 98, 7, .11);
  --skip: #64748b;  --skip-soft: rgba(100, 116, 139, .11);

  /* ---- the accent hues: which KIND of thing something is (2026-10-14) ----
     Never a status and never a surface: an icon chip, an avatar, a chart series. 700-weights,
     each at least 4.5:1 on white. See "A little colour" at the end of this file. */
  --c-teal: #0f766e;    --c-teal-soft: rgba(15, 118, 110, .10);
  --c-violet: #6d28d9;  --c-violet-soft: rgba(109, 40, 217, .09);
  --c-orange: #c2410c;  --c-orange-soft: rgba(194, 65, 12, .10);
  --c-pink: #be185d;    --c-pink-soft: rgba(190, 24, 93, .09);
  --c-sky: #0369a1;     --c-sky-soft: rgba(3, 105, 161, .10);
  --c-slate: #475569;   --c-slate-soft: rgba(71, 85, 105, .10);
  --on-hue: #ffffff;
  /* A job somebody is working on: the brand blue as a status. Its own token because the dark
     palette's brand blue is a button colour, 3.6:1 as small text on its own tint. */
  --info: #1d4ed8;      --info-soft: rgba(29, 78, 216, .09);
  /* A whole small tile in a status colour: solid, because the -soft tints are see-through and
     over the grey canvas the amber one turns to mud. */
  --pass-wash: #ecf8f0;  --warn-wash: #fdf6e3;  --fail-wash: #fdf0f0;
  /* The sales chart on Home: repairs, product sales, other, and the gross-profit line. Checked
     with the dataviz validator on white - see "Home: the money first". */
  --chart-1: #1d4ed8;  --chart-2: #0d9488;  --chart-3: #c2410c;  --chart-4: #7c3aed;

  /* Almost nothing, deliberately. A 45%-black drop shadow is a smudge on a
     white page rather than elevation. */
  --sh-1: 0 1px 2px rgba(15, 23, 42, .05), 0 1px 3px rgba(15, 23, 42, .04);
  --sh-2: 0 12px 32px rgba(15, 23, 42, .12);

  /* ---- space ---- */
  --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px; --s-5: 24px; --s-6: 32px;

  /* ---- radius ---- */
  --r-1: 6px; --r-2: 10px; --r-3: 14px; --r-pill: 999px;

  /* ---- type ---- */
  --fs-1: 11px; --fs-2: 12px; --fs-3: 13px; --fs-4: 14px; --fs-5: 16px;
  --fs-6: 20px; --fs-7: 26px; --fs-8: 32px;

  /* ---- motion ---- */
  --t-1: 120ms cubic-bezier(.2, 0, .2, 1);
  --t-2: 220ms cubic-bezier(.2, 0, .2, 1);

  /* The page pushed back behind the drawer. ANSWERED IN BOTH PALETTES, and it has to be:
     this ink over a white page is a clear dimming and the same ink over a near-black one is
     almost nothing, which would leave the panel floating with no edge to it. Same class of
     mistake as leaving a shadow tuned for a black canvas on a white page. */
  --scrim: rgba(16, 24, 40, .45);

  /* Two rings, not one: the inner ring is the surface colour, so the outer one
     reads on both panels and the brand-blue buttons. */
  --focus: 0 0 0 2px var(--bg-1), 0 0 0 4px var(--brand);

  --measure: 1180px;

  font-size: 15px;
}

/* ---------------------------------------------------------------------------
   Dark. The desktop app's palette, unchanged, so a technician who spends the
   day in the bench app and then opens this site is looking at the same product.

   Colours only. Space, radius, type and motion do not have a brightness.
   ------------------------------------------------------------------------ */
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg-0: #0a1020;
  --bg-1: #0f172a;
  --surface: #131c31;
  --surface-2: #1a2540;
  --surface-3: #24304f;

  --line: #222e4a;
  --line-2: #33415f;

  --text: #e8eef8;
  --text-2: #b0bdd2;
  --muted: #8494ad;

  --brand: #4f7df5;
  --brand-strong: #3d6cf0;
  --brand-soft: rgba(79, 125, 245, .16);
  --on-brand: #ffffff;

  --pass: #22c55e;  --pass-soft: rgba(34, 197, 94, .14);
  --fail: #ef4444;  --fail-soft: rgba(239, 68, 68, .14);
  --warn: #eab308;  --warn-soft: rgba(234, 179, 8, .14);
  --skip: #94a3b8;  --skip-soft: rgba(148, 163, 184, .14);

  --c-teal: #2dd4bf;    --c-teal-soft: rgba(45, 212, 191, .15);
  --c-violet: #a78bfa;  --c-violet-soft: rgba(167, 139, 250, .16);
  --c-orange: #fb923c;  --c-orange-soft: rgba(251, 146, 60, .15);
  --c-pink: #f472b6;    --c-pink-soft: rgba(244, 114, 182, .15);
  --c-sky: #38bdf8;     --c-sky-soft: rgba(56, 189, 248, .15);
  --c-slate: #94a3b8;   --c-slate-soft: rgba(148, 163, 184, .15);
  --on-hue: #0b1220;
  --info: #8fb0ff;      --info-soft: rgba(79, 125, 245, .18);
  --pass-wash: #12291f;  --warn-wash: #2a2614;  --fail-wash: #2c1820;
  --chart-1: #4f7df5;  --chart-2: #0d9488;  --chart-3: #ea580c;  --chart-4: #8b5cf6;

  --sh-1: 0 1px 2px rgba(0, 0, 0, .35);
  --sh-2: 0 12px 32px rgba(0, 0, 0, .45);

  /* Heavier than the light palette's. See the note there. */
  --scrim: rgba(0, 0, 0, .62);
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; height: 100%; }

/* ============================================================================
   The shell

   THE PAGE DOES NOT SCROLL. This site runs on touch screens where no scrollbar
   is available, so the document is a fixed grid the size of the glass and
   exactly ONE region inside it moves under a finger:

     signed in    header · nav · <main> (the pane) · the theme switch
     signed out   header · .stage (the page and its footer, together)

   It was the other way round - `body { min-height: 100vh }` and nothing else,
   so the document was the only scroller on every page and the header, the
   padding and the footer took about 324px of every screen before any content.

   THE LENGTHS BELOW LIVE HERE AND NOT IN :root, deliberately. themecheck
   requires every token in the :root block to be answered in the dark block,
   which is the right rule for a colour and meaningless for a length - a touch
   target has no dark value. Declared on body they are inherited by everything,
   including a <dialog> in the top layer, and they are nobody's palette.

     --tap     the smallest thing a finger is asked to hit. 48px, not the 44 of
               the guidelines: these screens are used standing up, at arm's
               length, by somebody holding a customer's laptop.
     --tap-sm  a second-rank control inside a row that already has a --tap one.
     --kb      how much of the bottom of the screen the on-screen keyboard is
               covering. Written by js/shell.js; zero until it says otherwise,
               so nothing depends on the script having run.
   ========================================================================= */
body {
  --tap: 48px;
  --tap-sm: 44px;
  --kb: 0px;
  --rail: 212px;
  --rail-slim: 92px;

  /* The space either side of whatever a pane holds: a plain margin on a narrow screen, and
     on a wide one whatever it takes to hold the content to --measure. A percentage, so it is
     worked out where it is USED, against the width of the thing it pads. */
  --gutter: max(var(--s-5), calc((100% - var(--measure)) / 2));

  margin: 0;
  height: 100vh;              /* for a browser with no dvh; the next line wins where it exists */
  height: 100dvh;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas: "top" "pane";
  background: var(--bg-0);
  color: var(--text);
  font-family: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: var(--fs-5);
  line-height: 1.55;

  /* TOUCH, ONCE, FOR THE WHOLE SITE. `manipulation` removes the 300ms wait and the
     double-tap zoom that turns a second tap on a button into a lurch; the overscroll
     rule stops a swipe that runs out of list from dragging the page or refreshing it;
     and the highlight is the grey flash a phone paints over anything tapped, which on
     a button that already has a pressed state reads as the screen flickering. */
  touch-action: manipulation;
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
}

/* NO BAR IS DRAWN ANYWHERE. A region that moves says so with a fade at its edge and the
   button js/shell.js puts in its corner - see .pane-more - not with a strip of chrome a
   finger cannot use and a kiosk does not show. */
* { scrollbar-width: none; }
*::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* Signed out: the page and its footer move together, as one region. */
.stage {
  grid-area: pane; min-width: 0; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column;
}
.stage > main { flex: 1 0 auto; }

/* Signed in: the wrapper steps aside, and <main> and the footer become cells of the
   grid themselves - one the pane, the other the foot of the rail. The navigation gets a
   row of its own along the bottom, until a wider screen moves it to the side. */
body.app {
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-areas: "top" "pane" "nav";
}
body.app .stage { display: contents; }
body.app main {
  grid-area: pane; min-width: 0; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
}

/* ---------- "There is more" ----------
   The only thing a scrollbar was still good for on a touch screen was saying that the
   region goes on. js/shell.js shows this while the pane can still move down, and a tap
   moves it a screenful - so a list is usable by somebody who never discovers the swipe. */
.pane-more {
  position: fixed; z-index: 6;
  /* js/shell.js writes these two from the pane's own edges. A screen divides <main> into a
     head, a body and a bar, and the button speaks for the BODY - placed against <main> it
     sat on top of the bar, over the very button the bar is there to offer. */
  inset-inline-end: var(--s-4); bottom: var(--s-4);
  width: var(--tap); height: var(--tap); border-radius: 50%;
  display: none; align-items: center; justify-content: center;
  background: var(--surface); color: var(--text-2);
  border: 1px solid var(--line-2); box-shadow: var(--sh-2);
  cursor: pointer;
}
.pane-more.show { display: inline-flex; }
.pane-more .ic { transform: rotate(90deg); }      /* #i-chevron points right */
.pane-more:active { transform: translateY(1px); }

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

/* One ring, everywhere, on the keyboard only. Nothing here removes an outline
   without putting something better in its place. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
summary:focus-visible { outline: none; box-shadow: var(--focus); border-radius: var(--r-1); }

::selection { background: var(--brand-soft); }

/* ---------- Icons ----------
   Sized in em so an icon inside a 13px chip and one inside a 16px button each
   match their text without a bespoke rule. Same class, same sprite and the same
   1.7 stroke as the desktop app. */
.ic {
  width: 1.15em; height: 1.15em; flex: 0 0 auto;
  display: inline-block; vertical-align: -.18em;
  fill: none; stroke: currentColor;
}
.ic.lg { width: 1.5em; height: 1.5em; vertical-align: -.32em; }

/* ---------- Skip link ----------
   The nav is six links before the content on every page.
   NOT `.skip`: that is also the grey tone of every status pill (.verdict.skip - booked in,
   in repair, not tested), and a bare `.skip` rule parked every one of those pills at
   left: -9999px, so the Stage column on Jobs read as empty. Found while widening the shell. */
.skip-link {
  position: absolute; inset-inline-start: var(--s-4); top: var(--s-4);
  background: var(--brand); color: var(--on-brand);
  padding: var(--s-3) var(--s-4); border-radius: var(--r-2); z-index: 20;
  /* Clipped to nothing until it takes focus, rather than parked at left: -9999px - which in a
     right-to-left page is the side that scrolls, and would have given every Urdu page a
     sideways scroll the width of the planet. */
  clip-path: inset(50%); white-space: nowrap;
}
.skip-link:focus { clip-path: none; text-decoration: none; }

/* ============================================================================
   Top bar
   ========================================================================= */
.topbar {
  grid-area: top; z-index: 10; min-width: 0;
  display: flex; align-items: center; gap: var(--s-5);
  padding: var(--s-2) var(--s-5);
  min-height: 60px;
  background: var(--bg-1);
  border-bottom: 1px solid var(--line);
}

/* THE MARK AND THE NAME (2026-10-13). The mark is a drawing with its own colours (#i-mark in
   _Icons.cshtml), so it is sized here and coloured nowhere. The product's name is the larger
   line and the maker the smaller: the desktop's "DS" tile put the company first, which is not
   the word anybody looking for the product types. */
.brand { display: flex; align-items: center; gap: var(--s-3); min-width: 0; min-height: var(--tap-sm); }
.brand:hover { text-decoration: none; }
.mark { width: 36px; height: 36px; flex: 0 0 auto; display: block; }
.brand-text { min-width: 0; line-height: 1.15; }
.brand-name {
  font-weight: 750; font-size: 18px; letter-spacing: -.015em;
  white-space: nowrap; color: var(--text);
}
.brand-sub { color: var(--muted); font-size: var(--fs-2); white-space: nowrap; }

/* ---------- The navigation ----------
   ONE ELEMENT, THREE PLACES, chosen by the glass and never by sniffing a device.

     narrow, or a portrait kiosk   a bar along the BOTTOM, within thumb reach, four
                                   sections and a More sheet
     a landscape tablet            a slim rail down the left, icon over label
     a counter monitor or a PC     a full rail, icon beside label

   It was a row in the header. There are between four and twelve tabs depending on who
   is signed in, and a row that does not wrap has room for about six of them at 1280px -
   the rest ran off the right of a header with no scrollbar behind it.

   The label is ALWAYS there. Below 660px it used to be dropped and the icon left to
   carry the section alone, which works where a tooltip can explain it and not on
   glass, where there is no hover to ask. */
.tabs {
  grid-area: nav; min-width: 0; min-height: 0;
  display: flex; align-items: stretch; gap: var(--s-1);
  padding: var(--s-1) var(--s-2);
  padding-bottom: max(var(--s-1), env(safe-area-inset-bottom));
  background: var(--bg-1);
  border-top: 1px solid var(--line);
}
.tab {
  flex: 1 1 0; min-width: 0; min-height: 56px;
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; padding: var(--s-1); border-radius: var(--r-2);
  color: var(--text-2); font-size: var(--fs-1); font-weight: 600;
  white-space: nowrap;
  transition: background var(--t-1), color var(--t-1);
}
.tab .ic { width: 22px; height: 22px; }
.tab span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.tab:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }

/* The bar holds five. Past that the fifth is More and the rest are in its sheet - which
   the layout decides, because it is the layout that knows how many this person has. */
.tab-extra { display: none; }

/* A rail, wherever there is the width for one beside the page. */
@media (min-width: 1100px), (min-width: 700px) and (orientation: landscape) {
  body.app {
    grid-template-columns: var(--rail-slim) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto;
    grid-template-areas: "top top" "nav pane" "foot pane";
  }
  .tabs {
    flex-direction: column; justify-content: flex-start;
    padding: var(--s-2); overflow-y: auto;
    border-top: 0; border-inline-end: 1px solid var(--line);
  }
  .tab { flex: 0 0 auto; min-height: 60px; }
  .tab-extra { display: inline-flex; }
  .tab-more { display: none; }
}

@media (min-width: 1100px) {
  body.app { grid-template-columns: var(--rail) minmax(0, 1fr); }
  .tab {
    flex-direction: row; justify-content: flex-start;
    gap: var(--s-3); padding: 0 var(--s-3); min-height: var(--tap);
    font-size: var(--fs-4); font-weight: 500;
  }
}

/* An active state at all, which is new. There was none: every tab looked
   identical whatever page you were on, so the nav told you where you could go
   and never where you were. */
.tab.on { background: var(--brand-soft); color: var(--brand); }
.tab.on:hover { background: var(--brand-soft); }

.spacer { flex: 1 1 auto; }

/* The identity chip is a menu. IT OPENS ON A TAP AND ON NOTHING ELSE - js/header.js
   toggles .open, and the keyboard reaches it through the same button.

   It used to open on :hover as well. On glass a hover is what a tap leaves behind: the
   menu opened under the finger that pressed something else, and stayed open until the
   next tap landed somewhere it did not cover. */
.who-menu { position: relative; flex: 0 0 auto; }

.who {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-1) var(--s-2); border-radius: var(--r-2);
  min-height: var(--tap);
  background: none; border: 0; cursor: pointer;
  color: var(--text); font: inherit;
  max-width: 240px;
}
.who:hover { background: var(--surface-2); }
.who-text {
  display: flex; flex-direction: column; line-height: 1.25;
  min-width: 0; text-align: end;
}
.who-text strong { font-weight: 600; font-size: var(--fs-3); }
.who-text span { color: var(--muted); font-size: var(--fs-2); }
.who-text strong, .who-text span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.who-caret {
  flex: 0 0 auto; color: var(--muted);
  transform: rotate(90deg);            /* #i-chevron points right; turn it down */
  transition: transform var(--t-1);
}
.who-menu.open .who-caret { transform: rotate(-90deg); }

.who-dropdown {
  position: absolute; inset-inline-end: 0; top: 100%;
  min-width: 240px; max-height: calc(100dvh - 72px); overflow-y: auto;
  display: none; flex-direction: column;
  padding: var(--s-1);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-3); box-shadow: var(--sh-2);
  z-index: 20;
}
.who-menu.open .who-dropdown { display: flex; }
.who-dropdown form { margin: 0; }

.who-item {
  display: flex; align-items: center; gap: var(--s-3); width: 100%;
  padding: var(--s-2) var(--s-3); border-radius: var(--r-2);
  min-height: var(--tap);
  background: none; border: 0; cursor: pointer;
  color: var(--text-2); font: inherit; font-size: var(--fs-4); text-align: start;
  white-space: nowrap;
}
.who-item:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.who-item .ic { color: var(--muted); flex: 0 0 auto; }
.who-item:hover .ic { color: var(--text); }
.who-sep { height: 1px; margin: var(--s-1) var(--s-2); background: var(--line); border: 0; }

/* ---------- The theme switch ----------
   Two renderings of one control. In the account menu it is a .who-item like
   every other row and needs nothing of its own beyond the form not adding a
   margin, which .who-dropdown form already handles.

   In the footer it is a small outlined button, sitting above the two lines of
   prose rather than beside them: the footer is centred text and dropping a
   control into that flow leaves it baseline-aligned with a sentence. */
.theme-form-inline { margin: 0 0 var(--s-4); }

.theme-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: var(--tap-sm);
  padding: var(--s-2) var(--s-3); border-radius: var(--r-pill);
  background: var(--surface); border: 1px solid var(--line);
  color: var(--text-2); font: inherit; font-size: var(--fs-3);
  cursor: pointer; white-space: nowrap;
  transition: background var(--t-1), color var(--t-1), border-color var(--t-1);
}
.theme-btn:hover { background: var(--surface-2); color: var(--text); border-color: var(--line-2); }
.theme-btn .ic { color: var(--muted); }
.theme-btn:hover .ic { color: var(--text); }

/* The whole page changes colour, so the change is not subtle and does not need
   an animation to be noticed - but the elements that DO animate (buttons,
   links, the icons above) would otherwise each run their own 120ms transition
   out of step, which reads as the page settling rather than switching. Nothing
   here overrides prefers-reduced-motion: var(--t-1) is one duration and the
   rule below is what turns it off. */
@media (prefers-reduced-motion: reduce) {
  .theme-btn { transition: none; }
}

/* ---------- The language switch ----------
   Three buttons, each a finger's size, the one in force pressed. In the footer they are the
   theme switch's outlined pills, in a row under it; in the account menu they are one row of the
   menu - the globe, then the three - so the menu does not grow by three rows to offer one choice.
   It shipped with no rules at all once, and the buttons were 21px tall: fitcheck A caught it on
   every signed-out page. */
.lang-form { margin: 0 0 var(--s-4); }
.lang-choices { display: inline-flex; align-items: center; gap: var(--s-1); }
.lang-choice {
  min-width: var(--tap-sm); min-height: var(--tap-sm);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-pill);
  background: var(--surface); border: 1px solid var(--line);
  color: var(--text-2); font: inherit; font-size: var(--fs-3);
  cursor: pointer; white-space: nowrap;
}
.lang-choice:hover { background: var(--surface-2); color: var(--text); border-color: var(--line-2); }
.lang-choice[aria-pressed="true"] {
  background: var(--brand-soft); color: var(--brand-strong); border-color: var(--brand); font-weight: 650;
}

.lang-form-menu { margin: 0; }
.lang-form-menu .lang-choices { display: flex; width: 100%; gap: var(--s-1); padding: 0 var(--s-3); }
.lang-form-menu .ic { color: var(--muted); flex: 0 0 auto; margin-inline-end: var(--s-2); }
.lang-form-menu .lang-choice {
  flex: 1 1 0; min-height: var(--tap); border-radius: var(--r-2);
  background: none; border-color: transparent; font-size: var(--fs-4);
}
.lang-form-menu .lang-choice:hover { background: var(--surface-2); border-color: transparent; }
.lang-form-menu .lang-choice[aria-pressed="true"] { background: var(--surface-2); border-color: var(--line); color: var(--text); }

/* ---------- A role's permissions ----------
   Grouped as Perm.All is - Repairs, Money, Sales and stock, Their shop - each tick with the
   sentence that says what it opens. A fieldset because a group of checkboxes is one question to
   a screen reader; its border and padding are the browser's and come off. */
fieldset.perm-group { border: 0; margin: 0; padding: 0; display: grid; gap: var(--s-1); }
fieldset.perm-group legend {
  padding: 0; margin: 0 0 var(--s-1);
  color: var(--muted); font-size: var(--fs-2); font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
}

/* Label/value pairs, e.g. the profile page. */
.facts {
  display: grid; grid-template-columns: max-content 1fr;
  gap: var(--s-2) var(--s-5); margin: 0 0 var(--s-4);
}
.facts dt { color: var(--muted); font-size: var(--fs-3); }
.facts dd { margin: 0; color: var(--text); }

/* ============================================================================
   Page frame
   ========================================================================= */
/* THE MEASURE IS PADDING NOW, NOT A MAX-WIDTH. <main> is the thing that scrolls, so it
   has to be as wide as its cell or a swipe that starts in the margin beside the content
   moves nothing. The side padding grows to hold the content to --measure instead. */
main {
  width: 100%;
  padding: var(--s-5) var(--gutter) var(--s-6);
}

.foot {
  border-top: 1px solid var(--line);
  padding: var(--s-4) var(--s-5);
  text-align: center; color: var(--muted); font-size: var(--fs-2);
  background: var(--bg-1);
}
.foot p { margin: var(--s-1) 0 0; }

/* SIGNED IN, THE FOOTER IS THE FOOT OF THE RAIL and holds the theme switch alone. The
   two sentences and the legal links cost about 164px of every screen; the links are in
   the account menu now, which is where somebody looks for them, and the switch stays
   here so there are still two on the page - the menu's and this one. On the bottom bar
   there is no rail to stand it in, and the menu's is one tap away. */
body.app .foot { display: none; }

@media (min-width: 1100px), (min-width: 700px) and (orientation: landscape) {
  body.app .foot {
    grid-area: foot; display: block;
    padding: var(--s-2);
    border-top: 1px solid var(--line); border-inline-end: 1px solid var(--line);
  }
  body.app .foot p { display: none; }
  body.app .foot .theme-form-inline { margin: 0; }
  body.app .foot .theme-btn {
    width: 100%; flex-direction: column; gap: 2px;
    border-radius: var(--r-2); font-size: var(--fs-1); white-space: normal;
  }
}

@media (min-width: 1100px) {
  body.app .foot .theme-btn { flex-direction: row; gap: var(--s-2); font-size: var(--fs-3); }
}

/* ---------- Page heading ---------- */
.head {
  display: flex; align-items: flex-end; gap: var(--s-4);
  flex-wrap: wrap; margin-bottom: var(--s-5);
}
.head > div { min-width: 0; flex: 1 1 320px; }
.head .actions { margin-inline-start: auto; display: flex; gap: var(--s-2); }

/* A single control dropped straight into .head - the export button - without needing an
   .actions wrapper for one child. `align-items: flex-end` above already sits it on the
   baseline of the heading block beside it. */
.head > .btn { margin-inline-start: auto; flex: 0 0 auto; }

/* ---------- Stat tiles ----------
   The counts across a whole filtered set, and each one is also the filter for its own slice:
   the number and the way to see what is behind it are the same control, so nobody has to find
   a separate chip that says the same word. */
.tiles {
  display: flex; flex-wrap: wrap; gap: var(--s-3);
  margin: 0 0 var(--s-4);
}
.tile {
  flex: 1 1 130px; min-width: 0;
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--line); border-radius: var(--r-3);
  background: var(--surface); color: var(--text-2);
  transition: border-color var(--t-1), background var(--t-1);
}
a.tile:hover { border-color: var(--line-2); text-decoration: none; }
.tile.on { border-color: var(--brand); background: var(--brand-soft); }

.tile-n {
  font-size: var(--fs-6); font-weight: 650; line-height: 1.1;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.tile-k,
.k {
  font-size: var(--fs-2); text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted);
}

/* Colour marks the exception, not the norm: the plain total stays plain, and only the three
   that mean something is wrong - or right - carry it. If every tile were coloured, none of
   them would be. */
.tile.pass .tile-n { color: var(--pass); }
.tile.fail .tile-n { color: var(--fail); }
.tile.warn .tile-n { color: var(--warn); }

h1 {
  font-size: var(--fs-7); line-height: 1.2; font-weight: 650;
  margin: 0 0 var(--s-1); letter-spacing: -.01em;
}
h2 {
  font-size: var(--fs-2); margin: 0 0 var(--s-3); color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em; font-weight: 700;
}
/* A LEAD NEEDS ITS OWN GAP. It was margin: 0, which is right only inside .head - that
   provides the space itself. At the top level, and .card has no margin-top, the first
   card sat flush against the sentence introducing it. Two pages did exactly that. */
.lead { color: var(--text-2); margin: 0 0 var(--s-5); font-size: var(--fs-5); }
.head .lead { margin-bottom: 0; }

.back {
  display: inline-flex; align-items: center; gap: var(--s-1);
  font-size: var(--fs-3); color: var(--muted); margin: 0 0 var(--s-2);
}
/* The way back is a control, not a caption: it was 20px of inline text. It is written two
   ways across the site - the class on a paragraph round the link, and on the link itself -
   and both have to be a target. */
.back a, a.back {
  display: inline-flex; align-items: center; gap: var(--s-1);
  min-height: var(--tap-sm); padding-inline-end: var(--s-3); color: inherit;
}
.back:hover { color: var(--brand); text-decoration: none; }

/* ---------- Cards ---------- */
.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-3); padding: var(--s-5);
  margin-bottom: var(--s-4);
}
.card > :last-child { margin-bottom: 0; }

/* The one card on a page of its own - sign in, first run, change password.
   Matches the desktop's lock screen: centred, lifted, one thing to do. */
.card.narrow {
  max-width: 440px; margin: 0 auto;
  background: var(--bg-1); box-shadow: var(--sh-2);
  padding: var(--s-6) var(--s-5) var(--s-5);
}

.cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); gap: var(--s-4); }

/* ---------- Disclosure ----------
   A card that opens. Used where a form is secondary to the data on the page - the
   register-a-customer form was 500px of inputs sitting above the customer list, so
   the thing an admin came to read started below the fold.

   <details> rather than a script: it keeps its own state, it is in the tab order,
   and a screen reader announces it as expanded or collapsed with nothing added. */
.disclose { padding: 0; }
/* A fold inside a card, rather than a card that folds. The class is on a wrapper there,
   so `.disclose > summary` never matched it and four of these rendered as bare browser
   disclosure triangles with 20px of text to aim at. */
.disclose > details > summary {
  display: flex; align-items: center; gap: var(--s-2);
  min-height: var(--tap-sm); cursor: pointer;
  color: var(--brand); font-size: var(--fs-4); font-weight: 500;
}

.disclose > summary {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-4) var(--s-5);
  min-height: var(--tap);
  cursor: pointer; list-style: none;
  font-size: var(--fs-2); font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em;
  border-radius: var(--r-3);
  transition: color var(--t-1), background var(--t-1);
}
.disclose > summary::-webkit-details-marker { display: none; }
.disclose > summary:hover { color: var(--text); background: var(--surface-2); }
.disclose > summary .marker { margin-inline-start: auto; transition: transform var(--t-1); }
.disclose[open] > summary { color: var(--text); border-radius: var(--r-3) var(--r-3) 0 0; }
.disclose[open] > summary .marker { transform: rotate(90deg); }
.disclose > *:not(summary) { margin-left: var(--s-5); margin-right: var(--s-5); }
.disclose > *:last-child { margin-bottom: var(--s-5); }

/* ---------- The lock screen ----------
   Its own shell so the sign-in, first-run and password pages sit centred in the
   viewport with the wordmark above them, exactly as signin.html does on a
   bench. Without it these pages hung off the top of a mostly empty screen. */
.lock {
  min-height: calc(100dvh - 250px);
  display: flex; flex-direction: column; justify-content: center;
  gap: var(--s-4);
}
.lock .brand { justify-content: center; }
.lock-foot {
  max-width: 440px; margin: 0 auto; text-align: center;
  color: var(--muted); font-size: var(--fs-2); line-height: 1.6;
}

/* ============================================================================
   Forms
   ========================================================================= */
label {
  display: block; font-size: var(--fs-2); font-weight: 600; color: var(--text-2);
  margin: var(--s-4) 0 var(--s-2); letter-spacing: .01em;
}
label:first-of-type { margin-top: 0; }

/* …and the form itself carries the gap instead. Without this the first label sits
   hard against whatever introduced the form - on the sign-in card, "Email address"
   was touching the sentence above it. */
.card form { margin-top: var(--s-3); }
.card form.form-grid { margin-top: var(--s-4); }

/* SIXTEEN PIXELS, and that number is not a taste. Below it iOS zooms the whole page in
   when a field takes focus and does not zoom back out, so every form on a tablet ended
   with the operator pinching the screen back to size. 48px tall for the same finger
   every button is sized for. */
input, select, textarea {
  width: 100%; padding: 11px 12px;
  min-height: var(--tap);
  background: var(--bg-0); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--r-2);
  font: inherit; font-size: var(--fs-5);
  transition: border-color var(--t-1);
}
/* A BOX THAT GROWS WITH WHAT IS TYPED INTO IT, rather than one that scrolls inside
   itself. A region that moves inside the one region that moves is a swipe trap: a finger
   that lands on it moves the box instead of the page and cannot tell which it has hold of.
   field-sizing is the browser doing this natively; where it is not understood the box is
   the height its rows ask for, exactly as before. */
textarea { line-height: 1.45; field-sizing: content; min-height: calc(3 * 1.45em + 24px); }
textarea:focus-visible { outline: none; box-shadow: var(--focus); }
input:hover, select:hover { border-color: var(--line-2); }
input::placeholder { color: var(--muted); }
input[type=checkbox], input[type=radio] {
  width: 22px; height: 22px; min-height: 0; padding: 0; flex: 0 0 auto;
  accent-color: var(--brand);
}
select {
  appearance: none;
  padding-inline-end: var(--s-6);
  /* A chevron drawn in the box itself, so a select does not read as a text
     field that mysteriously will not accept typing. currentColor cannot be used
     inside a data URI, so this is --muted written out; it is the one place in
     the file where a colour is not a token. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%237d8da6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-3) center;
}

/* The whole line is the target, not the 22px box at the start of it. */
.check {
  display: flex; align-items: center; gap: var(--s-3);
  min-height: var(--tap-sm); min-width: var(--tap-sm);
  margin: 0; font-weight: 400; font-size: var(--fs-4); color: var(--text-2);
  cursor: pointer;
}
.check input { margin: 0; }

.form-grid {
  display: grid; grid-template-columns: 170px minmax(0, 1fr);
  gap: var(--s-3) var(--s-4); align-items: center; margin-top: var(--s-4);
}
.form-grid label { margin: 0; }

.row { display: flex; gap: var(--s-2); align-items: center; margin-top: var(--s-4); flex-wrap: wrap; }
.inline { display: inline; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: var(--tap); min-width: var(--tap);
  border: 1px solid var(--line); background: var(--surface-2); color: var(--text);
  padding: 9px 18px; border-radius: var(--r-2);
  font: inherit; font-size: var(--fs-4); font-weight: 500;
  cursor: pointer; white-space: nowrap;
  transition: background var(--t-1), border-color var(--t-1), color var(--t-1);
}
.btn:hover { background: var(--surface-3); border-color: var(--line-2); text-decoration: none; }

/* A PRESSED STATE THAT CAN BE SEEN. It was a 1px nudge, which a mouse user notices in
   the corner of an eye and somebody whose finger is covering the button cannot. */
.btn:active { transform: translateY(1px); background: var(--surface-3); }
.btn[disabled] { opacity: .5; cursor: default; }

/* Sent, and waiting for the answer. js/shell.js sets this on the first tap of a form's
   button and refuses the second, because a double tap on glass is routine and on a
   payment form it is two payments. */
.btn.busy { opacity: .65; pointer-events: none; }
.btn.busy::after {
  content: ""; width: 14px; height: 14px; flex: 0 0 auto;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: spin .7s linear infinite;
}

.btn.primary {
  background: var(--brand); border-color: var(--brand);
  color: var(--on-brand); font-weight: 600;
}
.btn.primary:hover { background: var(--brand-strong); border-color: var(--brand-strong); }

.btn.ghost { background: transparent; color: var(--text-2); }
.btn.ghost:hover { background: var(--surface-2); color: var(--text); }

.btn.warn { background: transparent; border-color: var(--warn); color: var(--warn); }
.btn.warn:hover { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }

/* SMALLER IN TYPE, NOT IN REACH. This was 29px tall, and it was defined twice - the
   later copy, a thousand lines down, was the one that rendered. It is the button that
   sits in a table row, which is exactly where a miss lands on the row beside it. */
.btn.small { min-height: var(--tap-sm); min-width: var(--tap-sm); padding: 6px 12px; font-size: var(--fs-3); }
.btn.small .ic { width: 16px; height: 16px; }

/* ============================================================================
   Tables

   Wrapped in .scroll, always. A report table is nine columns and there is no
   width at which they all fit a phone; the page body must never be the thing
   that scrolls sideways.
   ========================================================================= */
.scroll { overflow-x: auto; margin: 0 calc(var(--s-1) * -1); padding: 0 var(--s-1); }

.grid { width: 100%; border-collapse: collapse; font-size: var(--fs-4); }
.grid th {
  text-align: start; font-size: var(--fs-1); color: var(--muted); font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  padding: var(--s-2) var(--s-3); border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.grid td {
  padding: var(--s-3); border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.grid tbody tr { transition: background var(--t-1); }
.grid tbody tr:hover { background: var(--surface-2); }
.grid.tight td { padding: var(--s-2) var(--s-3); }

/* THE LINK THAT OPENS A ROW was 22px of inline text - the job number, the device, the
   customer's name - and it is the single most-pressed thing on every list. It is a
   target now. Links inside a sentence are left alone: they are read, not aimed at.

   A TARGET, NOT A LINE OF ITS OWN. The 44px box reaches into the space around it by half the
   difference (the negative margin), so it lays out as one line of text: the job number sits
   on the same line as the customer beside it, and a name over "2 on the bench now" reads as
   two lines rather than two lines with a gap. It was a 44px block at the top of its cell, and
   every row was taller than its words with the link a few pixels lower than the text beside
   it. fitcheck measures the box, which is still 44px. */
.grid td > a:not(.btn):not(.chip),
.grid td > strong > a,
.tap {
  display: inline-flex; align-items: center;
  min-height: var(--tap-sm); min-width: var(--tap-sm);
  margin-block: calc((1lh - var(--tap-sm)) / 2);
}

/* .tap is the same thing anywhere else: a link that stands alone - a telephone number
   under a name, "download these as a spreadsheet" under a list - and so is pressed rather
   than read. CSS cannot tell that from a link inside a sentence (:only-child does not
   count the words around it), so the markup says which it is. */

/* A header that stays while its rows move under it. Only where the table is the thing
   that scrolls; inside a card that scrolls with the pane it would stick to nothing. */
.grid thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }

/* THE NOTE AN OPERATOR TYPED, which had a class and no rule at all - so it rendered at full
   body size with no width of its own, and one long note stretched the column until the
   verdict beside it was squeezed. It is secondary to the verdict, and it is free text, so it
   wraps rather than widens. */
.grid td.note {
  color: var(--text-2); font-size: var(--fs-3);
  max-width: 44ch; overflow-wrap: anywhere;
}
.grid tbody tr:last-child td { border-bottom: none; }

/* A table that IS the page needs the frame a card would have given it. */
.card > .scroll > .grid, .card > .grid { margin-top: var(--s-1); }
main > .scroll > .grid {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-3); overflow: hidden;
}
main > .scroll > .grid th { padding-inline-start: var(--s-4); }
main > .scroll > .grid th:last-child, main > .scroll > .grid td:last-child { padding-inline-end: var(--s-4); }
main > .scroll > .grid td { padding-inline-start: var(--s-4); }

.evidence td { padding-top: 0; }
.evidence pre {
  white-space: pre-wrap; word-break: break-word;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: var(--fs-2); line-height: 1.5;
  background: var(--bg-0); border: 1px solid var(--line);
  border-radius: var(--r-2); padding: var(--s-3);
  max-height: 320px; overflow: auto; margin: 0 0 var(--s-2);
}

/* ---------- Key/value ---------- */
.kv { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: var(--s-2) var(--s-4); margin: 0; }
.kv dt { color: var(--muted); font-size: var(--fs-3); }
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* The same list written as <div><span>label</span><b>value</b></div>, which the job page
   does. It had no rule at all, so each pair was one grid cell with the label run straight
   into the value - "NameBeta Customer", "Telephone07700 900222". The wrapper steps aside
   and the two halves take the two columns. */
.kv > div { display: contents; }
.kv > div > span { color: var(--muted); font-size: var(--fs-3); align-self: center; }
.kv > div > b { min-width: 0; overflow-wrap: anywhere; font-weight: 600; }

/* ============================================================================
   Status
   ========================================================================= */
.verdict {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: var(--r-pill);
  font-size: var(--fs-2); font-weight: 600; text-transform: capitalize;
  border: 1px solid var(--line); color: var(--text-2); background: var(--surface-2);
}
.verdict.pass, .verdict.active {
  color: var(--pass); border-color: transparent; background: var(--pass-soft);
}
.verdict.fail, .verdict.revoked, .verdict.disabled, .verdict.none {
  color: var(--fail); border-color: transparent; background: var(--fail-soft);
}
.verdict.incomplete, .verdict.suspended, .verdict.invited, .verdict.expired {
  color: var(--warn); border-color: transparent; background: var(--warn-soft);
}
.verdict.skip, .verdict.not-tested {
  color: var(--skip); border-color: transparent; background: var(--skip-soft);
}

/* A dot before the word. Colour alone is not a signal - about one man in twelve
   cannot separate the red from the green - and the shape gives the pill an
   anchor at a glance without reading it. */
.verdict::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: 0 0 auto;
}

.chips { display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: center; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--surface);
  font-size: var(--fs-3); color: var(--text-2);
  transition: background var(--t-1), border-color var(--t-1), color var(--t-1);
}
/* A chip that does something is a target; one that only says something is a label and
   stays the size of its words. */
a.chip, button.chip {
  min-height: var(--tap-sm); padding: var(--s-2) var(--s-4);
  font-size: var(--fs-4); cursor: pointer;
}
a.chip:hover { border-color: var(--line-2); color: var(--text); text-decoration: none; }
.chip.on {
  border-color: transparent; background: var(--brand-soft); color: var(--brand); font-weight: 600;
}
.chip b { font-weight: 700; opacity: .75; font-variant-numeric: tabular-nums; }
.chip.small { font-size: var(--fs-1); padding: 2px 8px; }

/* ---------- Banners ---------- */
.result {
  border: 1px solid var(--line); border-inline-start-width: 3px;
  border-radius: var(--r-2); padding: var(--s-3) var(--s-4);
  margin: 0 0 var(--s-4); font-size: var(--fs-4); color: var(--text-2);
}
.result > :first-child { margin-top: 0; }
.result > :last-child { margin-bottom: 0; }
.result.pass { border-inline-start-color: var(--pass); background: var(--pass-soft); }
.result.fail { border-inline-start-color: var(--fail); background: var(--fail-soft); }
.result.warn { border-inline-start-color: var(--warn); background: var(--warn-soft); }

.once {
  display: inline-block;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: var(--fs-5); letter-spacing: .06em;
  padding: var(--s-1) var(--s-2); color: var(--text);
  background: var(--surface-3); border-radius: var(--r-1);
  user-select: all;
}

/* ---------- Filters and paging ---------- */
.filters {
  display: flex; gap: var(--s-2); align-items: center;
  margin-bottom: var(--s-4); flex-wrap: wrap;
}
.filters .search { position: relative; flex: 0 1 340px; min-width: 220px; }
.filters .search input { padding-inline-start: 36px; }
.filters .search .ic {
  position: absolute; inset-inline-start: 11px; top: 50%; transform: translateY(-50%);
  color: var(--muted); pointer-events: none;
}
.paging {
  display: flex; gap: var(--s-2); margin-top: var(--s-4);
  align-items: center; justify-content: space-between;
}
.paging .count { color: var(--muted); font-size: var(--fs-3); }

/* ---------- Empty states ----------
   An icon, a sentence in the page's voice, and the reason. An empty table with
   the word "empty" under it tells somebody nothing about what to do next. */
.empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--s-2);
  text-align: center; color: var(--text-2);
  padding: var(--s-6) var(--s-5);
  background: var(--surface); border: 1px dashed var(--line-2);
  border-radius: var(--r-3);
  max-width: 620px; margin: 0 auto;
}
.empty .ic { width: 30px; height: 30px; color: var(--muted); margin-bottom: var(--s-1); }
.empty strong { color: var(--text); font-size: var(--fs-5); font-weight: 600; }
.empty p { margin: 0; font-size: var(--fs-4); line-height: 1.6; }

/* ---------- Photos ---------- */
.photos { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.photos a { display: block; border-radius: var(--r-2); overflow: hidden; line-height: 0; }
.photos img {
  width: 168px; height: 126px; object-fit: cover; display: block;
  background: var(--surface-2);
  transition: transform var(--t-1);
}
.photos a:hover img { transform: scale(1.03); }
.photo-missing {
  width: 168px; height: 126px; border: 1px dashed var(--line-2); border-radius: var(--r-2);
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  gap: var(--s-1); font-size: var(--fs-2); color: var(--muted);
  text-align: center; padding: var(--s-2);
}

/* The frozen printout keeps its white page - it is a document, not a panel, and
   it was authored for paper. */
/* THE FROZEN REPORT DOCUMENT, and only that. It is 620px of hard white because it holds a
   printed page inside an iframe - so it is scoped to the iframe rather than left as a bare
   class anybody can reach for. It was put on a <p> once, which turned one sentence of customer
   speech into a 620px white slab. For quoted text use .said below. */
iframe.frozen {
  width: 100%; height: 620px; border: 1px solid var(--line);
  border-radius: var(--r-2); background: #fff;
}

/* What the customer said, kept exactly as they said it. Sized by its own words, and pre-wrap
   because the line breaks somebody typed at the counter are part of what was reported. */
.said {
  margin: 0 0 var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-inline-start: 3px solid var(--line-2);
  border-start-start-radius: 0; border-end-start-radius: 0;
  border-start-end-radius: var(--r-2); border-end-end-radius: var(--r-2);
  background: var(--surface-2);
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.said:empty { display: none; }

/* The approved body of a WhatsApp template, shown so somebody can paste it into WhatsApp
   Manager character for character. pre-wrap because the line breaks ARE the template - a
   body reflowed on screen is one somebody retypes wrongly - and overflow-wrap because a
   placeholder run has no spaces to break at. No colour of its own: it inherits, so it needs
   no answer in the dark block. */
.tpl-body {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ---------- Utilities ---------- */
.muted { color: var(--muted); }
.small { font-size: var(--fs-2); }
.warn { color: var(--warn); }
.mono {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: .92em; letter-spacing: .01em;
}
.nowrap { white-space: nowrap; }
.num { font-variant-numeric: tabular-nums; }
code {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: .9em; background: var(--surface-2);
  padding: 1px 5px; border-radius: var(--r-1);
}

/* ============================================================================
   Narrower screens

   Three steps, shedding the least useful thing first: the brand words, then the
   tab labels, then the two-column layouts and the fixed-width form grid.
   ========================================================================= */
@media (max-width: 1040px) {
  .cols { grid-template-columns: minmax(0, 1fr); }
}

/* On a phone the panel is the screen. A 420px drawer on a 380px viewport is a drawer with
   a useless sliver of dimmed page beside it. */
@media (max-width: 520px) {
  .drawer-panel { width: 100%; border-inline-start: 0; }
}

@media (max-width: 860px) {
  body { --gutter: var(--s-4); }
  .topbar { gap: var(--s-3); padding: var(--s-2) var(--s-4); }
  .who { max-width: 170px; }
  main { padding: var(--s-4) var(--gutter) var(--s-5); }
}

/* The wordmark goes only where there is genuinely no room for it: the header holds the
   brand and the account menu and nothing else now, so it survives far narrower than it
   did when five tabs were competing with it. */
@media (max-width: 420px) {
  .brand-text { display: none; }
}

@media (max-width: 660px) {
  .form-grid { grid-template-columns: minmax(0, 1fr); gap: var(--s-2); }
  .form-grid label { margin-top: var(--s-3); }
  .kv { grid-template-columns: minmax(0, 1fr); gap: 0 0; }
  .kv dt { margin-top: var(--s-3); }
  h1 { font-size: var(--fs-6); }
  .card { padding: var(--s-4); }
}

/* The animations here are decoration - a hover tint, a 1px press, a photo
   nudge. Nothing depends on one, so honouring the preference costs nothing. */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
  .btn:active { transform: none; }
  .photos a:hover img { transform: none; }
}

/* PAPER IS NOT A SCREEN, so the shell lets go: a body pinned to the height of the glass
   with its overflow hidden prints exactly one screenful and cuts the rest off. */
@media print {
  .topbar, .tabs, .foot, .paging, .filters, .back, .btn, .pane-more, .toast { display: none !important; }
  html, body { height: auto; }
  body { display: block; overflow: visible; background: #fff; color: #000; }
  .stage, main, body.app main { display: block; overflow: visible; padding: 0; }
  .card { border-color: #ccc; break-inside: avoid; }
}

/* ============================================================================
   The public pages — landing, pricing, signup, checkout return

   Everything below is only ever seen by somebody who is not signed in yet, which
   makes it the one part of the site that has to sell as well as work. It reuses
   the same tokens as the rest: a marketing page in a different palette would say
   "different product" before anybody read a word.
   ========================================================================= */

/* ---------- The sign-in drawer ----------
   The way in, on the page rather than on a page of its own: a panel that slides in over
   the front page when somebody asks for it, and is already open when the site sent them
   here from a page they were not signed in for.

   IT OPENS WITHOUT JAVASCRIPT. The CSP is script-src 'self' and this site is built to work
   with scripting off, so the open state is :target — a link to #signin — plus a .open class
   the server writes when it already knows the answer. wwwroot/js/signin.js only adds
   Escape, the outside click and focus handling on top; nothing below depends on it.
   ========================================================================= */
.drawer {
  position: fixed; inset: 0; z-index: 40;
  visibility: hidden;
  /* Delayed so the panel finishes sliding OUT before the whole thing is taken away.
     Without it the drawer vanishes instantly and the transition is only ever seen once. */
  transition: visibility 0s var(--t-2);
}

.drawer-scrim {
  position: absolute; inset: 0;
  background: var(--scrim);
  opacity: 0; transition: opacity var(--t-2);
}

.drawer-panel {
  position: absolute; top: 0; inset-inline-end: 0; bottom: 0;
  width: min(420px, 100%);
  display: flex; flex-direction: column; gap: 0;
  padding: var(--s-5);
  overflow-y: auto;
  background: var(--bg-1);
  border-inline-start: 1px solid var(--line);
  box-shadow: var(--sh-2);
  transform: translateX(100%);
  transition: transform var(--t-2);
}

/* Open, from either source. Both selectors set the same three things, so the class and the
   fragment are interchangeable and the script can use one while a scriptless browser uses
   the other. */
.drawer.open,
.drawer:target { visibility: visible; transition: visibility 0s; }

.drawer.open .drawer-scrim,
.drawer:target .drawer-scrim { opacity: 1; }

.drawer.open .drawer-panel,
.drawer:target .drawer-panel { transform: none; }

.drawer-head { display: flex; align-items: flex-start; gap: var(--s-3); }
.drawer-head .signin-title { flex: 1 1 auto; }

.drawer-close {
  flex: 0 0 auto; margin-block: -8px 0; margin-inline: 0 -8px;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap); border-radius: var(--r-2);
  color: var(--muted); font-size: 24px; line-height: 1; text-decoration: none;
}
.drawer-close:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }

/* Every h2 in this sheet is a 12px uppercase grey section caption, so the heading on the
   drawer needs the same rescue .pricing-title needs — without it the door to the product
   renders as a footnote. */
.signin-title {
  font-size: var(--fs-6); text-transform: none; letter-spacing: normal;
  color: var(--text); font-weight: 650; margin-bottom: var(--s-2);
}

.signin-help { margin: var(--s-4) 0 0; }

/* The panel is not a .card, so it does not inherit the rule that gives a form its top
   margin inside one. */
.drawer-panel form { margin-top: var(--s-5); }

.hero { max-width: 760px; margin: 0 auto var(--s-6); text-align: center; }
.hero h1 {
  font-size: clamp(28px, 5vw, 40px); line-height: 1.15;
  letter-spacing: -.02em; margin-bottom: var(--s-3);
}
.hero .lead { font-size: var(--fs-6); line-height: 1.55; }
.hero .result { text-align: start; margin-top: var(--s-5); }

/* The one thing an existing customer came here to press. It is in the hero as well as the
   header because the header's copy is a 13px button in a corner, and on a phone the header
   sheds its labels first. */
.hero-cta { margin-top: var(--s-5); }


/* ---------- What it does ---------- */
.features {
  display: grid; gap: var(--s-4); margin-bottom: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.feature {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-3); padding: var(--s-5);
}
.feature .ic { color: var(--brand); margin-bottom: var(--s-2); }
.feature h3 { font-size: var(--fs-5); font-weight: 650; margin: 0 0 var(--s-2); }
.feature p { margin: 0; color: var(--text-2); font-size: var(--fs-4); line-height: 1.55; }

/* ---------- Pricing ---------- */
.pricing { margin-bottom: var(--s-6); scroll-margin-top: 80px; }
.pricing-title {
  font-size: var(--fs-7); text-transform: none; letter-spacing: -.01em;
  color: var(--text); font-weight: 650; margin-bottom: var(--s-1);
}

.plan-picker > label {
  display: block; text-align: center; margin: 0 0 var(--s-2);
  font-size: var(--fs-4); font-weight: 600; color: var(--text-2);
  text-transform: none; letter-spacing: 0;
}
.seat-row {
  display: flex; gap: var(--s-3); align-items: center; justify-content: center;
  flex-wrap: wrap; margin-bottom: var(--s-5);
}
.seat-row select { width: auto; min-width: 150px; }

.plans {
  display: grid; gap: var(--s-4);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  max-width: 760px; margin: 0 auto;
}
.plan {
  position: relative;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-3); padding: var(--s-5);
  display: flex; flex-direction: column; gap: var(--s-3);
}
/* The annual card is the one being recommended, and it says so with a border rather
   than a colour wash — a filled card reads as "selected", which it is not yet. */
.plan.best { border-color: var(--brand); box-shadow: var(--sh-1); }
.plan-badge {
  position: absolute; top: calc(var(--s-3) * -1); inset-inline-start: var(--s-5);
  background: var(--brand); color: var(--on-brand);
  font-size: var(--fs-1); font-weight: 700; letter-spacing: .04em;
  padding: 3px var(--s-3); border-radius: var(--r-pill);
}
.plan-name {
  font-size: var(--fs-2); font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted);
}
.plan-price { display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap; }
.plan-price .amount {
  font-size: 34px; font-weight: 700; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.plan-price .per { color: var(--muted); font-size: var(--fs-4); }
.plan-unit { color: var(--text-2); font-size: var(--fs-3); margin-top: calc(var(--s-2) * -1); }
.plan-points { margin: 0; padding-inline-start: var(--s-4); color: var(--text-2); font-size: var(--fs-4); }
.plan-points li { margin-bottom: var(--s-1); }
.plan .btn { margin-top: auto; justify-content: center; }
.vat-note { text-align: center; margin-top: var(--s-4); }

/* ---------- How it works ---------- */
.how { max-width: 640px; margin: 0 auto; }
.how h2 { text-align: center; }
.steps { color: var(--text-2); font-size: var(--fs-4); line-height: 1.7; padding-inline-start: var(--s-5); }
.steps strong { color: var(--text); }

/* ---------- The checkout return page ---------- */
.waiting { text-align: center; }
.waiting .spinner {
  width: 28px; height: 28px; margin: 0 auto var(--s-4);
  border: 3px solid var(--line-2); border-top-color: var(--brand);
  border-radius: 50%; animation: spin 900ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .waiting .spinner { animation: none; border-top-color: var(--line-2); }
}
.ref {
  display: inline-block; font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  background: var(--surface-2); border-radius: var(--r-1);
  padding: 2px var(--s-2); font-size: var(--fs-3); user-select: all;
}

@media (max-width: 660px) {
  .plans { grid-template-columns: minmax(0, 1fr); }
  .plan-badge { inset-inline-start: var(--s-4); }
  .hero .lead { font-size: var(--fs-5); }
}

/* ---------------------------------------------------------------- phone field
   The +44 is furniture, not an input: the field asks for the form people already know, and the
   server accepts either way round regardless. Sharing one border makes the two read as one
   control rather than a label that happens to sit nearby. */
.phone-field {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  background: var(--bg-0);
  overflow: hidden;
}
.phone-field:focus-within { border-color: var(--brand); box-shadow: var(--focus); }
.phone-cc {
  display: flex;
  align-items: center;
  padding: 0 var(--s-3);
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  background: var(--bg-1);
  border-right: 1px solid var(--line);
  white-space: nowrap;
}
.phone-field input {
  flex: 1;
  min-width: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}
.phone-field input:focus { box-shadow: none; }

.field-hint {
  margin: var(--s-2) 0 var(--s-4);
  font-size: var(--fs-3);
  color: var(--text-2);
}

/* ---------------------------------------------------------------- subscription management
   Three independent actions, so three boxes rather than one form: pressing Update on the seat
   count must not look like it might also switch the billing period. */
.manage-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s-4);
}
.manage-item {
  padding: var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  background: var(--bg-1);
}
.manage-item h3 { margin: 0 0 var(--s-2); font-size: var(--fs-5); }
.manage-item .row { margin-top: var(--s-3); align-items: center; }
.manage-item select { margin: 0; }

@media (max-width: 660px) {
  .manage-grid { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------- the payment form
   Ours, not Stripe's. The labels and headings above the mounts are this site's; only the two
   mounts are Stripe iframes, styled to match through the Appearance API in checkout.js — which
   reads these very tokens rather than repeating the palette, so the fields follow the site. */
.pay-form { margin-top: var(--s-5); }
.pay-form h2 { margin: 0 0 var(--s-4); font-size: var(--fs-5); }
.pay-block { margin-bottom: var(--s-4); }

/* Stripe measures its iframe against this, so it must not be display:none or zero-height while
   mounting — the element has to be laid out for the fields to size themselves correctly. */
.stripe-mount { min-height: 40px; }

/* Shown until Stripe says "ready", then removed. Not a spinner: the form arrives in pieces and a
   spinner would have to be guessed at when to remove, whereas a block the exact shape of what is
   coming makes the swap invisible. */
.mount-skeleton {
  height: 120px;
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  background: linear-gradient(90deg, var(--bg-0) 25%, var(--bg-1) 37%, var(--bg-0) 63%);
  background-size: 400% 100%;
  animation: mount-shimmer 1.4s ease infinite;
}
@keyframes mount-shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }

.pay-terms {
  margin: var(--s-4) 0;
  font-size: var(--fs-3);
  color: var(--text-2);
}

.pay-submit { width: 100%; justify-content: center; }

.spinner-inline {
  display: inline-block;
  width: 14px; height: 14px;
  margin-inline-start: var(--s-2);
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* The shimmer and the spinner are decoration; somebody who has asked not to see motion should
   still see that something is happening, not a still frame of an animation. */
@media (prefers-reduced-motion: reduce) {
  .mount-skeleton, .spinner-inline { animation: none; }
  .mount-skeleton { background: var(--bg-1); }
}

/* An element with `display` set in CSS ignores the `hidden` attribute — `hidden` is only a
   UA-stylesheet `display: none`, and any author rule beats it. The Subscribe button shipped with
   a permanently spinning spinner because of exactly this. Scoped rather than a blanket
   `[hidden] { display: none !important }` so it fixes the two elements that need it and does not
   quietly change every other rule in the file. */
.spinner-inline[hidden],
.result[hidden] { display: none; }

/* `.back` was written to sit above an <h1> in a `.head` block; it is now also used at the top of
   a `.card`, where the heading's own top margin would otherwise push it away from the edge. */
.card > .back:first-child { margin-top: 0; }
.card > .back:first-child + h1 { margin-top: var(--s-2); }

/* ---------------------------------------------------------------- summary strip
   Five numbers above a long table, answering "is anything wrong" before anybody reads a row.
   `bad` and `warn` colour only the count, not the whole tile: a wall of red reads as an outage
   when what it means is "one event needs looking at". */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--s-2);
  margin: var(--s-4) 0;
}
.stat {
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  background: var(--surface);
}
.stat.wide { grid-column: span 2; }
.stat-n {
  display: block;
  font-size: var(--fs-7);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}
.stat-l {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-3);
  color: var(--text-2);
}
.stat.bad  .stat-n { color: var(--fail); }
.stat.warn .stat-n { color: var(--warn); }

@media (max-width: 660px) {
  .stat-strip { grid-template-columns: repeat(2, 1fr); }
  .stat.wide  { grid-column: span 2; }
}

/* ---------------------------------------------------------------- legal pages
   Long-form prose, which the rest of the site has none of. Narrower measure and more air between
   headings than a dashboard card wants — these are read, not scanned. */
/* Wider than `.narrow`, which is sized for a sign-in form and squeezes the data table in the
   privacy notice into three unreadable columns; narrower than full width, because a 1400px line
   of prose is not read, it is skimmed and abandoned. */
.legal { max-width: 62rem; margin-inline: auto; }
.legal h2 { margin: var(--s-5) 0 var(--s-2); font-size: var(--fs-5); }
.legal p, .legal li { line-height: 1.65; }
.legal table { margin: var(--s-3) 0; }
.legal hr { margin: var(--s-5) 0 var(--s-3); border: 0; border-top: 1px solid var(--line); }
.legal-address { font-style: normal; line-height: 1.6; }

.foot-legal { margin-top: var(--s-2); }
.foot-legal a { color: var(--text-2); }
.foot-legal a:hover { color: var(--brand); }

/* ---------------------------------------------------------------------------
   Repair jobs
   --------------------------------------------------------------------------- */

/* A message at the top of a page: something needs attention, or something worked.
   Both tokens are defined in BOTH palettes above — a colour that exists only in the dark block
   inherits a value chosen for the opposite background, which is how the old light theme ended up
   with a pass green at about 1.9:1 on white. */
.warn-box,
.pass-box {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  border-inline-start: 3px solid var(--warn);
  background: var(--warn-soft);
}

.pass-box { border-inline-start-color: var(--pass); background: var(--pass-soft); }

/* A PARAGRAPH IS NOT A ROW. These two are flex so an icon can stand beside a block of
   text - and put on a <p> whose content is one running sentence with a link in it, flex
   turns the words either side of the link into three columns. */
p.warn-box, p.pass-box { display: block; padding: var(--s-3) var(--s-4); border-radius: var(--r-2); }

.warn-box .ic { color: var(--warn); flex: 0 0 auto; }
.pass-box .ic { color: var(--pass); flex: 0 0 auto; }

.warn-box p,
.pass-box p { margin: var(--s-1) 0 0; }

/* A row of controls that stays on one line: a form and a button side by side, or the several
   little status forms on a job. Each is its own <form> because each posts to its own handler. */
.inline {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

/* The status buttons on a job sit in a .chips row, and a form inside it must not break the flow. */
.chips form.inline { margin: 0; }

/* A tick box in a row of controls keeps its words on one line; wrapped, "manages it" ran
   underneath the button beside it. */
.inline .check { white-space: nowrap; }

/* Struck-through money on a voided payment. It stays visible on purpose — a payment is voided,
   never deleted, and the record of cash that moved has to remain readable. */
.grid s { color: var(--muted); }

/* The connect QR on the job page. `image-rendering: pixelated` is the load-bearing half: QRCoder
   draws a small PNG, and a browser smooths it when scaled — a smoothed QR is one a phone camera
   hunts for. The printed receipt carries the same rule in JobDocument's own stylesheet. */
/* The QR a customer scans, beside the link and the button that mints a new one. IT MUST WRAP:
   the code is 128px fixed, and on a 380px screen that left under 190px for a t.me link, a hint
   and a button crushed into a column beside it. The text block drops below the square instead,
   the same way .get-software just below here already does it. */
.connect-code { display: flex; gap: var(--s-4); align-items: flex-start; flex-wrap: wrap; }
.connect-code > div { flex: 1 1 220px; }
.connect-code .qr { width: 128px; height: 128px; flex: 0 0 auto; image-rendering: pixelated;
                    border-radius: var(--r-1); background: #fff; padding: var(--s-2); }
.connect-code > div { min-width: 0; }
.connect-code p { margin: 0 0 var(--s-2); word-break: break-all; }

/* The device picker on the intake form. `.grow` is what stops the select collapsing to its widest
   option's label, which for "Dell Latitude 7490 — ABC123 · Priya Raman" is most of the page. */
.pick-device .row { align-items: flex-end; gap: var(--s-3); }
.pick-device .grow { flex: 1 1 auto; min-width: 0; }
.pick-device select { width: 100%; }

/* "Set up a bench" on the plan page: prose on the left, the action on the right, collapsing to
   stacked on a narrow screen rather than squeezing the button to nothing. */
.get-software { display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap; }
.get-software > div { flex: 1 1 320px; min-width: 0; }
.get-software h2 { margin-top: 0; }

/* The numbered install steps on the download page. */
.steps { margin: 0 0 var(--s-3); padding-inline-start: var(--s-5); }
.steps li { margin-bottom: var(--s-2); }

/* A money box that lives in a table cell, on the price list. Narrow, right-aligned and the same
   height as the row's text, so a table of prices still reads as a table rather than as a form. */
.cell-money {
  width: 9ch; text-align: end; padding: 6px 8px;
  min-height: var(--tap-sm);
  font-size: var(--fs-5); font-family: inherit;
  border: 1px solid var(--line); border-radius: var(--r-1);
  background: var(--surface); color: var(--text);
}
.cell-money:focus-visible { outline: none; box-shadow: var(--focus); border-color: var(--brand); }

/* ============================================================================
   Modals

   A SCREEN SHOWS; A MODAL DOES. Every create, edit, confirm and document on
   this site opens in one of these rather than in a form halfway down a page -
   the page cannot scroll to it any more, and a form nobody can reach is a
   feature nobody has.

   It is the browser's own <dialog>, because showModal() supplies the four
   things the sign-in drawer had to go without: focus kept inside it, Escape,
   a page behind that cannot be pressed, and a place above everything else.
   Pages/Shared/ModalTagHelper.cs writes the markup; js/modal.js opens it.

   IT OPENS WITH SCRIPTING OFF, the same way the drawer does. A trigger is a
   link to the dialog's id, so :target shows it; and .is-open is the server
   saying it already knows the answer - a form that has just been refused comes
   back open, with the reason inside it, instead of as a sentence at the top of
   a page the operator was not looking at the top of.

   FOUR SHAPES, and the difference between them is only how much room they take:

     (none)   a form. 560px, anchored near the TOP of the screen rather than
              centred - the on-screen keyboard covers the bottom of the glass,
              and a centred box puts its buttons exactly where the keys are.
     .sheet   a long form or a list. The whole screen on anything narrow.
     .confirm one sentence and two buttons.
     .viewer  a document, as large as the glass allows.
   ========================================================================= */
dialog.modal {
  position: fixed; inset: 0; z-index: 50;
  margin: min(8dvh, 64px) auto auto;
  width: min(560px, calc(100vw - 2 * var(--s-4)));
  max-width: none;
  max-height: calc(100dvh - min(8dvh, 64px) - var(--s-4) - var(--kb));
  padding: 0; overflow: hidden;
  background: var(--bg-1); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--r-3);
  box-shadow: var(--sh-2);
}
dialog.modal[open],
dialog.modal:target,
dialog.modal.is-open { display: flex; flex-direction: column; }

dialog.modal::backdrop { background: var(--scrim); }

/* With scripting off there is no top layer and so no backdrop: the dimming is the
   dialog's own shadow, thrown far enough to cover the glass. */
dialog.modal:target:not([open]),
dialog.modal.is-open:not([open]) { box-shadow: 0 0 0 100vmax var(--scrim); }

dialog.modal.confirm { width: min(440px, calc(100vw - 2 * var(--s-4))); }

dialog.modal.sheet { width: min(720px, calc(100vw - 2 * var(--s-4))); }

dialog.modal.viewer {
  margin: var(--s-4) auto auto;
  width: min(980px, calc(100vw - 2 * var(--s-4)));
  height: calc(100dvh - 2 * var(--s-4) - var(--kb));
  max-height: none;
}

/* On a narrow screen a sheet and a viewer ARE the screen. A 720px panel on a 380px
   phone is a panel with a useless sliver of dimmed page round it. */
@media (max-width: 700px) {
  dialog.modal.sheet,
  dialog.modal.viewer {
    margin: 0; width: 100vw; border: 0; border-radius: 0;
    height: calc(100dvh - var(--kb)); max-height: none;
  }
}

.modal-head {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--s-3);
  padding-block: var(--s-2); padding-inline: var(--s-5) var(--s-2);
  border-bottom: 1px solid var(--line);
}
/* Every h2 in this sheet is a 12px grey caption; the name of a dialog is not one. */
.modal-title {
  flex: 1 1 auto; min-width: 0; margin: 0;
  font-size: var(--fs-6); font-weight: 650; color: var(--text);
  text-transform: none; letter-spacing: normal;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.modal-close {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap); border-radius: var(--r-2);
  color: var(--muted);
}
.modal-close:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.modal-close .ic { width: 22px; height: 22px; }

/* THE ONE THING IN A MODAL THAT MOVES. The heading stays above it and the buttons
   stay below it, so however long a form is the way out of it is always on the glass. */
.modal-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--s-5) var(--s-5) 0;
}
.modal-body:not(:has(.modal-foot)) { padding-bottom: var(--s-5); }
.modal-body > :first-child { margin-top: 0; }
.modal-body form { margin: 0; }
.modal-body .lead { font-size: var(--fs-4); margin-bottom: var(--s-4); }

/* Pinned to the bottom of the dialog whatever the form above it does, and the dialog
   already stops short of the keyboard - so the button that finishes the job is never
   underneath the keys that were used to fill it in. */
.modal-foot {
  position: sticky; bottom: 0; z-index: 1;
  display: flex; gap: var(--s-2); justify-content: flex-end; flex-wrap: wrap;
  margin: var(--s-5) calc(var(--s-5) * -1) 0;
  padding: var(--s-3) var(--s-5);
  background: var(--bg-1); border-top: 1px solid var(--line);
}
.modal-foot .btn { flex: 0 1 auto; }
.modal-foot .btn.primary { flex: 1 1 140px; max-width: 320px; }

/* Why it was refused, where the form that was refused is. */
.modal-error {
  display: flex; align-items: flex-start; gap: var(--s-3);
  margin: 0 0 var(--s-4); padding: var(--s-3) var(--s-4);
  border-inline-start: 3px solid var(--fail); border-radius: var(--r-2);
  background: var(--fail-soft); color: var(--text); font-size: var(--fs-4);
}
.modal-error .ic { color: var(--fail); flex: 0 0 auto; margin-top: 2px; }

/* A list of places to go, or things to do: the More sheet, and a screen's own menu. */
.sheet-list { display: flex; flex-direction: column; gap: var(--s-1); margin: 0; padding: 0; list-style: none; }
.sheet-item {
  display: flex; align-items: center; gap: var(--s-4); width: 100%;
  min-height: 56px; padding: var(--s-2) var(--s-4); border-radius: var(--r-2);
  background: none; border: 0; cursor: pointer; text-align: start;
  color: var(--text); font: inherit; font-size: var(--fs-5);
}
.sheet-item:hover { background: var(--surface-2); text-decoration: none; }
.sheet-item .ic { width: 22px; height: 22px; color: var(--muted); }
.sheet-item.on { background: var(--brand-soft); color: var(--brand); }
.sheet-item.on .ic { color: var(--brand); }

/* ---------- A document, on the screen it was asked for on ----------
   The receipt, the quote, the invoice and the frozen test report used to open in a new
   tab. A kiosk has no tabs and no back button, so each of them was a dead end - and
   none of them had a Print button, because the menu of a browser used to be enough.

   The frame is hard white in both themes for the reason iframe.frozen is: it holds a
   page that was authored for paper. */
.doc-frame {
  flex: 1 1 auto; min-height: 0; width: 100%;
  border: 0; background: #fff;
}
dialog.modal.viewer .modal-body { padding: 0; display: flex; flex-direction: column; overflow: hidden; }
dialog.modal.viewer .modal-foot { position: static; margin: 0; }
.doc-wait { padding: var(--s-5); color: var(--text-2); }
.doc-wait[hidden] { display: none; }

/* ---------- Toasts ----------
   "Saved." in a banner at the top of a page scrolls away with the page, and on a screen
   that does not scroll it takes a row of the glass for as long as the page is open. This
   floats over the head of the screen and then gets out of the way. AT THE TOP, not the
   bottom: along the bottom is where the next thing to press is, and a confirmation drawn
   over the button it confirms is a confirmation in the way. The words are the same words,
   in the HTML of the page, so anything that reads a page for its confirmation still finds
   them. */
.toast {
  position: fixed; left: 50%; z-index: 60;
  top: calc(60px + var(--s-3));
  transform: translateX(-50%);
  display: flex; align-items: center; gap: var(--s-3);
  max-width: min(560px, calc(100vw - 2 * var(--s-4)));
  padding: var(--s-3) var(--s-4);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line-2); border-inline-start: 3px solid var(--pass);
  border-radius: var(--r-2); box-shadow: var(--sh-2);
  font-size: var(--fs-4);
}
.toast .ic { color: var(--pass); flex: 0 0 auto; }
.toast.gone { display: none; }
.toast-x {
  flex: 0 0 auto; margin-block: calc(var(--s-2) * -1); margin-inline: 0 calc(var(--s-3) * -1);
  width: var(--tap-sm); height: var(--tap-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; border-radius: var(--r-2);
  color: var(--muted); cursor: pointer;
}

/* ============================================================================
   Screens

   A page that has been redesigned for the glass, as opposed to one that merely
   fits it. The shell gives every page one pane and lets it swipe; a SCREEN
   takes that pane over and divides it into the three things an application
   has and a document does not:

     .screen-head   what this is, and where in it you are. Does not move.
     .screen-body   the one region that does.
     .screen-bar    the next thing to press, along the bottom, within reach.

   A page opts in by setting ViewData["Pane"] = "own", which puts .own-pane on
   <main>. Until a page does, it is a document in a pane that scrolls - which
   fits the glass, and is what every page was the day the shell shipped.
   ========================================================================= */
body.app main.own-pane,
.stage > main.own-pane {
  overflow: hidden; padding: 0;
  display: flex; flex-direction: column; min-height: 0;
}
.stage:has(> main.own-pane) { overflow: hidden; }
.stage > main.own-pane { flex: 1 1 auto; }

.screen,
.screen-fill { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; margin: 0; }

/* ONE GAP BETWEEN WHATEVER THE HEAD HOLDS - the name, the totals, the chips, the search, the
   tabs. Each of those used to bring a margin of its own, so the gap between two of them was
   0, 8, 12 or 24px depending on which rule happened to match: the Devices page's branch chips
   sat on its tiles with nothing between them. "One rhythm" at the end of the file takes the
   children's own margins away. */
.screen-head {
  flex: 0 0 auto; padding: var(--s-4) var(--gutter) 0;
  display: flex; flex-direction: column; gap: var(--s-3);
}

.screen-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--s-4) var(--gutter) var(--s-5);
}

.screen-bar {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  padding: var(--s-3) var(--gutter);
  background: var(--bg-1); border-top: 1px solid var(--line);
}
.screen-bar form { margin: 0; }
.screen-bar .btn.primary { min-width: 168px; }
.bar-note { flex: 1 1 200px; min-width: 0; margin: 0; color: var(--text-2); font-size: var(--fs-3); }

/* A VERY SHORT SCREEN - a phone on its side - has no room for a head, a bar and a list
   between them. There the whole screen moves as one, which is the lesser evil: a list
   three rows tall between two fixed strips is not a list anybody can read. */
@media (max-height: 520px) {
  body.app main.own-pane { overflow-y: auto; display: block; }
  .screen, .screen-fill { display: block; }
  .screen-body { overflow: visible; }
}

/* WHILE SOMEBODY IS TYPING, the head gives up everything that is not its name. The
   keyboard has taken the bottom of the glass, and a row of totals nobody is reading
   should not take the top of it as well. */
body.kb-up .hide-kb { display: none; }

/* ---------- The top of a workspace: back, name, state ---------- */
.work-top { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.work-back {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap); border-radius: var(--r-2);
  background: var(--surface); border: 1px solid var(--line); color: var(--text-2);
}
.work-back:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.work-back .ic { width: 20px; height: 20px; }
.work-name { flex: 1 1 220px; min-width: 0; }
.work-name h1 { font-size: var(--fs-6); margin: 0; }
.work-name .lead {
  margin: 0; font-size: var(--fs-4);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------- Tabs ----------
   The parts of one thing. A job was eleven cards down a page; it is six of these, and the
   panel under the chosen one is the only thing that moves.

   WITH SCRIPTING OFF they are links to the ids of the panels, so :target shows the right one
   and the first shows when the address names none. js/screen.js then takes over (.tabbed) so
   a tap does not put six entries in the history. */
.work-tabs {
  display: flex; gap: var(--s-1); margin-top: var(--s-3);
  overflow-x: auto; border-bottom: 1px solid var(--line);
}
.work-tab {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: var(--tap); padding: 0 var(--s-4);
  color: var(--text-2); font-size: var(--fs-4); font-weight: 500; white-space: nowrap;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.work-tab:hover { color: var(--text); text-decoration: none; }
.work-tab.on { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }
.work-tab .count {
  min-width: 20px; padding: 0 6px; border-radius: var(--r-pill);
  background: var(--surface-3); color: var(--text-2);
  font-size: var(--fs-1); font-weight: 700; line-height: 20px; text-align: center;
  font-variant-numeric: tabular-nums;
}
.work-tab.on .count { background: var(--brand-soft); color: var(--brand); }

.work-panel { display: none; }
.work-panel:target,
.work-panel.on { display: block; }
.screen-body:not(.tabbed):not(:has(> .work-panel:target)) > .work-panel:first-of-type { display: block; }
.screen-body.tabbed > .work-panel:target:not(.on) { display: none; }
.work-panel > :last-child { margin-bottom: 0; }

/* A heading with the one thing you do to its section beside it. */
.panel-head { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; margin-bottom: var(--s-3); }
.panel-head h2 { margin: 0; flex: 1 1 auto; }

/* ---------- Steps ----------
   One on the glass at a time, because a keyboard leaves room for about six fields. With
   scripting off .stepped is never set and the steps are simply one under another. */
.step { min-width: 0; }
.step + .step { margin-top: var(--s-5); }
.step.card + .step.card { margin-top: 0; }
.stepped .step { display: none; margin-top: 0; }
.stepped .step.on { display: block; }
.stepped .step > h2:first-child { display: none; }   /* the dots say which step this is */

.step-dots { display: none; gap: var(--s-2); flex-wrap: wrap; margin: 0 0 var(--s-4); }
.stepped .step-dots { display: flex; }
.step-dot {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: var(--tap-sm); padding-block: 0; padding-inline: var(--s-1) var(--s-3);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-pill);
  color: var(--text-2); font: inherit; font-size: var(--fs-3); cursor: pointer;
}
.step-dot b {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--surface-3); color: var(--text-2); font-size: var(--fs-3);
}
.step-dot.on { border-color: var(--brand); color: var(--brand); font-weight: 600; }
.step-dot.on b { background: var(--brand); color: var(--on-brand); }
.step-dot.done b { background: var(--pass-soft); color: var(--pass); }

/* Back and Next exist only once the script has made the form a stepped one. */
[data-step-back], [data-step-next] { display: none; }
.stepped [data-step-back], .stepped [data-step-next] { display: inline-flex; }

/* The trap this file already documents, met again: a rule that sets `display` beats the
   hidden attribute, so a Next button hidden on the last step stayed exactly where it was. */
.btn[hidden], .stepped [data-step-back][hidden], .stepped [data-step-next][hidden] { display: none; }

/* ---------- Fields, stacked ----------
   A label ABOVE its box, in one column. The two-column .form-grid wants 170px for the label
   before the box starts, which on a 380px dialog leaves a box too narrow to read a serial
   number in. */
.fields { display: grid; gap: var(--s-4); }
.fields > label, .fields > .field { display: block; margin: 0; min-width: 0; }
.fields > label > span:first-child,
.field-name {
  display: block; margin-bottom: var(--s-1);
  font-size: var(--fs-3); font-weight: 600; color: var(--text-2);
}
.fields .field-hint { margin: var(--s-1) 0 0; font-weight: 400; }
.fields .pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3); }
.fields .pair > label { margin: 0; }
.fields .pair > label > span:first-child {
  display: block; margin-bottom: var(--s-1);
  font-size: var(--fs-3); font-weight: 600; color: var(--text-2);
}
.fields > label.check { display: flex; }
.fields > label.check > span:first-child { display: inline; margin: 0; font-weight: 400; font-size: var(--fs-4); }
@media (max-width: 420px) { .fields .pair { grid-template-columns: minmax(0, 1fr); } }

/* On a wide screen a stepped form has the room for two columns of fields. */
@media (min-width: 900px) {
  .screen-body .fields.wide { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .screen-body .fields.wide > .span { grid-column: 1 / -1; }
}

/* ---------- A choice of a few: pressed, not opened ----------
   A <select> with four options is two taps and a list that covers the form. These are the
   same radio inputs under the same name, laid out as the buttons they behave like - so the
   handler reads exactly what it read before. */
.seg { display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); gap: var(--s-2); }
/* Six choices are two rows of three, not five and a straggler. */
.seg.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.seg > label { position: relative; display: flex; margin: 0; min-width: 0; cursor: pointer; }
.seg > label > input {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer;
}
.seg > label > span {
  flex: 1 1 auto;
  display: flex; align-items: center; justify-content: center; text-align: center;
  min-height: var(--tap); padding: var(--s-1) var(--s-2);
  background: var(--bg-0); border: 1px solid var(--line); border-radius: var(--r-2);
  color: var(--text-2); font-size: var(--fs-4); font-weight: 500;
}
.seg > label > input:checked + span {
  background: var(--brand-soft); border-color: var(--brand); color: var(--brand); font-weight: 600;
}
.seg > label > input:focus-visible + span { box-shadow: var(--focus); }

/* A number, with a button either side of it. */
.bump { display: grid; grid-template-columns: var(--tap) minmax(0, 1fr) var(--tap); gap: var(--s-2); }
.bump input { text-align: center; }
.bump .btn { padding: 0; font-size: var(--fs-6); }

/* A row of one-tap answers under a field: "Today", "Exact balance". */
.quick { display: flex; gap: var(--s-2); flex-wrap: wrap; margin-top: var(--s-2); }
.quick .btn { min-height: var(--tap-sm); padding: 6px 12px; font-size: var(--fs-3); }

/* What a confirm is about, said large enough to be read before the button is pressed. */
.confirm-what { font-size: var(--fs-5); color: var(--text); margin: 0 0 var(--s-3); }
.confirm-what b { font-variant-numeric: tabular-nums; }

/* ---------- Lists ----------
   One row of tiles and one row of chips, each of which swipes sideways where the glass is
   narrow. Wrapped, the seven tiles and two rows of chips on the jobs list were most of a
   tablet before the first job. */
.tiles.strip, .chips.strip { flex-wrap: nowrap; overflow-x: auto; }
.tiles.strip { margin-bottom: var(--s-3); }
.tiles.strip .tile { flex: 1 0 auto; min-width: 124px; padding: var(--s-2) var(--s-4); }
.chips.strip { flex: 1 1 100%; min-width: 0; }
.chips.strip .chip { flex: 0 0 auto; white-space: nowrap; }
.chip-gap { flex: 0 0 var(--s-3); }
.screen-head .filters { margin-bottom: var(--s-3); }
.screen-head .filters .search { flex: 1 1 260px; }

/* A card in the head is a strip, not a panel: the head does not move, so every row it
   takes is a row the list under it does not get. */
.screen-head .card { padding: var(--s-3) var(--s-4); margin: var(--s-3) 0 0; }
.screen-head .card form, .screen-head form.card { margin-top: var(--s-3); }
.screen-head .pick-device .row { margin-top: 0; flex-wrap: nowrap; }
.screen-head .pick-device .grow { flex: 1 1 200px; margin: 0; }

/* The thing that opens a row does not break in the middle of its own name. */
.grid .row-link, .grid .row-link .mono { white-space: nowrap; }

/* A ROW IS THE TARGET. js/screen.js follows the row's own link when anywhere on it is
   pressed; with scripting off the link is still a link. Anything in the row that does
   something else - a telephone number - is marked .over and keeps its own press. */
.grid.rows tbody tr { cursor: pointer; }
.grid.rows tbody tr:active { background: var(--surface-3); }
main > .scroll > .grid, .screen-body > .scroll > .grid {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-3); overflow: hidden;
}
.screen-body > .scroll > .grid th, .screen-body > .scroll > .grid td { padding-inline-start: var(--s-4); }
.screen-body > .scroll > .grid th:last-child, .screen-body > .scroll > .grid td:last-child { padding-inline-end: var(--s-4); }

/* ON A PHONE A ROW STANDS UP AS A CARD. Eight columns do not fit 380px, and a table that
   swipes sideways inside a list that swipes down is two directions for one finger. Each
   cell says what it is from its own data-th, which the header above can no longer do. */
@media (max-width: 700px) {
  /* Pinned to the corner: left where it fell, a header hidden this way under the Home buttons sat
     below the fold, and with no positioned ancestor it stretched the DOCUMENT - the page scrolled. */
  .grid.stack thead { position: absolute; top: 0; inset-inline-start: 0; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .grid.stack, .grid.stack tbody, .grid.stack tr { display: block; width: 100%; }
  .grid.stack tr { padding: var(--s-2) 0; border-bottom: 1px solid var(--line); }
  .grid.stack tbody tr:last-child { border-bottom: 0; }
  .grid.stack td {
    display: grid; grid-template-columns: 96px minmax(0, 1fr); column-gap: var(--s-3);
    border: 0; padding: 2px var(--s-3); text-align: start;
  }
  .grid.stack td::before {
    content: attr(data-th); grid-column: 1; grid-row: 1 / span 9;
    color: var(--muted); font-size: var(--fs-1); font-weight: 700;
    text-transform: uppercase; letter-spacing: .06em; padding-top: 4px;
  }
  .grid.stack td > * { grid-column: 2; }
}

/* A PANEL THAT FILLS THE PANE: the frozen report. It was a fixed 620px whatever the glass,
   which on a tablet was a frame taller than the screen it sat in. The panel takes the height
   of the pane and the frame takes what is left of it under the button. */
.work-panel.fill:target, .work-panel.fill.on { display: flex; flex-direction: column; min-height: 100%; }
.work-panel.fill iframe.frozen { flex: 1 1 auto; height: auto; min-height: 360px; }

/* One strip made of several groups of chips: each group keeps its own wrapper, because the
   harness reads one of them by an attribute on it, and none of them wraps. */
.chips.strip > .chips { flex: 0 0 auto; flex-wrap: nowrap; }

/* A photograph, as large as the glass allows, on the screen it was pressed on. */
dialog.modal.viewer .photo-full {
  flex: 1 1 auto; min-height: 0; width: 100%; height: 100%;
  object-fit: contain; background: var(--bg-0);
}

/* The thing in a modal's foot that is NOT the way forward: on the far side from it. */
.modal-foot .aside { margin-inline-end: auto; }


/* ---------- Running the shop: people, branches, the plan ----------

   A THING YOU CAN DO TO THE SUBJECT OF A DIALOG, with what it means beside it. These were
   29px buttons in the last cell of a table row - Resend, Revoke, Make admin, Disable - with
   nothing to say what any of them would do. Each is a row now: the consequence on the left,
   the press on the right. */
.acts { display: grid; gap: var(--s-2); margin: var(--s-5) 0 var(--s-3); }
.act {
  display: flex; align-items: center; gap: var(--s-3) var(--s-4); flex-wrap: wrap;
  padding: var(--s-3) var(--s-4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-2);
}
.act > div { flex: 1 1 220px; min-width: 0; }
.act > div > b { display: block; color: var(--text); }
.act > div > p { margin: 2px 0 0; color: var(--text-2); font-size: var(--fs-3); line-height: 1.5; }
.act > form, .act > .btn { flex: 0 0 auto; margin: 0; }
.act .btn { min-width: 132px; }

/* A form that is one part of a dialog rather than the whole of it. */
.modal-body .act-form { margin-top: var(--s-5); }
.modal-body .kv + .act-form { padding-top: var(--s-4); border-top: 1px solid var(--line); }

/* THE WAY FORWARD IS LAST ON THE GLASS AND FIRST IN THE FORM. Enter in a field presses the
   first submit button in the document, so a foot with two of them - send an invite, or create
   with a password - has to put the one Enter should mean first in the markup, and this puts
   it back on the right where every other dialog has it. */
.modal-foot .end { order: 9; }

/* A BRANCH'S CARD says words where a stat strip usually says numbers - the name on its
   paperwork, the address customers collect from - and set at the size a count is set at, four
   of them were most of a tablet before the second branch. The name of the branch, meanwhile,
   was a 12px grey caption over them: the one thing on the card somebody is looking for. */
.stat-strip.words { margin: var(--s-3) 0; }
.stat-strip.words .stat-n { font-size: var(--fs-5); line-height: 1.35; font-weight: 600; }
.stat-strip.words .stat-l { margin: 0 0 2px; }
.card > .head > h2.card-name {
  font-size: var(--fs-6); font-weight: 650; color: var(--text);
  text-transform: none; letter-spacing: normal; margin: 0;
}

/* THE NEXT THING TO PRESS, PINNED UNDER A LIST that is still a document in the shell's own pane
   rather than a screen of its own: tick the rows, then choose and press, without the button
   having scrolled away with the first of them. */
.sticky-bar {
  position: sticky; bottom: 0; z-index: 2;
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  margin: var(--s-4) 0 0; padding: var(--s-3) 0;
  background: var(--bg-0); border-top: 1px solid var(--line);
}
.sticky-bar select { flex: 1 1 220px; min-width: 0; }

/* ---------- Sales against a target ----------
   Three cards - today, this week, this month - each a figure, a bar towards the branch's target
   and the split by heading. Colours are the palette's own: brand while it is short, pass once it
   is reached. No new token, so nothing here needs a second answer in the dark block. */
.sales-cards {
  display: grid; gap: var(--s-3);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  margin-bottom: var(--s-4);
}
.card.sales-card { margin: 0; } /* a grid cell: the grid's gap is its spacing */
.sales-card { display: flex; flex-direction: column; gap: var(--s-2); }
.sales-when { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); }
.sales-when h2 { margin: 0; }
.sales-figure { font-size: var(--fs-7); font-weight: 650; line-height: 1.1; }
.goal {
  display: block; height: 10px; border-radius: 999px; overflow: hidden;
  background: var(--line);
}
.goal > span { display: block; height: 100%; border-radius: inherit; background: var(--brand); }
.goal.met > span { background: var(--pass); }
.goal.small { display: inline-block; width: 72px; height: 6px; vertical-align: middle; }
.goal-words { margin: 0; font-size: var(--fs-4); }
.sales-branches { list-style: none; margin: var(--s-1) 0 0; padding: 0; display: grid; gap: var(--s-1); }
.sales-branches li { display: flex; align-items: center; gap: var(--s-2); font-size: var(--fs-4); }
.sales-branch-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sales-split {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2);
  margin: var(--s-2) 0 0; padding-top: var(--s-2); border-top: 1px solid var(--line);
}
.sales-split > div { min-width: 0; }
.sales-split dt { font-size: var(--fs-2); color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.sales-split dd { margin: 0; font-weight: 600; }
.sales-sub { margin: 0; }
.sales-sub:empty { display: none; }
.sales-money { display: grid; gap: var(--s-2); padding-top: var(--s-2); border-top: 1px solid var(--line); }
.sales-money > div { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--s-2); }
.sales-money .k { flex: 0 0 100%; }

/* The list's stretch of days: Day/Week/Month, and the arrows either side of its name. */
.list-when { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-2); margin-bottom: var(--s-3); }
.pager { display: inline-flex; align-items: center; gap: var(--s-2); }
.list-total { margin: var(--s-3) 0; }
.ic.flip { transform: scaleX(-1); }

/* The sales tile on /My/Jobs: a tile with the bar under its figure. */
.tile .goal { margin-top: 4px; }

/* ---------- A counter sale ----------
   The total sits at the right of the head, where the eye goes after the name; the most-used
   things on the price list are one press each at the top of the Add sheet. */
.sale-total { margin-inline-start: auto; font-size: var(--fs-7); font-weight: 650; white-space: nowrap; }
.quick-picks { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--s-2); margin: 0 0 var(--s-4); }
.quick-picks .btn { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; height: auto; min-height: var(--tap); padding: var(--s-2) var(--s-3); text-align: start; white-space: normal; }
.free-line { margin-top: var(--s-4); padding-top: var(--s-3); border-top: 1px solid var(--line); }
.sale-sums { margin-top: var(--s-3); }
.sale-docs { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-3); }
.open-sales { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-bottom: var(--s-3); }

/* ---------- The stock book ----------
   One item's sheet holds four small forms - count, adjust, send, warn - each a row of a box and
   its button, so none of them needs a dialog of its own. */
.stock-act { padding: var(--s-3) 0; border-top: 1px solid var(--line); }
.stock-act:first-of-type { border-top: none; padding-top: 0; }
.stock-act h2 { margin-bottom: var(--s-2); }
.stock-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s-2); }
.stock-row > label { flex: 1 1 140px; margin: 0; }
.keep-stock { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-2);
              margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--line); }
tr.warn td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
tr.fail td:first-child { box-shadow: inset 3px 0 0 var(--fail); }

/* ---------- The dashboard ----------
   Cards in a grid that is one column on a phone and three on a counter monitor; the month's
   charts take a whole row. Every colour is an existing token, so both palettes answer it and
   nothing here needs a second declaration in the dark block. */
.dash-grid {
  display: grid; gap: var(--s-3);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.card.dash-card { margin: 0; }
.dash-card { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
.dash-card h2 { margin: 0; }
/* The month's two charts, side by side once there is room for both to be read. */
.dash-grid + .dash-card { margin-top: var(--s-3); }
.dash-charts { display: grid; gap: var(--s-2) var(--s-5); }
@media (min-width: 1100px) { .dash-charts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.dash-more { margin-top: auto; align-self: flex-start; display: inline-flex; align-items: center; min-height: var(--tap); font-weight: 600; }
.dash-list { list-style: none; margin: 0; padding: 0; display: grid; }
.dash-list li {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  min-height: 40px; padding: 2px 0; border-top: 1px solid var(--line);
  font-size: var(--fs-4);
}
.dash-list li:first-child { border-top: none; }
/* A row that opens the list behind it: the link is the row's whole height, a finger's worth. */
.dash-list li > a { min-height: var(--tap); display: inline-flex; align-items: center; }
.dash-list li > span, .dash-list li > a { min-width: 0; }
.dash-list li > b { white-space: nowrap; font-variant-numeric: tabular-nums; }
.dash-list li.warn > b { color: var(--warn); }
.dash-compare tr.total td { font-weight: 650; border-top: 2px solid var(--line-2); }

/* The month, day by day. The bars STRETCH to the card's width at a fixed height, so a wide card
   is a wide chart rather than a picture centred in white space; the day numbers are an HTML row
   beneath, because text inside a stretched SVG stretches with it. */
.chart { margin: 0 0 var(--s-3); }
.chart svg { display: block; width: 100%; height: 140px; overflow: visible; }
.chart-days { display: grid; margin: 2px 0 var(--s-1); font-size: var(--fs-1); color: var(--muted); }
.chart-days span { text-align: center; white-space: nowrap; }
.chart-bar { fill: var(--brand); }
.chart-bar.met { fill: var(--pass); }
.chart-bar.second { fill: var(--line-2); }
.chart-bar.closed { fill: var(--line); }
.chart-target { stroke: var(--text); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.chart-today { fill: var(--brand-soft); }
.chart-key { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-4); font-size: var(--fs-3); color: var(--text-2); }
.chart-key span { display: inline-flex; align-items: center; gap: var(--s-1); }
.chart-key i { display: inline-block; width: 12px; height: 12px; border-radius: 3px; }
.chart-key .key-first { background: var(--brand); }
.chart-key .key-second { background: var(--line-2); }
.chart-key .key-target { height: 2px; background: var(--text); border-radius: 0; }

/* ============================================================================
   Full width, signed in (2026-10-01)
   The signed-in screens held their content to --measure (1180px) and centred it, so a counter
   monitor showed two broad empty bands either side of every list and card. The owner asked for
   the whole glass to be used. Everything here is under body.app, so the public pages - the front
   page, sign-in, signup and the legal text - keep the measure, where a readable line length is
   the point. Control sizes are untouched: --tap stays 48px, and fitcheck still checks it.
   ========================================================================= */
body.app { --gutter: var(--s-3); }

body.app main { padding: var(--s-3) var(--gutter) var(--s-4); }
body.app .screen-head { padding-top: var(--s-3); }
body.app .screen-body { padding: var(--s-3) var(--gutter) var(--s-4); }
body.app .screen-bar { padding: var(--s-2) var(--gutter); }

/* :where() keeps these at the weight of the base rules they replace (one class), so a more
   particular rule - .screen-head .card, .card.narrow, .tiles.strip - still wins as it did. */
body.app :where(.card) { padding: var(--s-4); margin-bottom: var(--s-3); }
body.app :where(.head) { margin-bottom: var(--s-3); }
body.app :where(.tiles) { margin-bottom: var(--s-3); }
body.app :where(.cols, .fields, .sales-cards, .dash-grid) { gap: var(--s-3); }
body.app :where(.sales-cards) { margin-bottom: var(--s-3); }

/* An empty list fills the pane like the list it stands in for. */
body.app :where(.empty) { max-width: none; padding: var(--s-5) var(--s-4); }

/* MORE COLUMNS, NOT WIDER BOXES. With no measure a form on a 1920px monitor would otherwise
   stretch every input across the screen; the room goes into a third column instead. */
@media (min-width: 1500px) {
  body.app .screen-body .fields.wide { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ============================================================================
   The front page (2026-10-06)

   ONE SCREEN, NO SCROLL, chosen by the owner from the "Classic" design: the headline and
   its buttons beside a picture of the product, what it does in a strip along the bottom,
   pricing and how-it-works in dialogs over it (Pages/Index.cshtml). Everything here is
   scoped to body.landing-page, .landing, .lp-* or the two dialogs, so no other page moves -
   the .pricing, .plan* and .steps rules above are shared with /My/Plan and are untouched.

   HOW IT FITS. The hero takes whatever height is left over and centres itself in it; the
   strip keeps its own. As the glass gets shorter the extras drop out in order - the feature
   descriptions, then the tick line, then the badge - and the h1 scales with the height as
   well as the width. Below all of that the stage still scrolls: clipped is worse.

   Lengths live on body.landing-page, never :root (themecheck wants every :root token in
   both palettes, and a length has no dark value). Colours are the palette's own.
   ========================================================================= */
body.landing-page {
  --measure: 1320px;
  --lp-gap: clamp(var(--s-4), 3vh, var(--s-6));
}

body.landing-page main {
  display: flex; flex-direction: column;
  padding-top: clamp(var(--s-3), 3vh, var(--s-6));
  padding-bottom: clamp(var(--s-3), 2.5vh, var(--s-5));
}

.landing {
  flex: 1 1 auto;
  display: grid; grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--lp-gap);
}

/* Read aloud, not drawn: the features' heading and the picture's caption. */
.lp-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* ---------- the header's own links ---------- */
.lp-head { display: flex; align-items: center; gap: var(--s-1); min-width: 0; }

.lp-link, .lp-lang {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap-sm); min-width: var(--tap-sm);
  padding: 0 var(--s-3); border-radius: var(--r-2);
  color: var(--text-2); font-size: var(--fs-4); font-weight: 500; white-space: nowrap;
}
.lp-link:hover, .lp-lang:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }

.lp-langs {
  display: flex; gap: 2px; padding: 2px; margin-inline-start: var(--s-2);
  border: 1px solid var(--line); border-radius: var(--r-2);
}
.lp-lang { padding: 0 var(--s-2); font-weight: 600; }
.lp-lang[aria-current="true"] { background: var(--brand-soft); color: var(--brand); }

/* The hero carries the same two buttons, so the header lets them go first. */
@media (max-width: 899px) {
  .lp-link { display: none; }
}

/* A phone: the wordmark and the lock go, so the language switch still fits beside Sign in. */
@media (max-width: 520px) {
  body.landing-page .brand-text { display: none; }
}
@media (max-width: 420px) {
  body.landing-page .topbar > .btn .ic { display: none; }
}

/* With the wordmark gone the brand is the 36px tile alone, which is not a target. */
body.landing-page .brand { min-width: var(--tap-sm); justify-content: center; }

/* ---------- the hero ---------- */
.lp-hero {
  display: grid; grid-template-columns: minmax(0, 1fr);
  align-items: center; align-content: center;
  gap: var(--lp-gap) clamp(var(--s-5), 4vw, 64px);
}

.lp-copy {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: clamp(var(--s-3), 2.2vh, var(--s-5)); min-width: 0;
}
.lp-copy > * { margin: 0; }

.lp-badge {
  display: inline-flex; align-items: center;
  padding: 5px var(--s-3); border-radius: var(--r-pill);
  background: var(--brand-soft); color: var(--brand);
  font-size: var(--fs-3); font-weight: 600;
}

/* Not the 26px h1 every other page has: this is the one place the site raises its voice. */
.lp-copy h1 {
  font-family: "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
  font-size: clamp(30px, min(6vw, 7vh), 60px);
  line-height: 1.06; letter-spacing: -.03em; font-weight: 700;
  color: var(--text); text-wrap: balance;
}

.lp-lead {
  color: var(--text-2); max-width: 60ch;
  font-size: clamp(var(--fs-5), 1.6vh, 18px); line-height: 1.6;
}

.lp-cta { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.lp-btn { min-height: 52px; padding-inline: var(--s-5); font-size: var(--fs-5); }
.lp-arrow { width: 18px; height: 18px; }
[dir="rtl"] .lp-arrow { transform: scaleX(-1); }

.lp-demo {
  display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--s-4);
  color: var(--text-2); font-size: var(--fs-4);
}
.lp-demo a { font-weight: 600; }

.lp-ticks {
  list-style: none; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-5);
  color: var(--text-2); font-size: var(--fs-4);
}
.lp-ticks li { display: inline-flex; align-items: center; gap: var(--s-2); }
.lp-ticks .ic { color: var(--pass); }

/* ---------- the picture of the product ---------- */
/* Room below the card for the message to hang into without covering its last row. */
.lp-preview { position: relative; margin: 0; padding-bottom: 68px; min-width: 0; }

.lp-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: 20px;
  box-shadow: var(--sh-2);
  padding: clamp(var(--s-4), 2.2vh, var(--s-5));
  display: flex; flex-direction: column; gap: clamp(var(--s-3), 1.8vh, var(--s-4));
}

.lp-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }
.lp-card-head > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.lp-where {
  font-size: var(--fs-2); font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em;
}
.lp-card-title { font-size: var(--fs-6); font-weight: 700; color: var(--text); }
.lp-periods {
  padding: 6px 10px; border-radius: var(--r-2); background: var(--surface-2);
  font-size: var(--fs-3); font-weight: 600; color: var(--text-2); white-space: nowrap;
}

.lp-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2); }
.lp-stats > div {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding: var(--s-3); border-radius: var(--r-3); background: var(--surface-2);
}
.lp-stats span { font-size: var(--fs-2); color: var(--muted); }
.lp-stats b {
  font-size: var(--fs-6); font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.lp-target { display: flex; flex-direction: column; gap: 6px; }
.lp-target-row { display: flex; justify-content: space-between; gap: var(--s-3); font-size: var(--fs-3); }
.lp-target-row span:first-child { font-weight: 600; color: var(--text); }
.lp-target-row span:last-child { color: var(--text-2); }
.lp-bar { height: 10px; border-radius: var(--r-pill); background: var(--brand-soft); overflow: hidden; }
.lp-bar i { display: block; width: 86%; height: 100%; border-radius: inherit; background: var(--brand); }

.lp-rows { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: var(--r-3); }
.lp-rows li { display: flex; align-items: center; gap: var(--s-3); min-height: 44px; padding: var(--s-2) var(--s-3); }
.lp-rows li + li { border-top: 1px solid var(--line); }
.lp-id {
  flex: 0 0 auto; width: 6ch;
  font-size: var(--fs-3); font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums;
}
.lp-what {
  flex: 1 1 auto; min-width: 0; font-size: var(--fs-4); color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lp-rows .verdict { flex: 0 0 auto; }

/* A figure is one word: "J-0042" broken at its hyphen reads as two things. */
.lp-preview bdi { white-space: nowrap; }

/* The message the customer got, in ink: dark on the light palette, light on the dark one,
   so it reads as something that left the shop rather than another panel of it. The green
   is the pass colour pulled towards the page so it stays legible on either ink. */
.lp-message {
  position: absolute; bottom: 0; inset-inline-end: -8px;
  width: min(300px, 78%);
  display: flex; flex-direction: column; gap: 6px;
  padding: var(--s-3) var(--s-4); border-radius: 16px;
  background: var(--text); color: var(--bg-1); box-shadow: var(--sh-2);
  font-size: var(--fs-4); line-height: 1.5;
}
.lp-message-head {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-size: var(--fs-2); font-weight: 700;
  color: color-mix(in srgb, var(--pass) 45%, var(--bg-1));
}

/* ---------- what it does ---------- */
.lp-features ul {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--s-3); grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lp-feature {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center;
  gap: var(--s-1) var(--s-3);
  padding: var(--s-3); border: 1px solid var(--line); border-radius: var(--r-3);
  background: var(--surface);
}
.lp-feature .ic {
  width: 36px; height: 36px; padding: 8px; box-sizing: border-box;
  border-radius: var(--r-2); background: var(--brand-soft); color: var(--brand);
}
.lp-feature h3 { margin: 0; font-size: var(--fs-4); font-weight: 650; line-height: 1.25; color: var(--text); }
.lp-feature p {
  display: none; grid-column: 1 / -1; margin: 0;
  font-size: var(--fs-3); line-height: 1.45; color: var(--text-2);
}

/* ---------- by size ---------- */
@media (min-width: 700px) {
  .lp-features ul { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1280px) {
  .lp-features ul { grid-template-columns: repeat(8, minmax(0, 1fr)); }
}

/* Landscape and wide enough: side by side, the words a little wider than the picture. */
@media (min-width: 1000px) and (orientation: landscape) {
  .lp-hero { grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); }
  .lp-copy h1 { font-size: clamp(30px, min(3.6vw, 7.2vh), 64px); }
}

/* One column (portrait, or narrow): the picture under the words, no wider than it reads. */
@media not ((min-width: 1000px) and (orientation: landscape)) {
  .lp-preview { width: 100%; max-width: 620px; }
}

/* Narrow AND short - a landscape tablet, a phone: no room for the picture at all. */
@media (max-width: 999px) and (max-height: 999px) {
  .lp-preview { display: none; }
}

/* Tall and wide: room to say a line about each feature, four to a row. */
@media (min-width: 1000px) and (min-height: 1000px) {
  .lp-features ul { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .lp-feature p { display: block; }
}

/* Portrait: the words, the picture and the strip are stacked, so the gaps between them are
   paid three times over - smaller ones. */
@media (orientation: portrait) {
  body.landing-page { --lp-gap: clamp(var(--s-4), 2vh, var(--s-5)); }
  .lp-copy { gap: clamp(var(--s-3), 1.6vh, var(--s-5)); }
}

/* Short glass: the extras go first. Side by side on a narrower screen the tick line wraps
   to two rows, so it goes sooner there. */
@media (max-height: 720px), (max-width: 1279px) and (max-height: 800px) {
  .lp-ticks { display: none; }
}
@media (max-height: 680px) {
  .lp-badge { display: none; }
}

/* The picture's rows are not controls, so on short glass they need not be a finger high. */
@media (max-height: 800px) {
  .lp-rows li { min-height: 38px; padding-block: 6px; }
}

@media (max-width: 699px) {
  body.landing-page { --lp-gap: var(--s-4); }
  body.landing-page main { padding-top: var(--s-3); }
  .lp-copy { gap: var(--s-3); }
  .lp-demo > span { display: none; }
  .lp-ticks { display: none; }
  .lp-btn { flex: 1 1 auto; padding-inline: var(--s-4); }
  .lp-features ul { gap: var(--s-2); }
  .lp-feature { padding: var(--s-2) var(--s-3); }
  .lp-feature .ic { width: 32px; height: 32px; padding: 7px; }

  /* Four lines of the pitch, and the rest behind the buttons: the alternative on a phone
     is a page that scrolls, which is the one thing this page is built not to be. */
  .lp-lead {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4;
    overflow: hidden;
  }
}
@media (max-width: 699px) and (max-height: 760px) {
  .lp-badge { display: none; }
}

/* ---------- the two dialogs ---------- */
.lp-dialog-lead { margin: 0 0 var(--s-4); color: var(--text-2); font-size: var(--fs-4); }

.lp-steps-title { margin: 0 0 var(--s-3); font-size: var(--fs-5); font-weight: 650; color: var(--text); }
.lp-steps + .lp-steps-title { margin-top: var(--s-5); }
.lp-steps {
  list-style: none; margin: 0; padding: 0; counter-reset: lp-step;
  display: grid; gap: var(--s-2);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}
/* Four steps: two by two, not three and an orphan. */
.lp-steps-pairs { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
.lp-steps li {
  counter-increment: lp-step;
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--s-3); border: 1px solid var(--line); border-radius: var(--r-3);
  background: var(--surface-2);
}
.lp-steps li::before {
  content: counter(lp-step, decimal-leading-zero);
  font-size: var(--fs-3); font-weight: 700; color: var(--brand);
}
.lp-steps b { font-weight: 650; color: var(--text); }
.lp-steps span { font-size: var(--fs-3); line-height: 1.45; color: var(--text-2); }
.lp-dialog-foot { margin: var(--s-4) 0 0; color: var(--text-2); }

/* ---------- the footer, as one short row ---------- */
body.landing-page .foot {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: var(--s-1) var(--s-4); padding: var(--s-2) var(--gutter);
}
body.landing-page .foot .theme-form-inline { margin: 0; }
body.landing-page .foot p { margin: 0; }
body.landing-page .foot .foot-note { display: none; }
/* The front page has its own language links in the header, so the footer's switch would be the
   same choice twice - and on portrait glass it wrapped the one-row footer into two and pushed the
   Urdu page 7px past the screen (17px on a phone). */
body.landing-page .foot .lang-form { display: none; }

/* ---------- Hindi and Urdu ----------
   Fonts the device already has: Nirmala UI is Windows' Devanagari, Segoe UI carries Arabic
   script. No webfont, because the CSP has no font-src and a front page that waits on a font
   server is a front page that flashes. Letter-spacing is taken off both: tracking breaks
   the joins between Urdu letters and the headstroke of Devanagari. */
.landing:lang(hi), dialog.modal:lang(hi) {
  font-family: "Nirmala UI", "Noto Sans Devanagari", "Segoe UI", system-ui, sans-serif;
}
.landing:lang(ur), dialog.modal:lang(ur) {
  font-family: "Segoe UI", "Noto Naskh Arabic", "Noto Nastaliq Urdu", system-ui, sans-serif;
}
.landing:lang(hi) h1, .landing:lang(ur) h1 { font-family: inherit; letter-spacing: normal; line-height: 1.25; }
.landing:lang(ur) .lp-lead { line-height: 1.8; }
.landing:lang(hi) .lp-where, .landing:lang(ur) .lp-where { letter-spacing: normal; }

/* ============================================================================
   Right to left, and the two scripts that are not Latin (2026-10-06)

   <html dir="rtl"> for Urdu (Language/Words.cs), and the stylesheet above is written in LOGICAL
   properties - inline-start rather than left - so almost all of it mirrors by itself and renders
   exactly as before in English. What is here is what no logical property covers.
   ========================================================================= */

/* The select chevron is a background image, and background-position has no logical keyword. */
[dir="rtl"] select { background-position: left var(--s-3) center; }

/* The drawer comes in from the side it sits on. Its open state (transform: none) outranks this. */
[dir="rtl"] .drawer-panel { transform: translateX(-100%); }

/* A warning row's bar, on the side a row starts from. box-shadow has no logical form. */
[dir="rtl"] tr.warn td:first-child { box-shadow: inset -3px 0 0 var(--warn); }
[dir="rtl"] tr.fail td:first-child { box-shadow: inset -3px 0 0 var(--fail); }

/* ARROWS POINT THE WAY THE PAGE READS. Back, onward and sign out are marked .dir in the markup;
   the carets that are turned to point down are not, and stay as they are. */
[dir="rtl"] .ic.dir { transform: scaleX(-1); }
[dir="rtl"] .ic.flip { transform: none; }

/* A chart reads left to right in every language: its bars are drawn in that order, and the day
   numbers under them have to line up with them. */
.chart { direction: ltr; }

/* A telephone number is written left to right whatever the page around it - the box it is typed
   into, and the link it is shown as. In a right-to-left paragraph "07700 900222" is two runs of
   digits, and the paragraph puts them in its own order: it read "900222 07700". */
.phone-field { direction: ltr; }
[dir="rtl"] a[href^="tel:"] { direction: ltr; unicode-bidi: isolate; }

/* THE TYPE. Fonts the device already has - Nirmala UI is Windows' Devanagari and Segoe UI carries
   Arabic script - so nothing is fetched (there is no font-src in the CSP, and a page that waits
   on a font server flashes). Letter-spacing comes off every tracked caption: tracking breaks the
   joins between Urdu letters and the headstroke of Devanagari, and uppercase means nothing to
   either script. */
html:lang(hi) body {
  font-family: "Nirmala UI", "Noto Sans Devanagari", "Segoe UI", system-ui, sans-serif;
}
html:lang(ur) body {
  font-family: "Segoe UI", "Noto Naskh Arabic", "Noto Nastaliq Urdu", system-ui, sans-serif;
  line-height: 1.7;
}
/* Except the front page, which is one screen with no scroll and already sets its own Urdu lines
   (the lead at 1.8, the headline at 1.25). The site-wide 1.7 reached its header, preview and
   feature strip as well, and put it 7px past an 800x1280 screen. */
html:lang(ur) body.landing-page { line-height: 1.55; }
html:lang(hi) *, html:lang(ur) * { letter-spacing: normal; }

/* ============================================================================
   THE REBRAND (2026-10-13)

   The owner asked for a site that somebody who has never used anything like it can find their
   way round, "in the range of professional sites", and chose from three: keep the name
   DeviceCheck, a task-first menu of seven words, clean white with one deep blue, and the front
   page rebuilt to match. What the research said such sites do, and what this section does:

     a menu of one- or two-word nouns, icon AND word, in a rail on a wide screen and a bar of a
       few along the bottom of a phone (Shopify's app navigation, Square's point of sale);
     a home page that is mostly big buttons for the everyday jobs, with today's numbers under
       them (Square's Dashboard);
     buttons that look like buttons - a border, a shadow, a strong colour for the main one - and
       nothing else that does (the plain-language guidance at health.gov);
     one box that finds anything (repair-shop software, RepairShopr's above all).

   Written as one section over the rules above rather than through them, so the rules it
   supersedes can be read for what they were and the rebrand can be read as one decision.
   Colours are tokens and nothing else: both palettes answer every one (themecheck).
   ========================================================================= */
body { --rail: 248px; }
body.app { --gutter: var(--s-4); }
@media (min-width: 1100px) { body.app { --gutter: var(--s-5); } }

/* ---------- The header: the mark, one search box, the person ---------- */
.topbar { min-height: 64px; gap: var(--s-4); padding: var(--s-2) var(--s-5); }

.top-search { position: relative; flex: 0 1 520px; min-width: 0; margin: 0; }
.top-search .ic {
  position: absolute; inset-inline-start: 14px; top: 50%; transform: translateY(-50%);
  color: var(--muted); pointer-events: none;
}
.top-search input {
  min-height: var(--tap-sm); padding-block: 8px; padding-inline: 44px 16px;
  border-radius: var(--r-pill); background: var(--surface-2); border-color: var(--line);
}
.top-search input:focus { background: var(--surface); border-color: var(--brand); }
.top-search-btn {
  display: none; flex: 0 0 auto;
  width: var(--tap); height: var(--tap); border-radius: 50%;
  align-items: center; justify-content: center; color: var(--text-2);
}
.top-search-btn:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.top-search-btn .ic { width: 22px; height: 22px; }
@media (max-width: 859px) {
  .top-search { display: none; }
  .top-search-btn { display: inline-flex; }
}

.who { gap: var(--s-3); }
.who-initials {
  width: 36px; height: 36px; border-radius: 50%; flex: 0 0 auto;
  display: grid; place-items: center;
  background: var(--brand); color: var(--on-brand);
  font-size: 13px; font-weight: 700; letter-spacing: .02em;
}
@media (max-width: 599px) {
  .who-text { display: none; }
  .brand-sub { display: none; }
}

/* ---------- The menu ---------- */
.tab { position: relative; }
.tab.on { background: var(--brand-soft); color: var(--brand); font-weight: 650; }
.nav-caption { display: none; }

@media (min-width: 1100px), (min-width: 700px) and (orientation: landscape) {
  .tabs { padding: var(--s-3) var(--s-2); gap: 2px; }

  /* SETTINGS, AND THE DOOR BETWEEN THE SHOP AND DEVSOFTUK, STAND APART at the foot of the rail
     under a line - the everyday sections above them, the things set up once below. */
  .tab:not(.tab-foot) + .tab-foot { margin-top: auto; }
  .tab:not(.tab-foot) + .tab-foot::after {
    content: ""; position: absolute; inset-inline: var(--s-2); top: -5px;
    height: 1px; background: var(--line);
  }
  .tab.tab-foot { flex: 0 0 auto; }

  .nav-caption {
    display: block; margin: 0; padding: var(--s-1) var(--s-3) var(--s-2);
    font-size: var(--fs-2); font-weight: 700; color: var(--muted);
  }
}

@media (min-width: 1100px) {
  .tab {
    min-height: var(--tap); padding: 0 var(--s-3); gap: var(--s-3);
    font-size: 15px; font-weight: 550; color: var(--text-2);
  }
  .tab .ic { width: 22px; height: 22px; }
  .tab.on { font-weight: 650; color: var(--brand); }

  /* Where you are, said twice: the tint, and a bar at the edge the rail starts from. */
  .tab.on::before {
    content: ""; position: absolute; inset-inline-start: calc(var(--s-2) * -1); top: 10px; bottom: 10px;
    width: 4px; background: var(--brand);
    border-start-end-radius: 4px; border-end-end-radius: 4px;
  }
}

/* ---------- Type ----------
   Section headings are written in sentence case at reading size. They were 12px uppercase grey
   captions, which reads as a label to be skimmed past - and uppercase is harder to read for
   exactly the person this rebrand is for. */
h1 { font-size: var(--fs-7); font-weight: 700; letter-spacing: -.02em; }
h2 {
  font-size: var(--fs-5); color: var(--text); font-weight: 650;
  text-transform: none; letter-spacing: -.005em; margin: 0 0 var(--s-3);
}
.tile-k, .k {
  font-size: var(--fs-3); text-transform: none; letter-spacing: 0; color: var(--muted);
}
fieldset.perm-group legend { text-transform: none; letter-spacing: 0; font-size: var(--fs-3); color: var(--text-2); }

/* ---------- Surfaces ---------- */
.card { border-radius: var(--r-3); box-shadow: var(--sh-1); }
.tile { border-radius: var(--r-3); box-shadow: var(--sh-1); }
a.tile:hover { border-color: var(--brand); }

/* ---------- Buttons look like buttons ----------
   White with a visible border and a shadow; the main one deep blue. A button that is only a
   tint on the page is a button somebody does not know they can press. */
.btn {
  background: var(--surface); border-color: var(--line-2); color: var(--text);
  font-weight: 600; box-shadow: var(--sh-1);
}
.btn:hover { background: var(--surface-2); border-color: var(--line-2); }
.btn:active { background: var(--surface-3); }
.btn.primary { background: var(--brand); border-color: var(--brand); color: var(--on-brand); box-shadow: 0 1px 2px rgba(15, 23, 42, .18); }
.btn.primary:hover { background: var(--brand-strong); border-color: var(--brand-strong); }
.btn.ghost { background: transparent; box-shadow: none; color: var(--text-2); border-color: var(--line-2); }
.btn.ghost:hover { background: var(--surface-2); color: var(--text); }
.btn.warn { background: transparent; box-shadow: none; border-color: var(--warn); color: var(--warn); }

/* ---------- Boxes that are typed into ---------- */
input, select, textarea { background: var(--surface); border-color: var(--line-2); }
input:focus, select:focus, textarea:focus { border-color: var(--brand); }

/* ---------- Choices ---------- */
.chip.on { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.chip.on b { opacity: .9; }
.tile.on { border-color: var(--brand); background: var(--brand-soft); box-shadow: inset 0 0 0 1px var(--brand); }

/* ---------- Tables ---------- */
.grid th {
  font-size: var(--fs-2); font-weight: 650; color: var(--muted);
  text-transform: none; letter-spacing: 0;
  background: var(--surface-2); padding-block: 10px;
}
.grid thead th { background: var(--surface-2); }

/* ---------- The bar along the bottom of a screen: the next thing to press ---------- */
.screen-bar {
  border-top: 1px solid var(--line);
  box-shadow: 0 -4px 16px rgba(15, 23, 42, .04);
}
.screen-bar .btn.primary { min-height: 52px; font-size: var(--fs-5); padding-inline: var(--s-5); }

/* ---------- Tabs inside a page ---------- */
.work-tab { font-size: 15px; }

/* ---------- Status pills, in sentence case ----------
   They were text-transform: capitalize, which capitalised every word - "Booked In", "Ready To
   Collect". The words are now capitalised where they are written (Words.Cap), the first letter
   only, as a sentence is. */
.verdict { text-transform: none; }

/* ---------- Page titles ----------
   The name of the screen you are on, at a size that says so. It was 20px - smaller than the
   headings inside some cards. */
.work-name h1 { font-size: var(--fs-7); }

/* ============================================================================
   Home (/My/Dashboard): the big buttons first
   ========================================================================= */
.home-hello { margin: 0 0 var(--s-4); }
.home-hello h1 { font-size: var(--fs-8); margin: 0 0 2px; }
.home-hello .lead { margin: 0; }

.tasks {
  display: grid; gap: var(--s-3); margin: 0 0 var(--s-5);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 290px), 1fr));
}
.task {
  display: flex; align-items: center; gap: var(--s-4); min-width: 0;
  min-height: 96px; padding: var(--s-4) var(--s-5);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--r-3); box-shadow: var(--sh-1);
  transition: border-color var(--t-1), background var(--t-1), transform var(--t-1);
}
.task:hover { border-color: var(--brand); text-decoration: none; }
.task:active { transform: translateY(1px); }
.task-ic {
  width: 52px; height: 52px; border-radius: 14px; flex: 0 0 auto;
  display: grid; place-items: center;
  background: var(--brand-soft); color: var(--brand);
}
.task-ic .ic { width: 28px; height: 28px; }
.task-text { display: flex; flex-direction: column; min-width: 0; }
.task-title { font-size: 17px; font-weight: 700; line-height: 1.25; }
.task-sub { font-size: var(--fs-3); color: var(--muted); line-height: 1.35; }
.task-n {
  margin-inline-start: auto; font-size: var(--fs-7); font-weight: 750;
  font-variant-numeric: tabular-nums; line-height: 1;
}


.task.pass .task-ic { background: var(--pass-soft); color: var(--pass); }
.task.warn .task-ic { background: var(--warn-soft); color: var(--warn); }
.task.fail .task-ic { background: var(--fail-soft); color: var(--fail); }
.task.fail .task-n { color: var(--fail); }
.task.warn .task-n { color: var(--warn); }
.task.calm .task-n { color: var(--muted); }

.section-title { font-size: var(--fs-6); font-weight: 700; letter-spacing: -.01em; margin: 0 0 var(--s-3); }

/* ============================================================================
   Settings (/My/Settings): one page of doors
   ========================================================================= */
.hub {
  display: grid; gap: var(--s-3);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
}
.hub + .section-title { margin-top: var(--s-5); }
.hub-item {
  display: flex; align-items: center; gap: var(--s-4); min-width: 0;
  min-height: 84px; padding: var(--s-4);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--r-3); box-shadow: var(--sh-1);
  transition: border-color var(--t-1);
}
.hub-item:hover { border-color: var(--brand); text-decoration: none; }
.hub-ic {
  width: 46px; height: 46px; border-radius: 12px; flex: 0 0 auto;
  display: grid; place-items: center; background: var(--brand-soft); color: var(--brand);
}
.hub-ic .ic { width: 24px; height: 24px; }
.hub-text { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.hub-text b { font-size: var(--fs-5); font-weight: 650; }
.hub-text span { font-size: var(--fs-3); color: var(--muted); line-height: 1.4; }
.hub-go { color: var(--muted); flex: 0 0 auto; }
.hub-card { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3) var(--s-5); margin-top: var(--s-5); }
.hub-card .lang-form, .hub-card .theme-form-inline { margin: 0; }
.hub-card .hub-label { font-weight: 650; min-width: 120px; }

/* ============================================================================
   Search (/My/Search): one box, three kinds of answer
   ========================================================================= */
.search-big { position: relative; max-width: 760px; margin: 0; }
.search-big .ic {
  position: absolute; inset-inline-start: 16px; top: 50%; transform: translateY(-50%);
  width: 22px; height: 22px; color: var(--muted); pointer-events: none;
}
.search-big input {
  min-height: 56px; font-size: 18px; padding-inline: 50px 16px;
  border-radius: var(--r-3); box-shadow: var(--sh-1);
}
.found { padding: 0; overflow: hidden; }
body.app :where(.found) { padding: 0; }
.found-head {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-4); border-bottom: 1px solid var(--line);
}
.found-head h2 { margin: 0; display: flex; align-items: center; gap: var(--s-2); }
.found-head h2 .ic { color: var(--brand); }
.found-head .count { color: var(--muted); font-size: var(--fs-3); }
.found-head a { margin-inline-start: auto; font-weight: 600; display: inline-flex; align-items: center; min-height: var(--tap-sm); }
.found-list { list-style: none; margin: 0; padding: 0; }
.found-list a {
  display: flex; align-items: center; gap: var(--s-3);
  min-height: 60px; padding: var(--s-2) var(--s-4);
  border-top: 1px solid var(--line); color: var(--text);
}
.found-list li:first-child a { border-top: 0; }
.found-list a:hover { background: var(--surface-2); text-decoration: none; }
.found-main { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.found-main b { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.found-main span { color: var(--muted); font-size: var(--fs-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.found-none { padding: var(--s-4); margin: 0; color: var(--muted); }

/* ============================================================================
   The front page, in the new brand (2026-10-13)

   Same one screen, same words, same dialogs and the same three languages (the owner's choice
   of 2026-10-06 stands); what changes is the picture and the colour around it. The picture is
   the NEW app - its menu down the side with Home lit, beside the shop's day - so what a
   stranger is shown is what a shop will open. A wash of the brand blue sits behind it, drawn
   from the token so both palettes answer it.
   ========================================================================= */
.lp-preview { isolation: isolate; }
.lp-preview::before {
  content: ""; position: absolute; z-index: -1;
  /* Never past the inline end: a wash that hangs off the side is a page that scrolls sideways
     (fitcheck: 3-5px at 1280 and 1366). */
  inset-block: -7% 6%; inset-inline: 10% 0;
  border-radius: 36px;
  background:
    radial-gradient(120% 90% at 85% 10%, color-mix(in srgb, var(--brand) 22%, transparent), transparent 65%),
    linear-gradient(150deg, color-mix(in srgb, var(--brand) 12%, transparent), transparent 70%);
}

.lp-card.lp-app { flex-direction: row; padding: 0; gap: 0; overflow: hidden; }
.lp-app-rail {
  flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 14px 10px; background: var(--surface-2); border-inline-end: 1px solid var(--line);
}
.lp-app-rail .mark { width: 30px; height: 30px; margin-bottom: 6px; }
.lp-app-rail span {
  width: 34px; height: 34px; border-radius: 9px;
  display: grid; place-items: center; color: var(--muted);
}
.lp-app-rail span.on { background: var(--brand-soft); color: var(--brand); }
.lp-app-rail .ic { width: 18px; height: 18px; }
.lp-app-main {
  flex: 1 1 auto; min-width: 0;
  padding: clamp(var(--s-4), 2.2vh, var(--s-5));
  display: flex; flex-direction: column; gap: clamp(var(--s-3), 1.8vh, var(--s-4));
}

/* The badge in the brand's own colour, filled, so it reads as the brand rather than a tint. */
.lp-badge { background: var(--brand); color: var(--on-brand); }

/* The feature strip: the same cards as the app's Home buttons. */
.lp-feature { box-shadow: var(--sh-1); }

/* On a phone the picture has no room for a menu as well. */
@media (max-width: 699px) {
  .lp-app-rail { display: none; }
}

/* ============================================================================
   One rhythm (2026-10-13)

   "Look at the spacing between the buttons and the tables, and all of it." Measured on every
   screen rather than judged by eye: the gap between two things in a screen's head was 0, 8,
   12, 16 or 24px depending on which of a dozen rules matched; two rows of chips on Home and
   on Sales touched; a form in a card stood 24px under its heading where a table stood 12, and
   so did a one-button form inside a table cell, which is why "Arrived" hung below its line; a
   button in a table row started where the text did and ran 24px below it.

   The rule now: things that sit one above the other in a head, a panel or a card are --s-3
   apart, and things in a row are centred on each other. The margins that said otherwise were
   taken out where they were written; what is here is what could not be.
   ========================================================================= */

/* The head's own gap is the spacing; a child's margin would add to it. The class is doubled
   so this outranks the per-thing margins written above it (.tiles.strip, .screen-head .card,
   .home-hello), which still apply wherever those things are NOT in a head.

   AND EACH IS ITS OWN HEIGHT. A strip of chips is told to take the whole row (flex: 1 1 100%)
   so it fills a .filters line beside a search; in a column that same rule asks for the whole
   HEIGHT of the head, and the Devices page's tiles were squeezed to a sliver under it. */
.screen-head.screen-head > * { margin-block: 0; flex: none; }

/* The back button keeps to the top of a name of three lines - a heading, a link and a lead -
   rather than floating to the middle of them. Two lines are the button's own height, so every
   other screen is where it was. */
.work-top > .work-back { align-self: flex-start; }

/* Two rows of chips, one above the other: a period and a branch on Home, a stretch and a
   heading on Sales. */
.chips.strip + .chips.strip { margin-top: var(--s-2); }

/* Home's chips are in the body, under "How the shop is doing", and what follows them - the
   cards, or the branches side by side - sat against the last row. */
.home-chips + :not(.home-chips) { margin-top: var(--s-3); }

/* A table on a tab is the same table as one on a list, and wears the same frame. On the Team,
   Plan and Sales tabs it sat on the bare canvas, a different object from the Repairs list. */
.work-panel > .scroll > .grid {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-3); overflow: hidden; box-shadow: var(--sh-1);
}
.work-panel > .scroll > .grid th, .work-panel > .scroll > .grid td { padding-inline-start: var(--s-4); }
.work-panel > .scroll > .grid th:last-child, .work-panel > .scroll > .grid td:last-child { padding-inline-end: var(--s-4); }

/* A ROW WITH A BUTTON IN IT is centred on the button. The cells are top-aligned, which is right
   for words, so a 44px button started level with the text and hung 24px below it - Void on a
   payment, Open, Void and Credit on an invoice, Restore on the price list. */
.grid tbody tr:has(.btn) > td { vertical-align: middle; }

/* A tight table has 8px in its cells, so a row's link reaches 8px out of its line and no further:
   any more and the last row's target would stand out of the table, and the .scroll round it -
   which scrolls sideways, and therefore up and down too - would grow a region that moves. */
.grid.tight td > a:not(.btn):not(.chip), .grid.tight td > strong > a, .grid.tight .tap { margin-block: -8px; }

/* A form inside something that is already spaced - a table cell, a row of buttons, a heading -
   adds nothing to it. Every form in a card carried the card form's top margin, so a one-button
   form beside a sentence stood lower than the sentence. */
.card :is(td, .row, .chips, .panel-head, .order-line, .modal-foot) form,
.card form.inline,
.card > form:first-child,
.card > :is(h2, .panel-head) + form { margin-top: 0; }

/* A ROW STOOD UP AS A CARD on a phone puts each value in a grid cell, and a grid cell stretches
   what is in it: a status pill ran the whole width of the card, a bar of colour rather than a
   word. Each value is its own width, at the start of its line. */
@media (max-width: 700px) {
  .grid.stack td > * { justify-self: start; }
}

/* A heading after a table starts a new part of the page, and said so with nothing: on Billing
   the events' heading sat on the last row of the checkouts. */
.scroll + h2, .scroll + h3 { margin-top: var(--s-5); }

/* A label and its value on one line: the label is smaller, and at the top of its cell it sat a
   few pixels above the value it names. On the baseline they read as one line. */
.kv { align-items: baseline; }

/* What a part on order is waiting for, on one line with the button that ends the wait. */
.order-line { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); margin-top: var(--s-2); }

/* The last of the shouting captions. The rebrand put headings and labels in sentence case at a
   reading size, and these four were missed: the headings over a sales card's split, a plan's
   name, a fold's title and the label beside each value when a table row stands up as a card on
   a phone. */
.sales-split dt, .plan-name, .disclose > summary, .grid.stack td::before {
  text-transform: none; letter-spacing: 0; font-size: var(--fs-3); font-weight: 600;
}

/* ============================================================================
   A press that does not reload the page (js/nav.js)

   While the next page is being asked for: a thin bar along the very top, after a moment, and
   the pointer saying so. On <html>, which stays while the body under it is replaced.
   ========================================================================= */
html.nav-busy, html.nav-busy * { cursor: progress; }
html.nav-busy body::before {
  content: ""; position: fixed; z-index: 1000; inset-inline: 0; top: 0; height: 3px;
  background: var(--brand); transform-origin: 0 50%;
  animation: nav-busy 1.4s ease-in-out infinite;
}
html[dir="rtl"].nav-busy body::before { transform-origin: 100% 50%; }
@keyframes nav-busy {
  0%   { transform: scaleX(0);   opacity: 1; }
  60%  { transform: scaleX(.85); opacity: 1; }
  100% { transform: scaleX(1);   opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  html.nav-busy body::before { animation: none; transform: none; opacity: .6; }
}

/* The answer never came: said in the colour of a fault, where the confirmations appear. */
.toast.nav-fail { border-inline-start-color: var(--fail); }
.toast.nav-fail .ic { color: var(--fail); }

/* ============================================================================
   A little colour (2026-10-14)

   The owner: "make the website a little colourful so it looks good, but not so much that it
   looks AI generated." Before this every icon chip, the avatar and the second series of every
   chart were the one deep blue, and every job still being worked on wore a grey pill - so Home,
   Settings and Search were rows of identical blue squares and the Repairs list a column of grey.

   What professional admin software does (Shopify's Polaris, Stripe, Linear) and what this
   section does: the chrome stays neutral and the main button stays the one brand blue; colour is
   kept for MEANING - a status, or which kind of thing something is - and it goes on the small
   icon square, never on a whole card. One colour per kind of thing, used the same way wherever
   that kind appears, so the colour helps somebody find their way:

     repairs       the brand blue          customers, people   violet
     selling, money teal                   devices             pink
     stock, prices orange                  places, delivery    sky
     you           slate

   THE SQUARES ARE SOLID, with a white mark - the pattern of the settings screens on a phone and
   on Windows. The first version of this section the same morning used 10% tints of the same
   colours, and the owner could not see that anything had changed: a pale wash on white is not
   colour to somebody glancing at a counter screen.

   And what it deliberately does not do, because each is what makes a page look machine-made:
   no gradient on a button, a heading or a background; no glass, blur or glow; no coloured stripe
   down the side of a card; no colour on a menu that is already telling you where you are.

   The hues are tokens, answered in both palettes (themecheck). The light ones are 700-weights,
   at least 4.5:1 against white either way round; the dark ones 400-weights, with near-black
   marks (--on-hue). A square reads --hue and --hue-ink, set on the thing that knows what it is,
   and falls back to the brand blue with its own white.
   ========================================================================= */

/* ---------- The squares ---------- */
.task-ic, .hub-ic, .card-ic, .lp-feature .ic, .found-head h2 .ic {
  background: var(--hue, var(--brand)); color: var(--hue-ink, var(--on-brand));
}

.task[data-task="ready"],
.hub-item[data-door="plan"],
.dash-card[data-card="sales"] { --hue: var(--c-teal); --hue-ink: var(--on-hue); }

.task[data-task="find"],
.hub-item[data-door="team"],
.found[data-found-kind="customers"],
.dash-card[data-card="stats"] { --hue: var(--c-violet); --hue-ink: var(--on-hue); }

.task[data-task="low"],
.hub-item[data-door="prices"] { --hue: var(--c-orange); --hue-ink: var(--on-hue); }

.task[data-task="late"],
.hub-item[data-door="branches"],
.dash-card[data-card="delivery"] { --hue: var(--c-sky); --hue-ink: var(--on-hue); }

.hub-item[data-door="download"],
.found[data-found-kind="devices"] { --hue: var(--c-pink); --hue-ink: var(--on-hue); }

.hub-item[data-door="profile"],
.hub-item[data-door="password"],
.dash-card[data-card="charts"] { --hue: var(--c-slate); --hue-ink: var(--on-hue); }

/* Home: something waiting, late or running out takes the colour of what it means, solid. */
.task.pass .task-ic { background: var(--pass); color: var(--on-hue); }
.task.warn .task-ic { background: var(--warn); color: var(--on-hue); }
.task.fail .task-ic { background: var(--fail); color: var(--on-hue); }
.task-ic { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .12); }

/* Settings: a little smaller than Home's, as a phone's settings rows are. */
.hub-ic { border-radius: 11px; }

/* Search: each kind of answer under its own square, as on Home and Settings. */
.found-head h2 .ic {
  width: 30px; height: 30px; padding: 6px; border-radius: 8px; flex: 0 0 auto;
}

/* The dashboard's cards: what each one is about, at a glance. */
.dash-card h2 { display: flex; align-items: center; gap: var(--s-2); }
.card-ic {
  width: 28px; height: 28px; border-radius: 8px; flex: 0 0 auto;
  display: grid; place-items: center;
}
.card-ic .ic { width: 16px; height: 16px; }

/* ---------- The totals above a list: the ones that mean something are washed in it ----------
   The plain total stays white; waiting, late and owed carry their colour on the whole small tile,
   which is what lets a shop see from across the counter that something wants doing. Chosen, the
   tile keeps the brand ring the plain ones get. */
.tile.pass { background: var(--pass-wash); border-color: color-mix(in srgb, var(--pass) 30%, transparent); }
.tile.warn { background: var(--warn-wash); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.tile.fail { background: var(--fail-wash); border-color: color-mix(in srgb, var(--fail) 30%, transparent); }
.tile.pass .tile-k, .tile.warn .tile-k, .tile.fail .tile-k { color: var(--text-2); }
.tile.on.pass, .tile.on.warn, .tile.on.fail { border-color: var(--brand); }

/* ---------- A job on the bench says so ----------
   ShopVocabulary.Tone answers "info" for diagnosing, diagnosed, approved, in repair and
   re-tested: somebody is working on it. Booked in and waiting stay grey, ready amber, done green,
   stopped red. */
.verdict.info { color: var(--info); border-color: transparent; background: var(--info-soft); }

/* ---------- The person's initials, in a colour of their own ----------
   Picked in _Layout from a stable hash of the name, so it is the same colour on every device -
   what Gmail, Slack and Teams do. */
.who-initials[data-hue="1"] { background: var(--c-teal);   color: var(--on-hue); }
.who-initials[data-hue="2"] { background: var(--c-violet); color: var(--on-hue); }
.who-initials[data-hue="3"] { background: var(--c-orange); color: var(--on-hue); }
.who-initials[data-hue="4"] { background: var(--c-pink);   color: var(--on-hue); }
.who-initials[data-hue="5"] { background: var(--c-sky);    color: var(--on-hue); }

/* ---------- Charts: two series are two colours ----------
   Repairs in and repairs out were blue and grey, which reads as one thing and its shadow. */
.chart-bar.second { fill: var(--c-teal); }
.chart-key .key-second { background: var(--c-teal); }

/* ---------- The front page ----------
   A flat panel, not a glow: the soft radial wash behind the picture was the one shape on this
   site that every generated landing page has. And the eight features in the colours the app
   gives the same things, in the order of featureIcons in Pages/Index.cshtml - testing, job
   cards, branches and roles, stock, WhatsApp, targets, language, printing. */
.lp-preview::before {
  background: var(--brand-soft);
  border: 1px solid color-mix(in srgb, var(--brand) 14%, transparent);
}
.lp-feature:nth-child(1) { --hue: var(--c-pink);   --hue-ink: var(--on-hue); }
.lp-feature:nth-child(3) { --hue: var(--c-violet); --hue-ink: var(--on-hue); }
.lp-feature:nth-child(4) { --hue: var(--c-orange); --hue-ink: var(--on-hue); }
.lp-feature:nth-child(5) { --hue: var(--c-teal);   --hue-ink: var(--on-hue); }
.lp-feature:nth-child(6) { --hue: var(--c-sky);    --hue-ink: var(--on-hue); }
.lp-feature:nth-child(7) { --hue: var(--c-slate);  --hue-ink: var(--on-hue); }

/* ============================================================================
   Home: the money first (2026-10-14)

   The owner: Home should open on the shop's revenue and its chart, with the gross profit where a
   customer can see it. So the page is now: the greeting with the two everyday buttons beside it,
   the period and branch chips, the MONEY PANEL (sales against target as the one big figure, the
   gross profit and its margin, the money taken, where the sales came from, and the month day by
   day), then the tiles for what needs doing and the repair cards.

   The chart's colours are its own four tokens, checked with the dataviz validator in both
   palettes (lightness band, chroma, colour-blind separation, contrast): repairs blue, product
   sales teal, other orange, and the gross-profit line violet. They are the same hues as the
   kinds of thing elsewhere, a step brighter where a chart needs it.
   ========================================================================= */

/* ---------- The two everyday buttons, beside the greeting ---------- */
.home-hello { align-items: center; }
.home-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-inline-start: auto; }
.home-actions .btn { min-height: var(--tap); gap: var(--s-2); padding-inline: var(--s-4); }
.home-actions .btn .ic { width: 20px; height: 20px; }
@media (max-width: 599px) {
  .home-actions { width: 100%; }
  .home-actions .btn { flex: 1 1 0; }
}

/* ---------- The chips: one row where there is room, two where there is not ---------- */
.home-filters { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); margin: 0 0 var(--s-3); }
.home-filters > .chips.strip { flex: 0 1 auto; min-width: 0; margin: 0; }

/* ---------- The money panel ---------- */
.card.money-panel { display: grid; gap: var(--s-4); margin: 0 0 var(--s-4); padding: var(--s-5); }
.money-panel { --hue: var(--c-teal); --hue-ink: var(--on-hue); }
.money-figs { display: flex; flex-wrap: wrap; gap: var(--s-4) var(--s-6); align-items: flex-start; }
.money-hero { flex: 2 1 320px; min-width: 0; display: grid; gap: var(--s-2); }
.money-hero h2 { display: flex; align-items: center; gap: var(--s-2); margin: 0; }
.money-hero .goal { max-width: 560px; }
/* THE ONE BIG FIGURE on the page, in the same face as everything else and proportional figures,
   because tabular ones look loose at this size. */
.hero-n { font-size: clamp(34px, 4.4vw, 52px); font-weight: 700; line-height: 1.05; letter-spacing: -.02em; }
.money-stat {
  flex: 1 1 180px; min-width: 0; display: grid; gap: 2px; align-content: start;
  padding-inline-start: var(--s-4); border-inline-start: 1px solid var(--line);
}
.money-stat .k { font-size: var(--fs-3); color: var(--muted); }
.money-stat > b { font-size: var(--fs-7); font-weight: 700; line-height: 1.15; }
.stat-sub { font-size: var(--fs-4); font-weight: 650; color: var(--text-2); }
@media (max-width: 599px) {
  .money-stat { padding-inline-start: 0; border-inline-start: none; padding-top: var(--s-3); border-top: 1px solid var(--line); }
  .card.money-panel { padding: var(--s-4); }
}

/* Where the sales came from: one bar in the chart's colours, the three figures under it. */
.money-split { display: grid; gap: var(--s-2); }
.split-bar { display: flex; gap: 2px; height: 10px; border-radius: 999px; overflow: hidden; background: var(--line); }
.split-bar > span { display: block; height: 100%; }
.split-bar > .s1, .sw.s1 { background: var(--chart-1); }
.split-bar > .s2, .sw.s2 { background: var(--chart-2); }
.split-bar > .s3, .sw.s3 { background: var(--chart-3); }
.money-split .sales-split { margin: 0; padding-top: 0; border-top: none; }
.money-split .sales-split dd { font-size: var(--fs-5); }

/* A key: a square for a column, a short stroke for a line, a tick for the target. */
.sw { display: inline-block; flex: 0 0 auto; width: 10px; height: 10px; border-radius: 3px; margin-inline-end: 6px; }
.sw.line { width: 16px; height: 3px; border-radius: 2px; background: var(--chart-4); }
.sw.tick { width: 14px; height: 2px; border-radius: 0; background: var(--text-2); }

/* ---------- The month, day by day ----------
   The marks stretch to the card, so every stroke is non-scaling and every word is HTML around
   the picture: the pounds up the side, the days along the bottom, the figures underneath. */
.rev-chart { margin: 0; display: grid; gap: var(--s-3); direction: ltr; border-radius: var(--r-2); }
.rev-chart:focus-visible { outline: none; box-shadow: var(--focus); }
.rev-plot { --rev-h: clamp(150px, 26vh, 240px); display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s-2); }
.rev-axis {
  position: relative; width: 46px; height: var(--rev-h);
  font-size: var(--fs-1); color: var(--muted); font-variant-numeric: tabular-nums;
}
.rev-axis span { position: absolute; inset-inline-end: 0; transform: translateY(50%); line-height: 1; white-space: nowrap; }
.rev-area { position: relative; min-width: 0; }
.rev-area svg { display: block; width: 100%; height: var(--rev-h); overflow: visible; }
.rev-grid { stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; }
.rev-grid.zero { stroke: var(--line-2); }
.rev-band { fill: transparent; }
.rev-band.closed { fill: var(--surface-2); }
.rev-band.on { fill: var(--brand-soft); }
.rev-seg { stroke: var(--surface); stroke-width: 2px; vector-effect: non-scaling-stroke; }
.rev-seg.s1 { fill: var(--chart-1); }
.rev-seg.s2 { fill: var(--chart-2); }
.rev-seg.s3 { fill: var(--chart-3); }
.rev-target { fill: none; stroke: var(--text-2); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.rev-profit {
  fill: none; stroke: var(--chart-4); stroke-width: 2.5; vector-effect: non-scaling-stroke;
  stroke-linejoin: round; stroke-linecap: round;
}
.rev-hit { fill: transparent; cursor: pointer; }
.rev-empty {
  position: absolute; inset-inline: 0; top: 0; height: var(--rev-h); margin: 0;
  display: grid; place-items: center; color: var(--muted); font-size: var(--fs-4); pointer-events: none;
}
.rev-key { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-4); font-size: var(--fs-3); color: var(--text-2); }
.rev-key span { display: inline-flex; align-items: center; }

/* The day's figures: today at rest, whichever day is pointed at or tapped after that. */
.rev-tips { display: grid; gap: var(--s-1); padding: var(--s-3) var(--s-4); background: var(--surface-2); border-radius: var(--r-2); }
.rev-tip { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-1) var(--s-5); }
.rev-tip[hidden] { display: none; }
.rev-tip-day { font-size: var(--fs-4); min-width: 110px; }
.rev-tip dl { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-5); margin: 0; }
.rev-tip dl > div { display: inline-flex; align-items: baseline; gap: var(--s-2); }
.rev-tip dt { display: inline-flex; align-items: center; font-size: var(--fs-3); color: var(--muted); }
.rev-tip dd { margin: 0; font-weight: 650; font-variant-numeric: tabular-nums; }
.rev-tip .lead-fig dd { font-size: var(--fs-5); }
.rev-hint { margin: 0; }

/* Repairs in and out, under the cards (or under the comparison). */
.dash-jobs-chart { margin-top: var(--s-3); }

/* The four tiles under the money panel sit four across where there is room: three and one left
   alone on a row of its own read as one forgotten. */
.money-panel ~ .tasks { grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr)); }

/* ============================================================================
   A returning customer (2026-10-14)

   Somebody the shop has served before is found, not written down again: by "been here before?"
   on the repair form, or from their own page, whose bar now books their next repair in or sells
   them something with them already on it. The banner is the people colour, because it says who.
   ========================================================================= */
.find-customer {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s-2);
  margin: 0 0 var(--s-3); padding-bottom: var(--s-3); border-bottom: 1px solid var(--line);
}
.find-customer .grow { flex: 1 1 240px; min-width: 0; margin: 0; display: grid; gap: 4px; }
.find-customer .btn { gap: var(--s-2); }
.find-none { margin: 0 0 var(--s-3); }
.found-people {
  list-style: none; margin: 0 0 var(--s-3); padding: 0; display: grid; gap: var(--s-2);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr));
}
.found-people a {
  display: flex; flex-direction: column; justify-content: center; gap: 2px; min-height: var(--tap);
  padding: var(--s-2) var(--s-3); border: 1px solid var(--line-2); border-radius: var(--r-2);
  background: var(--surface); color: var(--text);
}
.found-people a:hover { border-color: var(--brand); text-decoration: none; }
.found-people a .muted { font-size: var(--fs-3); }
.returning {
  display: flex; gap: var(--s-3); align-items: flex-start; margin: 0 0 var(--s-3);
  padding: var(--s-3) var(--s-4); border-radius: var(--r-2); background: var(--c-violet-soft);
}
.returning > .ic { flex: none; width: 22px; height: 22px; color: var(--c-violet); margin-top: 2px; }
.returning > div { display: grid; gap: 2px; min-width: 0; }
.returning-links { display: flex; flex-wrap: wrap; gap: 0 var(--s-4); }
.returning-links a { display: inline-flex; align-items: center; min-height: var(--tap-sm); font-weight: 600; }
