/* STAR Navigator — platform-side signage.
 *
 * The palette is a station at night: dark platform glass, enamel line bars, and
 * the amber of a departure board. Latin type follows MTR signage (Myriad
 * Semibold); Chinese follows MTR Sung, a Song face whose horizontals are
 * thickened for legibility at distance, so Song-style fallbacks come first.
 * Motion uses one small set of Material 3 curves rather than ad-hoc easings.
 */

:root {
  --ground: #0a0e13;
  --platform: #121822;
  --platform-high: #18202c;
  --edge: #253040;
  --ink: #eaf0f7;
  --muted: #8494a8;
  --amber: #f5b72e;
  --depart: #24c07e;
  --arrive: #ff5e4d;

  --font-latin: "MTR Sung", "Myriad Pro", "Myriad Set Pro", "Frutiger LT Std", Frutiger,
    "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-han: "MTR Sung", "Songti TC", "Songti SC", "Noto Serif TC", "Noto Serif HK",
    "Source Han Serif TC", "PingFang HK", serif;
  --font-board: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;

  --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
  --ease-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
  --ease-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15);
  --flash-period: 1200ms;
  --quick: 130ms;
  --normal: 220ms;
  --slow: 380ms;

  --radius: 14px;
  --radius-small: 9px;
  --shadow: 0 18px 50px rgba(0, 0, 0, 0.55);

  --endpoint-dot-x: 0.6rem;  /* centre line the endpoint dots and rail share */
  --rail-width: 25rem;
  --topbar-height: 3.5rem;

  --map-ground: #0a0e13;
  --map-casing: #05070c;
  --label-halo: rgba(10, 14, 19, 0.9);

  /* Glass, properly: thin enough that the map reads through it, saturated so the
     line colours underneath stay line colours, and lit along the top edge the way
     a real pane catches the light. */
  --glass: color-mix(in srgb, var(--platform) 52%, transparent);
  --glass-blur: blur(34px) saturate(215%) brightness(1.05);
  /* Lit along the top edge and shaded along the bottom, the way a real pane sits
     in a frame; the sheen is the reflection across its face. */
  --glass-lip:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(0, 0, 0, 0.3),
    inset 1px 0 0 rgba(255, 255, 255, 0.05);
  --glass-sheen: linear-gradient(148deg,
    rgba(255, 255, 255, 0.11) 0%,
    rgba(255, 255, 255, 0.028) 34%,
    rgba(255, 255, 255, 0) 62%);
  --glass-hairline: color-mix(in srgb, var(--edge) 55%, transparent);
}

/* Daylight: the same signage, read on a platform in the sun. */
[data-theme="light"] {
  --ground: #f4f1ea;
  --platform: #ffffff;
  --platform-high: #f7f5f0;
  --edge: #d8d2c6;
  --ink: #14181f;
  --muted: #6c7686;
  --amber: #c07d05;
  --depart: #0f8f57;
  --arrive: #d63b28;
  --shadow: 0 18px 40px rgba(31, 27, 20, 0.16);

  --map-ground: #efece4;
  --map-casing: #ffffff;
  --label-halo: rgba(244, 241, 234, 0.92);

  --glass: color-mix(in srgb, var(--platform) 58%, transparent);
  --glass-lip:
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    inset 0 -1px 0 rgba(31, 27, 20, 0.08);
  --glass-sheen: linear-gradient(148deg,
    rgba(255, 255, 255, 0.6) 0%,
    rgba(255, 255, 255, 0.18) 34%,
    rgba(255, 255, 255, 0) 62%);
}

* { box-sizing: border-box; }
/* Any `display` of our own beats the browser's default for [hidden], so the
   attribute has to be given the last word or hidden markup shows anyway. */
[hidden] { display: none !important; }
html, body { height: 100%; margin: 0; overflow: hidden; overscroll-behavior: none; }

body {
  background: var(--ground);
  color: var(--ink);
  font: 500 15px/1.5 var(--font-latin);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

html[lang="zh-Hant"] body { font-family: var(--font-han); }

button, input { font: inherit; color: inherit; }
button { background: none; border: none; padding: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }

/* --- stage --------------------------------------------------------------- */

.map-stage {
  position: fixed;
  inset: 0;
  background: var(--map-ground);
  transition: background var(--normal) var(--ease-emphasized);
}
[data-theme="dark"] .map-stage {
  background: radial-gradient(120% 90% at 70% 10%, #101722 0%, var(--ground) 60%);
}

/* --- top bar ------------------------------------------------------------- */

.topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--topbar-height);
  /* Three tracks, not a row: the middle one is the bar's own centre, so the view
     switch sits in the middle of the window rather than wherever the brand
     happens to end. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem;
  padding: 0 0.9rem;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--ground) 80%, transparent),
    color-mix(in srgb, var(--ground) 52%, transparent) 70%, transparent);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-lip);
  -webkit-backdrop-filter: var(--glass-blur);
  z-index: 40;
}

.brand { display: flex; align-items: center; gap: 0.55rem; min-width: 0; }
.brand-mark {
  width: 1.1rem; height: 1.1rem; flex: none;
  border-radius: 50%;
  border: 3px solid var(--amber);
}
.brand-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar-end { display: flex; align-items: center; gap: 0.5rem; justify-self: end; }
.topbar > .segmented { justify-self: center; }

/* Segmented control: the thumb slides, the labels hold still. */
.segmented {
  position: relative;
  display: flex;
  padding: 3px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border: 1px solid var(--edge);
}
.segment {
  position: relative;
  z-index: 1;
  padding: 0.32rem 0.85rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  transition: color var(--normal) var(--ease-emphasized);
}
.segment.is-selected { color: var(--ground); }
.segment:disabled { opacity: 0.35; cursor: not-allowed; }
.segment-thumb {
  position: absolute;
  top: 3px; left: 0;
  height: calc(100% - 6px);
  border-radius: 999px;
  background: var(--amber);
  transition: transform var(--normal) var(--ease-emphasized),
              width var(--normal) var(--ease-emphasized);
}
.segmented.is-compact .segment { padding: 0.32rem 0.6rem; font-size: 0.78rem; }

.icon-button {
  width: 2.1rem; height: 2.1rem;
  flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--edge);
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  color: var(--muted);
  transition: background var(--quick) var(--ease-emphasized),
              transform var(--quick) var(--ease-emphasized),
              border-color var(--quick) var(--ease-emphasized),
              color var(--quick) var(--ease-emphasized);
}
.icon-button:hover { background: color-mix(in srgb, var(--ink) 10%, transparent); border-color: var(--amber); color: var(--amber); }
.icon-button:active { transform: scale(0.92); }

.status {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.status-dot {
  width: 0.5rem; height: 0.5rem;
  border-radius: 50%;
  background: var(--muted);
  transition: background var(--normal) var(--ease-emphasized),
              box-shadow var(--normal) var(--ease-emphasized);
}
.status.is-online .status-dot { background: var(--depart); box-shadow: 0 0 0 4px rgba(36, 192, 126, 0.16); }
.status.is-offline .status-dot { background: var(--amber); }
.status.is-offline .status-text { color: var(--amber); }

/* --- sheet --------------------------------------------------------------- */

.sheet {
  position: fixed;
  z-index: 30;
  top: calc(var(--topbar-height) + 0.2rem);
  left: 0.9rem;
  bottom: 0.9rem;
  width: var(--rail-width);
  display: flex;
  flex-direction: column;
  background: var(--glass-sheen), var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow), var(--glass-lip);
  overflow: hidden;
}
.sheet-grip { display: none; }
.sheet-body {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

/* --- icons ---------------------------------------------------------------

   Lucide (ISC), drawn as strokes rather than as filled shapes, so one rule sizes
   them all and each one takes the colour of the button it sits in — which is
   what makes a hover or a disabled state carry to the glyph without a second
   rule per icon. */
.i {
  width: 1.05rem; height: 1.05rem;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon-button .i { width: 1.1rem; height: 1.1rem; }
.icon-button.is-small .i { width: 0.95rem; height: 0.95rem; }
.field-clear .i, .card-close .i {
  position: absolute;
  left: 50%; top: 50%;
  width: 0.85rem; height: 0.85rem;
  transform: translate(-50%, -50%);
  color: var(--muted);
}
.back .i { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); color: var(--muted); }
.swap .i { width: 1rem; height: 1rem; }
/* The theme button shows the sky you are switching to, not the one you are in. */
.i-light { display: none; }
[data-theme="light"] .i-dark { display: none; }
[data-theme="light"] .i-light { display: block; }

/* --- endpoints ----------------------------------------------------------- */

.planner { display: flex; flex-direction: column; gap: 0.9rem; }
.endpoints {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 2.4rem;
  grid-template-rows: auto auto;
  align-items: end;
  gap: 0.55rem 0.35rem;
}
.endpoints .field { grid-column: 1; }

/* One rail joins the two ends, the way a journey is drawn on a diagram. */
.endpoints-rail {
  position: absolute;
  left: calc(var(--endpoint-dot-x) - 1px);
  top: 2.1rem;
  bottom: 1.3rem;
  width: 2px;
  background: linear-gradient(180deg, var(--depart), var(--arrive));
  border-radius: 2px;
}

.field { position: relative; padding-left: 1.7rem; }
.field::before {
  content: "";
  position: absolute;
  left: calc(var(--endpoint-dot-x) - 0.375rem);
  top: 1.75rem;
  width: 0.75rem; height: 0.75rem;
  border-radius: 50%;
  border: 3px solid var(--depart);
  background: var(--platform);
  z-index: 1;
}
.field[data-endpoint="to"]::before { border-color: var(--arrive); }
.field-label {
  display: block;
  font-size: 0.688rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.15rem;
}
.field-input {
  display: flex;
  align-items: center;
  border-radius: var(--radius-small);
  border: 1px solid var(--edge);
  background: var(--platform-high);
  transition: border-color var(--quick) var(--ease-emphasized),
              box-shadow var(--quick) var(--ease-emphasized);
}
.field-input:focus-within {
  border-color: var(--amber);
  box-shadow: 0 0 0 3px rgba(245, 183, 46, 0.16);
}
.field-input input {
  flex: 1;
  min-width: 0;
  padding: 0.55rem 0.6rem;
  border: none;
  background: none;
  outline: none;
}
.field-input input::placeholder { color: var(--muted); }
.field-clear {
  position: relative;
  width: 1.6rem; height: 1.6rem;
  margin-right: 0.3rem;
  border-radius: 50%;
  opacity: 0;
  transform: scale(0.7);
  pointer-events: none;
  transition: opacity var(--quick) var(--ease-emphasized),
              transform var(--quick) var(--ease-emphasized),
              background var(--quick) var(--ease-emphasized);
}
.field.has-value .field-clear { opacity: 1; transform: scale(1); pointer-events: auto; }
.field-clear:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }

.swap {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  justify-self: center;
  width: 2.2rem; height: 2.2rem;
  border-radius: 50%;
  border: 1px solid var(--edge);
  background: var(--platform-high);
  color: var(--muted);
  transition: transform var(--normal) var(--ease-emphasized),
              border-color var(--quick) var(--ease-emphasized),
              color var(--quick) var(--ease-emphasized);
}
.swap:hover { border-color: var(--amber); color: var(--amber); }
.swap:active { transform: rotate(180deg); }

/* --- suggestions --------------------------------------------------------- */

.suggestions {
  position: absolute;
  z-index: 20;
  left: 1.7rem; right: 0;
  top: calc(100% + 0.3rem);
  max-height: 15rem;
  overflow-y: auto;
  border-radius: var(--radius-small);
  border: 1px solid var(--glass-hairline);
  background: var(--glass-sheen), var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--shadow), var(--glass-lip);
  animation: drop var(--normal) var(--ease-decelerate);
}
@keyframes drop {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}
.suggestion {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  width: 100%;
  padding: 0.5rem 0.65rem;
  text-align: left;
  transition: background var(--quick) var(--ease-emphasized);
}
.suggestion .primary { font-weight: 600; }
.suggestion .secondary { color: var(--muted); font-size: 0.85em; font-family: var(--font-han); }
.suggestion .region { margin-left: auto; font-size: 0.7rem; color: var(--muted); white-space: nowrap; }
.suggestion .tag {
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  padding: 0.05rem 0.35rem;
  border-radius: 4px;
  background: rgba(245, 183, 46, 0.18);
  color: var(--amber);
}
.suggestion.is-active { background: rgba(245, 183, 46, 0.14); }

/* --- switches ------------------------------------------------------------ */

.switches { display: flex; flex-direction: column; gap: 0.1rem; }
.switch {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.4rem 0.15rem;
  border-radius: var(--radius-small);
  transition: background var(--quick) var(--ease-emphasized);
}
.switch:hover { background: color-mix(in srgb, var(--ink) 5%, transparent); }
.switch-track {
  position: relative;
  width: 2.35rem; height: 1.35rem;
  flex: none;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 10%, transparent);
  border: 1px solid var(--edge);
  transition: background var(--normal) var(--ease-emphasized),
              border-color var(--normal) var(--ease-emphasized);
}
.switch-knob {
  position: absolute;
  top: 50%;
  left: 3px;
  width: 0.95rem; height: 0.95rem;
  border-radius: 50%;
  background: var(--muted);
  transform: translateY(-50%);
  transition: transform var(--normal) var(--ease-emphasized),
              background var(--normal) var(--ease-emphasized),
              width var(--quick) var(--ease-emphasized);
}
.switch[aria-checked="true"] .switch-track { background: rgba(245, 183, 46, 0.28); border-color: var(--amber); }
.switch[aria-checked="true"] .switch-knob { transform: translate(0.98rem, -50%); background: var(--amber); }
.switch:active .switch-knob { width: 1.15rem; }
.switch-label { font-size: 0.9rem; text-align: left; }

/* --- chips --------------------------------------------------------------- */

.chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.chip {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.16rem 0.55rem;
  border-radius: 999px;
  border: 1px solid var(--edge);
  background: color-mix(in srgb, var(--ink) 5%, transparent);
  transition: transform var(--quick) var(--ease-emphasized);
}
.chip.is-removable:hover { transform: translateY(-1px); }
.chips.saved { margin-top: 0.1rem; }
.chip.is-saved {
  border-color: color-mix(in srgb, var(--amber) 45%, transparent);
  color: var(--amber);
  cursor: pointer;
  transition: transform var(--quick) var(--ease-emphasized),
              background var(--quick) var(--ease-emphasized);
}
.chip.is-saved:hover { background: color-mix(in srgb, var(--amber) 14%, transparent); }
.chip.is-saved:active { transform: scale(0.96); }

/* --- routes -------------------------------------------------------------- */

.routes { display: flex; flex-direction: column; gap: 0.6rem; }
.hint { color: var(--muted); margin: 0; font-size: 0.9rem; }

.route {
  position: relative;
  display: grid;
  grid-template-columns: 0.5rem 1fr;
  gap: 0.75rem;
  padding: 0.75rem 0.85rem 0.75rem 0.6rem;
  border-radius: var(--radius);
  border: 1px solid var(--edge);
  background: var(--platform-high);
  cursor: pointer;
  text-align: left;
  width: 100%;
  transition: border-color var(--normal) var(--ease-emphasized),
              background var(--normal) var(--ease-emphasized),
              transform var(--quick) var(--ease-emphasized);
}
.route:hover { transform: translateY(-1px); border-color: #35435a; }
.route.is-selected { border-color: var(--amber); background: color-mix(in srgb, var(--amber) 8%, var(--platform-high)); }

/* The signature: a card's edge is the journey itself — one band per line, each
   band as tall a share of the ribbon as its leg is of the travel time. */
/* The ribbon is the itinerary's own rail seen edge on, so it starts and stops
   where the rail does rather than running the whole height of the card. */
.ribbon { display: flex; flex-direction: column; gap: 2px; padding: 0.2rem 0; }
.ribbon span {
  display: block;
  border-radius: 999px;
  transition: flex-grow var(--slow) var(--ease-emphasized);
}

.route-head { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.route-time { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.01em; }
.route-changes { color: var(--muted); font-size: 0.85rem; }
.badge {
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.12rem 0.4rem;
  border-radius: 4px;
  border: 1px solid currentColor;
  color: var(--muted);
}
.badge.is-fastest { color: var(--depart); }
.badge.is-estimated { color: var(--amber); }
.route-lines { margin-top: 0.45rem; }

/* One continuous timeline, not a stack of separate boxes. */
.timeline {
  list-style: none;
  margin: 0.7rem 0 0;
  padding: 0;
  display: none;
  flex-direction: column;
}
.route.is-selected .timeline { display: flex; animation: unfold var(--slow) var(--ease-decelerate); }
@keyframes unfold {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
/* One rail runs the whole itinerary: 7px is the centre of a 14px node, and each
   row's rail meets the next with no gap. */
.step { position: relative; padding: 0 0 0.55rem 1.55rem; min-height: 1.1rem; }
.step-rail {
  position: absolute;
  left: 5px;
  top: 0;
  bottom: -1px;
  width: 4px;
  background: var(--edge);
}
.step-rail.is-end { bottom: auto; height: 0.55rem; border-radius: 0 0 4px 4px; }
.step.is-station:first-child .step-rail { top: 0.55rem; border-radius: 4px 4px 0 0; }
.step:last-child { padding-bottom: 0; }
.step-node {
  position: absolute;
  left: 0;
  top: 0.2rem;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--platform-high);
  border: 3px solid var(--edge);
  z-index: 1;
}
.step.is-station { padding-bottom: 0.35rem; }
.step.is-ride, .step.is-walk { padding-bottom: 0.55rem; }
.step-station { font-weight: 600; }
.step-station .han { font-family: var(--font-han); color: var(--muted); font-weight: 500; margin-left: 0.3rem; font-size: 0.9em; }
.step-detail { color: var(--muted); font-size: 0.85rem; }
.step.is-walk .step-rail {
  background: repeating-linear-gradient(180deg, var(--muted) 0 4px, transparent 4px 9px);
}

/* Live departures, set like a platform board. */
.board-line {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.15rem;
  font-family: var(--font-board);
  font-size: 0.76rem;
  color: var(--amber);
}
.board-line.is-quiet { color: var(--muted); }

.detail-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding-bottom: 0.2rem;
}
.back {
  width: 2rem; height: 2rem;
  flex: none;
  position: relative;
  border-radius: 50%;
  border: 1px solid var(--edge);
  transition: background var(--quick) var(--ease-emphasized);
}
.back:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
.detail-summary { font-weight: 700; font-size: 1.05rem; margin-right: auto; }
.icon-button.is-small { width: 1.85rem; height: 1.85rem; }
#detail-save.is-on { color: var(--amber); border-color: var(--amber); }
#detail-share.is-done { color: var(--depart); border-color: var(--depart); }
.play {
  margin-left: auto;
  padding: 0.35rem 0.8rem;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ground);
  background: var(--amber);
  transition: transform var(--quick) var(--ease-emphasized), filter var(--quick) var(--ease-emphasized);
}
.play:hover { filter: brightness(1.08); }
.play:active { transform: scale(0.96); }
.rider {
  fill: var(--ink);
  stroke: var(--amber);
  stroke-width: 4;
  pointer-events: none;
}
.route.is-detail { border-color: var(--amber); }
.route.is-detail:hover { transform: none; }

.routes-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: -0.4rem;
}
.routes-title {
  font-size: 0.688rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-right: auto;
}
.card-close.is-inline { position: relative; top: auto; right: auto; flex: none; }

/* --- assistant ------------------------------------------------------------

   A launcher that sits clear of the sheet, and a panel that opens over the map
   the way a station card does — the same glass, the same corner radius, so it
   reads as part of the app rather than as a widget bolted onto it. */
.assistant-launch {
  position: fixed;
  right: 0.9rem;
  bottom: 0.9rem;
  z-index: 35;
  width: 3rem; height: 3rem;
  display: grid;
  place-items: center;
  border-radius: 999px;
  border: 1px solid var(--glass-hairline);
  background: var(--amber);
  color: #17130a;
  box-shadow: var(--shadow), var(--glass-lip);
  transition: transform var(--quick) var(--ease-emphasized),
              filter var(--quick) var(--ease-emphasized);
}
.assistant-launch .i { width: 1.35rem; height: 1.35rem; }
.assistant-launch:hover { filter: brightness(1.08); }
.assistant-launch:active { transform: scale(0.94); }
/* The panel flies out of the button that opened it: it grows from that corner
   rather than appearing whole, so the launcher and the panel read as one thing
   in two states. Closing runs the same move backwards, and the panel is only
   hidden outright once it has finished. */
.assistant {
  position: fixed;
  right: 0.9rem;
  bottom: 0.9rem;
  transform-origin: bottom right;
  transform: scale(0.82) translateY(0.6rem);
  opacity: 0;
  transition: transform var(--normal) var(--ease-emphasized),
              opacity var(--quick) var(--ease-emphasized);
  z-index: 36;
  width: min(23rem, calc(100vw - 1.8rem));
  max-height: min(32rem, calc(100dvh - var(--topbar-height) - 2rem));
  display: flex;
  flex-direction: column;
  background: var(--glass-sheen), var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow), var(--glass-lip);
  overflow: hidden;
}
.assistant.is-open { transform: none; opacity: 1; }
.assistant-launch {
  transition: transform var(--quick) var(--ease-emphasized),
              filter var(--quick) var(--ease-emphasized),
              opacity var(--quick) var(--ease-emphasized);
}
.assistant-launch.is-away { transform: scale(0.6); opacity: 0; pointer-events: none; }

.assistant-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 0.75rem 0.5rem 0.9rem;
}
.assistant-title {
  font-weight: 700;
  margin-right: auto;
}
.assistant-log {
  flex: 1;
  min-height: 6rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.2rem 0.9rem 0.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  scrollbar-width: thin;
}
.assistant-line {
  max-width: 90%;
  padding: 0.5rem 0.7rem;
  border-radius: var(--radius-small);
  background: var(--platform-high);
  border: 1px solid var(--edge);
  font-size: 0.9rem;
  line-height: 1.45;
  white-space: pre-wrap;
}
.assistant-line.is-mine {
  align-self: flex-end;
  background: color-mix(in srgb, var(--amber) 18%, var(--platform-high));
  border-color: color-mix(in srgb, var(--amber) 35%, var(--edge));
}
.assistant-line.is-waiting { color: var(--muted); font-style: italic; }
.assistant-route {
  align-self: flex-start;
  padding: 0.35rem 0.75rem;
  border-radius: 999px;
  border: 1px solid var(--amber);
  color: var(--amber);
  font-size: 0.85rem;
}
.assistant-form {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.7rem calc(0.7rem + env(safe-area-inset-bottom));
  border-top: 1px solid var(--edge);
}
.assistant-form input {
  flex: 1;
  min-width: 0;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-small);
  border: 1px solid var(--edge);
  background: var(--platform-high);
  outline: none;
}
.assistant-form input:focus { border-color: var(--amber); }
.assistant-send {
  width: 2.2rem; height: 2.2rem;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--amber);
  color: #17130a;
}
.assistant-send:disabled { opacity: 0.5; }

/* Voice. Both buttons carry the two states as two glyphs and show one, the way
   the theme switch does, so nothing has to be redrawn to flip them. */
.assistant-mic {
  width: 2.2rem; height: 2.2rem;
  display: grid;
  place-items: center;
  flex: none;
  border-radius: 999px;
  border: 1px solid var(--edge);
  color: var(--muted);
  transition: color var(--quick) var(--ease-emphasized),
              border-color var(--quick) var(--ease-emphasized),
              background var(--quick) var(--ease-emphasized);
}
.assistant-mic:hover { border-color: var(--amber); color: var(--amber); }
.assistant-mic[aria-pressed="true"] {
  color: #17130a;
  background: var(--amber);
  border-color: var(--amber);
  animation: listening 1.4s var(--ease-emphasized) infinite;
}
/* While it is listening the button keeps a soft pulse, which is the only signal
   that the microphone is live. */
@keyframes listening {
  0%, 100% { box-shadow: 0 0 0 0 rgba(245, 183, 46, 0.5); }
  70% { box-shadow: 0 0 0 0.5rem rgba(245, 183, 46, 0); }
}
.i-off { display: none; }
[aria-pressed="true"] .i-off, [aria-pressed="false"] .i-on { display: none; }
[aria-pressed="true"] .i-on, [aria-pressed="false"] .i-off { display: block; }
.assistant-line.is-speaking { border-color: color-mix(in srgb, var(--amber) 45%, var(--edge)); }

/* --- disclosure, key, meta ---------------------------------------------- */

.disclosure { border-top: 1px solid var(--edge); padding-top: 0.6rem; }
.disclosure-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  font-size: 0.688rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.disclosure-caret {
  width: 1rem; height: 1rem;
  transition: transform var(--normal) var(--ease-emphasized);
}
.disclosure-head[aria-expanded="true"] .disclosure-caret { transform: rotate(180deg); }
.disclosure-body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--normal) var(--ease-emphasized);
}
.disclosure-body > * { overflow: hidden; }
.disclosure.is-open .disclosure-body { grid-template-rows: 1fr; }

.line-key { list-style: none; margin: 0.5rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.line-key-item { display: flex; align-items: center; gap: 0.55rem; font-size: 0.85rem; }
.line-bar { width: 1.4rem; height: 0.4rem; border-radius: 999px; flex: none; }
.line-key-item.is-dashed .line-bar {
  background-image: repeating-linear-gradient(90deg, transparent 0 3px, rgba(0, 0, 0, 0.6) 3px 6px);
}
.line-key-item .tag { font-size: 0.62rem; color: var(--muted); letter-spacing: 0.08em; }
.line-avoid {
  margin-left: auto;
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  border: 1px solid var(--edge);
  transition: color var(--quick) var(--ease-emphasized),
              border-color var(--quick) var(--ease-emphasized);
}
.line-avoid.is-on { color: var(--amber); border-color: var(--amber); }

.meta { color: var(--muted); font-size: 0.7rem; margin: 0; }

/* --- station card -------------------------------------------------------- */

.station-card {
  position: fixed;
  z-index: 45;
  right: 1rem;
  top: calc(var(--topbar-height) + 0.6rem);
  width: min(21rem, calc(100vw - 2rem));
  padding: 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--glass-hairline);
  background: var(--glass-sheen), var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--shadow), var(--glass-lip);
  animation: card-in var(--normal) var(--ease-decelerate);
}
@keyframes card-in {
  from { opacity: 0; transform: translateY(-8px) scale(0.98); }
  to { opacity: 1; transform: none; }
}
.station-card h2 { margin: 0; font-size: 1.15rem; }
.station-card h2 .han { font-family: var(--font-han); color: var(--muted); font-weight: 500; margin-left: 0.4rem; font-size: 0.9rem; }
.card-sub { margin: 0.15rem 0 0.6rem; color: var(--muted); font-size: 0.8rem; }
.card-heading {
  margin: 0.9rem 0 0.35rem;
  font-size: 0.688rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.card-close {
  position: absolute;
  top: 0.55rem; right: 0.55rem;
  width: 1.8rem; height: 1.8rem;
  border-radius: 50%;
  transition: background var(--quick) var(--ease-emphasized);
}
.card-close:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }

.card-actions { display: flex; gap: 0.4rem; margin-top: 0.7rem; flex-wrap: wrap; }
.action {
  padding: 0.35rem 0.75rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  border: 1px solid transparent;
  transition: transform var(--quick) var(--ease-emphasized),
              filter var(--quick) var(--ease-emphasized);
}
.action:active { transform: scale(0.96); }
.action.is-depart { background: var(--depart); color: #04140c; }
.action.is-arrive { background: var(--arrive); color: #1c0603; }
.action.is-quiet {
  border-color: var(--edge);
  background: color-mix(in srgb, var(--ink) 4%, transparent);
}
/* brightness() does nothing to a transparent button, so the quiet one gets a
   real background to lift instead. */
.action.is-quiet:hover {
  background: color-mix(in srgb, var(--ink) 12%, transparent);
  border-color: color-mix(in srgb, var(--ink) 30%, transparent);
}
.action.is-quiet.is-on {
  background: color-mix(in srgb, var(--amber) 20%, transparent);
  border-color: var(--amber);
  color: var(--amber);
}
.action:hover { filter: brightness(1.08); }
.action.is-quiet:hover { filter: none; }

.board { display: flex; flex-direction: column; gap: 0.55rem; max-height: 15rem; overflow-y: auto; }
.board-group { display: flex; flex-direction: column; gap: 0.15rem; }
.board-head { display: flex; align-items: center; gap: 0.45rem; font-size: 0.85rem; }
.board-head .name { font-weight: 600; }
.board-head .line-bar { height: 0.8rem; width: 0.3rem; }
.board-head .name { flex: none; }
.board-head .dest {
  color: var(--muted);
  overflow: hidden;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
  /* Bounded so a long destination can never push the line's name onto a second
     line; what will not fit slides past instead of being clipped. */
  max-width: 11rem;
  mask-image: linear-gradient(90deg, #000 0 88%, transparent);
  -webkit-mask-image: linear-gradient(90deg, #000 0 88%, transparent);
}
.board-head .dest .scroll-inner { display: inline-block; white-space: nowrap; }
.board-head .dest.is-scrolling .scroll-inner {
  animation: slide-by var(--scroll-time, 7s) var(--ease-emphasized) infinite alternate;
}
@keyframes slide-by {
  0%, 18% { transform: translateX(0); }
  82%, 100% { transform: translateX(var(--scroll-by, 0)); }
}
.board-times {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding-left: 0.75rem;
  border-left: 2px solid var(--edge);
  margin-left: 0.1rem;
}
.board-train {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--font-board);
  font-size: 0.78rem;
}
.board-train .when { color: var(--amber); white-space: nowrap; min-width: 4.5rem; }
/* A train that is leaving blinks between grey and amber — a hard switch, the way
   a platform indicator does, each row on its own beat. */
@keyframes due-flash {
  0% { color: var(--amber); }
  50% { color: var(--muted); }
  100% { color: var(--amber); }
}
.is-due { animation: due-flash var(--flash-period) steps(1, end) infinite; }
.board-train .when.is-due,
.board-train:not(:first-child) .when.is-due { animation-name: due-flash; }
.board-train .at { color: var(--muted); }
.board-train:not(:first-child) .when { color: var(--muted); }
.muted { color: var(--muted); }
.fatal { padding: 2rem; }

/* --- loading ------------------------------------------------------------- */

/* A placeholder the shape of the thing being waited for. On a fast connection
   this is a blink; on a slow one it is the difference between "still asking" and
   "nothing here". */
.skeleton {
  display: inline-block;
  border-radius: 4px;
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--ink) 7%, transparent) 0%,
    color-mix(in srgb, var(--ink) 15%, transparent) 45%,
    color-mix(in srgb, var(--ink) 7%, transparent) 90%);
  background-size: 220% 100%;
  animation: shimmer 1.4s linear infinite;
}
@keyframes shimmer {
  from { background-position: 120% 0; }
  to { background-position: -120% 0; }
}
.board.is-loading .line-bar { background: color-mix(in srgb, var(--ink) 12%, transparent); }
.skeleton.name-bar { width: 4.5rem; height: 0.7rem; }
.skeleton.dest-bar { width: 6rem; height: 0.7rem; }
.skeleton.when-bar { width: 3rem; height: 0.66rem; }
.skeleton.at-bar { width: 2.2rem; height: 0.66rem; }
.skeleton.wait-bar { width: 8rem; height: 0.66rem; }
/* Every bar in one board pulses together rather than each starting where it was
   built, which is the difference between a loading state and a fairground. */
.board.is-loading .skeleton, .board-line.is-loading .skeleton { animation-delay: 0s; }

/* --- line card ----------------------------------------------------------- */

#line-name { transition: color var(--normal) var(--ease-emphasized); }
.line-stops {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 17rem;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--edge) transparent;
}
.line-stop {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 0.5rem;
}
/* The rail runs behind the whole list, so the stops read as one line rather than
   as a stack of rows. */
.line-stop-rail {
  width: 4px;
  align-self: stretch;
  flex: none;
  margin-left: 0.2rem;
  border-radius: 2px;
}
.line-stop:first-child .line-stop-rail { border-radius: 2px 2px 0 0; margin-top: 0.35rem; }
.line-stop:last-child .line-stop-rail { border-radius: 0 0 2px 2px; margin-bottom: 0.35rem; }
.line-stop-name {
  flex: 1;
  text-align: left;
  font-size: 0.87rem;
  padding: 0.24rem 0.4rem;
  border-radius: var(--radius-small);
  transition: background var(--quick) var(--ease-emphasized);
}
.line-stop-name:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
.line-stop-cross { display: flex; align-items: center; gap: 2px; flex: none; }
.cross-bar { width: 3px; height: 0.75rem; border-radius: 2px; }

.line-key-open {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  flex: 1;
  min-width: 0;
  text-align: left;
  padding: 0.15rem 0.3rem;
  margin-left: -0.3rem;
  border-radius: var(--radius-small);
  transition: background var(--quick) var(--ease-emphasized);
}
.line-key-open:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }

.chip.is-line { border-color: transparent; }
button.chip.is-line { cursor: pointer; }
button.chip.is-line:hover { transform: translateY(-1px); filter: brightness(1.08); }
button.chip.is-line:active { transform: scale(0.96); }
button.chip.is-line:disabled { cursor: default; }

/* --- map ----------------------------------------------------------------- */

.mapview { width: 100%; height: 100%; }
.map {
  width: 100%; height: 100%;
  touch-action: none;
  cursor: grab;
  /* A map is dragged, not read: without this a pan across the diagram sweeps a
     selection through every station name it crosses. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.map text { user-select: none; -webkit-user-select: none; }
.map.is-dragging, .map.dragging { cursor: grabbing; }

.map-live .label {
  fill: var(--ink);
  font-size: 12.5px;
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--label-halo);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.map-live .label-name { font-weight: 700; }
html[lang="zh-Hant"] .map-live .label-name { font-family: var(--font-han); }
.map-live.hide-labels .label { display: none; }
/* Fade rather than vanish: a name that has to give way at one zoom and comes back
   at the next reads as a flicker if it pops. */
.map-live .label { transition: opacity var(--quick) var(--ease-emphasized); }
.map-live .station.crowded .label { opacity: 0; }
.map-live .station.crowded:hover .label,
.map-live .station.crowded.on-route .label { opacity: 1; }

/* Bigger than the dot and invisible, so a station stays tappable at any zoom. */
.map-live .hit { fill: transparent; }

.map-live .dot {
  fill: var(--map-ground);
  stroke: var(--ink);
  stroke-width: 2.6;
  transition: stroke var(--quick) var(--ease-emphasized),
              stroke-width var(--quick) var(--ease-emphasized);
}
.map-live .station.foreign .dot { stroke: color-mix(in srgb, var(--muted) 80%, transparent); }
.map-live .station.foreign .label { fill: var(--muted); }
.map-live .station:hover .dot { stroke: var(--amber); stroke-width: 4; }
.map-live .station:hover .label { display: block; }

.map-plan { background: #f6f4ef; }
.map-plan .hit { fill: transparent; stroke: transparent; stroke-width: 6; }
.map-plan .station:hover .hit { fill: rgba(245, 183, 46, 0.45); }
.map-plan .planned-dot { fill: rgba(120, 120, 120, 0.22); stroke: #8b8b8b; stroke-dasharray: 2 2; }

.map.hide-foreign .station.foreign,
.map.hide-foreign .edge.foreign,
.map.hide-foreign .interchange-link.foreign { display: none; }

/* High speed rail and flights as one switch, stations included: on their own
   they are a few very long lines drawn across everything else. */
.map.hide-fast .edge.high-speed,
.map.hide-fast .station.fast-only { display: none; }

/* Two stations that are one interchange but carry no line between them. */
.interchange-link {
  fill: none;
  stroke: var(--ink);
  stroke-width: 3px;
  stroke-dasharray: 1 5;
  stroke-linecap: round;
  opacity: 0.5;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.map.has-route .interchange-link { opacity: 0.18; }

.station { cursor: pointer; }
.map-live .edge { cursor: pointer; }
.map-live .edge.core, .map-live .route-leg { cursor: default; pointer-events: none; }
.station.is-start .dot, .map-plan .station.is-start .hit { stroke: var(--depart); fill: var(--depart); }
.station.is-end .dot, .map-plan .station.is-end .hit { stroke: var(--arrive); fill: var(--arrive); }
.station.is-avoided .dot, .map-plan .station.is-avoided .hit { stroke: var(--amber); stroke-dasharray: 3 2; }

.edge { transition: opacity var(--normal) var(--ease-emphasized); }
/* Line widths belong to the reader, not to the map: a bar that fattens as you
   zoom in swallows the dots and the names sitting on it. non-scaling-stroke
   pins the width — and the dash pattern — to screen pixels at every zoom. */
.map-live .edge, .map-live .route-leg { vector-effect: non-scaling-stroke; }
.map-live .edge { stroke-width: 6px; }
.map-live .edge.high-speed { stroke-width: 5px; }
.map-live .edge.casing { stroke: var(--map-casing); stroke-width: 10px; }
.map-live .edge.casing.high-speed { stroke-width: 9px; }
.map-live .route-leg { stroke-width: 13px; }
/* A track only trains run one way along is hollowed out, and arrowed so the way
   they run is on the map rather than in a legend. */
.map-live .edge.core { stroke: var(--map-ground); stroke-width: 2px; }
.map-live .edge.high-speed.core { stroke-width: 1.8px; }
.way-arrow { stroke: var(--map-ground); stroke-width: 1; stroke-linejoin: round; }
.way-mark { pointer-events: none; animation: fade-in var(--normal) var(--ease-decelerate); }
.map.has-route .way-mark { opacity: 0.3; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.map.has-route .edge { opacity: 0.22; }
.map.has-route .edge.dim { opacity: 0.07; }
.map-live.has-route .station:not(.on-route) .dot { stroke: color-mix(in srgb, var(--muted) 70%, transparent); }
.map-live.has-route .station:not(.on-route) .label { display: none; }
.map-live.has-route .station.on-route .label { display: block; }
.map-live .station.on-route .dot { stroke-width: 3.4; }
.map-live .station.is-boarding .dot { r: 9px; stroke-width: 4.5; fill: var(--map-ground); }
.map-plan .station.is-boarding .hit { fill: rgba(255, 255, 255, 0.65); stroke: #111; stroke-width: 2; }

/* No drop-shadow here on purpose: a filter on a path this long forces the whole
   route into its own raster layer, and re-rasterising it is most of the cost of
   zooming in on a chosen journey. The casing under each line already separates
   it from what it crosses. */
.route-leg { animation: draw var(--slow) var(--ease-decelerate); }
@keyframes draw { from { opacity: 0; } to { opacity: 1; } }
.route-arrow, .route-pulse { pointer-events: none; }
/* The rider carries a smear stretched along the way it is going, so a fast leg
   reads as fast rather than as a dot being dragged. */
.rider-core { fill: var(--amber); stroke: var(--map-ground); stroke-width: 2; }
.rider-blur { fill: var(--amber); opacity: 0.42; filter: blur(5px); }
.rider { pointer-events: none; }

.marker { fill: none; stroke-width: 3; }
.marker-start { stroke: var(--depart); }
.marker-end { stroke: var(--arrive); }

/* --- mobile -------------------------------------------------------------- */

@media (max-width: 820px) {
  :root { --topbar-height: 3.1rem; }

  .brand-name, .status .status-text { display: none; }
  .topbar { padding: 0 0.6rem; gap: 0.5rem; }

  /* The panel becomes a sheet at one of three heights, dragged by its grip. */
  /* A pill rather than a slab: the sheet floats clear of all four edges, with a
     radius big enough to read as a rounded capsule at every height it stops at. */
  .sheet {
    top: auto;
    left: 0.5rem; right: 0.5rem;
    bottom: calc(0.5rem + env(safe-area-inset-bottom));
    width: auto;
    max-height: 92dvh;
    height: var(--sheet-height, 45dvh);
    border-radius: 1.75rem;
    transition: height var(--slow) var(--ease-emphasized);
  }
  .assistant-launch { bottom: auto; top: calc(var(--topbar-height) + 0.5rem); }
  .assistant {
    right: 0.5rem; left: 0.5rem;
    width: auto;
    bottom: auto;
    top: calc(var(--topbar-height) + 0.5rem);
    border-radius: 1.5rem;
  }
  .sheet.is-dragging { transition: none; }
  .sheet-grip {
    display: block;
    width: 100%;
    padding: 0.65rem 0 0.4rem;
    touch-action: none;
  }
  .sheet-grip span {
    display: block;
    width: 2.5rem; height: 4px;
    margin: 0 auto;
    border-radius: 999px;
    background: var(--edge);
    transition: background var(--quick) var(--ease-emphasized);
  }
  .sheet-grip:active span { background: var(--amber); }
  .sheet-body { padding: 0.4rem 1.1rem 1.2rem; }

  .station-card { right: 0.75rem; left: 0.75rem; width: auto; top: calc(var(--topbar-height) + 0.5rem); }
  .route { padding: 0.85rem 0.9rem 0.85rem 0.65rem; }
  .switch { padding: 0.55rem 0.15rem; }
  .action { padding: 0.5rem 0.9rem; }
  /* In the flow, not floating: a floating list cannot be scrolled inside a
     sheet that is itself scrolling, which is what broke picking on a phone. */
  .suggestions {
    position: static;
    left: auto; right: auto;
    max-height: none;
    margin-top: 0.35rem;
    box-shadow: none;
  }
  .suggestion { padding: 0.7rem 0.65rem; }
  .sheet-body { scroll-padding-bottom: 6rem; }
  .topbar { gap: 0.35rem; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}


/* --- motion -------------------------------------------------------------- */

/* Everything above animates; this turns all of it off at once for anyone whose
   system asks for less motion, rather than leaving it to each rule. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}
