/* SG Park Where — Soft Civic theme (locked, from design-handoff/README.md).
   Reference-fidelity port of design-handoff/*.dc.html: same tokens, same markup shapes,
   same interactions. Do not invent a new direction here — this is the shipped design. */

:root {
  --bg: #fbfaf7;
  --surface: #f1efe9;
  --card: #ffffff;
  --ink: #1e2421;
  --muted: #66706b;
  --body2: #3d4541;
  --hairline: #ebe8e1;
  --rule: #e6e3dc;
  --hover: #f3f1ec;
  --handle: #d3cfc6;

  /* 29 Sep 2026: rebranded to the cooler blue-teal (Ronnie's "Full A" call, mascot rebrand --
     was #0e7c66). Deliberately does NOT touch --lvl-plenty-bar, --lvl-plenty-num or --pin-plenty
     below, which stay the original green -- those are the semantic "plenty of lots" colour
     (parking-availability status), not the brand accent, and happened to share the old accent's
     hex only by coincidence. Keeping them green while the brand chrome goes blue also keeps the
     mascot's own green body/EV-charging colour story alive in the live map data, not just the icon. */
  --accent: #0e7c88;
  --accent-hover: #0b6872;
  --accent-tint: #eaf3f4;
  --accent-tint-fg: #0b6872;
  --on-accent: #ffffff;

  --sec: #1e2421;
  --sec-hover: #343c38;
  --on-sec: #ffffff;

  --lvl-plenty-bar: #0e7c66;
  --lvl-plenty-num: #0e7c66;
  --lvl-filling-bar: #d9822b;
  --lvl-filling-num: #b86a1c;
  --lvl-full-bar: #c9c5bc;
  --lvl-full-fg: #4a514d;
  --lvl-full-num: #8a918d;
  --lvl-nodata-bar: #d8d5cc;
  --lvl-nodata-num: #9aa19d;

  /* M10 (27 Sep 2026): weather -- a distinct blue (not the green --accent or amber staleness
     colour) so the "check the weather" prominence on surface/open-air carparks reads as its own
     signal, not a re-use of an existing one. */
  --rain: #2f6fb0;
  --rain-tint: #e8f1fa;

  --pin-plenty: #0e7c66;
  --pin-plenty-fg: #ffffff;
  --pin-filling: #d9822b;
  --pin-filling-fg: #ffffff;
  --pin-full: #c9c5bc;
  --pin-full-fg: #4a514d;
  --pin-ring: #ffffff;
  --pin-sel: #1e2421;
  --user-dot: #2563eb;

  --radius-sheet: 22px;
  --radius-btn: 14px;
  --radius-card-lg: 22px;
  --radius-card-md: 18px;
  --radius-card-sm: 16px;
  --radius-pill: 999px;
  --radius-bar: 4px;
  --radius-handle: 4px;

  --shadow-sheet: 0 -10px 30px rgba(30, 36, 33, 0.12);
  --shadow-chip: 0 4px 14px rgba(30, 36, 33, 0.12);
  --shadow-live-card: 0 8px 30px rgba(30, 36, 33, 0.08);
  --shadow-pin: 0 1px 3px rgba(0, 0, 0, 0.25);

  --font: "DM Sans", system-ui, sans-serif;

  /* 28 Sep 2026: first real motion in this app (drawer open/close) -- iOS-like drawer curve
     (Ionic's cubic-bezier) rather than an invented value, per the design-taste motion doctrine. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-drawer: 280ms;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font);
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }
button { font: inherit; }
a:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
summary::-webkit-details-marker { display: none; }
.tabular { font-variant-numeric: tabular-nums; }
.leaflet-control-attribution { font-size: 9px !important; }

/* ================= Site chrome (carpark page + directory) ================= */
.site-header {
  position: sticky;
  top: 0;
  z-index: 1000;
  background: rgba(251, 250, 247, 0.92);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--hairline);
}
.site-header-inner {
  max-width: 1040px;
  margin: 0 auto;
  padding: 12px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.site-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.02em;
  white-space: nowrap;
  flex: none;
}
.site-brand:hover { color: var(--ink); }
.site-brand-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--accent); }
.site-nav { display: flex; align-items: center; gap: 6px; }
.pill-link {
  padding: 10px 14px;
  border-radius: var(--radius-pill);
  color: var(--ink);
  font-size: 15px;
  font-weight: 500;
  white-space: nowrap;
  flex: none;
}
.pill-link:hover { background: var(--surface); color: var(--ink); }
.pill-link--accent {
  padding: 10px 16px;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
}
.pill-link--accent:hover { background: var(--accent-hover); color: var(--on-accent); }

.site-main {
  max-width: 1040px;
  margin: 0 auto;
  padding: 20px 20px 72px;
  display: flex;
  flex-direction: column;
  gap: 40px;
}
.site-footer { border-top: 1px solid var(--hairline); }
.site-footer-inner {
  max-width: 1040px;
  margin: 0 auto;
  padding: 24px 20px 40px;
  font-size: 14px;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 12px;
}

/* ================= Carpark page ================= */
.cp-crumb { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 14px; color: var(--muted); }
.cp-crumb .cur { color: var(--ink); }
.cp-head { display: flex; flex-direction: column; gap: 14px; }
.src-tag {
  display: inline-flex;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  background: var(--accent-tint);
  color: var(--accent-tint-fg);
  white-space: nowrap;
}
.cp-h1 { margin: 0; font-size: clamp(32px, 6vw, 52px); line-height: 1.05; font-weight: 700; letter-spacing: -0.02em; text-wrap: pretty; }
.cp-address { font-size: 17px; color: var(--muted); }

.cp-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), 1fr)); gap: 20px; }
.live-card {
  background: var(--card);
  border-radius: var(--radius-card-lg);
  box-shadow: var(--shadow-live-card);
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.live-card-top { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.live-card-title { font-size: 15px; font-weight: 700; white-space: nowrap; }
.live-dot-row { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.live-num-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.live-num { font-size: 72px; font-weight: 700; line-height: 0.9; letter-spacing: -0.03em; }
.live-of { font-size: 17px; color: var(--muted); white-space: nowrap; }
.bar-track { height: 8px; border-radius: var(--radius-bar); background: var(--hairline); overflow: hidden; }
.bar-fill { height: 8px; border-radius: var(--radius-bar); }
.live-status { font-size: 15px; font-weight: 500; }
.live-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: auto; }
.big-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 56px;
  border-radius: var(--radius-btn);
  color: #fff;
  font-weight: 700;
  font-size: 16px;
}
.big-btn--accent { background: var(--accent); }
.big-btn--accent:hover { background: var(--accent-hover); color: #fff; }
.big-btn--sec { background: var(--sec); }
.big-btn--sec:hover { background: var(--sec-hover); color: #fff; }

.rates-card { display: flex; flex-direction: column; gap: 14px; }
.rates-card .kv-card { flex: 1; }

/* Modest, fixed height (not min-height) so this can never stretch to swallow the viewport —
   it sits in its own row now (map comes after rates, see design change 26 Sep), and on mobile a
   one-finger swipe over it must scroll the page, not pan the map (see dragging:!L.Browser.mobile
   in carpark-page.js). */
.cp-map-wrap { position: relative; }
#cp-map { height: 300px; border-radius: var(--radius-card-lg); overflow: hidden; background: var(--surface); z-index: 1; }
.locate-btn {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 500;
  width: 48px;
  height: 48px;
  background: var(--bg);
  border: 0;
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow-chip);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}

.cp-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), 1fr)); gap: 40px 20px; }
.h2 { margin: 0; font-size: 24px; font-weight: 700; }
.kv-card { background: var(--card); border-radius: var(--radius-card-sm); padding: 4px 20px; }
.kv-row { display: flex; justify-content: space-between; gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--hairline); font-size: 16px; }
.kv-row:last-of-type { border-bottom: 0; }
.kv-label { color: var(--muted); }
.kv-value { font-weight: 700; text-align: right; }
.kv-value--reg { font-weight: 500; }
.kv-foot { padding: 12px 0; font-size: 13px; color: var(--muted); }

.nearby-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 10px; }
.nearby-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: 16px 18px;
  border-radius: var(--radius-card-sm);
  background: var(--card);
  color: var(--ink);
}
.nearby-card:hover { background: var(--hover); color: var(--ink); }
.nearby-name { font-size: 16px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nearby-sub { font-size: 14px; color: var(--muted); }
.nearby-num { font-size: 22px; font-weight: 700; }
.empty-note { font-size: 15px; color: var(--muted); }

.faq-list { display: flex; flex-direction: column; gap: 8px; }
.faq-item { background: var(--card); border-radius: var(--radius-card-sm); padding: 0 20px; }
.faq-item summary { list-style: none; cursor: pointer; padding: 18px 0; font-size: 17px; font-weight: 700; }
.faq-answer { padding: 0 0 18px; font-size: 16px; line-height: 1.55; color: var(--body2); text-wrap: pretty; }

/* ================= Directory ================= */
.dir-head { display: flex; flex-direction: column; gap: 10px; }
.dir-h1 { margin: 0; font-size: clamp(32px, 6vw, 52px); line-height: 1.05; font-weight: 700; letter-spacing: -0.02em; }
.dir-sub { font-size: 17px; color: var(--muted); text-wrap: pretty; }

.dir-controls {
  position: sticky;
  top: 65px;
  z-index: 5;
  background: var(--bg);
  padding: 8px 0 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.dir-search {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 56px;
  padding: 0 18px;
  border-radius: var(--radius-btn);
  background: var(--surface);
  border: 1px solid transparent;
}
.dir-search:focus-within { border-color: var(--accent); }
.dir-search input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: none;
  background: transparent;
  font: inherit;
  font-size: 17px;
  color: var(--ink);
}
.dir-chips { display: flex; flex-wrap: wrap; gap: 8px; }
/* 30 Sep 2026 (round 3): Traffic Info Board second-level sub-tabs (one expressway / one region at
   a time -- see tplInfoBoard()) -- horizontally scrollable instead of wrapping, since 8
   expressways or up to 5 regions can exceed one row on a phone width; wrapping would just
   reproduce the "long scroll" problem this feature exists to fix, one level down. */
.dir-chips--sub {
  flex-wrap: nowrap;
  overflow-x: auto;
  margin: 4px 0 12px;
  padding-bottom: 2px;
  scrollbar-width: none;
  /* 30 Sep 2026 (QA fix): the strip clipped its last tab mid-letter with no visible cue that more
     tabs exist off-screen. A mask fade is computed against this element's own frame (not the
     scrolled content), so it stays a fixed edge hint regardless of scroll position -- the same
     technique used for horizontally-scrolling tab strips generally. */
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
.dir-chips--sub::-webkit-scrollbar { display: none; }
.dir-chips--sub .dir-chip { flex: none; }
/* 30 Sep 2026 (round 5): Traffic Info Board's Times tab -- wrapping 2-row GRID (Ronnie's explicit
   ask: "2 rows of 4 in phones or just 5,4") instead of the horizontal scroll strip above, which
   stays in place for the Incidents tab's own region sub-tabs (untouched). 5 fixed columns means 9
   tabs (All + 8 expressways) lay out 5 top row / 4 second row, on mobile and desktop alike -- the
   sidebar is wide enough on desktop that this reads fine as a compact grid there too. */
.dir-chips--grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
  margin: 4px 0 12px;
}
.dir-chips--grid .dir-chip { flex: none; justify-content: center; padding: 0 4px; text-align: center; }
.dir-chip {
  height: 40px;
  padding: 0 16px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--rule);
  background: #fff;
  color: var(--ink);
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
}
.dir-chip[aria-pressed="true"] { background: var(--accent); color: #fff; border-color: var(--accent); }
/* M6 (28 Sep 2026, round 2): filter chips with a leading icon (drawer filter panel). */
.dir-chip--icon { display: inline-flex; align-items: center; gap: 6px; }
.dir-chip--icon svg { flex: none; }

.dir-groups { display: flex; flex-direction: column; gap: 32px; }
.dir-group { display: flex; flex-direction: column; gap: 10px; scroll-margin-top: 200px; }
.dir-group-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 0 4px; }
.dir-group-title { margin: 0; font-size: 22px; font-weight: 700; }
.dir-group-count { font-size: 14px; color: var(--muted); }
.dir-list { background: var(--card); border-radius: var(--radius-card-sm); overflow: hidden; }
.dir-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 16px;
  min-height: 72px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--hairline);
  color: var(--ink);
}
.dir-list a.dir-row:last-child { border-bottom: 0; }
.dir-row:hover { background: var(--hover); color: var(--ink); }
.dir-row-name { font-size: 17px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dir-row-sub { font-size: 14px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dir-row-num { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.dir-row-num-val { font-size: 22px; font-weight: 700; line-height: 1; }
.dir-row-num-cap { font-size: 12px; color: var(--muted); }
.dir-empty { padding: 32px 4px; font-size: 17px; color: var(--muted); display: none; }
.dir-empty.is-visible { display: block; }
.dir-group.is-hidden { display: none; }
.dir-row.is-hidden { display: none; }

/* ================= Map app ================= */
html.app-html, body.app-body { height: 100%; overscroll-behavior: none; -webkit-tap-highlight-color: transparent; }
.app-shell {
  position: fixed;
  inset: 0;
  background: var(--bg);
  overflow: hidden;
}
.app-shell, .app-shell * { box-sizing: border-box; }
#app-map {
  position: absolute;
  inset: 0;
  z-index: 1;
  font-family: var(--font);
  background: var(--surface);
}
/* Toned down from saturate(0.62) 26 Sep -- that level was washing out OneMap's own MRT/bus stop
   icons on the basemap itself (item 5, Ronnie's iPhone test pass), which we now also draw our own
   muted landmark layer over. Lighter filter keeps the Soft Civic muted look while leaving OneMap's
   native transit icons readable underneath our layer. */
#app-map .leaflet-tile-pane { filter: saturate(0.85) brightness(1.02) contrast(0.98); }

/* ---- Map landmarks (MRT/LRT + bus stops): muted background context, never competing with
   carpark pins -- see landmarksPane (z 450, below markerPane's 600) in app.js. ---- */
.spw-mrt-chip {
  display: inline-block;
  white-space: nowrap;
  font: 600 10px/1 var(--font, 'DM Sans', sans-serif);
  letter-spacing: 0.01em;
  color: #4a5450;
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid #b9c2bd;
  border-radius: 999px;
  padding: 3px 7px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  pointer-events: none;
}
.app-topbar {
  position: absolute;
  top: max(12px, env(safe-area-inset-top));
  left: 12px;
  right: 12px;
  /* 30 Sep 2026 (QA fix): must outrank .app-sheet-wrap's z-index (also 1000, but later in the DOM
     -- same-value ties paint the later sibling on top). A tall results/detail sheet was covering
     everything in the global time-popover below its header/toggle row (the popover's own
     z-index:1050 is scoped to THIS element's stacking context, so it can't out-rank a sibling of
     .app-topbar itself) -- looked exactly like a "blank" picker even though the picker's own CSS
     was fine. Global topbar controls should always stay reachable above sheet content. */
  z-index: 1010;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  pointer-events: none;
}
.chip {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg);
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow-chip);
  padding: 8px 12px;
  pointer-events: auto;
}
/* 29 Sep 2026 (mobile UX batch): the mascot's own face, chroma-keyed from the reference sheet
   (art/preliminary-concept.png's "Simplified Mark (Mini)" panel, already built "works at small
   sizes" per its own caption) -- was a plain flat-colour dot before the mascot ever reached the
   live UI. Sized as an <img>, not a background-colour div, so no border-radius clip is forced --
   the source crop is already a tight rounded mascot-face shape. */
.chip-brand-dot { width: 22px; height: 22px; object-fit: contain; flex: none; }
.chip-brand-mascot { width: 30px; height: 26px; }
.chip-brand-name { font-weight: 700; font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; }
.app-topbar-right { display: flex; align-items: flex-start; gap: 8px; position: relative; }
.chip-menu { border: 1px solid transparent; cursor: pointer; }
.chip-ev {
  border: 1px solid transparent;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}
.chip-ev[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }
.ev-badge { color: var(--accent); }
/* .chip-fav (topbar ♡ Favourites shortcut) removed 29 Sep 2026 (mobile UX batch, item 4) -- Ronnie
   asked for My Shortlist to be reachable ONLY via the drawer's own "♡ My Shortlist" row, not a
   second topbar entry point. See app.js: the #fav-toggle-btn wiring was removed the same day. */
/* 28 Sep 2026: expressway speed-band overlay toggle -- same shape as .chip-ev, reuses
   this app's existing --lvl-filling-num amber (already means "moderate/caution" everywhere else,
   e.g. the parking-availability bar) rather than inventing a 3rd accent colour. */
.chip-traffic {
  border: 1px solid transparent;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}
.chip-traffic[aria-pressed="true"] { background: var(--lvl-filling-num); color: #fff; }

/* 29 Sep 2026 (mobile UX batch): global time-check chip, LEFT of ⚡ EV -- relocates/extends the
   existing per-carpark Plan-ahead estimator's reachability (see app.js pickCarpark()/the
   #time-toggle-btn handler); does not invent new global-forecast data. "Armed" (no carpark
   selected yet, later-time pending) reuses the same accent-fill pressed-state language as the
   other topbar toggles. */
.chip-time {
  border: 1px solid transparent;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}
.chip-time[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }

/* 30 Sep 2026 (round 3): global time-check popover -- absorbs the old per-carpark inline "When are
   you parking?" card, now a lightweight anchored panel under the ⏱️ chip instead of a full-screen
   modal/drawer (same visual-weight class -- bg/radius/shadow tokens shared with .app-modal-card --
   just small and positioned, no backdrop). */
.time-popover {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 1050;
  width: min(84vw, 300px);
  background: var(--bg);
  border-radius: var(--radius-card-lg);
  box-shadow: var(--shadow-sheet);
  padding: 14px 14px 12px;
  pointer-events: auto;
}
.time-popover.hidden { display: none; }
.tpop-title { font-weight: 700; font-size: 14px; margin-bottom: 8px; }
.tpop-row { display: flex; gap: 8px; flex-wrap: wrap; }
.tpop-duration-row { margin-top: 10px; }
/* 30 Sep 2026 (round 5): the duration scroll picker sits below the quick chips -- this label just
   makes clear it's an alternative to them, not a required extra step. */
.tpop-subhint { font-size: 11px; color: var(--muted); margin: 8px 0 4px; }
.tpop-hint { font-size: 11px; color: var(--muted); margin-top: 10px; line-height: 1.4; }
/* The popover reuses .time-picker verbatim (same component as before) -- just needs a touch less
   outer margin at this smaller container width than the detail card gave it. */
.time-popover .time-picker { margin: 10px 0 2px; }

/* 30 Sep 2026 (round 3): Map Mode's own layers/settings panel -- sits inline in the Map Mode sheet
   (not a topbar popover like .time-popover, since it's only ever relevant inside that one mode),
   same card visual-weight tokens as everywhere else in this file. */
.mapmode-layers-panel {
  background: var(--surface);
  border-radius: var(--radius-btn);
  padding: 10px 14px 12px;
  margin: 10px 0 14px;
}
.mapmode-layer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 0;
  font-size: 14px;
  cursor: pointer;
}
.mapmode-layer-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); }

/* 29 Sep 2026: Traffic Info Board incident/VMS pins -- plain DOM divIcon (small count, <500, see
   traffic.js's module doc -- no canvas renderer needed here, unlike bus stops/speedbands). Sits
   in Leaflet's default markerPane, same z-order as carpark pins -- these only ever show while the
   info board itself is open, so there's no competing-with-pins concern the landmark layer has. */
.spw-incident-pin {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--bg);
  border: 2px solid var(--lvl-filling-num);
  box-shadow: var(--shadow-chip);
  font-size: 14px;
  transform: translate(-50%, -50%);
}

/* 28 Sep 2026: Parking Kaki drawer -- replaces the old brand + live-timer chips. Left slide-in
   (the menu button sits top-left), scaffolded here in M1; M2/M4/M6 mount real content into
   #drawer-body -- never fake "coming soon" stubs (see this journey's no-placeholder-clutter rule). */
.drawer-backdrop {
  position: absolute;
  inset: 0;
  z-index: 1900;
  background: rgba(30, 36, 33, 0.35);
  opacity: 0;
  transition: opacity var(--dur-drawer) var(--ease-out);
}
.drawer-backdrop.is-open { opacity: 1; }
.app-drawer {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  z-index: 1950;
  width: min(86vw, 340px);
  background: var(--bg);
  border-radius: 0 var(--radius-sheet) var(--radius-sheet) 0;
  box-shadow: 10px 0 30px rgba(30, 36, 33, 0.12);
  display: flex;
  flex-direction: column;
  padding: max(16px, env(safe-area-inset-top)) 18px 18px calc(18px + env(safe-area-inset-left));
  transform: translateX(-100%);
  transition: transform var(--dur-drawer) var(--ease-drawer);
  overflow-y: auto;
}
.app-drawer.is-open { transform: translateX(0); }
.drawer-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.drawer-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 15px; letter-spacing: 0.02em; }
.drawer-close {
  border: 0;
  background: var(--hover);
  color: var(--ink);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  flex: none;
}
.drawer-blurb { margin: 14px 0 0; font-size: 13px; color: var(--muted); line-height: 1.45; }
.drawer-body { margin-top: 18px; display: flex; flex-direction: column; gap: 18px; }
.drawer-section-title { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; }
.drawer-filter-note { margin: 10px 0 0; font-size: 12px; color: var(--muted); line-height: 1.4; }
.drawer-nav-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  height: 48px;
  padding: 0 14px;
  border: 0;
  border-radius: var(--radius-btn);
  background: var(--surface);
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  color: var(--ink);
  cursor: pointer;
}
.drawer-nav-count {
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 12px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* M6 (28 Sep 2026): generic centered modal -- "How to Use" walkthrough + iOS install instructions. */
.app-modal-backdrop {
  position: absolute;
  inset: 0;
  z-index: 2100;
  background: rgba(30, 36, 33, 0.45);
  opacity: 0;
  transition: opacity var(--dur-drawer) var(--ease-out);
}
.app-modal-backdrop.is-open { opacity: 1; }
.app-modal-card {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 2150;
  width: min(88vw, 360px);
  background: var(--bg);
  border-radius: var(--radius-card-lg);
  box-shadow: var(--shadow-sheet);
  padding: 24px 22px 20px;
  transform: translate(-50%, -50%) scale(0.94);
  opacity: 0;
  transition: transform var(--dur-drawer) var(--ease-drawer), opacity var(--dur-drawer) var(--ease-out);
  pointer-events: none;
}
.app-modal-card.is-open { transform: translate(-50%, -50%) scale(1); opacity: 1; pointer-events: auto; }
.app-modal-card .drawer-close { position: absolute; top: 14px; right: 14px; }
.app-modal-icon { font-size: 40px; line-height: 1; margin-bottom: 12px; }
.app-modal-title { font-size: 20px; font-weight: 700; line-height: 1.2; margin-bottom: 8px; padding-right: 24px; }
.app-modal-body { font-size: 14px; color: var(--body2); line-height: 1.55; }
.app-modal-body b { color: var(--ink); }
.app-modal-dots { display: flex; gap: 6px; margin-top: 18px; }
.app-modal-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--hairline); }
.app-modal-dots span.is-active { background: var(--accent); width: 18px; border-radius: var(--radius-pill); transition: width 200ms var(--ease-out); }
.app-modal-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 18px; }

@media (prefers-reduced-motion: reduce) {
  .drawer-backdrop, .app-drawer, .app-modal-backdrop, .app-modal-card { transition-duration: 1ms; }
}

/* M6(d) (28 Sep 2026, round 2): mobile-only reorientation cue after closing the drawer -- a soft
   ring flashes around the map's edge so the user's eye returns to it. box-shadow (not outline),
   inset, so it never shifts layout. Animates opacity of a pseudo box-shadow via a keyframe on the
   element itself since #app-map already owns position/inset for the tile layer. */
@keyframes spw-map-reorient-pulse {
  0% { box-shadow: inset 0 0 0 0 rgba(14, 124, 102, 0); }
  30% { box-shadow: inset 0 0 0 5px rgba(14, 124, 102, 0.45); }
  100% { box-shadow: inset 0 0 0 0 rgba(14, 124, 102, 0); }
}
.map-reorient-pulse { animation: spw-map-reorient-pulse 900ms var(--ease-out); }
@media (prefers-reduced-motion: reduce) {
  .map-reorient-pulse { animation: none; }
}
.weather-badge { color: var(--rain); }

/* M10 (27 Sep 2026): the compact "current location / destination" weather line on idle/results
   (map app) -- see app.js::weatherLineHtml(). */
/* 29 Sep 2026 (mobile UX batch): forced to a true single line -- SPW.weatherSummaryLine() already
   joins condition/rain/temp with " · " into one string, but a long combination could still wrap
   to 2 lines on a narrow phone; truncate with an ellipsis rather than let it wrap ("the extra is
   waste", Ronnie's own words). */
.weather-line { font-size: 13px; color: var(--body2); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Detail card's own per-carpark weather -- collapsed from 3 stacked rows to ONE line (icon +
   condition + rain + temp range), same single-line discipline as .weather-line above. The
   surface/open-air visual prominence (blue tint + left accent bar) is unchanged -- only the
   internal row count changed, not the card's own distinctiveness. */
.weather-card-line { display: flex; align-items: baseline; gap: 8px; padding: 0; font-size: 14px; }
.weather-card-line .wcl-label { font-weight: 700; flex: none; }
.weather-card-line .wcl-text { color: var(--body2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* EV quick-glance (item 3, mobile UX batch) -- a SMALL one-line summary sandwiched between the
   big "Lots free" availability figure and the Rates card; the full per-connector EV section
   further down the detail card is unchanged and still the authoritative detail view. */
.ev-quickglance { padding: 4px 16px 10px; display: flex; align-items: baseline; gap: 6px; font-size: 13px; }
.ev-quickglance .eqg-label { color: var(--muted); font-weight: 600; }
.ev-quickglance .eqg-val { color: var(--accent); font-weight: 700; }
/* Idle screen mascot (item 9, mobile UX batch) -- stands beside the "Where you parking?" search
   prompt per the reference concept sheet (art/preliminary-concept.png's Core Logo/Mascot panel).
   Source art is a composite reference sheet at modest resolution (see CLAUDE.md's mascot section)
   -- expect some softness at this display size, same known limitation as the app-icon crop. */
.idle-mascot-row { display: flex; align-items: center; gap: 12px; }
.idle-mascot-img { width: 56px; height: auto; flex: none; }
.idle-mascot-row .search-trigger { flex: 1; min-width: 0; }

/* Per-carpark weather card on the detail sheet (app.js::tplDetail) and the static carpark page
   (build_site.py::weather_section_html) -- plain by default, visually distinct (blue tint +
   left accent bar) when the carpark is surface/open-air, per Ronnie's explicit ask that rain
   matters a lot more for those than a basement/multi-storey carpark. */
.weather-card { padding: 12px 16px 4px; display: flex; flex-direction: column; }
.weather-card--surface {
  background: var(--rain-tint);
  border-left: 4px solid var(--rain);
  border-radius: var(--radius-card-sm);
  padding: 8px 16px 4px;
}
.weather-card--surface .kv-row,
.weather-card--surface .detail-rate-row { border-bottom-color: rgba(47, 111, 176, 0.18); }

/* 28 Sep 2026: area traffic feed -- sits directly below weatherLineHtml() on idle/results (same
   "ambient context near your search" placement, see app.js::trafficFeedHtml()) and reused
   verbatim for the full "See all" list (tplTrafficFull(), a `mode: "traffic"` view same pattern
   as My Shortlist). Reuses existing tokens only -- no new colour, this is a locked Soft Civic
   surface (see design-taste skill / CLAUDE.md). */
.traffic-feed { padding: 4px 16px 8px; }
.traffic-row { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--hairline); }
.traffic-row:last-of-type { border-bottom: 0; }
.traffic-row-icon { font-size: 16px; line-height: 1.4; flex-shrink: 0; }
.traffic-row-text { min-width: 0; }
.traffic-row-line { font-size: 14px; font-weight: 600; color: var(--ink); }
.traffic-row-msg { font-size: 13px; color: var(--muted); margin-top: 1px; overflow-wrap: break-word; }

.recenter-btn, .mapmode-entry-btn {
  position: absolute;
  right: 12px;
  bottom: calc(100% + 12px);
  width: 48px;
  height: 48px;
  background: var(--bg);
  border: 0;
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow-chip);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
/* 29 Sep 2026 (round 2): sits to the LEFT of recenter-btn (48px width + 8px gap = 68px). */
.mapmode-entry-btn { right: 68px; }

.app-sheet-wrap { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1000; }
.sidebar-resize-handle { display: none; } /* desktop-only, see the >=860px media query below */
.app-sheet {
  background: var(--bg);
  border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
  box-shadow: var(--shadow-sheet);
  padding-bottom: env(safe-area-inset-bottom);
  /* Detail mode (rates + weather + EV stacked) can exceed the viewport now that the sheet
     carries three cards instead of one -- .app-shell is position:fixed/overflow:hidden, so
     without this the overflow was just clipped and unreachable (reads as "frozen, can't
     scroll"). results-list manages its own inner scroll via drag-resize; this is a safety net
     for it too but only detail mode actually needs it in practice. */
  max-height: 82vh;
  max-height: 82dvh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
}

/* idle */
.sheet-idle { padding: 16px 16px 28px; display: flex; flex-direction: column; gap: 12px; }
.sheet-idle-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.sheet-label { font-size: 12px; font-weight: 600; color: var(--muted); white-space: nowrap; }
.search-trigger {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 56px;
  padding: 0 16px;
  background: var(--surface);
  border: 1px solid transparent;
  border-radius: var(--radius-btn);
  cursor: pointer;
  text-align: left;
  font: inherit;
  font-size: 17px;
  color: var(--muted);
  width: 100%;
}
/* 29 Sep 2026: the desktop-only copy of this trigger (see desktopSearchTriggerHtml() in app.js) --
   hidden on mobile, where results/shortlist/traffic/infoboard reach search via the back/close
   icon instead (no room for a 2nd search row on a phone-width sheet); shown in the desktop media
   query below. */
.desktop-search-trigger { display: none; margin: 0 16px 12px; width: calc(100% - 32px); }
.primary-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 56px;
  padding: 0 16px;
  background: var(--accent);
  border: 0;
  border-radius: var(--radius-btn);
  cursor: pointer;
  font: inherit;
  font-size: 17px;
  font-weight: 700;
  color: var(--on-accent);
  width: 100%;
}
.primary-btn:hover { background: var(--accent-hover); }

/* search */
.sheet-search-head { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px 10px; }
.text-btn { background: none; border: 0; font: inherit; font-size: 15px; font-weight: 600; cursor: pointer; padding: 6px 0; color: var(--ink); }
.sheet-search-list { max-height: 360px; overflow-y: auto; border-top: 1px solid var(--rule); }
.sheet-search-row {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 56px;
  padding: 10px 16px;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--ink);
}
.sheet-search-row:hover { background: var(--hover); }
.ssr-title { font-size: 16px; font-weight: 600; }
.ssr-sub { font-size: 13px; color: var(--muted); }
/* M1 (28 Sep 2026, round 2): the empty-shortlist search view's own suggestion row -- same shape as
   .sheet-search-row, plus a ♡ toggle so a carpark can be added without leaving the search. */
.ssr-textwrap { flex: 1; min-width: 0; }
.shortlist-search-row { justify-content: space-between; }
.shortlist-search-heart { flex: none; }
.sheet-search-empty { padding: 16px; font-size: 15px; color: var(--muted); }
.sheet-search-inputwrap { padding: 12px 16px 28px; border-top: 1px solid var(--rule); }
.search-input-row {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 56px;
  padding: 0 16px;
  background: var(--surface);
  border: 1px solid transparent;
  border-radius: var(--radius-btn);
}
.search-input-row.is-focused { border-color: var(--accent); }
.search-input-row input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: none;
  font: inherit;
  font-size: 17px;
  background: transparent;
  color: var(--ink);
}

/* results */
.drag-handle-row { height: 22px; display: flex; align-items: center; justify-content: center; cursor: ns-resize; touch-action: none; }
.drag-handle { width: 44px; height: 5px; border-radius: var(--radius-handle); background: var(--handle); }
/* 30 Sep 2026 (round 3): one-time discoverability nudge -- see app.js::showDragHintOnce(). Two
   short bounce cycles then stops (never a looping/distracting animation). */
@keyframes spw-drag-handle-hint {
  0%, 100% { transform: translateY(0); }
  25% { transform: translateY(-4px); }
  50% { transform: translateY(1px); }
  75% { transform: translateY(-2px); }
}
.drag-handle--hint { animation: spw-drag-handle-hint 480ms var(--ease-out) 2; background: var(--accent); }
@media (prefers-reduced-motion: reduce) { .drag-handle--hint { animation: none; } }
.results-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 0 16px 12px; }
.results-head-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
/* M4 (28 Sep 2026, round 2): shared sort-by control, top-right of both results + My Shortlist. */
.results-head-actions { display: flex; align-items: center; gap: 4px; flex: none; }
.sort-select {
  height: 36px;
  padding: 0 10px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--rule);
  background: #fff;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.sort-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.results-title { font-size: 22px; font-weight: 700; line-height: 1.15; text-wrap: pretty; }
.icon-btn {
  flex: none;
  width: 44px;
  height: 44px;
  background: none;
  border: 0;
  border-radius: var(--radius-btn);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.icon-btn:hover { background: var(--hover); }
/* touch-action: pan-y — vertical swipes inside the list scroll the list, never leak through to
   pan the Leaflet map underneath (scroll trap fix, 26 Sep; see app.js boot() for the JS side). */
.results-list { overflow-y: auto; border-top: 1px solid var(--rule); touch-action: pan-y; }
/* 29 Sep 2026: #infoboard-list/#traffic-full-list are the ONLY .results-list instances whose rows
   (.traffic-row, .detail-rate-row) carry no horizontal padding of their own -- .result-row (the
   carpark results list, the 3rd thing this container holds elsewhere) already pads itself, so
   padding .results-list generically would double-inset that one. Scoped to just these two ids,
   which were bleeding text to both edges with zero breathing room (Ronnie: "padding all squeeze
   to one side"). */
#infoboard-list, #traffic-full-list { padding: 0 16px; }
.result-row {
  width: 100%;
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  min-height: 68px;
  padding: 10px 16px;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--ink);
}
.result-row:hover { background: var(--hover); }
.rr-lots { display: flex; flex-direction: column; gap: 4px; }
.rr-lots-num { font-size: 24px; font-weight: 700; line-height: 1; }
.rr-bar-track { height: 4px; background: var(--hairline); border-radius: var(--radius-handle); overflow: hidden; }
.rr-bar-fill { height: 4px; }
.rr-name { font-size: 16px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rr-sub { font-size: 13px; color: var(--muted); }
.rr-rate { font-size: 14px; font-weight: 600; text-align: right; white-space: nowrap; }
.results-footer { padding: 12px 16px 28px; border-top: 1px solid var(--rule); }
.rr-of { color: var(--muted); font-weight: 400; }

/* M4 (28 Sep 2026): My Shortlist view -- reuses .results-head/.icon-btn/.dir-chip tokens. */
.shortlist-list { overflow-y: auto; border-top: 1px solid var(--rule); max-height: 62vh; }
/* M5 (28 Sep 2026): idle-screen shortlist teaser -- same rows, no forced scroll container. */
.idle-shortlist { margin: 4px 0 0; }
.shortlist-list--idle { border-top: 0; max-height: none; }
.shortlist-list--idle .shortlist-row:last-child { border-bottom: 0; }
.shortlist-row { display: flex; align-items: flex-start; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--hairline); }
.shortlist-remove { flex: none; }
.shortlist-row-main { flex: 1; min-width: 0; cursor: pointer; }
/* M5 (28 Sep 2026, round 2): the lot number is the HERO figure now ("1.5 size" per Ronnie), with
   the rate + EV available/total chip riding its BASELINE (align-items:baseline, not center -- see
   design-taste's display-typography.md §0, "centering aligns bounding boxes, not where letters
   sit"). 32px was picked as clearly dominant within this compact row's own context (2x the 16px
   carpark name above it, bigger than the plain results-list's 24px .rr-lots-num) without
   overpowering a list row the way a stat-slide's 60px+ figure would. */
.shortlist-stat-hero-row { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
.shortlist-stat-hero { font-size: 32px; font-weight: 700; line-height: 1; }
.shortlist-rate { font-size: 14px; font-weight: 600; color: var(--body2); margin-left: 4px; }
.shortlist-ev-chip { font-size: 13px; font-weight: 700; color: var(--accent); }
/* The $/kWh sub-line sits in its OWN row below, left-aligned under the hero figure -- not
   vertically centered against it -- at an even smaller size than the EV chip above (11px vs 13px),
   per Ronnie's "smaller...even smaller" size ordering (hero > EV chip > EV $/kWh line). */
.shortlist-ev-sub { font-size: 11px; color: var(--muted); margin-top: 3px; }
.shortlist-nav-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 10px; }
.dir-chip--sm { height: 32px; padding: 0 12px; font-size: 13px; }
.shortlist-go-btn {
  flex: none;
  display: flex;
  align-items: center;
  gap: 4px;
  height: 32px;
  padding: 0 14px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
}
.shortlist-go-btn:hover { background: var(--accent-hover); color: var(--on-accent); }

/* detail */
.detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 14px 16px 14px; }
.detail-head-text { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.detail-name { font-size: 26px; font-weight: 700; line-height: 1.1; text-wrap: pretty; }
.detail-stats { display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.detail-stat-cell { padding: 12px 16px; display: flex; flex-direction: column; gap: 4px; }
.detail-stat-cell--left { border-right: 1px solid var(--rule); }
.detail-stat-big-row { display: flex; align-items: baseline; gap: 6px; }
.detail-stat-big { font-size: 40px; font-weight: 700; line-height: 1; }
.detail-stat-of { font-size: 14px; color: var(--muted); }
.detail-stat-big2 { font-size: 40px; font-weight: 700; line-height: 1; }
.detail-stat-walk { font-size: 13px; color: var(--muted); }
.detail-rates { padding: 12px 16px 4px; display: flex; flex-direction: column; }
/* M7 (28 Sep 2026, round 2): inline thumb-scroll time picker -- replaces the native
   <input type="datetime-local">. Three CSS scroll-snap columns; a fixed-position highlighted band
   marks the "selected" center row (matches the native iOS wheel-picker convention drivers already
   know, without pulling in an external library). ROW_H below (36px) MUST match app.js's own
   ROW_H constant in wireTimePicker() -- both need the exact same row height to agree on which
   item is centered. */
.time-picker {
  position: relative;
  display: flex;
  gap: 4px;
  height: 144px; /* 4 rows visible (36px each) */
  margin-bottom: 10px;
  border-radius: var(--radius-btn);
  background: var(--surface);
  overflow: hidden;
}
.tp-selection-band {
  position: absolute;
  left: 8px;
  right: 8px;
  top: 54px; /* (144 - 36) / 2 -- vertically centers the 36px band */
  height: 36px;
  border-radius: 10px;
  background: var(--card);
  box-shadow: var(--shadow-chip);
  pointer-events: none;
  z-index: 1;
}
.tp-col {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.tp-col::-webkit-scrollbar { display: none; }
.tp-col-inner { display: flex; flex-direction: column; }
.tp-pad { height: 54px; flex: none; } /* matches .tp-selection-band's top offset, pads first/last item to the center */
.tp-item {
  height: 36px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: center;
  font-size: 14px;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
  transition: color 120ms var(--ease-out), font-weight 120ms var(--ease-out);
}
.tp-item.is-selected { color: var(--ink); font-weight: 700; font-size: 15px; }
/* 30 Sep 2026 (round 5): duration picker -- reuses .time-picker/.tp-col/.tp-item verbatim (2
   columns instead of 3, flex:1 already distributes width for any column count), just adds small
   "hr"/"min" unit labels between/after the two scroll columns. */
.time-picker--duration { gap: 2px; }
.tp-unit-label { display: flex; align-items: center; justify-content: center; flex: none; width: 26px; font-size: 12px; font-weight: 600; color: var(--muted); }
@media (prefers-reduced-motion: reduce) {
  .tp-col { scroll-behavior: auto; }
}
.detail-rate-row { display: flex; justify-content: space-between; gap: 16px; padding: 8px 0; border-bottom: 1px solid var(--hairline); font-size: 15px; }
/* 30 Sep 2026 (QA fix): the EV connector row's label (operator + plug type + speed tier, e.g.
   "FASTPARKNCHARGE PTE. LTD. · Combo 2 · 100kW · DC Fast") is much longer than every other user
   of this shared row (rates/ERP rows' labels are always short) -- with no min-width:0, a flex
   item's default min-width:auto floors it at its own unwrapped content width, so the label never
   shrinks/wraps and instead pushes the price ($0.74/kWh) off the edge of the viewport, where an
   ancestor's overflow clips it mid-character. Letting the label wrap onto 2 lines (same technique
   the rates table itself would need if it ever grew a long label) fixes this for every current and
   future long-label row, while every short-label row is visually unchanged (nothing to wrap). */
.detail-rate-row > *:first-child { flex: 1 1 auto; min-width: 0; white-space: normal; }
.detail-rate-row > *:last-child { flex: none; white-space: nowrap; }
/* 28 Sep 2026 (post-launch fix): collapsed disclosure for a carpark with zero published rates --
   replaces a permanently-open "Rates not available" block + a dead estimator that could never
   compute anything, per Ronnie's own "make it a drawer/button" ask. */
.rates-disclosure-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 15px;
  color: var(--muted);
  cursor: pointer;
}
.rates-disclosure-chevron { transition: transform 150ms var(--ease-out); }
.rates-disclosure-btn[aria-expanded="true"] .rates-disclosure-chevron { transform: rotate(180deg); }
.detail-meta-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0 4px; font-size: 12px; color: var(--muted); }
.detail-fulllink { flex: none; font-size: 13px; font-weight: 700; color: var(--accent); padding: 6px 0; }
/* M4 (28 Sep 2026): shortlist heart + a bigger "Full details" target sitting beside it. */
.detail-link-row { display: flex; align-items: center; gap: 10px; padding: 6px 0 10px; }
.heart-btn {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--rule);
  background: var(--card);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform 125ms var(--ease-out);
}
.heart-btn:active { transform: scale(0.88); }
.heart-btn[aria-pressed="true"] { border-color: #f3c3cb; background: #fdeef0; }
.detail-fulllink--btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  border-radius: var(--radius-btn);
  background: var(--hover);
  font-size: 14px;
}
.detail-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 8px 16px 28px; }
.detail-action-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 56px;
  padding: 0 14px;
  border-radius: var(--radius-btn);
  text-decoration: none;
  font-weight: 700;
  font-size: 16px;
}
.detail-action-btn--accent { background: var(--accent); color: var(--on-accent); }
.detail-action-btn--accent:hover { background: var(--accent-hover); color: var(--on-accent); }
.detail-action-btn--sec { background: var(--sec); color: var(--on-sec); }
.detail-action-btn--sec:hover { background: var(--sec-hover); color: var(--on-sec); }

/* leaflet pin (custom divIcon content, styled inline for portability; class hooks here for hover states) */
.spw-pin { display: flex; flex-direction: column; align-items: center; cursor: pointer; }
.spw-pin-chip { white-space: nowrap; font-weight: 700; font-family: var(--font); border: 2px solid #fff; box-shadow: var(--shadow-pin); font-variant-numeric: tabular-nums; }
.spw-pin-stem { width: 2px; background: currentColor; }
/* 30 Sep 2026 (round 5): density-cluster badge -- a real count of carparks that overlapped even
   after the golden-angle spiral above; tap to fly in and let them spread into individual pins. */
.spw-cluster { display: flex; flex-direction: column; align-items: center; cursor: pointer; }
.spw-cluster-badge {
  width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--on-accent);
  font-weight: 700; font-family: var(--font); font-size: 15px;
  border: 2px solid #fff; box-shadow: var(--shadow-pin);
}
.spw-cluster-label {
  margin-top: 3px; font-size: 10px; font-weight: 700; letter-spacing: 0.02em;
  color: var(--ink); background: #fff; padding: 1px 6px; border-radius: var(--radius-pill);
  box-shadow: var(--shadow-pin); white-space: nowrap;
}
.spw-dest-pin { background: var(--bg); border: 4px solid var(--ink); transform: rotate(45deg); }
.spw-user-dot { border-radius: 50%; background: var(--user-dot); border: 3px solid #fff; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.35); }

.toast {
  position: absolute;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 2000;
  background: var(--ink);
  color: #fff;
  padding: 10px 18px;
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 600;
  max-width: 90vw;
  text-align: center;
}
.hidden { display: none !important; }

/* ================= Desktop layout (29 Sep 2026) =================
   Ronnie's ask: on a wide screen, dock the search/results/detail panel to the RIGHT instead of
   the mobile bottom-sheet. RIGHT, not left -- the "Parking Kaki" menu button + its slide-in
   drawer (M1, 28 Sep 2026) already own the left edge; docking the panel there too would mean the
   two fight for the same space every time the drawer opens. Below this breakpoint nothing
   changes at all -- this is additive on top of the existing mobile-first build, not a rewrite.
   The drag-resize handle (state.listH, app.js) is a mobile-only interaction for a bottom sheet's
   height; here the panel simply fills the full viewport height instead, so the handle is hidden
   rather than reworked -- the underlying JS drag listeners just have nothing to grab. */
:root { --sheet-desktop-w: 420px; }
@media (min-width: 860px) {
  .app-sheet-wrap { left: auto; top: 0; right: 0; bottom: 0; width: var(--sheet-desktop-w); }
  .desktop-search-trigger { display: flex; }
  .app-sheet {
    height: 100%;
    max-height: none;
    border-radius: 0;
    box-shadow: -2px 0 16px rgba(0, 0, 0, 0.1);
  }
  .drag-handle-row { display: none; }
  /* .results-head (results/shortlist/traffic/infoboard headers) has zero top padding by design --
     on mobile the 22px .drag-handle-row sitting above it was ALSO acting as the header's top
     breathing room. Hiding the handle above removed that space too, so every one of those
     headers sat flush against the very top edge ("squeezed too high", Ronnie's screenshot of the
     Traffic Info Board). Restored directly here since the handle is never coming back on desktop. */
  .results-head { padding-top: 20px; }
  /* Overrides the inline style="height:${state.listH}px" app.js sets on this element for the
     mobile drag-resize -- the sidebar has no resize handle to drive that state on desktop, so the
     list just grows with its own content and scrolls inside .app-sheet's existing overflow-y. */
  .results-list { height: auto !important; }
  .app-topbar { right: calc(var(--sheet-desktop-w) + 12px); }
  /* Positioned relative to the mobile bottom sheet's own box (recenter-btn nests inside
     .app-sheet-wrap) -- on desktop the sheet is a right sidebar, not a bottom strip, so it needs
     repositioning to stay over the MAP, not the sidebar. */
  .recenter-btn { position: fixed; right: calc(var(--sheet-desktop-w) + 12px); bottom: 24px; }
  .mapmode-entry-btn { position: fixed; right: calc(var(--sheet-desktop-w) + 68px); bottom: 24px; }
  /* 30 Sep 2026 (round 5): desktop sidebar resize -- a thin grab-strip on the sidebar's LEFT edge,
     same spirit as the mobile .drag-handle-row/state.listH vertical resize (see app.js's
     wireSidebarResize()). Widened hit-area via a transparent halo so it's easy to grab without
     being visually heavy; the visible line only shows on hover/drag. */
  .sidebar-resize-handle {
    display: block; position: absolute; top: 0; bottom: 0; left: -4px; width: 8px;
    cursor: ew-resize; z-index: 1001; background: transparent;
  }
  .sidebar-resize-handle::after {
    content: ""; position: absolute; top: 0; bottom: 0; left: 3px; width: 2px;
    background: transparent; transition: background 0.15s ease;
  }
  .sidebar-resize-handle:hover::after, .sidebar-resize-handle.is-dragging::after { background: var(--accent); }
}

/* 30 Sep 2026: "Watch how to get in" button + lightweight YouTube modal (Soft Civic tokens). */
.video-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; height: 48px; border-radius: var(--radius-btn);
  background: var(--card); color: var(--accent); border: 1.5px solid var(--accent);
  font: inherit; font-weight: 700; font-size: 15px; cursor: pointer;
}
.video-btn:hover { background: var(--accent); color: var(--on-accent); }
.video-btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.video-btn-play { font-size: 12px; }
.detail-video-row { padding: 8px 16px 0; }
.live-card .video-btn { margin-top: 10px; }
.video-modal { position: fixed; inset: 0; z-index: 5000; display: flex; align-items: center; justify-content: center; padding: 16px; }
.video-modal-backdrop { position: absolute; inset: 0; background: rgba(30, 36, 33, 0.6); }
.video-modal-card {
  position: relative; width: min(94vw, 720px); background: var(--bg); border-radius: var(--radius-card-lg);
  box-shadow: var(--shadow-sheet); padding: 14px 14px 12px;
}
.video-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.video-modal-title { font-size: 16px; font-weight: 700; line-height: 1.2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.video-modal-close { position: static; flex: none; }
.video-modal-frame { position: relative; width: 100%; aspect-ratio: 16 / 9; border-radius: var(--radius-card-sm); overflow: hidden; background: #000; }
.video-modal-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.video-modal-credit { margin-top: 8px; font-size: 12px; color: var(--muted); }
