/* ==========================================================================
   THE REEL DEAL DECK — components
   --------------------------------------------------------------------------
   Buttons, objects, meta, forms, site chrome, and the homepage compositions.
   Consumes tokens.css only. No hard-coded colors, type sizes or spacing.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Local constants (not design tokens — deliberately not in tokens.css).

   --rdd-tap is an ergonomics floor, not a step on the spacing scale: the
   brief's hard requirement is a 44px minimum touch target, and 44px does not
   and should not exist in the 4px space scale. Declared once here so every
   control in the file agrees on it.

   --rdd-lift-* are the two speeds of the "pick up a card" interaction. A card
   rises slowly and is set down fast; one duration for both reads as a button,
   not as an object.
   -------------------------------------------------------------------------- */
:root {
  --rdd-tap: 2.75rem;          /* 44px */
  --rdd-lift-in: var(--dur);   /* rise   */
  --rdd-lift-out: 90ms;        /* set down — faster than it came up */
}

/* =========================================================== 1. ACTIONS == */

.btn {
  --btn-bg: transparent;
  --btn-fg: var(--c-text);
  --btn-bd: var(--c-border);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-5);
  /* Ergonomics floor. Every .btn on the site — including .btn--sm in the
     header — clears the 44px touch target without changing its type size. */
  min-height: var(--rdd-tap);
  /* A long label plus padding can exceed a 320px viewport and drag the whole
     document sideways. Let the text wrap rather than the page overflow. */
  max-width: 100%;
  text-wrap: balance;
  border: 1px solid var(--btn-bd);
  border-radius: var(--r-sm);
  background-color: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--f-body);
  font-size: var(--t-sm);
  font-weight: 650;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  appearance: none;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

/* nowrap keeps a two-word label on one line where there is room; below the
   narrowest phones a long CTA must be allowed to break or it drags the whole
   document sideways. `text-wrap: balance` (already set) evens the result. */
@media (max-width: 26rem) {
  .btn { white-space: normal; }
}

.btn:hover { text-decoration: none; }

.btn:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
}

.btn:active { transform: translateY(1px); }

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

/* The printed green in both themes, so the button matches the deck it is
   selling rather than the theme it happens to be rendered in. */
.btn--primary {
  --btn-bg: var(--c-brand-fill);
  --btn-fg: var(--c-brand-fill-text);
  --btn-bd: var(--c-brand-fill);
  box-shadow: var(--shadow-1);
}
.btn--primary:hover { --btn-bg: color-mix(in oklab, var(--c-brand-fill) 84%, var(--c-ink)); --btn-bd: var(--btn-bg); }
.btn--primary:active { --btn-bg: color-mix(in oklab, var(--c-brand-fill) 72%, var(--c-ink)); box-shadow: none; }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--c-text);
  --btn-bd: color-mix(in oklab, var(--c-text) 34%, var(--c-border));
}
.btn--ghost:hover { --btn-bg: color-mix(in oklab, var(--c-text) 6%, transparent); --btn-bd: var(--c-text); }
.btn--ghost:active { --btn-bg: color-mix(in oklab, var(--c-text) 12%, transparent); }

.btn--quiet {
  --btn-bg: transparent;
  --btn-fg: var(--c-text-muted);
  --btn-bd: transparent;
  padding-inline: var(--s-2);
}
.btn--quiet:hover { --btn-fg: var(--c-text); --btn-bg: color-mix(in oklab, var(--c-text) 6%, transparent); }
.btn--quiet:active { --btn-bg: color-mix(in oklab, var(--c-text) 12%, transparent); }

.btn--sm {
  padding: var(--s-2) var(--s-3);
  font-size: var(--t-xs);
}

.btn--lg {
  padding: var(--s-4) var(--s-6);
  font-size: var(--t-base);
}

.btn--block {
  display: flex;
  width: 100%;
}

/* =========================================================== 2. OBJECTS == */

.card {
  background-color: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-1);
  padding: var(--s-5);
  color: var(--c-text);
}

.card > * + * { margin-block-start: var(--s-3); }

a.card,
.card--link {
  display: block;
  text-decoration: none;
  transition:
    border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}
a.card:hover,
.card--link:hover {
  border-color: color-mix(in oklab, var(--c-accent) 50%, var(--c-border));
  box-shadow: var(--shadow-2);
  transform: translateY(-2px);
}
a.card:active,
.card--link:active { transform: translateY(0); box-shadow: var(--shadow-1); }

.card__title {
  font-family: var(--f-display);
  font-size: var(--t-lg);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  color: var(--c-text);
}

.card__text {
  color: var(--c-text-muted);
  font-size: var(--t-base);
  line-height: var(--lh-body);
}

.card-grid {
  display: grid;
  gap: var(--gap, var(--s-5));
  grid-template-columns: repeat(auto-fit, minmax(min(var(--min, 17rem), 100%), 1fr));
}

/* ------------------------------------------------------- playing card --- */
/*  A real 2.5:3.5 card face. The corner index mirrors itself bottom-right
    via a rotated pseudo-element — put the glyphs in `data-index` for the
    mirror to appear:
        <span class="playing-card__index" data-index="7&#9830;">7&#9830;</span>   */

.playing-card {
  position: relative;
  display: flex;
  flex-direction: column;
  aspect-ratio: var(--card-ratio);
  padding: var(--s-3);
  background-color: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-1);
  color: var(--c-text);
  text-decoration: none;
  overflow: hidden;
  transition:
    transform var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

/* the printed inner keyline every real card face has */
.playing-card::before {
  content: "";
  position: absolute;
  inset: var(--s-2);
  border: 1px solid color-mix(in oklab, var(--c-border) 70%, transparent);
  border-radius: calc(var(--r-card) - 4px);
  pointer-events: none;
}

a.playing-card:hover {
  transform: translateY(-4px) rotate(-0.6deg);
  box-shadow: var(--shadow-lift);
  border-color: color-mix(in oklab, var(--c-accent) 45%, var(--c-border));
}
a.playing-card:active { transform: translateY(-1px); box-shadow: var(--shadow-2); }
a.playing-card:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 3px; }

/* NB: deliberately NOT positioned — it is a flex item, so z-index still
   applies, and its ::after can resolve against .playing-card instead. */
.playing-card__index {
  z-index: 1;
  align-self: flex-start;
  font-family: var(--f-display);
  font-size: var(--t-base);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--ls-index);
  color: var(--c-accent);
  padding-inline-start: var(--s-1);
}

/* the mirrored bottom-right corner index */
.playing-card__index::after {
  content: attr(data-index);
  position: absolute;
  inset-inline-end: var(--s-4);
  inset-block-end: var(--s-3);
  transform: rotate(180deg);
  letter-spacing: var(--ls-index);
}

.playing-card__suit { font-size: 0.9em; }

.playing-card__body {
  position: relative;
  z-index: 1;
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-3) var(--s-5);
  text-align: center;
  min-height: 0;
}

.playing-card__name {
  font-family: var(--f-display);
  font-size: var(--t-base);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  color: var(--c-text);
  text-wrap: balance;
}

.playing-card__note {
  font-size: var(--t-xs);
  line-height: var(--lh-snug);
  color: var(--c-text-muted);
}

.playing-card__art {
  width: 72%;
  max-width: 9rem;
  color: var(--c-text);
  opacity: 0.9;
}

/* ============================================================== 3. META == */

.pill {
  --pill-tint: var(--c-text-faint);
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-3);
  border: 1px solid color-mix(in oklab, var(--pill-tint) 40%, transparent);
  border-radius: var(--r-pill);
  background-color: color-mix(in oklab, var(--pill-tint) 12%, var(--c-bg));
  color: color-mix(in oklab, var(--pill-tint) 72%, var(--c-text));
  font-size: var(--t-xs);
  font-weight: 650;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}

a.pill:hover { background-color: color-mix(in oklab, var(--pill-tint) 22%, var(--c-bg)); }

.pill--dry         { --pill-tint: var(--c-egg); }
.pill--nymph       { --pill-tint: var(--c-river); }
.pill--streamer    { --pill-tint: var(--c-red); }
.pill--wet         { --pill-tint: var(--c-riffle); }
.pill--terrestrial { --pill-tint: var(--c-red-deep); }
.pill--attractor   { --pill-tint: var(--c-ink-2); }

/* --c-river and --c-bark-deep are still the printed inks, which are too dark to
   read on a dark surface, so these three tints are lifted toward the text. */
.pill--nymph,
.pill--terrestrial,
.pill--attractor { --pill-tint: color-mix(in oklab, var(--pill-tint) 45%, var(--c-text)); }

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.stat__num {
  font-family: var(--f-display);
  font-size: var(--t-xl);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  color: var(--c-text);
  font-variant-numeric: tabular-nums;
}

.stat__label {
  font-size: var(--t-xs);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-index);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.breadcrumbs {
  font-size: var(--t-xs);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.breadcrumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
}

.breadcrumbs li { display: flex; align-items: center; gap: var(--s-2); }

.breadcrumbs li + li::before {
  content: "\2666";
  color: var(--c-border);
}

.breadcrumbs a { color: var(--c-text-muted); text-decoration: none; }
.breadcrumbs a:hover { color: var(--c-accent); text-decoration: underline; }
.breadcrumbs [aria-current="page"] { color: var(--c-text); }

.notice {
  --notice-tint: var(--c-text-faint);
  display: block;
  padding: var(--s-4) var(--s-5);
  border: 1px solid color-mix(in oklab, var(--notice-tint) 38%, var(--c-border));
  border-inline-start-width: 3px;
  border-radius: var(--r-sm);
  background-color: color-mix(in oklab, var(--notice-tint) 8%, var(--c-bg));
  color: var(--c-text-muted);
  font-size: var(--t-base);
  line-height: var(--lh-body);
}

.notice > * + * { margin-block-start: var(--s-2); }
.notice strong { color: var(--c-text); }

.notice--info { --notice-tint: var(--c-riffle); }

/* ============================================================= 4. FORMS == */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  min-width: 0;
}

.label {
  font-size: var(--t-xs);
  font-weight: 650;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-index);
  text-transform: uppercase;
  color: var(--c-text-muted);
}

.label .label__req { color: var(--c-accent); }

.input {
  width: 100%;
  min-width: 0;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background-color: var(--c-surface);
  color: var(--c-text);
  font-family: var(--f-body);
  font-size: var(--t-base);
  line-height: var(--lh-snug);
  appearance: none;
  transition:
    border-color var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}

textarea.input { min-height: var(--s-10); resize: vertical; line-height: var(--lh-body); }

select.input {
  cursor: pointer;
  padding-inline-end: var(--s-6);
}

.input::placeholder { color: var(--c-text-faint); }

.input:hover { border-color: color-mix(in oklab, var(--c-text) 30%, var(--c-border)); }

.input:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 1px;
  border-color: var(--c-focus);
}

/* --c-danger, NOT --c-red. In this palette --c-red is aliased to the brand
   green, because it names the SUIT colour hearts and diamonds are printed in,
   not an error state. A field marked with it drew a green border identical to
   focus, so a rejected field looked exactly like the one you were typing in. */
.input[aria-invalid="true"],
.input.is-invalid,
.input:not(:placeholder-shown):invalid {
  border-color: var(--c-danger);
  box-shadow: inset 0 0 0 1px var(--c-danger);
}

.input[disabled] {
  background-color: var(--c-bg-sunk);
  color: var(--c-text-faint);
  cursor: not-allowed;
}

.help {
  font-size: var(--t-xs);
  line-height: var(--lh-snug);
  color: var(--c-text-faint);
}

.help--error { color: var(--c-red); }

.form-row {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}

.form-inline {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s-3);
}

.form-inline .field { flex: 1 1 16rem; }
.form-inline .btn { flex: 0 0 auto; padding-block: var(--s-3); }

/* ============================================================ 5. HEADER == */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background-color: var(--c-bg);
}

.site-header::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--c-border);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.site-header.is-scrolled::after { opacity: 1; }
.site-header.is-scrolled { box-shadow: var(--shadow-1); }

/* No JS? Keep the divider permanently so the header never floats unbounded. */
.no-js .site-header::after { opacity: 1; }

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  min-height: var(--s-8);
  padding-block: var(--s-3);
  /* Flex items default to min-width:auto and refuse to shrink below their
     content, which is how the header used to push the whole document wider
     than the viewport on a 320px phone. */
  min-width: 0;
}

/* At 320px the header needs 342px: 16 padding + 190 brand + 16 gap + 104
   actions + 16 padding. The wordmark is what does not fit, so below the
   narrowest common phone it drops and the R-diamond card mark carries the
   brand on its own — it is the logo, not a decoration. Kept on every common
   handset width (360, 375, 390, 393, 414). */
@media (max-width: 349px) {
  .brand__word { display: none; }
}

/* ------------------------------------------------------------- brand --- */

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  color: var(--c-text);
  text-decoration: none;
  flex: 0 0 auto;
}

.brand__index {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.06em;
  width: var(--s-6);
  height: var(--s-7);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background-color: var(--c-surface);
  box-shadow: var(--shadow-1);
  font-family: var(--f-display);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
  color: var(--c-text);
  transition: transform var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.brand:hover .brand__index {
  transform: rotate(-4deg);
  border-color: color-mix(in oklab, var(--c-accent) 55%, var(--c-border));
}

.brand__index--lg {
  width: var(--s-7);
  height: var(--s-8);
  font-size: var(--t-md);
}

.brand__suit { color: var(--c-accent); }

.brand__word {
  font-family: var(--f-display);
  font-size: var(--t-md);
  font-weight: 600;
  letter-spacing: var(--ls-tight);
  line-height: var(--lh-tight);
  white-space: nowrap;
}

@media (max-width: 26rem) {
  .brand__word { font-size: var(--t-base); white-space: normal; }
}

/* --------------------------------------------------------------- nav --- */

.site-nav { display: none; }

.site-nav__list {
  display: flex;
  align-items: center;
  gap: var(--s-5);
}

.site-nav__list a {
  position: relative;
  display: inline-block;
  padding-block: var(--s-2);
  color: var(--c-text-muted);
  font-size: var(--t-sm);
  font-weight: 550;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}

.site-nav__list a::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--c-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}

.site-nav__list a:hover { color: var(--c-text); }
.site-nav__list a:hover::after { transform: scaleX(1); }

.site-nav__list a[aria-current="page"] { color: var(--c-text); }
.site-nav__list a[aria-current="page"]::after { transform: scaleX(1); }

.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex: 0 0 auto;
}

.site-header__actions .btn { display: none; }

.nav-toggle {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-1);
  width: var(--s-7);
  height: var(--s-7);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background-color: var(--c-surface);
}

.nav-toggle__bar {
  display: block;
  width: var(--s-4);
  height: 1px;
  background-color: var(--c-text);
  transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:first-child { transform: translateY(calc(var(--s-1) / 1.6)) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:last-child  { transform: translateY(calc(var(--s-1) / -1.6)) rotate(-45deg); }

.mobile-nav {
  padding: var(--s-4) var(--s-4) var(--s-6);
  border-top: 1px solid var(--c-border);
  background-color: var(--c-bg);
  max-height: calc(100svh - var(--s-9));
  overflow-y: auto;
}

.mobile-nav ul {
  display: grid;
  gap: var(--s-1);
  margin-block-end: var(--s-5);
}

.mobile-nav a:not(.btn) {
  display: block;
  padding: var(--s-3) var(--s-2);
  border-bottom: 1px solid var(--c-border);
  color: var(--c-text);
  font-family: var(--f-display);
  font-size: var(--t-lg);
  letter-spacing: var(--ls-tight);
  text-decoration: none;
}

.mobile-nav a:not(.btn):hover { color: var(--c-accent); }
.mobile-nav a[aria-current="page"] { color: var(--c-accent); }

@media (min-width: 62rem) {
  .site-nav { display: block; }
  .site-header__actions .btn { display: inline-flex; }
  .nav-toggle { display: none; }
  .mobile-nav { display: none !important; }
}

/* ============================================================ 6. FOOTER == */

.site-footer {
  margin-block-start: auto;
  padding-block: var(--s-7) var(--s-5);
  border-top: 1px solid var(--c-border);
  background-color: var(--c-bg-sunk);
  color: var(--c-text-muted);
  font-size: var(--t-base);
}

.site-footer__top {
  display: grid;
  gap: var(--s-7);
  padding-block-end: var(--s-6);
}

@media (min-width: 48rem) {
  .site-footer__top {
    grid-template-columns: minmax(14rem, 1fr) 2fr;
    gap: var(--s-8);
  }
}

.site-footer__brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-3);
}

.site-footer__tagline {
  font-family: var(--f-display);
  font-size: var(--t-lg);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  color: var(--c-text);
  max-width: 18ch;
}

.site-footer__meta {
  font-size: var(--t-sm);
  color: var(--c-text-faint);
  max-width: 26ch;
}

.site-footer__nav {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(min(10rem, 100%), 1fr));
}

.site-footer__heading {
  margin-block-end: var(--s-3);
  font-family: var(--f-body);
  font-size: var(--t-xs);
  font-weight: 650;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-index);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.site-footer__nav ul {
  display: grid;
  gap: var(--s-2);
}

.site-footer__nav a,
.site-footer__social a {
  color: var(--c-text-muted);
  font-size: var(--t-sm);
  text-decoration: none;
}

.site-footer__nav a:hover,
.site-footer__social a:hover {
  color: var(--c-accent);
  text-decoration: underline;
}

.site-footer__nav a[aria-current="page"] { color: var(--c-text); }

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding-block-start: var(--s-5);
  border-top: 1px solid var(--c-border);
}

.site-footer__legal {
  font-size: var(--t-xs);
  color: var(--c-text-faint);
  max-width: var(--measure);
}

.site-footer__social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5);
}

/* ================================================== 7. PAGE COMPOSITIONS = */

/* ------------------------------------------------------------- hero ----- */

.hero {
  padding-block: var(--s-7) var(--s-8);
  overflow: hidden;
}

.hero__grid {
  display: grid;
  gap: var(--s-8);
  align-items: center;
}

@media (min-width: 62rem) {
  .hero {
    padding-block: var(--s-9) var(--s-9);
  }
  .hero__grid {
    grid-template-columns: 1.05fr 1fr;
    gap: var(--s-8);
  }
}

.hero__title {
  margin-block-start: var(--s-4);
  font-size: var(--t-3xl);
  max-width: 16ch;
}

.hero__lede { margin-block-start: var(--s-5); max-width: 46ch; }

.hero__actions { margin-block-start: var(--s-6); }

.hero__note {
  margin-block-start: var(--s-5);
  font-size: var(--t-sm);
  color: var(--c-text-faint);
}

/* --------------------------------------------- CSS-only fanned hand ----- */

.card-fan {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 19rem;
  margin-inline: auto;
  max-width: 32rem;
  isolation: isolate;
}

@media (min-width: 48rem) {
  .card-fan { min-height: 23rem; }
}

.card-fan__card {
  position: absolute;
  width: 46%;
  min-width: 7.5rem;
  max-width: 11.5rem;
  aspect-ratio: var(--card-ratio);
  padding: var(--s-2);
  background-color: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-2);
  /* pivot well below the hand so the arc spreads sideways, like a real fan */
  transform-origin: 50% 215%;
  transform: rotate(var(--rot, 0deg)) translateY(var(--lift, 0));
  transition: transform var(--dur-slow) var(--ease), box-shadow var(--dur-slow) var(--ease);
}

.card-fan__card::before {
  content: "";
  position: absolute;
  inset: var(--s-1);
  border: 1px solid color-mix(in oklab, var(--c-border) 70%, transparent);
  border-radius: calc(var(--r-card) - 5px);
  pointer-events: none;
}

.card-fan__card:nth-child(1) { --rot: -16deg; z-index: 1; }
.card-fan__card:nth-child(2) { --rot: -8deg;  z-index: 2; }
.card-fan__card:nth-child(3) { --rot: 0deg; --lift: calc(var(--s-2) * -1); z-index: 3; box-shadow: var(--shadow-lift); }
.card-fan__card:nth-child(4) { --rot: 8deg;  z-index: 2; }
.card-fan__card:nth-child(5) { --rot: 16deg; z-index: 1; }

/* narrow screens: tighter hand so the outer cards stay inside the gutter */
@media (max-width: 40rem) {
  .card-fan { min-height: 15rem; }
  .card-fan__card { width: 44%; min-width: 6.5rem; max-width: 9.5rem; }
  .card-fan__card:nth-child(1) { --rot: -13deg; }
  .card-fan__card:nth-child(2) { --rot: -6.5deg; }
  .card-fan__card:nth-child(4) { --rot: 6.5deg; }
  .card-fan__card:nth-child(5) { --rot: 13deg; }
  .card-fan__name { font-size: var(--t-xs); bottom: 12%; }
}

.card-fan:hover .card-fan__card:nth-child(1) { --rot: -20deg; }
.card-fan:hover .card-fan__card:nth-child(2) { --rot: -10deg; }
.card-fan:hover .card-fan__card:nth-child(3) { --lift: calc(var(--s-4) * -1); }
.card-fan:hover .card-fan__card:nth-child(4) { --rot: 10deg; }
.card-fan:hover .card-fan__card:nth-child(5) { --rot: 20deg; }

/* static in flow (top-left of the padded card face) so the ::after mirror
   resolves against .card-fan__card */
.card-fan__index {
  display: block;
  padding-inline-start: var(--s-1);
  font-family: var(--f-display);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1;
  color: var(--c-accent);
}

.card-fan__index::after {
  content: attr(data-index);
  position: absolute;
  inset-inline-end: var(--s-3);
  inset-block-end: var(--s-2);
  transform: rotate(180deg);
}

.card-fan__art {
  position: absolute;
  inset: 20% 9% 22%;
  width: auto;
  height: auto;
  color: var(--c-text);
  opacity: 0.92;
}

.card-fan__name {
  position: absolute;
  inset-inline: var(--s-3);
  bottom: 14%;
  text-align: center;
  font-family: var(--f-display);
  font-size: var(--t-xs);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  color: var(--c-text-muted);
}

/* auto-FILL, not auto-fit. They are identical until a row has fewer cards than
   it has room for, and then auto-fit collapses the empty tracks and stretches
   what is left — which is how the Wet flies group, holding exactly one card,
   rendered it a meter wide next to neighbors a third of that. auto-fill keeps
   the empty tracks, so a lone card is the same size as every other card.

   min(…, 100%) so a 17rem floor cannot force a 320px viewport to scroll. */
.fly-strip {
  grid-template-columns: repeat(auto-fill, minmax(min(17rem, 100%), 1fr));
}

/* ------------------------------------------------------- proof strip ---- */

.proof {
  padding-block: var(--s-6);
  border-block: 1px solid var(--c-border);
  background-color: var(--c-bg-sunk);
}

.proof__grid {
  display: grid;
  gap: var(--s-5) var(--s-6);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 48rem) {
  .proof__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---------------------------------------------------- section headers --- */

.section-head {
  display: grid;
  gap: var(--s-4);
  margin-block-end: var(--s-7);
  max-width: var(--measure);
}

/* children may be grouped in a wrapper div — keep the same rhythm inside it */
.section-head > div > * + * { margin-block-start: var(--s-4); }

.section-head--split {
  max-width: none;
  align-items: end;
}

@media (min-width: 48rem) {
  .section-head--split {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--s-6);
  }
  .section-head--split > :first-child { max-width: var(--measure); }
}

.section-num {
  display: inline-flex;
  align-items: baseline;
  gap: 0.1em;
  font-family: var(--f-display);
  font-size: var(--t-sm);
  font-weight: 600;
  letter-spacing: var(--ls-index);
  color: var(--c-accent);
}

/* --------------------------------------------------------- three-up ----- */

.point {
  display: grid;
  gap: var(--s-3);
  align-content: start;
}

/* The shared fish mark takes its size from whoever draws it. The header sets
   its own; this is the founder card on /story/, where it stands in for the
   rank-and-suit index the other card carries. */
.point__index--mark .fish-mark { --fish-h: 1.45rem; transform: none; }

.point__index {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--s-6);
  height: var(--s-7);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background-color: var(--c-surface);
  font-family: var(--f-display);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1;
  color: var(--c-accent);
  box-shadow: var(--shadow-1);
}

.point__title {
  font-family: var(--f-display);
  font-size: var(--t-lg);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  color: var(--c-text);
}

.point__text {
  color: var(--c-text-muted);
  font-size: var(--t-base);
  max-width: 40ch;
}

/* ------------------------------------------------------------ doors ----- */

.door {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-5);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background-color: var(--c-surface);
  color: var(--c-text);
  text-decoration: none;
  box-shadow: var(--shadow-1);
  transition:
    transform var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.door:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-2);
  border-color: color-mix(in oklab, var(--c-accent) 50%, var(--c-border));
}
.door:active { transform: translateY(0); box-shadow: var(--shadow-1); }

.door__title {
  font-family: var(--f-display);
  font-size: var(--t-lg);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
}

.door__text { color: var(--c-text-muted); font-size: var(--t-base); flex: 1; }

.door__go {
  font-size: var(--t-xs);
  font-weight: 650;
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-accent);
}

.door:hover .door__go { text-decoration: underline; }

/* ----------------------------------------------------------- capture ---- */

.capture {
  display: grid;
  gap: var(--s-5);
  max-width: var(--wrap-narrow);
}

.capture__form { display: grid; gap: var(--s-3); }

/* --------------------------------------------------------- story bits --- */

/* ==========================================================================
   PHOTOGRAPHS OF THE MAKERS, AND THE TROUT UNLIMITED MARK
   The site was written around having no photographs of Ken and Audrey, so
   these are the first. They are real, they are theirs, and no stock
   photography joins them.
   ========================================================================== */

.photo {
  margin: var(--s-7) 0;
}
.photo img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  border-radius: var(--r-card);
  /* The photos are bright daylight shots on a near-black page; without this
     they read as holes punched in the page rather than prints laid on it. */
  border: 1px solid var(--c-border);
}
.photo figcaption {
  margin-block-start: var(--s-3);
  font-size: var(--t-sm);
  color: var(--c-text-faint);
}
/* A portrait at full column width is overbearing in a 760px measure, so the
   tall ones sit at a size you would actually print them. */
.photo--tall img { max-inline-size: 26rem; }
.photo--tall figcaption { max-inline-size: 26rem; }

/* The Trout Unlimited Business mark. Shown at a size that reads as a
   membership badge rather than a co-brand, because that is what it is. */
.tu-mark {
  display: block;
  inline-size: auto;
  block-size: 4.5rem;
  flex: 0 0 auto;
}
.tu-mark--lg { block-size: 6.5rem; }

/* The mark beside a line of text, which is how it appears on /story/ and
   /deck/: badge left, sentence right, aligned at the top. */
.tu-row {
  display: flex;
  align-items: flex-start;
  gap: var(--s-5);
  margin-block: var(--s-6);
}
.tu-row > :last-child { margin-block: 0; }
@media (max-width: 34rem) {
  .tu-row { flex-direction: column; gap: var(--s-4); }
}

.figure-note {
  font-size: var(--t-sm);
  color: var(--c-text-faint);
  border-inline-start: 1px solid var(--c-border);
  padding-inline-start: var(--s-4);
}

.byline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  font-size: var(--t-xs);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.person {
  display: grid;
  gap: var(--s-3);
  align-content: start;
}

.person__name {
  font-family: var(--f-display);
  font-size: var(--t-lg);
  font-weight: 600;
  letter-spacing: var(--ls-tight);
  line-height: var(--lh-snug);
  color: var(--c-text);
}

.person__role {
  font-size: var(--t-xs);
  letter-spacing: var(--ls-index);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.person__text { color: var(--c-text-muted); font-size: var(--t-base); }

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

.page-head {
  padding-block: var(--s-7) var(--s-6);
  border-bottom: 1px solid var(--c-border);
}

@media (min-width: 48rem) {
  .page-head { padding-block: var(--s-8) var(--s-7); }
}

.page-head__title { margin-block-start: var(--s-4); max-width: 18ch; }
.page-head__lede { margin-block-start: var(--s-5); }

/* --------------------------------------------------------- contact ------ */

.contact-grid {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
}

.contact-card__addr {
  font-style: normal;
  color: var(--c-text-muted);
  font-size: var(--t-base);
}

.contact-card__addr a { color: var(--c-accent); }

/* --- Hero: the printed tuck box ----------------------------------------- */
.hero-pack { margin: 0; display: grid; place-items: center; }
.hero-pack__img {
  display: block;
  width: 100%;
  max-width: 24rem;
  height: auto;
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-lift);
}

/* ==========================================================================
   8. WARMTH, ERGONOMICS AND THE PICKUP
   --------------------------------------------------------------------------
   Everything below is an evolution of classes already declared above (and, in
   one documented case, in flies.css). Nothing is renamed or removed.

   The brief: a family business that makes a beautiful object, without losing
   the precision. The warmth budget is spent on hairlines, tap ergonomics and
   two card details — not on texture, ornament or motion for its own sake.
   ========================================================================== */

/* -------------------------------------------------------- warm hairlines --
   Panels sat on a warm paper ground inside cold gray keylines. Pulling ~12%
   of the bark brown into every panel border warms the whole page without
   touching a single background color, and it works in both themes because
   --c-bark is remapped for night fishing. */

.card,
.door,
.notice,
.playing-card,
.brand__index,
.point__index,
.library-filters { border-color: color-mix(in oklab, var(--c-border) 88%, var(--c-bark)); }

/* ---------------------------------------------------------- tap targets --
   Brief requirement: every touch target >= 44px. .btn is handled at source.
   These are the remaining interactive runs. Static .pill labels stay compact;
   only the linked ones become controls. */

.site-nav__list a,
.mobile-nav a:not(.btn),
.site-footer__nav a,
.site-footer__social a {
  display: inline-flex;
  align-items: center;
  min-height: var(--rdd-tap);
}

/* the links now carry their own vertical rhythm — the list gap would double it */
.site-footer__nav ul { gap: 0; }
.mobile-nav ul { gap: 0; }

.breadcrumbs li { min-height: var(--rdd-tap); }
.breadcrumbs a { display: inline-flex; align-items: center; align-self: stretch; }

a.pill { min-height: var(--rdd-tap); }

.nav-toggle { min-width: var(--rdd-tap); min-height: var(--rdd-tap); }

/* ==========================================================================
   DETAIL ONE — THE PICKUP
   --------------------------------------------------------------------------
   .card-face is a photograph of a real printed card. Hovering or focusing it
   should read as picking that card up off the table, not as a web element
   growing a shadow:

     · it pivots from near its bottom edge, the way a card does under a thumb
     · it tilts, and alternate cards in a grid tilt opposite ways, so a row
       reads as a hand rather than as a carousel
     · it rises slowly and sets down fast — one duration for both is what makes
       an interaction feel like a button instead of an object
     · it lifts above its neighbors while it is up

   NOTE ON SPECIFICITY: .card-face is declared in flies.css, which loads AFTER
   this file (see src/_partials/head.html). flies.css belongs to another agent,
   so rather than edit it, these rules are given one extra class of specificity
   by repeating the selector. That is deliberate, not a typo.
   ========================================================================== */

a.card-face.card-face {
  position: relative;
  transform-origin: 50% 92%;
  transition:
    transform var(--rdd-lift-out) var(--ease),
    box-shadow var(--rdd-lift-out) var(--ease);
}

a.card-face.card-face:hover,
a.card-face.card-face:focus-visible {
  z-index: 2;
  transform: translateY(calc(var(--s-2) * -1)) rotate(-0.7deg) scale(1.014);
  box-shadow: var(--shadow-lift);
  transition-duration: var(--rdd-lift-in);
}

/* a hand, not a rank: neighbors lean away from each other */
a.card-face.card-face:nth-child(even):hover,
a.card-face.card-face:nth-child(even):focus-visible { transform: translateY(calc(var(--s-2) * -1)) rotate(0.7deg) scale(1.014); }

/* set down — faster than the rise, and it lands almost flat */
a.card-face.card-face:active,
a.card-face.card-face:nth-child(even):active {
  transform: translateY(calc(var(--s-1) * -1)) rotate(0deg) scale(1.002);
  box-shadow: var(--shadow-2);
  transition-duration: var(--rdd-lift-out);
}

/* ==========================================================================
   DETAIL TWO — THE CORNER INDEX, CARRIED THROUGH
   --------------------------------------------------------------------------
   The README names the card-corner index as the site's repeating motif. It is
   already drawn on .playing-card and .brand__index. base.css adds the pip to
   every .eyebrow and the mirrored index to every pulled quote; here the same
   idea finishes the two remaining index-shaped objects so the motif reads as
   a system rather than as three coincidences.
   ========================================================================== */

/* The brand mark is a card. On hover it should behave like one: it already
   rotates — give it the lift and the suit warming to match the pickup. */
.brand__index {
  transform-origin: 50% 92%;
  transition:
    transform var(--rdd-lift-out) var(--ease),
    border-color var(--rdd-lift-out) var(--ease),
    box-shadow var(--rdd-lift-out) var(--ease);
}

.brand:hover .brand__index,
.brand:focus-visible .brand__index {
  transform: translateY(calc(var(--s-1) * -1)) rotate(-4deg);
  box-shadow: var(--shadow-2);
  transition-duration: var(--rdd-lift-in);
}

.brand:active .brand__index {
  transform: translateY(0) rotate(-1.5deg);
  transition-duration: var(--rdd-lift-out);
}

/* .point__index is the same object at section scale. Same pivot, same tilt,
   but it is not interactive, so it only responds when its section is hovered
   — which on touch means never, and that is correct. */
.point__index {
  transform-origin: 50% 92%;
  transition: transform var(--rdd-lift-in) var(--ease), box-shadow var(--rdd-lift-in) var(--ease);
}
.point:hover .point__index {
  transform: rotate(-3deg);
  box-shadow: var(--shadow-2);
}

/* ------------------------------------------------------- reduced motion ---
   tokens.css already flattens durations globally, which leaves the transforms
   snapping instantly between two states. For an interaction whose whole point
   is the movement, the honest reduced-motion answer is no movement at all:
   keep the depth cue, drop the travel. */
@media (prefers-reduced-motion: reduce) {
  a.card-face.card-face:hover,
  a.card-face.card-face:focus-visible,
  a.card-face.card-face:active,
  a.card-face.card-face:nth-child(even):hover,
  a.card-face.card-face:nth-child(even):focus-visible,
  .brand:hover .brand__index,
  .brand:focus-visible .brand__index,
  .brand:active .brand__index,
  .point:hover .point__index,
  a.card:hover,
  .card--link:hover,
  .door:hover,
  a.playing-card:hover,
  .card-fan:hover .card-fan__card { transform: none; }
}

/* ==========================================================================
   9. EVIDENCE, NOT DECORATION
   --------------------------------------------------------------------------
   Research finding: this product has no reviews and must never fake any, and
   only ~43% of shoppers will buy something with none (docs/AUDIENCE.md §2).
   The substitute is verifiable manufacturing fact — USPCC, Bicycle stock,
   FSC paper, made in the USA. So the proof strip is typeset as a spec plate
   off the back of the tuck box: a printed rule over every fact, small-caps
   labels, tabular figures. Restraint here is the persuasion.
   ========================================================================== */

.proof .stat {
  gap: var(--s-2);
  padding-block-start: var(--s-3);
  border-top: 1px solid color-mix(in oklab, var(--c-border) 88%, var(--c-bark));
}

.stat__num { font-variant-numeric: tabular-nums lining-nums; }

/* ==========================================================================
   10. THE FAMILY
   --------------------------------------------------------------------------
   Research finding: what reads as genuine in small family outdoor brands is
   named, dated, checkable and under-stated — never staged (docs/AUDIENCE.md
   §3). .person is where Ken and Audrey appear, so it gets real hierarchy and
   room for a real photograph, and nothing that would make it look art-directed.
   ========================================================================== */

.person__name { text-wrap: balance; }

.person__role {
  color: var(--c-text-muted);      /* was --c-text-faint: this is a credential */
  font-weight: 650;
}

.person__text { max-width: var(--measure); text-wrap: pretty; }

/* Any image dropped inside a .person block behaves like a print plate: real
   photograph, honest crop, no vignette, no circle mask, no filter. */
.person img {
  width: 100%;
  max-width: 14rem;
  border-radius: var(--r-sm);
  border: 1px solid color-mix(in oklab, var(--c-border) 88%, var(--c-bark));
  background-color: var(--c-surface);
}

/* ==========================================================================
   11. RESPONSIVE + RHYTHM REPAIRS
   ========================================================================== */

/* .hero clips its own overflow, which also clips a lifted .card-face sitting
   inside it. Clip only the axis that actually needs clipping. */
.hero { overflow-x: clip; overflow-y: visible; }

/* The .card-grid rule that used to sit here was byte-identical to the one in
   section 2, so it guarded nothing — but it came AFTER .fly-strip at equal
   specificity, so it quietly won and .fly-strip's own columns never applied.
   Section 2 already carries the min(…, 100%) guard. */

/* Long unbroken strings — a URL in a notice, an email in the footer — are the
   other classic source of horizontal overflow. */
.notice,
.card__text,
.door__text,
.point__text,
.person__text,
.contact-card__addr { overflow-wrap: break-word; }

/* Section rhythm on a phone. --s-7 between sections and --s-7 under a section
   head put the same interval above and below a heading, which flattens the
   hierarchy on a small screen. Tighten the trailing space so the heading binds
   to the content it introduces. */
@media (max-width: 47.9375rem) {
  .section-head { margin-block-end: var(--s-6); gap: var(--s-3); }
  .page-head__title { max-width: none; }
  .hero__title { max-width: none; }
  .site-footer__tagline { max-width: 22ch; }
}

/* The footer bottom row is a space-between flex; at 320px the legal line and
   the social list fight for the same row before wrapping. Give it a floor. */
.site-footer__legal { flex: 1 1 18rem; }

/* --- section ancestry ----------------------------------------------------
   site.js marks the nav link whose section contains the current page (a fly
   page is inside the Fly-brary). Quieter than aria-current="page": the
   underline is there but half-drawn, so you can see where you are without
   being told you are somewhere you are not. */
.site-nav__list a[data-section-current] { color: var(--c-text); }
.site-nav__list a[data-section-current]::after {
  transform: scaleX(1);
  background-color: color-mix(in oklab, var(--c-accent) 45%, transparent);
}
.mobile-nav a[data-section-current],
.site-footer__nav a[data-section-current] { color: var(--c-text); }


/* ==========================================================================
   ORDER BAR — sitewide
   Moved here from blog.css: the bar now renders from the shell on every
   route, so its styles must load on every route. blog.css is only linked
   from blog pages.
   ========================================================================== */
/*  Constraints from docs/BLOG-CONTRACT.md: one line, one link, <= 3rem tall,
    dismissible, and it must never push the answer block below the fold. It is
    therefore a single flex row of --t-xs text that truncates rather than
    wrapping, and it sits in normal flow above the sticky header's content —
    it never overlays anything.                                              */

.aeo-bar {
  background-color: var(--c-bg-sunk);
  border-block-end: 1px solid var(--c-border);
}

.aeo-bar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  min-height: var(--s-6);          /* 2rem  */
  max-height: var(--s-7);          /* 3rem — the hard ceiling */
  padding-block: var(--s-1);
}

.aeo-bar__text {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  min-width: 0;                    /* lets the ellipsis actually engage */
  font-size: var(--t-xs);
  line-height: var(--lh-snug);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.aeo-bar__eyebrow {
  flex: 0 0 auto;
  font-weight: 650;
  letter-spacing: var(--ls-index);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.aeo-bar__link {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--c-accent);
  font-weight: 650;
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

.aeo-bar__close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--s-5);
  height: var(--s-5);
  border-radius: var(--r-sm);
  color: var(--c-text-faint);
  font-size: var(--t-base);
  line-height: 1;
}
.aeo-bar__close:hover {
  color: var(--c-text);
  background-color: color-mix(in oklab, var(--c-text) 8%, transparent);
}

/* Below the "eyebrow + link" comfort width, drop the eyebrow rather than
   letting the bar wrap to two lines. */
@media (max-width: 28rem) {
  .aeo-bar__eyebrow { display: none; }
}


/* ==========================================================================
   SHARED ANSWER + TABLE COMPONENTS
   Promoted from blog.css. `.aeo-answer` now renders on every fly page and on
   the product page, and `.post-table` is used outside articles, so both must load
   sitewide. Class names are unchanged deliberately — renaming them would
   break every existing article.
   ========================================================================== */

/* ==================================================== 2. THE ANSWER ====== */
/*  The single most important block on the page: 40–60 words, directly under
    the h1, before any preamble. Given a card-edge keyline so a human reads it
    first and a left rule so it survives being lifted out of context.        */

.aeo-answer {
  margin-block-start: var(--s-5);
  padding: var(--s-5);
  border: 1px solid var(--c-border);
  border-inline-start: 3px solid var(--c-accent);
  border-radius: var(--r-sm);
  background-color: var(--c-surface);
  box-shadow: var(--shadow-1);
}

.aeo-answer__label {
  margin-block-end: var(--s-2);
  font-size: var(--t-xs);
  font-weight: 650;
  letter-spacing: var(--ls-index);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.aeo-answer__text {
  font-family: var(--f-display);
  font-size: var(--t-lg);
  line-height: var(--lh-snug);
  color: var(--c-text);
  max-width: var(--measure);
  text-wrap: pretty;
}

/* ------------------------------------------------------- takeaways ------ */

.aeo-takeaways {
  margin-block-start: var(--s-6);
  padding: var(--s-5);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background-color: var(--c-bg-sunk);
}

.aeo-takeaways__title {
  font-size: var(--t-lg);
  margin-block-start: 0;
}

.aeo-takeaways__list {
  margin-block-start: var(--s-4);
  display: grid;
  gap: var(--s-3);
  color: var(--c-text-muted);
  font-size: var(--t-base);
  line-height: var(--lh-body);
}

.aeo-takeaways__list > li {
  position: relative;
  padding-inline-start: var(--s-5);
}
.aeo-takeaways__list > li::before {
  content: "\2666";
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  font-size: var(--t-sm);
  color: var(--c-accent);
}

/* ------------------------------------------------------------ tables ---- */
/*  A wide comparison table scrolls inside its own container. The page body
    must never scroll horizontally on a phone. tabindex makes the scroll
    container reachable — and scrollable — from the keyboard.               */

.post-tablewrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* The table inside sets min-width:28rem so columns stay readable. Without
     these two the wrapper inherits min-width:auto from its parent layout,
     grows to the table's width, and takes the whole document with it. */
  max-width: 100%;
  min-width: 0;
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background-color: var(--c-surface);
}
.post-tablewrap:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }

.post-table {
  width: 100%;
  min-width: 28rem;
  border-collapse: collapse;
  font-size: var(--t-base);
  line-height: var(--lh-snug);
}

.post-table__caption {
  padding: var(--s-3) var(--s-4);
  text-align: start;
  font-size: var(--t-xs);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-faint);
  border-block-end: 1px solid var(--c-border);
}

.post-table th,
.post-table td {
  padding: var(--s-3) var(--s-4);
  text-align: start;
  vertical-align: top;
  border-block-end: 1px solid var(--c-border);
}

.post-table thead th {
  background-color: var(--c-bg-sunk);
  color: var(--c-text);
  font-size: var(--t-xs);
  font-weight: 650;
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

.post-table tbody th { color: var(--c-text); font-weight: 650; }
.post-table tbody td { color: var(--c-text-muted); }
.post-table tbody tr:last-child th,
.post-table tbody tr:last-child td { border-block-end: 0; }

/* .aeo-answer responsive refinements — moved with the component so it behaves
   identically on fly pages, the product page and articles. */
@media (max-width: 34rem) {
  .aeo-answer { margin-block-start: var(--s-4); padding: var(--s-4); }
}
@media (min-width: 48rem) {
  .aeo-answer { padding: var(--s-6); }
}

/* --- /about/ and /press/ ------------------------------------------------ */
.about-layout { display: grid; gap: var(--s-7); align-items: start; }
@media (min-width: 54rem) {
  .about-layout { grid-template-columns: minmax(0, 1fr) 16rem; gap: var(--s-8); }
}
.about-card { margin: 0; }
.about-card img { display: block; width: 100%; height: auto; border-radius: var(--r-card); box-shadow: var(--shadow-lift); }
.about-card .figure-note { margin-block-start: var(--s-3); text-align: center; }

.about-facts { margin: var(--s-5) 0 0; }
.about-facts__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-1);
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--c-border);
}
@media (min-width: 40rem) {
  .about-facts__row { grid-template-columns: 10rem minmax(0, 1fr); gap: var(--s-4); }
}
.about-facts__row:last-child { border-bottom: 0; }
.about-facts__key {
  margin: 0;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-index);
  color: var(--c-text-faint);
}
.about-facts__val { margin: 0; color: var(--c-text); }

.about-quote {
  margin: var(--s-5) 0 0;
  padding: var(--s-5) var(--s-6);
  border-inline-start: 3px solid var(--c-green);
  background-color: var(--c-bg-sunk);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
.about-quote p {
  margin: 0 0 var(--s-4);
  font-family: var(--f-display);
  font-size: var(--t-md);
  line-height: var(--lh-body);
}
.about-quote p:last-of-type { margin-bottom: 0; }
.about-quote cite {
  display: block;
  margin-block-start: var(--s-4);
  font-family: var(--f-body);
  font-style: normal;
  font-size: var(--t-xs);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.press-asset { margin: 0; }
.press-asset img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-sm);
  background-color: var(--c-bg-sunk);
  border: 1px solid var(--c-border);
}
.press-asset figcaption { display: grid; gap: var(--s-1); margin-block-start: var(--s-3); font-size: var(--t-sm); }

/* --- Brand mark: the jester trout ---------------------------------------
   Vector extracted from the printer's joker_fish.pdf, so it is the same
   artwork that prints on the deck's jokers. Inline rather than an <img> so
   it inherits currentColor and follows the theme token — a flat PNG could
   not track the manual light/dark toggle.

   NB: width is computed from the height, never `auto`. Safari does not derive
   an intrinsic width from viewBox alone, so `width: auto` collapsed the mark
   to zero width there while Chrome and Firefox rendered it fine. The element
   also carries width/height attributes for the same reason. */
/* One mark, three places: the header, the footer and the founder card on
   /story/. They all draw {{> joker-fish }}, which emits .fish-mark and takes
   its size and colour from whatever contains it. */
.fish-mark {
  --fish-ratio: 0.708;              /* 81.9 / 115.7, the viewBox aspect */
  --fish-h: 1.6rem;

  display: block;
  height: var(--fish-h);
  width: calc(var(--fish-h) * var(--fish-ratio));
  aspect-ratio: 819 / 1157;
  color: var(--c-green);
  transition: color var(--dur-fast) var(--ease);
}
/* The mark is drawn facing left, which points it off the page when it sits in
   the top-left corner. Mirrored here so the trout looks into the site.
   Deliberately NOT applied to /brand/joker-fish-*, which reproduces the mark
   printed on the deck's three specials — the physical cards face the way they
   face. /brand/logo-* is the mirrored copy for logo use. */
.fish-mark { transform: scaleX(-1); }

.brand:hover .fish-mark { color: var(--c-green-deep); }
@media (min-width: 40rem) { .brand .fish-mark { --fish-h: 1.85rem; } }
.site-footer .fish-mark { --fish-h: 2.75rem; }

/* ==========================================================================
   FEED THE TROUT — the homepage game
   A matching toy: the trout wants one fly, three drift overhead, and tapping
   the match brings it up to that exact card. Nothing here is a quiz, so no
   rule depends on knowing anything — only on the artwork.

   Geometry is shared with feed.js and must stay in step: the water viewBox is
   400x150, the surface sits at y=26, and the trout idles at (--fx0, --fy0).
   feed.js sets --fx1 to the x of the card that was tapped, which is what aims
   both the rise and the splash.

   Progressive: with JS off the water and the trout render as a still
   illustration, no cards appear, and .feed__status stands on its own.
   ========================================================================== */

.feed {
  /* The water and the flies must share a width, or the stream sprawls across
     the whole column on a desktop while the three cards stay a narrow island. */
  max-width: 42rem;
  margin-inline: auto;
  margin-block-start: var(--s-5);
  --fx0: 145px;   /* where the trout holds */
  --fy0: 64px;
  --fx1: 145px;
  --rx: 150px;    /* where the splash breaks — the nose, not the group origin */   /* where it rises to — overwritten per tap */
}

.feed__lede { max-width: 54ch; }

/* --------------------------------------------------- what the trout wants */

.feed__want {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  margin: 0 0 var(--s-5);
}

.feed__want-label {
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-index);
  color: var(--c-text-faint);
}

/* A bubble rather than a caption, so it reads as the fish's idea and not as a
   question being asked of the player. */
.feed__want-card {
  position: relative;
  width: 5.5rem;
  padding: var(--s-2);
  background-color: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-1);
}

.feed__want-card::after {
  content: "";
  position: absolute;
  inset-inline-start: 50%;
  bottom: -7px;
  width: 12px;
  height: 12px;
  margin-inline-start: -6px;
  background-color: var(--c-surface);
  border-inline-end: 1px solid var(--c-border);
  border-bottom: 1px solid var(--c-border);
  transform: rotate(45deg);
}

.feed__want-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-card-art);
  background-color: #fff;          /* matches the printed stock */
}

/* -------------------------------------------------------- the three flies */

.feed__row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-3);
}

@media (min-width: 30rem) { .feed__row { gap: var(--s-5); } }

.feed__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2);
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: transform var(--dur) var(--ease);
}

.feed__card:hover:not([aria-disabled="true"]) { transform: translateY(-3px); }
.feed__card:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 3px; }
.feed__card[aria-disabled="true"] { cursor: default; }

/* The bob lives on the image, not the button: the button owns the hover and
   shake transforms, and two animations cannot share one property. */
.feed__card-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-card-art);
  background-color: #fff;
  box-shadow: var(--shadow-2);
  animation: feed-bob 3.6s ease-in-out infinite;
  animation-delay: var(--bob, 0s);
  transition: box-shadow var(--dur) var(--ease);
}

.feed__card:hover .feed__card-img { box-shadow: var(--shadow-lift); }

@keyframes feed-bob {
  0%, 100% { transform: translateY(0) rotate(-0.7deg); }
  50%      { transform: translateY(-6px) rotate(0.7deg); }
}

.feed__card-name {
  font-family: var(--f-display);
  font-size: var(--t-sm);
  line-height: var(--lh-snug);
  text-align: center;
  color: var(--c-text-muted);
}

.feed__card.is-wrong { animation: feed-shake 420ms var(--ease) both; }

@keyframes feed-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  45% { transform: translateX(5px); }
  70% { transform: translateX(-3px); }
}

/* Taken: the fly goes under, timed to land as the trout reaches the surface. */
.feed__card.is-taken .feed__card-img { animation: feed-taken 620ms ease-in 640ms both; }

@keyframes feed-taken {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(26px) scale(.82); }
}

.feed__card.is-fading { opacity: .35; transition: opacity var(--dur) var(--ease); }

/* ---------------------------------------------------------------- the water */

.feed__scene { position: relative; margin-block-start: var(--s-2); }

.feed__water {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 400 / 120;
}

.feed__surface { stroke: var(--c-border); stroke-width: 1.5; fill: none; }
.feed__seam    { stroke: var(--c-green); stroke-width: 1; fill: none; opacity: .22; }

/* Scaled here rather than in the path data, and mirrored when the trout turns
   to take a fly on its right — facing left it needs 111 units of room to the
   right of its nose, which runs out well before the third card. */
.feed__fish-scale {
  transform: scale(1.35);
  transform-box: view-box;
  transform-origin: 0 0;
}
.feed.is-flip .feed__fish-scale { transform: scale(-1.35, 1.35); }

.feed__fish-body { fill: var(--c-green); }
.feed__fish-tail,
.feed__fish-fin  { fill: var(--c-green); opacity: .72; }
.feed__fish-line { stroke: var(--c-egg); stroke-width: 1.4; fill: none; opacity: .55; }
.feed__fish-spot { fill: var(--c-bark); opacity: .5; }
.feed__fish-eye  { fill: var(--c-text); }

.feed__fish {
  transform: translate(var(--fx0), var(--fy0));
  animation: feed-idle 5.2s ease-in-out infinite;
}

@keyframes feed-idle {
  0%, 100% { transform: translate(var(--fx0), var(--fy0)); }
  50%      { transform: translate(calc(var(--fx0) - 6px), calc(var(--fy0) - 5px)); }
}

/* The rise. It tops out at y=6, putting the nose at 29 — a clear 5 units
   through the surface at y=34 — while keeping the dorsal fin, whose scaled path
   reaches y=-4, inside the top of the frame. */
.feed.is-take .feed__fish { animation: feed-take 1500ms cubic-bezier(.3,.05,.3,1) 120ms both; }

@keyframes feed-take {
  0%   { transform: translate(var(--fx0), var(--fy0)); }
  42%  { transform: translate(var(--fx1), 6px); }
  56%  { transform: translate(var(--fx1), 12px); }
  100% { transform: translate(var(--fx0), var(--fy0)); }
}

/* A miss costs a beat, not a life: the trout turns off it and settles again. */
.feed.is-refuse .feed__fish { animation: feed-refuse 700ms ease-in-out both; }

@keyframes feed-refuse {
  0%, 100% { transform: translate(var(--fx0), var(--fy0)); }
  30% { transform: translate(calc(var(--fx0) + 14px), calc(var(--fy0) + 10px)); }
  65% { transform: translate(calc(var(--fx0) + 5px), calc(var(--fy0) + 3px)); }
}

/* The splash rides the same --fx1, so it always breaks where the fish does. */
.feed__rings { transform: translate(var(--rx), 0px); }

.feed__ring {
  fill: none;
  stroke: var(--c-green);
  stroke-width: 1.2;
  opacity: 0;
  /* Without fill-box the scale would run from the SVG origin, not the ring. */
  transform-box: fill-box;
  transform-origin: center;
}

.feed.is-take .feed__ring    { animation: feed-ring 1000ms ease-out 760ms both; }
.feed.is-take .feed__ring--2 { animation: feed-ring 1000ms ease-out 920ms both; }

@keyframes feed-ring {
  0%   { opacity: .55; transform: scale(.35); }
  100% { opacity: 0;   transform: scale(1.9); }
}

/* -------------------------------------------------------------- the words */

.feed__status {
  min-height: 1.6em;
  margin: var(--s-4) 0 0;
  text-align: center;
  font-family: var(--f-display);
  font-size: var(--t-md);
  color: var(--c-text-muted);
}
.feed__status[data-tone="hit"]  { color: var(--c-green); }
.feed__status[data-tone="miss"] { color: var(--c-bark); }

.feed__score {
  min-height: 1.4em;
  margin: var(--s-2) 0 0;
  text-align: center;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--c-text-faint);
}

.feed__foot { margin-block-start: var(--s-5); font-size: var(--t-sm); }

/* Off-screen: stop the idle and the bob rather than animate to nobody. */
.feed.is-paused .feed__fish,
.feed.is-paused .feed__card-img { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .feed__card,
  .feed__card-img,
  .feed__card.is-fading { transition: none; }
  .feed__card:hover:not([aria-disabled="true"]) { transform: none; }
  .feed__card-img,
  .feed__fish,
  .feed__card.is-wrong,
  .feed.is-refuse .feed__fish,
  .feed.is-take .feed__ring,
  .feed.is-take .feed__ring--2 { animation: none; }

  /* The result still has to be visible without the motion that carried it. */
  .feed.is-take .feed__fish { animation: none; transform: translate(var(--fx1), 12px); }
  .feed.is-take .feed__ring { opacity: .5; }
  .feed__card.is-wrong { outline: 2px solid var(--c-red); outline-offset: 3px; }
  .feed__card.is-taken .feed__card-img { animation: none; opacity: 0; }
}

/* ==========================================================================
   INSTAGRAM STRIP — a revolving row of @reeldealdeck posts
   Rendered by instagramStrip() in src/templates/_blocks.mjs from the committed
   snapshot in data/instagram.json. No embed, no third-party script, no
   external request: the thumbnails are served from our own origin.

   The track holds the posts twice and translates by exactly -50%, so the loop
   is seamless. Motion is CSS; instagram.js only adds the pause button that
   WCAG 2.2.2 requires for content that moves for more than five seconds.
   ========================================================================== */

.ig-section { padding-block: var(--s-8); }

.ig__head {
  display: grid;
  gap: var(--s-4);
  align-items: end;
  margin-block-end: var(--s-6);
}
@media (min-width: 46rem) {
  .ig__head { grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-6); }
}
.ig__lede { max-width: 54ch; }

.ig {
  --ig-w: 8.5rem;
  --ig-gap: var(--s-3);
  position: relative;
  overflow: hidden;
  /* Fade both edges so items enter and leave rather than being sliced off. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
}
@media (min-width: 46rem) { .ig { --ig-w: 10.5rem; --ig-gap: var(--s-4); } }

.ig__track {
  display: flex;
  gap: var(--ig-gap);
  margin: 0;
  padding: 0;
  list-style: none;
  width: max-content;
  animation: ig-roll calc(var(--ig-count, 12) * 4s) linear infinite;
}

/* -50% is exactly one full copy of the posts, which is why the seam is
   invisible. It only holds because the track is rendered twice. */
@keyframes ig-roll {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(-50% - var(--ig-gap) / 2)); }
}

.ig:hover .ig__track,
.ig:focus-within .ig__track,
.ig.is-paused .ig__track { animation-play-state: paused; }

.ig__item { flex: 0 0 var(--ig-w); margin: 0; }

.ig__link {
  display: block;
  text-decoration: none;
  color: inherit;
  border-radius: var(--r-sm);
  transition: transform var(--dur) var(--ease);
}
.ig__link:hover { transform: translateY(-3px); }
.ig__link:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 3px; }

.ig__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 9 / 16;
  object-fit: cover;
  border-radius: var(--r-sm);
  background-color: var(--c-bg-sunk);
  box-shadow: var(--shadow-1);
}

.ig__meta {
  display: block;
  margin-block-start: var(--s-2);
  font-size: var(--t-xs);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.ig__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); margin: 0; }

/* Injected by site.js, so it never appears without a working handler. */
.ig__pause {
  padding: var(--s-1) var(--s-3);
  font: inherit;
  font-size: var(--t-xs);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-muted);
  background-color: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: 999px;
  cursor: pointer;
  opacity: .85;
}
.ig__pause:hover { opacity: 1; color: var(--c-text); }
.ig__pause:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  /* No motion at all: the strip becomes a row the reader scrolls themselves.
     The scroll belongs on .ig, which is the element with a fixed width — and
     the duplicated half is hidden, or it would read as the feed twice. */
  .ig { overflow-x: auto; -webkit-mask-image: none; mask-image: none; }
  .ig__track { animation: none; }
  .ig__item[aria-hidden="true"] { display: none; }
  .ig__link { transition: none; }
  .ig__link:hover { transform: none; }
  .ig__pause { display: none; }
}

/* ==========================================================================
   SUGGEST A FLY — /suggest/
   The one form on this site that actually delivers: suggest.js composes an
   email rather than writing to localStorage. The preview block below is what
   makes that honest — a mailto: link is silently useless to anyone without a
   mail client, so the message is always shown and always copyable.
   ========================================================================== */

.suggest__form { margin-block-start: var(--s-6); }

/* The submit handler is the whole feature: it composes the email. Without it
   the button would reload the page and silently empty the fields, so the form
   is not offered at all and .suggest__manual carries the job. The class is
   removed by the inline script in head.html, so there is no flash. */
.no-js .suggest__form { display: none; }

.suggest__manual {
  margin-block-start: var(--s-7);
  padding: var(--s-4) var(--s-5);
  border-inline-start: 3px solid var(--c-border);
  background-color: var(--c-bg-sunk);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  font-size: var(--t-sm);
  color: var(--c-text-muted);
}
.suggest__manual p { margin: 0; }
.suggest__manual-title {
  margin-block-end: var(--s-2) !important;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-index);
  color: var(--c-text-faint);
}

/* Injected by suggest.js — never present without a composed message in it. */
.suggest__out {
  margin-block-start: var(--s-6);
  padding: var(--s-5);
  background-color: var(--c-surface);
  border: 1px solid var(--c-green);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-1);
}

.suggest__out-title {
  margin: 0 0 var(--s-2);
  font-family: var(--f-display);
  font-size: var(--t-md);
  color: var(--c-green);
}

.suggest__preview {
  margin-block: var(--s-4);
  padding: var(--s-4);
  max-height: 22rem;
  overflow: auto;
  background-color: var(--c-bg-sunk);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-sm);
  line-height: var(--lh-body);
  color: var(--c-text);
  white-space: pre-wrap;
  word-break: break-word;      /* a long river name must not widen the page */
}
.suggest__preview:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }

/* ==========================================================================
   THE TUCK BOX IN 3D — hero of the homepage
   Six panels cut from the printer's dieline by scripts/box-panels.mjs, folded
   with CSS transforms. No 3D library: a tuck box is a cuboid, a cuboid is six
   rectangles, and three.js would be 150kB to draw a box.

   Proportions are the real ones measured off the dieline:
     W : H : D  =  1 : 1.393 : 0.26   (a poker tuck is 2.5 x 3.5 x 0.65in)
   Everything derives from --bw, so the box is resized in one place.

   Progressive: the resting three-quarter angle is CSS, so with JS off it is
   still a 3D box, just not a draggable one. box.js adds pointer and keyboard
   rotation and the idle drift.
   ========================================================================== */

.tuck {
  --bw: min(19rem, 62vw);
  --bh: calc(var(--bw) * 1.393);
  --bd: calc(var(--bw) * 0.26);
  --rx: -14deg;          /* resting three-quarter view; box.js takes over */
  --ry: -26deg;
  /* The whole orientation, in one place. CSS owns it as two Euler angles, which
     is all a resting pose needs. box.js replaces it with a matrix3d the moment
     anyone takes hold, because Euler angles gimbal-lock when the box is turned
     past vertical and this box now turns all the way over. */
  --tf: rotateX(var(--rx)) rotateY(var(--ry));
  display: grid;
  place-items: center;
  padding-block: var(--s-5);
}

@media (min-width: 60rem) { .tuck { --bw: min(21rem, 30vw); } }

.tuck__stage {
  perspective: 1500px;
  perspective-origin: 50% 45%;
  width: var(--bw);
  height: var(--bh);
  /* Room for the box to swing without clipping, and for the shadow. */
  overflow: visible;
}

.tuck__box {
  position: relative;
  width: var(--bw);
  height: var(--bh);
  transform-style: preserve-3d;
  transform: var(--tf);
  transition: transform 480ms var(--ease);
  /* The box owns every gesture that starts on it, both axes. This was pan-y —
     the page kept vertical scrolling — which made the box almost unturnable on
     a phone: any drag with a bit of vertical in it, which is most of them, got
     taken by the scroller mid-turn.

     The trade is real and was chosen deliberately: a swipe that starts ON the
     box no longer scrolls the page. Scrolling works everywhere else, including
     the margins either side of it, and the box is centred so both are
     reachable. If this ever feels like a trap, the fix is to go back to pan-y
     and accept that pitch is mouse-only. */
  touch-action: none;
}
/* Only offer the grab cursor where box.js is there to honour it. Without JS
   this is a handsome 3D box that does not turn, and it should not pretend. */
html:not(.no-js) .tuck__box { cursor: grab; }

/* THE TUCK BOX AND THE CARD FAN ARE THINGS YOU GRAB, NOT TEXT YOU READ.
   A drag across either is a gesture, and the browser's default is to paint it
   as a text selection: on the fan it highlighted every card name the pointer
   crossed and left them stuck highlighted, and on the box it selected the
   copy around it. Selection is switched off across both widgets, not just the
   images inside them — the fan's name labels were the ones actually showing up
   yellow, and they are not <img>.

   Scoped to the widgets themselves. `.fan-foot` and everything else on the page
   sit outside, so ordinary copyable text stays copyable, and the fly names are
   all selectable in full at /flies/. */
.tuck,
.fan-stage {
  -webkit-user-select: none;
          user-select: none;
}
.tuck.is-dragging .tuck__box { transition: none; cursor: grabbing; }
/* The intro spin and the sway are driven frame by frame; a transition on top of
   that is just lag. box.js removes this the moment a reader takes over. */
.tuck.is-animating .tuck__box { transition: none; }
.tuck__box:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 12px; }

.tuck__face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background-color: #4a762b;    /* the printed green, behind any panel gap */
  backface-visibility: hidden;
}

.tuck__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: fill;
  -webkit-user-drag: none;      /* Chrome/WebKit: no native image drag */
  pointer-events: none;         /* every pointer event belongs to the box */
}

/* Front and back are the full W x H panel. */
.tuck__face--front { transform: translateZ(calc(var(--bd) / 2)); }
.tuck__face--back  { transform: rotateY(180deg) translateZ(calc(var(--bd) / 2)); }

/* The spines are D wide, so they are centered on the box's vertical axis before
   being swung out to the sides. */
.tuck__face--left,
.tuck__face--right {
  width: var(--bd);
  left: calc(50% - var(--bd) / 2);
}
.tuck__face--right { transform: rotateY(90deg) translateZ(calc(var(--bw) / 2)); }
.tuck__face--left  { transform: rotateY(-90deg) translateZ(calc(var(--bw) / 2)); }

/* Top and bottom are D tall, centered on the horizontal axis. */
.tuck__face--top,
.tuck__face--bottom {
  height: var(--bd);
  top: calc(50% - var(--bd) / 2);
}
.tuck__face--top    { transform: rotateX(90deg) translateZ(calc(var(--bh) / 2)); }
.tuck__face--bottom { transform: rotateX(-90deg) translateZ(calc(var(--bh) / 2)); }

/* The base carries the barcode, the legal line and "Made in the USA", so it is
   a real panel cut from the dieline like the other five — not the flat glue
   flap it used to be. It is the reason the box turns all the way over. */

@media (prefers-reduced-motion: reduce) {
  .tuck__box { transition: none; }
}

/* --- the ground, and the affordance -------------------------------------- */

/* Not part of the 3D scene: a box floating on nothing reads as a sticker, and
   a real shadow plane inside preserve-3d costs more than it is worth here. */
.tuck__shadow {
  width: calc(var(--bw) * 0.86);
  height: calc(var(--bw) * 0.16);
  margin-block-start: calc(var(--bw) * -0.04);
  border-radius: 50%;
  background: radial-gradient(closest-side,
    color-mix(in oklab, var(--c-text) 26%, transparent), transparent 78%);
  filter: blur(6px);
  pointer-events: none;
}

.tuck__hint {
  margin: var(--s-4) 0 0;
  font-size: var(--t-xs);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-faint);
  transition: opacity var(--dur) var(--ease);
}
/* Once you have worked out that it turns, stop saying so. */
.tuck.is-touched .tuck__hint { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .tuck__hint { transition: none; }
}

/* ==========================================================================
   THE HUB DOORS — top of /flies/
   The Fly-brary is where everything that is not the product or the people
   lives: what is in the deck (/cards/), the flies themselves further down this
   page, the Virtual Guide (/blog/), and the flashcards at the foot of it. A
   reader arriving for one should not have to scroll to discover the rest.
   ========================================================================== */

.hub {
  display: grid;
  gap: var(--s-4);
  margin: var(--s-6) 0 var(--s-7);
  padding: 0;
  list-style: none;
}
/* Explicit counts, never auto-fit. With four doors auto-fit would leave a lone
   door on the last row stretched to double width whenever the track count did
   not divide evenly — the same bug that made a single fly card a meter wide.
   Two up at tablet, four across once there is room for them. */
@media (min-width: 46rem) {
  .hub { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-4); }
}
@media (min-width: 64rem) {
  .hub { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-5); }
}

.hub__door { margin: 0; }

.hub__link {
  display: grid;
  gap: var(--s-2);
  height: 100%;
  padding: var(--s-5);
  text-decoration: none;
  color: inherit;
  background-color: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-1);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

.hub__link:hover,
.hub__link:focus-visible {
  transform: translateY(-3px);
  box-shadow: var(--shadow-lift);
  border-color: color-mix(in oklab, var(--c-green) 45%, transparent);
}
.hub__link:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 3px; }

.hub__index {
  font-family: var(--f-display);
  font-size: var(--t-sm);
  letter-spacing: var(--ls-index);
  color: var(--c-green);
}

.hub__title {
  font-family: var(--f-display);
  font-size: var(--t-lg);
  line-height: var(--lh-snug);
  color: var(--c-text);
}

.hub__text {
  font-size: var(--t-sm);
  line-height: var(--lh-body);
  color: var(--c-text-muted);
}

/* Pushed to the bottom so both doors line their prompts up, however much text
   sits above them. */
.hub__go {
  align-self: end;
  margin-block-start: var(--s-3);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--c-green);
}

@media (prefers-reduced-motion: reduce) {
  .hub__link { transition: none; }
  .hub__link:hover, .hub__link:focus-visible { transform: none; }
}

/* --- "which one did we miss?" on /deck/ ---------------------------------- */

.deck-next { margin-block-start: var(--s-7); }

.deck-next__title {
  margin: var(--s-2) 0 var(--s-3);
  font-family: var(--f-display);
  font-size: var(--t-lg);
  line-height: var(--lh-snug);
  color: var(--c-text);
}

.deck-next__text {
  margin: 0 0 var(--s-4);
  color: var(--c-text-muted);
  line-height: var(--lh-body);
}

/* Sits under the two doors, deliberately quieter than them: it is an aside,
   not a third way in. */
.hub__aside {
  margin: calc(var(--s-7) * -1 + var(--s-4)) 0 var(--s-7);
  font-size: var(--t-sm);
  color: var(--c-text-muted);
}

/* ==========================================================================
   FLY-BRARY FLASHCARDS
   Name on the front, what it imitates on the back. Not a 3D flip: the two
   sides hold different amounts of text, and a fixed-height face that fits the
   longest card text is mostly empty for the shortest. They cross-fade in place
   instead, and the card grows to whatever is on it.

   Progressive: without flash.js the controls are never injected and both sides
   render stacked, so the information is all still there.
   ========================================================================== */

.flash-section { padding-block: var(--s-7); }
.flash__lede { max-width: 54ch; }

.flash {
  margin-block-start: var(--s-6);
}

.flash__card {
  position: relative;
  display: grid;
  min-height: 15rem;
  padding: var(--s-6) var(--s-5);
  text-align: center;
  align-content: center;
  gap: var(--s-2);
  background-color: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-1);
}

/* Only once flash.js is here: before that both sides show, which is the
   no-JS fallback rather than a broken flip. */
.flash--live .flash__card { cursor: pointer; }
.flash--live .flash__side--back { display: none; }
.flash--live.is-shown .flash__side--front { display: none; }
.flash--live.is-shown .flash__side--back { display: grid; gap: var(--s-3); }

.flash__card:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 3px; }

.flash__art {
  margin: 0 auto var(--s-3);
  width: min(11rem, 60%);
}

.flash__art img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-card-art);
  background-color: #fff;
  box-shadow: var(--shadow-2);
}

/* The prompt side hides the bottom of the card, where the printed line says
   what the fly imitates. 80% of the height is the same crop the homepage game
   uses; the name and the illustration stay, the answer does not. */
.flash__art--crop {
  /* 80% of the card's 400x559, so the box is shorter than the image inside it
     and overflow does the cropping. No explicit height: that would beat the
     aspect-ratio and collapse the figure. */
  aspect-ratio: 400 / 447;
  overflow: hidden;
  border-radius: var(--r-card-art);
}
.flash__art--crop img {
  border-radius: var(--r-card-art) var(--r-card-art) 0 0;
  box-shadow: none;
}

.flash__index {
  margin: 0;
  font-family: var(--f-display);
  font-size: var(--t-md);
  letter-spacing: var(--ls-index);
  color: var(--c-green);
}
/* Hearts and diamonds are green on this deck and spades and clubs are brown —
   the same rule the real cards follow. */
.flash__index[data-suit="spades"],
.flash__index[data-suit="clubs"] { color: var(--c-bark); }

.flash__name {
  margin: 0;
  font-family: var(--f-display);
  font-size: var(--t-xl);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  color: var(--c-text);
  text-wrap: balance;
}

.flash__cat {
  margin: 0;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--c-text-faint);
}

.flash__prompt {
  margin: var(--s-4) 0 0;
  font-size: var(--t-sm);
  color: var(--c-text-muted);
}
/* The prompt is a nudge for a mouse user and noise for a screen reader, which
   has already been told the card is a button that expands. */
.flash--live .flash__prompt { opacity: .75; }

.flash__imitates {
  margin: 0;
  font-family: var(--f-display);
  font-size: var(--t-lg);
  line-height: var(--lh-snug);
  color: var(--c-green);
  text-wrap: balance;
}

.flash__meta {
  margin: 0;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--c-text-faint);
}

.flash__text {
  margin: 0 auto;
  max-width: 44ch;
  font-size: var(--t-sm);
  line-height: var(--lh-body);
  color: var(--c-text-muted);
}

.flash__more { margin: 0; font-size: var(--t-sm); }

.flash__bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3);
  margin-block-start: var(--s-4);
}

.flash__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  margin-block-start: var(--s-4);
}

.flash__count {
  margin: 0;
  font-size: var(--t-xs);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-faint);
}


/* ==========================================================================
   THE FAN — all 54 printed faces, homepage
   =========================================================================

   Geometry. Every card sits in the same place (top of the stage, centered) and
   is rotated about a pivot --fan-pivot card-heights below its own top edge.
   That one transform produces the whole arc: the browser does the trigonometry
   and the markup carries nothing but an index.

   --fan-step, .fan's inline-size and its block-size move together. The height
   has to clear the drop of the end cards — rotating a point at radius R by θ
   lowers it by R(1 − cos θ), so with --fan-pivot 3.6 and a half-angle of
   26.5 × 1.5deg ≈ 39.8deg that is 3.6 × 0.231 ≈ 0.83 card-heights below the
   middle card — plus the card, plus the band the name label drops into.
   MEASURE THE END CARDS AFTER ANY CHANGE, never the middle one.

   =========================================================================
   THE HIT AREA IS THE SLIVER, NOT THE CARD. This is the whole design.

   Each card overlaps its neighbors almost entirely; what you can actually SEE
   of card N is a ~18px strip down its left edge, because card N+1 sits on top
   of the rest of it. So that strip, and only that strip, is what responds.

   Three rules make it true, and all three are required:

     .fan__card   pointer-events: none   — layout and z-order only
     .fan__link   pointer-events: auto   — a narrow full-height strip: the
                                           ONLY thing in the fan you can hit
     .fan__art    pointer-events: none   — the picture never captures anything

   The art rule is the one that fixes the bug this replaced. The art was the
   hit target, and a hovered card scales to 1.6, so a popped card blanketed
   138px of the row — about seven of its neighbors stopped responding to their
   own slivers until you moved clear of it. Pointing at a card did nothing,
   then a card five along would fire. That reads as broken hitboxes, and it was.

   The strip is a WEDGE, because the visible sliver is one. Cards rotate about a
   pivot 3.6 card-heights below them, so neighbors separate more at the top of
   the card than at the bottom: a point d above the pivot moves d·sin(step)
   sideways, and d is 3.6 card-heights at the card's top but only 2.6 at its
   foot. With --fan-step 1.5deg that is

       top     3.6 × 1.3975 × sin(1.5°) = 0.1317 card widths
       bottom  2.6 × 1.3975 × sin(1.5°) = 0.0951 card widths   (72.2% of it)

   so the strip is 0.1317 of a card wide and clip-path tapers it to 72.2% at the
   foot. clip-path clips hit-testing as well as paint, which is what makes this
   exact rather than approximate.

   THE CLIP LIVES ON A PSEUDO-ELEMENT, and that is not a style choice.
   clip-path clips an element's DESCENDANTS as well as itself, so putting it on
   .fan__link clipped the picture inside it and the whole fan rendered as
   slivers. .fan__link::after is a child of nothing: it carries the wedge and
   the pointer events, the link itself is pointer-events:none at full card
   width, and the art inside is untouched. Hitting a pseudo-element still
   triggers :hover and still activates the link it belongs to.

   It has to be exact. A rectangular strip wide enough to cover the top would
   overlap its neighbor at the bottom — and the moment a card is hovered its
   z-index jumps to 60, so that overlap would start winning hits that belong to
   the next card. A rectangle narrow enough never to overlap would leave the top
   of every card inert. The wedge is the only shape that is neither.

   RECOMPUTE BOTH NUMBERS if --fan-step or --fan-pivot changes.

   The last card has nothing above it, so it takes the full card width unclipped,
   which is also exactly how much of it you can see.

   Verify a change by probing elementFromPoint across the row at rest AND with
   a card popped; the two must name the same cards in the same places.
   =========================================================================

   Interaction. :hover and :focus-within get the same rules, so the fan is fully
   usable from the keyboard — every card is a real link and tab order is deck
   order. There is deliberately no neighbor-lean effect: it was four :has()
   rules doing sibling invalidation on every hover change across 54 elements,
   and it added motion to something that needed to feel precise.

   Only `transform` transitions, and only for 120ms. Transitioning the
   box-shadow on 54 transformed elements repaints rather than composites, and a
   sweep across the fan left a wake of still-animating cards that read as lag.

   OVERFLOW. The stage does NOT clip on desktop, so a lifted card rises over
   whatever is above it, like a card pulled out of a real fan. It only becomes a
   scroll container below 48rem, where the fan is wider than the screen and
   there is no hover to clip anyway. .fan-stage carries z-index 3 so the
   floating card clears its neighboring sections; the sticky header is 100 and
   deliberately stays above it.
   ========================================================================== */

.fan-stage {
  overflow: visible;
  position: relative;
  z-index: 3;
  margin-block-start: var(--s-5);
  padding-block-start: calc(var(--s-6) + 2.5rem);
  padding-block-end: var(--s-5);
}

/* Below this the fan is wider than the screen, so it scrolls — and a scroll
   container clips, which is why the padding above has to hold the labels. */
@media (max-width: 47.99rem) {
  .fan-stage {
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
  }
}

.fan {
  --fan-cw: clamp(76px, 13vw, 150px);            /* card width at rest */
  --fan-ch: calc(var(--fan-cw) * 1.3975);        /* the 400 x 559 card ratio */
  --fan-pivot: 3.6;                              /* pivot, in card-heights */
  --fan-step: 1.5deg;                            /* angle between neighbors */
  --fan-lift: 1.1rem;                            /* how far a card pops out */
  --fan-pop: 1.6;                                /* how big a lifted card gets */
  /* The hit strip: the gap between neighbors at the TOP of the card, where it
     is widest. The wedge below tapers it to the gap at the foot. Derived, not
     guessed — see the note above. */
  --fan-sliver: calc(var(--fan-cw) * 0.1317);
  --fan-sliver-foot: 72.2%;

  position: relative;
  list-style: none;
  margin: 0 auto;
  padding: 0;
  inline-size: calc(var(--fan-cw) * 7.44);
  block-size: calc(var(--fan-ch) * 2.25);
}

/* The touch scrub pad. Covers the fan, sits under every card, and hits nothing
   until fan.js arms it on a coarse pointer — see the block at the end of this
   section. On a mouse it does not exist as far as the page is concerned. */
.fan__scrub {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* --- layer 1: position and stacking. Hits nothing. ---------------------- */

.fan__card {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  inline-size: var(--fan-cw);
  block-size: var(--fan-ch);
  margin-inline-start: calc(var(--fan-cw) / -2);
  transform-origin: 50% calc(var(--fan-pivot) * 100%);
  transform: rotate(calc((var(--i) - var(--fan-mid)) * var(--fan-step)));
  z-index: calc(var(--i) + 1);
  pointer-events: none;
}

/* --- layer 2: the sliver. The only hit area in the fan. ----------------- */

.fan__link {
  position: relative;
  display: block;
  inline-size: 100%;
  block-size: 100%;
  /* The box hits nothing; its ::after does. */
  pointer-events: none;
  outline-offset: 3px;
}

/* The wedge: the hit area, and the only one. Clipped here rather than on the
   link because clip-path clips descendants — see the note above. */
.fan__link::after {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: var(--fan-sliver);
  clip-path: polygon(0 0, 100% 0, var(--fan-sliver-foot) 100%, 0 100%);
  pointer-events: auto;
}

/* Nothing covers the last card, so all of it is visible and all of it hits. */
.fan__card:last-child .fan__link::after {
  inline-size: var(--fan-cw);
  clip-path: none;
}

/* --- layer 3: the picture. Moves, and hits nothing. --------------------- */

.fan__art {
  --lift: 0px;
  --pop: 1;

  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: var(--fan-cw);
  pointer-events: none;
  /* Bottom center: a card grows up out of the fan and its base stays put. */
  transform-origin: 50% 100%;
  /* --nudge is 0 for every card at rest, and for every card on a desktop. Only a
     PICKED card on a phone takes the value fan.js measured for it, via
     --nudge-at below — applying it at rest would shift cards sideways in the
     closed fan, which is exactly the bug this comment replaces. */
  transform: translateX(var(--nudge, 0px)) translateY(var(--lift)) scale(var(--pop));
  border-radius: var(--r-card-art);
  /* The card art is cropped to its own keyline, so this radius is the card's
     own. See --r-card-art in tokens.css — it was measured, not guessed. */
  box-shadow: 0 1px 2px rgb(0 0 0 / .18), 0 6px 14px rgb(0 0 0 / .16);
  transition: transform .12s ease-out;
}

.fan__img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  border-radius: inherit;
  -webkit-user-drag: none;
  user-select: none;
}

/* --- the lift ----------------------------------------------------------- */

/* Three ways in, one appearance: pointer, keyboard, and the touch scrub. */
.fan__card:hover,
.fan__card:focus-within,
.fan__card.is-picked { z-index: 60; }

.fan__card:hover .fan__art,
.fan__card:focus-within .fan__art,
.fan__card.is-picked .fan__art {
  --lift: calc(var(--fan-lift) * -1);
  --pop: var(--fan-pop);
  box-shadow: 0 2px 8px rgb(0 0 0 / .2), 0 18px 36px rgb(0 0 0 / .28);
}

/* --- the name ------------------------------------------------------------ */
/* Centered on the CARD, not on the hit strip the strip is only a fraction of a
   card wide. Counter-rotated so it reads level while its card is tilted up to
   39.8deg, and opacity rather than display/visibility so it stays in the
   accessibility tree and gives every link a real accessible name at all times. */

.fan__name {
  position: absolute;
  inset-block-start: calc(100% + 0.6rem);
  inset-inline-start: calc(var(--fan-cw) / 2);
  inline-size: max-content;
  max-inline-size: 11rem;
  padding: .3em .6em;
  border-radius: var(--r-sm);
  background: var(--c-text);
  color: var(--c-bg);
  font-size: var(--t-xs);
  line-height: var(--lh-snug);
  text-align: center;
  opacity: 0;
  transform:
    translateX(-50%)
    rotate(calc((var(--fan-mid) - var(--i)) * var(--fan-step)));
  transition: opacity .12s ease-out;
  pointer-events: none;
}

.fan__card:hover .fan__name,
.fan__card:focus-within .fan__name { opacity: 1; }

.fan__idx {
  display: block;
  opacity: .7;
  font-variant-numeric: tabular-nums;
}

.fan-foot {
  margin-block-start: var(--s-5);
  max-inline-size: var(--measure);
}

/* --- motion and coarse pointers ----------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .fan__art,
  .fan__name { transition: none; }
}

/* There is no hover on a touch screen, and a :hover that latches on tap leaves a
   card stuck up. Neutralise it; the pick below is what lifts a card there. */
@media (hover: none) {
  .fan__card:hover { z-index: calc(var(--i) + 1); }
  .fan__card:hover .fan__art { --lift: 0px; --pop: 1; }
  .fan__card:hover .fan__name { opacity: 0; }
}

/* --- the touch scrub ------------------------------------------------------
   Hold and drag to walk the deck, release to keep a card up, tap it to open it.

   Everything here hangs off .is-scrubbable, which fan.js adds only on a coarse
   pointer. That is not decoration: with JS off, or on a mouse, none of it
   applies and the fan is exactly what it was — full-size cards, a stage that
   scrolls on a phone, and a tap that goes straight to the card's page.

   Why a phone needed its own answer at all: the fan is 7.44 card widths across,
   so at the old 104px card it was 774px inside a 390px screen. Half the deck
   was off the side, and each card showed an 11px strip, so a tap was a guess
   that navigated immediately. Nothing previewed, and most of the deck was
   unreachable. */

.fan-stage.is-scrubbable {
  /* No scroll container: the horizontal axis belongs to the scrub now. But the
     rotated cards spill ~20px past the fan's own box, which used to be absorbed
     by the scrolling stage and would otherwise give the whole PAGE a 4px
     horizontal scroll. `clip` stops that without creating a scroll container,
     and it is the one value that lets the other axis stay `visible`.
     It is safe only because every lifted card is nudged inside the screen
     first — measured, all 54 land within 8px of each edge. Without that this
     would cut a third off the end cards. */
  overflow-x: clip;
  overflow-y: visible;
  /* Horizontal is ours, vertical stays the page's — the same bargain the tuck
     box makes. fan.js also drops any gesture that is more vertical than
     horizontal, so a scroll that starts on the fan still scrolls. */
  touch-action: pan-y;
  /* Room for a card at --fan-pop to stand up without hitting the section above.
     Measured against the picked card's top, not guessed. */
  padding-block-start: calc(var(--s-6) + 4.5rem);
}

.fan-stage.is-scrubbable .fan {
  /* Fit all 54 on screen. Scrubbing a strip you cannot see is pointless, and
     7.44 is the fan's width in card widths, so this is the largest card that
     puts the whole deck inside the viewport. */
  --fan-cw: min(clamp(76px, 13vw, 150px), calc((100vw - 2rem) / 7.44));
  /* Each card is now a few pixels wide, so the picked one has to carry the
     whole preview. It is the only thing you can tap, which is also what keeps
     the real target well over the 24px minimum. */
  --fan-pop: 2.2;
  --fan-lift: 1.4rem;
}

/* The sliver is a mouse target. Here it would be ~6px, so it is switched off
   and the pad underneath takes every touch instead. */
.fan-stage.is-scrubbable .fan__link::after { pointer-events: none; }
.fan-stage.is-scrubbable .fan__scrub { pointer-events: auto; }

/* The picked card is the one tappable thing in the fan. It sits at z-index 60,
   above the pad, so a tap lands on it rather than picking again. */
.fan-stage.is-scrubbable .fan__card.is-picked .fan__art {
  pointer-events: auto;
  /* Only now, and only here: slide it back inside the screen. */
  --nudge: var(--nudge-at, 0px);
}

/* Its name is always up, because on a phone it is the only label you get. */
.fan-stage.is-scrubbable .fan__card.is-picked .fan__name { opacity: 1; }

/* Calibration measures where each card WOULD stand up, so it has to see the
   final geometry rather than the first frame of a 120ms transition. */
.fan.is-calibrating .fan__art { transition: none; }
.fan.is-calibrating .fan__card.is-picked .fan__art { --nudge: var(--nudge-at, 0px); }

/* For the length of one elementFromPoint call, fan.js turns the wedges back on
   so it can ask the browser which card is under the finger using the very same
   hit map that drives hover on a desktop. Switching them back off afterwards is
   what keeps a stray tap off a 6px strip from navigating. */
.fan.is-probing .fan__link::after { pointer-events: auto; }
.fan.is-probing .fan__card.is-picked .fan__art { pointer-events: none; }

/* The founding question on /story/ — the one line the whole page turns on, so
   it is set apart rather than buried in a paragraph of its own size. */
.story-question {
  margin-block: var(--s-6);
  padding-inline-start: var(--s-5);
  border-inline-start: 2px solid var(--c-green);
  font-family: var(--f-display);
  font-size: var(--t-lg);
  line-height: var(--lh-snug);
  color: var(--c-text);
  max-inline-size: var(--measure);
}

/* ===== Wholesale apply — progress ======================================== */
/* Shown only while an application is in flight. Two phases, two behaviours:
   a real 0-90% while the certificate uploads, then a travelling sliver while
   Shopify works. See the comment on .ws-prog in src/pages/wholesale-apply.mjs
   for why it is not one continuous bar. */

.ws-prog {
  margin: var(--s-3) 0 var(--s-4);
}

.ws-prog__track {
  block-size: 4px;
  border-radius: 999px;
  background: var(--c-border);
  overflow: hidden;           /* clips the travelling sliver to the track */
}

.ws-prog__bar {
  block-size: 100%;
  inline-size: 0%;
  border-radius: inherit;
  background: var(--c-green);
  transition: inline-size 180ms linear;
}

/* Phase two. The bar stops meaning "how far" and starts meaning "still alive",
   so it is a fixed-width sliver that travels rather than a width that grows. */
.ws-prog.is-waiting .ws-prog__bar {
  inline-size: 35%;
  transition: none;
  animation: ws-prog-travel 1.4s ease-in-out infinite;
}

@keyframes ws-prog-travel {
  from { transform: translateX(-110%); }
  to   { transform: translateX(400%); }
}

.ws-prog__step {
  margin: var(--s-2) 0 0;
  font-size: 0.875rem;
  color: var(--c-text-muted);
}

/* The spinner on the button. ::after rather than an element so the markup
   stays the same whether or not the form is in flight. */
.btn.is-busy {
  position: relative;
  cursor: progress;
}

.btn.is-busy::after {
  content: "";
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  box-sizing: border-box;
  margin-inline-start: 0.6em;
  vertical-align: -0.08em;
  border: 2px solid currentColor;
  border-inline-end-color: transparent;
  border-radius: 50%;
  animation: ws-spin 0.7s linear infinite;
}

@keyframes ws-spin {
  to { transform: rotate(360deg); }
}

/* Motion is decoration here — every state it conveys is also in the text of
   .ws-prog__step and the live region above it, so removing it loses nothing.
   The waiting bar becomes a dim full-width rule, which still reads as "busy"
   without anything moving. */
@media (prefers-reduced-motion: reduce) {
  .ws-prog__bar { transition: none; }
  .ws-prog.is-waiting .ws-prog__bar {
    inline-size: 100%;
    opacity: 0.45;
    animation: none;
  }
  .btn.is-busy::after { animation: none; }
}
