/* riso.css — the TRAVELLER skin. Loaded by index.html and by nothing else.
   Started as a copy of theme.css and diverged into the riso/print language.

   It is a whole sheet, not a layer on top of theme.css, because the two surfaces
   no longer share a design language. op.html / admin.html / console.html keep
   theme.css untouched — dashboard.css declares no tokens of its own, so anything
   edited in theme.css lands on the operator and admin dashboards as well. That is
   exactly how the riso tokens leaked into them, and the split is what prevents it.

   SKIN (operator) + semantic tokens + cockpit components. Dark/light is driven by
   [data-mode] (set from the destination's time of day), so it changes brightness
   while keeping the operator brand. Presentation scale (hero/gallery/border/plate)
   is defaulted here, overridden per tier in tiers.css. */

/* ── Riso tokens ──────────────────────────────────────────────────────────────
   Two plates on paper. The operator's colour is the accent, struck through every
   existing --brand reference so nothing downstream needs to know the theme changed.
   --acid is derived from it at runtime (public/riso.js) and is the second signature.

   The KEY PLATE (--chrome) never flips between day and evening. Header, tab bar,
   ticket stub, flat thumbnails and the duotone engine all print on it, and --acid is
   mixed to sit on it — when it flipped to cream in the evening every acid label
   dropped to 1.15:1 and the thumbnails collapsed to flat accent squares. */
:root {
  color-scheme: light;
  /* Skin — set at runtime from the operator */
  /* No operator yet (the four renderError screens boot before the trip payload
     arrives, and a locked trip never gets one). Fall back to the key plate rather
     than a neutral black, which printed as a third ink alongside it. */
  --brand: #16215A;
  --brand-2: #6b6b6b;
  --brand-ink: #ffffff;
  --brand-tint: color-mix(in srgb, var(--brand) 12%, var(--surface));
  --acid: #d5ea53;                       /* recomputed per operator in applyTheme */
  --brand-text: var(--brand);            /* recomputed per mode in applyMode */

  --chrome: #16215A; --chrome-ink: #F2F0E6;

  /* Paper and figure. --surface stays equal to --bg: a riso card is bordered, never
     raised, so there is no second surface to lift it onto. */
  --bg: #F2F0E6;
  --surface: #F2F0E6;
  --surface-2: color-mix(in srgb, #16215A 7%, #F2F0E6);
  --text: #16215A;
  --text-dim: color-mix(in srgb, #16215A 70%, #F2F0E6);
  --line: color-mix(in srgb, #16215A 18%, #F2F0E6);
  --rule: #16215A;                       /* the heavy 2px rule that structures a page */
  --ok: #1f7a44;
  --warn: #a8630a;
  --danger: #c4281c;

  /* Presentation. Hard edges and no shadow: the tier ladder is print richness now,
     carried by hero height, gallery and how freely the second plate is used. */
  --radius: 0px; --radius-sm: 0px;
  --shadow: none;
  /* Type. Latin faces are vendored (scripts/fetch_fonts.py); Chinese is the system stack
     on purpose. A font-family list resolves PER GLYPH, so a name stored as one mixed
     string — "Hangzhou Qiyue Westlake Culture Hotel / 杭州栖约西湖文化酒店" — sets its
     Latin in Work Sans and its Chinese in PingFang without splitting the content. The CJK
     entry must come AFTER the Latin face for that to work, never before. */
  --font-cjk: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif;
  --font-display: "Bricolage Grotesque", var(--font-cjk);
  --font-numeral: "Bricolage Grotesque", var(--font-cjk);
  --font-body: "Work Sans", var(--font-cjk);
  --font-mono: "DM Mono", var(--font-cjk), ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  --hero: 200px; --gallery: swipe; --motion: 200ms; --maxw: 720px;
}
/* Evening — the same press, the other plate. */
:root[data-mode="dark"] {
  color-scheme: dark;
  --bg: #0F1636;
  --surface: #0F1636;
  --surface-2: color-mix(in srgb, #EFEDE2 9%, #0F1636);
  --text: #EFEDE2;
  --text-dim: color-mix(in srgb, #EFEDE2 58%, #0F1636);
  --line: color-mix(in srgb, #EFEDE2 22%, #0F1636);
  --rule: #EFEDE2;
  --chrome: #1D2A6E;                     /* lifted a step so a dark bar keeps an edge */
  --brand-tint: color-mix(in srgb, var(--brand) 26%, var(--surface));
  --ok: #37d17f; --warn: #f0a53a; --danger: #ff6152;
}

@media (prefers-reduced-motion: reduce) { :root { --motion: 0ms; } * { animation: none !important; scroll-behavior: auto !important; } }

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
.ic { vertical-align: -0.14em; flex: none; }
.chip .ic, .cat .ic, .ex-tag .ic { vertical-align: -0.12em; }
/* iOS inflates the text of some blocks on its own, a leftover from making desktop pages
   legible on a phone. It picks its targets by block width and line count, so it hits a long
   single-line address inside a flex row and leaves the heading above it alone: the reported
   symptom was a hotel card whose address rendered LARGER than the hotel's name, despite
   being set two steps smaller. It only shows once the app is installed to the home screen,
   because a standalone window is not the viewport Safari was tuned for.
   100% means "respect the sizes as written" rather than "never scale", so a traveller who
   has raised their system text size still gets it. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font-body); background: var(--bg); color: var(--text);
  -webkit-font-smoothing: antialiased; line-height: 1.5; overscroll-behavior-y: none;
}
h1, h2, h3 { font-family: var(--font-display); line-height: 1.2; margin: 0 0 .4em; }
a { color: var(--brand-text); }
.dim { color: var(--text-dim); }
.ok { color: var(--ok); } .danger { color: var(--danger); } .center { text-align: center; }
.hidden { display: none !important; }
.app { max-width: var(--maxw); margin: 0 auto; min-height: 100vh; padding-bottom: 96px; }

/* ── Boot splash ── */
#boot { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; background: var(--bg); transition: opacity .35s; }
#boot.gone { opacity: 0; pointer-events: none; }
.boot-dot { width: 46px; height: 46px; border-radius: 0; background: var(--brand, #4338ca); animation: pulse 1.1s ease-in-out infinite; }
@keyframes pulse { 0%,100% { transform: scale(.9); opacity: .6; } 50% { transform: scale(1); opacity: 1; } }

/* ── Cockpit header (brand-as-accent, on surface) ── */
/* The header prints on the key plate in both modes. No blur: a riso sheet has no
   translucency, and a blurred bar over a halftone reads as a smear. */
.chead { position: sticky; top: 0; z-index: 20; background: var(--chrome); color: var(--chrome-ink); border-bottom: 3px solid var(--rule); }

/* ── Today marquee header (option E) ──────────────────────────────────────────
   Three flat siblings in #content, two of them sticky. The identity strip pins at 0 and
   the clock/nav bar pins directly beneath it, so the acid band scrolls up between them
   and the header settles to 99px without a single scroll listener. --hs-h must stay equal
   to the strip's real height or the bar will overlap it. */
:root { --hs-h: 48px; }

.chead-top { position: sticky; top: 0; z-index: 22; display: flex; align-items: center; gap: 10px;
  height: var(--hs-h); padding: 0 14px; background: var(--chrome); color: var(--chrome-ink); }
.hs-mark { width: 28px; height: 28px; flex: none; position: relative; overflow: hidden;
  background: var(--acid); color: var(--chrome); box-shadow: 0 0 0 2px var(--acid); }
.hs-mark.ini { display: grid; place-items: center; font-family: var(--font-display); font-weight: 800; font-size: .74rem; }
.hs-logo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.hs-name { font-family: var(--font-display); font-weight: 800; font-size: .95rem; letter-spacing: -.015em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1; }

/* The band. A whole field of the second plate — the first time acid is a ground rather
   than a highlight, which is what makes this header read as printed. */
.chead-mq { display: flex; align-items: flex-end; gap: 16px; padding: 0 16px;
  background: var(--acid); color: var(--chrome); border-bottom: 3px solid var(--rule); }
.mq-l { margin: 14px 0 12px; flex: none; }
.mq-lab { font-family: var(--font-mono); font-size: .56rem; letter-spacing: .2em; text-transform: uppercase;
  color: color-mix(in srgb, var(--chrome) 74%, transparent); }
.mq-num { font-family: var(--font-numeral); font-weight: 800; font-size: 5.6rem; line-height: .74;
  letter-spacing: -.07em; margin: 2px 0 8px; font-variant-numeric: tabular-nums; }
.ticks { display: flex; gap: 3px; width: 96px; }
.ticks i { height: 5px; flex: 1; background: color-mix(in srgb, var(--chrome) 24%, transparent); }
.ticks i.on { background: var(--chrome); }
/* Today is the one tick that is not the key plate, so "which day is live" survives even
   when you have paged forward to preview another one. */
.ticks i.now { background: var(--brand); box-shadow: 0 0 0 1.5px var(--chrome); }
.ticks.bar { width: 96px; height: 5px; background: color-mix(in srgb, var(--chrome) 24%, transparent); }
.ticks.bar i { height: 100%; flex: none; background: var(--chrome); }

.mq-r { flex: 1; min-width: 0; text-align: left; border: 0; background: none; color: inherit;
  cursor: pointer; padding: 0 0 22px; display: block; }
.mq-city { display: block; font-family: var(--font-mono); font-size: .62rem; letter-spacing: .22em;
  text-transform: uppercase; color: color-mix(in srgb, var(--chrome) 74%, transparent);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Two lines allowed: the city and the date each own a row now, which is the whole reason
   this header cannot reproduce the old subtitle's truncation. */
.mq-date { display: block; font-family: var(--font-numeral); font-weight: 800; font-size: 1.65rem;
  line-height: 1.04; letter-spacing: -.028em; margin-top: 4px; }

/* Trip variant: a name is longer than a date, so it wraps rather than shrinking to fit. */
.mq-date.sm { font-size: 1.24rem; line-height: 1.1; }
.mq-r.as-text { cursor: default; padding-bottom: 18px; }
.hb-install { display: inline-flex; align-items: center; gap: 7px; border: 0; background: none; cursor: pointer;
  color: var(--text); font-family: var(--font-mono); font-size: .66rem; letter-spacing: .12em;
  text-transform: uppercase; padding: 0 15px; height: 100%; -webkit-tap-highlight-color: transparent; }
.hb-install:active { background: var(--brand); color: var(--brand-ink); }

/* ── Day bookends: slim slabs ─────────────────────────────────────────────────
   Printed on the key plate so they bracket the day instead of competing with the stops
   inside it. The perforation always faces the timeline — morning tears off the bottom,
   night off the top — and is drawn in --bg so it reads as paper punched out of the slab. */
.slab { position: relative; width: calc(100% - 28px); margin: 14px 14px 0; display: grid;
  grid-template-columns: 44px 1fr 20px; gap: 11px; align-items: center; padding: 10px 11px;
  background: var(--chrome); color: var(--chrome-ink); border: 0; border-radius: 0;
  text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.slab:active { filter: brightness(1.12); }
.slab.night { margin-top: 0; }
.slab .sl-slot { width: 44px; height: 44px; }
.sl-ph { width: 44px; height: 44px; display: grid; place-items: center; background: var(--acid); color: var(--chrome); }
.sl-b { min-width: 0; }
.sl-r1 { display: flex; align-items: center; gap: 7px; min-width: 0; }
.sl-tag { flex: none; display: inline-flex; align-items: center; gap: 4px; background: var(--acid); color: var(--chrome);
  font-family: var(--font-mono); font-size: .54rem; letter-spacing: .13em; text-transform: uppercase; padding: 2px 5px; }
.slab.night .sl-tag { background: var(--brand); color: var(--brand-ink); }
.sl-nm { font-family: var(--font-display); font-weight: 800; font-size: .88rem; letter-spacing: -.02em;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sl-r2 { display: flex; align-items: center; gap: 5px; margin-top: 4px;
  font-family: var(--font-mono); font-size: .64rem; color: color-mix(in srgb, var(--chrome-ink) 74%, transparent); }
.sl-r3 { display: flex; align-items: center; gap: 6px; margin-top: 4px; min-width: 0;
  font-family: var(--font-mono); font-size: .62rem; color: color-mix(in srgb, var(--chrome-ink) 66%, transparent); }
.sl-ad { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sl-go { color: color-mix(in srgb, var(--chrome-ink) 55%, transparent); display: grid; place-items: center; }
.slab .gr { color: var(--acid); }
.perfB::after, .perfT::before { content: ""; position: absolute; left: 0; right: 0; height: 6px; z-index: 4;
  background: radial-gradient(circle at 4px var(--perf-y), var(--bg) 3.5px, transparent 4px) repeat-x;
  background-size: 8px 6px; }
.perfB::after { bottom: -1px; --perf-y: 6px; }
.perfT::before { top: -1px; --perf-y: 0px; }

/* Closes the timeline before the night slab, so the last stop does not run straight into it. */
.endrule { display: flex; align-items: center; gap: 10px; margin: 18px 16px 12px; }
.er-ln { flex: 1; height: 2px; background: var(--line); }
.er-tx { font-family: var(--font-mono); font-size: .58rem; letter-spacing: .2em; text-transform: uppercase; color: var(--text-dim); }

.chead-bar { position: sticky; top: var(--hs-h); z-index: 21; display: flex; align-items: stretch;
  background: var(--bg); border-bottom: 3px solid var(--rule); }
.hb-clocks { flex: 1; display: flex; gap: 20px; align-items: center; padding: 9px 16px;
  font-family: var(--font-mono); letter-spacing: .06em; min-width: 0; }
/* .clk is coloured for the navy identity header the Trip tab still uses. This bar sits on
   paper, so the same rule rendered cream on cream. */
.hb-clocks .clk { color: var(--text-dim); }
.hb-clocks .clk b { color: var(--text); }
.hb-nav { display: flex; border-left: 3px solid var(--rule); flex: none; }
.nb { border: 0; border-right: 2px solid var(--rule); width: 46px; background: none; color: var(--text);
  display: grid; place-items: center; cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: background var(--motion), color var(--motion); }
.nb:last-child { border-right: 0; }
.nb:active:not(:disabled) { background: var(--brand); color: var(--brand-ink); }
.nb:disabled { opacity: .3; cursor: default; }
.nb.nudge svg { animation: chev-nudge 1.4s ease-in-out 5; }
@media (prefers-reduced-motion: reduce) { .nb.nudge svg { animation: none; } }

/* Evening. A full acid field at night is a lamp; invert to the key plate and let the
   numeral carry the acid instead — the pairing acidFor() already guarantees is legible. */
:root[data-mode="dark"] .chead-mq { background: var(--chrome); color: var(--chrome-ink); }
:root[data-mode="dark"] .mq-num, :root[data-mode="dark"] .mq-date { color: var(--acid); }
:root[data-mode="dark"] .mq-lab, :root[data-mode="dark"] .mq-city { color: color-mix(in srgb, var(--chrome-ink) 66%, transparent); }
:root[data-mode="dark"] .ticks i { background: color-mix(in srgb, var(--chrome-ink) 26%, transparent); }
:root[data-mode="dark"] .ticks i.on { background: var(--acid); }
:root[data-mode="dark"] .ticks i.now { background: var(--brand); box-shadow: 0 0 0 1.5px var(--chrome-ink); }
:root[data-mode="dark"] .ticks.bar { background: color-mix(in srgb, var(--chrome-ink) 26%, transparent); }
:root[data-mode="dark"] .ticks.bar i { background: var(--acid); }
.chead::before { content: ""; display: block; height: 4px; background: var(--brand); }
/* Brand column + a stack of rows (identity, day nav, clocks). */
.chead-wrap { display: flex; align-items: stretch; gap: 10px; padding: 8px 12px 7px; }
.chead-body { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
/* The mark is square and sits beside the stacked rows, which is what lets it be large
   without a taller header; a non-square logo is cropped rather than stretched.
   Sized explicitly rather than stretched: in a row flex container the width resolves
   before the stretched height, so aspect-ratio cannot square it off the column. `.tall`
   is the Today header, which has the day nav and so one more row to sit beside. */
.chead-mark { width: 50px; height: 50px; align-self: center; flex: none; border-radius: 0; overflow: hidden; position: relative; background: var(--acid); color: var(--chrome); }
.chead-wrap.tall .chead-mark { width: 78px; height: 78px; border-radius: 0; }
.chead-mark.ini { display: grid; place-items: center; font-family: var(--font-display); font-weight: 800; font-size: 1.1rem; letter-spacing: .02em; }
.chead-wrap.tall .chead-mark.ini { font-size: 1.6rem; }
/* Absolutely positioned so the image cannot size its own wrapper: an <img> is a replaced
   element and would otherwise impose its intrinsic dimensions on the header. */
.chead-logo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.chead-id { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
/* The operator name must never wrap: a long name used to push the row onto a second line
   and grow the whole header. */
.chead-opname { font-family: var(--font-display); font-weight: 800; font-size: .9rem; letter-spacing: .01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* Both clocks get their own line, so they never squeeze the operator name. */
.chead-clocks { display: flex; justify-content: center; gap: 16px; border-top: 1px solid color-mix(in srgb, var(--chrome-ink) 22%, transparent); margin-top: 2px; padding-top: 5px; font-family: var(--font-mono); letter-spacing: .06em; }
.clk { font-size: .7rem; color: color-mix(in srgb, var(--chrome-ink) 62%, transparent); white-space: nowrap; text-transform: uppercase; }
.lang-btn { display: inline-flex; align-items: center; gap: 4px; border: 1.5px solid color-mix(in srgb, var(--chrome-ink) 38%, transparent); background: transparent; color: var(--chrome-ink); border-radius: 0; padding: 4px 8px; font-family: var(--font-mono); font-size: .66rem; letter-spacing: .08em; font-weight: 500; cursor: pointer; }
.lang-btn .ic { width: 14px; height: 14px; }
.lang-btn:active { transform: scale(.94); }
.langsheet h2 { margin: 0 0 14px; }
.lang-list { display: flex; flex-direction: column; gap: 8px; }
.lang-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; min-height: 58px; padding: 14px 16px; border: 2px solid var(--rule); border-radius: 0; background: var(--surface); color: var(--text); font-size: 1.15rem; font-weight: 600; cursor: pointer; }
.lang-row.on { border-color: var(--brand); color: var(--brand-text); }
.lang-row .ic { color: var(--brand-text); }

/* CJK font fallback for Chinese content — appends system CJK to the stacks */
[lang="zh"] {
  --font-display: -apple-system, "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", system-ui, sans-serif;
  --font-body: -apple-system, "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", system-ui, sans-serif;
}
.clk b { color: var(--chrome-ink); font-variant-numeric: tabular-nums; font-weight: 700; }
/* A live arrow and a dead one used to differ by opacity alone — same box, same border,
   same colour, just fainter. On a dark screen in daylight that is nearly nothing, and
   with no swipe gesture these arrows are the only way to reach another day. The live one
   now carries the operator's colour; the dead one drops to a bare outline. */
.chev { flex: none; width: 38px; height: 38px; border-radius: 0; display: grid; place-items: center;
        background: color-mix(in srgb, var(--brand) 30%, var(--chrome));
        border: 1.5px solid var(--brand);
        color: var(--chrome-ink); font-size: 20px; line-height: 1; cursor: pointer; -webkit-tap-highlight-color: transparent;
        transition: transform .2s cubic-bezier(.2,.7,.2,1), background-color .2s ease, border-color .2s ease, color .2s ease; }
.chev:active:not(:disabled) { transform: scale(.9); }
.chev:disabled { background: transparent; border-color: color-mix(in srgb, var(--chrome-ink) 30%, transparent); color: var(--chrome-ink); opacity: .35; cursor: default; }
/* Beckons five times the first time a trip is opened, then settles for good. The movement
   is a short glance at the end of each 1.4s cycle, not a constant twitch, and it is the
   arrow inside that moves so the button itself never shifts the header's layout. */
.chev.nudge svg { animation: chev-nudge 1.4s ease-in-out 5; }
@keyframes chev-nudge {
  0%, 68%, 100% { transform: translateX(0); }
  78%           { transform: translateX(4px); }
  88%           { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) { .chev.nudge svg { animation: none; } }
/* Neither line may wrap: with the brand column beside it, a wrapped subtitle silently
   added a second line to the day row and grew the whole header on narrow phones. */

/* ── Notice / update banner ── */
.notice { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 12px 16px; padding: 10px 14px; background: var(--brand-tint); border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: .9rem; }
.notice button { border: 2px solid var(--rule); background: var(--brand); color: var(--brand-ink); border-radius: 0; padding: 6px 12px; cursor: pointer; font-weight: 600; }
/* Operator broadcast — an unmistakable, brand-signed announcement. A solid brand
   header bar names the sender in plain words + flags it New (high contrast in light
   and dark); the message sits below on a clean surface. It slides in and the horn
   pulses briefly to catch the eye, then settles. */
/* Live meeting point — sits above any broadcast, because where to stand outranks
   what to know. Static by design: nothing behind it to open. */
/* Live inserts: a ruled block with a key-plate header, stamped onto the sheet rather
   than floated above it. */
.meetcard { margin: 12px 14px; border-radius: 0; overflow: hidden; border: 2px solid var(--rule); box-shadow: 4px 4px 0 var(--brand); animation: bc-in .32s cubic-bezier(.2,.7,.2,1) both; }
.mc-top { display: flex; align-items: center; gap: 8px; padding: 7px 11px; background: var(--chrome); color: var(--chrome-ink); }
.mc-ico { flex: none; display: grid; place-items: center; }
.mc-ico .ic { width: 14px; height: 14px; }
.mc-label { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: .64rem; font-weight: 500; letter-spacing: .16em; color: var(--acid); text-transform: uppercase; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mc-when { flex: none; font-family: var(--font-mono); font-size: .64rem; font-weight: 500; letter-spacing: .06em; text-transform: uppercase; padding: 3px 7px; border-radius: 0; background: var(--brand); color: var(--brand-ink); font-variant-numeric: tabular-nums; }
.mc-inner { padding: 9px 12px 10px; background: var(--surface); }
.mc-where { font-family: var(--font-display); font-size: 1.05rem; font-weight: 700; letter-spacing: -.015em; line-height: 1.32; color: var(--text); overflow-wrap: anywhere; }
.mc-time { display: flex; align-items: center; gap: 5px; margin-top: 4px; font-family: var(--font-mono); font-size: .7rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.mc-time .ic { width: 13px; height: 13px; flex: none; }
/* Inside the hour the pill breathes, so a glance catches it. */
.meetcard.soon .mc-when { animation: mc-pulse 2s ease-in-out infinite; }
@keyframes mc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .meetcard, .meetcard.soon .mc-when { animation: none; } }
/* Stacked with a broadcast they read as one block rather than two loose cards. */
.meetcard + #notice-slot .broadcast { margin-top: -2px; }

.broadcast { margin: 12px 14px; border-radius: 0; overflow: hidden; border: 2px solid var(--rule); box-shadow: 4px 4px 0 var(--brand); animation: bc-in .32s cubic-bezier(.2,.7,.2,1) both; }
.bc-top { display: flex; align-items: center; gap: 8px; padding: 7px 8px 7px 11px; background: var(--brand); color: var(--brand-ink); }
.bc-ico { flex: none; display: grid; place-items: center; transform-origin: center; animation: bc-ring 1.5s ease .35s 2; }
.bc-ico .ic { width: 15px; height: 15px; }
.bc-label { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: .64rem; font-weight: 500; letter-spacing: .16em; text-transform: uppercase; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bc-new { flex: none; font-family: var(--font-mono); font-size: .6rem; font-weight: 500; letter-spacing: .12em; text-transform: uppercase; padding: 3px 6px; border-radius: 0; background: var(--brand-ink); color: var(--brand); }
.bc-inner { padding: 9px 12px 10px; background: var(--surface); }
.bc-text { font-size: .98rem; font-weight: 500; line-height: 1.34; color: var(--text); overflow-wrap: anywhere; }
.bc-ts { display: block; margin-top: 5px; font-family: var(--font-mono); font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.wc-x { border: 1.5px solid color-mix(in srgb, var(--brand-ink) 45%, transparent); background: transparent; color: var(--brand-ink); border-radius: 0; width: 26px; height: 26px; padding: 0; display: grid; place-items: center; flex: none; cursor: pointer; transition: background var(--motion); }
.wc-x:hover { background: color-mix(in srgb, var(--brand-ink) 30%, transparent); }
@keyframes bc-in { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }
@keyframes bc-ring { 0%,100% { transform: rotate(0); } 20% { transform: rotate(-14deg); } 40% { transform: rotate(11deg); } 60% { transform: rotate(-7deg); } 80% { transform: rotate(4deg); } }
@media (prefers-reduced-motion: reduce) { .broadcast, .bc-ico { animation: none; } }

/* Rate today — end-of-day feedback panel pinned beneath the timeline */
.ratetoday { margin: 14px 14px; border: 2px solid var(--rule); border-radius: 0; background: var(--surface); box-shadow: none; overflow: hidden; }
.rt-bar { width: 100%; display: flex; align-items: center; gap: 11px; padding: 13px 14px; border: 0; background: transparent; color: var(--text); cursor: pointer; text-align: left; }
.rt-ic { flex: none; width: 30px; height: 30px; border-radius: 0; display: grid; place-items: center; background: var(--brand); color: var(--brand-ink); }
.ratetoday.done .rt-ic { background: var(--ok); }
.rt-ic .ic { width: 16px; height: 16px; }
.rt-bar-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.rt-bar-txt b { font-family: var(--font-display); font-size: .98rem; font-weight: 700; letter-spacing: -.01em; }
.rt-bar-txt span { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); }
.rt-chev { flex: none; color: var(--text-dim); transition: transform var(--motion); }
.rt-bar[aria-expanded="true"] .rt-chev { transform: rotate(180deg); }
.rt-form { padding: 4px 14px 16px; border-top: 1px solid var(--line); }
.rt-field { margin-top: 16px; }
.rt-q { display: flex; align-items: center; gap: 8px; font-size: .9rem; font-weight: 600; margin-bottom: 9px; }
.rt-req, .rt-opt { font-family: var(--font-mono); font-size: .6rem; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; padding: 3px 6px; border-radius: 0; }
.rt-req { color: var(--brand-text); background: var(--brand-tint); }
.rt-opt { color: var(--text-dim); background: var(--surface-2); }
.rt-stars { display: flex; gap: 6px; }
.rt-star { border: 0; background: transparent; padding: 2px; cursor: pointer; line-height: 0; transition: transform var(--motion); }
.rt-star:active { transform: scale(.88); }
.rt-star .ic { fill: none; stroke: var(--text-dim); }
.rt-star.on .ic { fill: #f5a623; stroke: #f5a623; }
.rt-opts { display: flex; flex-wrap: wrap; gap: 8px; }
.rt-chip { border: 2px solid var(--rule); background: transparent; color: var(--text); border-radius: 0; padding: 8px 13px; font-size: .85rem; font-weight: 500; cursor: pointer; transition: background var(--motion), border-color var(--motion); }
.rt-chip.on { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.rt-text { width: 100%; border: 2px solid var(--rule); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); font: inherit; padding: 11px 12px; resize: vertical; min-height: 74px; outline: none; }
.rt-text:focus { box-shadow: 4px 4px 0 var(--acid); }
.rt-text:focus { border-color: var(--brand); }
.rt-count { margin-top: 5px; font-size: .74rem; color: var(--text-dim); text-align: right; font-variant-numeric: tabular-nums; }
.rt-count.over { color: var(--danger); font-weight: 600; }
.rt-submit { width: 100%; margin-top: 18px; }
.rt-submit:disabled { opacity: .45; cursor: not-allowed; }
.rt-msg { margin-top: 9px; font-size: .82rem; color: var(--text-dim); text-align: center; }
/* Spotlight: the guide has asked the group to rate today — pulse for ~60s. */
.ratetoday.spotlight { border-color: var(--brand); animation: rt-glow 1.5s ease-in-out infinite; }
@keyframes rt-glow {
  0%, 100% { box-shadow: 0 0 0 1px var(--brand), 0 4px 14px color-mix(in srgb, var(--brand) 26%, transparent); }
  50% { box-shadow: 0 0 0 3px var(--brand), 0 8px 28px color-mix(in srgb, var(--brand) 55%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .ratetoday.spotlight { animation: none; box-shadow: 0 0 0 2px var(--brand); }
}

/* Attendance — one giant "I'm here" button (deliberately oversized + simple) */
.att-card { margin: 12px 14px; padding: 14px; border: 2px solid var(--rule); border-radius: 0; background: var(--surface); box-shadow: none; }
.ac-head { display: flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 13px; }
/* The operator's own colour, and it breathes for as long as the guide is asking — not on
   a 60-second timer, because a traveller who looks down two minutes late still needs the
   button to be the thing calling them. A ring rather than the fill: the surface stays put
   so the label never shimmers, and a pale brand cannot wash its own text out.
   One cycle is 1.8s (~0.55Hz), far below the 3Hz photosensitivity threshold. */
.ac-btn { width: 100%; min-height: 88px; border: 2px solid var(--rule); border-radius: 0; background: var(--chrome); color: var(--acid); font-family: var(--font-display); font-size: 1.55rem; font-weight: 800; letter-spacing: -.02em; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 10px; -webkit-tap-highlight-color: transparent; transition: transform var(--motion), background-color .25s ease, box-shadow .25s ease; animation: ac-halo 1.8s ease-in-out infinite; }
@keyframes ac-halo {
  0%, 100% { box-shadow: 4px 4px 0 var(--brand); }
  50%      { box-shadow: 8px 8px 0 var(--brand); }
}
}
.ac-btn:active:not(:disabled) { transform: translate(3px, 3px); box-shadow: none; transition-duration: .09s; }
.ac-hint { text-align: center; font-family: var(--font-mono); font-size: .66rem; letter-spacing: .1em; text-transform: uppercase; color: var(--text-dim); margin-top: 13px; }
/* A tap that did not reach the guide has to say so, and point at the thing that works. */
.ac-hint.bad { color: var(--danger); font-weight: 650; font-size: .86rem; line-height: 1.4; }
.ac-btn:disabled { cursor: progress; }
/* Pressed, the flashing stops dead — it is no longer asking for anything. */
.ac-btn.busy { animation: none; background: color-mix(in srgb, var(--chrome) 72%, var(--surface)); color: var(--acid); box-shadow: none; }
.ac-btn.done { animation: none; background: var(--brand); color: var(--brand-ink); box-shadow: none; }
.ac-btn.shake { animation: btn-shake .38s cubic-bezier(.36,.07,.19,.97) both; }
.ac-btn .spin { width: 20px; height: 20px; flex: none; border: 3px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: btn-spin .6s linear infinite; opacity: .9; }
.ac-btn .btn-tick { width: 28px; height: 28px; flex: none; animation: btn-pop .34s cubic-bezier(.2,.9,.3,1.4) both; }
/* One confirmation before a name is bound to this phone. */
.wru-confirm { text-align: center; }
.wru-name-big { font-family: var(--font-display); font-size: 1.9rem; font-weight: 700; margin: 14px 0 6px; line-height: 1.15; overflow-wrap: anywhere; }
.wru-confirm .wru-sub { margin-bottom: 22px; }
.wru-back { display: block; width: 100%; margin-top: 12px; min-height: 44px; border: 0; background: none; color: var(--text-dim); font-family: inherit; font-size: .92rem; font-weight: 650; cursor: pointer; }
.wru-back:active { color: var(--text); }
.att-card.done { display: flex; align-items: center; gap: 14px; background: var(--surface); border-color: var(--brand); box-shadow: 4px 4px 0 var(--brand); }
.ac-badge { flex: none; width: 52px; height: 52px; border-radius: 0; background: var(--chrome); color: var(--acid); display: grid; place-items: center; }
.ac-b { min-width: 0; }
.ac-b b { display: block; font-family: var(--font-display); font-size: 1.15rem; font-weight: 800; letter-spacing: -.015em; color: var(--text); }
.ac-b span { font-size: .85rem; color: var(--text-dim); }
.ac-change { border: 0; background: transparent; color: var(--brand-text); font: inherit; font-size: .85rem; padding: 0; cursor: pointer; text-decoration: underline; }
@media (prefers-reduced-motion: reduce) { .ac-btn { animation: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 40%, transparent); } .ac-btn:active:not(:disabled) { transform: none; } }

/* "Which one is you?" name picker */
.wru h2 { margin: 0 0 4px; }
.wru-sub { color: var(--text-dim); font-size: .9rem; margin: 0 0 16px; }
.wru-empty { color: var(--text-dim); }
.wru-list { display: flex; flex-direction: column; gap: 8px; }
.wru-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; min-height: 60px; padding: 14px 16px; border: 2px solid var(--rule); border-radius: 0; background: var(--surface); color: var(--text); cursor: pointer; text-align: left; }
.wru-row:active { background: var(--surface-2); }
.wru-name { font-size: 1.15rem; font-weight: 600; }
.wru-hint { flex: none; font-size: .85rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.offline-banner { display: none; text-align: center; padding: 8px; font-family: var(--font-mono); font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; background: var(--warn); color: #fff; }
body.is-offline .offline-banner { display: block; }
/* --warn and --danger invert brightness between the two modes: dark inks in daylight,
   bright ones in the evening. A hardcoded white sat correctly on the first and collapsed
   on the second — the offline banner measured 2.07:1 at night. Same rule as everywhere
   else on this page: dark ink on a bright plate. */
:root[data-mode="dark"] .offline-banner,
:root[data-mode="dark"] .sos-fab { color: #16215A; }

/* ── Trip-end review prompt ── */
.reviewcard { margin: 16px 16px 8px; padding: 18px 16px; border-radius: var(--radius); background: var(--brand-tint); border: 1px solid var(--line); text-align: center; }
.rc-title { font-family: var(--font-display); font-weight: 800; font-size: 1.12rem; }
.rc-sub { color: var(--text-dim); font-size: .88rem; margin: 4px 0 13px; }
.rc-btn { display: inline-flex; align-items: center; gap: 7px; border: 2px solid var(--rule); background: var(--brand); color: var(--brand-ink); font-family: var(--font-display); font-weight: 700; font-size: .92rem; padding: 11px 20px; border-radius: 0; cursor: pointer; }
.rc-btn:active { transform: scale(.97); }

/* ── Now banner (the brand hero) ── */
/* A ticket stub, torn along the bottom. Prints on the key plate so the acid caption
   reads, with the accent reserved for the progress bar and the countdown. */
.nowbanner { position: relative; margin: 14px 14px 10px; padding: 14px 15px 16px; background: var(--chrome);
             color: var(--chrome-ink); }
.nowbanner::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 8px;
  background: radial-gradient(circle at 5px 8px, var(--bg) 4.5px, transparent 5px) repeat-x; background-size: 10px 8px; }
.nb-kind { font-family: var(--font-mono); font-size: .62rem; text-transform: uppercase; letter-spacing: .18em;
           color: var(--acid); font-weight: 500; }
.nb-text { font-family: var(--font-display); font-size: 1.12rem; font-weight: 700; letter-spacing: -.015em; margin-top: 5px; }
.nb-bar { margin-top: 11px; height: 6px; border-radius: 0; background: color-mix(in srgb, var(--chrome-ink) 22%, transparent); overflow: hidden; }
.nb-bar span { display: block; height: 100%; background: var(--brand); border-radius: 0; }

/* ── Timeline ── */
.timeline { padding: 8px 16px 6px; }
.empty { color: var(--text-dim); padding: 20px 0; text-align: center; }
.trow { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 10px; }
.rail { display: flex; flex-direction: column; align-items: center; padding-top: 14px; color: var(--text-dim); font-family: var(--font-mono); font-size: .72rem; font-variant-numeric: tabular-nums; }
.rail b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
.rail i { font-style: normal; margin-top: 2px; }
.rail-line { flex: 1; width: 2px; background: var(--line); margin: 6px 0; border-radius: 0; min-height: 20px; }
.trow.is-now .rail-line { background: var(--brand); }
/* Bordered, never raised. A riso card is a ruled box on the same sheet. */
.stop { display: block; width: 100%; text-align: left; margin: 7px 0; padding: 11px; background: var(--surface); color: var(--text); border: 2px solid var(--rule); border-radius: 0; box-shadow: none; transition: transform var(--motion); }
.stop-main { display: flex; gap: 12px; align-items: flex-start; cursor: pointer; }
.stop-main:active { transform: scale(.99); }
.stop-chev { align-self: center; color: var(--text-dim); flex: none; }
/* The current stop is struck: the accent plate printed a hair out of register. */
.trow.is-now .stop { border-color: var(--brand); box-shadow: 4px 4px 0 var(--brand); }
.trow.is-now .stop .name { text-decoration: underline; text-decoration-color: var(--brand); text-decoration-thickness: 3px; text-underline-offset: 3px; }
.trow.is-past .stop { opacity: .7; border-color: var(--line); }
/* Per-stop "how was it?" — a compact footer inside the card */
.stopfb { display: flex; align-items: center; gap: 8px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-family: var(--font-mono); font-size: .7rem; letter-spacing: .04em; color: var(--text-dim); }
.fb-q { font-size: .76rem; color: var(--text-dim); }
.fb-btns { display: inline-flex; gap: 6px; margin-left: auto; }
.fb-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; min-width: 40px; height: 28px; padding: 0 10px; border-radius: 0; border: 1.5px solid var(--rule); background: transparent; color: var(--text); cursor: pointer; transition: transform var(--motion), background var(--motion); }
.fb-btn:active { transform: scale(.92); }
.fb-btn.up.on { background: var(--ok); border-color: var(--ok); color: #fff; }
.fb-btn.down.on { background: var(--danger); border-color: var(--danger); color: #fff; }
.fb-n { font-size: .8rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.fb-n[hidden] { display: none; }
/* Google rating: gold star, score, explicit "Google reviews" */
.grating { display: inline-flex; align-items: center; gap: 5px; margin-top: 2px; font-size: .8rem; flex-wrap: wrap; }
.gr-star { color: var(--brand-text); display: inline-flex; }
.gr-score { font-family: var(--font-display); font-weight: 800; font-variant-numeric: tabular-nums; }
.gr-rev { color: var(--text-dim); }
.gr-src { color: var(--text-dim); font-family: var(--font-mono); font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; font-weight: 500; }
.grating.big { font-size: .95rem; margin: 4px 0 12px; gap: 8px; }
.grating.big .gr-score { font-size: 1.2rem; }
/* ── Duotone: small only ──────────────────────────────────────────────────────
   Thumbnails and strips are decoration and get the accent plate; the drawer gallery
   stays true colour, because that is where someone is working out what a place looks
   like before they arrive. The wrapper exists so the plate and halftone have something
   to sit over — an <img> cannot carry them alone. */
.slot { position: relative; width: 56px; height: 56px; flex: none; overflow: hidden;
        background: var(--chrome); isolation: isolate; box-shadow: 3px 3px 0 var(--brand); }
.slot .thumb, .slot .hc-thumb { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
        border-radius: 0; box-shadow: none; filter: grayscale(1) brightness(1.5) contrast(1.12); mix-blend-mode: screen; }
.slot .plate { position: absolute; inset: 0; z-index: 2; background: var(--brand); mix-blend-mode: multiply; opacity: .84; }
.slot .half { position: absolute; inset: 0; z-index: 3; opacity: .28; mix-blend-mode: multiply;
        background-image: radial-gradient(var(--chrome) 40%, transparent 41%); background-size: 3.5px 3.5px; }
/* The placeholder is already ink and acid; nothing to tint. */
/* Also the hotel banner's, once a dead photo collapses its slot — .thumb.ph was only
   ever styled under .stop, so the fallback landed in the hotel card unsized. */
.stop .thumb.ph, .hotelcard .thumb.ph { width: 56px; height: 56px; flex: none; border-radius: 0; background: var(--chrome); color: var(--acid); display: grid; place-items: center; font-size: 22px; box-shadow: 3px 3px 0 var(--brand); }
.stop .body { min-width: 0; flex: 1; }
.stop .cat { font-family: var(--font-mono); font-size: .62rem; text-transform: uppercase; letter-spacing: .14em; color: var(--text-dim); font-weight: 500; }
.stop .name { font-family: var(--font-display); font-weight: 700; font-size: 1.02rem; letter-spacing: -.015em; line-height: 1.25; margin-top: 2px; }
.stop .note { font-size: .8rem; color: var(--text-dim); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
/* Printed tags, not pills. Squared, mono, tracked — the same mark the reference uses
   for "Included" and "Optional". */
.chip { font-family: var(--font-mono); font-size: .62rem; letter-spacing: .1em; text-transform: uppercase;
        padding: 3px 6px; border-radius: 0; background: transparent; border: 1px solid var(--line);
        color: var(--text-dim); font-weight: 500; }
.chip.rate { color: var(--warn); border-color: var(--warn); background: transparent; }
.chip.food { color: var(--chrome); background: var(--acid); border-color: var(--acid); }
.chip.hi { color: var(--brand-ink); background: var(--brand); border-color: var(--brand); }

/* Legs + NOW divider + past collapse */
.legrow { display: flex; align-items: center; gap: 8px; margin-left: 52px; padding: 6px 12px; color: var(--text-dim); font-size: .84rem; }
.legrow .leg-ic { opacity: .8; }
.nowdiv { display: flex; align-items: center; gap: 10px; margin: 12px 0 4px; }
.nowdiv::before, .nowdiv::after { content: ""; flex: 1; height: 1px; background: var(--brand); opacity: .5; }
.nowdiv span { font-size: .72rem; font-weight: 800; letter-spacing: .08em; color: var(--brand-text); }
.pastwrap { margin: 4px 0; }
.pastwrap summary { list-style: none; cursor: pointer; color: var(--text-dim); font-size: .85rem; font-weight: 600; padding: 8px 0 8px 52px; }
.pastwrap summary::-webkit-details-marker { display: none; }
.pastwrap summary::before { content: "▾ "; }

/* ── Section headers + wallet ── */
.sec-h { padding: 16px 14px 0; font-family: var(--font-display); font-size: .82rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.wallet { padding: 4px 16px; }
.doc, .addon { display: flex; align-items: center; justify-content: space-between; gap: 12px; background: var(--surface); border: 2px solid var(--rule); border-radius: 0; padding: 12px; margin: 8px 0; box-shadow: none; }
.addon-l { display: flex; align-items: center; gap: 12px; min-width: 0; }
.addon-l .thumb, .doc .thumb { width: 44px; height: 44px; border-radius: 0; object-fit: cover; }
/* A request row is a title, a contact line and a state. Without these two the badge is a
   flex sibling that competes for width, and the left column wrapped mid-value —
   "SGD" on one line and "90.00" on the next. */
.wallet .doc { display: grid; grid-template-columns: 1fr auto; align-items: start; column-gap: 12px; row-gap: 3px; }
.wallet .req-title { min-width: 0; }
.wallet .req-meta { grid-column: 1 / -1; min-width: 0; }
/* Was --ok green: a third ink on a two-plate page, and "requested" is a pending state
   rather than a success. Printed as a stamp instead. */
.req-badge { color: var(--text); border: 2px solid var(--rule); padding: 3px 7px; font-family: var(--font-mono); font-weight: 500; font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; }

/* ── Pre-trip essentials: entrypoint (Trip tab) + drawer ── */
.pretrip { display: flex; align-items: center; gap: 12px; width: calc(100% - 28px); margin: 2px 14px 14px; padding: 12px 13px; background: var(--surface); border: 2px solid var(--rule); border-radius: 0; box-shadow: none; cursor: pointer; text-align: left; transition: transform var(--motion); }
.pretrip:active { transform: scale(.99); }
.pretrip-ic { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 0; background: var(--chrome); color: var(--acid); flex: none; }
.pretrip-tx { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.pretrip-tx b { font-family: var(--font-display); font-size: .98rem; font-weight: 700; letter-spacing: -.01em; }
.pretrip-tx span { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); }
.pretrip-n { min-width: 24px; height: 22px; padding: 0 6px; display: grid; place-items: center; border-radius: 0; background: var(--brand); color: var(--brand-ink); font-family: var(--font-mono); font-size: .7rem; font-weight: 500; flex: none; }
.pretrip > svg { color: var(--text-dim); flex: none; }

/* The banner asks for attention only while it has something to ask for: tasks outstanding
   AND the trip not yet begun. Both stop conditions matter. A card that pulses for months is
   wallpaper, and one still pulsing on day three is telling a traveller who has landed to
   book ahead.
   The glow is a breathing shadow rather than a flash: it reads as alive at a glance and does
   not compete with the day's own cards for the eye. Colour comes from --brand, so an
   operator's own palette drives it and nothing here is hard-coded to indigo. */
@keyframes pretrip-breathe {
  0%, 100% { box-shadow: var(--shadow), 0 0 0 0 color-mix(in srgb, var(--brand) 38%, transparent); }
  50%      { box-shadow: var(--shadow), 0 0 0 7px color-mix(in srgb, var(--brand) 0%, transparent); }
}
.pretrip.glow {
  border-color: color-mix(in srgb, var(--brand) 55%, var(--line));
  animation: pretrip-breathe 2.4s ease-in-out infinite;
}
.pretrip.glow .pretrip-ic { color: var(--acid); background: var(--chrome); }
/* Somebody who has asked their phone to stop moving things has asked for this too. The
   border still thickens, so the banner is still marked out without anything animating. */
@media (prefers-reduced-motion: reduce) {
  .pretrip.glow { animation: none; box-shadow: var(--shadow), 0 0 0 3px color-mix(in srgb, var(--brand) 22%, transparent); }
}
.es-sheet { padding-bottom: 4px; }
.es-head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.es-head h2 { margin: 0; font-size: 1.15rem; }
.es-ico { color: var(--brand-text); flex: none; }
.es-h { font-family: var(--font-display); font-size: .76rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; color: var(--text); margin: 18px 0 8px; padding-bottom: 4px; border-bottom: 2px solid var(--rule); }
.es-list { display: flex; flex-direction: column; }
.es-item { padding: 12px 0; border-top: 1px solid var(--line); }
.es-item:first-child { border-top: 0; }
.es-item b { display: block; font-size: .95rem; margin-bottom: 2px; }
.es-item span { font-size: .88rem; color: var(--text-dim); line-height: 1.45; }
.es-hrow { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.es-count { font-family: var(--font-mono); font-size: .72rem; font-weight: 500; letter-spacing: .06em; color: var(--brand-text); font-variant-numeric: tabular-nums; }
.es-cap { font-size: .78rem; color: var(--text-dim); line-height: 1.4; margin: -2px 0 10px; }
.es-dos { display: flex; flex-direction: column; gap: 8px; }
/* A task is a button now, so it has to look pressable and show its state. Ticked rows fade
   and rule through: still readable, plainly finished, and they stay in place rather than
   moving to the bottom, because a list that reorders under a thumb loses the reader. */
.es-do { display: flex; gap: 11px; align-items: flex-start; width: 100%; text-align: left; background: var(--surface); border: 2px solid var(--rule); border-radius: 0; padding: 11px 13px; box-shadow: none; cursor: pointer; font: inherit; color: inherit; transition: opacity var(--motion), border-color var(--motion), transform var(--motion); }
.es-do:active { transform: scale(.995); }
.es-do.on { opacity: .7; border-color: var(--line); }
.es-do.on .es-do-tx b { text-decoration: line-through; text-decoration-thickness: 1px; }
.es-do.on .es-check { background: var(--acid); border-color: var(--acid); color: var(--chrome); }
.es-do.on .es-check svg { opacity: 1; }
.es-do .es-check svg { opacity: 0; transition: opacity var(--motion); }
.es-check { flex: none; width: 21px; height: 21px; border-radius: 0; display: grid; place-items: center; background: transparent; border: 2px solid var(--rule); color: var(--brand-text); margin-top: 2px; transition: background var(--motion), border-color var(--motion); }
.es-do-tx { min-width: 0; }
.es-do-tx b { display: block; font-family: var(--font-display); font-size: .95rem; font-weight: 700; }
.es-do-tx span { display: block; font-size: .84rem; color: var(--text-dim); line-height: 1.4; margin-top: 2px; }

/* ── Emergency (collapsible, understated) ── */
/* Emergency bottom-drawer (opened from the SOS FAB) */
.em-sheet { padding-bottom: 4px; }
.em-h { display: flex; align-items: center; gap: 10px; }
.em-h h2 { margin: 0; font-size: 1.15rem; }
.em-dot { width: 9px; height: 9px; border-radius: 0; background: var(--danger); flex: none; }
.em-lede { margin: 8px 0 6px; font-size: .85rem; color: var(--text-dim); }
.em-row { display: flex; align-items: center; gap: 12px; padding: 13px 0; border-top: 1px solid var(--line); }
.em-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.em-name { font-weight: 600; }
.em-num { color: var(--text-dim); font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: .82rem; letter-spacing: .04em; }
.em-acts { display: flex; gap: 8px; flex: none; }
.em-act { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 0; border: 2px solid currentColor; text-decoration: none; transition: transform var(--motion); }
.em-act:active { transform: scale(.92); }
.em-act.call { background: transparent; color: var(--danger); }
.em-act.wa { background: transparent; color: #1aa851; }

/* ── Optional extra (obviously not part of the fixed plan) ── */
/* Optional add-on card — same shape as a stop card, brand-tinted + tagged as optional. */
.excard { display: flex; gap: 12px; align-items: flex-start; margin: 7px 0 7px 52px; padding: 10px; background: var(--surface); border: 2px solid var(--brand); border-radius: 0; box-shadow: 3px 3px 0 var(--brand); cursor: pointer; transition: transform var(--motion); }
.excard:active { transform: scale(.99); }
.ex-thumb { width: 56px; height: 56px; flex: none; border-radius: 0; object-fit: cover; background: var(--chrome); display: grid; place-items: center; color: var(--acid); }
.ex-body { min-width: 0; flex: 1; }
.ex-tag { display: inline-block; font-family: var(--font-mono); font-size: .6rem; font-weight: 500; text-transform: uppercase; letter-spacing: .12em; color: var(--brand-text); }
.ex-name { font-weight: 700; line-height: 1.25; margin-top: 2px; }
.ex-desc { font-size: .82rem; color: var(--text-dim); margin-top: 3px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ex-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; align-self: stretch; flex: none; gap: 6px; }
.ex-price { font-family: var(--font-display); font-weight: 800; color: var(--brand-text); font-variant-numeric: tabular-nums; white-space: nowrap; }
.dw-optag { display: inline-flex; align-items: center; gap: 5px; font-size: .66rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; color: var(--brand-text); margin-bottom: 4px; }

/* ── Address line + booking chip on cards ── */
.addr-line { display: flex; align-items: center; gap: 5px; max-width: 100%; font-size: .78rem; color: var(--text-dim); margin-top: 3px; text-decoration: none; }
.addr-line span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.addr-line:active { color: var(--text); }
.addr-line .ic, .addr-line .ic-pin { flex: none; }
.chip.book { color: var(--ok); border-color: var(--ok); background: transparent; }

/* drawer address + booking */
.dw-addr { display: inline-flex; align-items: center; gap: 6px; color: var(--text); font-size: .9rem; margin: 8px 0 12px; text-decoration: none; padding: 8px 11px; background: transparent; border: 2px solid var(--rule); border-radius: 0; max-width: 100%; }
.dw-addr span { text-decoration: underline; text-underline-offset: 2px; }
.dw-note { font-size: .88rem; color: var(--text-dim); margin: 2px 0 10px; }
/* Hotel line in the Trip overview day rows */
.tr-hotel { display: flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: .68rem; letter-spacing: .04em; color: var(--brand-text); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dw-addr .ic:last-child { color: var(--text-dim); margin-left: 2px; }
.dw-book { display: flex; align-items: center; gap: 10px; margin: 8px 0 4px; }
.dw-book-t { font-size: .85rem; color: var(--text-dim); }

/* ── Location: soft pre-prompt + locate button + me dot ── */
.geo-prompt { position: absolute; left: 12px; right: 12px; bottom: calc(14px + env(safe-area-inset-bottom)); z-index: 1000; background: var(--surface); border: 2px solid var(--rule); border-radius: 0; box-shadow: 4px 4px 0 var(--brand); padding: 13px 14px; }
.geo-copy { font-size: .9rem; font-weight: 600; }
.geo-copy .dim { display: block; font-weight: 400; font-size: .78rem; margin-top: 2px; }
.geo-acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
.geo-acts .btn { min-height: 38px; padding: 0 16px; }
.geo-locate { position: absolute; right: 14px; bottom: calc(14px + env(safe-area-inset-bottom)); z-index: 1000; width: 44px; height: 44px; border-radius: 0; border: 2px solid var(--rule); background: var(--surface); font-size: 20px; cursor: pointer; box-shadow: 3px 3px 0 var(--brand); }
.me-dot span { display: block; width: 16px; height: 16px; border-radius: 0; background: #2b7fff; border: 3px solid #fff; box-shadow: 0 0 0 4px rgba(43,127,255,.3); }

/* ── Trip overview ── */
.metrics { display: flex; gap: 10px; padding: 8px 16px; }
.metric { flex: 1; background: var(--surface); border: 2px solid var(--rule); border-radius: 0; padding: 12px; text-align: center; box-shadow: none; }
.metric b { display: block; font-size: 1.7rem; font-family: var(--font-display); font-weight: 800; letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
.metric span { font-family: var(--font-mono); font-size: .64rem; letter-spacing: .12em; text-transform: uppercase; color: var(--text-dim); }
.triplist { padding: 6px 16px; }
.triprow { display: flex; align-items: center; gap: 14px; width: 100%; text-align: left; background: var(--surface); border: 2px solid var(--rule); border-radius: 0; padding: 12px; margin: 8px 0; box-shadow: none; cursor: pointer; color: var(--text); }
.tr-date { flex: none; width: 44px; text-align: center; }
.tr-date span { font-family: var(--font-mono); font-size: .64rem; letter-spacing: .1em; color: var(--text-dim); text-transform: uppercase; }
.tr-date b { display: block; font-size: 1.45rem; font-family: var(--font-display); font-weight: 800; letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
.tr-body { flex: 1; min-width: 0; }
.tr-h { font-family: var(--font-display); font-weight: 700; letter-spacing: -.01em; }
.tr-today { margin-left: 8px; font-family: var(--font-mono); font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; background: var(--brand); color: var(--brand-ink); padding: 3px 6px; border-radius: 0; vertical-align: 1px; }
.tr-m { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .04em; color: var(--text-dim); }
.tr-chev { color: var(--text-dim); font-size: 20px; }

/* ── Tab bar + map FAB ── */
.tabbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; display: flex; max-width: var(--maxw); margin: 0 auto; background: var(--chrome); border-top: 1px solid color-mix(in srgb, var(--chrome-ink) 20%, transparent); padding-bottom: env(safe-area-inset-bottom); }
.tab { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 10px 0; border: 0; background: transparent; color: color-mix(in srgb, var(--chrome-ink) 55%, transparent); font-family: var(--font-mono); font-size: .64rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 500; cursor: pointer; }
.tab[aria-current="true"] { color: var(--acid); }
/* The two floating buttons scroll over live card content. A round button with a soft
   shadow used to lift off the page on its own; a hard opaque square just collides with
   whatever border or headline it lands on. So each is knocked out of the page: a ring of
   paper wide enough to clear its own misregistration offset, printed behind both.
   Listed last, because box-shadows paint first-on-top. */
.map-fab { position: fixed; right: 18px; bottom: calc(72px + env(safe-area-inset-bottom)); z-index: 31; width: 52px; height: 52px; border-radius: 0; border: 2px solid var(--rule); background: var(--brand); color: var(--brand-ink); display: grid; place-items: center; box-shadow: 4px 4px 0 var(--chrome), 0 0 0 6px var(--bg); cursor: pointer; }
.sos-fab { position: fixed; right: 18px; bottom: calc(134px + env(safe-area-inset-bottom)); z-index: 31; width: 52px; height: 52px; border-radius: 0; border: 2px solid var(--rule); background: var(--danger); color: #fff; display: grid; place-items: center; box-shadow: 4px 4px 0 var(--chrome), 0 0 0 6px var(--bg); cursor: pointer; transition: transform var(--motion); }
.sos-fab:active { transform: scale(.92); }
@media (min-width: 760px) { .map-fab, .sos-fab { right: calc(50% - var(--maxw)/2 + 18px); } }

/* Install (Add to Home Screen) button — top of the Trip tab */

/* ── Bottom sheets (drawer + map) ── */
.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.45); opacity: 0; pointer-events: none; transition: opacity var(--motion); z-index: 40; }
.scrim.open { opacity: 1; pointer-events: auto; }
.drawer, .mapsheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 50; max-width: var(--maxw); margin: 0 auto; background: var(--surface); border-radius: 0; border-top: 3px solid var(--rule); transform: translateY(100%); transition: transform var(--motion); box-shadow: 0 -8px 40px rgba(0,0,0,.28); }
.drawer { max-height: 90vh; overflow-y: auto; overscroll-behavior: contain; }
.mapsheet { overscroll-behavior: contain; }
.drawer.open, .mapsheet.open { transform: translateY(0); }
.grab { width: 44px; height: 4px; background: var(--rule); border-radius: 0; margin: 10px auto; }
/* Sticky sheet header: keeps the grab handle + close button visible while the drawer scrolls. */
/* Tall enough to be the close button's own band: .drawer-x is absolutely positioned at
   top:10px and is 32px tall, so a short header let it sit on top of the first line of
   content. The grab stays at the top; the extra height is clearance below it. */
.sheet-head { position: sticky; top: 0; z-index: 6; display: grid; place-items: start center; min-height: 48px; padding: 2px 0 4px; background: linear-gradient(var(--surface) 72%, transparent); cursor: grab; touch-action: none; }
.sheet-head:active { cursor: grabbing; }
.sheet-head .grab { margin: 8px auto 2px; }
.drawer-x { position: absolute; top: 10px; right: 12px; width: 32px; height: 32px; border: 2px solid var(--rule); border-radius: 0; background: var(--surface); color: var(--text); display: grid; place-items: center; cursor: pointer; }
.drawer-x:active { transform: scale(.92); }
.pad { padding: 0 18px calc(20px + env(safe-area-inset-bottom)); }
.pad h2 { font-size: 1.35rem; }
.lede { color: var(--text); line-height: 1.6; }
/* Structured operator prose (see richText): paragraphs, a light heading, and bullets.
   An emoji-led bullet keeps its own marker, so it drops the list bullet to avoid two. */
.rt > :first-child { margin-top: 0; }
.rt > :last-child { margin-bottom: 0; }
.rt p { margin: 0 0 .7em; }
.rt h4 { font-family: var(--font-display); font-size: .95rem; font-weight: 700; color: var(--text); margin: 1.1em 0 .5em; }
.rt ul { margin: 0 0 .7em; padding-left: 1.15em; display: flex; flex-direction: column; gap: .42em; }
.rt ul ul { margin: .42em 0 0; padding-left: 1.1em; }
.rt li { line-height: 1.45; }
.rt li.pic { list-style: none; margin-left: -1.15em; }
.ratesum { color: var(--warn); font-weight: 700; margin-bottom: 8px; }
.gallery { display: flex; gap: 8px; overflow-x: auto; scroll-snap-type: x mandatory; margin: 8px 0; }
/* True colour — this is where someone works out what a place looks like before they
   arrive, and a tinted photograph cannot do that. It is still printed: a hard key-plate
   frame and a light halftone hold it in the same world without touching the colour. */
.gallery img { height: var(--hero); border-radius: 0; scroll-snap-align: start; object-fit: cover; border: 2px solid var(--rule); }
/* A single photo shouldn't be a scroller — fit it to the width, no swipe. */
.gallery.single { overflow: hidden; }
.gallery.single img { width: 100%; }
/* A licence obligation, so it sits on the plate rather than fading into the paper. */
.credit { display: inline-block; font-family: var(--font-mono); font-size: .62rem; letter-spacing: .1em;
          text-transform: uppercase; background: var(--chrome); color: var(--chrome-ink);
          padding: 4px 8px; margin-top: 0; line-height: 1.4; }
/* The credit is a reference, not a call to action: readable and checkable, but it must
   never compete with the card's own links for attention. */
.credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; text-decoration-color: color-mix(in srgb, currentColor 40%, transparent); }
.credit a:hover { text-decoration-color: currentColor; }
/* Framed like every other block on the page — it was the one bare rectangle. */
#dmap { height: 180px; border-radius: var(--radius-sm); border: 2px solid var(--rule); margin: 14px 0 4px; background: var(--surface-2); }
.field { display: block; margin: 8px 0; }
.field label { font-size: .82rem; color: var(--text-dim); }
/* Same key-plate rule as .err-input — a hairline field next to a 2px button read as
   two different design systems on one form. */
.field input { width: 100%; min-height: 44px; padding: 10px 12px; border: 2px solid var(--rule); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); font-size: 1rem; }
.field input:focus { outline: none; box-shadow: 4px 4px 0 var(--acid); }
.qr { display: block; max-width: 200px; margin: 12px auto; border-radius: var(--radius-sm); }
/* Squared, and it carries a plate rather than a fill alone — the press shifts it into
   its own registration mark, which is the tactile behaviour already built. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 18px; border-radius: 0; border: 2px solid var(--rule); cursor: pointer; background: var(--brand); color: var(--brand-ink); font-family: var(--font-display); font-weight: 700; letter-spacing: -.01em; -webkit-tap-highlight-color: transparent; }
/* Secondary, not lighter-weight: a ghost keeps the same key-plate rule as a primary and
   differs only in fill. It was still on the old hairline, which read as unstyled beside
   the button it sits next to ("Not now" against "Allow"). */
.btn.ghost { background: var(--surface); color: var(--text); border: 2px solid var(--rule); }

/* ── Tactile buttons ──────────────────────────────────────────────────────────
   A tap used to produce nothing: .fab, .month-pill and .att-row all had a press
   state, the primary buttons did not, and the first sign an action had started
   was a toast seconds later. Three layers — press, wait, resolve. See tactile.js
   for the JS half; the press below is CSS only so it lands before any network. */
.btn { transition: transform .26s cubic-bezier(.2,.7,.2,1), filter .26s ease, background-color .2s ease, border-color .2s ease; }
.btn:active:not(:disabled) { transform: scale(.965); filter: brightness(.93); transition-duration: .09s; }
.btn.ghost:active:not(:disabled) { background: var(--surface-2); }
.btn:disabled { opacity: .72; cursor: default; }
/* Busy keeps full opacity — it is working, not unavailable — and cannot be re-fired.
   A slow "Send to group" used to accept three taps and send three broadcasts. */
.btn.busy { cursor: progress; }
/* Busy and done both stay locked, but neither is "unavailable" — without this the
   confirmation lands as a washed-out green and reads as greyed out, not succeeded. */
.btn.busy:disabled, .btn.done:disabled { opacity: 1; }
.btn .spin { width: 14px; height: 14px; flex: none; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: btn-spin .6s linear infinite; opacity: .85; }
@keyframes btn-spin { to { transform: rotate(360deg); } }
/* The result lands on the control, where the thumb already is. */
.btn.done, .btn.ghost.done { background: var(--ok); border-color: var(--ok); color: #fff; }
.btn .btn-tick { width: 15px; height: 15px; flex: none; }
.btn.done .btn-tick { animation: btn-pop .32s cubic-bezier(.2,.9,.3,1.4) both; }
@keyframes btn-pop { from { transform: scale(.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.btn.fail { animation: btn-shake .38s cubic-bezier(.36,.07,.19,.97) both; }
@keyframes btn-shake { 10%,90% { transform: translateX(-2px); } 20%,80% { transform: translateX(4px); } 30%,50%,70% { transform: translateX(-6px); } 40%,60% { transform: translateX(6px); } }
@media (prefers-reduced-motion: reduce) {
  .btn:active:not(:disabled) { transform: none; }
  .btn.done .btn-tick, .btn.fail { animation: none; }
  .btn .spin { animation-duration: 2s; }
}
#req-msg, #priv-msg { margin-top: 8px; }

.mapsheet { height: 74vh; display: flex; flex-direction: column; }
.mapsheet-head { display: flex; align-items: center; justify-content: space-between; padding: 2px 16px 10px; font-family: var(--font-display); font-weight: 800; letter-spacing: -.01em; }
/* Leaflet's own chrome. The tiles stay true colour inside the key-plate frame — the same
   call as the drawer gallery, because a map is read rather than decorated — but the
   furniture Leaflet draws on top is the library default and has to join the page.
   The margin keeps the credit clear of the home indicator on a notched phone. */
.leaflet-control-attribution {
  background: var(--bg); color: var(--text-dim); font-family: var(--font-mono);
  font-size: .55rem; letter-spacing: .04em; padding: 2px 6px;
  border-top: 2px solid var(--rule); border-left: 2px solid var(--rule);
  margin-bottom: env(safe-area-inset-bottom, 0px);
}
.leaflet-control-attribution a { color: var(--text); }

/* Evening. The tiles were the one full-brightness surface left in the dark app — a lit
   rectangle that the acid pins and the pink route had to fight. Inverting and rotating
   the hue back folds OSM's own palette to night without swapping tile providers, and it
   only touches the raster: the pins are divIcons and the route is SVG, so both keep
   their plates. Applies to the day map and the drawer's mini map alike. */
:root[data-mode="dark"] .leaflet-tile {
  filter: invert(1) hue-rotate(180deg) brightness(.86) contrast(.92) saturate(.7);
}

/* Matches .drawer-x — the riso pass squared this one but left it a borderless grey fill,
   so the map sheet closed with a button from the old theme. */
.mapsheet-x { border: 2px solid var(--rule); background: var(--surface); color: var(--text); width: 32px; height: 32px; border-radius: 0; display: grid; place-items: center; cursor: pointer; }
.mapsheet-x:active { transform: scale(.92); }
/* True colour, framed. A duotoned map was beautiful and unreadable — you could not
   tell a street from a block — and this is the screen someone opens when they are lost.
   The frame admits it is a window onto the real world rather than part of the print. */
#map-el { flex: 1; border-radius: 0; border-top: 3px solid var(--rule); }
.pin span { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 0; background: var(--acid); color: var(--chrome); font-family: var(--font-mono); font-size: 11px; font-weight: 700; border: 2px solid var(--chrome); box-shadow: 2px 2px 0 var(--chrome); }
.pin-now span { background: var(--warn); animation: pinpulse 1.2s infinite; }
@keyframes pinpulse { 0%,100% { box-shadow: 0 2px 6px rgba(0,0,0,.4); } 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--warn) 40%, transparent); } }

/* ── Place guide (tier-gated content) ── */
.guideblock { margin-top: 4px; }
.gd-sec { margin-top: 16px; }
/* Rules do the structuring: a ruled header, bullets hanging on an accent dash, prices
   on a dotted leader like a printed menu. Everything in the reference is a four-word
   label; a guide is paragraphs, and this is what carries it. */
.gd-h { font-family: var(--font-display); font-size: .76rem; text-transform: uppercase; letter-spacing: .08em; color: var(--text); font-weight: 800; margin-bottom: 8px; padding-bottom: 4px; border-bottom: 2px solid var(--rule); }
.gd-p { color: var(--text); line-height: 1.55; margin: 0; }
.mustdo { list-style: none; margin: 0; padding: 0; }
.mustdo li { position: relative; padding: 6px 0 6px 20px; border-bottom: 1px solid var(--line); line-height: 1.45; }
.mustdo li:last-child { border-bottom: 0; }
.mustdo li::before { content: "\2014"; position: absolute; left: 0; top: 6px; width: auto; height: auto; border-radius: 0; background: none; color: var(--brand-text); font-family: var(--font-mono); font-size: .8rem; }
.mustdo.know li { color: var(--text-dim); }
.mustdo.know li::before { content: "\00b7"; color: var(--warn); background: none; }
.dish { display: flex; align-items: baseline; gap: 8px; padding: 8px 1px; border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; }
.dish:last-child { border-bottom: 0; }
.dn { flex: 1; font-weight: 600; }
.dp { flex: none; font-weight: 800; color: var(--brand-text); font-variant-numeric: tabular-nums; }

/* ── Day picker ── */
.daypick { display: flex; flex-direction: column; gap: 8px; }
.pk-row { display: flex; align-items: center; gap: 14px; width: 100%; text-align: left; padding: 12px; border-radius: 0; border: 2px solid var(--rule); background: var(--surface); color: var(--text); cursor: pointer; }
.pk-row.sel { border-color: var(--brand); box-shadow: 4px 4px 0 var(--brand); }
.pk-row.today { background: var(--surface-2); }
.pk-d { flex: none; width: 44px; text-align: center; }
.pk-d span { font-size: .7rem; color: var(--text-dim); text-transform: uppercase; }
.pk-d b { display: block; font-size: 1.4rem; font-family: var(--font-display); font-weight: 800; font-variant-numeric: tabular-nums; }
.pk-h { font-weight: 700; }

.loading { padding: 70px 24px; text-align: center; color: var(--text-dim); }

/* Full-screen error / empty states (no trip, locked, offline) */
.errstate { min-height: 100dvh; max-width: 400px; margin: 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 40px 28px calc(48px + env(safe-area-inset-bottom)); }
.err-badge { width: 76px; height: 76px; border-radius: 0; display: grid; place-items: center; margin-bottom: 20px; background: var(--chrome); color: var(--acid); border: 2px solid var(--rule); box-shadow: 5px 5px 0 var(--acid); }
.err-badge .ic { width: 34px; height: 34px; }
.err-badge.warn { background: color-mix(in srgb, var(--warn) 16%, var(--surface)); color: var(--warn); }
.err-title { font-family: var(--font-display); font-size: 1.55rem; font-weight: 800; letter-spacing: -.025em; margin: 0 0 9px; }
.err-hint { color: var(--text-dim); font-size: .95rem; line-height: 1.55; margin: 0 0 24px; }
.err-code { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .84em; background: var(--surface-2); border: 1px solid var(--line); border-radius: 0; padding: 2px 7px; color: var(--text); white-space: nowrap; }
.err-form { display: flex; gap: 8px; width: 100%; max-width: 320px; }
.err-input { flex: 1; min-width: 0; padding: 0 15px; height: 44px; border-radius: 0; border: 2px solid var(--rule); background: var(--surface); color: var(--text); font-size: 1rem; }
.err-input:focus { outline: none; border-color: var(--rule); box-shadow: 4px 4px 0 var(--acid); }
.err-btn { flex: none; }
.err-foot { margin-top: 36px; font-size: .74rem; letter-spacing: .03em; text-transform: uppercase; color: var(--text-dim); }
.err-foot b { color: var(--text); font-weight: 700; text-transform: none; letter-spacing: 0; }
/* Bottom padding clears the floating SOS/map buttons, which would otherwise sit over
   the last line once the page is scrolled to the end. */
.foot { padding: 18px 16px 84px; color: var(--text-dim); font-size: .78rem; }
.foot a { color: var(--text-dim); }
/* The footer is not the trip. Card treatment here (same fill, radius and shadow as a
   stop) made two offers read as two more events at the end of the day, so it collapses
   to one grey line and opens on demand. Underlined text, never a card. */
.foot-about > summary {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  padding: 12px 0 3px; list-style: none; cursor: pointer;
  font-size: .74rem; font-weight: 600;
  /* Was --text-dim faded to 70% alpha, which took an already-quiet colour down to
     3.45:1 in evening mode. --text-dim on its own is 5.76:1. */
  color: var(--text-dim);
}
.foot-about > summary::-webkit-details-marker { display: none; }
.foot-about > summary .ic { transition: transform var(--motion); }
.foot-about[open] > summary .ic { transform: rotate(180deg); }
.fa-acts {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 8px 16px;
  margin-top: 11px; padding-top: 12px; border-top: 1px solid var(--line);
}
.fa-act {
  display: inline-flex; align-items: center; gap: 6px; padding: 0 0 3px;
  background: none; border: 0; border-bottom: 1px solid var(--line);
  color: var(--text-dim); font: inherit; font-size: .8rem; text-decoration: none; cursor: pointer;
}
.fa-act .ic { flex: none; }
/* WhatsApp is Journee talking, not the operator — its own green, never the brand accent. */
/* #128C4A was 3.77:1 on the paper. */
.fa-act.wa { color: #0A6B3D; border-bottom-color: color-mix(in srgb, #25D366 34%, var(--line)); }
:root[data-mode="dark"] .fa-act.wa { color: #4ee38a; }
.foot-meta { margin-top: 12px; text-align: center; font-size: .76rem; line-height: 1.5; }

/* Booking outcome toast — the traveller's end of an off-platform confirmation. One line,
   pinned above the tab bar, gone for good once its X is tapped. */
.reqtoast {
  position: fixed; left: 12px; right: 12px; bottom: calc(64px + env(safe-area-inset-bottom, 0px) + 10px);
  z-index: 60; display: flex; align-items: center; gap: 9px; padding: 11px 12px;
  /* Key plate and a hard offset, not a hairline and a soft drop shadow — this floats over
     card content the same way the two FABs do, so it separates the same way. */
  background: var(--surface); border: 2px solid var(--rule); border-radius: var(--radius-sm);
  box-shadow: 4px 4px 0 var(--chrome); font-size: .84rem; color: var(--text);
}
.reqtoast.ok .ic:first-child { color: var(--ok); }
.reqtoast.no .ic:first-child { color: var(--warn); }
.reqtoast span { flex: 1; min-width: 0; }
.reqtoast .rt-x { flex: none; display: grid; place-items: center; width: 28px; height: 28px; border: 1.5px solid var(--rule); border-radius: 0; background: transparent; color: var(--text); cursor: pointer; }

/* ── One address, two languages ───────────────────────────────────────────────
   Drawer only, and only when a Chinese address exists — a card without one keeps the
   single .dw-addr pill it has always had. Loose beneath that pill the Chinese line read
   as an unrelated thing that happened to land there, and on a short address the copy
   button stranded itself against the far edge. Sharing one container split by a hairline
   says what is actually true: this is the address, and here it is twice. */
.dw-addrs { background: transparent; border: 2px solid var(--rule); border-radius: 0; margin: 8px 0 12px; overflow: hidden; }
.dwa-row { display: flex; align-items: center; gap: 8px; padding: 9px 12px; color: var(--text);
           font-size: .9rem; text-decoration: none; }
.dwa-row + .dwa-row { border-top: 1.5px solid var(--rule); align-items: flex-start; }
.dwa-t { flex: 1; min-width: 0; text-decoration: underline; text-underline-offset: 2px; }
.dwa-row .ic:last-child { color: var(--text-dim); flex: none; }
.zh-cn { flex: none; margin-top: 3px; font-family: var(--font-mono); font-size: .58rem; font-weight: 500; line-height: 1.4;
         padding: 2px 4px; border-radius: 0; background: var(--chrome); color: var(--chrome-ink); }
.zh-txt { flex: 1; min-width: 0; font-size: 1rem; font-weight: 600; line-height: 1.5; color: var(--text);
          overflow-wrap: anywhere; -webkit-user-select: all; user-select: all; }
/* Borderless so it sits in the row rather than on top of it. */
.zh-copy { flex: none; margin: -2px -4px 0 0; width: 30px; height: 30px; border-radius: 0; border: 0;
           background: transparent; color: var(--text-dim); display: grid; place-items: center; cursor: pointer;
           -webkit-tap-highlight-color: transparent; transition: transform .18s ease, background-color .18s ease, color .18s ease; }
.zh-copy:active { transform: scale(.88); }
.zh-copy.ok { background: var(--ok); color: #fff; }
/* Clipboard refused: the address is selected instead, and the button says so rather than
   looking like a dead control. */
.zh-copy.sel { background: color-mix(in srgb, var(--brand) 22%, transparent); color: var(--brand-text); }
