/*
 * app.css — the Anglo Doorstep app.
 *
 * BRAND
 * -----
 * Taken from customer.php's :root, which is the live source of truth for the site's palette,
 * not from the email templates (those carry a slightly different green, #60aa3f, which is
 * close enough to look right on its own and wrong next to the real one).
 *
 *   #60a940  primary green    the swoosh, the go-actions, everything affirmative
 *   #ef7b45  secondary orange the brand's other half — used for "coming up", not for danger
 *   #2c3e50  slate            headings and body copy; the van's body in the logo is near-black
 *   #f8f9fa  page ground
 *
 * ⚠️ The orange is a BRAND colour, not a warning colour. It marks the next collection and the
 * things worth noticing. Anything that actually needs care uses --danger, which is a distinctly
 * redder hue so the two never get confused at a glance.
 *
 * MOTION
 * ------
 * The logo is a van driving up a hill. That gives the app its motion language: things arrive
 * from the left and settle, and the spinner is the recycling loop turning. Every animation is
 * either feedback for something the customer did, or the app telling them it is working.
 * None of it loops forever in the background.
 *
 * Every single animation is disabled under prefers-reduced-motion at the bottom of this file.
 * Vestibular triggers are not a style preference — and the confirmation sequence is a
 * full-screen movement, which is exactly the kind that causes trouble.
 */

/* ============================================================
   Montserrat — self-hosted, copied from the WordPress theme.
   No CDN: a render-blocking request to fonts.googleapis.com on
   mobile data is the difference between an app and a web page.
   ============================================================ */
@font-face {
  font-family: "Montserrat";
  src: url("fonts/Montserrat-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("fonts/Montserrat-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("fonts/Montserrat-SemiBold.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("fonts/Montserrat-ExtraBold.woff2") format("woff2");
  font-weight: 800; font-style: normal; font-display: swap;
}

/* ============================================================
   Tokens — full light palette on bare :root.
   ============================================================ */
:root {
  --green:       #60a940;
  --green-dark:  #4a8730;
  --green-deep:  #35651f;
  --green-wash:  #eef7e9;

  --orange:      #ef7b45;
  --orange-dark: #c85c2b;
  --orange-wash: #fdf0e8;

  --ink:         #2c3e50;
  --ink-2:       #5b6b7a;
  --ink-3:       #8d9aa6;

  --ground:      #f8f9fa;
  --card:        #ffffff;
  --card-2:      #f1f4f2;
  --line:        #e2e8e4;

  --danger:      #c0392b;
  --danger-wash: #fbecea;

  --van-glass:   #ffffff;

  --radius:   16px;
  --radius-s: 11px;
  --shadow:   0 1px 2px rgba(44,62,80,.05), 0 4px 16px rgba(44,62,80,.06);
  --shadow-lg:0 8px 30px rgba(44,62,80,.13);

  --tap: 48px;

  /* One easing curve for the whole app: a gentle overshoot, so things arrive with a bit of
     life rather than stopping dead. */
  --spring: cubic-bezier(.22, 1.2, .36, 1);
  --ease:   cubic-bezier(.4, 0, .2, 1);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --green-wash:  #1b2c15;
    --orange-wash: #2e1c12;
    --ink:         #e7edf1;
    --ink-2:       #a6b3bf;
    --ink-3:       #74828e;
    --ground:      #141a1f;
    --card:        #1d252c;
    --card-2:      #252f37;
    --line:        #2f3a43;
    --danger:      #f0796a;
    --danger-wash: #331916;
    --van-glass:   #1d252c;
    --shadow:      0 1px 2px rgba(0,0,0,.4), 0 4px 16px rgba(0,0,0,.3);
    --shadow-lg:   0 8px 30px rgba(0,0,0,.45);
  }
}

:root[data-theme="dark"] {
  --green-wash:  #1b2c15;
  --orange-wash: #2e1c12;
  --ink:         #e7edf1;
  --ink-2:       #a6b3bf;
  --ink-3:       #74828e;
  --ground:      #141a1f;
  --card:        #1d252c;
  --card-2:      #252f37;
  --line:        #2f3a43;
  --danger:      #f0796a;
  --danger-wash: #331916;
  --van-glass:   #1d252c;
  --shadow:      0 1px 2px rgba(0,0,0,.4), 0 4px 16px rgba(0,0,0,.3);
  --shadow-lg:   0 8px 30px rgba(0,0,0,.45);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.55;
  overscroll-behavior-y: none;
}

/* ============================================================
   Header — the swoosh is the bottom edge, exactly as the logo
   has the van riding the green curve.
   ============================================================ */
.app-header {
  position: sticky; top: 0; z-index: 20;
  background: linear-gradient(150deg, var(--green) 0%, var(--green-dark) 100%);
  color: #fff;
  padding: calc(env(safe-area-inset-top) + 14px) 18px 26px;
  display: flex; align-items: center; gap: 11px;
}

/* The curve, painted in the page's ground colour so it reads as the header being cut away. */
.app-header::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -1px;
  height: 22px;
  background: var(--ground);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 60' preserveAspectRatio='none'%3E%3Cpath d='M0 54 C110 44 230 4 400 12 L400 60 L0 60 Z' fill='%23000'/%3E%3C/svg%3E") no-repeat center/100% 100%;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 60' preserveAspectRatio='none'%3E%3Cpath d='M0 54 C110 44 230 4 400 12 L400 60 L0 60 Z' fill='%23000'/%3E%3C/svg%3E") no-repeat center/100% 100%;
}

.app-header h1, .brand-title {
  margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -.01em;
}
.app-header .spacer { margin-left: auto; }
.app-header a, .app-header button.linkish {
  color: #fff; text-decoration: none; font-size: 14px; font-weight: 500;
  background: none; border: 0; padding: 9px; margin: -9px; cursor: pointer;
  border-radius: 8px;
}
.app-header a:active { background: rgba(255,255,255,.16); }

.brand-mark { display: block; width: 34px; flex: none; color: #fff; }
.brand-mark svg { width: 100%; height: auto; display: block; }

/* The van nudges forward when the header first paints — a wheel-roll, not a parade.
   Shortened from .7s: this replays on every tab tap, and the header is the first thing the
   eye checks to see whether the tap did anything. See the entrance note further down. */
.brand-van { animation: van-nudge .3s var(--spring) both; }
@keyframes van-nudge {
  from { transform: translateX(-14px); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}

main {
  padding: 18px 16px;
  padding-bottom: calc(env(safe-area-inset-bottom) + 88px);
  max-width: 640px; margin: 0 auto;
}
main.no-tabs { padding-bottom: calc(env(safe-area-inset-bottom) + 28px); }

/* ============================================================
   Entrance — content arrives in sequence rather than all at
   once, so the eye is led down the page.

   ⚠️ THE BUDGET HERE IS PERCEIVED PAGE SPEED, NOT TASTE.
   Every tab tap is a full page navigation, so this runs again
   on EVERY screen the customer opens — it is not a one-off
   flourish the way it is on a website you land on once.

   `both` carries `backwards`, which means each element sits at
   opacity 0 from the moment it renders until its delay has
   elapsed. So `delay + duration` is time the page is on screen
   with nothing readable on it. At .26s + .45s that was 0.71s
   per navigation of deliberate blankness, on top of the network
   round trip — and it is invisible to any server-side timing,
   which is why the app measured fast and felt slow.

   Now 0.20s worst case. The first element does not wait at all,
   so something is readable on the first frame; the rest follow
   closely enough to still read as a sequence. Do not put the
   delays back up without measuring what it costs.
   ============================================================ */
main > * { animation: rise .2s var(--spring) both; }
main > *:nth-child(1) { animation-delay: 0s; }
main > *:nth-child(2) { animation-delay: .02s; }
main > *:nth-child(3) { animation-delay: .04s; }
main > *:nth-child(4) { animation-delay: .06s; }
main > *:nth-child(5) { animation-delay: .07s; }
main > *:nth-child(n+6) { animation-delay: .08s; }

@keyframes rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* The ?trace=1 timing readout — see trace() in app.js. Deliberately plain and deliberately
   pinned to the top: it is a diagnostic, and it must not be mistaken for part of the app. */
#app-trace {
  position: fixed; top: 0; left: 0; right: 0; z-index: 9999;
  background: #1c2b1c; color: #cfe6c4;
  font: 500 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  padding: calc(env(safe-area-inset-top) + 4px) 8px 4px;
  text-align: center; letter-spacing: .01em;
  animation: none;                 /* never let the entrance animation delay the diagnostic */
}

/* ============================================================
   Cards
   ============================================================ */
.card {
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 18px;
  margin-bottom: 14px;
}
.card h2 { margin: 0 0 5px; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.card h3 { margin: 0 0 3px; font-size: 15px; font-weight: 600; }
.card p  { margin: 0 0 10px; color: var(--ink-2); font-size: 14px; }
.card p:last-child { margin-bottom: 0; }

/* The next collection gets the brand orange down its edge — the one thing on the home
   screen that should catch the eye before anything else. */
.card.next-up {
  border-left: 4px solid var(--orange);
  background: linear-gradient(100deg, var(--orange-wash) 0%, var(--card) 55%);
}

.card-row {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 0; border-bottom: 1px solid var(--line);
}
.card-row:last-child { border-bottom: 0; padding-bottom: 0; }
.card-row:first-child { padding-top: 0; }
.card-row .grow { flex: 1; min-width: 0; }

/* Removing a booking collapses its row away rather than blinking out. */
.card-row.removing, .card.removing {
  animation: collapse .42s var(--ease) forwards;
  overflow: hidden;
}
@keyframes collapse {
  to { opacity: 0; transform: translateX(28px); max-height: 0;
       padding-top: 0; padding-bottom: 0; margin-bottom: 0; border-width: 0; }
}

/* ============================================================
   Type
   ============================================================ */
.title    { font-size: 25px; font-weight: 800; margin: 0 0 6px; letter-spacing: -.028em; line-height: 1.15; }
.subtitle { color: var(--ink-2); font-size: 15px; margin: 0 0 22px; }
.muted    { color: var(--ink-3); font-size: 13px; }
.strong   { font-weight: 600; }
.nowrap   { white-space: nowrap; }

/* ============================================================
   Forms
   ============================================================ */
.field { margin-bottom: 15px; }
.field label {
  display: block; font-size: 13px; font-weight: 600;
  margin-bottom: 6px; color: var(--ink-2);
}
input[type=text], input[type=email], input[type=tel],
input[type=password], input[type=search], select, textarea {
  width: 100%; min-height: var(--tap);
  padding: 12px 14px;
  font: inherit; font-size: 16px;   /* 16 minimum — iOS zooms the page on focus below this */
  color: var(--ink);
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--radius-s);
  appearance: none;
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease);
}
textarea { min-height: 112px; resize: vertical; line-height: 1.55; }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--green);
  box-shadow: 0 0 0 3.5px var(--green-wash);
}
input[aria-invalid="true"] { border-color: var(--danger); }
/* A wrong password shakes the field — instant, physical, no reading required. */
input.shake { animation: shake .4s var(--ease); }
@keyframes shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-7px); }
  40%, 60% { transform: translateX(7px); }
}

.hint { font-size: 12.5px; color: var(--ink-3); margin: 6px 0 0; }

/* The Show button inside a password box (passwordPeek() in app.js, 29 Oct 2026). */
.pw-wrap { position: relative; display: block; }
.pw-wrap input { padding-right: 78px; }
.pw-peek {
  position: absolute; top: 0; right: 0; bottom: 0; min-width: 70px;
  padding: 0 14px; background: none; border: 0;
  font: inherit; font-size: 14px; font-weight: 600; color: var(--green-deep);
  cursor: pointer;
}

/* ============================================================
   Buttons
   ============================================================ */
.btn {
  position: relative; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: var(--tap);
  padding: 13px 20px;
  font: inherit; font-size: 16px; font-weight: 600; letter-spacing: -.01em;
  color: #fff;
  background: linear-gradient(150deg, var(--green) 0%, var(--green-dark) 100%);
  border: 1.5px solid transparent;
  border-radius: var(--radius-s);
  cursor: pointer; text-decoration: none;
  box-shadow: 0 2px 8px rgba(96,169,64,.28);
  transition: transform .12s var(--ease), box-shadow .2s var(--ease), opacity .12s var(--ease);
}
.btn:active { transform: scale(.975); box-shadow: 0 1px 4px rgba(96,169,64,.24); }
.btn[disabled] { opacity: .5; box-shadow: none; cursor: default; transform: none; }

.btn.secondary {
  background: var(--card); color: var(--ink);
  border-color: var(--line); box-shadow: none;
}
.btn.danger {
  background: var(--card); color: var(--danger);
  border-color: var(--danger); box-shadow: none;
}
.btn.small { width: auto; min-height: 40px; padding: 9px 16px; font-size: 14px; }
/* For a quiet secondary action beside content, e.g. account.php's Remove. Below the --tap
   target on purpose: anything it does is behind a confirm. */
.btn.tiny  { width: auto; min-height: 30px; padding: 4px 11px; font-size: 13px; border-width: 1px; }
.btn.link  {
  background: none; border: 0; color: var(--green-dark); width: auto;
  padding: 11px 0; min-height: auto; font-weight: 600; box-shadow: none;
  text-decoration: underline; text-underline-offset: 3px;
}
:root[data-theme="dark"] .btn.link { color: var(--green); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn.link { color: var(--green); }
}

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

/* The spinner is the logo's recycling loop, turning. */
.btn .spin, .spin {
  width: 17px; height: 17px; flex: none;
  border: 2.4px solid currentColor; border-right-color: transparent;
  border-radius: 50%;
  animation: spin .62s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.loop-spin { animation: spin .9s linear infinite; transform-origin: 50% 50%; }

/* ============================================================
   Messages
   ============================================================ */
.note {
  padding: 13px 15px;
  border-radius: var(--radius-s);
  font-size: 14px; margin-bottom: 15px;
  border: 1px solid transparent;
  animation: rise .3s var(--spring) both;
}
.note.error { background: var(--danger-wash); color: var(--danger);     border-color: currentColor; }
.note.ok    { background: var(--green-wash);  color: var(--green-deep);  border-color: var(--green); }
.note.warn  { background: var(--orange-wash); color: var(--orange-dark); border-color: var(--orange); }
.note:empty { display: none; }
:root[data-theme="dark"] .note.ok { color: var(--green); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .note.ok { color: var(--green); }
}

.badge {
  display: inline-block; padding: 4px 10px; border-radius: 99px;
  font-size: 12px; font-weight: 600; white-space: nowrap;
}
.badge.ok   { background: var(--green-wash);  color: var(--green-deep); }
.badge.warn { background: var(--orange-wash); color: var(--orange-dark); }
.badge.grey { background: var(--card-2);      color: var(--ink-3); }
:root[data-theme="dark"] .badge.ok { color: var(--green); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .badge.ok { color: var(--green); }
}

/* "Tomorrow" breathes, so the imminent one is unmistakable. Three pulses, then it stops —
   an indefinite loop in the corner of the eye is an irritant, not information. */
.badge.imminent {
  background: var(--orange); color: #fff;
  animation: pulse 1.5s var(--ease) 3;
}
@keyframes pulse {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(239,123,69,.5); }
  50%      { transform: scale(1.045); box-shadow: 0 0 0 8px rgba(239,123,69,0); }
}

/* ============================================================
   Driver tracking
   ============================================================
   A collection happening today outranks everything else on the
   screen, so its card takes the brand green outright rather
   than the orange edge the "next up" card gets.
   ============================================================ */
.card.tracking-live {
  border-left: 4px solid var(--green);
  background: linear-gradient(100deg, var(--green-wash) 0%, var(--card) 60%);
}

.track {
  margin-top: 13px; padding: 13px 14px;
  background: var(--card); border: 1.5px solid var(--green);
  border-radius: var(--radius-s);
}
.track-head { display: flex; align-items: center; gap: 10px; }
.track-line { font-size: 14px; font-weight: 600; line-height: 1.4; }
.track-sub  { margin: 7px 0 0; font-size: 12.5px; color: var(--ink-3); }
.track-btn  { width: 100%; margin-top: 12px; }

.track-icon { width: 30px; flex: none; color: var(--green-dark); }
.track-icon svg { width: 100%; height: auto; display: block; }
:root[data-theme="dark"] .track-icon { color: var(--green); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .track-icon { color: var(--green); }
}

/* Inside the arrival window: a live pip, the one thing in the app that keeps moving —
   because for the twenty minutes it is on screen, it is genuinely live information. */
.track.due { border-color: var(--orange); background: var(--orange-wash); }
.track-pip {
  position: relative; width: 13px; height: 13px; flex: none;
  display: grid; place-items: center;
}
.track-pip span {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--orange); display: block;
}
.track-pip::after {
  content: ""; position: absolute; inset: 0;
  border-radius: 50%; background: var(--orange);
  animation: ping 1.9s var(--ease) infinite;
}
@keyframes ping {
  0%   { transform: scale(1);   opacity: .55; }
  70%, 100% { transform: scale(2.4); opacity: 0; }
}
.track.due .track-btn {
  background: linear-gradient(150deg, var(--orange) 0%, var(--orange-dark) 100%);
  box-shadow: 0 2px 8px rgba(239,123,69,.32);
}

/* Future date with a slot already published — quiet reassurance, not a call to action. */
.track-slot {
  margin: 10px 0 0; font-size: 13px; font-weight: 600;
  color: var(--green-dark);
}
:root[data-theme="dark"] .track-slot { color: var(--green); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .track-slot { color: var(--green); }
}

/* ============================================================
   Date list
   ============================================================ */
.dates { display: grid; gap: 9px; margin: 0 0 18px; }
.date-opt {
  display: flex; align-items: center; gap: 13px;
  padding: 14px 15px;
  border: 1.5px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--card);
  cursor: pointer; min-height: var(--tap);
  transition: border-color .18s var(--ease), background .18s var(--ease),
              transform .18s var(--spring);
  animation: rise .2s var(--spring) both;
}
/* ⚠️ This stagger lands ON TOP of the <main> entrance one — the date list is rendered inside
   an element that is itself arriving — so book.php pays both. Kept short for that reason. */
.dates .date-opt:nth-child(1) { animation-delay: 0s; }
.dates .date-opt:nth-child(2) { animation-delay: .02s; }
.dates .date-opt:nth-child(3) { animation-delay: .04s; }
.dates .date-opt:nth-child(4) { animation-delay: .06s; }
.dates .date-opt:nth-child(5) { animation-delay: .07s; }
.dates .date-opt:nth-child(n+6) { animation-delay: .08s; }

.date-opt input { position: absolute; opacity: 0; pointer-events: none; }
.date-opt .dot {
  width: 21px; height: 21px; flex: none;
  border: 2.2px solid var(--line); border-radius: 50%;
  transition: border-width .18s var(--spring), border-color .18s var(--ease);
}
.date-opt:has(input:checked) {
  border-color: var(--green);
  background: var(--green-wash);
  transform: translateY(-1px);
}
.date-opt:has(input:checked) .dot { border-color: var(--green); border-width: 6.5px; }
.date-opt:has(input:focus-visible) { outline: 2px solid var(--green); outline-offset: 2px; }
/* ⚠️ An unavailable row is dimmed PART BY PART, never with `opacity` on .date-opt itself.
   CSS opacity creates a stacking context, so a child can never be less transparent than its
   parent — a blanket .5 on the row washes "Fully booked" out to pink and the one word the
   customer needs to read becomes the faintest thing in it. */
.date-opt.full { cursor: not-allowed; }
.date-opt.full .d-main { color: var(--ink-3); font-weight: 500; }
.date-opt.full .dot { opacity: .45; }
.date-opt:not(.full):active { transform: scale(.985); }

/* Stacked, so the reason sits under the date instead of running straight on from it. */
.date-opt .grow { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.date-opt .d-main { font-weight: 600; font-size: 15px; }
.date-opt .d-sub  { font-size: 12.5px; color: var(--danger); }

/* ============================================================
   The month grid — GU/KT/RG/SL/SM/TW, where the route runs
   Tue–Fri and there are no published dates. Drawn by
   App.dateList() in app.js. Not a native <input type=date>:
   that could not grey out the days we do not collect on.
   ============================================================ */
.cal {
  padding: 12px 12px 10px;
  border: 1.5px solid var(--line); border-radius: var(--radius-s);
  background: var(--card);
  margin: 0 0 14px;
  animation: rise .2s var(--spring) both;
}
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.cal-month { font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
/* Solid green, like the go-buttons: the arrows are how the customer reaches next month, and
   as faint chevrons on a grey disc they read as decoration. Disabled means "no more months",
   so it fades rather than turning grey. */
.cal-nav {
  width: 40px; height: 40px; flex: none;
  display: grid; place-items: center;
  font: inherit; font-size: 26px; line-height: 1; padding: 0 0 3px; color: #fff;
  background: linear-gradient(150deg, var(--green) 0%, var(--green-dark) 100%);
  border: 0; border-radius: 50%; cursor: pointer;
  box-shadow: 0 2px 8px rgba(96,169,64,.3);
  transition: transform .12s var(--ease), opacity .12s var(--ease), box-shadow .12s var(--ease);
}
.cal-nav:active { transform: scale(.9); box-shadow: 0 1px 4px rgba(96,169,64,.24); }
.cal-nav[disabled] { opacity: .3; cursor: default; box-shadow: none; }
.cal-nav:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }

.cal-dow, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px 2px; }
.cal-dow span { text-align: center; font-size: 11px; font-weight: 600; color: var(--ink-3); padding-bottom: 4px; }
.cal-grid { will-change: transform; }
.cal-grid.turn-next { animation: cal-turn-next .28s var(--spring) both; }
.cal-grid.turn-prev { animation: cal-turn-prev .28s var(--spring) both; }
@keyframes cal-turn-next { from { transform: translateX(22px);  opacity: 0; } }
@keyframes cal-turn-prev { from { transform: translateX(-22px); opacity: 0; } }

.cal-day {
  aspect-ratio: 1; min-height: 38px; width: 100%;
  display: grid; place-items: center;
  font: inherit; font-size: 15px; font-weight: 600; color: var(--ink);
  background: none; border: 1.5px solid transparent; border-radius: 50%;
  padding: 0; margin: 0 auto; max-width: 46px;
}
/* Not a collection day — there, so the month reads as a month, but plainly not on offer. */
.cal-day.off  { color: var(--ink-3); opacity: .45; font-weight: 400; }
/* Fully booked — a real day we cannot give them; struck through rather than faded so it is
   clearly "gone", not "never". */
.cal-day.full { color: var(--ink-3); text-decoration: line-through; text-decoration-thickness: 2px; }
.cal-day.open { cursor: pointer; border-color: var(--line); background: var(--card); transition: transform .12s var(--ease), border-color .18s var(--ease); }
.cal-day.open:active { transform: scale(.92); }
.cal-day.open:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.cal-day.is-selected {
  color: #fff; border-color: var(--green);
  background: linear-gradient(150deg, var(--green) 0%, var(--green-dark) 100%);
  box-shadow: 0 2px 8px rgba(96,169,64,.35);
  animation: cal-pick .32s var(--spring) both;
}
@keyframes cal-pick { 0% { transform: scale(.8); } 60% { transform: scale(1.1); } 100% { transform: scale(1); } }
.cal-chosen { margin: 10px 0 0; text-align: center; font-size: 13.5px; font-weight: 600; min-height: 1.2em; }
.cal-chosen:empty { display: none; }
.cal + .hint { margin-bottom: 16px; }

/* ============================================================
   Charity picker
   ============================================================
   Logo tiles rather than a dropdown. Choosing who your donation
   helps is the warmest moment in the flow and the reason most
   people are booking at all — it should not be a <select>.
   ============================================================ */
.charity-field { border: 0; padding: 0; margin: 0 0 15px; min-width: 0; }
.charity-field legend {
  padding: 0; margin-bottom: 9px;
  font-size: 13px; font-weight: 600; color: var(--ink-2);
}

.charities {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 9px;
}

.charity { display: block; cursor: pointer; min-width: 0; }
.charity input { position: absolute; opacity: 0; pointer-events: none; }

.charity-inner {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px; height: 100%; min-height: 82px;
  padding: 12px 9px;
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--radius-s);
  text-align: center;
  transition: border-color .18s var(--ease), background .18s var(--ease),
              transform .18s var(--spring);
}
/* The logos are supplied on white, so they need a light plate in dark mode to stay legible —
   the alternative is five charity brands rendered on a ground none of them designed for. */
.charity-inner img {
  max-width: 100%; max-height: 34px; width: auto; height: auto;
  object-fit: contain;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .charity-inner img {
    background: #fff; border-radius: 5px; padding: 3px;
  }
}
:root[data-theme="dark"] .charity-inner img {
  background: #fff; border-radius: 5px; padding: 3px;
}

.charity-name {
  font-size: 11.5px; font-weight: 600; line-height: 1.3;
  color: var(--ink-2);
}

.charity:active .charity-inner { transform: scale(.97); }
.charity input:checked + .charity-inner {
  border-color: var(--green);
  background: var(--green-wash);
  transform: translateY(-2px);
  box-shadow: 0 3px 12px rgba(96,169,64,.2);
}
.charity input:checked + .charity-inner .charity-name { color: var(--green-deep); }
:root[data-theme="dark"] .charity input:checked + .charity-inner .charity-name { color: var(--green); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .charity input:checked + .charity-inner .charity-name { color: var(--green); }
}
.charity input:focus-visible + .charity-inner { outline: 2px solid var(--green); outline-offset: 2px; }

/* "No preference" carries no logo, so it spans the row and reads as the opt-out it is. */
.charity-none { grid-column: 1 / -1; }
.charity-none .charity-inner { min-height: 0; padding: 11px; }

/* ============================================================
   Where will your donations be?
   ============================================================
   Reuses the date list's rows (.dates / .date-opt) so the two
   questions on book.php read as one form. Only the sub-line
   differs: on a date it is a warning ("Fully booked"), here it
   is a hint, so it must not be red.
   ============================================================ */
/* ⚠️ Deliberately NOT also `.field`: `.field label { display: block }` outranks `.date-opt`'s
   flex and stacks each row's parts vertically. The fieldset carries the field's margin itself. */
.leave-field { border: 0; padding: 0; margin: 0 0 15px; min-width: 0; }
.leave-field legend {
  padding: 0; margin-bottom: 9px;
  font-size: 13px; font-weight: 600; color: var(--ink-2);
}
.leave-opts { margin-bottom: 0; }
.leave-opts .d-sub { color: var(--ink-3); }
/* The "where?" follow-up sits inside the fieldset, so it needs the gap the list gave up. */
.leave-field .field { margin-top: 12px; margin-bottom: 0; }

/* "More than 20 bags or boxes?" (includes/app-book-large.php) — the doorstep rows again, one
   question further down. On the walk-through the step's heading asks the doorstep question and
   this is a second one on the same screen, so its legend reads as a question, not a field label. */
.large-field .large-hint { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }
.step .large-field legend { font-size: 15px; color: var(--ink); margin-top: 6px; }

/* ============================================================
   The step-by-step booking form (includes/app-book-steps.php)
   ============================================================
   Three questions, one at a time, and a road at the top with
   the van parked at the current stop. Answer a step and the van
   drives on; the stop it left becomes a tick. The road is the
   progress bar — the brand's own mark doing the job rather than
   a row of borrowed dots.

   --stop on .journey is the current stop, 0-based. Everything
   the van and the fill do is driven off that one number, so
   the JS sets a property and the CSS does the travelling.
   ============================================================ */
.journey { --stops: 3; margin: 2px 0 4px; padding: 0 18px; }

.journey-road {
  position: relative; height: 6px; margin: 30px 0 10px;
  /* the bar itself runs from the middle of the first stop to the middle of the last */
  margin-left: calc(100% / var(--stops) / 2); margin-right: calc(100% / var(--stops) / 2);
  background: var(--line); border-radius: 3px;
}
.journey-done {
  position: absolute; inset: 0 auto 0 0;
  width: calc(var(--stop) / (var(--stops) - 1) * 100%);
  background: var(--green); border-radius: 3px;
  transition: width .55s var(--ease);
}
.journey-van {
  position: absolute; bottom: 7px;
  left: calc(var(--stop) / (var(--stops) - 1) * 100%);
  width: 46px; margin-left: -23px;
  color: var(--ink);
  transition: left .55s var(--spring);
}
.journey-van svg { width: 100%; height: auto; display: block; }
/* The wheels are on the road: a bump while it moves, and a settle when it parks. */
.journey.is-driving .journey-van { animation: van-bump .18s ease-in-out 3; }
@keyframes van-bump {
  0%, 100% { transform: translateY(0) rotate(0); }
  50%      { transform: translateY(-2px) rotate(-1.5deg); }
}

.journey-stops {
  display: grid; grid-template-columns: repeat(var(--stops), 1fr);
  list-style: none; margin: 0; padding: 0;
}
.journey-stop { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.stop-dot {
  position: relative; width: 26px; height: 26px; margin-top: -23px;
  display: grid; place-items: center;
  border-radius: 50%; background: var(--card); color: var(--ink-3);
  border: 2px solid var(--line);
  font-size: 12.5px; font-weight: 700;
  transition: border-color .3s var(--ease), background .3s var(--ease), color .3s var(--ease),
              transform .3s var(--spring);
}
.stop-dot .stop-n, .stop-dot .stop-tick { grid-area: 1 / 1; }
.stop-dot .stop-tick { width: 15px; height: 15px; opacity: 0; transform: scale(.4); transition: opacity .2s, transform .3s var(--spring); }
.journey-stop.is-here .stop-dot {
  border-color: var(--green); color: var(--green-deep); transform: scale(1.12);
  box-shadow: 0 0 0 4px var(--green-wash);
}
.journey-stop.is-done .stop-dot { background: var(--green); border-color: var(--green); color: #fff; }
.journey-stop.is-done .stop-n { opacity: 0; }
.journey-stop.is-done .stop-tick { opacity: 1; transform: scale(1); }
.stop-label { font-size: 11.5px; font-weight: 600; color: var(--ink-3); letter-spacing: .01em; }
.journey-stop.is-here .stop-label, .journey-stop.is-done .stop-label { color: var(--ink-2); }
:root[data-theme="dark"] .journey-stop.is-here .stop-dot { color: var(--green); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .journey-stop.is-here .stop-dot { color: var(--green); }
}

.step-count { text-align: center; font-size: 12px; margin: 0 0 14px; }

/* The steps. Only one is in the document flow at a time (`hidden` on the rest); the
   transitions are pure animation so nothing needs measuring. Forward leaves to the left and
   arrives from the right — the customer is travelling. Back reverses both. */
/* ⚠️ `clip`, not `hidden`: hidden makes this a scroll container, and focusing the "where?"
   field on step 3 then scrolls the steps box internally — the road and the heading vanish
   upward and never come back. clip clips without scrolling; the hidden line is the fallback
   for a WebView too old to know it (iOS 15). */
.steps { position: relative; overflow-x: hidden; overflow-x: clip; margin: 0 -16px; padding: 0 16px; }
.step { will-change: transform, opacity; }
.step.leave-left  { animation: step-out-left  .2s var(--ease) both; }
.step.leave-right { animation: step-out-right .2s var(--ease) both; }
.step.enter-right { animation: step-in-right .34s var(--spring) both; }
.step.enter-left  { animation: step-in-left  .34s var(--spring) both; }
@keyframes step-out-left  { to { transform: translateX(-28px); opacity: 0; } }
@keyframes step-out-right { to { transform: translateX(28px);  opacity: 0; } }
@keyframes step-in-right  { from { transform: translateX(40px);  opacity: 0; } }
@keyframes step-in-left   { from { transform: translateX(-40px); opacity: 0; } }

.step-title { margin: 0 0 4px; font-size: 22px; font-weight: 800; letter-spacing: -.024em; }
.step-lead  { margin: 0 0 16px; font-size: 14px; }
.step-address { margin: -6px 0 14px; font-size: 13px; }
/* The list sits directly on the question here; the classic page's hint line gives it room. */
.step .dates { margin-bottom: 14px; }
.step .btn-row { margin-top: 4px; }
.step .btn-row .secondary { flex: 0 0 auto; width: auto; padding-inline: 22px; }

/* Steps 1 and 2's Continue only shows on the way back (a date or a cause moves on by itself),
   so it arrives rather than switches on. */
.step .step-next:not(.hidden) { animation: rise .3s var(--spring) both; }
/* Book my collection wakes when the last answer is given — one pulse, orange, the brand's
   "worth noticing" colour, then it is just a button waiting for its tap. */
.step #submit.is-ready { animation: pulse .55s var(--ease) 1; }
.step #submit:focus-visible { outline: 3px solid var(--orange); outline-offset: 3px; }

/* A doorstep option in the walk-through carries its picture on the left and its radio on the
   right; the classic page's rows keep the date list's radio-first shape. */
.leave-opt .grow { flex: 1; }
.leave-opt .leave-emoji {
  flex: none; width: 38px; height: 38px;
  display: grid; place-items: center;
  font-size: 21px; line-height: 1;
  background: var(--ground); border-radius: 10px;
  transition: transform .25s var(--spring);
}
.leave-opt:has(input:checked) .leave-emoji { transform: scale(1.12) rotate(-6deg); background: var(--card); }

/* A charity tile lands with a small bounce when picked — feedback for the tap, and the one
   warm moment in the flow deserves a little more than a border change. Walk-through only:
   the classic page is the control in this test and stays exactly as it was. */
.step .charity input:checked + .charity-inner { animation: tile-pop .32s var(--spring) both; }
@keyframes tile-pop {
  0%   { transform: scale(.96); }
  60%  { transform: scale(1.035); }
  100% { transform: scale(1); }
}

/* ============================================================
   Address picker
   ============================================================ */
.lookup-results {
  border: 1.5px solid var(--line); border-radius: var(--radius-s);
  max-height: 264px; overflow-y: auto; margin-top: 9px;
  background: var(--card);
  animation: rise .3s var(--spring) both;
}
.lookup-results:empty { display: none; }
.lookup-results button {
  display: block; width: 100%; text-align: left;
  padding: 13px 15px; min-height: var(--tap);
  background: none; border: 0; border-bottom: 1px solid var(--line);
  font: inherit; font-size: 14px; color: var(--ink); cursor: pointer;
}
.lookup-results button:last-child { border-bottom: 0; }
.lookup-results button:active { background: var(--green-wash); }

/* ============================================================
   Tab bar
   ============================================================ */
.tabs {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  display: flex;
  background: var(--card);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
  box-shadow: 0 -2px 14px rgba(44,62,80,.05);
  transition: transform .2s var(--ease);
}
.tabs a {
  flex: 1; text-align: center; padding: 10px 4px 8px;
  color: var(--ink-3); text-decoration: none;
  font-size: 11px; font-weight: 600;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  min-height: var(--tap);
  transition: color .18s var(--ease);
}
.tabs svg { width: 23px; height: 23px; display: block; transition: transform .3s var(--spring); }
.tabs a[aria-current="page"] { color: var(--green); }
/* The active tab's icon lifts and settles — the one bit of ambient personality in the chrome. */
.tabs a[aria-current="page"] svg { transform: translateY(-2px) scale(1.09); }
.tabs a:active svg { transform: scale(.9); }

/* ============================================================
   Thread
   ============================================================ */
.msg {
  padding: 12px 14px; border-radius: var(--radius-s);
  margin-bottom: 10px; font-size: 14px;
  animation: rise .35s var(--spring) both;
}
.msg.staff  { background: var(--green-wash); }
.msg.client { background: var(--card-2); border: 1px solid var(--line); }
.msg .who { font-size: 12px; font-weight: 600; color: var(--ink-3); margin-bottom: 4px; }

/* A photo on a support message — a thumbnail that opens the full picture. Capped so a portrait
   phone shot does not push the rest of the conversation off the screen. */
.msg .msg-photo { display: block; margin-top: 8px; line-height: 0; }
.msg .msg-photo img {
  max-width: 100%; max-height: 220px; width: auto; height: auto;
  border-radius: var(--radius-s); border: 1px solid var(--line); object-fit: contain;
}

/* The file picker on the help form and each reply. The native control is left as the
   platform draws it — on a phone it opens the camera roll — only sized to the field. */
/* The photo pickers (Help, a Help reply, the large-collection question). The native
   <input type="file"> is the WebView's grey "Choose file / No file chosen" control — Simon:
   "looked old and not designed" (22 Oct 2026) — so it is kept for the browser to open the
   picker but hidden off-canvas, and the <label for> is drawn as an app button: a label click
   opens the picker on iOS and Android alike. The input stays focusable for screen readers. */
.photo-field { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.photo-field .photo-btn {
  /* .field label sets display:block, which beats .btn's inline-flex — restated here so the
     icon and the words sit apart (seen on the Samsung: the camera touching the "A"). */
  display: inline-flex; gap: 8px; margin: 0; cursor: pointer;
}
.photo-field .photo-ico { width: 18px; height: 18px; flex: 0 0 auto; }
.photo-field .photo-input {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.photo-field .photo-input:focus-visible + .photo-name,
.photo-field .photo-input:focus-visible ~ .photo-name { outline: 2px solid var(--green); }
.photo-field .photo-name { flex: 1 0 100%; margin: 0; }
.photo-field .photo-name:empty { display: none; }

/* ============================================================
   The question sheet — App.ask(). One yes/no, from the bottom,
   where the thumb already is. The backdrop dims the page rather
   than hiding it, so the date they just tapped stays in view
   behind the question about it.
   ============================================================ */
.sheet-back {
  position: fixed; inset: 0; z-index: 55;
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(20, 30, 20, .42);
  animation: fade-in .18s var(--ease) both;
}
.sheet {
  width: 100%; max-width: 560px;
  padding: 22px 20px calc(env(safe-area-inset-bottom) + 20px);
  background: var(--card);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: 0 -8px 30px rgba(0,0,0,.14);
  animation: sheet-up .32s var(--spring) both;
}
@keyframes sheet-up { from { transform: translateY(100%); } }
.sheet-back.is-closing { animation: fade-out .2s var(--ease) both; }
.sheet-back.is-closing .sheet { animation: sheet-down .2s var(--ease) both; }
@keyframes fade-out { to { opacity: 0; } }
@keyframes sheet-down { to { transform: translateY(100%); } }
/* The little grab-handle every sheet has, so it reads as a sheet. */
.sheet::before {
  content: ""; display: block; width: 38px; height: 4px; margin: -8px auto 16px;
  background: var(--line); border-radius: 2px;
}
.sheet h3 { margin: 0 0 4px; font-size: 22px; font-weight: 800; letter-spacing: -.024em; text-align: center; }
.sheet p  { margin: 0 0 18px; color: var(--ink-2); font-size: 15px; text-align: center; }
.sheet .btn-row .secondary { flex: 0 0 auto; width: auto; padding-inline: 20px; }
@media (min-width: 600px) {
  .sheet-back { align-items: center; padding: 16px; }
  .sheet { border-radius: var(--radius); padding-bottom: 20px; }
  .sheet::before { display: none; }
}

/* ============================================================
   The confirmation moment.
   A booking is a van coming to your door, so booking one drives
   the van on. This is the payoff screen — the only place in the
   app that takes over the display.
   ============================================================ */
.celebrate {
  position: fixed; inset: 0; z-index: 60;
  display: flex; flex-direction: column; align-items: center;
  gap: 6px;
  /* It covers the whole display (viewport-fit=cover), so the top and bottom padding carry the
     safe areas. Until 22 Oct 2026 the van's road sat first and happened to push everything
     clear of the notch; with it gone the tick was "right at the top, under the camera" on an
     iPhone whenever the column overflowed and scrolled from the top. */
  padding: max(40px, calc(env(safe-area-inset-top) + 28px)) 24px max(24px, calc(env(safe-area-inset-bottom) + 16px));
  background: var(--ground);
  overflow-y: auto;
  animation: fade-in .22s var(--ease) both;
}
/* Centred by auto margins, not justify-content: center. With the notifications box and the
   review row both showing, a small phone runs out of height, and a centred column that
   overflows is cut off at the TOP, where no scroll can reach. Auto margins centre it while
   it fits and let it scroll from the top when it does not. */
.celebrate > .tick { margin-top: auto; }   /* the first child since the road went (22 Oct 2026) */
.celebrate > .btn  { margin-bottom: auto; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

.celebrate .road {
  position: relative; width: min(340px, 84vw); height: 108px;
  color: var(--green);
}
.celebrate .road > svg:first-child { width: 100%; height: 100%; display: block; }

.celebrate .van {
  position: absolute; left: 0; bottom: 26px;
  width: 108px; color: var(--ink);
  animation: drive 1.15s var(--spring) both .1s;
}
.celebrate .van svg { width: 100%; height: auto; display: block; }
@keyframes drive {
  from { transform: translateX(-130%) translateY(9px); }
  to   { transform: translateX(58%)   translateY(-9px); }
}

.celebrate .tick {
  width: 62px; height: 62px; color: var(--green);
  margin-top: 6px;
  animation: pop .5s var(--spring) both .95s;
}
.celebrate .tick svg { width: 100%; height: 100%; display: block; }
@keyframes pop {
  from { transform: scale(.3); opacity: 0; }
  to   { transform: scale(1);  opacity: 1; }
}
/* The tick draws itself on rather than appearing whole. */
.celebrate .tick-mark {
  stroke-dasharray: 34; stroke-dashoffset: 34;
  animation: draw .4s var(--ease) both 1.25s;
}
@keyframes draw { to { stroke-dashoffset: 0; } }

.celebrate h2 {
  margin: 14px 0 2px; font-size: 24px; font-weight: 800; letter-spacing: -.028em;
  text-align: center;
  animation: rise .45s var(--spring) both 1.15s;
}
.celebrate p {
  margin: 0; color: var(--ink-2); font-size: 15px; text-align: center;
  animation: rise .45s var(--spring) both 1.25s;
}
.celebrate .btn {
  margin-top: 26px; max-width: 300px;
  animation: rise .45s var(--spring) both 1.4s;
}

/* ============================================================
   Rating the driver — the end of the Collected page (notice.php)
   ============================================================ */
.feedback-photo {
  display: block; width: 100%; border-radius: 10px; margin: 0 0 14px;
  background: var(--card-2);
}

/* Five stars, each a real button: 48px tall so a thumb lands on one, not between two. The
   fill is a class, not :checked, so a second visit can light the ones they chose last time. */
.stars { display: flex; gap: 2px; margin: 8px 0 16px; }
.star {
  flex: 1; min-height: var(--tap); padding: 6px 0;
  background: none; border: 0; cursor: pointer;
  color: var(--line);
  transition: color .12s var(--ease), transform .12s var(--spring);
}
.star svg { width: 36px; height: 36px; fill: currentColor; display: block; margin: 0 auto; }
.star.on  { color: var(--orange); }
.star:active { transform: scale(.9); }
.card.feedback .btn-row { margin-top: 4px; }

/* The driver's tip buttons — £2 / £5 / £10 / Other in one row of four, or a single button when
   the driver's link cannot carry an amount. Absent entirely when the driver has no link. */
.tip-block { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
.tip-label { margin: 0 0 10px; font-size: 14px; font-weight: 600; color: var(--ink-2); text-align: center; }
.tip-amounts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.tip-amounts .btn { padding-inline: 0; font-weight: 700; }
/* Pick an amount, then one Send button (8 Oct 2026): the chosen amount fills green, the Send
   button stays faint until there is an amount it can carry, and the typed "Other" box sits
   between the two. See the tip block in notice.php. */
.tip-amt[aria-pressed="true"] { background: var(--green); border-color: var(--green); color: #fff; }
.tip-other { margin: 12px 0 0; }
.tip-go { width: 100%; margin-top: 12px; }
.tip-go[aria-disabled="true"] { opacity: .45; pointer-events: none; }
.tip-fine { margin: 8px 0 0; font-size: 12.5px; color: var(--ink-3); text-align: center; }

/* "Leave us a review" — two marks side by side, each a link off the app. Shared by the
   Collected page and the You're-booked-in screen; see app_review_row_html(). */
.review-row { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line); }
.review-lead { margin: 0 0 10px; font-size: 14px; font-weight: 600; color: var(--ink-2); text-align: center; }
.review-links { display: flex; gap: 9px; }
.review-link {
  flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap); padding: 9px 12px;
  background: var(--card); color: var(--ink); text-decoration: none;
  font-size: 14px; font-weight: 600;
  border: 1.5px solid var(--line); border-radius: var(--radius-s);
}
.review-link svg { width: 22px; height: 22px; flex: none; }
.review-link:active { transform: scale(.975); }
.celebrate .review-row {
  border-top: 0; margin-top: 18px; padding-top: 0; width: min(340px, 84vw);
  animation: rise .45s var(--spring) both 1.35s;
}

/* "Add to my calendar" on the booked and moved screens (App.celebrate's opts.calendar). The
   .celebrate .btn rule spaces and animates the screen's own last button; this one sits right
   under the words, so it takes a smaller gap and rises with them. */
.celebrate .add-cal { width: min(340px, 84vw); margin-top: 18px; animation: rise .45s var(--spring) both 1.3s; }
.celebrate .add-cal .btn { margin-top: 0; max-width: none; animation: none; }
.add-cal svg { width: 20px; height: 20px; flex: none; }

/* "Turn on notifications" (App.pushBox) — a card on the home screen, and "Get updates on
   this collection" on the booked screen. The .celebrate rules for p and .btn are written for
   the screen's own words and button, so the box opts back out of them. */
.push-box h3 { margin-bottom: 4px; }
.push-box p { margin-bottom: 6px; }
/* The "what you'd get" list: tick marks in the brand green, tight so four lines stay a card. */
.push-why { list-style: none; margin: 0 0 12px; padding: 0; font-size: 14px; text-align: left; }
.push-why li { position: relative; padding-left: 22px; margin: 0 0 4px; line-height: 1.35; }
.push-why li::before {
  content: ''; position: absolute; left: 2px; top: 5px; width: 6px; height: 10px;
  border: solid var(--green, #60a940); border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.push-how { font-size: 14px; margin: 0; }
.celebrate .push-box .push-why, .celebrate .push-box .push-how { animation: none; text-align: left; }
.celebrate .push-host { width: min(340px, 84vw); animation: rise .45s var(--spring) both 1.3s; }
.celebrate .push-box { margin: 18px 0 0; }
.celebrate .push-box p {
  margin: 0 0 10px; font-size: 14px; text-align: left; animation: none;
}
.celebrate .push-box .btn { margin-top: 0; max-width: none; animation: none; }

/* ============================================================
   Utility
   ============================================================ */
.stack > * + * { margin-top: 15px; }
.center { text-align: center; }
.hidden { display: none !important; }

.empty { text-align: center; padding: 38px 16px; color: var(--ink-3); }
.empty .van-idle { width: 92px; margin: 0 auto 14px; color: var(--ink-3); opacity: .45; }
.empty .van-idle svg { width: 100%; height: auto; display: block; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Sign-in screen logo lockup. */
.logo-lockup {
  display: block; width: min(260px, 68vw); height: auto;
  margin: 8px auto 26px;
  animation: rise .55s var(--spring) both;
}

#toast {
  position: fixed; left: 50%; transform: translateX(-50%) translateY(10px);
  bottom: calc(env(safe-area-inset-bottom) + 80px);
  z-index: 40;
  background: var(--ink); color: var(--ground);
  padding: 12px 20px; border-radius: 99px;
  font-size: 14px; font-weight: 600;
  box-shadow: var(--shadow-lg);
  opacity: 0; pointer-events: none;
  transition: opacity .22s var(--ease), transform .3s var(--spring);
  max-width: calc(100vw - 32px);
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ============================================================
   Typing — the tab bar steps aside, and the page gains enough
   room underneath to lift the focused field clear of the
   keyboard. `--kb` is the height the keyboard covers, measured
   from the visual viewport; it is 0 where the WebView is
   resized instead (Android), and there the room is real
   already. See keyboardAware() in app.js.
   ============================================================ */
html.kb-open .tabs { transform: translateY(110%); }

/* The 88px that reserved space for the tab bar is not needed while it is gone — what is
   needed is room to scroll the field up past the keyboard, on a platform that will not
   shrink the page for us. */
html.kb-open main { padding-bottom: calc(var(--kb, 0px) + 32px); }

/* `kb-blind` is the case where the keyboard is up and the page cannot see it at all — an
   Android WebView that edge-to-edge stopped resizing. There is no height to add, so add a
   screenful: enough for keyboardAware() to scroll the field right up under the header, which
   clears a keyboard of ANY height. It exists only while a field is focused, and every pixel
   of it sits under the keyboard that made it necessary. */
html.kb-blind main { padding-bottom: 80vh; }

/* A toast fired mid-typing ("Message sent") must not land behind the keyboard. */
html.kb-open #toast { bottom: calc(var(--kb, 0px) + 24px); }

/* The party — App.party(). A canvas over the celebrate screen; pointer events pass through
   so the button underneath is still the button. */
.celebrate .party { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
.celebrate > :not(.party) { position: relative; z-index: 2; }

/* ============================================================
   ⚠️ Reduced motion — everything above stops here.
   The confirmation sequence is a full-screen movement, which is
   precisely the kind that triggers vestibular symptoms. Content
   is still fully revealed; it just arrives without travelling.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  /* The spinners still need to read as "working", just calmly. */
  .spin, .loop-spin {
    animation: spin 1.6s linear infinite !important;
    animation-duration: 1.6s !important;
  }
  .celebrate .van { transform: translateX(58%) translateY(-9px); }
  .celebrate .tick-mark { stroke-dashoffset: 0; }
  /* The booking walk-through: the van still parks at the right stop, it just does not drive. */
  .journey-van, .journey-done { transition: none; }
  .sheet { transform: none; }
}
