/*
 * CurbCheck — page shell: the advisory strip, the floating rail, the map, and
 * the responsive behaviour. Component internals live in components.css.
 *
 * No remote assets of any kind (SPEC §3.4). The one web font is vendored and
 * served from this origin — see the face declared below and web/fonts/MANIFEST.md.
 * Every colour, space, and duration comes from tokens.css; nothing here invents
 * a value.
 *
 * Layout rule that the phone bug (UX_AUDIT P0-4, an 87,609 px document) turns
 * into law: the document itself never scrolls. `body` is a fixed-height flex
 * column and every scrollable region — the rail, the results list, the detail
 * sheet — owns its own `overflow-y`.
 */

/* One file, both axes (opsz 14–32, wght 100–900), same origin. `swap` is
   irrelevant in practice — the file is local and ~72 KB — but it guarantees the
   verdict words are readable even if the font response is slow. */
@font-face {
  font-family: "Inter var";
  src: url("./fonts/InterVariable-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}

body {
  display: flex;
  flex-direction: column;
  font-family: var(--font-sans);
  font-size: var(--t-body);
  line-height: var(--lh-normal);
  color: var(--c-text);
  background: var(--c-page);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Inter's `calt` contextual alternates are what make its ASCII punctuation
     sit correctly; optical sizing is what keeps 11 px caps from going spindly. */
  font-optical-sizing: auto;
  font-feature-settings: "calt" 1;
}

/* Every figure in this UI is compared down a column — walk minutes, prices,
   counts — so nothing is ever set in proportional digits. */
.card-walk,
.card-price,
.card-rank,
.facts dd,
.stat-pill,
.group-count,
.sign-meta,
.summary-line,
.text-input {
  font-variant-numeric: tabular-nums;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

button {
  cursor: pointer;
}

a {
  color: var(--c-accent);
}

h1,
h2,
h3,
h4 {
  letter-spacing: var(--track-display);
  line-height: var(--lh-tight);
}

:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--r-sm);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: calc(var(--strip-h) + var(--s-3));
  z-index: 40;
  padding: var(--s-4) var(--s-6);
  border-radius: var(--r-pill);
  background: var(--c-surface);
  box-shadow: var(--e-3);
  color: var(--c-accent-ink);
  font-size: var(--t-small);
  font-weight: var(--w-semibold);
  text-decoration: none;
}

.skip-link:focus-visible {
  left: var(--s-5);
}

.skip-link + .skip-link:focus-visible {
  left: 190px;
}

/* ---- Advisory strip (SPEC §17) ---------------------------------------- */

.strip {
  position: relative;
  z-index: 20;
  flex: 0 0 auto;
  min-height: var(--strip-h);
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: 0 var(--s-6);
  background: var(--c-strip);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  box-shadow:
    inset 2px 0 0 var(--v-ambiguous),
    0 1px 0 var(--c-glass-border);
  color: var(--v-ambiguous-ink);
}

.strip-text {
  margin: 0;
  font-size: var(--t-caption);
  line-height: var(--lh-tight);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-3);
}

.strip-text strong {
  font-weight: var(--w-semibold);
}

/* A text link, not a bordered pill: the strip is permanent, and a permanent
   button in it reads as an action the user is being asked to take. */
.strip-toggle {
  margin-left: auto;
  flex: 0 0 auto;
  color: inherit;
  font-size: var(--t-caption);
  font-weight: var(--w-medium);
  text-decoration-color: color-mix(in srgb, currentcolor 45%, transparent);
  text-underline-offset: 2px;
}

.strip-toggle:hover {
  text-decoration-color: currentcolor;
}

.full-notice {
  position: relative;
  z-index: 19;
  flex: 0 0 auto;
  max-height: 40vh;
  overflow-y: auto;
  padding: var(--s-6) var(--s-8);
  background: var(--c-strip);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  box-shadow: 0 1px 0 var(--c-glass-border);
  color: var(--v-ambiguous-ink);
}

.full-notice[hidden] {
  display: none;
}

.full-notice p {
  margin: 0;
  max-width: 96ch;
  font-size: var(--t-small);
  line-height: var(--lh-loose);
}

/* ---- Layout: the map is the page, everything else floats over it ------- */

.layout {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}

.map-area {
  position: absolute;
  inset: 0;
  /* How much of the map's right edge a floating sheet is standing on, set by
     `updatePadding()`. The attribution, the Recentre button, MapLibre's own
     controls and the legend's right bound all step out from under it: an
     OpenStreetMap credit no one can read or click is not an attribution, and
     the legend carries the sentence that stops an empty curb reading as a safe
     one (SPEC §11), which cannot be behind a panel while curb is still drawn. */
  --sheet-offset: 0px;
}

.map {
  position: absolute;
  inset: 0;
}

.map:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 3px var(--c-focus);
}

.map-attribution {
  position: absolute;
  right: var(--s-4);
  bottom: var(--s-4);
  z-index: 4;
  margin: 0;
  padding: var(--s-2) var(--s-5);
  border-radius: var(--r-pill);
  background: var(--c-surface-scrim);
  backdrop-filter: var(--blur-chip);
  -webkit-backdrop-filter: var(--blur-chip);
  box-shadow: var(--e-1);
  font-size: var(--t-micro);
  color: var(--c-text-secondary);
}

.map-attribution a {
  color: inherit;
}

/* The legend is a compact glass pill group at the bottom-left of the map, clear
   of the rail: four swatches, and the sentence that stops an empty curb reading
   as a safe one (SPEC §11), which is never a tooltip. */
.legend {
  position: absolute;
  left: calc(var(--panel-w) + var(--s-6) * 2);
  right: calc(var(--s-6) + var(--sheet-offset));
  /* One band above the attribution: when a sheet pushes the credit line left it
     would otherwise land on the no-data sentence. */
  bottom: calc(var(--s-6) + 26px);
  z-index: 4;
  width: fit-content;
  max-width: 420px;
  padding: var(--s-4) var(--s-6);
  border-radius: var(--r-pill);
  border: 1px solid var(--c-glass-border);
  background: var(--c-glass);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  box-shadow: var(--e-lift);
  font-size: var(--t-micro);
}

.legend-rows {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-6);
}

.legend-row {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
}

.legend-row .legend-word {
  color: var(--c-text);
  font-weight: var(--w-medium);
  letter-spacing: 0.01em;
}

.legend-swatch {
  flex: 0 0 auto;
  width: 22px;
  height: 0;
}

.legend-note {
  margin: var(--s-2) 0 0;
  line-height: var(--lh-normal);
  color: var(--c-text-muted);
}

/* The one map button that is ours rather than MapLibre's. */
.map-control {
  position: absolute;
  right: calc(var(--s-6) + var(--sheet-offset));
  top: var(--s-6);
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  min-height: 34px;
  padding: 0 var(--s-6) 0 var(--s-5);
  border: 1px solid var(--c-glass-border);
  border-radius: var(--r-pill);
  background: var(--c-glass);
  backdrop-filter: var(--blur-chip);
  -webkit-backdrop-filter: var(--blur-chip);
  box-shadow: var(--e-lift);
  font-size: var(--t-caption);
  font-weight: var(--w-medium);
  color: var(--c-text-secondary);
  transition: color var(--m-fast) var(--ease-out);
}

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

.map-control:hover {
  color: var(--c-text);
}

.map-control-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 2px solid var(--c-marker);
}

/* MapLibre positions its controls itself; the sheet still has to be able to
   push them aside, or the zoom buttons end up under a panel. */
.maplibregl-ctrl-bottom-right {
  right: var(--sheet-offset);
}

/* A button that reads as a link: the attribution line is prose. */
.link-button {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--c-accent);
  font-size: inherit;
  text-decoration: underline;
}

/* ---- The rail: a floating glass panel with its own scroll -------------- */

.rail {
  position: absolute;
  left: var(--s-6);
  top: var(--s-6);
  bottom: var(--s-6);
  width: var(--panel-w);
  z-index: 6;
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-radius: var(--r-2xl);
  border: 1px solid var(--c-glass-border);
  background: var(--c-glass);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  box-shadow: var(--e-glass);
  overflow: hidden;
}

.rail::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: var(--c-glass-hairline);
  pointer-events: none;
}

.rail-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-7) var(--s-7) var(--s-9);
  scrollbar-width: thin;
}

/* The wordmark is a signature, not a headline: at 15 px in the corner it names
   the product without competing with the answer (DESIGN_DIRECTION §2.4). */
.wordmark {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin: 0 0 var(--s-6);
  font-size: var(--t-display);
  font-weight: var(--w-semibold);
  letter-spacing: var(--track-display);
  color: var(--c-text-secondary);
}

/* First load: the search card sits in the middle of the rail with one line
   saying what the app answers. After the first search the rail is a list, so
   the tagline goes and the content returns to the top. */
.rail-scroll.is-first-load {
  display: flex;
  flex-direction: column;
  /* `safe` matters: a plain `center` in an overflowing scroll container pushes
     the first field above the scrollable area, where it cannot be reached.
     Browsers without `safe` reject the declaration and fall back to
     flex-start, which is the behaviour `safe` asks for anyway. */
  justify-content: safe center;
  /* The wordmark below is taken out of the flow and pinned to the corner, so
     the flow has to leave its corner empty: on a 14-inch laptop the content
     is taller than the rail, `safe center` becomes flex-start, and without
     this the tagline sat on top of the wordmark. */
  padding-top: calc(var(--s-7) + var(--t-display) * var(--lh-normal) + var(--s-6));
}

.rail-scroll.is-first-load .wordmark {
  position: absolute;
  left: var(--s-7);
  top: var(--s-7);
  margin: 0;
}

.tagline {
  margin: 0 0 var(--s-7);
  max-width: 28ch;
  font-size: var(--t-lead);
  font-weight: var(--w-medium);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-display);
  color: var(--c-text-secondary);
}

.rail-scroll:not(.is-first-load) .tagline {
  display: none;
}

.wordmark::before {
  content: "";
  width: 9px;
  height: 9px;
  flex: 0 0 auto;
  border-radius: 2px;
  background: var(--v-legal);
  transform: rotate(45deg);
}

/* An indeterminate bar, not a spinner: the search takes 2-6 s and there is no
   percentage to report. Under reduced motion it stops travelling and sits
   still as a full-width accent line, which still says "working". */
.rail-progress {
  position: absolute;
  inset: 0 0 auto;
  z-index: 2;
  height: 2px;
  overflow: hidden;
  background: var(--c-accent-soft);
}

.rail-progress[hidden] {
  display: none;
}

.rail-progress::after {
  content: "";
  position: absolute;
  inset: 0;
  width: 40%;
  background: var(--c-accent);
  animation: rail-progress 1.1s var(--ease-in-out) infinite;
}

@keyframes rail-progress {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(300%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .rail-progress::after {
    width: 100%;
    animation: none;
    opacity: 0.55;
  }
}

/* The previous answer while the next one is being fetched. Dimmed and inert,
   never removed: a screen that empties and a screen that is being replaced look
   identical, and only one of them is what happened. */
.results-region.is-stale,
.stats.is-stale,
.status.is-stale,
.caveat-summary.is-stale {
  opacity: 0.4;
  pointer-events: none;
  transition: opacity var(--m-base) var(--ease-out);
}

/* The drag handle is the phone drawer's detent control; on desktop the rail is
   always open and the handle would be noise. */
.drawer-handle {
  display: none;
}

/* ---- Status, notices, caveats ----------------------------------------- */

.status {
  margin: var(--s-4) 0 0;
  font-size: var(--t-caption);
  color: var(--c-text-muted);
  min-height: 1.2em;
}

.status-error {
  color: var(--v-illegal-ink);
  font-weight: var(--w-medium);
}

.notices:empty {
  display: none;
}

.notice-block {
  margin: var(--s-5) 0 0;
  padding: var(--s-5) var(--s-6);
  border-radius: var(--r-lg);
  font-size: var(--t-small);
  line-height: var(--lh-normal);
  box-shadow: var(--e-1);
}

.notice-block h3 {
  margin: 0 0 var(--s-2);
  font-size: var(--t-body);
  font-weight: var(--w-semibold);
}

.notice-block p {
  margin: 0;
}

.notice-block .ghost {
  margin-top: var(--s-4);
}

.notice-error {
  background: var(--v-illegal-soft);
  color: var(--v-illegal-ink);
  box-shadow:
    inset 3px 0 0 var(--v-illegal),
    var(--e-1);
}

.notice-warn {
  background: var(--v-ambiguous-soft);
  color: var(--v-ambiguous-ink);
  box-shadow:
    inset 3px 0 0 var(--v-ambiguous),
    var(--e-1);
}

.notice-empty {
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--e-1);
}

.caveats {
  list-style: none;
  margin: var(--s-2) 0 0;
  padding: 0;
  font-size: var(--t-caption);
  line-height: var(--lh-normal);
  color: var(--c-text-secondary);
}

.caveats li {
  position: relative;
  padding-left: var(--s-6);
}

.caveats li::before {
  content: "•";
  position: absolute;
  left: var(--s-2);
  color: var(--c-text-muted);
}

.caveats li + li {
  margin-top: var(--s-2);
}

/* ---- Toasts ----------------------------------------------------------- */

.toasts {
  position: fixed;
  left: 50%;
  bottom: var(--s-8);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  transform: translateX(-50%);
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-6);
  border-radius: var(--r-pill);
  background: var(--c-text);
  color: var(--c-text-inverse);
  font-size: var(--t-small);
  box-shadow: var(--e-3);
  pointer-events: auto;
  animation: toast-in var(--m-base) var(--ease-out);
}

.toast button {
  border: 0;
  background: transparent;
  color: inherit;
  font-weight: var(--w-semibold);
  text-decoration: underline;
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ---- Tablet: 641–1099 px ---------------------------------------------- */

@media (max-width: 1099px) {
  .rail {
    width: 340px;
    left: var(--s-5);
    top: var(--s-5);
    bottom: var(--s-5);
  }

  .rail-scroll {
    padding: var(--s-6) var(--s-6) var(--s-8);
  }

  /* Hugs the left of the map so the detail sheet, which is min(520px, 100%)
     here, does not sit on top of it. */
  .legend {
    left: calc(340px + var(--s-5) * 2);
    bottom: calc(var(--s-5) + 26px);
    max-width: 360px;
  }
}

/* ---- Phone: ≤640 px ---------------------------------------------------
   Map on top, the rail becomes a bottom drawer with three detents, and the
   detail sheet is a second layer over it. Nothing here scrolls the document. */

@media (max-width: 640px) {
  .strip {
    min-height: auto;
    padding: var(--s-4) var(--s-5);
    align-items: flex-start;
  }

  .strip-toggle {
    margin-left: var(--s-3);
  }

  /* Phone: the legend moves to the top-left — the bottom belongs to the
     drawer — and stays a wrapped pill group with its sentence attached. */
  .legend {
    left: var(--s-4);
    right: var(--s-4);
    bottom: auto;
    top: var(--s-4);
    max-width: none;
    padding: var(--s-3) var(--s-5);
    border-radius: var(--r-lg);
  }

  .legend-rows {
    gap: var(--s-1) var(--s-5);
  }

  .legend-swatch {
    width: 18px;
  }

  /* The drawer owns the bottom of a phone screen, so the two floating controls
     ride above it in their own bands. `.rail` precedes `.map-area`, so its
     detent is readable here as a sibling state and does not have to be
     mirrored in JS. */
  .map-area {
    --drawer-offset: var(--drawer-peek);
  }

  .rail[data-detent="half"] ~ .map-area {
    --drawer-offset: 58vh;
  }

  .map-attribution {
    bottom: calc(var(--drawer-offset) + var(--s-4));
  }

  .map-control {
    top: auto;
    bottom: calc(var(--drawer-offset) + var(--s-10));
    right: var(--s-4);
  }

  .rail[data-detent="full"] ~ .map-area .map-control,
  .rail[data-detent="full"] ~ .map-area .map-attribution {
    display: none;
  }

  .rail {
    left: 0;
    right: 0;
    top: auto;
    bottom: 0;
    width: auto;
    height: var(--drawer-peek);
    border-radius: var(--r-2xl) var(--r-2xl) 0 0;
    border-bottom: 0;
    background: var(--c-glass-raised);
    box-shadow: var(--e-sheet);
    transition: height var(--m-base) var(--ease-out);
  }

  .rail[data-detent="half"] {
    height: 58vh;
  }

  .rail[data-detent="full"] {
    height: calc(100% - var(--s-5));
  }

  .drawer-handle {
    display: flex;
    flex: 0 0 auto;
    flex-direction: column;
    align-items: center;
    gap: var(--s-2);
    width: 100%;
    min-height: var(--tap-min);
    padding: var(--s-3) var(--s-5) var(--s-2);
    border: 0;
    background: transparent;
    touch-action: none;
  }

  .drawer-grip {
    width: 40px;
    height: 4px;
    border-radius: var(--r-pill);
    background: var(--c-border-strong);
  }

  .drawer-summary {
    font-size: var(--t-small);
    font-weight: var(--w-medium);
    color: var(--c-text-secondary);
  }

  .rail-scroll {
    padding: var(--s-3) var(--s-6) var(--s-8);
  }

  /* The drawer's peek detent is 148 px: centring inside it would put the
     search field off the top of a two-line window. */
  .rail-scroll.is-first-load {
    display: block;
    /* The wordmark is hidden on a phone, so nothing needs the room above. */
    padding-top: var(--s-3);
  }

  .wordmark {
    display: none;
  }

  .tagline {
    margin-bottom: var(--s-5);
    font-size: var(--t-body);
  }

  .skip-link + .skip-link:focus-visible {
    left: 150px;
  }
}

/* ---- Motion and contrast preferences ---------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Forced colours strips the verdict hues, so the word, the symbol, and the
   border style have to survive on their own (UX_AUDIT (e) 11). */
@media (forced-colors: active) {
  .rail,
  .legend,
  .detail,
  .card,
  .strip {
    border: 1px solid CanvasText;
    background: Canvas;
    backdrop-filter: none;
  }
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .rail,
  .legend,
  .detail,
  .full-notice {
    background: var(--c-glass-solid);
  }

  .strip {
    background: var(--c-strip-solid);
  }
}
