/* == Components ============================================================ *
 * Recipes follow darwin-policy-wizard. The load-bearing conventions:
 *   - INK (--primary-500) is the primary action colour. Purple is the ACCENT and
 *     is reserved for Darwin's commercial CTA, shown only after the user has
 *     received value. The prototype had a single violet doing all nine accent
 *     jobs, which is most of why it read as a prototype.
 *   - Radii are deliberately tight: --radius-md (4px) for buttons and cards.
 *   - Cards are BORDER-ONLY. Selection shifts border-color; never a fill, ring,
 *     or scale.
 *   - Letter-spacing: negative on display type (tighter as it grows), zero on
 *     body, positive only on uppercase micro-labels.
 *   - Transitions name the property that changes. Never `all`. Hover is colour
 *     only — nothing moves.
 *   - Green means COMPLETED, never decoration. The prototype used lime
 *     #86BE0E as an accent, where it reads eco-startup against govtech and
 *     fails contrast at 2.2:1.
 * ========================================================================== */

/* == Topbar ================================================================ *
 * Byte-for-byte the same rules as darwin-policy-wizard/public/index.html
 * lines 60–67 and 148–150. Do NOT "improve" these — the requirement is that a
 * user tabbing between the two products sees no difference except the product
 * name, so any change here has to be made in both repos or not at all. */
.topbar{display:flex;align-items:center;gap:16px;height:60px;padding:0 24px;background:var(--white);border-bottom:1px solid var(--border-subtle);position:sticky;top:0;z-index:50}
.topbar img{height:26px}
.topbar .divider{width:1px;height:24px;background:var(--border-muted)}
.topbar .product{font:600 14px/1 var(--font-title);letter-spacing:-.005em}
.topbar .free{font:600 9px/1.2 var(--font-body);text-transform:uppercase;letter-spacing:.08em;background:var(--info-bg);color:var(--info-text);border:1px solid var(--info-border);padding:3px 8px;border-radius:var(--radius-pill)}
.topbar .right{margin-left:auto;display:flex;align-items:center;gap:16px}
.logo-home{background:none;border:none;padding:0;margin:0;cursor:pointer;display:flex;align-items:center;border-radius:var(--radius-sm)}
.logo-home img{height:26px;display:block}
.logo-home:focus-visible{box-shadow:var(--shadow-focus)}

/* == Buttons =============================================================== */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);border:none;
  border-radius:var(--radius-md);white-space:nowrap;text-decoration:none;
  transition:background var(--dur-med) var(--ease)}
.btn-primary{font:600 14px/1.2 var(--font-body);background:var(--primary-500);color:var(--gray-50);padding:11px 20px}
.btn-primary:hover{background:var(--primary-400)}
.btn-primary:disabled{background:var(--primary-300);cursor:not-allowed}
.btn-secondary{font:600 14px/1.2 var(--font-body);background:var(--white);color:var(--primary-500);
  border:1px solid var(--border-muted);padding:10px 20px}
.btn-secondary:hover{background:var(--gray-100)}
/* Purple. Darwin's own commercial CTA only, and only on a dark surface. */
.btn-accent{font:600 14px/1.2 var(--font-body);background:var(--accent-surface);color:var(--white);padding:11px 20px}
.btn-accent:hover{background:var(--accent-hover);color:var(--white)}
.btn-sm{padding:7px 12px;font-size:12px}
.btn-block{width:100%}

/* == Page shell ============================================================ */
.page{max-width:var(--page-max);margin:0 auto;padding:var(--space-10) var(--space-12)}
.stack > * + *{margin-top:var(--space-6)}

/* == Hero — the signature dark surface with a purple radial bloom ========== *
 * Policy-wizard's most recognisable move (.landing-left, .bridge-card,
 * .oip-hero all share it). Two blooms in opposite corners, one stronger,
 * rgba(140,90,210,…) = --purple-400 at 24–34%. Children need z-index:1. */
.hero{position:relative;overflow:hidden;background:var(--primary-500);color:var(--text-on-dark);
  padding:var(--space-14) var(--space-12)}
.hero::before{content:"";position:absolute;inset:0;pointer-events:none;z-index:0;
  background:radial-gradient(58% 48% at 12% 102%, rgba(140,90,210,.34) 0%, transparent 68%),
             radial-gradient(52% 44% at 102% -2%, rgba(140,90,210,.24) 0%, transparent 66%)}
.hero-inner{position:relative;z-index:1;max-width:var(--page-max);margin:0 auto}
.hero .overline{color:var(--accent-on-dark)}
.hero h1{font:700 36px/1.15 var(--font-title);letter-spacing:-.01em;margin-top:var(--space-3);max-width:20ch}
.hero h1 em{font-style:normal;color:var(--accent-on-dark)}
.hero p{margin-top:var(--space-4);max-width:46ch;color:var(--gray-300);font-size:15px}

/* -- Hero art: the WebGL compass (public/compass.js) ---------------------- *
 * Opt-IN, not opt-out. .hero-art is display:none and .hero-inner stays a
 * single column until app.js adds .with-art, which it does only AFTER the
 * module has actually loaded and only on desktop viewports with motion
 * enabled and WebGL present. Every other path — phone, prefers-reduced-motion,
 * no WebGL, a 404 on the vendored bundle — simply never adds the class, so the
 * hero renders exactly as it did before the compass existed. A CSS-first
 * approach (:has(.hero-art:empty)) would have left a 340px hole whenever the
 * import failed, and needs :has() besides.
 *
 * The art box is sized in the CROSS axis only: clamp() bounds how much height
 * the compass may add to the hero, because everything below it is the URL
 * input this page exists for. Width falls out of the grid track. compass.js
 * reads clientWidth/clientHeight and sets camera.aspect from them, so any box
 * shape is fine — no aspect-ratio needed here.
 *
 * The art track is min(42%, 460px), not a flat 460px: just above the 900px
 * breakpoint a fixed track left the copy column 305px wide, which squeezed the
 * h1 to three lines and well under the 20ch measure it is set to. Proportional
 * up to 1100px (--page-max), then capped, so the copy keeps its measure at
 * every desktop width and the compass stops growing once the page does. */
.hero-art{display:none}
.hero-inner.with-art{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,min(42%,460px));
  align-items:center;gap:var(--space-10)}
.hero-inner.with-art .hero-copy{min-width:0}
.hero-inner.with-art .hero-art{display:block;min-width:0;height:clamp(260px,26vw,340px)}
brass-compass{display:block;position:relative;width:100%;height:100%}

/* == Section header — hairline rule with a short heavy ink tick ============ *
 * From .oip-sechead. Cheap, distinctive, and recurs across policy-wizard's
 * document chrome, which is what makes the two products feel related. */
.sechead{position:relative;display:flex;align-items:baseline;gap:var(--space-3);flex-wrap:wrap;
  padding-bottom:11px;margin-bottom:var(--space-5);border-bottom:1px solid var(--border-subtle)}
.sechead::after{content:"";position:absolute;left:0;bottom:-1px;width:34px;height:2px;background:var(--text-title)}
.sechead h2{font:700 28px/1.2 var(--font-title);letter-spacing:-.005em}
.sechead h3{font:700 20px/1.25 var(--font-title);letter-spacing:-.005em}
.sechead .hint{margin-left:auto;font:400 12px/1.45 var(--font-body);color:var(--text-caption)}

/* == Form fields =========================================================== */
.field{margin-top:var(--space-6)}
.field > label{display:block;font:600 14px/1.55 var(--font-body);margin-bottom:var(--space-2)}
.field .req{color:var(--red-600)}
.field .help{margin-top:6px;font:400 12px/1.5 var(--font-body);color:var(--text-caption);max-width:60ch}
.input{width:100%;max-width:440px;padding:10px 12px;font:400 14px/1.4 var(--font-body);
  border:1px solid var(--border-muted);border-radius:var(--radius-sm);background:var(--white);
  color:var(--text-body);transition:border-color var(--dur-med) var(--ease)}
textarea.input{max-width:640px;min-height:96px;resize:vertical}
/* Deliberately glow-free: gray border on hover, ink border on focus. Matches
 * policy-wizard, which kills box-shadow on field focus on purpose. */
.input:hover{border-color:var(--gray-500)}
.input:focus,.input:focus-visible{border-color:var(--primary-500);box-shadow:none;outline:none}
.input[aria-invalid="true"]{border-color:var(--red-600)}
.field-error{margin-top:6px;font:500 12px/1.45 var(--font-body);color:var(--error-text)}

.url-row{display:flex;gap:var(--space-3);align-items:flex-start;flex-wrap:wrap}
.url-row .input{flex:1 1 320px;max-width:none}

/* == Mode tabs ============================================================= */
.modetabs{display:inline-flex;gap:var(--space-1);padding:var(--space-1);background:var(--bg-muted);
  border-radius:var(--radius-md)}
.modetab{font:600 13px/1.2 var(--font-body);padding:8px 14px;border:none;background:transparent;
  color:var(--text-caption);border-radius:var(--radius-sm);
  transition:background var(--dur-med) var(--ease),color var(--dur-med) var(--ease)}
.modetab:hover{color:var(--text-body)}
.modetab[aria-selected="true"]{background:var(--white);color:var(--text-title);box-shadow:var(--shadow-xs)}

/* == Bubbles (department / friction pickers) =============================== *
 * Real checkboxes: the native input is visually hidden but focusable, so these
 * are keyboard- and screen-reader-operable. In the prototype all 32 were
 * <div onclick> with no tabindex, meaning they could not be used at all
 * without a mouse. Min-height 44px for touch. */
.bubbles{display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-2)}
.bubble{position:relative;display:inline-flex;align-items:center;gap:6px;min-height:44px;
  padding:10px 14px;font:500 13px/1.3 var(--font-body);color:var(--text-body);
  background:var(--white);border:1px solid var(--border-subtle);border-radius:var(--radius-pill);
  cursor:pointer;transition:border-color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease)}
.bubble:hover{border-color:var(--border-muted)}
.bubble input{position:absolute;opacity:0;width:1px;height:1px;margin:0}
.bubble:has(input:checked){border-color:var(--primary-500);background:var(--bg-muted)}
.bubble:has(input:focus-visible){box-shadow:var(--shadow-focus)}
/* pointer-events:none — it is aria-hidden decoration, and letting it swallow
 * clicks made the checkbox unreachable to anything targeting the input itself
 * (automation, some assistive tooling) even though label-click still worked. */
.bubble .tick{width:14px;height:14px;opacity:0;pointer-events:none;transition:opacity var(--dur-fast) var(--ease)}
.bubble:has(input:checked) .tick{opacity:1}

/* == Grouped panel (the "Add detail" section) ============================== *
 * Without this the panel was a legend followed by a wall of pills, twice, with
 * nothing marking where one group ended and the next began. Same card recipe as
 * everything else — border-only, --radius-md — with hairline-separated groups
 * so the structure is visible rather than inferred from spacing alone.
 * ========================================================================== */
.panel{margin-top:var(--space-6);background:var(--bg-surface);border:1px solid var(--border-subtle);
  border-radius:var(--radius-md)}
.pgroup{border-bottom:1px solid var(--border-subtle);padding:var(--space-6)}
.pgroup-last{border-bottom:0}
.pgroup-head{display:flex;align-items:baseline;gap:var(--space-3);flex-wrap:wrap}
.pgroup-title{font:600 14px/1.4 var(--font-body);color:var(--text-title)}
.pgroup-hint{font:400 12px/1.45 var(--font-body);color:var(--text-caption)}
/* Selection count. Sits right so the eye can check state without re-reading
 * every pill; empty until something is picked, so it never adds noise. */
.pgroup-count{margin-left:auto;font:600 11px/1.2 var(--font-body);text-transform:uppercase;
  letter-spacing:.08em;color:var(--accent-text)}
.pgroup .bubbles{margin-top:var(--space-4)}
.pgroup textarea.input{margin-top:var(--space-4);max-width:none}
.pgroup .help{margin-top:var(--space-2)}

/* Tighter than before: 16 of these sit side by side, and the previous padding
 * made two rows look like an undifferentiated block. */
.bubble{padding:9px 13px}

/* == LaunchPad cards ======================================================= *
 * Border-only, selection by border-color — the house recipe. Two structural
 * requirements the first version missed:
 *
 *   1. flex column + margin-top:auto on the footer, so the tag/hours row pins
 *      to the BOTTOM of every card. Grid already equalised card heights, but the
 *      footer followed the description, so a short card and a long one in the
 *      same row had their footers 21-40px apart.
 *   2. symmetric padding. The old 3px ::before accent bar forced
 *      `20px 20px 20px 24px`, which read as a visible left/right imbalance.
 *      Dropped: border-color plus the check already carry selection, and even
 *      padding matters more than a bar that was invisible until selected.
 *
 * Selection is PURPLE here, unlike the rest of the app where ink is the action
 * colour. Deliberate: these are the user's picks, and the commercial CTA that
 * acts on them is purple too, so the selection and its consequence match.
 * ========================================================================== */
.cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-4);margin-top:var(--space-6)}
.card{position:relative;display:flex;flex-direction:column;background:var(--bg-surface);
  border:1px solid var(--border-subtle);border-radius:var(--radius-md);padding:var(--space-5);
  cursor:pointer;transition:border-color var(--dur-fast) var(--ease),box-shadow var(--dur-fast) var(--ease)}
.card:hover{border-color:var(--border-muted)}
.card:has(input:checked){border-color:var(--accent-surface);box-shadow:var(--shadow-sm)}
.card:has(input:focus-visible){box-shadow:var(--shadow-focus)}
.card input{position:absolute;opacity:0;width:1px;height:1px;margin:0}

/* padding-right reserves the check circle's lane so a long name never runs
 * underneath it. */
.card-head{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;padding-right:34px;min-height:24px}
.card-name{font:700 17px/1.25 var(--font-title);letter-spacing:-.005em;color:var(--text-title)}
.card-sub{margin-top:4px;font:600 12px/1.4 var(--font-body);color:var(--text-caption);padding-right:34px}
.card-desc{margin-top:var(--space-3);font:400 13px/1.6 var(--font-body);color:var(--text-subtitle)}

/* margin-top:auto is what aligns footers across a row. padding-top keeps it off
 * the description when the card is naturally tall. */
.card-foot{margin-top:auto;padding-top:var(--space-4);display:flex;align-items:center;
  gap:var(--space-2);flex-wrap:wrap}
/* Hours always sit right, so the eye can compare them down the column instead of
 * hunting for them at the end of a variable-length tag list. */
.card-foot .hours{margin-left:auto;padding-left:var(--space-2)}

.card-check{position:absolute;top:var(--space-5);right:var(--space-5);width:22px;height:22px;
  border:1px solid var(--border-muted);border-radius:var(--radius-pill);
  display:flex;align-items:center;justify-content:center;color:transparent;
  transition:background var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.card:has(input:checked) .card-check{background:var(--accent-surface);border-color:var(--accent-surface);color:var(--white)}

/* Skeleton slots. The grid holds RESULT_COUNT slots from first paint, so the
   page never reflows as cards land and the user can see how many are coming. */
.card-skeleton{position:relative;display:flex;flex-direction:column;background:var(--bg-surface);
  border:1px solid var(--border-subtle);border-radius:var(--radius-lg);padding:var(--space-5);
  min-height:190px;pointer-events:none}
.skel-line{height:10px;border-radius:var(--radius-pill);background:var(--gray-200);
  background-image:linear-gradient(90deg,var(--gray-200) 0%,var(--gray-100) 50%,var(--gray-200) 100%);
  background-size:200% 100%;animation:shimmer 1.4s linear infinite}
.skel-line + .skel-line{margin-top:var(--space-3)}
.skel-title{height:16px;width:58%}
.skel-sub{height:9px;width:38%;margin-top:var(--space-2)}
.skel-body{width:100%}
.skel-body-short{width:72%}
.skel-foot{margin-top:auto;padding-top:var(--space-4);height:12px;width:44%}
@keyframes shimmer{from{background-position:200% 0}to{background-position:-200% 0}}

/* == Badges / tags ========================================================= */
.badge{font:600 9px/1.2 var(--font-body);text-transform:uppercase;letter-spacing:.08em;
  padding:3px 8px;border-radius:var(--radius-pill);border:1px solid transparent}
.badge-turnkey{background:var(--success-bg);color:var(--success-text);border-color:var(--success-border)}
.tag{font:500 11px/1.3 var(--font-body);color:var(--text-caption);background:var(--bg-muted);
  border:1px solid var(--border-subtle);padding:4px 9px;border-radius:var(--radius-pill)}
.hours{font:600 12px/1.3 var(--font-body);color:var(--text-caption);font-variant-numeric:tabular-nums;white-space:nowrap}
/* Overflow count for departments beyond the first two; full list in title=. */
.tag-more{color:var(--text-caption);cursor:default}

/* == Notices =============================================================== */
.notice{display:flex;gap:var(--space-3);padding:var(--space-4);border-radius:var(--radius-md);
  border:1px solid transparent;font:400 13px/1.55 var(--font-body)}
.notice .icon{margin-top:1px}
.notice-warning{background:var(--warning-bg);color:var(--warning-text);border-color:var(--warning-border)}
.notice-error{background:var(--error-bg);color:var(--error-text);border-color:var(--error-border)}
.notice-info{background:var(--purple-50);color:var(--accent-text);border-color:var(--accent-border)}
.notice strong{font-weight:600}
.notice-actions{margin-top:var(--space-3)}

/* == Inline research strip ================================================= *
 * This replaces the full-screen policy-wizard-style loader that used to live
 * here (rpanel + Lottie mark + fun-fact panel). Deliberate reversal, not drift:
 * policy-wizard has nothing to show while it researches, so a centred loader is
 * the right answer there. Here the matched pre-built apps are on screen in
 * ~0.1s, and putting a full-screen panel in front of readable cards would be
 * strictly worse.
 *
 * The .rp-* rules below are the same ones ported from policy-wizard — same
 * type, colour and 360ms cross-fade — just laid out as a strip above the grid.
 * The honesty rules that drive them live in app.js and are unchanged.
 * ========================================================================== */
.rstrip{display:flex;align-items:flex-start;gap:var(--space-6);flex-wrap:wrap;
  margin:var(--space-5) 0 var(--space-2);padding:var(--space-4) var(--space-5);
  background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:var(--radius-md)}
.rstrip-main{flex:1 1 320px;min-width:0}
.rstrip-side{flex:0 1 300px;min-width:0;text-align:right}

/* The one line that never goes quiet. Real events can be 20s apart and the
   fill sits still between researched cards, so this carries "still working"
   on its own. The dot pulses; reduced motion leaves it lit. */
.rstrip .rp-title{font:600 12px/1.5 var(--font-body);color:var(--text-title);
  letter-spacing:.02em;display:flex;gap:8px;align-items:center;margin-bottom:var(--space-2)}

/* The Darwin mark lives here rather than on the bar's leading edge. The bar
   reports cards received, which holds still for most of a run, and a logo
   parked on a still bar reads as frozen. Beside the label it means "working",
   which is continuously true.

   The pulsing dot is the ::before underneath it — the Lottie player is ~300KB
   and loaded on demand, so it may arrive late or never. `has-anim` is set by
   mountMark() once the animation is actually on screen; until then, and
   forever if the load fails, the dot carries the signal. */
.rstrip .rp-mark{position:relative;width:18px;height:18px;flex:none;display:block}
.rstrip .rp-mark::before{content:"";position:absolute;inset:5.5px;border-radius:50%;
  background:var(--accent-surface);animation:rp-pulse 1.6s var(--ease-standard) infinite}
.rstrip .rp-mark.has-anim::before{display:none}
.rstrip .rp-mark svg{display:block;width:100%;height:100%}
@keyframes rp-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.75)}}

.rstrip .rp-now{font:400 14px/1.6 var(--font-body);color:var(--text-body);min-height:26px;
  display:inline-flex;gap:9px;align-items:center;max-width:100%;
  transition:opacity 360ms var(--ease-standard)}
.rstrip .rp-now svg{color:var(--accent-text);flex:none;width:15px;height:15px}
.rstrip .rp-now span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}

/* Fill is cards-received / 8 — real progress, never elapsed time. */
.rstrip .rp-bar{height:5px;border-radius:var(--radius-pill);background:var(--gray-200);
  overflow:hidden;margin-top:var(--space-3);width:100%;max-width:420px}
.rstrip .rp-bar i{display:block;height:100%;width:0;
  background:linear-gradient(90deg,var(--accent-press),var(--accent-surface));
  transition:width 500ms var(--ease)}

/* One line each, ellipsised: search queries get long, and a wrapped entry
 * changed the strip height mid-run, which moved everything below it. */
.rstrip .rp-trail{min-height:44px}
.rstrip .rp-trail div{font:400 12px/1.6 var(--font-body);color:var(--gray-500);
  display:flex;gap:8px;align-items:center;justify-content:flex-end;max-width:100%}
.rstrip .rp-trail div span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.rstrip .rp-trail svg{color:var(--gray-400);flex:none;width:13px;height:13px}
.rstrip .rp-trail div:nth-child(2){opacity:.55}
.rstrip .rp-count{font:400 11px/1.5 var(--font-body);color:var(--gray-500);min-height:17px;margin-top:6px}

/* == Sticky CTA bar ======================================================== */
.ctabar{position:sticky;bottom:0;z-index:40;background:var(--primary-500);color:var(--text-on-dark);
  border-top:1px solid var(--primary-400);padding:var(--space-4) var(--space-12)}
.ctabar-inner{max-width:var(--page-max);margin:0 auto;display:flex;align-items:center;
  gap:var(--space-4);flex-wrap:wrap}
.ctabar .count{font:600 14px/1.3 var(--font-body);font-variant-numeric:tabular-nums}
.ctabar .sub{font:400 12px/1.4 var(--font-body);color:var(--gray-400)}
.ctabar .actions{margin-left:auto;display:flex;gap:var(--space-3);align-items:center}
.ctabar .btn-secondary{background:transparent;color:var(--gray-200);border-color:var(--primary-400)}
.ctabar .btn-secondary:hover{background:var(--primary-400)}

/* == Modal ================================================================= */
.overlay{position:fixed;inset:0;background:rgba(27,27,31,.45);z-index:500;display:flex;
  align-items:center;justify-content:center;padding:var(--space-5)}
.modal{background:var(--white);border-radius:var(--radius-lg);box-shadow:var(--shadow-xl);
  padding:var(--space-6);width:460px;max-width:100%;max-height:calc(100vh - 40px);overflow-y:auto}
.modal h2{font:700 20px/1.25 var(--font-title);letter-spacing:-.005em}
.modal .sub{margin-top:6px;font:400 13px/1.55 var(--font-body);color:var(--text-subtitle)}
.modal .input{max-width:none}
.modal-row{display:flex;gap:var(--space-3)}
.modal-row > *{flex:1}
.modal-actions{margin-top:var(--space-6);display:flex;gap:var(--space-3)}
.modal-note{margin-top:var(--space-4);font:400 11px/1.5 var(--font-body);color:var(--text-caption)}
.picked{display:flex;flex-wrap:wrap;gap:6px;margin-top:var(--space-2)}

/* Delivery status. Modelled on policy-wizard's .m-status, which replaced a
 * toast because a toast was "too easy to miss for what is the whole point of
 * the flow." Three states in one component. */
.status{text-align:center;padding:var(--space-4) 0}
.status-icon{width:48px;height:48px;margin:0 auto var(--space-4);border-radius:50%;
  display:flex;align-items:center;justify-content:center}
.status-ok .status-icon{background:var(--success-bg);color:var(--success-text)}
.status-err .status-icon{background:var(--error-bg);color:var(--error-text)}
.status h2{margin-bottom:var(--space-2)}

/* == Footer ================================================================ */
.footer{border-top:1px solid var(--border-subtle);margin-top:var(--space-16);
  padding:var(--space-8) var(--space-12);background:var(--white)}
.footer-inner{max-width:var(--page-max);margin:0 auto;display:flex;gap:var(--space-4);
  align-items:center;flex-wrap:wrap;font:400 12px/1.5 var(--font-body);color:var(--text-caption)}
.footer a{color:var(--text-caption);text-decoration:underline}
.footer a:hover{color:var(--text-body)}
.footer .right{margin-left:auto;display:flex;gap:var(--space-4);flex-wrap:wrap}

/* == Motion ================================================================ */
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.rise{animation:rise var(--dur-slow) var(--ease-emphasize) both}
.cards .card:nth-child(1){animation-delay:0ms}
.cards .card:nth-child(2){animation-delay:40ms}
.cards .card:nth-child(3){animation-delay:80ms}
.cards .card:nth-child(4){animation-delay:120ms}
.cards .card:nth-child(5){animation-delay:160ms}
.cards .card:nth-child(6){animation-delay:200ms}
.cards .card:nth-child(7){animation-delay:240ms}
.cards .card:nth-child(8){animation-delay:280ms}

/* Honoured properly, unlike the prototype's six unguarded keyframes. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;
    transition-duration:.001ms !important;scroll-behavior:auto !important}
  .spinner{border-top-color:var(--accent-surface);animation:none}
  .skel-line{animation:none;background-image:none}
  .rstrip .rp-mark::before{animation:none;opacity:1;transform:none}
}
