/* =============================================================================
   barakahgames.com — /app

   THE ONE PAGE THAT STAYS FOREST, and that is deliberate. It is reached by
   scanning a QR code off a box by somebody who has already bought, so it
   belongs to the app's world rather than the store's. Do not make it light.
   CLAUDE.md says so too.

   The surface is handed over by class="on-forest" on <html> in the layout, so
   the chrome in site.css — the same header and footer every other page uses —
   renders dark without a second copy of it existing anywhere.

   WHAT THIS FILE IS. The rules /app actually used out of base.css, found by
   testing every selector in that file against this page's DOM rather than by
   reading it: 158 matched, of which the chrome ones are dropped because
   site.css carries them now. The token names are translated from the v1 set
   to the v2 surface layer — a rename per token, not a re-choice, since each
   v2 name is the same role. base.css and tokens-legacy.css are still loaded
   by /support and 404, and go when those move.

   Nothing above the fold waits on JavaScript: the store state is decided on
   the server, the ring is inline SVG baked at build, and the faces are
   self-hosted with font-display: swap.
   ============================================================================= */

/* ---------- The terracotta line ----------
   The same curve as components/TerracottaLine.tsx, not a redraw: the
   app's Catmull-Rom control points converted to cubic Béziers, which
   is exact for tension 0.5. Taken from the invite site, which did the
   conversion. preserveAspectRatio="none" reproduces the app's
   independent width/height scaling, and non-scaling-stroke keeps the
   weight at the app's 1.6 regardless of the box.

   Product-level, not brand-level. It belongs to One Step Closer, so
   it appears on the homepage and the product pages and never in the
   header or the footer. */

.line {
  display: block;
  width: 100%;
  height: clamp(2.25rem, 11vw, 3.5rem);
  margin: 0 auto var(--bg-space-lg);
  overflow: visible;
}
.line path {
  fill: none;
  stroke: var(--s-rule);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  /* pathLength="1" normalises the dash maths, so the draw-on needs
     no measured length. */
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: line-draw calc(var(--bg-motion-duration) * 2.5)
    var(--bg-motion-ease) both;
}
@keyframes line-draw {
  to {
    stroke-dashoffset: 0;
  }
}
/* A line used as a section divider is quieter than the hero's and
   does not redraw on every entry. Sparingly — it loses its power
   if it is everywhere. */
.line--divider {
  height: clamp(1.5rem, 6vw, 2.25rem);
  opacity: 0.7;
  margin-block: var(--bg-space-xl);
}
.line--divider path {
  animation: none;
  stroke-dashoffset: 0;
}
.lede {
  color: var(--s-ink-2);
  font-size: var(--bg-size-body-lg);
}
.hint {
  color: var(--s-ink-3);
  font-size: var(--bg-size-small);
}
/* ---------- Form states ----------
   Three states, one at a time. The success state replaces the form
   rather than sitting beneath it — the app never stacks a result
   under the control that produced it. */

.form-state[hidden] {
  display: none;
}
/* A placeholder is not a label — it vanishes the moment someone types
   and leaves a screen reader with an unnamed field. The label is
   present and hidden, not absent. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* ---- the year screen ----
   Every size is the app's own afs(n) = n x W / 320, so the screen is
   a container query rather than a set of fixed pixels: it stays exact
   at any frame width instead of at one. The fallback is the rule
   solved at the frame's own 17rem, which is its width nearly always. */
.scr {
  --afs: 0.85px;
  --scr-empty: var(--s-hair);
  --scr-ring: var(--s-ink-3);
  padding: calc(var(--afs) * 34) 9.375% 0;
  overflow: hidden;
  font-family: var(--bg-font-body);
  font-weight: 400;
}
/* One class of specificity, exactly. Higher and it outranks .scr__head
   and .scr__ms and eats their margins; at zero it loses to the plain
   `p` rule further up this file, whose margin-bottom then collapses
   against them and wins on size. The spec's vertical rhythm depends on
   this landing between the two. */
.scr :where(p, h3, ul) { margin: 0; }
/* ---- the loop ----
   Three screens, one frame: the year, tonight's card, the night
   sealing, and back to the year one dot fuller.

   The resting state is the year screen alone and finished. Everything
   else is display:none until assets/app.js has proved it can cycle and
   added .can-cycle, so no script, reduced motion, a frame that never
   scrolls into view, or a run that dies halfway all leave a still of
   the same screen that shipped before the loop existed. Nothing here
   is allowed to hide content it cannot bring back. */
.dev .scr--play,
.dev .scr--seal { display: none; }
.dev.can-cycle .scr {
  display: block;
  opacity: 0;
  transform: translateY(var(--bg-motion-rise));
  transition: opacity var(--bg-motion-duration) var(--bg-motion-ease),
              transform var(--bg-motion-duration) var(--bg-motion-ease);
}
.dev.can-cycle .scr.is-on { opacity: 1; transform: none; }
/* ---- screen two · the nightly card ----
   One centred block, one action. Ground only.

   The block centres against the frame, not against the space left over
   above the action — as a 1fr row it lands half the action's height too
   high, which on a screen this sparse is the only thing to look at. */
.scr--play {
  position: relative;
  padding-bottom: calc(var(--afs) * 34);
}
.dev.can-cycle .scr--play { display: grid; place-content: center; }
/* ---- screen three · the seal ----
   The base screen pads 34 at the top and nothing at the bottom, which
   is right for a screen that fills downward and puts a centred disc
   half the padding low. */
.scr--seal { padding-bottom: calc(var(--afs) * 34); }
.dev.can-cycle .scr--seal { display: grid; place-content: center; }
/* The disc lands and the glyph settles a fraction behind it, so it
   presses rather than appears. No overshoot on either: the app has a
   little, and nothing on this site bounces. */
.dev.can-cycle .scr--seal.is-on .seal {
  animation: seal-press var(--bg-motion-duration) var(--bg-motion-ease) both;
}
.dev.can-cycle .scr--seal.is-on .seal svg {
  animation: seal-mark var(--bg-motion-duration) 90ms var(--bg-motion-ease) both;
}
@media (prefers-reduced-motion: reduce){
  .dev.can-cycle .scr { transition: none; }
  .dev.can-cycle .scr--seal.is-on .seal,
  .dev.can-cycle .scr--seal.is-on .seal svg { animation: none; }
}
/* The resting state is drawn. close.js rewinds it only once it has
   proved it can play it forward — the animation on the shared .line
   fires at load, which at the foot of the page nobody is present for. */
.close__line path { animation: none; stroke-dashoffset: 0; }
/* =============================================================
   /app — the QR destination, and the page the nav points at.

   Two visitors with opposite needs. The scanner has just opened a box
   they paid for and is thirty seconds from either starting their year
   or giving up; they need the download and never scroll. The browser
   came from the nav, owns nothing, and wants the story. The top of the
   page is the download and everything below it is the story, so
   neither is compromised for the other.

   Ported from the approved mockup: its type scale, spacing and
   breakpoints, reading colour from the token layer rather than the
   inline block the mockup carried so it could run standalone.

   Everything is prefixed .ap- because this page runs a wider measure
   and a larger display scale than the homepage, and the two must not
   leak into each other.
   ============================================================= */

.ap {
  padding-block: clamp(4rem, 9vw, 7.5rem);
}
.ap-wrap {
  width: 100%;
  max-width: 70rem;
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 4vw, 2.5rem);
}
.ap-narrow { max-width: 40rem; margin-inline: auto; }
.ap-center { text-align: center; }
.ap-stack { display: flex; flex-direction: column; gap: 1.25rem; }
.ap-stack--sm { display: flex; flex-direction: column; gap: 0.625rem; }
/* This page's display scale is larger than the homepage's — the
   headline is the whole hero here, where the homepage's shares the
   frame with cards. */
.ap h1,
.ap h2,
.ap h3 {
  font-family: var(--bg-font-display);
  font-weight: var(--bg-weight-display);
  line-height: 1.14;
  letter-spacing: -0.012em;
  color: var(--s-ink);
  margin: 0;
  text-wrap: balance;
}
.ap h1 { font-size: clamp(2.5rem, 6vw, 4.25rem); }
.ap h2 { font-size: clamp(1.875rem, 4.2vw, 3rem); }
.ap h3 { font-size: clamp(1.125rem, 2vw, 1.5rem); }
/* Gold italic on the second half of a headline. There is no italic
   Playfair in assets/ — the browser synthesises the oblique, which is
   the same compromise the homepage hero already makes. */
.ap em { font-style: italic; color: var(--s-accent); }
/* margin-block, not margin. tokens.css caps every p at the measure and
   its shorthand zeroes the inline margins along with the bottom one —
   so a capped paragraph inside a centred block sits flush left and
   text-align only centres the words inside the box. Resetting the
   block margins alone leaves the inline pair free for the rule below,
   which is what actually centres them. Both rules are one class deep,
   so a shorthand here silently wins on source order.

   :where() so the reset carries no specificity of its own. As a plain
   descendant selector it outranks every per-paragraph rule further
   down this file and quietly ate their margins — the film's closing
   line and the privacy link both lost theirs to it. It still beats the
   bare p in tokens.css, which is all it needs to do. */
.ap :where(p) { margin-block: 0; }
/* Centred blocks, and the two paragraphs that carry .ap-center
   themselves rather than containing it. */
.ap-center p,
p.ap-center { margin-inline: auto; }
/* The measures the mockup set inline on each lede. Without them every
   paragraph falls back to the 34rem global cap, which is close enough
   to look deliberate and wrong enough to change where lines break. */
.ap-pairing__lede p,
.ap-features__lede p,
.ap-screens__lede p { max-width: 32rem; }
.ap-year__lede p { max-width: 30rem; }
.ap-year__caption { max-width: 28rem; }
.ap-film__lede p,
.ap-privacy__body p { max-width: 34rem; }
.ap .lede { font-size: clamp(1rem, 1.5vw, 1.1875rem); color: var(--s-ink-2); }
.ap .hint { color: var(--s-ink-3); font-size: 0.9375rem; }
/* ---------- the download ---------- */

.ap-hero {
  position: relative;
  overflow: hidden;
  padding-block: clamp(2rem, 5vw, 4rem) clamp(3.5rem, 7vw, 5.5rem);
}
.ap-hero__in {
  position: relative;
  z-index: 2;
  max-width: 34rem;
  margin-inline: auto;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 1.375rem;
  align-items: center;
}
/* The ring, framed two ways from one drawing.

   Below 960 it is cropped to a shallow arc above the headline — a full
   ring fights the text at phone widths. At 960 and up the same drawing
   becomes a halo with the content inside it. */
.ap-ring {
  position: relative;
  height: clamp(74px, 20vw, 120px);
  overflow: hidden;
  margin-bottom: clamp(1.5rem, 4vw, 2.25rem);
}
.ap-ring .yr__svg {
  position: absolute;
  left: 50%;
  top: 0;
  width: min(210vw, 1100px);
  transform: translateX(-50%);
  display: block;
}
.yr__empty {
  fill: none;
  stroke: var(--ap-hollow, rgba(248, 245, 240, 0.2));
  stroke-width: 1;
}
.yr__kept { stroke: none; opacity: 0.85; }
.yr__kept--open-book { fill: var(--p-dot-open-book); }
.yr__kept--deen-talk { fill: var(--p-dot-deen-talk); }
.yr__kept--quick-connect { fill: var(--p-dot-quick-connect); }
.yr__kept--ramadan { fill: var(--p-dot-ramadan); }
@media (min-width: 960px){
  .ap-hero {
    min-height: min(84vh, 860px);
    display: grid;
    place-items: center;
  }
  .ap-ring {
    position: absolute;
    left: 50%;
    top: 50%;
    width: min(78vh, 820px);
    height: auto;
    aspect-ratio: 1;
    transform: translate(-50%, -50%);
    margin: 0;
    overflow: visible;
    z-index: 0;
    pointer-events: none;
  }
  .ap-ring .yr__svg {
    position: static;
    width: 100%;
    height: 100%;
    transform: none;
  }
  .ap-hero__in { max-width: 30rem; }
}
.ap-actions {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  justify-content: center;
}
/* The quiet twin of the primary action, for the second store on
   desktop. Same geometry, no fill. */
.ap-btn--quiet {
  background: none;
  color: var(--s-ink);
  box-shadow: inset 0 0 0 var(--bg-hairline) var(--s-hair);
}
.ap-btn--quiet:hover {
  box-shadow: inset 0 0 0 var(--bg-hairline) var(--s-ink-3);
  filter: none;
}
/* ---- One store button, decided before the first paint ----

   snippets/head.liquid stamps plat-ios / plat-android on <html> from an
   inline script ahead of the stylesheets, so the right button is the only one
   ever drawn. A deferred script would paint both and then take one away, and
   on the page a QR code opens that flicker is the first thing anyone sees.
   There is no server-side answer to use instead: Shopify has no user agent at
   render time and is CDN-cached, and the static mirror is built ahead of any
   request.

   No class covers desktop, an unrecognised agent, and a dropped script alike.
   All three show both buttons, which is the honest answer rather than merely
   the safe one. */
.plat-ios [data-app-download]:not(.is-both) [data-store-android],
.plat-android [data-app-download]:not(.is-both) [data-store-ios] {
  display: none;
}

/* The line under the single button that offers the other one. It is only in
   the markup when both stores have a listing, and only shows while one of
   them is hidden — so desktop, which already shows both, never sees it. A
   button rather than a link because it reveals something on this page instead
   of going anywhere. */
.ap-other {
  display: none;
  margin: var(--bg-space-sm) auto 0;
  padding: 0 0 2px;
  border: 0;
  border-bottom: var(--bg-hairline) solid var(--s-hair);
  background: none;
  font: inherit;
  font-size: 0.9375rem;
  color: var(--s-ink-3);
  cursor: pointer;
}
.ap-other:hover { color: var(--s-ink-2); border-bottom-color: var(--s-ink-3); }
.plat-ios [data-app-download]:not(.is-both) .ap-other,
.plat-android [data-app-download]:not(.is-both) .ap-other { display: block; }

/* Both labels ship and the platform class picks, which is what keeps the
   choice in the pre-paint layer instead of handing it back to a script. */
.ap-other__ios,
.ap-other__android { display: none; }
.plat-ios .ap-other__ios { display: inline; }
.plat-android .ap-other__android { display: inline; }

/* Universal Links do not fire inside the Instagram and Gmail in-app
   browsers, so a real share of visitors land here with the app already
   installed. One recessive line rescues them. */
.ap-recessive {
  font-size: 0.9375rem;
  color: var(--s-ink-3);
  text-decoration: none;
  border-bottom: var(--bg-hairline) solid var(--s-hair);
  padding-bottom: 2px;
}
.ap-recessive:hover { color: var(--s-ink-2); border-bottom-color: var(--s-ink-3); }
.ap-hero .waitlist { width: 100%; }
.ap-note { max-width: 28rem; }
/* ---------- pairing ---------- */

.ap-three { display: grid; gap: 2rem; grid-template-columns: 1fr; }
@media (min-width: 760px){
  .ap-three { grid-template-columns: repeat(3, 1fr); gap: 2.5rem; }
}
/* Hairline-topped steps, not a numbered wizard — a wizard reads as
   instructions for a difficult thing, and this is not difficult. */
.ap-step {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  padding-top: 1.25rem;
  border-top: var(--bg-hairline) solid var(--s-hair);
}
.ap-step .ap-n {
  font-weight: var(--bg-weight-display);
  /* 18px is under the 18.66 where --s-accent stops passing on forest —
     measured 3.6 to one against 5.23 for --s-ink-2. Same rule as the step
     numerals on the product pages. Do not put the gold back. */
  font-family: var(--bg-font-display);
  font-style: italic;
  color: var(--s-ink-2);
  font-size: 1.125rem;
}
.ap-step p { font-size: 0.9375rem; }
/* ---------- what it does ---------- */

.ap-defs { display: grid; gap: 0 3rem; grid-template-columns: 1fr; }
@media (min-width: 820px){
  .ap-defs { grid-template-columns: 1fr 1fr; }
}
.ap-def {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding-block: 1.125rem;
  border-bottom: var(--bg-hairline) solid var(--s-hair);
}
.ap-def dt {
  font-weight: var(--bg-weight-display); font-family: var(--bg-font-display); font-size: 1.0625rem; color: var(--s-ink); }
.ap-def dd { margin: 0; font-size: 0.9375rem; color: var(--s-ink-2); }
/* ---------- the year ---------- */

.ap-split { display: grid; gap: 2.5rem; grid-template-columns: 1fr; align-items: start; }
@media (min-width: 900px){
  .ap-split { grid-template-columns: 1fr 1fr; gap: 4rem; }
}
.ap-panel {
  border: var(--bg-hairline) solid var(--s-hair);
  border-radius: 22px;
  background: var(--s-ground-2);
  padding: clamp(1.25rem, 2.5vw, 1.875rem);
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.ap-stats { display: flex; gap: 1rem; justify-content: space-between; flex-wrap: wrap; }
.ap-stat b {
  font-family: var(--bg-font-display);
  font-weight: var(--bg-weight-display);
  font-size: clamp(1.5rem, 2.6vw, 1.875rem);
  color: var(--s-ink);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  display: block;
}
.ap-stat b i { font-style: normal; font-size: 0.9375rem; color: var(--s-ink-3); }
.ap-stat .label { font-size: 0.5rem; margin-top: 0.375rem; }
.ap-rule { height: var(--bg-hairline); background: var(--s-hair); border: 0; margin: 0; }
.ap-grid { display: grid; grid-template-columns: repeat(19, 1fr); gap: clamp(3px, 0.55vw, 6px); }
.ap-dot { width: 100%; aspect-ratio: 1; border-radius: var(--bg-radius-full); }
.ap-dot--o { background: var(--p-dot-open-book); }
.ap-dot--d { background: var(--p-dot-deen-talk); }
.ap-dot--q { background: var(--p-dot-quick-connect); }
.ap-dot--r { background: var(--p-dot-ramadan); }
.ap-dot--empty { background: none; border: var(--bg-hairline) solid var(--ap-hollow, rgba(248, 245, 240, 0.15)); }
.ap-legend { display: flex; gap: 1.125rem; flex-wrap: wrap; }
.ap-leg {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  font-size: 0.8125rem;
  color: var(--s-ink-2);
  font-variant-numeric: tabular-nums;
}
.ap-leg i { width: 9px; height: 9px; border-radius: var(--bg-radius-full); display: block; }
/* The legend reads the ring above it, so it takes the same palette. The bare
   --p-open-book names are the pastel card-back inks in the v2 tokens; on
   forest they wash out and stop matching the dots they are labelling. */
.ap-leg--o i { background: var(--p-dot-open-book); }
.ap-leg--d i { background: var(--p-dot-deen-talk); }
.ap-leg--q i { background: var(--p-dot-quick-connect); }
.ap-leg--r i { background: var(--p-dot-ramadan); }
/* ---------- the film ---------- */

.ap-film {
  background: var(--s-ground-2);
  border-block: var(--bg-hairline) solid var(--s-hair);
}
/* Sealed frames as discs, one of them opening. Masked at both edges so
   the reel reads as a strip continuing past the page rather than a row
   of twenty-five things. */
.ap-reel {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  flex-wrap: nowrap;
  overflow: hidden;
  margin-block: clamp(1.5rem, 4vw, 2.25rem);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
}
.ap-frame {
  width: clamp(20px, 3.2vw, 34px);
  aspect-ratio: 1;
  border-radius: var(--bg-radius-full);
  flex: 0 0 auto;
  opacity: 0.9;
}
.ap-frame--open { outline: 2px solid var(--s-rule); outline-offset: 3px; opacity: 1; }
.ap-stage {
  border: var(--bg-hairline) solid var(--s-hair);
  border-radius: 24px;
  background: rgba(0, 0, 0, 0.1);
  aspect-ratio: 16 / 9;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.125rem;
  text-align: center;
  padding: 1.5rem;
  max-width: 56rem;
  margin-inline: auto;
  position: relative;
  overflow: hidden;
}
.ap-stage__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.ap-mediaring {
  width: 64px;
  height: 64px;
  border-radius: var(--bg-radius-full);
  border: var(--bg-hairline) solid var(--s-hair);
  display: grid;
  place-items: center;
}
.ap-mediaring span {
  font-size: 0.5rem;
  letter-spacing: 0.28em;
  color: var(--s-ink-3);
  text-transform: uppercase;
}
.ap-stage p,
.ap-slot p {
  font-size: 0.6875rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--s-ink-3);
  line-height: 1.9;
}
/* ---------- screens ---------- */

.ap-slots { display: grid; gap: 1.25rem; grid-template-columns: 1fr; }
@media (min-width: 640px){
  .ap-slots { grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
}
.ap-slot {
  border: var(--bg-hairline) solid var(--s-hair);
  border-radius: 20px;
  background: var(--s-ground-2);
  aspect-ratio: 9 / 16;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  text-align: center;
  padding: 1.5rem;
  overflow: hidden;
}
.ap-slot img { width: 100%; height: 100%; object-fit: cover; display: block; margin: -1.5rem; }
/* ---------- the vertical rhythm the mockup set inline ----------
   Each of these was a style attribute in the mockup. Porting the
   markup into sections turned them into class names, and the classes
   were never written — so a subhead sat hard against the rows under
   it, and the section that was meant to tuck under privacy carried a
   full section's worth of top padding instead of none. */

/* A centred head over the block it introduces. Pairing gets more room
   than the others: three columns need more separation from the words
   above them than a list of rows does. */
.ap-pairing__head { margin-bottom: clamp(2.5rem, 5vw, 3.5rem); }
.ap-features__head,
.ap-screens__head { margin-bottom: clamp(2rem, 4vw, 3rem); }
/* Sits under the stage rather than in the stack above it. */
.ap-film__closing { margin-top: 1.5rem; }
/* The curve needs air above the label, and the link needs a beat after
   the paragraph so it does not read as another line of it. */
.ap-privacy__label { margin-top: 1rem; }
.ap-privacy__link { margin-top: 0.5rem; }
/* .line--divider carries 3.5rem block margins for the document pages.
   Here the stack's own gap does that work and the divider's margins
   double it. Kept for the rest of the variant: it does not redraw. */
.ap-line { margin-block: 0; }
/* Follows privacy closely rather than opening a new section. */
.ap-help { padding-block: 0 clamp(3rem, 7vw, 5rem); }
/* ---------- privacy ---------- */

.ap-line { display: block; width: min(100%, 22rem); height: auto; margin-inline: auto; }
.ap-line path { fill: none; stroke: var(--s-rule); stroke-width: 1.8; stroke-linecap: round; }
/* The hidden attribute has to win.

   It sets display:none from the UA stylesheet at the lowest possible
   specificity, so any author display rule beats it — and .ap-actions
   is display:flex. Without this, hiding the store row does nothing and
   an Android phone is shown an App Store button it cannot use. That is
   the one failure this page must not have. */
[hidden] { display: none !important; }
/* ---------- entry ----------
   The hero only, and it fires on load rather than on an observer.
   Nothing on this page may sit at opacity 0 waiting to be scrolled to:
   the one thing above the fold is the action, and an action that has
   not appeared yet is an action nobody takes.

   No fill mode, deliberately. With backwards or both, an animation
   whose clock never advances — a throttled tab, a renderer that has
   stopped compositing — holds the opening frame, and the opening frame
   of a fade is an invisible download button. With no fill the element
   falls back to its own style, which is the finished one. */
@keyframes ap-rise {
  from { opacity: 0.001; transform: translateY(var(--bg-motion-rise)); }
  to   { opacity: 1; transform: none; }
}
.ap-enter { animation: ap-rise 600ms var(--bg-motion-ease); }
@media (prefers-reduced-motion: reduce){
  .ap-enter { animation: none; }
}
.pd-wrap .lede { font-size: clamp(1rem, 1.4vw, 1.1875rem); color: var(--s-ink-2); }
.pd-wrap .hint { color: var(--s-ink-3); font-size: 0.9375rem; }
.ab-wrap .hint { color: var(--s-ink-3); font-size: 0.9375rem; }

/* The hollow ring and the empty dots take a share of the ink rather than a
   flat grey, so they follow the surface. Both usages carry a literal fallback,
   which is what renders where color-mix does not — these @supports blocks were
   in base.css and the extractor, which only walked @media, left them behind. */
@supports (color: color-mix(in srgb, red, blue)) {
  .ap-hero { --ap-hollow: color-mix(in srgb, var(--s-ink) 20%, transparent); }
  .ap-panel { --ap-hollow: color-mix(in srgb, var(--s-ink) 15%, transparent); }
}
