/* Cross-document View Transitions — opt in to a native crossfade between
   `/vote` → `/@handle?thanks=1` and other same-origin navigations. The
   browser handles the snapshot/replace timing; the keyframes below shape
   the easing. Falls back to no animation on browsers without support. */
@view-transition {
  navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .35s;
  animation-timing-function: cubic-bezier(.2,.7,.3,1);
}

:root {
  --bg: #fafaf7;
  --ink: #111;
  --muted: #6b6b66;
  /* Warm secondary tint — used selectively on prose-y muted lines (subtitle,
     flavour, eta) to take the page from "screen" to "printed paper". */
  --muted-warm: hsl(32 18% 50%);
  --line: hsl(40 6% 88%);
  --accent: #62b0f5;
  /* Distinct from --accent — used only for genuine "watch out" moments
     (form errors, destructive action hovers). Keeps the brand blue
     reserved for invitations to act, so the user can tell the two
     contexts apart at a glance. */
  --danger: #c4452d;
  --ease: cubic-bezier(.2,.7,.3,1);
  --ff-display: "Fraunces", ui-serif, Georgia, serif;
  --ff-body: "Inter", ui-sans-serif, system-ui, sans-serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--ff-body);
  font-weight: 300;
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-variant-numeric: tabular-nums;
  height: 100%;
}
body {
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

a { color: inherit; text-decoration: none; border-bottom: 1px solid currentColor; }
a:hover { color: var(--accent); }

/* === Buttons & links — three families ====================================
   All clickable affordances on the site fall into one of three families.
   Keeping them tightly defined here (and mostly de-duplicated against
   each other) is what gives the UI a single voice. Reach for one of:

   1. PRIMARY CTA — `<button>` (default) and `<a class="cta">`.
      Editorial italic Fraunces in accent, no chrome at rest. The implied
      "button" reveals itself only via the hover breath (color → ink,
      letter-spacing widens, arrow slides). Use for the main action of
      a step: continue, verify, submit, tweet it, share, try again,
      cast your top 3.

   2. SECONDARY NAV — `<a class="footer-link">` and `<a class="back">`.
      Small lowercase Inter, low-contrast muted. Pure navigation, never
      an action. Hover lifts to accent. No persistent underline — the
      copy is small enough that adding chrome would make it loud.

   3. GHOST UTILITY — `<button class="ghost">` and `.ghost-danger`.
      Inline tools that sit beside prose (copy, cancel, delete). Small
      Inter caps with a persistent hairline so they read as buttons in
      a paragraph. `.ghost-danger` swaps the hover from accent to ink so
      the destructive action lands without screaming.
   ========================================================================= */

/* family 1 — primary CTA */
button,
.cta {
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  font-size: 1rem;
  color: var(--accent);
  background: transparent;
  border: none;
  padding: .55rem 1.5rem .6rem;
  letter-spacing: .005em;
  cursor: pointer;
  display: inline-flex;
  align-items: baseline;
  gap: .35rem;
  white-space: nowrap;
  transition:
    color .25s var(--ease),
    letter-spacing .35s var(--ease),
    opacity .25s var(--ease);
}
button:hover:not(:disabled),
.cta:hover {
  color: var(--ink);
  letter-spacing: .015em;
}
button:focus-visible,
.cta:focus-visible {
  outline: none;
  color: var(--ink);
}
button:disabled {
  color: var(--muted);
  cursor: not-allowed;
  opacity: .65;
}
/* Modern UA stylesheets wrap `[hidden]` in `:where(...)` so the
   default rule has zero specificity — author rules above (button,
   .cta, button.ghost, button.ghost-danger) all set explicit `display`
   and tie or beat it. Restore the expected behaviour with selectors
   that match the same specificity tier as the worst offender so source
   order forces them to win. */
button[hidden],
button.ghost[hidden],
button.ghost-danger[hidden],
.cta[hidden] { display: none; }

input {
  background: none;
  border: none;
  border-bottom: 1px solid var(--line);
  font: inherit;
  color: inherit;
  padding: .35rem 0;
  outline: none;
  transition: border-color .2s var(--ease);
  min-width: 0;
}
input:focus { border-bottom-color: var(--accent); }
input::placeholder { color: var(--muted); font-style: italic; }

.page {
  width: min(640px, 92vw);
  /* `dvh` follows the actual visible area — when an iOS keyboard opens,
     the page reflows into the smaller window instead of clipping behind
     the keyboard. Falls back to `vh` on browsers that don't support it. */
  height: 100vh;
  height: 100dvh;
  /* Content is centered both axes within the viewport — the corner-pinned
     logo (top-right) and the bottom-pinned nav (about / cast-your-top-3)
     are out-of-flow, so the column lives independently in the middle. */
  padding: 1.5rem 2.4rem 4rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0;
}

header {
  /* Logo anchors top-left of the viewport on every page — the eye is
     the masthead, not a centered display element. We use position:fixed
     (rather than text-align:left inside .page) because .page is itself
     a 640px column centered horizontally on wide displays; "left" inside
     that column is still in the middle of the screen. Pinning the header
     to the viewport guarantees true corner placement. */
  position: fixed;
  top: .25rem;
  right: .75rem;
  z-index: 10;
  text-align: left;
}
header .logo,
header a.logo-link {
  display: block;
  border: none;
  line-height: 0;
  /* The wrapper is a fixed-size "viewport" through which we see only the
     drawing: the SVG asset has substantial empty whitespace around the
     actual eye + lettering, so we oversize the <img> inside and shift it
     with negative margins so the drawing's top-left aligns with this
     wrapper's top-left. overflow:hidden crops the rest. */
  width: 175px;
  height: 110px;
  overflow: hidden;
  /* The eye follows the cursor — `--look-x/y` is set in JS on mousemove
     and the slow ease makes the gaze feel calm rather than reactive. No
     hover-dim: the eye stays alive at full presence when you reach for it.
     Gaze offsets (`--gaze-x/y`) layer on top: a step-specific bias that
     biases where the eye is "looking" — scanning during the wait step,
     centered while verifying, settled on done. */
  transform: translate3d(
    calc(var(--look-x, 0px) + var(--gaze-x, 0px)),
    calc(var(--look-y, 0px) + var(--gaze-y, 0px)),
    0
  );
  transition: transform .9s var(--ease);
  transform-origin: center center;
}

/* Gaze biases per body class (set by JS when steps change). The blink
   animation cycles independently. */
body.gaze-scanning {
  --gaze-x: 0px; --gaze-y: 0px;
  animation: gaze-scan 6s var(--ease) infinite;
}
body.gaze-fixed   { --gaze-x: 0px; --gaze-y: 0px; }
body.gaze-soft    { --gaze-x: 0px; --gaze-y: 2.5px; }
@keyframes gaze-scan {
  0%,  100% { --gaze-x: -4px; --gaze-y: -1px; }
  25%       { --gaze-x:  3px; --gaze-y: -2px; }
  50%       { --gaze-x:  4px; --gaze-y:  1px; }
  75%       { --gaze-x: -3px; --gaze-y:  2px; }
}
/* Some browsers don't animate raw CSS variables without @property. The
   key positions still snap, just without smooth tweening — acceptable. */
@property --gaze-x { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --gaze-y { syntax: "<length>"; inherits: true; initial-value: 0px; }

/* The SVG asset has substantial empty whitespace around the visible
   drawing (the drawing sits roughly in the middle 50% of a 1500×1500
   canvas). We oversize the <img> here and translate it via negative
   margins so the drawing's top-left lines up with the wrapper's
   top-left; the wrapper's overflow:hidden then crops the rest. The
   numbers below are tuned by visual inspection — adjust together if
   the wrapper size in `.logo` changes. */
header .logo img,
header a.logo-link img {
  display: block;
  width: 320px;
  height: 320px;
  margin: -95px 0 0 -65px;
  object-fit: fill;
  transform-origin: center 60%;
  animation:
    logo-in .6s var(--ease) both,
    blink 24s var(--ease) 4s infinite;
}

.subtitle {
  /* The slogan lives in the centered content column (sibling of the
     leaderboard), not in the corner-pinned header. It reads as a tagline
     above the list, not as a caption attached to the logo. */
  margin: 0 0 .35rem;
  color: var(--muted-warm);
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-size: 1.35rem;
  font-variation-settings: "opsz" 14;
  letter-spacing: -.005em;
  text-align: center;
}
/* Countdown line on the locked/done step — visible only when the user
   has already cast their picks for this round. */
.done-cycle {
  margin-top: .8rem;
  text-align: center;
  font-style: italic;
  color: var(--muted-warm);
  font-family: var(--ff-display);
  font-variation-settings: "opsz" 14;
}

.muted { color: var(--muted); }
.small { font-size: .9rem; }
.empty {
  color: var(--muted-warm);
  margin-top: 2rem;
  text-align: center;
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-size: 1.05rem;
  font-variation-settings: "opsz" 14;
}
/* In empty state, the message sits in the centered .page column; no
   special positioning needed since the column itself is vertically
   centered. The `body.board-empty` class still hides the footer CTA. */
.empty a {
  font-family: var(--ff-display);
  font-style: normal;
  color: var(--accent);
  border-bottom-color: var(--accent);
  margin-left: .25rem;
  padding-bottom: .12rem;
  transition: letter-spacing .35s var(--ease);
}
.empty a:hover { letter-spacing: .015em; }
.error { color: var(--danger); }

/* --------------------------------------------------------------- leaderboard */

.page-leaderboard header { margin-bottom: 1.6rem; flex-shrink: 0; }
/* Stick the slogan + leaderboard near the top of the viewport instead
   of vertically centering the column — the slogan is the masthead, it
   should read like a headline, not a caption hovering over a half-empty
   page. */
.page-leaderboard {
  justify-content: center;
  position: relative;
}
/* Slogan sits in flow on every viewport now — the absolute `top: 9rem`
   we had on desktop got crashed into by a tall board (especially once
   every row gained a bio line), and the previous "leaderboard centred
   in the column ignores the slogan" property turned into "the board
   bumps into the floating slogan." With static positioning the slogan
   is part of the centred column and always keeps clean breathing room
   above the rows. */
.page-leaderboard .subtitle {
  margin: 0 0 1.4rem;
}
.page-leaderboard .leaderboard {
  /* Sized by content, capped so it never pushes the footer off-screen.
     When there are few rows the section is short and the footer flows
     naturally right after; when there are many, the inner board scrolls. */
  flex: 0 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.page-leaderboard .board {
  /* The cap is the only thing keeping a long list from overflowing —
     internal scroll kicks in only when needed. */
  max-height: min(56vh, 30rem);
  min-height: 0;
  overflow-y: auto;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 8px, black calc(100% - 8px), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, black 8px, black calc(100% - 8px), transparent 100%);
}

.board-meta {
  color: var(--muted-warm);
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  margin: 0 0 1rem;
  font-size: .95rem;
  font-variation-settings: "opsz" 14;
  text-align: center;
  min-height: 1.4em;
}

.board {
  list-style: none;
  margin: 0;
  padding: 0;
}
.board li {
  display: grid;
  grid-template-columns: 1.8rem 1fr;
  gap: .8rem;
  align-items: center;
  border-bottom: 1px solid hsl(40 6% 94%);
  opacity: 0;
  transform: translateY(4px);
  animation: row-in .35s var(--ease) forwards;
}
/* No trailing line under the last entry — the board ends in air, not on
   a dangling rule. */
.board li:last-child { border-bottom: none; }
.board li:nth-child(n)  { animation-delay: calc(var(--i, 0) * 30ms); }

/* The row's clickable area — wraps avatar + who + the lazy arrow so the
   whole strip feels alive on hover, not just the name. Flex with auto
   content width means the arrow nestles right after the text instead of
   getting pushed to the far edge of the column. */
.board .row-link {
  display: flex;
  align-items: center;
  gap: .8rem;
  border: none;
  padding: .55rem 0;
  color: inherit;
  min-width: 0;
}
.board .row-link > .who {
  min-width: 0;
  flex: 0 1 auto;
  overflow: hidden;
}
.board .row-link:hover { color: inherit; }
.board .row-link:hover .name { color: var(--accent); }
.board .row-arrow {
  font-family: var(--ff-display);
  color: var(--accent);
  opacity: 0;
  transition: opacity .3s var(--ease);
  font-size: 1.05rem;
  flex-shrink: 0;
}
.board .row-link:hover .row-arrow { opacity: 1; }

.board .rank {
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 400;
  /* `opsz` 14 = Fraunces' text cut. The previous `opsz: 144` (display
     cut) was rendered at 14px and produced a hairline thin enough to
     look like a font-loading glitch, not a deliberate editorial mark.
     Italic + the text cut gives the digits the same "credits-roll"
     feel as the OG card without the rendering artefact. */
  font-variation-settings: "opsz" 14;
  font-feature-settings: "lnum" 1;
  color: var(--muted);
  font-size: .95rem;
  letter-spacing: 0;
  line-height: 1;
}

/* Single canonical avatar — same size and shape in every list (board,
   pick, done). Lists that need a different scale (board podium) override
   width/height locally; everything else inherits from here. */
.avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  object-fit: cover;
  background: hsl(40 6% 92%);
  display: block;
  flex-shrink: 0;
}
/* Initial-letter fallback for handles we haven't scraped a photo for —
   serif-stamped on the same disc shape. Reused on pick + done lists too. */
.board .avatar-blank,
.pick-list .avatar-blank,
.done-picks .avatar-blank {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ff-display);
  color: var(--muted);
  font-size: .9em;
  line-height: 1;
  text-transform: uppercase;
}

/* Podium — same avatar size and same name size as the long tail; the
   only thing that distinguishes the top 3 is the accent gradient on
   the rank counter and the italic one-line bio underneath. The size-
   based scaling we had before made the rows feel like uneven steps,
   and the disproportion between the bigger #1 avatar and the smaller
   rank chip beside it read as a layout bug, not a deliberate emphasis.
   Hierarchy now lives in colour + a single editorial line, not in
   typographic violence. */
.board li:nth-child(1) .rank { color: var(--accent); }
.board li:nth-child(2) .rank { color: hsl(207 78% 73%); }
.board li:nth-child(3) .rank { color: hsl(207 48% 78%); }

.board .who {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: .05rem;
}
.board .name {
  font-family: var(--ff-display);
  font-size: 1rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.2;
  transition: color .25s var(--ease);
}
.board .handle {
  font-family: var(--ff-body);
  font-size: .78rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.3;
}
/* `.at` policy: muted in tabular lists (here, pick-list, done-picks) so
   it doesn't shout 50× down the column; accent in input contexts and
   single identifiers (handle-form, share card signature). */
.board .handle .at { color: var(--muted); margin-right: .05rem; }
/* Bios sit under every name (not just the podium). One italic line,
   ellipsised — gives every row a touch of prose without making any
   particular position more important than another. */
.board .bio {
  display: block;
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  font-size: .82rem;
  color: var(--muted-warm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: .1rem;
}

/* leaderboard footer */

.page-leaderboard footer {
  /* Flows naturally right under the leaderboard, like the "back to
     leaderboard" link on the profile page. The leaderboard section
     itself is centered vertically in the page column, so the footer
     follows the rows wherever they end up — no longer pinned to the
     viewport floor. */
  margin: 1.6rem 0 0;
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 2.5rem;
  flex-wrap: wrap;
  text-align: center;
}

/* About modal — a `<dialog>` injected by app.js on every page (except
   `/about` itself). Clicking the floating about-pin opens it; on first
   visit it auto-opens after a short delay. The same content is also
   served as a real /about route for crawlers and no-JS fallback. */
.about-modal {
  border: none;
  background: var(--bg);
  color: var(--ink);
  /* The dialog itself NEVER scrolls — it's a fixed frame. The inner
     `.about-modal-body` flex child handles overflow so the crop marks
     (`::before`) and the close button stay anchored to the modal
     edges instead of drifting into the middle of the text mid-scroll. */
  padding: 0;
  max-width: min(36rem, 92vw);
  width: 100%;
  max-height: 86vh;
  overflow: hidden;
  box-shadow: 0 20px 60px rgba(17, 17, 17, .12);
  position: fixed;
  inset: 0;
  margin: auto;
}
.about-modal[open] {
  /* Flex column so the body fills available height — `height: 100%`
     wouldn't resolve here (the dialog only has `max-height`, no
     explicit height), but `flex: 1; min-height: 0` does. Scoped to
     `[open]` so the UA default `display: none` still hides the
     dialog when it's closed. */
  display: flex;
  flex-direction: column;
}
.about-modal-body {
  /* The actual scrolling region. Padding is moved off the dialog and
     onto this child so content has breathing room without dragging the
     crop marks along. `min-height: 0` lets it shrink below content
     intrinsic height — without it, flex would stretch to content and
     overflow the dialog instead of scrolling internally. */
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 2.6rem 2.4rem 2rem;
  -webkit-overflow-scrolling: touch;
  /* Symmetric fade at top + bottom — the prose is fully transparent
     within 1rem of either edge so text never crosses the horizontal
     line of the corner crop marks (~12px / 0.75rem from edge). The
     opaque band starts only beyond a 2rem mid-fade, giving the eye a
     clear breathing zone between corner marks and visible text. */
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0,
    transparent 1rem,
    black 2.5rem,
    black calc(100% - 2.5rem),
    transparent calc(100% - 1rem),
    transparent 100%);
          mask-image: linear-gradient(to bottom,
    transparent 0,
    transparent 1rem,
    black 2.5rem,
    black calc(100% - 2.5rem),
    transparent calc(100% - 1rem),
    transparent 100%);
}
.about-modal::backdrop {
  /* Soft warm wash — same paper colour as the page, with a slight
     darkening, so the modal doesn't feel like it's slammed onto a
     foreign surface. */
  background: rgba(45, 38, 28, .35);
  backdrop-filter: blur(2px);
}
/* Print-style corner crop marks — same treatment as the share card on
   /@handle. Eight 14×1px hairlines drawn on a single pseudo-element via
   stacked linear-gradients, sitting just inside the modal's padding so
   they read as the "edge of the print". */
.about-modal::before {
  content: "";
  position: absolute;
  inset: 12px;
  pointer-events: none;
  --c: hsl(40 6% 70%);
  background:
    linear-gradient(var(--c), var(--c)) top    left  / 14px 1px no-repeat,
    linear-gradient(var(--c), var(--c)) top    left  / 1px 14px no-repeat,
    linear-gradient(var(--c), var(--c)) top    right / 14px 1px no-repeat,
    linear-gradient(var(--c), var(--c)) top    right / 1px 14px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom left  / 14px 1px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom left  / 1px 14px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom right / 14px 1px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom right / 1px 14px no-repeat;
}
.about-modal[open] {
  animation: modal-in .35s var(--ease);
}
.about-modal .about-page {
  margin: 0;
  max-width: none;
}
.about-modal-close {
  /* Quiet × in the corner — built from two crossed bars instead of an
     ASCII glyph so the rotate animation lands on a perfect plus/cross
     and the lines stay crisp at any zoom. */
  position: absolute;
  top: .9rem;
  right: 1rem;
  z-index: 2;
  width: 1.6rem;
  height: 1.6rem;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--muted);
  /* Indicate clickability via text overlay for accessibility tools that
     read button content (covered by the bars visually). */
  font-size: 0;
  transition: color .3s var(--ease), transform .45s var(--ease);
}
.about-modal-close::before,
.about-modal-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.1rem;
  height: 1px;
  background: currentColor;
  transform-origin: center;
  transition: background-color .3s var(--ease), height .3s var(--ease);
}
.about-modal-close::before { transform: translate(-50%, -50%) rotate(45deg); }
.about-modal-close::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.about-modal-close:hover {
  color: var(--accent);
  transform: rotate(90deg) scale(1.08);
}
.about-modal-close:hover::before,
.about-modal-close:hover::after {
  height: 1.5px;
}
.about-modal-close:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 4px;
  border-radius: 2px;
}

/* Language switcher inside the about modal — three tiny lowercase
   letter-spaced links separated by middle dots, top-centred above the
   article. Same nav-style register as the about-pin / back-pin links
   on the live site, so it reads as part of the same vocabulary
   instead of an extra UI piece. The active language is the only one
   in `--ink`; inactives stay muted, hovers tint to accent. */
.about-lang-switch {
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: .35rem;
  margin: 0 0 1.2rem;
  font-family: var(--ff-body);
  font-size: .78rem;
  letter-spacing: .12em;
  text-transform: lowercase;
}
.about-lang-btn {
  background: transparent;
  border: none;
  padding: .15rem .25rem;
  font: inherit;
  letter-spacing: inherit;
  color: var(--muted);
  cursor: pointer;
  transition: color .25s var(--ease);
}
.about-lang-btn:hover {
  color: var(--accent);
  /* Override the global `button:hover` rule (which sets
     letter-spacing: .015em). Without this, hovering tightens the
     glyphs, the button width shrinks, and the whole flex line
     re-centres — visible as the row jumping sideways under the
     cursor. Locking back to the inherited tracking keeps width
     stable across the hover transition. */
  letter-spacing: inherit;
}
.about-lang-btn.active {
  color: var(--ink);
  cursor: default;
}
.about-lang-sep {
  color: var(--muted);
  opacity: .55;
}

/* RTL handling: when the article switches to Arabic, flip the arrow
   on the delete button so it points the way the eye reads. The
   modal frame (close button position, language switcher) stays
   physically the same — close-on-the-right is muscle memory we
   shouldn't override per language. */
.about-page[dir="rtl"] {
  text-align: right;
}
.about-page[dir="rtl"] .delete-zone,
.about-page[dir="rtl"] .delete-zone .muted {
  text-align: center;  /* keep the delete zone visually centred */
}
.about-page[dir="rtl"] #delete-btn .arrow {
  display: inline-block;
  transform: scaleX(-1);
}

@keyframes modal-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* Inside the modal, the delete zone gets centered horizontally — feels
   more like a deliberate footer to the privacy section than a left-
   aligned action stuck at the bottom of the column. */
.about-modal .delete-zone { text-align: center; }
.about-modal .delete-zone .delete-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .35rem;
}
.about-modal .delete-confirm { align-items: center; }

/* Floating "about" link — pinned to the bottom of the viewport on every
   page except `/about` itself. Uses the same muted secondary-nav style
   as `.footer-link` so it reads as a quiet way out, not a CTA. */
.about-pin {
  position: fixed;
  left: 50%;
  bottom: 1rem;
  transform: translateX(-50%);
  font-family: var(--ff-body);
  font-size: .9rem;
  color: var(--muted);
  border-bottom: none;
  letter-spacing: .04em;
  text-transform: lowercase;
  transition: color .25s var(--ease);
  z-index: 10;
}
.about-pin:hover { color: var(--accent); }
/* When the board is empty, the inline "be the first." link inside the
   empty-state copy already routes to /vote — hide the footer CTA so the
   page doesn't show two competing prompts. */
body.board-empty .page-leaderboard footer .cta { display: none; }

/* The default `.cta:hover` widens letter-spacing, which reflows width.
   That's fine in isolation but here the about link sits right next to
   the CTA inside a centered flex row — when the CTA grows, the row
   re-centers and about visibly slides. Override the hover for this
   footer only: no spacing change, just a soft transform that nudges
   only the hovered element without touching layout. */
.page-leaderboard footer .cta {
  transition:
    color .25s var(--ease),
    transform .35s var(--ease),
    opacity .25s var(--ease);
  transform-origin: center;
}
.page-leaderboard footer .cta:hover {
  letter-spacing: .005em;
  transform: translateY(-1px);
}

/* profile page (`/@handle`) */

.page-profile header { margin-bottom: 1rem; flex-shrink: 0; }
.profile {
  flex: 0 1 auto;
  text-align: center;
  max-width: 32rem;
  margin: 0 auto;
}
/* Banner shown on the profile page right after a successful vote (the
   `?thanks=1` flag from the vote flow). Sits above the regular profile
   content so the user gets a moment of acknowledgement, then sees the
   exact same artefact a returning visitor would see. */
.thanks-banner {
  text-align: center;
  margin: 0 0 1.6rem;
}
.thanks-banner h2 {
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  font-size: 1.6rem;
  letter-spacing: -.015em;
  color: var(--ink);
  margin: 0 0 .25rem;
  line-height: 1.1;
}
/* Handle gets the brand accent so the personalisation reads as a small
   moment of recognition — "thank you" stays editorial, the @ leans in. */
.thanks-banner .thanks-handle { color: var(--accent); }
.thanks-banner p { margin: 0; }
.profile-head .avatar {
  width: 4.5rem;
  height: 4.5rem;
  border-radius: 50%;
  display: block;
  margin: 0 auto .65rem;
  object-fit: cover;
  background: hsl(40 6% 92%);
}
.profile-head .avatar-blank {
  font-family: var(--ff-display);
  color: var(--muted);
  font-size: 1.8rem;
  display: flex;
  align-items: center;
  justify-content: center;
  text-transform: uppercase;
}
.profile-name {
  /* Same italic display weight 300 treatment as the about modal h1 —
     editorial pageheader, not bold roman label. */
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  font-size: 1.6rem;
  letter-spacing: -.015em;
  margin: 0 0 .15rem;
  line-height: 1.05;
}
.profile-handle {
  font-family: var(--ff-body);
  color: var(--muted);
  font-size: .95rem;
  margin-bottom: .55rem;
}
.profile-handle a { border: none; color: var(--muted); }
.profile-handle a:hover { color: var(--accent); }
.profile-bio {
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  color: var(--muted-warm);
  font-size: .98rem;
  font-variation-settings: "opsz" 14;
  margin: 0 auto .6rem;
  max-width: 28rem;
  line-height: 1.4;
}


.profile-top3 { margin: .9rem 0 0; }
.profile-top3 h2 {
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-size: 1rem;
  color: var(--muted-warm);
  margin: 0 0 .55rem;
  font-variation-settings: "opsz" 14;
}
.profile-top3 ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .25rem;
  align-items: center;
}
.profile-top3 li {
  display: flex;
  align-items: baseline;
  gap: .55rem;
}
.profile-top3 .rank {
  font-family: var(--ff-display);
  font-weight: 300;
  font-variation-settings: "opsz" 144;
  color: var(--accent);
  font-size: .95rem;
  min-width: 1.5rem;
  text-align: right;
}
.profile-top3 a {
  font-family: var(--ff-display);
  font-size: 1.05rem;
  color: var(--ink);
  border-bottom: 1px solid transparent;
  letter-spacing: -.005em;
  transition: color .25s var(--ease), border-bottom-color .25s var(--ease);
}
.profile-top3 a:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.page-profile .back {
  margin-top: 1rem;
  text-align: center;
}

/* about page */

.page-about {
  /* No-scroll constraint: the entire page must fit one viewport. Sizes
     and spacing below are tight on purpose to honour that. */
  justify-content: center;
  padding: 1.5rem 2.4rem 1.5rem;
  overflow: hidden;
}
.page-about header { margin-bottom: 0; flex-shrink: 0; }
.about-page {
  flex: 0 1 auto;
  max-width: 32rem;
  margin: 0 auto;
  width: 100%;
  text-align: left;
}
.about-page h1 {
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  font-size: 1.5rem;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0 0 .25rem;
  line-height: 1;
  text-align: center;
}
.about-page h2 {
  /* Section headings read as editorial captions, not section labels —
     small, italic, muted-warm, sitting just above the prose like a
     pull-quote header in a magazine column. */
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  font-size: .85rem;
  letter-spacing: 0;
  color: var(--muted-warm);
  margin: 0 0 .15rem;
  text-transform: lowercase;
}
.about-page p {
  margin: 0 0 .3rem;
  line-height: 1.35;
  font-size: .78rem;
  color: var(--ink);
}
.about-page p:last-child { margin-bottom: 0; }
.about-page p.lede {
  /* Editorial italic to match the page-title hierarchy on the private
     profile fallback. Sans-serif Inter felt like a system error
     message; Fraunces italic reads as the same voice as the headline
     above it ("private.") and keeps the page coherent with the rest
     of the site's prose moments. */
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  font-size: 1rem;
  line-height: 1.4;
  letter-spacing: 0;
  color: var(--ink);
  margin: 0 0 .7rem;
  text-align: center;
}
.about-page p.aside {
  /* Reads like a body paragraph now — only the section h2 above it
     wears the gold tint to mark hierarchy. */
  font-family: var(--ff-body);
  font-style: normal;
  font-size: .78rem;
  line-height: 1.35;
  color: var(--ink);
  margin-top: .3rem;
}
.about-page p.muted {
  color: var(--muted);
  font-size: .78rem;
}
.about-page em {
  font-style: italic;
  color: var(--accent);
  font-weight: 400;
}
.about-page strong {
  color: var(--ink);
  font-weight: 500;
}
.about-page .mono {
  font-family: var(--ff-display);
  color: var(--accent);
}

.about-section {
  /* Each section is its own breath of space, separated from neighbours
     by margin alone — no rules, no boxes. Tight enough to keep the
     whole page in one viewport, even with the transparency block. */
  margin: 0 0 .75rem;
}
.about-section:last-of-type { margin-bottom: .6rem; }

.about-page .back {
  margin: .6rem 0 0;
  padding: 0;
  border-top: none;
  text-align: center;
}

@keyframes row-in {
  to { opacity: 1; transform: none; }
}

/* Each wizard step (and the home page on first paint) eases in with a
   small lift — softer than a hard cut and reads as the eye opening to
   the next moment. */
@keyframes step-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes logo-in {
  from { opacity: 0; transform: scale(.96); }
  to   { opacity: 1; transform: none; }
}

/* The eye blinks — most of the cycle is wide open, a brief asymmetric
   scaleY collapse mid-cycle reads as a real blink rather than a fade.
   Two-stage close-then-half-reopen-then-close gives it natural cadence. */
@keyframes blink {
  0%, 96%, 100% { transform: scaleY(1); }
  96.8%         { transform: scaleY(0.08); }
  97.4%         { transform: scaleY(0.55); }
  98.0%         { transform: scaleY(0.08); }
  99%           { transform: scaleY(1); }
}

/* family 2 — secondary nav (footer link, back link).
   Centralised here so any `.footer-link` and any `.back a` share the
   exact same lockup, regardless of which page they appear on. */
.footer-link,
.back a {
  font-family: var(--ff-body);
  font-size: .9rem;
  color: var(--muted);
  border-bottom: none;
  letter-spacing: .04em;
  text-transform: lowercase;
  transition: color .25s var(--ease);
}
.footer-link:hover,
.back a:hover {
  color: var(--accent);
  border-bottom-color: transparent;
}

/* Arrow inside CTAs / buttons / footer links — slides on hover, like the
   eye looking forward. Wrapped in a span so we can move it independently
   of the surrounding text. */
.arrow {
  display: inline-block;
  margin-left: .2em;
  font-style: normal;
  transition: transform .35s var(--ease);
  will-change: transform;
}
.arrow.back { margin-left: 0; margin-right: .2em; }

button:hover:not(:disabled) .arrow,
.cta:hover .arrow,
a:hover .arrow {
  transform: translateX(.28em);
}
a:hover .arrow.back { transform: translateX(-.28em); }

/* Refresh-style arrow (↻) — used on the "refresh the page" CTA after a
   data-deletion. Spins on hover instead of sliding because the action
   is a reload, not a forward navigation. Slightly slower duration so
   the rotation reads as a deliberate sweep, not a flick. */
.arrow-refresh {
  transition: transform .7s var(--ease);
}
button:hover:not(:disabled) .arrow.arrow-refresh,
.cta:hover .arrow.arrow-refresh,
a:hover .arrow.arrow-refresh {
  transform: rotate(360deg);
}

/* --------------------------------------------------------------- vote page */

.page-vote header { margin-bottom: 1.4rem; flex-shrink: 0; }
.step {
  /* sized by content so the page-level justify-content can center the
     whole composition. The pick step caps its inner list with max-height
     to handle long mutual lists without forcing growth here. */
  flex: 0 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin: 0;
}
/* The native `hidden` attribute resolves to `display: none` via the
   user-agent stylesheet, but our explicit `display: flex` above wins on
   specificity — so we need to override hidden steps explicitly. */
.step[hidden] { display: none; }
.step:not([hidden]) {
  animation: step-in .42s var(--ease) both;
}
.step h2 {
  font-family: var(--ff-display);
  font-weight: 400;
  font-size: 1.7rem;
  letter-spacing: -0.01em;
  margin: 0 0 1rem;
  flex-shrink: 0;
  text-align: center;
}

.handle-form {
  display: flex;
  align-items: baseline;
  gap: .35rem;
  border-bottom: 1px solid var(--line);
  padding-bottom: .25rem;
  max-width: 28rem;
  margin: 0 auto;
  transition: border-bottom-color .25s var(--ease);
}
.handle-form:focus-within { border-bottom-color: var(--accent); }
.handle-form + .muted { text-align: center; }
/* `.at` accent here — input context (see policy note above .board .at). */
.handle-form .at {
  font-family: var(--ff-display);
  font-size: 1.4rem;
  color: var(--accent);
  transition: color .25s var(--ease);
}
.handle-form input {
  flex: 1;
  border: none;
  font-family: var(--ff-display);
  font-size: 1.4rem;
}
.handle-form input:focus { border: none; }
/* Centered action paragraphs — a consistent home for any framed CTA
   that follows a heading or a form. Generous vertical breathing so the
   button doesn't feel cramped between text blocks. */
.step-action,
.verify-actions,
.error-actions,
.done-share {
  text-align: center;
  margin: 1.4rem 0;
}

/* Helper prose under inputs and form actions across the wizard — h2,
   .step-action and .handle-form are already centred, so this aligns
   the trailing muted hint with them rather than left-edge by default. */
.step > .muted { text-align: center; }
.handle-form + .muted { margin-top: .75rem; }

/* waiting state */

/* "tracing your constellation" — dots scattered on a soft asymmetric
   pattern instead of a metronome row. Each outer span carries the Y
   offset (position), the inner <b> carries the dot itself + scale
   transitions, so the two transforms compose without fighting. */
.dots {
  display: flex;
  gap: .9rem;
  margin: 2.5rem auto 1.2rem;
  justify-content: center;
  align-items: center;
  height: 1.6rem;
}
#step-wait > .wait-counts,
#step-wait > .eta,
#step-wait > .flavour { text-align: center; }
#step-wait > .track { max-width: 22rem; margin-left: auto; margin-right: auto; }
.dots > span {
  display: block;
  transform: translateY(var(--y, 0));
}
.dots > span:nth-child(1)  { --y: -.4rem; }
.dots > span:nth-child(2)  { --y:  .25rem; }
.dots > span:nth-child(3)  { --y: -.15rem; }
.dots > span:nth-child(4)  { --y:  .5rem; }
.dots > span:nth-child(5)  { --y: -.55rem; }
.dots > span:nth-child(6)  { --y:  .15rem; }
.dots > span:nth-child(7)  { --y: -.3rem; }
.dots > span:nth-child(8)  { --y:  .45rem; }
.dots > span:nth-child(9)  { --y: -.1rem; }
.dots > span:nth-child(10) { --y:  .35rem; }
.dots > span > b {
  display: block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--line);
  transition: background .3s var(--ease), transform .3s var(--ease);
}
.dots > span.on > b {
  background: var(--ink);
  transform: scale(1.2);
}
.dots > span.pulse > b {
  background: var(--accent);
  animation: pulse 1.2s var(--ease) infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .35; transform: scale(.85); }
}

.wait-counts {
  font-family: var(--ff-display);
  font-size: 1.4rem;
  letter-spacing: -.01em;
}
.wait-counts .wait-sep { color: var(--muted); margin: 0 .35rem; font-style: italic; }

.eta {
  color: var(--muted-warm);
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  margin-top: .25rem;
  min-height: 1.4em;
}

.track {
  position: relative;
  height: 1px;
  background: var(--line);
  margin: 2rem 0 1.5rem;
  overflow: visible;
}
.track-dot {
  position: absolute;
  left: 0;
  top: 50%;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  transform: translate(-50%, -50%);
  transition: left .6s var(--ease);
}

.flavour {
  color: var(--muted-warm);
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-size: 1rem;
  font-variation-settings: "opsz" 14;
  letter-spacing: -.005em;
  margin-top: 1rem;
  min-height: 1.4em;
  transition: opacity .3s var(--ease);
}

/* verification step */

.mono-handle {
  font-family: var(--ff-display);
  color: var(--ink);
}

#step-verify > .muted,
.verify-actions { text-align: center; }
#step-verify > .muted { max-width: 32rem; margin: 0 auto 1rem; line-height: 1.45; }

.token-row {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 1rem;
  margin: 1.5rem auto .75rem;
  max-width: 30rem;
  border-bottom: 1px dashed var(--line);
  padding-bottom: .6rem;
}
.token {
  font-size: 1rem;
  color: var(--ink);
  flex: 1;
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
  font-family: var(--ff-body);
}
/* Reads as a sentence ("verifying my top 3 on eye on you ·") with a stamped
   suffix — the only mono in the whole UI, on purpose. */
.token .phrase-prose { color: var(--ink); }
.token .phrase-sep   { color: var(--muted); margin: 0 .35rem; }
.token .phrase-code {
  /* Mono for the unique token so it reads as a stamp ("· eoy-XXXXXXXX")
     vs prose, but the colour stays on `--ink` so the phrase is one
     unbroken line of "to copy" text — accent here was visually splitting
     the sentence in two. */
  font-family: ui-monospace, "Cascadia Code", "JetBrains Mono", Consolas, monospace;
  color: var(--ink);
  font-size: .95em;
  letter-spacing: 0;
  font-weight: 500;
}
/* family 3 — ghost utility (copy, cancel, delete).
   Shared base for `.ghost` and `.ghost-danger`. The persistent hairline
   under the label is what distinguishes a ghost from a primary CTA: the
   ghost is small, sans-serif, and clearly chrome-y in a paragraph; the
   CTA is the editorial italic that breathes on hover. The danger
   modifier only changes the hover hue (ink, not accent) so the
   destructive action lands quietly. */
button.ghost,
button.ghost-danger {
  font-family: var(--ff-body);
  font-style: normal;
  font-variation-settings: normal;
  font-weight: 400;
  color: var(--muted);
  border: none;
  border-bottom: 1px solid var(--line);
  background: transparent;
  padding: .15rem .1rem .2rem;
  font-size: .85rem;
  letter-spacing: .05em;
  text-transform: lowercase;
  display: inline;
  cursor: pointer;
  transition: color .2s var(--ease), border-bottom-color .2s var(--ease);
}
button.ghost:hover:not(:disabled) {
  color: var(--accent);
  border-bottom-color: currentColor;
}
button.ghost-danger:hover:not(:disabled) {
  color: var(--danger);
  border-bottom-color: currentColor;
}
button.ghost.copied { color: var(--ink); border-bottom-color: var(--ink); }

/* Same anti-doubling logic in the verify token row: the row carries the
   dashed line, the inline `copy` button just rides on it. */
.token-row .ghost { border-bottom: none; padding-bottom: .1rem; }

/* Delete-my-data zone on the about page — its own section now, with
   space around the action so it reads as deliberate rather than
   crammed against the privacy paragraph. Compact to keep the whole
   page in one viewport. */
.delete-zone .delete-actions {
  margin-top: .3rem;
}
.delete-confirm {
  margin-top: .4rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .35rem;
}
.delete-confirm[hidden] { display: none; }
.delete-confirm > .muted { font-style: italic; margin: 0; }
.delete-confirm-buttons {
  display: flex;
  gap: 1rem;
  align-items: baseline;
}

/* "tweet it now →" sits directly under the dashed token-row separator —
   the dashed line is the visual gap; an extra big margin would create a
   second one. Keep this tight. */
.verify-actions { margin: .35rem 0 .2rem; }

/* Soft prose separator between the "tweet it" CTA and the verification
   gate — replaces the hard <hr> for a more editorial cadence. */
.verify-prompt {
  text-align: center;
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  color: var(--muted-warm);
  font-size: .95rem;
  margin: 1.6rem 0 .55rem;
  position: relative;
}
.verify-prompt::before,
.verify-prompt::after {
  content: "";
  display: inline-block;
  width: 1.4rem;
  height: 1px;
  background: var(--line);
  vertical-align: middle;
  margin: 0 .8rem;
}

/* Verify gate — column-stacked so the button is always centered, the
   error message sits beneath without pushing the button off-center. */
.verify-confirm {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .55rem;
  margin-bottom: .4rem;
}
#verify-error { margin: 0; }

.error-actions { margin-top: 1.5rem; text-align: center; }
#step-error > .error,
#step-done > .muted { text-align: center; display: block; }
#step-error .cta { margin: 1.5rem auto 0; width: max-content; }

/* A single big serif dot above "hmm." — gives the error screen weight
   and warmth without slipping into icon-iconography. */
#step-error h2 { position: relative; }
#step-error h2::before {
  content: "·";
  display: block;
  font-family: var(--ff-display);
  font-size: 4rem;
  color: var(--muted);
  line-height: .4;
  margin-bottom: .6rem;
  text-align: center;
}

/* done step — recap of the three picks + share-on-x */
.done-picks {
  list-style: none;
  margin: 1.4rem auto 0;
  padding: 0;
  max-width: 28rem;
  width: 100%;
}
.done-picks li {
  display: grid;
  grid-template-columns: 1.8rem 2rem 1fr;
  align-items: center;
  gap: .7rem;
  padding: .55rem 0;
  border-bottom: 1px solid hsl(40 6% 94%);
}
.done-picks li:last-child { border-bottom: none; }
.done-picks .rank {
  font-family: var(--ff-display);
  font-size: 1rem;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.done-picks .who { min-width: 0; display: flex; flex-direction: column; gap: .05rem; }
.done-picks .name {
  font-family: var(--ff-display);
  font-size: 1.05rem;
  letter-spacing: -.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.done-picks .handle {
  font-family: var(--ff-body);
  font-size: .85rem;
  color: var(--muted);
}
/* `.at` muted here — tabular context (see policy note above .board .at). */
.done-picks .handle .at { color: var(--muted); margin-right: .05rem; }

/* Preview of the OG card on the done step — the user sees the
   artifact they're about to share before clicking. The corner crop
   marks + italic caption are deliberately printerly: they signal
   "this is a generated image you can share", not "this is page copy". */
.done-card {
  position: relative;
  margin: 1.4rem auto 2.6rem;
  max-width: 30rem;
  width: 100%;
  padding: 12px;
  display: flex;
  justify-content: center;
}
.done-card img {
  width: 100%;
  height: auto;
  display: block;
  /* Hairline border on the image itself — the "edge of the print",
     reinforced by the corner marks just outside it. */
  border: 1px solid var(--line);
  background: var(--bg);
  transition: opacity .35s var(--ease);
}
/* Loading state — while the OG card PNG is being fetched / generated,
   reserve the figure's space (1200×630 → ~52.5% aspect) and overlay a
   typographic placeholder ("drawing your card…") that fades out the
   moment the image arrives. The img is taken out of layout with
   `position: absolute` so the figure's `aspect-ratio` can claim the
   right space; the pseudo sits in the centre with the placeholder. */
.done-card[data-loading] {
  aspect-ratio: 1200 / 630;
  width: 100%;
  max-width: 30rem;
}
.done-card[data-loading] img {
  position: absolute;
  inset: 12px;
  width: calc(100% - 24px);
  height: calc(100% - 24px);
  opacity: 0;
}
/* Higher specificity (two attributes) to beat both
   `[data-no-caption]::after { display: none }` and
   `[data-caption]::after { content: ... }` rules below in source. */
.done-card[data-loading][data-no-caption]::after,
.done-card[data-loading][data-caption]::after,
.done-card[data-loading]::after {
  content: "drawing your card…" !important;
  position: absolute;
  inset: 12px;
  display: flex !important;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  background: var(--bg);
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  color: var(--muted-warm);
  font-size: 1rem;
  letter-spacing: -.005em;
  animation: pulse-soft 1.8s var(--ease) infinite;
  /* `inset: 12px` already overrides the default rule's `bottom: -1.6rem`,
     `left: 50%`, etc. — neutralise the transform from that rule so the
     loading box doesn't get shifted off-axis. */
  transform: none !important;
}
@keyframes pulse-soft {
  0%, 100% { opacity: .55; }
  50%      { opacity: .9; }
}
/* Print-style corner marks — eight 14×1px hairlines (one horizontal +
   one vertical per corner) drawn on a single pseudo-element via stacked
   linear-gradients. The 12px padding on the parent guarantees they sit
   just outside the image edge, like a physical print's crop guides. */
.done-card::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  --c: hsl(40 6% 70%);
  background:
    linear-gradient(var(--c), var(--c)) top    left  / 14px 1px no-repeat,
    linear-gradient(var(--c), var(--c)) top    left  / 1px 14px no-repeat,
    linear-gradient(var(--c), var(--c)) top    right / 14px 1px no-repeat,
    linear-gradient(var(--c), var(--c)) top    right / 1px 14px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom left  / 14px 1px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom left  / 1px 14px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom right / 14px 1px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom right / 1px 14px no-repeat;
}
.done-card::after {
  /* Default caption — overridden via [data-caption] when the same
     `.done-card` is reused on profile pages (where the viewer might be
     looking at someone else's card and the copy needs to read "their
     card"). The [data-no-caption] hook hides the caption entirely
     while we wait to figure out which one applies. */
  content: "your card";
  position: absolute;
  bottom: -1.6rem;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  color: var(--muted-warm);
  font-size: .85rem;
  font-variation-settings: "opsz" 14;
  letter-spacing: .005em;
}
.done-card[data-caption]::after { content: attr(data-caption); }
.done-card[data-no-caption]::after { display: none; }

/* On the viewer's own profile, the figure doubles as a share button.
   Pointer cursor across the whole card, accent-tinted caption, and a
   hover lift on the caption signal that clicking does something. */
.done-card.share-trigger { cursor: pointer; }
.done-card.share-trigger::after {
  color: var(--accent);
  transition: letter-spacing .35s var(--ease), color .25s var(--ease);
}
.done-card.share-trigger:hover::after { letter-spacing: .015em; }
.done-card.share-trigger:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 12px;
}

.done-share { text-align: center; margin: 1.2rem 0 0; }
.done-secondary { text-align: center; margin: .7rem 0 0; font-size: .9rem; }
.done-secondary a { color: var(--muted); }

/* picks — clickable list */

#pick-hint {
  margin-top: -.5rem;
  margin-bottom: 1.25rem;
  text-align: center;
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-size: 1rem;
  color: var(--muted-warm);
  font-variation-settings: "opsz" 14;
}

.pick-filter {
  border-bottom: 1px solid var(--line);
  padding-bottom: .3rem;
  margin: 0 auto .5rem;
  max-width: 28rem;
  width: 100%;
  transition: border-color .2s var(--ease);
}
/* Same focus treatment as the handle-form: the wrapper carries the
   underline, so we tint it via :focus-within when the inner input is
   active. Matches every other input on the site. */
.pick-filter:focus-within { border-bottom-color: var(--accent); }
.pick-filter input {
  width: 100%;
  border: none;
  padding: .35rem 0;
  font-family: var(--ff-display);
  font-size: 1.1rem;
  letter-spacing: -0.01em;
  text-align: center;
}
.pick-filter input::placeholder { font-style: italic; }

#pick-form {
  display: flex;
  flex-direction: column;
}
.pick-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: min(50vh, 26rem);
  overflow-y: auto;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 8px, black calc(100% - 8px), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, black 8px, black calc(100% - 8px), transparent 100%);
}
.pick-list li {
  display: grid;
  grid-template-columns: 1.8rem 2rem 1fr;
  align-items: start;
  gap: .7rem;
  padding: .65rem 0;
  border-bottom: 1px solid hsl(40 6% 94%);
  cursor: pointer;
  transition: opacity .15s var(--ease);
}
.pick-list li:hover { opacity: .65; }
.pick-list li[hidden] { display: none; }

.pick-list .rank-badge {
  font-family: var(--ff-display);
  font-size: .95rem;
  color: hsl(40 6% 80%);
  text-align: right;
  padding-top: .25rem;
  font-variant-numeric: tabular-nums;
}
.pick-list li.ranked .rank-badge {
  color: var(--accent);
  font-weight: 500;
}

.pick-list .avatar-blank { /* inherits size + shape from the base .avatar rule */ }

.pick-list .who {
  min-width: 0;  /* lets ellipsis work in children */
  display: flex;
  flex-direction: column;
  gap: .05rem;
}
.pick-list .name {
  font-family: var(--ff-display);
  font-size: 1.05rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pick-list .handle {
  font-family: var(--ff-body);
  font-size: .85rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* `.at` muted here — tabular context (see policy note above .board .at). */
.pick-list .handle .at { color: var(--muted); margin-right: .05rem; }
.pick-list .bio {
  font-family: var(--ff-body);
  font-size: .85rem;
  color: var(--muted);
  margin-top: .15rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.35;
}

.pick-list li.ranked .name { color: var(--ink); }
.pick-list li.ranked { opacity: 1; }

.pick-actions {
  margin-top: 1rem;
  padding-top: .8rem;
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1.5rem;
  flex-shrink: 0;
}

.pick-picked {
  color: var(--muted-warm);
  font-family: var(--ff-display);
  font-style: italic;
  font-weight: 300;
  font-variation-settings: "opsz" 14;
  font-size: .9rem;
}

/* responsive */
@media (max-width: 520px) {
  .page {
    width: 100vw;
    height: 100vh;
    padding: 110px 1.2rem 4rem;
    border: none;   /* on phones the screen IS the frame */
  }
  /* Smaller logo on mobile — the desktop wrapper (175×110) takes ~47%
     of a 375px viewport and overlaps the page content. Shrink the
     wrapper, scale the inner image proportionally, and tighten the
     negative margins so the drawing still pins to the top-left. */
  header .logo,
  header a.logo-link {
    width: 110px;
    height: 70px;
  }
  header .logo img,
  header a.logo-link img {
    width: 200px;
    height: 200px;
    margin: -60px 0 0 -40px;
  }
  /* `.board li` has two direct children (rank + row-link) — tighten
     the rank column so the row-link gets the rest of the viewport. */
  .board li {
    grid-template-columns: 1.5rem 1fr;
    gap: .55rem;
  }
  /* `.pick-list li` has THREE children (rank-badge + avatar + .who),
     so we keep three columns but trim them. Reusing the board's 2-col
     grid here would collapse the .who block into the avatar slot —
     that was the bug breaking the mobile pick step. */
  .pick-list li {
    grid-template-columns: 1.5rem 2rem 1fr;
    gap: .55rem;
  }
  /* About page on mobile: lift the no-scroll desktop constraint —
     the prose is too long for a 375×667 viewport and overlaps the
     fixed back-pin. Vertical scroll is the natural mobile UX anyway.
     `justify-content: flex-start` overrides the desktop `center`
     so the content starts at the top instead of being pulled above
     the viewport. Generous bottom padding leaves clearance for the
     fixed back-pin. */
  .page-about {
    justify-content: flex-start;
    overflow-y: auto;
    padding: 90px 1.2rem 5rem;
  }
  /* Bottom-fade gradient behind the pin so the content scrolling
     under it dissolves into the page background instead of bleeding
     through the small text label. The gradient lives on `body::after`
     so it sits above the page-about content (z-index: 0) but below
     the pin (z-index: 10) — we get a clean band that reads as the
     page fading into its bottom edge. */
  body::after {
    content: "";
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    height: 5rem;
    background: linear-gradient(to top,
      var(--bg) 0%,
      var(--bg) 55%,
      rgba(250, 250, 247, 0) 100%);
    pointer-events: none;
    z-index: 5;
  }
  .about-pin { z-index: 10; }

  /* Slogan is already static at the desktop level; mobile just
     tightens the margin and shrinks the font so the centered column
     doesn't waste vertical room on a small viewport. */
  .page-leaderboard .subtitle {
    margin: 0 0 .9rem;
    font-size: 1.15rem;
  }

  /* Verify step on mobile — keep the phrase on a single horizontal line
     so the "post this exact phrase" instruction reads as ONE thing to
     copy, not a paragraph that wrapped randomly mid-sentence. The font
     scales with viewport via `clamp()` so it stays legible from a tiny
     320px iPhone SE up to the breakpoint, never wrapping. The copy
     button is also tightened to free up width for the phrase. */
  .token-row {
    gap: .5rem;
    max-width: 100%;
  }
  .token {
    font-size: clamp(.7rem, 3.2vw, .9rem);
    white-space: nowrap;
    min-width: 0;
    text-align: center;
  }
  .token-row .ghost {
    font-size: .78rem;
    letter-spacing: .03em;
  }
}

/* short viewports — let the page scroll within itself instead of clipping */
@media (max-height: 620px) {
  body { overflow: auto; }
  .page { height: auto; max-height: none; min-height: 100vh; }
}
