:root {
  --ink: #0d1a18;
  --paper: #f2f5f3;
  --panel: rgba(255, 255, 255, 0.94);
  --muted: #63716d;
  --rule: #d3ddd9;
  --accent: #0f6068;
  --good: #2f6d4c;
  --caution: #9a6b12;
  --avoid: #a83a2c;
  /* News source badges — muted, distinct, one hue each. */
  --b-press: #8a5a1f;
  --b-bbc: #2b5ea8;
  --b-council: #4f6b35;
  --b-walk: #2f6d4c;
  --shadow: 0 2px 18px rgba(10, 26, 24, 0.16);
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  /* How much of the sheet stays on screen when collapsed: grip + peek. */
  --peek: 98px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e7efec;
    --paper: #0b1312;
    --panel: rgba(17, 28, 26, 0.94);
    --muted: #8b9a95;
    --rule: #26332f;
    --accent: #4fb3bc;
    --good: #63b584;
    --caution: #d9a93c;
    --avoid: #e58273;
    --b-press: #d8a765;
    --b-bbc: #86aae8;
    --b-council: #a8bd83;
    --b-walk: #63b584;
    --shadow: 0 2px 18px rgba(0, 0, 0, 0.5);
  }
}

/* Cross-document view transitions. Every page is now a real page served by
   the server, so moving between them is a browser navigation — this makes
   that a cross-fade rather than a blink. Browsers without it navigate
   exactly as before. */
@view-transition { navigation: auto; }

* { box-sizing: border-box; }

/* A `display` rule on an element that is sometimes `hidden` outranks the UA's
   `[hidden] { display: none }` — #legend and the old radar bar both showed
   through because of it. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

/* Only the map app locks the viewport. The server-rendered pages — a place,
   an area, a list, travel, credits — are documents and have to scroll, so
   the lock is scoped to the page that actually has a map in it rather than
   applied to every page the stylesheet is served to. */
html:has(#map), body:has(#map), body.app {
  height: 100%;
  overflow: hidden;
}

/* The map's space is reserved before MapLibre is even fetched: it is an
   absolutely positioned full-bleed box from the first paint, so the library
   arriving late moves nothing. The fill is a muted water colour so the gap
   before the first tiles reads as a map loading, not as a broken page. */
#map {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--accent) 12%, var(--paper));
}

/* With no JavaScript the server's own list is already in the sheet, but the
   sheet is collapsed and the only thing that opens it is a script. So hand
   that reader the content instead of a locked drawer: the map goes (it needs
   a script to exist at all) and the list stands open and scrolls.
   `scripting: none` rather than a class the JS adds, because a class arrives
   after the first paint and would show everyone the page flipping. */
@media (scripting: none) {
  html:has(#map), body:has(#map) { height: auto; overflow: auto; }
  body { max-width: 46rem; margin-inline: auto; padding: 8px 16px 40px; }
  /* The map needs a canvas and the sheet needs a gesture; neither exists
     here. What is left is a document: the tabs are inert so nothing may be
     hidden behind them, and every panel stands in the flow. */
  #map, #legend, #grip, #peek { display: none; }
  #sheet, #topsheet {
    position: static;
    transform: none;
    max-height: none;
    box-shadow: none;
    padding: 0;
  }
  #sheet-body, #topsheet-body { overflow: visible; }
}

/* ---------- the tab bar, and the panel behind two of its tabs ---------- */

/* Always on screen. This was a drawer you had to drag down before you could
   discover it had tabs in it — the app's only navigation, hidden behind an
   undocumented gesture. */
#top-tabs {
  position: absolute;
  top: var(--safe-top);
  left: 0;
  right: 0;
  z-index: 7;
  display: flex;
  gap: 2px;
  padding: 6px;
  background: var(--panel);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--rule);
  box-shadow: var(--shadow);
}

.top-tab {
  flex: 1 1 0;
  min-width: 0;
  min-height: 44px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 9px;
  cursor: pointer;
}
.top-tab.on {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.top-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

/* News and Traffic cover the map: reading a page through a map behind it
   serves neither. Weather hides this outright — the map *is* the weather
   view, which is the whole point of the arrangement. */
#topsheet {
  position: absolute;
  top: calc(var(--safe-top) + 56px);
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 6;
  background: var(--paper);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 10px 14px calc(var(--safe-bottom) + 20px);
}

/* With a panel in front, the map's furniture is noise. */
body.reading #legend,
body.reading #sheet,
body.reading .maplibregl-ctrl-bottom-right { display: none; }

/* No JavaScript: the tabs are inert, so nothing may be hidden behind them.
   The panel sits in the flow under the map with both sections stacked. */
@media (scripting: none) {
  #top-tabs { position: static; }
  #topsheet { position: static; height: auto; }
}


/* Each section keeps the heading it had when all three were stacked, but the
   first one in a panel sits directly under the tabs, and its rule would be a
   second line a pixel below theirs. */
.top-panel > .section-title:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* Grip at the lower edge, pointing the way the sheet moves. */

/* The weather sentence, now the Weather tab of the top sheet. */
.day-line {
  margin: 0 0 10px;
  color: var(--muted);
  font-size: 12.5px;
}

/* ---------- photos: the placeholder, and the fade onto it ----------

   Every <picture> carries a blurred 24px preview of its photo as an inline
   `background-image` — a ~220-byte data URI the server puts in the same
   response as the HTML. These rules only say how to paint it: filling the
   box, centred, once. An <img> is transparent until it decodes, so the blur
   shows through on the first frame and the photograph arrives over the top.
   No script is involved in any of that.

   The placeholder sits on the <picture> and not on the <img> deliberately,
   and this is the one thing here that is easy to get wrong: `opacity` on an
   element fades its own background along with it, so an <img> that carries
   both its placeholder and the fade fades the placeholder to nothing and
   leaves an empty box — which is exactly what happened the first time. Two
   elements, one behind the other, is what makes a cross-fade a cross-fade.

   `.photo-loading` is that fade, and it is the one part that is scripted
   (web/js/photos.js). It is added before the file loads and removed the
   instant it settles, so the failure mode of the fade — no JS, a module that
   threw, an image that already errored — is a photograph that is simply
   visible. Never write the opacity rule the other way round. */
picture {
  display: block;
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  /* The photo's own corners are cut here rather than on the <img>, so the
     placeholder behind it is cut to the same shape. */
  overflow: hidden;
}
picture img { display: block; width: 100%; height: auto; }
picture img.photo-loading { opacity: 0; }
picture img { transition: opacity 0.35s ease-out; }

@media (prefers-reduced-motion: reduce) {
  picture img { transition: none; }
}

/* The place photo in the sheet. Attribution is part of the picture, not a
   nicety: CC BY and CC BY-SA both require it wherever the image appears. */
.detail-photo { margin: 10px 0 14px; }
.detail-photo picture {
  border-radius: 10px;
  background-color: color-mix(in srgb, var(--muted) 12%, transparent);
}
.detail-photo figcaption {
  margin-top: 5px;
  font-size: 11px;
  color: var(--muted);
}
.detail-photo figcaption a { color: inherit; }

/* ---------- legend ---------- */

#legend {
  position: absolute;
  left: 8px;
  /* Clear of the zoom and locate buttons in the bottom-right corner. */
  right: 62px;
  bottom: calc(var(--safe-bottom) + var(--peek) + 10px);
  z-index: 4;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding: 7px 10px;
  font-size: 11.5px;
  color: var(--muted);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 9px;
  box-shadow: var(--shadow);
  max-width: max-content;
}
#legend span { display: inline-flex; align-items: center; gap: 4px; }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.dot.good { background: var(--good); }
.dot.caution { background: var(--caution); }
.dot.avoid { background: var(--avoid); }

/* ---------- bottom sheet ---------- */

#sheet {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 6;
  max-height: 78%;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--rule);
  border-radius: 16px 16px 0 0;
  box-shadow: var(--shadow);
  transition: transform 0.22s ease;
  padding-bottom: var(--safe-bottom);
}
#sheet.collapsed { transform: translateY(calc(100% - var(--peek) - var(--safe-bottom))); }

@media (prefers-reduced-motion: reduce) {
  #sheet { transition: none; }
}

/* The handle owns the vertical gesture: without this the browser pans or
   rubber-bands the page instead of giving us the pointermove events. */
#grip, #peek { touch-action: none; }

/* No transition while a finger is on it — the sheet must track the pointer
   exactly, not chase it. */
#sheet.dragging { transition: none; }

#grip {
  flex: none;
  width: 100%;
  height: 26px;
  border: 0;
  background: transparent;
  cursor: pointer;
  position: relative;
}
#grip::before {
  content: "";
  position: absolute;
  top: 9px;
  left: 50%;
  transform: translateX(-50%);
  width: 38px;
  height: 4px;
  border-radius: 2px;
  background: var(--rule);
}

/* The collapsed sheet's title. It says what the list is, and nothing that
   needs interpreting. */
#peek {
  flex: none;
  display: grid;
  gap: 2px;
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  background: transparent;
  border: 0;
  padding: 0 14px 12px;
  cursor: pointer;
}
#sheet:not(.collapsed) #peek {
  padding-bottom: 10px;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 10px;
}
#peek:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
#peek-main {
  display: block;
  min-width: 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- day selector ---------- */

/* A segmented control, not a dropdown: three days is few enough to show at
   once, and one tap to compare them is the whole point. It sits outside the
   scrolling body so it stays put on the detail page too. */
#day-tabs {
  flex: none;
  display: flex;
  gap: 3px;
  margin: 0 14px 10px;
  padding: 3px;
  background: color-mix(in srgb, var(--rule) 45%, transparent);
  border-radius: 11px;
}
.day-tab {
  flex: 1 1 0;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  /* 44px of thumb, not 30. These are three discrete targets sitting side by
     side, so a mis-tap silently shows you the wrong day's verdicts. */
  min-height: 44px;
  padding: 7px 6px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.day-tab.on {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
}
.day-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

#sheet-body {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0 14px 16px;
}

.sheet-title {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 8px;
}
.section-title {
  margin-top: 20px;
  padding-top: 12px;
  border-top: 1px solid var(--rule);
}

/* ---------- area rows ---------- */

.area-block + .area-block { border-top: 1px solid var(--rule); }

.area {
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  background: transparent;
  border: 0;
  padding: 11px 0 10px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 3px 8px;
  cursor: pointer;
}
.area-name {
  font-weight: 650;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.area-name::before {
  content: "›";
  display: inline-block;
  width: 12px;
  margin-right: 4px;
  color: var(--muted);
  font-weight: 400;
  transition: transform 0.15s ease;
}
.area.open .area-name::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
  .area-name::before { transition: none; }
}
/* How many places here are worth going to. Plain text, not a pill: it is a
   fact, and the badge shape is spoken for by the verdicts. */
.area-good {
  justify-self: end;
  align-self: center;
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--muted);
}
/* The same three words, and the same three colours, as the map legend and
   the dots the row is a summary of. `any` is the weather-independent group,
   which has no verdict colour because it was never rated on weather. */
.area-good.good { color: var(--good); }
.area-good.mixed { color: var(--caution); }
.area-good.rough { color: var(--avoid); }
.area-good.any { color: var(--muted); }

.area-meta {
  grid-column: 1 / -1;
  padding-left: 16px;
  font-size: 12px;
  color: var(--muted);
}
.area:focus-visible, .hike:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.points { padding-left: 0; }
/* Indent only under an area header — the flat fallback has none. */
.area-block .points { padding-left: 16px; }
.area-blurb { margin: 0 0 6px; font-size: 12.5px; color: var(--muted); }

/* ---------- point rows ---------- */

.hike {
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  background: transparent;
  border: 0;
  border-top: 1px solid var(--rule);
  padding: 9px 0;
  display: grid;
  grid-template-columns: minmax(0, max-content) max-content 1fr;
  align-items: baseline;
  gap: 2px 7px;
  cursor: pointer;
}
.hike:first-of-type { border-top: 0; }
.hike-name {
  font-weight: 600;
  font-size: 14.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.kind {
  font-size: 11px;
  color: var(--muted);
  text-transform: lowercase;
}
.hike .flag { justify-self: end; }
.hike-why {
  grid-column: 1 / -1;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.flag {
  align-self: center;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 3px 7px;
  border-radius: 4px;
  white-space: nowrap;
}
.flag.good { background: color-mix(in srgb, var(--good) 16%, transparent); color: var(--good); }
.flag.caution { background: color-mix(in srgb, var(--caution) 18%, transparent); color: var(--caution); }
.flag.avoid { background: color-mix(in srgb, var(--avoid) 16%, transparent); color: var(--avoid); }

/* ---------- detail ---------- */

.detail h2 { margin: 0 0 4px; font-size: 18px; letter-spacing: -0.01em; }
.detail .sub { margin: 0 0 10px; font-size: 12.5px; color: var(--muted); }
.detail p { margin: 0 0 10px; font-size: 13.5px; }
/* What the place is and why you'd go — the first thing on the page. */
.detail .why { font-size: 14px; line-height: 1.45; margin: 0 0 10px; }
.detail .numbers { margin: 0 0 4px; font-variant-numeric: tabular-nums; }
.detail .reasons {
  margin: 8px 0 10px;
  padding: 9px 11px;
  border-left: 3px solid var(--rule);
  font-size: 12.5px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.detail .reasons div + div { margin-top: 3px; }
/* The way back out of a place, or out of the About panel. Same button in
   both, built by `backButton` in detail.js, and it is the only way back to
   the area the place belongs to.

   Sticky to the top of `#sheet-body`, for the same reason the breadcrumbs
   are sticky on an article page: a detail panel is several screenfuls on a
   phone and the link left with the first one. `#sheet-body` is the scroll
   container, so `top: 0` pins it there — and because the day tabs live
   *outside* that container, as a flex sibling, the bar can never ride up
   under them.

   No script is involved and nothing is attached to the button: it still
   carries `data-act` and is still driven by the one delegated listener in
   render.js, which is what lets the morpher keep the node across a redraw. */
.back {
  font: inherit;
  font-size: 13px;
  color: var(--accent);
  border: 0;
  cursor: pointer;
  position: sticky;
  top: 0;
  z-index: 2;
  /* Full width of the scroller, bled out through `#sheet-body`'s padding, so
     nothing scrolls past in the margins beside it. */
  display: block;
  width: calc(100% + 28px);
  margin: 0 -14px 10px;
  padding: 8px 14px;
  text-align: left;
  /* `--panel` is translucent by design — the sheet is frosted glass over the
     map. Painting it over the detail text that scrolls beneath would leave
     that text faintly legible through the bar, so it is painted over an
     opaque `--paper` instead and the pair comes out the colour the sheet
     already is. */
  background-color: var(--paper);
  background-image: linear-gradient(var(--panel), var(--panel));
  border-bottom: 1px solid var(--rule);
}

.mwis-note {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--rule);
  font-size: 11.5px;
  color: var(--muted);
}

.maplibregl-ctrl-attrib { font-size: 10px; }

/* Lift the zoom and locate buttons clear of the collapsed sheet. MapLibre
   pins its bottom-right controls 10px from the bottom of the map, which on a
   phone puts them behind the sheet.

   The `#map` prefix is load-bearing: map.js injects MapLibre's stylesheet at
   runtime, so it lands *after* this file and wins at equal specificity. The
   id makes this rule specific enough to hold regardless of order. Same
   offset as #legend, so the two line up. */
#map .maplibregl-ctrl-bottom-right {
  bottom: calc(var(--safe-bottom) + var(--peek) + 10px);
}
.maplibregl-popup-content { display: none; }

/* A map that has given up shouldn't go on impersonating one that is still
   loading: the muted water colour becomes the page's own paper, with the
   message below the only thing in the box. */
#map.map-failed { background: var(--paper); }

/* ---------- the map explaining itself ---------- */

/* A map that fails used to be a blank blue rectangle, which is no use to
   someone on a phone with no devtools — so each way it can fail writes one
   quiet line here, and `?diag` adds the facts to report back. Centred in the
   map: the middle band is the strip of it that neither sheet covers.
   `pointer-events: none` on the wrapper so a half-working map can still be
   dragged through the message; the diagnostics take them back, because the
   point of them is that they can be selected and copied. */
.map-note {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  width: min(23rem, calc(100% - 32px));
  padding: 12px 14px;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: var(--panel);
  box-shadow: var(--shadow);
  backdrop-filter: blur(6px);
  color: var(--ink);
  pointer-events: none;
}
.map-note-msg { margin: 0; font-size: 13px; line-height: 1.45; }
.map-note-hint { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }

.map-diag {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 10px;
  margin: 0;
  font-size: 11.5px;
  line-height: 1.4;
  pointer-events: auto;
  user-select: text;
}
.map-note-msg + .map-diag {
  margin-top: 9px;
  padding-top: 9px;
  border-top: 1px solid var(--rule);
}
.map-diag dt { color: var(--muted); }
.map-diag dd { margin: 0; overflow-wrap: anywhere; }

/* ---------- best window + hourly strip ---------- */

.detail .window {
  margin: 0 0 10px;
  padding: 9px 11px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--good) 12%, transparent);
  color: var(--ink);
  font-size: 13.5px;
}

.strip-wrap { margin: 0 0 12px; }

/* The hours are fetched when a place opens (js/detail.js), so the slot is
   held open at the height the strip will be — 34px of cells, the hour labels
   hanging under them, and the key line below that — rather than letting the
   panel jump under the reader's thumb when it lands. A fetch that fails
   removes the slot altogether and the space goes back: an empty strip would
   read as a forecast of nothing. */
/* Measured, not guessed: the settled height of hours + labels + wind row +
   both keys, so nothing moves when the fetch lands. The narrow phone gets
   more because the wind key wraps to a second line there. */
.strip-wrap.pending { height: 117px; }
@media (max-width: 360px) { .strip-wrap.pending { height: 138px; } }

.strip {
  display: flex;
  gap: 2px;
  height: 34px;
}

.cell {
  flex: 1;
  min-width: 0;
  border-radius: 3px;
  background: color-mix(in srgb, var(--muted) 22%, transparent);
  position: relative;
}
.cell.in { background: color-mix(in srgb, var(--good) 55%, transparent); }
.cell.wet {
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0 3px,
    color-mix(in srgb, var(--accent) 45%, transparent) 3px 6px
  );
}
.cell span {
  position: absolute;
  bottom: -15px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 9.5px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Two classes deep to outrank `.detail p` and `.article p`, either of which
   would otherwise reset the margin and drop this line on top of the hour
   labels. Scoped to `.strip-wrap` rather than to `.detail`, because the
   strip is now also rendered into a server-rendered place page, where there
   is no `.detail` — and the key always lives inside the wrap. */
.strip-wrap .strip-key {
  /* Hour labels hang 15px below the strip on absolute positioning, so they
     take no layout space — this margin has to clear them. */
  margin: 32px 0 0;
  font-size: 11px;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
}
.strip-key span {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
/* Wind, as its own row under the hours. Same cell count and the same flex
   rules, so a column lines up with the hour above it; slim, because it is a
   second reading of the same day rather than a second chart. */
.wind-strip {
  display: flex;
  gap: 2px;
  height: 9px;
  margin-top: 3px;
}
.wind {
  flex: 1;
  min-width: 0;
  border-radius: 2px;
  background: color-mix(in srgb, var(--muted) 16%, transparent);
}
/* Bands, not a gradient: a colour ramp invites reading a value off it, and
   the useful question is which of four states the hour is in. */
.wind.breezy { background: color-mix(in srgb, var(--caution) 45%, transparent); }
.wind.strong { background: color-mix(in srgb, var(--caution) 85%, transparent); }
.wind.gale   { background: var(--avoid); }

.strip-key.wind-key { margin-top: 6px; }
.strip-key .wind {
  flex: none;
  width: 14px;
  height: 9px;
  border-radius: 2px;
}

/* The swatch is a real strip cell, so it can never drift from what the
   strip actually draws. */
.strip-key .cell {
  flex: none;
  width: 14px;
  height: 11px;
  border-radius: 2px;
}

/* ---------- getting around ---------- */

.travel {
  display: block;
  padding: 8px 0 8px 11px;
  border-left: 3px solid var(--caution);
  border-top: 1px solid var(--rule);
  font-size: 13px;
  color: inherit;
  text-decoration: none;
}
.travel:first-of-type { border-top: 0; }
.travel.sev-closed { border-left-color: var(--avoid); }
a.travel:hover { text-decoration: underline; }
a.travel:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* "Ferries and roads: nothing reported." and the folded-up minor notices. */
.travel-none { margin: 8px 0 0; font-size: 12.5px; color: var(--muted); }

/* ---------- news ---------- */

a.news {
  display: block;
  padding: 10px 0;
  border-top: 1px solid var(--rule);
  color: inherit;
  text-decoration: none;
}
a.news:first-of-type { border-top: 0; }
.news-head { display: block; font-size: 13.5px; line-height: 1.4; }
.news-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--muted);
}
.news-age { font-variant-numeric: tabular-nums; }
a.news:hover .news-head { text-decoration: underline; }

/* The day's digest, above the headlines it was written from. */
.news-digest { margin: 0; font-size: 13.5px; line-height: 1.5; }
.news-digest-age { margin: 3px 0 10px; font-size: 11px; color: var(--muted); }
a.news:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

.badge {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 5px;
  border-radius: 4px;
  white-space: nowrap;
}
.badge-press { background: color-mix(in srgb, var(--b-press) 16%, transparent); color: var(--b-press); }
.badge-bbc { background: color-mix(in srgb, var(--b-bbc) 16%, transparent); color: var(--b-bbc); }
.badge-council { background: color-mix(in srgb, var(--b-council) 18%, transparent); color: var(--b-council); }
.badge-walk { background: color-mix(in srgb, var(--b-walk) 16%, transparent); color: var(--b-walk); }
.badge-other { background: color-mix(in srgb, var(--muted) 16%, transparent); color: var(--muted); }
.badge-skye { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }

.news-toggle {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--accent);
  background: transparent;
  border: 0;
  border-top: 1px solid var(--rule);
  padding: 10px 0 4px;
  cursor: pointer;
}
.news-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.news-more a.news:first-of-type { border-top: 1px solid var(--rule); }

/* ---------- about ---------- */

.about-link {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 12.5px;
  color: var(--accent);
  background: transparent;
  border: 0;
  padding: 14px 0 4px;
  cursor: pointer;
}
.about-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.detail .about-heading { margin: 18px 0 8px; }

dl.verdict-key { margin: 0; font-size: 13px; }
dl.verdict-key dt {
  font-weight: 600;
  text-transform: uppercase;
  font-size: 10.5px;
  letter-spacing: 0.05em;
  color: var(--accent);
  margin-top: 8px;
}
dl.verdict-key dd { margin: 1px 0 0; color: var(--muted); font-size: 12.5px; }

.source {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 9px 0;
  border-top: 1px solid var(--rule);
}
.source:first-of-type { border-top: 0; }
.source-name { font-size: 13px; font-weight: 600; color: var(--accent); text-decoration: none; }
.source-name:hover { text-decoration: underline; }
.source-what { font-size: 12px; color: var(--ink); }
.source-age { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }

ul.caveats { margin: 0; padding-left: 18px; }
ul.caveats li { font-size: 12.5px; color: var(--muted); margin-bottom: 7px; line-height: 1.5; }

/* ---------- server-rendered pages ---------- */

/* Place, area, list, travel and credits pages. These are documents, not app
   screens: one column, and complete before any JavaScript runs.

   The class names are the server's — `.article` on <html> and <body>, then
   `.crumbs`, `.lede`, `.places`/`.row-head`/`.kind`/`.flag`, `.live`,
   `.verdict`, `.reasons`, `.basis`/`.basis-what`/`.basis-value`/
   `.basis-limit`, `.strip-wrap`/`.strip`/`.cell`/`.wind-strip`/`.wind`/
   `.strip-key`, `.nums`, `.opening`, `.asof`, `.muted`, `.notices`/`.sev-*`,
   `.news`,
   `.credits`, `.list-nav`. The templates inline a small fallback copy of the
   layout ones ahead of this file so a page is readable even if this never
   loads; these come later and win the ties — but only where the selectors
   match, so anything that has to *override* a fallback declaration must be
   written at the fallback's specificity (see `.crumbs` below).

   The last two groups are also what `js/detail.js` builds, so their rules
   are scoped to neither `.article` nor `.detail`.

   The component rules below are deliberately NOT scoped to `.article`: the
   same markup is what the server renders into `#sheet-body` and
   `#topsheet-body` on `/`, which is what a reader with no JavaScript (and a
   crawler) sees there. One set of rules covers both. */

body.article {
  padding: 0 16px 48px;
  max-width: 46rem;
  margin-inline: auto;
  /* The app locks the viewport; a document must not. `visible`, not `auto`:
     the templates inline `overflow:auto` here as a fallback, and `auto` makes
     <body> a scroll container of its own. Since `html.article` is also
     `overflow:auto`, nothing propagates to the viewport from here, so that
     container is one that never actually scrolls — and `position: sticky` on
     `.crumbs` resolves against the nearest scrollport, so it would silently
     never stick. This file is loaded after that inline copy and wins the tie. */
  height: auto;
  overflow: visible;
}

.article h1 { font-size: 25px; line-height: 1.2; letter-spacing: -0.015em; margin: 0.5em 0 0.3em; }
.article h2 { font-size: 17px; letter-spacing: -0.01em; margin: 1.7em 0 0.4em; }
.article p { margin: 0 0 12px; font-size: 14.5px; }
.article a { color: var(--accent); }
.article .crumbs a { color: var(--muted); }

/* The sentence the page exists to deliver: the `why` on a place page, the
   hand-written intro on a list page. */
.lede { font-size: 16px; line-height: 1.5; }

/* Breadcrumbs. The separators are the server's own "›" text, so this only
   has to make it small, wrap cleanly and not look like body copy.

   Sticky, because on a place page it is the only way back to the area and it
   used to scroll away in the first screenful. No JavaScript is involved: the
   bar is in the flow where it always was and the browser pins it.

   Three things make it hold up rather than smear:
   - an opaque `var(--paper)` behind it, so the text scrolling underneath
     doesn't show through. It is painted edge to edge of the reading column,
     which is also the only place anything is painted — `body.article` pads
     the gutters and nothing renders in them — so there is no need to bleed
     the background outside it and no gap either.
   - a hairline under it, so it reads as a bar once content is beneath it.
   - `--safe-top` in the padding rather than in `top`, so on a notched phone
     in landscape the inset is part of the bar's own background instead of a
     transparent strip above it. The `top` offset carries it too, or the bar
     would come to rest under the notch. */
.crumbs {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  line-height: 1.6;
  color: var(--muted);
}
/* The templates inline `.article .crumbs{margin:1rem 0 0;font-size:.8rem}`
   ahead of this file as their no-stylesheet fallback, and that is two classes
   deep. Anything this bar needs to *change* about that copy has to be written
   at the same specificity or it loses the tie — the margin in particular,
   which would otherwise leave a 16px band of paper above a bar that is
   pinned to 0. (Same trap the README records for the rest of these rules.) */
.article .crumbs {
  margin: 0;
  padding: calc(12px + var(--safe-top)) 0 7px;
  font-size: 12px;
}
.crumbs a:hover { color: var(--accent); }
.crumbs [aria-current="page"] { color: var(--ink); }
/* The heading sat 0.5em from a non-sticky bar; now the bar has its own
   padding, so the gap comes off the top of the page's first element. */
.crumbs + main { padding-top: 10px; }
/* Anchored links must not land under the bar. */
.article :target { scroll-margin-top: 3.4rem; }

/* Photo and attribution. The caption is not decoration — CC BY and CC BY-SA
   require the credit to travel with the image — so it never collapses away.
   width/height are on the <img>, so the box holds its space before the file
   arrives and nothing shifts. */
.article figure { margin: 14px 0; }
.article figure picture {
  border-radius: 10px;
  background-color: color-mix(in srgb, var(--rule) 60%, transparent);
}
.article figcaption {
  margin-top: 6px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--muted);
}
.article figcaption a { color: var(--muted); text-decoration: underline; }
.article figcaption a:hover { color: var(--accent); }

/* The live block: the one thing on these pages that is not evergreen. Boxed
   and stamped with its own age, so it reads as a reading taken at a moment
   rather than a fact about the place. */
.live {
  margin: 16px 0;
  padding: 11px 13px;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: color-mix(in srgb, var(--rule) 22%, transparent);
}
.live > :last-child { margin-bottom: 0; }
.live h2 {
  margin: 0 0 8px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
.asof { font-size: 11px; color: var(--muted); margin: 8px 0 0; }
.nums { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }
.article .muted, .article .nums { font-size: 12.5px; }

/* The verdict, in the same three words and the same colours as the badge on
   the map. */
.verdict { margin: 0 0 6px; font-size: 14px; }
.verdict strong {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 3px 7px;
  border-radius: 4px;
}
.verdict.good strong { background: color-mix(in srgb, var(--good) 16%, transparent); color: var(--good); }
.verdict.caution strong { background: color-mix(in srgb, var(--caution) 18%, transparent); color: var(--caution); }
.verdict.avoid strong { background: color-mix(in srgb, var(--avoid) 16%, transparent); color: var(--avoid); }
.verdict.unknown strong { background: color-mix(in srgb, var(--muted) 16%, transparent); color: var(--muted); }
.flag.unknown { background: color-mix(in srgb, var(--muted) 16%, transparent); color: var(--muted); }

/* The engine's own words. Quoted, the way the detail page in the app quotes
   them — a bulleted list read as a checklist of things to do. */
ul.reasons {
  list-style: none;
  margin: 8px 0 10px;
  padding: 2px 0 2px 11px;
  border-left: 3px solid var(--rule);
  font-size: 12.5px;
  color: var(--muted);
}
ul.reasons li + li { margin-top: 3px; }

/* The figures the verdict was actually taken on: what was measured, the
   reading, and the line it was measured against.

   Subordinate on purpose. The sentences above are the answer; this is for
   the reader who wants to disagree with us, and it has to look like the
   small print or it competes with the thing it supports. Hence: no rule of
   its own, indented to hang under the reasons' quote bar, a size down, and
   the only ink-coloured thing on it is the number.

   One rule for both shapes of page — `js/detail.js` builds exactly the
   markup `pages.rs` renders, so this is never scoped to `.detail` or
   `.article`. A place the weather has no say over carries no basis at all
   and this renders nothing, rather than an empty box.

   Each row is what was measured and the reading, then the line it was
   measured against under them — reading order, and the order they matter in.
   Not columns: "Summit wind", "Rain" and "Cloud-free tops" are wildly
   different lengths, so a per-row label column makes the readings start at
   three different places down the block, and a shared one wide enough for
   the longest leaves four words a line for the limit at 320px. The hairline
   between rows is what groups each pair, and it does it at every width. */
ul.basis {
  list-style: none;
  margin: -2px 0 12px;
  padding: 0 0 0 14px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
ul.basis li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 7px;
  padding: 4px 0;
}
ul.basis li + li { border-top: 1px solid color-mix(in srgb, var(--rule) 55%, transparent); }
.basis-what { flex: none; }
/* The only ink on the block: the number is the thing someone came down here
   to read, and everything around it is the caption to it. */
.basis-value { flex: none; color: var(--ink); }
/* Always its own line, indented under the reading it qualifies. */
.basis-limit { flex: 1 1 100%; padding-left: 10px; }

/* Opening hours, where OpenStreetMap has them. Sits among the practical
   facts with "Starts:", not with the forecast — it is a standing fact about
   the place, not today's weather, and it is one of the few lines on the page
   that is still true tomorrow.

   Quiet, because it is a hedge and not an assertion: the label carries the
   weight and the sentence behind it is muted, so nobody reads it as the
   site promising a door will be open. Both renderers emit this markup —
   `opening_line` in `pages.rs` and `buildDetail` in `js/detail.js` — so the
   rule is scoped to neither `.article` nor `.detail`. */
p.opening {
  margin: 0 0 10px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}
p.opening strong { color: var(--ink); font-weight: 600; }

/* Place rows on an area or list page: thumbnail, name, what it is, the
   verdict, and one plain line. The same information as a row in the sheet,
   laid out for a page. */
ul.places { list-style: none; margin: 12px 0 20px; padding: 0; }
.places li {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 11px 0;
  border-top: 1px solid var(--rule);
}
.places li:first-child { border-top: 0; }
.places picture {
  width: 96px;
  flex: none;
  border-radius: 8px;
  background-color: color-mix(in srgb, var(--rule) 60%, transparent);
}
.places > li > div { min-width: 0; flex: 1; }
.places p { margin: 0; font-size: 13px; color: var(--muted); }
.row-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 7px;
  margin-bottom: 2px !important;
}
.row-head a {
  font-size: 15px;
  font-weight: 650;
  color: inherit;
  text-decoration: none;
}
.row-head a:hover { text-decoration: underline; }
.row-head .kind { font-size: 11px; color: var(--muted); text-transform: lowercase; }
.row-head .flag { margin-left: auto; }

/* Ferries and roads. One line each, with the severity on the left edge —
   the same shape the sheet uses, so the page and the app agree. */
ul.notices { list-style: none; margin: 10px 0; padding: 0; }
.notices li {
  padding: 8px 0 8px 11px;
  border-left: 3px solid var(--caution);
  border-top: 1px solid var(--rule);
  font-size: 13px;
}
.notices li:first-child { border-top: 0; }
.notices li.sev-closed { border-left-color: var(--avoid); }
.notices li.sev-delays { border-left-color: var(--caution); }
.notices li.sev-running { border-left-color: var(--good); }
.notices li.sev-note { border-left-color: var(--rule); }
.notices .muted { font-size: 12px; }
.notices a { font-size: 12px; }

/* Headlines, and the photo credits: both are a link and a muted source. */
ul.news, ul.credits { list-style: none; margin: 10px 0; padding: 0; }
.news li, .credits li {
  padding: 9px 0;
  border-top: 1px solid var(--rule);
  font-size: 13.5px;
  line-height: 1.45;
}
.news li:first-child, .credits li:first-child { border-top: 0; }
.news .muted, .credits .muted { font-size: 11.5px; }
.credits li { font-size: 12.5px; }

/* The "ideas" links at the foot of every page. They are how someone who
   landed on one page finds the rest, so they get a little room. */
.list-nav {
  margin: 14px 0 0;
  font-size: 13px;
  line-height: 1.9;
  color: var(--muted);
}

.article footer {
  margin-top: 36px;
  padding-top: 14px;
  border-top: 1px solid var(--rule);
  font-size: 12px;
  color: var(--muted);
}
.article footer p { font-size: 12px; margin: 0 0 6px; }
.article footer a { color: var(--muted); }

/* The server also renders the list and the news into the two sheets on `/`,
   for a crawler and for the moment before the app's own render replaces it.
   It is inside a scrolling panel there, not a page, so it needs the page's
   margins taken back off. */
#sheet-body > h1, #topsheet-body > h1 { font-size: 17px; margin: 2px 0 6px; }
#sheet-body h2, #topsheet-body h2 { font-size: 14px; margin: 14px 0 4px; }
#sheet-body .lede, #topsheet-body .lede { font-size: 13.5px; }
#sheet-body .live, #topsheet-body .live { margin: 10px 0; }


/* Narrow phones: keep the sheet readable rather than pretty. */
@media (max-width: 360px) {
  body.article { padding: 0 12px 40px; }
  .article h1 { font-size: 22px; }
  .places picture { width: 72px; }
  .places li { gap: 10px; }
  #sheet-body { padding: 0 10px 16px; }
  /* The bleed has to match the body's narrower padding, or the bar stops
     short of the edge and content scrolls past in the gap. */
  .back { width: calc(100% + 20px); margin: 0 -10px 10px; padding: 8px 10px; }
  #peek { padding: 0 10px 12px; }
  #day-tabs { margin: 0 10px 8px; }
  .day-tab { font-size: 12px; padding: 6px 4px; }  /* min-height still applies */
  .area-block .points { padding-left: 10px; }
  .area-meta { padding-left: 10px; }
}
