/* ============================================================================
   "What you get" bento — startup-program page

   Everything is namespaced under .sp-bento so nothing can leak into Webflow's
   site-wide stylesheet. Colours and type come from the tokens already defined
   in css/site.css :root.

   Ordering matters: the @media blocks live at the very END of this file. Rules
   appended after them would silently win over the breakpoint overrides.
   ========================================================================== */

.sp-bento {
  /* one step paler than --semantic--c-stroke (#e0e0e0): at 0.75px the darker
     token read as a drawn line rather than a hairline. */
  --sp-ring: var(--primitive--c-neutral-300);
  --sp-ink: var(--semantic--c-foreground);
  --sp-ink-muted: var(--semantic--c-foreground-muted);
  --sp-ground: var(--primitive--c-neutral-100);
  --sp-line: var(--primitive--c-neutral-300);
  --sp-accent: var(--primitive--c-yellow);
  --sp-accent-2: var(--primitive--c-blue);
  --sp-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --sp-dur: 0.7s;
  --sp-brand: linear-gradient(150deg, #5eb3ff, #f58de3 55%, #fdd07b);

  /* The light sweep, defined ONCE for the whole section. Change a colour or
     the duration here and the plate, the map and the cookie chips all follow.
     Lifted from anchor_10.json - see the sweep block further down. */
  --sp-w0: #fff6e6;                      /* warm cream, the core   */
  --sp-w1: #feeafa;                      /* pale pink, the mid band */
  --sp-w2: #cfe7fc;                      /* pale blue, at rest      */
  --sp-wash: var(--sp-w0) 0%, var(--sp-w1) 31%, var(--sp-w2) 100%;
  --sp-wash-dur: 2s;
  background-color: var(--sp-ground);
  padding: 4.5rem 3.75rem;
}

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

.sp-bento-head {
  margin-bottom: 2rem;
}

/* Type lives in css/section-title.css — the one copy shared with .hw-title,
   .wf-title and .sp-title. Only spacing is this section's business. */
.sp-bento-title {
  margin: 0;
}

/* ---------- bands ---------- */

.sp-bento-grid {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.sp-band {
  display: grid;
  gap: 0.5rem;
}

/* band 1: left third splits in half, right two thirds is one full-height box */
.sp-band--1 {
  grid-template-columns: 1fr 2fr;
  grid-template-rows: 1fr 1fr;
}

.sp-box-a { grid-area: 1 / 1; }
.sp-box-b { grid-area: 2 / 1; }
.sp-box-c { grid-area: 1 / 2 / 3 / 3; }

/* band 2: three thirds at two-thirds height, D spanning both rows, G below E+F */
.sp-band--2 {
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: 2fr 1fr;
}

.sp-box-d { grid-area: 1 / 1 / 3 / 2; }
.sp-box-e { grid-area: 1 / 2; overflow: hidden; }
.sp-box-f { grid-area: 1 / 3; overflow: hidden; }
.sp-box-g { grid-area: 2 / 2 / 3 / 4; }

/* ---------- tiles ---------- */

.sp-tile {
  display: grid;
  grid-template-rows: auto 1fr;
  gap: 1rem;
  min-height: 0;
  padding: 1.25rem;
  background-color: var(--primitive--c-white);
  border-radius: 0;
  /* the hairline stays put on hover - deliberately. Everything that reacts to
     hover in this section is inside the tile (the illustrations, the strip, the
     map); darkening the box's own border on top of that read as heavy. */
  box-shadow: inset 0 0 0 0.75px var(--sp-ring);
}

/* grid/flex children must be allowed to shrink below their content width,
   otherwise the 200%-wide month strip forces the whole tile to overflow */
.sp-tile > *,
.sp-vis,
.sp-vis > *,
.sp-strip {
  min-width: 0;
}

.sp-tile-title {
  margin: 0;
  font-family: var(--font--saans);
  font-size: 1.0625rem;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--sp-ink);
}

.sp-tile-desc {
  margin: 0.625rem 0 0;
  font-size: 0.9375rem;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--sp-ink-muted);
}

/* visual rail, pinned to the bottom of the tile */
.sp-vis {
  align-self: end;
  margin-top: auto;
}

/* D, E and F run much taller than their content. Centring inside the leftover
   grid row still reads as low, so the visual is taken out of flow and centred
   on the space below the copy. */
.sp-box-d,
.sp-box-e,
.sp-box-f {
  position: relative;
}

.sp-box-d .sp-vis,
.sp-box-e .sp-vis,
.sp-box-f .sp-vis {
  position: absolute;
  inset: 5.5rem 0 0;
  display: grid;
  place-items: center;
  margin: 0;
  pointer-events: none;
  /* the base rule's align-self:end would stop an abspos grid child from
     stretching, leaving it pinned to the bottom instead of centred */
  align-self: stretch;
  justify-self: stretch;
}

/* C is as tall as A+B stacked, so it gets a third row and centres the number */
.sp-box-c {
  grid-template-rows: auto 1fr auto;
}

.sp-credits-block {
  align-self: center;
}

/* G is wide and short. The copy alone sets the height; the map is out of flow
   so it can run oversized and be clipped by the box. */
.sp-box-g {
  position: relative;
  overflow: hidden;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: 1fr;
  align-items: center;
}

.sp-box-g .sp-tile-copy {
  position: relative;
  z-index: 1;
  max-width: 55%;
}

/* ============================ A. slack lockup ============================= */

.sp-lockup {
  display: flex;
  align-items: center;
}

.sp-appicon {
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  padding: 0.5rem;
  background-color: var(--primitive--c-white);
  box-shadow: inset 0 0 0 0.75px var(--sp-ring);
  transition: transform var(--sp-dur) var(--sp-ease);
}

.sp-appicon img,
.sp-appicon svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.sp-appicon--anchor {
  color: var(--sp-ink);
}

.sp-lockup-join {
  width: 1.5rem;
  height: 1px;
  background-color: var(--sp-ring);
}

.sp-tile:hover .sp-appicon:first-child {
  transform: translateX(0.35rem);
}

.sp-tile:hover .sp-appicon--anchor {
  transform: translateX(-0.35rem);
}

/* ============================ B. captcha ================================== */

.sp-captcha {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1.125rem 1rem;
  background-color: var(--primitive--c-neutral-100);
  box-shadow: inset 0 0 0 0.75px var(--sp-line);
}

.sp-cb {
  position: relative;
  flex: 0 0 auto;
  width: 1.25rem;
  height: 1.25rem;
}

.sp-cb-box {
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 1.5px var(--primitive--c-neutral-500);
  transition:
    opacity 0.4s var(--sp-ease),
    transform 0.4s var(--sp-ease);
}

.sp-cb-tick {
  position: absolute;
  inset: 0.125rem;
  color: var(--sp-accent-2);
  opacity: 0;
  transform: scale(0.4);
  transition:
    opacity 0.45s var(--sp-ease),
    transform 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.sp-cb-tick svg {
  display: block;
  width: 100%;
  height: 100%;
}

.sp-tile:hover .sp-cb-box {
  opacity: 0;
  transform: scale(0.7);
}

.sp-tile:hover .sp-cb-tick {
  opacity: 1;
  transform: scale(1);
  transition-delay: 0.08s;
}

.sp-captcha-label {
  font-size: 0.875rem;
  color: var(--sp-ink-muted);
}

/* ============================ C. credits ================================== */

.sp-credits-num {
  display: block;
  font-family: var(--font--saans);
  font-size: 2.5rem;
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.035em;
  color: var(--sp-ink);
  font-variant-numeric: tabular-nums;
}

.sp-credits-unit {
  display: block;
  margin-top: 0.5rem;
  font-family: var(--font--saans-mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--sp-ink-muted);
}

/* six months on a slide twice the visible width: three show at rest */
.sp-strip {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 88%, transparent 100%);
  mask-image: linear-gradient(to right, #000 0, #000 88%, transparent 100%);
}

.sp-strip-slide {
  display: flex;
  width: 200%;
  transition: transform 1.4s var(--sp-ease);
}

.sp-tile:hover .sp-strip-slide {
  transform: translateX(-50%);
}

.sp-month {
  flex: 1 1 0;
  min-width: 0;
  padding-left: 0.625rem;
}

.sp-month + .sp-month {
  border-left: 1px solid var(--sp-line);
}

.sp-month-name {
  display: block;
  padding-bottom: 0.5rem;
  font-family: var(--font--saans-mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--sp-ink-muted);
}

.sp-month-lines {
  display: flex;
  gap: 2px;
}

.sp-month-lines i {
  flex: 1 1 0;
  min-width: 0;
  height: 3.5rem;
  background-color: var(--c, var(--sp-line));
}

/* ============================ D. isometric browsers ======================= */

/* Everything here sits on a WALL face, not a floor face: skewY keeps the left
   and right edges vertical and slopes the top and bottom, so the panels read as
   screens standing at an isometric angle rather than tiles lying flat. */
.sp-iso {
  --sp-iso-face: skewY(26deg);

  /* THE BOX IS A POSITIONING FRAME, NOT A BOUNDING BOX. The four plates are
     centred on it and then skewed and translated outwards, so the ink runs well
     past 12rem x 8.5rem: measured 166 x 178px at rest, 178 x 190px with the
     hover offsets applied. Above 991px that costs nothing - the drawing is out
     of flow in a tile tall enough to hold it - but anywhere the box has to
     RESERVE the room the drawing needs, 8.5rem under-reserves by 54px and the
     panels cross the card's bottom edge. Both narrow layouts did exactly that;
     the fix is in the media blocks at the end of the file.

     Three tokens carry that, and the narrow layouts SHRINK THE DRAWING to fit
     the room the card already has - the card is never grown to fit the drawing.

     --sp-iso-ink   the hover extent, 190px: what a reserving box must hold.
     --sp-iso-drop  how far the ink's own centre sits BELOW the box's centre -
                    the translates hang 111px down against 79px up - taken back
                    out with a translate on the host so the drawing centres in
                    what was reserved. Shifting the host moves all four plates
                    together, so the isometric plane is untouched; it is the
                    same reason D is scaled rather than retuned.
     --sp-iso-fit   the narrow-layout scale. 0.4 puts the ink at 76 x 71px,
                    which is what fits BOTH slots: at 991 the drawing sits in a
                    96px slot whose bottom edge IS the card's border (13rem row
                    less the 7rem the copy takes), leaving 10px of clearance;
                    at 767 the same 76px is the box's own height, with the
                    card's 1rem of padding under it. Anything past ~0.5 puts
                    the panels back over the 991 card's edge. */
  --sp-iso-ink: 11.875rem;
  --sp-iso-drop: 1rem;
  --sp-iso-fit: 0.4;

  position: relative;
  width: 12rem;
  height: 8.5rem;
}

.sp-iso-plate,
.sp-iso-screen {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 7.5rem;
  height: 5rem;
  margin: -2.5rem 0 0 -3.75rem;
  border-radius: 0.5rem;
  transform: var(--sp-iso-face);
  transition: transform var(--sp-dur) var(--sp-ease);
}

/* ---- the light sweep --------------------------------------------------------

   Straight from Anchor's own motion spec (the `.thumb-stage .pl` demo): an
   oversized radial gradient panned diagonally across the host.

     background-size: 260% 260%
     140% 118%  ->  -40% -22%      2s linear

   Why it cannot jump. radial-gradient(circle) with no explicit size resolves to
   farthest-corner, so the light's radius scales with the host automatically. At
   BOTH ends of the travel the centre sits ~103% of that radius from the plate,
   which means the face is flat #cfe7fc at the start and at the end - the wrap
   swaps two identical frames. Halfway through, the centre passes straight
   through the middle of the host, so the cream core sweeps corner to corner.
   One continuous pass, constant speed, no dead time.

   THIS REPLACED a much more elaborate reconstruction of the same effect built
   from the Lottie keyframes: two stacked layers, a travelling centre driven
   through @property custom properties, and a quadratic bloom cross-fading them.
   It was faithful to the file and wrong to watch - the light swept for 1s, FROZE
   for half a second, then a second light bloomed in from the opposite corner.
   That freeze-and-reappear is what read as a jump. Do not rebuild it.

   The gradient TILES - background-repeat stays at its default. That is not an
   oversight in the spec, it is load-bearing: with no-repeat the area the image
   does not cover falls back to a flat colour, and at the wrap the two endpoints
   then disagree about which corner carries the faint tint. Measured, that made
   the wrap the single worst step in the cycle at 23.8 against ~1 elsewhere.
   Tiling fills the far side with the neighbouring tile's own falloff, which is
   what keeps the loop continuous. */

/* Anything lit by the sweep. Nothing to configure - the gradient sizes itself
   to whatever box it lands on. Resting value is the loop's first frame, so
   starting causes no jump and the host still reads under reduced motion. */
.sp-lit {
  background-image: radial-gradient(circle, var(--sp-wash));
  background-size: 260% 260%;
  background-position: 140% 140%;
  animation: sp-wash var(--sp-wash-dur) linear infinite;
  /* gating pauses rather than removes: removing restarts the sweep from its
     first frame, so an observer toggling near its threshold made the light
     jump every time the tile crossed it */
  animation-play-state: paused;
}

.sp-lit.is-running {
  animation-play-state: running;
}

/* 162.5% per axis, not the spec's 180%/140%. A tiled background repeats exactly
   when it is shifted by a WHOLE tile, and one tile of travel is
   2.6 / 1.6 = 162.5% of position. The spec's numbers come to 1.108 tiles across
   and 0.862 down - close, but not whole, so the pattern does not line up at the
   wrap and the loop steps there. That ratio is fixed by the percentages, so it
   is the same on a square host too. Measured: the spec's numbers made the wrap
   the worst of 400 sampled steps (16 against a median of 0.39); at 162.5% it is
   exactly 0. The direction shifts from 37.9deg to 33.7deg, which is not a
   visible difference; the seam was. */
@keyframes sp-wash {
  from { background-position: 140% 140%; }
  to   { background-position: -22.5% -22.5%; }
}

/* The hero screen. No inset white ring - it was invisible against a white fill
   but would read as a halo inside the hairline now that the face is a gradient. */
.sp-iso-screen {
  display: grid;
  place-items: center;
  outline: 1px solid transparent;
  border-image: var(--sp-brand) 1;
}

.sp-iso-screen::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: 0.5rem;
  padding: 1px;
  background: var(--sp-brand);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

.sp-iso-screen-mark {
  width: 1.5rem;
  color: var(--primitive--c-white);
  line-height: 0;
}

.sp-iso-screen-mark svg {
  width: 100%;
  height: auto;
}

/* the empty ghost plate, offset up and left */
.sp-iso-plate--ghost {
  border: 1px dashed var(--primitive--c-blue);
  opacity: 0.4;
  transform: var(--sp-iso-face) translate(1.5rem, -1.15rem);
}

/* two gradient plates below, reading as coloured shadows */
.sp-iso-plate--shadow-a {
  background: linear-gradient(150deg, #eaf4ff, #f7f2ff 60%, #fff);
  opacity: 1;
  transform: var(--sp-iso-face) translate(1.1rem, 1.5rem);
}

.sp-iso-plate--shadow-b {
  background: var(--sp-brand);
  opacity: 1;
  transform: var(--sp-iso-face) translate(-1.35rem, 1.15rem);
}

.sp-tile:hover .sp-iso-screen {
  transform: var(--sp-iso-face) translateY(-0.4rem);
}

.sp-tile:hover .sp-iso-plate--ghost {
  transform: var(--sp-iso-face) translate(1.9rem, -1.55rem);
}

.sp-tile:hover .sp-iso-plate--shadow-a {
  transform: var(--sp-iso-face) translate(1.45rem, 1.9rem);
}

.sp-tile:hover .sp-iso-plate--shadow-b {
  transform: var(--sp-iso-face) translate(-1.75rem, 1.5rem);
}

/* ========================== E. the security locks ========================= */

/* Three plates light one after another and stay lit, then reset as one. The
   mechanic is borrowed from a Framer Motion SecurityBadge; the palette is not.
   The lit face is the section's own wash - the same --sp-w0/1/2 the hero plate
   sweeps - so the tile introduces no colour of its own, and all three plates
   are ONE hue rather than a saturation ramp. The hairline is the measured
   brand ramp at a 31% midpoint - at MID saturation, see .sp-lock-plate - not
   --sp-brand, which is still the older 55%.

   No captions: .sp-tile-desc carries the meaning and .sp-vis is aria-hidden,
   so the row is three plates and nothing else.

   Gating: js/bento.js runs [data-sp-locks] through loopWhenVisible(), which
   toggles .is-running. The animations are declared PAUSED and merely resume -
   never added and removed, or the row would restart from its first frame every
   time it crossed the observer threshold.

   Corners: 4px - the motion spec's flat-shape radius, where every shape in the
   language is either 4px or a pill. NOT the 0.5rem the plates used to take from
   .sp-iso-plate: at this size the row reads as an edge next to the tile's own
   edge rather than as a drawing, and 8px was too soft for that. The face takes
   3px, i.e. 4px less the 1px frame, the same bookkeeping the radius always had.
   D's isometric plates keep their 8px - a drawing in perspective, not chips.
   -------------------------------------------------------------------------- */

.sp-locks {
  --sp-lock-sz: 3rem;              /* 48px, at every breakpoint */
  --sp-lock-cycle: 4.8s;
  display: flex;
  gap: 1rem;                       /* 3 x 48 + 2 x 16 = 176px */
}

/* The row sits on the cookie's baseline, not on its own centre. E and F carry
   identical .sp-vis boxes and both visuals are centred in them, so the gap
   between the two bottoms is CONSTANT - it does not move with tile height, and
   one flat shift is the whole fix. 3.29rem is the measured drop from that
   shared centre to the cookie's lowest ink (the sp-cookie-side skirt, 52.68px
   at a 16px root); taking off half the row height lands the row's bottom on the
   same line. Written against --sp-lock-sz so it survives a resize of the locks.

   The trailing -23% is an optical correction, not part of the maths: a
   percentage in translateY resolves against the row's OWN height, so it is 23%
   of --sp-lock-sz - 11px at 48px - lifting the row clear of the geometric
   baseline. The cookie ends in a soft skirt and the plates end in a hard edge,
   so a mathematically exact match sits visually low. Dialled by eye in four
   passes, 5 + 8 + 5 + 5. Keep it proportional; do not hard-code the 11px.

   Cancelled at 991px, where the two-column band puts E and F in different rows
   and there is no longer a cookie beside them to align to. */
.sp-box-e .sp-locks {
  transform: translateY(calc(3.29rem - var(--sp-lock-sz) / 2 - 23%));
}

.sp-lock {
  position: relative;
  width: var(--sp-lock-sz);
  height: var(--sp-lock-sz);
  display: grid;
  place-items: center;
  border-radius: 4px;
  background-color: var(--sp-ground);
  box-shadow: inset 0 0 0 0.75px var(--sp-ring);
}

/* No hover lift on the plates - deliberate, and the reason there is no
   `transition: transform` on .sp-lock either. Every other tile answers the
   cursor (A's icons, B's tick, C's strip, D's panels, G's tags), but E already
   has a motion of its own running on a loop, and a second one layered under it
   read as two unrelated things happening at once. If a hover is ever wanted
   back here, it needs to be part of the lock cycle, not a nudge on top of it. */

/* the lit face: the brand hairline carried as 1px of padding, the shared wash
   as the fill it frames.

   The hairline is the MID ramp, not the saturated one. The motion spec sets
   saturation by role - pale for top surfaces, mid for a secondary surface,
   saturated for extrusion and shadow, never the other way round - and this is
   a frame around a top surface, so the saturated ramp read too loud here. It
   is also the exact ramp already drawing the cookie outline in F, which puts
   the two neighbouring tiles on one line. Middle stop stays at 31%. */
.sp-lock-plate {
  position: absolute;
  inset: 0;
  padding: 1px;
  border-radius: 4px;
  opacity: 0;
  background-image: linear-gradient(135deg, #fee4b2, #f9bdef 31%, #6bb4f6);
}

/* the fill is HALF A STEP BELOW --sp-wash: the same three stops taken another
   50% toward white, so the plate reads as a tint under the frame rather than a
   colour of its own. Written out here on purpose - --sp-wash is shared with the
   hero plate's sweep in D (.sp-lit), and paling the token would drag that with
   it. Middle stop still 31%. */
.sp-lock-face {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 3px;
  background-image: radial-gradient(circle, #fffbf3 0%, #fef5fd 31%, #e7f3fe 100%);
}

/* the icon is a FRACTION of the plate, so --sp-lock-sz alone sizes the whole
   drawing: 0.4 x 3rem = 19.2px, which renders the 1.6-unit stroke of a
   24-unit viewBox at 1.28px */
.sp-lock-ico {
  position: relative;
  width: calc(var(--sp-lock-sz) * 0.4);
  height: calc(var(--sp-lock-sz) * 0.4);
  color: var(--sp-ink-muted);
}

/* every bit of paint comes from here - no path in the markup carries a fill or
   stroke attribute, so a stylesheet rule can never lose to an inline one */
.sp-lock-ico path,
.sp-lock-ico rect {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* longhands, not the animation shorthand: a second shorthand on any of these
   selectors would replace what is set here instead of adding to it */
.sp-locks .sp-lock-plate,
.sp-locks .sp-lock-ico,
.sp-locks .sp-lock-sh {
  animation-duration: var(--sp-lock-cycle);
  animation-timing-function: var(--sp-ease);
  animation-iteration-count: infinite;
  animation-play-state: paused;
}

.sp-locks.is-running .sp-lock-plate,
.sp-locks.is-running .sp-lock-ico,
.sp-locks.is-running .sp-lock-sh {
  animation-play-state: running;
}

.sp-lock:nth-child(1) .sp-lock-plate { animation-name: sp-lock-p1; }
.sp-lock:nth-child(1) .sp-lock-ico   { animation-name: sp-lock-i1; }
.sp-lock:nth-child(1) .sp-lock-sh    { animation-name: sp-lock-s1; }
.sp-lock:nth-child(2) .sp-lock-plate { animation-name: sp-lock-p2; }
.sp-lock:nth-child(2) .sp-lock-ico   { animation-name: sp-lock-i2; }
.sp-lock:nth-child(2) .sp-lock-sh    { animation-name: sp-lock-s2; }
.sp-lock:nth-child(3) .sp-lock-plate { animation-name: sp-lock-p3; }
.sp-lock:nth-child(3) .sp-lock-ico   { animation-name: sp-lock-i3; }
.sp-lock:nth-child(3) .sp-lock-sh    { animation-name: sp-lock-s3; }

/* 4.8s: each plate takes 500ms to arrive, they start 800ms apart, all three
   hold from 2.1s, and the reset at 79% is shared - which is what makes the row
   read as one state being built rather than three separate blinks. */
@keyframes sp-lock-p1 { 0%         { opacity: 0 } 10.4%, 79% { opacity: 1 } 87.5%, 100% { opacity: 0 } }
@keyframes sp-lock-p2 { 0%, 16.7%  { opacity: 0 } 27.1%, 79% { opacity: 1 } 87.5%, 100% { opacity: 0 } }
@keyframes sp-lock-p3 { 0%, 33.3%  { opacity: 0 } 43.8%, 79% { opacity: 1 } 87.5%, 100% { opacity: 0 } }

@keyframes sp-lock-i1 { 0%        { color: var(--sp-ink-muted) } 10.4%, 79% { color: var(--sp-ink) } 87.5%, 100% { color: var(--sp-ink-muted) } }
@keyframes sp-lock-i2 { 0%, 16.7% { color: var(--sp-ink-muted) } 27.1%, 79% { color: var(--sp-ink) } 87.5%, 100% { color: var(--sp-ink-muted) } }
@keyframes sp-lock-i3 { 0%, 33.3% { color: var(--sp-ink-muted) } 43.8%, 79% { color: var(--sp-ink) } 87.5%, 100% { color: var(--sp-ink-muted) } }

/* the shackle drops shut as the plate arrives - the px is read in the icon's
   own user units, so it scales with --sp-lock-sz instead of drifting */
@keyframes sp-lock-s1 { 0%        { transform: translateY(-2.6px) } 10.4%, 79% { transform: translateY(0) } 87.5%, 100% { transform: translateY(-2.6px) } }
@keyframes sp-lock-s2 { 0%, 16.7% { transform: translateY(-2.6px) } 27.1%, 79% { transform: translateY(0) } 87.5%, 100% { transform: translateY(-2.6px) } }
@keyframes sp-lock-s3 { 0%, 33.3% { transform: translateY(-2.6px) } 43.8%, 79% { transform: translateY(0) } 87.5%, 100% { transform: translateY(-2.6px) } }
/* ============================ F. cookie =================================== */

/* A line-art cookie, eaten over six frames. Each frame is the real geometry of
   the cookie after another bite - outline, and the skirt that gives it height,
   both cut by the bite - so there is nothing to tween: the sequence is the
   animation. The frames are generated by js/gen_bento.py. */

.sp-cookie {
  display: grid;
  place-items: center;
  height: 6rem;
}

.sp-cookie svg {
  width: 7.5rem;
  height: 7.5rem;
  /* lies in the same isometric plane as the browser panels. Nothing else may
     animate this element's transform or the plane goes with it. */
  transform: matrix(0.866, 0.5, -0.866, 0.5, 0, 0);
}

/* every line on the cookie - top outline and skirt alike - is this one thin
   brand-gradient stroke */
.sp-cookie-edge {
  fill: none;
  stroke: url(#sp-cookie-g);
  /* 0.7 of a viewBox unit, which lands at 1.17px at the rendered 7.5rem. Don't
     go below ~0.6 (exactly 1px): the amber end of the gradient is the lightest
     part of the line and it starts breaking up first on a 1x screen. */
  stroke-width: 0.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The top face: the brand gradient's own three stops washed out on white, so
   the cookie reads as line art with a wash rather than a filled shape. It has
   to stay fully opaque whatever the tone - this fill is also what covers the
   stretch of wall running behind the cookie, and the outline reads as broken
   without it. */
.sp-cookie-face {
  fill: url(#sp-cookie-fill);
}

/* The side of a solid is always a shade deeper than the surface it belongs to.
   This fill is what makes the two layers read as one body: at 4.67px of height
   a corner closing them is a ~3.5px tick and simply does not register, so the
   connection has to be an area instead of a line. */
.sp-cookie-side {
  fill: url(#sp-cookie-side);
}

/* the chips read as light spots punched out of the wash on the top face, at
   half strength so they stay a hint rather than a shape */
.sp-chip {
  fill: #fff;
  fill-opacity: 0.5;
}

.sp-crumb {
  fill: #e7d8f8;
  opacity: 0;
}

.sp-cookie-frame {
  opacity: 0;
}

/* motion off, or the loop not started: the whole cookie, uneaten */
.sp-cookie-frame:first-of-type {
  opacity: 1;
}

/* six slots of 0.9s; each frame owns one and cuts straight to the next */
.sp-cookie.is-running .sp-cookie-frame {
  animation: sp-ck-frame 5.4s linear infinite;
  animation-delay: calc(var(--i, 0) * 0.9s);
}

/* the bite lands: one recoil per slot, on the wrapper so the svg keeps its
   isometric matrix */
.sp-cookie.is-running {
  animation: sp-ck-bite 0.9s var(--sp-ease) infinite;
}

/* crumbs are thrown by their own frame, then fall out of it */
.sp-cookie.is-running .sp-crumb {
  animation: sp-ck-speck 5.4s var(--sp-ease) infinite;
  animation-delay: calc(var(--i, 0) * 0.9s + var(--c, 0) * 0.05s);
}

/* and the cookie fades out and back at the end of the cycle, so going from
   the last stub to a whole cookie again is not a jump cut */
.sp-cookie.is-running svg {
  animation: sp-ck-cycle 5.4s linear infinite;
}

@keyframes sp-ck-frame {
  0%, 16.6%    { opacity: 1; }
  16.61%, 100% { opacity: 0; }
}

@keyframes sp-ck-bite {
  0%        { transform: scale(1.03, 0.97); }
  16%, 100% { transform: scale(1, 1); }
}

@keyframes sp-ck-speck {
  0%        { opacity: 0; transform: translate(0, 0) scale(0.4); }
  3%        { opacity: 1; transform: translate(0, 0) scale(1); }
  11%, 100% { opacity: 0; transform: translate(2px, 2px) scale(0.7); }
}

@keyframes sp-ck-cycle {
  0%, 3%     { opacity: 0; }
  8%, 89%    { opacity: 1; }
  97%, 100%  { opacity: 0; }
}

/* ============================ G. dotted world map ========================= */

/* the map runs larger than the box and the box clips it */
.sp-map {
  position: absolute;
  top: 50%;
  right: -1.5rem;
  width: 26rem;
  transform: translateY(-50%);
  line-height: 0;
}

.sp-map img {
  width: 100%;
  height: auto;
  transition: transform 1.2s var(--sp-ease);
}

.sp-tile:hover .sp-map img {
  transform: scale(1.04);
}

/* region names on light tags, each over its part of the map */
/* .sp-tags is a grid child of the tile, not a child of .sp-map, so that the
   middle band below can turn it into the tile's second row. While the tags
   belong ON the map it mirrors the map's box exactly - same width, same
   aspect-ratio as dotted-map.svg is 540 x 286.16 - which is what keeps every
   --x/--y landing where it did when these were children of .sp-map. */
.sp-tags {
  position: absolute;
  top: 50%;
  right: -1.5rem;
  z-index: 1;
  width: 26rem;
  aspect-ratio: 540 / 286.16;
  transform: translateY(-50%);
  pointer-events: none;
}

.sp-tag {
  position: absolute;
  left: var(--x);
  top: var(--y);
  transform: translate(-50%, -50%);
  padding: 0.2rem 0.45rem;
  border: 1px solid transparent;
  background:
    linear-gradient(#fff, #fff) padding-box,
    linear-gradient(100deg, #5eb3ff, #f58de3 50%, #fdd07b) border-box;
  font-family: var(--font--saans-mono);
  font-size: 0.625rem;
  line-height: 1.4;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sp-ink);
  white-space: nowrap;
  transition: transform var(--sp-dur) var(--sp-ease);
}

.sp-tile:hover .sp-tag {
  transform: translate(-50%, calc(-50% - 2px));
}

/* ---------- responsive (must stay last, before reduced motion) ---------- */

@media screen and (max-width: 991px) {
  .sp-bento {
    padding: 4.5rem 1.875rem;
  }

  .sp-band,
  .sp-band--1,
  .sp-band--2 {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: none;
    grid-auto-rows: minmax(13rem, auto);
  }

  .sp-bento .sp-tile {
    grid-area: auto;
  }

  /* G is a tile like any other here: half the row, not the whole one. It used
     to take `grid-column: 1 / -1`, which left F sitting alone beside an empty
     half and made G the only card in the section that broke the rhythm. */

  /* THE CREDITS BOX IS THE ONE THAT SPANS, and it goes FIRST - here exactly as
     on the phone. Band 1 holds three tiles (A, B, C) and two columns cannot
     divide three, so while C took half a row it left B stranded beside an empty
     half. Full width for C makes the whole section pair up cleanly: C alone on
     row 1, then A+B, then D+E, then F+G.

     On `order`: in source order C is the third child of .sp-band--1, so without
     it C lands on the band's SECOND row, under A and B. `order` works on a grid
     item exactly as it does on a flex one, and band 1 is the first child of
     .sp-bento-grid, so ordering C first inside its own band puts it first in
     the whole section. Same rule, same reason, in the 767 block - keep the two
     in step. */
  .sp-bento .sp-box-c {
    grid-column: 1 / -1;
    order: -1;
  }

  .sp-bento .sp-box-g {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto;
    align-items: start;
  }

  /* THE TAGS COME OFF THE MAP HERE. At half width the map is still an absolute
     backdrop and the copy runs straight across the middle of it - measured at
     900px, two of the four tags sat under the text. So they stop being pins and
     become a plain wrapping row under the copy; the map stays behind as the
     illustration it is. They go back on it at 767, where the map itself drops
     below the text and there is nothing left to collide with. */
  .sp-box-g .sp-tags {
    position: static;
    z-index: 1;
    width: auto;
    aspect-ratio: auto;
    transform: none;
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-top: 0.875rem;
  }

  .sp-box-g .sp-tags .sp-tag {
    position: static;
    transform: none;
  }

  /* the hover lift has to be re-expressed: the base rule nudges a tag that is
     centred on its own -50%/-50% offset, and reusing it here would throw each
     tag half its own size up and left. */
  .sp-tile:hover .sp-tags .sp-tag {
    transform: translateY(-2px);
  }

  /* copy wraps to three lines at this width */
  .sp-box-d .sp-vis,
  .sp-box-e .sp-vis,
  .sp-box-f .sp-vis {
    inset: 7rem 0 0;
  }

  /* D IS STEPPED DOWN HERE, and the card is left alone.

     The slot is 96px - a 13rem row less the 7rem the copy takes - and its
     bottom edge is the card's own border, not its padding. D's ink is 190px
     tall, so at full size the panels hung 83px BELOW that edge (measured at
     900px; D has no `overflow: hidden`, so it spilled rather than cropped).
     Growing the card to fit was tried and reverted: it took the row from 13rem
     to 21.5rem and left D and E towering over F and G. So the drawing is
     scaled to the room instead - the same move the 767 block has always made,
     one breakpoint earlier - and the row keeps its height.

     THE BOX HAS TO SHRINK WITH THE DRAWING, not just the drawing. `scale()`
     leaves the layout box at 12rem x 8.5rem, and a 136px box does not fit a
     96px slot - at which point grid stops centring the oversized item and
     aligns it to START instead, exactly as it does on the horizontal axis at
     767. The box then began at the copy and ran 19.5px low, which put the
     scaled ink 9px under the card's edge: measured, and the reason a plain
     `scale()` was not enough. Reserving the scaled ink as the height keeps the
     box inside the slot, so `place-items: center` centres it and the drawing
     lands with 10px to spare at both ends.

     --sp-iso-drop rides on the scale: `scale()` is listed FIRST so the shift
     that recentres the ink shrinks with the drawing. A transform list resolves
     right to left.

     Both narrow layouts take these two declarations - 767 only adds the width. */
  .sp-iso {
    height: calc(var(--sp-iso-ink) * var(--sp-iso-fit));
    transform: scale(var(--sp-iso-fit)) translateY(calc(var(--sp-iso-drop) * -1));
  }

  /* ALL FOUR OF BAND 2 ON ONE HEIGHT. The band's two rows are content-sized and
     land a hair apart - D|E on the 13rem floor, F|G a pixel over it on G's map
     - which reads as a misalignment straight down the middle of the pair. `1fr`
     as the max of the same minmax resolves every row to the tallest of them in
     an auto-height grid, so the four cards match without a fixed height and
     without the min-height the bands are not allowed to carry. */
  .sp-band--2 {
    grid-auto-rows: minmax(13rem, 1fr);
  }

  /* E and F are in different rows here, so there is no cookie to sit on */
  .sp-box-e .sp-locks {
    transform: none;
  }
}

@media screen and (max-width: 767px) {
  .sp-bento {
    padding: 1.875rem 1.25rem;
  }

  /* TWO up, not one. Both bands take the same two columns, so the whole bento
     reads as one grid even though it is still two separate ones. */
  .sp-band,
  .sp-band--1,
  .sp-band--2 {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: none;
    grid-auto-rows: auto;
  }

  .sp-bento .sp-tile {
    grid-area: auto;
    padding: 1rem;
  }

  /* the credits box is the one that stays full width, and it goes FIRST.
     `order` works on a grid item exactly as it does on a flex one, and C
     already lives in .sp-band--1 - the first child of .sp-bento-grid - so
     ordering it first inside that band puts it first in the whole section.
     This is why the markup needs no change. Do not chase this with a DOM
     reorder in gen_bento.py; the desktop grid depends on the source order.

     It keeps 1.5rem of padding because it is full width; the paired boxes drop
     to 1rem, which is what buys their illustrations the room to fit below. */
  .sp-bento .sp-box-c {
    grid-column: 1 / -1;
    order: -1;
    padding: 1.5rem;
  }

  /* ---- the illustrations have to come down to a 163.5px box ----------------

     Measured at 375px: the bento is 335px wide with an 8px gap, so a paired
     box is 163.5px and 1rem of padding leaves 131.5px inside. At full size the
     drawings are 176px (locks), 207.8px (cookie svg), 192px (iso) - every one
     of them wider than the box. E and F would crop, and D has no
     `overflow: hidden` at all, so it would spill into its neighbour.

     Three different mechanisms below, because the three drawings are built
     differently. Sizes verified in the browser at 375px: 124 / 124.7 / 89.4px,
     each inside its own box, with no horizontal overflow on the page. */

  /* E: one token sizes the whole drawing, by design */
  .sp-locks {
    --sp-lock-sz: 2.25rem;
    gap: 0.5rem;
  }

  /* F: the svg renders 1.732x its own width - the isometric matrix - so 4.5rem
     of svg is 124.7px on screen. This takes the outline down to ~0.7px, which
     still lands on ~2 device pixels at phone DPR; see NOTES on why the skirt
     survives it and an open path would not. */
  .sp-cookie {
    height: 4.5rem;
  }

  .sp-cookie svg {
    width: 4.5rem;
    height: 4.5rem;
  }

  /* D: ten interdependent rem values (plate size, the negative margin, four
     translates, four more on hover). Retuning them all would break the plane;
     scaling the host keeps the isometry exact.

     width:100% is part of the fix, not decoration. Left at 12rem the box is
     wider than the column, grid falls back to START alignment for an oversized
     item, and the drawing lands 30px right of centre and hangs out of the tile
     - scale alone does not move it back, because a transform does not change
     the layout box it is centred from.

     The height and the scale are NOT repeated here - the 991 block above sets
     both and this layout inherits them: 0.4 x 11.875rem is 76px of box holding
     76px of drawing, with the card's 1rem of padding under it. The old pair was
     `6rem` at 0.68, which reserved 96px for 129px of drawing and put the lowest
     panel 5px past the card's edge - the vertical half of the mistake the
     widths were stepped down to fix, and invisible for as long as only the
     widths were checked. Do not raise --sp-iso-fit to make the phone drawing
     bigger: the same token sizes the 991 layout, where the slot ends at the
     card's border rather than at its padding. */
  .sp-iso {
    width: 100%;
  }

  /* and the same one height across the pair, for the same reason as at 991:
     D|E and F|G are separate content-sized rows and settle 24px apart. */
  .sp-band--2 {
    grid-auto-rows: 1fr;
  }

  /* paired boxes size to their content, so the visuals go back in flow */
  .sp-box-d .sp-vis,
  .sp-box-e .sp-vis,
  .sp-box-f .sp-vis {
    position: static;
    inset: auto;
    margin-top: 1.25rem;
  }

  .sp-credits-num {
    font-size: 2.25rem;
  }

  /* G is a normal box here - it pairs with F rather than taking the full row.
     It still grows around the map instead of clipping it. */
  .sp-bento .sp-box-g {
    grid-template-rows: auto auto;
    align-items: start;
  }

  .sp-box-g .sp-tile-copy {
    max-width: none;
  }

  .sp-map {
    position: relative;
    top: auto;
    right: auto;
    width: 100%;
    margin-top: 1.25rem;
    transform: none;
  }

  /* AND BACK ONTO THE MAP. The map is in flow below the copy now, so there is
     nothing left for the tags to collide with.

     The layer is anchored to the TILE, not stacked in a shared grid cell.
     Putting .sp-vis and .sp-tags in the same cell looked tidier but did not
     resolve to the same row, and it left the tag box 24px in and 88px down from
     the map. Anchoring works because at this width the map IS the tile's
     content box: it is width:100% of a .sp-vis that spans the content width,
     and it is the last thing in the tile, so its bottom is the content bottom.
     Bottom-anchored with the map's own aspect, the two boxes coincide by
     construction rather than by matching two independent stacks of offsets. */
  .sp-box-g .sp-tags {
    position: absolute;
    inset: auto 1rem 1rem;
    display: block;
    width: auto;
    aspect-ratio: 540 / 286.16;
    margin: 0;
    transform: none;
  }

  .sp-box-g .sp-tags .sp-tag {
    position: absolute;
    transform: translate(-50%, -50%);
  }

  .sp-tile:hover .sp-tags .sp-tag {
    transform: translate(-50%, calc(-50% - 2px));
  }

  /* The tags are re-laid-out here, and they stop being geographic.

     Their --x/--y are percentages of the map, but their WIDTHS are set by the
     text, so they do not shrink with it. At the two-up width the map is 131.5px
     and the four tags total ~177px: measured, EU/Asia/Australia all overlapped
     each other. Nothing about font-size fixes that - dropping to 8px still
     collides, and anything smaller stops being readable.

     So below 767px they sit in a 3 + 1 arrangement instead: US / EU / Asia
     across the top, Australia centred underneath. At 131px the dotted map reads
     as texture rather than as a place, so the positions are carrying "four
     regions" and not "these coordinates" - which is what the copy says anyway.

     `!important` is required, and is not lazy: --x/--y are set in inline style
     attributes by gen_bento.py, and an inline custom property beats a
     stylesheet declaration on specificity alone. nth-of-type counts spans, so
     the <img> is skipped and 1..4 are the tags in source order. */
  .sp-box-g .sp-tag:nth-of-type(1) { --x: 16% !important; --y: 30% !important; }
  .sp-box-g .sp-tag:nth-of-type(2) { --x: 50% !important; --y: 30% !important; }
  .sp-box-g .sp-tag:nth-of-type(3) { --x: 84% !important; --y: 30% !important; }
  .sp-box-g .sp-tag:nth-of-type(4) { --x: 50% !important; --y: 72% !important; }
}

/* ---------- reduced motion ---------- */

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

  /* with the animation off, the locks' first frame is an empty plate. Their
     resting state is the finished one: all three lit. */
  .sp-bento .sp-lock-plate {
    opacity: 1;
  }

  .sp-bento .sp-lock-ico {
    color: var(--sp-ink);
  }
}
