/* Type system (#13): Young Serif display · Nunito Sans body · Vollkorn Black numbers */
@font-face {
  font-family: "Young Serif";
  src: url("../fonts/young-serif-latin.2e465c2f5f89.woff2") format("woff2");
  font-display: swap;
}
@font-face {
  font-family: "Nunito Sans";
  src: url("../fonts/nunito-sans-var-latin.29fab48fb240.woff2") format("woff2");
  font-weight: 200 1000;
  font-display: swap;
}
@font-face {
  font-family: "Vollkorn";
  src: url("../fonts/vollkorn-black-latin.836d973a5249.woff2") format("woff2");
  font-weight: 900;
  font-display: swap;
}

:root {
  /* Sentinel read by the self-heal script in base.html: if this value is not
     visible to JS, the stylesheet never applied and the client is running a
     broken cached copy of the site. */
  --pp-css: 1;
  /* Custard & Cherry (#30). bg/ink/accent/accent-soft/line are Mark's approved
     tokens verbatim from the issue thread -- do not re-derive or "improve" them.
     Cherry must never get darker than this: "I like the current cherry color.
     Don't make it darker" was his explicit, binding call. */
  --bg: #fdf7ea;
  --ink: #2a2018;
  --accent: #a81f2d;      /* ring color; also the "ride" kind hue below */
  --accent-soft: #fbdfdf;
  --line: #e8dcc4;
  --ok: #2e933c;
  --display: "Young Serif", Georgia, serif;
  --numbers: "Vollkorn", Georgia, serif;
  /* Text/controls that sit directly on solid --accent -- buttons, the active filter
     chip, the site-message banner (#47). This is what #30 was actually about: the
     old accent measured 3.59:1 here and failed WCAG AA; the new one measures
     6.79:1 light / 5.12:1 dark (WCAG relative-luminance formula; full table in
     .superpowers/sdd/cherry-palette-report.md). Paper in both modes, not a
     hardcoded white -- white-on-the-new-dark-accent is only 3.53:1 and would still
     fail there even though it clears light mode. */
  --accent-ink: var(--bg);
  --banner-ink: var(--accent-ink);  /* alias kept for #47's own naming at its call sites */

  /* Hues (#30, restructured for the shows/dining swap): the four actual colors --
     cherry, sky, butter, leaf -- are named independently of any KIND or STATE that
     happens to use them, defined ONCE here (own -ink companion, own -soft tint, own
     dark-mode override if it needs one). Nothing below this block ever spells out a
     hex again for these four; --kind-* and --tier-* all just point AT a --hue-*
     name. That indirection is the whole point: swapping which kind wears which hue
     (Mark: "shows should be yellow, dining blue") is a change to the
     --kind-show/--kind-dining pointers a few lines down and NOTHING else moves --
     --tier-sky keeps pointing at the same --hue-* name it always did, so it keeps
     its own color even though the kind that used to carry it changed. Before this
     restructure, --tier-sky aliased --kind-show directly, which meant a hue swap
     would have silently dragged it along too; that class of bug can't happen
     anymore because it doesn't name a kind at all.
     "Cherry" reuses --accent/--accent-soft rather than a separate literal: cherry is
     both the site's general brand accent and the ride hue, so there is one cherry,
     not two. Each -ink is whichever of --ink/--bg clears WCAG AA (4.5:1) as text
     sitting on the solid hue (measured, not assumed); each -soft is a pale tint for
     track/backdrop use, derived via color-mix rather than hand-picked since Mark
     didn't specify soft variants for the three non-cherry hues. JS that structurally
     can't resolve var() (canvas fillStyle in static/js/app.js) reads the CURRENT
     --tier-* value at the moment of use via getComputedStyle, so there is exactly one
     place any of these colors live -- see the palette report for how that's asserted
     in tests. */
  --hue-cherry: var(--accent);
  --hue-cherry-line: var(--hue-cherry);
  --hue-cherry-soft: var(--accent-soft);
  --hue-cherry-ink: var(--accent-ink);
  --hue-sky: #2f6f9e;
  --hue-sky-line: var(--hue-sky);
  --hue-sky-soft: color-mix(in srgb, var(--hue-sky) 15%, var(--bg));
  --hue-sky-ink: var(--bg);
  /* The value Mark reviewed and approved in the palette mockups. It was briefly
     deepened to #b58617 to give the Shows bar more weight against its track, which
     was a mistake: butter is not only a bar fill, it is also the background behind
     the "today" checkmark and chip, and a dark mustard under near-black ink is
     muddy at glyph size. Mark: "Why are all of the checkboxes black on dark gold
     now? ... That's unreadable" / "I liked the original butter color".
     Reverting also improves the mark: ink-on-butter goes 4.85:1 -> 5.89:1. */
  --hue-butter: #c8951a;
  /* Strokes need 3:1 against paper (WCAG non-text); butter as a FILL wants to stay
     bright so dark ink reads on it. Those pull opposite ways, so butter -- and only
     butter -- carries a darker stroke tone. The others already clear 3:1 and alias
     their own hue, so every kind border can say var(--kind-*-line) uniformly. */
  --hue-butter-line: #a87d14;
  --hue-butter-soft: color-mix(in srgb, var(--hue-butter) 15%, var(--bg));
  --hue-butter-ink: var(--ink);
  --hue-leaf: #2f6f4a;
  --hue-leaf-line: var(--hue-leaf);
  --hue-leaf-soft: color-mix(in srgb, var(--hue-leaf) 15%, var(--bg));
  --hue-leaf-ink: var(--bg);

  /* Kinds (#30): rides cherry, shows butter, dining sky, shops leaf -- each just a
     pointer at one of the --hue-* names above. This is the two-name block that moves
     on a future swap; nothing else in this file should ever need to. */
  --kind-ride: var(--hue-cherry);
  --kind-ride-line: var(--hue-cherry-line);
  --kind-ride-soft: var(--hue-cherry-soft);
  --kind-ride-ink: var(--hue-cherry-ink);
  --kind-show: var(--hue-butter);
  --kind-show-line: var(--hue-butter-line);
  --kind-show-soft: var(--hue-butter-soft);
  --kind-show-ink: var(--hue-butter-ink);
  --kind-dining: var(--hue-sky);
  --kind-dining-line: var(--hue-sky-line);
  --kind-dining-soft: var(--hue-sky-soft);
  --kind-dining-ink: var(--hue-sky-ink);
  --kind-shop: var(--hue-leaf);
  --kind-shop-line: var(--hue-leaf-line);
  --kind-shop-soft: var(--hue-leaf-soft);
  --kind-shop-ink: var(--hue-leaf-ink);

  /* Badge tiers (#30): park-% -> bronze/silver/gold/cherry (completion reads as
     progression), count-* -> sky (a different achievement family), rope-drop/
     closing-time -> plum (time-of-day feats). cherry and sky point at the --hue-*
     names above rather than repeating a literal -- one cherry, one sky, anywhere on
     the site -- and they name the HUE directly rather than a kind, so they don't
     move when a kind's hue does. Tier NAMES live in
     tracking/badges.py (Badge.tier); these are the only place the actual colors do,
     read by both the on-screen badge-card and the downloaded PNG (static/js/app.js)
     so the two can't drift apart (#30's own framing: "the badge PNG generator must
     use the same tier logic or downloads will not match the screen").
     bronze/silver/gold/plum's exact hexes were not specified in the issue (only the
     tier names were) -- these are an original choice, verified AA for both the
     4.5:1 text-on-fill use (PNG) and the 3:1 non-text use (badge-card border), see
     the palette report. Land-completion badges (tracking/badges.py's
     land-<area slug> slugs) aren't enumerated in the issue either, but
     earned_badges() emits them, so they get a seventh tier here too rather than
     falling through to the base badge-card rule and silently rendering cherry --
     colliding with park-100. "leaf" points at --hue-leaf (the issue's own name for
     that hue is literally "leaf") rather than a new literal. */
  --tier-bronze: #915627;
  --tier-bronze-ink: var(--bg);
  --tier-silver: #70675c;
  --tier-silver-ink: var(--bg);
  --tier-gold: #a38714;
  --tier-gold-ink: var(--ink);
  --tier-cherry: var(--hue-cherry);
  --tier-cherry-ink: var(--hue-cherry-ink);
  --tier-sky: var(--hue-sky);
  --tier-sky-ink: var(--hue-sky-ink);
  --tier-plum: #7a2e5e;
  --tier-plum-ink: var(--bg);
  --tier-leaf: var(--hue-leaf);
  --tier-leaf-ink: var(--hue-leaf-ink);
  color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1a1512; --ink: #f4ead8; --accent: #e8556b; --accent-soft: #3a1f24; --line: #302722;
    --hue-sky: #6fb3dd;
    --hue-butter: #e8b53f;        /* unchanged from the mockup -- only light mode read weak */
    --hue-butter-ink: var(--bg);  /* flips: dark neutral clears dark-mode butter at 9.58:1 */
    --hue-leaf: #5cbf85;
    --tier-bronze: #cd9f7a;
    --tier-silver: #b1a9a0;
    --tier-gold: #d9b526;
    --tier-gold-ink: var(--bg);    /* flips, same reasoning as butter */
    --tier-plum: #c577a8;
  }
}
* { box-sizing: border-box; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* Author display rules beat the UA [hidden] rule — filters rely on this. */
[hidden] { display: none !important; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 16px/1.45 "Nunito Sans", -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-tap-highlight-color: transparent;
}
main { max-width: 640px; margin: 0 auto; padding: 0 1rem 3rem; }

/* Header + ring */
.park-header { position: sticky; top: 0; background: var(--bg); padding: .5rem 0 .5rem; z-index: 10; border-bottom: 1px solid var(--line); }
.header-bar { display: flex; align-items: center; gap: .6rem; }
.park-header h1 { font-family: var(--display); font-weight: 400; font-size: 1.25rem; margin: 0; flex: 1; min-width: 0; }
.crumb { font-size: .8rem; color: var(--accent); text-decoration: none; display: inline-block; padding: .8rem .35rem; margin: -.5rem 0; white-space: nowrap; }
.ring-wrap { display: flex; align-items: center; gap: .6rem; flex: none; }
.ring { width: 84px; height: 84px; }
.ring-mini { display: none; font-family: var(--numbers); font-weight: 900; font-size: 1.05rem; font-feature-settings: "onum" 1; }
/* Condensed header while scrolled: ring and search give way to the list (#8). */
.park-header.is-condensed .ring,
.park-header.is-condensed #search { display: none; }
.park-header.is-condensed .ring-mini { display: inline; }
.park-header.is-condensed h1 { font-size: 1.05rem; }
.ring circle { fill: none; stroke-width: 10; }
.ring-bg { stroke: var(--line); }
.ring-fg {
  stroke: var(--accent); stroke-linecap: round;
  stroke-dasharray: 339.292;
  stroke-dashoffset: calc(339.292 * (1 - var(--p, 0) / 100));
  transform: rotate(-90deg); transform-origin: 60px 60px;
  transition: stroke-dashoffset .6s cubic-bezier(.22, 1, .36, 1);
}
.ring-text { font-family: var(--numbers); font-size: 26px; font-weight: 900; fill: var(--ink); font-feature-settings: "onum" 1; }

/* Chips + search — 44px effective targets (#7) */
.chips { display: flex; gap: .4rem; overflow-x: auto; padding: .35rem 0; }
.chips button { border: 1px solid var(--line); background: transparent; color: var(--ink); border-radius: 999px; padding: .6rem 1rem; font-size: .9rem; min-height: 44px; white-space: nowrap; flex: none; }
.chips button.is-active { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 700; }
/* Kind filter chips (#30, checklist only): app.js's kind filter reads data-filter off
   these SAME buttons (see #checklist[data-filter] below), so they double as the kind
   legend -- "self-describing," per the issue. Outlined in the kind hue even before a
   tap (a WCAG 1.4.11 non-text 3:1 use, all four verified >=3.08:1 against --bg in the
   palette report); solid-filled with the verified AA text color once active. Scoped
   to [data-filter="ride|show|dining|shop"] specifically so this never touches the
   home page's operator/state chips (templates/pages/home.html), which reuse the same
   .chips/.is-active classes for an unrelated filter and must keep the plain generic
   accent styling above untouched. */
.chips button[data-filter="ride"] { border-color: var(--kind-ride-line); }
.chips button[data-filter="show"] { border-color: var(--kind-show-line); }
.chips button[data-filter="dining"] { border-color: var(--kind-dining-line); }
.chips button[data-filter="shop"] { border-color: var(--kind-shop-line); }
.chips button[data-filter="ride"].is-active { background: var(--kind-ride); border-color: var(--kind-ride); color: var(--kind-ride-ink); }
.chips button[data-filter="show"].is-active { background: var(--kind-show); border-color: var(--kind-show); color: var(--kind-show-ink); }
.chips button[data-filter="dining"].is-active { background: var(--kind-dining); border-color: var(--kind-dining); color: var(--kind-dining-ink); }
.chips button[data-filter="shop"].is-active { background: var(--kind-shop); border-color: var(--kind-shop); color: var(--kind-shop-ink); }
#search { width: 100%; padding: .65rem .75rem; border: 1px solid var(--line); border-radius: .5rem; background: transparent; color: var(--ink); font-size: 16px; }

/* Site-wide message banner (#47): generic and backstage-editable — the beta
   notice is only its first piece of content. Lives in normal document flow
   (not position: sticky/fixed) above everything else in <body>, so it never
   interacts with .park-header's own sticky/condense behavior (#8): the
   condense thresholds in app.js key off scrollY, not this element's size or
   presence, and dismissing it only ever removes content above the header,
   never toggles the header's own state. Genuinely contrasting per the issue:
   full --accent fill (not --accent-soft) with --banner-ink text/controls, see
   that variable's definition above for the contrast ratios. */
.site-message { background: var(--accent); border-bottom: 1px solid var(--banner-ink); }
.site-message-inner {
  max-width: 640px; margin: 0 auto; padding: .55rem 1rem;
  display: flex; align-items: center; gap: .75rem;
}
.site-message p { margin: 0; font-size: .9rem; font-weight: 600; flex: 1; color: var(--banner-ink); }
.site-message a { color: var(--banner-ink); text-decoration: underline; text-underline-offset: 2px; }
.site-message-dismiss {
  flex: none; width: 32px; height: 32px; min-height: 32px; position: relative;
  border: 1px solid var(--banner-ink); border-radius: 999px; background: transparent;
  color: var(--banner-ink); font-size: 1.1rem; line-height: 1; cursor: pointer; touch-action: manipulation;
}
.site-message-dismiss::after { content: ""; position: absolute; inset: -6px; }  /* 44px effective target */

/* Site bar */
.site-bar { max-width: 640px; margin: 0 auto; padding: .4rem 1rem 0; }
.wordmark { font-family: var(--display); font-weight: 400; font-size: 1.15rem; letter-spacing: -.01em; color: var(--ink); text-decoration: none; display: inline-block; padding: .6rem .3rem; }

/* Areas + rows */
.area summary { list-style: none; cursor: pointer; padding: .9rem 0 .4rem; }
.area summary::before { content: "▸"; display: inline-block; margin-right: .45rem; color: var(--accent); transition: transform .15s; }
.area[open] summary::before { transform: rotate(90deg); }
.area summary h2 { display: inline; font-family: var(--display); font-weight: 400; font-size: 1rem; }
#no-results { padding: 1rem 0; }
#clear-filters, #clear-park-filters { border: 1px solid var(--line); background: transparent; color: var(--accent); border-radius: 999px; padding: .6rem 1rem; font-size: .9rem; min-height: 44px; cursor: pointer; touch-action: manipulation; }
.exp-list { list-style: none; margin: 0; padding: 0; }
/*
 * Row geometry (#48). Every row stacks two pieces inside .exp-body:
 *   1. .exp-info  — the name + info-icon "read more" button. Kept at exactly
 *      44px (the HIG tap-target floor) via min-height; the name itself is
 *      clamped to one line (nowrap+ellipsis) so a long name can never grow
 *      this past 44px the way it used to (Adventureland Treehouse: 89.75px
 *      vs. 78px for every other row, pre-#48). The full name lives in the
 *      tap panel now, not a `title` attribute.
 *   2. .exp-blurb — the always-visible description preview. Given an
 *      EXPLICIT height (2 × its line-height), not just -webkit-line-clamp,
 *      because line-clamp alone only caps long text — a one-line blurb
 *      would leave the box shorter than a two-line one, and rows would go
 *      back to varying height by content instead of by check state.
 * .exp's min-height is those two pieces' heights plus its own vertical
 * padding, written as a calc() of the same numbers rather than a separately
 * chosen round figure, so the row's height is a consequence of the stack
 * above, not a guess flexbox happens to center correctly.
 * Row height is CONSTANT across every lifetime count (#4, #98): tapping the
 * ride counter only ever swaps text/classes inside pieces that already have
 * their own fixed sizes (.ride-counter stays 44px square; its lifetime badge
 * is position:absolute on TOP of that same box, never a sibling that widens
 * a column), never anything counted in the calc() below.
 */
.exp {
  position: relative; display: flex; gap: .75rem; align-items: center;
  min-height: calc(44px + .3rem + 2.6rem + 1.2rem);
  padding: .6rem 0; border-bottom: 1px solid var(--line);
}
.exp { scroll-margin-top: 130px; }
.exp:target { background: color-mix(in srgb, var(--accent) 8%, var(--bg)); border-radius: .5rem; }
/* Thumbnail (#81): a fixed 64x64 square no matter what's inside -- an approved
   photo's derivative, or the placeholder below -- so it can never be the thing that
   varies the row's own locked height. 64px leaves real margin against the ~90px of
   content-box height .exp's min-height works out to (110.58px total, less its own
   .6rem top/bottom padding and border): comfortably bigger than the 44px ride
   counter without ever risking growing the row. flex: none, like .ride-counter,
   so it never competes with .exp-body for width. */
.exp-thumb {
  width: 64px; height: 64px; flex: none; border-radius: 16px; overflow: hidden;
  background: var(--accent-soft);
}
.exp-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* No approved photo (or one whose derivative hasn't been generated yet, #81) --
   a kind-tinted glyph instead of an empty box, so the row reads as deliberately
   designed rather than broken. Same --accent-soft/--accent pairing as everything
   else on this page (the ride counter's logged state, stat badges): a new icon
   per kind, not a new color per kind. */
.exp-thumb-placeholder {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
}
.exp-thumb-placeholder svg { width: 30px; height: 30px; opacity: .55; }
/* Tinted by kind (#30), not all-accent: data-kind is already on this element (see
   templates/tracking/_row.html), so no markup change was needed, only these four
   rules. The tile behind it (.exp-thumb, above) stays the universal --accent-soft --
   that's the site's general soft-card backdrop (same one badge-card/continue-card/
   the ride counter's logged fill use), not a per-kind surface itself; only the
   glyph carries the kind identity, per the issue's own wording ("tint each [icon]
   by its kind"). */
.exp-thumb-placeholder[data-kind="ride"] svg { color: var(--kind-ride); }
.exp-thumb-placeholder[data-kind="show"] svg { color: var(--kind-show); }
.exp-thumb-placeholder[data-kind="dining"] svg { color: var(--kind-dining); }
.exp-thumb-placeholder[data-kind="shop"] svg { color: var(--kind-shop); }
.exp-body { flex: 1; min-width: 0; align-self: center; }
.exp-info {
  display: flex; align-items: center; gap: .4rem; width: 100%; min-height: 44px;
  border: none; background: none; padding: 0; text-align: left;
  font: inherit; color: inherit; cursor: pointer; touch-action: manipulation;
}
/* Single line, always — a wrapped name is what broke uniform row height
   before #48. min-width: 0 lets the ellipsis clip inside the flex row
   instead of the row stretching to fit the text. */
.exp-name { display: block; min-width: 0; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* (i) info icon (#98), replacing the old chevron -- Mark: "the tiny chevron [was]
   not very discoverable." Static (no rotate-on-expand): an info glyph doesn't read
   as a directional affordance the way a chevron did, so aria-expanded on the
   button itself, and the panel's own appearance, carry the open/closed state now. */
.info-icon { font-size: .95rem; color: var(--accent); opacity: .7; flex: none; }
/* Always-visible description preview. height (not max-height) reserves the
   same 2 lines of space for every row regardless of how long its own blurb
   is; -webkit-line-clamp then ellipsizes the ~16% of blurbs that still
   overflow 2 lines at mobile widths even at the 160-char cap. The tap panel
   below is the "read more" path for those. */
.exp-blurb {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; margin: .3rem 0 0; font-size: .85rem; line-height: 1.3rem;
  height: 2.6rem; opacity: .75;
}
/* z-index sits BELOW .park-header's 10 (not above it): the panel must overlay the
   sibling rows beneath it, but scrolling with a panel open should slide it under the
   sticky park header and filter chips, not over them. .exp is position:relative with
   no z-index, so it creates no stacking context and this value competes directly with
   the header's in the same context. */
.exp-blurb-panel {
  position: absolute; top: 100%; left: 52px; right: 0; z-index: 5; margin-top: -8px;
  background: var(--bg); border: 1px solid var(--line); border-radius: .6rem;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .14); padding: .6rem .8rem;
}
/* The panel is the "full record" now that the preview line above is clamped:
   full name as a heading (the one place it's guaranteed unclipped), then the
   full description, then the map/report links — not a bare repeat of the
   preview text. */
.exp-panel-name { margin: 0 0 .2rem; font-size: .95rem; font-weight: 700; }
.exp-blurb-panel p { margin: 0; font-size: .9rem; }
/* Photo (#49). Confined entirely to this panel -- see the template comment next to
   .exp-photo's markup for why that's load-bearing, not incidental: it's what
   guarantees a photo can never change .exp's own min-height. */
.exp-photo { margin: .6rem 0 0; }
.exp-photo img { display: block; width: 100%; max-width: 280px; height: auto; border-radius: .5rem; }
.exp-photo figcaption { margin-top: .25rem; font-size: .72rem; line-height: 1.3; opacity: .65; }
.map-link { display: inline-block; padding: .6rem 0 .1rem; font-size: .85rem; font-weight: 700; }
.exp-blurb-panel .report-link { display: inline-block; margin-top: .4rem; font-size: .85rem; opacity: .8; }
.before-badge {
  font-size: .72rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink); opacity: .55; white-space: nowrap;
}
.chips button, .area summary, .crumb { touch-action: manipulation; }
/* Ride counter (#98), replacing the checkbox+today-chip pair. One control, same
   44px/12px-radius footprint the checkbox had, that ALWAYS shows "+" (never lies
   about what tapping does): never-logged is a muted cherry outline on transparent
   (the exact look the old checkbox always had), logged 1+ times fills solid
   cherry with a paper "+" -- lifetime state, not today's, so there is deliberately
   no separate "today" treatment here (Mark: "I do not want any today marker").
   position: relative so the badge below can anchor to this box specifically,
   not the row. */
/* Empty state is a MUTED cherry, not full strength: 85 unlogged rows at full accent
   read as a wall of red and nothing stands out, so the logged ones have to be the
   loud thing. Blended toward the paper rather than given an opacity so the badge and
   focus ring, which sit on top, keep their own full contrast. */
.ride-counter {
  position: relative; width: 44px; height: 44px; flex: none;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid color-mix(in srgb, var(--accent) 45%, var(--bg));
  border-radius: 12px; background: transparent;
  color: color-mix(in srgb, var(--accent) 60%, var(--bg)); line-height: 1;
  touch-action: manipulation;  /* immediate taps on iOS — no double-tap-zoom wait */
  cursor: pointer;
}
/* Matches .exp-thumb-placeholder svg's weight so the "+" and the kind icons read as
   one family; 26px of the 44px box keeps the arms long without crowding the badge. */
.ride-plus { width: 26px; height: 26px; }
.ride-counter.is-logged {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
  animation: pop .3s cubic-bezier(.22, 1.61, .36, 1);
}
@keyframes pop { 0% { transform: scale(.7); } 60% { transform: scale(1.15); } 100% { transform: scale(1); } }
/* Lifetime count badge (#98): --ink fill (not cherry -- it must read on BOTH the
   cherry fill and on paper, so it carries its own contrast rather than borrowing
   whatever's behind it) with a --bg ring for separation, the same "ring around a
   small mark so it pops off its backdrop" move map pins/legend swatches already
   use. min-width (not a fixed circle) plus horizontal padding lets two-digit
   counts widen rather than shrink their type to fit. Vollkorn oldstyle numerals
   (font-feature-settings: "onum" 1) so 12/99 sit on the baseline properly -- the
   same numeral treatment as the ring/typebar/badge-date numbers elsewhere on this
   page. aria-hidden in the template: the button's own aria-label already states
   the count in words, so the badge is a visual reinforcement, never the only way
   the count is conveyed. */
.ride-badge {
  position: absolute; top: -6px; right: -6px; min-width: 20px; height: 20px;
  padding: 0 5px; border-radius: 999px; box-sizing: border-box;
  background: var(--ink); color: var(--bg); border: 2px solid var(--bg);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--numbers); font-weight: 900; font-feature-settings: "onum" 1;
  font-size: .72rem; line-height: 1;
}
/* Editable history (#98), inside the details panel's own hidden subtree -- see the
   template comment next to its markup for why a list here can never affect .exp's
   own height. The remove button is a small round "×", given the same inset::after
   44px-effective-target treatment as .site-message-dismiss/.map-pin elsewhere,
   since its visible glyph is well under the HIG floor. */
.ride-history { margin-top: .6rem; padding-top: .5rem; border-top: 1px solid var(--line); }
.ride-history-heading { margin: 0 0 .35rem; font-size: .85rem; font-weight: 700; }
.ride-history-list { list-style: none; margin: 0; padding: 0; }
.ride-history-list li {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .3rem 0;
}
.ride-history-list li + li { border-top: 1px solid var(--line); }
.ride-history-when { font-size: .85rem; opacity: .8; }
.ride-history-remove {
  position: relative; flex: none; width: 24px; height: 24px; border-radius: 50%;
  border: 1px solid var(--line); background: transparent; color: var(--ink);
  font-size: 1rem; line-height: 1; cursor: pointer; touch-action: manipulation;
}
.ride-history-remove::after { content: ""; position: absolute; inset: -10px; }  /* 44px effective target */

/* Kind filtering — CSS only, driven by data-filter on #checklist */
#checklist[data-filter="ride"] .exp:not([data-kind="ride"]),
#checklist[data-filter="show"] .exp:not([data-kind="show"]),
#checklist[data-filter="dining"] .exp:not([data-kind="dining"]),
#checklist[data-filter="shop"] .exp:not([data-kind="shop"]) { display: none; }

/* Stats page */
.area-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: .5rem; list-style: none; padding: 0; }
.area-grid li { border: 1px solid var(--line); border-radius: .5rem; padding: .5rem; }
.area-grid .done { border-color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, transparent); }

/* Buttons + form controls (#14) */
.btn {
  display: inline-block; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-ink);
  border-radius: .6rem; padding: .7rem 1.2rem; font: inherit; font-weight: 600;
  cursor: pointer; min-height: 44px; touch-action: manipulation; text-decoration: none;
}
.btn-secondary { background: transparent; color: var(--accent); }
.btn-danger { background: #c0392b; border-color: #c0392b; }
.btn-danger-outline { background: transparent; border: 1px solid #c0392b; color: #c0392b; }
input[type="email"], input[type="text"], textarea {
  width: 100%; padding: .65rem .75rem; border: 1px solid var(--line); border-radius: .5rem;
  background: transparent; color: var(--ink); font: inherit; font-size: 16px;
}
.danger-zone { margin-top: 1.5rem; }
.danger-zone summary { list-style: none; cursor: pointer; }
.danger-confirm { margin-top: .75rem; padding: .8rem; border: 1px solid #c0392b; border-radius: .6rem; }
.danger-confirm p { margin: 0 0 .6rem; }

/* Stats type bars — numbers first (#15), one hue per kind (#30) */
.typebar { margin: 0 0 1rem; max-width: 560px; }
.typebar-head { display: flex; justify-content: space-between; align-items: baseline; font-size: .95rem; margin-bottom: .3rem; }
.typebar-label { font-weight: 700; }
.typebar-nums { font-family: var(--numbers); font-weight: 900; font-feature-settings: "onum" 1; }
.typebar-track { height: 8px; border-radius: 999px; background: var(--accent-soft); overflow: hidden; }
.typebar-fill { height: 100%; width: calc(var(--p, 0) * 1%); background: var(--accent); border-radius: 999px; }
/* Previously all four bars were this same cherry fill on cherry-soft track and had to
   be told apart by their label alone. templates/tracking/stats.html now stamps
   data-kind on .typebar (the loop already had `kind` in scope, so no :nth-of-type
   guessing). Base rule above stays as a harmless fallback for an unrecognized kind. */
.typebar[data-kind="ride"] .typebar-track { background: var(--kind-ride-soft); }
.typebar[data-kind="ride"] .typebar-fill { background: var(--kind-ride); }
.typebar[data-kind="show"] .typebar-track { background: var(--kind-show-soft); }
.typebar[data-kind="show"] .typebar-fill { background: var(--kind-show); }
.typebar[data-kind="dining"] .typebar-track { background: var(--kind-dining-soft); }
.typebar[data-kind="dining"] .typebar-fill { background: var(--kind-dining); }
.typebar[data-kind="shop"] .typebar-track { background: var(--kind-shop-soft); }
.typebar[data-kind="shop"] .typebar-fill { background: var(--kind-shop); }

/* Desktop stats layout (#20) */
@media (min-width: 1024px) {
  .stats-page main { max-width: 1000px; }
  .stats-page .area-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
}

main > h1, main > h2, main section > h2, .park-card h2 { font-family: var(--display); font-weight: 400; }

/* Footer, cards, misc */
.site-footer { max-width: 640px; margin: 2rem auto 0; padding: 1rem; font-size: .78rem; opacity: .7; border-top: 1px solid var(--line); }
.site-footer a { display: inline-block; padding: .85rem .3rem; }
main a:not(.btn):not(.park-card), .site-footer a { color: var(--accent); }
.tip-link { font-weight: 700; }
.churro { width: 1.15em; height: 1.15em; flex: none; margin-right: .3em; vertical-align: -.2em; }
main a:not(.btn):not(.park-card):visited, .site-footer a:visited { color: color-mix(in srgb, var(--accent) 55%, var(--ink)); }
.park-card { display: block; border: 1px solid var(--line); border-radius: .75rem; padding: 1rem; margin: .75rem 0; text-decoration: none; color: inherit; }
.park-card h2 { margin: 0 0 .25rem; }
.continue-card { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 6%, var(--bg)); }

/* Home operator/resort groups + filters (#23) */
.resort-head { font-family: var(--display); font-weight: 400; font-size: .95rem; opacity: .8; margin: .75rem 0 .25rem; }
.park-loc { font-size: .85rem; opacity: .7; margin: -.2rem 0 .3rem; }
.park-card h4 { margin: 0 0 .25rem; font-family: var(--display); font-weight: 400; font-size: 1.15rem; color: var(--ink); }
.num-pct { font-family: var(--numbers); font-weight: 900; font-feature-settings: "onum" 1; }
.stats-link { text-align: center; margin-top: 1rem; }
.stats-link a { display: inline-block; padding: .8rem .5rem; }
.qr { max-width: 220px; }
.link-code { font-size: 2rem; letter-spacing: .3em; font-weight: 700; }
.danger { color: #c0392b; }

/* Toasts (offline + milestones + undo, #98). One shared pill: app.js queues
   undo/milestone/offline toasts FIFO through the same showToast() so at most one
   is ever on screen -- see its own comment for why a queue rather than stacking
   or replacing outright. */
.toast {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  background: var(--ink); color: var(--bg); border-radius: 999px;
  padding: .7rem 1.2rem; font-weight: 700; z-index: 999; max-width: 90vw;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .25);
  display: flex; align-items: center; gap: .6rem;
}
/* Undo (#98) shares this same pill rather than a second floating control -- a
   text button, not a visually separate chip, so it reads as part of the one
   message. ::after gives it the 44px effective target the HIG floor asks for
   without growing the pill itself, the same inset trick .map-pin/.site-message-
   dismiss/.ride-history-remove already use for a small glyph in a bigger area. */
.toast-action {
  position: relative; flex: none; border: none; background: none; color: var(--bg);
  font: inherit; font-weight: 900; text-decoration: underline; text-underline-offset: 2px;
  padding: 0; cursor: pointer; touch-action: manipulation;
}
.toast-action::after { content: ""; position: absolute; inset: -14px -8px; }
@media (prefers-reduced-motion: no-preference) {
  .toast { animation: toast-in .25s ease-out; }
  .toast.is-going { transition: opacity .5s; opacity: 0; }
  @keyframes toast-in { from { transform: translate(-50%, 16px); opacity: 0; } }
}

/* Badge case */
.badge-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .6rem; list-style: none; padding: 0; }
.badge-card { border: 1px solid var(--accent); border-radius: .75rem; padding: .7rem; display: flex; flex-direction: column; gap: .25rem; background: color-mix(in srgb, var(--accent) 5%, var(--bg)); }
/* Tiers (#30): tracking/badges.py's Badge.tier property resolves each earned slug to
   one of these six names (bronze/silver/gold/cherry from the park-% thresholds, sky
   from count-*, plum from the time-of-day badges) and stats.html stamps it as
   data-tier -- the SAME attribute the downloaded PNG (static/js/app.js) reads its
   fill color from at download time, so a badge can never render one tier on screen
   and a different one in the file a visitor saves. Text inside stays plain --ink on
   this ~6%-tinted-toward-bg background in every tier (never full-tier-color-behind-
   text), so none of this needs its own AA pass -- it's the same safe pattern
   .continue-card already uses. Only the PNG puts text on the SOLID fill; that pairing
   is verified separately (see the palette report). */
.badge-card[data-tier="bronze"] { border-color: var(--tier-bronze); background: color-mix(in srgb, var(--tier-bronze) 6%, var(--bg)); }
.badge-card[data-tier="silver"] { border-color: var(--tier-silver); background: color-mix(in srgb, var(--tier-silver) 6%, var(--bg)); }
.badge-card[data-tier="gold"] { border-color: var(--tier-gold); background: color-mix(in srgb, var(--tier-gold) 6%, var(--bg)); }
.badge-card[data-tier="cherry"] { border-color: var(--tier-cherry); background: color-mix(in srgb, var(--tier-cherry) 6%, var(--bg)); }
.badge-card[data-tier="sky"] { border-color: var(--tier-sky); background: color-mix(in srgb, var(--tier-sky) 6%, var(--bg)); }
.badge-card[data-tier="plum"] { border-color: var(--tier-plum); background: color-mix(in srgb, var(--tier-plum) 6%, var(--bg)); }
.badge-card[data-tier="leaf"] { border-color: var(--tier-leaf); background: color-mix(in srgb, var(--tier-leaf) 6%, var(--bg)); }
.badge-card strong { font-family: var(--display); font-weight: 400; }
.badge-detail { font-size: .8rem; opacity: .75; }
.badge-date { font-family: var(--numbers); font-weight: 900; font-feature-settings: "onum" 1; font-size: .85rem; }
.badge-download { align-self: flex-start; padding: .5rem .8rem; min-height: 44px; }

/* Park days calendar (#38) */
.cal-head { font-family: var(--display); font-weight: 400; font-size: .95rem; margin: 1rem 0 .35rem; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: .25rem; margin-bottom: .5rem; }
.cal-day {
  aspect-ratio: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  border-radius: .35rem; font-size: .75rem; position: relative;
}
.cal-day.cal-hit { background: var(--accent-soft); }
.cal-day.cal-big { background: var(--accent); color: var(--accent-ink); }
.cal-day.cal-out { opacity: .25; }
.cal-day b { font-family: var(--numbers); font-weight: 900; display: block; font-size: .7rem; }

/* Map page */
.map-page main { max-width: 1000px; }
#map { height: 70vh; min-height: 420px; border-radius: .75rem; }
/* Pins (#30, #101): HUE = kind, STATE = everything else. map.js sets data-kind per
   feature (resolved to --pin-hue below) and one of two state classes -- never hue
   alone for state, so colour-blind visitors always have a non-hue way to tell logged
   from not (WCAG 1.4.1, "use of colour"): not-logged is a HOLLOW ring (no fill at
   all) that is also the BIGGEST pin with the heaviest border plus an extra white
   halo, while logged recedes to a small solid dot -- the to-do signal has to win at
   a glance while walking the park, which is the entire point of scanning a map, so
   it gets both the shape difference (hollow vs solid) and the size/halo difference,
   not just one. This inverts #30's original design, which put the most emphasis on
   "logged today"; Mark: the map doesn't need to carry "when" at all, only "if" --
   history is the checklist panel's job now, not the pin's. Map tiles are always the
   "light" basemap theme regardless of the page's own dark/light mode (map.js/
   backstage-map.js both hardcode parkpercentBasemapLayers(..., "light")), so the
   separating ring/halo stays a fixed light-paper literal in every state -- var(--bg)
   would go near-black in dark mode and stop contrasting with the map surface it's
   meant to separate the pin from. Kind hue itself is NOT pinned that way: --kind-*
   already varies by page theme like every other kind surface on the site, which is
   correct here too. */
.map-pin {
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid #fdf7ea; box-shadow: 0 1px 4px rgba(0,0,0,.4);
  cursor: pointer; position: relative;
}
.map-pin::after { content: ""; position: absolute; inset: -14px; }  /* 44px effective target */
.map-pin[data-kind="ride"] { --pin-hue: var(--kind-ride); }
.map-pin[data-kind="show"] { --pin-hue: var(--kind-show); }
.map-pin[data-kind="dining"] { --pin-hue: var(--kind-dining); }
.map-pin[data-kind="shop"] { --pin-hue: var(--kind-shop); }
/* Both states are a SOLID dot in the kind's hue -- neither is hollow. The only
   difference is the weight of the white border: thick = still to do, thin = done.
   Mark: "None of them should be hollow. Colored dot with the thin white border =
   logged, colored dot with thick white border = not yet logged". Border weight,
   not fill and not hue, is the channel carrying state -- hue is already spoken
   for by kind, and a border reads at a glance without doubling a pin's footprint
   the way a size change does on a map this dense. */
.map-pin.is-not-logged {
  width: 22px; height: 22px; border-width: 6px;
  background: var(--pin-hue);
  box-shadow: 0 1px 5px rgba(0, 0, 0, .45);
}
.map-pin.is-logged {
  width: 16px; height: 16px; border-width: 2px;
  background: var(--pin-hue);
}
.map-attr { font-size: .75rem; opacity: .6; }
/* Fixed literal, deliberately NOT var(--ink): the vendored maplibre-gl.css hardcodes
   .maplibregl-popup-content's own background to plain #fff regardless of page theme
   (it's not one of ours to restyle here), so its text has to stay a fixed dark colour
   too -- var(--ink) would go near-white in dark mode and vanish on that permanently-
   white bubble. Value tracks the new light-mode --ink hex (#30) since the bubble
   itself never leaves the light palette either. */
.maplibregl-popup-content { font-family: "Nunito Sans", sans-serif; color: #2a2018; }
.popup-kind { font-size: .8rem; opacity: .75; }

/* Map legend (#30, #101): four hues * two log states is more than a first-time
   visitor should be expected to decode unaided. Every swatch sits BESIDE its own
   text label, never inside/behind it, so reading the legend never depends on
   perceiving colour correctly to begin with -- text carries the meaning, colour
   reinforces it. This is normal page chrome (unlike the pins above), so its ring is
   the theme-aware var(--ink)/var(--bg), not the map's fixed light literal. */
.map-legend { display: flex; flex-direction: column; gap: .35rem; margin: .6rem 0 0; font-size: .8rem; }
.map-legend-row { display: flex; flex-wrap: wrap; gap: .25rem 1rem; margin: 0; }
.map-legend-row span { display: inline-flex; align-items: center; gap: .35rem; }
.map-legend-swatch { width: 12px; height: 12px; border-radius: 50%; flex: none; border: 2px solid var(--bg); box-shadow: 0 1px 2px rgba(0,0,0,.3); }
.map-legend-swatch[data-kind="ride"] { background: var(--kind-ride); }
.map-legend-swatch[data-kind="show"] { background: var(--kind-show); }
.map-legend-swatch[data-kind="dining"] { background: var(--kind-dining); }
.map-legend-swatch[data-kind="shop"] { background: var(--kind-shop); }
/* Legend mirrors the pins: solid dot either way, border weight is the difference. */
.map-legend-swatch.is-not-logged { background: var(--ink); border-width: 4px; width: 16px; height: 16px; }
.map-legend-swatch.is-logged { background: var(--ink); border-width: 2px; }
