/* Spark Achiever — "bauhaus field manual" design system.
   Warm paper, ink hairlines, flat color, geometric primitives.
   Circles are reserved for badges/the spark; everything else is square.
   No build step; plain CSS. */

/* Canopy — bauhaus × the natural world. Forest-floor dark, birch-bone ink,
   a leaf/moss/pine/bark/clay/berry ramp, and a pop of orange sunlight as the
   one hero accent. Energy-aware (low-blue) so it reuses on the spark's AMOLED.
   Full system + contrast ratios: /palette.html */
:root {
  --bg:        #0b0a08;   /* site — warm forest-floor lift off black */
  --surface:   #14110b;   /* barely-raised card */
  --surface-2: #1e1912;   /* hover / pressed raise */
  --ink:       #ece4d4;   /* birch-bone text — 15.7:1 */
  --ink-dim:   #b8ad97;   /* secondary — 8.9:1 */
  --ink-faint: #978d7a;   /* tertiary — 6.0:1 */
  --line:        rgba(236, 228, 212, 0.13);
  --line-soft:   rgba(236, 228, 212, 0.06);
  /* 0.40 and not 0.28 because this one is a CONTROL BOUNDARY, not a divider:
     an unfilled pill, a ghost button, a seg, the gear — the border is the only
     thing saying "this is a button", which is exactly what WCAG 1.4.11 asks to
     be legible at 3:1. At 0.28 it resolved to 2.14:1 against the ground; 0.40
     is the first step that clears it (3.21:1). --line and --line-soft stay
     where they are: a rule between two rows carries nothing the text doesn't.
     Checked by server/tools/contrast.py, which reads this file. */
  --line-strong: rgba(236, 228, 212, 0.40);   /* emphatic dividers / frames */

  --accent:        #f0954a;   /* the one hero — sunlight orange */
  --accent-bright: #f8a95f;   /* hover / focus / glow */
  --accent-mute:   #8a4f24;   /* dim borders / hard-shadow */
  --on-accent:     #1a0f04;   /* dark ink for text on an orange fill */

  /* natural ramp (all ≥4.5:1) */
  --leaf:  #6fa847;  --moss: #97a35e;  --pine: #3f8a6e;
  --bark:  #ab7d48;  --clay: #c75b39;  --berry: #b87396;  --wheat: #d3a63f;
  /* Messages' own colour. The ramp had no blue — deliberately, since Canopy is
     low-blue so it can reuse on the spark's AMOLED — so this one is muted to sit
     with the rest rather than shout past them: 5.9:1 on the ground, the same
     rung as berry. It is a web colour only; the puck's palette is its own
     (firmware-spark/src/menu_internal.h) and stays neutral-plus-accent. */
  --sky:   #5b8fc9;

  /* legacy aliases — existing rules + per-page inline styles resolve right */
  --paper:    var(--bg);
  --paper-2:  var(--surface);
  --ink-soft: var(--ink-dim);
  --ochre:    var(--wheat);
  --brick:    var(--accent);
  --blue:     var(--pine);
  --green:    var(--leaf);
  --plum:     var(--berry);

  --font: "Jost", sans-serif;
}

/* SELF-HOSTED, deliberately. Every page used to pull Jost from fonts.googleapis.com,
   which meant every page load told Google who was using this Klic and when — for a
   self-hosted, open-source system that is the wrong default. It also broke the app
   off-grid: no internet, no typeface, and the pages stalled on a preconnect to a
   host that wasn't there.
   ONE variable file covers everything the site asks for: wght 100-900 and a real
   ital axis (Jost-VF, SIL OFL — see fonts/OFL.txt), so 400/500/600 and italic all
   come from a single request instead of four.
   TTF rather than woff2 only because woff2 is a COMPRESSION format — identical
   outlines, identical rendering. 257KB is nothing on a LAN and it sits in the
   service worker's shell cache after first load. */
@font-face {
  font-family: "Jost";
  src: url("/fonts/Jost-VF.ttf") format("truetype-variations");
  font-weight: 100 900;
  font-style: normal;
  /* `optional`, not `swap`. Swap paints every page in the fallback face and then
     re-lays it out in Jost the moment the file lands — the text visibly shifting
     inside its own boxes. optional gives the font a short block window, and the
     file is preloaded in every page's <head>, so it wins that race virtually
     always; when it doesn't, the fallback is kept for the page's life rather
     than reflowing under the reader. */
  font-display: optional;
}
@font-face {
  font-family: "Jost";
  src: url("/fonts/Jost-VF.ttf") format("truetype-variations");
  font-weight: 100 900;
  font-style: italic;
  font-variation-settings: "ital" 1;
  font-display: swap;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html { color-scheme: dark; }

body {
  background-color: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.55;
  min-height: 100vh;
  min-height: 100dvh;   /* mobile: the real viewport, not the pre-toolbar guess */
  /* Column, so a SHORT page still pushes the mountain to the bottom of the screen
     instead of leaving it stranded mid-viewport with blank paper under it. The
     stretching is done by main below, NOT by margin-top:auto on the footer — auto
     collapses to 0 once content is tall enough, which would silently drop the 64px
     the footer is supposed to keep above itself on every long page. */
  display: flex;
  flex-direction: column;
  /* real topo contours — Mount Rainier elevation data, see tools/gen_topo.py */
  background-image: url("/topo.svg?v=canopy");
  /* 1536px is the drawing's own width and the scale the contours were designed at,
     so keep it — but it only repeats DOWN, which left bare paper either side of a
     viewport wider than that. max() holds the intended size until the screen
     outgrows it, then fills the width. `auto` keeps the aspect, so the contours get
     larger on a very wide display rather than stretched.
     Not `repeat`: this is real terrain, not a tile, and it would seam. */
  background-size: max(1536px, 100%) auto;
  background-position: center top;
  background-repeat: repeat-y;
}

/* WIDTH FIRST, and this line is load-bearing. Nearly every top-level block here is
   `max-width: X; margin: 0 auto` — which fills the width and centres under block
   layout. Under flex it does NOT: a cross-axis `auto` margin cancels the stretch, so
   each one collapsed to fit its own content and the whole page came out narrow
   ("messages is crushed"). Restoring width:100% gives max-width something to cap and
   the auto margins something to centre, which is exactly the block behaviour. */
body > * { width: 100%; }

/* takes up the slack on short pages; never shrinks the content on long ones */
body > main { flex: 1 0 auto; }
/* Every page is a shell its script fills, so <main> is EMPTY at first paint —
   which parked the mountain mid-screen and then shoved it down the instant
   content landed, on every single load. Reserving a viewport while empty starts
   the footer below the fold, so it lands once instead of sliding past the
   reader. The router holds the outgoing page's height on top of this
   (app.js routeTo), so a soft navigation doesn't move it at all. */
body > main:empty { min-height: 100vh; min-height: 100dvh; }
/* fixed height, and it must not be squeezed by a tall main */
body > footer.site { flex: none; }
/* The mountain waits for the page it sits under. Every page is a shell its
   script fills, so for the first beat of a load the footer has nothing above it
   — it sat mid-screen and was then shoved down as content landed, which was the
   single largest layout shift in the app. Held invisible until the page says it
   has rendered (app.js marks .page-ready from revealView), it simply fades in
   where it belongs: its position is already final, so nothing the reader can
   see ever moves. */
body > footer.site { opacity: 0; }
html.page-ready body > footer.site { opacity: 1; transition: opacity .4s ease .05s; }
@media (prefers-reduced-motion: reduce) {
  html.page-ready body > footer.site { transition: none; }
}

::selection { background: var(--ink); color: var(--paper); }

a { color: var(--ink); }

/* ---------- page frame -----------------------------------------------------
   Every page used to roll its own <main>: nine containers across the app with
   four different side paddings (14/20/28), five measures (520/680/700/720/
   1040/1080) and four top drops (0/6/24/28). Moving between Badges, Quests and
   Messages, the text shifted left and right and started at a different height
   each time. Worse, only .wrap ever read env(safe-area-inset-*), so eight pages
   ran under the notch in landscape.

   So the frame is four numbers, declared once. A page chooses a MEASURE and
   nothing else; the gutter, the drop and the tail are not its business. */
:root {
  --gutter: 28px;     /* side margin — header, content and footer share the line */
  /* 26, matching the margin a section bar's own tabs sit on — so the first
     thing you read starts at the same height whether the page carries that bar
     or not. At 40 a page without one (Quests, Messages, Account) began 14px
     lower than the Badges tabs beside it in the nav, which read as the pages
     not lining up with each other. The phone already agreed on 26. */
  --drop:   26px;     /* chrome to the first thing on the page */
  --drop-sub: 22px;   /* …when a section sub-bar already sits between them */
  --tail:   90px;     /* last thing on the page to the mountain */
  --measure-tool: 520px;    /* one focused job: capture, locate, sign-in */
  --measure-read: 680px;    /* a column of prose, rows, or messages */
  --measure-wide: 1080px;   /* a board: catalogs, the queue, the studio */
  /* the status bar / Dynamic Island, as a value SCRIPT can read: CSS gets
     env() directly, but a popover positioned from JS has to clamp against it
     too, and getComputedStyle can't see an env() that was never assigned */
  --sat: env(safe-area-inset-top);
}
@media (max-width: 720px) {
  :root { --gutter: 20px; --drop: 26px; --drop-sub: 16px; --tail: 64px; }
}

/* The gutter is the app's left edge, and the header, the content and the footer
   all start on it. max() rather than a plain value so a notched phone in
   landscape pushes content clear instead of running under the cutout. */
.wrap,
body > main {
  margin-left: auto; margin-right: auto;
  padding-left: max(var(--gutter), env(safe-area-inset-left));
  padding-right: max(var(--gutter), env(safe-area-inset-right));
}
.wrap { max-width: var(--measure-wide); }
/* The drop and the tail belong to <main>, not to whatever happens to be first
   inside it — that is how four different pages ended up four different heights.
   A page with a sub-bar above it drops less: the bar has already spent the
   space, and .has-subnav is set synchronously in the head of exactly the four
   pages that carry one, so the smaller drop is there at first paint.
   (app.js renderSubnav re-checks it on every soft nav — the head script cannot
   run again, and the class must not follow you onto a page with no bar.) */
body > main {
  /* A main that names no measure of its own takes the wide one. Without this it
     runs the full viewport, and any .wrap nested inside it then centres itself
     in a WIDER column than the header's .wrap — which put the studio's content
     a gutter to the left of the wordmark above it. */
  max-width: var(--measure-wide);
  padding-top: var(--drop);
  padding-bottom: var(--tail);
}
html.has-subnav body > main { padding-top: var(--drop-sub); }
/* …and a .wrap nested INSIDE main is a column, not the frame — the frame is
   already on <main>, so taking the gutter twice puts that page's content at
   double everyone else's. */
body > main .wrap { padding-left: 0; padding-right: 0; }
/* …and nothing that lands at the top of a page adds to that drop. One rule
   instead of chasing every .qq-head / .login-panel / .section-head that set its
   own — including pages whose <main> holds a single wrapper (home, badge). */
body > main > :first-child,
body > main > :only-child > :first-child { margin-top: 0; }
/* …except a whole-page empty state, which is meant to float down the page.
   One float for all of them, rather than 120 here and 24 or 40 there. */
body > main > .empty:first-child { margin-top: 120px; }

/* small lowercase letterspaced label — the system's only "label" voice */
.label, .kicker, .chip, .coords, .reqs-h, .when, .err, .empty {
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: lowercase;
  font-weight: 500;
}

/* bold text carries the accent */
b, strong { color: var(--accent); }

ol li {
  margin-bottom: 1.4em;
}

div > li,
ul li {
  list-style-type: none;
  margin-bottom: 1.4em;
}

/* ---------- header ---------- */

header.site {
  /* no rule under it — the bar's own fill and blur already separate it from
     whatever scrolls beneath */
  position: sticky; top: 0; z-index: 20;
  /* sit the sticky bar below the Dynamic Island / status bar; the blurred
     background extends up under it */
  padding-top: env(safe-area-inset-top);
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(8px);
}
header.site .wrap {
  /* named, because the two controls at the right end subtract it back out to
     land on the bar's own rhythm — and it is not the same number on a phone */
  --row-gap: 18px;
  display: flex; align-items: center; gap: var(--row-gap);
  padding-top: 16px; padding-bottom: 16px;
  /* the content row, RESERVED: the bar ships empty in every page's markup and is
     filled by renderChrome after first paint, so without this every page began
     36px short and dropped its whole content the moment the header appeared */
  min-height: 36px;
}
/* the mark: a spark alive inside the device disc */
/* THE FULL BAR HEIGHT: 68px = the wrap's 16px padding, its ~36px content row, and
   16px again. The negative margins cancel that padding so the spark spans the bar
   edge to edge WITHOUT making it taller — anything less leaves the mark floating in
   the middle of a bar built around something else. */
.mark {
  width: 68px; height: 68px; flex: none;
  margin-top: -16px; margin-bottom: -16px;
  /* pulls the wordmark in against the 18px wrap gap. Negative margin rather than a
     smaller gap, because that gap also spaces the bell and burger on the right and
     they are fine as they are. The spark's glow fades out well before the canvas
     edge, so the optical gap is wider than the measured one. */
  margin-right: -10px;
}
.mark svg, .mark img, .mark canvas { width: 100%; height: 100%; display: block; }
.wordmark {
  font-weight: 600; font-size: 23px; letter-spacing: 0.02em;
  text-transform: lowercase; color: var(--ink); text-decoration: none;
}
.wordmark em { color: var(--brick); font-style: normal; }
/* ONE rhythm down the right side of the bar: pill to pill, pill to the
   notification disc, disc to the burger — all 12px. It was 8 between the pills
   and 18 to everything after them, so the group read as two clusters that
   happened to be near each other. 12 is the pill gap, a little opened up.
   The two elements after this nav each cancel part of the header row's own 18px
   gap to land on the same number (see .nav-bell / .nav-burger). */
nav.site-nav { margin-left: auto; display: flex; gap: var(--nav-gap, 12px); }
/* ONE pill, both bars. Desktop wore underlined text and the phone wore pills —
   the same three destinations in two different clothes, and the desktop version
   was the one that didn't look like the rest of the app. */
nav.site-nav a, nav.quicknav a {
  flex: none; display: inline-flex; align-items: center; gap: 5px;
  padding: 7px 15px; border-radius: 999px;
  /* The outline wears the section's colour BEFORE you get there, held at 60%
     so the bar reads as three quiet hues and the one you're in is the only
     thing at full strength. The count chips already sit in these exact colours
     (.nc-badges / .nc-quests / .nc-msgs), so a pill now matches its own chip
     instead of framing it in grey. */
  border: 1px solid color-mix(in srgb, var(--pill, var(--ink-faint)) 60%, transparent);
  background: var(--surface);
  color: var(--ink-soft); text-decoration: none;
  font-size: 13px; letter-spacing: 0.06em; font-weight: 500;
  white-space: nowrap;
  transition: background-color .18s, border-color .18s, color .18s;
}
/* hover takes that same hue to full — the rung between "not here" and "here" */
nav.site-nav a:hover, nav.quicknav a:hover {
  color: var(--ink); border-color: var(--pill, var(--ink-faint));
}
/* Each section owns a colour: outlined in it always, filled with it when you
   are in that section. Declared once per destination as --pill/--pill-ink, so
   a single rule paints every copy of the nav (inline bar, phone pills) and
   adding a section is two custom properties rather than another selector in
   three places. */
[data-nav="badges"]   { --pill: var(--leaf);   --pill-ink: #14200b; }
[data-nav="quests"]   { --pill: var(--accent); --pill-ink: var(--on-accent); }
[data-nav="messages"] { --pill: var(--sky);    --pill-ink: #05121f; }
[data-nav="klic"]     { --pill: var(--berry);  --pill-ink: #1e0c16; }
nav.site-nav a.active, nav.quicknav a.active {
  background: var(--pill, var(--accent)); border-color: var(--pill, var(--accent));
  color: var(--pill-ink, var(--on-accent));
}
/* the count chip inverts on a filled pill, or it vanishes into it */
nav.site-nav a.active .nc, nav.quicknav a.active .nc {
  background: var(--pill-ink, var(--on-accent)); color: var(--pill, var(--accent));
}

/* nav counters ride INLINE on their own items: green in-progress badges beside
   "Badges", orange open quests beside "Quests". */
/* NO left margin. Every container that carries one of these is a flex row with
   its own `gap`, so a margin here was added ON TOP of it — 6 + 7 = 13px between
   a word and its count, which read as the disc drifting away from the label it
   belongs to. The gap owns the spacing now, at 5px, and it is the same 5 on the
   lens pills below (which are not flex, so those keep a margin). */
.nc {
  display: inline-grid; place-items: center; min-width: 18px; height: 18px;
  padding: 0 5px; vertical-align: 2px;
  border-radius: 999px; font-size: 11px; font-weight: 700; line-height: 1;
  /* digits share a width, so 9 -> 10 doesn't nudge the label beside it */
  font-variant-numeric: tabular-nums;
  transition: opacity .25s ease, transform .25s cubic-bezier(.34, 1.4, .64, 1),
              background-color .25s ease, min-width .25s ease;
}
.nc[hidden] { display: none; }
/* THE SAME DISC BESIDE A TAB, where the type is 20–28px rather than a nav
   pill's 13. At the nav's 18px it read as a small thing floating over the word:
   too light for the type it belongs to, and centred on the CAP band when the
   eye reads a word by its x-height — which is what put it visibly high. So it
   grows with the tab and drops the couple of pixels that set it level with the
   bulk of the word.
   `top` rather than `transform`: .nc.counting owns the transform (the waiting
   disc's shrink), and a translate here would win on specificity and cancel it. */
.section-head .tab .nc { min-width: 22px; height: 22px; font-size: 12.5px; position: relative; top: 2px; }
.lens-tab .nc { min-width: 19px; height: 19px; font-size: 11.5px; position: relative; top: 1.5px; }
/* no signal, and what's waiting because of it — quiet, but never hidden */
.offline-chip {
  margin-left: auto; flex: none; white-space: nowrap;
  font-size: 11px; letter-spacing: 0.1em; text-transform: lowercase; font-weight: 600;
  color: var(--ink-faint); border: 1px solid var(--line-strong);
  border-radius: 999px; padding: 3px 10px;
}
.offline-chip.is-offline { color: var(--accent); border-color: var(--accent-mute); }
.offline-chip + .nav-bell { margin-left: 12px; }
/* Waiting for its number: the circle is already there, empty and quiet, so the
   count arrives INTO a shape that was always on the page rather than appearing
   from nothing and shoving its neighbours along. */
.nc.counting, .pill-n.counting {
  color: transparent; background: var(--line); transform: scale(.82); opacity: .7;
}
/* the settle when the number lands */
.nc.landed, .pill-n.landed { transform: scale(1); opacity: 1; }
.nc-quests { background: var(--accent); color: var(--on-accent); }
.nc-badges { background: var(--leaf); color: #14200b; }
.nc-review { background: var(--brick); color: #000; }   /* lead: items waiting to review */
/* unread messages — blue now, the section's own colour, so the three discs read
   as one set that differ only in hue. (It was accent, matching the bell and the
   count on a conversation row; the bell keeps that, since it isn't a section.) */
.nc-msgs { background: var(--sky); color: #05121f; }
/* The nothing-waiting state: the same disc, carrying a dark slate dash instead
   of a count (app.js refreshMsgCount). Quieter than the near-black a count
   wears — 2.8:1 on the blue against the count's 5.6 — because it is a mark that
   should read without asking for attention.
   The 1px of BOTTOM padding is a half-pixel lift, not spacing. Geometric centre
   is not optical centre here: a bar sitting dead on the middle of a circle
   reads as sinking in it, so this puts the dash's ink a pixel above centre
   (8.0 of 18) where it looks level. In a centred grid a 1px pad moves the
   content half its width, hence 1 for half a pixel. */
.nc-msgs.nil { color: #33475c; padding-bottom: 1px; }
.nav-panel .nc { vertical-align: 3px; }

/* ---------- mobile menu: burger + slide-in panel from the right ---------- */

/* On EVERY width now, not just phones: it carries Account, Spark and Log out —
   the things that are about you rather than about a section — and those left the
   inline bar so the bar is only the three places you go. */
/* NO auto margin here on a desktop: nav.site-nav already claims the free space,
   and a second auto on the burger split the difference — pills stranded in the
   middle of the bar with the burger away at the edge. They travel as one group
   at the right now. The phone puts the auto back, because there the inline nav
   is hidden and nothing else is pushing. */
.nav-burger {
  display: flex; flex: none;
  flex-direction: column; justify-content: center; gap: 5px;
  width: 44px; height: 44px; padding: 0 9px;
  /* back to the bar's 12 from the row's 18, AND back by its own 9px of left
     padding — so the gap you SEE, to the first bar rather than to the button's
     box, matches the one before the disc. The padding stays for the tap target;
     only the optical spacing is corrected. */
  margin-left: calc(var(--nav-gap, 12px) - var(--row-gap, 18px) - 9px);
  background: none; border: 0; cursor: pointer;
}
.nav-burger i { display: block; height: 2px; background: var(--ink); }
.nav-scrim {
  position: fixed; inset: 0; z-index: 48;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0; pointer-events: none; transition: opacity 0.25s;
}
.nav-scrim.open { opacity: 1; pointer-events: auto; }
.nav-panel {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 49;
  width: min(78vw, 300px);
  display: flex; flex-direction: column;
  padding: calc(34px + env(safe-area-inset-top)) 28px 28px;
  background: var(--surface); border-left: 1px solid var(--line-strong);
  transform: translateX(100%); transition: transform 0.28s ease;
}
.nav-panel.open { transform: translateX(0); }
.nav-panel a, .nav-panel .nav-out {
  font-family: var(--font); font-size: 22px; font-weight: 600; text-align: left;
  color: var(--ink-soft); text-decoration: none;
  padding: 14px 0; border: 0; border-bottom: 1px solid var(--line);
  background: none; cursor: pointer;
}
.nav-panel a.active { color: var(--accent); }
.nav-panel a:hover, .nav-panel .nav-out:hover { color: var(--ink); }
/* Leaving is not a destination — it reads as the one thing here that undoes
   something, and it sits last with no rule under it. */
.nav-panel .nav-out { color: var(--ink-faint); border-bottom: 0; margin-top: 6px; }
.nav-panel .nav-out:hover { color: var(--clay); }

/* mobile-only quick-switch; the inline nav covers this on wider screens */
nav.quicknav { display: none; }

@media (max-width: 720px) {
  nav.site-nav { display: none; }
  /* the phone's push lives with the .nav-bell rules further down — a @media
     block up here adds no specificity and loses to them on source order */
  nav.quicknav {
    display: flex; gap: 8px;
    padding: 10px 20px;
    /* RESERVE THE FILLED HEIGHT. The element ships empty in every page's markup and
       app.js fills it — so without this it paints as a ~21px strip and then grows to
       ~56px, shoving the page down exactly like the injected version did. Matching
       heights means first paint and filled paint are the same layout. */
    min-height: 56px; align-items: center;
    /* Centred — but `safe` centring, declared after the plain one so older engines
       still get something. With plain `center` an overflowing row pushes its FIRST
       item off the scrollable start, where it cannot be reached; `safe` falls back
       to flex-start exactly when that would happen. Three short pills will not
       overflow a phone today, but a fourth section or a long word would. */
    justify-content: center;
    justify-content: safe center;
    /* one scrollable line rather than wrapping, same as .section-head below */
    overflow-x: auto; scrollbar-width: none;
    background: var(--paper);
  }
  nav.quicknav::-webkit-scrollbar { display: none; }
  /* the pill itself is shared with nav.site-nav above — only the strip that
     carries them is phone-only now */
  header.site .wrap { --row-gap: 12px; }
  .wordmark { white-space: nowrap; }
  footer.site { height: 200px; margin-top: 48px; }
  footer.site .caption { left: 16px; bottom: 14px; }
  footer.site .wrap    { right: 16px; bottom: 14px; }
  /* catalog tabs: one scrollable line instead of wrapping off-screen */
  .section-head { flex-wrap: nowrap; overflow-x: auto; }
  .section-head .tab { white-space: nowrap; flex: none; }
  .section-head h2 { white-space: nowrap; }
  /* the section bar itself sits a point smaller on a phone — it grew a fourth
     tab (View · Review · Photos · Manage) and is the widest row on the page */
  nav.subnav .section-head .tab { font-size: 27px; }
}

/* The signed-out marketing hero and its product-shot spark lived here. Both
   went with the page they were on: signed out never reaches the catalog now,
   so there was no visitor left to show them to. */

/* author `display` (on .btn, .tab…) overrides the UA [hidden] rule, so make the
   `hidden` attribute authoritative everywhere — otherwise hidden buttons/tabs
   still render (e.g. the lead-only "manage badges" button) */
[hidden] { display: none !important; }
.kicker { color: var(--brick); }

/* ---------- sections ---------- */

.section-head {
  display: flex; align-items: baseline; gap: 16px;
  margin: 56px 0 0; border-bottom: 1px solid var(--line-strong); padding-bottom: 14px;
}
.section-head h2 {
  font-weight: 600; font-size: 28px;
  letter-spacing: 0; text-transform: lowercase;
}
/* unified chip: dark-gray pill with light text. The section-head count is the
   same pill, so every count across the app matches it. */
.pill, .section-head .count {
  display: inline-flex; align-items: center; white-space: nowrap;
  font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase; font-weight: 600;
  color: var(--ink); background: #26221c; padding: 3px 11px;
}
/* colored variants */
.count.green, .pill.green { background: var(--leaf); color: #14200b; }
.count.orange, .pill.orange { background: var(--accent); color: var(--on-accent); }
.count.light, .pill.light { background: #3d3527; }

/* ---------- lens bar: the puck's badge header, on the web ----------
   Two labelled pill groups spread across one row (BADGES · TRACKS), each with
   its name + in-progress count above a snug row of pills. Mirrors menu.c's
   bd_build_tabs so the device and the site read as one control. */
.lens-bar {
  display: flex; flex-direction: column; gap: 20px;
  margin: 0;
}
/* Badges / Tracks as a tab level, one step quieter than the subnav's own
   View·Manage·Review above it. Phone-only — the media query switches it on and
   hides the group not chosen; desktop has the room for both groups at once. */
.lens-tabs { display: none; }
/* No underline on the selected one — full-strength ink against the others'
   45% is the whole signal, the same way the section bar above marks its own. */
.lens-tab {
  font: inherit; font-size: 20px; font-weight: 600; line-height: 1;
  background: none; border: 0; padding: 5px 0; cursor: pointer;
  color: var(--ink-soft); opacity: 0.45;
}
.lens-tab.active { color: var(--ink); opacity: 1; }
/* the two lens groups share the top row, side by side on the left */
.lens-groups { display: flex; flex-wrap: wrap; gap: 22px 40px; align-items: flex-start; }
.lens-group { display: flex; flex-direction: column; gap: 14px; }
/* THE SAME NAME AT THE SAME SIZE, whichever way the bar is folded. On a phone
   these names ARE the tab level (.lens-tab, 20px); on a desktop they stand over
   their own pill row instead — and they were a 12px uppercase kicker there,
   which read as a different, smaller kind of thing on each screen and as a
   different thing again from the tab level on Quick Quests. One level, one
   size, everywhere: 20px, sentence case as authored, full ink (nothing here is
   the unchosen one — both groups are on screen). */
.lens-label {
  display: flex; align-items: center; gap: 8px;
  font-size: 20px; font-weight: 600; line-height: 1;
  letter-spacing: 0; text-transform: none; color: var(--ink);
}
/* each tab pill carries a count: neutral by default, green on Working (in
   progress), orange on Earned (finished). On a filled active pill the count
   takes the pill's own ink. The Working pill wears the green identity active. */
/* the count is a little circle, like the nav menu counts: neutral / green
   (working) / orange (earned). On a filled active pill it inverts to a light
   chip so it stays legible. */
.lens-pill .pill-n {
  display: inline-grid; place-items: center; min-width: 18px; height: 18px;
  /* no margin — the pill is a flex row now and its `gap` sets this, exactly as
     .nc's does in the nav bar */
  padding: 0 5px; vertical-align: 2px; border-radius: 999px;
  font-size: 11px; font-weight: 700;
  background: #3d3527; color: var(--ink);
  font-variant-numeric: tabular-nums;
  transition: opacity .25s ease, transform .25s cubic-bezier(.34, 1.4, .64, 1),
              background-color .25s ease, min-width .25s ease;
}
/* Never display:none while a count is still unknown — the pill would resize
   under the reader when it arrived. Hidden is reserved for a KNOWN zero, and
   even that collapses on its own transition rather than vanishing. */
.lens-pill .pill-n[hidden] { display: none; }
/* the waiting disc, restated at this specificity: `.lens-pill .pill-n` above
   sets a colour and would otherwise win the tie against `.pill-n.counting` */
.lens-pill .pill-n.counting {
  color: transparent; background: var(--line); transform: scale(.82); opacity: .7;
}
.lens-pill.working .pill-n { background: var(--leaf); color: #14200b; }
.lens-pill.earned-lens .pill-n { background: var(--accent); color: var(--on-accent); }
.lens-pill.working.active { background: var(--leaf); border-color: var(--leaf); color: #14200b; }
/* on a filled active pill the circle goes dark with the state color as the number */
.lens-pill.active .pill-n { background: #17130c; color: var(--ink); }
.lens-pill.working.active .pill-n { color: var(--leaf); }
.lens-pill.earned-lens.active .pill-n { color: var(--accent); }
.lens-pills { display: flex; gap: 5px; }
/* THE SAME PILL AS THE NAV BAR'S, to the pixel — metrics, padding, fill, border
   and the 5px to its count. It was its own shape (14px type, 11/13 padding, a
   different surface and a min-width that never bound) which made it read as a
   heavier, squarer relative of the thing at the top of the page rather than the
   same control one level down.
   inline-flex matters as much as the numbers: these are built as `Active <span>`,
   and outside a flex row that source whitespace RENDERS — so the label sat ~9px
   off its disc where the header's sat at 5, which is the gap that looked wrong.
   As a flex item the trailing space is trimmed and `gap` is the whole story. */
.lens-pill {
  /* no line-height override: `font: inherit` already brings the body's, which
     is what makes the nav pill 36px tall. Pinning it to 1 here left these two
     pixels shorter than the bar above — same padding, different box. */
  font: inherit; font-size: 13px; font-weight: 500;
  letter-spacing: 0.06em;
  flex: none; display: inline-flex; align-items: center; gap: 5px;
  padding: 7px 15px; cursor: pointer;
  color: var(--ink-soft); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: 999px;
  transition: background .15s, color .15s, border-color .15s;
}
.lens-pill:hover { color: var(--ink); border-color: var(--ink-faint); }
.lens-pill.active {
  background: var(--accent); color: var(--on-accent);
  border-color: var(--accent);
}
.lens-pill[hidden] { display: none; }

/* PHONE. This block MUST stay below the base lens rules above: @media adds no
   specificity, so up in the header's own breakpoint block these rules tied with
   the defaults and lost on source order. */
@media (max-width: 720px) {
  /* the lens bar folds to one group at a time: the names leave their pill rows
     and become the tab level, and only the chosen group's pills stay */
  .lens-bar { gap: 14px; }
  .lens-tabs { display: flex; gap: 22px; }
  .lens-label { display: none; }
  .lens-bar[data-group="bd"] .lens-group[data-group="tr"],
  .lens-bar[data-group="tr"] .lens-group[data-group="bd"] { display: none; }
  /* three pills plus their counts can just miss on a narrow phone — same
     scrollable line the quicknav and the catalog tabs use, rather than a wrap */
  .lens-pills { overflow-x: auto; scrollbar-width: none; }
  .lens-pills::-webkit-scrollbar { display: none; }
  .lens-pill { flex: none; white-space: nowrap; }
}

/* ========== uniform badge display system ==========
   One congruent look for every badge collection. A .bd-coll is grid (lg/md/sm)
   or list (lg/md); each .bd-item reflows between a centered card and a
   coin·title·progress row. --coin drives the coin size per mode. */
.bd-coll.grid { display: grid; gap: 4px 2px;
  grid-template-columns: repeat(auto-fill, minmax(var(--col), 1fr));
  /* the catalog lattice's plain `.grid` further down also matches this element
     and gives it 96px of bottom margin — which inside a track level is 96px of
     empty box under two coins, and everywhere else is a gap nobody asked for.
     The template-columns there loses to this rule on specificity; the margin
     had nothing to lose to. */
  margin-bottom: 0; }
/* In a grid the coin is 3/4 of ITS COLUMN, measured rather than guessed at —
   --col still decides how many columns, and the coin fills three quarters of
   whatever that works out to. A fixed pixel size cannot do this: the same 150px
   that is 3/4 of a 200px desktop column is 42% of the single 353px column a
   phone gives Large, which is the case that looked wrong. */
/* Large means large: the column is 300 rather than 200, which on a desktop
   column is three badges across at ~256px each — double the 128 it drew before
   this pass, and the reason to have a Large at all. */
.bd-coll.grid.lg { --coin: 150px; --col: 300px; }
.bd-coll.grid.md { --coin: 114px; --col: 152px; }
.bd-coll.grid.sm { --coin: 81px;  --col: 108px; }
.bd-coll.grid .coin-wrap { width: 75%; }
/* Large takes 80 rather than 75. It is the size somebody picks to LOOK at the
   art, so it gets the extra; the smaller two are scanning sizes and want the
   breathing room more than the pixels. */
.bd-coll.grid.lg .coin-wrap { width: 80%; }
.bd-coll.grid .bd-coin { width: 100%; }
.bd-coll.list { display: flex; flex-direction: column; }
.bd-coll.list.lg { --coin: 120px; }
.bd-coll.list.md { --coin: 96px; }
.bd-coll.list.sm { --coin: 74px; }

.bd-item {
  display: flex; color: inherit; text-decoration: none; cursor: pointer;
  border: 0; background: none; font: inherit; -webkit-appearance: none; appearance: none;
}
.bd-item .coin-wrap { position: relative; flex: none; }
.bd-coin { display: block; width: var(--coin); aspect-ratio: 1; border-radius: 50%;
  box-shadow: 0 0 0 1.5px var(--line-strong); }
.bd-coin.started { box-shadow: 0 0 0 2px var(--leaf); }
.bd-coin.done { box-shadow: 0 0 0 2px var(--accent); }
.bd-coin img, .bd-coin svg { display: block; width: 100%; height: 100%;
  border-radius: 50%; object-fit: cover; }
.bd-title { transition: color 0.18s; }
.bd-sub { display: none; }   /* the one-line summary shows in list view only */
.bd-item:hover .bd-title { color: var(--accent); }
.bd-item.sel .bd-title { color: var(--accent); font-weight: 600; }   /* popover-picked */

/* track-only bd-items: the "+ add badge" elective and empty quota slots */
.bd-coin.add, .bd-coin.slot { display: grid; place-items: center; box-shadow: none;
  border: 2px dashed var(--line-strong); background: transparent;
  color: var(--ink-soft); font-size: calc(var(--coin) * 0.42); line-height: 1; }
.bd-item.add-badge { cursor: pointer; }
.bd-item.add-badge:hover .bd-coin.add { border-color: var(--accent); color: var(--accent); }
.bd-item.placeholder { cursor: default; }
.bd-item.placeholder .bd-title { color: var(--ink-soft); }
.bd-prog { font-size: 12px; color: var(--ink-faint); letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.bd-prog.started { color: var(--leaf); }   /* in progress */
.bd-prog.earned { color: var(--accent); text-transform: lowercase; }   /* finished */

/* GRID: centered vertical card, progress rides the coin */
/* 22px of vertical padding was most of the space between two rows of badges,
   on top of the grid's own gap — the coins now carry the cell and the padding
   is only what keeps a title off its neighbour. */
.bd-coll.grid .bd-item { flex-direction: column; align-items: center;
  text-align: center; gap: 9px; padding: 10px 6px; }
.bd-coll.grid .bd-prog { display: none; }
.bd-coll.grid .bd-text { align-self: stretch; text-align: center; }
.bd-coll.grid.lg .bd-title { font-size: 17px; font-weight: 600; }
.bd-coll.grid.md .bd-title { font-size: 15px; font-weight: 600; }
.bd-coll.grid.sm .bd-title { font-size: 12.5px; font-weight: 500; }

/* LIST: coin at the left, everything else STACKED beside it — title, summary,
   then the count under them both.
   Nothing is trimmed. Title and summary were each one nowrap line ending in an
   ellipsis, which on a phone meant most badges showed half a title and a
   summary cut mid-word — the two pieces of text that say what the badge IS. The
   row grows to fit them instead.
   Grid rather than a flex row, because the count has to sit UNDER the summary
   while the coin stays beside all of it, and the coin is the markup's first
   child. It spans both rows; everything else stacks in the second column. */
.bd-coll.list .steps-pill { display: none; }   /* not on the coin in list */
.bd-coll.list .bd-item {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: start; column-gap: 14px; row-gap: 4px;
  padding: 12px 6px; border-bottom: 1px solid var(--line);
}
.bd-coll.list .coin-wrap { grid-row: 1 / span 2; align-self: center; }
.bd-coll.list .bd-text { grid-column: 2; display: flex; flex-direction: column; gap: 2px; }
.bd-coll.list .bd-prog { grid-column: 2; justify-self: start; }
.bd-coll.list .bd-title { text-align: left; font-weight: 600; }
.bd-coll.list .bd-sub { display: block; font-size: 12.5px; color: var(--ink-faint);
  text-align: left; }
.bd-coll.list.lg .bd-title { font-size: 22px; }
.bd-coll.list.lg .bd-sub { font-size: 16px; color: var(--ink-soft); }
.bd-coll.list.md .bd-title { font-size: 19px; }
.bd-coll.list.md .bd-sub { font-size: 15px; color: var(--ink-soft); }
.bd-coll.list.sm .bd-title { font-size: 16px; }
.bd-coll.list.sm .bd-sub { font-size: 13.5px; color: var(--ink-soft); }
/* (no margin-left:auto any more — the count sits under the summary, not out at
   the far right of the row) */

/* the per-lens display controls (adventure page) */
.disp-row { display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 10px 0;
  border-bottom: 1px solid var(--line); }
.seg { display: inline-flex; border: 1px solid var(--line-strong); border-radius: 999px; overflow: hidden; }
.seg button { font: inherit; font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 6px 12px; background: transparent; color: var(--ink-soft); border: 0; cursor: pointer; }
.seg button + button { border-left: 1px solid var(--line); }
.seg button.on { background: var(--accent); color: var(--on-accent); }
.seg button:disabled { opacity: 0.3; cursor: default; }

/* The gear rides the END of the pill row, not the top corner of the bar: it
   changes what the pills show, so it belongs on their line. It is the last item
   in .lens-groups — pushed right by its own auto margin and bottom-aligned
   against the group boxes, which is what makes it level with the pills whether
   or not their names are above them (36px control, 36px pill). In flow, so
   nothing has to be positioned against the bar or measured in pixels. */
.lens-tools {
  position: relative;   /* the settings menu hangs off it */
  margin-left: auto; align-self: flex-end;
  display: flex; align-items: center; gap: 10px;
}
/* The type filter: a word and a chevron, no pill.

   The native <select> is laid TRANSPARENTLY over a label we draw ourselves,
   because a select's own rendered value cannot be made to sit tight against an
   arrow — the control is as wide as its widest option ("outdoors (14)"), and
   text-align is honoured inconsistently across engines. Three attempts at
   styling it directly gave a stranded word, then a clipped one, then a select
   the width of the row. So the select stops being the thing on screen and
   becomes only the thing you tap; the span is what anybody sees, and it is
   exactly as wide as the word plus the arrow. */
/* An outline pill, sized to the segmented pickers it now sits under in the
   drawer: same border, same radius, same 12px uppercase as a .seg button. It
   was a bare word on the bar; in a row of controls it needs a control's edge. */
.lens-cat-wrap {
  position: relative; display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border: 1px solid var(--line-strong); border-radius: 999px;
  font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-soft); cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.lens-cat-wrap:hover { border-color: var(--ink-faint); }
.lens-cat-wrap[hidden] { display: none; }
.lens-cat-wrap:hover { color: var(--accent); }
/* The same control cutting by PERSON rather than by type — whose photos are on
   the wall. A name is not a category: uppercased it reads as shouting, and the
   wide tracking a set of short category words wants is too airy under a name.
   So this one keeps the site's lowercase voice. */
.lens-cat-wrap.who { text-transform: lowercase; letter-spacing: 0.06em; }
/* masked rather than a background image, so the chevron takes the text's colour
   and the two change together on hover */
.lens-cat-wrap::after {
  content: ""; flex: none; width: 12px; height: 12px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat center / 12px 12px;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat center / 12px 12px;
}
/* invisible, exactly covering the label — the whole word-plus-arrow is the
   target, and nothing wider */
.lens-cat {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer; font: inherit; color-scheme: dark;
  appearance: none; -webkit-appearance: none; border: 0; padding: 0;
}
.gear-btn {
  display: grid; place-items: center; width: 36px; height: 36px; flex: none;
  background: none; border: 1px solid var(--line-strong); border-radius: 50%;
  color: var(--ink-soft); cursor: pointer; transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.gear-btn svg { width: 19px; height: 19px; }
.gear-btn:hover { color: var(--ink); border-color: var(--ink-soft); }
.gear-btn.active { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }
/* THE VIEW OPTIONS AS A MENU HANGING OFF THE GEAR. They were a drawer inside
   the bar, and opening one pushed the pills — and the whole grid under them —
   down the page: reaching for the control moved the thing the control is about.
   A floating box moves nothing. Chrome is the quest row's settings menu
   (quests.html .qq-menu), so the site has one shape for "a small menu off a
   button": surface, one hard border, a shadow to lift it clear.
   `visibility` rather than `display` so it can fade, and so a shut menu is out
   of the tab order instead of being invisible-but-focusable. */
.disp-pop {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 30;
  display: flex; flex-direction: column; align-items: stretch; gap: 10px;
  min-width: 172px; padding: 12px;
  background: var(--surface); border: 1px solid var(--line-strong);
  box-shadow: 0 12px 32px -10px rgba(0, 0, 0, 0.6);
  visibility: hidden; opacity: 0; transform: translateY(-4px);
  transition: opacity 0.16s ease, transform 0.16s ease, visibility 0s linear 0.16s;
}
.disp-pop.open { visibility: visible; opacity: 1; transform: none; transition-delay: 0s; }
/* stacked, not the one wide line they were on the bar — a box hanging off a
   36px button has no business being 360px across. Each control fills the width,
   so Grid|List and L|M|S read as two rows of the same ruler. */
.disp-pop .disp-row { border-bottom: 0; padding: 0; flex-direction: column;
  align-items: stretch; gap: 10px; }
.disp-pop .seg { display: flex; }
.disp-pop .seg button { flex: 1; text-align: center; padding: 6px 8px; }
.disp-pop .lens-cat-wrap { justify-content: space-between; }

/* ---------- badge catalog: a ruled lattice ---------- */

.grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  margin-bottom: 96px;
}

/* category section headers within the catalog */
/* type filter under the All Badges tab */
.cat-head {
  display: flex; align-items: baseline; gap: 10px;
  margin: 38px 0 0; padding-bottom: 10px;
}
.cat-head .dot {
  width: 10px; height: 10px; background: var(--c, var(--ink));
  align-self: center; flex: none;
}
.cat-head h3 {
  font-size: 13px; font-weight: 600;
  letter-spacing: 0.12em; text-transform: lowercase;
}
#catalog .grid { margin-bottom: 0; }
#catalog { margin-bottom: 96px; }

.badge-card {
  display: block; color: inherit; text-decoration: none;  /* it's a link now */
  padding: 40px 22px 30px; text-align: center; cursor: pointer;
  background: transparent;
  animation: rise 0.55s both;
}
.badge-card:hover, .badge-card:focus-visible { outline: none; }
/* the Working lens renders badge-cards as popover buttons — strip ALL native
   button chrome (macOS draws a bezel that border:0 alone leaves) so they're
   pixel-identical to the All grid's link cards */
button.badge-card {
  width: 100%; border: 0; font: inherit; background: none;
  -webkit-appearance: none; appearance: none;
}
button.badge-card:focus, button.badge-card:focus-visible { outline: none; }
@keyframes rise { from { opacity: 0; transform: translateY(12px); } }

/* the medallion coin — eases up a touch on hover */
.coin { width: 128px; aspect-ratio: 1; margin: 0 auto 20px; }
.coin img, .coin svg {
  display: block;   /* kill inline descender gap so the coin box stays square */
  width: 100%; height: 100%; border-radius: 50%; object-fit: cover;
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.badge-card:hover .coin img, .badge-card:hover .coin svg,
.badge-card:focus-visible .coin img, .badge-card:focus-visible .coin svg {
  transform: scale(1.1);
}

/* catalog cards: the coin sits in a wrap so the steps pill can ride its edge;
   on the big 128px coin the pill tucks onto the circle, not the bounding box */
.badge-card .coin-wrap { margin: 0 auto 20px; }
.badge-card .coin-wrap .coin { margin: 0; }
.badge-card .steps-pill { right: 2px; bottom: 6px; font-size: 11.5px; }

.badge-card h3 {
  font-weight: 600; font-size: 19px;
  letter-spacing: 0.01em; margin-bottom: 10px;
  transition: color 0.18s;
}
.badge-card:hover h3, .badge-card:focus-visible h3 { color: var(--accent); }
.chip {
  display: inline-flex; align-items: center; gap: 8px;
  color: var(--ink-soft);
}
.chip .dot { width: 9px; height: 9px; border-radius: 0; background: var(--c, var(--ink)); box-shadow: none; }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px 18px; }
.chip.skill { color: var(--leaf); }

/* ---------- catalog tabs (tracks is the default lens) ---------- */

.section-head .tab {
  font: inherit; font-weight: 600; font-size: 28px;
  display: inline-flex; align-items: center; gap: 5px;   /* disc rides tight + centered */
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--ink-soft); opacity: 0.45;
}
.section-head .tab.active { color: var(--ink); opacity: 1; }
.section-head .tab:not(.active):hover { opacity: 0.75; }
/* a section's sub-tabs are links (real pages), the studio's are buttons — one
   control, either element */
.section-head a.tab { text-decoration: none; }
.sub-tabs { gap: 20px; }
/* It used to pull IN 2px: a gap that reads as snug beside 13px type reads as a
   gap beside 28, so the small disc was tucked against the word. The disc is
   22px here now, heavy enough to crowd the last letter at that distance — so it
   gives the 2px back and stands a hair clear. */
.sub-tabs .tab .nc { margin-left: 2px; }
/* the shell-level bar that carries them: a sibling of the header, so the router
   swapping <main> leaves it standing (app.js renderSubnav) */
nav.subnav .section-head { margin: 26px 0 0; }
nav.subnav[hidden] { display: none; }
/* Its height is RESERVED before JS builds it — the same reason quicknav ships
   empty in every page's markup. app.js runs at the end of <body>, so the bar
   arrives after first paint, and without this every badge page dropped its
   whole content 84px the moment it appeared. The class is set by a one-line
   head script from the cached token, so a signed-OUT visitor reserves nothing.
   84px is the filled bar measured: 26 margin + 43 line + 14 padding + 1 border. */
html.has-subnav nav.subnav { min-height: 84px; }
/* the FIRST title on a page carries no drop of its own — <main> owns that now,
   so a page with a title and a page without one start at the same height.
   It caps up — same sentence-case treatment as the form labels, so the page's
   own name reads like a title rather than a whisper. Section titles further
   down the page stay lowercase; only the head of the page is promoted. */
.section-head.page-head { margin-top: 0; }
.section-head.page-head h2::first-letter { text-transform: uppercase; }
/* count pills tuck in close to the title they belong to, centered vertically */
.section-head > .count { align-self: center; margin-left: -6px; }

/* ---------- tracks: ladders of levels ---------- */

.track { margin-bottom: 64px; }
.track-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 6px; }
.track-head h3 { font-weight: 600; font-size: 22px; }
.track-desc { color: var(--ink-soft); font-size: 15px; max-width: 62ch; margin-bottom: 14px; }
/* The ladder's own header: a full-width accent band carrying how long the track
   is on the left and what you can do about it on the right. It sits flush on
   the first level, so the track reads as one stack from this bar down.
   `margin-right: auto` on the count rather than `justify-content:
   space-between`, because either side can be absent — a one-level track has no
   count, and signed out there is no control — and this keeps each on its own
   side in all three cases. */
.track-bar {
  display: flex; align-items: center; justify-content: flex-end;
  gap: 12px; padding: 9px 14px;
  background: var(--accent); color: var(--on-accent);
}
/* the count is the bar now, so it drops its own pill and just sets the type */
.track-bar > .count {
  margin-right: auto; background: none; padding: 0; color: inherit;
}
/* dark ink on the accent, inverting to the band's own colour on hover */
.track-bar .btn-sm {
  color: var(--on-accent); border-color: var(--on-accent); background: transparent;
}
.track-bar .btn-sm:hover { background: var(--on-accent); color: var(--accent); }
/* Remove is destructive, but a clay outline ON the accent band is a warning
   stripe drawn on a warning stripe — it just reads as noise. At rest it wears
   the band's own ink, exactly like start; the clay arrives on hover, where the
   intent is. (Beats the later `.btn-sm.danger` on specificity, not order.) */
.track-bar .btn-sm.danger { color: var(--on-accent); border-color: var(--on-accent); }
.track-bar .when, .track-bar .trk-done { color: var(--on-accent); }

.level { border: 1px solid var(--line); border-bottom: 0; }
.level:last-child { border-bottom: 1px solid var(--line); }

/* level rung = a dark filled bar (amber-tinted on the current level), matching
   the sim's ladder. The disc is a solid accent circle holding the number, or a
   check when the level is complete. */
.level-head {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 14px; border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.level.current .level-head { background: rgba(240, 149, 74, 0.16); }
.level-head h4 { margin: 0; font-weight: 600; font-size: 16px; color: var(--ink-faint); }
.level.current .level-head h4, .level.complete .level-head h4 { color: var(--ink); }
.lvl-n {
  width: 34px; height: 34px; flex: none; display: grid; place-items: center;
  background: var(--accent); color: var(--on-accent); border-radius: 50%;
  font-size: 14px; font-weight: 700;
}

/* the level's badges are a uniform bd-coll; this is just its padded frame */
.level-badges { padding: 6px 16px 12px; }
/* A row rule separates one badge from the NEXT one, so the last in a level has
   nothing to separate itself from — the level's own edge (or the next level's
   head) is already the line there, and a rule just above it read as a double.
   Catches the single-badge level too, where the only row is also the last. */
.level-badges .bd-coll.list .bd-item:last-child { border-bottom: 0; }
.track-bar .trk-done { color: var(--accent); font-weight: 600; }   /* Earned lens: "✓ complete" */

/* rule groups inside a level (Eagle-grade tracks): "pick 1 · …", "3 of
   life-skills · …" sub-heads; groups after the first get a dashed rule */
.rule-head {
  padding: 12px 18px 0; font-size: 11.5px; letter-spacing: 0.12em;
  text-transform: lowercase; font-weight: 600; color: var(--ink-soft);
}
.rule-head.done { color: var(--leaf); }
.rule-head + .level-badges { padding-top: 8px; }
.level-badges + .rule-head { border-top: 1px dashed var(--line); margin-top: 2px; }

/* the "+ add badge" elective tile */
.coin-badge.add-badge .coin-sm.add {
  display: grid; place-items: center; box-shadow: none;
  border: 2px dashed var(--line-strong); background: transparent;
  font-size: 30px; font-weight: 400; color: var(--ink-soft); line-height: 1;
}
.coin-badge.add-badge .lbl { color: var(--ink-soft); }
.coin-badge.add-badge:hover .coin-sm.add { border-color: var(--accent); color: var(--accent); }

/* empty elective slot — quota you haven't picked for yet */
.coin-badge.placeholder { cursor: default; }
.coin-badge.placeholder .coin-sm.slot {
  box-shadow: none; background: transparent;
  border: 2px dashed var(--line-strong);
  display: grid; place-items: center;
  color: rgba(236, 228, 212, 0.22); font-size: 22px; line-height: 1;
}

/* elective picker modal */
.picker-scrim {
  position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, 0.62);
  display: flex; align-items: flex-start; justify-content: center;
  /* 48px clears the status bar on most phones and NOT on a tall one, where the
     inset alone is 59 — so the dialog's own head went under the island */
  padding: max(48px, calc(env(safe-area-inset-top) + 16px)) max(16px, env(safe-area-inset-right))
           max(48px, calc(env(safe-area-inset-bottom) + 16px)) max(16px, env(safe-area-inset-left));
  overflow: auto;
}
.picker {
  width: 100%; max-width: 460px; max-height: min(70vh, 620px);
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--line-strong);
}
.picker-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--line);
}
.picker-head h2 { margin: 0; font-size: 16px; font-weight: 600; text-transform: lowercase; }
.picker-x { background: none; border: 0; color: var(--ink-soft); font-size: 20px;
  line-height: 1; cursor: pointer; padding: 2px 6px; }
.picker-x:hover { color: var(--ink); }
.picker-search {
  margin: 12px 16px 4px; padding: 10px 12px;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  color: var(--ink); font: inherit;
}
.picker-list { overflow-y: auto; padding: 8px 8px 12px; }
.picker-row {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  background: none; border: 0; cursor: pointer; padding: 9px 10px;
  color: inherit; font: inherit;
}
.picker-row:hover { background: var(--surface-2); }
.picker-row .coin-sm { display: block; width: 44px; aspect-ratio: 1; flex: none; }
.picker-row .picker-info { flex: 1; min-width: 0; text-align: left; }
.picker-row b { display: block; font-size: 15px; font-weight: 600; }
.picker-row .when { font-size: 12px; }
/* already sitting in a slot — visible but not double-addable */
.picker-row.placed { opacity: 0.4; cursor: default; }
.picker-row.placed:hover { background: none; }
/* one badge item — coin with the title UNDER it; a steps pill rides the coin's
   bottom-right corner (total steps / "current of total" / a check when earned).
   Used in the badges row AND the track ladder, selectable in the Progress tab. */
.coin-badge {
  display: inline-flex; flex-direction: column; align-items: center; gap: 9px;
  width: 96px; flex: none;
  color: inherit; text-decoration: none; font-family: inherit;
  padding: 4px 0 7px; background: none; border: 0; cursor: pointer;
  border-bottom: 2px solid transparent;   /* reserved so selecting doesn't shift */
}
.coin-badge.sel { border-bottom-color: var(--accent); }
.coin-badge .coin-sm { display: block; width: 64px; aspect-ratio: 1; margin: 0; }
/* the wrap + pill work on ANY coin (ladder items and catalog cards alike) */
.coin-wrap { position: relative; display: inline-block; z-index: 0; }
/* hover: a soft sunlight glow blooms behind the coin */
.coin-wrap::before {
  content: ""; position: absolute; inset: -16px; z-index: -1;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(240, 149, 74, 0.24), transparent 70%);
  opacity: 0; transition: opacity 0.2s;
}
.coin-badge:hover .coin-wrap::before,
.badge-card:hover .coin-wrap::before,
.badge-card:focus-visible .coin-wrap::before { opacity: 1; }
.steps-pill {
  position: absolute; right: -10px; bottom: -3px;
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.01em; line-height: 1.2;
  color: var(--ink); background: #26221c; border: 1px solid var(--line-strong);
  border-radius: 999px; padding: 2px 8px;
}
.steps-pill.started { background: var(--leaf); border-color: var(--leaf); color: #14200b; }   /* in progress */
.steps-pill.earned { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }   /* finished */
.coin-badge .lbl {
  font-size: 13px; line-height: 1.25; text-align: center; white-space: normal;
  transition: color 0.18s;
}
.coin-badge:hover .lbl { color: var(--accent); }
.coin-badge.sel .lbl { color: var(--accent); font-weight: 600; }

/* not earned yet: grayscale art inside a hairline ring */
.coin.ghost img, .coin.ghost svg { filter: grayscale(1); opacity: 0.35; }

/* every coin wears a ring — hairline by default, GREEN once started, accent
   (orange) when earned. Matches the sim's 1px line / 2px accent borders. */
.coin, .coin-sm, .coin-lg { border-radius: 50%; box-shadow: 0 0 0 1.5px var(--line-strong); }
.coin.started, .coin-sm.started { box-shadow: 0 0 0 2px var(--leaf); }
.coin.done, .coin-sm.done, .coin-lg.done { box-shadow: 0 0 0 2px var(--accent); }

/* ---- working-on row: the started-badges queue (Tracks-ladder badge style,
   scrolling horizontally). Tap a badge to open its next step underneath. */
.work-row {
  display: flex; gap: 2px 20px; overflow-x: auto;
  padding: 10px 2px 8px; scrollbar-width: thin;
}
.work-next {
  border: 1px solid var(--line); background: var(--surface);
  padding: 14px 16px; margin: 0 0 10px; display: none;
}
.work-next.open { display: flex; flex-direction: column; }   /* body scrolls, footer sits */
.work-next h4 { font-size: 16px; font-weight: 600; text-transform: capitalize; }
.work-next .inst { color: var(--ink-soft); font-size: 14.5px; margin-top: 8px; }
.work-next .inst p { margin: 4px 0; }
.work-empty { color: var(--ink-faint); font-size: 14px; padding: 4px 0 12px; }

/* account-page progress sections (account.html) + catalog lenses: section headers in
   the progress area lose their underline; a single divider sits between blocks. */
/* one consistent gap below the tab line for every catalog lens */
#view-tracks, #view-all { padding-top: 44px; }
#my-progress .section-head { border-bottom: 0; padding-bottom: 4px; margin-top: 40px; }
#my-progress .section-head h2 { text-transform: none; }   /* "Badges" / "Tracks", not lowercased */
.prog-divide { border-top: 1px solid var(--line-strong); margin: 44px 0; }
.work-next.open { border-top: 2px solid var(--accent); }

/* the next-step panel floats as a popover anchored to the picked badge — no
   layout shift. JS places it: prefers BELOW the badge, left-aligned; flips
   above (accent edge follows) only when there's more room up there. */
.step-pop {
  --caret-h: 16px;
  position: absolute; z-index: 45;
  width: min(480px, calc(100vw - 32px));
  max-height: min(226px, 37vh);
  margin: 0; padding: 0;   /* the scroller carries the padding; caret needs room outside */
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
}
/* the state bar: the panel's title, on the fill, meeting the caret that points
   back at the badge. Stays put while the body scrolls under it. */
.step-pop .pop-head {
  flex: none; display: flex; align-items: center; min-height: 40px;
  background: var(--accent); color: var(--on-accent);
  padding: 0 16px; font-size: 12.5px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: capitalize;
}
.step-pop .pop-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 14px 16px; }
/* the footer is the way out, and the whole bar IS the link — a target this
   wide shouldn't make anybody aim at the four words inside it. Filled to match
   the state bar up top, so the panel reads as one object between two ends. */
.step-pop .pop-foot {
  flex: none; display: flex; align-items: center; justify-content: flex-end;
  min-height: 40px; line-height: 1;
  background: var(--accent); color: var(--on-accent); text-decoration: none;
  padding: 0 16px; font-size: 15px; font-weight: 600; letter-spacing: 0.06em;
  text-transform: capitalize; white-space: nowrap;
}
.step-pop .pop-foot:hover { background: var(--accent-bright); }
.step-pop .pop-foot .arr { font-weight: 400; }
.step-pop.up {
  border-top: 1px solid var(--line);
  border-bottom: 2px solid var(--accent);
}
/* a wide, short triangle in the gap between badge and panel, pointing at the
   badge that opened it — coin-wide, centred on the coin (JS sets x + width) */
.step-pop .pop-caret {
  position: absolute; left: var(--caret-x, 48px); top: calc(-1 * var(--caret-h));
  width: var(--caret-w, 72px); height: var(--caret-h);
  transform: translateX(-50%);
  background: var(--accent);
  clip-path: polygon(0 100%, 50% 0, 100% 100%);
}
.step-pop.up .pop-caret {   /* flipped above the badge: the caret points down */
  top: auto; bottom: calc(-1 * var(--caret-h));
  clip-path: polygon(0 0, 50% 100%, 100% 0);
}
.pop-menu-btn {
  background: none; border: 0; cursor: pointer; padding: 0 4px;
  color: var(--ink-faint); font-size: 20px; line-height: 1;
}
.pop-menu-btn:hover { color: var(--ink); }
.pop-menu {
  position: absolute; left: 0; bottom: 30px; z-index: 1; min-width: 190px;
  background: var(--surface-2); border: 1px solid var(--line-strong);
}
.pop-menu button {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: none; border: 0; color: var(--clay); font: inherit;
  font-size: 13.5px; text-transform: lowercase; padding: 10px 14px;
}
.pop-menu button:hover { background: var(--surface); }

/* Quest groups — "time sensitive" then "anytime". A quiet rule-and-label, not
   a second heading: the page already has one, and these are a sorting aid, not
   a new level of hierarchy. */
.q-group { margin-top: 20px; }
.q-group + .q-group { margin-top: 30px; }
.q-group-head {
  display: flex; align-items: center; gap: 10px;
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 600;
  padding-bottom: 8px; margin-bottom: 14px;
  border-bottom: 1px solid var(--line-soft);
}
.q-group-head span { color: var(--ink-dim); letter-spacing: 0.08em; }

/* THE quest row — the one way quests render (my spark + completed page).
   Title pops; the meta text sits back small and gray. */
.quest-row {
  display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap;
  border: 1px solid var(--line); background: var(--surface);
  padding: 12px 14px; margin-bottom: 16px;
}
.quest-row h3 { margin: 0; font-size: 16.5px; font-weight: 600; }
.quest-row .when { font-size: 11.5px; color: var(--ink-faint); letter-spacing: 0.1em; }
.quest-row .q-main { flex: 1; min-width: 0; }
.quest-row .q-main h3 { margin-right: 10px; }
.quest-row .q-detail { color: var(--ink-dim); font-size: 13.5px; margin-top: 2px; }
/* points + mark-done stack at the right: +N chip on top, "mark done" under it */
.quest-row .q-side {
  margin-left: auto; flex: none;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
}
/* the due sits right under the title; a real date (proper-case month), the
   date itself in accent so it stands out */
.quest-row .q-due { margin-top: 3px; text-transform: none; letter-spacing: normal; }
.quest-row .q-date { color: var(--accent); }
/* overdue: the date (and the "overdue" tag) go red instead of accent */
.quest-row .q-due.overdue .q-date, .quest-row .q-late { color: var(--clay); }

/* ---------- skill meters (account) ----------
   The cross-badge totals. A meter of discrete blocks was the first idea and
   doesn't survive the real catalog: the skills range from 2 badges to 35, and
   35 squares don't fit a phone next to a name. A track and a fill do, at any
   denominator. Green, because that's the colour the "+1 nature" chips on the
   badge pages already speak in. */

.skill-meters { border: 1px solid var(--line); border-bottom: 0; margin-top: 14px; }
.skill-row {
  display: flex; align-items: center; gap: 16px;
  padding: 11px 14px; border-bottom: 1px solid var(--line);
}
.skill-name {
  width: 108px; flex: none; font-weight: 600; font-size: 14px;
  letter-spacing: 0.06em; text-transform: lowercase;
}
.skill-bar { flex: 1; min-width: 40px; height: 10px; background: var(--surface-2); }
.skill-bar i { display: block; height: 100%; background: var(--leaf); }
.skill-pts {
  flex: none; font-weight: 600; font-size: 14px;
  color: var(--leaf); font-variant-numeric: tabular-nums;
}
/* the denominator is context, not a score — it stays quiet next to the point */
.skill-pts em { font-style: normal; font-weight: 400; color: var(--ink-faint); }
/* nothing earned yet: still listed (it's the map of what's there to build),
   but it shouldn't read as loudly as a skill you've actually moved */
.skill-row.zero .skill-name { font-weight: 500; color: var(--ink-faint); }
.skill-row.zero .skill-pts { color: var(--ink-faint); }
@media (max-width: 480px) {
  .skill-row { gap: 12px; padding: 10px 12px; }
  /* wide enough for "getting started" on one line — the longest name in the
     starter pack, and a wrapped one makes its row taller than the rest */
  .skill-name { width: 100px; font-size: 13px; letter-spacing: 0.02em; }
}

/* ---------- badge detail overlay ---------- */

.scrim {
  position: fixed; inset: 0; z-index: 40; display: none;
  background: rgba(0, 0, 0, 0.62);
}
.scrim.open {
  display: grid; place-items: center; animation: fade 0.2s both;
  /* keep the centred modal clear of the island + home indicator on tall phones */
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
}
@keyframes fade { from { opacity: 0; } }

.detail {
  width: min(660px, calc(100vw - 32px)); max-height: 86vh; overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: 0;
  padding: 44px 44px 36px;
  animation: rise 0.3s both;
}
.detail .top { display: flex; gap: 28px; align-items: center; margin-bottom: 8px; }
.detail .coin-lg { width: 116px; aspect-ratio: 1; flex: none; }
.detail .coin-lg svg { width: 100%; height: 100%; }
.detail h2 {
  font-weight: 600; font-size: 36px;
  letter-spacing: -0.01em; line-height: 1.05;
}
.detail .summary { color: var(--ink-soft); margin-top: 6px; }
.detail .desc { margin: 20px 0 6px; color: var(--ink); }
.detail .desc p { margin: 10px 0; }
.detail .close {
  position: sticky; top: 0; float: right; margin: -24px -24px 0 0;
  background: var(--surface); border: 1px solid var(--line-strong); color: var(--ink);
  width: 34px; height: 34px; border-radius: 0; cursor: pointer; font-size: 16px;
}
.detail .close:hover { background: var(--ink); color: var(--paper); }

/* requirement steps */
.reqs { margin-top: 26px; }
.reqs-h { color: var(--ink); border-top: 1px solid var(--line-strong); padding-top: 14px; margin-bottom: 16px; }
.step { display: flex; gap: 18px; position: relative; padding-bottom: 24px; }
.step:last-child { padding-bottom: 4px; }
.step::before {
  content: ""; position: absolute; left: 15px; top: 34px; bottom: 0;
  width: 1px; background: var(--line);
}
.step:last-child::before { display: none; }
/* numbered discs — the wayfinding marker: a solid accent circle with a dark
   glyph. High contrast so the eye finds the start of each item. One treatment
   everywhere (steps, levels, working-on). Pending is a muted ochre ring. */
.step .n {
  flex: none; width: 31px; height: 31px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 14px; font-weight: 700;
  color: var(--on-accent); background: var(--accent); border: 0;
}
.step .n.pend { color: var(--ochre); background: transparent; border: 1.5px dashed var(--ochre); }
.step .n.done { background: var(--leaf); color: #14200b; }   /* approved */
.step h4 { font-size: 17px; font-weight: 600; text-transform: lowercase; }
.step .meta { margin-top: 4px; }
.step .inst { color: var(--ink-soft); font-size: 15.5px; margin-top: 6px; }
.step .inst p { margin: 4px 0; }

/* ---------- sub-requirements + resource links ----------
   The (a)/(b)/(c) items of a step share the step's one box and are ruled
   apart — the card is the step, the rules are its parts. One indent level
   covers the (b) -> (1)(2) nesting real merit badges use. Shared by the badge
   page, the next-step popover, and the builder's live preview. */
/* the rule goes BETWEEN items, not above the first: a step whose prose body is
   empty would otherwise open with a stray line just inside its box */
.sub { display: flex; gap: 10px; margin-top: 10px; }
.sub + .sub { border-top: 1px solid var(--line); padding-top: 10px; }
/* ...and not between a parent and the child tucked under it — the indent
   already says they belong together. Two indented siblings still get one. */
.sub:not(.ind) + .sub.ind { border-top: 0; padding-top: 0; margin-top: 6px; }
.sub.ind { margin-left: 22px; }
.sub .lab { flex: none; color: var(--accent); font-weight: 600; font-size: 14.5px; }
.sub .sub-text { min-width: 0; flex: 1; color: var(--ink-soft); font-size: 15.5px; }
.sub .sub-text > :first-child { margin-top: 0; }
.sub .sub-text p { margin: 4px 0; }

/* links get their own block at the foot of whatever owns them — an item, or
   the step itself. Tinted and rule-marked so they read as reference material
   rather than more requirement text. */
.res {
  display: flex; flex-direction: column; gap: 5px;
  margin-top: 10px; padding: 9px 11px;
  background: var(--surface-2); border-left: 2px solid var(--brick);
}
.res a { width: fit-content; color: var(--brick); font-size: 13.5px; text-decoration: none; }
.res a:hover { text-decoration: underline; }

/* ---------- badge landing page ---------- */

.badge-page { max-width: var(--measure-read); margin: 0 auto; animation: rise 0.3s both; }
/* One back-link voice everywhere (badge page, completed quests, review), and
   that voice now matches the step popover's "Open Badge" pointing the other
   way: capped, a size up, accent as INK, underlined on hover. */
.back-link {
  display: inline-block; margin-bottom: 26px;
  font-size: 15px; letter-spacing: 0.06em; text-transform: capitalize;
  font-weight: 600; color: var(--accent); text-decoration: none;
}
.back-link:hover { text-decoration: underline; }
.badge-page .top { display: flex; gap: 28px; align-items: center; margin-bottom: 8px; }
.badge-page .coin-lg { width: 132px; aspect-ratio: 1; flex: none; }
.badge-page .coin-lg img, .badge-page .coin-lg svg {
  width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block;
}
.badge-page h1 {
  font-weight: 600; font-size: 42px;
  letter-spacing: -0.01em; line-height: 1.05;
}
.badge-page .summary { color: var(--ink-soft); margin-top: 6px; font-size: 17px; }
/* Start / active / "complete X first" rides with the title now. It is the
   answer to "can I do this?", which belongs next to the badge being asked
   about rather than a scroll below it. */
.badge-page .cta { margin-top: 18px; }
/* What the badge is and what it feeds, in a band of its own under the coin.
   Tucked under the summary these read as a tail on the sentence; given a row
   they read as what they are — the badge's standing facts.

   The row wears its CATEGORY: --cat is the ramp colour, --cat-tint the same
   hex at 18% for the fill (both set inline per badge — a CSS var can't be
   picked from a class the catalog invents). That retires the little colour
   dot the category chip used to carry: the whole row is the dot now. */
.badge-page .grants {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 18px;
  margin-top: 26px; padding: 13px 16px;
  background: var(--cat-tint); border: 1px solid var(--cat);
}
.badge-page .grants .cat, .badge-page .grants .grant {
  font-size: 13px; letter-spacing: 0.1em; text-transform: lowercase;
}
.badge-page .grants .cat { font-weight: 600; color: var(--cat); }
.badge-page .grants .grant { font-weight: 500; color: var(--ink-dim); }
/* PHONE: the whole row on one line — category plus all three grants, which is
   as many as any badge in the catalog carries. The lever is the letterspacing,
   not the type size: 0.1em is a lot of air to give away four times over, and
   dropping it buys more room than shrinking 13px type would, without making
   the row smaller to read. Gap and side padding give up a few more.
   Checked by measuring all 46 badges, not the one that happened to be open —
   the widest is Star Gazer (outdoors · Navigation · Nature · Thinking), which
   clears by 31px at 393 and 13px at 375. A future badge tagged with a name as
   long as "Getting Started" alongside two others would be the first to wrap. */
@media (max-width: 480px) {
  .badge-page .grants { gap: 8px 12px; padding: 12px 13px; }
  .badge-page .grants .cat, .badge-page .grants .grant { letter-spacing: 0.02em; }
}
.badge-page .desc { margin: 24px 0 6px; color: var(--ink); font-size: 16.5px; }
.badge-page .desc p { margin: 10px 0; }
/* the requirement's text sits in a card, same box as a quest row; the numbered
   disc stays outside it in the left gutter */
.badge-page .step-body {
  min-width: 0; flex: 1;
  border: 1px solid var(--line); background: var(--surface); padding: 12px 14px;
}
.badge-page .empty { margin: 120px 0; text-align: center; font-size: 15px; }
.badge-page .empty a { color: var(--brick); }
/* the CTA row runs at ONE size — the inline .btn.ghost size. Left alone, the
   .btn primaries ("active", "start this badge") come in at their form size and
   tower over "remove from queue" standing next to them. */
.badge-page .cta .btn { width: auto; padding: 8px 18px; }

/* PHONE: the coin takes a line of its own, centred, and the title block drops
   underneath at full width — ranged left, NOT centred along with the coin.
   Side by side inside a 390px phone the two of them leave a 42px h1 about
   170px to live in, which is where the title was breaking up. */
@media (max-width: 720px) {
  .badge-page .top { flex-direction: column; align-items: stretch; gap: 18px; }
  /* on its own line the coin can be the picture it is: three quarters of the
     column on a phone. The cap keeps a tablet at the top of this range from
     opening on a 500px disc — 300px is still 3/4 of a large phone. */
  .badge-page .coin-lg { align-self: center; width: min(75%, 300px); }
}

/* step media — the same full-width blocks the spark renders in its column */
.media { margin: 16px 0 6px; }
.media img, .media video { display: block; width: 100%; border: 1px solid var(--line-strong); }
.media .chart { display: block; width: 100%; border: 1px solid var(--line); }
.media .cap {
  margin-top: 8px; font-size: 12px; letter-spacing: 0.12em;
  text-transform: lowercase; color: var(--ink-soft);
}
/* knowledge check — questions to ask aloud; answer + why behind a tap */
.quiz { margin-top: 12px; border-top: 1px solid var(--line); }
.quiz .qrule { font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase; color: var(--ink-soft); padding: 10px 0 4px; }
.quiz .qq { border-bottom: 1px solid var(--line); padding: 8px 0; }
.quiz .qq summary { cursor: pointer; list-style: none; display: flex; gap: 10px; align-items: baseline; font-size: 15px; }
.quiz .qq summary::-webkit-details-marker { display: none; }
.quiz .qn { flex: none; font-size: 12px; color: var(--brick); font-weight: 600; min-width: 16px; }
.quiz .qq ol { margin: 8px 0 0 26px; padding-left: 18px; font-size: 14.5px; color: var(--ink-soft); }
.quiz .qq li { padding: 2px 0; }
.quiz .qq li.right { color: var(--leaf); font-weight: 600; }
.quiz .why { margin: 8px 0 2px 26px; font-size: 14px; color: var(--ink); border-left: 2px solid var(--leaf); padding-left: 10px; }
/* what a step needs in your hands. A quantity column that lines up, because
   the whole point of this list is being read down at a lumber rack. */
.mats { margin-top: 12px; border-top: 1px solid var(--line); }
.mats-h {
  font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase;
  color: var(--ink-soft); padding: 10px 0 4px;
}
/* three columns: tick, quantity, thing. The quantity is right-set against the
   name so a column of them reads as a column. */
.mat {
  display: grid; grid-template-columns: 16px 3.4em minmax(0, 1fr); align-items: start;
  gap: 0 9px; padding: 5px 0; border-bottom: 1px solid var(--line);
  font-size: 14.5px; cursor: pointer;
}
.mat:last-child { border-bottom: 0; }
.mat input {
  grid-column: 1; width: 15px; height: 15px; margin: 2px 0 0;
  accent-color: var(--accent); cursor: pointer;
}
.mat .q { grid-column: 2; justify-self: end; color: var(--accent); font-weight: 600; white-space: nowrap; }
.mat .it { grid-column: 3; color: var(--ink); }
/* the whole-badge roll-up isn't tickable — no tick column to leave empty */
.shop .mat { grid-template-columns: 3.4em minmax(0, 1fr); cursor: default; }
.shop .mat .q { grid-column: 1; }
.shop .mat .it { grid-column: 2; }
.mat .note { color: var(--ink-soft); font-size: 13px; }
.mat.got .it, .mat.got .q { color: var(--ink-faint); text-decoration: line-through; }
/* the same lines rolled up for the store run */
.shop { margin: 18px 0 6px; border: 1px solid var(--line-strong); background: var(--surface); }
.shop summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px;
  padding: 11px 14px; font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase;
  color: var(--ink-soft);
}
.shop summary::-webkit-details-marker { display: none; }
.shop[open] summary { border-bottom: 1px solid var(--line); color: var(--ink); }
.shop .mats { margin: 0; border-top: 0; padding: 4px 14px 12px; }
/* inline video — a 16:9 frame in the step's column, captioned like media */
.embed { margin: 14px 0 6px; }
.embed iframe { display: block; width: 100%; aspect-ratio: 16 / 9; border: 1px solid var(--line-strong); background: #000; }
.embed .cap { margin-top: 8px; font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase; color: var(--ink-soft); }
.watch { margin-top: 10px; }
.watch a { color: var(--brick); font-size: 14.5px; text-decoration: none; border-bottom: 1px solid var(--brick); }
.watch a:hover { border-bottom-width: 2px; }

/* ---------- photo wall (badges · photos) -----------------------------------
   Every approved evidence photo, newest first — the album the puck has carried
   since src/photos.c, on a screen with room for it.

   The cells are square because the photos already ARE: app.js evidenceJPEG
   contains anything picked inside a 1024px black square before it is ever
   uploaded. So the grid crops nothing that matters, and the letterbox the
   lightbox shows disappears into its own scrim. */

/* the page carries no title of its own — the sub-bar above it already says
   Photos — so this thin row is the whole header: how many, and whose */
.pw-tools {
  display: flex; align-items: center; justify-content: flex-end; gap: 16px;
  min-height: 20px;
}
.pw-n { font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase;
  color: var(--ink-soft); }

.pw-grid {
  display: grid; gap: 4px; margin-top: 14px;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
}
.pw-cell { position: relative; }
.pw-open {
  display: block; width: 100%; aspect-ratio: 1; padding: 0; overflow: hidden;
  background: #000; border: 1px solid var(--line); cursor: pointer;
  transition: border-color 0.15s;
}
.pw-open img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pw-open:hover { border-color: var(--accent); }
.pw-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* the badge it was earned against, riding the corner — a link while that badge
   is still in the catalog, the same coin standing still once it is retired */
.pw-coin {
  position: absolute; right: 7px; bottom: 7px; z-index: 1;
  display: block; line-height: 0; transition: transform 0.18s;
}
.pw-coin .coin, .lb-what .coin { display: block; margin: 0; flex: none; }
.pw-coin .coin { width: 36px; height: 36px; }
/* the viewer holds ONE photo, and the coin under it is the badge that photo was
   earned against — at full size it reads as the badge itself rather than as a
   bullet beside the words */
.lb-what .coin { width: 120px; height: 120px; }
a.pw-coin:hover { transform: scale(1.12); }
.pw-more { margin: 30px auto 0; display: block; }

/* three across on a phone — the puck's own album grid, and the width a photo
   needs before it stops being a picture and becomes a swatch */
@media (max-width: 720px) {
  .pw-grid { grid-template-columns: repeat(3, 1fr); gap: 3px; }
  .pw-coin { right: 4px; bottom: 4px; }
  .pw-coin .coin { width: 26px; height: 26px; }
}

/* ---- the lightbox: one photo, and what it was for ---- */
.lb {
  position: fixed; inset: 0; z-index: 80; background: rgba(0, 0, 0, 0.92);
  display: grid; grid-template-rows: 1fr auto;
  opacity: 0; transition: opacity 0.2s ease;
}
.lb.on { opacity: 1; }
.lb[hidden] { display: none; }
/* Between photos the picture and its caption cross over rather than cutting:
   .swap is on from the moment a new src is set until it has actually decoded,
   so nothing is ever half-swapped on screen — old caption, new picture. */
.lb-img, .lb-what, .lb-meta { transition: opacity 0.16s ease; }
.lb.swap .lb-img, .lb.swap .lb-what, .lb.swap .lb-meta { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .lb, .lb-img, .lb-what, .lb-meta { transition: none; }
}
/* the page behind it holds still rather than scrolling under the picture */
body.lb-open { overflow: hidden; }
/* flex, not grid: `max-height: 100%` on the picture needs a parent with a
   DEFINITE height, and a grid's auto-sized implicit row is not one — the photo
   rendered at its own size and the bottom of it went under the caption bar. */
.lb-stage { position: relative; display: flex; align-items: center;
  justify-content: center; min-height: 0; overflow: hidden;
  /* the island and the notch are not screen: the photo starts below them */
  padding: calc(14px + env(safe-area-inset-top)) max(14px, env(safe-area-inset-right))
           14px max(14px, env(safe-area-inset-left)); }
.lb-img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }

.lb-x, .lb-nav {
  position: absolute; z-index: 2; display: grid; place-items: center;
  background: rgba(0, 0, 0, 0.5); border: 1px solid var(--line-strong);
  color: var(--ink); cursor: pointer; line-height: 1;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.lb-x:hover, .lb-nav:hover {
  background: var(--accent); color: var(--on-accent); border-color: var(--accent);
}
/* clear of the status bar / Dynamic Island: the close button spent its first
   day on iOS sitting inside it, where the OS gets the tap and you don't */
.lb-x {
  top: calc(12px + env(safe-area-inset-top));
  right: calc(12px + env(safe-area-inset-right));
  width: 40px; height: 40px; font-size: 17px;
}
.lb-nav { top: 50%; transform: translateY(-50%); width: 52px; height: 52px;
  font-size: 30px; padding-bottom: 4px; }
.lb-prev { left: calc(12px + env(safe-area-inset-left)); }
.lb-next { right: calc(12px + env(safe-area-inset-right)); }

.lb-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 18px;
  background: var(--surface); border-top: 1px solid var(--line-strong);
  padding: 12px max(16px, env(safe-area-inset-left))
           max(12px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-right));
}
.lb-what {
  display: flex; align-items: center; gap: 18px; min-width: 0;
  color: inherit; text-decoration: none;
}
/* sized to the coin beside them: 15px words against a 120px badge read as a
   caption that got left behind when the coin grew */
.lb-titles { min-width: 0; }
.lb-titles b { display: block; font-size: 26px; font-weight: 600; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-titles span { display: block; font-size: 16px; color: var(--ink-soft);
  margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
a.lb-what:hover b { color: var(--accent); }
.lb-meta {
  display: flex; align-items: center; gap: 12px; flex: none;
  font-size: 12px; letter-spacing: 0.1em; text-transform: lowercase; color: var(--ink-soft);
}
/* a person keeps their own capital letter, even in a row that lowercases
   everything else about the photo */
.lb-meta .lb-who { color: var(--ink); text-transform: none; }

@media (max-width: 720px) {
  .lb-nav { width: 44px; height: 44px; font-size: 26px; }
  .lb-prev { left: calc(6px + env(safe-area-inset-left)); }
  .lb-next { right: calc(6px + env(safe-area-inset-right)); }
  .lb-meta { gap: 9px; font-size: 11px; }
  /* the coin comes back down to a mark beside the words here, so the words
     come back down with it */
  .lb-what .coin { width: 30px; height: 30px; }
  .lb-titles b { font-size: 15px; }
  .lb-titles span { font-size: 13px; margin-top: 0; }
}

/* ---------- packs ---------- */

.pack-list { border-top: 1px solid var(--line); }
.pack-row {
  display: flex; align-items: center; gap: 22px;
  padding: 18px 4px; border-bottom: 1px solid var(--line);
}
.pack-row .pack-id { flex: 1; min-width: 0; }
.pack-row .pack-id b { display: block; font-weight: 600; text-transform: lowercase; font-size: 17px; }
.btn-sm {
  font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase; font-weight: 600;
  color: var(--ink); text-decoration: none; cursor: pointer;
  border: 1px solid var(--ink); padding: 7px 14px; background: var(--paper);
}
.btn-sm:hover { background: var(--ink); color: var(--paper); }
.btn-sm.danger { color: var(--clay); border-color: var(--clay); }
.btn-sm.danger:hover { background: var(--clay); color: var(--bg); }
/* de-emphasized until you mean it: dark gray at rest, solid accent on hover */
.btn-sm.dim { color: var(--ink-faint); border-color: var(--line-strong); background: transparent; }
.btn-sm.dim:hover { color: var(--on-accent); border-color: var(--accent); background: var(--accent); }
/* the primary "mark done" / claim action — filled accent */
.btn-sm.go { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }
.btn-sm.go:hover { filter: brightness(1.08); }
#import-area { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
#pack-file { font-size: 14px; }

/* ---------- my badges (dashboard) ---------- */

.panel {
  background: transparent;
  border: 1px solid var(--line); border-radius: 0; padding: 34px;
}
.login-panel {
  max-width: 420px; margin: 0 auto; animation: rise 0.5s both;
  background: var(--surface); border-color: var(--line-strong);
}
.login-panel h1 {
  font-weight: 600; font-size: 34px;
  letter-spacing: -0.01em; text-transform: lowercase; margin: 6px 0;
}
.login-panel .sub { color: var(--ink-soft); font-size: 15.5px; margin-bottom: 26px; }
.field { margin-bottom: 18px; }
.field label {
  display: block; font-size: 12px; letter-spacing: 0.12em;
  text-transform: lowercase; font-weight: 500; color: var(--ink);
  margin-bottom: 8px;
}
/* Sentence case, not a shout. The lowercase above stays so a label typed
   "Email" and one typed "email" render identically; ::first-letter then brings
   the initial back up. One rule caps every form label on the site — no retyping,
   and nothing can be missed. Title Case ("Who Gets Review Requests") was the
   other reading and reads clumsy on the long ones. */
.field label::first-letter { text-transform: uppercase; }

/* ---------- settings panels (My Spark) -------------------------------------
   One stack of cards, each a heading plus controls. Everything here used to be
   repeated inline on every panel — the same four declarations copy-pasted a
   dozen times, which is how they drifted. A panel is now `class="panel set"`
   and says nothing about its own padding. */
.panel.set { background: var(--surface); padding: 22px; margin-top: 16px; }
.panel.set h3 { font-size: 18px; font-weight: 600; margin-bottom: 14px; }
.panel.set > .when { margin-bottom: 14px; }
.panel.set > .when:last-of-type { margin: 10px 0 0; }   /* the status line */
/* a row of fields that ends in a Save */
.set-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }
.set-row .field { flex: 1; min-width: 150px; margin: 0; }
.set-row .field.wide { min-width: 200px; }
.set-row .btn { width: auto; }
.panel.set > .btn { width: auto; }
/* checkbox line, and the block that unfolds underneath one */
.set-check { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; cursor: pointer; }
.set-check input { width: auto; margin: 0; }
.set-check .when { margin: 0; }
.set-row + .set-check { margin-top: 14px; }
/* inside a helper line, emphasis is ink — it's a value being shown, not a shout */
.panel.set .when strong { color: var(--ink); }
.set-sub { margin: 0 0 14px 28px; }
.set-sub[hidden] { display: none; }
.set-times { display: flex; gap: 12px; }
.set-times .field { margin: 0; }
.field input {
  width: 100%; background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 0; padding: 12px 14px;
  font-family: var(--font); font-size: 16px;
}
.field input:focus { outline: none; border-color: var(--accent); }
/* A DROPDOWN IS AN INPUT THAT OPENS A LIST, so it wears the input's clothes.
   Left alone it renders as the OS's own widget — a different type size, a
   different height, and on some platforms a light control dropped into a dark
   page. `appearance: none` takes that off; it also takes the arrow, so we draw
   one (the stroke is --ink-faint's value, which a data URI cannot name), and
   `color-scheme` covers the one part still drawn by the OS: the open list.

   This was inline on account.html and build.html and simply missing on
   klic.html — the same drift the panels above were lifted out of. One rule now,
   and the next page with a dropdown gets it for free. */
/* `select.sel` is the same control standing on its own — a dropdown in a toolbar
   row rather than under a label (storage.html's prune row). Opt in by class, the
   way .btn does, rather than by another copy of the rule. */
.field select, select.sel {
  width: 100%; background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 0;
  padding: 12px 40px 12px 14px;
  font-family: var(--font); font-size: 16px; cursor: pointer;
  color-scheme: dark;
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23978d7a' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 13px center;
  transition: border-color .15s;
}
.field select:hover, select.sel:hover { border-color: var(--line-strong); }
.field select:focus, select.sel:focus { outline: none; border-color: var(--accent); }

/* ---------- ticks and dots --------------------------------------------------
   The native checkbox is the OS's: a blue rounded box on a warm dark page, off
   the palette entirely and the last unstyled control in the app. appearance:none
   takes the box and its tick together, so both are drawn here — an accent FILL
   carrying dark ink, which is exactly the relationship .btn already has with the
   page. The tick is sized in % rather than px so the two places that set their
   own box (review.html's 22px queue rows, setup.html's 16px pack picks) get a
   tick in proportion rather than a small mark in a big square.

   A RADIO STAYS ROUND. "Circles are reserved for badges and the spark" is the
   house rule, and this is the one place it gives way: round-vs-square is how a
   control says pick-one-of-these rather than tick-any-of-these, and that reading
   is older and more useful than our geometry. */
input[type="checkbox"], input[type="radio"] {
  appearance: none; -webkit-appearance: none;
  flex: none; width: 18px; height: 18px; margin: 0;
  display: inline-block; position: relative; vertical-align: -3px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: 0; cursor: pointer;
  transition: background-color .15s, border-color .15s;
}
input[type="radio"] { border-radius: 50%; }
/* INSET, not width/height. The mark is a percentage of its box so it stays in
   proportion when a page sets its own size — and a percentage only resolves
   against something definite, which an auto-sized box is not. Positioning it
   against the box makes the box the reference. */
input[type="checkbox"]::before, input[type="radio"]::before {
  content: ""; position: absolute; background: var(--on-accent);
  transform: scale(0); transition: transform .12s ease-out;
}
input[type="checkbox"]::before {
  inset: 18%;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
}
input[type="radio"]::before { inset: 27%; border-radius: 50%; }
input[type="checkbox"]:checked, input[type="radio"]:checked {
  background: var(--accent); border-color: var(--accent);
}
input[type="checkbox"]:checked::before, input[type="radio"]:checked::before { transform: scale(1); }
input[type="checkbox"]:hover:not(:checked), input[type="radio"]:hover:not(:checked) {
  border-color: var(--ink-faint);
}
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled { opacity: .45; cursor: default; }
/* ---------- buttons (the system) -----------------------------------------
   One shape, three intents. Square, lowercase, letterspaced — no shadows.
     .btn            primary call-to-action — the one orange FILL on a view
     .btn.ghost      secondary — neutral outline, fills on hover
     .btn.danger     destructive — clay outline, fills clay on hover
     .btn-sm         compact inline/toolbar button (neutral outline)
     .btn-sm.danger  compact destructive
   Width is full-bleed by default (forms); add .btn.auto or a flex parent for
   inline width. Accent = fill for the primary only; everywhere else we accent
   with an OUTLINE, never an offset shadow. */
.btn {
  font-family: var(--font); font-size: 14px; letter-spacing: 0.12em;
  text-transform: lowercase; font-weight: 600; cursor: pointer;
  display: inline-block; text-align: center; text-decoration: none;
  background: var(--accent); color: var(--on-accent); border: 1px solid var(--accent);
  border-radius: 0; padding: 13px 26px; width: 100%;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.btn:hover { background: var(--accent-bright); border-color: var(--accent-bright); }
.btn.auto { width: auto; }
.btn.ghost { background: transparent; color: var(--ink-soft); border-color: var(--line); width: auto; padding: 8px 18px; }
.btn.ghost:hover { color: var(--paper); background: var(--ink); border-color: var(--ink); }
.btn.danger { background: transparent; color: var(--clay); border-color: var(--clay); width: auto; padding: 8px 18px; }
.btn.danger:hover { background: var(--clay); color: var(--bg); border-color: var(--clay); }
.err { color: var(--clay); margin-top: 12px; min-height: 20px; }

.badges-row {
  display: flex; flex-wrap: wrap;
  margin-bottom: 8px;
}
.earned-card {
  text-align: center; width: 200px; padding: 34px 22px 26px;
  animation: rise 0.55s both;
}
.earned-card .coin { width: 132px; }
.earned-card .coin .face.front svg { filter: none; animation: none; }
.earned-card h3 { font-weight: 600; font-size: 18px; margin-top: 14px; }
.earned-card .when { color: var(--ink-soft); margin-top: 4px; }

.progress-card { margin: 18px 0 0; padding: 26px 28px; display: flex; gap: 24px; align-items: flex-start; }
.ring { flex: none; width: 64px; aspect-ratio: 1; border-radius: 50%; display: grid; place-items: center; position: relative; }
.ring .pct { font-size: 13px; font-weight: 600; color: var(--ink); }
.progress-card h3 { font-weight: 600; font-size: 21px; text-transform: lowercase; }
.steps-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.schip {
  font-size: 12.5px; letter-spacing: 0.06em; text-transform: lowercase; font-weight: 500;
  border-radius: 0; padding: 4px 12px; border: 1px solid var(--line); color: var(--ink-soft);
}
.schip.approved { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }
.schip.pending { color: var(--ochre); border-color: var(--ochre); border-style: dashed; }
.schip.rejected { color: var(--clay); border-color: var(--clay); }

.empty { color: var(--ink-soft); padding: 30px 0 56px; }

/* ---------- the footer IS the mountain ----------
   These were two stacked blocks: a 430px map band, then a separate bordered footer
   below it. Between them sat 72px of footer margin plus 26px/44px of footer padding,
   and the band carried its own 64px above — so the bottom of every page was a run of
   dead space with a thin strip of text stranded in it.
   One element now. The map is the footer, and the two bits of text it used to carry
   sit ON it as matching chips: the survey caption bottom-left, the Klic's name
   bottom-right. Nothing below the mountain but the safe-area inset. */
footer.site {
  margin-top: 64px;   /* the only gap left — page content off the map */
  border-top: 1px solid var(--line-strong);
  /* 430 was sized when this was an exhibit band with a footer under it. As the
     footer itself it only needs to read as terrain behind two chips, so it takes
     less of the screen — and less again on a phone, where 430px was most of a
     viewport spent on decoration. cover + centre keeps the summit framed. */
  height: 300px;
  background-image: url("/topo-bold.svg?v=canopy");
  background-size: cover;   /* the mountain fills the band edge to edge */
  /* summit (image center) vertically centered in the band */
  background-position: center center;
  background-repeat: no-repeat;
  background-color: var(--paper);
  position: relative;
  /* the home indicator sits under the mountain, not under a strip of text */
  margin-bottom: env(safe-area-inset-bottom);
  font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase; font-weight: 500;
}
/* one chip treatment, two corners */
footer.site .caption,
footer.site .wrap {
  position: absolute; bottom: 18px;
  font-size: 12px; letter-spacing: 0.12em; text-transform: lowercase; font-weight: 500;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line-strong);
  padding: 6px 14px;
  width: auto;   /* .wrap is a layout container elsewhere; here it is a chip */
}
footer.site .caption { left: 28px; }
footer.site .wrap    { right: 28px; }
/* an empty .wrap must not draw an empty box (pages that never set the name) */
footer.site .wrap:empty { display: none; }

/* Two chips, opposite corners — until there aren't two corners' worth of room.
   The survey caption alone runs about 440px at this size, so on any phone it
   drove straight through the name chip and the two boxes overlapped. Below this
   they stack on the left edge instead, caption on top, and the caption is
   allowed to wrap rather than run off the screen. Must stay BELOW the header's
   own 720px block, which pins both to the bottom line at equal specificity. */
@media (max-width: 560px) {
  footer.site .caption {
    left: 16px; right: auto; bottom: 56px;
    max-width: calc(100% - 32px);
  }
  footer.site .wrap { left: 16px; right: auto; bottom: 14px; }
}

@media (max-width: 760px) {
  .detail { padding: 30px 24px; }
}

/* ---- notification bell + drawer -------------------------------------------
   The record behind the toasts. Toasts are a glance and are capped at three;
   this is where everything actually lives. */
/* An accent disc with an exclamation in it, sitting between the section pills
   and the burger. It used to be an outline bell wearing a count chip; the disc
   is the same information at a glance and needs no second element to carry it. */
.nav-bell {
  /* the header row's gap is 18; this brings it back to the bar's own 12 */
  flex: none; margin-left: calc(var(--nav-gap, 12px) - var(--row-gap, 18px));
  display: grid; place-items: center;
  width: 26px; height: 26px; padding: 0; border: 0; border-radius: 50%;
  background: var(--accent); color: var(--on-accent);
  font: inherit; font-size: 15px; font-weight: 700; line-height: 1;
  cursor: pointer; transition: filter .15s;
}
.nav-bell:hover { filter: brightness(1.08); }
/* nothing waiting: INVISIBLE, not gone. It stands between two things that would
   both shift sideways if it collapsed. */
.nav-bell.quiet { visibility: hidden; }
/* PHONE: the inline nav is hidden, so the disc takes over its job of claiming
   the free space and the pair travels to the right edge together — the burger
   keeps its own correction, so the 12px between them is the desktop's 12px.
   This block sits HERE, below the rule it overrides: @media adds no specificity,
   and up in the header's own breakpoint it tied with the base and lost on
   source order. */
@media (max-width: 720px) { .nav-bell { margin-left: auto; } }

.notif-scrim { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 95; }
.notif-drawer {
  position: fixed; z-index: 96; top: 0; right: 0; bottom: 0;
  width: min(380px, 100vw); background: var(--bg, #0d0b07);
  border-left: 1px solid var(--line-strong);
  transform: translateX(100%); transition: transform 0.22s ease;
  display: flex; flex-direction: column;
  /* The bottom inset was here; the TOP one was missing, so on an iPhone added to
     the home screen the drawer's header sat under the Dynamic Island and its title
     and buttons were unreachable. viewport-fit=cover is set on every page, which
     means the app owns the full screen INCLUDING the cutout — the insets are not
     optional decoration, they are the only thing keeping content out from under
     the hardware. Right inset too, for landscape on a notched device. */
  padding-top: env(safe-area-inset-top);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
}
.notif-drawer.open { transform: none; }
.notif-head {
  display: flex; align-items: center; gap: 10px;
  padding: 18px 16px; border-bottom: 1px solid var(--line);
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
}
.notif-head strong { flex: 1; font-weight: 600; }
.notif-head button {
  background: none; border: 0; cursor: pointer; color: var(--ink-faint);
  font-size: 11px; letter-spacing: 0.1em; text-transform: lowercase;
}
.notif-head button:hover { color: var(--accent); }
.notif-head .notif-x { font-size: 20px; line-height: 1; padding: 0 4px; }
.notif-list { overflow-y: auto; flex: 1; }
.notif-item {
  display: block; padding: 14px 16px;
  border-bottom: 1px solid var(--line-soft);
  text-decoration: none; color: var(--ink);
}
.notif-item:hover { background: var(--surface); }
/* unread carries the accent bar; read rows recede rather than disappear */
.notif-item.unread { border-left: 2px solid var(--accent); padding-left: 14px; }
.notif-item:not(.unread) .notif-title { color: var(--ink-dim); }
.notif-title { display: block; font-size: 13.5px; font-weight: 600; }
.notif-body { display: block; margin-top: 3px; font-size: 13px; color: var(--ink-dim); }
.notif-when {
  display: block; margin-top: 5px;
  font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-faint);
}
.notif-empty { padding: 30px 16px; text-align: center; color: var(--ink-faint); font-size: 13px; }

/* ---- realtime toasts ------------------------------------------------------
   Announce and get out of the way. These land while someone is mid-task, so
   they sit in a corner, state the fact, and leave — no dimming, no modal, no
   blocking. Urgent ones are the exception: they stay until dismissed. */
.toast-host {
  position: fixed; z-index: 90;
  right: 18px; bottom: calc(18px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column-reverse; gap: 10px;
  max-width: min(360px, calc(100vw - 36px));
  pointer-events: none;   /* the strip never eats clicks; the cards re-enable it */
}
.toast-card {
  position: relative; pointer-events: auto;
  display: block; padding: 13px 38px 13px 15px;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  border-left: 2px solid var(--accent);
  color: var(--ink); text-decoration: none;
  opacity: 0; transform: translateY(6px);
  transition: opacity 0.22s ease, transform 0.22s ease;
}
.toast-card.in { opacity: 1; transform: none; }
.toast-card.urgent { border-left-color: var(--clay, #c2542f); }
a.toast-card:hover { background: var(--surface); border-color: var(--accent-mute); }
.toast-card strong {
  display: block; font-size: 12px; letter-spacing: 0.1em;
  text-transform: lowercase; font-weight: 600;
}
.toast-card span {
  display: block; margin-top: 4px; font-size: 13px;
  color: var(--ink-dim); line-height: 1.35;
}
/* what this card swallowed while it was on screen */
.toast-card .toast-more {
  margin-top: 6px; font-size: 11px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--accent);
}
.toast-x {
  position: absolute; top: 6px; right: 6px;
  width: 26px; height: 26px; line-height: 1;
  background: none; border: 0; cursor: pointer;
  color: var(--ink-faint); font-size: 17px;
}
.toast-x:hover { color: var(--ink); }

@media (max-width: 560px) {
  .toast-host { right: 12px; left: 12px; max-width: none; }
}

/* ---- soft navigation (app.js router): the shell stays, <main> swaps ----
   Out: the RETRACT — children sink and fade bottom-up (the last child leaves
   first), the way the puck pulls a pane back when you step out of settings.
   In: children rise top-down with a small stagger — the same roll the puck
   gives its panes. `backwards`/`forwards` fills so a delayed child holds its
   resting state instead of flashing; no hidden base style, so the
   reduced-motion kill below degrades to content simply appearing. */
body > main.page-out > * { animation: sink .2s ease forwards; }
body > main.page-out > *:nth-last-child(2) { animation-delay: 45ms; }
body > main.page-out > *:nth-last-child(3) { animation-delay: 90ms; }
body > main.page-out > *:nth-last-child(n+4) { animation-delay: 135ms; }
@keyframes sink { to { opacity: 0; transform: translateY(10px); } }

/* One row at a time, down the page. app.js `stagger` picks the rows — a level
   deeper than <main>'s own children where a container holds a list of like
   things — and numbers them in --i; the delay falls out of that. Replaces four
   nth-child rules that could only ever see main's direct children, which is why
   a page that was one grid arrived as a single slab. */
.rise-in {
  animation: rise .42s cubic-bezier(.22, .9, .32, 1) backwards;
  animation-delay: calc(var(--i, 0) * 38ms);
}

/* in-page view swaps (app.js revealView): same motion language as the pages.
   view-fade is opacity-only — for containers with position:fixed children,
   where a transform would hijack their containing block. */
.view-fade { animation: fade .24s ease backwards; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}
