/* Self-hosted Baloo 2 (SIL Open Font License 1.1) — variable weight axis 400-800.
   Self-hosting removes 2 cross-origin preconnects + a render-blocking stylesheet
   from every page. font-display:swap keeps text visible while it loads. */
@font-face{
  font-family:"Baloo 2";
  src:url("/https/pushup.quest/fonts/baloo2-latin.woff2") format("woff2");
  font-weight:400 800;
  font-style:normal;
  font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

:root{
  --bg:#0D0A1B; --bg-deep:#05030E; --surface:#181327; --surface-2:#120E20; --surface-3:#231C39;
  --ink:#F1ECDF; --ink-2:#A99FC4; --ink-3:#8B82A8;
  --brand:#F2764D; --brand-edge:#B8431F; --gold:#FFC94A; --vigor:#5FD08C; --mana:#6BA8FF; --arcane:#A98CFF;
  /* Fill for white text (primary CTA, step numerals, skip link). Deliberately NOT --brand:
     --brand is a foreground on dark surfaces (links, eyebrows, markers) where it already passes
     AA comfortably, so darkening it there would be a regression. The old fill was
     #FF8A5C -> #F2764D, which measures 2.32:1 at its top stop against white — about half the
     4.5:1 AA needs at 15px/600. These stops are the lightest pair that keeps white above 4.5:1
     at EVERY point of the gradient (4.68:1 top, 5.01:1 mid, 5.44:1 bottom), not merely at the
     darkest end. Recompute the whole ramp, not just one stop, before changing these. */
  --btn-top:#C74C24; --btn-bottom:var(--brand-edge);
  --border:rgba(255,255,255,.08); --border-2:rgba(255,255,255,.14);
  --r:14px; --r-lg:20px; --maxw:1080px;
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
  --display:"Baloo 2",var(--sans);
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{font-family:var(--sans);background:var(--bg);color:var(--ink);line-height:1.6;-webkit-font-smoothing:antialiased;
  background-image:radial-gradient(1200px 600px at 50% -200px,rgba(242,118,77,.16),transparent 70%),
    radial-gradient(900px 500px at 90% 10%,rgba(169,140,255,.10),transparent 70%);background-attachment:fixed}
a{color:inherit;text-decoration:none}
img{max-width:100%;display:block}
h1,h2,h3{font-family:var(--display);font-weight:700;line-height:1.1;letter-spacing:-.01em;text-wrap:balance}
/* balance evens a heading's lines so the last is never one stranded word; pretty does the same for
   the last line of running text. The hero headline opts out (.hero h1). */
.faq summary,.rc-title,.section-head p{text-wrap:balance}
.banner p,.post-lede,.invite .invite-hint,.notfound .nf-text{text-wrap:pretty}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 22px}

/* ⚠️ [hidden] IS A UA RULE AND LOSES TO ANY AUTHOR display. This is not a specificity contest —
   author-origin normal declarations beat the UA origin outright, so `.invite .invite-cta{display:flex}`
   makes the hidden attribute inert. That is not theoretical: /duel and /invite both render their
   post-JS success state as the initial state, so a visitor with a broken bout link sees a live
   "Open in the app" CTA that goes nowhere. Someone already hit this once and patched the single
   element (.beta-form[hidden] below), which left every other case broken; I hit it a third time on
   the hero deck this morning. Fix the class of bug, not the instance. */
[hidden]{display:none!important}

/* ⚠️ SC 2.4.11 FOCUS NOT OBSCURED (AA in WCAG 2.2). .nav is position:sticky and 65px tall, and the
   browser will not scroll a focused control clear of it — a 19px inline link or a 24px dot button
   focused near the top of the viewport ends up entirely behind the header, ring and all. You reach
   that state by the most ordinary keyboard flow there is: PageDown, then Tab. The file already knew
   the offset — .post-body h2/h3 carry scroll-margin-top:90px — but only for anchor jumps. */
html{scroll-padding-top:80px}
.muted{color:var(--ink-2)}
.eyebrow{font-family:var(--display);font-weight:600;font-size:13px;letter-spacing:.12em;text-transform:uppercase;color:var(--brand)}

/* nav */
.nav{position:sticky;top:0;z-index:20;backdrop-filter:blur(14px);background:rgba(13,10,27,.72);border-bottom:1px solid var(--border)}
.nav .wrap{display:flex;align-items:center;justify-content:space-between;height:64px}
.brand{display:flex;align-items:center;gap:10px;font-family:var(--display);font-weight:700;font-size:19px}
.brand .mark{width:30px;height:30px;flex:none;filter:drop-shadow(0 3px 7px rgba(242,118,77,.35))}
.nav-links{display:flex;align-items:center;gap:26px;font-weight:500;font-size:15px}
.nav-links a{color:var(--ink-2);transition:color .15s}
.nav-links a:hover{color:var(--ink)}
.btn{display:inline-flex;align-items:center;gap:8px;font-family:var(--display);font-weight:600;font-size:15px;
  padding:11px 20px;border-radius:999px;background:linear-gradient(180deg,var(--btn-top),var(--btn-bottom));color:#fff;
  box-shadow:0 6px 18px rgba(199,76,36,.45),inset 0 1px 0 rgba(255,255,255,.3);transition:transform .12s,box-shadow .12s}
.btn:hover{transform:translateY(-1px);box-shadow:0 10px 26px rgba(199,76,36,.55)}
.btn.ghost{background:var(--surface-3);box-shadow:none;color:var(--ink);border:1px solid var(--border-2)}
/* Phone header: logo, the store button, a menu. The links in .nav-links are hidden here and the
   <details class="nav-menu"> beside them holds the same links in a panel under the bar. The panel is
   opaque: the bar is see-through, and text sliding under a see-through list cannot be read. */
details.nav-menu{display:none}
@media(max-width:640px){
  .nav-links>a:not(.btn){display:none}
  .nav.has-menu .wrap{gap:10px}
  .nav.has-menu .brand{margin-right:auto;white-space:nowrap}
  .nav .btn{white-space:nowrap}
  details.nav-menu{display:block;flex:none}
  .nav-menu summary{list-style:none;display:grid;place-items:center;width:44px;height:44px;border-radius:12px;color:var(--ink);cursor:pointer;-webkit-tap-highlight-color:transparent}
  .nav-menu summary::-webkit-details-marker{display:none}
  .nav-menu summary:focus-visible{outline:2px solid #F2764D;outline-offset:-2px}
  .nav-menu .x,.nav-menu[open] .bars{display:none}
  .nav-menu[open] .x{display:inline}
  /* max-height: at 400% zoom a desktop window is 320 by 256px, and five links hung from a sticky bar
     ran off the bottom with no way to scroll to the last one. */
  .nav-menu-panel{position:absolute;left:0;right:0;top:100%;padding:6px 22px 12px;background:var(--bg);border-bottom:1px solid var(--border-2);box-shadow:0 18px 30px rgba(0,0,0,.45);max-height:calc(100vh - 64px);max-height:calc(100dvh - 64px);overflow-y:auto;overscroll-behavior:contain}
  /* Closed means gone. Chrome searches inside a closed <details> and opens it on a match, so Find in
     page for "Support", or a link to that word, opened the menu instead of going to the text. */
  .nav-menu:not([open]) .nav-menu-panel{display:none}
  .nav-menu-panel a{display:flex;align-items:center;min-height:44px;color:var(--ink-2);font-weight:500;font-size:16px}
  .nav-menu-panel a+a{border-top:1px solid var(--border)}
}
@media(max-width:400px){.nav .btn{padding:10px 14px;font-size:14px}}
/* The three items need 308px and a 352px screen has 308 between its margins, so the squeeze starts
   there. .has-menu keeps it off the bars that hold only a logo (/i, /d). Below 309px (a folded
   phone's cover screen, or heavy page zoom) the wordmark goes and the logo stays: its text is still
   the link's name. */
@media(max-width:352px){.nav.has-menu .wrap{padding-left:14px;padding-right:14px;gap:8px}.nav.has-menu .brand{font-size:16px;gap:7px}.nav.has-menu .btn{padding:10px 12px}}
@media(max-width:308px){.nav.has-menu .brand{font-size:0;gap:0}}

/* hero */
.hero{padding:72px 0 40px}
.hero .wrap{display:grid;grid-template-columns:1.05fr .95fr;gap:40px;align-items:center}
/* ⚠️ THE HEADLINE IS A LOCKUP: "Your pushups are" above the gradient phrase. Balanced, "are" drops
   onto the gradient line, hence text-wrap:wrap; nowrap keeps "the weapon." whole; the 11.2vw cap
   shrinks the type below 340px so line one (290px at 38px) still fits a 320px phone. */
.hero h1{font-size:min(clamp(38px,6vw,60px),11.2vw);margin:14px 0 18px;text-wrap:wrap}
.hero h1 .grad{background:linear-gradient(120deg,var(--gold),var(--brand));-webkit-background-clip:text;background-clip:text;color:transparent;white-space:nowrap}
.hero p.lead{font-size:19px;color:var(--ink-2);max-width:30ch;margin-bottom:18px}
.hero p.lead-fact{font-size:15px;line-height:1.55;color:var(--ink-3);max-width:52ch;margin-bottom:22px}
.hero-cta{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:26px}
.chip{font-size:13px;font-weight:600;color:var(--ink-2);background:var(--surface);border:1px solid var(--border);padding:7px 13px;border-radius:999px}
.chip b{color:var(--ink)}
.hero-art{position:relative;display:grid;place-items:center}
.hero-art .phone{display:block;width:300px;aspect-ratio:900/1955;border-radius:38px;border:1px solid var(--border-2);box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 0 8px rgba(255,255,255,.03);transform:rotate(-3deg)}
/* On narrow screens the hero stacks. Keep the DOM order (eyebrow → headline →
   lead → CTA → screenshot) rather than pulling the art up with order:-1: the
   phone shot is 522px tall, so hoisting it pushed the headline to 739px and
   left small phones with a first screen containing no value proposition. */
@media(max-width:820px){.hero .wrap{grid-template-columns:1fr;text-align:center}.hero p.lead,.hero p.lead-fact{margin-inline:auto}.hero-cta,.chips{justify-content:center}.hero-art{margin-top:34px}.hero-art .phone{width:235px}}

/* sections */
section{padding:60px 0}
.section-head{text-align:center;max-width:620px;margin:0 auto 44px}
.section-head h2{font-size:clamp(28px,4.2vw,40px);margin:10px 0 12px}
.section-head p{color:var(--ink-2);font-size:17px}

/* features */
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
@media(max-width:820px){.grid{grid-template-columns:1fr}}
.card{background:linear-gradient(180deg,var(--surface),var(--surface-2));border:1px solid var(--border);border-radius:var(--r-lg);padding:24px}
.card .ic{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;font-size:22px;margin-bottom:14px;background:var(--surface-3)}
.card h3{font-size:19px;margin-bottom:7px}
.card p{color:var(--ink-2);font-size:15px}

/* shots */
/* ⚠️ THE SCROLLER BOX IS THE COLUMN, NOT THE VIEWPORT. Two wrong versions preceded this one.
   Full-bleed with a flat 22px pad put the first screenshot at x=22 while the page column started
   at x=482, so zooming out read as the section having slid left. Full-bleed with column-matched
   PADDING fixed the left edge and made it worse: 482px of dead gutter on the left, and the strip
   still ran 1124px off the right of a 2000px viewport. Padding cannot fix this because the box
   itself was wrong. Constraining the box centres the band, matches every other section, and keeps
   the overflow inside it where it belongs. Below 1080px max-width is inert, so mobile is untouched
   and the strip is still edge to edge there. */
.shots{display:flex;gap:18px;overflow-x:auto;max-width:min(100%,1440px);margin-inline:auto;
  padding:6px 22px 18px;scroll-padding-inline:22px;
  scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}
/* ⚠️ tabindex="0" WITHOUT A VISIBLE RING IS A NEW FAILURE, NOT A FIX. Making the strip focusable
   satisfies axe's scrollable-region-focusable, but a keyboard user then lands on something with no
   indication they are there, which is 2.4.7 instead. The offset is negative so the ring draws
   inside the scroller rather than being clipped by its own overflow. */
.shots:focus-visible{outline:2px solid #F2764D;outline-offset:-3px;border-radius:8px}
.shots::-webkit-scrollbar{height:0}
.shots img{width:220px;aspect-ratio:900/1955;object-fit:cover;flex:0 0 auto;border-radius:26px;border:1px solid var(--border-2);box-shadow:0 18px 50px rgba(0,0,0,.5);scroll-snap-align:center}

/* steps */
.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;counter-reset:s}
@media(max-width:820px){.steps{grid-template-columns:1fr}}
.step{position:relative;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:26px 22px}
.step::before{counter-increment:s;content:counter(s);font-family:var(--display);font-weight:700;font-size:15px;color:#fff;
  width:30px;height:30px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(180deg,var(--btn-top),var(--btn-bottom));margin-bottom:14px}
.step h3{font-size:18px;margin-bottom:6px}.step p{color:var(--ink-2);font-size:15px}

/* privacy banner */
.banner{background:linear-gradient(120deg,rgba(95,208,140,.12),var(--surface));border:1px solid rgba(95,208,140,.3);border-radius:var(--r-lg);padding:30px;text-align:center;max-width:760px;margin:0 auto}
.banner h2{font-size:26px;margin-bottom:10px}.banner p{color:var(--ink-2)}

/* footer */
footer{border-top:1px solid var(--border);padding:40px 0;margin-top:30px}
/* ⚠️ :not(.site-footer) IS LOAD-BEARING. The rich footer's grid is a `footer .wrap` too, and
   `footer .wrap` (0,1,1) outranks `.foot-grid` and `.foot-legal` (0,1,0): while this matched both,
   the rich footer never got its grid, its phone columns or its centred legal row. */
footer:not(.site-footer) .wrap{display:flex;justify-content:space-between;align-items:center;gap:18px;flex-wrap:wrap}
/* Wrapped, space-between left logo and copyright at the left above the links. The row wraps below
   736px with six links and below 813px with the homepage's seven, so it is centred up to there. */
@media(max-width:812px){footer:not(.site-footer) .wrap{justify-content:center}}
/* Must wrap: the row is 442px with index.html's seven links and 384px with six, and a 390px phone
   has 346px, so without this those pages scrolled sideways. */
footer .flinks{display:flex;flex-wrap:wrap;justify-content:center;gap:10px 22px;font-size:14px;color:var(--ink-2)}
footer .flinks a:hover{color:var(--ink)}
footer small{color:var(--ink-3)}

/* legal pages */
.legal{max-width:760px;margin:0 auto;padding:50px 22px 70px}
.legal h1{font-size:34px;margin-bottom:6px}
.legal .updated{color:var(--ink-3);font-size:14px;margin-bottom:28px}
.legal h2{font-size:21px;margin:30px 0 10px;color:var(--ink)}
.legal h3{font-size:18px;margin:24px 0 8px;color:var(--ink)}
.legal p,.legal li{color:var(--ink-2);font-size:16px;margin-bottom:12px}
.legal ul,.legal ol{padding-left:22px}
.legal a{color:var(--brand)}
.legal a:hover{color:var(--gold)}
/* A link in running text needs a cue besides colour: --brand on --ink-2 text is 1.13:1, so in
   greyscale these were plain words. Same underline as .post-body a. */
.legal a,.faq details p a,.author-box span a,.banner a,.invite-hint a,.consent a{text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px}
.legal strong,.legal b{color:var(--ink)}

/* skip link (a11y) */
.skip-link{position:absolute;left:-9999px;top:0;z-index:100;background:linear-gradient(180deg,var(--btn-top),var(--btn-bottom));color:#fff;padding:10px 16px;border-radius:0 0 10px 0;font-family:var(--display);font-weight:600}
.skip-link:focus{left:0}
/* Screen-reader-only text (status messages). Not display:none: that hides it from them too. */
.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}

/* store glyphs + coming-soon note in CTA */
.btn .apple,.btn .gplay{width:17px;height:17px;fill:currentColor;flex:none;margin-top:-2px}
.cta-note{font-family:var(--display);font-weight:600;font-size:13px;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-2);margin-bottom:12px}
.cta-note b{color:var(--brand)}
button.btn{border:0;cursor:pointer;-webkit-appearance:none;appearance:none}

/* android beta waitlist form */
.beta-form{display:flex;flex-wrap:wrap;gap:10px;align-items:center;max-width:440px;margin:14px 0 0}
.beta-form[hidden]{display:none}
.beta-hp{position:absolute!important;left:-9999px!important;width:1px;height:1px;opacity:0}
.beta-hint{flex-basis:100%;margin:0;font-size:13px;line-height:1.45;color:var(--ink-2)}
.beta-hint b{color:var(--ink)}
.beta-status{flex-basis:100%;margin:0;font-size:13px;color:var(--ink-2)}
.beta-status.err{color:var(--brand)}
.beta-done{max-width:440px;margin:14px 0 0;color:var(--vigor);font-weight:600;font-size:15px}
.beta-done-sub{max-width:440px;margin:6px 0 0;font-size:14px;color:var(--ink-2)}
.beta-done-sub a{color:var(--brand);font-weight:600}

/* faq */
.faq{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:12px}
.faq details{background:linear-gradient(180deg,var(--surface),var(--surface-2));border:1px solid var(--border);border-radius:var(--r)}
.faq summary{list-style:none;cursor:pointer;padding:18px 20px;font-family:var(--display);font-weight:600;font-size:17px;display:flex;justify-content:space-between;align-items:center;gap:14px}
.faq summary::-webkit-details-marker{display:none}
/* The second content gives the glyph an empty text alternative, so the question is not read out
   with a trailing "plus". Keep it second: an engine that rejects the slash form keeps the first. */
.faq summary::after{content:"+";content:"+" / "";font-family:var(--display);font-weight:700;font-size:22px;line-height:1;color:var(--brand);transition:transform .2s}
.faq details[open] summary::after{transform:rotate(45deg)}
.faq details p{padding:0 20px 18px;margin:0;color:var(--ink-2);font-size:15px}
.faq details p a{color:var(--brand)}

/* 404 */
.notfound{max-width:600px;margin:0 auto;padding:80px 22px 110px;text-align:center}
.notfound .nf-logo{width:120px;height:120px;margin:0 auto 22px;border-radius:28px;box-shadow:0 18px 50px rgba(0,0,0,.5)}
.notfound h1{font-size:clamp(30px,5vw,44px);margin:8px 0 14px}
.notfound .nf-text{color:var(--ink-2);font-size:17px;margin-bottom:28px}
.notfound .nf-cta{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}

/* invite landing (/i/<code>) */
.invite{max-width:560px;margin:0 auto;padding:60px 22px 90px;text-align:center}
.invite .invite-logo{width:112px;height:112px;margin:0 auto 20px;border-radius:26px;box-shadow:0 18px 50px rgba(0,0,0,.5)}
.invite h1{font-size:clamp(28px,5vw,40px);margin:8px 0 14px}
.invite .invite-text{color:var(--ink-2);font-size:16px;max-width:42ch;margin:0 auto 22px}
.invite .invite-code{display:inline-flex;align-items:center;gap:10px;background:var(--surface);border:1px solid var(--border-2);border-radius:999px;padding:7px 7px 7px 20px;margin-bottom:26px}
.invite .invite-code .code{font-family:var(--display);font-weight:800;font-size:22px;letter-spacing:.18em;color:var(--gold)}
.invite .code-copy{font-family:var(--display);font-weight:600;font-size:13px;color:var(--ink);background:var(--surface-3);border:1px solid var(--border-2);border-radius:999px;min-height:44px;padding:0 18px;cursor:pointer;transition:background .15s}
.invite .code-copy:hover{background:var(--surface-2)}
/* Shown when no copy method worked; pulled up under the pill's 26px bottom margin. */
.invite .copy-help{margin:-14px 0 20px}
.invite .invite-cta{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-bottom:22px}
.invite .invite-hint{color:var(--ink-3);font-size:14px}
.invite .invite-hint + .invite-hint{margin-top:10px}
.invite .invite-hint b{color:var(--ink-2)}
.invite .invite-hint a{color:var(--brand)}
.invite .invite-hint a:hover{color:var(--gold)}

/* stats ledger */
.ledger{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;max-width:920px;margin:0 auto}
.ledger .led{background:linear-gradient(180deg,var(--surface),var(--surface-2));border:1px solid var(--border);border-radius:var(--r-lg);padding:22px 16px;text-align:center}
.ledger .led .v{font-family:var(--display);font-weight:800;font-size:34px;line-height:1;background:linear-gradient(120deg,var(--gold),var(--brand));-webkit-background-clip:text;background-clip:text;color:transparent}
.ledger .led .l{font-family:var(--display);font-weight:600;font-size:14px;margin-top:8px}
.ledger .led .g{color:var(--ink-3);font-size:12px;margin-top:3px}
/* Two columns down to the narrowest phone, the fifth card across both (one column took 719px to
   show five numbers). The tighter phone padding keeps the 127px caption on one line at 360. */
@media(max-width:820px){.ledger{grid-template-columns:repeat(2,1fr)}.ledger .led:last-child{grid-column:1/-1}}
@media(max-width:430px){.ledger{gap:10px}.ledger .led{padding:20px 8px}.ledger .led .g{text-wrap:balance}}
/* Below about 328px the hero kicker wraps; balanced, it does not leave "RPG" alone on a line. */
@media(max-width:820px){.hero .eyebrow{display:inline-block;text-wrap:balance}}

/* keyboard focus visibility (a11y) */
:focus-visible{outline:2px solid var(--gold);outline-offset:3px}
.btn:focus-visible{outline-offset:4px}

/* respect reduced-motion preferences */
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important}
}

/* ============================================================
   BLOG / GUIDES — article layout, TOC, cards, standardized footer
   ============================================================ */

/* article shell */
.article{padding:34px 0 40px}
.article .post{max-width:820px}
.crumbs{font-size:13.5px;color:var(--ink-3);margin:0 0 20px;display:flex;flex-wrap:wrap;gap:7px;align-items:center}
.crumbs a{color:var(--ink-2);text-decoration:underline;text-underline-offset:2px}
.crumbs a:hover{color:var(--brand)}
.crumbs span[aria-current]{color:var(--ink-2)}
.crumbs .sep,.crumbs>span:not([aria-current]){color:var(--ink-3)}

.eyebrow{display:inline-block;font-family:var(--display);font-weight:700;font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--brand);margin-bottom:12px}
.post-head h1{font-size:clamp(28px,4.6vw,42px);line-height:1.1;margin:0 0 14px}
.post-lede{font-size:19px;line-height:1.55;color:var(--ink-2);margin:0 0 6px;max-width:66ch}
.post-meta{font-size:14px;color:var(--ink-3);margin:0 0 6px}
.blog-hero{margin-bottom:8px}

/* table of contents */
.post-toc{background:linear-gradient(180deg,var(--surface),var(--surface-2));border:1px solid var(--border);border-radius:var(--r);padding:18px 22px;margin:26px 0 8px}
.post-toc b{font-family:var(--display);font-size:13px;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-3);display:block;margin-bottom:10px}
.post-toc ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}
.post-toc a{color:var(--ink-2);font-size:15px}
.post-toc a:hover{color:var(--brand)}
/* Collapsed variant, <details class="post-toc"><summary>, for a long list (/privacy has 29
   sections). The summary fills the box's padding so the whole card opens it. Inside .legal the
   page's own li and a rules leak in (12px margins, underlines), so they are reset. */
.post-toc summary{cursor:pointer;margin:-18px -22px;padding:18px 22px;border-radius:var(--r);font-family:var(--display);font-weight:700;font-size:13px;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-2)}
.post-toc[open]>summary{margin-bottom:0;padding-bottom:10px}
.legal .post-toc li{margin:0;font-size:15px}
.legal .post-toc a{text-decoration:none}

/* article body typography */
.post-body{font-size:17px;line-height:1.72;color:var(--ink);max-width:72ch}
.post-body>*{margin:0 0 20px}
/* Intro straight into body, no contents box: the lede's 6px was the whole gap. And after a contents
   box, a body that opens on a heading sat 8px under it (first-child zeroes its top margin). */
.post-head + .post-body{margin-top:22px}
.post-toc + .post-body>h2:first-child,.post-toc + .post-body>h3:first-child{margin-top:22px}
.post-body h2{font-size:clamp(22px,3.2vw,28px);line-height:1.2;margin:42px 0 14px;scroll-margin-top:90px}
.post-body h3{font-size:20px;line-height:1.25;margin:30px 0 12px;scroll-margin-top:90px}
.post-body p{color:var(--ink)}
.post-body a{color:var(--brand);text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px}
.post-body a:hover{color:var(--gold)}
/* A button or card in an article body is not an inline link: without these the rule above painted
   the press-kit button's label orange on orange (1.67:1) and underlined the screenshot cards. */
.post-body a.btn,.post-body a.btn:hover{color:#fff;text-decoration:none}
.post-body a.post-card,.post-body a.post-card:hover{color:inherit;text-decoration:none}
.post-body strong{color:var(--ink);font-weight:700}
.post-body ul,.post-body ol{padding-left:24px;color:var(--ink)}
.post-body li{margin:0 0 9px;padding-left:4px}
.post-body li::marker{color:var(--brand)}
.post-body blockquote{border-left:3px solid var(--brand);margin:24px 0;padding:4px 0 4px 20px;color:var(--ink-2);font-size:18px}
.post-body blockquote p{margin:0}
.post-body code{background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:1px 6px;font-size:.9em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.post-body table{width:100%;border-collapse:collapse;font-size:15px}
/* The wrapper scrolls, not the table: a table that is its own scroll box cannot be focused,
   and on a phone the overlay scrollbar stays invisible until a scroll starts, so columns that
   ran off the right edge looked like they did not exist. min-width keeps columns legible
   rather than crushing them, and the right-edge fade says there is more.
   Only for five or more columns: on every table it made two-column tables that fit a phone scroll. */
.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:18px 0;border-radius:10px}
.table-scroll table{margin:0}
.table-scroll:not(.stack) table:has(th:nth-child(5)){min-width:34rem}
.table-scroll:focus-visible{outline:2px solid #F2764D;outline-offset:3px}
@media(max-width:640px){.table-scroll:not(.stack):has(th:nth-child(5)){background:linear-gradient(to left,rgba(242,118,77,.13),transparent 2.2rem)}}
.post-body th,.post-body td{border:1px solid var(--border-2);padding:10px 14px;text-align:left}
.post-body th{background:var(--surface-2);font-family:var(--display);font-weight:700}
/* Phone layouts for tables of sentences, where a sideways scroll loses the row label. Two classes
   each, so they outrank `.post-body td` wherever they sit.
   .stack (set by the build when a cell passes about 40 characters): a row is a card, its first cell
   the title, each other cell under its column name from data-label. The header row is clipped, not
   display:none, so it is still read out.
   .facts (/press key facts, a <th> and a <td> per row): label above value. */
@media(max-width:640px){
  .table-scroll.stack{overflow:visible;background:none}
  .table-scroll.stack thead{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
  .table-scroll.stack table,.table-scroll.stack tbody,.table-scroll.stack tr,.table-scroll.stack td{display:block}
  .table-scroll.stack tr{border:1px solid var(--border-2);border-radius:10px;padding:14px 16px 4px}
  .table-scroll.stack tr+tr{margin-top:12px}
  .table-scroll.stack td{border:0;padding:0 0 10px;overflow-wrap:anywhere}
  .table-scroll.stack td:first-child{font-family:var(--display);font-weight:700;font-size:17px;line-height:1.3}
  .table-scroll.stack td[data-label]::before{content:attr(data-label);display:block;font-family:var(--display);font-weight:600;font-size:12px;line-height:1.4;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-3)}
  .table-scroll.facts{background:none}
  .table-scroll.facts tr,.table-scroll.facts th,.table-scroll.facts td{display:block}
  .table-scroll.facts th{border-bottom:0;padding:8px 14px 2px;font-size:13px}
  .table-scroll.facts td{border-top:0;padding:4px 14px 10px;overflow-wrap:anywhere}
  .table-scroll.facts tr+tr th{border-top:0}
}
.post-body img{max-width:100%;height:auto;border-radius:var(--r)}
.post-body h2:first-child,.post-body h3:first-child{margin-top:0}

/* in-article app CTA */
.post-cta{display:flex;align-items:center;gap:18px;flex-wrap:wrap;background:linear-gradient(135deg,var(--surface-2),var(--surface));border:1px solid var(--border-2);border-radius:var(--r-lg);padding:22px 24px;margin:40px 0}
.post-cta img{width:64px;height:64px;border-radius:16px;flex:0 0 auto}
.post-cta div{flex:1 1 240px}
.post-cta strong{font-family:var(--display);font-size:18px;display:block;margin-bottom:4px}
.post-cta p{margin:0;color:var(--ink-2);font-size:14.5px;line-height:1.5}
/* ⚠️ THE TWO STORE BUTTONS ARE ONE UNIT. As bare flex siblings they wrapped independently: at
   1280px the App Store button sat at x=783 on row one and Google Play dropped to x=277 on row two,
   back under the icon, which read as a broken card. It was on all 39 pages that carry this block,
   not just /about. Wrapping them means they move to a second row together.
   The group must stay shrinkable (0 1 auto, min-width:0), never 0 0 auto: the card still breaks
   lines on its unshrunk width, so the pair wraps as one unit, but when the pair alone is wider than
   the card (/about's labels are 408px, a 390px phone has 296px) only a shrinkable group can stack
   its buttons. At 0 0 auto Google Play ran 65px off a 390px screen. */
.post-cta .cta-actions{display:flex;gap:10px;flex-wrap:wrap;flex:0 1 auto;min-width:0;align-items:center}
.post-cta .btn{flex:0 0 auto}

/* FAQ inside article reuses .faq; give it spacing */
/* padding:0 here, on .related and on .cluster: all three are <section>, and the homepage's bare
   `section{padding:60px 0}` added 120px around each. By class, not `.article section`: the
   classless sections of the /stats template need that padding. */
.post-faq{margin:44px 0 0;padding:0}
.post-faq h2{font-size:clamp(22px,3.2vw,28px);margin:0 0 18px}
.post-faq .faq{margin:0;max-width:none}

/* author box (EEAT) */
.author-box{display:flex;gap:16px;align-items:flex-start;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:18px 20px;margin:40px 0 0}
.author-box img{width:44px;height:44px;border-radius:50%;flex:0 0 auto}
.author-box strong{font-family:var(--display);font-size:16px;display:block;margin-bottom:3px}
.author-box span{color:var(--ink-2);font-size:14px;line-height:1.55}

/* related posts */
.related{margin:48px 0 0;padding:0}
.related h2{font-size:22px;margin:0 0 16px}
.related-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
.related-card{display:flex;flex-direction:column;gap:8px;background:linear-gradient(180deg,var(--surface),var(--surface-2));border:1px solid var(--border);border-radius:var(--r);padding:18px;transition:border-color .15s,transform .15s}
.related-card:hover{border-color:var(--border-2);transform:translateY(-2px)}
.rc-kicker{font-family:var(--display);font-size:12px;letter-spacing:.05em;text-transform:uppercase;color:var(--brand)}
.rc-title{font-family:var(--display);font-weight:600;font-size:16px;line-height:1.3;color:var(--ink)}

/* pillar / blog-index cluster cards */
.cluster{margin:56px 0 0;padding:0}
.cluster>h2{font-size:clamp(20px,3vw,26px);margin:0 0 18px}
/* Link to a group's guide, under its heading on /blog and under "Keep reading" on a post. The
   negative top margin tucks it under the heading and only works with vertical-align:top (on the
   baseline the line's own height swallows it). The right margin separates two side by side. */
.cluster-more{display:inline-block;vertical-align:top;padding-block:10px;margin:-20px 16px 6px 0;color:var(--brand);font-family:var(--display);font-weight:600;font-size:15px}
.cluster-more:hover{color:var(--gold)}
/* /blog jump row. Not .chips, which centres below 820px for the homepage hero. Chip look, 44px
   tall because these are tap targets. */
.hub-jump{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-start;margin-top:18px}
.hub-jump a{display:inline-flex;align-items:center;min-height:44px;padding:7px 13px;border-radius:999px;font-size:13px;font-weight:600;color:var(--ink-2);background:var(--surface);border:1px solid var(--border)}
.hub-jump a:hover{color:var(--ink);border-color:var(--border-2)}
/* On a phone the nine pills wrapped to six rows (304px at 390 wide, eight rows at 320) and pushed
   every card off the first screen. There they are one row that scrolls sideways and runs to both
   screen edges; the pill cut off at the right edge, fading out, is what says there are more.
   overflow-x:auto makes overflow-y clip too, so the 6px of padding is room for the focus ring. */
@media(max-width:640px){.hub-jump{flex-wrap:nowrap;overflow-x:auto;margin:12px -22px -6px;padding:6px 22px;scroll-padding-inline:22px;scrollbar-width:none;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 28px),transparent)}.hub-jump::-webkit-scrollbar{display:none}.hub-jump a{flex:none}}
.card-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}
.post-card{display:flex;flex-direction:column;gap:8px;background:linear-gradient(180deg,var(--surface),var(--surface-2));border:1px solid var(--border);border-radius:var(--r-lg);padding:22px;transition:border-color .15s,transform .15s}
.post-card:hover{border-color:var(--border-2);transform:translateY(-2px)}
.post-card h3{font-family:var(--display);font-weight:700;font-size:18px;line-height:1.25;margin:0}
.post-card p{margin:0;color:var(--ink-2);font-size:14.5px;line-height:1.5}
.pc-more{margin-top:auto;font-family:var(--display);font-weight:600;font-size:14px;color:var(--brand)}

/* standardized site footer (blog/guides) */
.site-footer{border-top:1px solid var(--border);margin-top:60px;padding:48px 0 30px}
.foot-grid{display:grid;grid-template-columns:1.5fr 1fr 1fr;gap:36px;align-items:start}
.foot-brand .brand{margin-bottom:12px}
.foot-brand p{color:var(--ink-2);font-size:14.5px;line-height:1.55;max-width:34ch;margin:0 0 16px}
/* The store buttons are bare inline siblings: stacked on a phone they touched. Right margin on the
   first only; on both, the long-label pair needs 417px of a 413px column and stacks at 1440px. */
.foot-brand .btn{margin-bottom:10px}
.foot-brand .btn:not(:last-child){margin-right:8px}
/* Captions are <p class="foot-h"> (an <h4> after an <h2> skipped a level); h4 is kept for old markup. */
.foot-col h4,.foot-col .foot-h{font-family:var(--display);font-weight:700;font-size:13px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);margin:0 0 14px}
.foot-col{display:flex;flex-direction:column;gap:10px;align-items:flex-start}
.foot-col a{color:var(--ink-2);font-size:15px}
.foot-col a:hover{color:var(--brand)}
.foot-legal{margin-top:36px;padding-top:20px;border-top:1px solid var(--border)}
.foot-legal small{color:var(--ink-3)}

@media(max-width:780px){
  .related-grid{grid-template-columns:1fr}
  .card-grid{grid-template-columns:1fr}
  /* Brand across the top, link columns under it. Not 1fr 1fr: that wraps "Beginner progression" at 360px. */
  .foot-grid{grid-template-columns:3fr 2fr;gap:20px 24px}
  .foot-brand{grid-column:1/-1}
  .post-body{font-size:16px}
}

/* App Store rating (hero) — kept in sync with the live store by scripts/refresh-app-meta.mjs */
/* A wrapping flex row: the gap is the separator, so no dot dangles when the second figure wraps. */
.hero p.rating-note{font-size:14px;line-height:1.5;color:var(--ink-2);margin:-14px 0 20px;display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 12px}
.hero p.rating-note .stars{color:var(--gold);letter-spacing:1px;font-size:15px}
.hero p.rating-note b{color:var(--ink);font-weight:700}
/* Each store's figure is one unit: at 375px the line broke between "App Store worldwide" and its
   own count, which reads as the count belonging to the store named after it. Not nowrap: at 320px
   the 303px figure pushed the whole hero past the edge. This way it wraps inside itself instead. */
.hero p.rating-note .pair{display:inline-block;text-wrap:balance}

/* /stats: the one number each section exists for. Same gradient as the hero headline, because it is
   the same kind of claim — the difference is that this one is measured. */
.post-body p.stat-hero{font-size:clamp(34px,7vw,54px);line-height:1.05;margin:10px 0 2px}
.post-body p.stat-hero b{font-weight:800;background:linear-gradient(120deg,var(--gold),var(--brand));-webkit-background-clip:text;background-clip:text;color:transparent}
@media(max-width:820px){.hero p.rating-note{justify-content:center}}

/* medical disclaimer under the author box (YMYL trust signal) */
.med-disclaimer{max-width:820px;margin:14px 0 0;padding:14px 18px;border-left:3px solid var(--ink-3);background:var(--surface-2);border-radius:0 var(--r) var(--r) 0;font-size:14px;line-height:1.55;color:var(--ink-2)}
.med-disclaimer strong{color:var(--ink)}
.author-box span a{color:var(--brand)}

/* Footer social row. Inline SVG brand marks — no icon font, no extra requests, crisp at any DPR.
   Icon-only links MUST carry aria-label: unlike the store buttons (where an aria-label duplicating
   visible text was a WCAG 2.5.3 failure), these have no text at all, so the label is the only
   accessible name.

   Own line, full width. The simple footer is justify-content:space-between and was designed for
   three groups (brand | links | copyright); squeezing icons in as a fourth left them crammed
   against the copyright with no breathing room. width:100% forces a wrap so line one keeps its
   original three-group rhythm. */
.foot-social{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;order:4;
  margin-top:6px;padding-top:20px;border-top:1px solid var(--border)}
.foot-social a{display:grid;place-items:center;width:36px;height:36px;border-radius:10px;color:var(--ink-3);transition:color .15s,background .15s}
.foot-social a:hover{color:var(--ink);background:var(--surface-3)}
.foot-social a:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
.foot-social svg{display:block}
/* Rich footer: the legal line already has a rule above it, so the social row needs no second
   divider — and copyright and icons belong on ONE row (copyright left, icons right) rather than
   the copyright sitting left with the icons floating centred underneath it. */
.foot-legal{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
.foot-legal .foot-social{border-top:0;padding-top:0;margin-top:0;width:auto;order:0;justify-content:flex-end}
@media(max-width:560px){.foot-legal{justify-content:center}.foot-legal .foot-social{width:100%;justify-content:center}}

/* Press-kit screenshot cards: a journalist picking assets needs to SEE them, and needs the
   dimensions without opening each file. */
.shot-card img{width:100%;height:auto;border-radius:10px;margin-bottom:10px;background:var(--surface)}
.shot-grid{grid-template-columns:repeat(auto-fill,minmax(190px,1fr))}
/* Below 440px auto-fill fell to one column and ten screenshots took half the page. */
@media(max-width:439px){.shot-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.shot-card{padding:10px}}
.pc-meta{display:block;font-size:12px;color:var(--ink-2);margin-top:6px;font-variant-numeric:tabular-nums}
.press-kit-cta{margin:14px 0 10px}

/* Screenshot-strip position dots. The strip scroll-snaps on its own; these say where you are. */
.shot-dots{display:flex;justify-content:center;gap:0;margin:-4px 0 4px}
@media(prefers-reduced-motion:reduce){.deck-dot::before,.shot-dot::before{transition:none}}

/* Hero screen deck — five phones stacked, crossfading. The frame geometry is unchanged from the
   single-image hero: same 300px width, same aspect-ratio, same radius and shadow, so the layout
   does not move. Only which image is visible changes. */
.phone-deck{position:relative;width:300px;aspect-ratio:900/1955;margin:0 auto}
.phone-deck .phone{position:absolute;inset:0;width:100%;height:100%;opacity:0;transition:opacity .7s ease}
/* A <video> defaults to object-fit:contain, an <img> to fill. The encode is 600x1304 (0.46012)
   against the deck's 900/1955 (0.46036) — a 0.05% difference, but contain would still letterbox it
   by a hairline against the dark frame. cover crops that fraction of a pixel instead. */
.phone-deck video.phone{object-fit:cover}
.phone-deck .phone.is-on{opacity:1}
.deck-dots{position:absolute;left:0;right:0;bottom:-34px;display:flex;justify-content:center;gap:0}
@media(max-width:820px){.phone-deck{width:235px}}
@media(prefers-reduced-motion:reduce){
  .phone-deck .phone{transition:none}
}

/* ⚠️ 24×24 MINIMUM TARGET (WCAG 2.5.8). These were 8px buttons 17px apart and genuinely hard to
   hit — reported from a real machine, not from a checklist. The dot you SEE is 6px; it is a ::before
   inside a 24px transparent button, so the target grew and the design did not. Its fill is NOT
   var(--border-2) (rgba(255,255,255,.14)) — that measured 1.44:1 against #0D0A1B and failed 1.4.11,
   which needs 3:1 for a control that is the only evidence the control exists. .34 clears it on the
   flat background and on the brightest point of the hero radial. Do not raise --border-2 itself:
   it is also the border on .btn.ghost, .phone, .shots img and .post-body tables.
   gap:0 leaves adjacent targets exactly 24px apart, which is what the spacing exception wants;
   any gap here would be dead pixels between two things people are trying to click. */
.deck-dot,.shot-dot,.deck-toggle{width:24px;height:24px;padding:0;border:0;background:none;cursor:pointer;
  display:grid;place-items:center;border-radius:999px;-webkit-tap-highlight-color:transparent}
.deck-dot::before,.shot-dot::before{content:"";width:6px;height:6px;border-radius:999px;
  background:rgba(255,255,255,.34);transition:background .18s,transform .18s}
.deck-dot[aria-current="true"]::before,.shot-dot[aria-current="true"]::before{
  background:#F2764D;transform:scale(1.5)}
.deck-dot:focus-visible,.shot-dot:focus-visible,.deck-toggle:focus-visible{outline:2px solid #F2764D;outline-offset:-3px}
/* Pause/play for the hero deck, first in the dot row. The button has no text (aria-label names it),
   so the glyph is drawn here: bars while moving, a triangle once stopped. data-state comes from the
   script; the aria-label selector is a second hook on the same state. */
/* The glyph stays 24px like the dots; the area that takes a tap is 44px, and 8px keeps a thumb
   that misses off the first dot, which would jump to screen 1 and leave the deck stopped. */
.deck-toggle{position:relative;margin-right:8px}
.deck-toggle::after{content:"";position:absolute;inset:-10px -4px -10px -10px}
.deck-toggle::before{content:"";box-sizing:border-box;width:8px;height:10px;border:solid rgba(255,255,255,.6);border-width:0 3px}
.deck-toggle[data-state="paused"]::before,.deck-toggle[aria-label^="Play"]::before{border:0;background:rgba(255,255,255,.6);clip-path:polygon(0 0,100% 50%,0 100%);transform:translateX(1px)}
/* Forced colours (Windows High Contrast) drop author backgrounds, gradients and shadows: the dots
   vanished and a gradient button became bare text. System colours survive, so the dots use them and
   buttons get an outline in the text colour. `button.btn` is listed to outrank `button.btn{border:0}`. */
@media(forced-colors:active){
  .deck-dot::before,.shot-dot::before{background:ButtonText}
  .deck-dot[aria-current="true"]::before,.shot-dot[aria-current="true"]::before{background:Highlight}
  .deck-toggle[data-state="paused"]::before,.deck-toggle[aria-label^="Play"]::before{background:ButtonText}
  .btn,button.btn{border:1px solid}
}

/* Press key art. A press page that lists artwork without showing it makes an editor download three
   2MB PNGs to find out which one they want. The previews are 760px WebP (40-109KB); the PNG behind
   each is the 1536px master they will actually place. */
.keyart{display:grid;gap:22px;margin:18px 0 28px}
.keyart figure{margin:0}
.keyart img{width:100%;height:auto;border-radius:14px;border:1px solid var(--border-2);display:block}
.keyart figcaption{margin-top:8px;color:var(--ink-2);font-size:14.5px}
.keyart figcaption a{display:block}
@media(min-width:760px){.keyart{grid-template-columns:repeat(2,1fr)}.keyart figure:first-child{grid-column:1/-1}}

/* Push-up level calculator. Inputs are 16px minimum so iOS does not zoom the page on focus, and the
   output is an <output role="status" aria-live="polite"> so a screen reader announces the result
   without the user hunting for where it appeared. */
.calc{border:1px solid var(--border-2);border-radius:14px;padding:20px;margin:20px 0 26px;background:var(--surface-2,rgba(255,255,255,.03))}
.calc-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:12px}
.calc-row label{flex:0 0 190px;color:var(--ink-2);font-size:15px}
/* Outline in --ink-3 (5.28:1), not --border-2: that was 1.39:1 against the card, with a fill 1.03:1
   from it, so nothing showed where a field was. opacity:1 stops Firefox fading the placeholder. */
.calc-row input,.calc-row select{flex:1 1 160px;min-width:0;font:inherit;font-size:16px;padding:10px 12px;
  border-radius:10px;border:1px solid var(--ink-3);background:var(--bg);color:var(--ink)}
.calc-row input::placeholder{color:var(--ink-3);opacity:1}
.calc-row input:focus-visible,.calc-row select:focus-visible,.calc .btn:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
.calc .btn{margin-top:4px;min-height:44px}
.calc-out{display:block;margin-top:16px;padding:14px 16px;border-radius:10px;line-height:1.5;
  border:1px solid var(--border-2);background:var(--bg);color:var(--ink)}
.calc-out:empty{display:none}
@media(max-width:560px){.calc-row label{flex:1 0 100%}}

/* Consent banner. Only ever rendered by ads.js, and only when an ad id is configured AND the
   visitor has not already answered. The site sets no cookies without it, so on a default build
   this CSS is never used. Bottom-anchored rather than a full-screen interstitial: it is a request,
   not a toll gate, and the page stays readable behind it. */
.consent{position:fixed;left:12px;right:12px;bottom:12px;z-index:60;max-width:640px;margin:0 auto;
  padding:16px 18px;border-radius:14px;border:1px solid var(--border-2);background:var(--bg);
  box-shadow:0 10px 40px rgba(0,0,0,.45)}
.consent p{margin:0 0 12px;font-size:14.5px;line-height:1.5;color:var(--ink-2)}
.consent-actions{display:flex;gap:10px;flex-wrap:wrap}
.consent .btn{min-height:44px}
@media(prefers-reduced-motion:no-preference){.consent{animation:consent-in .18s ease-out}}
@keyframes consent-in{from{transform:translateY(8px);opacity:0}to{transform:none;opacity:1}}
