/* Mobile-first styles: cool off-white canvas, borderless shadow cards, an
   Industrial Blue primary, left sidebar on desktop, bottom nav on mobile.
   Light + dark via the `dark` class on <html> (a per-estate setting).

   The palette was a stock purple admin template, then deep forest green, and is
   Industrial Blue 175 (#648bb2 — hsl(210 34% 55%)) from 19/08/2026. Green was
   inherited wholesale from Storage Yard Manager, which is a different product
   for a different trade: a prospect who read the UnitLet marketing site (navy)
   and signed into a green app was looking at the yard's clothes.

   Every neutral is on the SAME hue, 210, rather than being grey: it is what
   makes the ground read as considered instead of default. The brand blue is
   kept dark where it carries text and left at full strength only where it is a
   large fill — #648bb2 holds white at just 3.57:1, so buttons use --btn-primary
   (6.01:1) and the swatch itself is reserved for the mark and the map lines.

   The status colours below are NOT branding and deliberately did not move.
   Green means paid, amber means due, red means late: that is a traffic light an
   operator reads without thinking, and repainting it to match a logo would cost
   more than it gained. */
:root {
  --bg: #f6f7f9;
  --card: #ffffff;
  --text: #1b2128;
  --heading: #131a20;
  --muted: #5d6974;
  --faint: #939ea9;
  --border: #dde3e9;
  --accent: #345779;
  --accent-hover: #274563;
  --accent-bg: #e9f0f7;
  --row-hover: #f5f7fa;
  --thead-bg: #eff2f6;
  --brand: #648bb2;
  --green: #15683d;
  --green-bg: #e3f3e9;
  --amber: #8a5a00;
  --amber-strong: #c47b12;
  --amber-bg: #fbeed8;
  --red: #b3261e;
  --red-bg: #fbe4e2;
  --cyan: #0d7d8c;
  --cyan-bg: #ddf1f4;
  --grey-bg: #edf0f2;
  --hatch-a: #edf0f2;
  --hatch-b: #d9dee4;
  /* Estate-map tile fills. Deliberately their own variables rather than the
     --*-bg pills: a tile is read across a whole estate at a glance and wants far
     more colour than a badge sitting inside a sentence.

     These stayed green on purpose. The tile ramp is a money signal — paid, due,
     late — not a brand surface, and it is the one place on the screen where
     colour IS the information. --tile-reserved and --tile-line are the
     exceptions: they mark chrome rather than money, so they carry the blue. */
  --tile-paid: #a9d9bd;
  --tile-due: #cfe9d9;
  --tile-overdue: #f7c3bd;
  --tile-vacant: #ffffff;
  --tile-vacant-edge: #b4bdc6;
  --tile-reserved: #dce8f2;
  --tile-line: #426b94;
  --tile-oos-a: #f6c58a;
  --tile-oos-b: #ffffff;
  --topbar-bg: rgba(255, 255, 255, 0.92);
  --shadow-sm: 0 2px 8px rgba(20, 32, 45, 0.08);
  --shadow-md: 0 4px 18px rgba(20, 32, 45, 0.12);
  --shadow-accent: 0 2px 6px rgba(52, 87, 121, 0.32);
}
/* Near-black rather than a blue-tinted dark: tinting the whole canvas blue
   makes the accent stop registering as an accent. Only the neutrals carry a
   faint blue bias, so the ground reads as considered rather than grey. */
html.dark {
  --bg: #0e1216;
  --card: #161c22;
  --text: #e1e6ea;
  --heading: #f2f5f7;
  --muted: #98a3ae;
  --faint: #687582;
  --border: #2b333b;
  --accent: #8badd0;
  --accent-hover: #acc7e2;
  --accent-bg: rgba(139, 173, 208, 0.18);
  --row-hover: rgba(255, 255, 255, 0.035);
  --thead-bg: #1d242a;
  --brand: #648bb2;
  --green: #4dd18c;
  --green-bg: rgba(45, 160, 100, 0.20);
  --amber: #f0b45e;
  --amber-strong: #f0b45e;
  --amber-bg: rgba(200, 130, 40, 0.20);
  --red: #f0908a;
  --red-bg: rgba(200, 60, 55, 0.22);
  --cyan: #5ecfdd;
  --cyan-bg: rgba(30, 160, 180, 0.18);
  --grey-bg: #23292f;
  --hatch-a: #23292f;
  --hatch-b: #2e353d;
  --tile-paid: rgba(45, 175, 108, 0.46);
  --tile-due: rgba(45, 175, 108, 0.24);
  --tile-overdue: rgba(214, 72, 64, 0.44);
  --tile-vacant: #f1f3f5;
  --tile-vacant-edge: rgba(0, 0, 0, 0.20);
  --tile-reserved: rgba(139, 173, 208, 0.16);
  --tile-line: #84a8cd;
  --tile-oos-a: #e2a463;
  --tile-oos-b: #f4f1ec;
  --topbar-bg: rgba(22, 28, 34, 0.94);
  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.40);
  --shadow-md: 0 4px 18px rgba(0, 0, 0, 0.50);
  --shadow-accent: 0 2px 6px rgba(52, 87, 121, 0.45);
}
/* Solid primary buttons use one blue in both modes: it carries white text at
   6.01:1 on the light canvas and still reads as a raised control on the dark
   one. Kept separate from --accent, which has to work as small link TEXT and so
   needs to be dark in light mode and light in dark mode.

   This is Industrial Blue darkened, NOT the swatch. #648bb2 holds white at only
   3.57:1 — fine for a logo, not for a button somebody has to read. */
:root, html.dark { --btn-primary: #3f668d; --btn-primary-hover: #315477; }
/* A field's background matches the card it sits on, so its BORDER is the only
   thing that draws the box. --border gives 9:1 against a white card but only
   1.38:1 against the dark one, which left dark-mode fields nearly invisible —
   hence a separate field border that is lifted in dark mode (3.26:1). */
/* Light mode needed the same treatment and never got it: the old #e6e6e8 drew
   a field box at 1.2:1 against a white card, so the only thing separating an
   input from the card was a line almost nobody could see. 3.1:1 clears WCAG's
   3:1 for a control boundary. It reads heavier than a typical field border --
   that is the price of the box being visible at all. */
:root { --field-border: #8794a1; }
html.dark { --field-border: #607080; --field-placeholder: #98a3ae; }

* { box-sizing: border-box; }
html { font-size: 16px; }
html.dark { color-scheme: dark; }
body {
  margin: 0;
  font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  -webkit-font-smoothing: antialiased;
  font-size: 0.9375rem;
  background: var(--bg);
  color: var(--text);
  padding-bottom: calc(4.5rem + env(safe-area-inset-bottom)); /* room for bottom nav */
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
h1 { font-size: 1.35rem; font-weight: 650; letter-spacing: -0.01em; color: var(--heading); margin: 1.1rem 0 0.8rem; }
h2 { font-size: 1.02rem; font-weight: 600; color: var(--heading); margin: 0 0 0.7rem; }
h3 { font-size: 0.92rem; font-weight: 600; color: var(--heading); margin: 1rem 0 0.45rem; }
code { background: var(--grey-bg); padding: 0.1em 0.35em; border-radius: 5px; font-size: 0.85em; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.container { max-width: 64rem; margin: 0 auto; padding: 0 1rem 2rem; }

/* Top bar — mobile only; desktop uses the sidebar */
.topbar {
  background: var(--topbar-bg);
  -webkit-backdrop-filter: saturate(1.4) blur(10px);
  backdrop-filter: saturate(1.4) blur(10px);
  box-shadow: var(--shadow-sm);
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.55rem 1rem;
  padding-top: calc(0.55rem + env(safe-area-inset-top));
  flex-wrap: wrap;
  position: sticky;
  top: 0;
  z-index: 20;
}
.brand {
  color: var(--heading); font-weight: 650; font-size: 1rem; letter-spacing: -0.01em;
  white-space: nowrap; display: flex; align-items: center; gap: 0.55rem;
}
.brand:hover { text-decoration: none; }
.brand-icon { width: 28px; height: 28px; border-radius: 8px; display: block; }
.logout-form { margin-left: auto; }
.linklike { background: none; border: none; color: var(--muted); cursor: pointer; font: inherit; font-size: 0.88rem; padding: 0.3rem; }
.linklike:hover { color: var(--text); }

/* Compact icon links in the mobile top bar (Reports / Maintenance / Settings /
   Billing — the pages the bottom tab bar doesn't cover) */
.topbar { gap: 0.6rem; }
.topnav-mobile { display: flex; gap: 0.1rem; margin-left: auto; align-items: center; }
.topnav-mobile a { position: relative; display: inline-flex; color: var(--muted); padding: 0.45rem; border-radius: 8px; }
.topnav-mobile a.active { color: var(--accent); background: var(--accent-bg); }
.topnav-mobile .badge { position: absolute; top: -1px; right: -3px; margin: 0; }
.topbar .logout-form { margin-left: 0.1rem; }
/* On narrow phones the wordmark would push Log out onto a second row —
   every page has its own heading, so the icon alone carries the brand. */
@media (max-width: 479px) { .topbar .brand span { display: none; } }

/* Sun/moon theme toggle (mobile top bar + desktop sidebar) */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; padding: 0; flex: none; cursor: pointer;
  background: var(--card); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
}
.theme-toggle svg { width: 18px; height: 18px; transition: transform 0.35s ease; }
.theme-toggle:hover svg { transform: rotate(35deg); }
.theme-toggle .moon { display: none; }
html.dark .theme-toggle .sun { display: none; }
html.dark .theme-toggle .moon { display: block; }
.sidebar-foot { display: flex; flex-direction: column; align-items: stretch; gap: 0.55rem; margin-top: 0.5rem; border-top: 1px solid var(--border); padding-top: 0.7rem; }
.sidebar-foot .foot-row { display: flex; align-items: center; gap: 0.5rem; }
/* Help sits with the theme toggle rather than in the menu: it is a thing you
   reach for at a moment of confusion, not a place you go, and every page already
   carries its own ? to the right bit of the manual. Same 38px box as the toggle
   so the footer reads as one row of small controls. */
.sidebar-foot .foot-row > a {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; flex: none;
  background: var(--card); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
}
.sidebar-foot .foot-row > a:hover { text-decoration: none; border-color: var(--accent); color: var(--accent); }
.sidebar-foot .foot-row > a.active { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); }
.sidebar-foot .foot-row > a svg { width: 18px; height: 18px; }
.sidebar-foot .sidebar-logout { margin: 0; border: 0; padding: 0; flex: 1; }

/* Which yard you're in, and who you are. One block, two placements: stacked
   above Log out in the desktop sidebar, and on its own wrapped row in the
   mobile top bar. It cannot share that bar's first row — below 480px there is
   so little width left that the wordmark is hidden outright, and a name would
   push Log out onto a second line anyway. min-width:0 lets the ellipsis work;
   without it a flex child refuses to shrink below its content. */
.who { display: flex; min-width: 0; }
.who-yard, .who-user { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.who-yard { font-weight: 620; color: var(--heading); }
.who-user { color: var(--muted); }
.sidebar-foot .who { flex-direction: column; padding: 0 0.6rem; line-height: 1.3; }
.sidebar-foot .who-yard { font-size: 0.92rem; }
.sidebar-foot .who-user { font-size: 0.82rem; }
.topbar .who { flex: 0 0 100%; align-items: baseline; gap: 0.4rem; font-size: 0.82rem; line-height: 1.2; }
.topbar .who-user::before { content: '·'; margin-right: 0.4rem; color: var(--faint); }

/* Sidebar (desktop) */
.sidebar {
  display: none;
  position: fixed; top: 0; left: 0; bottom: 0; width: 250px; z-index: 25;
  background: var(--card);
  box-shadow: var(--shadow-sm);
  flex-direction: column;
  padding: 1rem 0.85rem;
}
.sidebar .brand { padding: 0.25rem 0.6rem 1rem; font-size: 1.06rem; }
.sidenav { display: flex; flex-direction: column; gap: 0.15rem; flex: 1; overflow-y: auto; }
.sidenav a {
  display: flex; align-items: center; gap: 0.65rem;
  color: var(--muted); font-weight: 500; font-size: 0.92rem;
  padding: 0.55rem 0.75rem; border-radius: 8px; white-space: nowrap;
  transition: background 0.13s ease, color 0.13s ease;
}
.sidenav a svg { flex-shrink: 0; opacity: 0.9; }
.sidenav a:hover { color: var(--heading); text-decoration: none; background: var(--grey-bg); }
.sidenav a.active {
  color: #fff;
  /* Both ends carry white text: #3f668d is 6.0:1 and #4e7aa6 is 4.5:1, which
     is the lightest this hue goes before white stops being readable on it. */
  background: linear-gradient(72deg, #3f668d, #4e7aa6);
  box-shadow: var(--shadow-accent);
}
.sidenav a .badge { margin-left: auto; }
.sidebar-logout { margin-top: 0.5rem; border-top: 1px solid var(--border); padding-top: 0.7rem; }
.sidebar-logout .linklike {
  display: flex; align-items: center; gap: 0.65rem; width: 100%;
  padding: 0.55rem 0.75rem; border-radius: 8px; font-size: 0.92rem; font-weight: 500;
}
.sidebar-logout .linklike:hover { background: var(--grey-bg); }

/* Bottom nav (mobile) */
.bottomnav {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 20;
  display: flex;
  background: var(--topbar-bg);
  -webkit-backdrop-filter: saturate(1.4) blur(10px);
  backdrop-filter: saturate(1.4) blur(10px);
  box-shadow: 0 -2px 8px rgba(47, 43, 61, 0.08);
  padding-bottom: env(safe-area-inset-bottom);
}
.bottomnav a {
  flex: 1; text-align: center; padding: 0.5rem 0 0.45rem;
  color: var(--muted); font-size: 0.66rem; font-weight: 500; line-height: 1.4;
}
.bottomnav a:hover { text-decoration: none; }
.bottomnav a.active { color: var(--accent); }
.bottomnav .icon { display: block; position: relative; height: 24px; }
.bottomnav .icon svg { display: inline-block; vertical-align: top; margin-top: 1px; }
.bottomnav .icon .badge { position: absolute; top: -0.25rem; margin-left: 0.05rem; }

@media (min-width: 800px) {
  body { padding-bottom: 2rem; }
  .bottomnav { display: none; }
  .topbar { display: none; }
  .sidebar { display: flex; }
  .container { margin-left: 250px; max-width: 84rem; padding: 0.5rem 2rem 2.5rem; }
  h1 { font-size: 1.55rem; }
}
/* Above 1400 the container stops hugging the sidebar and centres instead, with
   the sidebar's width reserved as padding. That padding comes out of max-width,
   so the cap has to include it — otherwise the sidebar is subtracted from the
   content and the app gets NARROWER on a bigger screen: 1280px gave 966px of
   content, 1440px and every size above it gave 838px, a 2560px monitor
   included. The cap is expressed as content + chrome for that reason. */
@media (min-width: 1400px) {
  .container {
    margin-left: auto;
    padding-left: calc(250px + 2rem);
    max-width: calc(84rem + 250px + 2rem);
  }
}

/* Cards, stats */
.card {
  background: var(--card);
  border: none;
  border-radius: 12px;
  padding: 1.2rem 1.25rem;
  margin: 1.1rem 0;
  box-shadow: var(--shadow-sm);
}
@media (min-width: 800px) { .card { padding: 1.4rem 1.5rem; } }
.stat-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.8rem;
  margin: 1.1rem 0;
}
@media (min-width: 800px) { .stat-grid { grid-template-columns: repeat(3, 1fr); } }
.stat {
  background: var(--card);
  border: none;
  border-radius: 12px;
  padding: 1rem 1.05rem;
  color: var(--text);
  display: block;
  box-shadow: var(--shadow-sm);
  transition: box-shadow 0.13s ease, transform 0.13s ease;
}
.stat:hover { text-decoration: none; box-shadow: var(--shadow-md); transform: translateY(-2px); }
.stat .num { font-size: 1.6rem; font-weight: 650; letter-spacing: -0.01em; line-height: 1.25; color: var(--heading); }
.stat .num .sub { font-size: 1rem; color: var(--faint); font-weight: 500; letter-spacing: 0; }
.stat .label { color: var(--muted); font-size: 0.82rem; margin-top: 0.1rem; }
.stat.alert-stat .num { color: var(--red); }
.stat.alert-stat { box-shadow: var(--shadow-sm), inset 0 -3px 0 var(--red); }

/* Stat icon chips (Vuexy-style pastel squares), left of the figures.
   Only stats that have a chip flex into a row — report stats stay stacked. */
.stat:has(.stat-icon) { display: flex; align-items: center; gap: 0.85rem; }
.stat-body { min-width: 0; }
.stat-icon {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; width: 42px; height: 42px; border-radius: 10px;
}
.stat-icon.purple { background: var(--accent-bg); color: var(--accent); }
.stat-icon.green { background: var(--green-bg); color: var(--green); }
.stat-icon.amber { background: var(--amber-bg); color: var(--amber-strong); }
.stat-icon.red { background: var(--red-bg); color: var(--red); }
.stat-icon.cyan { background: var(--cyan-bg); color: var(--cyan); }
.stat-icon.grey { background: var(--grey-bg); color: var(--muted); }

/* Buttons */
.btn {
  display: inline-block;
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  padding: 0.5rem 1rem;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 500;
  cursor: pointer;
  text-align: center;
  min-height: 44px; /* touch target */
  line-height: 1.8;
  transition: background 0.13s ease, border-color 0.13s ease, box-shadow 0.13s ease, transform 0.13s ease;
}
.btn:hover { text-decoration: none; background: var(--grey-bg); border-color: var(--faint); }
.btn.primary {
  background: var(--btn-primary); border-color: var(--btn-primary); color: #fff;
  box-shadow: var(--shadow-accent); font-weight: 550;
}
.btn.primary:hover { background: var(--btn-primary-hover); border-color: var(--btn-primary-hover); transform: translateY(-1px); }
.btn.danger-outline { border-color: color-mix(in srgb, var(--red) 55%, transparent); color: var(--red); background: transparent; }
.btn.danger-outline:hover { background: var(--red-bg); border-color: var(--red); }
.btn.small { padding: 0.28rem 0.7rem; min-height: 36px; font-size: 0.82rem; line-height: 1.7; }
.btn.big { padding: 0.65rem 1.4rem; font-size: 1rem; }
.btn.block { display: block; width: 100%; }
.btn-row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.7rem 0 0.2rem; align-items: center; }
.btn-row form { margin: 0; }
.quick-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 1.1rem 0; }
.btn.ghost { background: transparent; border-color: transparent; color: var(--muted); box-shadow: none; }
.btn.ghost:hover { background: var(--grey-bg); color: var(--text); }

/* Empty-yard dashboard prompt */
.setup-hero { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; border-left: 4px solid var(--accent); }
.setup-hero h2 { margin: 0 0 0.25rem; }
.setup-hero p { margin: 0; max-width: 42ch; }

/* Onboarding wizard */
.wiz { max-width: 640px; margin: 0 auto; }
.wiz-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; flex-wrap: wrap; }
.wiz-skip { margin: 0; }
.wiz-steps { display: flex; gap: 0.4rem; list-style: none; padding: 0; margin: 1rem 0 1.3rem; counter-reset: none; }
.wiz-steps li { flex: 1; }
.wiz-steps li a { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.6rem; border-radius: 10px; color: var(--muted); border: 1px solid var(--border); background: var(--card); font-size: 0.84rem; font-weight: 500; }
.wiz-steps li a:hover { border-color: var(--faint); }
.wiz-num { display: inline-flex; align-items: center; justify-content: center; width: 1.55rem; height: 1.55rem; flex-shrink: 0; border-radius: 999px; background: var(--grey-bg); color: var(--muted); font-weight: 700; font-size: 0.82rem; }
.wiz-steps li.current a { border-color: var(--accent); color: var(--text); background: var(--accent-bg); }
.wiz-steps li.current .wiz-num { background: var(--btn-primary); color: #fff; }
.wiz-steps li.done .wiz-num { background: var(--green, #28c76f); color: #fff; }
.wiz-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wiz-card h2 { margin-top: 0; }
.wiz-card h3 { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 1.2rem 0 0.5rem; }
.wiz-card .opt { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--muted); }
.wiz-narrow { max-width: 22rem; }
.wiz-actions { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-top: 1.1rem; }
.wiz-actions form { margin: 0; }
.wiz-done { text-align: center; }
.wiz-check { width: 3.2rem; height: 3.2rem; margin: 0.2rem auto 0.6rem; border-radius: 999px; background: var(--green-bg); color: var(--green); display: flex; align-items: center; justify-content: center; font-size: 1.6rem; font-weight: 700; }
.wiz-summary { display: flex; gap: 0.6rem; justify-content: center; list-style: none; padding: 0; margin: 1rem 0; flex-wrap: wrap; }
.wiz-summary li { background: var(--grey-bg); border-radius: 10px; padding: 0.7rem 1.1rem; color: var(--muted); font-size: 0.82rem; }
.wiz-summary strong { display: block; font-size: 1.3rem; color: var(--text); }
.wiz-done .wiz-actions { justify-content: center; }
.wiz-assigned { background: var(--green-bg); color: var(--green); border-radius: 8px; padding: 0.6rem 0.85rem; font-size: 0.85rem; font-weight: 500; margin: 0 0 1rem; }

/* Public enquiry page */
.public-page { max-width: 560px; margin: 1.5rem auto; }
.enquiry-head { text-align: center; margin-bottom: 0.4rem; }
.enquiry-head h1 { margin-bottom: 0.2rem; }
.enquiry-intro { text-align: center; color: var(--text); margin: 0 0 1rem; }
.enquiry-vac h2 { margin-top: 0; }
.vac-list { list-style: none; padding: 0; margin: 0.6rem 0 0; display: grid; gap: 0.45rem; }
.vac-list li { display: flex; align-items: center; gap: 0.6rem; }
.vac-count { display: inline-flex; align-items: center; justify-content: center; min-width: 2rem; height: 2rem; border-radius: 8px; background: var(--accent-bg); color: var(--accent); font-weight: 700; }
.enquiry-done { text-align: center; }

/* Settings page groups + jump nav */
.settings-nav { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.9rem 0 0.2rem; position: sticky; top: 0; z-index: 5; background: var(--bg); padding: 0.5rem 0; }
.settings-nav a { font-size: 0.82rem; font-weight: 500; padding: 0.32rem 0.75rem; border-radius: 999px; background: var(--card); border: 1px solid var(--border); color: var(--muted); }
.settings-nav a:hover { color: var(--text); border-color: var(--faint); text-decoration: none; }
h2.settings-group { margin: 1.8rem 0 0.7rem; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); font-weight: 700; scroll-margin-top: 3.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.4rem; }

/* Customer portal */
.text-amber { color: var(--amber); font-weight: 600; }
.portal .portal-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 1.1rem; padding-bottom: 0.8rem; border-bottom: 1px solid var(--border); }
.portal-brand { font-weight: 650; font-size: 1.05rem; }
.portal-logout { margin: 0; }
.portal h1 { margin: 0 0 0.9rem; }
.portal .card { margin-bottom: 1rem; }
.portal-note { text-align: center; }
.portal-note .wiz-check { margin-inline: auto; }
.portal-dd.ok { border-left: 4px solid var(--green, #28c76f); }
.portal-dd { border-left: 4px solid var(--accent); }
.portal-vehicle { padding: 0.7rem 0; border-top: 1px solid var(--border); }
.portal-vehicle:first-of-type { border-top: none; padding-top: 0.3rem; }
.portal-vehicle details { margin-top: 0.5rem; }
.portal-vehicle summary { cursor: pointer; color: var(--accent); font-size: 0.88rem; font-weight: 500; }
.portal-vehicle details[open] summary { margin-bottom: 0.6rem; }
.portal-vehicle .form { margin-top: 0.4rem; }
@media (max-width: 560px) {
  .wiz-label { display: none; }
  .wiz-steps li a { justify-content: center; }
}

/* Badges & pills */
.badge {
  display: inline-block; min-width: 1.25rem; padding: 0.05rem 0.34rem;
  border-radius: 999px; font-size: 0.68rem; font-weight: 700;
  text-align: center; color: #fff; margin-left: 0.35rem; line-height: 1.5;
}
.badge.red { background: #ea5455; }
.badge.amber { background: #ff9f43; }
/* "There's something new" beside Help. A count would be a lie — it isn't a
   number of anything — so this is a plain dot, sized to read at a glance without
   competing with the overdue badge, which is the one that costs money. */
.badge.dot {
  min-width: 0; width: 0.5rem; height: 0.5rem; padding: 0;
  font-size: 0; line-height: 0; background: #ff9f43;
}
.pill {
  display: inline-block; padding: 0.16rem 0.62rem; border-radius: 6px;
  font-size: 0.74rem; font-weight: 600; white-space: nowrap; letter-spacing: 0.01em;
}
.pill.red, .pill.overdue { background: var(--red-bg); color: var(--red); }
.pill.amber, .pill.due_soon { background: var(--amber-bg); color: var(--amber); }
.pill.green, .pill.paid { background: var(--green-bg); color: var(--green); }
.pill.vacant { background: var(--grey-bg); color: var(--muted); }
.pill.reserved { background: var(--accent-bg); color: var(--accent); }
.pill.out_of_service { background: repeating-linear-gradient(45deg, var(--hatch-a), var(--hatch-a) 4px, var(--hatch-b) 4px, var(--hatch-b) 8px); color: var(--muted); }

/* Forms */
.form label { display: block; margin: 0 0 0.95rem; font-size: 0.84rem; color: var(--muted); font-weight: 500; }
.form input, .form select, .form textarea,
.inline-form input, .inline-form select, .search-row input, .filter-form input, .filter-form select {
  display: block; width: 100%;
  margin-top: 0.3rem;
  padding: 0.55rem 0.8rem;
  font: inherit;
  border: 1px solid var(--field-border);
  border-radius: 8px;
  background: var(--card);
  color: var(--text);
  min-height: 44px;
  transition: border-color 0.13s ease, box-shadow 0.13s ease;
}
.form input:focus, .form select:focus, .form textarea:focus,
.inline-form input:focus, .inline-form select:focus, .search-row input:focus,
.filter-form input:focus, .filter-form select:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg);
}
/* Dark only: the browser's default placeholder grey lands at 2.7:1 on the dark
   field. Light mode's default already passes, so it is left alone. */
html.dark input::placeholder, html.dark textarea::placeholder { color: var(--field-placeholder); opacity: 1; }
.form textarea { min-height: auto; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.7rem; }
.field-group { border: 1px solid var(--border); border-radius: 10px; padding: 0.9rem 1rem 0.2rem; margin: 0 0 0.95rem; }
.field-group legend { padding: 0 0.4rem; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.field-group .muted { margin-bottom: 0.8rem; }
.check-row { display: flex; gap: 1.2rem; margin-bottom: 0.9rem; }
label.check { display: flex; align-items: center; gap: 0.5rem; font-weight: 450; color: var(--text); margin-bottom: 0.9rem; }
label.check input { width: 1.25rem; height: 1.25rem; min-height: auto; margin: 0; accent-color: var(--btn-primary); }
.mode-row { display: flex; gap: 1.4rem; margin-bottom: 0.9rem; }
.mode-row label.check { margin-bottom: 0; }
.inline-form { display: flex; gap: 0.45rem; align-items: center; flex-wrap: wrap; margin: 0.45rem 0; }
.inline-form input, .inline-form select { display: inline-block; width: auto; flex: 1; min-width: 8rem; margin-top: 0; }
.inline-form .btn { flex-shrink: 0; }
/* The move form is a lone dropdown, so left to itself it would stretch the whole
   width of a desktop card and read as far bigger a decision than it is. */
.move-form .inline-form { max-width: 34rem; }
/* One row per occupant: tick, date, Save. The tick and the date read as a single
   statement, so they sit on one line and the date does not stretch. */
.leaving-block { margin-top: 0.9rem; padding-top: 0.7rem; border-top: 1px solid var(--border); }
.leaving-form { margin: 0 0 0.35rem; }
.leaving-form label.check { margin-bottom: 0; flex-shrink: 0; }
/* .inline-form stretches every input to fill the row, which is right for a
   dropdown and wrong for a tick box: Chrome centres the glyph inside the
   oversized control, so the tick drifts away from its own label. */
.leaving-form label.check input { flex: none; width: 1.25rem; min-width: 0; }
.leaving-form input[type="date"] { flex: 0 0 auto; width: auto; min-width: 0; }
.leaving-who { font-weight: 600; color: var(--heading); flex-shrink: 0; }
.leaving-block .muted { margin: 0.5rem 0 0; font-size: 0.82rem; }
.search-row { display: flex; gap: 0.5rem; margin: 0.9rem 0 0.5rem; }
.search-row input { margin: 0; flex: 1; }
.filter-form { display: flex; gap: 0.7rem; flex-wrap: wrap; align-items: end; }
.filter-form label { font-size: 0.82rem; font-weight: 500; color: var(--muted); flex: 1; min-width: 8rem; }
.hidden { display: none; }

/* Filter chips */
/* The yard map's status strip. Four facts, four links, one line — kept
   deliberately quiet (text weight, no boxes) because it sits above the map and
   the map is what you came to look at. A count of zero is dimmed rather than
   hidden: "0 overdue" is worth seeing, and a strip that changes shape as the
   numbers change is harder to read at a glance than one that never moves. */
.estate-strip {
  display: flex; flex-wrap: wrap; gap: 0.25rem 1.1rem;
  margin: 0.15rem 0 0.1rem; font-size: 0.88rem;
}
.estate-strip a { color: var(--muted); }
.estate-strip a strong { color: var(--heading); font-weight: 650; }
.estate-strip a:hover { text-decoration: none; color: var(--accent); }
.estate-strip a:hover strong { color: var(--accent); }
.estate-strip a.none, .estate-strip a.none strong { color: var(--faint); font-weight: 550; }

/* Group tabs — which VIEW of a place you are looking at (Map / List /
   Maintenance). Deliberately a different shape from .chip below: chips say what
   is being filtered, tabs say where you are, and two identical-looking rows on
   one screen is how people stop reading either of them. Underline rather than
   pill, and above the heading rather than under it, so the heading still reads as
   the name of what you are looking at. */
.tabs {
  display: flex; gap: 0.2rem; flex-wrap: wrap; align-items: flex-end;
  margin: 0 0 1rem; border-bottom: 1px solid var(--border);
}
.tabs a {
  padding: 0.5rem 0.85rem; color: var(--muted); font-size: 0.9rem; font-weight: 550;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tabs a:hover { text-decoration: none; color: var(--heading); }
.tabs a.on { color: var(--accent); border-bottom-color: var(--accent); }
.tabs .tab-count {
  display: inline-block; margin-left: 0.15rem; padding: 0 0.34rem;
  border-radius: 8px; background: var(--grey-bg); color: var(--muted);
  font-size: 0.75rem; font-weight: 600;
}
.tabs a.on .tab-count { background: var(--accent-bg); color: var(--accent); }

.filter-row { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 0.8rem 0; }
.chip {
  padding: 0.34rem 0.9rem; border-radius: 6px; border: none;
  background: var(--grey-bg); color: var(--muted); font-size: 0.83rem; font-weight: 550;
  transition: background 0.13s ease, color 0.13s ease;
}
.chip:hover { text-decoration: none; color: var(--heading); background: var(--border); }
.chip.on { background: var(--accent); color: #fff; box-shadow: var(--shadow-accent); }
/* Below the desktop breakpoint every chip and the log-out button grow to a 38px
   box. Measured at 28px and 29px, which is tappable at a desk and not tappable
   one-handed in a yard in January — and the filter chips are the most-tapped
   thing on the busiest screens. The text stays the same size; only the hit area
   changes, so nothing reflows. */
@media (max-width: 799px) {
  .chip, .linklike {
    display: inline-flex; align-items: center; min-height: 38px;
  }
  .chip { padding-top: 0; padding-bottom: 0; }
  /* Same for a tick box and its words. The box itself stays 20px — that is what a
     checkbox looks like — but the label around it is what you actually tap, and it
     was the height of one line of text. Settings alone has nineteen of them. */
  label.check { min-height: 38px; }
  /* Except inside a dense inline form or a table row, where a 38px box would
     stretch the row it sits in. The light/dark pair is a row of its own, so it
     gets the bigger target like everything else. */
  .leaving-form label.check, .review-table label.check { min-height: 0; }
  /* The bulk-select boxes in a table have no label to tap — they carry an
     aria-label instead — so the box itself is the target, and 18px of it is not
     enough on a phone. 24px is the WCAG 2.5.8 minimum, and the same size is given
     to the tick boxes in the price-review table, which are in a row too tight to
     grow. */
  .check-col input[type="checkbox"], label.check input { width: 24px; height: 24px; }
  /* A bare dropdown is 19px tall left to itself. Every other control on a phone
     is at least 38px; this makes the odd one out match. */
  select { min-height: 38px; }
}

/* Admin — password-reset link panel */
.reset-panel { border-left: 3px solid var(--accent); }
.reset-panel h2 { margin-top: 0; }
.reset-link-row { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.6rem; }
.reset-link-row input {
  flex: 1; min-width: 0; padding: 0.55rem 0.8rem; font: inherit; min-height: 44px;
  border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text);
}
.reset-link-row .btn { flex-shrink: 0; }

/* Admin — signups trend + sparkline */
.trend-card { margin-top: 0.9rem; }
.trend-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.trend-head h2 { margin: 0; }
.trend-figures { display: flex; gap: 1.1rem; flex-wrap: wrap; color: var(--muted); font-size: 0.82rem; }
.trend-figures .tnum { font-weight: 650; font-size: 1rem; color: var(--heading); margin-right: 0.15rem; }
.sparkline { display: flex; align-items: flex-end; gap: 4px; height: 52px; margin: 0.9rem 0 0.3rem; }
.spark-bar { flex: 1; background: var(--accent-bg); border-radius: 3px 3px 0 0; min-height: 3px; }
.spark-caption { font-size: 0.8rem; margin: 0; }
.pill.grey { background: var(--grey-bg); color: var(--muted); }

/* Bulk select/delete on the spaces list */
.check-col { width: 2.4rem; text-align: center; }
.check-col input { width: 1.15rem; height: 1.15rem; min-height: auto; margin: 0; accent-color: var(--btn-primary); cursor: pointer; }
.bulk-bar { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; margin: 0.8rem 0; }
.bulk-bar .muted { font-size: 0.85rem; }

/* Tables */
.table-card { padding: 0; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
th, td { text-align: left; padding: 0.66rem 1rem; border-bottom: 1px solid var(--border); }
th { background: var(--thead-bg); color: var(--muted); font-size: 0.71rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; border-bottom: none; }
thead tr th:first-child { border-top-left-radius: 12px; }
thead tr th:last-child { border-top-right-radius: 12px; }
tbody tr { transition: background 0.1s ease; }
tbody tr:hover { background: var(--row-hover); }
tbody tr:last-child td { border-bottom: none; }
.num-col { text-align: right; font-variant-numeric: tabular-nums; }

/* Lists */
.rows { list-style: none; margin: 0; padding: 0; }
.rows li {
  display: flex; justify-content: space-between; align-items: center; gap: 0.6rem;
  padding: 0.62rem 0; border-bottom: 1px solid var(--border); flex-wrap: wrap;
}
.rows li:last-child { border-bottom: none; padding-bottom: 0.1rem; }
.rows .right { margin-left: auto; text-align: right; }
.move-list .btn-row { margin: 0; justify-content: flex-end; }

/* Vehicle-movement direction pill + customer-list reg tag */
.move-pill {
  display: inline-block; font-size: 0.72rem; font-weight: 650; padding: 0.08rem 0.44rem;
  border-radius: 999px; vertical-align: middle; margin-right: 0.15rem;
}
.move-pill.out { background: var(--amber-bg); color: var(--amber); }
.move-pill.in { background: var(--green-bg); color: var(--green); }
.reg-tag { font-size: 0.78rem; letter-spacing: 0.02em; text-transform: uppercase; }
.row-actions { display: flex; gap: 0.4rem; justify-content: flex-end; flex-wrap: wrap; }
.row-actions form { margin: 0; }
/* A table card scrolls sideways when it will not fit, and the column you must
   never lose to that is the one with the buttons — so it stays pinned to the
   right edge while the rest slides under it. The hover tint is translucent, so
   it is layered OVER an opaque card colour rather than replacing it; otherwise
   the scrolled-under row would show through the pinned cell. */
.table-card td.row-actions {
  position: sticky; right: 0; z-index: 1;
  background: var(--card);
  box-shadow: -10px 0 10px -10px rgba(0, 0, 0, 0.3);
}
.table-card tbody tr:hover td.row-actions {
  background: linear-gradient(var(--row-hover), var(--row-hover)), var(--card);
}
/* Waiting list: pick the plot and convert in one action, so the select has to
   sit against its own button rather than stretching the actions cell. */
.convert-form { display: flex; gap: 0.35rem; align-items: center; }
.convert-form select {
  font: inherit; font-size: 0.85rem; padding: 0.3rem 0.4rem; max-width: 11rem;
  border: 1px solid var(--field-border); border-radius: 7px;
  background: var(--card); color: var(--text);
}
/* "Coming free" splits into empty-now and given-notice; the sub-headings sit
   inside a card, so they need less air above than a standalone h3. */
.card h3:first-of-type { margin-top: 0.2rem; }

/* Direct Debit mandate status pill */
.dd-pill {
  display: inline-block; font-size: 0.74rem; font-weight: 650; padding: 0.12rem 0.55rem;
  border-radius: 999px; vertical-align: middle;
}
.dd-pill.in { background: var(--green-bg); color: var(--green); }
.dd-pill.pending { background: var(--amber-bg); color: var(--amber); }
.dd-pill.out { background: var(--red-bg); color: var(--red); }
.dd-pill.off { background: var(--grey-bg); color: var(--muted); }

/* Key-value blocks */
.kv div { display: flex; gap: 0.8rem; padding: 0.32rem 0; }
.kv .k { color: var(--muted); min-width: 6.5rem; font-size: 0.82rem; padding-top: 0.12rem; }
.kv .total { border-top: 1px solid var(--border); margin-top: 0.4rem; padding-top: 0.6rem; }

/* Yard map */
.legend { display: flex; flex-wrap: wrap; gap: 0.9rem; font-size: 0.78rem; color: var(--muted); margin: 0.8rem 0; }
.legend .sq { display: inline-block; width: 0.85rem; height: 0.85rem; border-radius: 4px; vertical-align: -2px; margin-right: 0.3rem; border: 1px solid rgba(128, 128, 128, 0.2); }
.zone h2 { margin: 1.4rem 0 0.7rem; }
.unit-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(4.6rem, 1fr));
  gap: 0.55rem;
}
.unit-tile {
  /* A positioning context for the "leaving soon" dot. Map-canvas tiles override
     this with `absolute` further down, which is also a context — so the dot
     lands correctly whether the tile is on the grid or free-positioned. */
  position: relative;
  border-radius: 10px;
  padding: 0.5rem 0.3rem;
  text-align: center;
  color: var(--text);
  border: 1px solid rgba(128, 128, 128, 0.16);
  min-height: 3.7rem;
  display: flex; flex-direction: column; justify-content: center;
  transition: box-shadow 0.13s ease, transform 0.13s ease;
}
.unit-tile:hover {
  text-decoration: none; transform: translateY(-2px);
  box-shadow: 0 0 0 2px var(--accent), var(--shadow-md);
}
.unit-tile .tile-label { font-weight: 650; letter-spacing: -0.01em; color: var(--heading); }
.unit-tile .tile-sub { font-size: 0.67rem; opacity: 0.75; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Rent status is carried by the ring as well as the fill: no ring when paid, a
   thin one when due soon, a thick one when overdue.
   Colour alone did not survive contact with real eyes. Simulating deuteranopia
   (the common red-green deficiency, ~8% of men — and this audience is mostly
   men in their 50s and 60s) against these exact tints collapses paid vs overdue
   from a ΔE of 18.6 to 1.5. Below about 2.3 two colours are indistinguishable,
   so on the one screen whose whole job is "which plots need chasing", roughly
   one operator in twelve was reading noise. `reserved` and `out_of_service`
   already leaned on a dashed border and a hatch rather than hue; these three
   now follow suit.
   The rings are drawn with `outline`, not an inset box-shadow: .unit-tile:hover
   replaces box-shadow outright, which would make a ring vanish exactly when
   someone is pointing at the tile. */
.unit-tile.paid, .sq.paid { background: var(--tile-paid); }
.unit-tile.due_soon, .sq.due_soon { background: var(--tile-due); outline: 2px solid var(--amber); outline-offset: -2px; }
.unit-tile.overdue, .sq.overdue { background: var(--tile-overdue); outline: 3px solid var(--red); outline-offset: -3px; }
.unit-tile.vacant, .sq.vacant { background: var(--tile-vacant); border-color: var(--tile-vacant-edge); }
.unit-tile.reserved, .sq.reserved { background: var(--tile-reserved); border-style: dashed; border-color: var(--tile-line); }
.unit-tile.out_of_service, .sq.out_of_service {
  background: repeating-linear-gradient(45deg, var(--tile-oos-a), var(--tile-oos-a) 5px, var(--tile-oos-b) 5px, var(--tile-oos-b) 10px);
}
/* Vacant and out-of-service are light fills in BOTH themes, so their ink is set
   once here rather than per theme — the dark theme's own text colour would be
   unreadable on them. */
/* "Leaving soon" — a plot whose customer has confirmed a date. It is not a
   seventh status: a leaver is still paid, due soon or overdue, so this rides on
   top of whatever fill the tile already has. Hence a MARK, not a colour — every
   other channel (fill, ring, border-style, hatch) is taken, and the presence of
   the dot is the cue, so it survives colour blindness with nothing to tune.
   Leaving room for a second dot later (maintenance due, insurance expiring) is
   why this is a corner dot and not a folded corner. */
.unit-tile.leaving::after, .sq.leaving::after {
  content: ''; position: absolute; border-radius: 50%; background: var(--heading);
}
.unit-tile.leaving::after { top: 6px; right: 6px; width: 9px; height: 9px; }
.legend .sq.leaving { position: relative; }
.legend .sq.leaving::after { top: 2px; right: 2px; width: 5px; height: 5px; }
.unit-tile.vacant, .unit-tile.out_of_service { color: #464d54; }
.unit-tile.vacant .tile-label, .unit-tile.out_of_service .tile-label { color: #1f2529; }

/* Yard map canvas — free-positioned tiles on a snap grid (cell 94x68, tile 86x60;
   keep in sync with MAP_CW / MAP_CH in ui.js) */
.map-scroll { overflow-x: auto; padding-bottom: 0.3rem; }
/* width: max-content keeps wide maps scrollable before (or without) the zoom
   script, which overrides with an explicit scaled size. */
.map-zoom { overflow: hidden; width: max-content; }
.map-canvas { position: relative; border: 1.5px dashed var(--border); border-radius: 12px; transform-origin: 0 0; }
.map-canvas .unit-tile {
  position: absolute; width: 86px; height: 60px; min-height: 0;
  padding: 0.3rem 0.25rem; margin: 0; box-sizing: border-box;
}
.map-canvas .unit-tile:hover { transform: none; }
.map-canvas.editing {
  background-image: radial-gradient(circle, var(--border) 1.5px, transparent 1.5px);
  background-size: 94px 68px; background-position: 43px 30px;
  touch-action: none;
}
.map-canvas.editing .unit-tile { cursor: grab; user-select: none; -webkit-user-select: none; touch-action: none; }
.unit-tile.dragging { cursor: grabbing; z-index: 6; opacity: 0.92; box-shadow: 0 0 0 2px var(--accent), var(--shadow-md); }
.map-editbar { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin: 0.5rem 0 0.2rem; }
/* Move mode. The bar sticks to the top because on a big yard the tile you are
   aiming at is often scrolled well away from where you started. */
.map-movebar {
  position: sticky; top: 0; z-index: 8;
  display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
  margin: 0.6rem 0 0.2rem; padding: 0.6rem 0.9rem;
  border-radius: 8px; font-size: 0.9rem; font-weight: 500;
  background: var(--card); color: var(--text); box-shadow: var(--shadow-sm);
}
.map-movebar.picking { background: var(--accent-bg); color: var(--accent); }
.map-movebar.warn { background: var(--amber-bg); color: var(--amber); }
.unit-tile.movable { cursor: pointer; }
/* The picked tile keeps its status colour — you still need to read who is overdue
   while choosing where to put someone, so the other tiles are not dimmed. The
   marker is shape, not hue: overdue rings sit *inside* the tile edge, this one
   sits outside it with a visible gap, and the tile lifts off the page. That
   survives the red-green deficiency the status colours were already tuned for. */
.unit-tile.picked, .map-canvas .unit-tile.picked {
  outline: 3px solid var(--accent); outline-offset: 2px;
  box-shadow: var(--shadow-md); transform: translateY(-2px); z-index: 7;
}
.map-unplaced { margin: 0.8rem 0 0.4rem; }
.zone .section-head { margin: 1.4rem 0 0.7rem; }
.zone .section-head h2 { margin: 0; }
.block-tools { display: inline-flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.block-tools form { margin: 0; }
.grid-ctl { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.85rem; color: var(--muted); white-space: nowrap; }
.grid-ctl input { width: 2.8rem; text-align: center; font-variant-numeric: tabular-nums; }
.grid-ctl .grid-x { color: var(--muted); }
.zoom-ctl { display: inline-flex; align-items: center; gap: 0.35rem; }
.zoom-ctl .zoom-pct {
  font-size: 0.85rem; color: var(--muted); min-width: 3.2rem; text-align: center;
  font-variant-numeric: tabular-nums;
}
.bulk-zone { display: inline-flex; align-items: center; gap: 0.4rem; }

/* Page furniture */
.page-head { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.6rem; }
.page-head > div { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.section-head { display: flex; justify-content: space-between; align-items: center; }
.muted { color: var(--muted); font-size: 0.85rem; }
.text-red { color: var(--red); font-weight: 600; }
.alert { border-radius: 8px; border: none; padding: 0.75rem 1rem; margin: 0.9rem 0; font-size: 0.9rem; font-weight: 500; }
.page-head > div form { display: inline; margin: 0; }
.alert .restore-form { margin-top: 0.6rem; }
th .th-sort { color: inherit; text-decoration: none; white-space: nowrap; }
th .th-sort:hover { text-decoration: underline; }
th .th-sort.on { color: var(--accent); }
th .th-alt { font-weight: 400; font-size: 0.78rem; color: var(--muted); }
.alert.error { background: var(--red-bg); color: var(--red); }
.alert.ok { background: var(--green-bg); color: var(--green); }
.flash { background: var(--green-bg); color: var(--green); border-radius: 8px; padding: 0.75rem 1rem; margin: 0.9rem 0; font-size: 0.9rem; font-weight: 500; }

/* Overdue cards */
.overdue-card .overdue-head { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
.overdue-card .name { font-size: 1.02rem; font-weight: 650; letter-spacing: -0.01em; }
.overdue-meta { margin: 0.45rem 0; font-size: 0.9rem; }

/* Customer detail bits */
.lease-row { display: flex; justify-content: space-between; gap: 0.6rem; padding: 0.65rem 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.lease-row:last-child { border-bottom: none; }
.lease-row.ended { opacity: 0.55; }
.thumb { width: 44px; height: 44px; object-fit: cover; border-radius: 8px; vertical-align: middle; margin-right: 0.4rem; }
.preview { max-width: 100%; max-height: 240px; border-radius: 10px; }
.photo-row { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.9rem; }
.photo-row .thumb { width: 72px; height: 72px; }
/* The Help card that leads to the release notes, while they are still unread.
   A left edge rather than a filled panel: it should catch the eye on the way
   past without shouting over the "message the team" card, which is what
   somebody in trouble actually came for. */
.card-new { border-left: 3px solid var(--amber); }

/* The version line at the foot of Help. Quiet — it is reference, not content —
   but reachable, because it is what a customer reads back to us when something
   has gone wrong. */
.version-line { margin-top: 1.5rem; text-align: center; font-size: 0.85rem; }

/* "Preparing the photo…" under a file input while it is being shrunk. Its own
   line, because it sits inside the <label> next to the input and would
   otherwise wrap awkwardly against the file-picker button on a phone. */
.upload-note { display: block; margin-top: 0.35rem; font-size: 0.85rem; }
.policy-row > span:first-child { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }

/* Documents & photos list */
.doc-list { list-style: none; margin: 0; padding: 0; }
.doc-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.55rem 0; border-bottom: 1px solid var(--border); }
.doc-row:last-child { border-bottom: none; }
.doc-thumb { flex: 0 0 auto; width: 52px; height: 52px; border-radius: 8px; overflow: hidden; background: var(--grey-bg); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; }
.doc-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.doc-icon { display: flex; align-items: center; justify-content: center; color: var(--muted); }
.doc-icon svg { width: 24px; height: 24px; }
.doc-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.doc-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.doc-row form { flex: 0 0 auto; margin: 0; }

/* Login / signup */
.login-wrap { max-width: 23rem; margin: 10vh auto 0; text-align: center; }
.login-wrap .card { text-align: left; box-shadow: var(--shadow-md); padding: 1.5rem; }
.login-icon { width: 54px; height: 54px; border-radius: 14px; box-shadow: var(--shadow-md); margin-bottom: 0.2rem; }
.login-wrap h1 { font-size: 1.3rem; margin: 0.5rem 0 1rem; }
/* The app host's "start an account" and "try a demo" doors, under the sign-in form. */
.login-wrap .login-door { margin-top: 1rem; padding: 1.1rem 1.25rem; }
.login-door h2 { font-size: 1.05rem; margin: 0 0 0.3rem; }
.login-door p { margin: 0 0 0.8rem; }
.login-door form { margin: 0; }
.login-door form + form { margin-top: 0.5rem; }

/* Link box */
.linkbox { word-break: break-all; background: var(--grey-bg); padding: 0.7rem 0.8rem; border-radius: 8px; font-size: 0.88rem; }

/* Reports */
.report-head { margin: 0.7rem 0; }
.report-head h1 { margin-bottom: 0.15rem; }
.totals-row td { font-weight: 700; border-top: 2px solid var(--border); }
.invoice-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; }
.invoice-head h1 { margin: 0; }
.invoice-meta { text-align: right; }
.invoice-parties { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0; }
.invoice-parties .k { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.invoice-totals { margin-left: auto; max-width: 18rem; }
.invoice-totals div { display: flex; justify-content: space-between; padding: 0.2rem 0; }
.invoice-totals .grand { border-top: 2px solid var(--text); font-weight: 700; font-size: 1.1rem; margin-top: 0.3rem; padding-top: 0.4rem; }
.disclaimer { font-size: 0.78rem; color: var(--muted); border-top: 1px solid var(--border); margin-top: 1.2rem; padding-top: 0.6rem; }

/* Printing (receipts + reports) */
/* Invoice document */
.invoice-head { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.invoice-from { text-align: right; }
.invoice .paid { color: var(--green); }
.invoice-lines { margin: 1rem 0; }
.invoice-lines .num-lbl { text-align: right; }

.small { font-size: 0.82rem; }
.print-only { display: none; }
/* The wording of a message, shown as it will be sent.
   white-space: pre-wrap, not the <pre> default. A template body is a letter with
   its own line breaks, and those have to survive — but a <pre> that does not
   wrap makes the whole PAGE as wide as its longest line, which on a phone pushes
   the bottom navigation off the right-hand edge. Found by measure.mjs at 360 CSS
   pixels on the drifting-nudge screen, the moment that screen had a real
   template to show. */
.msg-preview {
  border-left: 3px solid var(--accent);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Digital agreements / e-sign */
.sign-page { max-width: 720px; }
.sign-doc h1 { margin-top: 0; }
.sign-body { white-space: normal; line-height: 1.6; margin: 1rem 0; }
.sign-body p { margin: 0 0 0.85rem; }
.sign-form { margin-top: 1.25rem; border-top: 1px solid var(--border); padding-top: 1.25rem; }
.sign-signed {
  margin-top: 1.25rem; padding: 0.85rem 1rem; border-radius: 8px;
  background: var(--green-bg); color: var(--green); border: 1px solid var(--green);
}
.sign-signed .muted { color: var(--green); opacity: 0.85; }
.sign-linkbox { display: flex; gap: 0.5rem; align-items: flex-end; flex-wrap: wrap; margin: 0.75rem 0; }
.sign-linkbox label { flex: 1 1 260px; }
.sign-linkinput { width: 100%; font-family: ui-monospace, monospace; font-size: 0.8rem; }

/* ---- Diary ----------------------------------------------------------------
   The yard's written record. Entries are read far more often than they are
   written, so the words get the weight and everything else — kind, author,
   buttons — sits quietly around them. */
.btn.tiny { padding: 0.16rem 0.55rem; min-height: 30px; font-size: 0.76rem; line-height: 1.6; }

.diary-day { margin: 1.4rem 0 0; }
.diary-date {
  margin: 0 0 0.5rem; font-size: 0.95rem; font-weight: 650; color: var(--heading);
  padding-bottom: 0.3rem; border-bottom: 1px solid var(--border);
}
.diary-date .muted { font-weight: 400; font-size: 0.85rem; }
.diary-day-line { margin: 0.9rem 0 0.2rem; font-size: 0.82rem; }

.diary-entry {
  background: var(--card); border-radius: 10px; box-shadow: var(--shadow-sm);
  padding: 0.8rem 0.95rem; margin: 0.55rem 0;
  border-left: 3px solid var(--border);
}
/* Inside a card the entry is already on card-coloured ground, so the shadow
   would read as a box within a box. */
.card .diary-entry { box-shadow: none; border: 1px solid var(--border); border-left-width: 3px; }
.diary-entry.pinned { border-left-color: var(--accent); }
.diary-head { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-bottom: 0.45rem; }
.diary-by { color: var(--faint); font-size: 0.78rem; margin-left: auto; }
.diary-link {
  font-size: 0.78rem; padding: 0.12rem 0.5rem; border-radius: 999px;
  background: var(--grey-bg); color: var(--muted); text-decoration: none;
}
.diary-link:hover { background: var(--border); color: var(--heading); text-decoration: none; }
/* pre-wrap so a note typed as three lines stays three lines — people use blank
   lines to separate one thing from the next, and flattening that loses it. */
.diary-body { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.diary-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.6rem; }
.diary-actions form { margin: 0; }
.diary-pinned { border-left: 3px solid var(--accent); }
/* The bare search box and category select are direct children of the filter row,
   so they pick up .filter-form's width:100% and each take a line to themselves.
   Inside a label they wouldn't — these are the two that aren't. */
.diary-filters { margin: 1.2rem 0 0.4rem; }
.diary-filters > input, .diary-filters > select { width: auto; margin-top: 0; }
.diary-filters > input[type="search"] { flex: 3 1 13rem; }
.diary-filters > select { flex: 1 1 9rem; }

.diary-quick { margin-top: 1rem; padding-top: 0.9rem; border-top: 1px solid var(--border); }
/* Two date fields side by side are indistinguishable without a word each —
   "28/07/2026" and an empty one say nothing about which is which. */
.diary-quick-row label.mini {
  display: flex; align-items: center; gap: 0.4rem; flex: 1 1 11rem;
  font-size: 0.82rem; color: var(--muted); margin: 0;
}
.diary-quick-row label.mini input { margin-top: 0; }

/* One colour per kind, reusing the palette the rest of the app already speaks:
   red is trouble, green is money, amber is the site itself. */
.pill.cat-general { background: var(--grey-bg); color: var(--muted); }
.pill.cat-site { background: var(--amber-bg); color: var(--amber); }
.pill.cat-customer { background: var(--cyan-bg); color: var(--cyan); }
.pill.cat-money { background: var(--green-bg); color: var(--green); }
.pill.cat-staff { background: var(--accent-bg); color: var(--accent); }
.pill.cat-incident { background: var(--red-bg); color: var(--red); }

/* ---- Help ------------------------------------------------------------------- */

/* The ? beside a page's heading. It is floated and emitted BEFORE the page body
   so it settles onto the first heading's line without every page template having
   to make room for it — the heading text simply flows around it. */
.page-help {
  float: right;
  margin: 1.15rem 0 0.4rem 0.75rem;
  width: 1.65rem; height: 1.65rem;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 50%;
  background: var(--card); color: var(--muted);
  font-size: 0.92rem; font-weight: 700; line-height: 1;
}
.page-help:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }

/* Visually hidden, still read aloud — so the ? has a real label without a
   tooltip being the only thing that explains it. */
.vh {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.help-jobs { list-style: none; margin: 0 0 0.9rem; padding: 0; }
.help-jobs li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 0.5rem;
  padding: 0.55rem 0; border-bottom: 1px solid var(--border);
}
.help-jobs li:last-child { border-bottom: none; }
.help-jobs a { font-weight: 600; }
.help-jobs .muted { font-size: 0.85rem; }

.manual-head { display: flex; gap: 0.5rem; margin: 1.1rem 0; }

/* The manual is a document, not an app screen: a narrower measure than the
   dashboard's full width, because long prose set across 84rem is unreadable. */
.manual {
  background: var(--card); border-radius: 12px; box-shadow: var(--shadow-sm);
  padding: 1.4rem 1.3rem; max-width: 46rem;
}
@media (min-width: 800px) { .manual { padding: 2rem 2.4rem; } }
/* The sticky top bar on phones would otherwise cover a heading jumped to by
   anchor — every ? link and every Contents entry lands via a fragment.
   --topbar-h is measured by the shell script in layout(); the fallback covers
   the phone case if that hasn't run. */
.manual h1, .manual h2, .manual h3 { scroll-margin-top: calc(var(--topbar-h, 10.5rem) + 0.75rem); }
@media (min-width: 800px) { .manual h1, .manual h2, .manual h3 { scroll-margin-top: 1rem; } }
.manual h1 { font-size: 1.25rem; margin: 2.2rem 0 0.9rem; }
.manual h1:first-child { margin-top: 0; }
.manual h2 { font-size: 1.02rem; margin: 1.7rem 0 0.6rem; }
.manual h3 { font-size: 0.95rem; font-weight: 650; color: var(--heading); margin: 1.3rem 0 0.5rem; }
.manual p, .manual li { line-height: 1.62; overflow-wrap: break-word; }
.manual p { margin: 0 0 0.95rem; }
.manual ul, .manual ol { margin: 0 0 1rem; padding-left: 1.4rem; }
.manual li { margin-bottom: 0.4rem; }
.manual li > ul, .manual li > ol { margin: 0.4rem 0 0.2rem; }
.manual hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
/* Addresses like yourname.storageyardmanager.co.uk are one unbreakable token and
   run straight off a phone screen without this. */
.manual code {
  background: var(--accent-bg); color: var(--text);
  padding: 0.1rem 0.32rem; border-radius: 5px; font-size: 0.88em;
  overflow-wrap: anywhere;
}
.manual pre {
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 0.8rem 1rem; overflow-x: auto; margin: 0 0 1rem;
}
.manual pre code { background: none; padding: 0; }
/* Tables in the manual carry their own scroller so a wide one can never make
   the whole page scroll sideways on a phone. */
.manual .table-wrap {
  overflow-x: auto; margin: 0 0 1.2rem;
  border: 1px solid var(--border); border-radius: 8px;
}
.manual th { background: var(--bg); }
.manual td, .manual th { padding: 0.5rem 0.8rem; }
.manual tr:last-child td { border-bottom: none; }

/* ---- arranging a screen's cards (?edit=1) -----------------------------------
   Per-login card order, same panel on every screen that has one. Arrows rather
   than drag-and-drop on purpose: this gets used one-handed on a phone out in the
   yard, and HTML5 drag has no touch support at all. app.js makes the arrows
   instant where it can; without it they are ordinary submit buttons that still
   work. */
.arrange-panel { border: 1px solid var(--accent); }
.arrange-list { list-style: none; margin: 0.6rem 0 0.9rem; padding: 0; }
.arrange-list li {
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  padding: 0.55rem 0; border-bottom: 1px solid var(--border);
}
.arrange-list li:last-child { border-bottom: none; }
.arrange-name { font-weight: 600; flex: 1 1 10rem; }
.arrange-tools { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
.arrange-tools .check { margin: 0; font-weight: normal; white-space: nowrap; }
.arrange-tools .btn { min-width: 2.4rem; }
/* Unticked, in the panel and on the page itself: dimmed rather than removed,
   because you need to see what you turned off in order to turn it back on. */
.arrange-list li.is-off .arrange-name { text-decoration: line-through; color: var(--muted); }
.card.is-off { opacity: 0.45; }

/* ---- the 29/07 growth batch ------------------------------------------------ */

/* Wide tables scroll inside their own box rather than pushing the page sideways.
   The manual already had this; the new reports need it too, so it is now a
   standalone class. */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-wrap table { min-width: 34rem; }

/* A .section-head whose right-hand side is a sentence rather than a button.
   The shared one is nowrap with no gap, which is right for "Vehicles  [+ Add]"
   and cramped when the right side is "+£864.00 a year (£72.00 a month)". */
.section-head.wrap-head { flex-wrap: wrap; gap: 0.15rem 0.9rem; align-items: baseline; }

/* Why somebody is leaving, next to the date. Allowed to grow, unlike the date
   beside it, because a reason is a sentence.
   Two classes deliberately: `.inline-form input` is class-plus-type and so
   outspecifies a lone `.leaving-why`, which silently lost this argument and left
   the box at its 8rem minimum showing "Price — goin". */
.leaving-form .leaving-why { flex: 1 1 14rem; }

/* Text replies. A work queue: each message is a block you read and clear, and a
   cleared one stays visible but recedes. */
.reply-list { list-style: none; margin: 0; padding: 0; }
.reply-row { padding: 0.8rem 0; border-bottom: 1px solid var(--border); }
.reply-row:last-child { border-bottom: 0; }
.reply-head { display: flex; gap: 0.6rem; align-items: baseline; flex-wrap: wrap; }
.reply-head .right { margin-left: auto; white-space: nowrap; }
.reply-body {
  margin: 0.4rem 0 0.6rem; padding: 0.55rem 0.7rem;
  background: var(--grey-bg); border-radius: 8px;
  border-left: 3px solid var(--accent);
  overflow-wrap: anywhere;
}
.reply-row.is-done { opacity: 0.55; }
.reply-row.is-done .reply-body { border-left-color: var(--border); }
.reply-form { margin-top: 0.6rem; }

/* Waiting-list matches: a plot, then the people who suit it. */
.match-row { padding: 0.7rem 0; border-bottom: 1px solid var(--border); }
.match-row:last-child { border-bottom: 0; }
.match-row > div:first-child { display: flex; gap: 0.5rem; align-items: baseline; flex-wrap: wrap; margin-bottom: 0.3rem; }
.match-hint { font-size: 0.8rem; color: var(--accent); }

/* Deposits. One block per contract, so a customer with two plots reads clearly. */
.deposit-block + .deposit-block { margin-top: 1.1rem; padding-top: 0.9rem; border-top: 1px solid var(--border); }
.deposit-block .section-head h3 { margin: 0; }
.deposit-form { margin-top: 0.7rem; padding-top: 0.7rem; border-top: 1px dashed var(--border); }

/* Price review. The fee box is edited in place, so it needs to look editable
   without being as wide as the column. */
.review-table td { vertical-align: top; }
.fee-input { max-width: 6.5rem; margin: 0; }
.review-table .check { margin: 0; }

/* Month-by-month cash bars — a chart without a charting library. */
.bar-list { list-style: none; margin: 0.6rem 0 0; padding: 0; display: grid; gap: 0.35rem; }
.bar-list li { display: flex; align-items: center; gap: 0.6rem; font-size: 0.85rem; }
.bar-label { flex: 0 0 4.6rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.bar-track { flex: 1 1 auto; height: 0.85rem; background: var(--grey-bg); border-radius: 4px; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
.bar-value { flex: 0 0 5.5rem; text-align: right; font-variant-numeric: tabular-nums; }

/* Reset the yard. Outlined in red because everything inside it deletes rows. */
.danger-card { border: 1px solid color-mix(in srgb, var(--red) 45%, transparent); }
.reset-list { list-style: none; margin: 0.6rem 0; padding: 0; display: grid; gap: 0.7rem; }
.reset-list li { display: grid; gap: 0.15rem; }
.reset-list .check { margin: 0; }
.reset-list .muted { font-size: 0.82rem; }
.reset-count { font-size: 0.8rem; color: var(--accent); font-variant-numeric: tabular-nums; }

@media (max-width: 600px) {
  .bar-label { flex-basis: 3.8rem; }
  .bar-value { flex-basis: 4.6rem; font-size: 0.8rem; }
  /* On a phone the arrange panel and these lists must not push the page wide. */
  .reply-head .right { margin-left: 0; }
  /* Sharing a row with the date leaves about 130px for the reason, which shows
     "Price — goin" and cuts the rest off. A sentence gets its own line. */
  .leaving-form .leaving-why { flex-basis: 100%; }
}

@media print {
  .topbar, .bottomnav, .sidebar, .no-print, .flash, .filter-form,
  .page-help, .manual-head,
  .diary-actions, .diary-quick { display: none !important; }
  /* The bar chart is the one visual on the yard-value report, so it has to
     survive being printed — a background-coloured span would not. */
  .bar-track { border: 1px solid #999; }
  .bar-fill { background: #666 !important; }
  .table-wrap { overflow: visible; }
  .print-only { display: block !important; }
  html { font-size: 15px; }
  body { background: #fff; padding: 0; color: #000; }
  .container { margin-left: 0; }
  .card, .table-card, .stat { break-inside: avoid; box-shadow: none; }
  .card { border: 1px solid #ccc; }
  /* Printing the manual should give a plain document, not a boxed card. */
  .manual { max-width: none; padding: 0; box-shadow: none; background: #fff; }
  .manual h1, .manual h2 { break-after: avoid; }
  .container { max-width: none; padding: 0; }
  th { background: #fff; }
  a { color: inherit; text-decoration: none; }
}

/* ---------------------------------------------------------------------------
   The industrial screens' own classes. These were written into the markup
   during the port and never given rules, so they rendered as bare browser
   defaults on the screens that sell this product — the service charge year end
   and the compliance register. Cosmetic, but they were cosmetic on the
   flagship.
   --------------------------------------------------------------------------- */

/* Service charge year end: budget / actual / variance / voids in one row.
   Same shape as .stat-grid but always four across on a wide screen, because the
   four figures are read against each other and wrapping breaks the comparison. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.8rem;
  margin: 1.1rem 0;
}
@media (min-width: 700px) { .stat-row { grid-template-columns: repeat(4, 1fr); } }
.stat-row .stat { padding: 0.85rem 0.95rem; }
.stat-n {
  display: block;
  font-size: 1.35rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: 1.25;
  color: var(--heading);
}
.stat-l { display: block; font-size: 0.82rem; color: var(--faint); margin-top: 0.15rem; }

/* "Why this exists" explainers — the lease/licence note and the compliance
   intervals. Set apart from the surrounding prose without shouting: this is
   reference material somebody reads once. */
.why {
  border-left: 3px solid var(--border);
  padding: 0.1rem 0 0.1rem 0.9rem;
  margin: 0.9rem 0;
}
.why dt { font-weight: 600; margin-top: 0.55rem; }
.why dt:first-child { margin-top: 0; }
.why dd { margin: 0.1rem 0 0; }
.why p:last-child { margin-bottom: 0; }
.why .mini { margin: 0.3rem 0; }

/* The heads-of-charge breakdown, folded away by default. */
.split-heads { margin: 0.9rem 0; }
.split-heads > summary {
  cursor: pointer;
  font-weight: 600;
  padding: 0.45rem 0;
}
.split-heads[open] > summary { margin-bottom: 0.4rem; }

/* ==========================================================================
   TAP IN BOOK — palette override. Everything ABOVE this block is UnitLet's
   stylesheet copied unchanged; everything below is this product's.

   Kept as an override rather than an edit so the two files stay diffable: the
   shell's markup lives in Chassis/platform/layout.js and is shared, so a class
   the shell gains needs styling in every product, and a byte-identical head
   makes that a copy instead of a hunt. Moving the shared rules into the chassis
   and leaving each product only a palette is the proper fix — named in HANDOFF,
   not done here.

   INK AND GREEN, 17/09/2026, and this is a REPAINT rather than a new coat. Until
   today this block was My Job Book's #F9B101 yellow, copied wholesale with the
   starter, and the app icon was My Job Book's open book with "My Job Book" as its
   alt text. A prospect who read the marketing site (ink and green) and then signed
   into a yellow app was looking at another product's clothes — the same failure
   this file's own header already describes happening once before.

   The values are the marketing site's and the kiosk's, which have agreed with
   each other since the site was built: ink #0f1115, green #2f6b4d, the darker
   #1f5038 where green carries TEXT on paper, and #6fbf93 where it carries text on
   the dark canvas.

   THE ONE RULE, and it is the opposite of yellow's: green holds WHITE. #2f6b4d
   under white is 5.4:1, so a green fill carries white text in both modes rather
   than ink. Green as small TEXT is the darker #1f5038 (8.6:1 on paper) — never
   #2f6b4d, which is a fill.

   Status colours (paid green, due amber, late red) are deliberately NOT touched,
   although the accent is now green as well: they are a traffic light an operator
   reads without thinking, and --green stays a much brighter green than --brand.
   ========================================================================== */
:root {
  --ink: #0f1115;
  --accent: #1f5038;
  --accent-hover: #163b29;
  --accent-bg: #e6f0ea;
  --brand: #2f6b4d;
  --brand-hover: #275a41;
  --tile-reserved: #e6f0ea;
  --tile-line: #2f6b4d;
  --shadow-accent: 0 2px 6px rgba(31, 80, 56, 0.28);
}
html.dark {
  --accent: #6fbf93;
  --accent-hover: #8fd2ac;
  --accent-bg: rgba(111, 191, 147, 0.14);
  --brand: #6fbf93;
  --brand-hover: #8fd2ac;
  --tile-reserved: rgba(111, 191, 147, 0.12);
  --tile-line: #6fbf93;
  --shadow-accent: 0 2px 8px rgba(111, 191, 147, 0.22);
}
/* One green for a solid control in both modes, as the site does it: white on it
   is 5.4:1 on paper and it still reads as a raised control on the dark canvas.
   Kept separate from --accent, which has to work as small link TEXT and so has to
   be dark in light mode and light in dark mode. */
:root, html.dark { --btn-primary: #2f6b4d; --btn-primary-hover: #275a41; }

/* Green fills carry WHITE, in both modes. The rules below exist because the
   stylesheet above them was written for a fill that could not. */
.btn.primary, .btn.primary:hover { color: #ffffff; }
.chip.on { background: var(--btn-primary); color: #ffffff; }
.wiz-steps li.current .wiz-num { color: #ffffff; }
.sidenav a.active { color: #ffffff; background: linear-gradient(72deg, #2f6b4d, #3f8a64); }
.sidenav a.active svg { opacity: 1; }
/* The "you are here" signal is a green mark under or beside the row. */
.tabs a.on { border-bottom-color: var(--brand); }
.bottomnav a.active { box-shadow: inset 0 3px 0 var(--brand); }
.outcome:has(input:checked) { border-color: var(--brand); }
.topnav-mobile a.active { box-shadow: inset 0 -3px 0 var(--brand); }
/* A link in running text is underlined in the brand green, so it still looks like
   a link where the surrounding text is nearly the same weight. */
:is(p, li, td, .alert, .muted, small, label, dd) > a:not([class]) {
  text-decoration: underline; text-decoration-color: var(--brand); text-decoration-thickness: 2px; text-underline-offset: 2px;
}
html.dark :is(p, li, td, .alert, .muted, small, label, dd) > a:not([class]) { text-decoration-color: color-mix(in srgb, var(--brand) 60%, transparent); }

/* ==========================================================================
   THE WORK SCREENS. Carried over with the starter and still describing the
   product it was written for; the rules are generic (a row, a card, a big
   control) and this product uses them for members, classes and sign-ins.

   Written for a phone in one hand, gloves on. Every control that does something
   on site is at least 48px tall; nothing depends on hover; the thing to tap next
   is the biggest thing on the card.
   ========================================================================== */

.pill.blue { background: var(--accent-bg); color: var(--accent); }
.pill.hatched { background: repeating-linear-gradient(45deg, var(--hatch-a), var(--hatch-a) 4px, var(--hatch-b) 4px, var(--hatch-b) 8px); color: var(--text); }
.alert.warn { background: var(--amber-bg); color: var(--amber); }
.btn.big { min-height: 52px; font-size: 1.02rem; font-weight: 600; }
.btn.block + .btn-row, .btn-row + .btn.block { margin-top: 0.6rem; }
.muted.small, .small.muted { line-height: 1.4; }

.empty-state { text-align: center; padding: 1.6rem 1.2rem; }
.empty-state h2 { margin: 0 0 0.3rem; font-size: 1.05rem; }
.empty-state .btn { margin-top: 0.6rem; }

.section-head-row { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; margin: 1.4rem 0 0.6rem; }
.section-head-row .section-head { margin: 0; }
.today-section > .section-head, section > .section-head { margin: 1.4rem 0 0.6rem; font-size: 0.95rem; }

/* Today */
.today-head { align-items: baseline; }
.add-job { margin: 0.8rem 0 0.4rem; }
.visit-card { margin: 0 0 0.8rem; }
.visit-card h3 { margin: 0.35rem 0 0.15rem; font-size: 1.08rem; line-height: 1.3; }
.visit-card h3 a { color: var(--heading); }
.visit-card p { margin: 0.2rem 0 0.6rem; }
.visit-card .alert { margin: 0.5rem 0; }
.visit-card form { margin: 0.7rem 0 0; }
.visit-card.is-done { opacity: 0.72; }
.visit-head { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; }
.visit-time { font-weight: 700; font-size: 1.05rem; font-variant-numeric: tabular-nums; margin-right: auto; }

/* Lists of jobs, customers, sites: one row is one tap target. */
.job-list, .rows.card { padding-top: 0.2rem; padding-bottom: 0.2rem; }
.rows li.job-row, .rows.card > li { padding: 0; }
.job-link { display: flex; align-items: center; gap: 0.8rem; padding: 0.75rem 0.1rem; min-height: 56px; color: inherit; text-decoration: none; width: 100%; }
.job-link:hover { text-decoration: none; background: var(--row-hover); }
.job-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.job-main strong { color: var(--heading); overflow-wrap: anywhere; }
.job-side { display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; text-align: right; flex-shrink: 0; }

/* One job */
.job-head h1 { margin: 0.1rem 0 0; overflow-wrap: anywhere; }
.job-pills { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.job-detail { white-space: pre-wrap; overflow-wrap: anywhere; }
.job-where p { overflow-wrap: anywhere; }
.on-site { border-bottom: 1px solid var(--border); padding-bottom: 1rem; margin-bottom: 0.8rem; }
.finish-form { margin-top: 1rem; }
.outcomes { border: none; padding: 0; margin: 0 0 0.9rem; display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
.outcomes legend { font-weight: 600; margin-bottom: 0.5rem; padding: 0; }
.form .outcome, .outcome {
  display: flex; align-items: center; gap: 0.5rem; min-height: 52px; margin: 0; padding: 0.4rem 0.75rem;
  border: 1px solid var(--field-border); border-radius: 10px; color: var(--text); font-size: 0.95rem; cursor: pointer;
}
.form .outcome input { width: auto; min-height: 0; margin: 0; }
.outcome:has(input:checked) { border-color: var(--accent); background: var(--accent-bg); color: var(--accent); font-weight: 600; }
.book-visit { margin-top: 0.8rem; }
.book-visit > summary, .inline-details > summary { list-style: none; display: inline-flex; cursor: pointer; }
.book-visit > summary::-webkit-details-marker, .inline-details > summary::-webkit-details-marker { display: none; }
.book-visit[open] > summary { margin-bottom: 0.6rem; }
.inline-details[open] { flex-basis: 100%; }
.notes p { margin: 0 0 0.2rem; white-space: pre-wrap; overflow-wrap: anywhere; }
.form .inline-choice, .inline-choice { display: inline-flex; align-items: center; gap: 0.4rem; margin: 0 1rem 0.5rem 0; color: var(--text); font-size: 0.92rem; }
.form .inline-choice input { width: auto; min-height: 0; margin: 0; }
.kinds { border: none; padding: 0; margin: 0 0 0.8rem; }
.kinds legend { padding: 0; margin-bottom: 0.3rem; color: var(--muted); }

/* Photos: a big button per stage, and every photo says whether it has been sent. */
.photo-take { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; margin: 0.6rem 0; }
.photo-take[hidden], .photo-gallery[hidden] { display: none; }
.photo-button { position: relative; justify-content: center; text-align: center; }
.photo-gallery { margin: 0.2rem 0 0.8rem; }
.photo-gallery summary { cursor: pointer; color: var(--muted); }
.photo-grid, [data-pending-photos] { list-style: none; margin: 0.4rem 0 0.8rem; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.6rem; }
.photo { display: flex; flex-direction: column; gap: 0.25rem; margin: 0; }
.photo img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; background: var(--grey-bg); display: block; height: auto; }
.photo-meta { font-size: 0.78rem; color: var(--muted); line-height: 1.35; }
.photo-warn { font-size: 0.78rem; color: var(--red); font-weight: 600; line-height: 1.35; }
.photo-warn:empty { display: none; }
.photo-state { display: inline-block; font-size: 0.72rem; font-weight: 700; padding: 0.05rem 0.45rem; border-radius: 99px; background: var(--grey-bg); color: var(--muted); }
.photo-state.is-captured, .photo-state.is-waiting, .photo-state.is-signin { background: var(--amber-bg); color: var(--amber); }
.photo-state.is-sending { background: var(--accent-bg); color: var(--accent); }
.photo-state.is-sent { background: var(--green-bg); color: var(--green); }
.photo-state.is-failed { background: var(--red-bg); color: var(--red); }
.photo.pending img { outline: 2px dashed var(--amber-strong); outline-offset: -2px; }
.photo.pending.is-sent img { outline: none; }
.photo form { margin: 0; }

/* The queue */
.outbox { border-left: 4px solid var(--amber-strong); }
.outbox h2 { font-size: 0.98rem; margin: 0 0 0.4rem; }
.outbox-list { list-style: none; margin: 0 0 0.5rem; padding: 0; }
.outbox-list li { padding: 0.35rem 0; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.outbox-list li:last-child { border-bottom: none; }
.offline-banner { background: var(--amber-bg); color: var(--amber); font-weight: 600; border-radius: 8px; padding: 0.7rem 1rem; margin: 0.6rem 0; }
.toast {
  position: fixed; left: 50%; bottom: calc(76px + env(safe-area-inset-bottom, 0px)); transform: translate(-50%, 1rem);
  max-width: min(92vw, 30rem); padding: 0.8rem 1rem; border-radius: 10px; background: var(--heading); color: var(--card);
  font-weight: 600; font-size: 0.92rem; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25); opacity: 0; pointer-events: none;
  transition: opacity 0.2s, transform 0.2s; z-index: 50;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
.toast.is-error { background: var(--red); color: #fff; }
@media (min-width: 800px) { .toast { bottom: 1.5rem; } }

@media (max-width: 420px) {
  .outcomes { grid-template-columns: 1fr; }
  .photo-take { grid-template-columns: 1fr 1fr 1fr; gap: 0.35rem; }
  .photo-take .btn { padding-left: 0.3rem; padding-right: 0.3rem; }
}

/* A page head with a small line above its title (a job ref, a customer name) stacks
   them: side by side, a long job summary wraps under its own reference. */
.page-head > div:first-child { display: flex; flex-direction: column; min-width: 0; }
.page-head > div:first-child > h1 { margin-top: 0.1rem; }

/* Work waiting on the phone, shown where it will land once sent (phone test, 13/09/2026). */
.pending-note p { font-style: italic; }
.pending-note, .pending-visit { border-left: 3px dashed var(--amber-strong); padding-left: 0.6rem; }
.pending-visit { margin: 0.4rem 0 0.8rem; }

/* Money (Phase 2). */
.price-lines li { display: flex; justify-content: space-between; gap: 0.8rem; align-items: flex-start; }
.line-main { display: flex; flex-direction: column; min-width: 0; }
.line-side { display: flex; flex-direction: column; align-items: flex-end; text-align: right; flex-shrink: 0; }
.line-side form { margin: 0; }
.margin-line { margin: 0.6rem 0 0; color: var(--green); }
.add-line { margin: 0.8rem 0; }
.add-line > summary { list-style: none; display: inline-flex; cursor: pointer; }
.add-line > summary::-webkit-details-marker { display: none; }
.add-line[open] > summary { margin-bottom: 0.6rem; }
.document { max-width: 52rem; }
.document .invoice-meta-left { color: var(--muted); font-size: 0.9rem; margin-top: 0.3rem; }
.pay-block { margin-top: 1.2rem; padding-top: 0.8rem; border-top: 1px solid var(--border); }
.pay-block .k { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.arrears-row { display: flex; flex-direction: column; }
.arrears-row .row-actions { justify-content: flex-start; padding-bottom: 0.6rem; }
.lead { font-size: 1.05rem; }
@media print { .document { box-shadow: none; border: none; max-width: none; } }

/* Checklists (Phase 2). */
.checklist-items, .template-items { list-style: none; margin: 0; padding: 0; }
.checklist-item { padding: 0.8rem 0; border-bottom: 1px solid var(--border); }
.checklist-prompt { margin: 0 0 0.5rem; }
.answer-choices { display: grid; grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); gap: 0.4rem; margin-bottom: 0.5rem; }
.outcome.is-negative:has(input:checked) { border-color: var(--red); background: var(--red-bg); color: var(--red); }
.template-item { padding: 0.6rem 0; border-bottom: 1px solid var(--border); }
.template-item-opts { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; align-items: center; margin-top: 0.4rem; }
.template-item-opts select { width: auto; }
.pill.small { font-size: 0.68rem; }

/* Sign-up form. The checkbox label is flex, so its sentence must be ONE item (a span) or every
   word and link becomes its own column; the web address sits beside its domain, with the hint
   on the line below. Both found on the first live sign-up, 15/09/2026. */
.terms-choice { align-items: flex-start; margin-right: 0; }
.terms-choice input { margin-top: 0.2rem; }
.terms-choice span { line-height: 1.45; }
.slug-row { display: flex; align-items: center; gap: 0.35rem; }
.slug-row input { flex: 1 1 auto; min-width: 0; }
.slug-row .muted { white-space: nowrap; }
.signup label small { display: block; margin-top: 0.25rem; }

/* Terms, privacy, refunds, data processing (src/legal.js). A reading column, not a card. */
.policy { max-width: 46rem; line-height: 1.6; }
.policy h1 { margin-bottom: 0.2rem; }
.policy h2 { font-size: 1.1rem; margin: 1.8rem 0 0.4rem; }
.policy .updated { color: var(--muted); font-size: 0.85rem; margin-top: 0; }
.policy li { margin-bottom: 0.4rem; }
.policy .table-scroll { overflow-x: auto; }
.policy-table { border-collapse: collapse; width: 100%; font-size: 0.92rem; }
.policy-table th, .policy-table td { text-align: left; vertical-align: top; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); }
.policy-table tbody th { width: 30%; font-weight: 600; }
.policy-foot { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.policy .fine { color: var(--muted); font-size: 0.78rem; }

/* The Companies Act s.82 disclosure, on EVERY page (src/index.js page()). Quiet
   and last: it is a legal requirement rather than something anybody came here to
   read, and on a 360px phone it has to wrap rather than push the page sideways. */
footer.statutory { margin: 2.5rem 0 0; padding-top: 0.9rem; border-top: 1px solid var(--border); }
footer.statutory .policy-foot { margin: 0 0 0.4rem; padding-top: 0; border-top: 0; font-size: 0.85rem; }
footer.statutory .policy-foot a { white-space: nowrap; }
footer.statutory .fine { color: var(--muted); font-size: 0.75rem; margin: 0; overflow-wrap: anywhere; }
/* Inside a policy page the article already draws its own; do not draw a second rule. */
.policy footer.statutory { border-top: 0; margin-top: 0; padding-top: 0; }

/* Printing a quote or invoice prints the document, not the app around it. */
@media print { .tabs, .page-help, .no-print, .who, .toast, [data-outbox] { display: none !important; } }

/* ============================================================================
   Tap In Book — this product's own additions.
 
   The rest of this file arrived with the chassis and is shared, near enough
   byte for byte, with three other products. Everything below is only this one's,
   kept at the end and in one block so that pulling a newer shared stylesheet
   across is a copy and a re-paste rather than a merge.
 
   Two things, both found by looking at a real screen rather than at a test.
   ============================================================================ */

/* A pairing code is read off a phone across a room and typed on a tablet. At
   body size it was six digits in a sentence; it needs to be the biggest thing on
   the screen, in tabular figures so 1 and 7 cannot be confused, and selectable
   because somebody will want to copy it. */
.code-big {
  margin: 0.2rem 0 0.8rem;
  font-size: clamp(2rem, 8vw, 3rem);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  line-height: 1.1;
  user-select: all;
}

/* The three set-up steps on an empty Today. The detail sits under its heading
   rather than running on from it — as one inline run it read as a single long
   sentence on a phone. */
.setup-steps { margin: 0.8rem 0 1.2rem; padding-left: 1.3rem; }
.setup-steps li { margin-bottom: 0.7rem; }
.setup-steps li > div { margin-top: 0.15rem; font-size: 0.92rem; }
.setup-steps li.done > strong { color: var(--muted); font-weight: 600; }

/* The paired tablets. A list rather than a table: a table of four columns with
   two buttons in the last one puts those buttons off the right-hand edge of a
   phone, and the whole office half of this product is used one-handed on one. */
.device-list { list-style: none; margin: 0.6rem 0 1rem; padding: 0; }
.device-list li { padding: 0.75rem 0; border-top: 1px solid var(--border); }
.device-list li:first-child { border-top: 0; }
.device-head { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.device-acts { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.55rem; }
.device-acts form { margin: 0; }
