/* Manrope ships with the app (public/fonts, SW-precached) — works offline. */
@font-face {
  font-family: 'Manrope';
  src: url('/fonts/manrope-var.woff2') format('woff2');
  font-weight: 200 800;
  font-display: swap;
}

:root {
  /* Greens run deeper than the original set — the darkest end carries the
     "premium" weight, the light end stays usable as a tint behind text.
     2026-08-20 tune: the whole ramp leans a few degrees warm toward olive
     and moss (blue-leaning hunter green reads dated; olive pairs with the
     brass) — same lightness stops, so every contrast pairing holds. */
  --green-950-solid: #0a2412;
  --green-900: #12351c;
  --green-800: #1c4a26;
  --green-700: #276333;
  --green-600: #348745;
  --green-100: #dcecd9;
  --green-50: #f0f6ed;
  /* Gold retuned to champagne brass (2026-08-20): flat and matte, never a
     gradient shine — Paramount's own logo is gold, and brass is how gold
     reads as premium now rather than as 2015. */
  --gold: #bfa15c;
  --gold-light: #e8d9a8;
  --gold-deep: #866c2e;
  --ink: #131912;
  --ink-soft: #5a6556;
  --line: #dfe6dc;
  --card: #ffffff;
  /* A second surface for panels that sit on top of cards. */
  --surface-2: #f8faf6;
  --bg: #f0f3ee;
  --red: #ab3227;
  /* Red as TEXT. --red doubles as a background (toast.error, low badges) so it
     cannot lift in dark mode; text sites use this token, which can. Measured
     2.3–2.5:1 in dark before it existed — below even large-text AA. */
  --red-ink: #ab3227;
  --red-bg: #fbe9e7;
  --amber-bg: #fdf2d4;
  --amber-ink: #7f6316;
  --radius: 16px;
  --radius-lg: 22px;
  /* Shadows are green-tinted and layered — a flat grey drop shadow is most of
     what made the old cards read as cheap. */
  --shadow-sm: 0 1px 2px rgba(6, 36, 20, .06), 0 2px 8px rgba(6, 36, 20, .05);
  --shadow-md: 0 2px 6px rgba(6, 36, 20, .09), 0 12px 30px rgba(6, 36, 20, .09);
  --shadow-lg: 0 4px 12px rgba(6, 36, 20, .12), 0 24px 60px rgba(6, 36, 20, .16);
  /* Motion tokens. --ease-out for entrances, --ease-spring for anything the
     finger touched. The spring is a REAL sampled damped spring (linear(),
     ζ=.65, 6.8% overshoot) and needs its full settle time, so it always
     travels with --dur-spring — never pair it with a shorter duration or the
     bounce gets truncated mid-flight. Older engines fall back to the old
     cubic approximation below. */
  --ease-out: cubic-bezier(.22, .8, .3, 1);
  --ease-spring: cubic-bezier(.34, 1.4, .5, 1);
  --dur-fast: .14s;
  --dur: .22s;
  --dur-slow: .36s;
  --dur-spring: .3s;
  /* Press-in is deliberately much faster than the sprung release — the
     asymmetry is what makes a control feel like a physical button. */
  --dur-press: .07s;
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  /* Hub accent (2026-08-19). The selected-state pair used to be hardcoded to
     the brand mint/deep-green everywhere; it now comes from these three, so a
     hub can carry its own temperature without touching the brand header, the
     gold, or anything unselected. Default IS the brand green — every screen
     outside the four material hubs looks exactly as it did. */
  --accent-tint: var(--green-100);
  --accent-ink: var(--green-800);
  --accent-line: #bfd9bc;
}

/* Each hub gets a temperature drawn from the MATERIALS themselves (re-derived
   2026-08-20 the way suppliers name colours): eucalyptus water for
   irrigation, misty onyx stone for hardscape, terracotta and chestnut timber
   for construction. Tints stay pale enough to read bold text at AA, and the
   hue never competes with the brass. Plants keeps the house green, so it
   inherits and is not restated. */
.screen[data-hub="irrigation"] { --accent-tint: #d2ece7; --accent-ink: #0e5147; --accent-line: #a5d6ca; }
.screen[data-hub="hardscape"] { --accent-tint: #e5e4e0; --accent-ink: #44433c; --accent-line: #cdccc3; }
.screen[data-hub="construction"] { --accent-tint: #f2e2d2; --accent-ink: #74431f; --accent-line: #dec4a8; }

/* Where linear() is supported (iOS 17.2+, all crew phones) the spring token
   becomes genuine spring physics; the duration grows to the spring's settle
   time. Generated by scripts/gen-spring.mjs — regenerate, never hand-edit. */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --ease-spring: linear(0, 0.027 3.6%, 0.098 7.1%, 0.196 10.7%, 0.309 14.3%, 0.427 17.9%, 0.542 21.4%, 0.649 25%, 0.745 28.6%, 0.828 32.1%, 0.897 35.7%, 0.953 39.3%, 0.995 42.9%, 1.027 46.4%, 1.048 50%, 1.061 53.6%, 1.067 57.1%, 1.068 60.7%, 1.065 64.3%, 1.06 67.9%, 1.053 71.4%, 1.045 75%, 1.037 78.6%, 1.029 82.1%, 1.022 85.7%, 1.016 89.3%, 1.01 92.9%, 1.006 96.4%, 1);
    --dur-spring: .45s;
  }
}

/* One switch for everything below. The crew uses this on old phones in the
   field; if the OS asks for less motion, every transition collapses to a
   cross-fade rather than being merely shortened. */
@media (prefers-reduced-motion: reduce) {
  :root { --dur-fast: .01s; --dur: .01s; --dur-slow: .01s; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body {
  font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  /* A flat fill was a big part of the "bland" — this is a barely-there wash
     that gives the page a top-light. Fixed so it doesn't repeat on scroll. */
  background: var(--bg);
  background-image:
    radial-gradient(120% 60% at 50% -10%, rgba(28, 74, 38, .07), transparent 60%),
    radial-gradient(90% 50% at 100% 0%, rgba(191, 161, 92, .07), transparent 55%);
  background-attachment: fixed;
  color: var(--ink);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* our own pull-to-refresh replaces the browser's */
  overscroll-behavior-y: contain;
}
button, input, select, textarea { font: inherit; color: inherit; }
button { -webkit-tap-highlight-color: transparent; }

.boot { padding: 34vh 0; text-align: center; color: var(--ink-soft); }
.boot img { width: 64px; height: 64px; display: block; margin: 0 auto 12px; opacity: .9; animation: mark-turn 3s linear infinite; }

/* ---------- Layout ---------- */
#app { min-height: 100vh; }
.screen { padding-bottom: calc(80px + var(--safe-bottom)); }

.topbar {
  background: linear-gradient(140deg, var(--green-900) 0%, var(--green-800) 55%, #2a5e30 100%);
  color: #fff;
  padding: calc(env(safe-area-inset-top, 0px) + 18px) 18px 16px;
  display: flex;
  align-items: center;
  gap: 10px;
  position: sticky;
  top: 0;
  /* iOS stale-viewport correction, top edition — same listener as the tab bar
     (app.js). 0 in the normal state; brings the header down to the true
     visible top when Safari leaves sticky elements stranded above it. */
  transform: translateY(var(--vv-top-shift, 0px));
  z-index: 20;
  border-radius: 0 0 22px 22px;
  box-shadow: 0 4px 18px rgba(11, 46, 27, .3);
}
.topbar::after {
  content: '';
  position: absolute;
  left: 22px; right: 22px; bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent, rgba(191, 161, 92, .65), transparent);
}
.topbar h1 { font-size: 21px; font-weight: 800; flex: 1; letter-spacing: -.3px; }
.topbar .sub { color: var(--gold-light); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.1px; }
.topbar button.icon { background: none; border: 0; color: #fff; font-size: 20px; padding: 10px; }
.back-btn {
  background: rgba(255, 255, 255, .14);
  border: 0; color: #fff;
  font-size: 24px; line-height: 1;
  width: 40px; height: 40px;
  border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  cursor: pointer;
  padding: 0 2px 2px 0;
  position: relative;
}
/* invisible hit-area extension — glove-friendly without growing the visual */
.back-btn::after { content: ''; position: absolute; inset: -5px; border-radius: 999px; }
.back-btn:active { background: rgba(255, 255, 255, .26); }

.content { padding: 16px 14px 22px; max-width: 720px; margin: 0 auto; animation: view-in var(--dur-slow) var(--ease-out); }
/* Fade only — deliberately NO transform. A transform on .content displaces
   every sticky child (the scale pulls a pinned search bar down by ~0.75% of
   the page height + 14px — 550px on a deep-scrolled catalog), and iOS's
   stale-rendering states can hold the animation mid-flight indefinitely,
   leaving search bars pinned mid-screen (the v1.45/v1.53 no-frames family).
   Only the container animates — never the list rows, which is where long
   catalogs (1,300+ plants) would start dropping frames on older phones. */
@keyframes view-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Offline banner — sits just under the top bar. */
.net-bar {
  background: var(--amber-bg);
  color: var(--amber-ink);
  border-bottom: 1px solid #ecd9a0;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  padding: 7px 12px;
}
.net-bar[hidden] { display: none; }

/* ---------- Bottom nav ---------- */
.tabbar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  /* iOS stale-viewport correction — see the visualViewport listener in app.js.
     0 in the normal state; pushes the bar down to the true visible bottom when
     Safari leaves the layout viewport stuck mid-screen. */
  transform: translateY(var(--vv-shift, 0px));
  background: rgba(255, 255, 255, .92);
  -webkit-backdrop-filter: saturate(1.4) blur(16px);
  backdrop-filter: saturate(1.4) blur(16px);
  border-top: 1px solid rgba(20, 60, 35, .08);
  display: flex;
  z-index: 30;
  padding-bottom: var(--safe-bottom);
  box-shadow: 0 -4px 20px rgba(13, 38, 24, .05);
}
.tabbar a {
  flex: 1;
  position: relative;
  text-align: center;
  padding: 9px 2px 10px;
  text-decoration: none;
  color: var(--ink-soft);
  font-size: 11px;
  font-weight: 600;
  transition: color var(--dur-fast) var(--ease-out);
}
.tabbar a .ico {
  display: block;
  font-size: 21px;
  line-height: 1.35;
  width: 54px;
  margin: 0 auto 1px;
  border-radius: 999px;
  transition: background var(--dur) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.tabbar a.active { color: var(--green-800); font-weight: 800; }
.tabbar a.active .ico { background: var(--green-100); transform: translateY(-1px); }
.tabbar a .badge {
  position: absolute;
  margin-left: 12px; margin-top: -35px;
  background: var(--red); color: #fff;
  font-size: 10px; font-weight: 800;
  border-radius: 9px; padding: 1px 5px;
  border: 2px solid #fff;
}

/* Update-ready bar (2026-08-07): a new version activated mid-session; tap to
   reload into it. Sits above the tab bar and rides the same stale-viewport
   correction. */
.update-bar {
  position: fixed;
  left: 50%; bottom: calc(76px + var(--safe-bottom));
  transform: translateX(-50%) translateY(var(--vv-shift, 0px));
  display: flex; align-items: center; gap: 8px;
  padding: 10px 18px;
  background: var(--green-800);
  color: #fff;
  border: 0;
  border-radius: 999px;
  font-size: 13.5px; font-weight: 700;
  box-shadow: 0 6px 22px rgba(11, 46, 27, .35);
  z-index: 29;
  cursor: pointer;
}
.update-bar:active { filter: brightness(1.12); }

/* ---------- Home hero & tiles ---------- */
.hero {
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(130% 150% at 88% -25%, rgba(191, 161, 92, .38), transparent 48%),
    radial-gradient(90% 120% at 0% 110%, rgba(52, 135, 69, .35), transparent 55%),
    linear-gradient(155deg, var(--green-950-solid) 0%, var(--green-900) 45%, #20562a 100%);
  color: #fff;
  border-radius: var(--radius-lg);
  padding: 22px 18px 18px;
  margin-bottom: 16px;
  box-shadow: var(--shadow-lg);
}
/* A hairline of light along the top edge — the trick that stops a dark panel
   looking like a flat rectangle. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(160deg, rgba(255, 255, 255, .28), rgba(255, 255, 255, 0) 42%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
.hero-date { color: var(--gold-light); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; }
.hero-hi { font-size: 24px; font-weight: 800; letter-spacing: -.4px; margin: 2px 0 16px; }
.hero-stats { display: flex; gap: 8px; }
.hero-stats a {
  flex: 1;
  background: rgba(255, 255, 255, .1);
  border: 1px solid rgba(255, 255, 255, .12);
  border-radius: 14px;
  padding: 10px 8px;
  text-align: center;
  text-decoration: none;
  color: #fff;
  transition: background var(--dur-fast) var(--ease-out);
}
.hero-stats a:active { background: rgba(255, 255, 255, .2); }
.hero-stats .n { display: block; font-size: 24px; font-weight: 800; letter-spacing: -.5px; font-variant-numeric: tabular-nums; }
.hero-stats .l { display: block; font-size: 11px; font-weight: 600; color: rgba(255, 255, 255, .75); }
.hero-stats a.alert .n { color: #ffb4a8; }

/* ---------- Global search entry + results (2026-08-07) ---------- */
.gs-open {
  display: flex; align-items: center; gap: 9px;
  width: 100%;
  margin-top: 12px;
  padding: 13px 15px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink-soft);
  font-size: 15px; font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.gs-open:active { background: var(--green-50); }
.gs-out .section-label .gs-n { font-weight: 600; opacity: .55; margin-left: 4px; }
.gs-more .title { font-weight: 600; }

/* ---------- Recently viewed strip (2026-08-07) ---------- */
.recent-strip {
  display: flex; gap: 9px;
  overflow-x: auto;
  padding: 2px 2px 6px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.recent-strip::-webkit-scrollbar { display: none; }

/* Jobsite weather strip (2026-08-19): seven day-chips over the job lists,
   with the frost / dry-window line beneath. Rides .recent-strip for scroll. */
.wx-strip { margin-bottom: 6px; }
.wx-day {
  flex: 0 0 52px;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 7px 2px;
  background: var(--surface-2);
  border-radius: 10px;
  font-variant-numeric: tabular-nums;
}
.wx-dow { font-size: 11px; font-weight: 600; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .4px; }
.wx-t { font-size: 14px; font-weight: 700; }
.wx-lo { font-size: 11px; color: var(--ink-soft); }
.wx-p { font-size: 11px; color: var(--ink-soft); }
.wx-p.wet { color: #2c5480; font-weight: 700; }
.wx-line { font-size: 13px; border-radius: 9px; padding: 7px 11px; margin-bottom: 6px; }
.wx-line.frost { background: var(--red-bg); color: var(--red-ink); }
.wx-line.dry { background: var(--green-100); color: var(--green-800); }
.wx-line .ic { vertical-align: -2px; margin-right: 3px; }

/* Inline text-button (warranty replacement "remove") — reads as a link, sits
   in running text where a block button would break the line. */
.wr-rm { background: none; border: 0; padding: 0; font-size: 12px; color: var(--red-ink); text-decoration: underline; cursor: pointer; }

/* QR bin labels (2026-08-19): a cut-out grid. On screen, tap to exclude
   (.off dims it); in print, .off vanishes and the dashed borders become the
   scissor lines. Labels stay white-on-black even in dark mode — they are a
   print artifact, and a dark-mode QR preview would lie about the output. */
.qr-sheet { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.qr-label {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 10px 6px;
  background: #fff; color: #000;
  border: 1px dashed #999;
  border-radius: 8px;
  text-align: center;
  cursor: pointer;
  break-inside: avoid;
}
.qr-label.off { opacity: .3; }
.qr-name { font-size: 12px; font-weight: 700; line-height: 1.25; }
.qr-sub { font-size: 10px; color: #555; }
.recent-card {
  flex: 0 0 96px;
  display: flex; flex-direction: column; gap: 5px;
  text-decoration: none;
  color: var(--ink);
}
.recent-card img, .recent-card .rc-ph {
  width: 96px; height: 72px;
  border-radius: 10px;
  object-fit: cover;
  border: 1px solid var(--line);
  background: var(--surface-2);
}
.recent-card .rc-ph { display: flex; align-items: center; justify-content: center; color: var(--ink-soft); opacity: .7; }
.recent-card .rc-name {
  font-size: 11.5px; font-weight: 600; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Star/save chip on the catalog item page */
.fav-row { display: flex; margin: 10px 0 12px; }

.section-label {
  font-size: 13px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .9px;
  color: var(--ink-soft);
  margin: 18px 2px 8px;
}

.tile-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.tile {
  display: flex; align-items: center; gap: 11px;
  background: linear-gradient(170deg, #fff, var(--surface-2));
  border: 1px solid rgba(18, 74, 41, .08);
  border-radius: var(--radius);
  padding: 13px 13px;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-spring) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out);
  text-align: left;
}
.tile:active { transform: scale(.955); box-shadow: var(--shadow-sm); }
.tile .tl { font-weight: 700; font-size: 14px; letter-spacing: -.1px; }
.ti {
  width: 40px; height: 40px;
  border-radius: 12px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 20px;
}
.ti-green { background: var(--green-100); color: var(--green-800); }
.ti-teal { background: #d7f0ec; color: #14635a; }
.ti-gold { background: #f7ecc9; color: #8a6414; }
.ti-blue { background: #e0e9fa; color: #2c5480; }
.ti-red { background: var(--red-bg); color: var(--red); }
.ti-neutral { background: #edf1ee; color: #5b6b60; }
/* SVG icons inherit currentColor; keep them optically centered. */
.ic { display: inline-block; vertical-align: -3px; flex-shrink: 0; }
.ti .ic, .tabbar .ico .ic { vertical-align: 0; }
.seg .ic, .crewseg .ic { vertical-align: -2px; margin-right: 2px; }
/* Icon-migration completion (2026-08-05): icons now live in headings, list-row
   titles, empty states and the view toggle too. */
.card h3 .ic, h2 .ic { vertical-align: -2.5px; margin-right: 3px; color: var(--green-700); }
.row .title .ic { vertical-align: -2.5px; margin-right: 5px; color: var(--green-700); }
.empty .big .ic { opacity: .4; color: var(--green-700); }
.viewtog .ic { vertical-align: -2px; }
/* Sticky calculator total (2026-08-05): while measurements are being entered
   further up the card, the running total pins just above the tab bar; it
   settles into its natural spot once scrolled past. Solid tint so list rows
   never bleed through. */
.calc-total {
  position: sticky;
  bottom: calc(74px + var(--safe-bottom));
  z-index: 5;
  background: var(--green-50);
  border: 1px solid var(--green-100);
  border-radius: 10px;
  padding: 10px;
  margin-top: 10px;
  box-shadow: var(--shadow-md);
}

/* Compact sync pill: the refresh glyph turns while a catalog sync runs. */
.sync-btn.busy .ic { animation: ic-turn 1s linear infinite; }
@keyframes ic-turn { to { transform: rotate(1turn); } }
.warn-box .ic, .warn .ic, .error-box .ic { color: inherit; }
.ok-box .ic { color: inherit; }

/* ---------- Cards & lists ---------- */
.card {
  background: var(--card);
  border: 1px solid rgba(18, 74, 41, .08);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 12px;
  box-shadow: var(--shadow-sm);
}
.card h3 { font-size: 15px; font-weight: 700; letter-spacing: -.1px; margin-bottom: 8px; }
.list {
  background: var(--card);
  border: 1px solid rgba(20, 60, 35, .06);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 15px;
  border-bottom: 1px solid var(--green-50);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  background: none;
  width: 100%;
  text-align: left;
  border-left: 0; border-right: 0; border-top: 0;
  transition: background var(--dur-fast) var(--ease-out);
}
.row:last-child { border-bottom: 0; }
.row:active { background: var(--green-50); }
.row .main { flex: 1; min-width: 0; }
.row .title { font-weight: 600; font-size: 15px; }
.row .sub { color: var(--ink-soft); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .end { text-align: right; flex-shrink: 0; }
.row .chev { color: #b9c6bd; font-size: 18px; }

.qty-pill {
  font-weight: 700; font-size: 14px;
  font-variant-numeric: tabular-nums;
  padding: 4px 11px; border-radius: 999px;
  background: var(--green-100); color: var(--green-800);
}
.qty-pill.low { background: var(--red-bg); color: var(--red-ink); }

.chip {
  display: inline-block;
  background: var(--green-50);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  border-radius: 999px;
  padding: 3px 11px;
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  margin: 2px 4px 2px 0;
}
button.chip { cursor: pointer; padding: 6px 13px; }
button.chip:active { background: var(--green-100); }
.chip.gold { background: var(--amber-bg); border-color: #ecd9a0; color: var(--amber-ink); }
.chip.green { background: var(--green-100); border-color: #bcd9c6; color: var(--green-800); }
.chip.blue { background: #e6eef7; border-color: #c3d5ea; color: #2c5480; }
.chip.purple { background: #efe6f7; border-color: #d3c2e6; color: #5b3a82; }
.chip.red { background: var(--red-bg); border-color: #e7c3bd; color: var(--red-ink); }

/* ---------- Supplier photo review grid ---------- */
.pr-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.pr-card {
  position: relative;
  border: 2px solid transparent;
  border-radius: 12px;
  overflow: hidden;
  padding: 0;
  background: var(--card);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  text-align: left;
}
.pr-card img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; background: var(--green-50); }
.pr-name { display: block; font-size: 11px; font-weight: 600; padding: 4px 6px 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pr-check {
  position: absolute; top: 6px; right: 6px;
  width: 22px; height: 22px; border-radius: 999px;
  background: rgba(255, 255, 255, .85); color: transparent;
  border: 1px solid var(--line);
  font-weight: 800; font-size: 13px;
  display: flex; align-items: center; justify-content: center;
}
.pr-card.sel { border-color: var(--red); }
.pr-card.sel .pr-check { background: var(--red); color: #fff; border-color: var(--red); }
.pr-card.sel img { opacity: .55; }

/* ---------- Crew (messages, tasks, inbox) ---------- */
.msg {
  padding: 10px 13px;
  border-radius: 15px 15px 15px 5px;
  margin-bottom: 8px;
  background: var(--card);
  border: 1px solid rgba(20, 60, 35, .06);
  box-shadow: var(--shadow-sm);
  max-width: 88%;
}
.msg.mine {
  background: linear-gradient(160deg, #e2f2e8, var(--green-100));
  border-color: #c4ddce;
  border-radius: 15px 15px 5px 15px;
  margin-left: auto;
}
.msg-head { font-size: 12px; font-weight: 700; color: var(--ink-soft); margin-bottom: 2px; }
.msg-body { font-size: 15px; white-space: pre-wrap; word-break: break-word; }
.mention { color: var(--green-700); font-weight: 700; }
button.tick { padding: 0; cursor: pointer; position: relative; }
/* the 26px circle is fine to see but small to hit — extend the tap area */
button.tick::after { content: ''; position: absolute; inset: -9px; }

/* ---------- Truck loadout checklist ---------- */
.area-head {
  font-size: 13px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .9px;
  color: var(--ink-soft);
  margin: 18px 2px 8px;
  display: flex; align-items: center; gap: 6px;
  cursor: pointer;
  -webkit-user-select: none; user-select: none;
  padding: 4px 2px;
}
.area-wrap:first-of-type .area-head { margin-top: 4px; }
.area-count { font-weight: 700; font-size: 12px; font-variant-numeric: tabular-nums; }
.tk-chev { color: #b9c6bd; font-size: 15px; transition: transform var(--dur) var(--ease-out); flex-shrink: 0; }
.collapsed > .area-head .tk-chev, .tk-group.collapsed .tk-chev { transform: rotate(-90deg); }
.area-wrap.collapsed .area-body { display: none; }
.grp-head { display: flex; align-items: center; gap: 8px; cursor: pointer; -webkit-user-select: none; user-select: none; }
.grp-head h3 { flex: 1; margin: 0; }
.tk-group .grp-body { margin-top: 8px; }
.tk-group.collapsed .grp-body { display: none; }
.tick {
  width: 26px; height: 26px; border-radius: 999px; flex-shrink: 0;
  border: 2px solid #cfdcd3; background: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 800; color: #fff;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.chk.done .tick {
  background: var(--green-700); border-color: var(--green-700);
  animation: tick-pop var(--dur) var(--ease-out);
}
@keyframes tick-pop { from { transform: scale(.55); } to { transform: scale(1); } }
.chk.done .title { color: var(--ink-soft); text-decoration: line-through; }
.chk .sub { white-space: normal; }
.flagbtn { background: none; border: 0; font-size: 17px; padding: 11px; opacity: .4; cursor: pointer; flex-shrink: 0; }
.chk.done .flagbtn { opacity: .22; }
.progress-track { height: 8px; border-radius: 999px; background: var(--green-100); overflow: hidden; }
.progress-fill {
  height: 100%; width: 0;
  background: linear-gradient(90deg, var(--green-700), var(--green-600));
  border-radius: 999px;
  transition: width var(--dur-slow) var(--ease-out);
}

/* Plant quality grades — color-coded worst → best, plus a distinct "dividable". */
.qual {
  display: inline-block;
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 12px;
  font-weight: 700;
  border: 1px solid transparent;
}
.qual-poor { background: var(--red-bg); color: var(--red-ink); border-color: #e7c3bd; }
.qual-rehab { background: #fbe6d2; color: #9a5a12; border-color: #f0cfa8; }
.qual-medium { background: var(--amber-bg); color: var(--amber-ink); border-color: #ecd9a0; }
.qual-good { background: var(--green-100); color: var(--green-800); border-color: #bcd9c6; }
.qual-excellent { background: var(--green-800); color: #fff; border-color: var(--green-900); }
.qual-dividable { background: #e2e9fb; color: #2f4b96; border-color: #c7d3f4; }

/* Compact inline quality picker used on job plant-list rows. */
select.qual-select { width: auto; padding: 5px 8px; font-size: 13px; border-radius: 8px; }

/* Auto quality prompt shown right after a plant is added to inventory / a job. */
.qual-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.qual-pick {
  padding: 16px 10px;
  border-radius: 13px;
  border-width: 1.5px;
  border-style: solid;
  font-weight: 700;
  font-size: 15px;
  cursor: pointer;
  transition: transform var(--dur-spring) var(--ease-spring);
}
.qual-pick:active { transform: scale(.97); }

/* ---------- Forms ---------- */
label.f { display: block; margin-bottom: 13px; }
label.f span { display: block; font-size: 13px; font-weight: 600; color: var(--ink-soft); margin-bottom: 5px; }
input[type=text], input[type=password], input[type=number], input[type=date], input[type=search], select, textarea {
  width: 100%;
  padding: 11px 13px;
  border: 1px solid #d9e3dc;
  border-radius: 11px;
  background: #fbfcfb;
  font-size: 16px; /* prevents iOS zoom-on-focus */
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
textarea { min-height: 90px; resize: vertical; }
input:focus, select:focus, textarea:focus {
  outline: none;
  background: #fff;
  border-color: var(--green-700);
  box-shadow: 0 0 0 3px rgba(26, 107, 58, .14);
}

.pw-wrap { position: relative; display: block; }
.pw-wrap input { padding-right: 48px; }
.pw-toggle {
  position: absolute;
  right: 2px; top: 50%;
  transform: translateY(-50%);
  background: none; border: 0;
  font-size: 19px;
  padding: 11px;
  cursor: pointer;
  line-height: 1;
}

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: linear-gradient(180deg, #29622f, var(--green-800));
  color: #fff;
  border: 0; border-radius: 12px;
  padding: 12px 18px;
  font-weight: 700; font-size: 15px;
  cursor: pointer;
  width: 100%;
  box-shadow: 0 1px 2px rgba(11, 46, 27, .25);
  transition: transform var(--dur-spring) var(--ease-spring), filter var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.btn:active { transform: translateY(1px); filter: brightness(.93); box-shadow: none; }
.btn.secondary {
  background: #fff; color: var(--green-800);
  border: 1.5px solid rgba(20, 83, 45, .35);
  box-shadow: var(--shadow-sm);
}
.btn.danger { background: #fff; color: var(--red); border: 1.5px solid rgba(179, 54, 43, .4); box-shadow: var(--shadow-sm); }
.btn.small { width: auto; padding: 8px 14px; font-size: 14px; border-radius: 10px; }
.btn:disabled { opacity: .5; }

.btn-row { display: flex; gap: 10px; }
.btn-row .btn { flex: 1; }

/* Sticky under the topbar so search never scrolls out of reach on long lists.
   --topbar-h is measured in shell() (the topbar's height varies with the
   safe-area inset). The background + padding mask rows sliding beneath. */
.search-wrap {
  display: flex; gap: 8px;
  position: sticky;
  top: var(--topbar-h, 58px);
  /* Rides the same correction as the topbar it hangs under, or the pair
     splits apart in the stale-viewport state. */
  transform: translateY(var(--vv-top-shift, 0px));
  z-index: 15;
  background: var(--bg);
  padding: 8px 0;
  margin: -8px 0 4px;
}
.search-wrap input { flex: 1; }
.search-wrap select { width: auto; max-width: 44%; }

/* ft/in picker beside a calculator measurement input — kept narrow. */
select.unit-sel { width: auto; flex-shrink: 0; padding: 11px 6px 11px 8px; }

/* Active-filter chip in the results meta line — tap to clear that filter. */
.fchip {
  display: inline-block;
  background: var(--green-100);
  border: 1px solid #bcd9c6;
  color: var(--green-800);
  border-radius: 999px;
  padding: 4px 11px;
  margin: 2px 0 2px 4px;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  max-width: 55vw;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  vertical-align: middle;
}
.fchip:active { background: #cfe6d8; }

/* Skeleton rows shown while a list loads. */
.skel-row { display: flex; align-items: center; gap: 12px; padding: 13px 15px; border-bottom: 1px solid var(--green-50); }
.skel-row:last-child { border-bottom: 0; }
.skel-b {
  background: linear-gradient(90deg, #e7eee8 25%, #f3f7f3 45%, #e7eee8 65%);
  background-size: 200% 100%;
  animation: skel-wave 1.1s linear infinite;
  border-radius: 7px;
}
.skel-av { width: 44px; height: 44px; border-radius: 10px; flex-shrink: 0; }
@keyframes skel-wave { from { background-position: 200% 0; } to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .skel-b { animation: none; } }

/* Pull-to-refresh indicator: follows the drag, arms past the threshold. */
.ptr {
  position: fixed;
  top: calc(var(--topbar-h, 58px) + 6px);
  left: 50%;
  width: 40px; height: 40px;
  margin-left: -20px;
  border-radius: 50%;
  background: #fff;
  border: 1px solid rgba(20, 60, 35, .14);
  box-shadow: var(--shadow-md);
  display: flex; align-items: center; justify-content: center;
  font-size: 19px;
  color: var(--green-800);
  z-index: 18;
  opacity: 0;
  transform: translateY(-56px);
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.ptr.pull { opacity: 1; transform: translateY(calc(var(--pull, 0px) * 0.45 - 46px)); }
.ptr span { display: block; transform: rotate(calc(var(--pull, 0px) * 2.2deg)); }
.ptr.ready { color: var(--green-900); border-color: var(--gold); background: #fdf7e3; }

.fab {
  position: fixed;
  right: 18px;
  bottom: calc(90px + var(--safe-bottom));
  width: 56px; height: 56px;
  border-radius: 50%;
  /* Flat matte brass — the gradient shine was the 2015 tell. */
  background: var(--gold);
  color: var(--green-900);
  font-size: 30px;
  border: 0;
  box-shadow: 0 6px 18px rgba(143, 116, 52, .4);
  z-index: 25;
  cursor: pointer;
  transition: transform var(--dur-spring) var(--ease-spring);
}
.fab:active { transform: scale(.93); }

/* Jump-to-top: appears after scrolling well past a screenful (~25 list rows).
   Bottom-left, opposite the gold + FAB, so the two never collide. */
.scrolltop {
  position: fixed;
  left: 18px;
  bottom: calc(90px + var(--safe-bottom));
  width: 46px; height: 46px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .94);
  -webkit-backdrop-filter: saturate(1.4) blur(12px);
  backdrop-filter: saturate(1.4) blur(12px);
  color: var(--green-800);
  font-size: 21px;
  font-weight: 800;
  line-height: 1;
  border: 1px solid rgba(20, 60, 35, .14);
  box-shadow: var(--shadow-md);
  z-index: 25;
  cursor: pointer;
  opacity: 0;
  transform: translateY(10px) scale(.9);
  pointer-events: none;
  transition: opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}
.scrolltop.show { opacity: 1; transform: none; pointer-events: auto; }
.scrolltop.show:active { transform: scale(.92); }

/* ---------- Utility ---------- */
/* Spacing/layout utilities (2026-08-05): the exact inline styles that appeared
   10+ times each in app.js, promoted to classes. Anything less common stays
   inline — consolidate opportunistically as work touches screens. */
.mt8 { margin-top: 8px; }
.mt10 { margin-top: 10px; }
.mt12 { margin-top: 12px; }
.mb8 { margin-bottom: 8px; }
.mb10 { margin-bottom: 10px; }
.mb12 { margin-bottom: 12px; }
.flex1 { flex: 1; }
.minw0 { min-width: 0; }
.noshrink { flex-shrink: 0; }
.bold { font-weight: 700; }
.fs12 { font-size: 12px; }
.hstack { display: flex; align-items: center; gap: 8px; }
.kv-tight { grid-template-columns: 1fr 1fr; gap: 0 10px; }
hr.sep { border: 0; border-top: 1px solid var(--line); margin: 12px 0; }
/* Round 2 (same day): the multi-use typography/border patterns. */
.mt2 { margin-top: 2px; }
.mt6 { margin-top: 6px; }
.mt14 { margin-top: 14px; }
.mb4 { margin-bottom: 4px; }
.mb6 { margin-bottom: 6px; }
.m0 { margin: 0; }
.fs11 { font-size: 11px; }
.fs13 { font-size: 13px; }
.fs14 { font-size: 14px; }
.semibold { font-weight: 600; }
.wrap { white-space: normal; }
.nowrap { white-space: nowrap; }
.prewrap { white-space: pre-wrap; }
.plainlink { text-decoration: none; color: inherit; }
.borderless { border: 0; }
.listline { padding: 8px 0; border-bottom: 1px solid var(--line); }
.rowline { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
.strong-green { font-weight: 800; color: var(--green-800); }
.thumb44 { width: 44px; height: 44px; border-radius: 8px; object-fit: cover; flex-shrink: 0; }
/* Supplier product shots (Ewing, 2026-08-05) are catalogue cut-outs on white,
   not our own photographs: contain them so a fitting keeps both its ends, and
   keep the white behind them in dark mode too — cropping or tinting a cut-out
   is how a part stops being recognisable. */
.supplier-shot { object-fit: contain; background: #fff; }
.cat-card .ph img.supplier-shot { padding: 8px; }
.item-hero.supplier { background: #fff; }
.item-hero.supplier img { object-fit: contain; padding: 14px 14px 46px; }
.item-hero.supplier .scrim { background: linear-gradient(to top, rgba(4, 20, 11, .82), rgba(4, 20, 11, .3) 38%, transparent); }
.input-like { width: 100%; padding: 10px 12px; border: 1.5px solid var(--line); border-radius: 10px; font: inherit; }
.scrollbox { max-height: 34vh; overflow-y: auto; }
.muted { color: var(--ink-soft); font-size: 13px; }
.center { text-align: center; }
.empty { text-align: center; color: var(--ink-soft); padding: 44px 16px; position: relative; }
.empty .big { font-size: 42px; display: block; margin-bottom: 10px; }
/* A zero-results empty state carries its own way out (2026-08-20). */
.empty .btn { width: auto; margin-top: 14px; }
/* Faint brand-mark watermark behind every empty state — branded quiet instead
   of a bare sentence. Sits behind the text (z-index -1) and stays readable. */
.empty::before {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 190px; height: 190px;
  transform: translate(-50%, -50%);
  background: url(/icons/emblem.png) center / contain no-repeat;
  opacity: .08;
  pointer-events: none;
  z-index: -1;
}
.error-box { background: var(--red-bg); color: var(--red-ink); border-radius: 11px; padding: 10px 13px; margin-bottom: 12px; font-size: 14px; }
.ok-box { background: var(--green-100); color: var(--green-800); border-radius: 11px; padding: 10px 13px; margin-bottom: 12px; font-size: 14px; }
.warn-box { background: var(--amber-bg); color: var(--amber-ink); border-radius: 11px; padding: 10px 13px; margin-bottom: 12px; font-size: 14px; }

.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; }
.kv .k { font-size: 12px; color: var(--ink-soft); font-weight: 600; text-transform: uppercase; letter-spacing: .5px; }
.kv .v { font-size: 15px; margin-bottom: 6px; font-variant-numeric: tabular-nums; }

.stat-row { display: flex; gap: 10px; margin-bottom: 12px; }
.stat {
  flex: 1;
  background: var(--card);
  border: 1px solid rgba(20, 60, 35, .06);
  border-radius: var(--radius);
  padding: 13px 12px;
  text-align: center;
  text-decoration: none;
  color: inherit;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-spring) var(--ease-spring);
}
.stat:active { transform: scale(.98); }
.stat .n { font-size: 27px; font-weight: 800; color: var(--green-800); letter-spacing: -.5px; font-variant-numeric: tabular-nums; }
.stat.alert .n { color: var(--red-ink); }
.stat .l { font-size: 12px; color: var(--ink-soft); font-weight: 600; }

/* ---------- Auth screens ---------- */
.auth-wrap {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* black-translucent status bar (2026-08-05): the page now extends under the
     iOS status bar, so keep the login clear of the notch. */
  padding: calc(env(safe-area-inset-top, 0px) + 24px) 24px calc(var(--safe-bottom) + 24px);
  max-width: 420px;
  margin: 0 auto;
}
.auth-logo { text-align: center; margin-bottom: 24px; }
/* The brand mark: the knot emblem alone (no lettering), turning slowly inside
   a still green tile — the spin lives on the inner img, not the tile. */
.auth-logo .mark {
  width: 128px; height: 128px;
  margin: 0 auto;
  border-radius: 32px;
  display: grid; place-items: center;
  background:
    radial-gradient(120% 120% at 82% -12%, rgba(191, 161, 92, .32), transparent 55%),
    linear-gradient(155deg, var(--green-900) 0%, var(--green-800) 60%, #20562a 100%);
  border: 1px solid rgba(191, 161, 92, .4);
  box-shadow: var(--shadow-md);
  animation: mark-in .5s var(--ease-out) both;
}
.auth-logo .mark img { width: 80%; height: 80%; animation: mark-turn 45s linear infinite; }
@keyframes mark-in { from { opacity: 0; transform: scale(.85); } }
@keyframes mark-turn { to { transform: rotate(1turn); } }
@media (prefers-reduced-motion: reduce) {
  .auth-logo .mark, .auth-logo .mark img, .boot img { animation: none; }
}
.auth-logo h1 { font-size: 23px; font-weight: 800; letter-spacing: -.3px; color: var(--green-800); margin-top: 14px; }
.auth-logo p { color: var(--ink-soft); font-size: 14px; }

/* ---------- Modal sheet ---------- */
.sheet-back {
  position: fixed; inset: 0;
  background: rgba(9, 24, 15, .5);
  z-index: 40;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  animation: dim-in var(--dur-fast) var(--ease-out);
}
@keyframes dim-in { from { background: rgba(9, 24, 15, 0); } to { background: rgba(9, 24, 15, .5); } }
.sheet {
  background: #fff;
  border-radius: 22px 22px 0 0;
  width: 100%;
  max-width: 720px;
  max-height: 88vh;
  overflow-y: auto;
  padding: 12px 16px calc(18px + var(--safe-bottom));
  animation: up var(--dur) var(--ease-out);
  box-shadow: 0 -10px 40px rgba(11, 46, 27, .18);
}
.sheet::before {
  content: '';
  display: block;
  width: 38px; height: 4px;
  border-radius: 999px;
  background: #dbe4dd;
  margin: 0 auto 14px;
}
@keyframes up { from { transform: translateY(40px); opacity: .5; } to { transform: none; opacity: 1; } }
/* Exit (2026-08-20): sheets slid in but blinked out. sheet() renames the
   backdrop to .sheet-out on close — so ".sheet-back" queries only ever match
   truly-open sheets — plays this, then removes the node. */
.sheet-out {
  position: fixed; inset: 0;
  z-index: 40;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(9, 24, 15, .5);
  pointer-events: none;
  animation: dim-out var(--dur-fast) var(--ease-out) forwards;
}
@keyframes dim-out { to { background: rgba(9, 24, 15, 0); } }
.sheet-out .sheet { animation: sheet-down var(--dur-fast) var(--ease-out) forwards; }
@keyframes sheet-down { to { transform: translateY(30px); opacity: 0; } }
.sheet h2 { font-size: 18px; font-weight: 800; letter-spacing: -.2px; margin-bottom: 14px; }

.seg {
  display: flex;
  position: relative;   /* anchors the sliding thumb */
  background: #e9efe9;
  border: 0;
  border-radius: 12px;
  padding: 3px;
  margin-bottom: 12px;
  box-shadow: inset 0 1px 2px rgba(13, 38, 24, .06);
}
/* Sliding thumb (2026-08-05): app.js keeps one .seg-thumb positioned under
   the active option; it glides on switch instead of the highlight teleporting.
   Where :has() is unsupported the button keeps its own white pill — same look,
   no slide. */
.seg .seg-thumb {
  position: absolute;
  top: 3px; bottom: 3px; left: 0;
  border-radius: 10px;
  background: #fff;
  box-shadow: 0 1px 4px rgba(13, 38, 24, .14);
  transition: transform var(--dur-spring) var(--ease-spring), width var(--dur) var(--ease-out);
  pointer-events: none;
}
.seg:has(.seg-thumb) button.on { background: none; box-shadow: none; }
.seg button {
  flex: 1; border: 0; background: none; padding: 10px 8px; border-radius: 10px;
  font-weight: 600; font-size: 14px; color: var(--ink-soft); cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}
.seg button.on {
  background: #fff;
  color: var(--accent-ink);
  font-weight: 700;
  box-shadow: 0 1px 4px rgba(13, 38, 24, .14);
}
.seg-4 button { font-size: 13px; padding: 8px 2px; }

/* Progress ring (2026-08-19) — how much of a job list is actually in the
   ground. It sits at the end of a row, so it takes the accent of whichever
   hub the list belongs to and needs no colour of its own. Complete flips to
   a plain green tick: "finished" should not need reading. */
.pring {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  line-height: 1;
}
/* Direct child only. `.pring svg` would also catch the tick icon nested in
   .pr-n and stand it on its side — a checkmark rotated -90deg reads as a
   chevron, which is what it did before this was scoped. */
.pring > svg { display: block; transform: rotate(-90deg); }
.pr-track { stroke: var(--line); }
.pr-fill { stroke: var(--accent-ink); transition: stroke-dashoffset var(--dur) var(--ease-out); }
.pring.done .pr-fill { stroke: var(--green-600); }
.pr-n {
  position: absolute;
  font-size: 10.5px;
  font-weight: 800;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.pring.done .pr-n { color: var(--green-600); }

/* A ticked line stays fully legible: struck-through text is hard to read in
   sun, so the row only lightens and the tick carries the state. */
.pli-done .title { color: var(--ink-soft); }
.tick.on { border-color: var(--green-600); background: var(--green-600); color: #fff; }
/* Grid parity: the tick rides the photo bottom-right — the ✕ owns top-left
   and the quantity badge top-right, so this is the one free corner. */
.cat-card .ph .tick-ov {
  position: absolute; bottom: 6px; right: 6px; z-index: 1;
  width: 30px; height: 30px;
  background: rgba(255, 255, 255, .92);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.cat-card .ph .tick-ov.on { background: var(--green-600); }
.cat-card.pli-done .nm { color: var(--ink-soft); }

/* "Mark up" sits opposite the close button, in the corner a right thumb
   reaches. Hidden while zoomed: at that point the finger is panning the
   photo and a button under it would be a mis-tap waiting to happen. */
.lb-mark {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  left: 12px;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, .16);
  color: #fff;
  font-size: 14px; font-weight: 600;
  padding: 11px 16px;
  display: flex; align-items: center; gap: 6px;
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.lb.zoomed .lb-mark { display: none; }

/* Photo markup (2026-08-19). A dark room around the photo, because the photo
   is the thing being judged and light chrome around it shifts how its colours
   read. Full-screen: drawing wants the whole slab. */
.mk-back {
  position: fixed; inset: 0; z-index: 70;
  background: #0b0f0d;
  display: flex; flex-direction: column;
}
.mk-bar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; }
.mk-top { padding-top: max(54px, calc(env(safe-area-inset-top, 0px) + 14px)); }
.mk-bottom {
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 12px);
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
}
.mk-btn {
  background: rgba(255, 255, 255, .12); border: 0; color: #fff;
  border-radius: 10px; padding: 9px 14px; font-size: 14px; font-weight: 600;
  cursor: pointer;
}
.mk-btn:disabled { opacity: .38; }
.mk-btn.primary { background: var(--green-600); }
.mk-stage {
  flex: 1; min-height: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 0 8px;
}
/* The canvas IS the drawing surface, so the browser must not read a drag on
   it as a scroll or a pinch — without this, a stroke pans the page instead. */
#mk-canvas {
  max-width: 100%; max-height: 100%;
  touch-action: none;
  border-radius: 8px;
  background: #000;
  cursor: crosshair;
}
.mk-tools { display: flex; gap: 8px; }
.mk-tool {
  background: rgba(255, 255, 255, .12); border: 0; color: #fff;
  border-radius: 10px; padding: 9px 13px; font-size: 13.5px; font-weight: 600;
  cursor: pointer;
}
.mk-tool.on { background: #fff; color: #10241a; }
.mk-colours { display: flex; gap: 8px; }
.mk-col {
  width: 32px; height: 32px; border-radius: 999px; cursor: pointer;
  border: 2px solid rgba(255, 255, 255, .35);
}
.mk-col.on { border-color: #fff; box-shadow: 0 0 0 3px rgba(255, 255, 255, .3); }

/* Delivery week (2026-08-19) — the in/out rows grouped by the day they are
   due. Each day is a card with a labelled header, and the header carries a
   tone so overdue and today register before the words are read. */
.dw-day { padding: 0; overflow: hidden; }
.dw-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  font-weight: 800;
  font-size: 13px;
  letter-spacing: .2px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.dw-head .dw-title { flex: 1; }
.dw-head .dw-count {
  min-width: 22px;
  text-align: center;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--card);
  color: var(--ink-soft);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
/* Today and overdue lead the eye — a hairline stripe, not a full wash.
   Text uses the ink tokens: --gold-deep measured 3.8:1 here in BOTH themes,
   and --red 2.3:1 in dark — the stripe keeps the brighter hue instead. */
.dw-head.gold { color: var(--amber-ink); box-shadow: inset 3px 0 0 var(--gold); }
.dw-head.red { color: var(--red-ink); box-shadow: inset 3px 0 0 var(--red); }
.dw-day .list.borderless { padding: 4px 6px 6px; }

/* Colour board (2026-08-19) — comparing two colours is the customer
   conversation, and the item page's small chips cannot settle it.
   Deliberately NO gap between compared tiles: a colour is judged against its
   neighbour, and a strip of page background between them is exactly what
   makes two similar greys look further apart than they really are. */
.cb-compare {
  display: flex;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.cb-big {
  flex: 1 1 0;
  min-width: 0;
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  display: flex;
  flex-direction: column;
}
/* Tall enough to read texture and blend, not just hue. */
.cb-big img { width: 100%; height: 132px; object-fit: cover; display: block; }
.cb-big-nm {
  padding: 7px 6px;
  font-size: 11.5px; font-weight: 700; line-height: 1.25;
  color: var(--ink);
  background: var(--card);
  text-align: center;
  overflow-wrap: anywhere;
}
.cb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; }
.cb-tile {
  border: 2px solid transparent;
  border-radius: 12px;
  overflow: hidden;
  padding: 0;
  background: var(--card);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-sm);
}
.cb-tile img { width: 100%; height: 74px; object-fit: cover; display: block; }
.cb-nm {
  padding: 6px 5px;
  font-size: 11px; font-weight: 600; line-height: 1.25;
  color: var(--ink-soft);
  text-align: center;
  overflow-wrap: anywhere;
}
/* Selected reads as selected without a tick covering the colour itself. */
.cb-tile.on { border-color: var(--accent-ink); }
.cb-tile.on .cb-nm { color: var(--accent-ink); font-weight: 800; }

/* Hub switcher: the four material hubs under the single Materials tab. */
.hubswitch {
  display: flex;
  gap: 6px;
  margin-bottom: 10px;
}
.hubswitch a {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 7px 2px 6px;
  border-radius: 11px;
  background: var(--card);
  border: 1px solid rgba(20, 60, 35, .08);
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-soft);
  text-decoration: none;
}
.hubswitch a .hi { font-size: 17px; line-height: 1; }
.hubswitch a.on {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-ink);
  font-weight: 700;
}

pre.console {
  background: #10241a; color: #cfe8d8;
  border-radius: 11px; padding: 12px;
  font-size: 12px; overflow-x: auto;
  font-family: ui-monospace, Consolas, monospace;
  white-space: pre-wrap; word-break: break-word;
  max-height: 300px; overflow-y: auto;
}

/* ---------- Photo viewer (pinch-zoom lightbox) ----------
   Any <img data-zoom> opens here. The app carries ~2,300 photos — colour-
   accurate paver swatches and plant shots — that were only ever visible at
   44px. This is where they're actually readable: pinch or double-tap to zoom,
   drag to pan, swipe down to dismiss. Photos are cache-first in the service
   worker, so this works offline on whatever the phone has already seen. */
.lb {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(5, 14, 9, .93);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  touch-action: none;           /* we drive every gesture ourselves */
  animation: lb-in var(--dur) var(--ease-out);
  -webkit-user-select: none; user-select: none;
}
@keyframes lb-in { from { opacity: 0; } to { opacity: 1; } }
.lb.closing { animation: lb-out var(--dur-fast) var(--ease-out) forwards; }
@keyframes lb-out { to { opacity: 0; } }

.lb img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  transform-origin: 0 0;
  will-change: transform;
  -webkit-user-drag: none;
  /* Entrance fades ONLY — never transform. A running CSS animation outranks an
     inline style, so animating transform here would mask the viewer's own
     translate/scale for as long as it ran (and forever in any context where
     frames aren't being produced, e.g. a backgrounded tab). Transform has
     exactly one owner: the pinch/pan code. */
  animation: lb-fade var(--dur) var(--ease-out);
}
@keyframes lb-fade { from { opacity: .3; } to { opacity: 1; } }
/* While a finger is down we kill the transition so tracking is 1:1; the class
   comes off on release so the snap-back eases. */
.lb img.settling { transition: transform var(--dur) var(--ease-out); }

.lb-close {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  right: 12px;
  width: 42px; height: 42px;
  border-radius: 999px;
  border: 0;
  background: rgba(255, 255, 255, .16);
  color: #fff;
  font-size: 22px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.lb-close:active { background: rgba(255, 255, 255, .3); }

.lb-cap {
  position: absolute;
  left: 0; right: 0;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 14px);
  padding: 0 20px;
  text-align: center;
  color: rgba(255, 255, 255, .92);
  font-size: 13px;
  font-weight: 600;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .6);
  pointer-events: none;
  transition: opacity var(--dur) var(--ease-out);
}
.lb.zoomed .lb-cap { opacity: 0; }

.lb-hint {
  position: absolute;
  left: 0; right: 0;
  top: calc(env(safe-area-inset-top, 0px) + 18px);
  text-align: center;
  color: rgba(255, 255, 255, .55);
  font-size: 12px;
  font-weight: 600;
  pointer-events: none;
  animation: lb-hint-fade 2.8s var(--ease-out) forwards;
}
@keyframes lb-hint-fade { 0%, 55% { opacity: 1; } 100% { opacity: 0; } }

/* Every served photo is zoomable by its URL rather than by a hand-added
   attribute, so screens nobody remembered to tag get it too. `data-zoom` stays
   as an explicit opt-in, and `data-nozoom` opts a photo out. */
img[src^="/api/photos/file/"],
img[src^="/api/gallery/file/"],
img[data-zoom] {
  cursor: zoom-in;
  transition: transform var(--dur-spring) var(--ease-spring);
}
img[src^="/api/photos/file/"]:active,
img[src^="/api/gallery/file/"]:active,
img[data-zoom]:active { transform: scale(.96); }
img[data-nozoom] { cursor: default; }
img[data-nozoom]:active { transform: none; }

/* ---------- Catalog grid (image-forward cards) ----------
   Shown instead of text rows on catalogs whose items actually have pictures.
   Gated on measured coverage, not hard-coded per hub: construction and
   irrigation currently have 0% photo coverage, and 1,182 identical placeholder
   tiles would be worse than the list it replaced. */
.cat-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 11px;
}
@media (min-width: 560px) { .cat-grid { grid-template-columns: repeat(3, 1fr); } }

.cat-card {
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid rgba(18, 74, 41, .08);
  border-radius: var(--radius);
  overflow: hidden;
  padding: 0;
  text-align: left;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-spring) var(--ease-spring), box-shadow var(--dur) var(--ease-out);
  color: inherit;
}
.cat-card:active { transform: scale(.965); box-shadow: var(--shadow-md); }

.cat-card .ph {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  background: linear-gradient(160deg, var(--green-50), var(--green-100));
  overflow: hidden;
}
.cat-card .ph img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  /* The card owns the tap; the viewer is opened from the card's own handler so
     a mis-aimed thumb still navigates rather than doing nothing. */
  pointer-events: none;
}
/* Photo-less items get the hub emoji rather than an empty hole. */
.cat-card .ph .ph-fallback {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 34px; opacity: .35;
}
/* A soft floor under the picture so white product shots don't bleed into the
   white card. */
.cat-card .ph::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0; height: 34%;
  background: linear-gradient(to top, rgba(6, 36, 20, .16), transparent);
  pointer-events: none;
}

/* Inventory & job-list cards: the quantity rides the photo as a badge, and the
   editors' ✕ overlays the opposite corner. Both sit above the ::after scrim. */
.cat-card .ph .qtyb {
  position: absolute; top: 8px; right: 8px; z-index: 1;
  background: rgba(8, 22, 14, .62); color: #fff;
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  border-radius: 999px; padding: 3px 9px;
  font-size: 12px; font-weight: 800;
  max-width: 78%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cat-card .ph .qtyb.low { background: var(--red); }

/* Quick-adjust stepper: +/− beside any inventory quantity. Spans styled as
   buttons (they live inside row/card <button>s, so real buttons would nest
   illegally); 34px circles keep the touch-target pass honest. */
.qstep { display: inline-flex; gap: 6px; align-items: center; flex-shrink: 0; }
.qs-b {
  width: 34px; height: 34px; border-radius: 999px;
  background: var(--green-50); border: 1.5px solid var(--line); color: var(--green-800);
  font-size: 18px; font-weight: 800; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; user-select: none; -webkit-user-select: none;
  transition: transform var(--dur-spring) var(--ease-spring), background var(--dur-fast) var(--ease-out);
}
.qs-b:active { background: var(--green-100); transform: scale(.92); }
.qty-pill[data-qset], .qtyb[data-qset] { cursor: pointer; }

/* Toasts: transient feedback pinned above the tab bar. */
.toast {
  position: fixed; left: 50%; bottom: calc(76px + env(safe-area-inset-bottom, 0px));
  transform: translate(-50%, 16px); opacity: 0;
  background: var(--green-900, #0d3018); color: #fff;
  border-radius: 999px; padding: 11px 20px;
  font-size: 14px; font-weight: 600; max-width: 86vw;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .35);
  transition: transform var(--dur-spring) var(--ease-spring), opacity var(--dur) var(--ease-out);
  z-index: 300; pointer-events: none;
}
.toast.on { transform: translate(-50%, 0); opacity: 1; }
.toast.error { background: var(--red, #b3372c); }
/* Undo (2026-08-07): the toast itself stays click-through, the action doesn't. */
.toast { display: flex; align-items: center; gap: 4px; }
.toast .toast-act {
  pointer-events: auto;
  background: none; border: 0; cursor: pointer;
  color: var(--gold-light, #e6c76a);
  font-size: 14px; font-weight: 800;
  padding: 6px 2px 6px 12px;
  margin: -6px 0;
  text-transform: uppercase; letter-spacing: .04em;
}

/* Swipe-to-act: past the threshold the row tints to say "release does it". */
.row.swipe-armed { background: var(--green-50); }

/* Screen cross-fade (View Transitions API; ignored by older phones). */
::view-transition-old(root), ::view-transition-new(root) { animation-duration: var(--dur-fast); }

/* Persistent chrome opts out of the root snapshot (2026-08-20): with their own
   names, the top and tab bars hold perfectly still while the content between
   them cross-fades — snapshotting fixed/sticky chrome inside root is what
   makes a web app's navigation read as a whole-page blink. The topbar's title
   still changes, so its own old/new pair fades on the same fast clock. */
.topbar { view-transition-name: topbar; }
.tabbar { view-transition-name: tabbar; }
::view-transition-old(topbar), ::view-transition-new(topbar),
::view-transition-old(tabbar), ::view-transition-new(tabbar) { animation-duration: var(--dur-fast); }

/* Shared-element morph (2026-08-05): the tapped card photo (tagged item-hero
   by app.js) grows into the item page's hero. The seg buttons stay above the
   thumb; the hero name lives here so every item page gets it for free. */
.item-hero img { view-transition-name: item-hero; }
::view-transition-group(item-hero) { animation-duration: var(--dur-slow); animation-timing-function: var(--ease-out); }
::view-transition-old(item-hero), ::view-transition-new(item-hero) { height: 100%; object-fit: cover; }
.seg button { position: relative; z-index: 1; }

/* Lazily loaded photos fade in instead of popping (img.ld set on load). */
img[loading="lazy"] { opacity: 0; transition: opacity var(--dur-slow) var(--ease-out); }
img[loading="lazy"].ld { opacity: 1; }
.cat-card .ph .qstep { position: absolute; bottom: 8px; right: 8px; z-index: 1; }
.cat-card .ph .qstep .qs-b { background: rgba(255, 255, 255, .94); border: 0; box-shadow: 0 2px 8px rgba(0, 0, 0, .28); }
.cat-card .ph .rm {
  position: absolute; top: 6px; left: 6px; z-index: 1;
  width: 28px; height: 28px; border-radius: 999px; border: 0;
  background: rgba(255, 255, 255, .92); color: var(--red);
  font-size: 13px; font-weight: 800; cursor: pointer;
  box-shadow: var(--shadow-sm);
}
.cat-card .mtw { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; font-size: 11px; color: var(--ink-soft); }
/* Right-aligned list/grid toggle row above inventories and job lists. */
.cv-row { display: flex; justify-content: flex-end; margin-bottom: 8px; }

.cat-card .bd { padding: 9px 10px 11px; display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.cat-card .nm {
  font-size: 13px; font-weight: 700; line-height: 1.3; letter-spacing: -.1px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.cat-card .mt {
  font-size: 11.5px; font-weight: 600; color: var(--ink-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cat-card .pr { color: var(--green-700); font-weight: 800; }

/* ---------- Home: material tiles ----------
   The 2×2 grid was four flat white boxes. Each hub now carries its own tint
   and its live catalog size, so the home screen says something rather than
   just offering four doors. */
.tile.hub {
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  padding: 13px 13px 12px;
  position: relative;
  overflow: hidden;
}
.tile.hub .ti { width: 38px; height: 38px; font-size: 19px; margin-bottom: 9px; }
.tile.hub .tl { font-size: 14.5px; font-weight: 800; letter-spacing: -.2px; }
.tile.hub .ct {
  font-size: 11.5px; font-weight: 700; color: var(--ink-soft);
  margin-top: 1px; font-variant-numeric: tabular-nums;
}
/* A wash of the hub's own colour, kept faint so the type stays the loudest
   thing on the tile. */
.tile.hub::after {
  content: '';
  position: absolute; inset: 0;
  background: radial-gradient(120% 90% at 100% 0%, var(--hub-tint, transparent), transparent 62%);
  pointer-events: none;
}
.tile.hub.h-plants { --hub-tint: rgba(52, 135, 69, .16); }
.tile.hub.h-irr { --hub-tint: rgba(23, 138, 118, .16); }
.tile.hub.h-hard { --hub-tint: rgba(122, 118, 104, .18); }
.tile.hub.h-con { --hub-tint: rgba(172, 94, 48, .16); }

/* ---------- Truck check: progress in the row ----------
   A morning check is a progress task, so the row shows progress instead of
   spelling out "17 of 82 checked" and making you do the arithmetic. */
.trow { display: block; }
.trow .tline { display: flex; align-items: center; gap: 12px; }
.trow .tbar {
  height: 6px; border-radius: 999px; background: var(--green-100);
  overflow: hidden; margin: 9px 0 1px;
}
.trow .tbar i {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--green-700), var(--green-600));
  border-radius: 999px;
  transition: width var(--dur-slow) var(--ease-out);
}
.trow.ready .tbar i { background: linear-gradient(90deg, var(--green-600), #46b061); }
.trow .tmeta {
  display: flex; justify-content: space-between; gap: 8px;
  font-size: 11.5px; font-weight: 600; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.trow .tmeta .warn { color: var(--red-ink); font-weight: 700; }

/* Per-area progress on the truck detail, so a collapsed area still tells you
   whether it's done. */
.area-prog {
  font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--ink-soft); background: var(--green-50);
  border: 1px solid var(--line); border-radius: 999px; padding: 1px 8px;
}
.area-prog.done { background: var(--green-100); border-color: #bcd9c6; color: var(--green-800); }

/* ---------- Crew: message identity ----------
   Every bubble looked the same, so a busy feed was a wall. An initial disc
   colour-keyed to the author makes scanning possible. */
.msg-row { display: flex; gap: 8px; align-items: flex-end; margin-bottom: 8px; }
.msg-row.mine { flex-direction: row-reverse; }
.msg-row .who {
  width: 28px; height: 28px; border-radius: 999px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 11.5px; font-weight: 800; color: #fff;
  margin-bottom: 2px;
}
.msg-row .msg { margin-bottom: 0; }
.msg-row.mine .who { background: var(--green-700) !important; }

/* ---------- Catalog item page ----------
   The photo leads. It used to sit below the calculator and the whole spec
   sheet, so you scrolled past everything to see the thing you were choosing. */
.item-hero {
  position: relative;
  margin: -16px -14px 12px;   /* bleed past .content's padding */
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: linear-gradient(160deg, var(--green-50), var(--green-100));
}
.item-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.item-hero .scrim {
  position: absolute; inset: auto 0 0 0; height: 72%;
  background: linear-gradient(to top, rgba(4, 20, 11, .92), rgba(4, 20, 11, .55) 42%, transparent);
  pointer-events: none;
}
.item-hero .txt { position: absolute; left: 16px; right: 16px; bottom: 13px; color: #fff; pointer-events: none; }
.item-hero .ln {
  font-size: 11px; font-weight: 800; letter-spacing: 1.1px;
  text-transform: uppercase; color: var(--gold-light);
}
.item-hero .nm { font-size: 20px; font-weight: 800; letter-spacing: -.3px; line-height: 1.2; margin-top: 3px; }
.item-hero .zm {
  position: absolute; top: 12px; right: 12px;
  background: rgba(8, 22, 14, .55); color: #fff;
  border-radius: 999px; padding: 5px 11px;
  font-size: 11px; font-weight: 700;
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  pointer-events: none;
}
/* No photo (irrigation and construction are at 0% coverage): a plain header,
   not an empty grey box pretending to be a hero. */
.item-head { margin-bottom: 12px; }
.item-head .ln { font-size: 11px; font-weight: 800; letter-spacing: 1.1px; text-transform: uppercase; color: var(--green-700); }
.item-head .nm { font-size: 21px; font-weight: 800; letter-spacing: -.4px; line-height: 1.22; margin-top: 2px; }

/* The two or three numbers people actually came for. */
.facts {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 12px;
}
.facts.n2 { grid-template-columns: repeat(2, 1fr); }
.facts.n3 { grid-template-columns: repeat(3, 1fr); }
.facts .fact { background: var(--card); padding: 11px 8px; text-align: center; }
.facts .v { font-size: 17px; font-weight: 800; letter-spacing: -.3px; font-variant-numeric: tabular-nums; }
.facts .v.pr { color: var(--green-700); }
.facts .k {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .5px; color: var(--ink-soft); margin-top: 1px;
}

/* Colour chips carry a swatch dot. The dot is deliberately neutral — see the
   note in specsCardHTML on why we don't invent hex values for finishes the
   crew has to match on site. */
.cchip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 11px 4px 5px; margin: 3px 5px 3px 0;
  border: 1.5px solid var(--line); border-radius: 999px;
  font-size: 12px; font-weight: 600;
  background: var(--card); color: inherit;
  cursor: default;
}
/* The swatch is a real photograph of that colour — a sibling catalog item in
   the same line — never a guessed hex. */
.cchip i {
  width: 20px; height: 20px; border-radius: 999px; flex-shrink: 0;
  background: linear-gradient(140deg, #b9bcb4, #8e918a);
  background-size: cover; background-position: center;
  box-shadow: inset 0 0 0 1px rgba(6, 36, 20, .12);
}
.cchip.mine {
  border-color: var(--green-600); color: var(--green-800);
  font-weight: 800; background: var(--green-50);
}
/* Carried colours are tappable — they go to that colour's own item page. */
button.cchip { cursor: pointer; transition: transform var(--dur-spring) var(--ease-spring); }
button.cchip:active { transform: scale(.95); }
/* Not in our catalog: shown, because the crew should know the line offers it,
   but unmistakably not orderable. */
.cchip.nostock {
  border-style: dashed;
  color: var(--ink-soft);
  background: none;
}
.cchip.nostock i { opacity: .35; }
.cchip .nx { font-size: 10px; font-weight: 700; opacity: .8; }
.cstock-note { font-size: 11px; color: var(--ink-soft); margin-top: 6px; }

/* Sizes as scannable rows instead of one wall of text. */
.szrow {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid var(--green-50);
  font-size: 12.5px;
}
.szrow:last-of-type { border-bottom: 0; }
.szrow b { font-weight: 700; }
.szrow span { color: var(--ink-soft); white-space: nowrap; font-variant-numeric: tabular-nums; }
.szmore {
  font-size: 12px; font-weight: 800; color: var(--green-700);
  background: none; border: 0; padding: 8px 0 0; cursor: pointer;
}
.appchip {
  display: inline-block; margin: 3px 4px 0 0;
  background: var(--green-50); border: 1px solid var(--green-100);
  color: var(--green-800); border-radius: 999px;
  padding: 3px 10px; font-size: 11.5px; font-weight: 700;
}

/* List ⇄ grid switch, sat next to the result count. */
.viewtog {
  display: inline-flex;
  border: 1px solid var(--line);
  background: var(--card);
  border-radius: 999px;
  overflow: hidden;
  flex-shrink: 0;
}
.viewtog button {
  border: 0; background: none; cursor: pointer;
  /* Was 6px, giving a 27px-tall target — small for a gloved thumb, on the
     screens crews use most. 11px takes it to 37, matching the Add/Sync
     buttons beside it (37-39px), so the toolbar lines up as well as it taps.
     A halo like .back-btn's can't work here: .viewtog clips to its pill with
     overflow:hidden, which cuts any ::after extension off. */
  padding: 11px; font-size: 14px; line-height: 1;
  color: var(--ink-soft);
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.viewtog button.on { background: var(--accent-tint); color: var(--accent-ink); }

@media (min-width: 720px) {
  .tabbar { max-width: 720px; left: 50%; transform: translateX(-50%) translateY(var(--vv-shift, 0px)); border-left: 1px solid var(--line); border-right: 1px solid var(--line); }
}

/* ---------- Desktop detail pane (2026-08-07, >=1000px only) ----------
   A catalog item beside the list instead of over it. The list stays live;
   #app squeezes left while the pane is up. Never rendered on phones. */
.side-pane {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(560px, 46vw);
  background: var(--bg, #fff);
  border-left: 1px solid var(--line);
  box-shadow: -14px 0 40px rgba(13, 38, 24, .12);
  z-index: 25; /* above content, below sheets (40) and the photo viewer */
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 18px calc(30px + var(--safe-bottom));
  animation: pane-in var(--dur) var(--ease-out);
}
@keyframes pane-in { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }
.sp-top {
  position: sticky; top: 0;
  display: flex; justify-content: flex-end;
  padding: 10px 0 4px;
  background: var(--bg, #fff);
  z-index: 2;
}
.sp-close {
  width: 36px; height: 36px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.sp-close:active { background: var(--green-50); }
@media (min-width: 1000px) {
  body.pane-open #app { padding-right: min(560px, 46vw); transition: padding-right var(--dur) var(--ease-out); }
  body.pane-open .tabbar { transform: translateX(calc(-50% - min(280px, 23vw))) translateY(var(--vv-shift, 0px)); }
}
@media (prefers-reduced-motion: reduce) { .side-pane { animation: none; } }

/* ---------- Print (2026-08-19) ----------
   Suppliers and county inspectors still ask for paper. Ctrl+P (or the iOS
   share sheet) on a job list should produce a clean materials document, not
   a screenshot of an app: chrome and controls disappear, the topbar becomes
   a plain document header, rows keep their photos and stop splitting across
   page breaks. */
@media print {
  .tabbar, .hubswitch, .seg, .search-wrap, .fab, .update-bar, .toast, .net-bar,
  .back-btn, .btn, .btn-row, .rm, .chev, .gs-open, .recent-strip, .side-pane,
  .viewtog, .sheet-back, .ptr, .scrolltop { display: none !important; }
  body { background: #fff !important; color: #000; }
  .topbar {
    position: static;
    background: none !important;
    color: #000;
    box-shadow: none;
    border-radius: 0;
    border-bottom: 2px solid #000;
    padding: 0 0 10px;
  }
  .topbar::after { display: none; }
  .topbar .sub { color: #555; }
  .screen { padding-bottom: 0; }
  .content { max-width: none; padding: 12px 0 0; animation: none; }
  .card, .row, .listline { break-inside: avoid; box-shadow: none; }
  /* QR labels: excluded ones vanish; the Print/All/None toolbar is .btn so
     it is already hidden; keep the grid tight for cutting. */
  .qr-label.off { display: none; }
  .qr-label { border-radius: 0; }
  .qr-sheet { gap: 0; }
  .card { border: 1px solid #bbb; }
  .list .row, button.row { border-bottom: 1px solid #ccc; color: #000; }
  .cat-grid { grid-template-columns: repeat(4, 1fr); }
  a { color: inherit; text-decoration: none; }
}

/* ---------- Dark mode (premium-UI pass, 2026-07-31) ----------
   The OS decides (prefers-color-scheme). Brand greens and gold hold their
   ground; surfaces drop to deep green-blacks and text lifts. Photos stay at
   full brightness. Tint/chip pairs are re-stated where their light values
   were hardcoded rather than tokenized. */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0c1310;
    --card: #16211b;
    --surface-2: #1c2921;
    --ink: #e7eee9;
    --ink-soft: #97a79d;
    --line: #26352c;
    --green-50: #1a2a20;
    --green-100: #21402c;
    --red-bg: #3a1713;
    --red-ink: #f0998f;
    --amber-bg: #352a10;
    --amber-ink: #e3c464;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4), 0 2px 8px rgba(0, 0, 0, .3);
    --shadow-md: 0 2px 6px rgba(0, 0, 0, .45), 0 12px 30px rgba(0, 0, 0, .4);
    --shadow-lg: 0 4px 12px rgba(0, 0, 0, .5), 0 24px 60px rgba(0, 0, 0, .55);
  }
  body { color: var(--ink); }
  .tile { background: linear-gradient(170deg, var(--card), var(--surface-2)); }
  .tick { background: var(--surface-2); border-color: #3a4b40; }
  input[type=text], input[type=password], input[type=number], input[type=date], input[type=search], select, textarea { background: var(--surface-2); color: var(--ink); }
  input:focus, select:focus, textarea:focus { background: var(--card); }
  .btn.secondary { background: var(--card); color: #9fd8b4; }
  .btn.danger { background: var(--card); color: #f1968b; border-color: rgba(241, 150, 139, .4); }
  .skel-b { background: linear-gradient(90deg, #1b271f 25%, #24322a 45%, #1b271f 65%); background-size: 200% 100%; }
  .ptr { background: var(--card); }
  .ptr.ready { background: #33290f; color: var(--gold-light); }
  .sheet { background: var(--card); }
  /* The grab handle is the ::before pseudo-element — there is no .grab node. */
  .sheet::before { background: #3a4b40; }
  /* Selected-state pairs. Light mode couples a mint tint (--green-100) with
     deep green text (--green-800); in dark the tint flips to deep green so the
     text must flip to mint — restated for every control that uses the pair. */
  /* The accent pair flips with the surface: in light the tint is pale and the
     ink is dark, in dark that inverts. Restated per hub here so the dark
     palette gets the same care as the light one — never a naive inversion. */
  :root { --accent-tint: var(--green-100); --accent-ink: #a9e2bf; --accent-line: #3c6a4b; }
  .screen[data-hub="irrigation"] { --accent-tint: #10403a; --accent-ink: #87d9c8; --accent-line: #26635a; }
  .screen[data-hub="hardscape"] { --accent-tint: #34322c; --accent-ink: #ccc8bd; --accent-line: #54524a; }
  .screen[data-hub="construction"] { --accent-tint: #40291a; --accent-ink: #eec49f; --accent-line: #66422c; }
  .seg { background: #0f1a15; box-shadow: inset 0 1px 3px rgba(0, 0, 0, .5); }
  .seg button.on { background: var(--accent-tint); color: var(--accent-ink); box-shadow: 0 1px 4px rgba(0, 0, 0, .4); }
  .seg .seg-thumb { background: var(--accent-tint); box-shadow: 0 1px 4px rgba(0, 0, 0, .4); }
  .seg:has(.seg-thumb) button.on { background: none; box-shadow: none; }
  .hubswitch a { border-color: rgba(255, 255, 255, .06); }
  .hubswitch a.on { border-color: var(--accent-line); color: var(--accent-ink); }
  .viewtog button.on { color: var(--accent-ink); }
  .tabbar a.active { color: #a9e2bf; }
  .wx-p.wet { color: #83b3e8; }
  .wx-line.dry { color: #a9e2bf; }
  .auth-logo h1 { color: #a9e2bf; }
  .fchip { border-color: #3c6a4b; color: #a9e2bf; }
  .fchip:active { background: #2b4a37; }
  .msg.mine { background: linear-gradient(160deg, #1d3527, var(--green-100)); border-color: #31543e; }
  input[type=text], input[type=password], input[type=number], input[type=date], input[type=search], select, textarea { border-color: var(--line); }
  .net-bar { border-bottom-color: #5a4816; }
  .cat-card .pr, .facts .v.pr { color: #5ecd89; }
  .ti-green { color: #86d3a2; }
  .ti-teal { background: #11332e; color: #74cdbf; }
  .ti-gold { background: #362c10; color: #e0c162; }
  .ti-blue { background: #16283c; color: #83b3e8; }
  .ti-red { color: #f2938a; }
  .ti-neutral { background: #232d26; color: #9daba2; }
  .chip.green { border-color: #2e5039; color: #9adfb4; }
  .chip.gold { border-color: #57451c; color: #e5c96e; }
  .chip.blue { background: #16283c; border-color: #27436b; color: #8fc0f0; }
  .chip.purple { background: #2b2138; border-color: #453363; color: #c9aef0; }
  .chip.red { border-color: #5c2a23; }
  .qual-poor { border-color: #5c2a23; }
  .qual-rehab { background: #3a2a12; color: #eab868; border-color: #57431c; }
  .qual-medium { border-color: #57451c; }
  .qual-good { border-color: #2e5039; color: #9adfb4; }
  .qual-dividable { background: #1a2740; color: #9db8f0; border-color: #2c406b; }
  .tabbar { background: rgba(15, 23, 19, .92); border-top-color: rgba(255, 255, 255, .07); box-shadow: 0 -4px 20px rgba(0, 0, 0, .45); }
  .tabbar a .badge { border-color: #131c17; }
  .scrolltop { background: rgba(24, 34, 28, .94); color: #9fd8b4; }
  .pr-check { background: rgba(24, 34, 28, .85); }
  .qs-b { background: var(--green-50); color: #9fd8b4; }
  .card { border-color: rgba(255, 255, 255, .05); }
  .ok-box { color: #9adfb4; }
  .card h3 .ic, h2 .ic, .row .title .ic, .empty .big .ic { color: #86d3a2; }
  .strong-green { color: #9adfb4; }
}

/* ---------- Slice D polish (2026-07-31) ---------- */
/* Native selects wear the input styling instead of OS chrome. */
select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2355655b' stroke-width='2.5' 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 11px center;
  background-size: 15px;
  padding-right: 34px;
}
@media (prefers-color-scheme: dark) {
  select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2397a79d' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  }
}
/* Photo-less catalog/inventory cards: faint brand emblem instead of a
   floating emoji (same treatment the public shop got). */
.cat-card .ph .ph-fallback img { width: 54px; height: 54px; opacity: .22; }
@media (prefers-color-scheme: dark) { .cat-card .ph .ph-fallback img { opacity: .3; filter: brightness(1.6); } }

/* ---------- Slice E: disclosure sections (2026-07-31) ---------- */
/* Calculator sub-sections and grouped admin settings use native <details>
   so the wiring inside never changes — content stays in the DOM. */
details.calc-sec > summary, details.set-group > summary {
  list-style: none;
  cursor: pointer;
  font-weight: 800;
  padding: 11px 2px;
  display: flex;
  align-items: center;
  user-select: none;
  -webkit-user-select: none;
}
details.calc-sec > summary::-webkit-details-marker, details.set-group > summary::-webkit-details-marker { display: none; }
details.calc-sec > summary::after, details.set-group > summary::after {
  content: '';
  width: 8px; height: 8px;
  border-right: 2px solid var(--ink-soft);
  border-bottom: 2px solid var(--ink-soft);
  transform: rotate(45deg);
  margin-left: auto;
  margin-right: 4px;
  transition: transform var(--dur-fast) var(--ease-out);
}
details.calc-sec[open] > summary::after, details.set-group[open] > summary::after { transform: rotate(225deg); }
details.calc-sec { border-top: 1px solid var(--line); margin-top: 10px; }
details.set-group > summary {
  background: var(--card);
  border: 1px solid rgba(18, 74, 41, .08);
  border-radius: var(--radius);
  padding: 15px 16px;
  box-shadow: var(--shadow-sm);
  margin-bottom: 12px;
  font-size: 16px;
}
details.set-group[open] > summary { margin-bottom: 8px; }
details.set-group > .card { margin-left: 8px; }
@media (prefers-color-scheme: dark) { details.set-group > summary { border-color: rgba(255, 255, 255, .05); } }

/* ---------- Field mode (2026-08-19) ----------
   Bigger type, bigger targets, harder contrast — for full sun and gloved
   thumbs. Toggled per phone in Settings; the class rides <html> so every
   screen and sheet inherits it. Overrides are deliberately a SHORT list of
   the things a crew actually reads at arm's length, not a reflow of the
   whole design: the layout must stay recognisably the same app. */
html.field-mode { --ink-soft: #3d4e44; --line: #c2d1c8; }
html.field-mode body { font-size: 17px; }           /* inputs/selects inherit */
html.field-mode .row .title { font-size: 17px; }
html.field-mode .row .sub { font-size: 14.5px; }
html.field-mode .row { min-height: 56px; }
html.field-mode .row .chev { color: #8b9c90; }
html.field-mode .muted { font-size: 14.5px; }
html.field-mode .fs12 { font-size: 13.5px; }
html.field-mode .fs13 { font-size: 14.5px; }
html.field-mode .cat-card .nm { font-size: 15.5px; }
html.field-mode .cat-card .pr { font-size: 14.5px; }
html.field-mode .btn { font-size: 16.5px; padding: 14px 18px; }
html.field-mode .btn.small { font-size: 15px; padding: 10px 15px; }
html.field-mode .chip { font-size: 13.5px; padding: 4px 12px; }
html.field-mode .qty-pill { font-size: 16px; padding: 6px 13px; }
html.field-mode .seg button { font-size: 15.5px; padding: 12px 8px; }
html.field-mode .seg-4 button { font-size: 14px; }
html.field-mode .hubswitch a { font-size: 12.5px; padding: 9px 2px 8px; }
html.field-mode .tabbar a { font-size: 12.5px; }
html.field-mode .tabbar a .ico { font-size: 23px; }
html.field-mode .search-wrap input { font-size: 17px; }
html.field-mode .f > span { font-size: 14px; }
/* Pass 2 (2026-08-20), to the ruggedized spec: the arm's-length DATA the
   first pass skipped — the facts strip, KV values, weather chips, size rows
   and calculator totals — plus the critical tap targets lifted toward 48px
   (research puts gloved fingers at 20-25mm of precision; 37px buttons miss). */
html.field-mode .facts .v { font-size: 19px; }
html.field-mode .facts .k { font-size: 11px; }
html.field-mode .kv .k { font-size: 13px; }
html.field-mode .kv .v { font-size: 16.5px; }
html.field-mode .wx-day { flex-basis: 60px; }
html.field-mode .wx-dow, html.field-mode .wx-lo, html.field-mode .wx-p { font-size: 12.5px; }
html.field-mode .wx-t { font-size: 16px; }
html.field-mode .wx-line { font-size: 14.5px; }
html.field-mode .calc-total { font-size: 17px; }
html.field-mode .szrow { font-size: 14px; }
html.field-mode .qs-b { width: 40px; height: 40px; }
html.field-mode .viewtog button { padding: 13px; }
html.field-mode .back-btn { width: 44px; height: 44px; }
html.field-mode button.tick::after { inset: -12px; }
/* Dark keeps its own contrast bump — the light values would vanish there. */
@media (prefers-color-scheme: dark) {
  html.field-mode { --ink-soft: #b7c6bc; --line: #35473c; }
  html.field-mode .row .chev { color: #7e8f84; }
}

/* Position pill for item-to-item swiping — quiet, non-interactive, above the
   tab bar. It answers "where am I in the list" and hints the gesture without
   shouting about it. */
.swipe-pos {
  position: fixed;
  left: 50%;
  bottom: calc(84px + var(--safe-bottom));
  transform: translateX(-50%);
  background: rgba(10, 34, 20, .72);
  color: #e6f2ea;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .4px;
  font-variant-numeric: tabular-nums;
  padding: 5px 13px;
  border-radius: 999px;
  pointer-events: none;
  z-index: 18;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* Reorder mode (2026-08-19). The handle owns its touches (touch-action:none)
   so a drag never fights the page scroll; everywhere else in the row scrolls
   normally, which is why the handle is the ONLY drag surface. */
.ro-handle {
  touch-action: none;
  cursor: grab;
  font-size: 22px;
  color: var(--ink-soft);
  padding: 8px 12px;
  margin: -8px 0 -8px -6px;
  user-select: none;
  -webkit-user-select: none;
}
.ro-row.drag-on {
  opacity: .92;
  box-shadow: var(--shadow-md);
  background: var(--card);
  border-radius: 12px;
  transform: scale(1.01);
}

/* Bed drawer canvas + readout. Same dark room as photo markup (it reuses the
   mk- chrome); the canvas owns its touches like every drawing surface here. */
.ad-canvas { max-width: 100%; max-height: 100%; touch-action: none; border-radius: 8px; cursor: crosshair; }
.bd-read { color: #e6f2ea; font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* Asymmetric press (2026-08-20): pressing snaps down fast, releasing springs
   back on the full spring settle — that asymmetry is what makes a control feel
   like a physical button. Base rules carry the sprung release; this one late
   rule makes every press-in immediate. */
.tile:active, .cat-card:active, .stat:active, .qual-pick:active, .btn:active,
.qs-b:active, .fab:active, button.cchip:active, .scrolltop.show:active,
img[src^="/api/photos/file/"]:active, img[src^="/api/gallery/file/"]:active,
img[data-zoom]:active { transition-duration: var(--dur-press); }

/* Completion moment (2026-08-20): a self-drawing check for the big beats ONLY
   — PO pushed, a job list reaching 100%, a delivery received. successMark()
   in app.js. Deliberately no confetti anywhere: routine work stays quiet. */
.smark { position: fixed; inset: 0; z-index: 320; display: grid; place-items: center; pointer-events: none; }
.smark .disc {
  width: 92px; height: 92px;
  border-radius: 999px;
  background: var(--green-700);
  display: grid; place-items: center;
  box-shadow: var(--shadow-lg);
  animation: smark-pop var(--dur-spring) var(--ease-spring);
}
.smark path {
  stroke: #fff; stroke-width: 3.4; fill: none;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 29; stroke-dashoffset: 29;
  animation: smark-draw .4s var(--ease-out) .1s forwards;
}
@keyframes smark-pop { from { transform: scale(.5); opacity: 0; } }
@keyframes smark-draw { to { stroke-dashoffset: 0; } }
.smark.out { animation: lb-out var(--dur) var(--ease-out) forwards; }

/* Anchored section chips (2026-08-20): long item pages get a sticky row of
   section jumps under the topbar (the Procore daily-log pattern). Rides the
   same stale-viewport correction as the search bar, or the pair splits. */
.anchor-chips {
  position: sticky;
  top: var(--topbar-h, 58px);
  transform: translateY(var(--vv-top-shift, 0px));
  z-index: 14;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 8px 2px 7px;
  margin-bottom: 4px;
  background: var(--bg);
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.anchor-chips::-webkit-scrollbar { display: none; }
.achip {
  flex-shrink: 0;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-soft);
  border-radius: 999px;
  padding: 6px 13px;
  font-size: 12.5px;
  font-weight: 700;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.achip.on { background: var(--accent-tint); border-color: var(--accent-line); color: var(--accent-ink); }
.card[data-anchor], .facts[data-anchor] { scroll-margin-top: calc(var(--topbar-h, 58px) + 50px); }
.side-pane .anchor-chips { top: 50px; }
html.field-mode .achip { font-size: 14px; padding: 8px 15px; }

/* ---------- Keyboard + pointer layer (2026-08-20) ----------
   The desktop side pane made mouse and keyboard real surfaces; until now the
   stylesheet had zero :hover and zero :focus-visible rules. */

/* Focus: inputs keep their existing glow; everything else gets an accent ring.
   :focus-visible fires for keyboard access only, so taps stay clean. */
:focus-visible { outline: 2.5px solid var(--accent-ink); outline-offset: 2px; }
input:focus-visible, select:focus-visible, textarea:focus-visible { outline: none; }
/* On the dark brand surfaces a green ring vanishes — gold carries it there. */
.topbar :focus-visible, .item-hero :focus-visible, .lb :focus-visible,
.mk-back :focus-visible, .hero :focus-visible { outline-color: var(--gold-light); }

/* Hover, gated to devices that actually hover — phones never see a stuck
   hover state. Quiet by design: a tint, a 1px lift, never a colour change. */
@media (hover: hover) and (pointer: fine) {
  .row:hover { background: var(--green-50); }
  .tile:hover, .cat-card:hover, .stat:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
  .btn:hover { filter: brightness(1.06); }
  .btn.secondary:hover { filter: none; background: var(--green-50); }
  .btn.danger:hover { filter: none; background: var(--red-bg); }
  button.chip:hover { background: var(--green-100); }
  .tabbar a:hover { color: var(--accent-ink); }
  .hubswitch a:hover { border-color: var(--accent-line); }
  .viewtog button:hover { color: var(--accent-ink); }
  .cb-tile:hover { border-color: var(--accent-line); }
  .qs-b:hover { background: var(--green-100); }
  .seg button:hover { color: var(--ink); }
  .fab:hover, .scrolltop.show:hover { transform: scale(1.05); }
  .back-btn:hover, .lb-close:hover { background: rgba(255, 255, 255, .24); }
  .sp-close:hover { background: var(--green-50); }
}
