/* ─────────────────────────────────────────────
   TRAVLR — Design Token Layer
   All values sourced from frontendstyle JSON.
   theme-loader.js will override these at runtime
   once multi-theme support is wired up.
───────────────────────────────────────────── */

:root {
  /* sidebar */
  --sidebar-w:     220px;
  --sidebar-w-col: 64px;
  --sidebar-dur:   0.25s ease;

  /* app split — the seam between the content panel and the map. Two widths, stated once,
     because three rules depend on the pair agreeing: the handle's own column, the edge drawn
     inside it, and how far the map reaches back UNDER that edge. The hit area stays wider than
     the edge on purpose — what you aim at is more forgiving than what you see. */
  --split-edge-w:  10px;
  --split-hit-w:   16px;
  /* The corner both the panel and the edge turn. Its own token rather than a step off the radius
     scale, because the two have to match and this is a seam, not a card. Wider than the edge, so
     the edge's own right corners scale to its width and cap it. */
  --split-radius:  14px;

  /* surface — palette.surface */
  --surface-app:      #202020;
  --surface-menu:     #3D3D3D;
  --surface-selector: #353535;

  /* text — palette.text */
  --text-primary: #EEEEEE;
  --text-subtle:  #CCCCCC;

  /* accent — palette.accent */
  --accent-highlight:   #EEEEEE;
  --accent-variable:    #FFFFFF;
  --accent-interactive: #BBBBBB;
  --accent-route:       #EEEEEE;
  --accent-tracking:    #FF7C7C;

  /* extended semantic accents — PALETTE-INVARIANT CONSTANTS.
     These do NOT change with the style switcher (theme.js never overrides them):
     "green = online / red = destructive" must read identically across every
     palette and every external product. Documented in the frontendstyle schema
     as shared constants — intentional, not an un-tokenised oversight. */
  --accent-pro:     #FFB432;   /* gold — Pro/premium */
  --accent-success: #4ade80;   /* green — online / confirmed */
  --accent-warning: #f6c843;   /* yellow — coming soon / caution */
  --accent-info:    #60a5fa;   /* blue — social / informational */
  --accent-danger:  #fc8181;   /* red — destructive / error */

  /* border — palette.border */
  --border-inactive: #696969;

  /* on-accent — text/icon colour on coloured buttons */
  --color-on-accent: #fff;

  /* shadows — shadows */
  --shadow-card:  rgba(0, 0, 0, 0.12);
  --shadow-popup: rgba(0, 0, 0, 0.10);

  /* typography */
  --font-sans: 'Inter', 'Helvetica Neue', Arial, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;

  /* spacing scale */
  /* Panel-header veil — the band BELOW a desktop panel header, not the header itself.
     The header is solid; this begins at its lower edge and falls away to nothing, so
     content scrolling up dissolves into the app surface just before it reaches the bar.
     It is also what the header does to the contour field: the veil IS --surface-app, so
     it darkens the topo on dark palettes and lightens it on light ones, with no theme
     branching — and because it thins downward, the field emerges gradually rather than
     starting at an edge. Applied by :root[data-ui="desktop"] .panel-head::after
     (topo-field.css), which is also where the gradient itself is written: a var() nested
     in a custom property resolves against the element the property is DECLARED on, so a
     token here could not read --panel-head-lift (published on .app-body) and would
     silently take its fallback. Only the tunable length lives here. --panel-head-veil-h
     is the falloff BELOW the bar; --panel-head-lift is the bar's own bottom padding, over
     which the veil is already solid — that is what puts the fully-opaque edge just under
     the title row instead of under the bar. */
  --panel-head-veil-h: 44px;
  /* The foot band: the contour field's run at the bottom of the panel, and the veil it
     sits on. One length for both so they rise and fall together (topo-field.css). */
  --panel-foot-h: 150px;
  /* The top field's run. It is how much artwork is drawn, NOT how much shows: the panel's copy is
     clipped by the header above it and the header's own copy is masked to fade out below the bar,
     so the two share this one length and stay registered with each other. Was written three times
     as a bare 200px — the two copies' box heights and the header overhang — which is what let the
     artwork's SCALE drift with the header height. See topo-field.css. */
  --topo-top-run: 200px;
  /* How much colour the contour field keeps where it thins out. 0.2 = 80% desaturated at
     the far end, full accent where the field is dense (topo-field.css). It rides on the
     STROKES, so only the lines drain — never the surface or the content under them. */
  --topo-desat: 0.2;

  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  20px;
  --space-6:  24px;
  --space-8:  32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-24: 96px;

  /* radius */
  --radius-sm: 6px;
  --radius-md: 12px;
  --radius-lg: 20px;
  --radius-pill: 999px;

  /* scrollbars — slim, neutral, theme-blended (see global rule below).
     Derived from --text-primary-rgb so the thumb stays visible on light palettes
     (a fixed near-white thumb vanished on light backgrounds). */
  --scrollbar-size:        9px;
  --scrollbar-thumb:       rgba(var(--text-primary-rgb, 238, 238, 238), 0.14);
  --scrollbar-thumb-hover: rgba(var(--text-primary-rgb, 238, 238, 238), 0.30);

  /* ── Halftone ────────────────────────────────────────────────────────────────
     The app surface's texture: a staggered dot lattice, the same one travlr.earth
     carries, so the marketing page and the product do not read as two products.

     Two layers offset by half a cell. One layer alone is a square dot grid, which
     still has strong horizontal and vertical rows in it; staggering a second into
     the gaps gives the diagonal packing of a real halftone screen, with no axis to
     read along. That matters more here than on a flat page — this sits behind a
     globe, contour fields and route lines, and a pattern with a direction argues
     with all of them.

     0.9px/1.6px rather than one hard stop: a hard-edged dot this small aliases into
     a square on a 1x display, and the falloff costs nothing and keeps it round.

     Tuned by INK, which is coverage times opacity, not by opacity alone — about
     0.05% of the surface. Light enough to read as a surface rather than a pattern.

     APPLIED PER SURFACE, never to <body>. A viewport-wide overlay would sit over
     the MAP, and a dot screen over satellite imagery is noise, not texture. Only
     the panels and the mobile content backdrop take it; see the two rules that
     use it. */
  --halftone:
    radial-gradient(rgba(var(--border-inactive-rgb, 105, 105, 105), 0.085) 0.9px, transparent 1.6px),
    radial-gradient(rgba(var(--border-inactive-rgb, 105, 105, 105), 0.085) 0.9px, transparent 1.6px);
  --halftone-size: 30px 30px;
  --halftone-pos:  0 0, 15px 15px;

  /* ── Raw RGB channels for alpha-composited colours ──────────────────────────
     Use: rgba(var(--surface-app-rgb, 32, 32, 32), 0.85)
     The fallback inside var() keeps the dark theme working even if a theme
     only overrides the hex token and forgets the -rgb counterpart.         */
  --surface-app-rgb:       32,  32,  32;   /* #202020 */
  --surface-menu-rgb:      61,  61,  61;   /* #3D3D3D */
  --border-inactive-rgb:  105, 105, 105;   /* #696969 */
  --accent-tracking-rgb:  255, 124, 124;   /* #FF7C7C */
  --accent-pro-rgb:       255, 180,  50;   /* #FFB432 */
  --accent-warning-rgb:   246, 200,  67;   /* #f6c843 */
  --accent-info-rgb:       96, 165, 250;   /* #60a5fa */
  --accent-success-rgb:    74, 222, 128;   /* #4ade80 */
  --accent-danger-rgb:    252, 129, 129;   /* #fc8181 */
  --accent-remove:        #FF3B3B;         /* vivid red — delete / map hazard */
  --accent-remove-rgb:    255,  59,  59;   /* #FF3B3B */
  --color-muted-rgb:      160, 174, 192;   /* cool blue-gray used for dim text */
  --text-primary-rgb:     238, 238, 238;   /* #EEEEEE */
  --text-subtle-rgb:      204, 204, 204;   /* #CCCCCC */

  /* ── Map categorical palette — PER-STYLE, minted by theme.js ─────────────────
     Multi-colour map features (Seasons categories, Empire polygons, diary route
     transport modes) key onto these 12 slots. theme.js applyStyle() overrides
     them per palette (hue-rotations of the style's tracking accent — see
     _catColors); the values below are the boot defaults for the default
     `satelite` style, generated by the SAME algorithm. cat-1 is always the
     tracking accent itself. GL layers resolve these live via map-colors.js. */
  /* Map overlay chip — the contrast pair for elements floating ON the basemap
     (markers, clusters). chip = the style's ink, on-chip = its surface; minted
     per palette by theme.js alongside the categorical slots below. */
  --map-chip:        #EEEEEE;  --map-chip-rgb:    238, 238, 238;
  --map-on-chip:     #202020;  --map-on-chip-rgb: 32, 32, 32;
  /* Globe atmosphere glow (three.js, space-background.js) — boot default = the
     satellite palette's Earth-blue; theme.js re-derives per style at runtime. */
  --atmosphere-color: #4D94FF;  --atmosphere-color-rgb: 77, 148, 255;
  --atmosphere-intensity: 1;
  /* Starfield sky (space-background.js): far = deep space, horizon = tint at the limb,
     star = star tint, opacity mode-scaled. Boot default = satellite (dark). */
  --space-color: #0A0F17;   --space-color-rgb: 10, 15, 23;
  --space-horizon: #12203A; --space-horizon-rgb: 18, 32, 58;
  --space-star-color: #EAF0FF; --space-star-color-rgb: 234, 240, 255;
  --space-star-opacity: 0.9;
  --map-cat-1:  #FF7C7C;  --map-cat-1-rgb:  255, 124, 124;
  --map-cat-2:  #7CFFBE;  --map-cat-2-rgb:  124, 255, 190;
  --map-cat-3:  #E596FF;  --map-cat-3-rgb:  229, 150, 255;
  --map-cat-4:  #FFFF62;  --map-cat-4-rgb:  255, 255, 98;
  --map-cat-5:  #7CBDFF;  --map-cat-5-rgb:  124, 189, 255;
  --map-cat-6:  #FF96CA;  --map-cat-6-rgb:  255, 150, 202;
  --map-cat-7:  #8AFF62;  --map-cat-7-rgb:  138, 255, 98;
  --map-cat-8:  #9D7CFF;  --map-cat-8-rgb:  157, 124, 255;
  --map-cat-9:  #FFCA96;  --map-cat-9-rgb:  255, 202, 150;
  --map-cat-10: #62FFFF;  --map-cat-10-rgb: 98, 255, 255;
  --map-cat-11: #FF7CFF;  --map-cat-11-rgb: 255, 124, 255;
  --map-cat-12: #E5FF96;  --map-cat-12-rgb: 229, 255, 150;

  /* ── Generic black / white — structural overlays, shadows, glass tints ───────
     Every white/black literal in the app routes through these so a future theme
     can retint glass + shadows. -rgb channels for alpha compositing.          */
  --color-white:     #fff;
  --color-white-rgb: 255, 255, 255;
  --color-black:     #000;
  --color-black-rgb: 0, 0, 0;

  /* ── Decorative & feature palette — PALETTE-INVARIANT CONSTANTS ───────────────
     One-off art colours (dataset-badge gradients, space/navy backdrops, theme +
     map-style preview swatches, live-map legend dots, feature accents). These are
     CONSTANT across every style (the switcher never overrides them) — feature art
     like the space/plan hero and dataset badges reads the same on every palette,
     and the values are shared with external products via the frontendstyle schema.
     Tokenised for a single source of truth, not for per-palette theming. */
  /* deep backdrops (alpha-composited) */
  --backdrop-navy-rgb:        13, 21, 32;
  --backdrop-space-rgb:        4,  8, 15;
  --backdrop-space-deep-rgb:   4,  8, 20;
  --backdrop-abyss-rgb:        8, 13, 20;
  --glow-mint-rgb:           104, 211, 145;
  /* navy hero / banner gradient (profile) + stats */
  --banner-navy-1: #0d1a2e;
  --banner-navy-2: #0a1628;
  --banner-navy-3: #0d2240;
  --banner-navy-4: #101828;
  --stats-navy:    #1a1a2e;
  /* Pro badge (gold gradient + ink) */
  --pro-badge-ink:    #1a1a1a;
  --accent-pro-deep:  #e8a020;
  --accent-pro-light: #ffb27c;
  /* feature accents */
  --accent-gold:      #facc15;  --accent-gold-rgb: 250, 204, 21;
  --accent-sky:       #7cc4ff;  --accent-sky-rgb:  124, 196, 255;
  --accent-red-soft:  #f87171;
  --accent-red-bright:#ff6b6b;
  /* plan (space hero + violets + amber + ink) */
  --plan-space-1: #1a1f4e;
  --plan-space-2: #0d1030;
  --plan-space-3: #1a0d30;
  --plan-ink:     #111;
  --plan-violet:  #c084fc;
  --plan-amber:   #fbbf24;  --plan-amber-rgb: 251, 191, 36;
  --plan-violet-a-rgb:    120, 80, 255;
  --plan-violet-b-rgb:    130, 80, 255;
  --plan-violet-soft-rgb: 180, 160, 255;
  --plan-violet-pale-rgb: 200, 180, 255;
  /* live-map legend (data-viz) */
  --legend-quake:   #FFC107;
  --legend-disease: #B57BEE;
  --legend-volcano: #FF6B35;
  /* settings theme + map-style preview swatches */
  --swatch-dark:      #0d1520;
  --swatch-light:     #f7fafc;
  --swatch-streets-1: #1a2030;
  --swatch-streets-2: #263040;
  --swatch-sat-1:     #1a3a1a;
  --swatch-sat-2:     #2d5a2d;
  --swatch-sat-3:     #0d2a0d;
  --swatch-mapdark-1: #080808;
  --swatch-mapdark-2: #141414;
  --swatch-topo-1:    #2a3520;
  --swatch-topo-2:    #3a4a28;
  --swatch-topo-3:    #4a5a30;
  /* explore dataset-badge gradients (from / to) */
  --ds-nature-1:   #1a4030;  --ds-nature-2:   #0c2318;
  --ds-battles-1:  #3a1a1a;  --ds-battles-2:  #1f0909;
  --ds-food-1:     #3d2a14;  --ds-food-2:     #201508;
  --ds-film-1:     #1a1a3a;  --ds-film-2:     #0a0a22;
  --ds-default-1:  #2a2a3a;  --ds-default-2:  #14141f;
  --ds-sports-1:   #14323d;  --ds-sports-2:   #08181f;
  --ds-history-1:  #3a2a14;  --ds-history-2:  #1f1508;
  --ds-heritage-1: #3a2f14;  --ds-heritage-2: #1f1908;
  --ds-arch-1:     #2f2f33;  --ds-arch-2:     #161618;
  --ds-music-1:    #33143a;  --ds-music-2:    #190820;
  --ds-culture-1:  #2a1a3a;  --ds-culture-2:  #150a22;
  --ds-science-1:  #142a3d;  --ds-science-2:  #08151f;
  --ds-games-1:    #143d2a;  --ds-games-2:    #081f15;
  --ds-travel-2:   #220a0a;
}

/* ─── Reset ─── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { scroll-behavior: smooth; }

body {
  background-color: var(--surface-app, #202020);
  color: var(--text-primary, #EEEEEE);
  font-family: var(--font-sans, 'Inter', 'Helvetica Neue', Arial, sans-serif);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }

/* ─── Scrollbars ───────────────────────────────────────────────────────────────
   One slim, minimal style for every scroll container. The thumb is a low-opacity
   neutral that blends into the dark surface and only firms up on hover; the track
   is invisible. A 2px transparent border + background-clip keeps the visible thumb
   ~4–5px so it reads as a hairline, not a bar. Elements that intentionally hide
   their scrollbar (horizontal pill/badge strips) override this with their own
   `scrollbar-width: none` / `::-webkit-scrollbar { display: none }` — class
   specificity wins, so those stay hidden. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}
*::-webkit-scrollbar { width: var(--scrollbar-size); height: var(--scrollbar-size); }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border: 2px solid transparent;
  background-clip: padding-box;
  border-radius: var(--radius-pill);
}
*::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover);
  background-clip: padding-box;
}
*::-webkit-scrollbar-corner { background: transparent; }

/* ─── Skeleton loaders ───────────────────────────────────────────────────────
   Shared base for every "shape of the real content, painted before the fetch
   resolves" placeholder across the app. Surfaces size/shape their own pieces
   (avatar circle, text line, image rect) by combining this with a local
   modifier class, e.g. `class="tlr-skel tlr-skel-circle cm-skel-av"`. */
.tlr-skel {
  display: block;
  background: rgba(var(--color-white-rgb, 255, 255, 255), 0.07);
  border-radius: var(--radius-sm, 6px);
  animation: tlr-skel-pulse 1.4s ease-in-out infinite;
}
.tlr-skel-circle { border-radius: 50%; }
@keyframes tlr-skel-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .tlr-skel { animation: none; } }

/* ─── Flags (assets/flags/<ISO>.webp, native 3:2) ─── */
.flag {
  display: inline-block;
  width: 1.35em;
  height: 0.9em;
  object-fit: cover;
  border-radius: 2px;
  vertical-align: -0.13em;
  margin-right: 0.42em;
  flex-shrink: 0;
  box-shadow: 0 0 0 0.5px rgba(var(--color-white-rgb, 255, 255, 255), 0.2), 0 1px 2px rgba(var(--color-black-rgb, 0, 0, 0), 0.35);
}
.flag--lg { width: 1.6em; height: 1.07em; }

/* Nepal — the only non-rectangular national flag, and the only asset that isn't 3:2. Its file is the
   true double-pennon with alpha (320×394, PORTRAIT) while every other flag, Switzerland and the
   Vatican included, is normalised to 384×256. Dropped into any of the fixed 3:2 boxes above with
   `object-fit: cover` it crops to a horizontal sliver of red — unrecognisable as Nepal.
   Match the box HEIGHT its neighbours use and let the width follow, so it reads correctly and rows
   stay aligned. The rounded corners and rim go too: they'd frame the transparent corners of a shape
   that isn't a rectangle.
   !important because this is a shape exception that has to beat every per-surface sizing rule
   (.pf-country-flag .flag, #mc-own-profile … .flag, .mp-flag, and any added later); the alternative
   is patching each one and re-patching every new one. Keyed off the asset path, so it applies
   wherever a flag is rendered without each call site having to know Nepal is special. */
img[src$="/NP.webp"] {
  object-fit: contain !important;
  width: auto !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  filter: drop-shadow(0 1px 2px rgba(var(--color-black-rgb, 0, 0, 0), 0.35));
}

/* ─── Set artwork (Wikimedia logomarks; scripts/canon/set-art.js) ──────────────
   A FILM'S MARK IS AN ICON AND TAKES THE PALETTE LIKE ONE. That is the whole reason the artwork
   is a monochrome logomark rather than a poster: one colour re-inks, a picture does not. Every
   glyph in assets/icons/collections is `stroke="currentColor"`; these sit in the same rails and
   have to answer the style switcher the same way, or the row has one foreign object in it.

   The ink comes from #set-art-ink, the SVG filter defined at the end of index.html: it floods the
   palette's --text-primary and clips it to the artwork's ALPHA. What this replaced,
   `brightness(0) invert(var(--set-art-invert, 1))`, could only reach black or white, and
   --set-art-invert was never declared anywhere in the app — so every mark took the fallback 1 and
   came out pure white: right on the dark palettes by luck, invisible on the light ones. A filter
   also reaches the tinted inks a switch cannot, like Emerald's #C8F4E8.

   A FILTER RATHER THAN A MASK BECAUSE OF CORS — a cross-origin mask image must pass a CORS check
   on every redirect hop and Special:FilePath's hops send no Access-Control-Allow-Origin, so a
   masked mark never paints at all. The note in index.html has the measurement.

   Per-surface geometry (size, fit, fade) stays with each surface; this is only the ink. */
.set-art-mark[data-art-paint="ink"] { filter: url(#set-art-ink); }
/* Two fills: the silhouette in ink, the bright face the artwork drew in the tracking accent.
   Opt-in per artwork — see the filter in index.html for why it cannot be derived from tone. */
.set-art-mark[data-art-paint="duo"] { filter: url(#set-art-ink-duo); }

/* ─── Nav ─── */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-8);
  background: rgba(var(--surface-app-rgb, 32, 32, 32), 0.72);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid rgba(var(--border-inactive-rgb, 105, 105, 105), 0.3);
}

.nav-logo {
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--accent-variable, #FFFFFF);
}

.nav-link--active { color: var(--text-primary, #EEEEEE) !important; }

.nav-cta {
  font-size: 0.875rem;
  font-weight: 600;
  padding: var(--space-2) var(--space-4);
  background: var(--accent-variable, #FFFFFF);
  color: var(--surface-app, #202020);
  border-radius: var(--radius-pill);
  transition: opacity 0.2s;
}
.nav-cta:hover { opacity: 0.85; }

/* Pre-collapsed state applied via inline <head> script before first render */
:root[data-sidebar-pre-collapsed] .sidebar  { width: var(--sidebar-w-col); }
:root[data-sidebar-pre-collapsed] .app-body { margin-left: var(--sidebar-w-col); }

/* ─── Sidebar ──────────────────────────────────────────────────────────────────
   Collapsed is the designed base: items are centered icon-only.
   Expanded (.sidebar:not(.collapsed)) layers in labels, gap, and left-alignment.
────────────────────────────────────────────────────────────────────────────── */

.sidebar {
  position: fixed;
  left: 0; top: 0; bottom: 0;
  width: var(--sidebar-w);
  background: var(--surface-menu, #3D3D3D);
  border-right: 1px solid rgba(var(--border-inactive-rgb, 105, 105, 105), 0.2);
  display: flex;
  flex-direction: column;
  z-index: 200;
  transition: width var(--sidebar-dur);
  overflow: hidden;
}
.sidebar.collapsed { width: var(--sidebar-w-col); }
.sidebar.nav-locked .sidebar-item { pointer-events: none; }

/* ── Header — static layout; only toggle and logo crossfade animate ─────────── */
/* Padding never changes so the brand mark's position is fixed throughout transition. */

.sidebar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 10px 18px 14px;
  gap: 6px;
  min-height: 60px;
  flex-shrink: 0;
}

/* Logo container always flex:1 — its left edge never moves */
.sidebar-logo {
  flex: 1;
  min-width: 0;
  position: relative;
  height: 34px;
  overflow: visible;
  text-decoration: none;
}

/* The brand mark — one animated object, not two crossfading images.
   components/brand-logo.js builds an inline SVG here and drives it between the badge and the
   wordmark as the sidebar collapses; it sizes the svg itself, so this only has to place it.

   currentColor rather than a fixed fill, for the same reason the masks it replaces used a
   token: the mark follows the active palette's --text-primary like every nav icon.

   Anchored left, not centred. The old badge was centred in a slot that is `flex: 1` and so
   changes width during the transition — which slid the mark sideways while it was already
   morphing. Held at a fixed left it only does the one motion. */
.sidebar-brand {
  position: absolute;
  left: 5px; top: 50%;
  transform: translateY(-50%);
  display: block;
  /* width: max-content is LOAD-BEARING, and its absence was a bug rather than a style choice.
     Absolutely positioned with a left and no width, this box shrink-to-fits and is capped by its
     containing block — and .sidebar-logo is only 23px wide in the collapsed rail, so the cap was
     23 - 5 = 18px. Measured at HEAD: brand-logo.js asked for 34x34 and the browser rendered 18 wide
     by 34 tall, letterboxing the mark down to 16.1px of visible ink — SMALLER than the 20px nav
     glyphs beside it. The badge had never once drawn at its authored size. With the cap gone, the
     h in brand-logo.js is what actually reaches the screen. */
  width: max-content;
  color: var(--text-primary, #EEEEEE);
  pointer-events: none;
  /* The collapsed anchor below moves on the sidebar's own clock, so the slide happens WITH the
     badge-to-wordmark morph (which sidebar.js drives for the same duration) instead of jumping the
     moment the class flips. */
  transition: left var(--sidebar-dur);
}
/* Collapsed, the mark is a square badge in a 64px rail and should sit on the same vertical line as
   the buttons under it — measured centred at 31.5px (8 .sidebar-nav pad + 14 .sidebar-item pad + 10
   half-glyph). left is relative to .sidebar-logo, whose edge is 14px in, so a square of side h
   centres there at 31.5 - h/2 - 14; at the authored h 34 that is 0.5.

   Scoped, not shared: expanded, the mark is the 2.7x-wider wordmark and wants the text margin it
   has always had (left: 5px above), not the badge's centring. One value cannot do both. */
.sidebar.collapsed .sidebar-brand { left: 0.5px; }
.sidebar-brand svg { display: block; overflow: visible; }

/* Toggle — visible in expanded, collapses out */
.sidebar-toggle {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--text-subtle, #CCCCCC);
  cursor: pointer;
  padding: 5px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, color 0.15s, opacity 0.15s, width 0.25s, padding 0.25s;
}
.sidebar-toggle:hover { background: var(--surface-selector, #353535); color: var(--text-primary, #EEEEEE); }
.sidebar.collapsed .sidebar-toggle {
  width: 0;
  padding: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
}

.toggle-arrow { transition: transform var(--sidebar-dur); }

/* ── Nav ─────────────────────────────────────────────────────────────────────── */

.sidebar-nav {
  flex: 1;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
}

/* Layout never changes between states — only the sidebar's outer width animates.
   - Item padding is constant: 14px left/right keeps the icon's center pinned at
     22+10 = 32px from the sidebar's left edge, in BOTH states (collapsed 64px →
     icon centred at 32px = exact horizontal centre; expanded → same x-position).
   - Gap is constant: no flex re-layout to chase. The label simply sits past the
     viewport edge once the sidebar narrows; `.sidebar { overflow: hidden }` clips it.
   - Only `opacity` animates the label, so text fades — no max-width box-clip = no
     hard-cut artifact. */
.sidebar-item {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  color: var(--text-subtle, #CCCCCC);
  font-size: 0.875rem;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  min-height: 44px;
  transition: background 0.15s, color 0.15s;
}
.sidebar-item:hover   { background: var(--surface-selector, #353535); color: var(--text-primary, #EEEEEE); }
.sidebar-item--active { background: var(--surface-selector, #353535); color: var(--text-primary, #EEEEEE); }
.sidebar-item--active .sidebar-icon { color: var(--accent-tracking, #FF7C7C); }

/* EXEMPT FROM THE GLOBAL `img, svg { max-width: 100% }` ABOVE, because in the collapsed rail there
   is exactly one pixel less room than the icon needs, and that cap spent it by squashing the icon
   to 19px wide while leaving it 20 tall. A one-pixel horizontal squash on a 20px glyph is not
   subtle — it is a stroke width — and it only happened in one of the two states, so the icons
   flinched every time the rail closed.

   Where the pixel goes: --sidebar-w-col is 64px and .sidebar is border-box with a 1px right border,
   so the content is 63 → minus .sidebar-nav's 8+8 → 47 → minus .sidebar-item's 14+14 → 19. The
   comment above works in the 64 the token names; the border quietly takes one of them.

   flex-shrink: 0 does not cover this. It governs the flex algorithm, and max-width is applied after
   it — the item was never shrunk, it was capped.

   Letting it overflow that box by a pixel is not a workaround, it is what the geometry above already
   describes: 8 + 14 + 10 puts the glyph's centre at 32px, the exact centre of a 64px rail, and it
   only lands there if the glyph is its full 20. The overflow runs into the gap before the label,
   which the rail clips anyway. */
.sidebar-icon { flex-shrink: 0; max-width: none; }

/* Label fade — same duration as the sidebar's width animation so the two move
   together: as the sidebar narrows from 220→64, the label fades 1→0 in parallel.
   The label gets progressively clipped by the narrowing sidebar's overflow:hidden,
   but because its opacity is dropping in lockstep, the visual is a smooth fade,
   never a hard cut.
   On expand: sidebar widens first; label fade-in is delayed slightly so the text
   doesn't pop in before there's room for it. */
.sidebar-label {
  white-space: nowrap;
  opacity: 1;
  transition: opacity 0.22s ease 0.08s;
}
.sidebar.collapsed .sidebar-label {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease;
}

/* ── Footer ─────────────────────────────────────────────────────────────────── */

.sidebar-footer {
  padding: 12px 8px;
  border-top: 1px solid rgba(var(--border-inactive-rgb, 105, 105, 105), 0.15);
  flex-shrink: 0;
}

/* Avatar position is fixed — 8+8 padding/margin maths put the 32px avatar's centre
   at the same x as the sidebar-item icons (32px from sidebar's left edge in both
   collapsed and expanded states). Gap is constant — no shift. */
.sidebar-account {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  padding: 8px;
  border-radius: var(--radius-md);
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  min-height: 44px;
  transition: background 0.15s;
}
.sidebar-account:hover { background: var(--surface-selector, #353535); }

/* Settings lives in the footer (below the account, separate from the mode nav). */
.sidebar-item--footer { min-height: 40px; margin-top: 2px; }

.account-avatar {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--accent-tracking, #FF7C7C);
  color: var(--color-on-accent, #fff);
  font-size: 0.8125rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  letter-spacing: 0.03em;
  overflow: hidden;
}

/* Holding the default astronaut avatar, which brings its own disc — the accent fill underneath it
   would only show as a rim of the wrong colour.

   AND IT STOPS CROPPING. The chip clips to a circle for a photo, which is right for a photo; the
   astronaut's sign-in badge hangs off the bottom of its disc on purpose, and this crop is the one
   thing between it and being visible. Only for the default face — a real avatar still gets clipped. */
.account-avatar[data-default] { background: none; overflow: visible; }

/* The badge is for the COLLAPSED rail, where the disc is the entire control and nothing else on it
   says what pressing it does. Expanded, the chip's own name reads "Sign in" two pixels to the
   right, and a pill repeating it is the same word twice — so it fades out and leaves the label to
   do the talking. Opacity, not display: the sidebar animates between the two states, and a badge
   that vanished on the first frame would pop rather than hand over. */
body:not(.sidebar-collapsed) .account-avatar .astro-badge { opacity: 0; }
.account-avatar > img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }

/* Name — same opacity-only fade as .sidebar-label */
.account-name {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-subtle, #CCCCCC);
  white-space: nowrap;
  opacity: 1;
  transition: opacity 0.22s ease 0.08s;
}
.sidebar.collapsed .account-name {
  opacity: 0;
  transition: opacity 0.22s ease;
}

/* Legal links: visible in expanded, hidden when collapsed */
.sidebar-legal {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px 2px;
  overflow: hidden;
  opacity: 1;
  transition: opacity var(--sidebar-dur);
}
.sidebar.collapsed .sidebar-legal {
  opacity: 0;
  pointer-events: none;
}
.sidebar-legal a,
.sidebar-legal-btn {
  font-size: 10px;
  color: rgba(var(--color-muted-rgb, 160, 174, 192), 0.38);
  text-decoration: none;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
  transition: color 0.15s;
  white-space: nowrap;
}
.sidebar-legal a:hover,
.sidebar-legal-btn:hover { color: rgba(var(--color-muted-rgb, 160, 174, 192), 0.75); }
.sidebar-legal-dot { font-size: 10px; color: rgba(var(--color-muted-rgb, 160, 174, 192), 0.22); }

/* ─── Sidebar upgrade button ─── */
.sidebar-pro-btn {
  display: flex;
  align-items: center;
  justify-content: flex-start;   /* expanded: "Upgrade to Pro" left-aligned */
  padding: 8px 10px;
  margin: 0 0 6px;
  border-radius: 8px;
  border: 1px solid rgba(var(--accent-tracking-rgb, 255, 124, 124), 0.22);
  background: rgba(var(--accent-tracking-rgb, 255, 124, 124), 0.06);
  color: var(--accent-tracking, #FF7C7C);
  font-size: 12.5px; font-weight: 600; font-family: var(--font-sans, 'Inter', 'Helvetica Neue', Arial, sans-serif);
  cursor: pointer;
  text-decoration: none;
  transition: background 0.14s, border-color 0.14s;
  width: 100%;
  overflow: hidden;
}
.sidebar-pro-btn:hover { background: rgba(var(--accent-tracking-rgb, 255, 124, 124), 0.12); border-color: rgba(var(--accent-tracking-rgb, 255, 124, 124), 0.40); }
/* Expanded → "Upgrade to Pro" (left, indented to line up with the nav labels).
   Collapsed → just "Pro" (centered). */
.sidebar-pro-label { white-space: nowrap; margin-left: 35px; }
.sidebar-pro-mark { display: none; font-weight: 700; }
.sidebar.collapsed .sidebar-pro-btn { justify-content: center; padding-left: 0; padding-right: 0; }
.sidebar.collapsed .sidebar-pro-label { display: none; }
.sidebar.collapsed .sidebar-pro-mark  { display: inline; }

/* ─── App body — contains view-panel, resize handle, and map panel ─── */
.app-body {
  display: flex;
  height: 100vh;
  overflow: hidden;
  margin-left: var(--sidebar-w);
  transition: margin-left var(--sidebar-dur);
  /* Containing block for the panel's edge shading (::after, below). Not a stacking context —
     position:relative with z-index:auto is not one — so the children's z-indexes still resolve
     against each other exactly as they did. */
  position: relative;
}
body.sidebar-collapsed .app-body { margin-left: var(--sidebar-w-col); }

/* Home mode: no map, view-panel fills full width. No seam to be the edge of, so it takes neither
   the corner nor the shading — see the desktop-only block below, which excludes it by selector. */
.app-body.no-map .view-panel  { flex: 1; overflow-y: auto; height: 100vh; }
.app-body.no-map .app-resize,
.app-body.no-map .map-panel   { display: none; }

/* View panel — left side, width controlled by app-level resize handle */
.view-panel {
  flex: 0 0 var(--view-w, 520px);
  min-width: 280px;
  overflow-y: auto;
  height: 100%;
  position: relative;

  /* The panel's texture. Painted as the element's own BACKGROUND rather than a
     pseudo-element, which puts it at the very back by construction: a positioned
     descendant at z-index -1 still paints above an element's background, so the
     contour fields keep sitting on top of this and the view's content on top of
     them, with nothing to order by hand.

     No background-COLOUR here — the dots sit on whatever the body is showing, so a
     style switch recolours the surface underneath them for free.

     Default background-attachment means it stays put while the panel scrolls,
     which is what the fixed overlay did on travlr.earth. */
  background-image:    var(--halftone);
  background-size:     var(--halftone-size);
  background-position: var(--halftone-pos);
  /* Makes the panel a stacking context, which the decorative contour fields inside it
     depend on: they sit at z-index -1 so the view's content paints over them, and without
     a stacking context here that would drop them behind the panel entirely rather than
     behind its content. See components/panel-topo.js. */
  z-index: 1;
}

/* ── The panel's edge treatment — DESKTOP, and only where there is a seam ─────────────────────
   Excluded on mobile (#view is a full-bleed surface there: a rounded right side would cut two
   corners out of the screen) and in no-map (the panel IS the app body). */
:root[data-ui="desktop"] .app-body:not(.no-map) .view-panel {
  /* Rounded on the MAP side, square against the sidebar — the same corner the edge turns, so the
     panel and the strip beside it read as one slab rather than two neighbours. The panel carries
     no background COLOUR, so this clips the halftone, the content, and the shading below. */
  border-radius: 0 var(--split-radius, 14px) var(--split-radius, 14px) 0;
}

/* The inward shading. OVER THE WHOLE PANEL, from outside it.
   
   It has now been in three places, and the two it left were both blocked by something the panel
   draws over itself:
   
   · as an inset box-shadow on .view-panel it painted with the panel's BACKGROUND, and a background
     is below every descendant an element has — so the contour field at z-index -1 was drawn
     straight over it and the shading survived only in the gaps.
   · as a layer inside the panel at z-index -1 it cleared the field and the halftone, but not the
     SCRIMS: the header is a solid bar and the foot veil sits at z-index 2 with the foot field at 3,
     all of them necessarily above content because their job is to fade content out. The top and
     bottom of the panel therefore had no shading at all.
   
   There is no slot that is above those scrims and below the content, because they are above the
   content by design. So the shading stops trying to live inside the panel and lies over it, as a
   pseudo-element of .app-body: nothing #view draws can reach it, it needs no sticky pinning because
   app-body does not scroll, and it covers the panel edge to edge — header, foot and all. It stays
   under .app-resize (z-index 10), so the edge beside it is never dimmed, and it is
   pointer-events:none, so nothing under it stops being clickable.
   
   The cost, and it is deliberate: it is over the panel's CONTENT too. At ~24px of reach against a
   panel that pads its content further in than that, what it actually touches is card edges — which
   is the vignette that makes the panel read as a surface lying ON the app rather than a region cut
   out of it. That was the point of it.
   
   Each edge is one inset shadow whose negative spread outruns its offset, so the darkness starts at
   the edge, carries a few solid px, and falls away over the blur. Nothing reaches the top or bottom
   edges, where a plain inset box would have drawn a frame. */
:root[data-ui="desktop"] .app-body:not(.no-map)::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: var(--view-w, 520px);
  z-index: 2;              /* over .view-panel (1) and the map, under .app-resize (10) */
  border-radius: 0 var(--split-radius, 14px) var(--split-radius, 14px) 0;
  pointer-events: none;
  box-shadow:
    inset  30px 0 40px -26px rgba(var(--color-black-rgb, 0, 0, 0), 0.21),
    inset -30px 0 40px -26px rgba(var(--color-black-rgb, 0, 0, 0), 0.21);
}

/* App-level resize handle */
.app-resize {
  flex-shrink: 0;
  width: var(--split-hit-w, 16px);
  cursor: col-resize;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The grab area runs past the edge on the MAP side only — the panel side is already the panel.
     Padding rather than a wider ::before, so the flex centring below still centres the grip on the
     EDGE (content box = the edge) instead of on the hit area. */
  padding-right: calc(var(--split-hit-w, 16px) - var(--split-edge-w, 12px));
  background: transparent;
  position: relative;   /* anchors the edge below */
  z-index: 10;          /* over the map, which now reaches back underneath this whole column */
  touch-action: none;   /* drag resizes instead of scrolling/zooming the page */
}

/* The panel's edge: a slim rounded strip down the seam, in --surface-menu — the same
   surface the sidebar and the rail cards are built from, so the panel is bounded by the
   app's own chrome rather than by a line invented for the purpose.

   It earns its place twice. The panel surface and the app background sit close in tone, so
   without it the boundary only reads where content happens to reach it; and it gives the
   drag indicator something to live IN, which is what makes the seam legible as a control
   rather than as a gap. The indicator itself is unchanged — it sits on this, not instead
   of it.

   Full height, and rounded only on the MAP side. It is the panel's edge, so it is flush
   where it meets the panel and turns away from it — square left, curved right. Rounding
   both sides would have made it a floating divider between two things instead of the
   boundary of one of them.

   Narrower than the 12px hit area on purpose: what you aim at stays more forgiving than what you
   see. The two are set together — widening the edge without the hit area would invert that, and an
   edge wider than its own handle would overflow the seam it is drawing. */
.app-resize::before {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  /* FLUSH with the panel, not centred in the column. It is the panel's edge, so there is nothing
     for it to be centred in — a centred strip left a hairline of app background between the two,
     which is exactly the gap that made the seam read as a divider instead of a boundary. */
  left: 0;
  width: var(--split-edge-w, 12px);
  border-radius: 0 var(--split-radius, 14px) var(--split-radius, 14px) 0;
  background: var(--surface-menu, #3D3D3D);
}
/* The grip, riding on the edge above. Deliberately still a short centred mark: the edge
   is what separates, this is only what says the edge moves. z-index lifts it clear of the
   ::before it sits on. */
.app-resize-bar {
  position: relative;
  z-index: 1;
  width: 2px;
  height: 48px;
  border-radius: 2px;
  background: var(--text-subtle, #CCCCCC);
  opacity: 0.3;
  transition: opacity 0.15s, height 0.15s;
}
.app-resize:hover .app-resize-bar,
.app-resize.is-dragging .app-resize-bar {
  opacity: 1;
  height: 64px;
  background: var(--accent-tracking, #FF7C7C);
}

/* Map panel — right side, fills remaining space */
.map-panel {
  flex: 1;
  position: relative;
  min-width: 300px;
  overflow: hidden;
}
/* The map reaches BACK under the whole handle column, so the edge is a strip lying on the globe
   rather than a wall between two panes. Without it the edge's rounded corners cut two notches of
   flat app background out of the seam, which is the one place the rounding is meant to show the
   map through. The edge stays on top by its z-index, and so does the grab area.
   Desktop-scoped: on mobile .map-panel is a fixed full-bleed layer and has no seam to hide under. */
[data-ui="desktop"] .map-panel { margin-left: calc(-1 * var(--split-hit-w, 16px)); }
#map {
  position: absolute;
  inset: 0;
}

/* ─── Hero ─── */
.hero {
  min-height: 100vh;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: var(--space-12);
  padding: 120px var(--space-8) var(--space-16);
  max-width: 1200px;
  margin: 0 auto;
  position: relative;
  overflow: visible;
}

@keyframes float {
  0%, 100% { transform: translateY(0); }
  50%       { transform: translateY(-14px); }
}

.hero-eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent-tracking, #FF7C7C);
  margin-bottom: var(--space-4);
}

.hero-title {
  font-size: clamp(2.75rem, 8vw, 6rem);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--accent-variable, #FFFFFF);
  margin-bottom: var(--space-6);
}

.hero-title span {
  color: var(--accent-tracking, #FF7C7C);
}

.hero-sub {
  max-width: 520px;
  font-size: clamp(1rem, 2vw, 1.2rem);
  color: var(--text-subtle, #CCCCCC);
  margin-bottom: var(--space-8);
}

.hero-actions {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  justify-content: flex-start;
}

.btn-primary {
  padding: var(--space-3) var(--space-8);
  background: var(--accent-variable, #FFFFFF);
  color: var(--surface-app, #202020);
  font-size: 0.9375rem;
  font-weight: 700;
  border-radius: var(--radius-pill);
  transition: opacity 0.2s, transform 0.15s;
}
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }

.btn-secondary {
  padding: var(--space-3) var(--space-8);
  border: 1px solid var(--border-inactive, #696969);
  color: var(--text-subtle, #CCCCCC);
  font-size: 0.9375rem;
  font-weight: 600;
  border-radius: var(--radius-pill);
  transition: border-color 0.2s, color 0.2s, transform 0.15s;
}
.btn-secondary:hover {
  border-color: var(--text-subtle, #CCCCCC);
  color: var(--text-primary, #EEEEEE);
  transform: translateY(-1px);
}

/* radial glow behind hero phone */

/* ─── Features ─── */
.features {
  padding: var(--space-24) var(--space-8);
  max-width: 1100px;
  margin: 0 auto;
}

.section-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent-tracking, #FF7C7C);
  text-align: center;
  margin-bottom: var(--space-3);
}

.section-title {
  font-size: clamp(1.75rem, 4vw, 2.75rem);
  font-weight: 700;
  text-align: center;
  color: var(--text-primary, #EEEEEE);
  margin-bottom: var(--space-12);
  letter-spacing: -0.01em;
}

.features-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-4);
}

.feature-desc {
  font-size: 0.875rem;
  color: var(--text-subtle, #CCCCCC);
  line-height: 1.65;
}

/* Single column since the brand-adaptation image was removed. It used to be the right-hand half of
   this band, so left-aligned copy now floats in a mostly empty full-bleed strip — centring is what
   keeps the section looking deliberate rather than half-loaded. */

.accent-tracking { color: var(--accent-tracking, #FF7C7C); }

/* ─── CTA Band ─── */

/* ─── Footer ─── */
.footer {
  border-top: 1px solid rgba(var(--border-inactive-rgb, 105, 105, 105), 0.2);
  padding: var(--space-8);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  max-width: 1100px;
  margin: 0 auto;
}

.footer-links {
  display: flex;
  gap: var(--space-6);
  list-style: none;
}

.footer-links a {
  font-size: 0.8125rem;
  color: var(--text-subtle);
  transition: color 0.2s;
}
.footer-links a:hover { color: var(--text-primary); }

/* ─── Responsive ─── */
@media (max-width: 900px) {
  .hero {
    flex-direction: column;
    text-align: center;
    padding-top: 100px;
  }
  .hero-actions { justify-content: center; }

}

@media (max-width: 600px) {
  .footer { flex-direction: column; align-items: flex-start; }
}

/* Transport glyphs — one file per mode, tinted by the mode's colour.
   mask, not <img>: an <img> cannot inherit currentColor, and these take seven different colours
   across two themes. The mask paints `color` through the glyph's alpha instead, so one file covers
   every case. Requires the SVG to be stroke-only with no baked fills — see
   assets/transport-icons/README.md. */
.tp-ico {
  display: inline-block;
  width: var(--tp-ico-size, 16px);
  height: var(--tp-ico-size, 16px);
  vertical-align: -0.15em;
  background-color: currentColor;
  -webkit-mask: var(--tp-ico-url) no-repeat center / contain;
          mask: var(--tp-ico-url) no-repeat center / contain;
}

/* Leg badge with a mode glyph: icon left, measurement right, both on the accent colour. */
.leg-badge-pill.has-ico { display: inline-flex; align-items: center; gap: 6px; }
.leg-badge-pill .tp-chip { flex: 0 0 auto; }
/* The Arrival field's leading glyph sits where the "?" help circle was, same box either way. */
#cm-arrival-ico { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; }

/* ── Stop banner ──────────────────────────────────────────────────────────────
   The numbered bookmark that labels a stop in a sequence (components/stop-banner.js). Shared by
   the marker popup's header and the route modal's leg navigator, so the colours live here and each
   host sets only the HEIGHT — the banner tracks whatever type scale it sits in. */
.stop-banner { display: block; width: auto; flex: none; }
.stop-banner .b-fill { fill: var(--accent-route, #EEEEEE); }
.stop-banner .b-num  {
  fill: var(--surface-app, #202020);
  font-family: var(--font-sans, 'Inter', sans-serif);
  font-weight: 600;
}

/* Mode glyph knocked out of a filled circle in the mode's colour: the circle takes the mode colour,
   the glyph is masked out of it in --tp-chip-fg. */
.tp-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tp-chip-size, 24px);
  height: var(--tp-chip-size, 24px);
  border-radius: 50%;
  flex: 0 0 auto;
  /* THE APP SURFACE, not white — and this is a legibility fix, not a tonal preference.
     This rule used to say the opposite, citing surface ratios of 1.9–3.3. Those numbers were taken
     against an older, darker set of mode colours; the palette is bright pastels now, which inverts
     the answer completely. Measured against the live --map-cat ramp:

       glyph            worst        best
       --surface-app    6.5 (bus)    15.3 (car)
       white            1.1 (car)    2.5  (bus)

     White on car yellow is 1.1:1 — the glyph is essentially not there. Re-measure before changing
     this again; the right answer follows the palette's brightness, not the theme's name. */
  color: var(--tp-chip-fg, var(--surface-app, #202020));
}

/* A chip built from a ROUTE_MODE (route-modes.js routeModeChipHtml) carries its glyph UNSIZED —
   that list is drawn at several scales and bakes no px — so the disc sizes it here instead. Two
   selectors because the two glyph shapes are genuinely different: a transport is a masked .tp-ico,
   and the default mode is an inline <svg>, which no mask rule reaches. 60% matches the 0.6 factor
   transportChipHtml bakes in, so a chip drawn either way comes out the same size.
   Both are overridden by the inline --tp-ico-size transportChipHtml still sets, so existing chips
   are untouched. */
.tp-chip > .tp-ico { --tp-ico-size: 60%; }
.tp-chip > svg { width: 60%; height: 60%; }

/* ── Transport slider ─────────────────────────────────────────────────────────
   The looping mode picker (components/transport-slider.js): a three-disc window that scrolls and
   snaps to its centre. Shared by the mobile route sheet and the desktop route editor, so the
   structure lives here and each host sets only --tps-size and --tps-gap — the picker tracks the
   type scale of wherever it has been dropped. */
.tps {
  --tps-size: 40px;
  --tps-gap: 8px;
  flex: 0 0 auto;
  position: relative;
  overflow: hidden;
  /* Exactly three discs wide: the chosen one, and a glimpse of its neighbours either side. */
  width: calc(3 * var(--tps-size) + 2 * var(--tps-gap));
}
/* Fades on both edges, so the window reads as a peephole onto a longer list rather than as a row
   that happens to be clipped. */
.tps::before, .tps::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  width: calc(var(--tps-size) * 0.6);
  z-index: 2;
  pointer-events: none;
}
.tps::before { left: 0;  background: linear-gradient(to right, rgba(var(--surface-app-rgb, 32,32,32), 0.85), transparent); }
.tps::after  { right: 0; background: linear-gradient(to left,  rgba(var(--surface-app-rgb, 32,32,32), 0.85), transparent); }

.tps-scroller {
  display: flex;
  align-items: center;
  gap: var(--tps-gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* One disc + one gap of padding each side, so the first and last can reach the centre. */
  padding: 4px calc(var(--tps-size) + var(--tps-gap));
}
.tps-scroller::-webkit-scrollbar { display: none; }

.tps-opt {
  flex-shrink: 0;
  width: var(--tps-size);
  height: var(--tps-size);
  border-radius: 50%;
  border: none;
  padding: 0;
  /* The glyph is knocked out of the disc in the APP SURFACE colour — the same rule the map's leg
     badges follow, so a mode looks identical wherever it is drawn. See .tp-chip above for why. */
  color: var(--surface-app, #202020);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  scroll-snap-align: center;
  /* A ring in the surface colour separates the disc from whatever it is scrolling over. */
  box-shadow: 0 0 0 2px rgba(var(--surface-app-rgb, 32,32,32), 1);
  opacity: 0.45;
  transform: scale(0.88);
  transition: opacity 0.18s, transform 0.18s, box-shadow 0.18s;
  -webkit-tap-highlight-color: transparent;
}
.tps-opt.is-active {
  opacity: 1;
  transform: scale(1);
  box-shadow: 0 0 0 2px rgba(var(--surface-app-rgb, 32,32,32), 1),
              0 0 0 4px rgba(var(--color-white-rgb, 255, 255, 255), 0.75);
}
.tps-opt:not(.is-active):hover { opacity: 0.75; }
.tps-opt:not(.is-active):active { opacity: 0.7; }
/* The glyph fills most of the disc. Proportional, not a fixed px size, so it is the same shape at
   every tier — a flat 20px read as a dot inside the larger discs. */
.tps-opt .tp-ico { --tp-ico-size: calc(var(--tps-size) * 0.7); }
.tps-opt > svg   { width: calc(var(--tps-size) * 0.6); height: calc(var(--tps-size) * 0.6); }

/* The mode chip at the middle of a saved diary leg. Non-interactive: it is a legend for the line,
   not a control, and it must never eat a click meant for the leg's own hit layer. */
.leg-mode-badge {
  pointer-events: none;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .35));
}
