/* Keyboardo — components.css. Keycap control system + every page surface.
   Dense one-rule-per-line style and a minimal comment set are DELIBERATE: the
   4 sheets share the 32 KB render-blocking budget (CONTRACTS §7) and comments
   ship (no build step). RTL: logical properties only; the two physical rules
   (#caret, .caps-warning) keep their rationale and must not be converted. */

/* ===== 1. KEYCAP SYSTEM =====================================================
   Cap seated in a recessed plate. 3 painted layers on ONE element: shell = the
   element, face = ::before inset 2px with a 7px BOTTOM SKIRT, legend = its own
   text (isolation + z-index:-1 puts the face under the text, so no legend
   wrapper element is needed). THE PHYSICS — never simplify to a bare
   translateY: on :active the cap drops 3px AND the skirt shrinks 7px→4px while
   the top highlight weakens; that compression is what reads as a switch
   bottoming out. The legend adds 1px via a padding-block swap (border-box +
   min-height ⇒ the box does not resize ⇒ no layout shift). overflow is NOT
   hidden: it would clip the focus ring, and nothing overflows anyway.
   Colours are color-mix()ed from the §7 tokens (no new custom property — §7
   freezes the list at 19); white/black mix partners are polarity-agnostic, so
   light/paper/solarized-light stay legible. `.tab` is matched directly because
   pages/leaderboard.js rebuilds those buttons with className='tab …' and would
   wipe an added .key; every other control takes .key (see THE CLASS CONTRACT). */
.key,.tab{position:relative;isolation:isolate;display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:44px;min-inline-size:44px;padding-inline:14px;padding-block:0;border:0;border-radius:9px;color:var(--key-ink,var(--text));background:color-mix(in srgb,var(--surface) 68%,#000);box-shadow:0 0 0 1px color-mix(in srgb,var(--border) 34%,#000),0 3px 5px rgba(0,0,0,.42);cursor:pointer;transition:transform .09s ease-in-out,box-shadow .09s ease-in}
.key::before,.tab::before{content:"";position:absolute;z-index:-1;inset-block:2px 7px;inset-inline:2px;border-radius:7px;background:linear-gradient(179deg,color-mix(in srgb,var(--surface-alt) 78%,#fff) 0%,color-mix(in srgb,var(--surface-alt) 90%,#fff) 55%,var(--surface-alt) 100%);box-shadow:inset 0 1px 0 rgba(255,255,255,.22),inset 0 -4px 6px rgba(0,0,0,.3);transition:inset-block-end .09s ease-in-out,box-shadow .09s ease-in-out}
.key:hover::before,.tab:hover::before{filter:brightness(1.12)}
.key:active,.tab:active{transform:translateY(3px);padding-block:2px 0;box-shadow:0 0 0 1px color-mix(in srgb,var(--border) 34%,#000),0 1px 2px rgba(0,0,0,.4)}
.key:active::before,.tab:active::before{inset-block-end:4px;box-shadow:inset 0 1px 0 rgba(255,255,255,.12),inset 0 -2px 4px rgba(0,0,0,.34)}
.key:focus-visible,.tab:focus-visible{outline-offset:3px} /* clears the rim + drop shadow */
/* Selected cap. All three spellings on purpose: .is-on for markup, plus the
   states the existing JS already writes (menus.js → aria-pressed, leaderboard.js → .is-active). */
.key.is-on,.key[aria-pressed="true"],.tab.is-active{color:var(--key-on-ink,var(--accent-text))}
.key.is-on::before,.key[aria-pressed="true"]::before,.tab.is-active::before{background:linear-gradient(179deg,color-mix(in srgb,var(--key-on-face,var(--accent)) 82%,#fff) 0%,var(--key-on-face,var(--accent)) 55%,color-mix(in srgb,var(--key-on-face,var(--accent)) 86%,#000) 100%);box-shadow:inset 0 1px 0 rgba(255,255,255,.3),inset 0 -4px 6px rgba(0,0,0,.22)}
.key-lg{min-height:56px;padding-inline:20px}
.key:disabled,.tab:disabled,.btn:disabled{opacity:.42;cursor:default;transform:none}
/* The plate: the recessed tray a group of caps is seated in. */
.keyplate,#menu-bar,#theme-menu,.tabset{background:color-mix(in srgb,var(--bg) 80%,#000);border-radius:10px;padding:11px;gap:7px;box-shadow:inset 0 2px 6px rgba(0,0,0,.55)}

/* Native selects. A <select> renders no ::before/::after in any engine, so it
   takes a FLAT keycap skin (same rim, gradient and press travel, no skirt)
   painted onto background + box-shadow. The caret lives on the .key-select
   WRAPPER: a background-image caret can only be positioned physically and
   would mirror wrong at dir=rtl, whereas a wrapper ::after takes
   inset-inline-end. Without the wrapper the select still works, caret-less. */
select.menu-select{appearance:none;-webkit-appearance:none;min-height:44px;padding-inline:14px 34px;padding-block:0;border:0;border-radius:9px;max-width:11rem;color:var(--text);cursor:pointer;background:linear-gradient(179deg,color-mix(in srgb,var(--surface-alt) 78%,#fff) 0%,color-mix(in srgb,var(--surface-alt) 90%,#fff) 55%,var(--surface-alt) 100%);box-shadow:0 0 0 1px color-mix(in srgb,var(--border) 34%,#000),0 3px 5px rgba(0,0,0,.42),inset 0 1px 0 rgba(255,255,255,.22),inset 0 -5px 7px rgba(0,0,0,.3);transition:transform .09s ease-in-out,box-shadow .09s ease-in}
select.menu-select:hover{filter:brightness(1.12)}
select.menu-select:active{transform:translateY(3px);box-shadow:0 0 0 1px color-mix(in srgb,var(--border) 34%,#000),0 1px 2px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.12),inset 0 -3px 5px rgba(0,0,0,.34)}
select.menu-select:focus-visible{outline-offset:3px}
select.menu-select option{background:var(--surface);color:var(--text)} /* the popup is UA-drawn and needs a real background */
.key-select{position:relative;display:inline-flex}
/* border-inline (both sides, symmetric ⇒ direction-neutral, same idiom as
   .caps-warning) + border-block-start ⇒ a triangle that always points DOWN. */
.key-select::after{content:"";position:absolute;z-index:1;inset-inline-end:13px;inset-block-start:calc(50% - 3px);inline-size:0;block-size:0;border-inline:5px solid transparent;border-block-start:6px solid var(--text);opacity:.7;pointer-events:none}

/* ===== 2. SECONDARY BUTTONS (.btn-2 = the existing .btn family) =============
   The layered stone button, three layers again on ONE element so links, form
   submits AND the buttons app/pages/*.js builds via className all get it with
   no markup churn: bezel = ::before inset:-4px (the sketch's outer wrap),
   body = the element (radius 12 + drop shadow), face = ::after inset:4px (the
   lit panel, radius 8). Ink stays --text over a face within ~6% of
   --surface-alt, preserving the text-on-surface contrast every theme already
   guarantees (mixing further toward --text sinks solarized-dark below AA).
   .btn-primary re-skins the same three layers from --accent/--accent-text. */
.btn,.btn-2{position:relative;isolation:isolate;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding-inline:18px;padding-block:0;border:0;border-radius:12px;font-weight:600;text-decoration:none;color:var(--text);background:linear-gradient(180deg,color-mix(in srgb,var(--surface-alt),var(--text) 12%),var(--surface-alt));box-shadow:0 1px 3px rgba(0,0,0,.5);cursor:pointer;transition:box-shadow .1s ease,transform .1s ease}
.btn::before,.btn-2::before{content:"";position:absolute;z-index:-2;inset:-4px;border-radius:16px;background:linear-gradient(180deg,color-mix(in srgb,var(--text) 16%,transparent),color-mix(in srgb,var(--text) 2%,transparent))}
.btn::after,.btn-2::after{content:"";position:absolute;z-index:-1;inset:4px;border-radius:8px;background:linear-gradient(180deg,var(--surface-alt),color-mix(in srgb,var(--surface-alt),var(--text) 6%))}
.btn:hover,.btn-2:hover{text-decoration:none;filter:brightness(1.06)}
.btn:active,.btn-2:active{transform:scale(.995);box-shadow:0 0 1px rgba(0,0,0,.5)}
.btn:focus-visible,.btn-2:focus-visible{outline-offset:5px}
.btn-primary{color:var(--accent-text);background:linear-gradient(180deg,color-mix(in srgb,var(--accent),#fff 20%),var(--accent))}
.btn-primary::after{background:linear-gradient(180deg,color-mix(in srgb,var(--accent),#fff 10%),color-mix(in srgb,var(--accent),#000 8%))}
.btn-primary::before{background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 34%,transparent),transparent)}
.btn-danger{color:var(--error)}

/* ===== 3. MENU BAR + THEME MENU ============================================ */
#menu-bar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;row-gap:9px;font-size:.8125rem} /* position anchors #theme-menu */
.menu-group{display:flex;align-items:center;gap:5px}
.menu-group+.menu-group{padding-inline-start:11px;border-inline-start:1px solid color-mix(in srgb,var(--border) 70%,#000)}
#lang-note{margin-inline-start:6px}
#theme-menu{position:absolute;top:calc(100% + 6px);inset-inline-end:8px;z-index:30;display:grid;grid-template-columns:repeat(2,minmax(8.5rem,1fr));box-shadow:inset 0 2px 6px rgba(0,0,0,.55),0 10px 30px rgba(0,0,0,.35)}
.theme-option{justify-content:start;font-size:.8125rem}

/* ===== 4. LANGUAGE SWITCHER + DETECTION BANNER =============================
   Zero layout shift is structural: both panels are out of flow (absolute /
   fixed) and nothing animates, so reduced-motion needs no branch here. */
.lang-switch{position:relative}
.lang-switch>summary{display:inline-flex;align-items:center;gap:4px;min-height:44px;padding-inline:8px;border-radius:8px;color:var(--sub);cursor:pointer;list-style:none} /* .lang-caret is the affordance */
.lang-switch>summary::-webkit-details-marker{display:none} /* Safari draws its own otherwise */
.lang-switch>summary:hover,.lang-switch[open]>summary{color:var(--text)}
.lang-caret{font-size:.7rem}
.lang-menu{position:absolute;top:calc(100% + 6px);inset-inline-start:0;z-index:30;min-width:11rem;margin:0;padding:6px;list-style:none;background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35)}
#site-header .lang-menu a{display:flex;align-items:center;min-height:44px;padding-inline:10px;border-radius:6px;color:var(--sub);white-space:nowrap}
#site-header .lang-menu a:hover{color:var(--text);background:var(--surface-alt);text-decoration:none}
#site-header .lang-menu a[aria-current="true"]{color:var(--accent)}
.lang-banner{position:fixed;bottom:16px;inset-inline-start:16px;z-index:90;display:flex;align-items:center;gap:12px;max-width:min(24rem,calc(100vw - 32px));padding:10px 12px;background:var(--surface);color:var(--text);border:1px solid var(--border);border-inline-start:3px solid var(--accent);border-radius:var(--radius);box-shadow:0 6px 20px rgba(0,0,0,.3);font-size:.875rem} /* z 90 keeps it under #toast-stack (100) */
.lang-banner-link{color:var(--accent)}
.lang-banner-close{display:inline-flex;align-items:center;justify-content:center;min-inline-size:44px;min-block-size:44px;margin-inline-start:auto;color:var(--sub);font-size:1.1rem;line-height:1;border-radius:8px}
.lang-banner-close:hover{color:var(--text);background:var(--surface-alt)}

/* ===== 5. LIVE STATS + GHOST RACE =====  fixed height + 4ch per readout ⇒ a
   value appearing can never jiggle a sibling; nothing animates in here. */
#live-stats{display:flex;align-items:center;gap:24px;color:var(--accent);font-size:1.25rem}
#live-stats span{min-width:4ch}
#live-ghost{display:inline-flex;align-items:baseline;gap:6px;color:var(--sub);font-size:1rem;font-variant-numeric:tabular-nums}
#live-ghost .ghost-tag{min-width:0;font-size:.75rem;opacity:.8}
#live-ghost.ahead #live-ghost-delta{color:var(--success)}
#live-ghost.behind #live-ghost-delta{color:var(--error)}

/* ===== 6. WORDS / CHARS / CARET / INPUT =====  DOM: #words > span.word >
   span.char (+ span.char.extra). Char state classes change COLOUR ONLY. */
#words{font-size:var(--test-font);line-height:var(--test-line);color:var(--sub);user-select:none;cursor:default}
#words[dir="rtl"]{direction:rtl}
.word{display:inline-block;margin-inline-end:1ch} /* logical: the inter-word space flips in RTL */
.word.error{text-decoration:underline;text-decoration-color:var(--error)}
.char.correct{color:var(--text)}
.char.incorrect{color:var(--error)}
.char.extra{color:var(--error-extra)}
/* `left:0` is PHYSICAL ON PURPOSE and must NOT become inset-inline-start. It is
   not a layout choice, it is the ORIGIN the JS measures against: engine/caret.js
   computes `edge - wrap.left` from getBoundingClientRect() — always a physical
   left-based coordinate — and writes translate(X,Y). Under dir=rtl a logical
   anchor would move the origin to the right edge while the arithmetic stayed
   left-based, putting the caret a container-width away. RTL is handled INSIDE
   caret.js (isRtl() picks rect.left vs rect.right); this rule stays out of it. */
#caret{position:absolute;top:0;left:0;width:2px;height:calc(var(--test-font) * 1.2);background:var(--caret);pointer-events:none;will-change:transform;z-index:3}
body.smooth-caret #caret{transition:transform 100ms linear}
#caret[data-caret="block"]{opacity:.4}
#caret[data-caret="underline"]{opacity:.9}
#caret.blink{animation:caret-blink 1s step-end infinite}
@keyframes caret-blink{50%{opacity:0}}
#hidden-input{position:absolute;inset:0;width:100%;height:100%;z-index:2;opacity:0;border:0;background:none;color:transparent;font-size:1rem;cursor:default} /* >=16px font prevents iOS focus zoom */
#hidden-input:focus-visible{outline:none} /* the caret IS the focus indicator here */
#focus-overlay{position:absolute;inset:0;z-index:4;display:none;align-items:center;justify-content:center;text-align:center;padding:8px;color:var(--text);font-size:1rem;cursor:pointer}
body.focus-lost #focus-overlay{display:flex}
body.focus-lost #words{filter:blur(6px);opacity:.4}
body.focus-lost #caret{visibility:hidden}

/* ===== 7. TEST CHROME ====================================================== */
#test-wrap{position:relative;display:grid;gap:16px;justify-items:center;padding-block:24px} /* position anchors .caps-warning */
#test-wrap>#live-stats,#test-wrap>#words-wrap{justify-self:stretch}
#restart-btn{min-height:56px;padding-inline:20px}
.hint{color:var(--sub);font-size:.8125rem;text-align:center}
.noscript-note{color:var(--warning);text-align:center}
/* left:50% + translateX(-50%) is the centring idiom and is direction-NEUTRAL:
   both halves are physical, so they cancel identically in LTR and RTL. Making
   the inset logical while transform stayed physical (transforms are never
   flipped by `direction`) is what would actually break it under dir=rtl. */
.caps-warning{position:absolute;top:0;left:50%;transform:translateX(-50%);z-index:5;background:var(--warning);color:var(--bg);padding:4px 12px;border-radius:var(--radius);font-size:.8125rem} /* absolute ⇒ toggling shifts nothing */

/* ===== 8. RESULTS + SHARE ================================================== */
#results-screen{position:relative;display:grid;gap:20px;padding-block:8px}
#results-screen:focus-visible{outline:2px solid var(--focus-ring);outline-offset:4px}
.results-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.stat{background:var(--surface);border-radius:10px;padding:12px 16px}
.stat-label{display:block;color:var(--sub);font-size:.75rem}
.stat-value{display:block;color:var(--accent);font-size:1.9rem;font-weight:700;line-height:1.3;min-height:2.5rem}
#wpm-chart{width:100%;height:200px;background:var(--surface);border-radius:10px} /* fixed height: redraw can never shift layout */
#result-words{display:flex;flex-wrap:wrap;gap:8px;font-size:.875rem}
.replay-word{background:var(--surface);border-radius:6px;padding:4px 8px}
.replay-word .replay-typed{color:var(--error);text-decoration:line-through;margin-inline-start:6px}
#next-test-btn{justify-self:center;min-height:56px;padding-inline:26px}
/* Close cap: keycap skin from §1, geometry only here. Logical inset ⇒ mirrors. */
.results-close{position:absolute;inset-block-start:0;inset-inline-end:0;inline-size:44px;padding-inline:0;font-size:1.5rem;line-height:1;color:var(--sub)}
/* Height RESERVED for the worst case per breakpoint, so revealing
   #share-native-btn (capability-gated) or #share-link-btn (logged-in only)
   changes the row's WIDTH, never the page's height. Under the mobile
   breakpoint 3-4 buttons wrap, so the narrow reserve is 2 rows (2x44 + 8 gap).
   Literals are unavoidable: §7 freezes the custom-property list at 19. */
.share-group{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;justify-self:center;align-content:center;min-height:44px}
.share-btn{font-size:.875rem;padding-inline:14px}

/* ===== 9. FORMS (auth + settings reuse these) ============================== */
.form{display:grid;gap:14px;width:100%;max-width:22rem;margin-inline:auto}
.form-field{display:grid;gap:5px}
.form-field label{color:var(--sub);font-size:.8125rem}
.form-field input{min-height:44px;background:var(--surface);border:1px solid var(--border);color:var(--text);border-radius:var(--radius);padding-inline:11px}
.form-field input:focus-visible{border-color:var(--accent)}
/* A 20px checkbox is under the 44px target, so the label carries the height. */
.form-field-check{display:flex;align-items:center;gap:10px}
.form-field-check input{inline-size:20px;block-size:20px;min-height:0;margin:0;accent-color:var(--accent)}
.form-field-check label{display:flex;align-items:center;min-height:44px;flex:1;cursor:pointer}
.field-error{color:var(--error);font-size:.8125rem}
.field-error:empty{display:none}
#app.auth-main{max-width:22rem;margin-inline:auto;padding-block:48px 64px}
#app.auth-main .page-title{text-align:center}
.form-intro{color:var(--sub);font-size:.875rem;text-align:center;margin-bottom:14px}
.form-links{display:flex;flex-direction:column;gap:8px;margin-top:4px;font-size:.875rem}
.form-links a{color:var(--accent)}
.form .hint{color:var(--sub);font-size:.75rem;margin:2px 0 0}
.settings-section{display:grid;gap:10px;padding-block:8px 24px}
.settings-section .form{margin-inline:0}

/* ===== 10. TOASTS ========================================================== */
#toast-stack{position:fixed;bottom:16px;inset-inline-end:16px;z-index:100;display:flex;flex-direction:column;gap:8px;max-width:20rem}
.toast{background:var(--surface);border:1px solid var(--border);border-inline-start:3px solid var(--accent);color:var(--text);padding:10px 14px;border-radius:var(--radius);font-size:.875rem;box-shadow:0 6px 20px rgba(0,0,0,.3);animation:toast-in .15s ease-out}
.toast.success{border-inline-start-color:var(--success)}
.toast.error{border-inline-start-color:var(--error)}
@keyframes toast-in{from{transform:translateY(6px);opacity:0}}

/* ===== 11. LONG-FORM SURFACES =====  #seo-content, /about /privacy /terms,
   /blog and the guides were 3 near-identical prose skins; merged here. */
#seo-content,.legal-page,.article-page{color:var(--sub)}
.legal-page,.article-page{max-width:72ch;margin-inline:auto}
#seo-content h1{color:var(--text);font-size:1.5rem}
.legal-page h1,.article-page h1{color:var(--text);font-size:1.6rem;margin-bottom:8px}
#seo-content h2,.legal-page h2,.article-page h2{color:var(--text);font-size:1.15rem;margin-top:28px}
.article-page h2{margin-top:32px;scroll-margin-top:16px} /* in-page TOC links land clear of the heading */
.article-page h3{color:var(--text);font-size:1rem;margin-top:20px}
#seo-content p,.legal-page p,.article-page p,.article-page li{margin-top:12px;line-height:1.7}
.legal-page .legal-effective{margin-top:0;font-size:.8125rem}
.article-page ul,.article-page ol{padding-inline-start:22px}
.article-page table{width:100%;margin-top:16px;border-collapse:collapse;font-size:.9375rem}
.article-page th,.article-page td{border:1px solid var(--border);padding:8px 10px;text-align:start}
.article-page th{color:var(--text);background:var(--surface)}
.breadcrumb,.article-meta{display:flex;flex-wrap:wrap;gap:6px;font-size:.8125rem}
.breadcrumb{padding-block:20px 6px}
.article-meta{margin-top:4px}
.article-answer{color:var(--text);font-size:1.0625rem;border-inline-start:3px solid var(--accent);padding-inline-start:14px;margin-top:20px} /* the direct answer: first prose, and the anchor of the page */
.article-toc,.article-facts,.article-cta{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;margin-top:32px}
.article-toc h2,.article-facts h2,.article-cta h2{margin-top:0;font-size:1rem}
.article-related li>span{display:block;font-size:.875rem}
.article-allguides{margin-top:32px}
.blog-list{list-style:none;padding-inline-start:0}
.blog-item{border-top:1px solid var(--border);padding-bottom:14px}
.blog-item h3{margin-top:14px}
#landing-guides .blog-item h3{color:var(--text);font-size:1rem}
.page-title{font-size:1.6rem;margin-block:24px 12px}
#faq{margin-top:28px}
.faq-item{border-top:1px solid var(--border)}
.faq-item:last-child{border-bottom:1px solid var(--border)}
.faq-item summary{cursor:pointer;padding:14px 0;color:var(--text);font-weight:600;list-style-position:inside} /* 14px block padding already makes this a 49px target */
.faq-item p{margin-top:0;padding-bottom:14px}

/* ===== 12. PROFILE (/u/{username}) ========================================= */
.profile-page{display:grid;gap:28px}
.profile-head{display:grid;gap:4px}
.profile-username{color:var(--text);font-size:1.9rem;word-break:break-word}
.profile-note,.profile-caption,.hm-caption,.daily-streak-signed-out{color:var(--sub)}
.profile-since,.best-key,.best-sub{color:var(--sub);font-size:.75rem}
.profile-since{font-size:.8125rem}
.profile-caption{margin:0;text-align:start}
.profile-section{display:grid;gap:14px}
.section-title{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px;color:var(--text);font-size:1.2rem}
.profile-stats,.daily-streak-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
.profile-stats .stat-value,.daily-streak-stats .stat-value{font-size:1.5rem}
.profile-notfound,.share-notfound{display:grid;gap:14px;justify-items:start;padding-block:24px}
.bests-group{display:grid;gap:8px}
.bests-group+.bests-group{margin-top:12px}
.bests-group-title{color:var(--sub);font-size:.8125rem;letter-spacing:.06em;text-transform:uppercase}
.bests-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}
.best-card{display:grid;gap:3px;background:var(--surface);border-radius:10px;padding:12px 14px}
.best-wpm{color:var(--accent);font-size:1.6rem;font-weight:700;line-height:1.2}
.best-wpm.best-empty{color:var(--sub);font-weight:400}
/* Heatmap + sparkline. Neither mirrors at dir=rtl and neither should: both are
   time series read oldest→newest, like a printed calendar. */
.hm-cell{fill:var(--chart-grid)}
.hm-cell.l1{fill:var(--accent);fill-opacity:.3}
.hm-cell.l2{fill:var(--accent);fill-opacity:.55}
.hm-cell.l3{fill:var(--accent);fill-opacity:.8}
.hm-cell.l4{fill:var(--accent);fill-opacity:1}
.hm{display:grid;gap:10px;margin:0}
.hm-scroll{overflow-x:auto}
.hm-svg{max-width:100%;height:auto} /* NOT width:100% — it must keep its intrinsic width and scroll inside .hm-scroll */
.spark{width:100%;height:auto}
.hm-foot{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 16px;color:var(--sub);font-size:.75rem}
.hm-legend{display:inline-flex;align-items:center;gap:6px}
.hm-legend-svg{display:block}
.spark-wrap{background:var(--surface);border-radius:10px;padding:10px 12px}
.spark-line{fill:none;stroke:var(--accent);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.spark-area{fill:var(--accent);fill-opacity:.12;stroke:none}
.spark-dot{fill:var(--accent)}
.spark-axis{fill:var(--sub);font-size:10px}
.ach-count{color:var(--sub);font-size:.8125rem;font-weight:400}
.ach-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px;list-style:none}
.ach-badge{display:flex;gap:10px;background:var(--surface);border-radius:10px;padding:12px 14px}
.ach-icon{color:var(--accent);font-size:1.1rem;line-height:1.3}
.ach-body{display:grid;gap:2px}
.ach-name{color:var(--text);font-size:.9375rem;font-weight:600}
.ach-desc{color:var(--sub);font-size:.8125rem}
.ach-earned{color:var(--sub);font-size:.75rem;margin-top:2px}

/* ===== 13. SHARED RESULT PAGE (/r/{token}) =====  one screen, no scrolling on
   a phone: arrive from a chat link, see score + who + CTA without moving. */
.share-page{display:grid;gap:28px;justify-items:center;padding-block:32px}
.share-card{display:grid;gap:10px;justify-items:center;width:100%;max-width:44rem;background:var(--surface);border-radius:14px;padding:28px 24px;text-align:center}
.share-by{color:var(--sub);font-size:.875rem;margin:0}
.share-score{color:var(--accent);font-size:4.5rem;font-weight:700;line-height:1.1;margin:0}
.share-unit{color:var(--sub);font-size:1.5rem;font-weight:400}
.share-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;width:100%;margin-top:8px}
.share-stats .stat{background:var(--bg);text-align:center}
.share-stats .stat-value{font-size:1.35rem}
.share-stats .share-mode{color:var(--text);font-size:1rem;line-height:1.5;word-break:break-word} /* words, not a number: must not inherit the numeric size, must wrap */
.share-cta{display:grid;gap:10px;justify-items:center}
.share-cta-btn{font-size:1.0625rem;min-height:56px;padding-inline:28px}
.share-note{color:var(--sub)}

/* ===== 14. LEADERBOARD + DAILY ============================================= */
.board-page,.daily-page{display:grid;gap:24px}
.board-head,.daily-head{display:grid;gap:8px}
.board-intro,.daily-intro{color:var(--sub);max-width:72ch;line-height:1.6}
.board{display:grid;gap:16px}
.board-controls{display:flex;flex-wrap:wrap;gap:10px 18px}
.tabset{display:inline-flex;flex-wrap:wrap;align-items:center;padding:7px;gap:6px} /* a tabset IS a keyplate — skin comes from §1 */
.tab{font-size:.875rem;font-weight:600}
.board-status{color:var(--error);font-size:.875rem}
.board-status:empty{display:none}
.board-table-wrap{overflow-x:auto}
.board-table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}
.board-table th,.board-table td{padding:10px 12px;text-align:start;white-space:nowrap}
.board-table thead th{color:var(--sub);font-size:.75rem;letter-spacing:.06em;text-transform:uppercase;border-bottom:1px solid var(--border)}
.board-table tbody tr{border-bottom:1px solid var(--border)}
.board-table .col-rank{color:var(--sub);width:3rem}
.board-table .col-num{text-align:end}
.board-table .col-user a{color:var(--text);font-weight:600}
.board-table .col-user a:hover{color:var(--accent)}
.board-table tr.is-me{background:var(--surface-alt)}
.board-table tr.is-me .col-rank{color:var(--accent)}
.me-tag{display:inline-block;background:var(--accent);color:var(--accent-text);font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:1px 6px;border-radius:999px;vertical-align:middle}
.board-empty-row td{color:var(--sub);text-align:center;padding-block:28px}
.board-me{color:var(--text);font-weight:600}
.board-me[hidden]{display:none}
.board-pager{display:flex;align-items:center;justify-content:center;gap:16px}
.board-page-status{color:var(--sub);font-size:.875rem;font-variant-numeric:tabular-nums}
.daily-card{display:grid;gap:6px;justify-items:start;background:var(--surface);border-radius:var(--radius);padding:20px 22px}
.daily-today-label{color:var(--sub);font-size:.75rem;letter-spacing:.06em;text-transform:uppercase}
.daily-date{color:var(--text);font-size:1.4rem;font-weight:700}
.daily-config{color:var(--accent);font-size:.9375rem}
.daily-same-words{color:var(--sub);max-width:60ch;line-height:1.5}
.daily-play{margin-top:10px}
.daily-streak,.daily-board{display:grid;gap:12px}
.daily-streak-status.is-done{color:var(--success)}
.daily-streak-status.is-pending{color:var(--sub)}

/* ===== 15. TOUCH TARGETS for inline text links =============================
   These must NOT visually grow (that would move the text they sit in), so the
   hit area is expanded with an out-of-flow ::after overlay: 45x(text+14) at
   zero layout cost. SPACING IS CHECKED — header/footer nav carry a 24px ROW
   gap and a 16px column gap (layout.css) against overlays of 12px block / 7px
   inline, so no two overlays touch. The .lang-menu links opt OUT: they are
   already real 44px flex targets packed with no gap, so an overlay there WOULD
   overlap. Deliberately NOT applied inside flowing prose — at line-height 1.7
   an overlay covers the lines above and below, and overlapping hit areas are
   worse than small ones. */
#site-header nav a,#site-footer nav a,.form-links a,.breadcrumb a,.article-meta a,.blog-item h3 a,.hm-foot a,.lang-banner-link{position:relative;display:inline-block}
#site-header nav a::after,#site-footer nav a::after,.form-links a::after,.breadcrumb a::after,.article-meta a::after,.blog-item h3 a::after,.hm-foot a::after,.lang-banner-link::after{content:"";position:absolute;inset-block:-13px;inset-inline:-7px}
#site-header nav .lang-switch a::after{content:none}

/* ===== 16. REDUCED MOTION =====  base.css zeroes transition-duration; this
   removes the MOVEMENT of the press itself (travel, skirt, legend nudge),
   leaving the shadow/colour change — a press stays perceivable, it just
   must not move. */
@media (prefers-reduced-motion:reduce){
.key:active,.tab:active,select.menu-select:active,.btn:active,.btn-2:active{transform:none;padding-block:0}
.key:active::before,.tab:active::before{inset-block-end:7px}
}

/* ===== 17. BREAKPOINTS ===================================================== */
@media (max-width:600px){
.results-grid{grid-template-columns:repeat(2,1fr)}
/* Two-row reserve — see .share-group. From 600px, not the ~480px where EN
   labels wrap, because the wrap width is locale-dependent (FR is longer) and a
   reserve that is right in one language only is not a reserve. */
.share-group{min-height:96px}
.share-stats{grid-template-columns:repeat(2,1fr)}
.share-score{font-size:3.5rem}
#menu-bar{padding:8px;gap:6px}
.menu-group+.menu-group{padding-inline-start:8px}
}
