/* Visitor trip page — photo-forward journal, issue 34 re-skin.
   Visual contract: .scratch/mocks/v3/f-synthesis.html (palette, Light type,
   vibrant dots, day-based dock).
   Structure unchanged from slice-19/33; this file replaces palette + type only.
   Fraunces removed from visitor page (deferred cleanup — still in fonts.css). */

:root {
  /* Greige palette (replaces cream/terracotta from slice-19) */
  --paper: #E7E4DD;
  --paper-2: #EFEDE7;
  --ink: #17150F;
  --ink-2: #5C574C;
  --line: #CDC8BD;

  /* Runtime accent — greige until set by scroll-spy from photo HSV.
     NOTE: coloured text is accepted per spec; clamp/fallback to --ink only if
     a hue actually fails contrast on --paper (watch-item for future audit). */
  --accent: #8A8580;
  --accent-ink: #6E6A64;

  /* Type: display = Helvetica Neue Light (native Apple) with Inter Light as the
     self-hosted fallback for Windows/Android. Body = Outfit. Data = mono. */
  --display: 'Helvetica Neue', 'Inter', Arial, sans-serif;
  --display-wght: 300;
  --display-track: -.012em;
  --body: 'Outfit', system-ui, sans-serif;
  --mono: ui-monospace, 'SF Mono', Menlo, monospace;

  --pad: 18px;
  --dock-h: 62px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
  font-family: var(--body);
  background: var(--paper);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}
img { display: block; }
::selection { background: var(--accent); color: #fff; }

/* Two columns: ~42% sticky map / ~58% story. Fixed bottom dock reserves space.
   dvh (not vh) so the fixed dock tracks the iPad Firefox address bar instead of
   hanging in the air. Residual: layout height recomputes as the bar animates, so
   the map/dock "bounce" slightly during that transition. If that becomes
   annoying, switch these 100dvh refs to 100svh (sizes to the bar-visible height,
   no mid-scroll resize) — cost is a background strip under the dock when the bar
   hides. See fix/ipad-firefox-dvh-footer. */
.app {
  display: grid;
  grid-template-columns: 42% 58%;
  min-height: 100dvh;
  padding-bottom: var(--dock-h);
}

/* ---------- MAP COLUMN ---------- */
.map {
  position: sticky;
  top: 0;
  height: calc(100dvh - var(--dock-h));
  border-right: 1px solid var(--ink);
  overflow: hidden;
}
#map {
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
  z-index: 1;
  /* Deliberate, unconditional: takes the last colour out of the Positron
     tiles (light_nolabels + light_only_labels, built in mapTiles.js) so the
     route line is the only saturated thing on the map. */
  filter: grayscale(.4) contrast(.95);
}
.leaflet-container { background: #DFDCD4; }

/* Map tag top-left corner */
.map-tag {
  position: absolute;
  left: 16px;
  top: 16px;
  z-index: 610;
  font: 600 10px/1 var(--mono);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: var(--paper);
  border: 1px solid var(--line);
  padding: 6px 9px;
}
.map-tag i {
  display: inline-block;
  width: 8px;
  height: 8px;
  background: var(--accent);
  margin-right: 7px;
  vertical-align: middle;
  transition: background .3s;
}

/* Vibrant dot markers — replaces photo thumbnails from slice-19.
   Each dot's colour is set via inline style from dotColor() in index.js.
   No count text in dot; count on hover via marker title attribute. */
.dot-wrap { background: none !important; border: none !important; }
.dot {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 2px solid var(--paper);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .32);
  transition: transform .2s, box-shadow .2s, background-color .25s;
}
/* Clusters: faint ring hints "more than one"; size carries count */
.dot.cluster {
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px rgba(23, 21, 15, .22), 0 1px 4px rgba(0, 0, 0, .32);
}
/* Active marker: scale + ink ring */
.dot.active {
  transform: scale(1.5);
  box-shadow: 0 0 0 3px var(--paper), 0 0 0 5px var(--ink), 0 1px 5px rgba(0, 0, 0, .4);
  z-index: 1000;
}

/* ---------- STORY COLUMN ---------- */
.feed { padding-bottom: calc(var(--dock-h) + 48px); }

/* Masthead lead: timeframe eyebrow + description (no longer in the dock). */
.masthead {
  padding: 48px var(--pad) 26px;
  border-bottom: 1px solid var(--ink);
}
.masthead-ek {
  font: 600 11px/1 var(--mono);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--accent-ink);
  transition: color .3s;
}
.masthead-desc {
  max-width: 52ch;
  margin-top: 16px;
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.6;
}

/* Step editorial cards */
.step {
  position: relative;
  border-bottom: 1px solid var(--line);
  padding: 26px 0 30px;
  cursor: pointer;
}
/* Active step: left accent bar */
.step::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: transparent;
  transition: background .3s;
}
.step.active::before { background: var(--accent); }

.eyebrow {
  padding: 0 var(--pad);
  font: 600 11px/1 var(--mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent-ink);
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  transition: color .3s;
}
.eyebrow .eyebrow-day { color: var(--ink); opacity: .4; }
.eyebrow .eyebrow-date { color: var(--ink-2); }

.step h2 {
  font-family: var(--display);
  font-weight: var(--display-wght);
  font-size: clamp(20px, 2.4vw, 28px);
  letter-spacing: calc(var(--display-track) + .005em);
  line-height: 1.12;
  margin: 10px 0 0;
  padding: 0 var(--pad);
}
.step h2:empty { display: none; }

/* Aspect-aware media (slice 33) — templates unchanged, colours updated. */
.media { margin-top: 16px; display: grid; gap: 5px; }
.media img {
  width: 100%;
  display: block;
  object-fit: cover;
  object-position: center 30%;
  background: var(--line);
  cursor: zoom-in;
}

/* 1 photo */
.media.heroW img { aspect-ratio: 4/3; max-height: 62vh; }
.media.heroS { justify-items: center; }
.media.heroS img { aspect-ratio: 1/1; max-width: 64%; }
.media.heroT { justify-items: center; }
.media.heroT img {
  width: auto;
  max-width: 74%;
  max-height: 80vh;
  object-fit: contain;
  background: transparent;
  box-shadow: 0 6px 26px rgba(23, 21, 15, .10);
}

/* Equal columns */
.media.cols2 { grid-template-columns: 1fr 1fr; }
.media.cols3 { grid-template-columns: 1fr 1fr 1fr; }
.media.cols2 img, .media.cols3 img { aspect-ratio: 3/4; }

/* Stacked rows */
.media.rows { grid-template-columns: 1fr; }
.media.rows img { aspect-ratio: 3/2; }

/* Squares */
.media.sq2 { grid-template-columns: 1fr 1fr; }
.media.sq3 { grid-template-columns: 1fr 1fr 1fr; }
.media.sq2 img, .media.sq3 img { aspect-ratio: 1/1; }

/* T + WW */
.media.twoWW { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.media.twoWW img { height: 100%; }
.media.twoWW img:first-child { grid-row: 1 / span 2; }

/* TT + W */
.media.wTT { grid-template-columns: 1fr 1fr; }
.media.wTT img:first-child { grid-column: 1 / span 2; aspect-ratio: 3/2; }
.media.wTT img { aspect-ratio: 3/4; }

/* 2×2 grid — dominant fallback */
.media.grid { grid-template-columns: 1fr 1fr; }
.media.grid img, .media.grid .more img { aspect-ratio: 1/1; }

/* Overflow count-chip */
.media .more { position: relative; cursor: pointer; overflow: hidden; }
.media .more img { height: 100%; }
.media .more::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(120% 120% at 100% 100%, rgba(23, 21, 15, .30), transparent 55%);
}
.media .more .chip-count {
  position: absolute;
  right: 12px;
  bottom: 12px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px 6px 9px;
  border-radius: 999px;
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: rgba(255, 255, 255, .16);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
  border: 1px solid rgba(255, 255, 255, .38);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .22);
  transition: background .2s;
}
.media .more:hover .chip-count { background: rgba(255, 255, 255, .26); }
.media .more .chip-count svg { width: 15px; height: 15px; opacity: .95; }

/* Hero caption inline */
.cap { padding: 11px var(--pad) 0; font-size: 13px; color: var(--ink-2); font-style: italic; }

/* Inline message (404 / 401 / no-coords) */
.inline-msg { padding: 64px var(--pad); text-align: center; color: var(--ink-2); font-size: 15px; line-height: 1.6; }

/* ---------- BOTTOM DOCK ---------- */
/* Trip identity (left: swatch dot + title) + orientation (right: Day N + place).
   Top edge = accent progress fill. Pulses while ongoing, still when finished. */
.dock {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1100;
  min-height: var(--dock-h);
  display: flex;
  align-items: center;
  gap: 28px;                 /* wide gutter separates identity from orientation */
  padding: 10px 28px;
  background: rgba(231, 228, 221, .78);
  -webkit-backdrop-filter: blur(16px) saturate(1.1);
  backdrop-filter: blur(16px) saturate(1.1);
  border-top: 1px solid var(--ink);
}
/* Left group: swatch + title read as one unit (tight gap). */
.dock-id {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
  flex: 0 1 auto;
}
.dock-id .dock-title { overflow: hidden; text-overflow: ellipsis; }
.dock-prog {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 3px;
  background: var(--line);
}
.dock-prog span {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width .45s ease, background .3s;
}
/* Ongoing trips read newest-first, so the fill is right-anchored: the newest
   (top) post is a full bar, and each older post leaves the filled segment
   pinned to the right edge (oldest = a sliver on the far right). Mirrors the
   reversed feed. Finished trips keep the default left-anchored growth. */
.dock.ongoing .dock-prog span { margin-left: auto; }
/* ONE swatch dot — pulses while ongoing, still when finished. */
.dock-swatch {
  position: relative;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
  transition: background .3s;
}
.dock.ongoing .dock-swatch::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--accent);
  animation: pulse 1.7s ease-out infinite;
}
@keyframes pulse {
  0%   { transform: scale(1); opacity: .55; }
  100% { transform: scale(3.4); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .dock.ongoing .dock-swatch::after { animation: none; }
  /* Skip easing on dot transitions under reduced motion; colour change is instant. */
  .dot { transition: none; }
}
.dock-title {
  font-family: var(--display);
  font-weight: var(--display-wght);
  font-size: 20px;
  letter-spacing: var(--display-track);
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);         /* identity = the darkest, most prominent string */
}
/* Right group: a stacked readout (place over day), right-aligned. The vertical
   stack is what separates it from the identity group — no divider needed. */
.dock-live {
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: 4px;
  min-width: 0;
  text-align: right;
}
.dock-here {
  font-family: var(--display);
  font-weight: var(--display-wght);
  font-size: 17px;           /* smaller than the title so brand ≠ current place */
  letter-spacing: var(--display-track);
  line-height: 1;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.dock-here:empty { display: none; }
/* The odometer line: accent-tinted mono, letter-spaced like an instrument readout. */
.dock-day {
  font: 500 11px/1 var(--mono);
  letter-spacing: .14em;
  color: var(--accent-ink);
  white-space: nowrap;
  transition: color .3s;
}

/* ---------- MOBILE: collapsible mini-map banner (slice-19, re-skinned) ---------- */
.map-toggle { display: none; }
@media (max-width: 760px) {
  :root { --dock-h: 56px; --pad: 20px; }
  .app { grid-template-columns: 1fr; }
  .map {
    position: relative;
    top: 0;
    height: 190px;
    border-right: 0;
    border-bottom: 1px solid var(--line);
    transition: height .28s ease;
  }
  .map.expanded { height: calc(100dvh - var(--dock-h)); }
  .map-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    z-index: 5;
    right: 12px;
    bottom: 12px;
    width: 40px;
    height: 40px;
    border-radius: 999px;
    border: 0;
    cursor: pointer;
    color: #fff;
    background: rgba(23, 21, 15, .72);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    box-shadow: 0 4px 14px rgba(0, 0, 0, .3);
  }
  .map-toggle svg { width: 20px; height: 20px; }
  /* Keep the two groups on one row; the stacked right readout fits the 56px dock. */
  .dock { padding: 8px 16px; gap: 16px; }
  .dock-title { font-size: 18px; }
  .dock-here { font-size: 15px; }
  .masthead { padding: 28px var(--pad) 18px; }
  .step h2 { font-size: 22px; }
  .media.heroW img { aspect-ratio: 4/3; max-height: 50vh; }
  .media.heroT img { max-width: 88%; max-height: 70vh; }
}

/* ---------- LIGHTBOX (dark overlay; glass-chip controls; unchanged structure) ---------- */
.lightbox-overlay {
  display: flex;
  visibility: hidden;
  opacity: 0;
  position: fixed;
  inset: 0;
  z-index: 9999;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  background: radial-gradient(120% 120% at 50% 40%, #1c2124 0, #121517 70%, #0d0f10 100%);
  transition: opacity .25s ease, visibility .25s ease;
}
.lightbox-overlay.show { visibility: visible; opacity: 1; }
body.lightbox-open { overflow: hidden; }
.lightbox-stage {
  position: relative;
  max-width: 92vw;
  max-height: 94vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}
/* Issue 73: removeAttribute('src') collapses #lightbox-img to zero height.
   showImage() reserves the loading/error box at the photo's own aspect ratio
   (lightbox.js _sizeStageFor, inline width/height) so the reveal never
   resizes the stage; this clamp is only the fallback for photos with no
   known dimensions (legacy rows — same nullable width/height orientOf()
   already defaults to wide for). */
.lightbox-stage.is-loading,
.lightbox-stage.is-error {
  min-width: clamp(240px, 60vw, 480px);
  min-height: clamp(240px, 50vh, 480px);
}
#lightbox-img {
  max-width: 92vw;
  max-height: 84vh;
  border-radius: 4px;
  display: block;
  box-shadow: 0 30px 80px rgba(0, 0, 0, .55);
  transition: transform .25s cubic-bezier(.4, 0, .2, 1);
}
/* Hidden via visibility (not display:none) while loading/errored, so it never
   contributes a broken-image icon or stale frame to the stage. (No opacity
   companion: the transition above is transform-only, so opacity does no
   fading work here.) */
.lightbox-stage.is-loading #lightbox-img,
.lightbox-stage.is-error #lightbox-img {
  visibility: hidden;
}
.lightbox-stage:has(#lightbox-caption:empty) #lightbox-img { max-height: 90vh; }
@keyframes lightboxFade { 0% { opacity: .7; transform: scale(.98); } 100% { opacity: 1; transform: scale(1); } }
#lightbox-img.pulse { animation: lightboxFade .25s cubic-bezier(.4, 0, .2, 1); }

/* Spinner + error placeholder are both a hidden overlay centred on the
   stage; only the deltas (layout direction, colour) differ. */
.lightbox-spinner,
.lightbox-error {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
/* Visibility is driven by aria-hidden alone (the one thing anything actually
   observes/asserts) — no separate `.show` class to keep in sync by hand. */
.lightbox-spinner[aria-hidden="false"] { display: flex; }
.lightbox-spinner {
  flex-direction: column;
  gap: 10px;
}
.lightbox-spinner-ring {
  width: 34px;
  height: 34px;
  border: 3px solid rgba(255, 255, 255, .25);
  border-top-color: rgba(255, 255, 255, .9);
  border-radius: 50%;
  animation: lightbox-spin .8s linear infinite;
}
@keyframes lightbox-spin { to { transform: rotate(360deg); } }
/* Static ring + no reveal-pulse under reduced motion (companion to the
   reduced-motion block at the top of the file, kept here so the base
   animation/transition rules it overrides — just above — stay adjacent in
   source order and actually win the cascade: equal specificity means an
   earlier block would lose). */
@media (prefers-reduced-motion: reduce) {
  .lightbox-spinner-ring { animation: none; }
  #lightbox-img.pulse { animation: none; }
  #lightbox-img { transition: none; }
}
.lightbox-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Error placeholder: a failed photo ends here, not in an endless spinner. */
.lightbox-error {
  color: rgba(255, 255, 255, .75);
  font-size: 14px;
  letter-spacing: .02em;
}
.lightbox-stage.is-error .lightbox-error { display: flex; }
.chip {
  position: fixed;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  cursor: pointer;
  background: rgba(255, 255, 255, .13);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
  backdrop-filter: blur(16px) saturate(160%);
  border: 1px solid rgba(255, 255, 255, .30);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .30);
  transition: background .2s;
}
.chip:hover { background: rgba(255, 255, 255, .24); }
.chip svg { width: 22px; height: 22px; }
.lightbox-close { top: 22px; right: 24px; width: 42px; height: 42px; border-radius: 999px; }
.lightbox-btn { top: 50%; transform: translateY(-50%); width: 50px; height: 50px; border-radius: 999px; }
.lightbox-btn:hover { transform: translateY(-50%); }
#lightbox-prev { left: 24px; }
#lightbox-next { right: 24px; }
.lightbox-counter {
  position: fixed;
  top: 24px;
  left: 26px;
  color: rgba(255, 255, 255, .82);
  font-size: 13px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: .03em;
}
#lightbox-caption {
  max-width: 680px;
  text-align: center;
  color: rgba(255, 255, 255, .88);
  font-size: 15px;
  line-height: 1.55;
}
#lightbox-caption:empty { display: none; }
.lightbox-hint {
  position: fixed;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, .4);
  font-size: 11.5px;
  letter-spacing: .04em;
}
@media (max-width: 760px) {
  #lightbox-img { max-height: 64vh; max-width: 94vw; }
  .lightbox-btn { display: none; }
  #lightbox-caption { padding: 0 18px; font-size: 14px; }
}

/* Map credits — ODbL and CARTO's free tier both require the OpenStreetMap and
   CARTO attribution to stay reachable from the map. It collapses to an (i) so
   it does not sit across the route, which the OSMF attribution guideline
   allows as long as the licence info stays findable: this expands on hover, on
   tap (.is-open, set by mapTiles.js) and on keyboard focus. The links are
   clipped rather than display:none precisely so they stay tabbable, which is
   what makes :focus-within fire. Bottom-LEFT, because .map-toggle owns the
   bottom-right corner under 760px. */
.leaflet-control-attribution.map-credits {
  max-width: 22px;
  overflow: hidden;
  white-space: nowrap;
  cursor: pointer;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 0;
  box-shadow: none;
  color: var(--ink-2);
  font: 500 10px/1.6 var(--mono);
  padding: 2px 5px;
  transition: max-width .22s ease;
}
.leaflet-control-attribution.map-credits::before {
  content: "\24D8";
  font-size: 11px;
  margin-right: 6px;
}
.leaflet-control-attribution.map-credits:hover,
.leaflet-control-attribution.map-credits:focus-within,
.leaflet-control-attribution.map-credits.is-open { max-width: 42ch; }
.leaflet-control-attribution.map-credits a { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
  .leaflet-control-attribution.map-credits { transition: none; }
}
