/* ── Popsicle look (design/popsicle-vibe.md): mustard ground, cream cards,
   raspberry red / green / orange accents, flat offset shadows.
   Elderly-first stays: big type, big targets, strong contrast. ── */

/* NOTE: the vendored filenames are swapped — fraunces.woff2 holds the ITALIC
   cut and fraunces-italic.woff2 holds the ROMAN cut. The src lines below map
   each style to the file that really holds it; keep filenames as-is (cached). */
@font-face {
  font-family: 'Fraunces';
  src: url('/vendor/fraunces-italic.woff2') format('woff2');
  font-weight: 600 900;
  font-display: swap;
}
@font-face {
  font-family: 'Fraunces';
  src: url('/vendor/fraunces.woff2') format('woff2');
  font-weight: 600 900;
  font-style: italic;
  font-display: swap;
}
/* Body face: Geist (SIL OFL 1.1, Vercel), variable 100–900, vendored from
   Google Fonts — no CDN. Latin only; CJK falls through to the system faces
   in --body below, so 中文 keeps PingFang / Noto Sans SC. */
@font-face {
  font-family: 'Geist';
  src: url('/vendor/geist.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Geist';
  src: url('/vendor/geist-latin-ext.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --ground: #ecbb4c;       /* mustard page ground */
  --ground-deep: #d9a53c;  /* flat shadow on the ground */
  --cream: #f2edbf;        /* card / sheet surfaces */
  --paper: #faf6e0;        /* brightest chips + buttons */
  --red: #d95561;
  --red-dark: #b03c49;
  --orange: #e37a45;
  --yellow-pale: #f7e7b0;  /* pale-mustard card tint / light text on red */
  --mint: #99ccb8;
  --mint-pale: #d8eadf;
  --peach: #f8ddc6;        /* pale-orange card tint */
  --ink: #33211c;
  --ink-soft: #6b5638;
  --sand: #d6c69a;
  /* one notch down from 24/18 (Colin, 24 Aug 2026). Keep these CONCENTRIC
     with the inner radii below: a box inside a padded box wants
     inner = outer - padding, or the inner corner reads rounder than the
     outer one. A .stopblk has 10px side padding, so its screen is 10px. */
  --r-lg: 20px;
  --r-md: 15px;
  --shadow: 0 4px 0 rgba(140, 96, 20, 0.18);
  /* ── the VFD screen, shared by BOTH boards (.sb-screen and .times) ──
     A shadow painted on near-black has nowhere to go, so the depth is built
     on the CREAM side: a dark hairline above the cut (the underside of the
     top lip, in shadow) and a bright line below it (the lower wall catching
     the light). The pane is lifted off pure black as well, so the inset well
     behind it has room to fall. Change it HERE, never in one board only. */
  --vfd: #2e2318;
  --vfd-well:
    0 -1.5px 0 rgba(116, 88, 36, 0.55),
    -1.5px 0 0 rgba(140, 112, 56, 0.32),
    1.5px 0 0 rgba(140, 112, 56, 0.32),
    0 2px 0 rgba(255, 252, 236, 0.95),
    0 4px 3px rgba(140, 96, 20, 0.22),
    inset 0 0 0 2px #120d08,
    inset 0 -4px 3px -2px rgba(255, 198, 122, 0.22),
    inset 0 3px 3px -1px rgba(0, 0, 0, 0.6),
    inset 0 8px 12px -4px rgba(0, 0, 0, 0.85),
    inset 8px 0 10px -6px rgba(0, 0, 0, 0.65),
    inset -8px 0 10px -6px rgba(0, 0, 0, 0.6),
    inset 0 -6px 9px -5px rgba(0, 0, 0, 0.4);
  --display: 'Fraunces', Georgia, serif;
  --body: 'Geist', -apple-system, "PingFang SC", "Noto Sans SC", sans-serif;
}

* { box-sizing: border-box; margin: 0; -webkit-tap-highlight-color: transparent; }
html { font-size: 20px; }
body {
  font-family: var(--body);
  color: var(--ink);
  background: var(--ground);
  background-image: radial-gradient(rgba(217, 85, 97, 0.12) 1.2px, transparent 1.2px);
  background-size: 22px 22px;
}
button { font: inherit; color: inherit; border: 0; background: none; cursor: pointer; }

.screen { max-width: 480px; margin: 0 auto; min-height: 100vh; padding: 0.7rem 0.8rem 2rem; }

/* ── splash ── */
#splash {
  position: fixed; inset: 0; z-index: 9999;
  background: #ecbb4c;  /* popsicle mustard — design/popsicle-vibe.md */
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.4rem;
  transition: opacity 0.45s ease, visibility 0.45s;
}
#splash.done { opacity: 0; visibility: hidden; }
#splash .wordmark { width: min(92vw, 430px); display: block; }
#splash .bus { font-size: 2.4rem; animation: drive 1.1s ease-in-out infinite alternate; }
@keyframes drive { from { transform: translateX(-26px) } to { transform: translateX(26px) } }
/* duck peeks out of the gap between IS and MY, then ducks back behind MY */
#splash .peek-duck {
  transform-box: fill-box; transform-origin: 50% 100%;  /* else SVG rotates around the viewBox origin */
  animation: peek 2.8s ease-in-out 0.25s infinite;  /* early: cached-font splashes are short */
}
@keyframes peek {
  0%       { transform: translateX(0); }
  12%      { transform: translateX(-50px) rotate(-8deg); }  /* springy overshoot */
  18%, 55% { transform: translateX(-44px) rotate(-5deg); }  /* partial peek: tail stays behind MY */
  68%,100% { transform: translateX(0); }
}

/* ── shared header-idiom bits (edit pencil, stop-code chip) — used by the
   sheet's stop line and the FROM line ── */
.editmark {
  align-self: center; flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem; border-radius: 50%;
  background: var(--paper); box-shadow: var(--shadow);
  color: var(--red-dark); font-size: 1.5rem; line-height: 1;
}
.editmark svg { width: 58%; height: 58%; }
.stop-code {
  font-family: ui-monospace, Menlo, monospace; letter-spacing: 0.12em; font-size: 0.8rem;
  background: var(--paper); border-radius: 8px; padding: 0.12rem 0.45rem;
  box-shadow: inset 0 1px 2px rgba(51, 33, 28, 0.25); color: var(--ink-soft);
}
.lang {
  background: none; border-radius: var(--r-md); padding: 0.6rem 0.7rem;
  font-weight: 800; color: var(--ink-soft);
}
.lang:active { background: rgba(51, 33, 28, 0.05); }

/* ── search ── */
.searchwrap { position: relative; }
.search {
  width: 100%; font-size: 1.2rem; padding: 0.95rem 3rem 0.95rem 1.1rem;
  border: 2.5px solid var(--ink); border-radius: var(--r-lg);
  background: var(--paper); color: var(--ink); box-shadow: var(--shadow);
}
.search::placeholder { color: var(--ink-soft); }
.search:focus { outline: none; border-color: var(--red); }
.clear { position: absolute; right: 0.4rem; top: 50%; transform: translateY(-50%); font-size: 1.2rem; color: var(--ink-soft); padding: 0.6rem; }

/* ── gps fallback ── */
.codebox {
  background: var(--yellow-pale); border: 2.5px solid var(--orange);
  border-radius: var(--r-lg); padding: 1rem; margin-bottom: 0.8rem;
  display: flex; flex-direction: column; gap: 0.5rem; font-size: 1.05rem;
}
.search.code { letter-spacing: 0.3rem; font-size: 1.5rem; text-align: center; }

/* ── suggestions / recent places ── */
.suggests, .places { margin-top: 0.85rem; display: flex; flex-direction: column; gap: 0.55rem; }
.suggest, .place {
  background: var(--paper); border-radius: var(--r-md); padding: 1rem 1.1rem;
  font-size: 1.05rem; text-align: left; box-shadow: var(--shadow);
  display: flex; gap: 0.6rem; align-items: center;
}
.place span, .suggest { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── duck pacing along the signboard's top edge (overlay, takes no layout space) ── */
.duck-walker {
  position: absolute; top: -31px; left: 0; width: 32px; height: 32px;
  background: url(/duck-walk.png?v=1) 0 0 / 400% 100% no-repeat;
  image-rendering: pixelated;
  /* two clocks: leg frames (steps) + pacing with a turn at each end.
     tuned together: ~27 px/s travel × 0.45 s cycle ≈ 12 px per 4-frame
     stride — matched so the feet grip instead of walk-sliding */
  animation: duck-step 0.45s steps(4) infinite, duck-pace 32s linear infinite;
}
@keyframes duck-step { to { background-position-x: 133.333%; } }
@keyframes duck-pace {          /* sprite faces right → scaleX(-1) = walking left */
  0%    { left: 0; transform: scaleX(1); }
  49.9% { left: calc(100% - 32px); transform: scaleX(1); }
  50%   { left: calc(100% - 32px); transform: scaleX(-1); }
  99.9% { left: 0; transform: scaleX(-1); }
  100%  { left: 0; transform: scaleX(1); }
}

/* ── signboard: cassette-futurism departure board ──
   chunky warm-plastic bezel around a dark VFD screen with glowing amber digits */
.signboard {
  position: relative;   /* anchor for .duck-walker on the top edge */
  margin-top: 0.9rem;
  background: linear-gradient(#f7f0d0, #e6dcae);
  border-radius: var(--r-lg);
  /* sides 10px -> 14px, top 6px -> 10px: the lit wall needs room to exist */
  padding: 0.5rem 0.7rem 0.6rem;
  box-shadow: var(--shadow), inset 0 2px 0 rgba(255, 255, 255, 0.65), inset 0 -3px 0 rgba(51, 33, 28, 0.16);
}
.sb-label {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0.15rem 0.5rem 0.45rem;
  font-weight: 800; letter-spacing: 0.18em; font-size: 0.85rem; color: var(--red-dark);
}
.sb-code {
  font-family: ui-monospace, Menlo, monospace; letter-spacing: 0.12em; font-size: 0.8rem;
  background: var(--paper); border-radius: 8px; padding: 0.12rem 0.45rem;
  box-shadow: inset 0 1px 2px rgba(51, 33, 28, 0.25); color: var(--ink-soft);
}
.sb-screen {
  position: relative;
  background: var(--vfd);
  border-radius: 10px;
  padding: 0.4rem 0.75rem;
  box-shadow: var(--vfd-well);
  overflow: hidden;
}
.sb-screen::after {
  /* scanlines + a soft glare at the top of the glass */
  content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background:
    linear-gradient(rgba(255, 255, 255, 0.05), transparent 18%),
    repeating-linear-gradient(0deg, transparent 0 2px, rgba(0, 0, 0, 0.15) 2px 3px);
}
.sb-row {
  position: relative;   /* anchors the tap-to-play .busdrive bus */
  display: flex; align-items: flex-start; gap: 0.6rem;
  padding: 0.55rem 0.1rem;
  color: #ffb63c;
  text-shadow: 0 0 8px rgba(255, 170, 40, 0.45);
}
.sb-row + .sb-row { border-top: 1px solid rgba(255, 182, 60, 0.14); }
.sb-left { display: flex; flex-direction: column; gap: 0.2rem; flex: 1; min-width: 3.2ch; }
/* number + destination share a line and sit centred on each other; the whole
   chain needs min-width:0 or a long terminal name pushes the times off-screen */
.sb-line { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }
.sb-svc { flex: none; font-family: var(--display); font-style: italic; font-weight: 900; font-size: 1.7rem; line-height: 1; letter-spacing: 0.02em; }
.sb-away { font-size: 0.7rem; color: #d99a2e; letter-spacing: 0.03em; white-space: nowrap; }
/* where a repeated service number's bus ends up — secondary, so dim amber */
.sb-dest { flex: 0 1 auto; min-width: 0; font-size: 0.7rem; color: #d99a2e; letter-spacing: 0.03em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sb-times { display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; }
.sb-laters { display: flex; gap: 0.35rem; }
.sb-first { font-family: var(--display); font-style: italic; font-weight: 800; font-size: 1.35rem; line-height: 1.05; white-space: nowrap; }
.sb-first.isnow { color: #7dffa0; text-shadow: 0 0 9px rgba(90, 255, 140, 0.5); animation: sbblink 1.1s steps(2) infinite; }
@keyframes sbblink { 50% { opacity: 0.45 } }
.sb-later {
  font-size: 0.8rem; color: #cf9231; white-space: nowrap;
  border: 1px solid rgba(255, 182, 60, 0.35); border-radius: 5px;
  padding: 0.05rem 0.35rem; line-height: 1.3;
  text-shadow: none;
}
.sb-empty { padding: 1.1rem 0.2rem; text-align: center; color: #d99a2e; text-shadow: 0 0 8px rgba(255, 170, 40, 0.4); letter-spacing: 0.2em; }

/* ── signboard boot state ──
   one bright line ("Finding your stop…") covers both waits — GPS and the
   first arrivals — with a slow steps() blink. Nothing else on the screen:
   placeholder numbers were tried (v81 iterations) and read as loaded data. */
.sb-bootline {
  text-align: center;
  font-family: var(--body); font-weight: 650; font-size: 1.05rem;
  letter-spacing: 0.04em;
  padding: 0.7rem 0.2rem 0.45rem;
  color: #ffb63c; text-shadow: 0 0 8px rgba(255, 170, 40, 0.45);
  animation: sbboot 1.6s steps(2) infinite;
}
@keyframes sbboot { 50% { opacity: 0.55 } }

/* where-to entry under the board */
.wheretobtn {
  width: 100%; margin-top: 0.8rem;
  display: flex; align-items: center; justify-content: center; gap: 0.55rem;
  background: var(--paper); border: 2.5px solid var(--ink);
  border-radius: var(--r-lg); padding: 0.9rem 1rem;
  font-size: 1.15rem; font-weight: 700; box-shadow: var(--shadow);
}
.wt-icon { color: var(--red); font-weight: 900; }

/* ── results ── */
.destline { display: flex; align-items: center; padding: 0 0.2rem 0.7rem; }
.destname {
  font-family: var(--display); font-weight: 700;
  font-size: 1.25rem; color: var(--red); flex: 1;
}
.msg { background: var(--paper); border-radius: var(--r-lg); padding: 1.2rem; text-align: center; color: var(--ink-soft); box-shadow: var(--shadow); }

/* ── trips-first cards (design/trips-first-cards.md) ──
   ONE card per trip: the total, the buses, and the journey inside it —
   board stop and the walk to it included. Exactly one card is expanded. */
/* ── ticket-stub card (Colin, 28 Aug 2026) ──
   Expanded = a torn ticket: the header keeps the tint (--stub), the body
   drops to a near-white of it (--stub-lite), the outline is a darker shade
   (--stub-dark), and a perforation seam (dashed line + two edge notches)
   separates them. Collapsed = one plain tinted row, NO seams.
   The outline lives on .card::before (an inset ring ABOVE the header's
   background) — a real border can't work with the notch trick below. */
.card {
  --lgcol: 2rem;                   /* the dot column; the rail runs down its middle */
  border-radius: var(--r-lg); padding: 0; margin-bottom: 0.85rem;
  box-shadow: var(--shadow);
  background: var(--stub);
  position: relative; overflow: hidden;
  transition: background-color 0.28s ease;   /* the tear eases, no color snap */
}
.card::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 2.5px var(--stub-dark);
  pointer-events: none; z-index: 1;
}
.card.open { background: var(--stub-lite); }
/* collapsed: ONE row — pills left, "~30 min ›" right. Same tint, same shadow.
   The header is pills-then-total in the DOM, so both states read the same. */
.card:not(.open):active { transform: translateY(2px); }
/* the notches: clip-path trick — each notch is clipped at the card's outer
   edge (the card's own overflow: hidden), so its background (the sheet's
   mustard ground, i.e. what lies OUTSIDE the card) erases the outline ring behind it
   and only the inner arc of its own ring survives — a clean semicircular
   bite instead of a full floating ring. Centered on the seam line. */
.cardtop::before,
.cardtop::after {
  content: ''; position: absolute; bottom: -13px; width: 24px; height: 24px;
  border-radius: 50%; box-sizing: border-box;
  /* the sheet is the mustard ground since v101 (the cream destination sheet
     is gone) — the bite must paint what lies OUTSIDE the card, or it reads
     as a floating cream ring */
  background: var(--ground); border: 2.5px solid var(--stub-dark);
  z-index: 2; pointer-events: none;
  opacity: 0; transition: opacity 0.28s ease;  /* the bite fades with the tear */
}
.cardtop::before { left: -12px; }
.cardtop::after { right: -12px; }
.card.open .cardtop::before,
.card.open .cardtop::after { opacity: 1; }
/* the perforation: always there but transparent while collapsed (a colored
   border would show as a seam on the closed row) — the dashes fade in */
.cardbody { border-top: 2px dashed transparent; }
.card.open .cardbody { border-top-color: var(--stub-dark); }
/* the biggest text on the card: this is the answer the user came for */
.tot {
  flex: none; display: inline-flex; align-items: baseline; gap: 0.35rem;
  font-family: var(--display); font-weight: 800; font-size: 1.2rem;
  color: var(--ink); white-space: nowrap; line-height: 1.1;
}
.chev { font-size: 1.3rem; font-weight: 800; color: var(--red); }
.card.open .chev { display: none; }
/* the drawer animates to its OWN height (0fr -> 1fr), which no max-height
   guess can do. Height only, and springy — under 0.3 s. */
.cardbody {
  display: grid; grid-template-rows: 0fr;
  /* plain ease-out (native parity): the old overshoot bounced the list
     below the card and hard-stopped at closed */
  transition: grid-template-rows 0.28s cubic-bezier(0.33, 1, 0.68, 1),
              border-top-color 0.28s ease;
  /* the tint has to travel all the way down to .legs: `background: inherit`
     copies the PARENT's computed value, so one transparent wrapper in the
     chain and the spine's end-cover paints nothing */
  background: inherit;
}
.card.open .cardbody { grid-template-rows: 1fr; }
/* NO padding here: at 0fr a box's own padding still renders (border-box can't
   go below it) and the "closed" drawer would keep ~1.4rem of height. The
   children carry margins instead — at 0fr they simply clip away. */
.cardinner { overflow: hidden; min-height: 0; background: inherit; }
.cardinner .legs { margin: 0.7rem 0.7rem 0; }
/* toy bus drives across the top strip of the card when its bus reaches "Now" */
.busdrive {
  position: absolute; top: 0.3rem; left: -3rem; font-size: 1.6rem;
  pointer-events: none; z-index: 2;
  animation: busdrive 2.4s cubic-bezier(0.45, 0.05, 0.55, 0.95) forwards;
}
/* drive in, pause at the middle like a real stop, drive off.
   scaleX(-1) mirrors the emoji: 🚌 faces left, but it drives to the right. */
@keyframes busdrive {
  0%   { left: -3rem; transform: scaleX(-1) rotate(4deg); }
  38%  { left: 44%;   transform: scaleX(-1) rotate(-3deg); }
  42%  { transform: scaleX(-1) rotate(0deg); }
  58%  { left: 44%;   transform: scaleX(-1) rotate(0deg); }
  62%  { transform: scaleX(-1) rotate(3deg); }
  100% { left: 105%;  transform: scaleX(-1) rotate(-4deg); }
}
/* "bus is here" ring. It MUST live on a pseudo-element: .card already owns an
   `animation` (cardIn, below), so putting cardglow on .card.flash swapped the
   shorthand — and removing the class swapped it back, which REPLAYED cardIn and
   made the card blink out and in at the end of the drive-by. */
.card::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 4px var(--orange);
  opacity: 0; pointer-events: none; z-index: 3;
}
.card.flash::after { animation: cardglow 2.4s ease-out forwards; }
/* holds while the toy bus is on the card, then fades out with it */
@keyframes cardglow { 0%, 80% { opacity: 1; } 100% { opacity: 0; } }
/* stub shades per tint: header / near-white body / darker outline */
.card:nth-child(3n+1) { --stub: var(--yellow-pale); --stub-lite: #fcf8e9; --stub-dark: #cfae62; }
.card:nth-child(3n+2) { --stub: var(--mint-pale);   --stub-lite: #f1f8f4; --stub-dark: #a4c8b5; }
.card:nth-child(3n)   { --stub: var(--peach);       --stub-lite: #fdf4ec; --stub-dark: #dcae86; }
/* the header ALWAYS paints --stub: on a collapsed card it matches the card
   exactly, so only the body under it reads as "torn off" when opening */
.cardtop { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 1rem; position: relative; background: var(--stub); }
/* a change pair (two pills + arrow) crowds the ETA on narrow phones:
   shrink the pills, and let them wrap under as the last resort */
.buses { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; min-width: 0; }
.buses:has(.arrow) .busno { font-size: 1.25rem; padding: 0.1rem 0.6rem; }
.busno {
  font-family: var(--display); font-style: italic; background: var(--red); color: var(--cream);
  font-weight: 800; font-size: 1.5rem; padding: 0.12rem 0.75rem; border-radius: 14px;
  box-shadow: 0 2px 0 var(--red-dark);
}
.busno.big { font-size: 2rem; }
.busno.mini { font-size: 0.95rem; padding: 0.02rem 0.5rem; border-radius: 9px; box-shadow: 0 1.5px 0 var(--red-dark); margin-right: 0.15rem; }
.arrow { color: var(--red-dark); font-size: 1.2rem; }
.eta { font-weight: 800; font-size: 1rem; color: var(--red); }
.etalater {
  font-size: 0.6rem; font-weight: 700; color: var(--ink-soft); white-space: nowrap;
  border: 1.5px solid var(--ink-soft); border-radius: 5px;
  padding: 0.05rem 0.35rem; line-height: 1.3; opacity: 0.75;
}

/* ── the journey spine ──
   A bus pill (or the orange end dot) in a fixed left column, the stop and its
   facts to the right, and ONE unbroken bead rail down the column's middle.
   The rail is drawn for the whole block (no per-row patterns → no clipped
   beads at the seams); the pills paint over it, and the end row covers the
   tail below its dot. Paint order: row backgrounds < rail (z1) < the end
   cover (z2) < pills, dots and text (z3). */
/* Type one notch down, air one notch up (Colin, 28 Aug 2026: the spine read
   as cramped on the phone). Smaller AND roomier — the row height is about
   the same, the block breathes, and the saving pays for the walk strip. */
.legs { position: relative; background: inherit; font-size: 0.9rem; }
.legs::before {
  content: ''; position: absolute; left: calc(var(--lgcol) / 2 - 3px);
  top: 0.9rem; bottom: 0; width: 6px; z-index: 1;   /* +0.3rem: the tap rows' padding */
  background-image: radial-gradient(circle, var(--red) 2.1px, transparent 2.5px);
  background-size: 6px 10px; background-repeat: repeat-y; background-position: center top;
}
.lg { display: flex; align-items: flex-start; gap: 0.5rem; position: relative; }
.lgcol { flex: none; width: var(--lgcol); display: flex; justify-content: center; }
.lgcol .tldot { position: relative; z-index: 3; }
/* the bus pill LEADS the meta line ([117] 2 stops · ~3 min) — out of the
   column, which also kills the 410W truncation risk for good.
   flex: none, or a 4-character service (859B) squeezes into an oval */
.lgmeta .busno.mini { flex: none; white-space: nowrap; font-size: 0.78rem; padding: 0.06rem 0.55rem; margin: 0; }
/* min-width: 0 on EVERY item in the chain, or a long stop name refuses to
   shrink and pushes the card off the screen */
.lgtxt {
  min-width: 0; position: relative; z-index: 3; line-height: 1.35;
  color: var(--ink-soft); padding-top: 0.1rem;
}
.lgtxt b { color: var(--ink); font-weight: 700; }
/* the stop's facts, one quiet line under its name, led by the bus pill */
.lgmeta { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85em; margin-top: 0.15em; }
/* live wait on the row's right edge, the later-times boxes stacked under it */
.lgright {
  flex: none; margin-left: auto; position: relative; z-index: 3;
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.15rem;
}
.lglaters { display: flex; gap: 0.3rem; }
/* the segs are gone — the nodes themselves keep the spine's rhythm.
   0.35 + the rows' own 0.3 padding top and bottom = the 0.9 gap this was. */
.lg + .lg { margin-top: 0.35rem; }
.tldot {
  flex: none; width: 14px; height: 14px; border-radius: 50%;
  /* dot center = text padding-top + half a text line (em, so the 20px phone
     base font keeps it true) minus half the dot. A px guess sat high. */
  margin-top: calc(0.1rem + 0.675em - 7px); box-sizing: border-box;
  background: var(--red); border: 3px solid var(--red);
}
.tldot.end { background: var(--orange); border-color: var(--red); }
/* the change stop: a paper-filled ring on the rail */
.tldot.mid { background: var(--paper); }
/* the tail below the last dot: `background: inherit` gives the cover the card
   tint; the row itself paints UNDER the rail, so only this cover hides it */
.lg.end { background: inherit; }
.lg.end::before {
  content: ''; position: absolute; left: 0; right: 0;
  top: calc(0.4rem + 0.675em);      /* dot center: row padding + text padding + half a line */
  bottom: -6px; background: inherit; z-index: 2;
}

/* ── every stop name is a place (FLOWS R20) ──
   The whole row is the button and there is NO resting cue on the name: modern
   map apps hide these taps and people still find them (Colin, 28 Aug 2026).
   The press wash is the whole affordance, so it must be unmistakable at
   touch time. The end row paints its rail-cover at z2, so the wash has to
   sit under the text but over that cover — hence the row background only.

   TAP AREA: negative margin + equal padding grows the BOX without moving the
   content — the dots must stay on the rail, which is drawn on .legs and knows
   nothing about row padding. The end row carries no meta line, so it came out
   half the height of the others (Colin, 28 Aug 2026); min-height puts a floor
   under every row instead of patching that one. 2.6rem = 52 px at the 20 px
   base font, above the 44 px both platforms ask for. */
.lg.tap {
  border-radius: 12px; box-sizing: border-box; min-height: 2.6rem;
  padding: 0.3rem 0.45rem; margin-left: -0.45rem; margin-right: -0.45rem;
  transition: background-color 0.12s ease, transform 0.12s ease;
}
.lg.tap:active { background: rgba(51, 33, 28, 0.09); transform: scale(0.985); }

.ridebtn {
  display: block; width: 100%; margin-top: 0.75rem;
  background: var(--red); color: var(--cream);
  font-size: 1.25rem; font-weight: 800; padding: 0.9rem; border-radius: 999px;
  box-shadow: 0 3px 0 var(--red-dark);
}
.ridebtn:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--red-dark); }
/* inside the card drawer the inner clips (overflow: hidden for the 0fr→1fr
   animation) — the bottom margin is the stub body's own padding AND the room
   that keeps the button's 3px shadow off the clip edge */
.cardinner .ridebtn { width: calc(100% - 2rem); margin: 0.75rem 1rem 0.85rem; font-size: 1.1rem; padding: 0.75rem; }

/* cards screen: "a nearer stop has a direct bus" banner + the map branch */
.betterstop {
  display: flex; align-items: center; gap: 0.6rem; width: 100%; text-align: left;
  background: var(--yellow-pale); border: 2.5px solid var(--orange);
  border-radius: var(--r-lg); padding: 0.85rem 1rem; margin-bottom: 0.85rem;
  box-shadow: var(--shadow); line-height: 1.35;
}
.betterstop:active { transform: translateY(2px); box-shadow: none; }
.bs-txt { flex: 1; font-size: 1rem; }
.bs-txt b { color: var(--red-dark); }
.bs-arr { font-size: 1.6rem; font-weight: 800; color: var(--red); }
/* The BOARD / ALIGHT rows are GONE from the sheet (v101): the two ends live
   on the trip bar now — see .tripbar / .tb-line below and design/one-sheet.md. */

/* nearby-stop recommendation */
.codebox.sug { margin-top: 0; background: var(--yellow-pale); border-color: var(--orange); }
.sugbtns { display: flex; gap: 0.5rem; }
.sugbtns .ridebtn { margin-top: 0.2rem; font-size: 1.05rem; padding: 0.7rem; }
.ridebtn.alt { background: var(--mint); color: var(--ink); box-shadow: 0 3px 0 #74ab92; }

/* ── map ── */
.mapscreen { padding: 0; max-width: none; }
/* right gap = 0.8 margin + 2.9 close circle + 0.7 breathing room */
.mapsearch { position: fixed; top: 0.8rem; left: 0.8rem; right: 4.4rem; z-index: 1000; }
.mapsearch .search { font-size: 1.05rem; padding: 0.75rem 2.6rem 0.75rem 0.9rem; }
.mapsearch .suggests { margin-top: 0.5rem; max-height: 45vh; overflow-y: auto; }
.mapsearch .suggest { padding: 0.85rem 1rem; }
.maplocate {
  /* --pickcard-h is measured in app.js (syncCardH): the duck button rides above
     the picker action card, whatever the card is saying. 0 on the switcher map. */
  position: fixed; right: 0.8rem; bottom: calc(var(--pickcard-h, 0px) + 3.4rem); z-index: 1000;
  transition: bottom 0.22s cubic-bezier(0.34, 1.3, 0.64, 1);
  width: 3.4rem; height: 3.4rem; border-radius: 50%;
  background: var(--paper); border: 2.5px solid var(--ink);
  box-shadow: 0 3px 0 rgba(51,33,28,.25);
  display: flex; align-items: center; justify-content: center;
}
.maplocate img { width: 2.1rem; height: auto; display: block; }
#map { position: fixed; inset: 0; background: var(--ground); }
/* icon only: the ✕ is the whole control, so it is a circle the size of the
   other round map buttons — no word, no pill. */
.mapclose {
  position: fixed; top: 0.8rem; right: 0.8rem; z-index: 1000;
  width: 2.9rem; height: 2.9rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--paper);
  font-size: 1.2rem; font-weight: 800; line-height: 1;
  box-shadow: 0 3px 0 rgba(51,33,28,.25);
}
.maphint {
  position: fixed; bottom: 1.2rem; left: 50%; transform: translateX(-50%); z-index: 1000;
  /* translucent + blur: the map stays visible through the pill, the cream
     text stays readable on the darkened glass */
  background: rgba(51, 33, 28, 0.72);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  color: var(--cream); border-radius: 999px;
  /* a helper whisper, not a control: small type, thin padding, one line */
  padding: 0.35rem 0.9rem; font-size: 0.8rem; font-weight: 600; white-space: nowrap;
}
/* ── picker action card ──────────────────────────────────────────────
   The markers only select; THIS commits. It sits at the bottom of the map,
   in thumb reach, and is the one control on the screen — so a map label can
   never be mistaken for a button again. */
.pickcard {
  /* same desktop treatment as .sheet: phone-width, centred */
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1000;
  max-width: 480px; margin: 0 auto;
  /* NO border: a border on one edge only dies half-way round the corner
     radius and leaves a stub. .sheet solves this the same way — the shadow
     does the separating. */
  background: var(--cream);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: 0.6rem 0.9rem calc(0.6rem + env(safe-area-inset-bottom));
  box-shadow: 0 -6px 24px rgba(51, 33, 28, 0.25);
  animation: cardUp 0.28s cubic-bezier(0.34, 1.4, 0.64, 1);
}
@keyframes cardUp { from { transform: translateY(100%); } to { transform: none; } }
/* two quiet lines: stop name (small — the card must stay low on the map),
   then the reason to board here. */
.pk-head { display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; min-width: 0; }
.pk-name {
  max-width: 100%; min-width: 0;   /* without this the name refuses to shrink and overflows */
  font-family: var(--display); font-weight: 800; font-size: 1.05rem; line-height: 1.2;
  color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pk-why {
  font-size: 0.95rem; font-weight: 700; color: var(--red-dark);
  max-width: 100%;   /* long destination names wrap to a second line, never cut */
}
/* the one button on the map: full width, red, unmistakable. Still a big
   target (~54 px) — the height came out of the rows around it, not this. */
.pk-go {
  display: block; width: 100%; margin-top: 0.5rem;
  background: var(--red); color: var(--cream);
  font-size: 1.2rem; font-weight: 800; padding: 0.7rem; border-radius: 999px;
  box-shadow: 0 3px 0 var(--red-dark);
}
.pk-go:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--red-dark); }
/* state B: no stop chosen yet — the card states the task instead */
.pk-task { font-size: 1.05rem; font-weight: 800; color: var(--ink); line-height: 1.3; }
/* Legend: it explains the MAP, so it sits ON the map, under the search bar —
   the card stays a control. Drawn as the marker colours themselves, never
   named; the dots must match .stopdot.pd / .pc / .psel. */
.maplegend {
  position: fixed; top: 4.2rem; left: 0.8rem; z-index: 1000;
  width: max-content; max-width: calc(100% - 1.6rem);
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem 0.9rem;
  background: var(--paper); border-radius: 999px;
  padding: 0.4rem 0.8rem; box-shadow: var(--shadow);
  font-size: 0.8rem; font-weight: 700; color: var(--ink-soft);
}
.pk-key { display: inline-flex; align-items: center; gap: 0.4rem; }
.pk-dot {
  width: 15px; height: 15px; border-radius: 50%; flex: none;
  border: 2.5px solid var(--paper); box-shadow: 0 1px 3px rgba(51, 33, 28, 0.4);
}
.pk-dot.direct { background: var(--red); }
.pk-dot.change { background: #9a8f80; }
.pk-dot.mine { background: #2e6f94; }
/* "My Location": the way back from a hand-picked stop to the fan-out. It sits
   under the legend, reads as a control (raised, ink border) and carries the
   duck — the same word for "where I am" the locate button uses. */
.pk-reset {
  position: fixed; top: 7.1rem; left: 0.8rem; z-index: 1000;
  display: inline-flex; align-items: center; gap: 0.4rem;
  background: var(--paper); border: 2.5px solid var(--ink); border-radius: 999px;
  padding: 0.35rem 0.85rem 0.35rem 0.5rem;
  font-size: 0.9rem; font-weight: 800; color: var(--ink);
  box-shadow: 0 3px 0 rgba(51, 33, 28, 0.25);
}
.pk-reset img { width: 1.4rem; height: auto; display: block; }
.pk-reset:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(51, 33, 28, 0.25); }
/* no bus reaches the destination at all */
.pickcard.warn { background: var(--red); }
.pickcard.warn .pk-task { color: var(--cream); }
.stopdot .dot { width: 18px; height: 18px; border-radius: 50%; background: #2e7d4f; border: 3px solid var(--paper); box-shadow: 0 1px 4px rgba(0,0,0,.4); }
.stopdot .lbl { font-size: 12px; font-weight: 700; background: rgba(255,253,244,.96); border: 2px solid rgba(51,33,28,.55); border-radius: 7px; padding: 1px 6px; margin-top: 2px; white-space: nowrap; transform: translateX(calc(-50% + 12px)); width: max-content; max-width: 160px; overflow: hidden; text-overflow: ellipsis; }
.stopdot.sel .dot { width: 26px; height: 26px; background: var(--red); border-width: 4px; }
.stopdot.sel .lbl { background: var(--red); color: var(--cream); border-color: var(--red-dark); }
/* on a red label the info line must be light, not dark red */
.stopdot.sel .lbl .lblinfo { color: var(--yellow-pale); }
/* stop-picker: direct stops loud, change-only stops quiet */
.stopdot.pd .dot {
  width: 26px; height: 26px; background: var(--red); border-width: 4px;
  animation: pickpulse 1.8s ease-out infinite;
}
/* radiating ring: "these are the stops you can pick" */
@keyframes pickpulse {
  0%   { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), 0 0 0 0 rgba(217, 85, 97, 0.55); }
  70%  { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), 0 0 0 16px rgba(217, 85, 97, 0); }
  100% { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), 0 0 0 0 rgba(217, 85, 97, 0); }
}
.stopdot.pd .lbl { border: 2px solid var(--red); max-width: 180px; }
.stopdot.pc .dot { width: 16px; height: 16px; background: #9a8f80; }
.stopdot.pc .lbl { opacity: 0.82; }
/* picker: the stop the user stands at now — blue, so red(direct)/grey(change) keep their meaning */
.stopdot.psel .dot { width: 30px; height: 30px; background: #2e6f94; border-width: 5px; }
.stopdot.psel .lbl { background: #2e6f94; color: #fff; border-color: #1f4d68; opacity: 1; }
.stopdot.psel .lbl .lblinfo { color: var(--mint-pale); }
/* picker: the stop the user TAPPED — a dark ring ties the pin to the action
   card at the bottom. Selection only; the card's button commits.
   Must stay after .pd/.pc/.psel — same specificity, source order decides. */
.stopdot.ptap .dot {
  width: 30px; height: 30px; border-width: 5px;
  /* box-shadow ring, not outline: it also cancels the .pd "pick me" pulse,
     which is a box-shadow animation on the same element */
  box-shadow: 0 0 0 3px var(--paper), 0 0 0 6px var(--ink);
  animation: none;
}
.stopdot.ptap .lbl { border-color: var(--ink); border-width: 3px; opacity: 1; max-width: 200px; }
/* ── stop peek (FLOWS K1) ──
   ONE pin, in the colour of the card dot it came from, so the card and the
   map say the same thing twice. Keep these in step with .tldot / .tldot.mid
   / .tldot.end in the card spine — a pin that does not match its dot is
   worse than no colour at all. */
.stopdot.pk .dot { width: 30px; height: 30px; border-width: 5px; background: var(--red); }
.stopdot.pk.change .dot { background: var(--paper); border-color: var(--red); }
.stopdot.pk.end .dot { background: var(--orange); border-color: var(--red); }
.stopdot.pk .lbl { border: 2px solid var(--red); max-width: 220px; opacity: 1; }
.stopdot.pk.end .lbl { border-color: var(--orange); }
/* the place the user asked for, at the far end of the get-off walk: a small
   ink dot, deliberately quieter than the stop it walks from.
   NOT `.place` — that class already dresses the recent-places buttons, and a
   marker wearing it inherited their padding and background (v99). */
.stopdot.destmark .dot { width: 18px; height: 18px; background: var(--ink); border-width: 4px; }
.stopdot.destmark .lbl { border: 2px solid var(--ink); }
/* the dashed walk line: the same 1-13 dotted rhythm the card spine uses */
.walkline { stroke: var(--ink); stroke-width: 5; stroke-linecap: round; stroke-dasharray: 1 13; opacity: 0.72; }
.lblinfo { display: block; color: var(--red-dark); font-weight: 800; }
.stopdot.pc .lbl .lblinfo { color: var(--ink-soft); font-weight: 600; }
.meduck { text-align: center; }
.leaflet-container .meduck img {
  /* leaflet.css forces marker imgs to width:auto with !important-level specificity — out-rank it */
  width: 52px !important; height: auto !important; display: block; margin: 0 auto;
  filter: drop-shadow(0 4px 3px rgba(51, 33, 28, 0.35));
  animation: waddle 1.6s ease-in-out infinite;
  transform-origin: 50% 90%;
}
/* zoomed out: shrink the duck so it stops covering nearby stop markers/labels.
   Scale the wrapper, not the img — the img's transform is the waddle animation,
   and scaling the layout width would move the feet up away from the icon anchor. */
.leaflet-container .meduck .duckbody { transform-origin: 50% 100%; transition: transform 0.2s ease-out; }
.leaflet-container .meduck.far .duckbody { transform: scale(0.58); }
.meduck.far .melbl2 { display: none; }
@keyframes waddle {
  0%, 100% { transform: rotate(-4deg); }
  50%      { transform: rotate(4deg) translateY(-2px); }
}
.melbl2 {
  margin-top: 1px; background: var(--red); color: var(--cream);
  font-size: 12px; font-weight: 800; border-radius: 999px; padding: 2px 9px;
  white-space: nowrap; width: max-content; margin-left: 50%; transform: translateX(-50%);
  box-shadow: 0 2px 0 var(--red-dark);
}

/* ── ride ── */
.ride { display: flex; flex-direction: column; min-height: 100vh; background: var(--mint); background-image: radial-gradient(rgba(217,85,97,.10) 1.2px, transparent 1.2px); background-size: 22px 22px; transition: background-color 0.3s; }
.ride.alert { background-color: var(--orange); }
.ridehead { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; }
.ridehead .lang { background: var(--paper); }
.countdown { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.bignum { font-family: var(--display); font-style: italic; font-size: 9.5rem; font-weight: 900; line-height: 1; color: var(--red-dark); }
.bignum.mid { font-family: var(--display); font-style: italic; font-size: 5rem; background: var(--red); color: var(--cream); border-radius: 22px; padding: 0.5rem 1.5rem; margin: 0.8rem 0; box-shadow: 0 4px 0 var(--red-dark); }
.bigword { font-size: 1.6rem; font-weight: 700; color: var(--ink); margin-top: 0.5rem; }
.ridefoot { text-align: center; padding-bottom: 1rem; font-size: 1.1rem; color: var(--ink-soft); }
.offbig { font-family: var(--display); font-style: italic; font-size: 1.6rem; font-weight: 800; color: var(--ink); }
.offnow { margin-top: 0.6rem; font-size: 1.8rem; font-weight: 900; color: var(--red-dark); animation: blink 0.8s infinite; }
@keyframes blink { 50% { opacity: 0.2; } }

/* ── game feel: small, springy, never busy ── */
@keyframes cardIn {
  from { opacity: 0; transform: translateY(18px) scale(0.96); }
  to   { opacity: 1; transform: none; }
}
.card, .codebox.sug { animation: cardIn 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; }
.card:nth-child(3n+1) { animation-delay: 0.05s; }
.card:nth-child(3n+2) { animation-delay: 0.13s; }
.card:nth-child(3n)   { animation-delay: 0.21s; }

@keyframes rowIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: none; } }
.suggest, .place { animation: rowIn 0.3s ease-out backwards; }
.suggests > *:nth-child(2), .places > *:nth-child(2) { animation-delay: 0.06s; }
.suggests > *:nth-child(3), .places > *:nth-child(3) { animation-delay: 0.12s; }
.suggests > *:nth-child(4), .places > *:nth-child(4) { animation-delay: 0.18s; }
.suggests > *:nth-child(n+5), .places > *:nth-child(n+5) { animation-delay: 0.24s; }

@keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
.pin { display: inline-block; animation: bob 2.4s ease-in-out infinite; }

.eta.isnow { animation: pulse 1s ease-in-out infinite; }

@keyframes starPop { 0% { transform: scale(0.6) rotate(-20deg); } 70% { transform: scale(1.35); } 100% { transform: scale(1); } }
.favstar.on { animation: starPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); }

@keyframes numPop { 0% { transform: scale(1.35); opacity: 0.4; } 100% { transform: scale(1); opacity: 1; } }
.bignum { animation: numPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }

@keyframes dotIn { from { transform: scale(0); } to { transform: scale(1); } }
.stopdot .dot { animation: dotIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; }
.stopdot .lbl { animation: rowIn 0.3s 0.1s ease-out backwards; }

.busno { transition: transform 0.15s; }
.card:active .busno { transform: rotate(-3deg) scale(1.06); }
.destname { animation: rowIn 0.3s ease-out backwards; }

/* ── sliding sheet: a card over the dimmed home ──
   The destination sheet was DELETED in v101 (design/one-sheet.md): one sheet,
   with a `step`. What is left here serves the saved-stops peek (.favsheet)
   and the two small ★ sheets. .sheethead / .sheetbody / .grabber are shared
   with the home sheet. */
.sheetback { position: fixed; inset: 0; background: rgba(51, 33, 28, 0.38); z-index: 60; }
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; margin: 0 auto; max-width: 480px;
  height: 90vh; height: 90dvh;
  background: var(--cream); border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: 0 -6px 24px rgba(51, 33, 28, 0.25); z-index: 61;
  display: flex; flex-direction: column;  /* overflow stays visible: the ✕ hangs outside */
}
.sheethead {
  flex: none; display: flex; align-items: center; justify-content: center;
  padding: 0.5rem 0 0rem; touch-action: none;
}
.sheetbody { flex: 1; overflow-y: auto; padding: 0 0.8rem 2rem; overscroll-behavior: contain; }
.grabber { width: 46px; height: 5px; border-radius: 3px; background: var(--ink-soft); opacity: 0.55; }
/* the old .sheetclose that hung outside the sheet is gone — every sheet
   closes with the small .hs-close circle inside its header */
.sheetup-enter-active { transition: transform 0.38s cubic-bezier(0.26, 0.9, 0.38, 1.02); }
.sheetup-leave-active { transition: transform 0.3s ease-in; }
.sheetup-enter-from, .sheetup-leave-to { transform: translateY(105%); }
.dim-enter-active, .dim-leave-active { transition: opacity 0.3s; }
.dim-enter-from, .dim-leave-to { opacity: 0; }

/* ── home: map canvas + signboard sheet ──
   design/home-v2-map-canvas.md. The map is the world; the home page is a
   card floating over it, snapping between three heights (detents). */
#map2 { position: fixed; inset: 0; z-index: 0; background: var(--ground); }  /* z-index:0 contains leaflet's own panes */
.home2 .lang {
  position: fixed; top: 0.8rem; right: 0.8rem; z-index: 55;
  background: var(--paper); box-shadow: 0 3px 0 rgba(51, 33, 28, 0.25);
}
/* the veil: while the board is up the map is context, not a control —
   every touch lands here, and one tap slides the sheet to the map detent */
.mapveil { position: fixed; inset: 0; z-index: 40; touch-action: none; }  /* a pinch on the veil must not zoom the page */
/* duck locate button rides just above the sheet's top edge, whatever detent */
.home2 .maplocate, .home2 .favlocate { z-index: 55; transition: bottom 0.32s cubic-bezier(0.26, 0.9, 0.38, 1.02); }
.home2.rest .maplocate, .home2.rest .favlocate { bottom: calc(62dvh + 0.9rem); }
/* the trips step rests LOWER (45dvh, Google-Maps proportions — R23/R24). Three
   classes, so it beats .home2.rest whatever the source order. Keep it in step
   with .homesheet.step-trips.rest and with restH() in app.js: one number,
   three places. */
.home2.step-trips.rest .maplocate, .home2.step-trips.rest .favlocate { bottom: calc(45dvh + 0.9rem); }
.home2.map  .maplocate, .home2.map  .favlocate { bottom: calc(140px + 0.9rem); }
.home2.full .maplocate, .home2.full .favlocate { display: none; }   /* a sliver of map is not a map */
/* hint pill must never run under the duck locate button. The pill is
   centred, the duck owns the right 4.2rem — a centred pill collides only
   past 100vw - 8.4rem, so cap just under that and wrap as the last resort */
.home2 .maphint {
  z-index: 45; bottom: calc(140px + 0.9rem);
  max-width: calc(100vw - 8.6rem); white-space: normal; text-align: center;
}
.home2 .mapsearch { z-index: 55; }   /* above the sheet, still under the destination sheet (61) */
.homesheet {
  position: fixed; left: 0; right: 0; bottom: 0; margin: 0 auto; max-width: 480px; z-index: 50;
  /* the sheet IS the home page sliding over the world — mustard ground,
     same confetti dots as the body, cream card edges do the rest */
  background: var(--ground);
  background-image: radial-gradient(rgba(217, 85, 97, 0.12) 1.2px, transparent 1.2px);
  background-size: 22px 22px;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: 0 -6px 24px rgba(51, 33, 28, 0.25);
  display: flex; flex-direction: column;
  height: 62dvh; transition: height 0.32s cubic-bezier(0.26, 0.9, 0.38, 1.02);
  overflow: hidden;
  touch-action: pan-y;   /* scroll yes, browser pinch-zoom no */
}
.homesheet.full { height: 92dvh; }
.homesheet.map  { height: 140px; }
/* Steps (design/one-sheet.md): at search and trips the sheet must sit UNDER
   the trip bar, so its full height is "the screen minus the bar". --tripbar-h
   is the bar's OUTER BOTTOM EDGE, measured in app.js (measureTripbar), and
   fullH() in app.js repeats this sum for the detent maths — change both.
   Search has ONE detent (T8), so it is not gated on .full. These MUST stay
   after .homesheet.full: same specificity, source order decides. */
.homesheet.step-search,
.homesheet.step-trips.full { height: calc(100dvh - var(--tripbar-h, 0px) - 0.6rem); }
/* R23/R24: the trips step rests at 45dvh, not 62 — the map above it carries
   the drawn route, so the map gets the bigger half (Colin, 2 Sep 2026).
   restH() in app.js repeats this number for the detent snapping. */
.homesheet.step-trips.rest { height: 45dvh; }
.hs-top { flex: none; touch-action: none; }   /* the drag band owns its touches */
.hs-stop {
  display: flex; align-items: center; gap: 0.5rem; min-width: 0;
  padding: 0 1rem; cursor: pointer;
}
.hs-name {
  flex: 0 1 auto; min-width: 0;   /* the whole flex chain needs min-width:0 or long names overflow */
  font-family: var(--display); font-weight: 700; font-size: 1.2rem;
  color: var(--red); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hs-stop .editmark { width: 1.5rem; height: 1.5rem; font-size: 1.3rem; }
/* trips-step header: a label and the ✕ that leaves the trip. Not a tap row. */
/* the same head as the saved-stops sheet (.pv-head), minus its top air —
   the grabber band above already gives that */
.hs-head { cursor: default; gap: 0.6rem; padding: 0.3rem 0.6rem 0.6rem 1rem; }
.hs-head .pv-ic svg { width: 1.05rem; height: 1.05rem; }
.homesheet.step-trips .results { padding-top: 0; }
.hs-close {
  flex: none; width: 1.9rem; height: 1.9rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--paper); color: var(--ink); font-size: 0.85rem; font-weight: 800;
  box-shadow: 0 2px 0 rgba(51, 33, 28, 0.25);
}
.hs-close:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(51, 33, 28, 0.25); }
.hs-name.dim { color: var(--ink-soft); font-family: var(--body); font-weight: 400; font-size: 1.05rem; }
/* the ★ holds the right edge; the name takes every pixel left over
   (the stop code moved into the signboard label) */
.hs-stop .favstar { margin-left: auto; }
.homesheet.map .sheetbody { overflow: hidden; }   /* the peeking board sliver must not scroll */
/* the pacing duck stands 31px above the board's top edge; the sheetbody's
   scroll clipping cuts its head off without this headroom */
.homesheet .signboard { margin-top: 32px; }
/* the results block starts right under the grabber at the trips step */
.homesheet .results { padding-top: 0.4rem; }

/* ── the trip bar (design/one-sheet.md, FLOWS T1–T8) ──
   Google-Maps direction bar, popsicle build: a cream card riding on the map
   above the sheet. ONE component with two states, so the search box never
   jumps from the sheet to the map between steps. z 56 = above the sheet (50)
   and the map controls (55); it owns the top edge, so the 中文 button hides. */
.tripbar {
  position: fixed; top: 0.8rem; left: 0.8rem; right: 0.8rem; z-index: 56;
  max-width: 480px; margin: 0 auto;
  display: flex; align-items: center; gap: 0.5rem; min-width: 0;
  background: var(--cream); border-radius: var(--r-lg);
  padding: 0.25rem 0.4rem;
  box-shadow: var(--shadow);
}
.tripbar .searchwrap { flex: 1; min-width: 0; }
.tripbar .search { font-size: 1rem; padding: 0.55rem 2.4rem 0.55rem 0.8rem; }
/* the round back control: the same circle as .mapclose, one shade smaller so
   the search-step bar matches the two-line trips bar in height */
.tb-back {
  flex: none; align-self: center;
  width: 2.3rem; height: 2.3rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--paper); color: var(--ink);
  box-shadow: 0 3px 0 rgba(51, 33, 28, 0.25);
}
.tb-back svg { width: 1.2rem; height: 1.2rem; }
.tb-back:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(51, 33, 28, 0.25); }
/* Swap: a small INLINE icon at the right end of the destination line, the way
   Google Maps does it (Colin, 2 Sep 2026). No circle and no shadow at rest —
   a raised button that size read as the main control of the bar. */
.tb-swap {
  flex: none; align-self: center;
  width: 2rem; height: 2rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: none; color: var(--ink-soft); box-shadow: none;
}
.tb-swap svg { width: 1.1rem; height: 1.1rem; }
.tb-swap:active:not(:disabled) { background: rgba(51, 33, 28, 0.08); }
/* no origin end to swap: say so instead of taking a tap and doing nothing */
.tb-swap:disabled { opacity: 0.35; box-shadow: none; cursor: default; }
/* the two ends, stacked. min-width:0 on EVERY item in the chain or a long
   place name refuses to shrink and pushes the bar off the screen. */
.tb-lines { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* the destination line and the inline swap icon share one row — a <button>
   cannot hold another <button>, so they are siblings in this wrapper */
.tb-row { display: flex; align-items: center; min-width: 0; }
.tb-row .tb-line { flex: 1; }
.tb-line {
  /* a <button> with display:flex is CENTRED by the UA stylesheet — say
     flex-start, or the two names sit at different indents */
  display: flex; align-items: center; justify-content: flex-start; text-align: left;
  width: 100%; min-width: 0; gap: 0.55rem;
  min-height: 2rem;               /* 40 px at the 20 px base font: compact, still a target */
  padding: 0 0.3rem; border-radius: 10px;
}
/* the hairline is its OWN element: a border-top on the rounded button
   curved at both ends (Colin, 2 Sep 2026) */
.tb-sep { flex: none; height: 1.5px; margin: 0 0.3rem; background: rgba(51, 33, 28, 0.14); }
.tb-line:active { background: rgba(51, 33, 28, 0.08); }
.tb-txt {
  flex: 1; min-width: 0;
  font-family: var(--display); font-weight: 700; font-size: 0.9rem;
  color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* My Location = the duck, the same marker the map uses for "you" */
.tb-duck { flex: none; width: 1.15rem; height: auto; display: block; }
/* the dots ARE the labels: red = where you get on, orange = where you go.
   Keep them in step with .tldot / .tldot.end in the card spine. */
.tb-dot {
  flex: none; width: 14px; height: 14px; border-radius: 50%; box-sizing: border-box;
  background: var(--red); border: 3px solid var(--red);
}
.tb-dot.end { background: var(--orange); border-color: var(--red); }

/* ── favourite stops (design/favourite-stops.md) ──
   A saved stop is a place you READ from anywhere. Everything here is
   paper/cream: amber-on-black stays the live board of the stop you stand at. */

/* the ☆ / ★ on the signboard's stop row: outline saves, filled opens
   Rename / Remove — so one tap can never lose a saved name */
.favstar {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 2.6rem; height: 2.6rem; margin: -0.4rem -0.4rem -0.4rem 0;   /* 48px target, no extra row height */
  color: var(--ink-soft); background: none;
}
.favstar.on { color: var(--red); }
.staricon { width: 1.45rem; height: 1.45rem; }
.favstar:active { transform: scale(0.9); }

/* ── the saved-stops button: the duck locate button's mirror ──
   Bottom-LEFT of the map canvas, same 3.4rem circle, same ride above the
   sheet's top edge, gone at the full detent (rules above, shared with
   .maplocate). It costs nothing to place: the duck owns the right 4.2rem and
   the hint pill is capped at calc(100vw - 8.6rem) and centred, so this slot
   was already reserved and empty. Icon only — no word to size around, and the
   corner therefore looks identical every single time the app opens. */
.favlocate {
  position: fixed; left: 0.8rem; bottom: 3.4rem; z-index: 1000;
  width: 3.4rem; height: 3.4rem; border-radius: 50%;
  background: var(--paper); border: 2.5px solid var(--ink);
  box-shadow: 0 3px 0 rgba(51, 33, 28, 0.25); color: var(--red);
  display: flex; align-items: center; justify-content: center;
}
.favlocate .staricon { width: 1.75rem; height: 1.75rem; }
.favlocate:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(51, 33, 28, 0.25); }

/* ── the popped peek ──
   Every saved stop already showing its times. No list to walk, no row to
   expand, no card inside a card: flat rows on ONE cream surface, hairlines
   between stops. Height follows the content, so two saved stops do not open
   a full-height sheet. */
/* the ground shows through between the cards, exactly like the map home:
   mustard ground, cream cards. The sheet body is therefore NOT cream. */
.favsheet { height: auto; max-height: 84dvh; background: var(--ground); }
.pv-head {
  flex: none; display: flex; flex-direction: row; align-items: center; gap: 0.6rem;
  padding: 0.8rem 0.6rem 0.6rem 1rem; touch-action: none;   /* the head band drags the sheet */
}
.pv-ic { display: inline-flex; color: var(--red); }
.pv-ic .staricon { width: 1.05rem; height: 1.05rem; }
.pv-title { flex: 1; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; font-size: 0.8rem; color: var(--red-dark); }
.pv-refresh {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; border-radius: 50%;   /* = .hs-close: one size for every sheet action */
  background: var(--paper); color: var(--ink-soft); box-shadow: 0 2px 0 rgba(51, 33, 28, 0.25);
}
.pv-refresh svg { width: 0.95rem; height: 0.95rem; }
.pv-refresh:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 1px 0 rgba(140, 96, 20, 0.18); }
/* with every card closed there is nothing to refresh — the button says so
   instead of taking a tap and doing nothing */
.pv-refresh:disabled { opacity: 0.35; box-shadow: none; cursor: default; }
/* the dim is the fallback feedback: the blanket reduced-motion rule at the
   bottom of this file kills the spin, and a control must still answer a tap */
.pv-refresh.spin { opacity: 0.6; }
.pv-refresh.spin svg { animation: favspin 0.8s linear infinite; }
@keyframes favspin { to { transform: rotate(360deg); } }

.pv-list { flex: 1; overflow-y: auto; padding: 0.2rem 0.9rem 1.6rem; overscroll-behavior: contain; }

/* ── one card per saved stop ──
   The old flat list ran every stop together under hairlines and read as one
   long wall. Each stop now owns a card: name row on top, times panel inside.
   Card gap does the separating, so the hairline between stops is gone. */
/* the card IS a signboard: the same warm-plastic bezel, the same dark VFD
   screen inside it. Bezel numbers copied from .signboard so the two boards
   look like one product. */
.stopblk {
  /* --r-md, one step tighter than the main signboard's --r-lg. The side
     padding drops with it (10px -> 5px) so 15 - 5 = 10 still equals .times'
     radius: the screen stays concentric AND keeps matching the main board's
     own screen. NB: 1rem = 20px here (elderly-first base). */
  background: linear-gradient(#f7f0d0, #e6dcae); border-radius: var(--r-md);
  padding: 0.45rem 0.25rem 0.65rem;
  box-shadow: var(--shadow), inset 0 2px 0 rgba(255, 255, 255, 0.65), inset 0 -3px 0 rgba(51, 33, 28, 0.16);
}
.stopblk + .stopblk { margin-top: 0.7rem; }
/* closed, the card is just the plaque — so it needs its bottom padding to
   match its top instead of the room the screen leaves */

/* the drawer. grid 0fr -> 1fr animates to the content's OWN height, which no
   max-height guess can do; the inner div does the clipping. */
.blk-body {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows 0.34s cubic-bezier(0.26, 0.9, 0.38, 1.02);
}
.stopblk.open .blk-body { grid-template-rows: 1fr; }
/* padding on the clipped box would sit OUTSIDE the 0fr track and keep a
   closed card ~7px too tall — only an OPEN card gets the gap */
.blk-inner { overflow: hidden; min-height: 0; }
.stopblk.open .blk-inner { padding-top: 0.4rem; }
/* the name band = the board's .sb-label. .bh-sub / .bh-plain split the two
   cases (with and without a nickname) so each can be padded on its own. */
.blk-head {
  display: flex; align-items: center; gap: 0.5rem; min-width: 0; padding: 0rem 0.25rem;
  min-height: 1.6rem;   /* 32px — the ★ overflows it, so it costs no height */
  cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent;
}
/* press feedback dims the row instead of moving it — a card that jumps while
   its drawer is opening reads as two animations fighting */
.blk-head:active .blk-txt, .blk-head:active .pv-star { opacity: 0.5; }
/* Why the one-line head needs no padding: the 2.4rem ★ is taller than the
   name, so it opens ~9px of slack above and below on its own. Two lines are
   taller than the ★, so that slack disappears and the head reads crammed —
   .bh-sub puts the same space back by hand. */
.bh-sub { padding: 0.15rem 0.25rem 0rem; }
.blk-txt { flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.blk-label {
  font-family: var(--display); font-weight: 800; font-size: 1.15rem; line-height: 1.15; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* only when the stop carries a nickname — the code is never printed here */
.blk-sub { font-size: 0.65rem; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pv-star {
  flex: none; margin: -0.1rem -0.25rem -0.1rem auto;   /* taller than the row, but not part of it */
  width: 1.8rem; height: 1.8rem;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; color: var(--red);
}
.pv-star .staricon { width: 1.15rem; height: 1.15rem; }
.pv-star:active { transform: scale(0.9); }

/* the screen inside the card — same glass as .sb-screen, scanlines and all */
.times, .tload {
  position: relative;
  background: var(--vfd); border-radius: 10px;   /* concentric with the card */
  padding: 0.4rem 0.75rem;
  box-shadow: var(--vfd-well);
  overflow: hidden;
}
.times::after, .tload::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background:
    linear-gradient(rgba(255, 255, 255, 0.05), transparent 18%),
    repeating-linear-gradient(0deg, transparent 0 2px, rgba(0, 0, 0, 0.15) 2px 3px);
}
.trow {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0.1rem;
  color: #ffb63c; text-shadow: 0 0 8px rgba(255, 170, 40, 0.45);
}
.trow + .trow, .trow + .thint, .thint + .thint { border-top: 1px solid rgba(255, 182, 60, 0.14); }
/* same shape as .sb-line: number and destination on one line, centred */
.tsvc { flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.4rem; }
.tnum {
  flex: none;
  font-family: var(--display); font-style: italic; font-weight: 900; font-size: 1.35rem;
  line-height: 1.15; letter-spacing: 0.02em;
}
.tdest { flex: 0 1 auto; min-width: 0; font-family: var(--body); font-style: normal; font-weight: 500; font-size: 0.7rem; color: #d99a2e; letter-spacing: 0.03em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-shadow: none; }
/* soonest on the LEFT, then the later ones in the board's boxes */
.tmins { flex: none; display: flex; align-items: center; gap: 0.35rem; font-variant-numeric: tabular-nums; }
.tm1 { font-family: var(--display); font-style: italic; font-weight: 800; font-size: 1.1rem; line-height: 1.05; white-space: nowrap; margin-right: 0.3rem; text-align: center; width: 1.8rem;}
.tm1.isnow { color: #7dffa0; text-shadow: 0 0 9px rgba(90, 255, 140, 0.5); animation: sbblink 1.1s steps(2) infinite; font-size:0.65rem; text-transform: uppercase;}
.tm2 {
  font-size: 0.8rem; color: #cf9231; white-space: nowrap;
  border: 1px solid rgba(255, 182, 60, 0.35); border-radius: 5px;
  padding: 0.05rem 0.35rem; line-height: 1.3;
  text-shadow: none;
}
.thint { display: flex; align-items: center; gap: 0.55rem; padding: 0.45rem 0.1rem; font-size: 0.85rem; color: #b8801f; letter-spacing: 0.04em; font-variant-numeric: tabular-nums; }
.thint b { font-family: var(--display); font-style: italic; font-weight: 900; font-size: 1.05rem; }
.tload { padding: 0.75rem 0.85rem; font-size: 0.9rem; color: #d99a2e; text-shadow: 0 0 8px rgba(255, 170, 40, 0.4); }

/* signboard hint line: a service the timetable rules out. Dim amber, small —
   it must never compete with a real departure. */
.sb-hint {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding: 0.3rem 0.1rem;
  color: #b8801f; font-size: 0.85rem; letter-spacing: 0.04em;
}
.sb-row + .sb-hint, .sb-hint + .sb-hint { border-top: 1px solid rgba(255, 182, 60, 0.1); }
.sb-hsvc { font-family: var(--display); font-style: italic; font-weight: 900; font-size: 1.05rem; }
.sb-hfrom { font-variant-numeric: tabular-nums; }

/* ── the two small sheets: "Give it a name?" and Rename / Remove ──
   In-app only. A browser confirm() would block the page and its timers. */
.favback { z-index: 70; }
.favmini {
  position: fixed; left: 0; right: 0; bottom: 0; margin: 0 auto; max-width: 480px;
  z-index: 71; background: var(--cream);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: 0 -6px 24px rgba(51, 33, 28, 0.25);
  padding: 1.4rem 1.1rem calc(1.4rem + env(safe-area-inset-bottom));
}
.fm-head {
  display: flex; align-items: center; gap: 0.45rem;
  font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;
  font-size: 0.85rem; color: var(--ink-soft);
}
.fm-ic { display: inline-flex; color: var(--red); }
.fm-ic .staricon { width: 1.1rem; height: 1.1rem; }
.fm-name { font-family: var(--display); font-weight: 800; font-size: 1.4rem; color: var(--red); margin-top: 0.25rem; }
.fm-sub { font-size: 0.9rem; color: var(--ink-soft); margin-top: 0.15rem; }
.fm-ask { font-size: 1.05rem; color: var(--ink); margin: 0.9rem 0 0.6rem; }
.favmini .search { margin-top: 0.8rem; }
.fm-btns { display: flex; gap: 0.6rem; margin-top: 0.9rem; }
.fm-btn {
  flex: 1; padding: 0.9rem 0.6rem; border-radius: var(--r-lg);
  font-size: 1.1rem; font-weight: 700; box-shadow: var(--shadow);
}
.fm-btn.ghost { background: var(--paper); color: var(--ink); border: 2.5px solid var(--ink); }
.fm-btn.go, .fm-btn.danger { background: var(--red); color: var(--cream); box-shadow: 0 4px 0 var(--red-dark); }
.fm-btn:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(140, 96, 20, 0.18); }
.fm-remove {
  width: 100%; margin-top: 0.8rem; padding: 0.7rem;
  font-size: 1rem; font-weight: 700; color: var(--red-dark);
}

/* keep this block LAST: blanket animation kill-switch */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}
