/* Member badges — what a member's own rows can prove about their climbing.
 *
 * Its own sheet, layered over console.css, for the same reasons perks.css is:
 * one self-contained component, the only other thing in the app that animates
 * for joy rather than for feedback, and keeping it separate means a badge can
 * never be the reason a console page repaints. Deliberately NOT loaded by the
 * staff console's base template — the desk is a working surface a staffer
 * stares at for eight hours, and a shimmering medal in it is noise (perks.css
 * already rules on this for the birthday confetti). The exceptions link it
 * themselves: console/badge_settings.html (which previews the medals), the
 * public home page (index.html) and the customer display (display/screen.html).
 *
 * TOKENS — read this before adding a colour. The portal loads console.css,
 * whose tokens are bare (--orange, --teal-4, --border). It NEVER loads
 * style.css, whose tokens are prefixed (--clr-orange). A bare var(--clr-orange)
 * in this file resolves to nothing and the badge renders invisible at HTTP 200
 * — the failure mode that looks like a passing page. Every colour below
 * therefore resolves console-name → --clr-name → literal hex, so the sheet is
 * correct in either shell and correct on its own.
 *
 * MOTION: A BADGE THAT HAS AN ANIMATION ALWAYS PLAYS IT. Every effect below is
 * keyed off the badge's OWN class — .bdg--sheen sweeps, .bdg--shiny twinkles,
 * .bdg--ring turns, the bat swings — so a member sees each badge the way it was
 * drawn wherever the tile is rendered: the wall, the home strip, the climb
 * celebration and the reveal stage alike.
 *
 * It used to be gated on .bdg--spotlight, one tile per page. That rule cost
 * more than it saved: a shiny the ladder had not picked sat there as a
 * differently coloured medal, a freshly earned badge never sheened at all, and
 * the REVEAL — the one screen where a member looks closely at a badge — was the
 * stillest of the lot, because the tile handed back by badge_reveal is not the
 * page's spotlight and never could be.
 *
 * The restraint that replaced it lives in the CATALOGUE, not in this sheet: most
 * of gym/badges.py carries Anim.NONE and is still by design, so a wall of twenty
 * is a few moving tiles among still ones rather than twenty shouting at once.
 * .bdg--spotlight is still stamped on the server's one pick, and it still means
 * something — but it now means BRIGHTER, not MOVING: the rocket goes to warp,
 * the flame burns harder, the gem glows. A reward, not a switch.
 *
 * Two states still cancel motion outright, and both are deliberate:
 *   - SEALED, because a shroud that flickers leaks which badge is under it;
 *   - prefers-reduced-motion, where every effect keeps a still form that still
 *     says which tier it is (see the block near the end of this file).
 */

:root {
  /* ---- Tier accents ------------------------------------------------------ */
  --clr-badge-chalk:    var(--muted,  var(--clr-muted,  #9ca3af));
  --clr-badge-grit:     var(--teal-4, var(--clr-teal-4, #2dd4bf));
  /* amber-300, not --amber #f59e0b: that is the fill weight, this is the ink
     weight, and only this one clears 4.5:1 as small text on #0b1220. */
  --clr-badge-rock:     #fcd34d;
  /* violet-300. Not a new hue in this app — #a78bfa is already the fourth
     confetti colour in perks.css, so gym-history reads as family, not as a
     colour someone bolted on. */
  --clr-badge-history:  #c4b5fd;
  --clr-badge-locked:   var(--muted,  var(--clr-muted,  #9ca3af));
  --clr-badge-flame:    var(--orange, var(--clr-orange, #f97316));

  /* ---- Alpha variants ---------------------------------------------------- */
  /* Literal rgba() rather than color-mix(): console.css writes rgba() literals
     throughout, and this sheet must not be the first thing in the app to
     require Safari 16.2. */
  --clr-badge-grit-a55:    rgba(45,212,191,.55);
  --clr-badge-grit-a28:    rgba(45,212,191,.28);
  --clr-badge-grit-a22:    rgba(45,212,191,.22);
  --clr-badge-rock-a55:    rgba(252,211,77,.55);
  --clr-badge-rock-a28:    rgba(252,211,77,.28);
  --clr-badge-history-a28: rgba(196,181,253,.28);
  --clr-badge-history-a55: rgba(196,181,253,.55);
  --clr-badge-flame-a55:   rgba(249,115,22,.55);
  --clr-badge-flame-a24:   rgba(249,115,22,.24);

  /* ---- Surfaces ---------------------------------------------------------- */
  --clr-badge-tile:      rgba(255,255,255,.035);                 /* earned: raised */
  --clr-badge-tile-lock: var(--bg, var(--clr-bg-dark, #0b1220)); /* locked: recessed */
  --clr-badge-face:      var(--bg, var(--clr-bg-dark, #0b1220));
  --clr-badge-rim:       var(--border, var(--clr-border, #374151));
  --clr-badge-sheen:     rgba(255,255,255,.16);

  /* ---- Grade-derived ------------------------------------------------------
     Stamped inline from Route.GRADE_BANDS by the existing grade_style filter.
     Templates never re-map a grade hex — gym/templatetags/grades.py owns that
     mapping and this sheet only consumes what it stamps. */
  --badge-band:      transparent;
  --badge-band-ink:  var(--text, var(--clr-text, #f3f4f6));
  /* Band 7 "Black" is #18181b — 1.21:1 against the card. Without a rim it is
     not a dark medallion, it is a hole in the page. Applied to EVERY grade
     medallion rather than only to Black, so the rule cannot be forgotten the
     day a ninth band is added. */
  --badge-band-rim:  rgba(255,255,255,.28);

  /* ---- Geometry ---------------------------------------------------------- */
  /* --hold-scale IS PAIRED WITH --badge-size AND MOVES WITH IT, the same way
     --badge-orbit-r does and for the same reason. It is the factor the grade
     hold on the project badge is scaled by, and it is UNITLESS on purpose:
     `scale()` takes a NUMBER, and `calc(var(--badge-size) * .006)` is a LENGTH,
     which makes the whole transform invalid — so the browser silently drops the
     positioning along with it and the hold lands full-size in the wrong place.
     That presents as a placement bug, not a units bug, which is why it is
     written here. Dividing by 1px yields a real number but wants Safari 16.4,
     and this sheet must not be the first thing in the app to demand 16.2. So it
     is declared, at .006 of the size, at all six places --badge-size is. */
  --badge-size:      56px;
  --hold-scale:    0.336;
  --badge-size-sm:   40px;
  --badge-rim-w:     2px;
  --badge-tile-min:  104px;   /* → 3 across at 360px, the narrowest real phone */
  --badge-gap:       .6rem;
  --badge-orbit-r:   32px;
  --badge-hex: polygon(50% 0%, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%);

  /* ---- Timing ------------------------------------------------------------ */
  --badge-ease:        cubic-bezier(.4, 0, .2, 1);
  --badge-sheen-dur:   9s;
  --badge-ring-dur:    28s;
  --badge-aurora-dur:  11s;
  --badge-flicker-dur: 3.7s;
  --badge-orbit-dur:   6.4s;
  --badge-holo-dur:    14s;
}

/* ==========================================================================
 * Grid
 * ========================================================================== */

.bdg-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--badge-tile-min), 1fr));
  gap: var(--badge-gap);
  margin-top: .9rem;
}

/* Section heading inside the badge page — quieter than an <h2>, because the
   families are a way to scan, not six separate announcements. */
.bdg-family {
  margin-top: 1.5rem;
  font-size: .78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--muted, #9ca3af);
}
.bdg-family:first-of-type { margin-top: .5rem; }

/* ==========================================================================
 * Tile
 * ========================================================================== */

.bdg {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .4rem;
  padding: .8rem .5rem .7rem;
  border-radius: 14px;
  border: 1px solid var(--clr-badge-rim);
  background: var(--clr-badge-tile);
  text-align: center;
  /* Runs once and ends. Staggered by the --i the template stamps, the same
     mechanism the confetti already uses. */
  animation: bdg-rise .38s var(--badge-ease) both;
  animation-delay: calc(var(--i, 0) * 34ms);
}

/* THE BARE COIN — the medallion with the tile taken off it, for places that
   already have their own words. The Week streak stat tile on the portal home
   page is the first: it prints the number and the sentence itself, so a second
   name, sentence and tier word underneath the fire would say everything twice.

   Scaled with a TRANSFORM, never by re-sizing --badge-size. Every rim, hexagon
   and orbit radius in this sheet is px against that token, so changing it would
   rebuild the badge at a different shape instead of shrinking the one they
   earned — the same rule the reveal stage follows, for the same reason.

   The box is the SCALED size while the mount inside it keeps its full 56px and
   is squeezed to fit, which is what keeps the wildfire crown — the one rung
   that deliberately burns outside its own medallion — from being clipped. */
.bdg--coin {
  --coin-scale: .62;
  /* THE ORBIT RADIUS IS TUNED AS A PAIR WITH --badge-size AND HAS TO BE PINNED
     WITH IT. Every :root block in this sheet moves the two together — 50/29,
     56/32, 68/38, 78/44 — because the dot is meant to clear the medallion's own
     edge by about 4px and read as a moon going round it.

     A coin PINS --badge-size (every caller does: .fly__mark, .cmty-chip__icon,
     .fly-lead__mark and the season board all pin 56px) and until now inherited
     --badge-orbit-r from :root, which on a wide desktop is 44px — the value for
     a 78px tile. Around a 56px mount that put the dot 16px past the edge, so
     "First in the book" flew a gold speck orbiting well outside its own badge
     and, on a leaderboard row, out over the rank number beside it.

     Derived rather than pinned to a number, so it stays correct at whatever
     size a caller pins and there is no fifth place to update. */
  --badge-orbit-r: calc(var(--badge-size) / 2 + 4px);
  display: grid;
  place-items: center;
  width:  calc(var(--badge-size) * var(--coin-scale));
  height: calc(var(--badge-size) * var(--coin-scale));
  padding: 0;
  gap: 0;
  border: 0;
  background: none;
  overflow: visible;
  /* The tile's entrance rise belongs to a grid of tiles arriving together; a
     single coin sitting next to a number has nothing to arrive with. */
  animation: none;
}

/* CENTRED BY ABSOLUTE POSITION, NOT BY THE GRID. The mount keeps its full
   --badge-size (78px on a wide desktop) inside a box scaled to .62 of it, so it
   is always LARGER than the area it sits in. An oversized grid item does not
   honour `place-items: center` the way a fitting one does — Chrome lays it out
   from the content-box start instead — so the drawing came out low and to the
   right of its own box, over the caption underneath it.

   top/left 50% puts the mount's corner on the coin's centre; the -50% translate
   pulls it back by half its own 78px border box (percentages here resolve
   against the UNSCALED box, which is what makes this exact); the scale then
   shrinks it about that same centre. Net: the drawn coin is concentric with its
   own box whatever the padding around it happens to be — which is the property
   the grid was being asked for and could not give. */
.bdg--coin .bdg__mount {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(var(--coin-scale));
  transform-origin: center;
}

@keyframes bdg-rise {
  from { opacity: 0; transform: translateY(7px); }
}

.bdg__mount {
  position: relative;
  width: var(--badge-size);
  height: var(--badge-size);
  display: grid;
  place-items: center;
}

.bdg__face {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  overflow: hidden;                     /* clips the sheen sweep to the face */
  display: grid;
  place-items: center;
  background: var(--clr-badge-face);
  border: var(--badge-rim-w) solid transparent;
}

.bdg__icon {
  font-size: 1.5rem;
  line-height: 1;
  position: relative;
  z-index: 2;                           /* above sheen/aurora pseudos */
}

.bdg__name {
  font-size: .8rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--text, #f3f4f6);
}

.bdg__meta {
  font-size: .68rem;
  color: var(--muted, #9ca3af);
  line-height: 1.25;
}

/* The tier word. Colour is never the only carrier of tier — this is. */
.bdg__tier {
  font-size: .6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .1em;
}

/* ==========================================================================
 * Tiers — four redundant channels: silhouette, rim, hue, printed word.
 * No single channel is load-bearing, so the system survives greyscale,
 * colour-blindness, forced-colors, and a 3-across phone grid.
 * ========================================================================== */

/* Chalk — most people who climb here. No rim: "common" must look common, or
   the rarer tiers have nothing to be rarer than. */
.bdg--chalk .bdg__face { border-color: transparent; }
.bdg--chalk .bdg__tier { color: var(--clr-badge-chalk); }

/* Grit — you keep coming back. */
.bdg--grit .bdg__face {
  border-width: 1px;
  border-color: var(--clr-badge-grit-a55);
  background:
    radial-gradient(80% 60% at 50% 0%, var(--clr-badge-grit-a22), transparent 70%),
    var(--clr-badge-face);
}
.bdg--grit .bdg__tier { color: var(--clr-badge-grit); }

/* Rock — few people here hold this. Hexagon: this is the glance cue, and it is
   the one that survives every accessibility mode. */
.bdg--rock .bdg__face {
  border: 0;
  border-radius: 0;
  clip-path: var(--badge-hex);
  background:
    radial-gradient(80% 60% at 50% 0%, var(--clr-badge-rock-a28), transparent 70%),
    var(--clr-badge-face);
}
/* clip-path cuts the border off, so the rim is drawn as a slightly larger
   hexagon behind the face rather than as a border on it. */
.bdg--rock .bdg__mount::before,
.bdg--history .bdg__mount::before {
  content: "";
  position: absolute;
  inset: -2px;
  clip-path: var(--badge-hex);
  background: var(--clr-badge-rock);
  z-index: 0;
}
.bdg--rock .bdg__face,
.bdg--history .bdg__face { z-index: 1; }
.bdg--rock .bdg__tier { color: var(--clr-badge-rock); }

/* Gym history — happens once. Hexagon plus a halo no other tier has. */
.bdg--history .bdg__mount::before { background: var(--clr-badge-history); }
.bdg--history .bdg__face {
  border: 0;
  border-radius: 0;
  clip-path: var(--badge-hex);
  background: var(--clr-badge-face);
}
.bdg--history .bdg__tier { color: var(--clr-badge-history); }

/* ==========================================================================
 * ARTWORK — drawn behind the icon, per badge.
 *
 * A bare rocket is a rocket on any badge in any app. What makes it the Rainbow
 * Rocket is that it is GOING somewhere — the gym's big dyno, straight up.
 *
 * This replaced a rainbow ARCH, and the arch was wrong in a way worth writing
 * down because the mistake is easy to make twice. An arch is a shape that goes
 * up and comes back down. The teaser line in badges.py says "the big dyno,
 * straight up." The artwork was contradicting the sentence underneath it. The
 * same six colours turned through ninety degrees stopped being weather and
 * became exhaust.
 * ========================================================================== */

.bdg__art {
  position: absolute;
  left: 50%;
  pointer-events: none;
  z-index: 1;                 /* above the face, below the icon (z-index: 2) */
}

/* ---- rocket-launch: the Rainbow Rocket's contrail --------------------------
 *
 * A flare, not a beam: narrow at the nozzle and spreading as it falls away,
 * which is what stops six stacked bands reading as a flag. Hot at the top
 * where the engine is, cooling to violet at the tail — that ordering is what
 * makes the eye read the shape as MOVING rather than as a striped block parked
 * underneath a rocket.
 *
 * Flat, saturated, hard-edged — a child's-drawing rainbow, not a soft glow.
 * The bands are the full primary spectrum rather than the portal's UI palette:
 * a rainbow rendered in teal-400 and amber-300 reads as a tasteful gradient,
 * which is precisely what it must not be. It is the one place in this sheet
 * that deliberately ignores the house tokens.
 * -------------------------------------------------------------------------- */

.bdg-art--rocket-launch .bdg__art {
  width: 17px;
  height: 26px;
  bottom: 6px;
  transform: translateX(-50%);
  transform-origin: 50% 0;    /* it stretches from the nozzle, not the middle */
  clip-path: polygon(41% 0%, 59% 0%, 100% 100%, 0% 100%);
  background: linear-gradient(to bottom,
      #e5202e 0 17%,
      #f57c20 17% 34%,
      #fdc82f 34% 50%,
      #22a63f 50% 67%,
      #2264cc 67% 84%,
      #7a4fa3 84% 100%);
  /* Faded at the tail, and this is the whole difference between a trail and a
     traffic cone. A solid trapezoid is an OBJECT the rocket is sitting on; the
     same trapezoid dispersing as it falls away is exhaust it is leaving
     behind. Drawn solid on the first pass, and it read as a party hat. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0 34%, rgba(0,0,0,.62) 72%, rgba(0,0,0,0) 100%);
  mask-image: linear-gradient(to bottom, #000 0 34%, rgba(0,0,0,.62) 72%, rgba(0,0,0,0) 100%);
  filter: drop-shadow(0 0 5px rgba(255,255,255,.14));
  /* Engine throb. Short, small, and on its own period so it never syncs with
     the sky below — two rhythms read as a machine working, one reads as a
     pulse someone put on a div. */
  animation: bdg-thrust .46s ease-in-out infinite alternate;
}

@keyframes bdg-thrust {
  to { transform: translateX(-50%) scaleY(1.07); opacity: .9; }
}

/* On the shiny the face is ALREADY a rainbow, so a rainbow flare on top of it
   is mud. The flare becomes white light instead — same shape, same meaning,
   and it reads as the rocket's own thrust against the foil. */
.bdg.bdg--shiny.bdg-art--rocket-launch .bdg__art {
  background: linear-gradient(to bottom,
      rgba(255,255,255,.97) 0 24%,
      rgba(255,255,255,.66) 24% 58%,
      rgba(255,255,255,.28) 58% 100%);
  filter: drop-shadow(0 0 6px rgba(255,255,255,.8));
}

/* ---- the sky it is flying through -----------------------------------------
 *
 * MOTION EXCEPTION, the third in this sheet and the one that cost the most
 * thought. The house rule — one moving thing, and the SERVER picks it — is
 * what keeps a wall of twenty medals readable, and every exception spends a
 * little of that.
 *
 * The argument is the flame's argument and it is no weaker here: this badge's
 * SUBJECT is motion. It is the gym's big dyno. A rocket parked on a static
 * starfield is not restraint, it is a contradiction, in the same way a flame
 * that does not flicker is a leaf. It is also exactly one badge, held only by
 * whoever has stuck one particular problem.
 *
 * TWO GEARS, which is what keeps the exception cheap: a slow drift always, and
 * warp speed only when the tile wins the spotlight. A grid of these at full
 * speed is the migraine the rule exists to prevent; a grid of these drifting
 * is a grid of rockets in flight, which is the point.
 *
 * THE ROCKET DOES NOT MOVE. The sky does. That is how every side-scroller ever
 * written sells speed, and it is also why the drawing stays perfectly crisp
 * while the tile reads as travelling.
 *
 * Painted on .bdg__face::before rather than into the face's own background,
 * because the face background is where the TIER lives — the rock glow, the
 * gym-history plate, the shiny's foil — and a sky that replaced it would
 * flatten all three into one. ::before is free here: it belongs to `aurora`,
 * and a signature climb is never aurora.
 *
 * Six layers, three of stars and three of speed lines, each on its own tile
 * size. Every layer travels its OWN tile height per cycle, so the big tiles
 * move furthest and read as nearest — which is why the near layers are also
 * the brightest and the fattest. That is the whole parallax, and it costs one
 * keyframe and no elements.
 * -------------------------------------------------------------------------- */

.bdg-art--rocket-launch .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    /* speed lines, near to far */
    linear-gradient(to bottom, rgba(255,255,255,0) 0 46%, rgba(255,255,255,.46) 46% 66%, rgba(255,255,255,0) 66%),
    linear-gradient(to bottom, rgba(255,255,255,0) 0 22%, rgba(255,255,255,.32) 22% 40%, rgba(255,255,255,0) 40%),
    linear-gradient(to bottom, rgba(255,255,255,0) 0 62%, rgba(255,255,255,.20) 62% 78%, rgba(255,255,255,0) 78%),
    /* stars, near to far. Three tile sizes with no common factor, so the grids
       never line up and the field reads as scattered rather than as a lattice. */
    radial-gradient(circle closest-side at 30% 22%, rgba(255,255,255,.95) 0 8%, rgba(255,255,255,0) 8%),
    radial-gradient(circle closest-side at 68% 54%, rgba(255,255,255,.7) 0 7%, rgba(255,255,255,0) 7%),
    radial-gradient(circle closest-side at 44% 78%, rgba(255,255,255,.5) 0 7%, rgba(255,255,255,0) 7%);
  background-size:
    1.5px 36px, 1.3px 30px, 1.1px 24px,
    37px 37px, 27px 27px, 19px 19px;
  background-repeat:
    repeat-y, repeat-y, repeat-y,
    repeat, repeat, repeat;
  background-position:
    23% 0, 70% 0, 46% 0,
    0 0, 0 0, 0 0;
  animation: bdg-flyby 3.4s linear infinite;
}

/* Each layer scrolls by exactly one tile, which is what makes the loop
   seamless — land on anything else and the field visibly jumps once a cycle. */
@keyframes bdg-flyby {
  to {
    background-position:
      23% 36px, 70% 30px, 46% 24px,
      0 37px, 0 27px, 0 19px;
  }
}

/* Warp speed, and only here. Same layers, same distances, a third of the time
   — so winning the spotlight makes the rocket ACCELERATE rather than switching
   its engine on, which is the difference between a reward and a toggle. */
.bdg--spotlight.bdg-art--rocket-launch .bdg__face::before,
.bdg-reveal__badge .bdg-art--rocket-launch .bdg__face::before { animation-duration: 1.15s; }
.bdg--spotlight.bdg-art--rocket-launch .bdg__art,
.bdg-reveal__badge .bdg-art--rocket-launch .bdg__art          { animation-duration: .3s; }

/* On the foil the sky has to back off: white stars over a full-saturation
   rainbow are noise, and the foil is already the loudest thing on the tile. */
.bdg.bdg--shiny.bdg-art--rocket-launch .bdg__face::before { opacity: .55; }

/* ---- bathold: the hold the bat hangs off ----------------------------------
 * A moulded rail, not a cave roof. This is a climbing badge and the bat is
 * hanging off the same plastic everybody else is pulling on. Flat rather than
 * domed: a dome up here with a dark shape under it is a mushroom, which is
 * what three drafts of the bat turned out to be.
 *
 * Drawn as ART and the bat as a MARK, which is not filing tidiness — it is
 * what lets the bat swing on the spotlight without dragging the hold with it.
 * -------------------------------------------------------------------------- */
.bdg-art--bathold .bdg__art {
  /* NARROW — narrower than the bat's own shoulders, and that is the whole
     spec. Drawn wide, a grey ellipse sitting above a dark shape that widens
     downward is a flying saucer over a tractor beam, which is the fourth thing
     this badge accidentally became. A hold the bat is bigger than reads as a
     hold. */
  width: 13px;
  height: 4.5px;
  top: 9px;
  transform: translate(-50%, -50%);
  border-radius: 30% 70% 38% 62% / 58% 54% 46% 42%;
  background: linear-gradient(158deg, #7a8397 0 34%, #545c6e 34% 68%, #383f4e 68%);
  box-shadow: 0 1px 2px rgba(0,0,0,.55), inset 0 .5px 0 rgba(255,255,255,.16);
}

/* ---- kickflip-arc: the air the board came up through ------------------------
 *
 * ONE stroke, under the board, gone at both ends.
 *
 * IT REPLACED A DASHED ELLIPSE RINGED RIGHT ROUND THE DECK, and that draft is
 * worth recording because its reasoning was sound and the drawing was still
 * wrong. The argument went: a solid ring is an object the board is sitting
 * inside — a hoop, a plate, a planet's ring — where a broken one is a motion
 * line, which is the oldest convention in drawing for "this is going round".
 * All true, and all irrelevant at this size. A 1.5px dash pattern on a 44px
 * ellipse resolves to about twenty ticks two pixels long, and twenty two-pixel
 * ticks are not a convention, they are litter. Worse, the ring still enclosed
 * the board, which is the exact reading the dashes were spent preventing:
 * whatever the CSS meant, the tile showed a skateboard in a hoop.
 *
 * So the ring lost to a single arc that never closes and never crosses the
 * deck. It sits BELOW the board, clear of the wheels, masked with a smooth
 * ramp instead of a dash pattern — brightest under the nose, where the board
 * has just been, and faded to nothing by the tail. That is the ruling the
 * rocket's contrail already won, applied again: a complete shape is a thing,
 * an incomplete one is a movement.
 *
 * Under the board rather than around it is also the only honest place for it.
 * A kickflip turns about the deck's LONG axis, and a circle drawn in that
 * plane projects to a straight line when you stand side-on — so the old
 * ellipse was a diagram of a rotation nobody can see from where the camera is.
 * This arc claims less and earns it: it is the air the board came up through,
 * and air is something a side-on view can actually show.
 * -------------------------------------------------------------------------- */
.bdg-art--kickflip-arc .bdg__art {
  width: 44px;
  height: 27px;
  top: 45%;
  /* Flatter than the deck's own -26deg on purpose. Struck at the same angle
     the arc reads as a second board; twisted off it, it reads as ground the
     board has left. */
  transform: translate(-50%, -50%) rotate(-20deg);
  border-radius: 50%;
  border: 1.6px solid rgba(232, 228, 220, .60);
  /* The whole ellipse is drawn and then all but the bottom eighty degrees of
     it is masked away, which is why this is one border rather than a hand-cut
     shape: a border is a stroke of CONSTANT width, and a constant width is
     what makes a curve read as one continuous stroke rather than as a wedge. */
  -webkit-mask-image: conic-gradient(from 90deg,
      rgba(0,0,0,0) 0 8%, rgba(0,0,0,.14) 15%, #000 34% 39%, rgba(0,0,0,0) 45%);
  mask-image: conic-gradient(from 90deg,
      rgba(0,0,0,0) 0 8%, rgba(0,0,0,.14) 15%, #000 34% 39%, rgba(0,0,0,0) 45%);
}

/* On the foil the chalk line sinks into a full-saturation rainbow, so it goes
   to near-white — same shape, same meaning. */
.bdg.bdg--shiny.bdg-art--kickflip-arc .bdg__art {
  border-color: rgba(255, 255, 255, .80);
}

/* boltholes: DELETED 30 August 2026, with the peeltag drawing it stood behind.
 * "Last ascent" is now the wall strip and the climber on it, and the bolt holes
 * survive INSIDE that drawing — they were the best thing in the deleted one, and
 * the recipe is unchanged there: a dark disc with the light crescent BELOW it,
 * because a hole is lit on its far side and a sticker is lit on its near one.
 * Removed rather than left orphaned: nothing names this art any more. */

/* A locked fixture is a thing you have not done yet. Nothing about it moves,
   and the artwork greys out with the mark rather than flying at somebody who
   has not earned it. */
.bdg--locked .bdg__art { filter: grayscale(1); opacity: .3; animation: none; }
.bdg--locked.bdg-art--rocket-launch .bdg__face::before {
  filter: grayscale(1);
  opacity: .18;
  animation: none;
}

/* ==========================================================================
 * SHINY — the collector's edition.
 *
 * The same badge, in its other colourway: same silhouette, same icon, same
 * name. A shiny is NOT a second, better badge — it is proof you got this one
 * the hard way, and the appeal is that another climber recognises it instantly.
 *
 * Placed AFTER the tier rules on purpose. The tier selectors are two classes
 * deep (`.bdg--history .bdg__face`), so these are written three deep
 * (`.bdg.bdg--shiny .bdg__face`) rather than relying on source order alone.
 * A single-class rule here would silently lose to the tier's flat face and the
 * shiny would render as an ordinary badge — the same specificity trap that
 * already ate the grade medallions once.
 *
 * The rainbow is not decoration picked at random: the climb this ships for is
 * the Rainbow Rocket, and a holographic sweep is what a foil collectible looks
 * like. It is STILL by default — the material is what marks it out, not
 * motion, so the one-moving-thing rule survives intact. Sparkles are the
 * reward for winning the spotlight, and the spotlight ladder ranks a shiny
 * above any ordinary recent badge, so in practice the member who owns one sees
 * it sparkle.
 * ========================================================================== */

.bdg.bdg--shiny .bdg__face {
  background:
    conic-gradient(from 210deg,
      #f97316 0deg, #fbbf24 52deg, #34d399 108deg, #22d3ee 160deg,
      #60a5fa 212deg, #a78bfa 264deg, #f472b6 312deg, #f97316 360deg);
  /* The icon has to stay legible on top of a full-saturation rainbow, so the
     gradient is knocked back behind a dark scrim rather than being toned down
     — which would lose the foil look entirely.
     Held at .5 rather than lower: the standard edition carries a vivid rainbow
     arch, and a heavier scrim here left the SHINY looking flatter than the
     badge it is supposed to be the rare version of. The hierarchy has to read
     the right way round at a glance. */
  box-shadow:
    inset 0 0 0 100px rgba(11, 18, 32, .5),
    inset 0 1px 0 rgba(255, 255, 255, .35),
    inset 0 -1px 0 rgba(0, 0, 0, .5);
}

/* The rim: a brighter slice of the same rainbow, so the hexagon or circle edge
   reads as foil rather than as the tier colour it would otherwise wear. */
.bdg.bdg--shiny .bdg__mount::before {
  background: conic-gradient(from 210deg,
    #f97316, #fbbf24, #34d399, #22d3ee, #60a5fa, #a78bfa, #f472b6, #f97316);
}
.bdg.bdg--shiny.bdg--chalk .bdg__face,
.bdg.bdg--shiny.bdg--grit .bdg__face {
  /* Circle tiers have no hexagon plate behind them, so their rim is a real
     border and has to be recoloured directly. */
  border-color: rgba(255, 255, 255, .55);
}

/* The word under a shiny says what it is. Colour alone must never be the
   carrier — and "SHINY" is the thing a collector actually wants to see. */
.bdg.bdg--shiny .bdg__tier {
  background: linear-gradient(92deg, #fbbf24, #34d399, #22d3ee, #a78bfa, #f472b6);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-weight: 800;
}
/* Fallback for anything that can't paint text through a gradient: without this
   the label is transparent-on-dark, i.e. invisible. */
@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  .bdg.bdg--shiny .bdg__tier { color: #fbbf24; background: none; }
}

/* Sparkles. Real elements rather than pseudos because the face and the mount
   have both already spent theirs on the tier and animation layers — and
   because this is exactly the trick the birthday confetti already uses: empty
   spans whose whole behaviour is derived in CSS from the --s the template
   stamps. Nothing random, so every render is identical and a test can count
   them. */
/* Their own wrapper, so :nth-child counts SPARKS and not the face element that
   shares .bdg__mount with them — an off-by-one there would stack all three
   twinkles on the same coordinates. */
.bdg__sparks {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
}

.bdg__spark {
  position: absolute;
  width: 6px;
  height: 6px;
  top: 50%;
  left: 50%;
  margin: -3px 0 0 -3px;
  pointer-events: none;
  opacity: 0;
  /* A four-pointed twinkle, not a dot: a circle at this size just reads as
     dust on the screen. */
  clip-path: polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%);
  background: #fff;
  transform: translate(var(--sx, 0), var(--sy, 0)) scale(0);
  z-index: 3;
}
.bdg__spark:nth-child(1) { --sx: -15px; --sy: -12px; }
.bdg__spark:nth-child(2) { --sx:  16px; --sy:  -6px; }
.bdg__spark:nth-child(3) { --sx:  -6px; --sy:  15px; }

/* On EVERY shiny, not only the spotlight. A collector's item nobody ever sees
   sparkle is just a differently coloured medal, and a member who flashed a
   grade a month ago should still catch it twinkling.

   The duty cycle is what pays for that: the twinkle occupies 16% of a 3.4s
   cycle, and the three spans are staggered 1.1s apart, so even a row of shinies
   is a glint here and a glint there rather than a field of strobing dots. */
.bdg--shiny .bdg__spark {
  animation: bdg-spark 3.4s ease-in-out infinite;
  animation-delay: calc(var(--s, 0) * 1.1s);
}

@keyframes bdg-spark {
  0%, 72%, 100% { opacity: 0; transform: translate(var(--sx), var(--sy)) scale(0) rotate(0deg); }
  80%           { opacity: 1; transform: translate(var(--sx), var(--sy)) scale(1) rotate(45deg); }
  88%           { opacity: 0; transform: translate(var(--sx), var(--sy)) scale(.4) rotate(90deg); }
}

/* ==========================================================================
 * engrave — permanence. The deliberate opposite of sheen: this is OLD and it
 * is YOURS. Free (paint-once), and it is the default treatment for every Rock
 * and Gym-history badge that is NOT the spotlight — which is how a rare badge
 * stays visibly rare while standing perfectly still.
 * ========================================================================== */

.bdg__face--engraved {
  box-shadow:
    inset 0  1px 0 rgba(255,255,255,.09),
    inset 0 -1px 0 rgba(0,0,0,.55),
    inset 0  0   0 1px rgba(255,255,255,.05);
}
.bdg--rock .bdg__name,
.bdg--history .bdg__name {
  text-shadow: 0 1px 0 rgba(255,255,255,.10), 0 -1px 0 rgba(0,0,0,.65);
}

/* ==========================================================================
 * Animations. Every one is CSS-only — the CSP blocks inline handlers (a nonce
 * covers script elements, never on* attributes; config/middleware.py), so
 * nothing here may depend on JS.
 * ========================================================================== */

/* ---- sheen: freshly earned. NEW, not RARE. ------------------------------ */
/* Duty cycle is the whole design: the sweep occupies 18% of a 9s cycle — 1.6s
   of movement, 7.4s of stillness. A sheen that sweeps continuously is a strobe,
   and the eye stops reading the tile underneath it. */
.bdg--sheen .bdg__face::after,
.bdg--spotlight .bdg__face::after,
.bdg-reveal__badge .bdg__face::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg,
      transparent 38%, var(--clr-badge-sheen) 48%, transparent 58%);
  transform: translateX(-140%);
  animation: bdg-sheen var(--badge-sheen-dur) var(--badge-ease) infinite;
  /* STAGGERED off the --i the template already stamps for the entrance, the
     same lever the sparkles use for --s. This matters now that more than one
     tile can sheen: nine badges sharing a 9s cycle and a flat 1.2s delay sweep
     in perfect lockstep, and a row of medals flashing together every nine
     seconds reads as the PAGE blinking rather than as badges catching the
     light. .9s per tile takes the row past a full cycle, so no two neighbours
     are ever in phase. */
  animation-delay: calc(1.2s + var(--i, 0) * .9s);
  z-index: 1;
}

@keyframes bdg-sheen {
  0%   { transform: translateX(-140%); }
  18%  { transform: translateX(140%); }
  100% { transform: translateX(140%); }
}

/* ---- ring: the rarest material, and the only continuously-moving HELD
 * element in the system. Rotate the ELEMENT, never the conic's `from` angle:
 * animating the angle needs @property registration and repaints the whole
 * gradient every frame. Rotating the box is composited. ------------------- */
.bdg--ring .bdg__mount::after {
  content: "";
  position: absolute;
  inset: -7px;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
      transparent 0 38%,
      var(--clr-badge-history) 50%,
      transparent 62% 100%);
  /* An annulus, not a disc — the mask punches the middle out so it reads as a
     ring AROUND the hexagon rather than a glow behind it. */
  -webkit-mask: radial-gradient(closest-side, transparent 0 72%, #000 74%);
          mask: radial-gradient(closest-side, transparent 0 72%, #000 74%);
  animation: bdg-ring var(--badge-ring-dur) linear infinite;
  pointer-events: none;
}

@keyframes bdg-ring { to { transform: rotate(1turn); } }

/* ---- aurora: rarity as MATERIAL, not as event. Something with depth in it.
 * EXPENSIVE — a blurred layer repaints on every frame it changes size. The
 * blur box is ~95px and never the tile, and at most one gym-history badge is
 * on screen in practice, so it is paid once. --------------------------------*/
.bdg--aurora .bdg__face::before {
  content: "";
  position: absolute;
  inset: -30%;
  background:
    radial-gradient(38% 38% at 30% 30%, var(--clr-badge-history) 0%, transparent 70%),
    radial-gradient(34% 34% at 72% 62%, var(--clr-badge-grit)    0%, transparent 68%),
    radial-gradient(30% 30% at 55% 85%, var(--clr-badge-rock)    0%, transparent 70%);
  filter: blur(9px);
  opacity: .5;
  animation: bdg-aurora var(--badge-aurora-dur) ease-in-out infinite alternate;
}

@keyframes bdg-aurora {
  from { transform: translate3d(-6%, -4%, 0) scale(1); }
  to   { transform: translate3d( 6%,  5%, 0) scale(1.12); }
}

/* ---- flicker: a live streak. Pairs with the 🔥 already on the stats card. -*/
.bdg--flicker .bdg__mount::after {
  content: "";
  position: absolute;
  inset: -10px;
  border-radius: 50%;
  background: radial-gradient(closest-side,
      var(--clr-badge-flame-a55) 0%, transparent 72%);
  pointer-events: none;
  animation: bdg-flicker var(--badge-flicker-dur) ease-in-out infinite;
}

/* Irregular stops are what separate a flame from a sine pulse. Evenly spaced
   keyframes read as a notification badge asking to be tapped. */
@keyframes bdg-flicker {
  0%, 100% { opacity: .42; transform: scale(1); }
  22%      { opacity: .78; transform: scale(1.06); }
  35%      { opacity: .50; transform: scale(1.01); }
  61%      { opacity: .92; transform: scale(1.09); }
  78%      { opacity: .55; transform: scale(1.03); }
}

/* An at-risk streak breathes QUICKER, never REDDER. member_stats.py is
   explicit that a down month is muted grey and not red — "this is a member's
   own leisure, not a business KPI". The same ruling applies to a streak about
   to lapse: hurry it, don't scold it. */
.bdg--at-risk { --badge-flicker-dur: 2.6s; }

/* ---- orbit: singularity. A single point circling an object says "one". ----*/
.bdg--orbit .bdg__mount::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: var(--clr-badge-rock);
  box-shadow: 0 0 7px 1px var(--clr-badge-rock-a55);
  animation: bdg-orbit var(--badge-orbit-dur) linear infinite;
  pointer-events: none;
}

/* The trailing counter-rotate holds the dot's own orientation fixed. Moot for
   a circle; load-bearing the day it becomes a spark or a chalk fleck. */
@keyframes bdg-orbit {
  from { transform: rotate(0turn) translateX(var(--badge-orbit-r)) rotate(0turn); }
  to   { transform: rotate(1turn) translateX(var(--badge-orbit-r)) rotate(-1turn); }
}

/* ---- holo: a physical object you can turn over in your hand. --------------
 *
 * THE FOIL IS ALWAYS THERE AND ALWAYS DRIFTING. It is a material, and a
 * material does not switch on.
 *
 * The whole of this used to live inside the hover query below, starting at
 * `opacity: 0` — which meant the four Anim.HOLO badges were completely inert
 * on the surface this actually ships to. A phone has no hover at all, so a
 * member holding one had a still tile wearing a plain tier face and no way of
 * ever finding out it was supposed to be foil. That is the exact failure the
 * header is about: an effect nobody ever sees is the same as no effect.
 *
 * Drifted by BACKGROUND-POSITION rather than by moving the layer. It is the one
 * place in this sheet that pays for a repaint instead of compositing, and it is
 * paid knowingly: the box is 56px, and the hover state below was already
 * transitioning this same property — one mechanism rather than two. Slow (14s)
 * and shallow, so it reads as light crossing foil rather than as something
 * asking to be tapped.
 *
 * CSS alone cannot track a cursor — there are no pointer coordinates without
 * JS, and the CSP forbids inline handlers. So the TILT stays an honest
 * tilt-on-hover, not a cursor-following one, and it is layered on top of the
 * drift for the desks and laptops that have a pointer at all.
 * -------------------------------------------------------------------------- */
.bdg--holo .bdg__face::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: repeating-linear-gradient(115deg,
      var(--clr-badge-grit-a28)    0    12px,
      var(--clr-badge-history-a28) 12px 24px,
      var(--clr-badge-flame-a24)   24px 36px,
      var(--clr-badge-grit-a28)    36px 48px);
  background-size: 260% 260%;
  mix-blend-mode: color-dodge;
  /* Under color-dodge this is already a bright layer. .3 is the weight at which
     the foil reads as foil while the icon on top of it stays legible — the same
     judgement the shiny's inset scrim makes about its rainbow. */
  opacity: .3;
  z-index: 1;
  animation: bdg-holo var(--badge-holo-dur) ease-in-out infinite alternate;
}

/* Never the full 0%→100%: the corners of a 260% background are the flattest
   part of it, and a sweep that reaches them reads as a wipe rather than as a
   card being turned. */
@keyframes bdg-holo {
  from { background-position: 14% 30%; }
  to   { background-position: 86% 70%; }
}

@media (hover: hover) and (pointer: fine) {
  .bdg--holo { transition: transform .22s var(--badge-ease); }
  .bdg--holo:hover { transform: perspective(600px) rotateX(6deg) rotateY(-8deg); }
  .bdg--holo .bdg__face::after { transition: opacity .18s; }
  /* Turning it over in your hand: the foil brightens and the drift HOLDS where
     the member caught it, rather than sliding on under the cursor — which is
     the difference between picking something up and watching it. */
  .bdg--holo:hover .bdg__face::after {
    opacity: .5;
    animation-play-state: paused;
  }
}

/* ==========================================================================
 * Locked — progress toward a HELD badge. A locked MOMENT is never rendered:
 * "Back on the wall — locked, take 8 weeks off" is grotesque.
 * ========================================================================== */

.bdg--locked {
  background: var(--clr-badge-tile-lock);   /* recessed, not raised */
  border-style: dashed;
  border-color: rgba(156,163,175,.28);
}
.bdg--locked .bdg__icon { filter: grayscale(1); opacity: .45; }
.bdg--locked .bdg__name { color: var(--muted, #9ca3af); font-weight: 600; }
.bdg--locked .bdg__face { border-color: rgba(156,163,175,.25); }
/* A locked tile never carries a tier colour — it has not earned one yet. */
.bdg--locked .bdg__tier { color: var(--clr-badge-locked); }

/* ---- fill: progress toward. NEVER used on an earned badge. --------------- */
.bdg__fill {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(var(--badge-pct, 0) * 1%);
  background: linear-gradient(180deg,
      var(--clr-badge-grit-a55), var(--clr-badge-grit-a22));
  transition: height .6s var(--badge-ease);
  z-index: 0;
}

/* The meniscus wobbles via TRANSFORM, never via height — animating height
   relayouts the tile every frame. */
.bdg__fill::before {
  content: "";
  position: absolute;
  left: -25%;
  right: -25%;
  top: -6px;
  height: 12px;
  border-radius: 50%;
  background: var(--clr-badge-grit-a55);
  animation: bdg-meniscus 5.5s ease-in-out infinite;
}

@keyframes bdg-meniscus {
  0%, 100% { transform: translateX(-6%) rotate(-1.5deg); }
  50%      { transform: translateX( 6%) rotate( 1.5deg); }
}

/* Progress is stated in the member's own units — "38 of 50 days" — never as a
   bare percentage. Nobody thinks of their climbing as 76% of anything. */
.bdg__progress {
  font-size: .66rem;
  color: var(--clr-badge-grit);
  font-weight: 600;
}

/* ==========================================================================
 * Grade medallions — fill comes from Route.GRADE_BANDS, stamped inline by the
 * grade_style filter. The rim is non-negotiable: see --badge-band-rim.
 * ========================================================================== */

/* Selector-list rather than a bare `.bdg__face--band`: the tier rules above are
 * two-class selectors (`.bdg--rock .bdg__face`), so a single-class band rule
 * loses on specificity and every grade medallion on a Rock or Gym-history tile
 * silently renders with the tier's dark face instead of its grade colour —
 * which is the whole point of the badge. Found by looking at the rendered page;
 * it passes every HTTP assertion either way. */
.bdg .bdg__face--band,
.bdg--rock .bdg__face--band,
.bdg--history .bdg__face--band {
  background: var(--badge-band);
  color: var(--badge-band-ink);
  box-shadow: inset 0 0 0 1.5px var(--badge-band-rim);
}
.bdg__face--band .bdg__icon {
  font-size: 1.15rem;
  font-weight: 900;
  color: var(--badge-band-ink);
}

/* ==========================================================================
 * Nudge — a one-line coaching sentence with NO medallion. A nudge dressed as
 * a trophy is a lie about what the member did: they have not achieved this,
 * they are near it.
 * ========================================================================== */

.bdg-nudge {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .6rem .8rem;
  border-radius: 10px;
  border: 1px dashed rgba(45,212,191,.3);
  background: rgba(20,184,166,.07);
  font-size: .85rem;
  color: var(--muted-3, #d1d5db);
  text-align: left;
}
.bdg-nudge__icon { font-size: 1.1rem; line-height: 1; flex: 0 0 auto; }

/* ==========================================================================
 * Home-page strip — ONE tile plus a link. On an ordinary Tuesday nothing on
 * the home page moves, which is exactly what makes the spotlight mean
 * something on the day it fires.
 * ========================================================================== */

.bdg-strip {
  display: flex;
  align-items: center;
  gap: .9rem;
  /* Wraps so the text block drops BELOW the row on a phone rather than
     squeezing it to nothing beside it. The tiles themselves never wrap — see
     .bdg-strip__tiles. */
  flex-wrap: wrap;
}

/* The row. Takes whatever width the text block leaves and shows as many badges
   as fit in it; the rest are a sideways scroll away.
   
   SCROLL, NOT WRAP, and that is the whole design of this card. The rule it is
   held to is that it stays ONE ROW TALL at every width — twenty tiles wrapping
   down the page would bury the birthday celebration and the personal-best
   callout above it, which are the two moments the home page exists for. A
   scroller is identical in height on a 390px phone and a 2560px monitor.
   
   It is also the honest option for the alternatives considered and rejected:
   clipping the row with overflow:hidden leaves a half-tile sliced at the edge,
   and collapsing the extra grid rows to zero height hides tiles that are still
   focusable and still read out by a screen reader — a keyboard user would tab
   into badges nobody can see. Everything in a scroller is reachable by every
   input, and focus scrolls it into view for free. */
.bdg-strip__tiles {
  display: flex;
  gap: .6rem;
  /* Three requirements pulling against each other, and all three are load
     bearing — this is the combination that satisfies them:
     
       flex-grow: 1   the row takes the width the text block does not need.
       basis 12rem    a SMALL basis, because flex-wrap decides line breaks on
                      the hypothetical size, not the shrunk one. With `auto`
                      the row's natural 1738px claims the whole first line by
                      itself and the heading wraps underneath it — on a
                      1210px desktop card, which is not the phone layout that
                      wrap exists for.
       max-content    the cap that stops growth at the width the badges
                      actually occupy. Without it a member holding ONE badge
                      gets a tile at the far left and the heading stranded
                      against the right edge, with a canyon between a sentence
                      and the badge it describes. */
  flex: 1 1 12rem;
  max-width: max-content;
  min-width: 0;                    /* or a long row forces the CARD wider */
  overflow-x: auto;
  overscroll-behavior-x: contain;  /* swiping the row must not navigate back */
  scroll-snap-type: x proximity;
  /* Room under the tiles for the scrollbar, so an overlay bar on macOS/iOS
     lands on padding instead of across the tier word. */
  padding-bottom: .4rem;
  scrollbar-width: thin;
}

.bdg-strip .bdg {
  flex: 0 0 auto;
  width: 8.5rem;
  /* `proximity` rather than `mandatory`: a member flicking the row should be
     able to stop between two badges without it being yanked. */
  scroll-snap-align: start;
}

/* Deliberately does NOT grow. Free space belongs to the badges — with both
   items growing at rate 1 they split it evenly, and the heading ended up 661px
   wide to hold one sentence while the row was squeezed to three tiles.
   
   That does not strand the text when there are only one or two badges, because
   the row above caps itself at max-content: it cannot take space it has no
   badges to put in, so the block still sits right beside them.
   
   `min-width` is the floor for the wrapped phone layout and for a long row:
   under pressure flexbox shrinks in proportion to basis, and 20rem beside a
   1738px row would squeeze to about 11rem — not enough for "See all 20 badges"
   without it breaking across two lines. */
.bdg-strip__rest { flex: 0 1 20rem; min-width: 14rem; }
.bdg-strip__line { font-size: .9rem; color: var(--muted-3, #d1d5db); }

/* A row that scrolls has to say so. The end tile sitting flush against a hard
   edge reads as the end of the collection; faded, it reads as more to come.
   Only painted when the row actually overflows — `animation-timeline: scroll()`
   is inert where it is unsupported, so the fallback is simply no fade. */
@supports (animation-timeline: scroll(x self)) {
  .bdg-strip__tiles {
    animation: bdg-strip-fade linear both;
    animation-timeline: scroll(x self);
  }
  @keyframes bdg-strip-fade {
    /* Masked at the right while there is more to the right, and at the left
       once you have scrolled away from the start.
       
       A FIXED 3rem, not a percentage: a percentage fade is ~130px inside a
       880px card, which swallows an entire 8.5rem tile and reads as a broken
       one rather than as "keep scrolling". The hint has to stay the same size
       as the card changes. */
    from {
      mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent 100%);
    }
    to {
      mask-image: linear-gradient(to right, transparent 0, #000 3rem);
    }
  }
}
@media (prefers-reduced-motion: reduce) {
  /* Scroll-driven, not time-driven, so it is not motion in the sense this
     query is about — but it is decoration keyed to movement, and the row is
     completely legible without it. */
  .bdg-strip__tiles { animation: none; mask-image: none; }
}

/* ==========================================================================
 * Preview banner — staff only. Loud on purpose: a staffer must never come away
 * believing a member holds something they don't.
 * ========================================================================== */

.bdg-preview {
  margin: .5rem 0 1rem;
  padding: .7rem .9rem;
  border-radius: 10px;
  border: 1px solid rgba(249,115,22,.45);
  background: rgba(249,115,22,.12);
  color: #fcd34d;
  font-size: .88rem;
  font-weight: 600;
}

/* ==========================================================================
 * Mobile
 * ========================================================================== */

@media (max-width: 420px) {
  :root {
    --badge-tile-min: 96px;
    --badge-size: 50px;
    --hold-scale:    0.300;
    --badge-orbit-r: 29px;
  }
  .bdg__name { font-size: .74rem; }
  .bdg__meta { font-size: .64rem; }
}

/* ==========================================================================
 * Reduced motion
 *
 * The governing principle is already written into perks.css: under reduced
 * motion the confetti stays visible as a still scatter rather than
 * disappearing — "a member who asked their phone for less movement should
 * still be able to tell it's their birthday."
 *
 * Same rule here. Motion stops; MEANING never does. Every effect keeps a still
 * form that still says which tier it is.
 * ========================================================================== */

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

  .bdg--holo,
  .bdg--holo .bdg__face::after,
  .bdg__fill { transition: none !important; }

  /* Held mid-sweep: reads as gloss on the face rather than vanishing. */
  .bdg--sheen .bdg__face::after,
  .bdg--spotlight .bdg__face::after,
  .bdg-reveal__badge .bdg__face::after { transform: translateX(-25%); opacity: .45; }
  /* Still lit — the streak is visibly burning, it just isn't flickering. */
  .bdg--flicker .bdg__mount::after  { opacity: .60; transform: scale(1.04); }
  /* Depth without drift. */
  .bdg--aurora .bdg__face::before   { opacity: .38; transform: none; }
  /* One point parked on the rim — "one" still reads. */
  .bdg--orbit .bdg__mount::after    { transform: rotate(45deg) translateX(var(--badge-orbit-r)); }
  /* A static arc: gym-history is still the only tier wearing a halo. */
  .bdg--ring .bdg__mount::after     { transform: rotate(28deg); }
  .bdg--holo .bdg__face::after      { opacity: .40; background-position: 70% 20%; }
  /* Sparkles hold at full size instead of vanishing. The rainbow face already
     says "shiny" on its own, but a collector's badge that loses its twinkle
     entirely under reduced motion is a worse badge — so the twinkle becomes a
     still spray of foil, the same ruling perks.css makes about the confetti. */
  .bdg__spark {
    opacity: .9;
    transform: translate(var(--sx), var(--sy)) scale(1) rotate(45deg);
  }
}

/* ==========================================================================
 * Forced colors (Windows high contrast). clip-path silhouettes and
 * gradient rims both disappear here, so the tier word and the border carry
 * the whole message — which is why the word is printed on every tile.
 * ========================================================================== */

@media (forced-colors: active) {
  .bdg { border: 1px solid CanvasText; }
  .bdg__face::before,
  .bdg__face::after,
  .bdg__mount::before,
  .bdg__mount::after { display: none; }
}

/* ==========================================================================
 * SEALED — an earned badge they haven't watched unwrap yet.
 *
 * A dark silhouette of the real thing: same tile, same medallion geometry, the
 * icon knocked back to a shadow. It shakes, because a thing that shakes is a
 * thing that wants opening, and it is the only affordance on the page that
 * needs no label to be understood.
 *
 * The wrapper is a <form> and the tile is a <button> (see _badge_tile.html):
 * the CSP forbids inline handlers, and a form still works with the script
 * blocked. So these rules have to undo the browser's button styling rather
 * than style a div.
 * ========================================================================== */

.bdg--sealed {
  padding: 0;
  border-color: rgba(148, 163, 184, .3);
  background: var(--clr-badge-tile-lock);
}

.bdg__seal {
  /* Reset the button back to the tile it is pretending to be. */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .4rem;
  width: 100%;
  padding: .8rem .5rem .7rem;
  border: 0;
  border-radius: inherit;
  background: none;
  color: inherit;
  font: inherit;
  text-align: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.bdg__seal:focus-visible {
  outline: 2px solid var(--clr-badge-grit);
  outline-offset: 2px;
}

/* The shroud. A flat dark face with the icon reduced to a silhouette — the
   SHAPE of what is coming is visible, the identity is not. brightness(0)
   collapses the emoji's own colours to black; the low opacity lifts it just
   far enough off the face to read as an object rather than a smudge. */
.bdg__face--shroud {
  background:
    radial-gradient(90% 70% at 50% 12%, #2a3446, transparent 70%),
    #161d2b;
  border-color: rgba(148, 163, 184, .35);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .05);
}
.bdg__face--shroud .bdg__icon {
  filter: brightness(0) saturate(0);
  opacity: .42;
}

/* Shakes on a long cycle: a hard rattle for a fifth of a second, then four
   seconds of stillness. Continuous shaking is a nervous tic, and a member who
   leaves this tab open should not be sitting next to a vibrating box. */
.bdg--sealed .bdg__mount {
  animation: bdg-shake 4.4s ease-in-out infinite;
}

/* …except inside a bare coin, where the shake would win an argument it must
   not. A running animation on `transform` beats the declared one, and the
   coin's centring AND its scale are both declared transforms on this same
   element — so a sealed badge drawn as a coin came out at full 56px, low and
   to the right of its own box. Killing the animation is the honest fix rather
   than folding the shake into the coin's transform: a 30px medallion beside
   somebody's name rattling every four seconds is a tic, and .bdg-reveal__seal
   already sets the same precedent for the same reason. */
.bdg--coin.bdg--sealed .bdg__mount { animation: none; }

@keyframes bdg-shake {
  0%, 86%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  88%  { transform: translate3d(-2px, 0, 0) rotate(-4deg); }
  90%  { transform: translate3d( 2px, 0, 0) rotate( 4deg); }
  92%  { transform: translate3d(-2px, 0, 0) rotate(-3deg); }
  94%  { transform: translate3d( 2px, 0, 0) rotate( 3deg); }
  96%  { transform: translate3d(-1px, 0, 0) rotate(-1deg); }
}

/* A question mark over the silhouette, so "sealed" reads even in the still
   frame a reduced-motion viewer gets. */
.bdg--sealed .bdg__mount::after {
  content: "?";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 1.35rem;
  font-weight: 900;
  color: rgba(226, 232, 240, .85);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
  z-index: 3;
  pointer-events: none;
}

.bdg--sealed .bdg__name { color: var(--text, #f3f4f6); }
.bdg--sealed .bdg__meta { color: var(--clr-badge-grit); font-weight: 600; }
.bdg--sealed .bdg__tier { color: rgba(148, 163, 184, .9); }

/* ==========================================================================
 * REVEAL — the sealed badge crosses to the middle of the screen, rattles,
 * and BREAKS OPEN.
 *
 * Built by static/js/badge_reveal.js, which injects the stage once and reuses
 * it. Every step below is driven by animation-delay off a single `is-run`
 * class rather than by a chain of setTimeouts: the browser owns the timing, a
 * slow frame cannot desynchronise the sequence, and prefers-reduced-motion can
 * flatten the whole thing by cancelling animations instead of by unpicking JS.
 *
 * `is-run` sits on the STAGE and `is-open` on the overlay, deliberately. The
 * "open the next one" button replays the sequence without closing anything, and
 * replaying it means removing and re-adding the class that drives it — which,
 * on the overlay, would blink the backdrop off and on between two badges.
 *
 * THE SEQUENCE, and why it is in this order:
 *
 *   0.00s  the SEALED shroud lands, springing in from small
 *   0.40s  it rattles — the same "this wants opening" tic the grid tile has
 *   0.70s  it BREAKS: flash, rays, shards out, and the real badge springs up
 *          from behind it as the shroud blows apart
 *   1.12s  the caption arrives, once there is something to caption
 *
 * The shroud is the whole point. An earlier version of this file put the
 * already-revealed badge on the stage and then flashed a light over it, which
 * is a reveal with nothing to reveal — the member watched a badge they could
 * already see get brighter. The script now clones the shroud off the tile they
 * tapped (so there is exactly one shroud design, in _badge_tile.html, and this
 * cannot drift from it) and hands it over to be broken.
 *
 * SIZING. One token, --reveal-scale, drives the medallion, its margins, the
 * rays and the shards, so a screen size is one number in one media query
 * rather than four numbers that fall out of step. The medallion is scaled with
 * a transform and NOT by re-sizing --badge-size: every rim, hexagon and orbit
 * radius in this sheet is expressed in px against that token, so changing it
 * would rebuild the badge at a different shape rather than enlarging the one
 * they earned.
 * ========================================================================== */

.bdg-reveal {
  --reveal-scale: 3.6;
  /* Where the break happens. Everything downstream is written as an offset
     from this, so retiming the sequence is one number. */
  --rv-break: .70s;

  position: fixed;
  inset: 0;
  z-index: 1000;
  display: grid;
  place-items: center;
  /* The stage can be taller than a phone held sideways (see the landscape
     rules below). It scrolls rather than clipping: a member must always be
     able to reach the button that dismisses their own badge.

     overflow-X is pinned to hidden, and that is load-bearing rather than
     tidiness. Setting ONE axis to auto makes the other compute from `visible`
     to `auto` (CSS Overflow 3), so `overflow-y: auto` alone silently turns on
     horizontal scrolling too — and the rays hold their final frame (a 250%
     square rotated 26deg) forever, which put ~100px of scrollable width on
     every phone. A sideways swipe then dragged the badge and its caption off
     the left edge and exposed an undimmed, un-tappable strip of the page
     behind. Nothing here is ever meant to scroll sideways. */
  padding: clamp(.75rem, 3vw, 1.5rem);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  opacity: 0;
  /* pointer-events rather than visibility, because an element inside a
     visibility:hidden subtree CANNOT TAKE FOCUS — and the script focuses the
     dismiss button in the same frame it opens the stage. With visibility the
     focus() call was a silent no-op and the dialog opened with the document
     body focused, so Tab walked into the page BEHIND the overlay and a screen
     reader never entered the dialog at all. */
  pointer-events: none;
  transition: opacity .26s var(--badge-ease);
}
/* .bdg-reveal sets display:grid, which beats the UA sheet's [hidden] rule —
   so the attribute has to be honoured explicitly or the stage is only ever
   hidden by its opacity and still eats taps. */
.bdg-reveal[hidden] { display: none; }
.bdg-reveal.is-open { opacity: 1; pointer-events: auto; }

.bdg-reveal__backdrop {
  /* Fixed, not absolute. An absolute backdrop sizes to the overlay's padding
     box, so the moment the overlay scrolls at all the dimming slides with the
     content and leaves a bright, un-tappable band of the real page showing at
     the edge. Fixed is the viewport, which is what "everything behind this is
     out of play" actually means. */
  position: fixed;
  inset: 0;
  background:
    /* A glow that blooms behind the badge at the moment it breaks — the light
       is coming OFF the badge, so it belongs to the backdrop rather than to a
       ring drawn around the medallion. */
    radial-gradient(60% 44% at 50% 42%, rgba(45, 212, 191, .16), transparent 70%),
    rgba(3, 7, 18, .93);
  /* Safari — iPhone and iPad both — needs the prefix. Without it the blur
     silently does nothing and the badge grid stays legible behind the
     overlay, which is the one place the reveal has to be the only thing on
     screen. */
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
}

.bdg-reveal__stage {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  text-align: center;
  max-width: min(92vw, 25rem);
  /* Never wider than the space it was given, so the flex row the landscape
     rules switch to cannot push the caption off the side. */
  width: 100%;
}

/* --------------------------------------------------------------------------
 * The medallion and everything that happens around it.
 *
 * Its own box, sized to the SCALED badge, for two reasons. The flex column
 * above needs a real height to lay out against — a transform does not change
 * layout, so without this the caption sat under an unscaled 56px box and the
 * enlarged badge overlapped it. And the rays, flash and shards are positioned
 * against this box, which means they centre on the BADGE. They used to be
 * positioned against the stage, whose centre is somewhere in the caption text
 * — which is exactly why the rays were drawn straight across the badge name.
 * -------------------------------------------------------------------------- */
.bdg-reveal__medal {
  position: relative;
  width:  calc(var(--badge-size) * var(--reveal-scale));
  height: calc(var(--badge-size) * var(--reveal-scale));
  display: grid;
  place-items: center;
  flex: none;
}

.bdg-reveal__badge,
.bdg-reveal__seal {
  position: absolute;
  width: var(--badge-size);
  height: var(--badge-size);
  display: grid;
  place-items: center;
  transform: scale(var(--reveal-scale));
  transform-origin: center;
}
.bdg-reveal__seal { z-index: 2; }
.bdg-reveal__badge { z-index: 1; }

/* Both the real tile and the cloned shroud arrive wearing full tile chrome —
   a border, a background, the grid's entrance animation and, on the shroud,
   the endless rattle. On the stage the medallion is the ONLY thing; its name
   and sentence are printed larger below, and a tile captioning itself reads
   as the badge saying its own name twice. */
.bdg-reveal__badge .bdg,
.bdg-reveal__seal .bdg {
  border: 0;
  background: none;
  padding: 0;
  animation: none;
}
.bdg-reveal__seal .bdg__mount { animation: none; }
.bdg-reveal__badge .bdg__name,
.bdg-reveal__badge .bdg__meta,
.bdg-reveal__badge .bdg__tier,
.bdg-reveal__seal .bdg__name,
.bdg-reveal__seal .bdg__meta,
.bdg-reveal__seal .bdg__tier { display: none; }

/* ---- The shroud: land, rattle, shatter ---------------------------------- */
/*
 * Three animations on one element, sequenced by delay. NONE of the later two
 * may use `both`: a backwards fill applies an animation's `from` state during
 * its own delay, so a `both` rattle would stamp its opening frame over the
 * landing spring for the first 400ms and the badge would arrive already at
 * rest. `forwards` contributes from its start time onward and nothing before,
 * which is the behaviour a sequence needs.
 *
 * The delays are butt-joined to the durations — .42s is exactly the landing's
 * length, and the break at .70s is .42 + the rattle's .28 — rather than
 * overlapped. Two animations live on one property at once resolve to the LAST
 * in the list, so even a 20ms overlap is a frame of the landing spring being
 * replaced mid-bounce by the rattle's opening frame: a visible snap, for
 * nothing.
 */
.bdg-reveal__stage.is-run .bdg-reveal__seal {
  animation:
    bdg-rv-land .42s cubic-bezier(.2, 1.45, .4, 1) both,
    bdg-rv-rattle .28s ease-in-out .42s forwards,
    bdg-rv-shatter .30s ease-out var(--rv-break) forwards;
}

@keyframes bdg-rv-land {
  from { transform: scale(calc(var(--reveal-scale) * .3)); opacity: 0; }
  to   { transform: scale(var(--reveal-scale));            opacity: 1; }
}

/* Harder and faster than the grid tile's idle tic: this one is the lid coming
   off, not a box asking to be picked up. */
@keyframes bdg-rv-rattle {
  0%, 100% { transform: scale(var(--reveal-scale)) rotate(0deg); }
  15% { transform: scale(var(--reveal-scale)) rotate(-6deg) translate3d(-3px, 0, 0); }
  35% { transform: scale(var(--reveal-scale)) rotate( 6deg) translate3d( 3px, 0, 0); }
  55% { transform: scale(var(--reveal-scale)) rotate(-5deg) translate3d(-2px, 0, 0); }
  75% { transform: scale(var(--reveal-scale)) rotate( 4deg) translate3d( 2px, 0, 0); }
  90% { transform: scale(var(--reveal-scale)) rotate(-2deg); }
}

/* The break. The shroud blows outward and off rather than fading in place —
   a shell that dissolves reads as the badge changing its mind, one that flies
   apart reads as something opening. */
@keyframes bdg-rv-shatter {
  from { transform: scale(var(--reveal-scale));                 opacity: 1; filter: none; }
  to   { transform: scale(calc(var(--reveal-scale) * 1.55));    opacity: 0; filter: brightness(2.4); }
}

/* ---- The real badge, springing up from behind the broken shroud ---------- */
.bdg-reveal__stage.is-run .bdg-reveal__badge {
  animation: bdg-rv-pop .55s cubic-bezier(.18, 1.5, .4, 1) var(--rv-break) both;
}

@keyframes bdg-rv-pop {
  from { transform: scale(calc(var(--reveal-scale) * .45)); opacity: 0; }
  60%  { opacity: 1; }
  to   { transform: scale(var(--reveal-scale));             opacity: 1; }
}

/* ---- The white flash at the moment of the break -------------------------- */
.bdg-reveal__flash {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 170%;
  height: 170%;
  margin: -85% 0 0 -85%;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, rgba(255,255,255,.55) 32%, transparent 68%);
  opacity: 0;
  pointer-events: none;
  z-index: 3;
  transform: scale(0);
}
.bdg-reveal__stage.is-run .bdg-reveal__flash {
  animation: bdg-rv-flash .55s ease-out var(--rv-break) backwards;
}

@keyframes bdg-rv-flash {
  0%   { opacity: 0;  transform: scale(.35); }
  16%  { opacity: .9; transform: scale(1); }
  100% { opacity: 0;  transform: scale(1.8); }
}

/* ---- Rays --------------------------------------------------------------- */
/*
 * Spokes on one conic gradient, masked to a ring and spun out — one element
 * rather than twelve, and composited rather than repainted.
 *
 * Sized in PERCENT of the medallion box, so they grow with --reveal-scale and
 * a phone never gets desktop-sized rays. Blurred and masked out well before
 * their own edge: at 3deg hard-edged and unblurred they read as scratches
 * across the screen rather than as light, and on a short screen they reached
 * the caption and struck through the badge's name.
 */
.bdg-reveal__rays {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 250%;
  height: 250%;
  margin: -125% 0 0 -125%;
  pointer-events: none;
  opacity: 0;
  z-index: 0;
  background: repeating-conic-gradient(
      from 0deg,
      rgba(255,255,255,.42) 0deg 5deg,
      transparent 5deg 30deg);
  filter: blur(2px);
  /* Collapsed at rest. See the note on fill-mode below. */
  transform: scale(0);
  -webkit-mask: radial-gradient(closest-side, transparent 0 20%, #000 30%, transparent 62%);
          mask: radial-gradient(closest-side, transparent 0 20%, #000 30%, transparent 62%);
}
.bdg-reveal__stage.is-run .bdg-reveal__rays {
  /* `backwards`, NOT `both`. A forwards fill holds the last frame for the life
     of the page, and this element's last frame is a 250%-of-medal square
     rotated 26deg — invisible at opacity 0, but still a layout box, and it was
     adding ~100px of scrollable width to the overlay long after the animation
     finished. `backwards` fills the delay (which is what the sequence needs)
     and then lets the element fall back to its resting scale(0), which
     occupies nothing. Same reasoning for the flash and the shards. */
  animation: bdg-rv-rays 1.1s ease-out var(--rv-break) backwards;
}

@keyframes bdg-rv-rays {
  0%   { opacity: 0;   transform: scale(.45) rotate(0deg); }
  18%  { opacity: .75; }
  100% { opacity: 0;   transform: scale(1.15) rotate(26deg); }
}

/* ---- Shards ------------------------------------------------------------- */
/*
 * Ten chips of the broken shroud, thrown outward. Position, angle, distance
 * and delay are all derived in CSS from the --s stamped on each span — the
 * same no-JavaScript trick the sparkles and the birthday confetti already use,
 * so there is nothing random to keep in step and a test can assert the effect
 * by counting spans.
 */
.bdg-reveal__shards {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 4;
}
.bdg-reveal__shard {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 2px;
  background: #2a3446;
  box-shadow: inset 0 0 0 1px rgba(148, 163, 184, .5);
  opacity: 0;
  /* Odd shards throw further than even ones, so the burst is not a wheel. */
  --shard-throw: calc(var(--badge-size) * var(--reveal-scale) * .62);
  transform: rotate(calc(var(--s) * 36deg)) scale(0);
}
.bdg-reveal__stage.is-run .bdg-reveal__shard {
  animation: bdg-rv-shard .62s cubic-bezier(.2, .7, .4, 1)
             calc(var(--rv-break) + var(--s) * 8ms) backwards;
}

@keyframes bdg-rv-shard {
  0% {
    opacity: 0;
    transform: rotate(calc(var(--s) * 36deg)) translateY(0) scale(.6);
  }
  12% { opacity: 1; }
  100% {
    opacity: 0;
    transform: rotate(calc(var(--s) * 36deg))
               translateY(calc(var(--shard-throw) * -1))
               rotate(140deg) scale(.35);
  }
}

/* --------------------------------------------------------------------------
 * The word side of the stage — caption, buttons, backlog counter.
 *
 * Its own wrapper rather than three siblings of the medallion, because the
 * short-viewport rules below turn the stage on its side and every one of these
 * has to travel to the right of the badge as one block. Without the wrapper
 * that layout is a four-column row with the buttons beside the name.
 * -------------------------------------------------------------------------- */
.bdg-reveal__side {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .9rem;
  width: 100%;
  min-width: 0;
}

/* ---- The caption -------------------------------------------------------- */
.bdg-reveal__caption {
  opacity: 0;
  display: flex;
  flex-direction: column;
  gap: .3rem;
}
.bdg-reveal__stage.is-run .bdg-reveal__caption {
  animation: bdg-rv-caption .45s var(--badge-ease) calc(var(--rv-break) + .42s) both;
}

@keyframes bdg-rv-caption {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.bdg-reveal__eyebrow {
  font-size: .7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .18em;
  color: var(--clr-badge-grit);
}
.bdg-reveal__name {
  font-size: clamp(1.3rem, 5vw, 1.75rem);
  font-weight: 900;
  letter-spacing: -.02em;
  color: #fff;
  line-height: 1.15;
  /* A badge name is short but not guaranteed short, and "Rainbow Rocket" on a
     360px phone is one long word away from a horizontal scrollbar. */
  overflow-wrap: anywhere;
}
.bdg-reveal__line {
  font-size: .95rem;
  color: var(--muted-3, #d1d5db);
  line-height: 1.5;
}
.bdg-reveal__tierword {
  font-size: .68rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--muted, #9ca3af);
}

/* ---- Actions ------------------------------------------------------------ */
/*
 * "Open the next one" exists because a member coming back to a page with a
 * backlog — the import cohort will each land on a dozen at once — otherwise
 * has to close the stage, find the next shaking tile in a grid of twenty, and
 * tap it, twelve times. The chain keeps them on the stage.
 *
 * It is the SECOND button, not the first: the primary action after seeing a
 * badge is to be done, and a member who wants the next one is choosing to
 * carry on rather than being pushed.
 */
.bdg-reveal__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  justify-content: center;
  opacity: 0;
}
.bdg-reveal__stage.is-run .bdg-reveal__actions {
  animation: bdg-rv-caption .45s var(--badge-ease) calc(var(--rv-break) + .52s) both;
}

.bdg-reveal__close,
.bdg-reveal__next {
  /* 44px minimum: this is the control that dismisses the overlay, and on a
     phone it is the only one. */
  min-height: 44px;
  padding: .6rem 1.4rem;
  border-radius: 999px;
  font: inherit;
  font-weight: 700;
  font-size: .9rem;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.bdg-reveal__close {
  border: 1px solid rgba(255,255,255,.28);
  background: rgba(255,255,255,.06);
  color: #fff;
}
.bdg-reveal__close:hover { background: rgba(255,255,255,.14); }
.bdg-reveal__next {
  border: 1px solid var(--clr-badge-grit-a55);
  background: var(--clr-badge-grit-a22);
  color: var(--clr-badge-grit);
}
.bdg-reveal__next:hover { background: var(--clr-badge-grit-a28); }
.bdg-reveal__close:focus-visible,
.bdg-reveal__next:focus-visible {
  outline: 2px solid var(--clr-badge-grit);
  outline-offset: 2px;
}

/* Announcement-only. A chained reveal swaps the dialog's contents without
   re-opening it or moving focus, so there is nothing a screen reader would
   otherwise notice; this is the only thing that tells a non-sighted member
   which badge just opened. Never shown, never a layout participant, but NOT
   display:none — a hidden live region is not announced. */
.bdg-reveal__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* How many are still wrapped, so somebody with a backlog knows to keep going
   rather than assuming that was the lot. */
.bdg-reveal__more {
  font-size: .78rem;
  color: var(--muted, #9ca3af);
  opacity: 0;
}
.bdg-reveal__stage.is-run .bdg-reveal__more {
  animation: bdg-rv-caption .45s var(--badge-ease) calc(var(--rv-break) + .58s) both;
}
.bdg-reveal__more[hidden] { display: none; }

/* --------------------------------------------------------------------------
 * Sizes.
 *
 * One number per screen. Desktops and laptops get a genuinely big medallion —
 * the reveal is the one moment this feature exists for and at the old fixed
 * 3× it was a 168px hexagon adrift in a 1512px window.
 * -------------------------------------------------------------------------- */

/* Laptop and desktop. */
@media (min-width: 1080px) {
  .bdg-reveal { --reveal-scale: 4.6; }
  .bdg-reveal__stage { max-width: min(92vw, 28rem); gap: 1.15rem; }
}

/* Phones, portrait. Keeps a comfortable margin either side of the medallion on
   the narrowest real handset (360px). */
@media (max-width: 480px) {
  .bdg-reveal { --reveal-scale: 3; }
  .bdg-reveal__stage { gap: .85rem; }
  .bdg-reveal__line { font-size: .88rem; }
  /* Full-width buttons stacked: at this size two pills side by side leaves
     each one too narrow to read, and the dismiss is the one control that must
     never be a guess.

     `column`, never `column-reverse`: reversing paints "Open the next one"
     above "Nice one" while Tab still visits them in source order, so the
     keyboard order contradicts the reading order (WCAG 2.4.3). The row layout
     on wider screens reads dismiss-then-continue left to right; stacking it
     keeps the same order top to bottom. */
  .bdg-reveal__actions { flex-direction: column; width: 100%; }
  .bdg-reveal__close,
  .bdg-reveal__next { width: 100%; }
}

/* --------------------------------------------------------------------------
 * SHORT viewports — a phone turned sideways, a split-screen iPad, a laptop
 * with a short browser window.
 *
 * At 844x390 the column layout was 450px tall in a 390px window: the "Nice
 * one" button was sliced in half by the bottom edge and the "N more sealed"
 * line was off-screen entirely. The container scrolls now so nothing is ever
 * unreachable, but scrolling to dismiss your own badge is a poor moment — so
 * below 600px of height the stage turns on its side and the caption sits
 * BESIDE the medallion instead of under it, which is the shape that fits.
 * -------------------------------------------------------------------------- */
@media (max-height: 600px) and (min-width: 560px) {
  .bdg-reveal { --reveal-scale: 2.9; }
  .bdg-reveal__stage {
    flex-direction: row;
    align-items: center;
    justify-content: center;
    text-align: left;
    gap: 1.5rem;
    /* Content-sized, not full-width: a row stretched to its max-width pins the
       badge and its caption to the left edge of a 672px box and leaves a third
       of the screen empty beside them. Auto lets the pair shrink to what it is
       and the overlay's own place-items:center do the centring. */
    width: auto;
    max-width: min(94vw, 42rem);
  }
  .bdg-reveal__caption { align-items: flex-start; }
  .bdg-reveal__actions { justify-content: flex-start; }
  /* The caption, buttons and counter travel to the right of the medallion as
     one block, and stop stretching: a row does not want a full-width column. */
  .bdg-reveal__side { align-items: flex-start; gap: .7rem; width: auto; }
}

/* Short AND narrow — a small phone on its side. Nothing fits beside anything;
   shrink the medallion and let the container scroll. */
@media (max-height: 600px) and (max-width: 559px) {
  .bdg-reveal { --reveal-scale: 2.1; }
  .bdg-reveal__stage { gap: .6rem; }
}

/* Short AND narrow enough that even the row layout has nowhere to go. Scoped
   to narrow on purpose: an ordinary phone on its side is 390px tall and gets
   the row layout above, where the medallion has a whole column to itself and
   shrinking it to 1.9 would make the big moment the smallest thing on screen. */
@media (max-height: 420px) and (max-width: 559px) {
  .bdg-reveal { --reveal-scale: 1.7; }
}

/* --------------------------------------------------------------------------
 * Reduced motion
 *
 * The reveal still HAPPENS — it is the whole point of the interaction, and a
 * member who asked their phone for less movement has not asked to be denied
 * their badge. What goes is the travel: no landing spring, no rattle, no
 * shatter, no rays, no shards. The overlay cross-fades, the badge is simply
 * there, and the caption is legible immediately. Same ruling perks.css makes
 * about the confetti.
 *
 * The shroud is REMOVED rather than left on the stage: with no animation to
 * break it, a shroud stacked over the badge is just a lid nothing lifts.
 * -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .bdg--sealed .bdg__mount { animation: none !important; }
  .bdg-reveal__seal { display: none; }
  .bdg-reveal__stage.is-run .bdg-reveal__badge,
  .bdg-reveal__stage.is-run .bdg-reveal__caption,
  .bdg-reveal__stage.is-run .bdg-reveal__actions,
  .bdg-reveal__stage.is-run .bdg-reveal__more {
    animation: none !important;
  }
  .bdg-reveal__badge { opacity: 1; }
  .bdg-reveal__caption,
  .bdg-reveal__actions,
  .bdg-reveal__more { opacity: 1; }
  .bdg-reveal__rays,
  .bdg-reveal__flash,
  .bdg-reveal__shards { display: none; }
}

/* Windows high contrast: the gradients, masks and blurs all disappear, so the
   decoration is removed outright rather than left as grey rectangles, and the
   printed words carry the whole reveal. */
@media (forced-colors: active) {
  .bdg-reveal__rays,
  .bdg-reveal__flash,
  .bdg-reveal__shards,
  .bdg-reveal__seal { display: none; }
  .bdg-reveal__backdrop { background: Canvas; }
  .bdg-reveal__badge { opacity: 1; }
}

/* Neutralise EVERY tier and animation cue while a badge is sealed.
 *
 * Without this the shroud leaks: the rim keeps its tier colour, the gym-history
 * halo still turns, the orbit dot still circles, and a rare badge announces
 * itself before it is opened. The brief was a dark shadow of the badge, and a
 * violet-rimmed hexagon with a spark going round it is not a shadow.
 *
 * The SILHOUETTE is deliberately kept — a hexagon still reads as a hexagon —
 * so there is a tease in the shape without a spoiler in the colour. That is the
 * whole trick: enough to want it open, not enough to know what it is.
 *
 * Written after the tier and animation blocks and one class deeper than they
 * are, because those are the rules it has to beat. */
.bdg.bdg--sealed .bdg__face,
.bdg.bdg--sealed .bdg__mount::before {
  background: #161d2b;
  border-color: rgba(148, 163, 184, .35);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .05);
}
.bdg.bdg--sealed .bdg__mount::before { background: rgba(148, 163, 184, .4); }
/* Ring, orbit, flicker, aurora, sheen, sparkles, artwork — all of it stays
   behind the shroud until the reveal. */
.bdg.bdg--sealed .bdg__mount::after,
.bdg.bdg--sealed .bdg__face::before,
.bdg.bdg--sealed .bdg__face::after,
.bdg.bdg--sealed .bdg__art,
.bdg.bdg--sealed .bdg__sparks { display: none; }
/* ...except the "?" the shroud puts back, which shares ::after with the ring
   and orbit and therefore has to be re-declared once they are gone. */
.bdg.bdg--sealed .bdg__mount::after {
  content: "?";
  display: grid;
  position: absolute;
  /* Every one of these resets a property the orbit rule sets on this same
     pseudo-element. Without them the "?" inherits a 5px box with a negative
     margin and renders as a speck adrift near the centre — the shroud's own
     marker wearing the orbiting dot's geometry. */
  inset: 0;
  top: 0;
  left: 0;
  width: auto;
  height: auto;
  margin: 0;
  place-items: center;
  font-size: 1.35rem;
  font-weight: 900;
  color: rgba(226, 232, 240, .85);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
  background: none;
  /* The orbit dot's glow lives in a box-shadow on this same pseudo. Left
     standing it stretches across the reset inset:0 box and paints a gold
     square behind the "?" — the rarest tier announcing itself through the
     shroud, which is the one thing this block exists to stop. */
  box-shadow: none;
  border-radius: 0;
  animation: none;
  z-index: 3;
  pointer-events: none;
  -webkit-mask: none;
          mask: none;
}

/* ==========================================================================
 * CLIMBING HOLDS — the grade badges, drawn as the thing they are about.
 *
 * A grade badge used to be a flat coloured medallion with a paint-palette
 * emoji on it. It said "grade" only because a caption underneath said so. This
 * draws the actual object: a bolt-on hold in that grade's colour, with the
 * grit of real polyurethane, a bolt hole, and light coming from above.
 *
 * THE DESIGN IS THE PROGRESSION. The holds get WORSE as the grade gets harder,
 * exactly as they do on the wall:
 *
 *   1 Yellow  a fat jug          — a whole hand, incut, generous
 *   2 Blue    a chunky edge      — still positive
 *   3 Purple  a pocket           — two fingers, and a hole in it
 *   4 Green   a pinch            — tall, squeezed from both sides
 *   5 Orange  a sloper           — a dome with nothing to pull on
 *   6 Red     a crimp            — a thin edge
 *   7 Black   a small crimp      — thinner, meaner
 *   8 White   a micro edge       — barely there
 *
 * Every climber reads that instantly, and it means the badge for a hard grade
 * LOOKS hard before a word is read. It also gives the set a second carrier of
 * meaning: eight distinct silhouettes, so the grade survives greyscale, a
 * colour-blind viewer, and the two bands (Black and White) that are nearly
 * invisible against a dark and a light ground respectively.
 *
 * Drawn entirely in CSS. No images to ship, no sprite to keep in step with
 * Route.GRADE_BANDS, and the colour is the SAME token the chips and the
 * medallions already stamp — so a re-band changes the holds with everything
 * else.
 * ========================================================================== */

.bdg__hold,
.bdg__mark--woodhold {
  position: relative;
  display: block;
  width: var(--hold-w, 30px);
  height: var(--hold-h, 22px);
  z-index: 2;
  background-color: var(--badge-band, #6b7280);
  /* Volume. A broad soft highlight from above rather than a specular dot:
     these are matte polyurethane with sand through them, not polished plastic,
     and a glossy pin-light is the single fastest way to make it read as a
     sweet instead of a hold. */
  background-image:
    radial-gradient(120% 90% at 32% 8%, rgba(255,255,255,.5), rgba(255,255,255,0) 55%),
    radial-gradient(130% 110% at 60% 118%, rgba(0,0,0,.5), rgba(0,0,0,0) 60%);
  border-radius: var(--hold-r, 46% 54% 44% 56% / 62% 58% 42% 38%);
  box-shadow:
    /* the rim catching the light, and the underside losing it */
    inset 0 1.5px 1px rgba(255,255,255,.42),
    inset 0 -2px 2px rgba(0,0,0,.45),
    /* seated on the wall, not floating above it */
    0 2px 3px rgba(0,0,0,.55),
    0 0 0 .5px rgba(0,0,0,.35);
  transform: rotate(var(--hold-rot, -4deg));
}

/* The grit.
 *
 * feTurbulence, not a hand-built pile of radial-gradients: fractal noise is
 * what the surface of a hold actually is, and forty stacked gradient speckles
 * would cost more to paint and still tile visibly. One inline SVG, no request,
 * and `overlay` lets the grain darken and lighten the band colour rather than
 * greying it — which is the difference between sandy and dusty.
 *
 * The data URI is deliberately single-quoted inside a double-quoted url() with
 * the fragment escaped as %23: an unescaped # ends the URI at the filter
 * reference and the whole rule is dropped, silently, leaving a smooth hold. */
.bdg__hold::before,
.bdg__mark--woodhold::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='90' height='90' filter='url(%23g)'/%3E%3C/svg%3E");
  /* 22px, not 42: a badge hold is ~30px across, so at the larger tile it showed
     less than one repeat of the noise and read as a smudge. Small enough that
     several repeats land on even the micro edge, which is what makes it sand
     rather than dirt. */
  background-size: 22px 22px;
  mix-blend-mode: overlay;
  opacity: .7;
  pointer-events: none;
}

/* The bolt hole. Every bolt-on hold has one, and its absence is the detail
   that makes a drawn hold look like a bean. Recessed: dark well, a lip catching
   light on the lower edge, and the countersink flare around it. */
.bdg__hold::after,
.bdg__mark--woodhold::after {
  content: "";
  position: absolute;
  left: 50%;
  top: var(--bolt-y, 46%);
  width: var(--bolt-d, 7px);
  height: var(--bolt-d, 7px);
  margin: calc(var(--bolt-d, 7px) / -2) 0 0 calc(var(--bolt-d, 7px) / -2);
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 35%, rgba(0,0,0,.85), rgba(0,0,0,.6) 60%, rgba(0,0,0,.35) 100%);
  box-shadow:
    inset 0 -1px 1px rgba(255,255,255,.3),
    0 0 0 1px rgba(0,0,0,.25);
  pointer-events: none;
  z-index: 1;
}

/* ---- The eight, easiest to hardest ------------------------------------- */

/* 1 Yellow — a fat jug. The biggest thing in the set: a whole hand fits, and
   it should look like it does. */
.bdg__hold--g1 {
  --hold-w: 34px; --hold-h: 25px; --bolt-d: 8px; --hold-rot: -5deg;
  --hold-r: 52% 48% 38% 62% / 58% 62% 38% 42%;
}
/* 2 Blue — a chunky edge. Still positive, a shade less generous. */
.bdg__hold--g2 {
  --hold-w: 32px; --hold-h: 21px; --bolt-d: 7px; --hold-rot: 4deg;
  --hold-r: 44% 56% 46% 54% / 64% 60% 40% 36%;
}
/* 3 Purple — a pocket. Two fingers, and the hole is the point, so the bolt
   moves off-centre to make room for it (see --pocket below). */
.bdg__hold--g3 {
  /* The bolt is deliberately small and low here: at equal weight the pocket
     and the bolt read as two eyes rather than as a hold with a hole in it. */
  --hold-w: 31px; --hold-h: 25px; --bolt-d: 4.5px; --bolt-y: 80%; --hold-rot: -3deg;
  --hold-r: 50% 50% 48% 52% / 54% 54% 46% 46%;
}
.bdg__hold--g3 .bdg__hold-pocket {
  position: absolute;
  left: 50%;
  top: 34%;
  width: 16px;
  height: 7px;
  margin: -3.5px 0 0 -8px;
  border-radius: 50%;
  /* A hole, not a dot: dark at the back, and the lower lip lit because that is
     the edge your fingers pull on. */
  background: radial-gradient(ellipse at 50% 22%, #000 0%, rgba(0,0,0,.82) 55%, rgba(0,0,0,.5) 100%);
  box-shadow: inset 0 -1.5px 1px rgba(255,255,255,.28), 0 1px 0 rgba(255,255,255,.18);
  z-index: 1;
}
/* 4 Green — a pinch. Tall rather than wide, with the squeeze faces shaded down
   both sides. */
.bdg__hold--g4 {
  --hold-w: 21px; --hold-h: 27px; --bolt-d: 6px; --hold-rot: 3deg;
  --hold-r: 42% 58% 42% 58% / 34% 32% 68% 66%;
  background-image:
    radial-gradient(90% 70% at 50% 6%, rgba(255,255,255,.48), rgba(255,255,255,0) 55%),
    linear-gradient(90deg, rgba(0,0,0,.5), rgba(0,0,0,0) 26%, rgba(0,0,0,0) 74%, rgba(0,0,0,.5));
}
/* 5 Orange — a sloper. A dome. Nothing to pull on, and the drawing should make
   that obvious: no lip, no edge, just falling away in every direction. */
.bdg__hold--g5 {
  --hold-w: 30px; --hold-h: 20px; --bolt-d: 6px; --bolt-y: 52%; --hold-rot: 0deg;
  --hold-r: 50% 50% 46% 54% / 88% 84% 16% 20%;
  background-image:
    radial-gradient(70% 120% at 42% 4%, rgba(255,255,255,.55), rgba(255,255,255,0) 62%),
    radial-gradient(120% 90% at 50% 108%, rgba(0,0,0,.55), rgba(0,0,0,0) 55%);
}
/* 6 Red — a crimp. A thin edge, and from here down the whole hold is mostly
   the edge. */
.bdg__hold--g6 {
  --hold-w: 32px; --hold-h: 14px; --bolt-d: 5px; --bolt-y: 58%; --hold-rot: -2deg;
  --hold-r: 30% 30% 46% 46% / 62% 62% 38% 38%;
}
/* 7 Black — a small crimp. */
.bdg__hold--g7 {
  --hold-w: 28px; --hold-h: 11px; --bolt-d: 4.5px; --bolt-y: 60%; --hold-rot: 2deg;
  --hold-r: 26% 26% 44% 44% / 66% 66% 34% 34%;
}
/* 8 White — a micro edge. Barely there, which is the whole joke and also the
   truth. */
.bdg__hold--g8 {
  --hold-w: 24px; --hold-h: 9px; --bolt-d: 4px; --bolt-y: 62%; --hold-rot: -3deg;
  --hold-r: 22% 22% 42% 42% / 70% 70% 30% 30%;
}

/* Band 7 Black is #18181b and band 8 White is #e5e7eb — one nearly invisible
   against the dark face, the other blowing out against it. Both get a rim in
   the opposite direction so the silhouette survives either way. */
.bdg__hold--g7 { box-shadow:
    inset 0 1.5px 1px rgba(255,255,255,.3),
    inset 0 -2px 2px rgba(0,0,0,.5),
    0 0 0 1px rgba(255,255,255,.3),
    0 2px 4px rgba(0,0,0,.6); }
.bdg__hold--g8 { box-shadow:
    inset 0 1.5px 1px rgba(255,255,255,.6),
    inset 0 -2px 2px rgba(0,0,0,.3),
    0 0 0 1px rgba(0,0,0,.5),
    0 2px 4px rgba(0,0,0,.6); }

/* A hold badge keeps its dark face: the hold is the object ON the badge, and
   filling the medallion with the band colour too would leave a coloured blob
   on a coloured disc with no silhouette to read. */
.bdg__face--hold { background: var(--clr-badge-face); }
.bdg--locked .bdg__hold { filter: grayscale(1); opacity: .4; }

@media (prefers-reduced-motion: reduce) {
  .bdg__hold { transform: rotate(var(--hold-rot, -4deg)); }
}

/* ==========================================================================
 * DRAWN MARKS — a shape in place of the emoji.
 *
 * Same reasoning as the climbing holds: a glyph is a system font's drawing, so
 * the same badge is a different picture on iOS, Android and Windows. And the
 * one thing an emoji cannot do at any price is MOVE.
 * ========================================================================== */

.bdg__mark {
  position: relative;
  display: block;
  z-index: 2;
}

/* ---- flame ---------------------------------------------------------------
 * The week streak. A teardrop with the point at the top, built from a
 * square-cornered border-radius rotated 45deg, in two layers: a body in
 * orange-to-red and a hotter core in white-to-amber sitting low inside it,
 * which is where a real flame is brightest.
 *
 * The two layers flicker on DIFFERENT durations and out of phase. That is the
 * whole trick — one animated flame reads as a wobbling leaf, two reading
 * against each other reads as burning.
 *
 * MOTION EXCEPTION, deliberate and the second in this sheet. The house rule is
 * that only the spotlight tile moves. A flame is the one mark where stillness
 * is not restraint but a contradiction: a static flame is a leaf, and the badge
 * would be worse for it. It is also exactly one badge, and the streak is the
 * only thing on the page that can be LOST this week, which is the argument the
 * spotlight ladder already makes for letting it flicker. So the flame licks
 * always, gently; winning the spotlight makes it burn harder rather than
 * turning it on.
 * -------------------------------------------------------------------------- */

/* SQUARE, and big enough for the rotated diagonal. A 19px square turned 45deg
 * needs 19 * sqrt(2) = 27px in both directions; at 22x26 the box was smaller
 * than its own contents in one axis, so the flame could not sit in the middle
 * of it however the pseudo was positioned. Square also means the face's
 * `place-items: center` centres the mark itself, rather than centring a box the
 * flame is off-centre inside. */
.bdg__mark--flame {
  width: 28px;
  height: 28px;
}

.bdg__mark--flame::before,
.bdg__mark--flame::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  /* Square top-left corner + rotate(45deg) puts the point at the top, and the
     result is symmetric about the vertical axis — so centring the SQUARE
     centres the flame. */
  border-radius: 0 50% 50% 50%;
  /* Rotate about the shape's own centre. The first pass pivoted at 50% 80% to
     mimic a flame hinging at its base, which is a good instinct and the wrong
     mechanism: an off-centre origin moves the rotated bounding box, so the
     flame sat low and left of the medallion. The hinge feeling comes from the
     scale/skew in the keyframes instead, which costs nothing and does not move
     the shape off its mark. */
  transform-origin: 50% 50%;
}

/* Body. Every number is a custom property whose DEFAULT is the value that
   shipped, so an un-staged flame draws exactly what it always drew and rung 12
   — which sets no overrides at all — is unchanged to the pixel. */
.bdg__mark--flame::before {
  width: var(--fb, 19px);
  height: var(--fb, 19px);
  margin: calc(var(--fb, 19px) / -2) 0 0 calc(var(--fb, 19px) / -2);
  background: var(--f-body, linear-gradient(150deg, #fbbf24 0%, #f97316 45%, #ef4444 100%));
  /* Spread was 2px against a 9px blur; kept as a ratio so it tracks the glow. */
  box-shadow: 0 0 var(--fg, 9px) calc(var(--fg, 9px) / 4.5) var(--f-glow, rgba(249, 115, 22, .55));
  /* Paint order inside .bdg__mark, which is its own stacking context: tongues
     (0) sit BEHIND the body (1), the core (2) is hotter and in front of both,
     and loose pieces — chips, shimmer, embers (3) — ride on top. Without this
     a tongue lands on top of the body, because a child paints above ::before
     and the whole point of a second tongue is that it reads BEHIND the first. */
  z-index: 1;
  animation: bdg-flame-body var(--fd, 1.9s) ease-in-out infinite;
}

/* Core, low and hot. */
.bdg__mark--flame::after {
  width: var(--fc, 10px);
  height: var(--fc, 10px);
  margin: calc(var(--fc, 10px) / -2) 0 0 calc(var(--fc, 10px) / -2);
  background: var(--f-core, linear-gradient(150deg, #fffbeb 0%, #fde68a 55%, #fbbf24 100%));
  z-index: 2;
  animation: bdg-flame-core var(--fdc, 1.35s) ease-in-out infinite;
}

/* Irregular stops, as with the glow behind it: evenly spaced keyframes read as
   a pulse asking to be tapped rather than as combustion. */
@keyframes bdg-flame-body {
  0%, 100% { transform: rotate(45deg) scale(1, 1)       skewX(0deg); }
  22%      { transform: rotate(45deg) scale(.94, 1.1)   skewX(-3deg); }
  41%      { transform: rotate(45deg) scale(1.04, .97)  skewX(2deg); }
  63%      { transform: rotate(45deg) scale(.97, 1.06)  skewX(-2deg); }
  81%      { transform: rotate(45deg) scale(1.02, .99)  skewX(1.5deg); }
}

/* translate BEFORE rotate, so the 4px is straight down the medallion rather
   than 4px along the rotated axis — which would push the core out sideways. */
@keyframes bdg-flame-core {
  0%, 100% { transform: translateY(var(--fl, 4px)) rotate(45deg) scale(1, 1);      opacity: .95; }
  30%      { transform: translateY(var(--fl, 4px)) rotate(45deg) scale(.88, 1.14); opacity: 1; }
  55%      { transform: translateY(var(--fl, 4px)) rotate(45deg) scale(1.06, .92); opacity: .82; }
  78%      { transform: translateY(var(--fl, 4px)) rotate(45deg) scale(.94, 1.05); opacity: 1; }
}

/* Spotlit — and on the reveal stage, where the badge is alone on a dimmed
   screen and is the only thing being looked at: burn harder and faster, rather
   than switch on. A RATIO of whatever the rung already runs at, not a fixed
   number — otherwise a wildfire (1.35s) would barely speed up and a smoulder
   would be whipped into a flame. The multipliers are today's numbers over
   today's numbers, so rung 12 spotlit is still 1.25s and .85s exactly. */
.bdg--spotlight .bdg__mark--flame::before,
.bdg-reveal__badge .bdg__mark--flame::before { animation-duration: calc(var(--fd, 1.9s) * .66); }
.bdg--spotlight .bdg__mark--flame::after,
.bdg-reveal__badge .bdg__mark--flame::after  { animation-duration: calc(var(--fdc, 1.35s) * .63); }

/* An at-risk streak already breathes quicker through the glow behind it
   (--badge-flicker-dur). The flame follows, so the two agree instead of
   beating against each other. */
.bdg--at-risk .bdg__mark--flame::before { animation-duration: calc(var(--fd, 1.9s) * .71); }
.bdg--at-risk .bdg__mark--flame::after  { animation-duration: calc(var(--fdc, 1.35s) * .67); }

/* Sealed and locked: the mark is behind the shroud / greyed with everything
   else, and a flame licking inside a sealed badge would leak that this is the
   streak before it is opened. */
.bdg.bdg--sealed .bdg__mark { display: none; }
.bdg--locked .bdg__mark { filter: grayscale(1); opacity: .4; }

@media (prefers-reduced-motion: reduce) {
  /* Held at the top of the flicker rather than stopped flat: a flame frozen
     mid-lick still reads as fire, which is the perks.css confetti ruling
     applied to the one mark that cannot afford to look inert. */
  .bdg__mark--flame::before {
    animation: none;
    transform: rotate(45deg) scale(.97, 1.06) skewX(-2deg);
  }
  .bdg__mark--flame::after {
    animation: none;
    transform: translateY(var(--fl, 4px)) rotate(45deg) scale(.92, 1.09);
  }
  /* The extra pieces hold a readable pose too: a tongue frozen mid-lick still
     reads as fire, and the chips frozen mid-flight still read as a strike. */
  .bdg__fbit { animation: none !important; }
  .bdg--fire-flint .bdg__fbit {
    opacity: .75;
    transform: translate(var(--sx), var(--sy)) scale(.8);
  }
  .bdg--fire-blaze .bdg__fbit,
  .bdg--fire-inferno .bdg__fbit:nth-child(-n+2),
  .bdg--fire-wildfire .bdg__fbit:nth-child(-n+2) {
    transform: rotate(45deg) scale(.84, 1.2) skewX(-5deg);
    opacity: .7;
  }
  .bdg--fire-inferno .bdg__fbit:nth-child(n+3),
  .bdg--fire-wildfire .bdg__fbit:nth-child(n+3) { opacity: .6; }
  .bdg__crown-lick { animation: none !important; }
  .bdg--fire-smoulder .bdg__mark--flame::before,
  .bdg--fire-smoulder .bdg__mark--flame::after { animation: none; transform: none; }
}

/* ==========================================================================
 * THE FIRE LADDER — six rungs, and every one is a different OBJECT.
 *
 * The argument, in one line: at nineteen pixels a slightly larger flame and a
 * flame are the same picture. So the ladder cannot be a scale, it has to be a
 * sequence of different things — heat with no fire, a coal, the teardrop
 * catching, the shipped flame, a second tongue, and a fire that has got away.
 *
 * Rung 12 (`flame`) sets NOTHING. That is deliberate and it is the guarantee:
 * every default above is the value that shipped, so the badge every current
 * holder has does not move by a pixel or a millisecond.
 *
 * Sizes, heat, lift and speed all arrive as custom properties on the tile, so
 * one block of CSS draws the whole ladder and a rung is five declarations.
 * ========================================================================== */

/* The extra pieces. Six empty spans ride on every flame tile and each rung
   shows only the ones it needs — the same no-JavaScript trick the sapphire
   inclusions use, and for the same reason: the markup does not have to know
   which rung it is drawing. */
.bdg__fbit {
  display: none;
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 3;
  border-radius: 0 50% 50% 50%;
  transform-origin: 50% 50%;
}

/* ---- rung 1 · Flint · 2 weeks — heat, and no fire yet ---------------------
   No body at all, which is the point: week 2 is not a small fire, so week 4 is
   a change of KIND rather than of size.

   Drawn as the STRIKE, never the stone. A grey flint chip was the obvious
   reading of the name and the wrong one — a cold grey mark in a medallion reads
   as locked, and this badge is earned. */
.bdg--fire-flint { --fdc: 2.1s; }
.bdg--fire-flint .bdg__mark--flame::before { display: none; }
.bdg--fire-flint .bdg__mark--flame::after {
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff7e6 0%, #fde68a 45%, #f59e0b 100%);
  box-shadow: 0 0 6px 1px rgba(251, 191, 36, .5);
  animation: bdg-flint var(--fdc, 2.1s) ease-in-out infinite;
}
@keyframes bdg-flint {
  0%, 100% { transform: translateY(3px) scale(1);    opacity: .78; }
  26%      { transform: translateY(3px) scale(1.22); opacity: 1; }
  48%      { transform: translateY(3px) scale(.9);   opacity: .6; }
  71%      { transform: translateY(3px) scale(1.12); opacity: .95; }
}
/* The two chips. ease-OUT, not ease-in-out: a spark leaves fast and dies slowly,
   which is the opposite of the breathing every other rung does. */
.bdg--fire-flint .bdg__fbit:nth-child(1),
.bdg--fire-flint .bdg__fbit:nth-child(2) {
  display: block;
  width: 2.5px;
  height: 2.5px;
  margin: -1.25px 0 0 -1.25px;
  border-radius: 50%;
  background: #fde68a;
  opacity: 0;
  animation: bdg-flint-chip var(--fdc, 2.1s) ease-out infinite;
}
.bdg--fire-flint .bdg__fbit:nth-child(1) { --sx:  7px; --sy: -7px; animation-delay: 0s; }
.bdg--fire-flint .bdg__fbit:nth-child(2) { --sx: -6px; --sy: -5px; animation-delay: -1.05s; }
@keyframes bdg-flint-chip {
  0%        { opacity: 0;   transform: translate(0, 3px) scale(.5); }
  8%        { opacity: 1;   transform: translate(0, 3px) scale(1); }
  38%       { opacity: .55; transform: translate(var(--sx), var(--sy)) scale(.7); }
  55%, 100% { opacity: 0;   transform: translate(calc(var(--sx) * 1.4), calc(var(--sy) * .5)) scale(.3); }
}

/* ---- rung 2 · Smoulder · 4 weeks — a coal, not a flame --------------------
   Wide, low, round-bottomed and deep red. A coal in a draught pulses; it does
   not lick, so both layers breathe on ONE duration here rather than beating
   against each other the way the teardrop's two do. */
.bdg--fire-smoulder { --fdc: 2.9s; }
.bdg--fire-smoulder .bdg__mark--flame::before {
  width: 15px;
  height: 10px;
  margin: -3px 0 0 -7.5px;
  border-radius: 50%;
  background: linear-gradient(165deg, #f97316 0%, #c2410c 48%, #7c2d12 100%);
  box-shadow: 0 0 9px 1px rgba(194, 65, 12, .45);
  animation: bdg-smoulder var(--fdc, 2.9s) ease-in-out infinite;
}
.bdg--fire-smoulder .bdg__mark--flame::after {
  width: 7px;
  height: 4px;
  margin: 0 0 0 -3.5px;
  border-radius: 50%;
  background: linear-gradient(160deg, #fde68a, #f59e0b);
  animation: bdg-smoulder-core var(--fdc, 2.9s) ease-in-out infinite;
}
@keyframes bdg-smoulder {
  0%, 100% { opacity: .88; transform: scale(1); }
  34%      { opacity: 1;   transform: scale(1.05); }
  62%      { opacity: .8;  transform: scale(.98); }
}
@keyframes bdg-smoulder-core {
  0%, 100% { opacity: .6; }
  34%      { opacity: 1; }
  62%      { opacity: .45; }
}
/* Heat shimmer above the coal — the first thing on the ladder that rises. */
.bdg--fire-smoulder .bdg__fbit:nth-child(1) {
  display: block;
  top: 3px;
  width: 9px;
  height: 9px;
  margin-left: -4.5px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(251, 191, 36, .34), transparent 75%);
  animation: bdg-smoulder-shimmer var(--fdc, 2.9s) ease-in-out infinite;
}
@keyframes bdg-smoulder-shimmer {
  0%, 100% { opacity: .25; transform: translateY(0)    scaleX(1); }
  40%      { opacity: .55; transform: translateY(-3px) scaleX(1.25); }
  70%      { opacity: .3;  transform: translateY(-1px) scaleX(.95); }
}

/* ---- rung 3 · Kindling · 8 weeks — the teardrop arrives, small and single -
   It has caught. Nothing new is drawn; this is the shipped flame at roughly
   two thirds, which is exactly why it needs the rung underneath it to be a
   coal — a small teardrop next to a slightly larger teardrop would be no
   ladder at all. */
.bdg--fire-kindling { --fb: 13px; --fc: 7px; --fg: 7px; --fl: 3px; --fd: 2.05s; --fdc: 1.45s; }

/* ---- rung 4 · Flame · 12 weeks — the badge that shipped -------------------
   No overrides, on purpose. Every default in this sheet IS this rung. */

/* ---- rung 5 · Blaze · 26 weeks — a second tongue, out of phase ------------
   The step that matters here is not size. One shape scaled up is a zoom; two
   shapes reading against each other is fire. */
.bdg--fire-blaze {
  --fb: 22px; --fc: 11px; --fg: 13px; --fl: 4px; --fd: 1.6s; --fdc: 1.1s;
  --f-body: linear-gradient(150deg, #fde68a 0%, #f97316 42%, #dc2626 100%);
}
/* The mark box has to hold the rotated diagonal: a 22px square turned 45deg
   needs 22 * sqrt(2) = 31px, and at 28px the box was smaller than its own
   contents. */
.bdg--fire-blaze .bdg__mark--flame { width: 32px; height: 32px; }
.bdg--fire-blaze .bdg__fbit:nth-child(1) {
  display: block;
  width: 11px;
  height: 11px;
  margin: -9px 0 0 -11px;
  background: linear-gradient(150deg, #fbbf24 0%, #f97316 60%, rgba(239, 68, 68, .6) 100%);
  opacity: .82;
  z-index: 0;
  animation: bdg-flame-tongue 2.35s ease-in-out infinite;
}
@keyframes bdg-flame-tongue {
  0%, 100% { transform: rotate(45deg) scale(.9, 1)     skewX(4deg);  opacity: .5; }
  27%      { transform: rotate(45deg) scale(.8, 1.28)  skewX(-6deg); opacity: .9; }
  53%      { transform: rotate(45deg) scale(1, .86)    skewX(3deg);  opacity: .42; }
  79%      { transform: rotate(45deg) scale(.86, 1.16) skewX(-4deg); opacity: .78; }
}

/* ---- rungs 6 and 7 · Inferno · 40 weeks — three tongues and embers rising -
   The face clips at the medallion rim, so the embers rise INSIDE the coin and
   die at the edge. That constraint is a gift: nothing escapes into the badge
   name underneath it.

   SHARED WITH WILDFIRE at 52, deliberately. At 25px there is no bigger fire
   left to draw, and a wildfire that was merely a larger inferno would be the
   exact mistake this ladder exists to avoid. The last step is drawn outside
   the coin instead — see .bdg__crown at the bottom of this block. */
.bdg--fire-inferno,
.bdg--fire-wildfire {
  --fb: 25px; --fc: 12px; --fg: 18px; --fl: 5px; --fd: 1.35s; --fdc: .92s;
  --f-body: linear-gradient(150deg, #fef3c7 0%, #fbbf24 30%, #f97316 62%, #dc2626 100%);
  --f-core: linear-gradient(150deg, #ffffff 0%, #fef3c7 50%, #fbbf24 100%);
  --f-glow: rgba(249, 115, 22, .6);
}
/* 25 * sqrt(2) = 36px. */
.bdg--fire-inferno .bdg__mark--flame,
.bdg--fire-wildfire .bdg__mark--flame { width: 36px; height: 36px; }
.bdg--fire-inferno .bdg__fbit:nth-child(1),
.bdg--fire-inferno .bdg__fbit:nth-child(2),
.bdg--fire-wildfire .bdg__fbit:nth-child(1),
.bdg--fire-wildfire .bdg__fbit:nth-child(2) {
  display: block;
  width: 13px;
  height: 13px;
  background: linear-gradient(150deg, #fde68a 0%, #f97316 60%, rgba(220, 38, 38, .55) 100%);
  z-index: 0;
  animation: bdg-flame-tongue 2.1s ease-in-out infinite;
}
.bdg--fire-inferno .bdg__fbit:nth-child(1),
.bdg--fire-wildfire .bdg__fbit:nth-child(1) { margin: -11px 0 0 -13px; animation-delay: -.7s; }
.bdg--fire-inferno .bdg__fbit:nth-child(2),
.bdg--fire-wildfire .bdg__fbit:nth-child(2) { margin: -10px 0 0 0;     animation-delay: -1.4s; }
/* Four embers, on a LINEAR timing — an ember drifts, it does not breathe. */
.bdg--fire-inferno .bdg__fbit:nth-child(n+3),
.bdg--fire-wildfire .bdg__fbit:nth-child(n+3) {
  display: block;
  top: auto;
  bottom: 4px;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #fbbf24;
  opacity: 0;
  animation: bdg-flame-ember 3.1s linear infinite;
}
.bdg--fire-inferno .bdg__fbit:nth-child(3),
.bdg--fire-wildfire .bdg__fbit:nth-child(3) { margin-left: -7.5px; animation-delay: -.78s; }
.bdg--fire-inferno .bdg__fbit:nth-child(4),
.bdg--fire-wildfire .bdg__fbit:nth-child(4) { margin-left: -2.5px; animation-delay: -1.56s; }
.bdg--fire-inferno .bdg__fbit:nth-child(5),
.bdg--fire-wildfire .bdg__fbit:nth-child(5) { margin-left:  2.5px; animation-delay: -2.34s; }
.bdg--fire-inferno .bdg__fbit:nth-child(6),
.bdg--fire-wildfire .bdg__fbit:nth-child(6) { margin-left:  7.5px; animation-delay: -3.12s; }
@keyframes bdg-flame-ember {
  0%   { opacity: 0;   transform: translate(0, 0)        scale(.7); }
  14%  { opacity: .95; }
  62%  { opacity: .7;  transform: translate(3px, -15px)  scale(1); }
  100% { opacity: 0;   transform: translate(-2px, -26px) scale(.4); }
}

/* ---- rung 7 · Wildfire · 52 weeks — the fire gets out ---------------------
 * The coin is Inferno's, unchanged. The whole of the last step is out here.
 *
 * Six licks, one per hexagon vertex. --badge-hex is pointy-top with a
 * circumradius of exactly half the badge, so rotate(n * 60deg) lands a lick on
 * every corner — and because the radius is expressed in --badge-size rather
 * than in pixels, it stays on the corners at 50px on a phone and at the 104px
 * the reveal stage blows the badge up to.
 *
 * Drawn BEHIND the face (z-index 0), so each lick emerges from under the
 * medallion instead of sitting on top of it.
 * -------------------------------------------------------------------------- */
.bdg__crown {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* The heat the crown throws. Wider than the badge and soft, so the tile reads
   as hot before any single lick is picked out. */
.bdg__crown::before {
  content: "";
  position: absolute;
  inset: calc(var(--badge-size) * -.16);
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(249, 115, 22, .32) 38%, transparent 76%);
  animation: bdg-crown-glow 2.6s ease-in-out infinite;
}
@keyframes bdg-crown-glow {
  0%, 100% { opacity: .6;  transform: scale(1); }
  34%      { opacity: 1;   transform: scale(1.06); }
  67%      { opacity: .52; transform: scale(.98); }
}

.bdg__crown-lick {
  --ang: calc(var(--a) * 60deg);
  --r: calc(var(--badge-size) * -.54);
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--badge-size) * .2);
  height: calc(var(--badge-size) * .28);
  margin: calc(var(--badge-size) * -.14) 0 0 calc(var(--badge-size) * -.1);
  /* Fades to nothing at the base, so a lick reads as coming OFF the rim rather
     than as a petal stuck to it. */
  background: linear-gradient(180deg, #fff3d0 0%, #fbbf24 32%, #f0620f 72%, rgba(220, 38, 38, 0) 100%);
  border-radius: 50% 50% 46% 46% / 66% 66% 34% 34%;
  transform-origin: 50% 50%;
  animation: bdg-crown-lick 1.55s ease-in-out infinite;
  animation-delay: calc(var(--a) * -.26s);
}
/* rotate THEN translate, so the lick travels out along its own axis and ends up
   pointing away from the badge. The rotate and the translate are repeated in
   every keyframe because a transform is replaced wholesale, not composed —
   dropping them from one stop would fling the lick back to the centre. */
@keyframes bdg-crown-lick {
  0%, 100% { transform: rotate(var(--ang)) translateY(var(--r)) scale(1, 1)      skewX(0deg);  opacity: .82; }
  26%      { transform: rotate(var(--ang)) translateY(var(--r)) scale(.8, 1.28)  skewX(-7deg); opacity: 1; }
  52%      { transform: rotate(var(--ang)) translateY(var(--r)) scale(1.08, .86) skewX(5deg);  opacity: .6; }
  78%      { transform: rotate(var(--ang)) translateY(var(--r)) scale(.9, 1.16)  skewX(-4deg); opacity: .95; }
}

/* Sealed: the crown goes with the mark. Six flames licking around a shroud
   would announce which badge is under it before it is opened, which is the
   same ruling the mark itself already gets. */
.bdg.bdg--sealed .bdg__crown { display: none; }
.bdg--locked .bdg__crown { filter: grayscale(1); opacity: .4; }

@media (prefers-reduced-motion: reduce) {
  .bdg__crown::before { animation: none; opacity: .8; }
  .bdg__crown-lick {
    transform: rotate(var(--ang)) translateY(var(--r)) scale(.88, 1.18) skewX(-5deg);
    opacity: .9;
  }
}

/* ==========================================================================
 * The climb celebration card — the no-JavaScript ending of a first send.
 *
 * This is NOT the celebration. The celebration is the reveal stage above,
 * which static/js/badge_reveal.js opens over the top of this card the moment
 * the page settles. What this styles is the fallback the card falls back TO:
 * script blocked, script failed, script still downloading, or a member who
 * turned it off — and in every one of those the congratulation still has to
 * land, because a first Red is a first Red whether or not a file downloaded.
 *
 * So it is deliberately quiet. Loud enough that it reads as a moment rather
 * than as a validation error, still enough that it is not competing with the
 * stage that is about to cover it. It is on screen for about 200ms in the
 * normal case.
 *
 * Tokens resolve console-name -> --clr-name -> literal hex, exactly as the
 * rest of this sheet does. A bare var(--clr-teal-4) here would render the card
 * invisible at HTTP 200 in the portal shell — see the header of this file.
 * ========================================================================== */

.climbwin {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  margin: 0 0 1.1rem;
  padding: 1.1rem 1rem 1rem;
  border-radius: 14px;
  border: 1px solid var(--clr-badge-grit-a55);
  background:
    radial-gradient(90% 70% at 50% 0%, var(--clr-badge-grit-a22), transparent 72%),
    var(--surface, var(--clr-surface, #1f2937));
  text-align: center;
  /* Runs once and ends, like the tile entrance it sits beside — never a loop.
     The one-moving-thing rule applies to the page this lands on too. */
  animation: bdg-rise .4s var(--badge-ease) both;
}

.climbwin__eyebrow {
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--clr-badge-grit);
}

.climbwin__headline {
  font-size: 1.15rem;
  font-weight: 800;
  line-height: 1.25;
  color: var(--text, var(--clr-text, #f3f4f6));
  text-wrap: balance;
}

/* The tile inside the card is the badge itself, so it keeps its own rules and
   only loses the grid's border — a bordered tile inside a bordered card reads
   as a box in a box. */
.climbwin .bdg {
  border-color: transparent;
  background: transparent;
  padding-top: .2rem;
}

.climbwin__go { margin-top: .35rem; }

@media (prefers-reduced-motion: reduce) {
  .climbwin { animation: none; }
}

/* ---- woodhold: the wooden problem on Wall 9 -------------------------------
 *
 * A grade 7 set entirely on timber holds, which is the joke the badge is built
 * around: you did, technically, climb a tree.
 *
 * It borrows the climbing hold's geometry, volume and bolt hole (the selectors
 * above), and replaces the two things that make a hold a HOLD rather than a
 * piece of wood: the colour, and the surface.
 *
 * WOOD IS NOT GRIT. The grade holds use isotropic fractal noise, which is what
 * sand through polyurethane looks like. Timber runs one way, so the same
 * feTurbulence is stretched anisotropically — a low frequency across the grain
 * and a high one along it — and the result is streaks rather than speckle. Two
 * turbulence layers at different scales, because one reads as corduroy: the
 * coarse pass is the grain, the fine pass is the roughness on top of it.
 *
 * Deliberately NOT tinted from --badge-band. The problem is a Black 7 and the
 * band colour would paint the hold charcoal, which is exactly the fact the
 * badge is joking about hiding. Wood is the point; the grade is in the words.
 * -------------------------------------------------------------------------- */

.bdg__mark--woodhold {
  --hold-w: 30px;
  --hold-h: 23px;
  --bolt-d: 6px;
  --hold-rot: -3deg;
  /* Chunkier and squarer than a moulded hold: a timber blank is cut, not cast,
     so the silhouette keeps its corners. */
  --hold-r: 34% 40% 36% 42% / 44% 46% 40% 38%;
  background-color: #b07a42;
  background-image:
    radial-gradient(120% 90% at 32% 8%, rgba(255, 240, 214, .45), rgba(255,255,255,0) 55%),
    radial-gradient(130% 110% at 60% 118%, rgba(60, 30, 8, .55), rgba(0,0,0,0) 60%),
    /* Growth rings, off to one side the way an off-centre cut shows them. */
    repeating-radial-gradient(ellipse 60% 120% at 22% 52%,
      rgba(94, 54, 20, .22) 0 2px,
      rgba(94, 54, 20, 0) 2px 6px);
  box-shadow:
    inset 0 1.5px 1px rgba(255, 235, 205, .4),
    inset 0 -2px 2px rgba(50, 25, 6, .5),
    0 2px 3px rgba(0, 0, 0, .55),
    0 0 0 .5px rgba(40, 20, 5, .45);
}

/* The grain. Coarse streaks along the length, blended so they darken and
   lighten the timber rather than greying it — the same `overlay` reasoning the
   grade holds' grit uses. */
.bdg__mark--woodhold::before {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='60'%3E%3Cfilter id='w'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.014 .62' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='120' height='60' filter='url(%23w)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cfilter id='r'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='60' height='60' filter='url(%23r)'/%3E%3C/svg%3E");
  background-size: 34px 17px, 18px 18px;
  mix-blend-mode: overlay;
  opacity: .8;
}

/* The bolt sits in a countersunk washer, which timber holds need and moulded
   ones do not — the reason every wooden hold has that pale ring around it. */
.bdg__mark--woodhold::after {
  box-shadow:
    inset 0 -1px 1px rgba(255, 240, 214, .35),
    0 0 0 1.5px rgba(255, 235, 205, .22),
    0 0 0 2.5px rgba(60, 30, 8, .35);
}

/* ---- rockhold: the real-rock climb ----------------------------------------
 *
 * The gym has one climb on actual stone. Every other hold in the building was
 * poured into a mould, so this is the only thing on the wall that nobody made
 * — which is the whole joke, and the reason the badge cannot just be a grey
 * version of a moulded hold.
 *
 * THREE THINGS SEPARATE STONE FROM THE OTHER TWO SURFACES, and they are all
 * shape rather than colour, so the difference survives greyscale:
 *
 *  - It is FACETED, not blobby. A moulded hold is a smooth cast form and gets
 *    border-radius; timber is a cut blank with soft corners; rock is fractured,
 *    so it gets an irregular clip-path polygon and hard-stopped gradients
 *    standing in for flat planes catching the light at different angles.
 *  - It has a QUARTZ VEIN. One bright seam running off-axis, which is the
 *    single most recognisable thing about a piece of real rock at 30px.
 *  - It has NO BOLT HOLE. Every other hold in this sheet has one, because every
 *    other hold is bolted on. Real rock is the wall. Its absence is the point,
 *    so the shared ::after is repurposed for the vein rather than suppressed.
 * -------------------------------------------------------------------------- */

.bdg__mark--rockhold {
  --hold-w: 31px;
  --hold-h: 26px;
  --hold-rot: -2deg;
  position: relative;
  display: block;
  z-index: 2;
  width: var(--hold-w);
  height: var(--hold-h);
  /* Fractured, not cast. Eight irregular vertices — no symmetry anywhere, which
     is what stops it reading as a gemstone. */
  clip-path: polygon(20% 6%, 58% 0%, 88% 18%, 100% 52%, 82% 88%, 40% 100%, 8% 78%, 0% 34%);
  border-radius: 0;
  background-color: #857e73;
  /* The stone itself, held in a custom property so the shiny variant can layer
     sapphire OVER it without restating any of this — two copies of five
     gradients is two things to keep in step, and they would not stay in step. */
  --rock-body:
    /* Flat planes. Hard stops, because a facet has an edge and a gradient
       ramp reads as a dent. */
    linear-gradient(118deg, rgba(255, 252, 244, .26) 0 34%, rgba(255,255,255,0) 34%),
    linear-gradient(-38deg, rgba(24, 18, 10, .34) 0 28%, rgba(0,0,0,0) 28%),
    linear-gradient(72deg, rgba(24, 18, 10, .22) 0 18%, rgba(0,0,0,0) 18%),
    radial-gradient(120% 90% at 34% 6%, rgba(255, 250, 238, .3), rgba(255,255,255,0) 58%),
    radial-gradient(130% 110% at 62% 116%, rgba(20, 14, 8, .5), rgba(0,0,0,0) 60%);
  background-image: var(--rock-body);
  transform: rotate(var(--hold-rot));
  box-shadow: 0 2px 3px rgba(0, 0, 0, .55);
}

/* Mineral speckle: finer and higher-contrast than the moulded holds' grit,
   because sand IN a resin is soft-edged and crystal ON a rock face catches the
   light hard. Two frequencies — coarse inclusions over fine grain. */
.bdg__mark--rockhold::before {
  content: "";
  position: absolute;
  inset: 0;
  clip-path: inherit;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='70' height='70'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='70' height='70' filter='url(%23s)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='70' height='70'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.32' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='70' height='70' filter='url(%23c)'/%3E%3C/svg%3E");
  background-size: 15px 15px, 30px 30px;
  mix-blend-mode: overlay;
  opacity: .85;
  pointer-events: none;
}

/* The quartz vein — the shared bolt-hole slot, spent on the thing that says
   "this is stone" instead of on the thing that says "this was bolted on". */
.bdg__mark--rockhold::after {
  content: "";
  position: absolute;
  left: -12%;
  right: -12%;
  top: 46%;
  height: 2.5px;
  clip-path: inherit;
  background: linear-gradient(90deg,
      rgba(255,255,255,0) 0%,
      rgba(255, 253, 245, .72) 22%,
      rgba(255, 253, 245, .9) 48%,
      rgba(255, 253, 245, .55) 70%,
      rgba(255,255,255,0) 100%);
  transform: rotate(-19deg);
  filter: blur(.3px);
  pointer-events: none;
}

/* ---- the shiny rock: sapphire in the stone --------------------------------
 *
 * Keiran's, and it is a better idea than the house treatment for this one
 * badge. Everything else in this system says "shiny" with foil, which is the
 * language of a trading card. A rock has its own way of being precious, and it
 * is gem inclusions — so the shiny rock grows sapphire.
 *
 * Five specks, no two alike, at irregular positions and sizes. Each is drawn
 * as three stops rather than a dot: a near-white glint, a deep sapphire body,
 * and a dark rim. That is what stops them reading as blue confetti — a gem
 * catches light on one facet and swallows it everywhere else, and the glint
 * has to sit OFF-CENTRE for the speck to look faceted rather than spherical.
 *
 * Layered over --rock-body rather than replacing it, so the facets, the
 * volume and the shadow underneath are untouched and there is only ever one
 * definition of what the stone looks like.
 *
 * STATIC, like every other shiny material in this sheet. The rainbow foil does
 * not move either; the three sparkle spans are what animate, and only when the
 * tile wins the spotlight. Five twinkling gems on top of those would be the
 * wall of motion the whole system is written against.
 * -------------------------------------------------------------------------- */

.bdg__gem { display: none; }

/* Sapphire, hexagonal and faceted.
 *
 * Corundum crystallises hexagonal, so a round blue dot is the one detail a
 * climber who knows rock would notice. The silhouette reuses --badge-hex, the
 * same polygon the Rock and Gym-history medallions are cut from, so the gems
 * rhyme with the tier shapes instead of introducing a ninth outline.
 *
 * The facets are a conic-gradient with HARD stops: six wedges radiating from
 * the centre, alternating light and dark, which is what the crown of a cut
 * stone does to light. A smooth conic would read as a soap bubble. On top of
 * that sits a small off-centre table highlight, because a gem's brightest point
 * is never its middle.
 *
 * Each gem takes its position, size and rotation from the --g the template
 * stamps. Rotating them differently is what stops five identical hexagons
 * reading as a pattern someone stamped on.
 */
.bdg--shiny .bdg__gem {
  display: block;
  position: absolute;
  width: var(--g-size);
  height: var(--g-size);
  margin: calc(var(--g-size) / -2) 0 0 calc(var(--g-size) / -2);
  clip-path: var(--badge-hex);
  transform: rotate(var(--g-rot));
  /* drop-shadow, not box-shadow: box-shadow is drawn on the element's border
     box and then clipped away with everything outside the hexagon, so it never
     appears. A filter shadow follows the CLIPPED shape, which is what seats the
     gem in the rock instead of leaving it sitting on top like a sticker. */
  filter: drop-shadow(0 .5px .6px rgba(6, 12, 30, .75));
  background:
    /* girdle: the outer edge darkens, the way a stone set into something does
       where it meets the surface */
    radial-gradient(circle closest-side at 50% 50%,
      rgba(0,0,0,0) 0 58%, rgba(4, 10, 28, .55) 92%),
    /* table: off-centre, small, and the only near-white in the gem */
    radial-gradient(circle closest-side at 42% 36%,
      rgba(255,255,255,.95) 0 22%, rgba(219,234,254,.5) 22% 44%, rgba(255,255,255,0) 44%),
    /* Crown facets, and they are TRANSLUCENT.
     *
     * Sapphire transmits light — an opaque blue hexagon is a bead sitting on
     * the rock, not a crystal in it. Carrying the alpha in the gradient STOPS
     * rather than as `opacity` on the element is what lets the stone's grain
     * show faintly through the body while the table highlight above stays at
     * full strength: element opacity would fade the glint too, and the glint is
     * the whole reason it reads as cut rather than as a blue shape.
     *
     * The facets keep slightly different alphas as well as different blues.
     * A real stone is not uniformly clear, and varying both is what stops the
     * six wedges reading as a flat pinwheel. */
    conic-gradient(from 12deg,
      rgba(30, 58, 138, .82) 0deg 60deg,
      rgba(96, 165, 250, .72) 60deg 120deg,
      rgba(29, 78, 216, .80) 120deg 180deg,
      rgba(59, 130, 246, .74) 180deg 240deg,
      rgba(23, 37, 84, .86) 240deg 300deg,
      rgba(37, 99, 235, .78) 300deg 360deg);
  z-index: 2;
}

/* Five stones, none of them the same size, none on the quartz vein, and none
   turned the same way. */
.bdg--shiny .bdg__gem:nth-child(1) { --g-size: 7px;   --g-rot: 8deg;   top: 30%; left: 24%; }
.bdg--shiny .bdg__gem:nth-child(2) { --g-size: 5px;   --g-rot: -22deg; top: 19%; left: 66%; }
.bdg--shiny .bdg__gem:nth-child(3) { --g-size: 6.5px; --g-rot: 34deg;  top: 63%; left: 72%; }
.bdg--shiny .bdg__gem:nth-child(4) { --g-size: 5.5px; --g-rot: -9deg;  top: 72%; left: 35%; }
.bdg--shiny .bdg__gem:nth-child(5) { --g-size: 4px;   --g-rot: 18deg;  top: 45%; left: 50%; }

/* The stone keeps its own body: the gems are IN it, not painted over it. */
.bdg.bdg--shiny .bdg__mark--rockhold { background-image: var(--rock-body); }


/* The stone's own glow when it wins the spotlight — cool rather than the warm
   light the other holds sit under, because that is what blue stone does to the
   light around it. Cheap: one box-shadow, no extra layer. */
.bdg--spotlight.bdg--shiny .bdg__mark--rockhold,
.bdg-reveal__badge .bdg--shiny .bdg__mark--rockhold {
  box-shadow:
    0 2px 3px rgba(0, 0, 0, .55),
    0 0 10px 1px rgba(59, 130, 246, .45);
}

/* ---- rocket: the Rainbow Rocket, drawn --------------------------------------
 *
 * Red nose, white body, red fins, one porthole. The rocket every four-year-old
 * draws, on purpose — this is the badge on the wall that is allowed to be fun.
 *
 * Fifteen points and no curves. A curve costs nothing here and reads as mush at
 * 19px, where a hard silhouette still reads as a rocket.
 * -------------------------------------------------------------------------- */

.bdg__mark--rocket {
  width: 19px;
  height: 30px;
  /* Nose, shoulders, straight body, fins flaring at the base, nozzle notch. */
  clip-path: polygon(
    50% 0%, 68% 17%, 74% 44%, 74% 58%,
    93% 79%, 93% 96%, 68% 84%, 66% 90%,
    34% 90%, 32% 84%, 7% 96%, 7% 79%,
    26% 58%, 26% 44%, 32% 17%
  );
  background:
    linear-gradient(to bottom, #e2202e 0 16%, rgba(0,0,0,0) 16%),   /* nose cone */
    linear-gradient(to bottom, rgba(0,0,0,0) 0 70%, #e2202e 70%),   /* fins */
    /* Lit from the left, hard stops: the body is a cylinder and a soft ramp at
       this size reads as a smudge rather than as a curved surface. */
    linear-gradient(96deg, #ffffff 0 25%, #e3e8f0 25% 54%, #aeb8c9 54% 79%, #79839a 79%);
  filter: drop-shadow(0 1px 1.5px rgba(0,0,0,.65));
}

/* It rides up out of its own flare rather than sitting in the middle of it.
   Thirteen pixels is the difference between launching and hovering. */
.bdg-art--rocket-launch .bdg__mark--rocket { margin-bottom: 13px; }

/* The porthole. Off-centre glint, the same trick the sapphire specks use: a
   flat disc reads as a sticker, a glint reads as glass. */
.bdg__mark--rocket::before {
  content: "";
  position: absolute;
  left: 50%; top: 25%;
  width: 7.5px; height: 7.5px;
  margin-left: -3.75px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 28%,
    #eaf6ff 0 24%, #6fb2ea 24% 52%, #2a6bb8 52% 82%, #13375f 82%);
  box-shadow: 0 0 0 1px rgba(10,16,28,.55);
}

/* ---- bathang: the bat hang start -------------------------------------------
 *
 * Every other mark in this sheet sits on the ground. This one HANGS, and that
 * single inversion is what makes it readable at 36px before anybody reads the
 * name — no other badge can be confused for it at any size.
 *
 * THE SILHOUETTE TOOK FOUR GOES and each failure is worth recording, because
 * every one of them looked reasonable in the CSS:
 *
 *   Square body, stubby wings            -> a squid.
 *   Tapered wings held out sideways      -> a scarecrow with its arms out. A
 *                                           bat wing is only a bat wing because
 *                                           its trailing edge ZIG-ZAGS; without
 *                                           that it is an arm.
 *   Wings held level with the hold       -> a mushroom. The grey hold was the
 *                                           cap and the body was the stem.
 *   Wings SWEPT DOWN into a V            -> a bat. It stops touching the hold,
 *                                           the silhouette goes narrow at the
 *                                           top and wide below, and the whole
 *                                           thing finally reads as hanging
 *                                           rather than perched.
 *
 * Two finger points per wing, not four: four is anatomy and turns to mush at
 * this size, two is the read.
 * -------------------------------------------------------------------------- */

.bdg__mark--bathang {
  position: absolute;
  top: 11px;
  left: 50%;
  margin-left: -19px;
  width: 38px;
  height: 32px;
  /* Clockwise from the hooked feet: shoulder, leading edge, wingtip, back along
     the scalloped trailing edge to the body, then the head — which is at the
     BOTTOM, with two ears, because the whole animal is upside down. */
  clip-path: polygon(
    46% 0%, 54% 0%,
    58% 12%, 76% 26%, 100% 50%,
    84% 52%, 90% 70%, 70% 56%, 76% 78%, 60% 54%,
    58% 68%, 64% 88%, 54% 84%, 50% 100%, 46% 84%, 36% 88%, 42% 68%,
    40% 54%, 24% 78%, 30% 56%, 10% 70%, 16% 52%,
    0% 50%, 24% 26%, 42% 12%
  );
  /* Violet-black, not black. #c4b5fd is already this sheet's gym-history hue,
     so the bat reads as family — where a true black silhouette on a #0b1220
     face stops being an animal and becomes a hole in the tile. The bottom stop
     deliberately falls short of black so the scalloped edge still separates
     from the face. Lit along the top, because a bat lit from below is a bat in
     a horror film. */
  background: linear-gradient(to bottom,
    #8878b4 0 8%, #635291 8% 22%, #453a68 22% 52%, #322a4d 52%);
  filter:
    drop-shadow(0 -1px 0 rgba(196,181,253,.5))
    drop-shadow(0 2px 3px rgba(0,0,0,.7));
}

/* Two eyes, on the head, low. At 38px these are 2.2px across and they are the
   entire difference between a bat and a moth. */
.bdg__mark--bathang::after {
  content: "";
  position: absolute;
  left: 50%; top: 76%;
  width: 6px; height: 2.2px;
  margin-left: -3px;
  background:
    radial-gradient(circle closest-side, #fcd34d 0 62%, rgba(0,0,0,0) 62%) left center / 2.2px 2.2px no-repeat,
    radial-gradient(circle closest-side, #fcd34d 0 62%, rgba(0,0,0,0) 62%) right center / 2.2px 2.2px no-repeat;
}

/* It swings, always — like the flame licks and the rocket's sky drifts. A bat
   hanging perfectly still is a dead bat, and hanging is the entire read of this
   badge. Slow and shallow (9 degrees over 3.6s) so it is a breath rather than a
   metronome. Origin at the feet, so it swings from where it is holding on — and
   the hold is a separate element, so it stays put while the bat moves. */
.bdg__mark--bathang {
  transform-origin: 50% 3%;
  animation: bdg-batsway 3.6s ease-in-out infinite;
}

@keyframes bdg-batsway {
  0%, 100% { transform: rotate(-4.5deg); }
  50%      { transform: rotate(4.5deg); }
}

/* Except on a TEASER. A locked fixture is a thing you have not done yet, and
   this sheet already rules that nothing about one moves (see .bdg--locked
   .bdg__art, which greys the artwork and stops the rocket's sky). A bat
   swinging cheerfully on a badge somebody has not climbed is the same mistake
   in a different element.

   Targeted at the bat rather than blanketed over .bdg--locked *, because the
   flame's SHAPE comes out of its keyframes — a square with one rounded corner
   turned 45deg — so cancelling animation wholesale on a locked tile would
   leave a teardrop badge wearing an unrotated blob. */
.bdg--locked .bdg__mark--bathang { animation: none; }

/* On the foil the violet loses its contrast, so the shiny bat goes darker and
   keeps a brighter top edge — same silhouette, still a hole punched in the
   rainbow rather than a smudge sitting on it. */
.bdg.bdg--shiny .bdg__mark--bathang {
  background: linear-gradient(to bottom,
    #574a7c 0 7%, #332a4c 7% 34%, #1c1730 34%);
  filter:
    drop-shadow(0 -1px 0 rgba(255,255,255,.65))
    drop-shadow(0 2px 3px rgba(0,0,0,.78));
}

/* ---- kickflip: the Kick Flip, drawn -----------------------------------------
 *
 * The deck in SIDE PROFILE — kicked nose, kicked tail, two trucks, two wheels
 * — tilted, with the arc it came up through behind it.
 *
 * IT WAS DRAWN FROM BELOW FIRST and that draft is worth recording, because it
 * was the more interesting idea and it failed completely. The reasoning went:
 * a board the right way up is a skateboard, a board upside down is a skateboard
 * MID-TRICK, so draw the underside with its printed graphic and the wheels
 * pointing at you. On the page it was a FACE. Two pale wheels, symmetrical, at
 * the vertical centre of an orange lozenge, are two eyes — and once seen it
 * could not be unseen at any size. The bat's four rejected silhouettes
 * (a squid, a scarecrow, a mushroom, a flying saucer) are the same lesson: at
 * 30px the drawing you get is the one the eye's pattern-matcher offers first,
 * not the one the CSS describes.
 *
 * So legibility took the argument. Side-on is the silhouette every human being
 * already holds for "skateboard", it cannot be read as anything else, and the
 * board being in the AIR is carried by the tilt and the arc rather than by the
 * viewing angle.
 *
 * THE SECOND DRAFT WAS A SKATEBOARD AND STILL NOT A GOOD ONE, which is the
 * more useful failure of the two, because everything wrong with it was
 * craft rather than concept:
 *
 *   An 18-POINT POLYGON for the outline. A deck is two long shallow curves a
 *   constant distance apart, and a polygon can only approximate a curve with
 *   corners — at 34px the kicks came out as chipped wedges and the belly as a
 *   sag. `clip-path: path()` takes the same beziers the shape is actually made
 *   of, so the kicks are kicks and the rail is a rail.
 *
 *   FIVE BANDS OF MAPLE, drawn as a 2px repeating gradient. A deck is
 *   seven-ply and five was already the concession to legibility; the honest
 *   number at three pixels of visible ply is ONE. Five bands at that size is
 *   not five bands, it is a stripe pattern beating against the pixel grid, and
 *   the deck read as a grey-brown smear. It is now a single warm rail, lit
 *   under the grip and dark at the bottom edge, which is what the eye takes
 *   from a real deck at arm's length anyway.
 *
 *   A HORIZONTAL BLACK BAND for the grip tape. Grip tape follows the deck; a
 *   band cut straight across the box does not, so at the kicked ends the black
 *   landed halfway down the wood. The grip is now its own path sharing the
 *   deck's top curve exactly, so it stays on top of the board wherever the
 *   board goes.
 *
 * Three surfaces, and the split is structural rather than tidy: a clip-path
 * clips its own pseudo-elements, so anything outside the deck outline has to
 * live outside the clipped elements. The ELEMENT's own background carries the
 * running gear — baseplate, hanger, wheel, bearing, painted in that order so
 * the wheel hides the axle the way it does side-on — and the two pseudos carry
 * the deck: ply on ::before, grip tape on ::after.
 *
 * SIZE IS PINNED BY THE SMALLEST FACE, not by the biggest. Below 420px the
 * portal drops --badge-size to 50px, and the hexagon is only 43px across at
 * its widest; a board long enough to look right at 68px had its tail sliced
 * off by the clip on a phone. The board is drawn to fit the 50px hexagon with
 * a couple of pixels to spare and is the same 36px everywhere, which is the
 * same trade every other mark in this sheet makes.
 *
 * The FLIP is spent where a member is actually looking — see the keyframes.
 * -------------------------------------------------------------------------- */

.bdg__mark--kickflip {
  width: 36px;
  height: 20px;
  /* perspective() FIRST so the flip below is a board turning in space rather
     than a rectangle being squashed flat. rotateX(0) is the resting pose,
     written out rather than omitted so the still state and the keyframe's 0%
     frame are visibly the same thing. */
  transform: perspective(175px) rotate(-26deg) rotateX(0deg);
  /* A hairline of white along the top edge and a real shadow under it. The
     first is what stops a near-black grip tape dissolving into a near-black
     face; the second is what keeps the board an object sitting in front of the
     tile rather than a hole cut in it. */
  filter:
    drop-shadow(0 -.5px 0 rgba(255,255,255,.22))
    drop-shadow(0 2px 2.5px rgba(0,0,0,.72));
  /* It turns, and it turns on the badge's own class. See the keyframes. */
  animation: bdg-kickflip 5.5s cubic-bezier(.34, 0, .32, 1) infinite;
  /* Running gear, front layer first. The bearing sits inside the wheel, the
     wheel covers the end of the hanger, the hanger hangs off the baseplate,
     and the baseplate disappears under the deck drawn on ::before — which is
     the order the parts actually stack when you look at a board side-on.
     Circles are the one thing a background layer draws better than a
     clip-path, and a wheel is nothing but circles. */
  background:
    radial-gradient(circle closest-side,
      #262b33 0 46%, #6d7482 46% 78%, rgba(0,0,0,0) 80%)
      8.3px 13.3px / 2.8px 2.8px no-repeat,
    radial-gradient(circle closest-side,
      #262b33 0 46%, #6d7482 46% 78%, rgba(0,0,0,0) 80%)
      24.4px 13.3px / 2.8px 2.8px no-repeat,
    /* Urethane, lit from up and left, with the bearing showing. A flat disc is
       a sticker; a disc with a hole in it is a wheel — the porthole's trick,
       borrowed. */
    radial-gradient(circle closest-side at 40% 32%,
      #fffaee 0 16%, #f7e6c2 44%, #e0c79a 66%, #ab946c 86%, rgba(0,0,0,0) 88%)
      6.1px 11.1px / 7.2px 7.2px no-repeat,
    radial-gradient(circle closest-side at 40% 32%,
      #fffaee 0 16%, #f7e6c2 44%, #e0c79a 66%, #ab946c 86%, rgba(0,0,0,0) 88%)
      22.2px 11.1px / 7.2px 7.2px no-repeat,
    /* The hanger. Three pixels of dark between deck and axle, and it is not
       decoration: without it the wheels float, and two cream discs floating
       under a plank is a trolley. */
    radial-gradient(ellipse closest-side,
      #525b6b 0 58%, #3b4350 58% 78%, rgba(0,0,0,0) 80%)
      7.1px 9.6px / 5.3px 4.7px no-repeat,
    radial-gradient(ellipse closest-side,
      #525b6b 0 58%, #3b4350 58% 78%, rgba(0,0,0,0) 80%)
      23.2px 9.6px / 5.3px 4.7px no-repeat,
    linear-gradient(#616b7b, #3b4350) 6.6px 8.8px / 6.2px 2px no-repeat,
    linear-gradient(#616b7b, #3b4350) 22.7px 8.8px / 6.2px 2px no-repeat;
}

/* The deck — ply. Two shallow curves a constant thickness apart, kicked up at
   both ends, which is the whole difference between a skateboard and a plank.

   Beziers rather than a polygon, because that is what the shape is: the top
   curve here and the top curve of the grip tape below are the SAME four
   points, so the two can never drift apart and the grip can never slide off
   the board.

   One rail, not five bands. Lit just under the grip and dark at the bottom
   edge, because that is where the light is on a deck stood on its side, and
   because a warm line reading as timber is worth more here than a ply count
   nobody can resolve. */
.bdg__mark--kickflip::before {
  content: "";
  position: absolute;
  inset: 0;
  clip-path: path("M 1.4 2.5 C 2.3 3.9, 3.7 5, 6 5.7 C 10.7 6.8, 24.8 6.8, 29.5 5.7 C 31.8 5, 33.2 3.9, 34.1 2.5 L 35.1 4 C 34.6 5.6, 32.7 7.3, 29.9 8.3 C 24.8 9.4, 10.7 9.4, 5.6 8.3 C 2.8 7.3, 0.9 5.6, 0.4 4 Z");
  background: linear-gradient(to bottom,
    #f2d2a5 0 7px, #e0ae7c 7.8px, #bf8447 8.5px, #8f5e2a 9.3px, #62401d 20px);
}

/* The deck — grip tape. The same top curve as the ply, one pixel of it, so the
   black follows the board round both kicks instead of being cut across them.
   Lighter at the very top edge: grip is the one surface on a board that faces
   straight up. */
.bdg__mark--kickflip::after {
  content: "";
  position: absolute;
  inset: 0;
  clip-path: path("M 1.4 2.5 C 2.3 3.9, 3.7 5, 6 5.7 C 10.7 6.8, 24.8 6.8, 29.5 5.7 C 31.8 5, 33.2 3.9, 34.1 2.5 L 34.5 3.1 C 33.9 4.4, 32.2 5.7, 29.7 6.7 C 24.8 7.8, 10.7 7.8, 5.8 6.7 C 3.2 5.7, 1.6 4.4, 1 3.1 Z");
  background: linear-gradient(to bottom,
    #454d5c 0 3.2px, #2c333e 4.7px, #1a1f27 6.1px, #0f1218 7.9px, #0c0e13 20px);
}

/* THE FLIP. One full turn about the deck's long axis, which is what a kickflip
   is.

   IT TURNS ALWAYS, keyed off the badge's own class, because this sheet's
   header rules that the spotlight means BRIGHTER and never MOVING — and the
   first draft of this mark broke that rule, flipping only when the tile was the
   page's pick. That is the old on/off gate wearing a new mark: the same badge
   would have been a still drawing on the badges page, a moving one on the
   reveal, and a different thing again depending on what else the member
   happened to hold that week.

   The 0-78% HOLD is what makes a fourth moving mark affordable at all. Five and
   a half seconds, of which four and a quarter are the board sitting perfectly
   still: on a wall of twenty tiles this is a flip caught out of the corner of
   the eye every few seconds, not a fairground ride. A board revolving without
   pause is not a trick — a trick has a before and an after, which is exactly
   what the hold draws. */
@keyframes bdg-kickflip {
  0%, 78% { transform: perspective(175px) rotate(-26deg) rotateX(0deg); }
  100%    { transform: perspective(175px) rotate(-26deg) rotateX(360deg); }
}

/* Winning the spotlight — or landing on the reveal stage — lands it more often.
   A gear change and not a switch: same keyframes, same distance, a third of the
   wait between goes. The rocket's bargain with its sky, exactly. */
.bdg--spotlight .bdg__mark--kickflip,
.bdg-reveal__badge .bdg__mark--kickflip { animation-duration: 2.1s; }

/* Except on a teaser. A locked fixture is a thing you have not done yet and
   nothing about one moves — the bat's rule, needed here for the bat's reason:
   the pose lives in the keyframes, so this hands the still `transform` on the
   mark back. */
.bdg--locked .bdg__mark--kickflip { animation: none; }

/* And except when the phone has asked for less of it. `animation: none` returns
   the board to the resting transform declared on the element rather than to
   wherever a keyframe was cancelled — which is the entire reason that pose is
   written on the element and not only in the 0% stop. A still board is not a
   loss here: the line under this badge reads "You LANDED the Kick Flip", past
   tense, and a photograph does not have to keep moving to be of something that
   was. */
@media (prefers-reduced-motion: reduce) {
  .bdg__mark--kickflip { animation: none; }
}

/* On the foil the slate half of the graphic sinks into the rainbow, so the deck
   keeps a lit top edge and a heavier shadow — same silhouette, still an object
   sitting on the foil rather than a stain in it. Exactly the fix the shiny bat
   needed, for exactly the same reason. */
.bdg.bdg--shiny .bdg__mark--kickflip {
  filter:
    drop-shadow(0 -1px 0 rgba(255,255,255,.6))
    drop-shadow(0 2px 3px rgba(0,0,0,.82));
}

/* PORTED 30 August 2026 from the badges Artifact, replacing the drawing that
 * was here. The Artifact is the design surface for this set and had moved
 * ahead of main; the badge, its name, its tier and its mechanic are unchanged
 * — only the picture inside the coin. The superseded drawing is DELETED rather
 * than parked behind a flag: it has no callers, and a second copy of a rejected
 * picture is a thing somebody re-enables by accident in a year.
 *
 * One change on the way down, the same one every drawing off that page gets:
 * the Artifact keeps a STILL copy and a MOVING copy because which of the two
 * ships had not been decided. It has — the moving one — so the modifier folds
 * into the art class and a Badge names its art and gets the whole drawing. */

/* ---- firstlight: the sky the doors open onto -------------------------------
 *
 * The second badge in this set to make the whole face a WINDOW rather than a
 * dark field with an object in it, and it is here because of the first: the
 * night owl's sky is the best thing on this page, and "Doors open" sitting
 * beside it as a small warm object floating in a dark disc looked like the
 * draft it was.
 *
 * WHICH IMMEDIATELY BREAKS THE ARGUMENT THAT KEPT THESE TWO APART. The sundown
 * block below separated them structurally — empty dark field versus full sky —
 * and half of that separation has just been spent. It is rebuilt here on
 * different ground, and the block below has been rewritten to say so rather
 * than left standing as a claim the drawing no longer honours:
 *
 *   Doors open  — a COLD sky. Indigo down through blue to a single narrow warm
 *                 band at the horizon. Seven rays. The sun is ABOVE the line
 *                 and rising. Nothing stands between you and it: no wall, no
 *                 bird, no star. Empty is still the read — it has just moved
 *                 from the field to the FOREGROUND, which is where it always
 *                 belonged, because the badge is about arriving before anyone.
 *   Night owl   — a WARM sky. Plum, rose and orange from the zenith down. No
 *                 rays. The sun is BEHIND the line and going. A wall, a hold,
 *                 a bird and a star are in front of it.
 *
 * Cold versus warm, rising versus setting, rays versus none, and an empty
 * foreground versus a built one. Four differences where there were three, and
 * the two that carry it at 56px — the colour of the sky and the direction of
 * the sun — are the two you can see before you can see anything else.
 *
 * The atmospheric claim is real and not a convenience. A sunset sky is red
 * high up because a whole day of heat and dust is still suspended in it; the
 * air at first light has had all night to settle, so the warmth is confined to
 * a narrow strip at the horizon and everything above it is still last night's
 * blue. Painting dawn as a mirrored sunset is the mistake, and it is the one
 * that would have made these two badges the same picture in different colours.
 *
 * PINNED IN PX FROM THE FACE'S CENTRE, for the sundown block's reason and with
 * one extra constraint it did not have: this sky has to line up with a mark
 * that was drawn before it. The sunrise mark is 34x29 and centred, and its lit
 * line sits at local y=22 — which is 7.5px below the mark's own centre, and
 * therefore 7.5px below the face's. Every hard feature below is measured from
 * calc(50% + 7.5px). Get that wrong by a pixel and the badge has two horizons.
 * -------------------------------------------------------------------------- */

/* The sky draws the horizon rim to rim, so the mark must not draw one as well.
   This is the sundown badge's own ruling inverted: there the top-out lip IS the
   line and the sky layer has none; here the sky IS the line and the mark drops
   its own. Either way exactly one layer owns the horizon. */
.bdg-art--firstlight .bdg__mark--sunrise { --sun-line: none; }

.bdg-art--firstlight .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background:
    /* The air the sun is coming up through. Wide and shallow, because that is
       what haze at a horizon does to a light source, and centred exactly on the
       mark's own sun so the disc sits IN the sky rather than on top of it. */
    radial-gradient(23px 12px at 50% calc(50% + 7.5px),
      rgba(255,235,186,.52) 0 10%,
      rgba(255,206,128,.30) 36%,
      rgba(246,150,86,.11)  64%,
      rgba(230,120,80,0)    100%),
    /* The ramp. Cold nearly all the way down and then a narrow warm strip, as
       against the sundown ramp which is warm from the zenith. The stops between
       calc(50% - 2px) and the horizon do the entire job in nine pixels; spread
       them over thirty and this is a beach sunset with the hue rotated. */
    linear-gradient(to bottom,
      #050a1e  0%,
      #0a1435 16%,
      #16305c 33%,
      #24587f calc(50% - 8px),
      #3f8298 calc(50% - 2px),
      #86adab calc(50% + 2px),
      #d3b98d calc(50% + 5px),
      #ffdc9c calc(50% + 7.5px),
      /* Under the line it is still night. The ground has not caught up with
         the sky and will not for another twenty minutes, which is the whole
         reason the badge's own light-on-the-ground reads as light at all —
         drop a lit ground in here and that wash has nothing to be brighter
         than. */
      #2b211d calc(50% + 8.8px),
      #17121a calc(50% + 15px),
      #08070a 100%);
  /* The lit top edge and the shadow under it. Same reason the sundown sky
     carries its own: `.bdg__face--engraved` is an INSET box-shadow on the face
     and an inset shadow paints beneath the face's children, so on any tile
     whose ::before covers the face wall to wall the engraving is drawn and
     then immediately buried. */
  box-shadow:
    inset 0  1px 0 rgba(255,255,255,.14),
    inset 0 -1px 0 rgba(0,0,0,.5);
}

/* Ground mist. The only thing in the foreground, and it is deliberately not a
 * cloud: cloud bars are spoken for one badge over, where they are up in the
 * sky doing the sunset's weather. Dawn's weather is on the GROUND — cold air
 * sitting in the low places until the sun burns it off — so the two badges do
 * not end up drawing the same bar in different places and calling it different
 * weather.
 *
 * Two, at different heights and neither centred under the sun. Symmetrical mist
 * is a smudge; mist that has drifted is mist. The near one is cold because
 * nothing has reached it yet, and the far one — closer to the horizon, on the
 * sun's side — has just caught the first of the light. That pair is the badge's
 * whole sentence in two gradients.
 *
 * Full-bleed rather than a centred bar, so it reaches the rim at every face
 * size and in both silhouettes. */
.bdg-art--firstlight .bdg__art {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  transform: none;
  background:
    radial-gradient(17px 1.7px at calc(50% + 5px) calc(50% + 10.4px),
      rgba(255,226,180,.26) 0 44%, rgba(255,226,180,0) 100%),
    radial-gradient(25px 3px at calc(50% - 4px) calc(50% + 14px),
      rgba(188,209,222,.13) 0 44%, rgba(188,209,222,0) 100%);
}

/* Winning the spotlight turns the light UP, never on — the house rule, and now
   the fourth badge in a row to inherit it unchanged. The sky warms a shade and
   the mist catches more of it. The sun does not climb, for the reason the
   sunrise block gives at length. */
/* Two custom properties rather than a bare filter, because a moving copy of
   this badge animates the same property and would otherwise throw this rule
   away. The static filter stays for the still copy, which has no animation to
   read the variables. */
.bdg--spotlight.bdg-art--firstlight .bdg__face::before,
.bdg-reveal__badge .bdg-art--firstlight .bdg__face::before {
  --sky-sat: 1.14;
  --sky-bri: 1.08;
  filter: saturate(var(--sky-sat)) brightness(var(--sky-bri));
}
.bdg--spotlight.bdg-art--firstlight .bdg__art,
.bdg-reveal__badge .bdg-art--firstlight .bdg__art {
  filter: brightness(1.22);
}

/* Locked. The sky is the reward, so the sky is what a locked tile does not get
   — knocked back to almost nothing and drained, which leaves the recessed dark
   face the locked treatment is supposed to be. Exactly the sundown ruling and
   exactly its numbers, because two badges that share a construction should not
   grey differently. The mist goes with it: a lit mist bar over a drained sky
   would be the one thing in the drawing that had not noticed it was not
   earned. */
.bdg--locked.bdg-art--firstlight .bdg__face::before {
  filter: grayscale(1);
  opacity: .16;
}
.bdg--locked.bdg-art--firstlight .bdg__art { opacity: .2; }

/* Foil. The sky yields to the rainbow rather than fighting it — the rocket's
   ruling — held at .72 rather than the rocket's .55, because on that badge the
   sky is scenery and on this one the sky is half the badge. */
.bdg.bdg--shiny.bdg-art--firstlight .bdg__face::before { opacity: .72; }

/* ---- sunrise: doors open ----------------------------------------------------
 *
 * A sun that has just cleared the horizon. Seven rays, a half disc, and the
 * light thrown down onto the ground in front of it.
 *
 * It was drawn as a MARK ON AN EMPTY FIELD and it is not one any more: the
 * `firstlight` sky above puts it in a window, and the two are designed as one
 * drawing even though they are separate classes. The split is deliberate and
 * it is the sundown badge's arrangement copied — a mark can be lifted onto a
 * plain tier face by dropping one class, which is what the whole badge did for
 * its first day, and everything below still works there. The one thing the sky
 * takes away is the horizon line, and it takes it by setting `--sun-line` to
 * `none` rather than by overriding a gradient.
 *
 * THIS REPLACES A ROLLER SHUTTER, and it overturns a decision this sheet had
 * already recorded, so the reversal gets written down rather than quietly done.
 * The objection stands and has not been answered, only outvoted: the gym opens
 * at 14:00 Monday to Thursday, and a sunrise says dawn to four days in seven.
 * What is bought with that is legibility. The shutter needed three light rails,
 * a bottom rail, a lintel and a wedge of spill before it stopped reading as a
 * document icon — five parts arguing their way to a picture — and a sun coming
 * up is understood at a glance, at any size, by everybody. It is the drawing
 * every language already has for a thing OPENING.
 *
 * The sentence under the badge is what carries the accuracy: within half an
 * hour of the doors unlocking, measured from the hours table. The mark is
 * allowed to be the metaphor. That is a knowing trade and the note under the
 * specimen says so out loud.
 *
 * IT IS HALF UP, never whole — the shutter's one argument kept intact. A full
 * disc in a clear field is the middle of the day. A disc cut by the horizon is
 * a sun ARRIVING, which is the minute this badge counts and not the hours
 * after it.
 *
 * NOTHING MOVES, and here the house default survives its best objection: a
 * rising sun is the one subject in this sheet with motion in its name. But that
 * motion is invisible at human speed. A sun you can watch move is a time-lapse,
 * and a time-lapse on a 56px medallion is a loading spinner with a face on it.
 * The flame licks because a still flame is a lie; a still sunrise is a
 * photograph, and photographs of sunrises are fine. The spotlight turns the
 * light UP instead — which is the only thing a spotlight is allowed to mean
 * anywhere on this sheet.
 * -------------------------------------------------------------------------- */

.bdg__mark--sunrise {
  /* One colour for all seven rays, hoisted so the spotlight can change the
     light without restating a fourteen-stop gradient underneath it. */
  --sun-ray: rgba(255,201,113,.62);
  /* A no-op shadow the moving copy's keyframes can always read; the foil
     overrides it below. Declared here so the still mark carries the default
     and `.bdg-art--firstlight` adds motion and nothing else. */
  --sun-seat: 0 0 0 rgba(0,0,0,0);
  /* Brightest directly beneath the sun and gone by both ends, because a line of
     even weight is a rule drawn UNDER the sun rather than an edge the sun is
     coming over. */
  --sun-line: linear-gradient(to right,
    rgba(253,196,110,0)   0%,
    rgba(255,231,183,.92) 28%,
    #fff9ee               50%,
    rgba(255,231,183,.92) 72%,
    rgba(253,196,110,0)   100%);
  /* The box IS the drawing's bounds — rays to the top edge, the light on the
     ground to the bottom — so the face's `place-items: center` centres the
     whole composite and no margin nudge is needed. That is the candle's fix
     avoided rather than repeated: every offset below is measured from one
     number, the horizon at y=22. */
  width: 34px;
  height: 29px;
  background:
    /* The lit horizon, and it is a VARIABLE because a badge carrying the
       first-light sky does not want it: that sky draws its own horizon rim to
       rim, and a 34px line laid over the top of it is a shelf with two ends
       floating in mid-air. `--sun-line: none` leaves a valid empty layer and
       the rest of the drawing untouched, which is cheaper and far harder to get
       wrong than restating three gradients in an override. */
    var(--sun-line) 0 21.2px / 100% 1.4px no-repeat,
    /* The sun. A circle of radius 11 pinned to the bottom of an 11px-tall
       layer, which is what makes it a half disc without spending a box on it:
       the lower half of the circle simply falls outside its own layer.
       Smooth ramps rather than the hard stops the rocket's body uses. That is
       a cylinder lit from one side and needs an edge to read as curved; this
       is a light source, and hard stops on a light source draw
       concentric bands — which on a half disc is an archery target, or worse,
       the rainbow arch this sheet has already paid for once. */
    radial-gradient(circle 11px at 50% 100%,
      #fff9e6 0 16%,
      #fde68a 42%,
      #fbbf24 66%,
      #f97316 86%,
      #ea580c 96%,
      rgba(234,88,12,0) 100%) 6px 11px / 22px 11px no-repeat,
    /* The haze it is coming up through, stopped dead at the horizon: light in
       the air ABOVE the line, nothing below it. A bloom that leaked past the
       line would put the sun in front of the ground rather than behind it. */
    radial-gradient(circle 16px at 50% 100%,
      rgba(253,186,89,.26) 0 38%,
      rgba(251,146,60,0) 100%) 1px 6px / 32px 16px no-repeat;
  /* The warmth on everything around it. A filter and not a box-shadow, because
     the rays live on a pseudo and a box-shadow would light the BOX rather than
     the drawing. */
  filter: drop-shadow(0 0 5px rgba(251,146,60,.32));
}

/* The rays, hinged on the sun's own centre — one repeating gradient standing in
   for a fan of separate strokes. The Full Spectrum pinwheel is the counter-case
   and worth holding next to this one: eight things that are genuinely separate
   objects will not come out of a gradient, so that badge pays for eight
   elements. Light is not eight objects. It is one thing that is radial, and a
   gradient is the exact shape of that fact.
 *
 * SEVEN, 22deg apart, one straight up and the outermost pair 24deg clear of the
 * horizon. Nine were drawn first, at 20deg, and the outermost pair landed 10deg
 * off flat — which is not a ray, it is a strikethrough: two tan bars lying along
 * the horizon on either side of the sun, arguing with the one line that is
 * supposed to BE the horizon. Hence an explicit gradient rather than a
 * repeating one, because a repeat cannot be told where to stop and what it kept
 * adding was exactly the flat pair.
 *
 * THE MASK CUTS THE DISC OUT, and that is not a flourish. The obvious
 * construction is a negative z-index, so the sun's own background covers the
 * roots of its rays — and it does not work here: `.bdg__mark` carries
 * `z-index: 2`, so the mark is its own stacking context and a `z-index: -1`
 * pseudo paints above that background rather than behind it. Rays crossed the
 * disc in pale stripes. The mask is the better answer anyway: it starts each ray
 * at the rim with a hair of daylight between, and it does the second job in the
 * same declaration — rays that all stop dead at a common radius are a cog, and
 * the fade is the whole difference between a gear and light. */
.bdg__mark--sunrise::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  width: 44px;
  height: 22px;
  margin-left: -22px;
  /* 291.6deg puts the first ray's leading edge where the leftmost ray belongs;
     every ray is 4.8deg wide on a 22deg pitch, and the run ends at 136.8deg so
     the transparent tail carries all the way round rather than wrapping a ray
     back over the left of the fan. */
  background: conic-gradient(from 291.6deg at 50% 100%,
    var(--sun-ray) 0 4.8deg,        rgba(0,0,0,0) 4.8deg 22deg,
    var(--sun-ray) 22deg 26.8deg,   rgba(0,0,0,0) 26.8deg 44deg,
    var(--sun-ray) 44deg 48.8deg,   rgba(0,0,0,0) 48.8deg 66deg,
    var(--sun-ray) 66deg 70.8deg,   rgba(0,0,0,0) 70.8deg 88deg,
    var(--sun-ray) 88deg 92.8deg,   rgba(0,0,0,0) 92.8deg 110deg,
    var(--sun-ray) 110deg 114.8deg, rgba(0,0,0,0) 114.8deg 132deg,
    var(--sun-ray) 132deg 136.8deg, rgba(0,0,0,0) 136.8deg);
  -webkit-mask-image: radial-gradient(circle 22px at 50% 100%,
    rgba(0,0,0,0) 0 50%, #000 57%, rgba(0,0,0,.5) 78%, rgba(0,0,0,0) 100%);
  mask-image: radial-gradient(circle 22px at 50% 100%,
    rgba(0,0,0,0) 0 50%, #000 57%, rgba(0,0,0,.5) 78%, rgba(0,0,0,0) 100%);
}

/* What the light does when it lands. The roller shutter's one genuinely good
   idea, kept: light drawn as a rectangle is a yellow block, and light drawn
   with a shape that falls away from its source is light. An ellipse here rather
   than that badge's wedge — a door is a source in a wall and throws a beam; a
   sun is a source in the open and washes the ground it has just cleared. Tight
   and bright rather than wide and dim: warm mid-alpha spread thin over a navy
   face does not read as glow, it reads as a brown stain. */
.bdg__mark--sunrise::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 22px;
  width: 28px;
  height: 6px;
  margin-left: -14px;
  background: radial-gradient(14px 6px at 50% 0%,
    rgba(255,244,222,.5) 0 16%,
    rgba(255,214,150,.13) 52%,
    rgba(251,146,60,0) 100%);
}

/* Winning the spotlight — or landing on the reveal stage — TURNS THE LIGHT UP.
   It does not raise the sun, and the shutter's ruling is inherited whole
   because it was right both times: a badge whose picture depends on which tile
   the server happened to pick that week is two different badges, and a sun that
   has finished rising is no longer about the minute the gym opens. Brighter
   rays, a wider glow, same sun at the same height. */
.bdg--spotlight .bdg__mark--sunrise,
.bdg-reveal__badge .bdg__mark--sunrise {
  --sun-ray: rgba(255,236,190,.95);
  filter: drop-shadow(0 0 9px rgba(253,186,89,.6));
}

/* On the foil there is a full-saturation rainbow behind the drawing and the
   sun's own warmth is no longer the brightest thing in the tile. A contact
   shadow under it — the fix the shiny bat and the shiny deck both needed, for
   the same reason: an object sitting ON the foil rather than a stain in it. */
.bdg.bdg--shiny .bdg__mark--sunrise {
  --sun-seat: 0 1px 2px rgba(0,0,0,.75);
  filter:
    drop-shadow(0 0 5px rgba(251,146,60,.32))
    drop-shadow(0 1px 2px rgba(0,0,0,.75));
}
.bdg--spotlight.bdg--shiny .bdg__mark--sunrise,
.bdg-reveal__badge .bdg--shiny .bdg__mark--sunrise {
  --sun-ray: rgba(255,236,190,.95);
  filter:
    drop-shadow(0 0 9px rgba(253,186,89,.6))
    drop-shadow(0 1px 2px rgba(0,0,0,.75));
}

/* ---- daybreak: the copy that moves ------------------------------------------
 *
 * The same badge, and the same arrangement the night owl's `nightfall` uses
 * further down this sheet: ONE CLASS ON THE TILE, never a second mark. Every
 * drawing rule above is untouched, so there is one sun here and it is drawn
 * once — the moulded hold's rule applied to a light source — and in the
 * catalogue this would be one Anim value, the identical lever every still badge
 * already pulls by carrying Anim.NONE. Which of the two ships is a catalogue
 * decision and it has not been taken; that is why the specimen shows both.
 *
 * THE SUN DOES NOT RISE. The ruling is stated in full three sections up and is
 * not re-argued here: a sun you can watch move is a time-lapse, and a badge
 * that has finished rising has stopped being about the minute the gym opens.
 * What is animated is the only thing that visibly changes at 6am anyway — the
 * REACH of the light. The disc sits exactly where the still copy has it.
 *
 * REBUILT 29 AUGUST, and the first version is worth writing down because it was
 * wrong in an instructive way. It ran on 4.1s / 5.4s / 6.7s with deliberately
 * IRREGULAR keyframes — four turning points inside every cycle — on the theory
 * that evenly spaced stops read as a heartbeat and a heartbeat on a medallion
 * is a notification. That theory is correct and it was borrowed from the wrong
 * badge. The flicker keyframes are irregular because FIRE is irregular, and at
 * 1.9 seconds the eye reads the unevenness as combustion. Light at a horizon is
 * the opposite kind of thing: it is one of the slowest events a person ever
 * watches, and four direction changes in five seconds is not a slow event, it
 * is a twitch. The tile shook.
 *
 * So: ONE TURN PER CYCLE, and cycles four times longer. Out and back, nothing
 * else, on a curve that spends most of its time near the extremes rather than
 * crossing the middle. The anti-heartbeat job moves off the keyframes and onto
 * the PERIODS, which is where it always belonged and where it costs nothing.
 *
 * FIVE LAYERS ON FIVE PRIME PERIODS: 11s, 17s, 23s, 29s and 41s. Every pair is
 * co-prime, so the composite repeats once every 11 x 17 x 23 x 29 x 41 seconds
 * — a little under fifty-nine days. Nobody will ever see this loop come back
 * round to where they found it, which is the honest version of the claim the
 * first draft was making with three periods and half a minute.
 *
 * They are also spread across FOUR TIMESCALES on purpose, and that is the other
 * half of the answer to "it feels short". Three layers all breathing on five
 * seconds is one idea repeated three times; a glow on eleven, a fan on
 * seventeen, a wash on twenty-three, mist on twenty-nine and a whole sky on
 * forty-one is a drawing where something is always arriving and nothing ever
 * arrives twice together.
 * -------------------------------------------------------------------------- */

/* The glow swelling. An animated `filter` outranks every static `filter` in the
   cascade whatever its specificity, so the two the still copy relies on — the
   spotlight's wider glow and the foil's contact shadow — would simply stop
   applying the moment this class went on. Both are therefore hoisted into
   custom properties the keyframes read: --sun-glow for the reward, --sun-seat
   for the foil, defaulting to a no-op shadow so a plain tile pays nothing for
   the arrangement. */
.bdg-art--firstlight .bdg__mark--sunrise {
  --sun-glow: rgba(253,186,89,.5);
  animation: bdg-sunswell 11s var(--sun-ease) infinite;
}

/* The curve every layer below rides, and it is the other half of the fix. A
   plain ease-in-out spends its whole middle in transit, so the drawing is
   always visibly on its way somewhere; this one accelerates late and arrives
   early, which leaves the light sitting near the top or the bottom of its range
   most of the time and moving between them only briefly. Slow is not enough on
   its own — slow and CONSTANT still reads as a mechanism. */
.bdg-art--firstlight .bdg__mark--sunrise,
.bdg-art--firstlight .bdg__mark--sunrise::before,
.bdg-art--firstlight .bdg__mark--sunrise::after,
.bdg-art--firstlight .bdg__art,
.bdg-art--firstlight .bdg__face::before {
  --sun-ease: cubic-bezier(.62, 0, .38, 1);
}

@keyframes bdg-sunswell {
  0%, 100% { filter: drop-shadow(0 0 3px rgba(251,146,60,.18)) drop-shadow(var(--sun-seat)); }
  50%      { filter: drop-shadow(0 0 13px var(--sun-glow))     drop-shadow(var(--sun-seat)); }
}

/* The rays reaching. Origin on the pivot the fan is already hinged on, so they
   grow OUT of the sun; scaled from the middle of the box instead they would
   slide off their own rim and the mask would cut them in the wrong place.
   Twenty-four per cent end to end — nearly twice the twitchy first draft's
   swing, and it reads calmer rather than louder, because seventeen seconds of
   it works out at about a tenth of a pixel per frame. Amplitude was never what
   made the old one shake; direction changes were. */
.bdg-art--firstlight .bdg__mark--sunrise::before {
  transform-origin: 50% 100%;
  animation: bdg-sunreach 17s var(--sun-ease) infinite;
}

@keyframes bdg-sunreach {
  0%, 100% { transform: scale(.92); opacity: .66; }
  50%      { transform: scale(1.14); opacity: 1; }
}

/* The wash on the ground, widening and narrowing. scaleX only: light spreading
   sideways along a floor is what this is, and scaling it vertically would walk
   the near edge away from the horizon it is supposed to be leaving. The slowest
   of the three on the mark, because it is the biggest shape and the big shape
   moving fastest is what makes a drawing look nervous. */
.bdg-art--firstlight .bdg__mark--sunrise::after {
  transform-origin: 50% 0;
  animation: bdg-sunwash 23s var(--sun-ease) infinite;
}

@keyframes bdg-sunwash {
  0%, 100% { transform: scaleX(.9);  opacity: .72; }
  50%      { transform: scaleX(1.2); opacity: 1; }
}

/* The mist drifting. The fourth period, and the one that answers the complaint
 * that the loop felt short: the other three all live on the sun and read as one
 * thing breathing, so however long you make them the tile has a single idea in
 * it. Something in the FOREGROUND moving on its own clock is what gives the
 * drawing a second timescale, and it is free — the mist is already a full-bleed
 * box, and two and a half pixels of translate is a composited transform.
 *
 * Only sideways, and only two and a half pixels. Mist that rises is steam and
 * mist that thickens is fog; cold air sitting in a low place at dawn slides
 * along it, slowly, and then the sun takes it. Twenty-nine seconds for a five
 * pixel round trip is roughly the speed of a shadow moving on a wall — which is
 * to say you cannot catch it doing it, you can only notice it is somewhere else
 * than it was. */
.bdg-art--firstlight .bdg__art {
  animation: bdg-sunmist 29s var(--sun-ease) infinite;
}

@keyframes bdg-sunmist {
  0%, 100% { transform: translateX(-2.5px); opacity: .82; }
  50%      { transform: translateX(2.5px);  opacity: 1; }
}

/* The sky coming up, and this is the layer that makes the tile read as an
 * animation rather than as a detail that moves. The other four all live on the
 * sun and add up to one idea breathing; the sky is nine tenths of the medallion,
 * so a seven per cent swing on it is a larger change than everything else put
 * together and it is still the slowest thing here.
 *
 * It is also the only honest candidate left. The sun may not rise — that ruling
 * is three sections up and it has held through two rewrites — but a sunrise is
 * not really an object moving, it is a sky filling with light, and that is what
 * this animates. Saturation and brightness together rather than brightness
 * alone: dawn does not fade up like a lamp on a dimmer, the colour arrives with
 * the light.
 *
 * `filter` on a full-face pseudo is the expensive-looking choice and it is the
 * cheap one here — saturate and brightness are shader ops on an already
 * composited 56px layer, and it runs once every forty-one seconds. What it is
 * NOT free of is the cascade: an animated filter out-ranks every static one, so
 * the spotlight's warmer sky and the locked tile's greyscale would both have
 * stopped working the moment this class went on. The spotlight's two values are
 * hoisted into --sky-sat / --sky-bri and multiplied inside the keyframes; the
 * locked tile turns the animation off entirely, below, and gets its greyscale
 * back by having nothing to out-rank it. */
.bdg-art--firstlight .bdg__face::before {
  animation: bdg-skyrise 41s var(--sun-ease) infinite;
}

@keyframes bdg-skyrise {
  0%, 100% {
    filter: saturate(calc(var(--sky-sat, 1) * .92))
            brightness(calc(var(--sky-bri, 1) * .92));
  }
  50% {
    filter: saturate(calc(var(--sky-sat, 1) * 1.08))
            brightness(calc(var(--sky-bri, 1) * 1.07));
  }
}

/* Winning the spotlight is a GEAR CHANGE, not a switch — the kickflip's ruling,
   and the only one available here: this copy is already moving, so the reward
   cannot be that it starts. Same keyframes, same distances, about a third off
   every wait, plus the brighter rays and wider swell the still copy gets.
   7 / 11 / 15 / 19 rather than four thirds of the base set, because the gear
   change must not cost the co-primality the section header just made a promise
   about: those four share no factor either, so a spotlit tile repeats every six
   hours instead of every thirty-four. */
.bdg--spotlight.bdg-art--firstlight .bdg__mark--sunrise,
.bdg-reveal__badge .bdg-art--firstlight .bdg__mark--sunrise {
  --sun-glow: rgba(255,222,164,.72);
  animation-duration: 7s;
}
.bdg--spotlight.bdg-art--firstlight .bdg__mark--sunrise::before,
.bdg-reveal__badge .bdg-art--firstlight .bdg__mark--sunrise::before { animation-duration: 11s; }
.bdg--spotlight.bdg-art--firstlight .bdg__mark--sunrise::after,
.bdg-reveal__badge .bdg-art--firstlight .bdg__mark--sunrise::after  { animation-duration: 15s; }
.bdg--spotlight.bdg-art--firstlight .bdg__art,
.bdg-reveal__badge .bdg-art--firstlight .bdg__art { animation-duration: 19s; }
.bdg--spotlight.bdg-art--firstlight .bdg__face::before,
.bdg-reveal__badge .bdg-art--firstlight .bdg__face::before { animation-duration: 29s; }

/* A teaser does not move — the bat's rule, and here it is load-bearing rather
   than tasteful. `.bdg--locked .bdg__mark` greys the mark with a static filter
   and a running filter animation would out-rank it, so a badge nobody has
   earned would sit there glowing warm orange through its own greyscale. The
   mist stops with it, for the reason the sky is drained on a locked tile: one
   part of the drawing still behaving as though it had been earned is worse than
   the whole thing being obviously off. */
.bdg--locked .bdg__mark--sunrise,
.bdg--locked .bdg__mark--sunrise::before,
.bdg--locked .bdg__mark--sunrise::after,
.bdg--locked.bdg-art--firstlight .bdg__art,
.bdg--locked.bdg-art--firstlight .bdg__face::before { animation: none; }

@media (prefers-reduced-motion: reduce) {
  /* Held at the reach rather than stopped flat. Stopping it dead would be
     defensible — the still copy is right there — but then two tiles that exist
     to differ would render identically, and a comparison that collapses under
     an accessibility setting is a broken comparison. */
  .bdg-art--firstlight .bdg__mark--sunrise { animation: none; }
  .bdg-art--firstlight .bdg__mark--sunrise::before {
    animation: none;
    transform: scale(1.07);
    opacity: .96;
  }
  .bdg-art--firstlight .bdg__mark--sunrise::after {
    animation: none;
    transform: scaleX(1.1);
    opacity: .95;
  }
  .bdg-art--firstlight .bdg__art {
    animation: none;
    transform: none;
  }
  .bdg-art--firstlight .bdg__face::before { animation: none; }
}

/* PORTED 30 August 2026 from the badges Artifact, replacing the drawing that
 * was here. The Artifact is the design surface for this set and had moved
 * ahead of main; the badge, its name, its tier and its mechanic are unchanged
 * — only the picture inside the coin. The superseded drawing is DELETED rather
 * than parked behind a flag: it has no callers, and a second copy of a rejected
 * picture is a thing somebody re-enables by accident in a year.
 *
 * One change on the way down, the same one every drawing off that page gets:
 * the Artifact keeps a STILL copy and a MOVING copy because which of the two
 * ships had not been decided. It has — the moving one — so the modifier folds
 * into the art class and a Badge names its art and gets the whole drawing. */

/* ---- cakehold + confetti: a birthday on the wall ----------------------------
 *
 * DRAWN TWICE ON 29 August 2026, and both passes are recorded here because the
 * second one overturns the first.
 *
 * PASS ONE replaced the candle-in-a-hold that shipped on the 25th. That drawing
 * was not wrong, it was QUIET: a 9px wax cylinder and a 7px flame standing in
 * the middle of a flat dark disc — a tile that fires on exactly one day of the
 * year, carrying the word HAPPY underneath it, and somehow the second-stillest
 * drawing on the page. Pass one lit the face, threw confetti across it and
 * wound a spiral round the wax, and every one of those three survives below.
 *
 * PASS TWO made it a CAKE, and that is the part that overturns something. Both
 * earlier drawings were built on the sheet's oldest rule — names may be
 * obvious, drawings must be specific — and both said out loud that a cake is a
 * birthday badge for any app in the world where a candle screwed to a bit of
 * moulded plastic is a birthday badge for a bouldering gym. THE OBJECTION HAS
 * NOT BEEN ANSWERED. It has been overruled, which is a different thing, and it
 * is left standing in the cake block below rather than quietly deleted so that
 * whoever re-opens this argument re-opens it with the objection attached. What
 * it buys is the read: one candle needs its caption to say why it is on fire,
 * and three candles on a cake is a birthday to everybody who has ever seen one.
 *
 * The spiral went with it — a 2.6px candle cannot carry three turns of anything
 * — and what replaced it is three candles in three colours. The rest of the
 * badge is untouched, which is most of it:
 *
 *   THE STRIPE — and this is the one paragraph here that no longer describes
 *   the drawing. It is kept rather than cut because the reasoning outlived the
 *   spiral: see the candle block below, where the same problem is solved with
 *   colour instead. A smooth wax cylinder with a flame on top is a dinner
 *   candle, and nobody in the world has ever put one on a cake. The spiral is the whole
 *   difference between the two objects and it costs one repeating gradient. It
 *   is the tier's teal rather than the traditional red, and that is not house
 *   politics: the flame directly above it is orange, and a candle whose stripes
 *   are the same hue as its own flame loses both of them at 56px.
 *
 *   THE LIGHT. Lit candles in a dark room light the room. Drawing them so they
 *   did not was the actual bug — the face stayed the same flat field it is on
 *   every other Grit badge, so the flames were coloured specks rather than a
 *   source. It now throws a warm pool across the whole face and the rim falls
 *   away behind it, which is the single change that did most of the work. The
 *   tier's own teal wash at the top is buried underneath it and that is
 *   accepted: the rim and the printed word carry Grit, and a teal cast over a
 *   birthday is the tier being louder than the badge.
 *
 *   THE CONFETTI. Borrowed rather than invented — perks.css already throws
 *   confetti at this member on this day for the free birthday drink, and the
 *   badge and the perk are the same event, so they are the same colours. The
 *   violet is that sheet's fourth confetti colour, named in the header of this
 *   file; the rest are the tier accents and the shiny's pink, so nothing here
 *   is a hue the app did not already own.
 *
 * ELEVEN FLECKS IN THREE PLANES, and every one of them is AUTHORED. Nothing
 * about a badge is stored, so a scatter drawn from a random number would be a
 * different scatter on every page load — the gem's ruling, and it applies to
 * anything on a tile that looks like chance. The planes are tilted -13, +8 and
 * -26 degrees and the far one is smaller and dimmer, because confetti that all
 * hangs at one angle and one size is a texture rather than a scatter.
 *
 * Each plane is FOUR TILES TALL with the same eleven flecks repeating down it,
 * so the moving copy can fall exactly one tile and land back where it started.
 * That is the whole reason for the arithmetic below: height 400%, tile 25%,
 * travel 25%. Change one of the three and the loop shows a seam.
 * -------------------------------------------------------------------------- */

/* The light the candles throw.
 *
 * PINNED IN PX FROM THE FACE'S CENTRE, never in percent, and this sheet has
 * already been caught by it twice — see the sundown and noon blocks. The medal
 * is 50px on a phone, 56, 68, 78 in the grid and 104 in the header band; the
 * MARK is a fixed 22x14 at every one of them. A pool placed at "30% down" is
 * on the flames at 56px and two and a half pixels above them at 78px, which
 * reads as candles lighting the air over their own heads.
 *
 * The cake box is 14 tall with a 7px top margin and the face centres the margin
 * box, so the icing starts 3.5px above centre and the wicks sit 11px above it.
 * That number is the only one in this block, and it survived the cake: the
 * candles are taller than the one they replaced and the cake is deeper than the
 * hold was, and the two changes cancel to within half a pixel. */
.bdg-art--confetti .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 0;
  background:
    /* the pool. Wide and soft — a naked flame is a point source and the light
       it throws has no edge anywhere. */
    radial-gradient(34px 30px at 50% calc(50% - 11px),
      rgba(255,226,166,.50) 0 4%,
      rgba(253,186,116,.30) 32%,
      rgba(249,115,22,.12)  60%,
      rgba(249,115,22,0)   100%),
    /* the room. Without this the pool has nothing to be brighter THAN, which
       was the whole failure of the first drawing. The extent stays in percent
       — this one is the FACE falling away, not a feature of the drawing, so it
       is the one thing here that should grow with the medal. */
    radial-gradient(112% 98% at 50% calc(50% - 9px),
      rgba(4,7,14,0) 34%, rgba(4,7,14,.30) 70%, rgba(4,7,14,.56) 100%);
  /* Two custom properties rather than a bare filter, the daybreak copy's
     ruling: the moving copy animates this exact property and a static filter
     here would be thrown away the moment it does. */
  --lit-bri: 1;
  --lit-sat: 1;
  filter: brightness(var(--lit-bri)) saturate(var(--lit-sat));
  /* `.bdg__face--engraved` is an inset shadow on the face and an inset shadow
     paints beneath the face's children, so any tile whose ::before covers the
     face wall to wall has to carry its own lit edge. The sundown and firstlight
     skies both pay this; it is the same line. */
  box-shadow:
    inset 0  1px 0 rgba(255,255,255,.13),
    inset 0 -1px 0 rgba(0,0,0,.5);
}

/* The tier's own teal wash across the top of the face is knocked back to a
   third of its weight on this one badge, and only this one. Everywhere else it
   is free — it lands on a dark field and reads as Grit. Here it lands in the
   candles' warm pool, and teal crossing amber at low alpha is olive: a green
   haze in the top corner of a birthday. The tier still has three of its four
   channels — the rim, the printed word and the circular silhouette — and the
   header of this sheet is explicit that no single channel is load-bearing.
   Three classes deep on purpose: `.bdg--grit .bdg__face` is two, and a
   two-class rule here would be a coin toss decided by source order — the same
   specificity trap that has already eaten the grade medallions once. */
.bdg.bdg-art--confetti .bdg__face {
  background:
    radial-gradient(72% 48% at 50% 0%, rgba(45,212,191,.09), transparent 74%),
    var(--clr-badge-face);
}

/* The three confetti planes. One element and its two pseudos — the template
   gives the artwork exactly that, and eleven flecks out of three boxes is only
   possible because each box carries its flecks as BACKGROUND LAYERS rather
   than as elements. The Full Spectrum pinwheel is where that same trade was
   taken and then RETURNED: eight vanes each needed a silhouette, a lit side and
   a place in the overlap order, none of which a background layer can hold, so
   it bought eight spans. Flecks need a position and a colour. That is all a
   background layer is, so these stay free.

   The pseudos inherit the parent's rotation, so their own tilts are DELTAS:
   -13 on the plane, +21 on ::before lands at +8, -13 on ::after lands at -26.
   Get that wrong and two planes sit parallel. */
.bdg-art--confetti .bdg__art {
  --cf-tilt: -13deg;
  left: -35%;
  top: -150%;
  width: 170%;
  height: 400%;
  transform: rotate(var(--cf-tilt));
  background-repeat: repeat-y;
  background-size: 100% 25%;
  background-image:
    radial-gradient(3.6px 1.05px at 30% 13%, #fbbf24 80%, rgba(251,191,36,0)  100%),
    radial-gradient(1.25px 2.9px at 68% 34%, #f472b6 80%, rgba(244,114,182,0) 100%),
    radial-gradient(2.6px 1.15px at 41% 63%, #2dd4bf 80%, rgba(45,212,191,0)  100%),
    radial-gradient(1.6px 1.6px  at 72% 82%, #fff1cf 80%, rgba(255,241,207,0) 100%);
}

.bdg-art--confetti .bdg__art::before,
.bdg-art--confetti .bdg__art::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: rotate(var(--cf-tilt));
  background-repeat: repeat-y;
  background-size: 100% 25%;
}

.bdg-art--confetti .bdg__art::before {
  --cf-tilt: 21deg;
  background-image:
    radial-gradient(2.6px 1.15px at 35% 40%, #a78bfa 80%, rgba(167,139,250,0) 100%),
    radial-gradient(1.05px 3.4px at 63%  9%, #f97316 80%, rgba(249,115,22,0)  100%),
    radial-gradient(2.3px 1.05px at 27% 87%, #f472b6 80%, rgba(244,114,182,0) 100%),
    radial-gradient(1.4px 1.4px  at 57% 66%, #fcd34d 80%, rgba(252,211,77,0)  100%);
}

/* The far plane. Smaller, dimmer and at the steepest tilt — depth, so the
   scatter has a front and a back instead of being one sheet of dots. */
.bdg-art--confetti .bdg__art::after {
  --cf-tilt: -13deg;
  background-image:
    radial-gradient(2px .95px  at 66% 24%, rgba(45,212,191,.72)  80%, rgba(45,212,191,0)  100%),
    radial-gradient(.95px 2px  at 38% 54%, rgba(167,139,250,.72) 80%, rgba(167,139,250,0) 100%),
    radial-gradient(1.7px .85px at 56% 90%, rgba(251,191,36,.68) 80%, rgba(251,191,36,0)  100%);
}

/* ---- the cake ---------------------------------------------------------------
 *
 * A CAKE, from 29 August 2026, and it overturns a decision this page has
 * recorded twice and argued for in two separate blocks. The objection has not
 * been answered — it has been overruled, which is a different thing and is
 * written down as such rather than quietly deleted.
 *
 * The objection stands and is worth reading before anybody re-opens it: a cake
 * is a birthday badge for ANY app in the world, and a candle screwed to a bit
 * of moulded plastic is a birthday badge for a bouldering gym. That is the
 * sheet's oldest rule — names may be obvious, drawings must be specific — and
 * the night owl block cites this badge as its worked example. That citation has
 * been rewritten to cite the trade instead, because the drawing no longer
 * honours the claim.
 *
 * WHAT IS BOUGHT is the read, and it is not a small thing. One candle is a
 * candle: it says fire, it says wax, and it needs the sentence underneath it to
 * say WHY. Three candles on a cake is a birthday to everybody who has ever seen
 * one, at 56px, with no caption, in a strip a member scrolls past. "Doors open"
 * already made exactly this trade one entry down and said so out loud — the
 * sentence carries the accuracy, the mark is allowed to be the metaphor.
 *
 * WHAT IS KEPT is the hold. The cake is standing on the same moulded plastic
 * the bat hangs off and the candle stood in, and it is now the only thing in
 * the drawing that belongs to this gym rather than to birthdays in general.
 * That is a thinner claim than the one it replaces and it is not pretending
 * otherwise — but a cake on a climbing hold is still two objects with no
 * business being near each other, which was the original joke, and the joke is
 * the half of the old drawing worth carrying forward.
 *
 * THE CAKE IS THE MARK'S OWN BOX; the candles are the ::before above it and the
 * hold the ::after below. That allocation is forced rather than chosen — the
 * template gives a mark an element and two pseudos and nothing else — and it is
 * the reason the three flames flicker in unison further down: three background
 * layers on one box cannot be given three phases. At 56px three flames licking
 * together is a cake with candles on it; three flames licking out of phase
 * would cost three elements this mark does not have.
 * -------------------------------------------------------------------------- */

.bdg__mark--cakehold {
  width: 22px;
  height: 14px;
  /* The candles sit above this box and the hold below it, so the box has to sit
     HIGH of centre for the assembly to be centred. The face centres the box
     plus its margin, so 7px here moves the cake down 3.5 and the composite up
     the same — which is what the candles, cake and hold measure out to. */
  margin-top: 7px;
  border-radius: 2px 2px 3px 3px;
  background:
    /* THE FILLING, and it is doing more than decoration. A brown rectangle
       under a white one is a brown rectangle; one pale line across it is a
       cake in section, which is how a cake is drawn in every picture book
       there has ever been. Low in the sponge rather than centred, so it never
       collides with the drips above it. */
    linear-gradient(to bottom,
      rgba(0,0,0,0) 0 10.8px, #eed6a9 10.8px 11.8px, rgba(0,0,0,0) 11.8px),
    /* THE FROSTING, painted over the drips so the two are one object. Three
       stops rather than a ramp: the top face is in full candlelight, the front
       face is turned away from it, and the line between them is where the cake
       stops being flat. */
    linear-gradient(to bottom,
      #fffaee 0 1.5px, #fbeed6 1.5px 3.6px, #f1ddb8 3.6px 4.8px, rgba(0,0,0,0) 4.8px),
    /* THE DRIPS — five, at five different depths, and the unevenness is the
       whole point. Evenly spaced circles of one size are a scalloped doily;
       icing runs where it runs. They hang off the frosting's lower edge and
       the frosting is painted on top of them, so there is no seam. */
    radial-gradient(2.2px 2.0px at 2.3px 4.6px,  #f7e7c8 0 84%, rgba(247,231,200,0) 100%),
    radial-gradient(2.5px 2.4px at 6.9px 5.2px,  #f7e7c8 0 84%, rgba(247,231,200,0) 100%),
    radial-gradient(2.1px 1.9px at 11.2px 4.6px, #f7e7c8 0 84%, rgba(247,231,200,0) 100%),
    radial-gradient(2.6px 2.6px at 15.5px 5.5px, #f7e7c8 0 84%, rgba(247,231,200,0) 100%),
    radial-gradient(2.2px 2.1px at 19.8px 4.7px, #f7e7c8 0 84%, rgba(247,231,200,0) 100%),
    /* THE SPONGE. Warm brown, and it is a knowing exception to the house
       palette — the same licence the rocket's contrail takes for its rainbow.
       There is no brown anywhere else in this app, and there is no other colour
       a cake can be: a sponge in teal-400 is a prop. Lit from ABOVE rather than
       from the side, because what is lighting it is standing on top of it. */
    linear-gradient(to bottom,
      #9c6440 0%, #7f5030 44%, #633c22 76%, #4d2e19 100%);
  box-shadow:
    inset 0 -2px 3px -2px rgba(0,0,0,.42),
    0 2px 3px -1px rgba(0,0,0,.5);
}

/* THE CANDLES AND THEIR FLAMES, six background layers on one box.
 *
 * THREE, and the count was argued rather than picked. One is the drawing this
 * replaces. Two is a pair of anything. Three is the number a child draws, it is
 * odd so it has a middle, and it fits across 20px at a width where a candle is
 * still a candle rather than a hair.
 *
 * THREE COLOURS, because the spiral died with the single candle and something
 * had to replace it. A 2.6px candle cannot carry a stripe — three turns of
 * anything across two and a half pixels is a smudge — but it can carry one
 * colour, and teal / cream / pink is unmistakably a party rather than a dinner
 * table. The teal is the tier's own and the pink is the shiny's, so the three
 * of them are already in the confetti falling past.
 *
 * The flames are ellipses with the core placed LOW, not the streak's rotated
 * square: at 3.4px a rotated square with a border-radius on three corners is
 * four pixels of aliasing. An ellipse whose brightest point is at 64% fades out
 * fastest at the top, which is the taper doing the work the geometry did on the
 * bigger flame. Both drop-shadows are on the BOX, so the three flames share one
 * halo — which is what three flames two millimetres apart actually do. */
.bdg__mark--cakehold::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: calc(100% - 1px);
  width: 20px;
  height: 12px;
  margin-left: -10px;
  /* Scaled about the box's own BOTTOM, which is the cake's top face. The wax
     stretches by half a pixel and the flames rise by one — anchored at the
     middle instead, the candles would sink into the icing on every cycle. */
  transform-origin: 50% 100%;
  background:
    radial-gradient(1.7px 2.9px at 50% 64%,
      #fffdf4 0 12%, #fef3c7 26%, #fde68a 40%, #fbbf24 56%, #f97316 76%,
      rgba(234,88,12,0) 100%)  2.2px .2px / 3.6px 6.2px no-repeat,
    radial-gradient(1.7px 3.1px at 50% 64%,
      #fffdf4 0 12%, #fef3c7 26%, #fde68a 40%, #fbbf24 56%, #f97316 76%,
      rgba(234,88,12,0) 100%)  8.2px 0    / 3.6px 6.5px no-repeat,
    radial-gradient(1.7px 2.8px at 50% 64%,
      #fffdf4 0 12%, #fef3c7 26%, #fde68a 40%, #fbbf24 56%, #f97316 76%,
      rgba(234,88,12,0) 100%) 14.2px .5px / 3.6px 6.0px no-repeat,
    linear-gradient(102deg, #d9fbf4 0 30%, #6ee7d7 30% 64%, #2dd4bf 64%)
       2.7px 5.3px / 2.6px 6.7px no-repeat,
    linear-gradient(102deg, #fffaf0 0 30%, #f5e9d2 30% 64%, #dcc8a4 64%)
       8.7px 5.3px / 2.6px 6.7px no-repeat,
    linear-gradient(102deg, #fde4f0 0 30%, #f9a8d4 30% 64%, #f472b6 64%)
      14.7px 5.3px / 2.6px 6.7px no-repeat;
  filter:
    drop-shadow(0 0 3.5px rgba(249,115,22,.75))
    drop-shadow(0 0 10px rgba(251,191,36,.44));
  /* THE FLAMES MOVE ON BOTH COPIES, and this is not a new exception — the sheet
     ruled on flames once already, for the streak: a flame that does not flicker
     is a leaf. That ruling is about fire, not about which badge the fire is on.
     They lick smaller and slower than the streak does, because a birthday is a
     quieter thing than a run you are about to lose. */
  animation: bdg-cakelick 2.1s ease-in-out infinite alternate;
  z-index: 1;
}

@keyframes bdg-cakelick {
  to {
    transform: scaleY(1.08);
    filter:
      drop-shadow(0 0 5px rgba(249,115,22,.88))
      drop-shadow(0 0 13px rgba(251,191,36,.55));
  }
}

/* The hold it is standing on. Same plastic as the bat's, same lopsided radius,
   same three hard stops — there is one moulded hold in this sheet and it is
   drawn once, so a member who holds both badges sees the same object twice
   rather than two different guesses at one. Wider than the hold the candle
   stood in, because a cake needs a ledge rather than a socket.

   BEHIND the cake rather than in front of it, which is the one thing that
   changed with the drawing. The candle was jammed INTO the hold and the hold
   was painted over its base; a cake is set down ON something, so the parts that
   show are the ends sticking out either side. Its top highlight is warm for the
   reason it always was: the light in this drawing comes from the light. */
.bdg__mark--cakehold::after {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(100% - 2.5px);
  width: 26px;
  height: 6px;
  margin-left: -13px;
  border-radius: 30% 70% 38% 62% / 58% 54% 46% 42%;
  background: linear-gradient(158deg, #7a8397 0 34%, #545c6e 34% 68%, #383f4e 68%);
  box-shadow:
    0 1px 2px rgba(0,0,0,.55),
    inset 0 .75px 0 rgba(255,220,168,.5);
  /* Safe because the mark carries z-index: 2 and a positioned element with a
     z-index is a stacking context, so -1 goes behind the cake and no further. */
  z-index: -1;
}

/* ---- manyhappy: the copy that moves -----------------------------------------
 *
 * The same badge with one class on it — one Anim value in the catalogue, the
 * identical lever every still badge here pulls by carrying Anim.NONE.
 *
 * THIS IS THE ONE BADGE WHERE THE RESTRAINT ARGUMENT DOES NOT APPLY, and it is
 * worth saying plainly because the argument is a good one everywhere else. The
 * reason most of the catalogue is deliberately still is the WALL: twenty tiles
 * in a grid, and a fifth living thing among them is a fifth thing shouting.
 * This tile cannot be one of twenty moving, because it is on a member's page
 * for one day in three hundred and sixty-five and it is the only badge in the
 * set whose entire subject is that today is different from yesterday. A still
 * birthday badge is a card nobody opened.
 *
 * Three planes on three periods — 13, 19 and 29 seconds — so the scatter comes
 * back round to where you found it about once every two hours. Slow on purpose:
 * a plane crosses the face in thirteen seconds, roughly four pixels a second,
 * which is confetti hanging in the air rather than falling through it. Confetti
 * at real speed on a 56px medallion is a snowstorm in a bottle cap.
 *
 * The candlelight breathes on its own period and deliberately NOT on the
 * flames'. A real pool of light pulses with its flames, and at two seconds a
 * whole face pulsing twice a second is a notification asking to be tapped. The
 * flames keep the fast movement; the light they throw is slow.
 * -------------------------------------------------------------------------- */

.bdg-art--confetti .bdg__art         { animation: bdg-conf-fall 13s linear infinite; }
.bdg-art--confetti .bdg__art::before { animation: bdg-conf-fall 19s linear infinite; }
.bdg-art--confetti .bdg__art::after  { animation: bdg-conf-fall 29s linear infinite; }
.bdg-art--confetti .bdg__face::before {
  animation: bdg-candleswell 6.5s ease-in-out infinite alternate;
}

/* Exactly one tile, along the plane's OWN axis — which is why the rotation is
   restated inside the keyframe instead of being left on the element. Written
   the other way round the plane would fall down the SCREEN, the tiles would no
   longer line up with the travel, and the loop would show a seam every cycle. */
@keyframes bdg-conf-fall {
  from { transform: rotate(var(--cf-tilt)); }
  to   { transform: rotate(var(--cf-tilt)) translateY(25%); }
}

/* Reads the same two custom properties the spotlight sets, so a spotlit moving
   copy breathes AROUND its brighter resting point instead of throwing the
   spotlight away. */
@keyframes bdg-candleswell {
  from { filter: brightness(calc(var(--lit-bri) * .93)) saturate(var(--lit-sat)); }
  to   { filter: brightness(calc(var(--lit-bri) * 1.1)) saturate(var(--lit-sat)); }
}

/* Winning the spotlight turns the candles UP, never on — the house rule, and the
   fifth badge in a row to inherit it unchanged. The light gets warmer, the
   flame burns harder, and the confetti falls about a third quicker. Nothing
   starts that was not already going. */
.bdg--spotlight.bdg-art--confetti .bdg__face::before,
.bdg-reveal__badge .bdg-art--confetti .bdg__face::before { --lit-bri: 1.2; --lit-sat: 1.12; }
.bdg--spotlight .bdg__mark--cakehold::before,
.bdg-reveal__badge .bdg__mark--cakehold::before { animation-duration: 1.4s; }
.bdg--spotlight.bdg-art--confetti .bdg__art,
.bdg-reveal__badge .bdg-art--confetti .bdg__art         { animation-duration: 9s; }
.bdg--spotlight.bdg-art--confetti .bdg__art::before,
.bdg-reveal__badge .bdg-art--confetti .bdg__art::before { animation-duration: 13s; }
.bdg--spotlight.bdg-art--confetti .bdg__art::after,
.bdg-reveal__badge .bdg-art--confetti .bdg__art::after  { animation-duration: 21s; }

/* Locked. A locked MOMENT is never rendered, so in practice this never fires —
   it is here because the birthday settles into a COUNT after the day, and a
   counted badge can be drawn locked. The light is the reward, so the light is
   what a locked tile does not get: exactly the firstlight ruling and exactly
   its numbers, because two badges that flood their face the same way should
   not grey differently. */
.bdg--locked.bdg-art--confetti .bdg__face::before { filter: grayscale(1); opacity: .16; }
.bdg--locked.bdg-art--confetti .bdg__art::before,
.bdg--locked.bdg-art--confetti .bdg__art::after {
  animation: none;
  filter: grayscale(1);
  opacity: .3;
}

@media (prefers-reduced-motion: reduce) {
  /* Held part-way down rather than stopped at zero, the daybreak ruling: two
     tiles that exist to differ must not render identically the moment somebody
     turns motion off, or the comparison collapses under an accessibility
     setting. And the confetti is still THERE — which is perks.css's own rule
     about this exact day: a member who asked their phone for less movement
     should still be able to tell it is their birthday. */
  .bdg-art--confetti .bdg__art         { transform: rotate(-13deg) translateY(11%); }
  .bdg-art--confetti .bdg__art::before { transform: rotate(21deg)  translateY(17%); }
  .bdg-art--confetti .bdg__art::after  { transform: rotate(-13deg) translateY(6%); }
  /* Still lit, just not breathing. */
  .bdg-art--confetti .bdg__face::before {
    filter: brightness(calc(var(--lit-bri) * 1.04)) saturate(var(--lit-sat));
  }
}

/* PORTED 30 August 2026 from the badges Artifact, replacing the drawing that
 * was here. The Artifact is the design surface for this set and had moved
 * ahead of main; the badge, its name, its tier and its mechanic are unchanged
 * — only the picture inside the coin. The superseded drawing is DELETED rather
 * than parked behind a flag: it has no callers, and a second copy of a rejected
 * picture is a thing somebody re-enables by accident in a year.
 *
 * One change on the way down, the same one every drawing off that page gets:
 * the Artifact keeps a STILL copy and a MOVING copy because which of the two
 * ships had not been decided. It has — the moving one — so the modifier folds
 * into the art class and a Badge names its art and gets the whole drawing. */

/* ---- pinwheel: the Full Spectrum --------------------------------------------
 *
 * REDRAWN 29 August 2026, replacing the eight-tag FAN that shipped here on the
 * 25th. The fan was a half-disc of eight conic wedges hinged on a rivet, and
 * the reason it went is the reason it was hard to argue with at the time: it
 * was CORRECT. Eight bands, in Route.GRADE_BANDS order, easiest on the left,
 * hairlined apart so the black one did not dissolve. Every sentence written to
 * defend it was true, and the drawing was still a colour picker with a stud in
 * it. A half-disc of saturated wedges has exactly one thing it can look like.
 *
 * The rarest badge in the set was the flattest object on the wall, and the
 * lesson is worth keeping: the drawing that survives an argument is not the
 * same thing as the drawing that survives being LOOKED at.
 *
 * So the fan is now a PINWHEEL. Eight vanes on one pin, in the same eight
 * Route.GRADE_BANDS colours in the same band order, running clockwise from
 * yellow so the ladder still reads easiest-to-hardest — the sequence the fan
 * spent itself defending is intact, it has simply been wrapped round the pin
 * instead of laid out flat. What it buys is the thing the fan could never have:
 * a pinwheel is an object that is ABOUT to move even when it is standing still,
 * which is the only honest picture of a badge for doing all eight in one go.
 *
 * EIGHT ELEMENTS, and that is the sheet's second exception after the stone's
 * sapphire inclusions. The fan's whole cleverness was drawing eight things out
 * of one gradient, and that cleverness is exactly what capped it: a gradient
 * can hand a wedge a colour and nothing else. A vane needs a silhouette that
 * tapers to the pin, a lit shoulder, a shaded flank, a hairline, and a place in
 * the overlap order, and none of those five survive being a colour stop. Eight
 * spans cost eight spans. It is a boring trade and it is obviously the right
 * one, and it turned out to pay a bill nobody had sent yet: under
 * `forced-colors: active` a background-image is dropped, so the fan rendered as
 * an EMPTY TILE on Windows high contrast — checked, not assumed. A shape made
 * of elements survives that mode as a silhouette. The badge for climbing all
 * eight colours was invisible to the setting most likely to be switched on by
 * somebody who cannot see all eight colours, which is not irony anybody wants
 * shipped.
 *
 * SIZED OFF --badge-size, alone in this sheet, and it is not tidiness. Every
 * other mark is a small drawing sitting in the middle of a large face and can
 * be pinned in fixed pixels; this one FILLS its face. At 56px the wheel is
 * 44.5px across inside a hexagon that is 48.4px wide at the waist — 2px of air
 * — and the mobile breakpoint takes the face to 50px without touching a single
 * other mark. A hard-coded 44.5px wheel would be a wheel with its vane tips
 * sliced off by the hexagon on every phone in the gym, and it would look like a
 * rendering bug rather than a decision.
 *
 * The colours are hard-coded and that is the fan's debt inherited rather than
 * repaid: everywhere else a grade hex comes from gym/templatetags/grades.py,
 * which owns the map. Eight :nth-child rules are at least a map you can read,
 * which the conic gradient's twenty-four comma-separated stops were not. If a
 * ninth band is ever added to Route.GRADES the 45deg pitch has to be rewritten
 * by hand. There is a test that will tell you.
 * -------------------------------------------------------------------------- */

.bdg__mark--pinwheel {
  /* One place that says how big the wheel is. The vane is the unit — the wheel
     has no radius of its own, it is however far a vane reaches from the pin. */
  --pw-vane-w: calc(var(--badge-size) * .175);
  --pw-vane-h: calc(var(--badge-size) * .355);
  --pw-hub:    calc(var(--badge-size) * .165);
  /* Both halves of the filter hoisted, which is the daybreak copy's hard-won
     rule: a filter set by an animation outranks every static filter in the
     cascade whatever its specificity, so a spotlight glow written as a plain
     filter would stop applying the day this mark is ever animated on that
     property. The halo defaults to a no-op shadow, so an unspotlit tile pays
     nothing for the arrangement. */
  --pw-seat: drop-shadow(0 1.5px 2.5px rgba(0,0,0,.62));
  --pw-halo: drop-shadow(0 0 0 rgba(0,0,0,0));

  width:  calc(var(--badge-size) * .8);
  height: calc(var(--badge-size) * .8);
  /* ONE shadow under the whole wheel, not eight. drop-shadow on the parent
     traces the union of what it contains; a box-shadow per vane would draw
     eight shadows including the ones that fall on other vanes, which is a
     drawing of eight loose petals rather than of one object. */
  filter: var(--pw-seat) var(--pw-halo);
}

/* Everything that turns. The mark itself never does, and that split is the
   whole reason this element exists: a pinwheel spins ABOUT its pin, so a pin
   that spins with it is not a pin, it is a sticker. Keeping the wheel separate
   means the hub below can hold still and catch the light from one fixed
   direction while the vanes go past underneath — which is what tells the eye
   the thing is mounted on something rather than tumbling. */
.bdg__wheel {
  position: absolute;
  inset: 0;
  transform-origin: 50% 50%;
}

/* A vane. Anchored at its own POINT — transform-origin: 0 0 with the point in
   the top-left corner — so all eight converge on the pin exactly and the 45deg
   pitch is the only number that positions them.
 *
 * `border-radius: 0 100% 100% 100%` is the whole silhouette: one square corner
 * and three fully round ones is a teardrop, and a teardrop hung point-inward is
 * a vane. Rounded petals were drawn first, on the reasoning that a softer shape
 * would read as paper — and eight rounded petals about a hub is a FLOWER, which
 * is a worse answer than the colour picker was. The point is not decoration; it
 * is the only part of the shape that says which end is attached.
 *
 * -126deg is the sweep. At -90 the vanes sit dead radial and the wheel is a
 * daisy: symmetrical, and therefore with no direction to spin in. Leaning every
 * vane 36deg off its own radius is what makes the wheel handed, and a handed
 * wheel is one the eye reads as already turning. */
.bdg__vane {
  position: absolute;
  left: 50%;
  top: 50%;
  width:  var(--pw-vane-w);
  height: var(--pw-vane-h);
  transform-origin: 0 0;
  transform: rotate(calc(var(--pw-i) * 45deg - 126deg));
  border-radius: 0 100% 100% 100%;
  /* Band 1 on top, band 8 underneath. The overlap has to break SOMEWHERE — a
     ring of eight overlapping things cannot be consistently stacked — and this
     puts the break under White, which is the one vane bright enough to read as
     in front whatever the paint order says. Stacked the other way the white
     vane sat on top of the black one and the pair read as a rendering fault. */
  z-index: calc(8 - var(--pw-i));
  /* Lit shoulder, shaded flank. Two flat washes and not a radial: a radial
     gradient models a curved surface, and a vane is a piece of bent card
     catching light on one face — the same rule the peel tag's draft settled. */
  background-image:
    linear-gradient(200deg, rgba(255,255,255,.26) 0%, rgba(255,255,255,0) 45%),
    linear-gradient(15deg,  rgba(0,0,0,.30)      0%, rgba(0,0,0,0)      50%);
  /* The inner hairline is --badge-band-rim's argument again and it is still
     load-bearing rather than styling: band 7 "Black" is #18181b on a #0b1220
     face and without an outline it is not a dark vane, it is a hole. Applied to
     ALL EIGHT and not to the black one alone, so it cannot be forgotten the day
     a ninth band arrives. The outer keyline is the other half: eight vanes that
     overlap need a seam the eye can find, or the wheel reads as one blot. */
  box-shadow:
    inset 0 0 0 .75px rgba(255,255,255,.46),
    0 0 0 .6px rgba(5,9,16,.9);
}

/* The band map. Route.GRADE_BANDS order, clockwise from yellow. */
.bdg__vane:nth-child(1) { --pw-i: 0; background-color: #eab308; }  /* 1 Yellow */
.bdg__vane:nth-child(2) { --pw-i: 1; background-color: #3b82f6; }  /* 2 Blue   */
.bdg__vane:nth-child(3) { --pw-i: 2; background-color: #a855f7; }  /* 3 Purple */
.bdg__vane:nth-child(4) { --pw-i: 3; background-color: #22c55e; }  /* 4 Green  */
.bdg__vane:nth-child(5) { --pw-i: 4; background-color: #f97316; }  /* 5 Orange */
.bdg__vane:nth-child(6) { --pw-i: 5; background-color: #ef4444; }  /* 6 Red    */
.bdg__vane:nth-child(7) { --pw-i: 6; background-color: #18181b; }  /* 7 Black  */
.bdg__vane:nth-child(8) { --pw-i: 7; background-color: #e5e7eb; }  /* 8 White  */

/* The pin. Outside .bdg__wheel, so it is the one part of the drawing that is
   still when everything else is moving.
 *
 * A DISC OF LIGHT over the whole wheel was drawn here first — a fixed highlight
 * for the vanes to turn under, which is how a real spinning object catches a
 * lamp — and it was cut. On a hexagonal face 48px wide a 45px circle of white
 * at 20% has a visible edge, so it did not read as light on the wheel; it read
 * as a smudge on the FACE. Masking it to a silhouette that turns is not
 * something this template can do, so the light lives on the vanes instead and
 * turns with them. Physically wrong, invisibly so, and the alternative was a
 * thumbprint on every gym-history badge. */
.bdg__mark--pinwheel::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width:  var(--pw-hub);
  height: var(--pw-hub);
  margin: calc(var(--pw-hub) / -2) 0 0 calc(var(--pw-hub) / -2);
  border-radius: 50%;
  /* Above every vane, including the one on top. A pin under a vane is a bead. */
  z-index: 10;
  background: radial-gradient(circle at 36% 30%,
    #eaeff8 0 24%, #9aa3b5 24% 58%, #3d4557 58%);
  box-shadow: 0 0 0 1px rgba(8,13,22,.8), 0 1px 2px rgba(0,0,0,.65);
}

/* Winning the spotlight GLOWS it — the gem's treatment, and this is still the
   only other badge in the set that has earned it. Note what the spotlight does
   NOT do: it does not start the wheel turning. Motion here is a catalogue
   decision (see .bdg__mark--pinwheel below), and a badge that spins only while it happens
   to be spotlit would be a badge that spins for reasons the member cannot see. */
.bdg--spotlight .bdg__mark--pinwheel,
.bdg-reveal__badge .bdg__mark--pinwheel {
  --pw-halo: drop-shadow(0 0 8px rgba(255,255,255,.40));
}

/* ---- gust: the moving copy --------------------------------------------------
 *
 * The same badge with one class on it, exactly as the daybreak pair is — one
 * Anim value in the catalogue, the identical lever every still badge pulls by
 * carrying Anim.NONE. Which of the two ships has not been decided, which is why
 * both are on the page rather than one.
 *
 * The fan REFUSED motion and was right to: a riffle across eight saturated
 * wedges is a strobe, not a reward. Nothing about that argument has been
 * repealed — what changed is the subject. A fan that opens and shuts is a
 * gadget being operated; a pinwheel turning is a pinwheel doing the only thing
 * it does. The picture asks for it now, where before it merely tolerated it.
 *
 * TWO CLOCKS ON ONE ELEMENT, which is the trick that keeps it off the pile of
 * loading spinners. A constant turn is a spinner — the eye has seen ten
 * thousand of them and files this one instantly. So the steady turn runs on
 * `rotate` and a gust runs on `transform`, and because those are separate
 * properties both apply to the same box and simply add.
 *
 *   base    360deg / 19s              = 18.9 deg/s
 *   gust    +/-15deg over 7.9s        = +/-11.9 deg/s at its steepest
 *   result  7 deg/s at the lull, 31 deg/s at the surge
 *
 * A four-fold swing in speed, and — the number that matters — it never reaches
 * zero and never reverses. A wheel that stops is broken and a wheel that backs
 * up is a scrub bar; a wheel that surges and coasts is a wheel in a room with a
 * door that keeps opening. 19 and 7.9 are close to coprime, so the composite
 * does not come back round to where you found it for about two and a half
 * minutes, which is the honest version of "it does not loop".
 * -------------------------------------------------------------------------- */
.bdg__mark--pinwheel .bdg__wheel {
  animation:
    bdg-pw-turn 19s  linear                      infinite,
    bdg-pw-gust 7.9s cubic-bezier(.45, 0, .55, 1) infinite;
}

@keyframes bdg-pw-turn { to { rotate: 1turn; } }

@keyframes bdg-pw-gust {
  0%, 100% { transform: rotate(-15deg); }
  50%      { transform: rotate(15deg); }
}

/* Winning the spotlight is a GEAR CHANGE and not a switch — daybreak's rule,
   for daybreak's reason: a copy that is already moving cannot be rewarded by
   starting. Both periods come down by about a third, and they come down
   together so the surge-to-lull ratio the drawing was tuned on survives. */
.bdg--spotlight.bdg__mark--pinwheel .bdg__wheel,
.bdg-reveal__badge .bdg__mark--pinwheel .bdg__wheel { animation-duration: 12s, 5.3s; }

/* A locked tile never moves. `.bdg--locked .bdg__mark` already greys and dims
   the wheel with a static filter, and a greyed-out pinwheel still merrily
   turning is the worst of both — it reads as earned and broken at once. */
.bdg--locked .bdg__wheel { animation: none; }

@media (prefers-reduced-motion: reduce) {
  /* Parked at an angle, not reset to zero. Stopping it flat would render the
     moving copy pixel-identical to the still one, and a comparison that
     collapses under an accessibility setting is a broken comparison. The
     offsets are deliberately not multiples of 45deg: at 45 the wheel lands back
     on its own symmetry and looks like it never moved. */
  .bdg__mark--pinwheel .bdg__wheel {
    animation: none;
    rotate: 24deg;
    transform: rotate(-7deg);
  }
}

/* PORTED 30 August 2026 from the badges Artifact, replacing the drawing that
 * was here. The Artifact is the design surface for this set and had moved
 * ahead of main; the badge, its name, its tier and its mechanic are unchanged
 * — only the picture inside the coin. The superseded drawing is DELETED rather
 * than parked behind a flag: it has no callers, and a second copy of a rejected
 * picture is a thing somebody re-enables by accident in a year.
 *
 * One change on the way down, the same one every drawing off that page gets:
 * the Artifact keeps a STILL copy and a MOVING copy because which of the two
 * ships had not been decided. It has — the moving one — so the modifier folds
 * into the art class and a Badge names its art and gets the whole drawing. */

/* ---- last ascent: the wall, and the person on it ---------------------------
 *
 * DRAWN 29 August 2026 on the badges Artifact and ported into this sheet on the
 * 30th (the PORTED note directly above); gym/badges.py names it as
 * art="wallstrip". On the Artifact it sat below a boundary between what shipped
 * and what had not, which is why some prose in this sheet still talks as if it
 * were unshipped. It replaced `bdg-art--boltholes` and `bdg__mark--peeltag`, the
 * circuit tag, both since deleted.
 *
 * Somebody climbs the wall, sends it over the top, and the holds come off
 * behind them. That is the sentence the caption has always made — "you caught
 * it before it came down" — drawn instead of implied.
 *
 * FIVE LAYERS, and each is one noun:
 *   .bdg__face::before   the wall: three bands and a grid of fixings  (still)
 *   .bdg__art            the three low holds        (leave second)
 *   .bdg__art::after     the two high holds         (leave first)
 *   .bdg__art::before    the chalk they were climbed with  (stays behind)
 *   .bdg__mark--climber  the person
 * That split is the bat's ruling reused — the hold is art and the bat is a mark
 * so the bat can swing without dragging the hold. Here it is load bearing in
 * both directions: the wall has to be able to empty without moving the climber,
 * and the climber has to be able to leave without moving the wall.
 *
 * ONE DURATION GOVERNS ALL FIVE, --strip-dur, and that is the only reason a
 * sequence this long is affordable: the parts are not kept in step with each
 * other, they are all reading the same clock at the same percentages. The
 * spotlight changes one number.
 * -------------------------------------------------------------------------- */

.bdg-art--wallstrip { --strip-dur: 10s; }

/* THE WALL — three bands of paint and a grid of fixings.
 *
 * THE BANDS ARE NOT THIRDS, and that is the only rule about them worth writing
 * down. Three equal stripes is a flag, and a flag is a thing with a meaning;
 * 27 / 34 / 39 is a wall somebody painted in blocks, which is what nearly every
 * bouldering gym in the country looks like. Teal at the top, off-white through
 * the middle, deep violet at the bottom.
 *
 * THE ORDER IS DRIVEN BY THE HOLDS, not by taste. Every hold has to clear the
 * paint behind it: orange sits on teal, green and purple on the white, blue and
 * yellow on the violet. Any other arrangement puts at least one hold on its own
 * colour, and a purple hold on a purple wall is not a hold, it is a bruise.
 *
 * OFF-WHITE (#e7e3d8), never #fff. Pure white on a 56px tile in a dark portal
 * is a hole punched in the page, and it is also not what wall paint looks like
 * under gym lights. The same bone the page's own --pg-chalk uses.
 *
 * THE FIXINGS ARE A PATTERN, one repeating tile at a 9px pitch, not a scatter
 * placed by hand. A grid is what a climbing wall actually has — a t-nut every
 * 200mm, in rows, whether or not anything is bolted to it — and it is the thing
 * that makes the face read as a WALL rather than as a coloured disc with shapes
 * on it. Every climber has that in well under a second.
 *
 * The pitch is not decoration either: every hold below is centred on a grid
 * point, because a hold that is not over a fixing is a hold that is not bolted
 * to anything. When one slides away the fixing it was on is already there.
 *
 * Drawn as RECESSES rather than as dots — the one thing the deleted bolt-hole
 * artwork got exactly right, inherited verbatim: a dark disc with the light
 * crescent BELOW it, because a hole is lit on its far side and a sticker is lit
 * on its near one. Two layers, because one tile cannot hold both. */
.bdg-art--wallstrip .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  --tnut-hole: radial-gradient(circle 1.05px at 8px 8px,
    rgba(10,12,16,.46) 0 60%, rgba(10,12,16,0) 78%);
  --tnut-lip: radial-gradient(circle 1.5px at 8px 8.8px,
    rgba(255,255,255,.2) 0 52%, rgba(255,255,255,0) 62%);
  background:
    var(--tnut-hole) 0 0 / 10px 10px repeat,
    var(--tnut-lip)  0 0 / 10px 10px repeat,
    /* the joint between two sheets, off to the right so it never runs behind
       the climber */
    linear-gradient(90deg,
      rgba(0,0,0,0) 0 84.5%, rgba(6,8,12,.26) 84.5% 85.8%,
      rgba(255,255,255,.12) 85.8% 86.8%, rgba(0,0,0,0) 86.8%),
    /* Grain, on TWO periods that do not divide into each other. One was a
       corduroy: a 1px line every 7px at any alpha you can see is a stripe, and
       stripes are the one thing painted ply has not got. Nine and seventeen
       beat against each other and it stops repeating to the eye. Both a light
       and a dark pass, so the same grain works over white paint and over
       violet. */
    repeating-linear-gradient(91deg,
      rgba(255,255,255,.03) 0 1px, rgba(0,0,0,0) 1px 4px,
      rgba(0,0,0,.03) 4px 5px, rgba(0,0,0,0) 5px 9px),
    repeating-linear-gradient(88.6deg,
      rgba(255,255,255,.022) 0 1px, rgba(0,0,0,0) 1px 8px,
      rgba(0,0,0,.025) 8px 9px, rgba(0,0,0,0) 9px 17px),
    /* The line where one colour was taped off against the next. A hard stop
       between two flat fields is a graphic; a hard stop with a shadow under it
       is paint that was rolled on in two sessions. */
    linear-gradient(180deg,
      rgba(0,0,0,0) 0 26.8%, rgba(10,12,16,.3) 26.8% 28.2%, rgba(0,0,0,0) 28.2%,
      rgba(0,0,0,0) 60.7%, rgba(10,12,16,.26) 60.7% 62%, rgba(0,0,0,0) 62%),
    /* the room's lights, which are always above the wall */
    linear-gradient(180deg,
      rgba(255,255,255,.08) 0 6%, rgba(0,0,0,0) 32%, rgba(0,0,0,.2) 100%),
    /* the paint */
    linear-gradient(180deg,
      #1b8f84 0 26.8%, #ded8c9 26.8% 60.7%, #4a3670 60.7% 100%);
}

/* THE HOLDS — five, and the line reads yellow, blue, purple, green, orange from
 * the bottom up.
 *
 * FIVE COLOURS, AND NOT ONE, and this is the deleted tag's ruling kept rather
 * than overturned. That badge's tag was blank because in this gym a colour IS a
 * grade — Route.GRADE_BANDS is a list of colour names — so a coloured tag would
 * have claimed a band the badge says nothing about. A single-colour line of
 * holds makes exactly the same claim and would be exactly as wrong. Five
 * colours claims none of them.
 *
 * It is also what actually comes down: this gym strips a WALL at a time, on a
 * Monday, not a problem at a time. The whole face empties for the same reason.
 *
 * NINETY PIXELS WIDE on a 56px face, which looks like a mistake and is the
 * opposite of one. The holds travel by background-position, and a background
 * layer is clipped by its own box — so a 56px box would delete each hold at an
 * invisible vertical line partway through the strip instead of letting the
 * face's circle take it at the edge, which is the only edge a member can see.
 * The extra 34px is the run-off. Every position below is the face coordinate
 * plus 17. */
.bdg-art--wallstrip .bdg__art {
  width: 90px;
  height: 56px;
  top: 50%;
  transform: translate(-50%, -50%);
  /* One shape, five times: a highlight up and left, the body, a hard shaded
     rim, and nothing past it. `closest-side ellipse` is what stops the last
     stop painting into the tile's corners and turning a hold into a square.
     The rim is not styling — it is what keeps the yellow and the orange off
     the white band and the blue off the violet one. */
  background:
    radial-gradient(closest-side ellipse at 34% 24%,
      rgba(255,255,255,.62) 0 10%, #b76ef8 20% 46%, #7b34c6 46% 74%,
      #431a75 74% 96%, rgba(0,0,0,0) 97%) 30px 24px / 10px 8px no-repeat,
    radial-gradient(closest-side ellipse at 34% 24%,
      rgba(255,255,255,.62) 0 10%, #529af9 20% 46%, #2360c4 46% 74%,
      #123566 74% 96%, rgba(0,0,0,0) 97%) 49px 34px / 12px 8px no-repeat,
    radial-gradient(closest-side ellipse at 34% 24%,
      rgba(255,255,255,.7) 0 10%, #f0bc18 20% 46%, #b98705 46% 74%,
      #6d4c03 74% 96%, rgba(0,0,0,0) 97%) 28.5px 44px / 13px 8px no-repeat;
  /* One shadow for all five, the pair on ::after included: a hold that casts
     nothing is printed on the wall rather than bolted to it. */
  filter: drop-shadow(0 2px 2px rgba(4,7,12,.5));
  animation: bdg-strip-low var(--strip-dur) both infinite;
}

/* The two high holds, on their own layer for one reason: they come off FIRST.
   A wall is stripped from the top down, because the alternative is standing
   under the bit you have not taken off yet. */
.bdg-art--wallstrip .bdg__art::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(closest-side ellipse at 34% 24%,
      rgba(255,255,255,.7) 0 10%, #fb923c 20% 46%, #d2620d 46% 74%,
      #7a3606 74% 96%, rgba(0,0,0,0) 97%) 38.5px 3.5px / 13px 9px no-repeat,
    radial-gradient(closest-side ellipse at 34% 24%,
      rgba(255,255,255,.62) 0 10%, #2fd06a 20% 46%, #17954a 46% 74%,
      #0a5527 74% 96%, rgba(0,0,0,0) 97%) 49.5px 14.5px / 11px 7px no-repeat;
  animation: bdg-strip-high var(--strip-dur) both infinite;
}

/* THE CHALK, and it is the reason the bare wall is worth looking at for a
   second and a half. An empty wall is an empty wall; an empty wall with five
   smudges on it is a wall that was being climbed until this morning. It is the
   only part of the drawing that is about the member rather than about the gym.

   NO EDGE ANYWHERE IN IT. The first pass had a defined ellipse at half opacity
   and it read as a sixth hold in grey, which is the worst possible outcome for
   a drawing whose whole job is to say the holds are GONE. Chalk has no outline,
   so every stop fades, the patch is wider than the hold it outlived, and it
   sits a pixel or two off the fixing: chalk gathers where hands went, not where
   the bolt was.

   TWO TONES, not one, and that is the white band's doing. White chalk on white
   paint is invisible — physically true and useless as a drawing — so the core
   is chalk and the halo is the cool grey of the dust around it. On the violet
   the core reads; on the white the halo does. */
.bdg-art--wallstrip .bdg__art::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  --wall-chalk: radial-gradient(closest-side ellipse at 50% 46%,
    rgba(255,255,255,.34) 0 16%, rgba(255,255,255,.17) 40%,
    rgba(108,112,130,.15) 62%, rgba(108,112,130,0) 80%);
  background:
    var(--wall-chalk) 37.5px    2px / 17px 11px no-repeat,
    var(--wall-chalk) 45.5px 13.5px / 17px 11px no-repeat,
    var(--wall-chalk)   28px 23.5px / 17px 11px no-repeat,
    var(--wall-chalk) 45.5px 33.5px / 17px 11px no-repeat,
    var(--wall-chalk) 27.5px 43.5px / 17px 11px no-repeat;
  animation: bdg-chalk var(--strip-dur) both infinite;
}

/* The spotlight is a GEAR CHANGE, never a switch — the rocket's sky and the
   flame's lick make the same move. One number, because all five layers read
   it. */
.bdg--spotlight.bdg-art--wallstrip,
.bdg-reveal__badge .bdg-art--wallstrip { --strip-dur: 6s; }

/* A locked wall is a wall nobody has been on. `.bdg--locked .bdg__art` in the
   sheet above already greys and dims the holds and their pseudos with them;
   what a filter cannot do is stop a pseudo's own animation, so the wall is
   stopped here. Not reachable today — `last_ascent` has no teaser tile — and
   written anyway, because the day somebody adds a rung teaser is not the day to
   remember it. */
.bdg--locked.bdg-art--wallstrip .bdg__face::before { filter: grayscale(1); opacity: .32; }
.bdg--locked.bdg-art--wallstrip .bdg__art::before,
.bdg--locked.bdg-art--wallstrip .bdg__art::after { animation: none; }

/* ---- climber: the person on it ---------------------------------------------
 *
 * A CLIMBER, not a silhouette, and the difference cost the drawing its first
 * version. A white cut-out of a person is a pictogram — the thing on a toilet
 * door — and on a wall with an off-white band in the middle of it, it also
 * vanished. So the figure is dressed: skin, a red tee with short sleeves, dark
 * shorts, climbing shoes, hair. Five flat colours in blocks big enough to
 * survive 21 pixels, and the blocks are what make it read as a person rather
 * than as a shape shaped like one. The red is doing a second job: it is the one
 * hue on the tile that clears teal, bone and violet alike, which is what lets
 * the same figure cross all three bands without ever being lost against one.
 *
 * IT IS A TWO-FRAME SPRITE. One SVG carries both poses side by side — settled
 * on the left, reaching on the right — the image is sized to twice the box and
 * `background-position` steps between them. There is no third frame and there
 * does not need to be one: reach, pull while still extended, land compact is
 * the entire cycle of moving up a wall, and two drawings hold all three of
 * those beats between them.
 *
 * `steps(1, end)` is load bearing. background-position is an interpolable
 * property, so without it the image SLIDES between poses and what you see is
 * not a climber, it is a person being smeared sideways.
 *
 * A BACKGROUND-IMAGE, NOT A MASK, and that is the change that let him be
 * dressed at all: a mask has one channel, so a masked figure can only ever be
 * one colour. The pose sprite works identically either way.
 *
 * IT MOVES IN BEATS, NOT IN A LINE. Four fast pulls of about a third of a
 * second, separated by dwells nearly twice as long. A figure that rises
 * smoothly up a wall is on an escalator; climbing seen from across a room is
 * mostly standing still deciding, and then sudden.
 *
 * THE SHADOW IS ON THE MARK AND THE IMAGE IS ON THE PSEUDO, which is not
 * tidiness: it keeps the transform (on the mark) and the pose (on the pseudo)
 * on separate elements, so freezing one does not mean freezing the other, and
 * the page's storyboard below depends on being able to seek both at once.
 * -------------------------------------------------------------------------- */

.bdg__mark--climber {
  width: 21px;
  height: 28px;
  /* The still form, and it is declared here rather than in the keyframes for
     the reason the board was: cancelling the animation hands this back, so
     reduced motion gets a climber at the top of the wall without a single line
     in the media query. Drawn as a still image first, animated second. */
  transform: translate(-1px, -12px);
  filter: drop-shadow(0 1.5px 1.5px rgba(4,7,12,.7));
  animation: bdg-climb var(--strip-dur) cubic-bezier(.32,.06,.2,1) both infinite;
}

.bdg__mark--climber::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='32'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.9 22.6L8.4 27.4' stroke='%23d9a066' stroke-width='2.9'/%3E%3Cpath d='M16.9 22.0L16.2 26.8' stroke='%23d9a066' stroke-width='2.9'/%3E%3Cpath d='M11.88 18.03L7.9 22.6' stroke='%2326303f' stroke-width='3.6'/%3E%3Cpath d='M11.88 18.03L16.9 22.0' stroke='%2326303f' stroke-width='3.6'/%3E%3Cpath d='M8.34 26.82L8.4 27.4' stroke='%2312161d' stroke-width='3.1'/%3E%3Cpath d='M16.28 26.22L16.2 26.8' stroke='%2312161d' stroke-width='3.1'/%3E%3Cpath d='M12.3 11.9L11.88 18.03' stroke='%23e8483c' stroke-width='5.4'/%3E%3Cpath d='M11.1 12.6L7.6 8.6 7.1 4.9' stroke='%23d9a066' stroke-width='2.9'/%3E%3Cpath d='M13.6 12.6L17.3 8.7 17.8 5.0' stroke='%23d9a066' stroke-width='2.9'/%3E%3Cpath d='M11.1 12.6L9.7 11.0' stroke='%23e8483c' stroke-width='3.5'/%3E%3Cpath d='M13.6 12.6L15.08 11.04' stroke='%23e8483c' stroke-width='3.5'/%3E%3Ccircle cx='12.4' cy='7.9' r='3.1' fill='%23d9a066' stroke='none'/%3E%3Cpath d='M9.61 6.55A3.1 3.1 0 0 1 15.19 6.55Z' fill='%23241a12' stroke='none'/%3E%3Cpath d='M31.0 22.9L31.9 28.0' stroke='%23d9a066' stroke-width='2.9'/%3E%3Cpath d='M41.1 24.0L40.1 29.2' stroke='%23d9a066' stroke-width='2.9'/%3E%3Cpath d='M35.88 17.59L31.0 22.9' stroke='%2326303f' stroke-width='3.6'/%3E%3Cpath d='M35.88 17.59L41.1 24.0' stroke='%2326303f' stroke-width='3.6'/%3E%3Cpath d='M31.79 27.39L31.9 28.0' stroke='%2312161d' stroke-width='3.1'/%3E%3Cpath d='M40.22 28.58L40.1 29.2' stroke='%2312161d' stroke-width='3.1'/%3E%3Cpath d='M36.3 10.2L35.88 17.59' stroke='%23e8483c' stroke-width='5.4'/%3E%3Cpath d='M35.0 10.9L31.4 6.0 30.9 1.6' stroke='%23d9a066' stroke-width='2.9'/%3E%3Cpath d='M37.7 10.9L41.2 7.8 41.7 4.2' stroke='%23d9a066' stroke-width='2.9'/%3E%3Cpath d='M35.0 10.9L33.56 8.94' stroke='%23e8483c' stroke-width='3.5'/%3E%3Cpath d='M37.7 10.9L39.1 9.66' stroke='%23e8483c' stroke-width='3.5'/%3E%3Ccircle cx='36.4' cy='6.4' r='3.1' fill='%23d9a066' stroke='none'/%3E%3Cpath d='M33.61 5.05A3.1 3.1 0 0 1 39.19 5.05Z' fill='%23241a12' stroke='none'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 200% 100%;
  background-position: 0 0;
  background-repeat: no-repeat;
  animation: bdg-climb-pose var(--strip-dur) steps(1, end) both infinite;
}

.bdg--locked .bdg__mark--climber,
.bdg--locked .bdg__mark--climber::before { animation: none; }

/* THE SEQUENCE. Every percentage below is read against --strip-dur, and the
 * four wall layers are reading the same clock, so the order is fixed by
 * arithmetic rather than by five durations somebody has to keep in step:
 *
 *   0-48%   four pulls up the set          climber
 *   48-60%  sends it, over the top, gone   climber
 *   60-69%  the two high holds come off    art::after
 *   64-73%  the three low holds follow     art
 *   73-88%  bare paint, fixings, chalk     art::before   <- the badge
 *   88-97%  the next set goes on           art / art::after
 *   90-100% somebody starts it again       climber
 *
 * THE LONGEST STILL BEAT IS THE EMPTY WALL, and that is the whole argument for
 * the sequence being in this order rather than the other one. The climb is what
 * makes you look; the fifteen per cent of the cycle where there is nothing left
 * but chalk and a grid of empty fixings is what the badge is actually about.
 *
 * HE SENDS IT. He does not come back down, and the first version of this badge
 * did — a fade-out that ran DURING the jump back to the ground rather than
 * before it, so for about a tenth of a second he was visibly streaking down his
 * own wall. On a badge for catching a problem, an accidental fall is the single
 * worst thing the drawing could say, and it took somebody watching the loop to
 * catch it.
 *
 * The fix is the two keyframes at 60% and 60.5%: the transform is PINNED above
 * the lip while the opacity goes to zero, and only then is he moved back to the
 * ground. Never fade and travel in the same interval — the fade takes time and
 * the travel crosses the frame inside it. The pause at the top is second
 * longest. Nobody tops out and leaves. */
@keyframes bdg-climb {
  0%,   4%   { transform: translate(0, 18px);  opacity: 1; }
  8%,  12%   { transform: translate(-4px, 10.5px); }
  16%, 20%   { transform: translate(4px, 3px); }
  24%, 28%   { transform: translate(2px, -4.5px); }
  /* the top hold, and the look at what is left above it */
  32%, 48%   { transform: translate(-1px, -12px);
               animation-timing-function: cubic-bezier(.5,0,.9,.45); }
  /* over the lip, accelerating, and out of the tile */
  56%        { transform: translate(1px, -34px); }
  60%        { transform: translate(1px, -56px); opacity: 1; }
  60.5%      { transform: translate(1px, -56px); opacity: 0; }
  62%, 88%   { transform: translate(0, 56px); opacity: 0; }
  90%        { opacity: 1; }
  97%, 100%  { transform: translate(0, 18px); opacity: 1; }
}

/* Reach, hold the reach through the pull, land settled. The 3% offsets are the
   reach going up before the body follows it, which is the order it happens in.
   The last one is the mantel over the lip and it stays extended until he is
   gone. */
@keyframes bdg-climb-pose {
  0%   { background-position:   0 0; }
  3%   { background-position: 100% 0; }
  8%   { background-position:   0 0; }
  11%  { background-position: 100% 0; }
  16%  { background-position:   0 0; }
  19%  { background-position: 100% 0; }
  24%  { background-position:   0 0; }
  27%  { background-position: 100% 0; }
  32%  { background-position:   0 0; }
  48%  { background-position: 100% 0; }
  62%  { background-position:   0 0; }
  100% { background-position:   0 0; }
}

/* The holds leaving, and coming back. Top pair first, then the low three, each
   accelerating away and easing back in — a hold being carried off gets faster,
   a hold being put on does not.
   The jump from one side to the other happens deep inside the empty beat, where
   both positions are well outside the face. */
@keyframes bdg-strip-high {
  0%, 60%    { background-position: 38.5px 3.5px, 49.5px 14.5px;
               animation-timing-function: cubic-bezier(.5,0,.82,.4); }
  69%, 79%   { background-position: 92.5px 6px, 103.5px 17px; }
  79.5%, 88% { background-position: -15.5px 3.5px, -4.5px 14.5px;
               animation-timing-function: cubic-bezier(.2,.72,.3,1); }
  95%, 100%  { background-position: 38.5px 3.5px, 49.5px 14.5px; }
}

@keyframes bdg-strip-low {
  0%, 64%    { background-position: 30px 24px, 49px 34px, 28.5px 44px;
               animation-timing-function: cubic-bezier(.5,0,.82,.4); }
  73%, 80%   { background-position: 84px 26.5px, 103px 36.5px, 82.5px 46.5px; }
  80.5%, 90% { background-position: -24px 24px, -5px 34px, -25.5px 44px;
               animation-timing-function: cubic-bezier(.2,.72,.3,1); }
  97%, 100%  { background-position: 30px 24px, 49px 34px, 28.5px 44px; }
}

/* The chalk waits for the LAST hold to leave, and is wiped by the next set
   going on. Earlier than that and it appears beside holds that are still on the
   wall, which is a smudge next to a hold rather than a smudge where one was. */
@keyframes bdg-chalk {
  0%, 74%   { opacity: 0; }
  80%, 87%  { opacity: 1; }
  93%, 100% { opacity: 0; }
}

/* Reduced motion.
 *
 * THIS IS THE HARD ONE IN THE SET, because "last ascent" is the only badge here
 * whose meaning is a SEQUENCE rather than an object, and a sequence has no
 * single resting frame. Frozen at 0% it is a climber on a full wall, which is
 * every climbing badge ever drawn and says nothing about a strip. Frozen at 80%
 * it is an empty wall, which says nothing about the climb.
 *
 * So it is frozen HALF WAY THROUGH THE STRIP — climber at the top, the two high
 * holds already sliding off, chalk where they were, the fixings behind them
 * bare. That single frame carries the before and the after at once, which is
 * more than any frame the animation actually passes through does, and it is the
 * only place in this sheet where the still form is a drawing the moving version
 * never shows.
 *
 * The low three and the climber need no line here: their own still values are
 * already exactly where this frame wants them. */
@media (prefers-reduced-motion: reduce) {
  /* Away and dropping, not gone. These are not invented numbers — they are what
     the browser computes at 6.6s of the real cycle, which is the third cell of
     the storyboard on the page this badge was designed on. Full opacity on
     purpose: a hold that is leaving is a hold, and fading it would say
     "disappearing", which is not what happens to it. */
  .bdg-art--wallstrip .bdg__art::after {
    background-position: 56.2px 4.3px, 67.2px 15.3px;
  }
  /* The chalk animates up from zero, so without this the one thing on the wall
     that is about the member vanishes for the people who asked for less
     movement. perks.css's birthday ruling, applied to a strip.
     ONLY THE TWO THAT HAVE BEEN VACATED, and this is the whole reason the five
     patches are separate background layers rather than one image: chalk paints
     ABOVE the low holds, which in this frame are still bolted on, so their
     three patches are parked off the left of the box. A smudge on top of a hold
     that is still there is not chalk, it is a smear on the lens. */
  .bdg-art--wallstrip .bdg__art::before {
    opacity: .9;
    background-position: 37.5px 2px, 45.5px 13.5px, -40px 23.5px, -40px 33.5px, -40px 43.5px;
  }
}

/* ---- Tile geometry, by how much room there actually is -------------------
   Raising the tokens rather than rewriting the grid: every tile rule already
   reads them, so the tiles, the mounts, the holds and the grade medallions all
   scale together and nothing can fall out of proportion with the rest. */
@media (min-width: 700px) {
  :root {
    --badge-tile-min: 132px;
    --badge-size:     68px;
    --hold-scale:    0.408;
    --badge-gap:      .7rem;
    --badge-orbit-r:  38px;
  }
}
@media (min-width: 1080px) {
  :root {
    --badge-tile-min: 152px;
    --badge-size:     78px;
    --hold-scale:    0.468;
    --badge-gap:      .8rem;
    --badge-orbit-r:  44px;
  }
}

/* The type inside a tile grows too, but far less than the medal — the name is
   a label, not a headline, and a 1rem name under a 78px medal reads as a
   caption competing with it. */
@media (min-width: 1080px) {
  .bdg { padding: 1rem .7rem .85rem; border-radius: 16px; }
  /* …but NOT the bare coin, which is the tile with the tile taken off it.
     .bdg--coin zeroes this padding ~2,900 lines above; that rule and this one
     have the same specificity, so from here on the LATER one wins and the coin
     silently got its tile chrome back — on desktop only, which is why the Week
     streak fire sat wrong at 1080px and looked fine on a phone. Two classes,
     so source order stops deciding it. Padding alone: raising the whole
     .bdg--coin block to .bdg.bdg--coin would also outrank the per-tier rules
     below it that legitimately paint the coin. */
  .bdg.bdg--coin { padding: 0; }
  .bdg__name { font-size: .86rem; }
  .bdg__meta, .bdg__progress { font-size: .73rem; }
  .bdg__icon { font-size: 2rem; }
}

/* ==========================================================================
 * Header band
 * ========================================================================== */

.bdg-head {
  display: grid;
  gap: 1.1rem;
  align-items: center;
  margin-bottom: .4rem;
}

/* Two columns once there is room for the hero to sit BESIDE the text rather
   than push it down. `auto` on the hero and `minmax(0, 1fr)` on the text: the
   badge is a fixed object and the prose takes what is left, which is also what
   stops a long badge name blowing the column out. */
@media (min-width: 860px) {
  .bdg-head {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 2rem;
  }
}

.bdg-head__title {
  margin: 0;
  line-height: 1.1;
}

/* The count as a real number rather than a grey aside. It is the first thing
   a member came to see, and it was previously set in the same muted 1rem as
   the sentence explaining how badges work. */
.bdg-head__count {
  margin: .35rem 0 0;
  font-size: clamp(1.05rem, .9rem + .6vw, 1.4rem);
  font-weight: 800;
  letter-spacing: -.01em;
  color: var(--text, var(--clr-text, #f3f4f6));
}

/* The measure is capped on the PROSE and never on the band. Capping the band
   would pull the hero in off the right edge while the grid below it still ran
   the full width of .wrap — two different right margins stacked on one page,
   which reads as a broken layout rather than as a narrower column. */
.bdg-head__blurb {
  margin: .55rem 0 0;
  max-width: 46ch;
  font-size: .9rem;
  line-height: 1.45;
  color: var(--muted, var(--clr-muted, #9ca3af));
}
.bdg-head__blurb a { color: var(--teal-4, var(--clr-teal-4, #2dd4bf)); }

/* ---- The tally ----------------------------------------------------------
   Rarest first, and the one honest summary of a collection this page can
   give. Colour is NEVER the only carrier: the pip is decoration and the tier's
   printed word sits right beside it, the same rule the tiles follow, so this
   survives greyscale, colour-blindness and forced-colors. */
.bdg-tally {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem .5rem;
  margin: .7rem 0 0;
  padding: 0;
  list-style: none;
}

.bdg-tally__item {
  display: inline-flex;
  align-items: baseline;
  gap: .34rem;
  padding: .26rem .55rem;
  border-radius: 999px;
  border: 1px solid var(--clr-badge-rim);
  background: var(--clr-badge-tile);
  font-size: .78rem;
  line-height: 1;
}

.bdg-tally__pip {
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: currentColor;
  align-self: center;
  flex: 0 0 auto;
}

.bdg-tally__n { font-weight: 800; font-size: .88rem; }

.bdg-tally__word {
  color: var(--muted, var(--clr-muted, #9ca3af));
  text-transform: uppercase;
  letter-spacing: .07em;
  font-size: .66rem;
  font-weight: 700;
}

/* One rule per tier, reusing the SAME four accents the tiles use — the tally
   is a legend for the wall below it, so a fifth palette here would be a
   different system wearing the same words. */
.bdg-tally__item--chalk   { color: var(--clr-badge-chalk); }
.bdg-tally__item--grit    { color: var(--clr-badge-grit); }
.bdg-tally__item--rock    { color: var(--clr-badge-rock); }
.bdg-tally__item--history { color: var(--clr-badge-history); }

/* The two rarest tiers get their rim as well as their pip. Rock and gym
   history are the ones worth spotting from across the band, and it is the same
   escalation the tiles make — chalk has no rim there either. */
.bdg-tally__item--rock    { border-color: var(--clr-badge-rock-a55); }
.bdg-tally__item--history { border-color: var(--clr-badge-history-a55); }

/* ---- The hero -----------------------------------------------------------
   The spotlight badge, drawn large. Deliberately the SAME _badge_tile.html as
   every other tile, scaled by this wrapper: one tile design, so the reveal —
   the one moment a member looks closely at a badge — cannot show them a
   second, differently-built version of it.

   Sized by overriding the tokens rather than by transform: scale(). A scaled
   tile blurs its own text, and the sheen, the orbit ring and the shiny rim are
   all computed from these variables — scaling the box would leave every one of
   them drawn for a 56px medal and then stretched. */
.bdg-hero {
  justify-self: start;
}
@media (min-width: 860px) {
  .bdg-hero { justify-self: end; }
}

.bdg-hero__tile {
  --badge-size: 104px;
  --hold-scale:   0.624;
  --badge-rim-w: 3px;
  --badge-orbit-r: 58px;
  width: min(15rem, 72vw);
}

.bdg-hero__tile .bdg {
  width: 100%;
  padding: 1.35rem 1rem 1.15rem;
  border-radius: 20px;
  gap: .55rem;
  /* No entrance stagger on the hero: it is first, so a delay reads as the page
     hesitating rather than as a sequence. */
  animation-delay: 0s;
}

.bdg-hero__tile .bdg__icon { font-size: 2.7rem; }
.bdg-hero__tile .bdg__name { font-size: 1.05rem; }
.bdg-hero__tile .bdg__meta { font-size: .82rem; }
.bdg-hero__tile .bdg__tier { font-size: .68rem; }

/* The sealed hero. Bigger than any other shroud on the page, which is the
   point — an unopened badge IS the most interesting thing here, and this is
   the tile a member is meant to reach for first. The wrapper survives the
   reveal by design: badge_reveal.js swaps the <form> for the unsealed .bdg
   with parentNode.replaceChild, so every rule above still matches afterwards
   and the badge stays big once it opens. */
.bdg-hero__tile .bdg__seal { padding: 0; }

@media (min-width: 1080px) {
  .bdg-hero__tile {
    --badge-size: 120px;
    --hold-scale:   0.720;
    --badge-orbit-r: 66px;
  }
  .bdg-hero__tile { width: 16.5rem; }
  .bdg-hero__tile .bdg__icon { font-size: 3.1rem; }
}

/* ==========================================================================
 * Sections
 * ========================================================================== */

.bdg-section { margin-top: 1.9rem; }

.bdg-section__note {
  margin: .25rem 0 0;
  font-size: .85rem;
  color: var(--muted, var(--clr-muted, #9ca3af));
}
/* The word "shiny" printed in the shiny's own gold. Not a gradient: this is
   one word in a sentence, and text that needs background-clip to be legible is
   one unsupported property away from invisible. */
.bdg-section__shiny { color: var(--clr-badge-rock); }

/* ==========================================================================
 * What's next
 *
 * Still a native <details>, and still closed by default when there is more
 * than one thing in it — this is the only block on the page about what a
 * member has NOT done, and it must not be what they land on. What changed is
 * that it now looks like a section of this page instead of a browser default:
 * a bare "▶ What's next" under a wall of medals read as a debug disclosure.
 * ========================================================================== */

.bdg-next {
  margin-top: 1.9rem;
  border: 1px solid var(--clr-badge-rim);
  border-radius: 14px;
  background: var(--clr-badge-tile-lock);
  overflow: hidden;
}

.bdg-next__summary {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .85rem 1rem;
  cursor: pointer;
  list-style: none;                  /* the marker is drawn below */
  user-select: none;
}
.bdg-next__summary::-webkit-details-marker { display: none; }

.bdg-next__title {
  font-weight: 700;
  font-size: .95rem;
  color: var(--text, var(--clr-text, #f3f4f6));
}

/* How many, in the summary, so a member can decide whether to open it without
   opening it. */
.bdg-next__count {
  display: inline-grid;
  place-items: center;
  min-width: 1.45rem;
  height: 1.45rem;
  padding: 0 .35rem;
  border-radius: 999px;
  border: 1px solid var(--clr-badge-rim);
  font-size: .72rem;
  font-weight: 800;
  color: var(--muted, var(--clr-muted, #9ca3af));
}

/* The chevron, drawn rather than typed: ▶ is a system font's glyph and renders
   as a different size, weight and baseline on every platform. Pushed hard
   right so the row reads title-then-state across the full width. */
.bdg-next__summary::after {
  content: "";
  margin-left: auto;
  width: .5rem;
  height: .5rem;
  border-right: 2px solid var(--muted, #9ca3af);
  border-bottom: 2px solid var(--muted, #9ca3af);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .18s var(--badge-ease);
}
.bdg-next[open] .bdg-next__summary::after {
  transform: rotate(225deg) translate(-2px, -2px);
}

.bdg-next[open] .bdg-next__summary {
  border-bottom: 1px solid var(--clr-badge-rim);
}

.bdg-next .bdg-grid {
  margin: 0;
  padding: .9rem 1rem 1.05rem;
}

.bdg-next__summary:focus-visible {
  outline: 2px solid var(--teal-4, var(--clr-teal-4, #2dd4bf));
  outline-offset: -2px;
}

/* ==========================================================================
 * Footnote
 * ========================================================================== */

.bdg-foot {
  margin-top: 1.6rem;
  max-width: 62ch;
  font-size: .8rem;
  line-height: 1.5;
  color: var(--muted, var(--clr-muted, #9ca3af));
}

/* ==========================================================================
 * Reduced motion / forced colors — the page furniture, same rulings the tiles
 * already make above.
 * ========================================================================== */

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

@media (forced-colors: active) {
  /* Every surface above is drawn with background + border and no text of its
     own, so forced-colors keeps the structure. The tally is the exception: its
     meaning rides on `color`, which is overridden — and the printed tier WORD
     beside each pip is exactly why that is survivable. */
  .bdg-tally__item,
  .bdg-next,
  .bdg-next__count { border-color: ButtonBorder; }
  .bdg-tally__pip { background: CanvasText; }
}

/* ==========================================================================
 * Hover — desktop only, and it is FEEDBACK rather than decoration.
 *
 * This does not break the one-moving-thing rule, and the distinction is the
 * reason the rule exists: the ban is on AMBIENT motion, twenty medals shimmering
 * at a member who did not ask. A tile that responds to the pointer sitting on it
 * moves only while somebody is pointing at it, is never more than one tile at a
 * time, and is the ordinary contract of a pointer-driven surface. The whole
 * sheet is already gated on (hover: hover) for the holo tilt for the same
 * reason: the surface this ships to is a phone, and a phone has no hover at all.
 * ========================================================================== */
@media (hover: hover) and (pointer: fine) {
  .bdg {
    transition:
      border-color .18s var(--badge-ease),
      background   .18s var(--badge-ease),
      transform    .18s var(--badge-ease);
  }
  .bdg:hover {
    border-color: var(--muted, var(--clr-muted, #9ca3af));
    background: rgba(255, 255, 255, .06);
  }
  /* The lift is withheld from a holo tile, which already answers the pointer
     with its own tilt — stacking a translate on that transform would replace
     it outright, and the collector's edition losing its one trick on hover is
     the opposite of what either effect is for. */
  .bdg:not(.bdg--holo):hover { transform: translateY(-2px); }

  /* A sealed tile is already shaking to be tapped. Lifting it as well reads as
     two competing invitations. */
  .bdg--sealed:hover { transform: none; }

  /* A BARE COIN IS NOT A TILE AND ANSWERS NO POINTER. Everything above is
     written for a tile on the badge wall: a bordered, padded card that is the
     thing you are pointing at. A coin has neither border nor padding — it is a
     mark embedded in something else, beside a name in a heading, inside a
     label, inside a button, or in a leaderboard row — so the tile's feedback
     lands wrong twice over. The lift moves a 40px medallion two pixels out of a
     row it is aligned to, and the panel paints a translucent 16px-radius SQUARE
     behind a round badge (.bdg--coin clears the border and background but never
     the radius, because a tile's corner is invisible until something fills it).
     Where the coin sits inside a control, that control already answers the
     pointer itself, and a mark that separately lifts inside its own chip reads
     as two things reacting to one hover.

     .bdg.bdg--coin, doubled deliberately: the lift rule above is
     .bdg:not(.bdg--holo):hover, and :not() contributes its argument's
     specificity — 0-3-0. A single .bdg--coin:hover is 0-2-0 and would lose
     however far down the file it sat. */
  .bdg.bdg--coin:hover {
    transform: none;
    background: none;
    border-color: transparent;
  }
}

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


/* ==========================================================================
 * THE FIVE MINERALS — one value ladder each, and nothing else.
 *
 * A mineral is SIX NUMBERS, not a second copy of a drawing: --gm-1 is the
 * lightest step, --gm-5 the darkest, --gm-glow is what the tile throws when it
 * is lit. Bare rgb channels, so the drawing below composes its own alphas from
 * them. The geometry is written once; all five really are one stone in five
 * colours.
 *
 * The ladders are deliberately WIDE. A stone is not six shades of one medium
 * tone — the range from near-white to near-black inside a single hue is what
 * makes a facet break look like a break, and a narrow ladder is the quiet half
 * of why the first draw read as a moulded ball rather than as something cut.
 *
 * WHICH mineral a member gets is decided in Python, once, and never here — see
 * gym/badges.py::_gem_for. It is drawn from their own member id, so it is the
 * same stone on every page load for ever without a row being stored.
 * ========================================================================== */

.bdg--gem-sapphire {
  --gm-1: 191 219 254; --gm-2: 96 165 250; --gm-3: 37 99 235;
  --gm-4: 29 58 138;   --gm-5: 15 23 60;   --gm-glow: 59 130 246;
}
.bdg--gem-emerald {
  --gm-1: 167 243 208; --gm-2: 52 211 153; --gm-3: 5 150 105;
  --gm-4: 6 95 70;     --gm-5: 2 44 34;    --gm-glow: 16 185 129;
}
.bdg--gem-ruby {
  --gm-1: 254 205 211; --gm-2: 251 113 133; --gm-3: 225 29 72;
  --gm-4: 159 18 57;   --gm-5: 63 7 21;     --gm-glow: 244 63 94;
}
.bdg--gem-amethyst {
  --gm-1: 221 214 254; --gm-2: 167 139 250; --gm-3: 124 58 237;
  --gm-4: 91 33 182;   --gm-5: 46 16 101;   --gm-glow: 139 92 246;
}
.bdg--gem-citrine {
  --gm-1: 253 230 138; --gm-2: 251 191 36; --gm-3: 217 119 6;
  --gm-4: 146 64 14;   --gm-5: 59 23 3;    --gm-glow: 245 158 11;
}

/* ==========================================================================
 * THE HOLD ITSELF, CUT FROM GEMSTONE.
 *
 * The shiny rock grew sapphire specks INSIDE a grey hold. At 104px — the size
 * the portal actually gives it — those specks are 4-7px inside 31px of stone,
 * and the hold is already speckled, because the rockhold mark carries two
 * frequencies of mineral grain underneath them. The gems and the grain were
 * doing the same job in the same pixels. This throws the grey away instead:
 * the shiny hold is not rock with gems in it, it is a hold somebody cut out of
 * a single crystal, and it changes all 31px.
 *
 * It keeps the ROCK'S OWN SILHOUETTE — the same eight irregular vertices the
 * standard edition is clipped to — because that is what makes the two editions
 * the same badge. This sheet's rule for a shiny is "the SAME badge worn
 * differently": same name, same icon, same silhouette. Change the outline and
 * it stops being a colourway and starts being a second badge. (An emerald cut
 * was drawn and rejected on exactly that: a better picture of a gem, a worse
 * shiny.)
 *
 * It is also the first shiny here that says rare with MATERIAL rather than
 * with foil, which is the right language for the one climb in the building on
 * real stone.
 *
 * THE GLOW IS ALWAYS ON, and it is the first always-on light in this file.
 * That is deliberate and it does not touch the one-moving-thing rule: it is
 * material, not motion. Winning the spotlight makes it burn brighter, the same
 * shape of exception the flame and the rocket already carry.
 * ========================================================================== */

/* The inclusions belong to the other approach. Off for every gem hold. */
.bdg.bdg--shiny[class*="bdg--gem-"] .bdg__gem { display: none; }

.bdg.bdg--shiny[class*="bdg--gem-"] .bdg__mark--rockhold {
  background-color: rgb(var(--gm-4));
  background-image:
    /* girdle, lit edge — a thin hard band along the top-left border */
    linear-gradient(120deg, rgba(255, 255, 255, .46) 0 5%, rgba(255,255,255,0) 5%),
    /* girdle, shadow edge — the same band on the far side, and hard, because a
       soft one here is the circular rim all over again */
    linear-gradient(120deg, rgba(0,0,0,0) 0 89%, rgb(var(--gm-5) / .92) 89%),
    /* girdle, bottom edge */
    linear-gradient(188deg, rgba(0,0,0,0) 0 91%, rgb(var(--gm-5) / .8) 91%),
    /* pavilion — one broad plane turning under, low-right */
    linear-gradient(-40deg, rgb(var(--gm-5) / .46) 0 22%, rgba(0,0,0,0) 22%),
    /* crown — eight kite facets. Every boundary angle below is the bearing of
       one of the silhouette's vertices from the table's centre, computed in
       the hold's real 31×26px, which is why they are not round numbers. */
    conic-gradient(from 0deg at 44% 40%,
      rgb(var(--gm-2))   0deg  22.6deg,   /* up          */
      rgb(var(--gm-3))  22.6deg  67.3deg, /* up-right    */
      rgb(var(--gm-5))  67.3deg 100.2deg, /* right       */
      rgb(var(--gm-4)) 100.2deg 136.7deg, /* right-down  */
      rgb(var(--gm-2)) 136.7deg 184.5deg, /* down — the flash */
      rgb(var(--gm-5)) 184.5deg 228.5deg, /* down-left   */
      rgb(var(--gm-3)) 228.5deg 276.5deg, /* left        */
      rgb(var(--gm-1)) 276.5deg 319.9deg, /* up-left — faces the light */
      rgb(var(--gm-2)) 319.9deg 360deg);  /* up          */
  /* The stone IS the shiny, so it carries its own light standing still. That is
     material, not motion — the sheet's one-moving-thing rule is untouched. */
  box-shadow:
    0 2px 3px rgba(0, 0, 0, .55),
    0 0 7px rgb(var(--gm-glow) / .30);
}

/* Mineral grain is what makes something read as ROCK, so most of it goes. What
   is left at .18 is veiling — the faint cloudiness inside a real stone, which
   is the difference between a gem and a moulded piece of acrylic. */
.bdg.bdg--shiny[class*="bdg--gem-"] .bdg__mark--rockhold::before {
  opacity: .18;
  mix-blend-mode: soft-light;
}

/* THE TABLE — the shared bolt-hole slot, spent again.
 *
 * On the grey rock this pseudo-element is the quartz vein. On a cut stone a
 * bright seam running the full width is a CRACK, so it is reinterpreted rather
 * than deleted: it becomes the flat top of the stone, which is the one thing
 * the background layers cannot draw. A background-image cannot make a polygon;
 * an element can, because an element can be clipped.
 *
 * Its outline is the hold's own eight vertices scaled down and re-fitted to
 * this box — a table really is a small copy of the girdle outline, which is
 * why a step cut looks like nested rings of the same shape.
 *
 * drop-shadow rather than box-shadow, for the reason given on .bdg__gem: a
 * box-shadow is drawn on the border box and clipped away with everything
 * outside the polygon. Two of them here — a hard white one up-left and a soft
 * dark one down-right — give the table a lit edge and a shadowed edge, which
 * is what makes it read as a PLANE STANDING ABOVE the crown rather than as a
 * lighter patch painted on it.
 * -------------------------------------------------------------------------- */
.bdg.bdg--shiny[class*="bdg--gem-"] .bdg__mark--rockhold::after {
  content: "";
  display: block;
  position: absolute;
  left: 18%; right: 30%; top: 14%; bottom: 35%;
  height: auto;
  clip-path: polygon(69% 4%, 96% 31%, 99% 59%, 84% 86%, 46% 99%, 13% 83%, 0% 44%, 18% 12%);
  transform: none;
  filter:
    drop-shadow(-.5px -.5px 0 rgba(255, 255, 255, .62))
    drop-shadow(.55px .6px .3px rgba(0, 0, 0, .62));
  background:
    /* two reflections lying flat in the plate, both with straight edges. A
       round highlight here is what the first draft had, and a round highlight
       in the middle of a shape is the definition of a sphere. */
    linear-gradient(118deg, rgba(255, 255, 255, .5) 0 20%, rgba(255,255,255,0) 20%),
    linear-gradient(-24deg, rgba(255, 255, 255, .15) 0 28%, rgba(255,255,255,0) 28%),
    /* the plate itself — and it is CALM. The crown around it is eight facets
       arguing; the table is one plane, so it gets one value break and nothing
       else. Drawn dark it became a crater, because a dark centre ringed by
       brighter facets is a hole. It sits mid-ladder instead, brighter than the
       shadow facets and quieter than the lit ones, which is what reads as the
       flat top of a stone. */
    linear-gradient(146deg, rgb(var(--gm-2)) 0 54%, rgb(var(--gm-3)) 54%);
  pointer-events: none;
}

.bdg--spotlight.bdg--shiny[class*="bdg--gem-"] .bdg__mark--rockhold,
.bdg-reveal__badge .bdg--shiny[class*="bdg--gem-"] .bdg__mark--rockhold {
  box-shadow:
    0 2px 3px rgba(0, 0, 0, .55),
    0 0 13px 2px rgb(var(--gm-glow) / .55);
}

/* The word under a gem shiny is the STONE, and it is painted in that stone's
   own light rather than in the rainbow every other shiny wears. The rainbow
   gradient is the foil's signature; on a badge whose whole argument is that it
   is cut from one material, a rainbow label contradicts the drawing above it.
   Written three classes deep to beat `.bdg.bdg--shiny .bdg__tier`, which sets
   the gradient and would otherwise win on specificity and leave this
   transparent — the exact trap that has already eaten two rules in this
   sheet. */
.bdg.bdg--shiny[class*="bdg--gem-"] .bdg__tier {
  background: none;
  -webkit-background-clip: border-box;
          background-clip: border-box;
  color: rgb(var(--gm-1));
  text-shadow: 0 0 8px rgb(var(--gm-glow) / .5);
}

/* ==========================================================================
 * ARMED — the stage is open and the shroud is waiting on the server.
 *
 * The reveal used to do nothing at all until the POST came back: tap, dead
 * tile, then a full-screen overlay cutting in a few hundred milliseconds
 * later. On a phone on gym wifi that reads exactly like a page load, which is
 * what it was reported as. The overlay opens on the TAP now, with the shroud
 * already in it, and the break waits for the answer instead of the whole
 * moment waiting for it.
 *
 * So the sequence is in two halves that can be separated by any amount of
 * network: land-and-wait, then break. When the answer arrives the shroud has
 * already landed, so the release skips the landing and starts at the rattle —
 * and --rv-break, the one timing token everything downstream reads, is moved
 * to match. Nothing else in the sequence needs to know this happened.
 * ========================================================================== */

.bdg-reveal__stage.is-armed .bdg-reveal__seal {
  animation:
    bdg-rv-land .42s cubic-bezier(.2, 1.45, .4, 1) both,
    bdg-rv-hold 1.9s ease-in-out .42s infinite;
}

/* A breath with a tic in it, NOT the hard rattle the break uses. This one is a
   box sitting in your hands; that one is the lid coming off. Making them the
   same shake would spend the break's own gesture before it happens, and the
   member would watch the shroud rattle for as long as the request took. */
@keyframes bdg-rv-hold {
  0%, 60%, 100% { transform: scale(var(--reveal-scale)) rotate(0deg); }
  68%  { transform: scale(calc(var(--reveal-scale) * 1.035)) rotate(-3deg); }
  76%  { transform: scale(calc(var(--reveal-scale) * 1.035)) rotate( 3deg); }
  84%  { transform: scale(var(--reveal-scale)) rotate(-1.5deg); }
}

/* Released from armed: it has already landed, so the landing is not replayed.
   Straight to the rattle, then the break. */
.bdg-reveal__stage.is-run.is-released .bdg-reveal__seal {
  animation:
    bdg-rv-rattle .26s ease-in-out both,
    bdg-rv-shatter .30s ease-out .26s forwards;
}

/* The caption and the actions belong to the badge, not to the wait — held back
   until there is something to caption. */
.bdg-reveal__stage.is-armed .bdg-reveal__caption,
.bdg-reveal__stage.is-armed .bdg-reveal__actions { opacity: 0; }

/* ---- The tap itself -------------------------------------------------------
 * Feedback in the same frame as the finger, so the tap is acknowledged before
 * the network is even asked. Transform only — a box-shadow or a colour change
 * here would repaint a tile that is about to be cloned. */
.bdg--sealed .bdg__seal { transition: transform .12s var(--badge-ease); }
.bdg--sealed .bdg__seal:active { transform: scale(.94); }
.bdg--sealed.is-opening { animation: none; }
.bdg--sealed.is-opening .bdg__mount { animation: none; opacity: .35; }

/* ---- Open all ------------------------------------------------------------
 * Added by the script and only when there are two or more sealed, because it
 * is an enhancement over a page that already works one tap at a time. */
.bdg-openall {
  display: inline-flex; align-items: center; gap: .45rem;
  margin: .9rem 0 .2rem;
  padding: .5rem .95rem;
  border-radius: 999px;
  border: 1px solid var(--clr-badge-grit-a55);
  background: var(--clr-badge-grit-a22);
  color: var(--text, #f3f4f6);
  font: inherit; font-size: .85rem; font-weight: 700;
  cursor: pointer;
}
.bdg-openall:hover { background: var(--clr-badge-grit-a28); }
.bdg-openall:focus-visible { outline: 2px solid var(--clr-badge-grit); outline-offset: 2px; }
.bdg-openall__n { font-variant-numeric: tabular-nums; opacity: .75; font-weight: 500; }

@media (prefers-reduced-motion: reduce) {
  .bdg-reveal__stage.is-armed .bdg-reveal__seal { animation: none; }
  .bdg--sealed .bdg__seal:active { transform: none; }
}

/* ===========================================================================
 * TWO NAMED WALL CLEARS — ported from the badges Artifact, 2026-08-30.
 *
 * `cleared_the_wall` shipped with a BROOM for an icon and no drawing at all,
 * and it is the badge that got named as the example of what not to do
 * ("this broom badge is a perfect example of a badge I hate", Keiran). The
 * mechanic is fine — clearing a wall is the hardest thing a climber can do to
 * one — so what changed is the name and the picture, per wall, over a badge
 * that is otherwise untouched. As of 31 August 2026 a wall with no drawing
 * earns nothing: the generic badge is deleted, not fallen back to.
 *
 * Both drawings come from the Artifact
 * (claude.ai/code/artifact/8b44bc93-9859-47c8-a22b-a23e164959ec), where they
 * were argued to their fourth and fifth drafts. Ported verbatim except for one
 * change, made in both: the Artifact kept a STILL copy and a MOVING copy of
 * each, because which of the two would ship had not been decided. It has now —
 * the moving one — so `.bdg--infall` and `.bdg--roost` collapse into
 * `.bdg-art--eventhorizon` and `.bdg-art--batcave`, and there is no modifier to
 * remember to stamp. A Badge names its art and gets the whole drawing.
 *
 * GRAVITY WAS DRAWN TWICE IN ONE DAY. The first port was the chalk block, and
 * the Artifact replaced it with the event horizon hours later — see the port
 * note directly below, which is the only reason this header names a drawing
 * that had a predecessor.
 * =========================================================================== */
/* PORTED 30 August 2026, over the top of a drawing that shipped from the same
 * page that morning. `bdg-art--weightless` + `bdg__mark--chalkblock` went out
 * with the wall-clear work and was deleted from the Artifact hours later, by a
 * parallel session; its own header below says why, and the reasoning is the
 * part worth keeping. The dead drawing is NOT kept behind a flag: it has no
 * callers — gym/wall_clears.py names one art per wall — and a second copy of a
 * rejected picture is a thing somebody re-enables by accident in a year.
 *
 * ONE CHANGE on the way down, the same one the cave got: `.bdg--infall` is
 * folded into `.bdg-art--eventhorizon`, so a Badge names its art and gets the
 * whole drawing rather than needing a second class stamped beside it. */

/* ---- eventhorizon: the strongest gravity there is, and one person ignoring it
 *
 * DRAWN 30 AUGUST 2026, IN THIS FILE, REPLACING A DRAWING THAT WAS ON THIS PAGE
 * FOR ONE DAY. What was here was a block of pressed chalk and the piece snapped
 * off it, hanging motionless in an empty dusty dark. Every ruling that drawing
 * fought for is recorded in the prose below rather than deleted with the CSS,
 * because the reason it lost is more useful than the drawing was:
 *
 *   IT DREW AN ABSENCE, AND AN ABSENCE IS NOT VISIBLE. The whole argument was
 *   that the tile had no horizon, no lit side, no shadow and no direction —
 *   that a member could not tell which way was down on it. All of that was true
 *   and none of it was legible. Nothing in the frame was falling, so nothing in
 *   the frame was NOT falling either, and a badge called "You defy Gravity" was
 *   left proving its point by omission to somebody looking at a 56px disc for
 *   four seconds.
 *
 * So the subject is inverted. Instead of removing gravity from the picture, put
 * the most gravity that physically exists in the middle of it and let
 * everything in frame fall in — then hang one boulderer above it, doing nothing
 * at all. The defiance is a CONTRAST now rather than a vacancy, and a contrast
 * can be seen. The chalk survives where it earns its place: it is the material
 * going down the drain.
 *
 * IT IS ALSO THE FIRST FULL-FACE DRAWING IN THIS SHEET WITH ITS LIGHT SOURCE IN
 * FRAME. The sunrise, the noon field and the dusk sky are lit by a sun at the
 * edge of the picture; the ply strip is lit by a room light above it that is
 * never drawn. Here the thing lighting the tile is the disc, it is at the
 * centre of it, and everything else is lit from below by it — which is why the
 * climber carries a warm underglow and no top light at all.
 * -------------------------------------------------------------------------- */

.bdg-art--eventhorizon {
  --eh-outer: 23s;   /* the outer disc, orbiting slowly */
  --eh-inner:  9s;   /* the inner disc, orbiting fast   */
}

/* THE VOID, AND THE BLOOM. The same near-black field the chalk drawing
   established, which is the one part of it worth keeping, minus the fourteen
   motes that turned it into a star field. The bloom is new: a big soft warm
   wash centred on the disc, because a light source that does not light anything
   around it is a sticker. It is what stops the tile reading as a hole punched
   in the page.
   EVERY CENTRE HERE IS `50% calc(50% + 16px)` AND NEVER A BARE PIXEL PAIR, and
   the first draft got that wrong in the direction this sheet has not been
   caught in before. The known trap is a mark placed in PERCENT — see the noon
   and sundown blocks, which both drifted off their suns when the face grew.
   The inverse bites just as hard: a gradient pinned at `28px 40px` is centred
   on a 56px face and a long way up and to the left of centre on the 78px one
   the grid actually uses, which is where this was first looked at. Offsets from
   the centre are the only form that survives 50, 56, 68, 78 and 104. */
.bdg-art--eventhorizon .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(16px 13px at 50% calc(50% + 16px), rgba(249,158,72,.26) 0%, rgba(249,158,72,.08) 46%, rgba(249,158,72,0) 100%),
    radial-gradient(27px 23px at 50% calc(50% + 16px), rgba(234,120,42,.14) 0%, rgba(234,120,42,.038) 54%, rgba(234,120,42,0) 100%),
    radial-gradient(125% 125% at 50% 62%, #1a1a2b 0%, #0e1120 46%, #070a12 100%);
}

/* THE EVENT HORIZON, on the element's own background so that it never turns: a
   perfectly black disc, and it is the only thing on this page allowed to be
   flat black. Everywhere else in this sheet a featureless black area is called
   a hole in the badge — the band-7 ruling, the night owl's wall, the chalk
   drawing's own void. This one IS a hole, in the only drawing where that is the
   subject rather than the mistake, and it is the exception the rule was always
   going to have to make exactly once.
   Hard-edged, with no falloff at all: an event horizon is not a fog bank, it is
   the sharpest edge in physics, and one pixel of blur turns it into a smudge of
   soot on the lens. */
.bdg-art--eventhorizon .bdg__art {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  transform: none;
  background: radial-gradient(circle 8.0px at 50% calc(50% + 16px), #000 0 95%, rgba(0,0,0,0) 100%);
}

/* THE OUTER DISC. A conic gradient masked to an annulus — the conic does the
   angular brightness, the mask makes the ring, and the pair is the whole trick.
   NOT AN EVEN GLOW. Real accretion material is lumpy and one side of a disc is
   always brighter than the other, and more to the point an evenly-lit ring does
   not read as ROTATING however fast it is spun: with nothing on it to track, a
   uniform annulus turning is a uniform annulus sitting still. The bright arcs
   are the only reason the eye can see that it is going round at all. */
.bdg-art--eventhorizon .bdg__art::before {
  content: "";
  position: absolute;
  inset: 0;
  background: conic-gradient(from 0deg at 50% calc(50% + 16px),
    rgba(255,206,140,.95) 0deg, rgba(214,84,22,.30) 40deg, rgba(104,30,10,.10) 82deg,
    rgba(255,188,112,.86) 130deg, rgba(190,66,20,.24) 172deg, rgba(88,24,8,.08) 212deg,
    rgba(255,220,166,.98) 256deg, rgba(206,76,22,.28) 300deg, rgba(96,28,10,.09) 334deg,
    rgba(255,206,140,.95) 360deg);
  -webkit-mask-image: radial-gradient(circle at 50% calc(50% + 16px),
    rgba(0,0,0,0) 0 10.9px, #000 12.1px 14.4px, rgba(0,0,0,.26) 16.0px, rgba(0,0,0,0) 17.6px);
  mask-image: radial-gradient(circle at 50% calc(50% + 16px),
    rgba(0,0,0,0) 0 10.9px, #000 12.1px 14.4px, rgba(0,0,0,.26) 16.0px, rgba(0,0,0,0) 17.6px);
  transform-origin: 50% calc(50% + 16px);
}

/* THE INNER DISC AND THE PHOTON RING, on one layer because they share a centre
   and neither is worth an element of its own.
   THE INNER RING ORBITS FASTER THAN THE OUTER ONE, and that is not a flourish
   to justify a second duration — it is the one fact about accretion discs a
   drawing can actually show. Orbital period grows with radius, so a disc does
   not turn like a plate, it SHEARS: the inside laps the outside. Two rings on 9
   and 23 seconds is that, at the only fidelity 56px can carry, and it is also
   why the tile never reads as a loading spinner. A spinner is one rigid ring.
   Nothing here is rigid.
   The photon ring is the hairline hard against the horizon and it does not turn
   at all. It is not material — it is light that went round the back and came
   out again, and it is identical on every side by definition. */
.bdg-art--eventhorizon .bdg__art::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 50% calc(50% + 16px),
      rgba(0,0,0,0) 0 7.8px, rgba(255,248,232,.98) 8.1px 8.7px, rgba(0,0,0,0) 9.1px),
    conic-gradient(from 200deg at 50% calc(50% + 16px),
      rgba(255,246,220,.98) 0deg, rgba(240,132,44,.42) 54deg, rgba(150,44,14,.16) 106deg,
      rgba(255,240,204,1) 168deg, rgba(232,110,32,.38) 230deg, rgba(128,36,12,.13) 292deg,
      rgba(255,246,220,.98) 360deg);
  /* THE CORE IS TRANSPARENT, and the first draft of this mask had it opaque —
     so the conic filled the middle and what the tile drew was a polished brass
     doorknob. A black hole is defined by the part of it you cannot see; the
     mask has to CUT that part out and let the flat black underneath show. */
  -webkit-mask-image: radial-gradient(circle at 50% calc(50% + 16px),
    rgba(0,0,0,0) 0 7.6px, #000 8.0px 8.8px, rgba(0,0,0,0) 9.2px 9.6px, #000 10.0px 11.1px, rgba(0,0,0,0) 12.2px);
  mask-image: radial-gradient(circle at 50% calc(50% + 16px),
    rgba(0,0,0,0) 0 7.6px, #000 8.0px 8.8px, rgba(0,0,0,0) 9.2px 9.6px, #000 10.0px 11.1px, rgba(0,0,0,0) 12.2px);
  transform-origin: 50% calc(50% + 16px);
}

/* ---- freeclimber: the one thing in the frame that is not falling ------------
 *
 * A boulderer in a dead hang, arms up on two holds that are not there, one knee
 * drawn up, directly above the disc. Nothing is holding them and nothing is
 * happening to them, which is the entire badge: everything else in this picture
 * is on its way in.
 *
 * THIS IS THE SECOND FIGURE IN THE SET AND THE TWO ARE TOLD APART BY WHAT THEY
 * ARE TOUCHING. The last-ascent mark is a full-colour two-frame sprite in a red
 * top and navy shorts, on a brown wall, moving up it in four beats. This is a
 * single pale monochrome pose, on nothing, that never moves at any point in any
 * cycle. One is on a wall; one is on nothing. At 56px that is not a subtle
 * distinction, and neither is a sheet of ply against a black hole.
 *
 * PALE RATHER THAN A SILHOUETTE, which overturns the night owl's rule rather
 * than ignoring it. That badge argues that a shape against a lit sky is BLACK,
 * and it is right — for a shape IN FRONT of the light. This figure is above the
 * disc against unlit void, so the light is below it rather than behind it, and
 * a black figure on near-black is not a silhouette, it is a missing figure. The
 * lighting is honoured in the one place it can be: the warm shadow is thrown
 * UPWARDS off the underside, because the only lamp in this picture is under
 * their feet. There is no top light at all, and that is worth the line because
 * every other object in this sheet has one — the strip's ply says in as many
 * words that the room lights are always above the wall. There is no room here.
 * -------------------------------------------------------------------------- */

.bdg__mark--freeclimber {
  width: 23px;
  height: 28px;
  /* Pinned in px above the face's centre, never in percent — the medal is 50px
     on a phone and 104 in the header band while the mark stays 26x32, so a
     figure placed at "24% down" hangs clear of the disc at one size and inside
     it at another. Both the sundown and noon blocks were caught by this.
     THIRTEEN PIXELS, and the number is the gap rather than the position. The
     disc's bright outer arc tops out 1.6px below the face's centre line and the
     figure's feet stop just above it, so there is unlit void between them at
     every size. The draft before this had the two touching, and a boulderer
     whose feet are IN the accretion disc is not defying anything. */
  transform: translate(0, -13px);
  /* Lit from below, by the only thing in frame that emits anything. */
  filter: drop-shadow(0 2px 2.5px rgba(249,140,50,.55));
}

.bdg__mark--freeclimber::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='32'%3E%3Cg fill='none' stroke='%23e7edf8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.1 11.6L13.4 20.2' stroke-width='4.2'/%3E%3Cpath d='M11.5 12.1L8.2 7.4 7.3 2.6' stroke-width='2.9'/%3E%3Cpath d='M14.8 12.1L18.2 7.6 19.2 2.9' stroke-width='2.9'/%3E%3Cpath d='M12.9 20.0L10.0 24.6 10.9 29.6' stroke-width='3.1'/%3E%3Cpath d='M14.0 20.0L18.1 22.6 17.0 27.2' stroke-width='3.1'/%3E%3Ccircle cx='13.0' cy='8.0' r='3.05' fill='%23e7edf8' stroke='none'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

/* ---- infall: the copy that moves --------------------------------------------
 *
 * Two rules, and they are the exact inverse of the two the chalk drawing was
 * built on. That one banned every easing curve in CSS on the grounds that
 * acceleration IS gravity, and that a badge with this name could not afford a
 * single statement that a force was acting. It was right about the physics and
 * wrong about the badge. THIS tile is a badge about a force acting — about the
 * largest one there is, acting on everything in frame except one person — so
 * the disc is allowed to move like something being pulled.
 *
 * WHAT DOES NOT MOVE IS THE CLIMBER, and that is load-bearing now in a way it
 * was not before. On the old drawing a motionless figure would have been one
 * more motionless thing on a motionless tile. Here it is the ONLY motionless
 * thing, and it is motionless while the fastest object on the page turns
 * underneath it.
 * -------------------------------------------------------------------------- */
.bdg-art--eventhorizon .bdg__art::before { animation: bdg-eh-orbit var(--eh-outer) linear infinite; }
.bdg-art--eventhorizon .bdg__art::after  { animation: bdg-eh-orbit var(--eh-inner) linear infinite; }

@keyframes bdg-eh-orbit { to { transform: rotate(360deg); } }

/* The spotlight is a gear change and never a switch, the same move the rocket's
   sky and the flame's lick make. Both rings a third quicker and still sheared
   against each other: 9 and 23 share no factor and neither do 6 and 15, so the
   pair does not return to the same arrangement for 207 seconds either way. */
.bdg--spotlight.bdg-art--eventhorizon,
.bdg-reveal__badge .bdg-art--eventhorizon {
  --eh-outer: 15s;
  --eh-inner:  6s;
}

/* A locked horizon is a disc nobody has hung over. The face drains to the
   recessed dark every other locked tile wears, the disc greys with it, and both
   rings stop — a filter cannot stop a pseudo's animation, so they are named.
   Not reachable today, because the entry carries no teaser ids and so has no
   locked tile at all. Written anyway, for the strip's reason: the day somebody
   adds one is not the day to remember this. */
.bdg--locked.bdg-art--eventhorizon .bdg__face::before { filter: grayscale(1); opacity: .34; }
.bdg--locked.bdg-art--eventhorizon .bdg__art::before,
.bdg--locked.bdg-art--eventhorizon .bdg__art::after { filter: grayscale(1); opacity: .4; animation: none; }
.bdg--locked .bdg__mark--freeclimber { filter: grayscale(1); opacity: .45; }

/* Motion off, and the tile is still the badge: a black hole, a lit disc, and
   somebody hanging above it. The whole sentence survives, because the one thing
   that was never moving is the one thing the badge is about. */
@media (prefers-reduced-motion: reduce) {
  .bdg-art--eventhorizon .bdg__art::before,
  .bdg-art--eventhorizon .bdg__art::after { animation: none; }
}

/* ---- batcave: the underside of the overhang, and who is left on it ----------
 *
 * Drawn 29 August 2026 on the badges Artifact, beside the sundown and noon
 * badges, and ported into this sheet with them; gym/badges.py names it as
 * art="batcave" (one of gym/wall_clears.py's per-wall drawings).
 *
 * THIS ONE WAS NOT LIKE THE OTHER TWO, and the difference is the whole reason it
 * was worth drawing. When it was drawn, "Night owl" and "Midday" had no engine
 * at all — no rung table, no id, nothing that computed (both have one now).
 * This badge's engine had been in main for weeks: `cleared_the_wall`,
 * Tier.ROCK, Anim.SHEEN, held back with the other sixteen, and wearing a broom
 * emoji. So what is below was not a proposal for a badge. It is a FACE for one
 * that already existed, and a name for it on the one wall that could carry a
 * joke.
 *
 * EVERY OTHER FULL-FACE DRAWING IN THIS SHEET IS LIT FROM ABOVE. The sunrise,
 * the sunset, the noon sky — three badges whose light source is a sky, hitting
 * the top edge of everything underneath it. This is the first drawing in the
 * set whose light comes from the FLOOR, and that single inversion does more
 * work than any shape in it:
 *
 *   the roof recedes UPWARD into black, away from the light
 *   the lit edge is on the UNDERSIDE of the lip, not the top of it
 *   the holds have a bright cap BELOW them and a dark body above
 *   the bats are lit along their heads and dark at their feet
 *
 * Which is not a stylistic choice — it is the only honest way to draw the
 * inside of a bouldering gym. There is no sky in this building. The brightest
 * thing in the room is the mat, and it is orange, and it is the gym's own
 * accent colour rather than a colour picked for a badge.
 *
 * IT REUSES THE BAT HANG'S SILHOUETTE VERBATIM. The 25-point polygon below is
 * copied character for character out of `.bdg__mark--bathang` in the sheet
 * above, at three sizes. That is deliberate and it is the cheap half of the
 * drawing: that shape took four goes (a squid, a scarecrow, a mushroom, then a
 * bat) and the argument for it is already written down thirty lines up. Drawing
 * a SECOND bat here would have produced a second-best bat and quietly broken
 * the one thing that makes these two badges read as a pair.
 *
 * WHAT SEPARATES IT FROM BAT HANG IS THE COUNT, not the animal. One bat on one
 * moulded rail in an empty dark field is a CLIMB — the grade 5 Green with the
 * upside-down start. Three bats on three holds under a roof is a WALL, and the
 * wall is the badge. At 56px you cannot read a caption but you can count to
 * three, so the two are told apart by arithmetic before anybody reads a word.
 *
 * AND THE THREE ARE HANGING OFF THE HOLDS, which is the entire joke made
 * structural rather than left in the name. You cleared the wall: every problem
 * on it is done, so the only things still hanging on that plastic are the bats.
 * A bat on the rock beside a hold would be a cave with wildlife in it. A bat on
 * the hold is the punchline, and it costs nothing — the holds had to be drawn
 * anyway or the roof is a cave rather than a climbing wall.
 * -------------------------------------------------------------------------- */

.bdg-art--batcave {
  /* One knob for the crossing. It moves ONCE, on the reveal — see the spotlight
     block near the bottom, which is the only place in this drawing where a
     duration is allowed to change. */
  --fly-dur: 14s;
}

/* The cave. Painted on .bdg__face::before for the reason the three skies above
   already establish: the face's own background is where the TIER lives, and a
   cave written into it would flatten the rock glow, the gym-history plate and
   the foil into one dark hole. ::before belongs to `aurora`, and a wall badge
   is never aurora.

   PINNED IN PX FROM THE CENTRE, not in percent — the sundown ruling, inherited
   without argument. --badge-size is 56px on a phone, 68px at 700px and 78px at
   1080px, and every mark in this sheet is fixed px, so a roof line at "41%"
   is a roof line that slides up the drawing at each breakpoint while the bats
   bolted to it do not move at all. */
.bdg-art--batcave .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;      /* circle tiers; the hex tiers clip-path anyway */
  pointer-events: none;
  background:
    /* The mat. #f97316 is not a colour chosen for this badge — it is
       --clr-accent out of console.css, the orange on every primary button in
       the building. The one light source in the drawing is the gym itself, and
       using its own colour for it is the difference between a cave with a
       lamp in it and a cave inside THIS gym. */
    radial-gradient(25px 9px at 50% calc(50% + 22px),
      rgba(249,115,22,.40) 0 32%,
      rgba(249,115,22,.17) 64%,
      rgba(249,115,22,0)  100%),
    /* Its bounce, climbing the far dark. Wide and weak: light off a mat is the
       softest source there is, and a hard edge here would read as a spotlight
       aimed up at the roof, which is a stage and not a gym. */
    radial-gradient(33px 24px at 50% calc(50% + 18px),
      rgba(255,178,104,.18) 0 28%,
      rgba(255,146,74,.07)  66%,
      rgba(255,146,74,0)   100%),
    /* The air. Cold at the roof, warming all the way down — nine stops rather
       than three, because a cave done in three is a black-to-brown fade and
       every cave in every clipart pack is a black-to-brown fade. The bands
       that make it a room are the two in the middle, where the mat light stops
       reaching and the stone takes over. */
    linear-gradient(to bottom,
      #05070c  0%,
      #070a11 14%,
      #0b0f19 29%,
      #10141f 42%,
      #171825 54%,
      #201d27 65%,
      #2a2228 75%,
      #332622 86%,
      #392a20 100%);
}

/* The roof, the lit lip, and the three holds screwed to its underside.
 *
 * THE ROOF RECEDES UPWARD, which is the band-7 ruling turned over. That ruling
 * says a featureless black area is not a dark thing, it is a hole in the badge,
 * and it was won for a wall going DOWN into shadow. A roof goes the other way:
 * the nearest part of it is the lip, right above your head and catching the
 * mat; the far part is deep in the cave and gets nothing. So the gradient runs
 * from near-black at the top of the tile to the one hot line at the lip, and
 * the three stops of falloff are what put a ceiling back where a flat fill
 * would leave a bite taken out of the disc.
 *
 * THE HOLDS ARE THE BATHOLD'S RULING, INVERTED. That hold is a lit cap over a
 * dark body because the room is above it. These have a lit cap UNDER a dark
 * body because the room is below them, and two pixels of difference between
 * "lit on top" and "lit underneath" is the whole read at this size. Grey
 * plastic on purpose, and the same greys: the bats are hanging off exactly the
 * same holds everybody else is pulling on. Three, evenly spaced, because a
 * cleared wall is a wall and a wall is more than one problem. */
.bdg-art--batcave .bdg__art {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  transform: none;
  background:
    /* left hold — the lit underside, then the body above it */
    radial-gradient(3.2px 0.8px at calc(50% - 15px) calc(50% - 8.1px),
      rgba(255,196,140,.56) 0 74%, rgba(255,196,140,0) 100%),
    radial-gradient(3.4px 1.5px at calc(50% - 15px) calc(50% - 9.4px),
      #414757 0 80%, rgba(65,71,87,0) 100%),
    /* centre hold — the big one, because the big bat hangs off it */
    radial-gradient(4.0px 0.9px at 50% calc(50% - 8px),
      rgba(255,202,148,.62) 0 74%, rgba(255,202,148,0) 100%),
    radial-gradient(4.2px 1.8px at 50% calc(50% - 9.5px),
      #4e5566 0 80%, rgba(78,85,102,0) 100%),
    /* right hold */
    radial-gradient(3.2px 0.8px at calc(50% + 15px) calc(50% - 8.1px),
      rgba(255,196,140,.56) 0 74%, rgba(255,196,140,0) 100%),
    radial-gradient(3.4px 1.5px at calc(50% + 15px) calc(50% - 9.4px),
      #414757 0 80%, rgba(65,71,87,0) 100%),
    /* the roof itself, and the one hot line where the mat light stops */
    linear-gradient(to bottom,
      #090d17               0px,
      #0c1120               calc(50% - 22px),
      #121828               calc(50% - 16px),
      #1d2438               calc(50% - 11.6px),
      rgba(255,186,122,.66) calc(50% - 10.7px),
      rgba(255,186,122,.14) calc(50% - 9.9px),
      rgba(0,0,0,0)         calc(50% - 9.5px));
}

/* ---- the colony ------------------------------------------------------------
 *
 * FOUR BATS ON THREE ELEMENTS' WORTH OF PSEUDOS, and the allocation is forced
 * rather than chosen, so it is written down:
 *
 *   .bdg__art::before   the one that let go        (z-index 1, behind)
 *   .bdg__art::after    left bat, small            (z-index 1)
 *   .bdg__mark--colony  the container. No clip-path — a clipped parent would
 *                       clip the flyer down to a bat-shaped keyhole and the
 *                       crossing would never be visible.
 *   ::before            centre bat, large, the only one with eyes
 *   ::after             right bat, small
 *
 * The consequence is a real one and it happens to be right: the three hanging
 * bats sit on TWO different stacking layers, and the flyer is under all of
 * them. Which is the depth cue this drawing would have had to invent anyway —
 * the colony is on the near roof, the one in the air is further into the cave.
 * The layer budget drew it for free.
 *
 * ONLY THE CENTRE BAT HAS EYES, and that is the drawing's one real decision.
 * Two amber dots 2.2px across are the entire difference between a bat and a
 * moth (the bathang note, and it is still true). But THREE pairs of them in a
 * 56px circle is not a colony, it is a face — the eye finds three pairs of eyes
 * before it finds anything else in a picture, and the tile stops being a cave
 * and becomes a crowd looking at you. One animal you can see the eyes of and
 * two you can only see the shape of is what a colony in the dark actually looks
 * like, and it is the reading a member gets in the half second they give it.
 * -------------------------------------------------------------------------- */

/* The shared silhouette. Copied out of `.bdg__mark--bathang` above, unchanged:
   clockwise from the hooked feet — shoulder, leading edge, wingtip, back along
   the scalloped trailing edge to the body, then the head at the BOTTOM with its
   two ears, because the whole animal is upside down. */
.bdg-art--batcave .bdg__art::after,
.bdg__mark--colony::before,
.bdg__mark--colony::after {
  content: "";
  position: absolute;
  clip-path: polygon(
    46% 0%, 54% 0%,
    58% 12%, 76% 26%, 100% 50%,
    84% 52%, 90% 70%, 70% 56%, 76% 78%, 60% 54%,
    58% 68%, 64% 88%, 54% 84%, 50% 100%, 46% 84%, 36% 88%, 42% 68%,
    40% 54%, 24% 78%, 30% 56%, 10% 70%, 16% 52%,
    0% 50%, 24% 26%, 42% 12%
  );
  /* It swings from where it is holding on. The hold is painted into .bdg__art's
     background, so it stays bolted to the roof while the animal moves — the
     bathold's ruling, and the reason that badge separated art from mark in the
     first place. */
  transform-origin: 50% 3%;
}

/* The two on the outside. Darker than the centre one and lit less: they are
   further from the middle of the mat, and a colony where every animal is the
   same brightness is a row of stickers. Violet-black rather than black, so
   they read as family with the Bat Hang and not as three holes punched in the
   tile — but lit along the BOTTOM, because in this room the light is under
   them. Same animal, opposite shading, which is how you say "same bat,
   different wall" without a caption. */
.bdg-art--batcave .bdg__art::after,
.bdg__mark--colony::after {
  width: 13px;
  height: 11px;
  top: calc(50% - 8px);
  background: linear-gradient(to bottom,
    #1a1529 0 34%, #292242 34% 60%, #3b3260 60% 84%, #53457e 84%);
  filter:
    drop-shadow(0 1px 0 rgba(196,181,253,.34))
    drop-shadow(0 -2px 2px rgba(0,0,0,.6));
}
.bdg-art--batcave .bdg__art::after { left: calc(50% - 21.5px); }
.bdg__mark--colony::after          { left: calc(50% + 8.5px); }

/* The container. Nothing is drawn on it — it exists so the two pseudos above
   and below have something to be absolute inside, and so the colony can be
   greyed as one thing on a locked tile. */
.bdg__mark--colony {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* The centre bat. Bigger, closer, and the only one carrying a face.
   The eyes are two stops in its OWN background rather than a pseudo-element,
   which is what buys the fourth bat: the clip-path keeps them inside the head
   for free, so a pair of eyes costs two gradient layers instead of the last
   element in the budget.

   THEY ARE 1.7px AND 2.3px APART, and those are not taste, they are the
   silhouette's own limit. Down the shared polygon the head is at the BOTTOM and
   it tapers: at the eye line it is a shade over four pixels across. A pair of
   the Bat Hang's 2.2px eyes at its spacing does not fit inside that — it was
   drawn on a 38px animal and this one is 20px — and the clip-path does not warn
   you, it just quietly takes the outer half of each eye off and leaves the bat
   winking. Found by looking at the rendered pixel, which is the only place it
   is visible. The owl paid a pseudo per eyelid because its lids had
   to move independently; these do not blink at all — blinking is spoken for,
   it is the night owl's whole animation, and a second badge that blinks would
   turn a signature into a house style. */
.bdg__mark--colony::before {
  left: calc(50% - 10px);
  top: calc(50% - 6.6px);
  width: 20px;
  height: 17px;
  background:
    radial-gradient(circle closest-side, #fcd34d 0 60%, rgba(0,0,0,0) 60%)
      calc(50% - 1.15px) 77% / 1.7px 1.7px no-repeat,
    radial-gradient(circle closest-side, #fcd34d 0 60%, rgba(0,0,0,0) 60%)
      calc(50% + 1.15px) 77% / 1.7px 1.7px no-repeat,
    linear-gradient(to bottom,
      #2a2340 0 32%, #3d3460 32% 58%, #574a7c 58% 82%, #7f6cae 82%);
  filter:
    drop-shadow(0 1px 0 rgba(196,181,253,.5))
    drop-shadow(0 -2px 3px rgba(0,0,0,.7));
}

/* ---- the one that let go ---------------------------------------------------
 *
 * A bat crossing the cave, in silhouette, out where the mat light does not
 * reach it. Drawn flat black-violet with no interior at all — it is 13px wide
 * and moving, and anything painted inside it at that size is mud.
 *
 * IT IS NOT A SECOND ANIMATION. The wingbeat is stops in the SAME keyframes as
 * the crossing, which is the one genuinely clever thing in this drawing and it
 * is clever for a boring reason: `transform` cannot change a shape, and a wing
 * is the only thing on this badge that has to. So the crossing animates
 * `transform` and the wings animate `clip-path`, and both live in one rule that
 * is idle for the thirteen seconds the bat is not there. Two animations would
 * have had the browser interpolating a polygon all day for an element at
 * opacity 0.
 *
 * IT FLIES IN SCALLOPS, NOT IN A LINE, and that is the only thing about it
 * anybody will consciously recognise. A bird crosses a window on a smooth arc;
 * a bat flickers — up on the downstroke, dropping between beats — which is why
 * people who cannot name one bat can still tell you there was a bat in the
 * room. The y value alternates 4, 1, 4, 0, 4, 0, 4 across the crossing and it
 * is doing more for the read than the silhouette is.
 * -------------------------------------------------------------------------- */
.bdg-art--batcave .bdg__art::before {
  content: "";
  position: absolute;
  left: calc(50% - 6px);
  top: calc(50% + 12px);
  width: 12px;
  height: 7px;
  background: linear-gradient(to bottom, #241d38 0 60%, #171227 100%);
  /* Wings down — the resting frame, and the one the crossing starts and ends
     on. Eighteen points, matched one for one by the up frame in the keyframes,
     because clip-path only interpolates between polygons of equal length. */
  clip-path: polygon(
    50% 0%, 57% 18%, 72% 10%, 88% 26%, 100% 52%,
    82% 48%, 88% 66%, 66% 50%, 58% 60%,
    50% 74%, 42% 60%, 34% 50%, 12% 66%, 18% 48%,
    0% 52%, 12% 26%, 28% 10%, 43% 18%
  );
  filter: drop-shadow(0 0 2px rgba(0,0,0,.7));
  /* NOT THERE unless the tile is a moving one, and this is the declaration that
     makes reduced motion honest rather than eerie: `animation: none` returns an
     element to what is written here, so a member who has asked for less
     movement gets a cave with three bats hanging in it — not a bat frozen
     mid-air in the middle of the room, which is a bat that has been shot. */
  opacity: 0;
}

/* ---- .bdg-art--batcave: the moving copy -----------------------------------------
 *
 * THE CAVE DOES NOT MOVE. No pan, no drift, no light coming up. That is the
 * sunrise's ruling, inherited by the sunset and the noon sky and now by this:
 * a scene you can watch change is a time-lapse, and a time-lapse on a 56px
 * medallion is a loading spinner with a picture on it. Everything below moves
 * inside a room that is completely still.
 *
 * TWO GESTURES, NOT TWO ELEMENTS. The colony breathes and one bat crosses.
 * The breathing is three bats on 4.9s, 6.7s and 5.3s — no shared factor, so
 * they drift through every phase against each other and the roof never settles
 * into a beat. Three on 5s would be a mobile hanging over a cot.
 *
 * THE BIG ONE SWINGS LESS, and it is the only new keyframe in the whole
 * drawing. The two small bats reuse `bdg-batsway` out of the shipped sheet
 * verbatim, 9 degrees over their own durations; the centre one gets 6 because
 * it is bigger and a heavier animal on the same hold swings shallower. Give
 * all three the same arc and three separate durations still read as one
 * mechanism with a fault in it.
 *
 * THE CROSSING IS 12% OF THE CYCLE. The bat is in frame for 1.9 seconds out of
 * 14 — against the owl's blink at 4.8% and the sheen's 18% — so what a member
 * gets on a wall of twenty tiles is a cave with bats hanging in it, and, once,
 * something going across it. That is the opposite of a strobe. It is the thing
 * that makes them look back at the badge, which is the only reason any of this
 * moves at all.
 * -------------------------------------------------------------------------- */
.bdg-art--batcave .bdg__art::after { animation: bdg-batsway 4.9s ease-in-out infinite; }
.bdg-art--batcave .bdg__mark--colony::before        { animation: bdg-colonysway 6.7s ease-in-out infinite; }
.bdg-art--batcave .bdg__mark--colony::after         { animation: bdg-batsway 5.3s ease-in-out infinite -1.7s; }
.bdg-art--batcave .bdg__art::before {
  animation: bdg-batcross var(--fly-dur) linear infinite;
}

/* Shallower than `bdg-batsway`'s 4.5, and that is the whole difference. */
@keyframes bdg-colonysway {
  0%, 100% { transform: rotate(-3deg); }
  50%      { transform: rotate(3deg); }
}

/* One crossing, three wingbeats, and eighty-five per cent of the cycle spent
   off the left of the tile doing nothing. The rotate goes from -7deg to 8deg
   across the room because an animal that crosses a frame at a fixed attitude is
   a cursor. */
@keyframes bdg-batcross {
  0%, 85% {
    opacity: 0;
    transform: translate(-30px, 4px) rotate(-7deg);
    clip-path: polygon(
      50% 0%, 57% 18%, 72% 10%, 88% 26%, 100% 52%,
      82% 48%, 88% 66%, 66% 50%, 58% 60%,
      50% 74%, 42% 60%, 34% 50%, 12% 66%, 18% 48%,
      0% 52%, 12% 26%, 28% 10%, 43% 18%);
  }
  86.5% {
    opacity: .85;
    transform: translate(-22px, 1px) rotate(-5deg);
    clip-path: polygon(
      50% 0%, 57% 18%, 72% 4%, 90% 8%, 100% 26%,
      80% 30%, 86% 48%, 66% 40%, 58% 56%,
      50% 70%, 42% 56%, 34% 40%, 14% 48%, 20% 30%,
      0% 26%, 10% 8%, 28% 4%, 43% 18%);
  }
  88.5% {
    opacity: .95;
    transform: translate(-13px, 4px) rotate(-2deg);
    clip-path: polygon(
      50% 0%, 57% 18%, 72% 10%, 88% 26%, 100% 52%,
      82% 48%, 88% 66%, 66% 50%, 58% 60%,
      50% 74%, 42% 60%, 34% 50%, 12% 66%, 18% 48%,
      0% 52%, 12% 26%, 28% 10%, 43% 18%);
  }
  90.5% {
    opacity: .95;
    transform: translate(-4px, 0px) rotate(1deg);
    clip-path: polygon(
      50% 0%, 57% 18%, 72% 4%, 90% 8%, 100% 26%,
      80% 30%, 86% 48%, 66% 40%, 58% 56%,
      50% 70%, 42% 56%, 34% 40%, 14% 48%, 20% 30%,
      0% 26%, 10% 8%, 28% 4%, 43% 18%);
  }
  92.5% {
    opacity: .95;
    transform: translate(6px, 4px) rotate(3deg);
    clip-path: polygon(
      50% 0%, 57% 18%, 72% 10%, 88% 26%, 100% 52%,
      82% 48%, 88% 66%, 66% 50%, 58% 60%,
      50% 74%, 42% 60%, 34% 50%, 12% 66%, 18% 48%,
      0% 52%, 12% 26%, 28% 10%, 43% 18%);
  }
  94.5% {
    opacity: .9;
    transform: translate(15px, 0px) rotate(5deg);
    clip-path: polygon(
      50% 0%, 57% 18%, 72% 4%, 90% 8%, 100% 26%,
      80% 30%, 86% 48%, 66% 40%, 58% 56%,
      50% 70%, 42% 56%, 34% 40%, 14% 48%, 20% 30%,
      0% 26%, 10% 8%, 28% 4%, 43% 18%);
  }
  96.5% {
    opacity: .62;
    transform: translate(24px, 4px) rotate(7deg);
    clip-path: polygon(
      50% 0%, 57% 18%, 72% 10%, 88% 26%, 100% 52%,
      82% 48%, 88% 66%, 66% 50%, 58% 60%,
      50% 74%, 42% 60%, 34% 50%, 12% 66%, 18% 48%,
      0% 52%, 12% 26%, 28% 10%, 43% 18%);
  }
  98.5%, 100% {
    opacity: 0;
    transform: translate(32px, 1px) rotate(8deg);
    clip-path: polygon(
      50% 0%, 57% 18%, 72% 4%, 90% 8%, 100% 26%,
      80% 30%, 86% 48%, 66% 40%, 58% 56%,
      50% 70%, 42% 56%, 34% 40%, 14% 48%, 20% 30%,
      0% 26%, 10% 8%, 28% 4%, 43% 18%);
  }
}

/* Winning the spotlight turns the light UP, never on — the house rule, and the
   fourth badge in a row to inherit it. The mat burns a shade hotter and the
   centre bat catches more of it. The roof does not move and the colony does not
   speed up, for the reason the sun does not sink: a badge whose picture depends
   on which tile the server happened to pick this week is two different badges.

   ONE EXCEPTION, AND IT IS DELIBERATE. --fly-dur drops from 14s to 5.2s. The
   reveal is the single context in this system where the member is GUARANTEED to
   be looking at one tile — they tapped a sealed badge to break it open, and the
   whole point of that ceremony is that they watch it. A crossing tuned to be
   rare on a wall of twenty is a crossing that does not happen inside the four
   seconds somebody is staring at this one. So the rarity moves and nothing else
   does: it changes WHEN, not WHAT, which is the line the still roof is drawn
   on. The wallstrip badge already won this argument for itself with
   `--strip-dur: 6s` on exactly the same two selectors, and this is that ruling
   applied a second time rather than a new one. */
.bdg--spotlight.bdg-art--batcave,
.bdg-reveal__badge .bdg-art--batcave { --fly-dur: 5.2s; }

.bdg--spotlight.bdg-art--batcave .bdg__face::before,
.bdg-reveal__badge .bdg-art--batcave .bdg__face::before {
  filter: saturate(1.18) brightness(1.1);
}
.bdg--spotlight .bdg__mark--colony::before,
.bdg-reveal__badge .bdg__mark--colony::before {
  filter:
    drop-shadow(0 1px 0 rgba(196,181,253,.66))
    drop-shadow(0 -2px 3px rgba(0,0,0,.7))
    brightness(1.12);
}

/* Locked. The cave is the reward, so the cave is what a locked tile does not
   get: drained and knocked back to almost nothing, which leaves the recessed
   dark face the locked treatment is supposed to be. The roof and the colony go
   with it — the sheet greys `.bdg__icon` on a locked tile and a drawn mark is
   an icon by another name, so three full-colour bats with lit eyes on a dashed
   tile would be the one thing in the drawing that had not noticed it was not
   earned yet. The flyer needs no rule at all: it is opacity 0 until .bdg-art--batcave
   turns it on, and a locked tile is never a roosting one. */
.bdg--locked.bdg-art--batcave .bdg__face::before {
  filter: grayscale(1);
  opacity: .16;
}
.bdg--locked.bdg-art--batcave .bdg__art { filter: grayscale(1); opacity: .38; }
.bdg--locked .bdg__mark--colony { filter: grayscale(1); opacity: .45; }
.bdg--locked.bdg-art--batcave .bdg__art::after,
.bdg--locked .bdg__mark--colony::before,
.bdg--locked .bdg__mark--colony::after { animation: none; }

/* Foil. This cannot currently happen — shiny is proof you took the hard way up
   a NAMED climb, and a wall is not a climb — and the rule is here anyway, at
   one line, because the sheet must never be the thing that breaks the day
   somebody disagrees. The cave yields to the rainbow rather than fighting it,
   at .70 rather than the rocket's .55, because on that badge the scenery is
   scenery and on this one the room is the badge. */
.bdg.bdg--shiny.bdg-art--batcave .bdg__face::before { opacity: .70; }

/* The still form. Three bats hanging straight, and NO fourth bat — the crossing
   is the one thing here that a member who asked for less movement should not be
   given a frozen frame of. `animation: none` returns every element to what is
   declared on it rather than to wherever a keyframe was cancelled, which is the
   whole reason the flyer's opacity: 0 is written on the element and not left to
   the 0% stop. */
@media (prefers-reduced-motion: reduce) {
  .bdg-art--batcave .bdg__art::before,
  .bdg-art--batcave .bdg__art::after,
  .bdg-art--batcave .bdg__mark--colony::before,
  .bdg-art--batcave .bdg__mark--colony::after { animation: none; }
}


/* ---- chalked: DELETED 31 August 2026 ----------------------------------------
 *
 * `bdg__mark--chalked` drew the generic wall clear — a sheet of ply with eight
 * holds on it, every one covered in chalk. The drawing was fine and the BADGE
 * is gone: `cleared_the_wall` awarded the same sentence about a different wall
 * every time ("you've cleared Slab Bay, all 9"), which is a template with a
 * wall dropped into it. Only the two walls with a joke in their name earn a
 * clear now — see gym/wall_clears.py, which says so at length.
 *
 * Recorded here rather than left as a gap, for this sheet's usual reason: a
 * gap is unreadable, and the next person cannot tell a removal from an
 * omission. The one idea in it worth keeping is written down where it was
 * learned — chalk sits UP AND LEFT of a hold, on the edge a hand pulls, and
 * chalk centred on a hold draws a white pebble.
 * -------------------------------------------------------------------------- */

/* ===========================================================================
 * TWO BADGES ABOUT WHEN YOU CLIMB — ported from the badges Artifact, 2026-08-30.
 *
 * Night owl and Midday were the Artifact's "fourth state": drawn, argued at
 * length, and with no code behind them at all — no catalogue entry, no rung
 * table, no test, and nothing at the desk they could be switched on with. They
 * have all of that now, and they arrive switched off like every other badge
 * that has never been released.
 *
 * They are the dusk and noon twins of "Doors open", and the three drawings had
 * to be pulled apart ON PURPOSE, because all three are a sun at a horizon and
 * at 56px that is one picture. The separation is STRUCTURAL rather than in the
 * captions: doors open is light ARRIVING in an empty dark field — rays, a lit
 * line, nothing else; the owl is a full sky with black shapes in front of it
 * and no rays at all; noon is a small hard sun a long way up with the whole
 * middle of the frame deliberately left empty.
 * =========================================================================== */

/* ---- sundown: the sky, and the lip it sets behind ---------------------------
 *
 * The first mark in this set that is NOT an object in the middle of a dark
 * face. It is a WINDOW: the whole face is sky, and everything else in the
 * drawing is a black shape in front of it.
 *
 * That is not a flourish, it is the only honest way to tell this badge apart
 * from "Doors open" at 56px. Both are a sun at a horizon, and drawn the house
 * way — a small warm object floating in the tier's dark face — they are the
 * same picture. So the two are separated STRUCTURALLY rather than by their
 * captions.
 *
 * THIS PARAGRAPH ORIGINALLY SAID that the separation was empty field versus
 * full sky, and it is rewritten rather than left standing because "Doors open"
 * has since been given a sky of its own (`firstlight`, above). Half the
 * separation was spent. What replaced it is better, and it is written down here
 * so the next person to reach for a sky knows what the two are actually doing:
 *
 *   Doors open  — a COLD sky. Indigo down through blue to one narrow warm band
 *                 at the horizon. Seven rays. Sun ABOVE the line and rising.
 *                 Nothing in the foreground at all.
 *   Night owl   — a WARM sky. Plum, rose and orange from the zenith down. No
 *                 rays. Sun BEHIND the line and going. A wall, a hold, a bird
 *                 and a star in front of it.
 *
 * Cold vs warm, rising vs setting, rays vs none, empty foreground vs built one.
 * Four differences where there were three, and the two that carry it at 56px —
 * the colour of the sky and the direction of the sun — are the two you can see
 * before you can see anything else. Nobody has to be told which is which, and
 * no caption is carrying the read.
 *
 * PINNED IN PX FROM THE FACE'S CENTRE, not in percent. --badge-size is 56px on
 * a phone, 68px at 700px and 78px at 1080px, and the marks are all fixed px —
 * so a horizon at "67%" is a horizon that slides three pixels up the drawing
 * every breakpoint while the owl standing on it does not move at all. Every
 * offset below is therefore calc(50% + Npx): the sun, the sun's glow, the lip
 * and the bird are measured from one point and cannot come apart.
 * -------------------------------------------------------------------------- */

/* The sky. Painted on .bdg__face::before for the rocket's reason, restated:
   the face's own background is where the TIER lives, and a sky written into it
   would flatten the rock glow, the gym-history plate and the foil into one.
   ::before is free — it belongs to `aurora`, and a habit badge is never
   aurora. */
.bdg-art--sundown .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;      /* circle tiers; the hex tiers clip-path anyway */
  pointer-events: none;
  background:
    /* Two cloud bars, and they are the cheapest thing on this tile that makes
       it read as an evening rather than as a gradient. Lit from BELOW and in
       the right order: the high one is pink because the sun is under it, the
       low one is dark because it is between you and the light. Reverse the two
       and the sky is instantly wrong in a way nobody can name. */
    radial-gradient(11px 1.1px at calc(50% + 5px) calc(50% - 9px),
      rgba(228,146,152,.32) 0 52%, rgba(228,146,152,0) 100%),
    radial-gradient(22px 1.6px at calc(50% - 9px) calc(50% - 1px),
      rgba(78,24,58,.55) 0 55%, rgba(78,24,58,0) 100%),
    /* The last sliver of the disc. Centred 13px below the face's middle with a
       7px radius, so 3.5px of it clears the lip and the rest is behind the
       wall. A whole sun sitting on a horizon is a sunset in a clipart pack; a
       sun three-quarters gone is the four minutes this badge is named after. */
    radial-gradient(circle 6.5px at calc(50% + 18px) calc(50% + 13px),
      #fffbee 0 74%, rgba(255,248,226,.35) 88%, rgba(255,248,226,0) 100%),
    /* Its glow, wider than it is tall because that is what haze at a horizon
       does to a light source. Sat 18px right of centre, which is exactly the
       owl's right edge — the bird is beside the sun, never in front of it.
       An owl eclipsing the sun is a different and much worse badge. */
    radial-gradient(30px 15px at calc(50% + 18px) calc(50% + 9px),
      rgba(255,248,224,.95) 0 8%,
      rgba(255,214,132,.80) 30%,
      rgba(250,150,80,.42)  56%,
      rgba(216,90,90,0)     100%),
    /* The ramp. Twelve stops rather than four: a sunset done in four is a
       purple-to-orange fade, which is a phone wallpaper. The bands that make
       it weather are the narrow ones — the plum at 45% and the rose at 53% —
       and they are narrow because that is how far up the sky the colour
       actually gets. It peaks at 64% and falls away below, because everything
       under 62–67% is behind the wall and a gradient that keeps brightening
       into a shape you cannot see is paint spent on nothing. */
    linear-gradient(to bottom,
      #090d2a  0%,
      #131a46 13%,
      #2a1b58 26%,
      #542166 36%,
      #8b2b5d 45%,
      #bc4050 53%,
      #e2683d 59%,
      #f89a3a 64%,
      #ffc768 69%,
      #ffe0a0 76%,
      #d9975f 88%,
      #6b3f44 100%);
  /* The engraving, and it lives HERE for a reason worth writing down: the
     sheet's `.bdg__face--engraved` is an inset box-shadow on the face, and an
     inset shadow paints under the face's own children — so on a tile whose
     ::before covers the face wall to wall, the engraving is drawn and then
     immediately buried. The class was on this badge for an afternoon doing
     absolutely nothing. Same family as the specificity trap that ate the grade
     medallions: it passes every assertion either way, and the only way to find
     it is to look at the rendered pixel. */
  box-shadow:
    inset 0  1px 0 rgba(255,255,255,.14),
    inset 0 -1px 0 rgba(0,0,0,.5);
}

/* The wall, and it is the horizon. There is no horizon line in the sky layer
 * at all — the top-out lip IS the line, which is both simpler and true: from
 * the mats there is no sky in this building, and the badge is not pretending
 * otherwise. It is what is happening OUTSIDE while you are in here, drawn on
 * the one edge of the gym that faces it.
 *
 * Three bands and not one flat black bar, because the bands are the difference
 * between a wall and a hill:
 *   1px  the lit edge      — the last light landing on the top-out
 *   2px  the top face      — plum, in the light but turned away from it
 *   rest the wall face     — flat, vertical, in shadow. A built thing.
 * A hill has no second band. That is the entire read, and it costs two stops.
 *
 * Full-bleed rather than a centred bar, so the lip reaches the rim at every
 * face size and in both silhouettes — a 34px lip inside a 78px hexagon is a
 * plank floating in the sky. */
.bdg-art--sundown .bdg__art {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  transform: none;
  background:
    /* One hold, screwed to the wall just under the lip. ONE, and the count is
       the argument again: two is a pair, three is a route, and this badge is
       not about a route. It is the smallest possible thing that turns a dark
       mass into a CLIMBING wall, and it is drawn the way the bolt holes are —
       a lit cap above a dark body, because two pixels of difference between
       "lit on top" and "lit underneath" is the whole read at this size. */
    radial-gradient(3.1px 1.1px at calc(50% - 12px) calc(50% + 14.9px),
      rgba(222,147,116,.55) 0 70%, rgba(222,147,116,0) 100%),
    radial-gradient(3.2px 2px at calc(50% - 12px) calc(50% + 16.1px),
      #1e2033 0 74%, rgba(30,32,51,0) 100%),
    linear-gradient(to bottom,
      rgba(0,0,0,0)          0    calc(50% + 9.5px),
      rgba(255,201,138,.62)  calc(50% + 9.5px)  calc(50% + 10.5px),
      rgba(126,64,74,.92)    calc(50% + 10.5px) calc(50% + 12.5px),
      /* The face of the wall, and it RECEDES rather than being one flat black.
         The band-7 ruling, applied to a shape instead of to a medallion: a
         featureless black area is not a wall in shadow, it is a hole in the
         badge. Three stops of falloff is all it takes to put a surface back. */
      #16182a                calc(50% + 12.5px),
      #0a0c16                calc(50% + 22px),
      #05060d                100%);
}

/* The first star. ONE, and the count is the whole argument: a field of stars
 * over a sun that is still in the sky is a contradiction the drawing cannot
 * survive, and one point of light is the oldest way there is of saying dusk.
 * (The orbit ruling, borrowed: a single point says "one".)
 *
 * Deliberately NOT the four-pointed twinkle .bdg__spark uses. That shape is
 * spoken for — it means SHINY everywhere else in this system, and a member who
 * has one would read a sparkle here as a foil they had not earned. A soft
 * round point cannot be confused with it. */
.bdg-art--sundown .bdg__art::before {
  content: "";
  position: absolute;
  left: 30%;
  top: 21%;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: radial-gradient(circle,
    #fff8e2 0 28%, rgba(255,246,220,.55) 52%, rgba(255,246,220,0) 100%);
  /* The still form. The breath below swings around this, so a tile with motion
     turned off is not a tile with the star turned off. */
  opacity: .78;
}

/* ---- nightowl: the bird on the lip -----------------------------------------
 *
 * "Night owl" is the most obvious two words in the English language for this
 * badge, and that is the point rather than a lapse. The sheet's rule has never
 * been that names must be clever — it is that DRAWINGS must be specific. The
 * birthday badge used to be this rule's worked example and is now its worked
 * COUNTER-example: it was a candle jammed into a climbing hold and it is a cake
 * standing on one, which is legible to everybody and specific to nobody. That
 * trade was made knowingly and is argued at length in the cake block; it does
 * not loosen the rule here. An owl floating in a circle is a badge for any app
 * in the world. An owl sitting on the top-out lip of a bouldering wall is a
 * badge for this gym, and the name is then free to be the two words a member
 * would use themselves.
 *
 * A SILHOUETTE, with no rim light, and there are two reasons — one physical and
 * one mechanical, and the physical one came first.
 *
 *   Physical: a bird against a lit sky at dusk is BLACK. The light goes round
 *   it, not on it. What warmth it has is bounce off the lit lip it is standing
 *   on, which is what the radial below is: hot at the feet, gone by the chest.
 *
 *   Mechanical: the halo could not have been drawn anyway. `filter` is applied
 *   BEFORE `clip-path` in the painting order, so a drop-shadow on this element
 *   is cast and then clipped away by the very silhouette that is supposed to be
 *   casting it. Written down because the mistake is invisible — no error, no
 *   warning, a shape that simply has no glow — and it is the second time this
 *   sheet has been bitten by a rule about paint order (see the sunrise's rays,
 *   which lost the same argument to z-index and had to become a mask).
 *
 * The backlight is real, though: the sun's glow is pinned 18px right of centre
 * and the bird is 11px wide either side of it, so the brightest part of the sky
 * sits directly against the owl's edge. The rim light is done by the SKY,
 * which is where it belongs.
 *
 * clip-path rather than stacked border-radius, because ear tufts are the one
 * feature that turns a dark blob into an owl and they cannot be got from a
 * corner radius at any price.
 *
 * Twenty-four points, and it took three goes. Nineteen was enough at 22px and
 * fell apart the moment the tile grew: the straight runs between points began
 * to show and the dome between the tufts collapsed into a V, which is not an
 * owl, it is a CAT.
 *
 * The cat came back a second time when the fix was a neck. Giving the head a
 * hard step in at the shoulders does separate it from the body — and what it
 * produces is a big head on a small torso, which is a gremlin. Owls have no
 * visible neck at all.
 *
 * The cat came back a THIRD time when the fix was a taper. Narrowing the
 * whole shape from the cheeks down does produce a bird — it also produces a
 * rounded triangle with two points on top, which is a cat's FACE, and a cat's
 * face is the one thing worse than a cat.
 *
 * What settled it was not the outline at all, it was the aspect ratio. A cat's
 * head is as wide as it is tall; a perched owl is a barrel standing up. At
 * 24×25 nothing could be done to the points that stopped it reading as a face.
 * At 21×26 — one axis shorter, one longer, the outline barely touched — it is
 * a whole bird, and the tufts stopped having to do all the work.
 *
 * None of which is visible at 56px, and all of which is why the pair below is
 * shown at 190% as well: the enlargement is not a luxury, it is the size the
 * drawing had to be checked at before it could be trusted at the small one.
 * -------------------------------------------------------------------------- */
.bdg__mark--nightowl {
  /* TALLER THAN IT IS WIDE, 21 by 26, and that ratio is the drawing. Every
     draft that came out square came out a cat: a cat's head is as wide as it
     is tall and its body is behind it, so a square silhouette with two points
     on top is a cat face however the outline is smoothed. A perched owl is a
     BARREL standing up. */
  width: 21px;
  height: 26px;
  /* Nudged up 2.5px off dead centre so the feet land ON the lip rather than
     3px behind it. The rocket already needed this lever and for the same
     reason: `place-items: center` centres a BOX, and none of these drawings
     are centred inside their own box. */
  margin-bottom: 5px;
  clip-path: polygon(
    17%   2%,                                   /* left tuft, tip */
    30%  17%,                                   /* its inner base */
    38%  13%, 50%  12%, 62%  13%,               /* the crown — BROAD and FLAT */
    70%  17%,
    83%   2%,                                   /* right tuft, tip */
    90%  16%, 95%  24%, 96%  33%,               /* widest at the cheeks */
    95%  46%, 93%  60%, 90%  74%,               /* the barrel, tapering gently */
    85%  86%, 78%  95%, 70% 100%,
    30% 100%,                                   /* the base: two feet, no more */
    22%  95%, 15%  86%, 10%  74%,
     7%  60%,  5%  46%,  4%  33%,
     5%  24%, 10%  16%
  );

  /* Bounce off the lit lip, and kept on a SHORT leash — the first draft ran
     the warmth a third of the way up the chest and the bird stopped being
     solid: a silhouette you can see through is smoke. It now dies out by the
     time it reaches the belly, which is as far as light bouncing off two
     pixels of stone would actually get. */
  background: radial-gradient(150% 76% at 50% 110%,
    rgba(255,168,92,.40)  0 2%,
    rgba(138,62,60,.45)  12%,
    rgba(34,21,40,.94)   30%,
    #05070f              52%);
}

/* The eyes, and they are the entire difference between an owl and a rock.
 *
 * Two ELEMENTS rather than one two-dot layer, which is the bathold's ruling
 * reused: the hold is art and the bat is a mark so the bat can swing without
 * dragging the hold. Here the owl is the mark and each eye owns a pseudo, so
 * they can blink 70ms apart. Both lids falling on the same frame is a shutter;
 * 70ms of stagger is an animal.
 *
 * Big — 4.6px on a 21px bird, most of its cheek each — because owls'
 * eyes are, and a proportionate dot is a sparrow. Amber rather than white: a
 * white pinprick on a black silhouette is a highlight, and two of them is a
 * skull. The clip-path on the parent clips these too, which is free insurance:
 * an eye can never escape the head however the numbers drift. */
.bdg__mark--nightowl::before,
.bdg__mark--nightowl::after {
  content: "";
  position: absolute;
  top: 22%;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  /* Off-centre highlight — the pupil looking slightly down the wall rather
     than dead at the viewer, which is the difference between a bird that has
     noticed you and one that is watching you. */
  background: radial-gradient(circle at 50% 40%,
    #fffaea 0 20%,
    #fcd34d 44%,
    #ef9a1e 70%,
    rgba(124,45,18,.9) 90%,
    rgba(124,45,18,0) 100%);
  transform-origin: 50% 45%;
}
.bdg__mark--nightowl::before { left:  15%; }
.bdg__mark--nightowl::after  { right: 15%; }

/* ---- nightfall: the copy that moves ----------------------------------------
 *
 * The same badge. Not a second, better one — one class, and in the catalogue it
 * would be one Anim value, the same lever every still badge already pulls by
 * carrying Anim.NONE. Which of the two ships is a catalogue decision and it has
 * not been taken; that is why both are on this page rather than one.
 *
 * THE SUN DOES NOT SINK, and that is the sunrise's ruling inherited whole
 * rather than re-argued: a sun you can watch move is a time-lapse, and a
 * time-lapse on a 56px medallion is a loading spinner with a face on it. It was
 * the obvious animation and it is the wrong one. Everything below moves around
 * a sky that is completely still.
 *
 * TWO THINGS MOVE, on 5.6s and 9.2s. That is the flame's trick — one animated
 * flame is a wobbling leaf, two reading against each other is fire — and the
 * durations share no factor, so the blink and the star drift through every
 * phase against each other and the tile never settles into a beat. A pair on
 * 5s and 10s would land together every ten seconds, and a tile that pulses on
 * the tens is a notification.
 *
 * The duty cycle is what buys the exception. The blink holds the lids down for
 * 0.27s of every 5.6s — 4.8%, against the sheen's 18% — so what a member sees
 * on a wall of twenty is a bird that blinked, once, a moment ago. That is the
 * opposite of a strobe: it is the thing that makes them look BACK.
 * -------------------------------------------------------------------------- */
.bdg-art--sundown .bdg__mark--nightowl::before {
  animation: bdg-owlblink 5.6s ease-in-out infinite;
}
.bdg-art--sundown .bdg__mark--nightowl::after {
  animation: bdg-owlblink 5.6s ease-in-out infinite .07s;
}
.bdg-art--sundown .bdg__art::before {
  animation: bdg-firststar 9.2s ease-in-out infinite;
}

/* One full blink and, half a cycle earlier, a half-lid that never closes.
   Evenly spaced blinks are a metronome — the same objection the flicker
   keyframes already answer for fire. The lid falls faster than it lifts (0.09s
   down, 0.10s up) because that is how eyelids work, and the 1.5% of the cycle
   spent fully shut is what stops it reading as a flinch. */
@keyframes bdg-owlblink {
  0%, 41%     { transform: scaleY(1); }
  42.4%       { transform: scaleY(.3); }
  43.6%, 93%  { transform: scaleY(1); }
  94.6%       { transform: scaleY(.06); }
  96%         { transform: scaleY(.06); }
  97.8%, 100% { transform: scaleY(1); }
}

/* The star coming up. Slow, shallow, and it never reaches zero — a star that
   switches off is a pixel fault, a star that swims is the atmosphere between
   you and it. Same ruling the holo won: a material does not switch on. */
@keyframes bdg-firststar {
  0%, 100% { opacity: .42; transform: scale(.9); }
  46%      { opacity: .98; transform: scale(1.12); }
}

/* Winning the spotlight turns the light UP, never on — the house rule, and the
   third badge in a row to inherit it unchanged. The sky burns a shade hotter,
   the star opens, the eyes catch more of it. The sun stays exactly where it
   is, for the same reason it does not sink: a badge whose picture depends on
   which tile the server happened to pick this week is two different badges. */
.bdg--spotlight.bdg-art--sundown .bdg__face::before,
.bdg-reveal__badge .bdg-art--sundown .bdg__face::before {
  filter: saturate(1.16) brightness(1.09);
}
.bdg--spotlight.bdg-art--sundown .bdg__art::before,
.bdg-reveal__badge .bdg-art--sundown .bdg__art::before {
  opacity: 1;
  box-shadow: 0 0 5px rgba(255,246,220,.7);
}
.bdg--spotlight .bdg__mark--nightowl::before,
.bdg--spotlight .bdg__mark--nightowl::after,
.bdg-reveal__badge .bdg__mark--nightowl::before,
.bdg-reveal__badge .bdg__mark--nightowl::after {
  box-shadow: 0 0 4px rgba(252,211,77,.65);
}

/* Locked. The sky is the reward, so the sky is what a locked tile does not get:
   knocked back to almost nothing and drained, which leaves the recessed dark
   face the locked treatment is supposed to be. The bird greys with it — the
   sheet greys `.bdg__icon` on a locked tile and a drawn mark is an icon by
   another name, so a full-colour owl with lit eyes on a dashed tile would be
   the one thing in the drawing that had not noticed it was not earned yet. */
.bdg--locked.bdg-art--sundown .bdg__face::before {
  filter: grayscale(1);
  opacity: .16;
}
.bdg--locked .bdg__mark--nightowl { filter: grayscale(1); opacity: .45; }
.bdg--locked .bdg__mark--nightowl::before,
.bdg--locked .bdg__mark--nightowl::after { animation: none; }

/* Foil. This cannot currently happen — shiny is proof you took the hard way up
   a named climb, and there is no hard way to climb after dark — and the rule is
   here anyway, at one line, because the sheet must never be the thing that
   breaks the day somebody disagrees. The sky yields to the rainbow rather than
   fighting it, which is the rocket's ruling; held at .72 and not the rocket's
   .55, because on that badge the sky is scenery and on this one the sky IS the
   badge. */
.bdg.bdg--shiny.bdg-art--sundown .bdg__face::before { opacity: .72; }

/* The still form. Eyes open, star at its own .78 — never eyes shut, and never
   a star that has faded out and stayed out. `animation: none` returns both to
   the values written above rather than to wherever a keyframe was cancelled,
   which is the whole reason the base opacity is declared on the element and
   not left to the 0% stop. */
@media (prefers-reduced-motion: reduce) {
  .bdg-art--sundown .bdg__mark--nightowl::before,
  .bdg-art--sundown .bdg__mark--nightowl::after,
  .bdg-art--sundown .bdg__art::before { animation: none; }
}

/* ---- noon: the third hour of the same day -----------------------------------
 *
 * Drawn 29 August 2026 on the badges Artifact, beside the sundown badge above
 * it, and ported into this sheet with it on the 30th ("TWO BADGES ABOUT WHEN
 * YOU CLIMB" above); gym/badges.py names it as art="midday", the class below.
 *
 * WHAT IT SHARES WITH THE OTHER TWO is the arrangement, not the scenery: the
 * sky is painted on the same layer of the same element, the horizon sits on the
 * same pixel the dusk badge's top-out lip sits on, and every offset is measured
 * in px from the face's centre. It was drawn as the same WALL first, and that
 * is written up in full over the ground rule below along with why it is a field
 * now. What a time-of-day badge is ABOUT is where the light is coming from —
 * and at noon the light is coming straight down, which means it lands on the
 * ground and nowhere else.
 *
 *   Doors open  — a COLD sky: indigo down to one narrow warm strip at the
 *                 horizon. Half a disc on the line, seven rays going up,
 *                 nothing at all in front of it.
 *   Midday      — a PALE sky with nothing in it. A small hard sun a long way
 *                 above a green field, rays in every direction, field in front.
 *   Night owl   — a WARM sky: plum down to orange. The sun going behind the
 *                 line, no rays, a wall with a bird standing on it in front.
 *
 * All three are windows. `firstlight` gave the dawn badge a sky of its own an
 * hour before this was drawn, which spent the empty-field half of the dusk
 * badge's separation and is written up in both of those blocks. What is left
 * carrying it is the temperature of the sky and where the sun is: cold and
 * rising, white and overhead, warm and going.
 *
 * The separation that matters is not the objects, it is the VALUE: dark field,
 * light field, mid-dark field. Desaturate the three and they are still three
 * badges, which is the test the grade medallions had to pass and the only test
 * that survives a colour-blind member at 56px.
 *
 * PINNED IN PX FROM THE FACE'S CENTRE. --badge-size is 56px on a phone, 68px at
 * 700px and 78px at 1080px; a horizon written in percent slides up the drawing
 * at every breakpoint while a sun written in px does not move with it. Same
 * ruling as the sundown sheet, same reason, and here it binds two drawings
 * together rather than one: this badge's horizon and the dusk badge's lip are
 * the same number, and a number that drifts at a breakpoint makes them two
 * different places.
 * -------------------------------------------------------------------------- */

/* The noon sky. Everything the sunset is not: no clouds, no bands, no weather.
 *
 * THE EMPTY MIDDLE IS THE DRAWING. Dusk gets twelve stops and two cloud bars
 * because dusk is an event; noon is the absence of one — nothing is arriving,
 * nothing is leaving, and the hour's whole character is that there is nothing
 * in the sky to look at. Five stops, one bleach, and a large amount of blue
 * doing nothing. It also buys the corona the only thing it needs, which is room.
 *
 * The bleach is BLUE-white and not white, and that is a legibility fix rather
 * than a nicety: the disc above it is white in the middle, and a white disc on
 * a white bloom is a hole with an orange ring. Cooling the bloom by a few
 * points of blue puts the sun back in front of its own glare. */
.bdg-art--midday .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background:
    /* The bleach around the sun. Centred on the disc, not on the face — it is
       the sun's own glare in the air, so it moves with the sun and with
       nothing else. */
    radial-gradient(circle 13px at 50% calc(50% - 5px),
      rgba(240,250,255,.92) 0 14%,
      rgba(214,238,253,.42) 52%,
      rgba(190,226,250,0)  100%),
    /* The ramp. It gets DEEPER upward rather than paler, which is the one thing
       a midday sky does that a sunset does not: at noon the darkest part of the
       sky is directly overhead and the pale part is the haze at the bottom. The
       sunset ramp peaks in the middle and falls away at both ends. Inverting it
       is most of why these two tiles cannot be confused at any size. */
    linear-gradient(to bottom,
      #1a6ec8   0%,
      #2d84d8  15%,
      #57a3e4  33%,
      #7fb8ea  52%,
      #99c8ef  66%,
      #a8d0f1  74%,
      #b4d6f3 100%);
  /* The engraving lives here for the reason the sundown sheet found the hard
     way: `.bdg__face--engraved` is an inset box-shadow on the face, and an
     inset shadow paints under the face's own children — so on a tile whose
     ::before covers the face wall to wall it is drawn and immediately buried.
     Inherited as a fact, not rediscovered. Darkened slightly against the dusk
     tile's copy: the same 14% white lip is invisible over a pale sky. */
  box-shadow:
    inset 0  1px 0 rgba(255,255,255,.5),
    inset 0 -1px 0 rgba(0,0,0,.32);
}

/* THE GROUND IS A FIELD, and it replaced a top-out lip on 29 August 2026.
 *
 * The first draft of this badge reused the dusk badge's wall wholesale — same
 * lip on the same pixel, same single hold, chalk, panel seams — on the argument
 * that three badges about one place should be one place. It was a good argument
 * and it is written down here rather than deleted, because what was wrong with
 * it is worth keeping: at noon the wall is the least interesting thing in the
 * frame. The sun is straight up, so a vertical face gets no light at all and
 * the whole bottom third of the badge went dark — the one hour with the most
 * light in it, drawn with the darkest ground in the set.
 *
 * A FIELD IS WHAT NOON LOOKS LIKE, because at noon the light lands on the
 * ground and nowhere else. A field under a vertical sun has nothing in it to
 * cast a shadow with: it is flat, bright, and slightly bleached, and that
 * flatness IS the hour. Draw the same field at either of the other two times
 * and it is all shadow and rake.
 *
 * WHAT IT COSTS is the one thing the wall was buying: this badge no longer
 * proves it is THIS gym. The trade is not new and it is not being made quietly
 * — "Doors open" already made it, and its entry says so out loud: the sentence
 * under the badge carries the accuracy and the mark is allowed to be the
 * metaphor. A sun over a field is the drawing every language already has for
 * the middle of the day, the same way a sun coming up is the drawing every
 * language has for a thing opening.
 *
 * WHAT IT BUYS, past legibility, is the last unspoken colour in the set. Dawn
 * is cold, dusk is warm, and green belongs to neither of them — so at 56px the
 * three tiles now separate on hue before they separate on anything else, which
 * is the fastest read there is.
 *
 * The horizon stays on the pixel the lip was on, calc(50% + 9.5px), which is
 * the one thing kept from the first draft. It is a good horizon: low enough to
 * leave the sun a long way clear of it, high enough that the ground is a field
 * rather than a strip. */
.bdg-art--midday .bdg__art {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  transform: none;
  /* ABOVE THE MARK, which is the one place in either sheet where the art layer
     outranks the drawing it is sharing a face with — everywhere else z-index 1
     under z-index 2 is the house order. The corona is symmetrical and reaches
     past the horizon on the way down, and the ground is between the member and
     the sky, so the ground has to occlude the light rather than the light
     painting over the ground. Anything else is a sun in front of a field. */
  z-index: 3;
  background:
    /* THREE SOFT BANDS, and they are the only texture on the field. Wide, low
       contrast, off-centre and unevenly spaced — a field is not flat, it rolls,
       and three lazy changes of level is the cheapest way to say so. Evenly
       spaced they would be mowing stripes, and mowing stripes in a drawing with
       no perspective in it are a barcode. The pale ones are the tops catching
       the sun and the dark one is a dip; that ordering is what makes it ground
       rather than a gradient with lines in it. */
    radial-gradient(76% 2px at 44% calc(50% + 13.6px),
      rgba(255,255,255,.17) 0 38%, rgba(255,255,255,0) 100%),
    radial-gradient(66% 2.6px at 58% calc(50% + 18.2px),
      rgba(12,44,10,.16) 0 38%, rgba(12,44,10,0) 100%),
    radial-gradient(92% 3.4px at 42% calc(50% + 25px),
      rgba(255,255,255,.09) 0 38%, rgba(255,255,255,0) 100%),
    /* THE FAR EDGE, bleached almost white. This is the whole difference between
       a field going away from you and a green rectangle: the grass at the
       horizon is seen through a mile of hot air and it loses its colour, and
       the grass at your feet is not and does not. One pixel of pale at the top
       does more for the depth than anything below it.
       It is also, deliberately, NOT a lit line. The dusk badge's lip is a hard
       bright edge because a top-out is a surface catching light; a horizon is
       just where the ground runs out, so this one fades in from the sky instead
       of being drawn on top of it. */
    linear-gradient(to bottom,
      rgba(0,0,0,0)          0    calc(50% + 9.5px),
      rgba(247,252,226,.62)  calc(50% + 9.5px)  calc(50% + 10.1px),
      rgba(247,252,226,0)    calc(50% + 11.2px)),
    /* The field. Pale and yellow at the horizon, deeper and bluer at the
       bottom, which is haze doing what haze does — and it is also what stops
       the green reading as paint. Six stops rather than two: grass in full sun
       is not one colour, it is a bright top surface over its own shade, and a
       flat green band under a blue sky is a flag. */
    linear-gradient(to bottom,
      rgba(0,0,0,0)  0    calc(50% + 9.5px),
      #c2dc8b        calc(50% + 9.5px),
      #a5cf65        calc(50% + 12px),
      #85c04a        calc(50% + 16px),
      #5fa53c        calc(50% + 22px),
      #438433        calc(50% + 30px),
      #2f6a2b        100%);
}

/* The air over the field. A hot surface in direct sun bends the light above it,
 * and this is the only thing in the drawing with any business moving — which is
 * why it exists on the still copy too, as a pale smear, rather than being
 * conjured by the animation. A layer that only appears when the motion class is
 * on is a second badge wearing the first one's name.
 *
 * It sits on the art's ::before, which is the slot the dusk badge spends on its
 * first star. Neither drawing needs both, and the pairing is exact: the last
 * thing dusk adds to the sky is a point of light, the last thing noon adds is
 * the air. */
.bdg-art--midday .bdg__art::before {
  content: "";
  position: absolute;
  left: 8%;
  right: 8%;
  top: calc(50% + 5.4px);
  height: 4.6px;
  background: radial-gradient(58% 100% at 50% 100%,
    rgba(255,255,246,.5)  0 26%,
    rgba(230,244,206,.22) 60%,
    rgba(230,244,206,0)  100%);
  /* Declared on the element, never left to a 0% keyframe stop: `animation:
     none` has to land the still form back on a written value. The dusk star's
     rule, and it is the reason both tiles survive prefers-reduced-motion
     looking like themselves. */
  opacity: .82;
}

/* ---- middaysun: small, hard, and a long way up ------------------------------
 *
 * SMALL. The dawn sun is 22px across and soft; this one is 14px and hard, and
 * the difference is not a style choice — it is what the two hours look like. A
 * sun at the horizon is seen through the whole depth of the atmosphere, which
 * bloats it and blurs its edge; a sun at the top of the sky is seen through the
 * least air there is, so it is small, its edge is definite and the glare is in
 * the corona rather than in the disc. Drawing noon as a big soft sun is drawing
 * dawn twice.
 *
 * A LONG WAY ABOVE THE GROUND, and the gap is the badge. Dawn puts the disc ON
 * the line; dusk puts it through it. Noon is the hour with the maximum possible
 * distance between the sun and the horizon, so the drawing spends the whole
 * middle of the face on empty sky to say so. It is the only one of the three
 * where the two objects are not touching.
 *
 * TWELVE RAYS, SIX LONG AND SIX SHORT, alternating. An even corona of twelve is
 * a cog and a corona of six is a compass rose; alternating the lengths is what
 * every human being has drawn for a sun since they were four, and this is not
 * the place to be original — the badge has about a fifth of a second to say
 * "noon" on a wall of twenty tiles. The two sets live on the mark's two pseudos
 * rather than in one gradient, and that is not tidiness: it is what lets them
 * breathe on different clocks in the moving copy without a third element.
 *
 * A repeating-conic-gradient here where the sunrise needed an explicit one, and
 * the difference is instructive. There the repeat could not be told where to
 * STOP, and what it kept adding was a flat pair of rays lying along the horizon
 * — a strikethrough. Here the repetition is the entire subject: a corona is a
 * fan with no ends, so the construction that could not be stopped is exactly
 * the correct one.
 * -------------------------------------------------------------------------- */
.bdg__mark--middaysun {
  /* Two ray colours, hoisted so the spotlight can turn the light up without
     restating either gradient. The short set is dimmer as well as shorter —
     alternating on one axis reads as a fault at 56px, on two it reads as
     glare. */
  --sun-ray:   rgba(255,252,238,.82);
  --sun-ray-2: rgba(255,248,224,.56);
  /* The reward glow and the foil's contact shadow, both hoisted for the reason
     the daybreak copy discovered: an animated `filter` outranks every static
     one in the cascade whatever its specificity, so a spotlight rule written as
     a plain filter would simply stop applying the moment the motion class went
     on. Defaults are a no-op shadow, so a plain still tile pays nothing. */
  --sun-glow: rgba(255,222,150,.5);
  --sun-seat: 0 0 0 rgba(0,0,0,0);
  /* The box is the DISC only. The corona is drawn by two pseudos pinned to the
     disc's own centre, so the sun can be moved up the face by one number and
     the light follows it. */
  width: 14px;
  height: 14px;
  border-radius: 50%;
  /* Up 5px off dead centre — `place-items: center` centres a box and none of
     these drawings are centred inside their own. margin rather than transform,
     which is the owl's idiom and here it is load-bearing: the moving copy
     spends transform on the corona and a mark that has spent it on a nudge
     cannot lend it back. */
  margin-bottom: 10px;
  /* Ramps, not stops. A light source with hard stops draws concentric bands,
     which is an archery target; the one hard-ish edge is at the rim, where a
     noon sun genuinely has one. Centre pulled up to 44% so the disc has a
     direction — a perfectly concentric sun is a button. */
  background: radial-gradient(circle at 50% 44%,
    #ffffff  0 30%,
    #fffbe4 54%,
    #ffe9a8 74%,
    #ffcc55 90%,
    #f9a825 97%,
    rgba(249,168,37,0) 100%);
  /* The warmth on everything around it. A filter and not a box-shadow: the rays
     live on pseudos, and a box-shadow would light the BOX rather than the
     drawing. */
  filter: drop-shadow(0 0 6px rgba(255,208,110,.5));
}

/* The six long rays. 6.4deg wide on a 60deg pitch, one of them straight up.
 *
 * The mask does two jobs in one declaration, both of them the sunrise's: it
 * starts every ray a hair outside the disc so the corona is separated from its
 * own sun by a line of daylight, and it fades the tips, because rays that all
 * stop dead at a common radius are a cog and the fade is the whole difference
 * between a gear and light.
 *
 * The ray pointing straight DOWN is deliberate and it is the drawing's one
 * piece of luck: it runs from the sun down to the horizon and is then cut off
 * by the ground, which is what puts the light ON the field instead of merely
 * near it. */
.bdg__mark--middaysun::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 38px;
  height: 38px;
  margin: -19px 0 0 -19px;
  background: repeating-conic-gradient(from -2.9deg,
    var(--sun-ray) 0 6.4deg, rgba(0,0,0,0) 6.4deg 60deg);
  -webkit-mask-image: radial-gradient(circle 19px at 50% 50%,
    rgba(0,0,0,0) 0 38%, #000 48%, rgba(0,0,0,.55) 76%, rgba(0,0,0,0) 100%);
  mask-image: radial-gradient(circle 19px at 50% 50%,
    rgba(0,0,0,0) 0 38%, #000 48%, rgba(0,0,0,.55) 76%, rgba(0,0,0,0) 100%);
}

/* The six short ones, offset by half a pitch so they sit between the long ones.
   Two thirds the reach and a third less light: on the long set's own radius
   they would read as twelve equal rays with a rendering fault. */
.bdg__mark--middaysun::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 27px;
  height: 27px;
  margin: -13.5px 0 0 -13.5px;
  background: repeating-conic-gradient(from 27.4deg,
    var(--sun-ray-2) 0 5.6deg, rgba(0,0,0,0) 5.6deg 60deg);
  -webkit-mask-image: radial-gradient(circle 13.5px at 50% 50%,
    rgba(0,0,0,0) 0 53%, #000 66%, rgba(0,0,0,.5) 86%, rgba(0,0,0,0) 100%);
  mask-image: radial-gradient(circle 13.5px at 50% 50%,
    rgba(0,0,0,0) 0 53%, #000 66%, rgba(0,0,0,.5) 86%, rgba(0,0,0,0) 100%);
}

/* Winning the spotlight TURNS THE LIGHT UP. The sun does not climb, does not
   grow and does not move — the same ruling both other suns inherit, for the
   same reason: a badge whose picture depends on which tile the server happened
   to pick this week is two different badges. Brighter rays, a wider swell, the
   sky a shade hotter, the same sun in the same place. */
.bdg--spotlight .bdg__mark--middaysun,
.bdg-reveal__badge .bdg__mark--middaysun {
  --sun-ray:   rgba(255,255,248,.98);
  --sun-ray-2: rgba(255,252,236,.72);
  --sun-glow:  rgba(255,231,168,.72);
  filter: drop-shadow(0 0 10px rgba(255,214,120,.62));
}
.bdg--spotlight.bdg-art--midday .bdg__face::before,
.bdg-reveal__badge .bdg-art--midday .bdg__face::before {
  filter: saturate(1.1) brightness(1.06);
}
.bdg--spotlight.bdg-art--midday .bdg__art::before,
.bdg-reveal__badge .bdg-art--midday .bdg__art::before {
  opacity: 1;
}

/* Locked. The sky is the thing being earned, so the sky is what a locked tile
   does not get — drained and knocked back to the recessed dark face every other
   locked tile wears. The sun greys with it: the sheet greys `.bdg__icon` on a
   locked tile and a drawn mark is an icon by another name, so a full-colour sun
   blazing over a dashed tile would be the one object in the frame that had not
   noticed it was not earned yet. `animation: none` on the pseudos as well,
   because a running filter animation out-ranks the static greyscale. */
.bdg--locked.bdg-art--midday .bdg__face::before {
  filter: grayscale(1);
  opacity: .16;
}
.bdg--locked .bdg__mark--middaysun {
  filter: grayscale(1);
  opacity: .45;
  /* `animation: none` on the MARK and not only on its pseudos. Not defensive
     tidying: the glare below animates the mark's own filter, and a running
     filter animation out-ranks a static one however specific it is, so without
     this line a locked tile would sit on a dashed border glowing warm orange
     straight through its own greyscale. The sunrise carries the identical three
     lines for the identical reason — inherited, not rediscovered, and repeated
     in full rather than assumed, because it is the kind of fault that passes
     every assertion either way and can only be caught in the rendered pixel. */
  animation: none;
}
.bdg--locked .bdg__mark--middaysun::before,
.bdg--locked .bdg__mark--middaysun::after { animation: none; }
.bdg--locked.bdg-art--midday .bdg__art::before { animation: none; opacity: .3; }

/* Foil. As unreachable as the dusk badge's — shiny is proof of a hard way up a
   named climb and there is no hard way to climb at lunchtime — and written
   anyway, at three lines, because the sheet must never be the thing that breaks
   the day somebody disagrees. The sky yields to the rainbow (the rocket's
   ruling), and the sun gets the contact shadow the shiny bat and the shiny deck
   both needed: an object sitting ON the foil rather than a stain in it. */
.bdg.bdg--shiny.bdg-art--midday .bdg__face::before { opacity: .72; }
.bdg.bdg--shiny .bdg__mark--middaysun {
  --sun-seat: 0 1px 2px rgba(0,0,0,.75);
  filter:
    drop-shadow(0 0 6px rgba(255,208,110,.5))
    drop-shadow(0 1px 2px rgba(0,0,0,.75));
}

/* ---- noonday: the copy that moves -------------------------------------------
 *
 * One class on the tile, never a second mark and never an extra element. In the
 * catalogue this would be one Anim value — the identical lever every still
 * badge already pulls by carrying Anim.NONE — and which of the two ships has
 * not been decided, which is why the specimen shows both.
 *
 * THE SUN DOES NOT MOVE, and on this badge that is not a restraint, it is the
 * subject. The other two inherit the ruling because a sun you can watch move is
 * a time-lapse; this one would be wrong even if the ruling did not exist. Solar
 * noon IS the stationary point — the one minute in the day when the sun has
 * stopped climbing and has not started falling — so a midday badge with a
 * moving sun is not merely a spinner, it is the one drawing in the set that
 * contradicts its own name. The disc has no animation on it at all.
 *
 * WRITTEN AFTER THE DAWN COPY WAS REBUILT, and it takes that rebuild's finding
 * rather than repeating the mistake it fixed. `daybreak` shook on four-to-seven
 * second periods with four turning points a cycle, and the diagnosis is in that
 * block: irregular keyframes belong to FIRE, because fire is irregular, and
 * light at a horizon is one of the slowest events a person watches. Direction
 * changes were what made it twitch; amplitude was not. So the three layers here
 * that are drawing LIGHT — the glare and the two ray sets — run one turn per
 * cycle on 13s, 19s and 23s, on the same dwell-at-the-extremes curve the dawn
 * copy uses, and their swings are wider than the first draft's rather than
 * narrower.
 *
 * AND ONE LAYER DELIBERATELY DOES NOT. The shimmer over the far grass keeps a
 * fast,
 * uneven, four-stop cycle, because it is the only layer here that is not
 * drawing light: it is drawing AIR over a hot surface, which is turbulence, and
 * turbulence is the flame's family rather than the horizon's. A heat haze that
 * eases smoothly out and back over twenty seconds is a curtain. It is the one
 * exception in the tile and it is the reason the tile has a second timescale at
 * all — everything else is a slow breath, and one quick thing near the ground
 * is what stops five layers of slow reading as a single object pulsing.
 *
 * FOUR PRIME PERIODS: 13, 19, 23 and 7 seconds. Every pair is co-prime, so the
 * composite cannot repeat before their product — 39,767 seconds, which is
 * eleven hours and three minutes, or about three minutes longer than the doors
 * are open on a Saturday. Nobody can watch this tile come back round to where
 * they found it without going home first. (The dawn copy makes the same
 * argument with five periods and gets fifty-nine days; it can, because none of
 * its layers has any business being quick.)
 * -------------------------------------------------------------------------- */

/* The curve, borrowed whole from the dawn rebuild for the reason given there: a
   plain ease-in-out spends its entire middle in transit, so the drawing is
   always visibly on its way somewhere. This one arrives early and leaves late,
   which parks the light near the top or the bottom of its range and moves it
   between them only briefly. Slow is not enough on its own — slow and CONSTANT
   still reads as a mechanism. Not applied to the shimmer, which wants to be
   caught in transit. */
.bdg-art--midday .bdg__mark--middaysun,
.bdg-art--midday .bdg__mark--middaysun::before,
.bdg-art--midday .bdg__mark--middaysun::after {
  --sun-ease: cubic-bezier(.62, 0, .38, 1);
}

/* The glare swelling. Reads the two hoisted custom properties rather than
   writing colours into the keyframes, so the spotlight's wider glow and the
   foil's contact shadow both survive the animation taking over the filter. */
.bdg-art--midday .bdg__mark--middaysun {
  animation: bdg-noonglare 13s var(--sun-ease) infinite;
}

@keyframes bdg-noonglare {
  0%, 100% { filter: drop-shadow(0 0 4px rgba(255,208,110,.3)) drop-shadow(var(--sun-seat)); }
  50%      { filter: drop-shadow(0 0 13px var(--sun-glow))     drop-shadow(var(--sun-seat)); }
}

/* The long rays reaching. Origin dead centre, which for once needs no argument:
   the corona is hinged on the disc's own middle and scaling it from anywhere
   else would walk the light off its own sun. Nineteen per cent end to end,
   against the first draft's twelve, and it reads calmer — nineteen seconds of
   it works out at about a twentieth of a pixel per frame. */
.bdg-art--midday .bdg__mark--middaysun::before {
  transform-origin: 50% 50%;
  animation: bdg-noonreach 19s var(--sun-ease) infinite;
}

@keyframes bdg-noonreach {
  0%, 100% { transform: scale(.93); opacity: .74; }
  50%      { transform: scale(1.12); opacity: 1; }
}

/* The short set, and it STARTS AT THE OTHER END of its own range: out while the
   long rays are in. Not a delay — a delay on a different period is a lie that
   lasts one cycle — just the keyframes written the other way round, so the
   corona opens opposed, drifts through every phase over the next few minutes,
   and never sits in step long enough to become one shape breathing. */
.bdg-art--midday .bdg__mark--middaysun::after {
  transform-origin: 50% 50%;
  animation: bdg-noonglint 23s var(--sun-ease) infinite;
}

@keyframes bdg-noonglint {
  0%, 100% { transform: scale(1.1); opacity: .98; }
  50%      { transform: scale(.94); opacity: .66; }
}

/* The shimmer over the far grass — the exception argued in the header. scaleX
   and a sub-pixel slide, never scaleY: heat haze runs ALONG a hot surface, and
   scaling it vertically would lift it off the ground it is coming from. The
   slide is half a pixel, which is less than a device pixel on a phone and is
   meant to be: it is the difference between air moving and a bar getting wider.
   Four stops rather than two, unevenly spaced, on the flicker's reasoning and
   for the flicker's subject. */
.bdg-art--midday .bdg__art::before {
  transform-origin: 50% 100%;
  animation: bdg-noonhaze 7s ease-in-out infinite;
}

@keyframes bdg-noonhaze {
  0%, 100% { transform: translateX(0)     scaleX(1);    opacity: .74; }
  23%      { transform: translateX(.5px)  scaleX(1.07); opacity: .96; }
  49%      { transform: translateX(-.4px) scaleX(.95);  opacity: .7; }
  74%      { transform: translateX(.25px) scaleX(1.03); opacity: .88; }
}

/* Winning the spotlight is a GEAR CHANGE and not a switch — the kickflip's
   ruling, and the only one available on a copy that is already moving: the
   reward cannot be that it starts. Same keyframes, same distances, roughly half
   off every wait, and 7 / 11 / 17 / 5 rather than half the base set, because
   the gear change must not spend the co-primality the header just promised.
   Those four share no factor either, so a spotlit tile repeats every hour and
   three quarters instead of every eleven hours. */
.bdg--spotlight.bdg-art--midday .bdg__mark--middaysun,
.bdg-reveal__badge .bdg-art--midday .bdg__mark--middaysun { animation-duration: 7s; }
.bdg--spotlight.bdg-art--midday .bdg__mark--middaysun::before,
.bdg-reveal__badge .bdg-art--midday .bdg__mark--middaysun::before { animation-duration: 11s; }
.bdg--spotlight.bdg-art--midday .bdg__mark--middaysun::after,
.bdg-reveal__badge .bdg-art--midday .bdg__mark--middaysun::after { animation-duration: 17s; }
.bdg--spotlight.bdg-art--midday .bdg__art::before,
.bdg-reveal__badge .bdg-art--midday .bdg__art::before { animation-duration: 5s; }

/* The still form. Corona at its own written scale, shimmer at its own .82 —
   never a corona frozen mid-swell and never a haze that faded out and stayed
   out. `animation: none` returns each layer to the value declared on the
   element rather than to wherever a keyframe was cancelled, which is the entire
   reason those values are on the elements and not in the 0% stops. */
@media (prefers-reduced-motion: reduce) {
  .bdg-art--midday .bdg__mark--middaysun,
  .bdg-art--midday .bdg__mark--middaysun::before,
  .bdg-art--midday .bdg__mark--middaysun::after,
  .bdg-art--midday .bdg__art::before { animation: none; }
}

/* ===========================================================================
 * TOPOUT — the moment it finally goes
 *
 * DRAWN 10 SEPTEMBER 2026 for the `redpoint` badge, and it is the THIRD drawing
 * this badge has had in a day. Both predecessors are deleted; what they got
 * wrong is the only reason this header is long, because they got it wrong in
 * the same direction twice and that direction is easy to walk into again.
 *
 *   FIRST: a Rotpunkt — a weathered red X at the foot of a route with a fresh
 *   red dot painted over it, after Kurt Albert's mark in the Frankenjura. It
 *   was an ARGUMENT rather than a picture: airtight on paper, and at 56px a red
 *   circle with four brown arms whose entire payload sat behind a reference
 *   almost nobody in a bouldering gym has. A badge cannot ask to be explained.
 *
 *   SECOND: the climber, three times — two chalk ghosts falling away down a
 *   dark wall with one solid figure above them. Legible, honest, and WRONG
 *   ABOUT THE FEELING, which is the failure worth keeping. "Sending your
 *   project is a really exciting thing, the badge should feel like a great
 *   achievement" (Keiran). Both dead drawings were about the STRUGGLE — a mark
 *   left behind, then a record of failures — and both were therefore drawn in
 *   the past tense, dim, and slightly sad. A member does not open their badge
 *   wall to be reminded how many times they came off.
 *
 * SO THIS ONE IS THE MOMENT, NOT THE HISTORY. Topped out, standing on the
 * boulder, one fist up, against the brightest light in the set.
 *
 * IT IS DELIBERATELY THE ONLY BLAZING TILE ON THE WALL. Every other badge here
 * is dark or muted — a night wall, an event horizon, a cave, a dusk sky, and
 * a fire that is only ever a small warm thing inside a dark coin. Against that
 * set, a face of full gold does the job no amount of drawing inside a dark tile
 * can: it says THIS ONE IS THE BIG ONE before a member has read a word. The
 * restraint everywhere else in this sheet is what buys this.
 *
 * THE FIGURE IS BACKLIT AND SOLID BLACK, which is the reverse of the two
 * figures already in the set (the last-ascent sprite is full colour on ply,
 * Gravity's free climber is pale on near-black) and is what makes it read at
 * 50px. A silhouette against a bright ground is the highest-contrast shape
 * available, and it costs no internal detail at all — there is nothing inside
 * it to lose when the tile shrinks.
 *
 * ONE FIST, NOT TWO ARMS. Symmetric arms overhead is the universal shape for
 * celebrating and it is also, at this size, the universal shape for a jumping
 * jack — the second drawing's ghosts were caught reading exactly that way with
 * exactly that pose. An asymmetric stance, one arm punched up and one out for
 * balance, reads as a person who has just done something instead of a pictogram
 * of joy, and asymmetry is what makes a stick figure look like a body.
 *
 * THE FACE PSEUDOS ARE SPENT HERE, and that is safe only because this badge is
 * Anim.NONE. `.bdg--sheen .bdg__face::after` and `.bdg--aurora .bdg__face::before`
 * exist, so giving this badge either of those animations would silently cancel
 * half the drawing. If a later change wants it to sheen, the light and the rays
 * have to move somewhere else first — see _badge_mark.html's note about exactly
 * this hazard.
 *
 * NO CHILD SPANS. Six layers, all of them elements templates/portal/_badge_mark.html
 * already emits for any badge naming an `art` and a `mark`. Full Spectrum drew
 * an empty coin for weeks because badges.css expected eight spans the template
 * never wrote; nothing here can catch on that.
 * =========================================================================== */

.bdg-art--topout {
  --to-rays: 34s;   /* the light wheeling behind them, slow enough to be dignity */
  --to-glow: 5.5s;  /* the glory breathing */
  /* Standing, one fist up. Same authoring rules as every figure in this sheet:
     a 26x34 viewBox, round caps, and a head that is a filled circle rather than
     a stroked one so it never goes hollow at small sizes. */
  --to-figure: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 34' width='26' height='34'%3E%3Cg fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12.9 11.2L13.6 20.4' stroke-width='4.4'/%3E%3Cpath d='M13.6 12.0L16.8 7.4 17.6 2.4' stroke-width='2.8'/%3E%3Cpath d='M12.4 12.6L8.4 13.8 5.6 11.4' stroke-width='2.6'/%3E%3Cpath d='M13.0 20.2L10.8 25.6 10.0 31.4' stroke-width='3.0'/%3E%3Cpath d='M14.0 20.2L16.6 25.8 17.4 31.6' stroke-width='3.0'/%3E%3Ccircle cx='12.8' cy='8.0' r='3.0' fill='%23fff' stroke='none'/%3E%3C/g%3E%3C/svg%3E");
}

/* THE LIGHT. A single source sitting just behind the climber's chest, running
   from near-white through gold to a deep bronze at the rim.
   THE CORE IS OFF-WHITE AND NOT WHITE. A pure white core on a gold field blows
   out to a flat disc at 50px and the tile stops having a light source in it —
   it just has a hole. Holding the brightest stop at #fff8e6 keeps the falloff
   readable at every size, which is the same lesson the flame's heart learned.
   Centred at 50%/42% — above the middle, behind the figure's upper body, so the
   rays fan from behind their shoulders rather than from between their feet. */
.bdg-art--topout .bdg__face::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(58% 52% at 50% 42%, #fff8e6 0%, #ffe6a6 26%, #f9bf55 52%, #e08a28 74%, #a95713 100%);
}

/* THE RAYS. A repeating conic gradient masked into an annulus so they fade out
   before the core and again before the rim.
   MASKED AT BOTH ENDS, and both matter. Run them into the centre and they cut
   the light source into a pinwheel — the badge stops being lit and starts being
   a beach umbrella. Run them to the rim and they collide with the tier ring,
   which is a different colour on all four tiers and cannot be designed against.
   UNEVEN WIDTHS, five degrees in thirty. Even spokes at even spacing is a
   radiation symbol; light through dust is irregular, and the irregularity is
   the whole difference between a sunburst and a hazard sign. */
.bdg-art--topout .bdg__face::after {
  content: "";
  position: absolute;
  inset: 0;
  background: repeating-conic-gradient(from 4deg at 50% 42%,
    rgba(255,252,240,.30) 0deg 2.2deg,
    rgba(255,252,240,0)   2.2deg 16deg,
    rgba(255,252,240,.17) 16deg 17.4deg,
    rgba(255,252,240,0)   17.4deg 41deg);
  -webkit-mask-image: radial-gradient(circle at 50% 42%,
    rgba(0,0,0,0) 0 15%, #000 32% 54%, rgba(0,0,0,0) 72%);
  mask-image: radial-gradient(circle at 50% 42%,
    rgba(0,0,0,0) 0 15%, #000 32% 54%, rgba(0,0,0,0) 72%);
  transform-origin: 50% 42%;
  animation: bdg-to-wheel var(--to-rays) linear infinite;
  z-index: 1;
}

@keyframes bdg-to-wheel { to { transform: rotate(360deg); } }

/* The art element is a BARE CONTAINER and carries no drawing. A mask on a
   parent clips its children, and the drawing this replaced lost three drafts to
   exactly that: two of its three figures could only ever paint where they
   overlapped the third's silhouette, with nothing wrong in any computed style.
   Keep it empty. */
.bdg-art--topout .bdg__art {
  position: absolute;
  inset: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transform: none;
  /* ABOVE THE RAYS, AND THAT IS THE WHOLE POINT OF SAYING SO. The rays sit on
     the face's ::after at z-index 1; the first draft left this at 1 too, and
     the later element won — light streaked ACROSS the rock, which drew a
     translucent hill rather than a solid boulder in front of a sun. The rock is
     the nearest object in the picture and everything luminous is behind it. */
  z-index: 2;
}

/* THE BOULDER — LOW, WIDE, AND ONLY HALF SEEN.
   The first shipped version crowned at 68% and was 14% narrower than the face,
   so a third of the tile was stone and the whole rock sat inside the frame,
   which quietly made it a small one. Crowning at 76% with the shoulders 22%
   past the face does two things at once: it gives most of the tile back to the
   light, and it flattens the arc, so what shows reads as the top of something
   much bigger than the badge. A rock that fits inside its own frame is a
   pebble; a rock you can only see half of is a boulder.

   IT IS STONE NOW, NOT A SHAPE. Two linear gradients — what this used to be —
   are smooth and even, and at 104px unmistakably a piece of vector art. Real
   rock is high-frequency and low-contrast at two scales AT ONCE, and no stack
   of colour stops gets there. Four layers do:
     CRACKS on top, following the dome. Three and no more: stone breaks along a
   few big lines and then along a thousand small ones, and only the big ones
   survive at 56px — a fine craquelure just greys the rock out.
     GRAIN at 26px, the same feTurbulence trick and nearly the same numbers the
   holds already use for their sand. `overlay` rather than a flat alpha, which
   is what lets it darken AND lighten the stone instead of dusting it.
     MOTTLE, the same noise an order of magnitude coarser at 150px, so the face
   has big soft patches of light and dark UNDER the grain. This is the layer
   that stops it reading as sandpaper: grain alone is a texture, grain plus
   mottle is a material.
     THE RAMP beneath, cool grey, pitched two steps darker than the first
   attempt at a light ground — which came out the colour of snow. The chalk band
   and the rim light both pile up at the crown, so a pale ramp there reads as a
   drift, and worse, the grain stops showing: overlay noise on a near-white base
   has almost no range to work in. Darkening the ramp is what makes the texture
   visible, not adding more of it.

   The rim light along the lip is NEAR-WHITE rather than gold now. Its whole job
   is to say the sun is behind the rock, and it can only do that by
   out-brightening what it sits on — a warm edge on warm stone is invisible. */
.bdg-art--topout .bdg__art::before {
  content: "";
  position: absolute;
  left: -22%;
  right: -22%;
  bottom: -18%;
  height: 42%;
  border-radius: 46% 46% 0 0 / 96% 96% 0 0;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 60' preserveAspectRatio='none'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round'%3E%3Cpath d='M18 14C30 22 34 34 30 60' stroke-width='1.5' opacity='.30'/%3E%3Cpath d='M63 4C66 16 78 22 94 26' stroke-width='1.7' opacity='.26'/%3E%3Cpath d='M78 24C74 36 76 48 84 60' stroke-width='1.2' opacity='.20'/%3E%3Cpath d='M30 26C40 30 48 30 58 26' stroke-width='1' opacity='.16'/%3E%3C/g%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cfilter id='r'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='90' height='90' filter='url(%23r)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='m'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.055' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23m)'/%3E%3C/svg%3E"),
    linear-gradient(179deg, rgba(255,255,255,.26) 2%, rgba(255,255,255,.07) 11%, rgba(255,255,255,0) 24%),
    linear-gradient(184deg, #aab1bd 0%, #868e9c 20%, #646b78 44%, #3d434d 78%, #2b3038 100%);
  background-size: 100% 100%, 26px 26px, 150px 150px, 100% 100%, 100% 100%;
  background-repeat: no-repeat, repeat, repeat, no-repeat, no-repeat;
  background-blend-mode: normal, overlay, overlay, normal, normal;
  box-shadow: inset 0 calc(var(--badge-size) * .0095) 0 rgba(255,252,244,.92);
}

/* THE GLORY. A soft warm bloom sitting behind the figure and breathing, so the
   tile has one slow moving thing in it besides the wheel.
   IT BREATHES RATHER THAN PULSES. A sharp repeating flash is an alert, and this
   badge is the opposite of an alert; a five-and-a-half second swell that never
   quite goes out reads as light rather than as a signal. The two durations
   share no factor with each other, so the swell never lands on the same spoke
   twice running. */
.bdg-art--topout .bdg__art::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 42%;
  width:  calc(var(--badge-size) * .62);
  height: calc(var(--badge-size) * .62);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(255,250,230,.55) 0%, rgba(255,232,170,.28) 34%, rgba(255,214,120,0) 70%);
  animation: bdg-to-glory var(--to-glow) ease-in-out infinite;
}

@keyframes bdg-to-glory {
  0%, 100% { opacity: .72; transform: translate(-50%, -50%) scale(1); }
  50%      { opacity: 1;   transform: translate(-50%, -50%) scale(1.1); }
}

/* ---- sent: the climber, on top of it ---------------------------------------
 *
 * Solid, backlit, standing on the lip with one fist up. Everything about the
 * drawing is in the silhouette, which is why there is no shading in it at all:
 * a figure this size holds one shape and nothing else, and every gradient
 * inside it would only soften the edge that shape depends on.
 *
 * PLACED SO THE FEET LAND ON THE LIP, and the offset is in medallion units
 * rather than pixels. The badge is 50px on a phone and 120px in the header
 * band; a figure lifted a fixed 13px stands on the rock at one size and inside
 * it at the other. Every drawing in this sheet that pinned a mark in px has
 * been caught by that, in both directions.
 * -------------------------------------------------------------------------- */
.bdg__mark--sent {
  width:  calc(var(--badge-size) * .26);
  height: calc(var(--badge-size) * .34);
  /* FEET ON THE LIP, and the offset is derived rather than nudged. The rock's
     crown sits at 76% of the face -- bottom:-18% puts the box's foot 18% BELOW
     the face, so with height:42% its top is at 118% - 42% = 76%, and getting
     that subtraction backwards is what put the figure in mid-air twice.
     The mark box is .26 x .34 of the face, which makes ONE unit of the figure's
     26x34 viewBox exactly ONE PERCENT of the face on both axes — a coincidence
     of those two numbers, and a useful one: any point on the drawing can be read
     straight off the SVG. The soles sit at 31.4, so the box centre has to land
     at 76% - (31.4 - 17) = 61.6%, which is 11.6% BELOW the medallion's own
     centre. Eyeballed, this reads as jumping. */
  transform: translate(0, calc(var(--badge-size) * .116));
  -webkit-mask-image: var(--to-figure);
  mask-image: var(--to-figure);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  background: #170c03;
  /* A hair of warm spill around the edge, because a silhouette with a perfectly
     hard edge against a bright ground reads as a shape CUT OUT of the light
     rather than as a person standing in front of it. */
  filter: drop-shadow(0 0 calc(var(--badge-size) * .022) rgba(255,206,122,.75));
  /* In front of the rock they are standing on, which is in front of the light. */
  z-index: 3;
}

/* ---- the grade you were projecting, held above the fist -------------------
 *
 * THE HOLD IS THE SHIPPED `.bdg__hold`, NOT A SECOND DRAWING OF ONE. All eight
 * already live in this sheet for the grade ladder — a fat jug at band 1 down to
 * a micro edge at band 8, each with its own silhouette, grit and bolt hole, all
 * driven off --hold-w / --hold-h / --hold-r / --hold-rot / --bolt-d and coloured
 * by --badge-band. A second copy here would be a second thing to keep in step
 * with Route.GRADE_BANDS and it would drift the first time a band is repainted.
 * This rule only MOVES and SIZES them.
 *
 * IT SITS OUTSIDE THE MARK, IN FACE CO-ORDINATES. The mark is masked to the
 * figure, and a mask on a parent clips its children — a hold nested in there
 * would only paint where it overlapped the climber's own silhouette. As a
 * sibling it is free.
 *
 * ANCHORED BY ITS UNDERSIDE, WHICH IS THE WHOLE RULE. The eight are deliberately
 * different thicknesses (25px at band 1, 9px at band 8), so centring them on one
 * line puts their undersides up to five percent of the medallion apart and the
 * thin ones visibly float higher than the fat ones with every number identical.
 * What the eye measures is the gap between the hold and the hand, so that is
 * what gets pinned: subtracting the visible half-height puts every underside on
 * 41.5% whatever the grade. It reads --hold-h and --hold-scale rather than
 * listing eight numbers, because those heights belong to the grade ladder and a
 * copy of them here would drift silently the first time a hold is reshaped.
 *
 * SCALED, NOT RESIZED. A transform takes the grit, the bolt hole, the rim and
 * all four box-shadows down with the silhouette, in proportion. Overriding
 * --hold-w/--hold-h would flatten the eight into one size and throw away the
 * only thing that makes them readable apart in greyscale.
 * -------------------------------------------------------------------------- */
.bdg-art--topout .bdg__hold {
  position: absolute;
  left: 53%;
  top: calc(41.5% - var(--hold-h, 30px) * var(--hold-scale, .34) / 2);
  transform: translate(-50%, -50%) rotate(var(--hold-rot, -4deg))
             scale(var(--hold-scale, .34));
  /* In front of the climber, who is in front of the rock, which is in front of
     the light. */
  z-index: 4;
}

.bdg--locked.bdg-art--topout .bdg__hold { filter: grayscale(1); opacity: .4; }

/* The gear change, never a switch — both layers were already moving. The wheel
   picks up and the glory breathes faster on the tile the page is about. 22 and
   3.7 share no factor with each other or with the base pair, so a spotlit badge
   does not settle into a rhythm the resting one already had. */
.bdg--spotlight.bdg-art--topout,
.bdg-reveal__badge .bdg-art--topout {
  --to-rays: 22s;
  --to-glow: 3.7s;
}

/* Locked is the one state this badge should feel genuinely cold in: the light
   goes out, the rays stop, and what is left is a dark rock with somebody on top
   of it who has not been lit yet. Not reachable today — the catalogue entry
   carries no teaser ids on purpose — but written for the reason every other
   locked block here is written. */
.bdg--locked.bdg-art--topout .bdg__face::before { filter: grayscale(1) brightness(.42); }
.bdg--locked.bdg-art--topout .bdg__face::after  { animation: none; opacity: .12; filter: grayscale(1); }
.bdg--locked.bdg-art--topout .bdg__art::before  { filter: grayscale(1) brightness(.6); }
.bdg--locked.bdg-art--topout .bdg__art::after   { animation: none; opacity: .1; filter: grayscale(1); }
.bdg--locked .bdg__mark--sent { filter: grayscale(1); opacity: .5; }

/* Motion off, and the badge loses nothing it was carrying. The wheel and the
   swell are both atmosphere on top of a picture that is already complete: the
   light, the rock, the lip and the person standing on it are four static
   layers. The glory is pinned at its own written opacity rather than left
   wherever `animation: none` drops it — the same reason every still form in
   this sheet declares its value on the element and not in a 0% stop. */
@media (prefers-reduced-motion: reduce) {
  .bdg-art--topout .bdg__face::after,
  .bdg-art--topout .bdg__art::after { animation: none; }
  .bdg-art--topout .bdg__art::after { opacity: .82; }
}
