:root {
  /* Neutral hierarchy - Phase 1 architectural theme (see this
     feature's own report). Every value here is a straight darken/
     bolden of the previous token of the same name; none of the
     status/accent hues below changed. */
  --color-bg: #f7f7f5;
  --color-surface: #ffffff;
  --color-border: #cfcfcb;
  --color-text-primary: #0a0a0a;
  --color-text-secondary: #555555;
  --color-text-tertiary: #737373;
  --color-accent: #2f6fed;
  --color-success: #1f9d55;
  --color-warning: #b8720f;
  --color-error: #c9432f;
  --color-surface-hover: #eeeeeb;

  /* .place-continue-bar's own translucent backdrop - kept as a named
     token (not a literal rgba on the rule itself) so the dark theme
     below can swap it too. Was a hardcoded near-white that stayed
     near-white even in dark mode, reading as a stray pale slab
     stapled across an otherwise dark page. */
  --color-continue-bar-bg: rgba(250, 250, 250, 0.96);

  /* Executive Theme - the two literal colors .btn-primary/
     .route-builder-map-instruction-strip already rendered
     (background: var(--color-text-primary) paired with a hardcoded
     white/#2a2a2a), now named so a dark theme can invert them (a
     LIGHT primary-button background needs a DARK "on-primary" text
     color, and a lighter hover shade instead of a darker one) without
     touching every call site's own class name. Light-mode values only
     - unchanged from what was already literally rendered. */
  --color-on-primary: #ffffff;
  --color-primary-hover: #2a2a2a;

  /* Two-tone loading-skeleton shimmer (@keyframes pulseBg) - named
     for the same reason: a light-mode-tuned pair of near-white tones
     would be invisible against a dark surface. */
  --color-skeleton-a: #f4f4f4;
  --color-skeleton-b: #eaeaea;

  /* Status background/border tints - formalized from the literal
     hex values already rendered by .selection-summary(.is-large/
     .is-too-large), unchanged in value, just named. */
  --color-success-bg: #f3fbf6;
  --color-success-border: #cfe7d8;
  --color-warning-bg: #fffaf0;
  --color-warning-border: #f2ddb8;
  --color-error-bg: #fff7f5;
  --color-error-border: #f0cbc5;
  /* Pre-existing badge tint (.maker-mark-included-badge) - numerically
     distinct from --color-success (#1f9d55) even though both read as
     "success green" - preserved exactly as already rendered rather
     than silently corrected, per this phase's own "no unrelated
     cleanup" scope. See this feature's own report. */
  --color-success-badge-bg: rgba(47, 158, 91, 0.12);

  /* Accent (blue) opacity variants - every rgba(47, 111, 237, ...)
     literal already in this file, named but numerically unchanged.
     See this feature's own report for which usages were reclassified
     to a dark-neutral ring instead of one of these. */
  --color-accent-06: rgba(47, 111, 237, 0.06);
  --color-accent-08: rgba(47, 111, 237, 0.08);
  --color-accent-10: rgba(47, 111, 237, 0.1);
  --color-accent-12: rgba(47, 111, 237, 0.12);
  --color-accent-20: rgba(47, 111, 237, 0.2);

  /* Race & Route Mode's own accent (warm, energetic) plus the exact
     same set of opacity variants as --color-accent above, at the
     same rgb(226, 84, 12) - kept as literal rgba values (not
     color-mix()) to match this file's own established convention for
     every other accent variant. Applied via a single scoped override
     (see .configurator-main-card.race-mode-on below) - never touched
     directly by any other rule. */
  --color-accent-race: #e2540c;
  --color-accent-race-06: rgba(226, 84, 12, 0.06);
  --color-accent-race-08: rgba(226, 84, 12, 0.08);
  --color-accent-race-10: rgba(226, 84, 12, 0.1);
  --color-accent-race-12: rgba(226, 84, 12, 0.12);
  --color-accent-race-20: rgba(226, 84, 12, 0.2);
  /* Text-only variant - --color-accent-race itself is ~3.8:1 on white,
     which passes for borders/buttons but fails WCAG AA (4.5:1) for the
     toggle's own small label text. Darker in light mode, lighter in
     dark mode (redefined below/in both dark blocks) - the inverse of
     most tokens here, since a fixed value can't hit 4.5:1 against both
     a white and a near-black surface. */
  --color-accent-race-text: #a83e0a;

  /* Dark-neutral selected-state ring - used only where a decorative
     accent ring was reclassified as selection-state chrome layered on
     top of an already-meaningful color (trail/route swatch dots) -
     see this feature's own report. Matches --color-text-primary
     exactly (#0a0a0a = rgb(10, 10, 10)). */
  --color-selected-ring: var(--color-text-primary);
  --color-selected-ring-soft: rgba(10, 10, 10, 0.12);

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 40px;
  --space-7: 64px;

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;

  /* Flattened - the large diffuse 8px/24px layer removed, opacity of
     the remaining tight layer raised slightly (0.04 -> 0.06) so
     "minimal separation" is preserved without the soft glow. Applies
     only to ordinary cards/surfaces; structural shadows (print-frame
     scrim, mobile sheet/drawer, sticky bar, switch-thumb, focus rings)
     are untouched - see this feature's own report. */
  --shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.06);

  /* One consistent, high-contrast focus treatment, applied to the
     .btn family (which previously had none) and available for the
     handful of existing outline-based focus rules to reference
     instead of repeating the same literal. Still blue in Phase 1 -
     see this feature's own report on why focus keeps the accent
     color rather than moving to black/white. */
  --focus-ring: 2px solid var(--color-accent);
  --focus-ring-offset: 2px;

  --rail-width: 268px;

  /* ---------------------------------------------------------------
     Formal typography/border/transition tokens (Phase 1 audit,
     section 3) - DEFINED here for the documented inventory this phase
     asked for, but deliberately NOT substituted into the ~90 existing
     font-size/font-weight/line-height/letter-spacing/border-width
     declarations below: every one of those numbers is unchanged from
     before this phase, and rewriting each occurrence as a var()
     reference of the identical value would be exactly the "broad
     refactoring" this phase's own scope excludes, for zero rendered
     difference. Font-family is the one typography property actually
     substituted in this phase (see the selectors below) since that is
     the actual requested visual change. Transition/animation values
     are untouched entirely, per this phase's own hard constraint -
     these two are documented only, for later use.
     --------------------------------------------------------------- */
  --font-family-sans: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
  --font-family-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --font-weight-regular: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;

  --font-size-2xs: 10px;
  --font-size-xs: 11px;
  --font-size-sm: 12px;
  --font-size-base: 13px;
  --font-size-md: 14px;
  --font-size-lg: 15px;
  --font-size-xl: 16px;
  --font-size-2xl: 18px;
  --font-size-3xl: 22px;
  --font-size-4xl: 24px;
  --font-size-5xl: 26px;
  --font-size-6xl: 28px;

  --line-height-tight: 1;
  --line-height-snug: 1.2;
  --line-height-normal: 1.5;
  --line-height-relaxed: 1.6;

  --letter-spacing-tight: -0.01em;
  --letter-spacing-wide-xs: 0.02em;
  --letter-spacing-wide-sm: 0.03em;
  --letter-spacing-wide-md: 0.04em;
  --letter-spacing-wide-lg: 0.05em;
  --letter-spacing-wide-xl: 0.06em;

  --border-width-thin: 1px;
  --border-width-thin-alt: 1.5px;
  --border-width-medium: 2px;
  --border-width-thick: 3px;

  /* Documented only - not applied anywhere this phase. */
  --transition-fast: 0.15s ease;
  --transition-base: 0.2s ease;
}

/* =====================================================================
   EXECUTIVE THEME - a second, dark palette for every --color-* token
   above (spacing/radius/typography/shadow-shape tokens are untouched -
   only color changes between themes). A refined graphite, never pure
   black, with warm off-white text instead of stark white - deliberately
   NOT the harsh near-black-plus-neon-accent look; see this feature's
   own design proposal for the reasoning.

   Three-state pattern, matching how every theme-aware surface in this
   app already needs to resolve:
     - no explicit choice, OS light  -> bare :root above (unchanged)
     - no explicit choice, OS dark   -> the prefers-color-scheme block
       below, guarded with :not([data-theme="light"]) so an explicit
       Light choice always wins over a dark OS setting
     - explicit choice (either way)  -> the two [data-theme="..."]
       blocks below always win, regardless of OS setting

   applyThemePreference() in app.js is the ONLY place that ever sets
   data-theme on <html> - see that function's own docstring. Tokens
   defined here as var(--other-token) (e.g. --color-selected-ring)
   are deliberately NOT redefined below - they already resolve
   correctly once the token they reference changes. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #17181a;
    --color-surface: #1e2022;
    --color-border: #35383b;
    --color-text-primary: #ece9e4;
    --color-text-secondary: #a6a49f;
    --color-text-tertiary: #79776f;
    --color-accent: #5b8cff;
    --color-success: #4bcf8a;
    --color-warning: #e3ab55;
    --color-error: #e2776a;
    --color-surface-hover: #262829;

    --color-on-primary: #14151a;
    --color-primary-hover: #d9d5cc;
    --color-skeleton-a: #202224;
    --color-skeleton-b: #26282a;

    --color-success-bg: rgba(75, 207, 138, 0.08);
    --color-success-border: rgba(75, 207, 138, 0.28);
    --color-warning-bg: rgba(227, 171, 85, 0.08);
    --color-warning-border: rgba(227, 171, 85, 0.28);
    --color-error-bg: rgba(226, 119, 106, 0.08);
    --color-error-border: rgba(226, 119, 106, 0.28);
    --color-success-badge-bg: rgba(75, 207, 138, 0.16);

    --color-accent-06: rgba(91, 140, 255, 0.08);
    --color-accent-08: rgba(91, 140, 255, 0.1);
    --color-accent-10: rgba(91, 140, 255, 0.12);
    --color-accent-12: rgba(91, 140, 255, 0.16);
    --color-accent-20: rgba(91, 140, 255, 0.26);

    --color-selected-ring-soft: rgba(236, 233, 228, 0.14);

    --shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.3);
    --color-continue-bar-bg: rgba(30, 32, 34, 0.92);
    --color-accent-race-text: #ff8f4d;
  }
}

:root[data-theme="dark"] {
  --color-bg: #17181a;
  --color-surface: #1e2022;
  --color-border: #35383b;
  --color-text-primary: #ece9e4;
  --color-text-secondary: #a6a49f;
  --color-text-tertiary: #79776f;
  --color-accent: #5b8cff;
  --color-success: #4bcf8a;
  --color-warning: #e3ab55;
  --color-error: #e2776a;
  --color-surface-hover: #262829;

  --color-on-primary: #14151a;
  --color-primary-hover: #d9d5cc;
  --color-skeleton-a: #202224;
  --color-skeleton-b: #26282a;

  --color-success-bg: rgba(75, 207, 138, 0.08);
  --color-success-border: rgba(75, 207, 138, 0.28);
  --color-warning-bg: rgba(227, 171, 85, 0.08);
  --color-warning-border: rgba(227, 171, 85, 0.28);
  --color-error-bg: rgba(226, 119, 106, 0.08);
  --color-error-border: rgba(226, 119, 106, 0.28);
  --color-success-badge-bg: rgba(75, 207, 138, 0.16);

  --color-accent-06: rgba(91, 140, 255, 0.08);
  --color-accent-08: rgba(91, 140, 255, 0.1);
  --color-accent-10: rgba(91, 140, 255, 0.12);
  --color-accent-12: rgba(91, 140, 255, 0.16);
  --color-accent-20: rgba(91, 140, 255, 0.26);

  --color-selected-ring-soft: rgba(236, 233, 228, 0.14);

  --shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.3);
  --color-continue-bar-bg: rgba(30, 32, 34, 0.92);
  --color-accent-race-text: #ff8f4d;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  color: var(--color-text-primary);
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* Mobile customize-sheet scroll lock (see app.js's own
   lockBackgroundScroll/unlockBackgroundScroll) - the standard iOS-
   Safari-compatible technique: pinning the body at a fixed position is
   what actually stops the document from rubber-banding/scrolling
   behind a fixed-position overlay, which position:fixed on the
   overlay alone does not reliably prevent on iOS. `top` is the one
   property this class deliberately does NOT set - it's applied inline
   in JS from the captured scroll position, since it's a per-open
   dynamic value, not a static class property. */
.body-scroll-locked {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes pulseBg {
  0%, 100% {
    background-color: var(--color-skeleton-a);
  }
  50% {
    background-color: var(--color-skeleton-b);
  }
}

@keyframes pulseGlyph {
  0%, 100% {
    opacity: 0.35;
    transform: scale(0.97);
  }
  50% {
    opacity: 0.9;
    transform: scale(1);
  }
}

@keyframes pulseDot {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}

@keyframes dotFade {
  0%, 80%, 100% {
    opacity: 0.2;
  }
  40% {
    opacity: 1;
  }
}

@keyframes slideUp {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

@keyframes bounceDown {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(3px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .view,
  .model-panel.is-loading,
  .loading-glyph-wrap,
  .step-active::before,
  .loading-dots span,
  .steps-sheet,
  .location-confirmation,
  .location-confirmation-scroll-icon {
    animation: none !important;
  }
}

/* ---------- Header ---------- */

.app-header {
  padding: var(--space-5) var(--space-6);
  border-bottom: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.app-title {
  font-family: var(--font-family-mono);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--color-text-secondary);
}

/* Groups the header's own right-hand-side controls (currently just
   the theme toggle - Phase C2.3 moved #cart-pill out of the header
   entirely into its own position: fixed top-right card, since a fixed
   element does not need to participate in this flex layout) so
   .app-header's own justify-content: space-between still puts exactly
   two things apart: the title and this group. */
.app-header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* Executive Theme toggle - deliberately a single small pill matching
   .cart-pill's own established visual language (border/radius/
   font), not a new three-way segmented control, so the header gains
   one more small button rather than a second distinct UI pattern.
   Cycles Auto -> Light -> Dark -> Auto on each click (see app.js's
   own initThemeToggle/applyThemePreference). */
.theme-toggle-button {
  flex: none;
  padding: var(--space-2) var(--space-3);
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.theme-toggle-button:hover {
  border-color: var(--color-accent);
  background: var(--color-surface-hover);
}

.theme-toggle-button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* ---------- App shell: rail + content ---------- */

.app-shell {
  flex: 1;
  display: flex;
  align-items: flex-start;
  width: 100%;
}

.main-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.rail {
  width: var(--rail-width);
  flex: none;
  position: sticky;
  top: 0;
  align-self: flex-start;
  max-height: 100vh;
  overflow-y: auto;
  padding: var(--space-6) var(--space-5) var(--space-5);
  border-right: 1px solid var(--color-border);
}

.rail-brand {
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
  margin-bottom: var(--space-5);
}

.rail-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.rail-step {
  position: relative;
  padding-bottom: var(--space-4);
}

.rail-step::before {
  content: "";
  position: absolute;
  left: 8px;
  top: 22px;
  bottom: 0;
  width: 1px;
  background: var(--color-border);
}

.rail-step:last-child::before {
  display: none;
}

.rail-step-done::before {
  background: var(--color-text-primary);
}

.rail-step-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font-family: inherit;
  text-align: left;
  cursor: default;
}

button.rail-step-row {
  cursor: pointer;
}

button.rail-step-row:hover .rail-step-label {
  text-decoration: underline;
}

.rail-step-dot {
  flex-shrink: 0;
  width: 17px;
  font-family: var(--font-family-mono);
  font-size: 13px;
  line-height: 1;
  color: var(--color-text-tertiary);
  text-align: center;
}

.rail-step-done .rail-step-dot {
  color: var(--color-text-primary);
}

.rail-step-current .rail-step-dot {
  color: var(--color-accent);
}

.rail-step-label {
  font-family: var(--font-family-mono);
  font-size: 14px;
  color: var(--color-text-tertiary);
}

.rail-step-done .rail-step-label {
  color: var(--color-text-secondary);
}

.rail-step-current .rail-step-label {
  color: var(--color-text-primary);
  font-weight: 700;
}

.rail-step-context {
  margin: var(--space-1) 0 0 29px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--color-text-secondary);
  max-width: 190px;
}

.rail-nownext {
  margin: var(--space-4) 0;
  padding: var(--space-4) 0;
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

.rail-nownext-row {
  margin: 0 0 var(--space-2);
  font-family: var(--font-family-mono);
  font-size: 13px;
  display: flex;
  gap: var(--space-2);
}

.rail-nownext-row:last-child {
  margin-bottom: 0;
}

.rail-eyebrow {
  flex-shrink: 0;
  font-family: var(--font-family-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
  width: 38px;
}

.rail-selections {
  padding-top: var(--space-1);
}

.rail-selections > .rail-eyebrow {
  display: block;
  width: auto;
  margin-bottom: var(--space-3);
}

.rail-selections-list {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-3);
}

.rail-selections-list dt {
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-tertiary);
}

.rail-selections-list dd {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 500;
  text-align: right;
  color: var(--color-text-primary);
}

.rail-selection-link {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-family: var(--font-family-mono);
  font-weight: 500;
  color: var(--color-text-primary);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--color-border);
}

.rail-selection-link:hover {
  text-decoration-color: var(--color-accent);
}

/* ---------- Mobile steps header + sheet ---------- */

.mobile-header {
  display: none;
}

@media (max-width: 899px) {
  .rail {
    display: none;
  }

  .mobile-header {
    display: block;
    width: 100%;
    position: sticky;
    top: 0;
    z-index: 20;
    background: var(--color-surface);
    border: none;
    border-bottom: 1px solid var(--color-border);
    padding: var(--space-3) var(--space-4);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }
}

.mobile-header-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-family: var(--font-family-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

.mobile-steps-caret {
  color: var(--color-accent);
}

.mobile-header-name {
  display: block;
  margin-top: 2px;
  font-family: var(--font-family-mono);
  font-size: 15px;
  font-weight: 700;
  color: var(--color-text-primary);
}

.mobile-progress-track {
  display: block;
  margin-top: var(--space-2);
  height: 3px;
  border-radius: 2px;
  background: var(--color-border);
  overflow: hidden;
}

.mobile-progress-fill {
  display: block;
  height: 100%;
  background: var(--color-accent);
  transition: width 0.2s ease;
}

.mobile-header-next {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-secondary);
}

.steps-sheet-scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
  z-index: 29;
}

.steps-sheet-scrim[hidden] {
  display: none;
}

.steps-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  max-height: 80vh;
  overflow-y: auto;
  background: var(--color-surface);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18);
  padding: var(--space-3) var(--space-5) var(--space-6);
  animation: slideUp 0.2s ease;
}

.steps-sheet[hidden] {
  display: none;
}

.steps-sheet-handle {
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: var(--color-border);
  margin: 0 auto var(--space-4);
}

.steps-sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-family: var(--font-family-mono);
  font-size: 15px;
  font-weight: 700;
  margin-bottom: var(--space-4);
}

.steps-sheet-close {
  background: none;
  border: none;
  font-family: var(--font-family-mono);
  font-size: 16px;
  color: var(--color-text-secondary);
  cursor: pointer;
  padding: var(--space-1);
}

.steps-sheet-list {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
}

.steps-sheet-selections {
  margin: 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-3);
}

.steps-sheet-selections dt {
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-tertiary);
}

.steps-sheet-selections dd {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 500;
  text-align: right;
  color: var(--color-text-primary);
}

/* ---------- Email stage (#email-view - a real screen, not a modal) ---------- */

.email-consent-row {
  margin-top: var(--space-4);
}

.email-consent-note {
  margin-top: var(--space-1);
}

/* ---------- Shared view/card primitives ---------- */

.view {
  flex: 1;
  animation: fadeIn 0.25s ease;
}

/* !important: several views (#display-view, #loading-view, #review-view,
   etc.) set their own `display` via an ID selector, which outranks this
   class+attribute selector on specificity alone - without !important,
   "hidden" views stay laid out (just visually last-painted), so every
   step ends up stacked in the page at once. */
.view[hidden] {
  display: none !important;
}

.field-label {
  display: block;
  margin-top: var(--space-5);
  margin-bottom: var(--space-2);
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-secondary);
}

.field-label-compact {
  margin-top: 0;
}

input[type="text"],
input[type="email"] {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: var(--font-family-mono);
  font-size: 15px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

input[type="text"]:focus,
input[type="email"]:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

.feedback-text {
  min-height: 16px;
  margin: var(--space-2) 0 0;
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-secondary);
}

.feedback-text.error {
  color: var(--color-error);
}

.feedback-text.success {
  color: var(--color-success);
}

.feedback-text[hidden] {
  display: none;
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 24px;
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  font-family: var(--font-family-mono);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
  text-decoration: none;
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Every .btn variant (.btn-primary/.btn-secondary/.btn-compact/map
   action buttons) previously had NO custom focus style at all - only
   the browser default outline. One consistent, high-contrast ring via
   the shared --focus-ring token (still blue in Phase 1 - see this
   feature's own report). Purely additive: no width/height/padding
   changes, so component dimensions are unaffected. */
.btn:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.btn-primary {
  background: var(--color-text-primary);
  color: var(--color-on-primary);
}

.btn-primary:hover:not(:disabled) {
  background: var(--color-primary-hover);
}

.btn-primary:active:not(:disabled) {
  opacity: 0.85;
}

.btn-secondary {
  background: transparent;
  border-color: var(--color-border);
  color: var(--color-text-primary);
}

.btn-secondary:hover {
  background: var(--color-surface-hover);
}

.btn-block {
  width: 100%;
  margin-top: var(--space-4);
}

.btn-large {
  padding: 16px 36px;
  font-size: 16px;
}

.btn-link {
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-secondary);
  cursor: pointer;
}

.btn-link:hover {
  color: var(--color-text-primary);
}

.btn-link:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.back-link {
  display: inline-block;
  margin-bottom: var(--space-4);
}

.center-link {
  display: block;
  margin: var(--space-3) auto 0;
}

.center-text {
  text-align: center;
}

.actions-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  min-height: 42px;
  margin-top: var(--space-5);
}

.actions-row-center {
  justify-content: center;
}

/* ---------- Step screens (Select Display / Choose Place / Personalize) ---------- */

.step-card {
  max-width: 420px;
  width: 100%;
  margin: 0 auto;
  padding: var(--space-6) var(--space-5);
}

.step-card-wide {
  max-width: 760px;
}

/* Present the configurator as one concise customer-facing sequence while
   preserving the route builder's established markup and behavior. */
#place-view .step-card-wide {
  display: flex;
  flex-direction: column;
}

#place-view .back-link { order: -7; }
#place-view .step-heading { order: -6; }
#place-view .step-subheading { order: -5; }
#place-view .configurator-main-card { order: -4; }
#place-view .location-step-heading,
#place-view .location-step-helper,
#place-view .search-row,
#place-view #use-my-location-button,
#place-view #use-my-location-confirmation,
#place-view #search-feedback,
#place-view #search-attribution { order: -3; }
/* Same order as .journey-route-section below (a deliberate tie,
   matching this block's own established multi-selector grouping
   pattern above) - relies on these two appearing BEFORE it in DOM
   order (see index.html) to win the tie-break, so the toggle always
   renders immediately above the section it frames. */
#place-view .race-mode-row,
#place-view #race-mode-confirm { order: -2; }
#place-view .journey-route-section { order: -2; }
/* Shape/size sit last inside the card, immediately adjacent to the map
   below (no visual gap) - moved here so a desktop screen recording can
   show shape/size selection and the map together without scrolling.
   Location search and the optional route flow keep their existing
   relative order, just above this now instead of below it. */
#place-view .shape-selector { order: -1; }
#place-view .map-step-heading,
#place-view .route-builder-map-instruction-strip,
#place-view .map-shortcuts-row,
#place-view .map-panel,
#place-view .map-actions,
#place-view .area-summary,
#place-view #place-next-button { order: -1; }

.step-heading {
  margin: 0 0 var(--space-1);
  font-family: var(--font-family-mono);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -0.01em;
  outline: none;
}

.step-subheading {
  margin: 0 0 var(--space-5);
  font-size: 15px;
  color: var(--color-text-secondary);
}

.step-label-row {
  margin: 0 0 var(--space-3);
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
}

.step-helper {
  margin: var(--space-3) 0 0;
  font-size: 13px;
  color: var(--color-text-secondary);
}

#display-view,
#place-view,
#email-view {
  display: flex;
  padding: var(--space-6);
}

.search-row {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.search-row input {
  flex: 1;
}

/* align-self: flex-start overrides the column-flex parent's default
   stretch (see #place-view .step-card-wide above) - without it, this
   button (now a bordered .btn, not a borderless .btn-link) would
   stretch to the full card width and look like an oversized bar
   instead of a compact action. */
.location-search-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  margin-bottom: var(--space-2);
}

.location-search-link:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.location-search-link-icon {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
}

.location-search-link-icon circle,
.location-search-link-icon path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.location-confirmation {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-success-border);
  border-radius: var(--radius-md);
  background: var(--color-success-bg);
  animation: fadeIn 0.25s ease;
}

/* This project's own established convention (see .feedback-text[hidden]/
   #route-builder-sticky-busy-label[hidden]) - an author-origin `display`
   declaration always beats the UA [hidden] rule at equal specificity,
   so every element with its own unconditional `display` needs this
   explicit override or `hidden` silently stops working. */
.location-confirmation[hidden] {
  display: none;
}

.location-confirmation-header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
}

.location-confirmation-icon {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  color: var(--color-success);
}

.location-confirmation-icon circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

.location-confirmation-icon path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.location-confirmation-text {
  display: grid;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
}

.location-confirmation-dismiss {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-tertiary);
  cursor: pointer;
  touch-action: manipulation;
}

.location-confirmation-dismiss svg {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.location-confirmation-dismiss:hover {
  color: var(--color-text-primary);
  background: var(--color-success-badge-bg);
}

.location-confirmation-dismiss:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.location-confirmation-title {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text-primary);
}

.location-confirmation-hint {
  margin: 0;
  font-size: 13px;
  color: var(--color-text-secondary);
}

.location-confirmation-scroll-button {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--color-success-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-success);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.location-confirmation-scroll-button:hover {
  background: var(--color-success-bg);
}

.location-confirmation-scroll-button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.location-confirmation-scroll-icon {
  width: 12px;
  height: 12px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  animation: bounceDown 1.4s ease-in-out infinite;
}

/* ---------- Compact display-shape selector (map screen) ---------- */

.journey-route-section {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}

/* Same collapse treatment as .classic-displays-summary. Desktop starts
   open (unchanged from before this was collapsible); mobile starts
   closed by default (see app.js's own one-time init check against
   CUSTOMIZE_PANEL_MOBILE_BREAKPOINT) - it's optional, and on a small
   screen it previously added real height before shape/size/map. */
.journey-route-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  list-style: none;
}

.journey-route-summary::-webkit-details-marker {
  display: none;
}

.journey-route-summary .field-label {
  margin: 0;
}

.shape-selector {
  margin-bottom: var(--space-3);
}

.configurator-options-card {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

.configurator-main-card {
  display: flex;
  flex-direction: column;
  margin-bottom: var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

/* Race & Route Mode - most of the visual reskin is this one scoped
   custom-property override. Every button, focus ring, and selected-
   shape/size border inside this card already reads var(--color-accent)
   /var(--color-accent-NN) (see .shape-size-btn.is-selected etc.) - so
   redefining them here is most of what this feature needs. Shape/size
   OPTIONS themselves are untouched by this rule - only the color
   they're drawn in. */
.configurator-main-card.race-mode-on {
  --color-accent: var(--color-accent-race);
  --color-accent-06: var(--color-accent-race-06);
  --color-accent-08: var(--color-accent-race-08);
  --color-accent-10: var(--color-accent-race-10);
  --color-accent-12: var(--color-accent-race-12);
  --color-accent-20: var(--color-accent-race-20);
}

/* Two more pieces of the "you just unlocked something" feel: the
   toggle's own text picks up the race color (using the text-safe
   variant above, not --color-accent, which doesn't hit AA at this text
   size), and the "Add route" summary heading right below it disappears
   - once the toggle itself says Race & Route Mode is on and the
   section is already forced open (see renderRaceModeSection in app.js),
   a second "Add route" heading in between just repeats the same
   announcement. The content below the summary is untouched by this -
   <details> shows its children whenever [open] is set regardless of
   whether the summary itself is visible, so hiding the summary here
   only removes the redundant heading, never the buttons under it. Off
   state (no .race-mode-on) is completely unaffected - "Add route" stays
   exactly as it was for anyone who never touches the toggle. */
.configurator-main-card.race-mode-on #race-mode-label,
.configurator-main-card.race-mode-on .race-mode-hint {
  color: var(--color-accent-race-text);
}

.configurator-main-card.race-mode-on .journey-route-summary {
  display: none;
}

/* Extends the same "you unlocked something" color to the headers
   inside the route content itself once it's revealed - deliberately
   NOT every piece of text in there (helper/instruction paragraphs stay
   neutral, matching this feature's own restraint elsewhere: shape/size
   OPTIONS never change with the toggle, only color does). Scoped to
   #journey-route-section so it only ever touches text that's actually
   part of what the toggle reveals, never something else on the page
   that happens to share these classes. */
.configurator-main-card.race-mode-on #journey-route-section .field-label,
.configurator-main-card.race-mode-on #journey-route-section .route-builder-alternatives-label {
  color: var(--color-accent-race-text);
}

.race-mode-row {
  margin-bottom: var(--space-3);
}

.race-mode-copy {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.race-mode-hint {
  margin: 0;
}

.configurator-step-heading {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-2);
}

.configurator-step-heading .field-label {
  margin: 0;
}


.map-step-heading {
  margin: var(--space-5) 0 var(--space-2);
}

.configurator-step-helper {
  margin: calc(-1 * var(--space-1)) 0 var(--space-2);
  color: var(--color-text-secondary);
  font-size: 13px;
  line-height: 1.45;
}


.journey-route-section > .field-label {
  margin-top: 0;
}

.journey-route-section .your-route-control {
  margin-bottom: 0;
}

.shape-selector-label {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
}

.shape-selector-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  max-width: 100%;
}

.shape-size-selector-label {
  margin-top: var(--space-3);
}

.shape-size-selector-grid {
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  max-width: 100%;
}

.shape-size-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}

/* Outer perimeter mirrors whichever shape is selected in "Choose
   shape" above (see app.js's own SHAPE_SIZE_BUTTON_ASPECT and the
   shape-size-btn-{shapeId} class it sets) - width/height come from
   app.js as inline styles (scaled per Small/Medium/Large tier), never
   from this file, so this rule only ever supplies shape/color/state. */
.shape-size-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  padding: 0;
  border: 1.5px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-primary);
  cursor: pointer;
  font-family: var(--font-family-mono);
  font-weight: 700;
  /* Without this, two quick taps in close succession (comparing sizes,
     or the two tucked-away shortcuts right next to the map) can be
     read by the browser as a double-tap-to-zoom gesture instead of two
     separate clicks - manipulation drops that gesture for this element
     without disabling scrolling elsewhere on the page. */
  touch-action: manipulation;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease,
    width 0.15s ease, height 0.15s ease;
}

.shape-size-btn:hover {
  border-color: var(--color-accent);
  background: var(--color-surface-hover);
}

.shape-size-btn:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

.shape-size-btn.is-selected {
  border-color: var(--color-accent);
  background: var(--color-accent-06);
  box-shadow: 0 0 0 2px var(--color-accent);
  color: var(--color-accent);
}

.shape-size-btn-square,
.shape-size-btn-rectangle {
  border-radius: 22%;
}

.shape-size-btn-circle {
  border-radius: 50%;
}

/* Same polygon as .shape-btn-outline-octagon above - one shared
   silhouette, never a second, independently-tuned octagon path. */
.shape-size-btn-octagon {
  border-radius: 0;
  clip-path: polygon(29.3% 0, 70.7% 0, 100% 29.3%, 100% 70.7%, 70.7% 100%, 29.3% 100%, 0 70.7%, 0 29.3%);
}

.shape-size-btn-number {
  white-space: nowrap;
}

.shape-size-btn-caption {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.shape-size-btn-label,
.shape-size-btn-price {
  font-size: 11px;
  font-weight: 700;
}

.shape-size-btn-price {
  font-family: var(--font-family-mono);
  color: var(--color-text-secondary);
  font-weight: 600;
}

.shape-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  flex: 0 0 auto;
  min-width: 68px;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  cursor: pointer;
  font-family: var(--font-family-mono);
  /* Same fix as .shape-size-btn above - two quick taps comparing
     shapes read as double-tap-to-zoom without this. */
  touch-action: manipulation;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease;
}

.shape-btn:hover {
  border-color: var(--color-accent);
  background: var(--color-surface-hover);
}

.shape-btn:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

.shape-btn.is-selected {
  border-color: var(--color-accent);
  background: var(--color-accent-06);
  box-shadow: 0 0 0 2px var(--color-accent);
}

.shape-btn-outline {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 26px;
}

.shape-btn-outline-box {
  width: 24px;
  height: 24px;
  border: 1.5px solid var(--color-text-tertiary);
  border-radius: 2px;
  background: var(--color-bg);
}

.shape-btn-outline-rectangle {
  width: 30px;
  height: 20px;
}

.shape-btn-outline-circle {
  border-radius: 50%;
}

.shape-btn-outline-octagon {
  clip-path: polygon(29.3% 0, 70.7% 0, 100% 29.3%, 100% 70.7%, 70.7% 100%, 29.3% 100%, 0 70.7%, 0 29.3%);
}

.shape-btn.is-selected .shape-btn-outline-box {
  border-color: var(--color-accent);
  background: var(--color-surface);
}

.shape-btn-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text-primary);
  white-space: nowrap;
}

/* Shape shortcuts (left) and size shortcuts (right, tucked at the
   far edge) share this one row - flex-end so a short shape row and a
   taller size-with-price-tag column still baseline together at the
   bottom, right above the map. */
.map-shortcuts-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

/* Shares .shape-btn's border/hover/focus/is-selected treatment as-is
   (same class, applied alongside this one) - only geometry is
   overridden here, so a future restyle of the real picker's states
   never needs a second, duplicated set of rules. */
.map-shape-shortcut-grid {
  display: flex;
  gap: var(--space-2);
}

.map-shape-shortcut-btn {
  min-width: 40px;
  min-height: 40px;
  padding: 7px;
  gap: 0;
}

.map-shape-shortcut-btn .shape-btn-outline {
  height: 22px;
}

.map-shape-shortcut-btn .shape-btn-outline-box {
  width: 20px;
  height: 20px;
}

.map-shape-shortcut-btn .shape-btn-outline-rectangle {
  width: 26px;
  height: 16px;
}

/* Second spot for the same size choice (the real one stays in "Choose
   size" above) - deliberately styled differently (a price tag on a
   stem above a tiny S/M/L, not a dimension number below) so the two
   placements can be told apart at a glance while testing which one
   actually gets used. */
.map-size-shortcut-grid {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  flex: 0 0 auto;
}

.map-size-shortcut-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.map-size-shortcut-price {
  font-family: var(--font-family-mono);
  font-size: 10px;
  font-weight: 700;
  color: var(--color-text-secondary);
  white-space: nowrap;
}

.map-size-shortcut-stem {
  width: 1px;
  height: 6px;
  background: var(--color-border);
}

/* Shares .shape-size-btn's border/hover/focus/is-selected treatment
   AND its shape-size-btn-{shapeId} silhouette (square/rectangle/circle/
   octagon) as-is - only geometry/font-size are overridden here, same
   reuse pattern as .map-shape-shortcut-btn above. Width/height come
   from app.js as inline styles (see MAP_SIZE_SHORTCUT_DIAMETER_PX),
   scaled per tier same as the real picker, just at a fraction of the
   size. */
.map-size-shortcut-btn {
  min-width: 0;
  font-size: 10px;
}

/* is-selected lives on .map-size-shortcut-item (the wrapper), not the
   button itself - the stem/price are the button's siblings, not its
   descendants, so only the shared wrapper can style all three at once. */
.map-size-shortcut-item.is-selected .map-size-shortcut-stem {
  background: var(--color-accent);
}

.map-size-shortcut-item.is-selected .map-size-shortcut-price {
  color: var(--color-accent);
}

.shape-selector-hint {
  margin: var(--space-2) 0 0;
  font-size: 12px;
  color: var(--color-text-tertiary);
}

/* Surfaces the cart-quantity discount BEFORE a customer has committed
   to generating anything - the only other place this same discount is
   ever mentioned (.cart-panel-checkout-note) is inside the cart
   panel, reachable only from the confirmed/post-generation screen
   after "Add to cart" - a first-time customer had no way to know the
   incentive existed until they were already deep in that flow. */
.cart-discount-hint {
  margin: var(--space-2) 0 0;
  font-size: 12px;
  color: var(--color-text-secondary);
}

.cart-discount-hint strong {
  color: var(--color-text-primary);
}

/* ---------------------------------------------------------------
   Shape Display Shape/Size picker used on the landing screen.

   This project's own established convention (see .feedback-text[hidden]/
   .view[hidden]/.your-route-upload-button[hidden] elsewhere in this
   file): a class rule that sets its own `display` must be paired with a
   same-specificity [hidden] override, since a plain class selector and
   the UA stylesheet's own `[hidden] { display: none }` rule are NOT the
   same specificity - without this pairing, the class rule below would
   silently defeat the native `hidden` attribute the moment the panel
   opens for the first time. This pairing is REQUIRED, not optional.
   --------------------------------------------------------------- */

.shape-display-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.shape-display-picker[hidden] {
  display: none;
}

.shape-display-fieldset {
  margin: 0;
  padding: 0;
  border: 0;
}

.shape-display-options,
.shape-display-size-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* Visually hides the native radio input WITHOUT display:none/
   visibility:hidden, which would also remove it from the accessibility
   tree and the tab order - it must stay a real, focusable, announced
   radio button; only its own default appearance is hidden, since its
   paired <label> (styled as .shape-btn) is what's actually shown. */
.visually-hidden-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.visually-hidden-input:focus-visible + label.shape-btn {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

.visually-hidden-input:checked + label.shape-btn {
  border-color: var(--color-accent);
  background: var(--color-accent-06);
  box-shadow: 0 0 0 2px var(--color-accent);
}

.shape-display-size-options {
  display: flex;
  flex-direction: column;
}

/* Real <button> elements, deliberately NOT styled/behaving as radios -
   see this feature's own plan for why Size is a set of explicit commit
   actions rather than a native radio group (arrow-key movement through
   a radio group both selects AND fires `change`, which could commit an
   intermediate size before the customer finishes comparing). */
.shape-display-size-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-width: 96px;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  cursor: pointer;
  font-family: var(--font-family-mono);
  text-align: left;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease;
}

.shape-display-size-btn:hover {
  border-color: var(--color-accent);
  background: var(--color-surface-hover);
}

.shape-display-size-btn:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

.shape-display-size-btn.is-selected {
  border-color: var(--color-accent);
  background: var(--color-accent-06);
  box-shadow: 0 0 0 2px var(--color-accent);
}

.shape-display-size-btn-label {
  font-size: 13px;
  font-weight: 700;
  color: var(--color-text-primary);
}

.shape-display-size-btn-footprint {
  font-size: 11px;
  color: var(--color-text-secondary);
}

.shape-display-size-btn-price {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-text-primary);
}

/* z-index: 0 (paired with the pre-existing position: relative) is a
   deliberate, minimal stacking-context isolation - Leaflet's own
   internal panes/controls (observed z-index up to 1000) would
   otherwise carry no isolating ancestor of their own and could
   compete directly, at the page's root stacking context, against
   unrelated fixed-position application UI (e.g. #route-builder-
   sticky-actions) wherever the two geometrically overlap. Isolating
   here confines the map's entire painted stack - tiles, panes,
   markers, tooltips, popups, attribution, zoom, and the Map/Satellite
   control alike - beneath this one root-level slot, without touching
   any of their own z-index values or relative order to each other. */
.map-panel {
  position: relative;
  z-index: 0;
  height: 420px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-subtle);
  border: 1px solid var(--color-border);
}

#map {
  width: 100%;
  height: 100%;
}

.print-frame {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 500;
  transform: translate(-50%, -50%);
  border: 3px solid #ffffff;
  border-radius: 3px;
  box-shadow: 0 0 0 2px rgba(17, 17, 17, 0.9), 0 0 0 9999px rgba(17, 17, 17, 0.3);
  pointer-events: none;
  transition: width 320ms cubic-bezier(0.4, 0, 0.2, 1), height 320ms cubic-bezier(0.4, 0, 0.2, 1);
}

.print-frame.print-frame-enter {
  animation: printFrameEnter 320ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Shape Display Phase 3A.1 correction pass (human-eval follow-up),
   generalized in Phase 3B.2.3 into a reusable canonical-polygon
   renderer - real dimming mask + ring outline for every canonical-ring
   shape (see canonicalBoundaryRingsByShapeId lookup in app.js), all
   real SVG geometry built per-point from that shape's own live-fetched
   canonical ring by applyPrintFrameBoundaryShape() in app.js - see
   index.html's own comment on #print-frame-polygon-svg for why plain
   CSS (border+clip-path, or box-shadow+clip-path) cannot render either
   of these correctly. Class/id names are deliberately shape-neutral
   ("polygon" meaning "any canonical-ring shape", never a literal
   geometric octagon or similar) - this CSS has no shape-specific
   geometry of its own; only app.js's own shape-keyed ring lookup
   decides which product shapes use it. Sized to the FULL map-panel
   (not just #print-frame's own smaller box) so the dimming mask covers
   the entire visible map, including the corner regions between the
   polygon and its square bounding box - .map-panel's own existing
   `overflow: hidden` (see that rule above) is what keeps it from
   painting outside the visible map area, replacing box-shadow's old
   9999px-spread hack with an exact, correctly-sized fill instead of an
   oversized approximation. pointer-events: none is required (not just
   decorative) - without it, this element (positioned above the map)
   would intercept every drag/click/zoom gesture. Empty (no
   mask/outline drawn) for every product without a canonical ring -
   #print-frame's existing border/box-shadow below keep doing the
   entire job for Square/Rectangle, completely untouched by this
   element's existence. */
.print-frame-polygon-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 499;
}

.print-frame-polygon-svg .print-frame-polygon-mask {
  fill: rgba(17, 17, 17, 0.3);
  fill-rule: evenodd;
}

.print-frame-polygon-svg .print-frame-polygon-ring {
  fill: none;
  stroke: #ffffff;
  stroke-width: 3px;
}

/* #print-frame's own square border/box-shadow are both hidden whenever
   the SVG mask+ring above are active, so only the true ring-driven
   outline and mask show - never a square border/edge-shadow visible
   behind/around them at the corners the polygon doesn't reach. */
.print-frame.print-frame--polygon-boundary {
  border-color: transparent;
  box-shadow: none;
}

@keyframes printFrameEnter {
  from {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.92);
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .print-frame {
    transition: none;
  }

  .print-frame.print-frame-enter {
    animation: none;
  }
}

.print-frame::before,
.print-frame::after {
  content: "";
  position: absolute;
  background: rgba(255, 255, 255, 0.85);
}

.print-frame::before {
  left: 50%;
  top: -8px;
  bottom: -8px;
  width: 1px;
}

.print-frame::after {
  top: 50%;
  left: -8px;
  right: -8px;
  height: 1px;
}

/* Inset just inside the frame's own top edge, not floating above it
   (was top: -34px, meaning "34px above the frame") - a large frame
   (near the map panel's full height) put that floating position close
   enough to the map panel's own top edge to visually crowd whichever
   corner control lives there (previously Map/Satellite at top-right,
   now the zoom control at top-left after that one moved - see this
   feature's own report). Sitting inside the frame's own boundary
   instead keeps it structurally tied to "labels the frame", not "sits
   in the map's general corner-control space", regardless of how large
   the frame gets. */
.print-frame-label {
  position: absolute;
  left: 50%;
  top: 10px;
  transform: translateX(-50%);
  max-width: calc(100% - 12px);
  padding: 5px 9px;
  border-radius: 999px;
  background: rgba(17, 17, 17, 0.9);
  color: #ffffff;
  font-family: var(--font-family-mono);
  font-size: 11px;
  font-weight: 700;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.map-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.btn-compact {
  min-height: 40px;
  padding: 8px 14px;
  font-size: 13px;
}

/* Shape Display Phase 2B correction - narrowly scoped to this ONE
   button, not a broad `.btn[hidden]` change: the shared `.btn` class
   sets `display: inline-flex` with no paired `[hidden]` override
   anywhere in this file (a pre-existing gap, confirmed to predate this
   feature entirely - CUBE, an existing, untouched square product,
   already exhibited the identical "hidden property true, still
   visually rendered" behavior before Shape Display existed). Widening
   this fix to every `.btn` in the app is out of this feature's own
   scope and risks unaudited side effects elsewhere; this single,
   id-scoped pairing only ever affects the one control Shape Display's
   own isSquareProduct()/updateRotateButtonUi() toggle. */
#rotate-selection-button[hidden] {
  display: none;
}

.selection-summary {
  display: grid;
  gap: 2px;
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-success-border);
  border-radius: var(--radius-md);
  background: var(--color-success-bg);
  color: var(--color-text-primary);
}

.selection-summary-label {
  font-family: var(--font-family-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-success);
}

.selection-summary strong {
  font-family: var(--font-family-mono);
  font-size: 15px;
}

#selection-message {
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-secondary);
}

/* Four detail tiers - good/moderate share the same success styling
   (both are "everything will look fine," just at different resolutions
   the live width/height/area readout already shows); large and
   too-large get their own warning/error treatment. Selectors target
   #selection-message by id, never span:last-child - the shrink-to-fit
   button below is also a child of this same container and must never
   be picked up by a positional selector meant for the message text. */
.selection-summary.is-large {
  border-color: var(--color-warning-border);
  background: var(--color-warning-bg);
}

.selection-summary.is-large .selection-summary-label,
.selection-summary.is-large #selection-message {
  color: var(--color-warning);
}

.selection-summary.is-too-large {
  border-color: var(--color-error-border);
  background: var(--color-error-bg);
}

.selection-summary.is-too-large .selection-summary-label,
.selection-summary.is-too-large #selection-message {
  color: var(--color-error);
}

.shrink-to-fit-button {
  margin-top: var(--space-2);
  justify-self: start;
}

.place-continue-bar {
  margin-top: var(--space-5);
}

.place-continue-bar .btn-block {
  margin-top: 0;
}

.place-continue-bar .feedback-text {
  margin-bottom: 0;
}

.leaflet-control-zoom a {
  width: 40px;
  height: 40px;
  line-height: 40px;
}

/* On-map "locate me" control - .leaflet-bar (on its container, set in
   app.js) already supplies the white background/border/shadow every
   other Leaflet control here uses; this only needs its own button
   sizing (matching the 40px touch target above) and icon. Colors are
   deliberately literal (Leaflet's own #fff/#333 defaults), not this
   app's theme tokens - the zoom and layer controls sitting right next
   to it are unthemed Leaflet chrome too (map tiles aren't theme-aware
   either), so matching them keeps every map control visually
   consistent instead of this one alone looking mismatched in dark
   mode. */
.map-locate-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: none;
  background: #fff;
  color: #333;
  cursor: pointer;
  touch-action: manipulation;
}

.map-locate-button:hover {
  background: #f4f4f4;
}

.map-locate-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.map-locate-button svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.leaflet-control-layers-expanded {
  padding: 7px 10px;
  font: 13px/1.5 "Inter", sans-serif;
}

.leaflet-control-layers-selector {
  width: 16px;
  height: 16px;
  margin-right: 5px;
  vertical-align: -3px;
}

/* ---------- Display grid ---------- */

.classic-displays-disclosure {
  margin-top: var(--space-5);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  box-shadow: var(--shadow-subtle);
}

.classic-displays-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  cursor: pointer;
  list-style: none;
}

.classic-displays-summary::-webkit-details-marker {
  display: none;
}

.classic-displays-summary::after {
  content: "+";
  flex: 0 0 auto;
  font-family: var(--font-family-mono);
  font-size: 22px;
  line-height: 1;
  color: var(--color-text-secondary);
}

.classic-displays-disclosure[open] .classic-displays-summary::after {
  content: "−";
}

.classic-displays-summary:hover,
.classic-displays-summary:focus-visible {
  background: var(--color-accent-06);
}

.classic-displays-summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.classic-displays-heading {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 15px;
  font-weight: 700;
}

.classic-displays-summary-note {
  margin-left: auto;
  font-size: 12px;
  color: var(--color-text-secondary);
}

.classic-displays-content {
  padding: 0 var(--space-4) var(--space-4);
  border-top: 1px solid var(--color-border);
}

.classic-displays-content > .feedback-text:first-child {
  margin-top: var(--space-4);
}

/* Landing hierarchy redesign - three Classic cards only (Block/Cube/
   Vista); Shape Display is no longer one of these cards, see
   .shape-display-featured above it in the DOM/markup. */
.display-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-4);
}

.display-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-subtle);
  /* Landing hierarchy redesign - Classic Displays is now the secondary
     section (the featured Shape Display panel above carries the
     visual weight), so this card's own padding/proportions are
     deliberately smaller than before - see .shape-display-featured. */
  padding: var(--space-4) var(--space-3);
  cursor: pointer;
  font-family: inherit;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

.display-badge {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  padding: 4px 8px;
  border-radius: 999px;
  background: var(--color-accent);
  color: #fff;
  font-family: var(--font-family-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  text-transform: uppercase;
}

.display-card:hover {
  border-color: var(--color-accent);
  transform: translateY(-2px);
}

.display-card.is-selected {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

.display-card.is-selected .display-name::after {
  content: "✓";
  margin-left: var(--space-2);
  color: var(--color-accent);
  font-weight: 700;
}

.display-proportion {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: 76px;
  margin-bottom: var(--space-2);
}

.display-proportion-box {
  background: var(--color-bg);
  border: 1.5px solid var(--color-text-tertiary);
  border-radius: 3px;
}

.display-name {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 16px;
  font-weight: 600;
}

.display-comparison {
  margin: 0;
  font-size: 13px;
  color: var(--color-text-secondary);
}

.display-best-for {
  margin: 0;
  font-size: 12px;
  color: var(--color-text-tertiary);
  min-height: 30px;
}

.display-includes {
  margin: var(--space-1) 0 0;
  font-size: 12px;
  color: var(--color-text-secondary);
}

.display-price {
  margin: var(--space-1) 0 0;
  font-family: var(--font-family-mono);
  font-size: 15px;
  font-weight: 700;
}

/* ---------- Shape Display featured panel (landing hierarchy redesign) ----------
   Same required convention as .shape-display-picker below: a class rule
   that sets its own `display` must be paired with a same-specificity
   [hidden] override, or the native hidden attribute would be silently
   defeated once any class-based display rule exists. */

.shape-display-featured {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  margin-bottom: var(--space-5);
  padding: var(--space-5);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-lg);
  background: var(--color-accent-06);
  box-shadow: var(--shadow-subtle);
}

.shape-display-featured[hidden] {
  display: none;
}

.shape-display-featured-badge {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--color-accent);
  color: #fff;
  font-family: var(--font-family-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  line-height: 1.6;
  text-transform: uppercase;
}

.shape-display-featured-heading {
  margin: var(--space-2) 0 0;
  font-family: var(--font-family-mono);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.shape-display-featured-description {
  margin: 0;
  font-size: 14px;
  color: var(--color-text-secondary);
}

.shape-display-featured-price {
  margin: var(--space-1) 0 0;
  font-family: var(--font-family-mono);
  font-size: 18px;
  font-weight: 700;
  color: var(--color-text-primary);
}

.shape-display-featured-instruction {
  margin: var(--space-3) 0 0;
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
}

/* The shared .shape-display-picker is already visually a bordered card
   of its own (see below) - inside the featured panel it sits on the
   featured panel's own card chrome instead, so its outer border/
   background/margin are neutralized here without touching the shared
   base rule the map-screen family panel still relies on unchanged. */
.shape-display-featured .shape-display-picker {
  width: 100%;
  margin-top: var(--space-2);
  padding: 0;
  border: 0;
  background: transparent;
}

/* ---------- Theme picker + Frame Text ---------- */

.theme-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
}

.theme-swatch-button {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  cursor: pointer;
  text-align: left;
  font-family: var(--font-family-mono);
  font-size: 14px;
  color: var(--color-text-primary);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.theme-swatch-button:hover {
  background: var(--color-surface-hover);
}

.theme-swatch-button:focus-visible {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

.theme-swatch-button.is-selected {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

.theme-swatch-button.is-selected .theme-swatch-name::after {
  content: "✓";
  margin-left: var(--space-2);
  color: var(--color-accent);
  font-weight: 700;
}

.theme-swatch-dots {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  flex-shrink: 0;
}

.color-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.12);
  flex-shrink: 0;
}

.theme-swatch-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.theme-swatch-name {
  font-weight: 500;
}

.theme-swatch-description {
  /* Explicit override - stays Inter (informational/description prose,
     see this feature's own report) despite its parent
     .theme-swatch-button now being mono; without this it would
     otherwise inherit the mono font-family. */
  font-family: var(--font-family-sans);
  font-size: 12px;
  color: var(--color-text-secondary);
}

.theme-palette-preview {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-secondary);
}

.theme-palette-preview .color-dot {
  width: 16px;
  height: 16px;
}

/* ---------- Advanced options (Place screen) - Nearby trails/Add a
   route both stay pre-generation (see app.js's own state-machine
   docstring for why), collapsed by default via native <details> so
   neither ever reads as a required decision. ---------- */

.advanced-options {
  margin-bottom: var(--space-5);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
}

/* Race Mode exclusive (see renderRaceModeSection in app.js, which
   toggles this element's own `hidden`) - always visible only alongside
   .race-mode-on, so its border picks up the race accent instead of the
   neutral default. border-color specifically, not the whole --color-
   accent cascade this card already gets for free elsewhere: --color-
   border is its own neutral token, never redefined by .race-mode-on. */
.configurator-main-card.race-mode-on .advanced-options {
  border-color: var(--color-accent);
}

.advanced-options[open] {
  padding-bottom: var(--space-4);
}

.advanced-options-summary {
  cursor: pointer;
  font-family: var(--font-family-mono);
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text-primary);
  list-style: none;
  padding: var(--space-2) 0;
}

.advanced-options-summary::-webkit-details-marker {
  display: none;
}

.advanced-options-summary::before {
  content: "▸ ";
  color: var(--color-text-tertiary);
}

.advanced-options[open] .advanced-options-summary::before {
  content: "▾ ";
}

.advanced-options-body {
  margin-top: var(--space-3);
}

.advanced-options-helper {
  margin: 0 0 var(--space-3);
  font-size: 13px;
  color: var(--color-text-secondary);
}

/* ---------- Nearby trails (Place screen, inside Advanced options) ---------- */

.trails-control {
  margin-bottom: var(--space-5);
}

.trails-color-picker {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.trail-color-swatch {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  background: var(--color-surface);
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.trail-color-swatch:hover {
  background: var(--color-surface-hover);
}

/* Dark-neutral rather than accent blue - this ring is purely a
   "this one is chosen" indicator layered on top of the swatch's own
   already-meaningful trail color (set via app.js, untouched here), so
   it qualifies as decorative selected-state chrome (see this
   feature's own report) rather than a functional affordance. A dark
   ring also reads consistently regardless of the swatch's own hue,
   including when that hue happens to be blue itself. */
.trail-color-swatch.is-selected {
  border-color: var(--color-selected-ring);
  box-shadow: 0 0 0 3px var(--color-selected-ring-soft);
}

.trail-color-swatch-dot {
  width: 18px;
  height: 18px;
}

/* ---------- Add a route (Place screen, inside Advanced options) -
   visually distinct from Nearby trails above: a card-style upload/
   summary block, never sharing a class name with the trail-color-
   swatch/-picker classes even though the swatch look is intentionally
   similar. ---------- */

.your-route-control {
  margin-bottom: var(--space-5);
}

.your-route-helper-row {
  margin: 0 0 var(--space-3);
}

.your-route-helper {
  margin: 0 0 var(--space-1);
  font-size: 13px;
  color: var(--color-text-secondary);
}

/* .add-route-help (a <details>, block by default) sits on its own
   short line right below the helper text - deliberately not squeezed
   inline into a flex row, so its content box gets the full container
   width when open instead of wrapping into a narrow column. */

/* Tap-to-reveal help, not hover - works identically on mobile and
   desktop, no separate mobile-only affordance to maintain. Content
   expands in normal document flow (not a floating popover) so it can
   never collide with a viewport edge or need z-index management -
   simple by construction, matching this feature's own "keep it
   simple, text only for now" scope. */
.add-route-help-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  list-style: none;
  user-select: none;
  font-size: 13px;
  color: var(--color-text-secondary);
}

.add-route-help-trigger::-webkit-details-marker {
  display: none;
}

.add-route-help-icon {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1.5px solid var(--color-text-secondary);
  font-family: var(--font-family-mono);
  font-size: 11px;
  font-weight: 700;
}

.add-route-help-trigger:hover,
.add-route-help-trigger:focus-visible {
  color: var(--color-accent);
}

.add-route-help-trigger:hover .add-route-help-icon,
.add-route-help-trigger:focus-visible .add-route-help-icon {
  border-color: var(--color-accent);
}

.add-route-help-trigger:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.add-route-help[open] .add-route-help-trigger,
.add-route-help[open] .add-route-help-icon {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.add-route-help-content {
  margin-top: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-hover);
  font-size: 13px;
  line-height: 1.5;
  color: var(--color-text-secondary);
}

.add-route-help-content p {
  margin: 0 0 var(--space-2);
}

.add-route-help-content p:last-child {
  margin-bottom: 0;
}

.add-route-help-content ul {
  margin: 0 0 var(--space-2);
  padding-left: var(--space-4);
}

.add-route-help-content li {
  margin-bottom: var(--space-1);
}

.add-route-help-content strong {
  color: var(--color-text-primary);
}

.your-route-upload-button {
  display: inline-flex;
}

.your-route-upload-button[hidden] {
  display: none;
}

.your-route-uploaded-card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  background: var(--color-surface);
}

.your-route-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.your-route-filename {
  font-family: var(--font-family-mono);
  font-weight: 600;
  font-size: 14px;
  color: var(--color-text-primary);
}

.your-route-stats {
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-secondary);
}

.your-route-color-picker {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-3) 0;
}

.route-color-swatch {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  background: var(--color-surface);
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.route-color-swatch:hover {
  background: var(--color-surface-hover);
}

/* Same reclassification as .trail-color-swatch.is-selected above -
   see its own comment. */
.route-color-swatch.is-selected {
  border-color: var(--color-selected-ring);
  box-shadow: 0 0 0 3px var(--color-selected-ring-soft);
}

.route-color-swatch-dot {
  width: 18px;
  height: 18px;
}

.your-route-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-2);
}

/* ---------- Route builder (Phase 3) ---------- */
/* .route-builder-panel reuses .your-route-uploaded-card's own border/
   padding/background - see index.html - so only genuinely new layout
   rules live here. */

.route-builder-status {
  margin: 0 0 var(--space-3);
  font-size: 13px;
  color: var(--color-text-secondary);
}

.route-builder-alternatives {
  margin-bottom: var(--space-3);
}

.route-builder-alternatives-label {
  margin: 0 0 var(--space-2);
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-primary);
}

.route-builder-alternatives-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
}

.route-builder-alt-button {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: var(--font-family-sans);
  font-size: 13px;
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}

.route-builder-alt-button:hover {
  background: var(--color-surface-hover);
}

.route-builder-alt-button:focus-visible {
  border-color: var(--color-selected-ring);
  box-shadow: 0 0 0 3px var(--color-selected-ring-soft);
}

.route-builder-alt-button:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* Alternative-route clarity correction - requirement 6: a strong,
   persistent selected state, distinct from the transient :hover/
   :focus-visible treatments above (which remain unchanged - hover/
   focus already reads clearly on their own; .is-selected only needs to
   ALSO look clearly selected once the mouse/keyboard has moved on). */
.route-builder-alt-button.is-selected {
  border-color: var(--color-selected-ring);
  background: var(--color-accent-08);
  box-shadow: 0 0 0 3px var(--color-selected-ring-soft);
}

.route-builder-alt-button.is-dimmed {
  opacity: 0.5;
}

/* Requirement 2: letter badge - matching identity with the map line's
   own color (routeBuilderCandidateStyle in app.js is the single source
   of truth for these 4 hues; kept in sync here since Leaflet path
   colors are set via JS, not CSS - see createRouteBuilderCandidateBadge's
   own comment). Redundant with the map line's dash pattern and the
   comparison text below - never color alone. */
.route-builder-alt-badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 700;
  color: #ffffff;
}

.route-builder-alt-badge-a { background: #2563EB; }
.route-builder-alt-badge-b { background: #7C3AED; }
.route-builder-alt-badge-c { background: #DB2777; }
.route-builder-alt-badge-d { background: #0891B2; }

.route-builder-alt-info {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-width: 0;
}

.route-builder-alt-distance {
  font-weight: 600;
}

.route-builder-alt-comparison {
  color: var(--color-text-secondary);
  font-size: 12px;
}

/* Requirement 3: "Shortest" gets a redundant (text ALREADY says it;
   this is additive, never the only cue) success-colored reinforcement. */
.route-builder-alt-comparison.is-shortest {
  color: var(--color-success);
  font-weight: 600;
}

/* Phase 2 - the three-method selector replaces the single arm button
   (see index.html's own comment). Explicit method selection visibly
   changes appearance while active (aria-pressed="true" carries the
   same information to assistive tech; this is the sighted-customer
   equivalent) - same visual treatment the old arm button used. */
.route-point-method-selector {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

/* This project's own established convention (see .feedback-text[hidden]/
   .view[hidden]/.your-route-upload-button[hidden] elsewhere in this
   file, and #route-builder-start-button's own neighboring comment on
   why): a class rule that sets its own `display` must be paired with a
   same-specificity [hidden] override, since a plain class selector and
   the UA stylesheet's own `[hidden] { display: none }` rule are NOT
   `!important` - an author-origin class rule beats it regardless of
   selector order. Found missing here during a release-candidate audit
   (real, browser-measured: the group and its three buttons stayed
   visually rendered, though correctly disabled, while
   methodGroupEl.hidden/el.hidden were true during a busy state) - .btn's
   own shared `display: inline-flex` has the identical unpaired gap, but
   is deliberately left untouched here (route-builder-scoped fix only,
   matching this file's own established "never change the shared .btn-
   link/.btn-compact classes themselves" principle - see the 44px touch-
   target block below) by scoping this override to buttons specifically
   inside this container. */
.route-point-method-selector[hidden],
.route-point-method-selector button[hidden] {
  display: none;
}

.route-point-method-selector button[aria-pressed="true"] {
  background: var(--color-selected-ring-soft);
  border-color: var(--color-selected-ring);
  color: var(--color-text-primary);
}

.route-point-map-instruction {
  margin: 0 0 var(--space-3);
  font-size: 13px;
  color: var(--color-text-secondary);
}

/* Destination-placement UX correction - "one obvious primary action"
   (requirements 3/7): app.js's own renderRouteBuilderSection toggles
   the arm/next-destination button between the existing .btn-primary
   and .btn-secondary classes (both already defined above) depending on
   state - never a new button-visual class here. Only the destination-
   related states (awaiting a destination click, unarmed) get
   .btn-primary; the starting-point button (already understood to work
   well, per real production feedback) keeps its original .btn-secondary
   look untouched. */

/* Prominent "make the next step unmistakable" instruction - toggled
   only for the unarmed, awaiting-a-destination-click states (see
   app.js's own renderRouteBuilderSection). Still the SAME #route-
   builder-status aria-live element as every other status message -
   only the visual size/weight changes here, never a second live
   region. */
.route-builder-status.is-prominent {
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-accent-06);
}

.route-builder-status-headline {
  display: block;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--color-text-primary);
  margin-bottom: 2px;
}

.route-builder-status-subtext {
  display: block;
  font-size: var(--font-size-base);
  color: var(--color-text-secondary);
}

/* Production-evaluation UX correction: one contextual action,
   RELOCATED (not mirrored - see app.js's own renderRouteBuilderSection,
   "Sticky contextual action" block, and the *HomeParent constants'
   own comment) into this dock from wherever its own control (proposal
   confirm / choose-route / finish-route) normally lives. Desktop AND
   mobile both use position: fixed - an earlier sticky-within-the-panel
   desktop treatment was found, by real-browser measurement, to still
   scroll out of the viewport at some scroll positions (the panel and
   the map are DIFFERENT elements - see this section's own comment in
   index.html), so a genuine viewport guarantee needs fixed positioning
   on both breakpoints, not just mobile. --dock-left/--dock-width are
   set by app.js's own updateStickyActionDockGeometry (on every render
   AND on window resize) from the route-builder panel's own live
   bounding rect, capped at a sane max-width - visually aligned with/
   under that panel, never an edge-to-edge bar across a wide browser
   window. Mobile's own @media (max-width: 560px) override further
   below replaces these with the full viewport width instead,
   preserving that breakpoint's existing edge-to-edge behavior
   unchanged. --sticky-bottom-offset (both breakpoints) keeps this dock
   stacked directly above .place-continue-bar, never overlapping it. */
.route-builder-sticky-actions {
  position: fixed;
  left: var(--dock-left, 0px);
  width: var(--dock-width, 480px);
  bottom: var(--sticky-bottom-offset, 0px);
  z-index: 20;
  display: flex;
  padding: var(--space-3);
  background: rgba(250, 250, 250, 0.96);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  backdrop-filter: blur(10px);
}

/* Author-origin `.route-builder-sticky-actions { display: flex }` above
   (and the shared `.btn` class its own button uses) would otherwise
   beat the browser's own UA-origin `[hidden] { display: none }` rule
   regardless of source order - same established pattern as this file's
   own .route-point-method-selector[hidden] etc. elsewhere. */
.route-builder-sticky-actions[hidden] {
  display: none;
}

.route-builder-sticky-actions .btn {
  width: 100%;
  min-height: 44px;
}

/* Same unpaired-[hidden] gap as .route-builder-sticky-actions itself
   above - #route-builder-sticky-busy-label's own `.btn`/`.btn-primary`
   classes are author-origin and would otherwise beat the browser's
   UA-origin `[hidden] { display: none }` rule regardless of source
   order or specificity, leaving a disabled ghost button occupying real
   layout space in the dock (found via a real, deterministic browser
   test: it was winning elementFromPoint over the actual relocated
   confirm/choose/finish button at some viewports). */
#route-builder-sticky-busy-label[hidden] {
  display: none;
}

/* Guarantees nothing between the route-builder panel and .place-
   continue-bar (search, shape selector, map actions, advanced options,
   etc.) is ever left permanently unreachable behind the fixed dock -
   applies at every width now that the dock is position: fixed on
   desktop too, not just inside the mobile breakpoint. Toggled on
   <body> only while the dock is actually visible (see app.js's own
   sticky-action block). */
body.has-route-builder-sticky-action {
  padding-bottom: 88px;
}

/* Visible armed-state cue "on and around the map" (requirement 4) -
   crosshair cursor plus an inset highlight ring on the map itself, and
   a small floating banner (destination-arming only - see app.js) atop
   it. Purely additive to the existing .map-panel rule above; toggled
   via JS only while builderState.armed is true. */
.map-panel.is-armed-for-placement {
  box-shadow: inset 0 0 0 3px var(--color-selected-ring);
}

.map-panel.is-armed-for-placement #map,
.map-panel.is-armed-for-placement .leaflet-container {
  cursor: crosshair;
}

/* Compact strip, NORMAL DOCUMENT FLOW, directly above .map-panel (see
   index.html) - replaces the former absolutely-positioned on-map
   banner, which overlapped Leaflet's own top-right Map/Satellite
   control at mobile widths. No `display` override here on purpose -
   the browser's own default `[hidden] { display: none }` UA rule
   applies unmodified, so it reserves no space while hidden. */
.route-builder-map-instruction-strip {
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-text-primary);
  color: var(--color-on-primary);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  text-align: center;
}

/* Compact progress indicator (requirement 9) - decorative only (see
   index.html's own aria-hidden comment on #route-builder-progress).
   Arrow connectors are CSS-generated content, never real text nodes -
   nothing here is ever read by a screen reader. */
.route-builder-progress {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
  font-size: var(--font-size-sm);
  color: var(--color-text-tertiary);
}

.route-builder-progress-step {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.route-builder-progress-step:not(:last-child)::after {
  content: "→";
  margin-left: var(--space-1);
  color: var(--color-text-tertiary);
}

.route-builder-progress-step.is-current {
  color: var(--color-text-primary);
  font-weight: var(--font-weight-semibold);
}

.route-builder-progress-step.is-done {
  color: var(--color-success);
}

.route-builder-progress-step.is-done::before {
  content: "✓";
  margin-right: 2px;
}

/* Numbered start/destination markers and the distinct proposed-
   (uncommitted-)destination marker (requirement 8) - a Leaflet
   permanent Tooltip on top of the existing L.circleMarker, not a
   switch to L.marker/image icons (see redrawRouteBuilderLayers's own
   comment on why this codebase avoids those). Overrides Leaflet's own
   default tooltip chrome (background/border/arrow) entirely. */
.route-builder-marker-label,
.route-builder-candidate-badge {
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0 !important;
  color: #ffffff;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  text-align: center;
  pointer-events: none;
}

.route-builder-marker-label::before,
.route-builder-candidate-badge::before {
  display: none;
}

/* Alternative-route clarity correction - requirement 6: a persistently
   previewed candidate's badge stays full-strength; a dimmed one (some
   OTHER candidate is active) fades - the underlying circleMarker's own
   opacity/fillOpacity is set directly in JS (applyRouteBuilderCandidate
   Emphasis, Leaflet's own setStyle), this only fades the TEXT to match. */
.route-builder-candidate-badge.is-dimmed {
  opacity: 0.35;
}

.route-builder-candidate-badge.is-selected {
  font-size: 13px;
}

.route-builder-coordinate-entry {
  margin-bottom: var(--space-3);
}

.route-builder-coordinate-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

.route-builder-coordinate-row input[type="text"] {
  flex: 1 1 100px;
  min-width: 0;
}

/* Phase 2 - address/place search for a route point. Deliberately its
   own namespace (.route-point-search-*), distinct from the general
   location search's .search-row/#search-input further down this page -
   the two are unrelated features that happen to look similar. */
.route-point-search {
  margin-bottom: var(--space-3);
}

/* Stage B (pin-first UX): explicitly frames search as approximate -
   "select a result, then drag the pin" - never implies an exact match. */
.route-point-search-helper {
  margin: 0 0 var(--space-2);
  font-size: 12px;
  color: var(--color-text-secondary);
}

.route-point-search-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

.route-point-search-row input[type="text"] {
  flex: 1 1 160px;
  min-width: 0;
}

.route-point-search-status {
  margin: var(--space-2) 0 0;
  font-size: 13px;
  color: var(--color-text-secondary);
}

.route-point-search-results {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  padding: 0;
  max-height: 260px;
  overflow-y: auto;
}

/* Same [hidden]-pairing gap as .route-point-method-selector above (see
   that rule's own comment for the full explanation) - has no visible
   effect today ONLY because renderRouteSearchUI always clears this
   list's own textContent in every branch where it's hidden, but fixed
   here anyway for defensive correctness (a future change that stops
   clearing content on hide would otherwise silently resurrect stale
   results on screen). */
.route-point-search-results[hidden] {
  display: none;
}

.route-point-search-result-button {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: var(--font-family-sans);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.route-point-search-result-button:hover {
  background: var(--color-surface-hover);
}

.route-point-search-result-button:focus-visible {
  border-color: var(--color-selected-ring);
  box-shadow: 0 0 0 3px var(--color-selected-ring-soft);
}

.route-point-search-result-button:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

.route-point-search-result-label {
  font-weight: 600;
}

.route-point-search-result-detail {
  font-size: 12px;
  color: var(--color-text-secondary);
}

/* Requirement: attribution visible immediately beside/beneath results,
   whenever results are displayed - a plain, always-legible text line
   with a real, linked citation (never decorative-only). Shared by the
   route-builder's own search results AND the general map-area location
   search (Phase 3) - same rule, both callers use search-derived
   results from the same backend endpoint. */
.route-point-search-attribution,
.search-attribution {
  margin: var(--space-2) 0 0;
  font-size: 12px;
  color: var(--color-text-tertiary);
}

.route-point-search-attribution a,
.search-attribution a {
  color: inherit;
  text-decoration: underline;
}

/* Coverage advisory - shown whenever the point a tap/coordinate-entry/
   search-result just placed falls outside the advertised coverage
   area. Not inside a staging wrapper (there is none, post tap-and-go) -
   sits directly under #route-builder-status. */
.route-point-proposal-coverage-advisory {
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: 12px;
  color: var(--color-text-secondary);
  background: var(--color-surface);
}

.route-builder-containment {
  margin: 0 0 var(--space-3);
  font-size: 13px;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
}

.route-builder-containment.is-fully-inside {
  color: var(--color-text-primary);
  border-color: var(--color-border);
}

.route-builder-containment.is-partial,
.route-builder-containment.is-fully-outside {
  color: var(--color-error);
  border-color: var(--color-error);
}

.route-builder-confirm {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.route-builder-confirm p {
  margin: 0 0 var(--space-2);
  font-size: 13px;
  color: var(--color-text-primary);
}

.route-builder-confirm-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* Touch targets (Phase 5 accessibility correction) - route-builder-
   scoped only, deliberately NOT a change to the shared .btn-link/
   .btn-compact classes themselves (those are used throughout the whole
   app; changing them globally risks an unrelated visual regression
   elsewhere - see this correction's own instructions). Covers every
   interactive control inside #route-builder-panel (arm/add-destination,
   coordinate submission, alternatives, undo, clear, finalize, cancel,
   confirmation buttons, retry) plus #route-builder-start-button (the
   entry point, which lives outside the panel in the upload row).

   .btn-link instances (undo/clear/exit/retry/"Keep editing") keep their
   existing small, text-like DESKTOP appearance - min-height here is an
   invisible hit-area expansion via padding, not a visible size change,
   preserving the existing visual hierarchy exactly as it reads today.
   .btn-compact instances (arm button, "Use these coordinates", the
   confirm "yes" button) and the alternative-route buttons get a small,
   visible bump (40px/~35px -> 44px) - a few pixels, not a redesign. */
#route-builder-panel .btn-link {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-1);
}

/* Corrected during the sticky-action-relocation audit: this rule's own
   comment above already flagged the unpaired-[hidden] risk in theory
   ("deliberately left untouched" for .btn's shared version) but this
   route-builder-scoped one was itself missing the pairing - a REAL,
   test-verified defect once #route-builder-choose-button started being
   relocated out of its normal DOM position (see app.js's own "Sticky
   contextual action" block): #route-builder-error-retry-button, hidden
   and inert while no error is showing, stayed keyboard-focusable
   (computed display: inline-flex, not none) purely because Tab could
   now reach it earlier than before. Same established pairing pattern
   as .feedback-text[hidden]/.view[hidden]/.your-route-upload-button
   [hidden] elsewhere in this file. */
#route-builder-panel .btn-link[hidden] {
  display: none;
}

/* min-height ONLY, deliberately no display/align-items override here -
   #route-builder-start-button[hidden] must keep winning against its own
   .your-route-upload-button base rule (see that selector's own [hidden]
   pairing above, "Standardize theme palettes" era fix) - an ID selector
   setting `display` unconditionally, even to the SAME inline-flex value
   the button already has while visible, has higher specificity than a
   class+attribute [hidden] rule and would silently defeat it, exactly
   the bug that fix exists to prevent. */
#route-builder-start-button {
  min-height: 44px;
}

#route-builder-panel .btn-compact {
  min-height: 44px;
}

.route-builder-alt-button {
  min-height: 44px;
}

.route-point-search-result-button {
  min-height: 44px;
}

/* ---------- Loading view ---------- */

#loading-view {
  display: flex;
  justify-content: center;
  padding: var(--space-6);
}

.loading-card {
  width: 100%;
  max-width: 480px;
  text-align: center;
  background: var(--color-surface);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-subtle);
  padding: var(--space-6) var(--space-5);
}

.loading-heading {
  margin: 0 0 var(--space-1);
  font-family: var(--font-family-mono);
  font-size: 24px;
  font-weight: 600;
  letter-spacing: -0.01em;
  outline: none;
}

.loading-subheading {
  margin: 0 0 var(--space-5);
  font-size: 14px;
  color: var(--color-text-secondary);
}

.loading-glyph-wrap {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-5);
  color: var(--color-text-tertiary);
  animation: pulseGlyph 2.6s ease-in-out infinite;
}
/* With the tesseract mounted (app.js tesseract(); tesseract.js) the sign takes the glyph's place: the wrap
   becomes its stage, the old cube goes, the pulse stops. */
.loading-glyph-wrap.has-tesseract { position: relative; height: 190px; animation: none; }
.loading-glyph-wrap.has-tesseract .loading-glyph { display: none; }
.loading-glyph-wrap.has-tesseract .tess svg { width: 150px; }

.stepper {
  list-style: none;
  margin: 0 auto var(--space-5);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 280px;
  text-align: left;
}

.step {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

.step::before {
  content: "○";
  flex-shrink: 0;
  width: 1.2em;
  font-size: 14px;
  color: var(--color-text-tertiary);
}

.step-label {
  font-family: var(--font-family-mono);
  font-size: 14px;
  color: var(--color-text-tertiary);
}

.step-done .step-label {
  color: var(--color-text-secondary);
}

.step-done::before {
  content: "✓";
  color: var(--color-text-primary);
}

.step-active .step-label {
  color: var(--color-text-primary);
  font-weight: 600;
}

.step-active::before {
  content: "●";
  color: var(--color-accent);
  animation: pulseDot 1.6s ease-in-out infinite;
}

.loading-footnote {
  margin: 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--color-text-tertiary);
}

.loading-dots span {
  display: inline-block;
  animation: dotFade 1.4s ease-in-out infinite;
}

.loading-dots span:nth-child(2) {
  animation-delay: 0.2s;
}

.loading-dots span:nth-child(3) {
  animation-delay: 0.4s;
}

.error-card {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
  text-align: left;
}

.error-heading {
  margin: 0 0 var(--space-2);
  font-family: var(--font-family-mono);
  font-size: 16px;
  font-weight: 600;
  color: var(--color-error);
}

.error-message {
  margin: 0 0 var(--space-4);
  font-family: var(--font-family-mono);
  font-size: 14px;
  color: var(--color-text-secondary);
}

.error-card .actions-row {
  justify-content: flex-start;
  margin-top: 0;
}

/* ---------- Reveal + Review (absorbs the former Checkout) ---------- */

#review-view {
  display: flex;
  flex-direction: column;
  max-width: 1100px;
  width: 100%;
  margin: 0 auto;
  padding: var(--space-6);
}
/* The model is the star of this screen (Rizwan, 2026-09-19): the card
   takes most of the viewport on a desk, and on a phone it comes FIRST -
   before the heading and the Customize button - full width, tall. */
#review-summary-panel { display: flex; flex-direction: column; }

.review-heading {
  margin: 0 0 var(--space-1);
  font-family: var(--font-family-mono);
  font-size: 28px;
  font-weight: 600;
  letter-spacing: -0.01em;
  outline: none;
  text-align: center;
}

.review-subheading {
  margin: 0 0 var(--space-5);
  font-size: 15px;
  color: var(--color-text-secondary);
  text-align: center;
}

/* Prominent primary CTA - customization is the clearest next action
   once a preview is ready (see index.html's own comment on this block
   and app.js's open-customize-button listener). Placed before
   .model-and-customize-row deliberately, so it renders above the model
   panel and stays visible without scrolling on a 375px viewport. */
.review-primary-cta {
  text-align: center;
  margin-bottom: var(--space-5);
}

/* ---------- Post-generation customization (Frame details/Color style) ---------- */

.model-and-customize-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-5);
  margin-bottom: var(--space-5);
}

.model-and-customize-row .model-panel {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}

.model-panel {
  background: var(--color-surface);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-subtle);
  padding: var(--space-5);
  height: min(74vh, 780px);
  display: flex;
  margin-bottom: var(--space-5);
  position: relative;
}

/* Live-update feedback while a customize request is in flight - dims
   the preview without the "still generating" pulseBg treatment
   (.is-loading), since the model is already there and just updating. */
/* See Layers/buildings.py's own TERRAIN-ONLY FALLBACK section - the
   authoritative, post-generation notice that this specific job dropped
   the buildings layer. Pinned to the bottom of the already-relative
   .model-panel rather than taking up flex space of its own, so it
   overlays the preview without shifting the model-viewer/customize
   column layout. */
/* Buildings Mode row - Detail/Landmark/Terrain Mode (see app.js's own
   BUILDINGS_MODE_COPY). A neutral, informational indicator naming which
   of three real states a selection is in - deliberately NOT the
   warning/error (yellow/red) treatment .selection-summary.is-large/
   is-too-large use elsewhere on this same box, since this isn't
   reporting a problem. Lives INSIDE #place-status (the same "Selected
   area" box, not a second one) as its own row, set off by a thin top
   divider rather than a competing border/background of its own - a
   card-inside-a-card would fight the parent box's own good/moderate/
   large/too-large coloring instead of sitting quietly alongside it.
   Built entirely from the existing accent color tokens (--color-accent/
   -06/-08/-10/-12/-20), already themed for both light and dark - no new
   color tokens needed. The three .mode-* variants fade the SAME accent
   color rather than shifting hue (solid dot for Detail, hollow accent
   for Landmark, hollow neutral for Terrain) - "fewer buildings, fainter
   marker" reads as a real gradient, not a stoplight. */
.buildings-mode-badge {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.buildings-mode-badge[hidden] {
  display: none;
}

.buildings-mode-dot {
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--color-accent);
}

.buildings-mode-label {
  margin: 0 0 2px;
  font-family: var(--font-family-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-accent);
}

.buildings-mode-message {
  margin: 0;
  font-size: 13px;
  color: var(--color-text-secondary);
}

.buildings-mode-badge.mode-landmark .buildings-mode-dot {
  background: var(--color-accent-20);
  border: 1px solid var(--color-accent);
}

.buildings-mode-badge.mode-terrain .buildings-mode-dot {
  background: transparent;
  border: 1px solid var(--color-text-tertiary);
}

.buildings-mode-badge.mode-terrain .buildings-mode-label {
  color: var(--color-text-secondary);
}

/* Overlay variant - the one place this still needs its own free-standing
   card look (pinned to the bottom of the already-relative .model-panel,
   overlaying the 3D preview rather than living inside another box). */
.buildings-mode-badge-overlay {
  position: absolute;
  left: var(--space-5);
  right: var(--space-5);
  bottom: var(--space-5);
  margin-top: 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-accent-20);
  border-top: 1px solid var(--color-accent-20);
  border-radius: var(--radius-md);
  background: var(--color-accent-08);
}

.buildings-mode-badge-overlay.mode-landmark {
  background: var(--color-accent-06);
}

.buildings-mode-badge-overlay.mode-terrain {
  background: transparent;
  border-color: var(--color-border);
}

.model-panel.is-updating .model-viewer-el {
  opacity: 0.55;
  transition: opacity 0.15s ease;
}

.model-panel.is-updating:not(.has-tesseract)::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  border-radius: 50%;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-accent);
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* Desktop: a permanently-visible side panel - the mobile-only drawer
   toggle/scrim stay hidden entirely. No independent scroll region of
   its own (see the nav-scroll-behavior fix's own report) - it flows
   naturally at its own content height, so the document is the single
   scroll owner; .model-panel below is what stays sticky/visible while
   this column scrolls past it. */
.review-customize-panel {
  width: 300px;
  flex-shrink: 0;
  background: var(--color-surface);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-subtle);
  padding: var(--space-5);
  /* Establishes this panel's own actual rendered width as a query
     container for .info-card-list's two-column breakpoint below -
     this panel is fixed at 300px on desktop regardless of the
     browser's own viewport width, which is exactly why a viewport
     media query previously let two columns turn on here even though
     300px never has room for them (see .info-card-list's own
     @container rule). */
  container-type: inline-size;
}

/* Keeps the model preview visible while the (now naturally-flowing,
   often taller) customization column scrolls past it - deliberately
   NOT applied to .review-customize-panel itself, since sticky content
   taller than the viewport becomes partially unreachable; only this
   shorter, fixed-height panel is safe to pin. Scoped to width > the
   existing 900px mobile-drawer breakpoint - .model-panel is a normal
   in-flow block on mobile and must stay that way. */
@media (min-width: 901px) {
  .model-and-customize-row .model-panel {
    position: sticky;
    top: 0;
  }
}

.customize-panel-header {
  display: none;
}

.customize-panel-toggle {
  display: none;
}

.customize-panel-close-button {
  display: none;
}

/* #customize-panel-scrim reuses .steps-sheet-scrim's own rules
   (position/background/z-index, plus [hidden] { display: none }) - it
   only ever needs to actually show on mobile, and only when JS clears
   its hidden attribute (see openCustomizePanel/closeCustomizePanel in
   app.js), so no ID-selector override is added here that would fight
   that attribute at higher specificity. */

.customize-section {
  display: block;
}

/* This project's own established convention (see .feedback-text[hidden]/
   .view[hidden] elsewhere in this file): a class rule that sets its own
   `display` must pair it with a same-specificity [hidden] override,
   since a plain class selector and the UA stylesheet's own [hidden] {
   display: none } rule tie on specificity - without this, JS setting
   .hidden = true on a <details class="customize-section"> (Journey-only
   sections on a Place job) would silently stay visible. */
.customize-section[hidden] {
  display: none;
}

.customize-section + .customize-section {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}

/* .customize-section is a <details> element - card-based Preview &
   Customize (see this feature's own report): native <details>/<summary>
   gives every section collapse/expand behavior with zero custom ARIA
   (keyboard toggling, expanded-state announcement, and focus handling
   all come from the browser itself) rather than a hand-rolled
   accordion. The marker is replaced with a custom chevron rather than
   hidden entirely, so expand/collapse state is never communicated by
   position/color alone (see this file's own accessibility notes). */
.customize-section-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-2) 0;
  font-family: var(--font-family-mono);
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}

.customize-section-title::-webkit-details-marker {
  display: none;
}

.customize-section-title::before {
  content: "";
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--color-text-tertiary);
  border-bottom: 2px solid var(--color-text-tertiary);
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
}

.customize-section[open] > .customize-section-title::before {
  transform: rotate(45deg);
}

.customize-section-title:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm);
}

.customize-section-body {
  padding-top: var(--space-3);
}

.recommended-badge {
  font-family: var(--font-family-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--color-accent);
  background: var(--color-accent-10);
  border-radius: 999px;
  padding: 2px 8px;
}

.customize-subsection-label {
  margin: var(--space-3) 0 var(--space-2);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-tertiary);
}

.frame-toggle-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  cursor: pointer;
}

.switch-label {
  font-family: var(--font-family-mono);
  font-size: 14px;
  color: var(--color-text-primary);
}

.switch-label-group {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Mirrors .maker-mark-included-badge's own styling exactly - the same
   "included/free" visual language, reused here so the complimentary
   engraving toggle reads as unmistakably free at a glance. */
.switch-label-badge {
  flex: 0 0 auto;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--color-success-badge-bg);
  color: var(--color-success, #2f9e5b);
  font-family: var(--font-family-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.switch-input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.switch-track {
  position: relative;
  flex-shrink: 0;
  width: 38px;
  height: 22px;
  border-radius: 999px;
  background: var(--color-border);
  transition: background-color 0.15s ease;
}

.switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
  transition: transform 0.15s ease;
}

.switch-input:checked + .switch-track {
  background: var(--color-accent);
}

.switch-input:checked + .switch-track .switch-thumb {
  transform: translateX(16px);
}

.switch-input:focus-visible + .switch-track {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.customize-actions-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-3);
}

.customize-undo-redo-row {
  gap: var(--space-2);
}

.btn-secondary-sm {
  flex: 1;
  padding: 6px 10px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-primary);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.btn-secondary-sm:hover:not(:disabled) {
  background: var(--color-surface-hover);
}

.btn-secondary-sm:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.btn-secondary-sm:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.theme-picker-compact .theme-swatch-button {
  padding: var(--space-2) var(--space-3);
}

.theme-picker-compact .theme-swatch-description {
  display: none;
}

.customize-status-message {
  min-height: 16px;
  margin: var(--space-3) 0 0;
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-secondary);
}

.customize-status-message.error {
  color: var(--color-error);
}

/* ---------------------------------------------------------------
   INFORMATION CARDS - card-based Preview & Customize (see this
   feature's own report). Reused for BOTH the frame-engraving fields
   (city name/coordinates/scale/area/north arrow) and the Journey
   display fields (distance/start/finish/direction arrow) - one shared
   component, not a Journey-only pattern (B2's own "Place and Journey
   should share reusable rendering" requirement) - restrained two-column
   layout on wider viewports (see the media query below), single column
   on narrow ones.
   --------------------------------------------------------------- */

.info-card-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
  max-width: 100%;
}

.info-card {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  /* Lets this GRID ITEM (see the >=1200px media query below) shrink
     below its own content's min-content width instead of forcing its
     column wider than the panel - a plain 1fr grid track's implicit
     minimum is auto (its content's min-content size, driven here by
     the Hide button plus an unbreakable label word like
     "Coordinates"), not 0, which is exactly what pushed this card
     outside the Customize sidebar into the model-preview area. Safe on
     the narrow flex-column layout too - flex's own default
     align-items: stretch already fills the row there regardless. */
  min-width: 0;
}

.info-card-text {
  min-width: 0;
}

.info-card-label {
  display: block;
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-primary);
  /* Mirrors .info-card-value's own overflow-wrap below - a single
     unbreakable word (e.g. "Coordinates") must never force this card
     wider than its narrow two-column grid track. */
  overflow-wrap: anywhere;
}

.info-card-value {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}

/* Unavailable state (e.g. direction arrow on a loop route) - never
   color alone: the label text itself changes ("Not available for loop
   routes") and the hide/restore control is removed entirely, not just
   dimmed. */
.info-card.is-unavailable {
  opacity: 0.7;
}

.info-card.is-unavailable .info-card-value {
  font-style: italic;
}

.info-card-hide-button {
  flex-shrink: 0;
  padding: 4px 10px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-secondary);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.info-card-hide-button:hover {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
}

.info-card-hide-button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.add-info-wrapper {
  margin-top: var(--space-2);
}

.add-info-toggle-button {
  padding: 6px 12px;
  border-radius: var(--radius-md);
  border: 1px dashed var(--color-border);
  background: transparent;
  color: var(--color-text-secondary);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.add-info-toggle-button:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.add-info-toggle-button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.add-info-panel {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background: var(--color-surface-hover);
}

.add-info-panel[hidden] {
  display: none;
}

.add-info-option {
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.add-info-option:hover {
  border-color: var(--color-accent);
  background: var(--color-accent-06);
}

.add-info-option:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* Route Annotation System Phase 1 - a small, independent unit choice
   attached to the Distance card, not a card of its own (see app.js's
   own renderJourneyDistanceUnitControl). Pill buttons matching
   .add-info-option's own shape/sizing; the pressed state borrows
   .theme-swatch-button.is-selected's accent treatment so "the current
   choice" reads the same way everywhere this project already marks a
   selection. */
.distance-unit-control {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.distance-unit-control[hidden] {
  display: none;
}

.distance-unit-control-label {
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-tertiary);
}

.distance-unit-buttons {
  display: flex;
  gap: var(--space-1);
}

.distance-unit-button {
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.distance-unit-button:hover {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
}

.distance-unit-button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.distance-unit-button[aria-pressed="true"] {
  border-color: var(--color-accent);
  color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-12);
}

/* Two columns only once .review-customize-panel's own actual width
   (see its container-type: inline-size above) has room for them -
   a container query, not a viewport media query, because this panel
   is a fixed 300px sidebar regardless of the browser's own viewport
   width; a >=1200px VIEWPORT query previously turned two columns on
   at that same fixed 300px panel width, which never had room for
   them (see the incident this fix responds to). 420px is comfortably
   above the ~400px a two-column layout needs for the longest single
   unbreakable label word ("Coordinates") to avoid a mid-word break -
   this panel's own fixed 300px therefore correctly stays single-
   column, exactly like the mobile drawer's own narrower width. */
@container (min-width: 420px) {
  .info-card-list {
    display: grid;
    /* minmax(0, 1fr), not a bare 1fr - a bare 1fr track's implicit
       minimum is auto (its content's own min-content width), so two
       equal 1fr tracks do NOT actually split available space evenly
       once either column's content (the Hide button plus an
       unbreakable label word) needs more than half - the track simply
       grows past the container instead of shrinking, which is exactly
       what pushed the Coordinates/Area dimensions cards outside the
       Customize sidebar into the model-preview area. minmax(0, 1fr)
       removes that content-driven floor so both columns always split
       the container's actual available width evenly, wrapping their
       own text instead. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2);
  }
}

/* ---------------------------------------------------------------
   RECOMMENDATION SLOT - one reusable, dismissible slot at the top of
   the customize panel (see this feature's own report) - never a score,
   never stacked, at most one visible at a time.
   --------------------------------------------------------------- */

.recommendation-slot {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-accent-06);
  border: 1px solid var(--color-accent-20);
}

.recommendation-slot[hidden] {
  display: none;
}

.recommendation-text {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-primary);
}

.recommendation-dismiss-button {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--color-text-secondary);
  font-family: var(--font-family-mono);
  font-size: 12px;
  cursor: pointer;
}

.recommendation-dismiss-button:hover {
  background: var(--color-accent-12);
  color: var(--color-text-primary);
}

.recommendation-dismiss-button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

@media (prefers-reduced-motion: reduce) {
  .customize-section-title::before {
    transition: none;
  }
}

.underside-name-field {
  margin: var(--space-2) 0 var(--space-3);
}

.underside-name-field input[type="text"] {
  padding: 10px 12px;
  font-size: 14px;
}

.underside-status-text {
  min-height: 14px;
  margin: var(--space-1) 0 0;
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-tertiary);
}

.underside-status-text.error {
  color: var(--color-error);
}

.underside-status-text.good {
  color: var(--color-success, #2f9e5b);
}

.underside-maker-preview {
  margin: var(--space-1) 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-hover);
  /* Was already its own separate mono stack (no IBM Plex Mono) -
     consolidated onto the shared token so it now gets the same
     webfont as the rest of the app, still with the identical system
     fallback chain underneath it. */
  font-family: var(--font-family-mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--color-text-secondary);
  white-space: pre-line;
}

.maker-mark-included-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.maker-mark-included-row strong,
.maker-mark-included-row small {
  display: block;
}

.maker-mark-included-row small {
  margin-top: 2px;
  color: var(--color-text-tertiary);
  font-size: 12px;
}

.maker-mark-included-badge {
  flex: 0 0 auto;
  padding: 4px 8px;
  border-radius: 999px;
  background: var(--color-success-badge-bg);
  color: var(--color-success, #2f9e5b);
  font-family: var(--font-family-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

#view-underside-button[aria-pressed="true"] {
  border-color: var(--color-accent);
  color: var(--color-accent);
  background: var(--color-accent-08);
}

.model-panel.is-loading {
  animation: pulseBg 1.8s ease-in-out infinite;
}

.model-viewer-el {
  width: 100%;
  height: 100%;
  --poster-color: transparent;
}

.review-summary {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-subtle);
  padding: var(--space-5);
}

.info-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-4);
  margin: 0;
}

.info-list dt {
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-secondary);
}

.info-list dd {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 14px;
  font-weight: 500;
  text-align: right;
  color: var(--color-text-primary);
}

.info-list .review-price {
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-border);
  color: var(--color-text-primary);
  font-size: 16px;
  font-weight: 700;
}

.trust-line {
  margin: var(--space-4) 0 0;
  text-align: center;
  font-size: 13px;
  color: var(--color-text-secondary);
}

/* ---------- Confirmation (order placed) ---------- */

.confirmation-card {
  max-width: 420px;
  width: 100%;
  margin: 0 auto;
  padding: var(--space-6) var(--space-5);
  text-align: center;
}

/* Cart Orders Phase C2.1 - the confirmed panel's own SECONDARY action,
   visually subordinate to the primary Shopify checkout button above it
   (a top border + extra top margin, never a competing color/size) -
   see #add-to-cart-button's own class (.btn-secondary, not
   .btn-primary) for the same hierarchy carried through to the button
   itself. Reuses .step-helper/.feedback-text as-is (no new text
   styles needed) and .confirmation-card's own max-width/padding, so
   this never introduces its own overflow risk at narrow widths. */
.add-to-cart-section {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}

/* ---------- Cart Orders Phase C2.3 - always-visible persistent cart card + management panel ---------- */

/* #cart-pill starts [hidden] in index.html and is only ever un-hidden
   by renderCartPill() in app.js once the cart-orders feature flag
   itself is confirmed enabled - it no longer hides just because
   currentCart is null/empty; a validated-empty or absent cart now
   renders the card's own "Empty" state instead (#cart-pill-empty).
   position: fixed keeps it pinned top-right at all times, independent
   of .app-header's normal document flow and of page scroll - confirmed
   safe (no transform/filter/will-change on body/html/.app-header would
   otherwise create a containing block that traps it there instead).
   z-index 38 keeps it below .cart-panel-scrim (39) and .cart-panel
   (40) so the existing modal still layers correctly above it when
   #cart-pill-manage-button opens it. Sized as a compact card (capped
   width, modest padding/font, internal scroll past a handful of
   distinct items) since - unlike the old click-to-open pill - this is
   now permanently on screen and must stay visually light. */
.cart-pill {
  position: fixed;
  top: var(--space-5);
  right: var(--space-5);
  z-index: 38;
  width: min(240px, calc(100vw - var(--space-5) * 2));
  max-height: 60vh;
  display: flex;
  flex-direction: column;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-primary);
  box-shadow: var(--shadow-subtle);
}

.cart-pill[hidden] {
  display: none;
}

/* Base/desktop default: always shown, laid out the same way its
   contents (.cart-pill-head/.cart-pill-empty/.cart-pill-items/
   .cart-pill-manage-button) already were before this wrapper existed.
   The @media block below is the only place this is ever hidden. */
.cart-pill-body {
  display: flex;
  flex-direction: column;
}

/* Phase C2.5 - the collapsed-on-mobile state's own control: just the
   cart glyph plus #cart-pill-badge. display:none by default (every
   breakpoint) - the @media block below is the only place this is ever
   shown, since desktop never collapses at all. */
.cart-pill-icon-toggle {
  display: none;
  position: relative;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-primary);
  cursor: pointer;
  box-shadow: var(--shadow-subtle);
  padding: 0;
}

.cart-pill-icon-toggle:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.cart-pill-icon {
  width: 20px;
  height: 20px;
}

.cart-pill-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--color-text-primary);
  color: var(--color-surface);
  font-family: var(--font-family-mono);
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.cart-pill-badge[hidden] {
  display: none;
}

/* A <button> (Phase C2.4 - see index.html's own comment: it's the
   mobile card collapse toggle), so the usual UA button chrome
   (border/background/padding/centered text) is reset back to the
   plain-header look this had as a <div> before. Always clickable at
   every breakpoint - toggleCartPillExpanded in app.js is itself what
   makes clicking it a no-op on desktop, not this CSS. */
.cart-pill-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  border: none;
  background: none;
  padding: 0;
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--color-text-secondary);
  text-align: left;
  cursor: default;
}

.cart-pill-head:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.cart-pill-count {
  font-variant-numeric: tabular-nums;
  color: var(--color-text-primary);
}

/* A small CSS triangle, not an icon font/SVG - matches this card's
   own "no extra asset" footprint. Hidden entirely above the mobile
   breakpoint (see the @media block below) since desktop always shows
   the item list and there is nothing to indicate. */
.cart-pill-chevron {
  display: none;
  flex: none;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--color-text-tertiary);
  transition: transform 0.15s ease;
}

.cart-pill-empty {
  margin: var(--space-2) 0 0;
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-tertiary);
}

.cart-pill-empty[hidden] {
  display: none;
}

.cart-pill-items {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  overflow-y: auto;
}

.cart-pill-item-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  font-family: var(--font-family-mono);
  font-size: 12px;
}

.cart-pill-item-label {
  color: var(--color-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cart-pill-item-qty {
  flex: none;
  color: var(--color-text-tertiary);
  font-variant-numeric: tabular-nums;
}

.cart-pill-manage-button {
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.cart-pill-manage-button:hover {
  background: var(--color-surface-hover);
}

.cart-pill-manage-button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* Below 900px, .mobile-header takes over as a position: sticky,
   full-width bar pinned at top: 0 (see that selector above) - this
   card's own top-right fixed position would otherwise sit directly on
   top of it and intercept taps meant for that bar. Dropped below the
   mobile header's own rendered bounding box - a real 94px-tall strip
   from y:82 to y:176 at a 390px viewport, measured directly via
   Playwright (its own "Step N of M" line varies in width with the
   current stage's name, so a fixed value tuned by eye against one
   stage's text is not reliable - a prior 96px value here overlapped
   this exact icon toggle against a longer stage name).

   Phase C2.5 - collapsed is the default down here: #cart-pill sheds
   its own card chrome entirely (no padding/border/background/shadow -
   .cart-pill-icon-toggle supplies its own) and #cart-pill-body (the
   header, empty/item list, and #cart-pill-manage-button together)
   stays hidden until ".cart-pill--expanded" is added - see
   toggleCartPillExpanded in app.js, which only ever adds it in
   response to a genuine tap on #cart-pill-icon-toggle or the header
   inside #cart-pill-body. Expanded restores the exact card look this
   had before Phase C2.5, narrowed to match the tighter viewport. */
@media (max-width: 899px) {
  /* With the site's bottom tab bar mounted (site-nav.js, body.sn-mounted) the Cart lives there, like every
     other page: the floating icon goes, and the card opens as a sheet just above the bar. */
  body.sn-mounted .cart-pill-icon-toggle { display: none; }
  body.sn-mounted .cart-pill.cart-pill--expanded {   /* outranks site-nav.css's body.sn-maps .cart-pill top */
    position: fixed;
    top: auto;
    left: var(--space-3);
    right: var(--space-3);
    bottom: calc(64px + env(safe-area-inset-bottom) + var(--space-2));
    width: auto;
    z-index: 60;
  }
  body.sn-mounted .review-customize-panel { bottom: calc(64px + env(safe-area-inset-bottom)); }
  .cart-pill {
    top: 184px;
    right: var(--space-3);
    width: auto;
    max-height: none;
    padding: 0;
    border: none;
    background: none;
    box-shadow: none;
  }

  .cart-pill-icon-toggle {
    display: flex;
  }

  .cart-pill-body {
    display: none;
  }

  .cart-pill--expanded {
    width: min(200px, calc(100vw - var(--space-3) * 2));
    max-height: 60vh;
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    background: var(--color-surface);
    box-shadow: var(--shadow-subtle);
  }

  .cart-pill--expanded .cart-pill-icon-toggle {
    display: none;
  }

  .cart-pill--expanded .cart-pill-body {
    display: flex;
  }

  .cart-pill-head {
    cursor: pointer;
  }

  /* Only ever visible inside #cart-pill-body, i.e. only once expanded
     (see above) - so it always means "tap to collapse", pointing up,
     never the down-pointing "tap to expand" state the icon toggle's
     own presence/absence already communicates instead. */
  .cart-pill-chevron {
    display: block;
    transform: rotate(180deg);
  }
}

.cart-panel-scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
  z-index: 39;
}

.cart-panel-scrim[hidden] {
  display: none;
}

.cart-panel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 40;
  width: min(440px, calc(100vw - var(--space-5) * 2));
  max-height: 85vh;
  overflow-y: auto;
  background: var(--color-surface);
  border-radius: var(--radius-xl);
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18);
  padding: var(--space-5);
}

.cart-panel[hidden] {
  display: none;
}

.cart-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-4);
}

.cart-panel-head h2 {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: 16px;
  font-weight: 700;
  color: var(--color-text-primary);
}

.cart-panel-close {
  background: none;
  border: none;
  font-family: var(--font-family-mono);
  font-size: 16px;
  color: var(--color-text-secondary);
  cursor: pointer;
  padding: var(--space-1);
}

.cart-panel-close:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.cart-panel-items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.cart-panel-item {
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.cart-panel-item-info {
  margin-bottom: var(--space-2);
}

.cart-panel-item-label {
  font-family: var(--font-family-mono);
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text-primary);
}

.cart-panel-item-size {
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-tertiary);
}

.cart-panel-item-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.cart-panel-qty {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.cart-panel-qty-button {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: var(--font-family-mono);
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.cart-panel-qty-button:hover:not(:disabled) {
  background: var(--color-surface-hover);
}

.cart-panel-qty-button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.cart-panel-qty-button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.cart-panel-qty-value {
  min-width: 24px;
  text-align: center;
  font-family: var(--font-family-mono);
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.cart-panel-item-remove {
  background: none;
  border: none;
  font-family: var(--font-family-mono);
  font-size: 13px;
  color: var(--color-text-secondary);
  cursor: pointer;
  padding: var(--space-1);
}

.cart-panel-item-remove:hover:not(:disabled) {
  color: var(--color-error);
}

.cart-panel-item-remove:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.cart-panel-item-remove:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.cart-panel-item-feedback {
  margin: var(--space-2) 0 0;
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-secondary);
}

.cart-panel-item-feedback.error {
  color: var(--color-error);
}

.cart-panel-item-feedback:empty {
  display: none;
}

.cart-panel-empty {
  margin: var(--space-3) 0;
  font-size: 14px;
  color: var(--color-text-secondary);
}

.cart-panel-empty[hidden] {
  display: none;
}

.cart-panel-actions {
  margin-top: var(--space-4);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
}

/* Cart Orders Phase D.2 - same narrowly id-scoped fix as
   #rotate-selection-button[hidden] above, for the same reason: the
   shared .btn class sets display: inline-flex with no paired [hidden]
   override anywhere in this file, so the hidden attribute alone does
   not visually hide a .btn element. Scoped to this one button only. */
#cart-checkout-button[hidden] {
  display: none;
}

.cart-panel-checkout-note {
  margin: calc(var(--space-3) * -1) 0 0;
  font-size: 13px;
  color: var(--color-text-secondary);
  text-align: center;
}

.cart-panel-checkout-note[hidden] {
  display: none;
}

.cart-panel-clear-link {
  color: var(--color-text-tertiary);
}

.cart-panel-clear-link:hover {
  color: var(--color-error);
}

/* Independent from .body-scroll-locked/lockBackgroundScroll (the
   Customize panel's own mobile-only lock) - deliberately never shared
   state. The cart panel is reachable from every app state, including
   "review" while the Customize panel may independently be open on
   mobile; sharing one boolean/class would let closing either panel
   first incorrectly unlock scrolling behind the other, still-open one. */
.cart-panel-scroll-locked {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
}

/* ---------- Responsive ---------- */

@media (max-width: 900px) {
  .map-panel {
    /* Was a flat 340px - short for the single most important
       interaction on this page. Scales with the actual device height
       (never smaller than the old value, capped so it doesn't take
       over a tablet-width screen). */
    height: clamp(340px, 50vh, 500px);
  }

  /* Alternative-route clarity correction - requirement 9: the smallest
     layout that keeps the map meaningfully visible. #route-builder-panel
     sits in normal document flow ABOVE .map-panel (this view is a
     single stacked column at every width - see .step-card-wide's own
     max-width, there is no side-by-side grid to lose here), so a tall
     vertically-stacked card list would push the map (and its now-
     colored/badged highlighted line) far down the page while comparing
     alternatives. A horizontally scrollable tray - not a new bottom-
     drawer state machine like .review-customize-panel below (deliberately
     NOT reused here - that pattern exists for a much larger, collapsible
     3D-viewer-adjacent panel; this is a handful of small cards) - keeps
     every card reachable via native touch scroll while staying compact.
     "Choose this route" sits OUTSIDE this scroll container (see
     index.html) so it stays reachable by normal vertical scroll, never
     trapped inside the horizontal one. */
  .route-builder-alternatives-list {
    flex-direction: row;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
    padding-bottom: var(--space-1);
  }

  /* The <li> (not the <button> inside it) is the actual flex item once
     .route-builder-alternatives-list becomes flex-direction: row. */
  .route-builder-alternatives-list > li {
    flex: 0 0 auto;
    min-width: 168px;
    scroll-snap-align: start;
  }

  /* A phone: the card is the viewer - a slimmer pad so the map gets the
     width (and the view cube its room), the terrain-mode note UNDER the
     viewer instead of over it, the height following its content. */
  #review-view { padding: var(--space-3) var(--space-3) var(--space-6); }
  .model-and-customize-row { order: -1; margin-bottom: var(--space-4); }
  .review-heading { font-size: 24px; }
  .model-panel {
    height: auto;
    min-height: 0;
    padding: var(--space-2);
    flex-direction: column;
    border-radius: var(--radius-lg);
  }
  .model-panel .model-viewer-el {
    height: min(64vh, 560px);
    min-height: 380px;
    flex: none;
    border-radius: var(--radius-md);
  }
  .buildings-mode-badge-overlay {
    position: static;
    left: auto;
    right: auto;
    bottom: auto;
    margin-top: var(--space-3);
  }

  /* Mobile: a collapsible bottom drawer instead of a side panel -
     anchored to the viewport bottom, height-animated (not
     translate-animated) so the collapsed state naturally clips away
     everything below the header via overflow:hidden, which also means
     collapsed content can never intercept a touch - there is nothing
     there to hit. model-viewer's own rotate gesture happens well above
     this short strip regardless of state.

     A vertical flex column with an explicit non-scrolling header (see
     .customize-panel-header's own flex:none below) and a single
     scrolling body (#customize-panel-body) - never overflow-y:auto on
     this outer element itself once open (see the nav-scroll-behavior
     fix's own report on why that swallowed the header into the
     scrollable area). Not draggable - open/close is click/tap-only via
     the toggle, the dedicated Close button, the backdrop, or Escape;
     see app.js's own comment on why no touch/pointer drag listeners
     exist here. */
  .review-customize-panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    width: auto;
    max-height: 52px;
    overflow: hidden;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18);
    transition: max-height 0.25s ease;
    padding: 0 var(--space-5);
    display: flex;
    flex-direction: column;
  }

  .review-customize-panel.is-open {
    max-height: 75vh;
  }

  .customize-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    flex: none;
  }

  .customize-panel-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    flex: 1 1 auto;
    min-width: 0;
    background: none;
    border: none;
    padding: var(--space-2) 0;
    cursor: pointer;
    font-family: inherit;
  }

  /* A small accent mark next to the label, not a centered pill above
     it - deliberately does not read as a drag handle (see the nav-
     scroll-behavior fix's own report: this sheet is not draggable,
     open/close is click/tap-only). */
  .customize-panel-divider {
    flex-shrink: 0;
    width: 16px;
    height: 2px;
    border-radius: 1px;
    background: var(--color-border);
  }

  .customize-panel-toggle-label {
    font-family: var(--font-family-mono);
    font-size: 13px;
    font-weight: 600;
    color: var(--color-text-primary);
  }

  .customize-panel-close-button {
    display: none;
    flex: none;
    padding: 8px 14px;
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border);
    background: transparent;
    color: var(--color-text-primary);
    font-family: var(--font-family-sans);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    min-height: 40px;
  }

  .customize-panel-close-button:focus-visible {
    outline: var(--focus-ring);
    outline-offset: var(--focus-ring-offset);
  }

  /* Only meaningful once actually open - collapsed, there is nothing
     to close, and the header has no room for a second control next to
     the full-width toggle. */
  .review-customize-panel.is-open .customize-panel-close-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  .customize-panel-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    padding-bottom: max(var(--space-4), env(safe-area-inset-bottom));
  }

}

@media (prefers-reduced-motion: reduce) {
  .review-customize-panel {
    transition: none;
  }
}

@media (max-width: 560px) {
  .step-heading {
    font-size: 22px;
  }

  .step-card {
    padding: var(--space-5) var(--space-4);
  }

  .classic-displays-summary {
    align-items: flex-start;
    flex-wrap: wrap;
  }

  .classic-displays-summary-note {
    order: 3;
    width: 100%;
    margin-left: 0;
  }

  .display-grid {
    grid-template-columns: 1fr;
  }

  .actions-row {
    flex-direction: column;
  }

  .actions-row .btn {
    width: 100%;
  }

  .theme-palette-preview {
    flex-wrap: wrap;
  }

  .search-row {
    align-items: stretch;
  }

  .search-row .btn {
    padding-left: 16px;
    padding-right: 16px;
  }

  .route-builder-coordinate-row {
    flex-direction: column;
    align-items: stretch;
  }

  .route-builder-coordinate-row input[type="text"],
  .route-builder-coordinate-row .btn {
    width: 100%;
  }

  .route-builder-confirm-actions {
    flex-direction: column;
  }

  .route-builder-confirm-actions .btn,
  .route-builder-confirm-actions .btn-link {
    width: 100%;
    text-align: center;
  }

  .route-point-method-selector {
    flex-direction: column;
    align-items: stretch;
  }

  .route-point-method-selector .btn {
    width: 100%;
  }

  .route-point-search-row {
    flex-direction: column;
    align-items: stretch;
  }

  .route-point-search-row input[type="text"],
  .route-point-search-row .btn {
    width: 100%;
  }

  .map-panel {
    height: 380px;
    margin-left: calc(-1 * var(--space-4));
    margin-right: calc(-1 * var(--space-4));
    border-radius: 0;
  }

  .shape-selector-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    overflow: visible;
    padding-bottom: 0;
  }

  .shape-btn {
    width: 100%;
    min-width: 0;
  }

  /* .map-shape-shortcut-btn also carries .shape-btn (shares its border/
     hover/selected treatment - see that rule's own comment) so it
     inherits the full-width override above unless undone here - this
     row is meant to stay compact/icon-sized on mobile too, not stretch
     into 4 full-width rows. */
  .map-shape-shortcut-btn {
    width: auto;
    min-width: 40px;
  }

  .place-continue-bar {
    /* Was position: sticky - switched to fixed after a real, live,
       every-time report of a large phantom gap under this bar on a
       real iPhone (Safari), not reproducible in either Chromium or
       real WebKit device emulation, and confirmed NOT a cache issue
       (persisted in a fresh private-browsing tab). sticky bottom:0 +
       backdrop-filter + safe-area-inset has known real-iOS-Safari-
       specific quirks that desktop-hosted engines (including desktop
       WebKit) don't reproduce - fixed is the more robust, standard
       pattern for an always-visible bottom bar. Reserving space for it
       now happens via #place-view's own padding-bottom below (fixed
       elements are removed from normal flow entirely, so the old
       "margin-top on the bar's own resting position" trick from
       sticky no longer applies) - same --place-continue-bar-height
       custom property, same ResizeObserver in app.js, just consumed
       by a different rule. */
    position: fixed;
    z-index: 800;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 6px var(--space-4) max(6px, env(safe-area-inset-bottom));
    background: var(--color-continue-bar-bg);
    border-top: 1px solid var(--color-border);
    box-shadow: 0 -6px 16px rgba(0, 0, 0, 0.05);
    backdrop-filter: blur(10px);
  }

  /* Slightly less tall and more rounded than the default .btn on this
     one full-width bar - the default 12px/8px-radius combo reads as a
     large, blocky slab when it's the only control pinned to the
     screen edge; a touch less padding and a softer radius keeps it
     easy to tap while feeling more like a refined docked bar. */
  .place-continue-bar .btn-block {
    padding: 10px 24px;
    border-radius: var(--radius-lg);
  }

  /* Reserves space at the true end of scrollable content for the now-
     fixed bar above (see its own comment) - without this, the last
     content before where the bar used to sit in normal flow (Advanced
     options / "Name this place") would stay permanently covered, the
     exact bug the sticky version's own margin-top used to prevent. */
  #place-view {
    padding-bottom: calc(var(--space-6) + var(--place-continue-bar-height, 94px));
  }

  /* Mobile: preserves the existing edge-to-edge fixed positioning,
     safe-area support, and place-continue-bar stacking unchanged -
     left/right/width here deliberately override the desktop default
     rule's --dock-left/--dock-width (width: auto is required so `left`
     + `right` govern sizing instead of the desktop rule's own `width`
     declaration winning, per CSS's own over-constrained-box rules).
     --sticky-bottom-offset is set from app.js to .place-continue-bar's
     own live-measured height, so this bar always sits directly ABOVE
     it rather than overlapping it (both would otherwise independently
     claim the same bottom:0 position). */
  .route-builder-sticky-actions {
    left: 0;
    right: 0;
    width: auto;
    bottom: var(--sticky-bottom-offset, 0px);
    padding: var(--space-3) var(--space-4) max(var(--space-3), env(safe-area-inset-bottom));
    border: none;
    border-top: 1px solid var(--color-border);
    border-radius: 0;
    box-shadow: 0 -8px 20px rgba(0, 0, 0, 0.06);
  }

  /* IBM Plex Mono's wider glyphs push #rotate-selection-button's own
     label onto a second line at this width (see this fix's own
     report) - reverting the .map-actions row specifically back to
     Inter's narrower metrics is enough to fit it back on one line,
     without touching padding/min-height/font-size/wording, and
     without affecting any other button (desktop map actions and every
     other mono control elsewhere are unaffected - this rule only
     exists inside this breakpoint). */
  .map-actions .btn {
    font-family: var(--font-family-sans);
  }
}

/* ------------------------------------------------------------------
   Persona presets - Purpose screen cards, the "Made for" chip on Place,
   and the terrain relief picker (see app.js's persona section and
   personas.json). Token-only colors so the dark theme needs no extra
   rules.
   ------------------------------------------------------------------ */
.purpose-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.purpose-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  padding: var(--space-4);
  text-align: left;
  font: inherit;
  color: var(--color-text-primary);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  cursor: pointer;
}

.purpose-card:hover {
  background: var(--color-surface-hover);
}

.purpose-card:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.purpose-card[aria-pressed="true"] {
  box-shadow: 0 0 0 2px var(--color-selected-ring);
}

.purpose-card-icon {
  font-size: 26px;
  line-height: 1;
  margin-bottom: var(--space-1);
}

.purpose-card-label {
  font-weight: 700;
}

.purpose-card-tagline {
  font-size: 13px;
  line-height: 1.4;
  color: var(--color-text-secondary);
}

.persona-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  padding: 4px 12px;
  font-size: 13px;
  color: var(--color-text-primary);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 999px;
}

.persona-chip[hidden] {
  display: none;
}

.persona-chip-eyebrow {
  color: var(--color-text-tertiary);
}

.persona-chip-change {
  font-size: 13px;
}

.terrain-relief-label {
  margin-top: var(--space-4);
}

.terrain-relief-picker {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.terrain-relief-option {
  padding: 6px 14px;
  font: inherit;
  font-size: 13px;
  color: var(--color-text-primary);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  cursor: pointer;
}

.terrain-relief-option:hover {
  background: var(--color-surface-hover);
}

.terrain-relief-option:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.terrain-relief-option[aria-checked="true"] {
  color: var(--color-on-primary);
  background: var(--color-text-primary);
  border-color: var(--color-text-primary);
}

/* Place card is a flex column ordered explicitly (see the #place-view
   order rules above) - keep the persona chip right under the
   subheading rather than letting it fall to the unordered tail. */
#place-view .persona-chip {
  order: -5;
  align-self: flex-start;
}

/* ------------------------------------------------------------------
   Layers panel (rail + mobile steps sheet) - see renderLayersPanel().
   ------------------------------------------------------------------ */
.rail-layers {
  padding-top: var(--space-4);
}

.rail-layers > .rail-eyebrow,
.steps-sheet-layers-title {
  display: block;
  width: auto;
  margin: 0 0 var(--space-3);
}

.steps-sheet-layers-title {
  margin-top: var(--space-4);
}

.layers-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.layer-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-primary);
}

.layer-row--off {
  color: var(--color-text-tertiary);
}

.layer-row--off .layer-swatch {
  opacity: 0.35;
}

.layer-swatch {
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  border: 1px solid rgba(0, 0, 0, 0.15);
}

.layer-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.layer-note {
  flex: none;
  font-size: 10px;
  color: var(--color-text-tertiary);
}

.layer-toggle {
  flex: none;
  width: 30px;
  height: 18px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  padding: 0;
  cursor: pointer;
  position: relative;
}

.layer-toggle::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--color-text-tertiary);
  transition: transform 120ms ease, background 120ms ease;
}

.layer-toggle[aria-checked="true"] {
  background: var(--color-text-primary);
  border-color: var(--color-text-primary);
}

.layer-toggle[aria-checked="true"]::after {
  background: var(--color-on-primary);
  transform: translateX(12px);
}

.layer-toggle:disabled {
  cursor: default;
  opacity: 0.55;
}

.layer-toggle:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------
   Persona guide card + framing tips (see renderPersonaGuide) and the
   per-persona accent on the configurator card - same variable-swap
   pattern as .configurator-main-card.race-mode-on above.
   ------------------------------------------------------------------ */
#place-view .persona-guide {
  order: -5;
  margin: 0 0 var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--persona-accent, var(--color-accent));
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.persona-guide[hidden] {
  display: none;
}

.persona-guide-title {
  margin: 0 0 var(--space-2);
  font-family: var(--font-family-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

.persona-guide-intro {
  margin: 0 0 var(--space-3);
  font-size: 15px;
  line-height: 1.5;
}

.persona-guide-includes {
  list-style: none;
  margin: 0 0 var(--space-3);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.persona-guide-includes li {
  padding: 3px 10px;
  border-radius: 999px;
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-text-primary);
  background: color-mix(in srgb, var(--persona-accent, var(--color-accent)) 12%, var(--color-surface));
  border: 1px solid color-mix(in srgb, var(--persona-accent, var(--color-accent)) 30%, var(--color-border));
}

.persona-guide-how summary {
  cursor: pointer;
  font-size: 13px;
  color: var(--color-text-secondary);
}

.persona-guide-how p {
  margin: var(--space-2) 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--color-text-secondary);
}

.persona-tips {
  margin: 0 0 var(--space-3);
  padding-left: 18px;
  font-size: 13px;
  color: var(--color-text-secondary);
}

.persona-tips[hidden] {
  display: none;
}

/* Persona accents - set on the configurator card as data-persona. */
.configurator-main-card[data-persona="golfer"] { --persona-accent: #2e8b3d; }
.configurator-main-card[data-persona="hiker"] { --persona-accent: #a86a2a; }
.configurator-main-card[data-persona="runner"] { --persona-accent: var(--color-accent-race); }
.configurator-main-card[data-persona="architecture"] { --persona-accent: #4a4a4a; }
.configurator-main-card[data-persona="home"] { --persona-accent: #c9743d; }
.configurator-main-card[data-persona="graduation"] { --persona-accent: #6b4fbb; }
#place-view .step-card-wide[data-persona="golfer"] { --persona-accent: #2e8b3d; }
#place-view .step-card-wide[data-persona="hiker"] { --persona-accent: #a86a2a; }
#place-view .step-card-wide[data-persona="runner"] { --persona-accent: var(--color-accent-race); }
#place-view .step-card-wide[data-persona="architecture"] { --persona-accent: #4a4a4a; }
#place-view .step-card-wide[data-persona="home"] { --persona-accent: #c9743d; }
#place-view .step-card-wide[data-persona="graduation"] { --persona-accent: #6b4fbb; }

/* ?trail= preset card (the hub's trail gallery): the finished map's picture and "area set" */
.trail-preset { display: flex; gap: 12px; align-items: center; margin: 8px 0 10px; padding: 10px; border: 1px solid var(--color-border); border-radius: 12px; background: var(--color-surface); }
.trail-preset img { width: 96px; height: 96px; border-radius: 8px; object-fit: cover; flex: 0 0 auto; }
.trail-preset strong { display: block; font-size: 14px; margin-bottom: 2px; }
.trail-preset span { display: block; font-size: 12.5px; color: var(--color-text-secondary); }
.trail-preset .btn-link { margin-top: 4px; font-size: 12.5px; }

/* The key under the model (app.js renderModelKey): a row of chips, one per layer on this map, in the theme's
   colours; the elevation chip carries the four bands. Tap one and that layer lights up on the model. */
.model-key { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: var(--space-3); }
.model-key[hidden] { display: none; }
.model-key-lead { flex-basis: 100%; font-size: 11.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--color-text-tertiary); margin-bottom: 2px; }
.model-key-chip { display: inline-flex; align-items: center; gap: 7px; padding: 5px 10px 5px 6px; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-bg); color: var(--color-text-primary); font-family: inherit; font-size: 12.5px; line-height: 1; cursor: pointer; }
.model-key-chip:hover, .model-key-chip:focus-visible { border-color: var(--color-text-secondary); outline: none; }
.model-key-chip:active { transform: translateY(1px); }
.model-key-dot { width: 14px; height: 14px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.18); flex: none; }
.model-key-bands { display: inline-flex; height: 14px; border-radius: 4px; overflow: hidden; border: 1px solid rgba(0, 0, 0, 0.18); flex: none; }
.model-key-bands span { width: 11px; height: 100%; display: block; }
.model-key-elevation { padding-left: 6px; }
@media (max-width: 899px) { .model-key { margin-top: var(--space-2); } .model-key-chip { font-size: 12px; } }
