/* Mica — the APP layer over el sistema Mica v2 (Esmalt).
   The palette, scales, inks/fills and elevation recipes live in
   css/mica-tokens.css; the shared component vocabulary in css/mica-sistema.css.
   BOTH load before this file (index.html), so anything declared here wins —
   which is the migration model: blocks below keep their literals until a phase
   touches them, then snap onto the system. Never re-declare a system token
   here; this :root keeps only what the system does not own. */

:root {
  /* Legacy card shadow — pre-Esmalt blocks still consume it; each migrates to
     --shadow-1/2/3 when its phase lands. */
  --shadow: 0 1px 2px rgba(43, 38, 32, 0.05), 0 4px 14px rgba(43, 38, 32, 0.06);
  /* The display face's pre-token name: aliased so every var(--display) in this
     file re-resolves to the system family (Cormorant Garamond, falling back to
     the local Apple serifs — no network fetch, offline-safe). */
  --display: var(--font-display);
  --sky-tint: var(--bg);   /* time-of-day tint, set per band on <body> below */

  /* ---- layout (docs/LAYOUT.md > SISTEMA.md where they disagree) ----
     The token layer proposes 820/1100/66ch; the desktop shell was built and
     verified on these. Layout geometry does not change in a visual refactor. */
  --w-browse: 1000px;  /* grids and galleries that earn more than one column */
  --w-canvas: 1120px;  /* the desktop shell cap — main widens to this at ≥900 */
  --measure: 68ch;     /* prose line-length cap for reading columns */
}

/* Every dark rule left in this file comes in this two-block shape: the media
   block guarded by :root:not([data-theme="light"]) (OS dark, no light pin) plus
   a [data-theme="dark"] twin (explicit pin, any OS). One block alone breaks one
   of the two pin directions. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --shadow: 0 1px 2px rgba(0, 0, 0, 0.3); }
}
[data-theme="dark"] { --shadow: 0 1px 2px rgba(0, 0, 0, 0.3); }

/* A pinned theme must carry the UA's own surfaces with it — overscroll canvas,
   scrollbars, caret, form-control chrome. Tokens flip the palette; this flips
   the UA. (The unpinned `light dark` default lives in mica-sistema.css.) */
[data-theme="dark"] { color-scheme: dark; }
[data-theme="light"] { color-scheme: light; }

html, body { margin: 0; height: 100%; }
.overline { font-size: 11px; line-height: 1.4; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted); }
.grout-divider { width: 100%; height: 1px; margin: 24px 0; padding: 0;
  border: 0; background: var(--bar-track); }
body {
  font: var(--fs-4)/var(--lh-body) var(--font-body);
  /* The colour layer is load-bearing, not decoration: body's background propagates to the
     canvas, and the fixed gradient only paints the viewport rect — so without a colour the
     canvas stays transparent and iOS paints its rubber-band overscroll white in both themes.
     Keep this on body: an html background would stop the propagation and bury body::before. */
  background: linear-gradient(180deg, var(--sky-tint), var(--bg) 42%) no-repeat var(--bg);
  background-attachment: fixed;
  color: var(--text);
  display: flex;
  flex-direction: column;
  transition: background 1.2s ease;
}
/* Time-of-day sky — set on <body> by main.js */
body.sky-dawn  { --sky-tint: #f6e3d0; }
body.sky-day   { --sky-tint: #edf2f2; }
body.sky-dusk  { --sky-tint: #f1d8c0; }
body.sky-night { --sky-tint: #e4dfe9; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.sky-dawn  { --sky-tint: #2c2018; }
  :root:not([data-theme="light"]) body.sky-day   { --sky-tint: #1d2426; }
  :root:not([data-theme="light"]) body.sky-dusk  { --sky-tint: #2d1f17; }
  :root:not([data-theme="light"]) body.sky-night { --sky-tint: #161422; }
}
[data-theme="dark"] body.sky-dawn  { --sky-tint: #2c2018; }
[data-theme="dark"] body.sky-day   { --sky-tint: #1d2426; }
[data-theme="dark"] body.sky-dusk  { --sky-tint: #2d1f17; }
[data-theme="dark"] body.sky-night { --sky-tint: #161422; }

/* ---- chrome ---- */
/* Skip links: off-screen until focused, then a pill at top-left over the
   chrome. On --z-chrome so they sit with (not above) the app chrome. */
.skip-link {
  position: fixed; top: -60px; left: 8px; z-index: var(--z-chrome);
  padding: 10px 16px; border-radius: 999px;
  /* Fills carry white (law 1): literal white in both themes; --accent-fill
     deepens in dark so it passes (5.44 L / 4.64 D). --on-accent is retired. */
  background: var(--accent-fill); color: #fff; text-decoration: none;
  font-size: 15px; font-weight: 600;
  transition: top var(--dur-1) var(--ease-glass);
}
.skip-link:focus { top: max(8px, env(safe-area-inset-top)); outline: 3px solid var(--text); outline-offset: 2px; }

.topbar {
  display: flex; align-items: baseline; gap: 14px;
  padding: 14px 20px 8px;
  max-width: var(--w-focus); width: 100%; margin: 0 auto;
  /* The system's topbar is a full-bleed sticky glass band; this one is a
     width-capped strip that scrolls with the page. Adopting the glass on a
     640px-wide floating strip reads as a stray card, so neutralise the surface
     until a phase adopts the v2 topbar whole. (position is pinned to relative
     further down — the D2 desktop pill depends on it.) */
  background: none; border-bottom: 0;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.brand { font-family: var(--display); font-weight: 700; letter-spacing: 0.04em;
  color: var(--accent); font-size: 22px; font-style: italic; text-decoration: none;
  display: inline-flex; align-items: center; gap: 7px; }
.brand .logo-mica { display: block; flex: 0 0 auto; }

/* ---- «la pica s'omple» — the loading indicator (actius de marca 1b) ----
   The mark fills in three measured steps, holds, and drains. It is the app's
   ONLY spinner and it is reserved for a real network wait; `.is-ocupat` covers
   local, sub-second work and deliberately ships no spinner at all.

   The rise is a clipPath rect translated in USER units — the viewBox is
   0 0 128 128, so 130px of CSS transform inside the SVG clears the mark
   whatever pixel size the loader is rendered at (22 and 36 today). */
.mica-loader { display: block; flex: 0 0 auto; }
.mica-loader-ghost { color: var(--accent-soft); }
.mica-loader > g:not(.mica-loader-ghost) { color: var(--accent); }
.mica-loader-nivell { animation: omplir-loop 2.8s var(--ease-glass) infinite; }
@keyframes omplir-loop {
  0%, 4%  { transform: translateY(130px); }
  18%     { transform: translateY(88px); }  26% { transform: translateY(88px); }
  40%     { transform: translateY(55px); }  48% { transform: translateY(55px); }
  62%     { transform: translateY(28px); }  70% { transform: translateY(28px); }
  80%     { transform: translateY(0); }     90% { transform: translateY(0); }
  100%    { transform: translateY(130px); }
}
/* The static twin: the full mark, level at the brim. `loader()` also drops the
   clip in JS when the query matches — belt and braces, because a clip left in
   place with the animation removed would rest wherever the rect's own default
   put it, and an empty basin is not a resting state anyone can read. */
@media (prefers-reduced-motion: reduce) {
  .mica-loader-nivell { animation: none; transform: translateY(0); }
}
/* A waiting line: the mark beside the sentence that says what is being waited
   for. Rides whatever text class the surface already used (.hint, .stat-line),
   so the only thing this adds is the row. */
.espera { display: flex; align-items: center; gap: 10px; }

/* Theme control — quiet chrome, the .sound-toggle idiom with a 44px target. */
/* min-width alongside min-height: at ≤480 the side air trims to 2px and
   #idioma's own word («CA») rendered a 21px-wide target — under WCAG 2.2 SC
   2.5.8's 24×24 floor, measured at 390px (8 Sep 2026). The floor costs three
   pixels on the one control that was short of it and nothing on the rest. */
.tema-toggle { min-height: 44px; min-width: 24px; padding: 0 8px;
  background: none; border: none;
  color: var(--muted); font: inherit; font-size: 13px; cursor: pointer;
  border-radius: var(--radius-pill);
  transition: color var(--dur-1) var(--ease-glass); }
/* #tema is the first of the right-hand chrome and always present, so it alone
   carries the margin-left:auto that pins the cluster (tema + the optional
   idioma toggle) to the edge. This used to live on the now-removed .due figure;
   putting it on the shared .tema-toggle class gave BOTH toggles an auto and
   split the free space, stranding a gap between them on two-pack profiles. */
#tema { margin-left: auto; }
.tema-toggle:hover { color: var(--accent-ink); }
/* El so — the one chrome control that is a MARK rather than a word. It rides
   .tema-toggle for the 44px target, the pill radius and the hover, and adds
   only what an icon needs: a square-ish box and a centred glyph. State is in
   the SHAPE first (a cone with waves / a cone with a cross) and in the ink
   second, so it never depends on colour alone; `on` writes in --accent-ink,
   which is where every other toggle in this strip only goes on hover. */
.so-toggle { display: inline-flex; align-items: center; justify-content: center;
  padding: 0 7px; }
.so-toggle .ico { display: block; }
/* THE STRIP IS BASELINE-ALIGNED BELOW 900, AND AN ICON HAS NO BASELINE (9 Aug
   2026). `.topbar` is `align-items: baseline` (the ≥900 pill switches it to
   center, which is why the desktop strip was always right). Every other control
   there ends in real text — «auto», «CA», the medallion's initial — so the row
   has a shared text baseline; the glyph does not, and a flex container whose
   only item is a replaced element takes that item's BOTTOM MARGIN EDGE as its
   synthesized baseline. So the icon's bottom edge was being aligned with the
   words' baseline: the glyph hung entirely above the line like a capital, 6px
   high of the medallion beside it, and its outsized ascent pushed the whole
   strip's baseline 4.4px down with it (the brand moved too — measured at 375).
   A zero-width space gives the button a line box, i.e. the same text baseline
   its neighbours have, and baseline alignment then does the right thing by
   itself. Verified at 375: the glyph, «auto» and the medallion agree within
   1px, and the strip's geometry is byte-identical to what it was before el so
   was added. The button's name comes from aria-label, so the pseudo-element's
   content is not part of its accessible name. */
.so-toggle::before { content: "\200b"; width: 0; }
.so-toggle[aria-pressed="true"] { color: var(--accent-ink); }
/* La butxaca — one compact availability disclosure in the chrome. The mark is
   the persistent status; its full, qualified copy opens only on request, so
   away mode never takes a view-sized bite out of the app. It sits before
   #tema, whose auto margin still pins the preference cluster to the right. */
.butxaca-indicator { display: inline-grid; place-items: center; flex: 0 0 auto;
  min-width: 44px; padding: 0 7px; color: var(--tile-amber-ink);
  border: 1px solid color-mix(in srgb, var(--tile-amber) 42%, var(--edge-soft));
  background: color-mix(in srgb, var(--tile-amber) 15%, transparent); }
.butxaca-indicator[hidden] { display: none; }
.butxaca-indicator:hover { color: var(--tile-amber-ink);
  background: color-mix(in srgb, var(--tile-amber) 24%, transparent); }
.butxaca-modal { border: 1px solid var(--edge-soft); }
.butxaca-modal h2 { margin-bottom: var(--space-3); }
.butxaca-modal p { margin: 0 0 var(--space-5); color: var(--text);
  line-height: var(--lh-body); }
.butxaca-modal .btn { width: 100%; }
/* El delit S1 · the control answers the press. The toggle's whole body IS its
   label (auto/clar/fosc), so the swap is the feedback: the new word rises in
   rather than snapping. The 1.2s body fade stays what it always was — that is
   the PAGE changing; this is the button saying it heard you. Restarted from JS
   by removing/re-adding the class (main.js initTheme), never on first paint. */
.tema-toggle.is-canviat { animation: tema-canvi var(--dur-2) var(--ease-glass); }
@keyframes tema-canvi {
  from { opacity: 0; transform: translateY(-3px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .tema-toggle { transition: none; }
  .tema-toggle.is-canviat { animation: none; }
}

main {
  flex: 1; width: 100%; max-width: var(--w-focus); margin: 0 auto;
  padding: 8px 20px 90px; outline: none;
}

/* ---- desktop shell (≥900px) · docs/LAYOUT.md ----
   main widens to the canvas so browse surfaces have room, but each direct
   child of #view stays a focused column by default (margin-inline centres it
   in the wider main). Browse/canvas surfaces opt IN per-view with a lay-*
   class on their root element. Entirely inert below 900px — main stays
   --w-focus and the opt-in classes have no consumers yet (that's expected;
   Phase 1 adds them). Same specificity as the base rules above, so nothing
   leaks below the breakpoint by cascade accident. */
@media (min-width: 900px) {
  main { max-width: var(--w-canvas); }
  #view > * { max-width: var(--w-focus); margin-inline: auto; }
  #view > .lay-browse { max-width: var(--w-browse); }
  #view > .lay-canvas { max-width: var(--w-canvas); }
}

.tabbar {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: var(--z-chrome);
  display: flex; justify-content: center; gap: 4px;
  background: var(--surface);
  /* The system tabbar is a free-standing pill (full border + 999px radius);
     this one is the phone's edge-to-edge bottom bar — keep only the top
     hairline and square corners here. The ≥900 block below re-adopts the pill. */
  border: 0; border-top: 1px solid var(--bar-track); border-radius: 0;
  padding: 8px max(12px, env(safe-area-inset-left)) max(10px, env(safe-area-inset-bottom));
}
.tab {
  min-height: 44px; padding: 8px 18px; border-radius: 999px;
  text-decoration: none; color: var(--muted); font-size: 15px;
  display: inline-flex; align-items: center; justify-content: center;
}
.tab.active { background: var(--accent-soft); color: var(--accent-ink); font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--accent); }
/* Els llocs are MARKS on the phone and WORDS on the desktop pill (9 Aug 2026,
   design/handoffs/icones-llocs/): five labels at 375px were a squeeze, and a
   house · mortarboard · pencil · bars · globe read faster than the words did.
   Mobile-only by construction — the ≥900 block re-shows the word and hides the
   glyph, so the top pill is untouched. The word stays IN the DOM (and on
   aria-label) rather than being swapped out, so the tab's accessible name never
   depends on which width is on screen. The mark carries NO colour of its own:
   it inherits .tab's --muted and .tab.active's --accent-ink through
   currentColor, which is what makes the dark theme free and the active chip
   (and la bombolla under it) exactly what they were. */
.tab-mot { display: none; }

/* D2 (18 Jul 2026): the desktop nav moved to the TOP of the shell — the rules
   live after the richer-pass .tabbar restyling below (which would otherwise
   out-cascade them at equal specificity), see "frosted tab bar". */

/* ---- session player ---- */
.session-progress {
  display: flex; justify-content: space-between; align-items: center;
  gap: 8px; margin: 10px 0 4px; font-size: 13px; color: var(--muted);
}
/* The sortida shares the header with the block label; the label yields first,
   because "where am I going back to" beats "which block is this". */
.session-progress-left { display: flex; align-items: center; gap: 6px; min-width: 0; }
.session-progress-left .block-label { min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.block-label { letter-spacing: 0.08em; font-weight: 600; }
.bar { height: 6px; background: var(--bar-track); border-radius: 3px; overflow: hidden; flex: 1; }
.bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s ease;
  background: linear-gradient(90deg, var(--accent), var(--accent-2)); }

/* La Visita — the street round played as a scene */
.scene-line { margin: 14px 0 2px; font-size: 15px; font-style: italic; color: var(--muted);
  line-height: 1.4; }
.scene-dots { display: flex; gap: 6px; margin: 2px 0; }
.scene-dot { width: 7px; height: 7px; border-radius: 50%;
  background: var(--bar-track); transition: background var(--dur-2) var(--ease-glass); }
.scene-dot.done { background: color-mix(in srgb, var(--tile-terra) 55%, transparent); }
/* El delit S1 · the dot that is live swells once as it arrives. The row is
   rebuilt per card (player.js sceneDots), so this fires as the scene steps on —
   a beat of "you moved", never an idle pulse. The colour alone is the static
   twin. */
.scene-dot.on { background: var(--tile-terra);
  animation: dot-pop var(--dur-2) var(--ease-pop); }
@keyframes dot-pop {
  0% { transform: scale(1); } 45% { transform: scale(1.5); } 100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .scene-dot { transition: none; }
  .scene-dot.on { animation: none; }
}

.exercise {
  background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow);
  border: 1px solid color-mix(in srgb, var(--bar-track) 60%, transparent);
  padding: 24px; margin-top: 14px;
  animation: rise 0.25s ease-out;
}
@keyframes rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .exercise, .feedback { animation: none !important; }
  .bar-fill { transition: none !important; }
}
.prompt { font-size: 19px; margin: 0 0 16px; }
/* A conjugation prompt names its tense; the «?» beside it says what that word
   MEANS. Same grid trick as .frase-epigraf: `.gloss` is display:contents so the
   button sits on the prompt line and the revealed line spans underneath. */
.prompt-fila { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline; column-gap: 6px; margin: 0 0 16px; }
.prompt-fila .prompt { margin: 0; }
.prompt-fila .gloss { display: contents; }
.prompt-fila .meaning-line { grid-column: 1 / -1; font-size: 13px; color: var(--muted);
  margin: 6px 0 0; }
.say-line { font-size: 21px; font-weight: 600; color: var(--accent); margin: 6px 0 12px; }

.answer-input {
  width: 100%; font: inherit; font-size: 19px;
  padding: 12px 14px; margin-top: 8px;
  border: 2px solid var(--bar-track); border-radius: 10px;
  background: var(--bg); color: var(--text);
}
.answer-input:focus { border-color: var(--accent); outline: none; }

.scene { font-style: italic; color: var(--muted); font-size: 15px;
  margin: 0 0 10px; padding-left: 10px; border-left: 3px solid var(--accent-soft); }
.actions-inline { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

.es-bridge { margin-top: 12px; font-size: 14px; color: var(--muted); }
.es-bridge summary { cursor: pointer; width: max-content; padding: 2px 10px;
  border: 1px solid var(--bar-track); border-radius: 999px; list-style: none; }
.es-bridge summary::-webkit-details-marker { display: none; }
.es-bridge span { display: block; padding-top: 6px; font-style: italic; }

.actions { display: flex; gap: 10px; margin-top: 16px; }

.btn {
  font: inherit; border: none; cursor: pointer; border-radius: 10px;
  padding: 11px 20px; background: var(--bar-track); color: var(--text);
  text-decoration: none; /* anchor-buttons match button-buttons (A4) */
  transition: transform 0.08s ease, box-shadow 0.15s ease, filter 0.15s ease;
}
.btn:hover:not(:disabled) { filter: brightness(1.05); box-shadow: 0 2px 8px rgba(43, 38, 32, 0.12); }
.btn:active:not(:disabled) { transform: scale(0.97); }
/* El delit S1 · the keyboard presses it too. `:active` is a pointer state, so
   answering with Enter — the primary mode at a desk — got no acknowledgment at
   all. player.js's morphToNext flashes this for ~120ms: the SAME dip as a
   click, so the two input modes speak one language. It must also outrank the
   system layer's `.btn:disabled { transform: none }` (equal specificity, later
   file), because morphToNext disables the button in the same breath. */
.btn.is-premut { transform: scale(0.97); }
@media (prefers-reduced-motion: reduce) { .btn.is-premut { transform: none; } }
/* Fills carry white (law 1, v2): literal white in BOTH themes over
   --accent-fill, which deepens in dark. The ochre warmth rides the gradient in
   LIGHT only (4.97:1 measured): white over ANY amber-warmed end of the dark
   fill tops out at 4.48:1 — under AA. Same trap the 18 Jul 2026 audit caught
   on the v1 gradient: the far end is lighter than the flat fill the mocks
   measured. So dark drops the warmth and runs the flat fill (4.64:1). */
.btn-primary { --btn-warm: color-mix(in srgb, var(--accent-fill) 88%, var(--accent-2));
  background: linear-gradient(140deg, var(--btn-warm), var(--accent-fill));
  color: #fff; font-weight: 600; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn-primary { --btn-warm: var(--accent-fill); }
}
[data-theme="dark"] .btn-primary { --btn-warm: var(--accent-fill); }
/* Filled buttons hover DOWN, never up (22 Jul 2026). The base .btn:hover
   brightens, which is right for a pale glass control and exactly wrong for a
   white-on-hue fill: white cannot brighten past white, so lightening the ground
   only closes the gap — the terracotta CTAs washed out under the cursor and the
   label all but vanished. Deepening the fill instead keeps the same "something
   happened" feedback (plus the lift and the sweep, untouched) while contrast
   IMPROVES on hover rather than degrading. */
.btn-primary:hover:not(:disabled), .btn-next:hover:not(:disabled) {
  filter: brightness(0.92) saturate(1.06); }
/* A filled control that happens to be a LINK keeps its white label (23 Jul 2026).
   `a:hover { color: var(--accent-ink) }` (mica-sistema.css §links) is (0,1,1) and
   beat `.btn-primary { color:#fff }` at (0,1,0), so hovering an anchor CTA — el
   fil's «Continua», any .btn-primary rendered as an <a> — repainted the label
   dark terracotta ON the terracotta fill: ~1.2:1, effectively invisible. Light
   only, because dark overrides --accent-ink back to the luminous raw hue. Match
   the hover pseudo-class so specificity ties are impossible to lose. */
a.btn-primary:hover, a.btn-primary:focus,
a.btn-next:hover, a.btn-next:focus,
a.skip-link:hover, a.skip-link:focus { color: #fff; }
.btn-ghost { background: transparent; color: var(--muted); text-decoration: underline; }
/* Comprova morphed into Següent. It drops the accent deliberately: terracotta is
   the app's "make a decision" colour, and advancing is a continuation, not a
   decision. It must still read as fully LIVE, though — the first attempt used the
   neutral .btn base (--bar-track, the progress-track fill) and that is the app's
   inactive-background token, so the live button read as greyed out. A saturated
   fill with white text is unambiguous, and blue is the cool counterweight. */
.btn-next { background: var(--advance); color: #fff;
  font-weight: 600; letter-spacing: 0.01em; }
.btn-small { padding: 6px 14px; font-size: 14px; text-decoration: none; display: inline-block; }
.btn-audio { background: var(--accent-soft); color: var(--accent-ink); margin-bottom: 8px; }
.btn:disabled { opacity: 0.5; cursor: default; }

/* ---- feedback ---- */
.feedback { border-radius: var(--radius); margin-top: 14px; padding: 0 22px; }
/* The system layer gives .feedback a border, and the element is mounted empty
   (it is the aria-live region), so an unanswered question sat above a 2px
   hairline sliver — a card's worth of chrome for nothing. Emptied rather than
   hidden: a live region must stay in the DOM to announce when it fills. */
.feedback:empty { border: 0; margin-top: 0; }
.feedback.ok, .feedback.miss { animation: rise 0.2s ease-out; }
.feedback.ok { background: var(--ok-soft); padding: 16px 22px;
  border-left: 4px solid var(--ok); }
.feedback.miss { background: var(--miss-soft); padding: 16px 22px;
  border-left: 4px solid var(--miss); }
.examples { font-size: 15px; margin: 6px 0; }
.examples em { color: var(--accent); font-style: normal; font-weight: 600; }
.verdict { font-size: 19px; font-weight: 700; margin: 0 0 6px; }
/* El delit S4 · the verdict inks in. The panel already rises as a whole (`rise`,
   above); this is the headline WORD arriving — the `gram-ink` blur→sharp idiom
   one surface over, so a verdict lands like a stamp pressed on paper rather
   than a label swapped in. Three things keep it from becoming a wait:
   · --dur-1, the app's shortest token. This is felt on every single answer, and
     the plan's own risk note for it is «heavy when felt every answer» — at 160ms
     it is under the reaction floor, more texture than event.
   · NO fill mode (l'onada's rule): the resting state is the true one, so a
     timeline that never advances — a throttled tab — still shows a fully inked
     verdict rather than a blurred ghost.
   · No class dance is needed: renderFeedback clears the live region and builds a
     fresh .verdict node per answer, so the animation fires on mount. (The panel
     itself cannot do this — it persists, and remove+add of .ok/.miss inside one
     task never restarts an animation.) */
.verdict { animation: verdict-tinta var(--dur-1) var(--ease-glass); }
@keyframes verdict-tinta { from { opacity: 0.4; filter: blur(1.4px); } to { opacity: 1; filter: blur(0); } }
@media (prefers-reduced-motion: reduce) { .verdict { animation: none; } }
.ok .verdict { color: var(--ok); }
.miss .verdict { color: var(--miss); }
/* Register-recognition miss (tria): a "not quite", not a fault — the pick was
   valid Catalan, only the wrong register. Amber, overriding the red miss ink
   at higher specificity than `.miss .verdict`. */
.miss .verdict.verdict-quasi { color: var(--tile-amber-ink); }
/* Two-phase headline («L'orella ✗ · la resposta ✓»): it carries its own marks,
   one per half, so painting the whole line the panel's single verdict colour
   would contradict the half that went right. Neutral ink, at the specificity
   the .ok/.miss rules use. */
.ok .verdict.verdict-dues, .miss .verdict.verdict-dues { color: var(--text); }
/* …and the half that FAILED wears the miss ink, so which muscle slipped is
   read off the colour rather than off a 19px glyph. The passing half stays
   neutral — that is what keeps the line from reading as one verdict. */
.ok .verdict-dues .fase-fallida, .miss .verdict-dues .fase-fallida { color: var(--miss); }
/* Miss corrections: one chip per real error, «what you wrote» → «the fix».
   The learner reads the culprit word directly instead of hunting it in the
   whole sentence. Accent-only slips never reach here (diffWords forgives them). */
.diff { margin: 6px 0; display: flex; flex-wrap: wrap; gap: 8px; }
.diff-fix { display: inline-flex; align-items: baseline; gap: 5px;
  background: var(--surface); border: 1px solid var(--miss);
  border-radius: 6px; padding: 2px 8px; }
.diff-jo { color: var(--muted); text-decoration: line-through; }
.diff-fletxa { color: var(--muted); }
.diff-bad { font-weight: 700; color: var(--miss); }
/* Names the chips that carry only one side: a surplus word or a missing one.
   Without it the chip is a lone word with no verdict attached. */
.diff-nota { font-size: 12px; color: var(--muted); }
/* busca-error's correction line: the tap names the word that is WRONG, this
   names the one that belongs. Neutral ground and per-half ink, the .verdict-dues
   rule one rung down — the line carries its own before and after, so painting
   the whole thing the panel's single verdict colour would contradict one half
   of it. It renders on a hit as well as a miss: same information either way. */
.correccio { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px;
  font-size: 16px; margin: 6px 0; }
/* --muted-firm, not --muted: this line is 16px body copy on the feedback's own
   tinted ground, where --muted measures 4.14:1. The firmer secondary ink clears
   AA on both the ok and the miss panel (5.2 / 5.6) at the same visual weight. */
.correccio-fora { color: var(--muted-firm); text-decoration: line-through; }
.correccio-fletxa { color: var(--muted-firm); }
/* The corrected form is BOLD, not green. It first shipped in --tile-green-ink,
   which measured 1.61:1 on the dark .feedback.ok ground — the one panel this
   line has to be legible on, since it renders on a HIT as well as a miss, and
   green ink on the green correct-panel is green on green. The strike-through
   opposite already says which of the two words is out; --text is the ink the
   .why beneath it uses and it passes on both grounds, in both themes. */
.correccio-dins { font-weight: 700; color: var(--text); }
/* classifica's per-piece map — the diff idiom's vocabulary at piece grain.
   One line per phrase: the ones he sorted right say so quietly, the ones he did
   not name the drawer they belong in. Never a second copy of the canonical
   answer string, which is a serialization and not a sentence. */
.calaixos-repas { display: flex; flex-direction: column; gap: 4px; margin: 6px 0; }
.calaix-repas { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
  font-size: 14px; margin: 0; }
.calaix-peca { color: var(--text); }
.calaix-ok { color: var(--tile-green-ink); }
.calaix-mal { color: var(--miss); font-weight: 700; }
.why { font-size: 15px; margin: 8px 0; }
.capture { font-size: 14px; color: var(--muted); margin: 6px 0; }
.footnote { font-size: 13px; color: var(--muted); margin: 6px 0; }
/* The register rung's nudge. NOT a .footnote: it used to be the faintest line
   on a panel whose .why argues for the street form, so the one sentence that
   reconciled "✓ correcte" with that why was the easiest thing to miss. Amber =
   "started, not yet there", matching the gimnàs cell ladder. */
.nudge-registre {
  font-size: 15px;
  font-weight: 600;
  color: var(--tile-amber-ink);
  margin: 6px 0;
}
.hint { font-size: 14px; color: var(--muted); }
.feedback .btn { margin: 10px 0 4px; }

/* On-demand English meaning — a quiet ghost toggle, then a plain reveal.
   The aids row exists to keep the passive gloss on its own line rather than
   inline with the verdict. It used to carry a hairline separating it from the
   action row below; Següent now lives up in the card, so the row is last and
   the hairline underlined nothing. */
.feedback-aids { margin: 10px 0 2px; }
.feedback-aids .btn-reveal { margin: 0; padding-left: 0; padding-right: 0; font-size: 14px; }
.btn-reveal { display: inline-flex; align-items: center; gap: 5px; }
.btn-reveal .reveal-ico { display: inline-flex; }
.meaning-line { font-size: 15px; margin: 8px 0 2px; color: var(--text);
  font-style: italic; opacity: 0.9; }
.meaning-line[hidden] { display: none; }
/* El delit S1 · the meaning rises rather than appearing. `hidden` still governs
   display (it is what keeps the panel out of the a11y tree while closed, and
   `display` cannot transition), so this is an ANIMATION on the open class that
   gloss.js adds in the same breath — the app's existing `rise`, borrowed from
   .exercise. Closing stays instant: taking a reveal back is not an arrival.
   Post-answer glosses happen dozens of times a session, so it is --dur-1: soft,
   never something to wait through. */
.meaning-line.is-obert { animation: rise var(--dur-1) var(--ease-glass); }
@media (prefers-reduced-motion: reduce) { .meaning-line.is-obert { animation: none; } }

/* The keyboard keeper (components/teclat.js): a real, focusable text input that
   holds the software keyboard open across a re-render. It must NOT be
   display:none, visibility:hidden or opacity-only-on-a-zero-box — all of those
   are either unfocusable or open no keyboard, which is the entire job. Fixed,
   1px, fully transparent and inert to the pointer: reachable by focus() and by
   nothing else. Never in the tab order (tabIndex -1) and aria-hidden. */
.teclat-keeper {
  position: fixed; top: 0; left: 0;
  width: 1px; height: 1px;
  opacity: 0; border: none; padding: 0; margin: 0;
  pointer-events: none;
  font-size: 16px; /* the iOS zoom law applies even to a field you cannot see */
}

/* ---- Amb el teclat obert ----
   The keyboard is now HELD open across the whole answer→verdict→next cycle
   (app/js/components/teclat.js), which is what removes the redundant tap — but
   it also costs roughly half a phone screen for the entire cycle instead of
   just the typing. So the session reclaims that height while the keyboard is
   up: `.is-teclat-obert` is set on <html> by initTeclat() whenever the visual
   viewport shrinks past a keyboard-sized threshold, and drops off the moment it
   returns. Every value in THIS half is a tightening of an existing one — no
   element is hidden or moved; the two that are hidden are the app's own chrome
   and are ruled on at the foot of the block. Hiding the prompt once answered
   would buy the most room and was rejected, and stays rejected: re-reading the
   question IS how you read the correction.
   Mobile-tier only by nature (no software keyboard, no class), so these need no
   media query — and no desktop rule may be added to this block. */
.is-teclat-obert .session-progress { margin: var(--space-1) 0 2px; }
/* The medallion is the one rigid object in the session header, and at 48px it
   is what SETS that row's height — the block label, the sortida and «7/20» are
   all shorter than it. It is the live mosaic filling, so it earns its size on a
   full screen and not on a third of one; at 32px the row costs 16px less and
   the piece is still perfectly readable as it fills. The svg carries a viewBox,
   so `100%/100%` scales the drawing rather than cropping it, and the flying
   tessel·la still lands on it — the flight reads the medallion's rect. */
.is-teclat-obert .mosaic-medallion { width: 32px; height: 32px; flex: 0 0 32px; }
.is-teclat-obert .mosaic-medallion svg { width: 100%; height: 100%; }
/* Keep ordinary card spacing while the keyboard is open. The sticky action
   owns the bottom inset so scrolling text never leaks beneath the button. */
.is-teclat-obert .exercise {
  --peu-targeta: 18px;
  padding: 18px 16px; margin-top: var(--space-2);
}
/* The card's own rhythm closes up the last notch, the same treatment the
   feedback's internals get below: measured on a 393x852 iPhone the tallest
   ORDINARY card — a translate with a full answer box, a two-line why and the
   «què vol dir?» row — finished one pixel under the keys, and a keyboard whose
   predictive bar is a few px taller than the one measured would have buried it.
   Nothing is hidden and no line-height moves; only the gaps between the blocks
   close, and the field keeps its 19px (the iOS zoom law) and its 44px target. */
.is-teclat-obert .answer-input { margin-top: 10px; }
.is-teclat-obert .feedback.ok, .is-teclat-obert .feedback.miss { padding: var(--space-4) 18px; }
/* The verdict stamp is the one piece of pure ornament in the panel, and at 26px
   display type in a rotated frame it was the tallest block in it — a single
   word outweighing the correction it introduces. It keeps its tilt, its rule
   and its thunk; it just stops being the biggest thing in a band it is sharing
   with the answer. Scoped to the round's feedback, so the session-end stamp on
   a full screen is untouched. */
.is-teclat-obert .feedback .stamp { font-size: 21px; padding: 2px 13px; }
.is-teclat-obert .feedback .stamp-rare { font-size: 24px; padding: 3px 18px; }
.is-teclat-obert .prompt { margin-bottom: var(--space-3); }
.is-teclat-obert .actions { margin-top: var(--space-4); }
.is-teclat-obert .es-bridge { margin-top: var(--space-3); }
.is-teclat-obert .scene-line { margin: var(--space-2) 0 2px; }
.is-teclat-obert .feedback { margin-top: var(--space-4); }
/* The worst case for fitting is a long MISS — verdict, diff, «capturat», the
   why line and the gloss row stacked — and its internal rhythm is the most
   compressible height left in the session. Measured: without this the card's
   prompt lost its first line to the top edge (424px of content into a 420px
   band). Line-height is untouched; only the gaps between blocks close up. */
.is-teclat-obert .verdict { margin-bottom: var(--space-2); }
.is-teclat-obert .feedback .examples,
.is-teclat-obert .feedback .diff,
.is-teclat-obert .feedback .why,
.is-teclat-obert .feedback .capture,
.is-teclat-obert .feedback .nudge-registre,
.is-teclat-obert .feedback .footnote { margin-top: var(--space-2); margin-bottom: var(--space-2); }
.is-teclat-obert .feedback-aids { margin: var(--space-3) 0 0; }
/* The stage's radial glow is drawn from a -14px/-18px bleed that a compacted
   card no longer has room for; shrink the bleed with it or it laps the tabbar.
   BLOCK ONLY, and that is the whole point of the longhand. Written as the
   `inset` shorthand this also restated the -16px INLINE bleed, and at (0,2,1)
   it out-specified the ≤480 clamp below (`inset: -14px 0 -18px`, added because
   the negative horizontal reach overruns the small-phone gutter) — so the one
   state that always holds a keyboard was the one state that widened the
   document: measured scrollWidth 394 against a 393 clientWidth on an iPhone
   mid-answer. One px of overflow is enough for iOS to hand the whole page an
   elastic horizontal drag, which is how a scroll DOWN to read the verdict came
   back as a page slid sideways. The width tier owns the inline insets; this
   rule may only ever speak about the vertical ones. */
.is-teclat-obert .session-stage::before { inset-block: -6px -8px; }
/* LA BANDA ÉS LA FINESTRA — I NO HI HA RES MÉS PER DESPLAÇAR (9 Sep 2026).
   This used to reserve the keyboard's own height BELOW the stage, so the page
   had somewhere to scroll to when the verdict was stuck under the keys. That
   made sense while the window was the thing doing the scrolling; since the card
   became the scroller it is pure liability, and Ryan's third screenshot is what
   it costs: ~350px of empty document under the card that a swipe scrolls into,
   revealing the fixed tabbar stranded in the middle of the screen and panning
   the visual viewport hard enough to flip `.is-teclat-obert` off and on — which
   resizes the document mid-gesture, which is the enormous jitter (the latch and
   the hysteresis in teclat.js are the other half of that fix).
   So: no reserve here at all, and `main` ends its content box where the keys
   begin instead (see «the band, measured from the bottom» below). With nothing
   below the fold there is nothing to scroll to, the tabbar cannot be reached,
   and the only scrolling left in the round happens inside the card, which is
   bounded and contains its own overscroll. */
.is-teclat-obert .session-stage { padding-bottom: var(--space-3); }

/* ---- THE CHROME YIELDS WHILE THE KEYBOARD IS UP ----
   A software keyboard takes roughly half a phone screen, and everything it
   takes comes off the ONE band the learner is reading in. Measured on a
   393x852 iPhone, band 517px: a resolved card ran to 560px on a plain «exacte»
   and 726px on a long miss, so the verdict was under the keys on every single
   answer and the round became «answer, scroll, read, scroll back». The two
   things standing in that band that are not the card are the app's own chrome,
   and neither can be used while a keyboard is over it — so while the keyboard
   is up they get out of the way. This is what makes the verdict fit rather
   than something the page has to scroll to reach: the page not moving on a
   resolve is a standing rule (see keepVisible's `minVisible`), so the room has
   to be FOUND, never borrowed from the scroll position.

   THE TABBAR IS NEVER VISIBLE HERE ANYWAY, AND THAT IS THE BUG IT FIXES. It is
   `position: fixed` to the bottom of the LAYOUT viewport, which a keyboard does
   not shrink — so it sits behind the keys, and the only way it ever reached the
   eye was as an artefact: scroll far enough and iOS pans the visual viewport
   down over it, landing a full navigation bar in the middle of the screen,
   between the card and the keyboard. Hiding it says what was already true.
   `main`'s bottom padding goes with it, because that padding exists for exactly
   one reason — clearing the fixed tabbar — and left standing it is 78px of dead
   document that only ever served to make that over-scroll possible.

   THE TOPBAR YIELDS ONLY OVER A CARD. It is preferences plus la butxaca's
   availability disclosure (theme, llengua, so, perfil, away status), none of
   which is what anyone is doing mid-answer, and it is the one thing in the band belonging to no part of the
   exercise — 64px plus its gutter, the single biggest reclaim available and the
   only one that costs no legibility. Scoped with `:has()` to a surface that is
   actually a card at a time: registre's catalogue has a search field too, and a
   browse surface must not shed its chrome the moment you type into it. */
.is-teclat-obert .tabbar { display: none; }
.is-teclat-obert main { padding-bottom: 0; }
:root.is-teclat-obert:has(.session-stage) .topbar { display: none; }

/* Keyboard-open sessions use the visible band. Main owns keyboard clearance;
   the card owns scrolling, and feedback stays in that same card. */
:root.is-teclat-obert:has(.session-stage) main {
  display: flex; flex-direction: column;
  /* THE BAND, AND IT IS MEASURED FROM THE BOTTOM ON PURPOSE. `main` already
     stretches to the foot of the body shell, so padding its own bottom by the
     keyboard's height ends its CONTENT box exactly where the keys begin — and
     because the padding is on the element the flex container is already sizing,
     the box does not grow and the document stays exactly one viewport tall.
     That is the whole fix for the scroll: there is nowhere left to scroll TO.
     Sizing it from the top instead (height = the visual viewport) was tried and
     is wrong by 8px, because `main` does not start at the top of the screen —
     anything above it has to be subtracted, and nothing here should have to know
     what that is. Measuring from the bottom needs no such knowledge.
     This must outrank `.is-teclat-obert main { padding-bottom: 0 }` above, which
     exists only to drop the fixed tabbar's clearance; it does, at (0,3,1). */
  padding-bottom: var(--teclat-h, 0px);
}
.is-teclat-obert .session-stage {
  flex: 1 1 0; min-height: 0;
  display: flex; flex-direction: column;
}
.is-teclat-obert .session-stage > * { flex: none; }
/* Question flow · 10 Sep 2026. The prompt keeps its place when feedback
   arrives. The card is the sole scroll surface; the action stays at its foot.
   No resolved-state shrinking, no nested feedback scroll, no bottom-aligned
   prompt that jumps by the height of its explanation. */
.is-teclat-obert .session-stage > .exercise {
  flex: 1 1 0; min-height: 0; overflow-y: auto;
  margin-top: var(--space-2); padding-bottom: 0;
  display: flex; flex-direction: column;
  overscroll-behavior: contain;
}
.is-teclat-obert .exercise > * { flex: none; }
.is-teclat-obert .exercise > .feedback { order: 1; flex: none; }
.is-teclat-obert .exercise > .actions {
  order: 2; position: sticky; bottom: 0; z-index: 1;
  margin-top: auto; padding-top: var(--space-4);
  padding-bottom: var(--peu-targeta, var(--space-4));
  background: var(--surface);
}
.is-teclat-obert .exercise > .actions::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 100%;
  height: var(--space-4); pointer-events: none;
  background: linear-gradient(to top, var(--surface), transparent);
}
.is-teclat-obert .actions .btn { flex: 1 1 auto; }
/* Keep the answer and bridge geometry stable. Resolved inputs retain focus to
   hold iOS's keyboard, but the caret and touch-only focus halo retire. */
.is-resolt .answer-input, .is-resolt .cloze-gap { caret-color: transparent; }
.is-teclat-obert .is-resolt .answer-input,
.is-teclat-obert .is-resolt .cloze-gap {
  outline: none !important; box-shadow: none;
}
/* The global focus rule is !important. Touch typing uses the gap’s underline
   and tinted fill; hardware-keyboard navigation keeps the global ring. */
.is-teclat-obert .cloze-gap:focus-visible { outline: none !important; }
.is-teclat-obert .is-resolt .cloze-gap {
  border-bottom-color: var(--accent); background: transparent;
}
.session-stage .actions > .btn {
  /* Colour and label acknowledge the answer without moving the touch target. */
  transform: none;
  min-height: var(--space-8); padding: var(--space-3) var(--space-4);
  font-size: var(--fs-4); line-height: var(--lh-ui);
}
.session-stage .feedback .stamp,
.session-stage .feedback .stamp-rare {
  font-size: var(--fs-5); line-height: var(--lh-ui);
  padding: var(--space-1) var(--space-3); margin: 0;
}
.session-stage .feedback.ok, .session-stage .feedback.miss {
  padding: var(--space-4); box-shadow: none;
}
.session-stage .es-bridge summary {
  min-height: var(--space-7); display: flex; align-items: center;
  padding-inline: var(--space-3);
}

/* Answer → review. The keyboard is dismissed for reading; the document gets
   its height back. No scrolling card, sticky overlay, fading text or doubled
   frame. Exceptional long explanations use ordinary page scrolling. */
.session-stage > .exercise.is-reading-answer {
  display: flex; flex-direction: column;
  flex: none; overflow: visible; max-height: none;
  padding: var(--space-4); margin-top: var(--space-2);
}
.exercise.is-reading-answer > .feedback { order: 1; }
.exercise.is-reading-answer > .actions {
  order: 2; position: static; padding: 0;
  margin-top: var(--space-5);
}
.exercise.is-reading-answer > .actions::before { content: none; }
.exercise.is-reading-answer > .actions .btn { width: 100%; }
.exercise.is-reading-answer .es-bridge { display: none; }
.exercise.is-reading-answer input, .exercise.is-reading-answer textarea {
  outline: none !important; box-shadow: none; caret-color: transparent;
}
.session-stage .exercise::before { content: none; }
.session-stage .exercise {
  backdrop-filter: none; -webkit-backdrop-filter: none;
  background: var(--surface); box-shadow: var(--shadow-1);
}
.session-stage .feedback { backdrop-filter: none; -webkit-backdrop-filter: none; }
.session-stage .feedback .verdict {
  font-size: var(--fs-5); line-height: var(--lh-ui);
}
/* Avoid a preferences header returning above the sentence when the keys close.
   Navigation returns below the reading card, retaining the existing way out. */
@media (max-width: 480px) {
  :root:has(.session-stage) .topbar { display: none; }
}
:root:has(.is-reading-answer) main {
  display: block; padding-bottom: calc(var(--space-8) + var(--space-6) + env(safe-area-inset-bottom));
}
.session-stage:has(> .is-reading-answer) { display: block; }

/* ---- unscramble ---- */
.unscramble-line { min-height: 44px; padding: 8px; border-radius: 10px;
  background: var(--bg); margin-bottom: 12px; }
.unscramble-pool { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  font: inherit; font-size: 16px; padding: 7px 14px; cursor: pointer;
  border: 1.5px solid var(--bar-track); border-radius: 999px;
  background: var(--surface); color: var(--text);
}
.chip-chosen { background: var(--accent-soft); border-color: var(--accent); margin: 2px; }
.chip-used { opacity: 0.25; }

/* ---- busca l'error (tap the word that's wrong) ---- */
/* The sentence stays a sentence — words in reading order, no boxes until you
   hover one — but every word is a real 44px target, which is what pushes the
   line-height up. Deliberately NOT the .chip pill: a pill row reads as a POOL
   of loose words (unscramble's meaning), and here the order is the sentence's
   own and carries the error. */
.frase-mots { display: flex; flex-wrap: wrap; gap: 2px; margin: 0 0 10px; }
.mot {
  display: inline-flex; align-items: center; min-height: 44px;
  font: inherit; font-size: 18px; padding: 0 7px; cursor: pointer;
  border: 1.5px solid transparent; border-radius: 8px;
  background: none; color: var(--text);
  transition: border-color 0.15s ease, background 0.15s ease;
}
@media (hover: hover) and (pointer: fine) { .mot:hover { border-color: var(--bar-track); } }
.mot.is-triat { border-color: var(--accent); background: var(--accent-soft);
  color: var(--accent-ink); font-weight: 600; }

/* ---- classifica (sort the phrases into their drawers) ---- */
/* One row per piece, the buckets as a segmented control on the right. Rows over
   drag-and-drop: dragging is a mouse idiom that becomes a fiddle on a phone,
   and this way every target is a button, keyboard-operable for free. */
.classifica { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 2px; }
.classifica-fila {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 8px; padding: 8px 12px;
  border: 1.5px solid var(--bar-track); border-radius: 12px; background: var(--surface);
}
.classifica-peca { font-size: 16px; flex: 1 1 190px; }
.classifica-calaixos { display: flex; gap: 6px; }
.calaix-tria {
  display: inline-flex; align-items: center; min-height: 44px;
  font: inherit; font-size: 15px; padding: 0 14px; cursor: pointer;
  border: 1.5px solid var(--bar-track); border-radius: 999px;
  background: var(--bg); color: var(--text-secondary);
  transition: border-color 0.15s ease, background 0.15s ease;
}
@media (hover: hover) and (pointer: fine) { .calaix-tria:hover { border-color: var(--accent); } }
.calaix-tria.is-triat { border-color: var(--accent); background: var(--accent-soft);
  color: var(--accent-ink); font-weight: 600; }

/* ---- ordena el diàleg (put the exchange back in order) ---- */
/* Unscramble's line-and-pool, one granularity up: a turn is a BLOCK, so the two
   halves stack instead of wrapping as chips. The built exchange is an <ol> — the
   numbers are the whole feedback the surface gives while he works (turn 3 is
   where he thinks it goes), and they come free with the element. */
.dialeg-linia { list-style: none; display: flex; flex-direction: column; gap: 4px;
  counter-reset: replica; margin: 0 0 16px; padding: 0; }
.dialeg-linia li { counter-increment: replica; display: flex; align-items: stretch; gap: 8px; }
.dialeg-linia li::before { content: counter(replica); flex: 0 0 18px; padding-top: 12px;
  font-size: 13px; color: var(--muted-firm); text-align: right; }
.dialeg-buida { counter-increment: none; }
.dialeg-buida::before { content: none; }
.dialeg-pool { display: flex; flex-direction: column; gap: 6px; }
.replica {
  display: block; width: 100%; min-height: 44px; padding: 10px 14px;
  font: inherit; font-size: 16px; text-align: left; cursor: pointer;
  border: 1.5px solid var(--bar-track); border-radius: 12px;
  background: var(--surface); color: var(--text);
  transition: border-color 0.15s ease, background 0.15s ease;
}
@media (hover: hover) and (pointer: fine) { .replica:hover { border-color: var(--accent); } }
.replica-triada { border-color: var(--accent); background: var(--accent-soft);
  color: var(--accent-ink); }
/* --op-spent, the consumed rung: the card stays in the pool holding its place,
   so the shuffle he has been reading does not reflow under his finger. */
.replica-usada { opacity: var(--op-spent); cursor: default; }

/* ---- el dictogloss (hear it, hold it, rebuild it) ---- */
/* No renderer furniture of its own beyond the audio row — the card is a prompt,
   a play button and the retell box. The hint sits between them because it is the
   rule of the exercise («de memòria»), not a footnote to the answer field. */
.dictogloss .hint { margin: 10px 0 0; }
.dictogloss .retell-box { min-height: 88px; }
/* The rebuilt sentence in the feedback: a SENTENCE, not a chip row, so it undoes
   .diff's flex. He is reading a line of Catalan with the words he lost inked, and
   8px between every word turns that back into a word list. */
.diff-frase { display: block; font-size: 17px; line-height: 1.6; }

@media (prefers-reduced-motion: reduce) {
  .mot, .calaix-tria, .replica { transition: none; }
}

/* ---- session end ---- */
.session-end { text-align: center; padding-top: 40px; }
/* h1 included for the repte/dos "Encara no" gate landings (promoted h2→h1).
   font-size matches the old h2; the margin pin below restores the h2 block
   margin so parity holds (UA h1 is 0.67em, h2 is 0.83em). */
.session-end h1, .session-end h2 { font-size: 26px; }
.session-end h1 { margin: 0.83em 0; }
.end-stats { font-size: 18px; }
.session-end .actions { justify-content: center; margin-top: 24px; }

/* ---- curriculum marks (●◉○ — the Curs ladder and the gram phase heads;
        the old Progrés camí ledger is gone, position lives in Curs) ---- */
.path-mark { z-index: 1; width: 30px; color: var(--muted); font-size: 17px; line-height: 1.4;
  text-align: center; text-shadow: 0 0 0 var(--bg); }

/* ---- dashboard / lists ---- */
/* A view's landing title is an h1 (one per view); section titles stay h2.
   font-size: 1.5em pins the promoted h1 to the old UA h2 size so the h1→h2
   promotion is visually byte-identical — without it the UA h1 default (2em)
   would leak. A no-op for h2, which UA already sizes at 1.5em here. */
.dashboard h1, .dashboard h2 { font-family: var(--display); margin: 14px 0 4px; font-size: 1.5em; }
.session-end h1, .session-end h2 { font-family: var(--display); }
.dash-head { display: flex; justify-content: space-between; align-items: baseline; }
.cefr { font-size: 14px; color: var(--muted); }
.dash-section { background: var(--surface); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 14px 18px; margin-top: 14px; }
/* h2 included for the A5 heading repair (dash-section titles under a view h1);
   font-family pins the body face so the .dashboard h2 display-serif rule above
   can't leak into these small uppercase labels.
   NOT INSIDE A PROGRÉS ZONE (9 Aug 2026). Progrés v2 reuses `dash-section` for
   its three zones and already neutralises this block's BOX (see
   `.progres-page .progres-zone`), but the heading rule kept reaching the h3
   every family, la corba, Paraules and la biblioteca name themselves with —
   uppercasing them, tracking them out and painting them `--muted`. A block
   title then read as a second overline inside a zone that is allowed exactly
   one, and «PRONOMS FEBLES» is ~19 % wider than «Pronoms febles», which is what
   pushed it onto two lines at every width the page has. Scoped out at the
   source rather than counteracted property by property downstream: the v2 head
   grammar (Cormorant 600 / 26px / `--text`) is then simply what `.rhythm-family
   h3` and the system's own heading rule say. Escoltar's cards, the other
   `dash-section` caller, are untouched — the small uppercase label is right
   there. */
.dash-section:not(.progres-zone) h2,
.dash-section:not(.progres-zone) h3 { margin: 0 0 10px; font-size: 15px; letter-spacing: 0.04em;
  color: var(--muted); text-transform: uppercase; font-family: inherit; }
.skill-row { display: grid; grid-template-columns: 1fr 2fr 34px; gap: 10px;
  align-items: center; margin: 7px 0; font-size: 15px; }
.skill-val { text-align: right; color: var(--muted); font-size: 13px; }
.low-confidence { opacity: 0.55; }
.stat-line { margin: 4px 0; font-size: 15px; }
.faint { color: var(--muted); }
.error-list { margin: 4px 0; padding-left: 20px; font-size: 14px; color: var(--muted); }

/* ---- Món · la portada viva (docs/MON-REDESIGN-PLAN.md) ----
   A cultural front page in two acts, not five instances of one doorway. The
   hierarchy is carried by CONTENT SCALE AND MATERIAL, never by chrome: the
   cover story is the largest cultural object on the page, the quiosc reads as
   newsprint on --paper, the radio as deeper enamel on --mat, the rebost as
   ceramic on --surface. Each module keeps exactly one action.

   Elevation honesty: every card here is a link, so raised is right; the
   portada, the «aquesta setmana» overline and the module rubrics are flat.
   No new vidre, so no new backdrop-filter and no @supports fallback. */
/* THE PORTADA HEAD (design/handoffs/titols-v2, 6 Aug 2026): the front page is
   dated and says so, then names itself at display scale over the contour. Same
   recipe as el curs's head one section up and as the three llocs that already
   had one — the five rooms open the same way. */
.mon-portada { position: relative; overflow: hidden; margin: 14px 0 0;
  padding: clamp(12px, 2vw, 26px) 0 clamp(10px, 1.6vw, 18px); }
.mon-portada-copia { position: relative; z-index: 1; max-width: 34ch;
  display: flex; flex-direction: column; gap: 10px; }
.mon-portada-rubric { margin: 0; font-size: 11px; letter-spacing: 0.16em;
  text-transform: uppercase; font-weight: 600; color: var(--accent-ink); }
.mon-portada h1 { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(40px, 5.4vw, 58px); line-height: 1.02;
  letter-spacing: 0.005em; text-wrap: balance; }
/* Read, never acted on, so it stays bare prose — no card, no glaze (la frase
   del dia's rule, and el camí's hero epigraph, same face). */
.mon-portada-epigraf { margin: 0; font-family: var(--display); font-style: italic;
  font-size: 20px; line-height: 1.45; color: var(--muted); text-wrap: pretty; }
/* The entrance fills BACKWARDS and mon.js settles it on a timer whether or not
   a frame was served (the mosaic's rule; the sections below are unchanged and
   still arrive painted). */
.mon-portada.is-entrant { animation: mon-puja var(--dur-3) var(--ease-glass) backwards; }
@keyframes mon-puja { from { opacity: 0; transform: translateY(12px); } }
@media (prefers-reduced-motion: reduce) {
  .mon-portada.is-entrant { animation: none !important; }
}
/* The atmospheric layer — Collserola from the valley, in ink, behind and to
   the right of the words. Flat by elevation honesty (it is not actionable),
   inert to the pointer, and invisible to assistive tech. It is drawn in tokens
   so the theme flip is free; nothing here animates, so nothing owes a
   reduced-motion twin. */
.mon-contorn { position: absolute; inset: 0 0 0 auto; width: min(58%, 540px);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 42%);
  mask-image: linear-gradient(to right, transparent, #000 42%); }
.mon-contorn svg { display: block; width: 100%; height: 100%; }
/* On a phone the copy takes the whole column, so a full-height contour runs its
   ridges straight through the epigraph's tail — legible (the words paint over
   it) but busy. Anchored to the top it sits beside the overline and the title
   instead of across the sentence under them. The prac-scatter's rule, same
   shape: the ornament gives way to the words, it never thins them. */
@media (max-width: 560px) {
  .mon-contorn { bottom: auto; height: 58%; width: min(54%, 240px); }
}
.mon-contorn-cresta path { stroke: var(--accent); }
.mon-contorn-cresta .is-lluny { stroke-width: 1; opacity: .16; }
.mon-contorn-cresta .is-mig { stroke-width: 1.2; opacity: .24; }
.mon-contorn-cresta .is-prop { stroke-width: 1.4; opacity: .32; }
.mon-contorn-pins path { stroke: var(--tile-green-viu); stroke-width: 1.2; opacity: .34; }
/* La colla on Món is one doorway into the service, never a second inbox. The
   real conversation list owns #placa; this card says what is waiting and lets
   the roster's own faces carry the invitation. */
.mon .mon-colla { margin: var(--space-5) 0 0; }
.mon-colla-head { display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-3); padding: 0 2px var(--space-3); }
.mon-colla-head h2 { margin: 0; color: var(--text);
  font: 700 var(--fs-7)/1.1 var(--display); }
.mon-colla-porta { display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
  gap: var(--space-4); min-height: 112px; padding: var(--space-5);
  border: 1px solid var(--edge-soft); border-radius: var(--radius-l);
  background:
    radial-gradient(100% 140% at 0 0,
      color-mix(in srgb, var(--accent-soft) 54%, transparent), transparent 60%),
    var(--surface);
  box-shadow: var(--shadow-2); color: var(--text); text-decoration: none;
  transition: transform var(--dur-1) var(--ease-glass),
              box-shadow var(--dur-1) var(--ease-glass); }
.mon-colla-porta:hover { transform: translateY(-2px); box-shadow: var(--shadow-3); }
.mon-colla-cares { display: flex; align-items: center; min-width: 0; }
.mon-colla-cares > :not(:first-child) { margin-left: calc(-1 * var(--space-3)); }
.mon-colla-cares > * { box-shadow: 0 0 0 3px var(--surface); }
.mon-colla-copia { display: flex; min-width: 0; flex-direction: column;
  gap: var(--space-1); }
.mon-colla-compte { color: var(--accent-ink); font-size: var(--fs-1);
  font-weight: 700; letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.mon-colla-nom { overflow: hidden; font: 700 var(--fs-6)/1.15 var(--display);
  text-overflow: ellipsis; white-space: nowrap; }
.mon-colla-linia { display: flex; min-width: 0; align-items: center;
  gap: var(--space-1); overflow: hidden; color: var(--text-secondary);
  font-size: var(--fs-3); }
.mon-colla-linia .colla-dit { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mon-colla-cta { color: var(--accent-ink); font-size: var(--fs-2); font-weight: 700;
  white-space: nowrap; }
@media (max-width: 480px) {
  .mon-colla-porta { grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-3); padding: var(--space-4); }
  .mon-colla-cares > :nth-child(3) { display: none; }
  .mon-colla-cta { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .mon-colla-porta { transition: none; }
  .mon-colla-porta:hover { transform: none; }
}

/* THE TWO ACTS NO LONGER WEAR ONE LABEL TREATMENT — superseded by la colla v3
   (design/handoffs/colla-v3, 7 Aug 2026), and deliberately, not by accident.
   The 4 Aug ruling («typed the same way; prominence comes from the SCALE OF THE
   CONTENT, never from a bigger heading») was written when la colla was one of
   five peer features and the page's problem was that all five looked alike. It
   is not one of five any more: it is a messaging service with its own state to
   report — how many conversations are waiting — and a count pill needs a title
   at its own scale to sit beside. So «La colla» is now a display heading with
   its own hairline and «Aquesta setmana» keeps the quiet overline, which is the
   hierarchy the page actually has. THE H-TREE IS UNTOUCHED: still h1 → one h2
   per destination, and «aquesta setmana» is still flat prose. */
.mon-setmana { margin-top: var(--space-6); }
.mon-overline { margin: 0 0 var(--space-4); padding: 0 0 var(--space-2);
  border-bottom: 1px solid var(--edge-soft); color: var(--muted-firm);
  font-family: inherit; font-size: var(--fs-1); font-weight: 700;
  line-height: 1.4; letter-spacing: var(--tracking-caps); text-transform: uppercase; }
/* One module = one section. The rubric names it quietly; the content is what
   should be loud. */
.mon-modul { display: flex; flex-direction: column; min-width: 0; }
.mon-rubrica { margin: 0 0 var(--space-2); font-family: inherit;
  font-size: var(--fs-1); font-weight: 700; letter-spacing: var(--tracking-caps);
  text-transform: uppercase; color: var(--muted); }
.mon-modul > a { flex: 1; }

/* La coberta — the almanac's in-season entry as the lead cultural feature.
   ONE link, art at cap scale, and no secondary «obre l'almanac» row: the index
   rides one hop behind the entry's own sortida (§0 C2). */
.mon-coberta-porta { display: block; overflow: hidden; color: var(--text);
  text-decoration: none; border: 1px solid var(--edge-soft);
  border-radius: var(--radius-l); background: var(--surface);
  box-shadow: var(--shadow-2);
  transition: transform var(--dur-1) var(--ease-glass),
              box-shadow var(--dur-1) var(--ease-glass); }
/* The art is the cover's largest object, and it has to LOOK authored: the
   entry-page treatment at entry-page scale, not an icon floating in a band.
   The panel is an almanac PLATE now — the entry's own hue washed deeper than
   the tile default, a soft top light, and a hairline frame inset like a
   printed page — with the calendar-leaf segell pinned in its corner (the
   shared .almanac-segell, styled with the almanac's art). The frame is an
   ::after so it rides over the art without touching the tile component. */
.mon-coberta-art { display: block; position: relative; }
.mon-coberta-art::after { content: ""; position: absolute; inset: 10px;
  pointer-events: none; border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--text) 16%, transparent); }
.mon-coberta-art .almanac-imatge.is-cap { height: 160px; border: 0;
  border-radius: 0; border-bottom: 1px solid var(--edge-soft);
  background:
    radial-gradient(120% 90% at 50% 0%,
      color-mix(in srgb, var(--surface) 55%, transparent), transparent 62%),
    color-mix(in srgb, var(--almanac-glassa) 17%, var(--surface)); }
.mon-coberta-art .almanac-imatge.is-cap svg { width: 128px; height: 128px; }
.mon-coberta-copia { display: block; padding: var(--space-4) var(--space-5) var(--space-5); }
.mon-coberta-rubric { display: block; color: var(--accent-ink);
  font-size: var(--fs-1); font-weight: 700; letter-spacing: var(--tracking-caps);
  text-transform: uppercase; }
.mon-coberta-titol { display: block; margin-top: var(--space-2);
  font-family: var(--display); font-size: var(--fs-7); font-weight: 600;
  line-height: 1.12; letter-spacing: -.01em; }
.mon-coberta-resum { display: block; margin-top: var(--space-2);
  max-width: var(--measure); color: var(--text-secondary);
  font-size: var(--fs-3); line-height: 1.5; }

/* El quadern — the supporting spread. Its column arithmetic is a class views/
   mon.js assigns from the count of modules that actually rendered; no :has(),
   no sibling counting. Below 900 it is one column whatever the class says. */
.mon-quadern { display: grid; gap: var(--space-4); margin-top: var(--space-5); }

/* Reviewed external companions — static metadata, visibly separate from the
   weekly editorial spread and from Mica's own learning evidence. */
.mon-resources-zone { margin-top: var(--space-6); padding-top: var(--space-5);
  border-top: 1px solid var(--edge-soft); }
.resources-body, .unit-resources { display: grid; gap: var(--space-4); }
.resource-intro { max-width: var(--measure); margin: 0; color: var(--text-secondary);
  line-height: 1.55; }
.resource-controls { display: flex; align-items: end; justify-content: space-between;
  flex-wrap: wrap; gap: var(--space-3); }
.resource-controls label { display: grid; gap: var(--space-1); color: var(--muted);
  font-size: var(--fs-2); }
.resource-controls select { min-width: 10rem; }
.resource-count { margin: 0; color: var(--muted); font-size: var(--fs-2); }
.resource-shelf { display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(270px, 100%), 1fr)); }
.resource-card { display: flex; min-width: 0; flex-direction: column; gap: var(--space-2);
  padding: var(--space-5); border: 1px solid var(--edge-soft);
  border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-2); }
.resource-card[hidden] { display: none; }
.resource-card h3, .resource-card p { margin: 0; }
.resource-provider { color: var(--accent-ink); font-size: var(--fs-1);
  font-weight: 700; letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.resource-card h3 { font: 700 var(--fs-5)/1.2 var(--display); }
.resource-description { color: var(--text-secondary); line-height: 1.5; }
.resource-meta, .resource-access, .resource-attribution { color: var(--muted);
  font-size: var(--fs-2); line-height: 1.45; }
.resource-link { align-self: start; margin-top: auto; }
.unit-resources { margin-top: var(--space-5); padding-top: var(--space-5);
  border-top: 1px solid var(--edge-soft); }
.unit-resources h2, .unit-resources .overline { margin: 0; }
.resource-card.is-compact { box-shadow: none; background: var(--surface-sunk); }

/* Shared card mechanics for the three spread modules. */
.mon-quiosc-carta, .mon-radio-carta, .mon-rebost-carta {
  display: flex; height: 100%; min-width: 0; color: var(--text);
  text-decoration: none; border: 1px solid var(--edge-soft);
  border-radius: var(--radius); box-shadow: var(--shadow-2);
  transition: transform var(--dur-1) var(--ease-glass),
              box-shadow var(--dur-1) var(--ease-glass); }

/* El quiosc — today's newspaper. The paper field and the display headline ARE
   the identity; the freshness line is chrome and stays quiet, never coloured
   as failure. The headline swaps in when the cached edition arrives. */
/* Optically centred, not pinned top-and-bottom: the spanning column is as
   tall as the radio and the rebost stacked, and a headline nailed to the top
   edge of that leaves a hole underneath it. Centred, the white space reads as
   a front page's own margin. */
.mon-quiosc-carta { flex-direction: column; justify-content: center;
  gap: var(--space-4); padding: var(--space-5); background: var(--paper);
  text-align: center; }
/* The dateline masthead. The module's h2 above the card is the nameplate, so
   inside the card the edition's date sits under a printer's double rule — the
   one place a front page keeps it. Chrome ink, never a verdict colour. */
.mon-quiosc-mast { display: block; padding-bottom: var(--space-2);
  border-bottom: 3px double color-mix(in srgb, var(--text) 42%, transparent);
  color: var(--muted-firm); font-size: var(--fs-1); font-weight: 700;
  letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.mon-quiosc-camp { display: block; min-width: 0; }
.mon-quiosc-rubrica { display: block; min-height: 1.2em; color: var(--tile-terra-ink);
  font-size: var(--fs-1); font-weight: 700; letter-spacing: var(--tracking-caps);
  text-transform: uppercase; }
.mon-quiosc-titular { display: block; margin-top: var(--space-1);
  overflow: hidden; overflow-wrap: anywhere;
  font-family: var(--display); font-size: var(--fs-6); font-weight: 700;
  line-height: 1.18; letter-spacing: -.01em; }
/* Real headlines run long: clamp to four lines so the spread cannot break. The
   plain overflow above is the fallback where -webkit-box is unavailable. */
@supports (-webkit-line-clamp: 4) {
  .mon-quiosc-titular { display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 4; }
}
/* A real headline is the loudest thing in the spread — that is what makes the
   quiosc read as a newspaper and not as a status row. */
.mon-quiosc-carta.is-titular .mon-quiosc-titular { font-size: var(--fs-7);
  margin-top: var(--space-2); }
/* Faux body columns — three columns of suggested newsprint under the real
   headline, fading out like a fold. Set dressing only: aria-hidden in the DOM,
   hidden here until the REAL headline lands (fake text under a chrome
   fallback would dress an empty shelf as a full one). The "lines" are a
   repeating gradient in low-alpha text ink, so both themes get grey print for
   free and there is not a word in them to translate. */
.mon-quiosc-columnes { display: none; }
.mon-quiosc-carta.is-titular .mon-quiosc-columnes { display: grid;
  grid-template-columns: repeat(3, 1fr); gap: var(--space-4); height: 54px;
  -webkit-mask-image: linear-gradient(#000 25%, transparent 96%);
  mask-image: linear-gradient(#000 25%, transparent 96%); }
.mon-quiosc-columnes span { background: repeating-linear-gradient(to bottom,
  color-mix(in srgb, var(--text) 24%, transparent) 0 2px, transparent 2px 8px); }
.mon-quiosc-meta { display: flex; align-items: center; justify-content: center;
  gap: var(--space-2); color: var(--muted-firm); font-size: var(--fs-2); }
.mon-quiosc-ico { display: inline-flex; color: var(--muted); }
.mon-quiosc-ico svg { display: block; }

/* La ràdio — the neighbourhood's voice, on the deeper enamel ground. */
.mon-radio-carta { gap: var(--space-3); padding: var(--space-4) var(--space-5);
  background: var(--mat); }
.mon-radio-ico { flex: none; display: inline-flex; margin-top: 2px;
  color: var(--tile-amber-ink); }
.mon-radio-ico svg { display: block; }
.mon-radio-cos { display: block; min-width: 0; flex: 1; }
.mon-radio-data { display: block; color: var(--muted-firm); font-size: var(--fs-1);
  font-weight: 700; letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.mon-radio-titol { display: block; margin-top: var(--space-1); overflow-wrap: anywhere;
  font-family: var(--display); font-size: var(--fs-6); font-weight: 700; line-height: 1.2; }
.mon-radio-guio { display: block; margin-top: var(--space-1); color: var(--muted);
  font-size: var(--fs-2); line-height: 1.4; }
.mon-radio-guio:empty { display: none; }
.mon-radio-prosa { display: block; margin-top: var(--space-2);
  color: var(--text-secondary); font-size: var(--fs-3); line-height: 1.45; }
.mon-radio-cta { flex: none; align-self: flex-end; color: var(--accent-ink);
  font-size: var(--fs-3); font-weight: 600; }

/* El rebost — the quietest of the three (§0 C6): the dish art gets room, the
   count is supporting copy, and there is no bar and no ceremony here. */
/* The dish art gets room to breathe (§5.6) — it is the module's identity, and
   the count beneath the name is supporting copy, never the principal mark. */
.mon-rebost-carta { gap: var(--space-4); align-items: center;
  padding: var(--space-5); background: var(--surface); }
.mon-rebost-plat { flex: none; display: grid; place-items: center;
  width: 76px; height: 76px; }
.mon-rebost-plat .reb-plat { width: 100%; }
.mon-rebost-copia { display: block; min-width: 0; }
.mon-rebost-dish { display: block; overflow-wrap: anywhere;
  font-family: var(--display); font-size: var(--fs-5); font-weight: 700; line-height: 1.2; }
.mon-rebost-meta { display: flex; flex-wrap: wrap; gap: 0 var(--space-2);
  margin-top: var(--space-1); color: var(--muted-firm); font-size: var(--fs-2); }
.mon-rebost-temporada { color: var(--tile-green-viu-ink); font-weight: 600; }

@media (hover: hover) and (pointer: fine) {
  .mon-coberta-porta:hover, .mon-quiosc-carta:hover,
  .mon-radio-carta:hover, .mon-rebost-carta:hover {
    transform: translateY(-2px); box-shadow: var(--shadow-3); }
}
@media (prefers-reduced-motion: reduce) {
  .mon-coberta-porta, .mon-quiosc-carta, .mon-radio-carta, .mon-rebost-carta {
    transition: none; }
  .mon-coberta-porta:hover, .mon-quiosc-carta:hover,
  .mon-radio-carta:hover, .mon-rebost-carta:hover { transform: none; }
}

@media (min-width: 900px) {
  /* The portada title's own clamp spans both tiers since the big-title pass —
     the step-up that used to live here would cap it at 32px. */
  /* At browse width the cover becomes an editorial spread: the illustration
     holds a panel of its own beside the copy, instead of a wide shallow band
     with a small emblem adrift in it. */
  .mon-coberta-porta { display: grid; grid-template-columns: 360px minmax(0, 1fr);
    align-items: stretch; }
  .mon-coberta-art .almanac-imatge.is-cap { height: 100%; min-height: 260px;
    border-bottom: 0; border-right: 1px solid var(--edge-soft); }
  .mon-coberta-art .almanac-imatge.is-cap svg { width: 230px; height: 230px; }
  .mon-coberta-copia { align-self: center;
    padding: var(--space-5) var(--space-6) var(--space-5) var(--space-5); }
  .mon-coberta-titol { font-size: var(--fs-8); }
  /* The spread proper. `is-tres` is deliberately asymmetric — the quiosc's
     newsprint column runs the full height beside the radio and the rebost —
     and the first child IS the quiosc whenever three modules render (mon.js
     builds them in that order), so the placement can never point at the wrong
     card. minmax(0, …) keeps a long headline from widening its track. */
  .mon-quiosc-carta.is-titular .mon-quiosc-titular { font-size: var(--fs-8); }
  .mon-quiosc-carta { padding: var(--space-6) var(--space-5); }
  /* A lone module does not stretch the whole canvas: it takes a column the
     width of a reading measure, left-aligned with everything above it, so a
     sparse world reads as composed rather than as a gap. */
  .mon-quadern.is-un { grid-template-columns: minmax(0, 620px); }
  .mon-quadern.is-dos { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .mon-quadern.is-tres { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
  .mon-quadern.is-tres > :first-child { grid-row: 1 / span 2; }
}

/* ---- Pràctica v2 · the hub as a composition (5 Aug 2026,
       design/handoffs/practica-v2/) ----
   The flat 2×2 grid of small tiles became a staged composition: a vidre hero
   carrying the title and the recommendation as its plaque, two group columns of
   glazed ceramic doors keyed to one trencadís hue each, and the closing motto.
   Everything the hub SAYS is the code it was (views/practica.js) — this block is
   presentation, and law 2 is what draws the line: a door is raised and lifts, a
   locked joc is a dashed <div> that never does.

   FOUR PLACES THE HANDOFF WAS NOT FOLLOWED, each a rule that outranks it:
   (1) The page widens by naming an ARCHETYPE — `lay-canvas`, not the handoff's
       raw 1080px. docs/LAYOUT.md outranks a handoff value (Progrés v2's, la
       unitat v2's and el vocabulari v2's ruling, all the same week).
   (2) It redraws the app's global header row (lockup left, «Pràctica» right).
       That chrome already exists and is shared by every view — the handoff's
       own note says merge rather than duplicate, so the hero starts at the h1.
   (3) The page wash is a RADIAL fading to transparent, not the handoff's
       `linear-gradient(…, var(--bg) 380px)`: an opaque wash bounded by the
       composition draws two vertical seams down any window wider than the
       column — measured on the porta, plainly visible at 1280.
   (4) The plaque's grid is `minmax(0, 1fr) auto auto`, not the handoff's
       `auto minmax(0, 1fr) auto`. The flexible column has to be the SENTENCE:
       with the copy at `auto` a long recommendation pushes the gauge and the
       arrow off the 640px plaque instead of wrapping.
   And the pulse reuses `cami-batec` rather than declaring a second keyframe
   with the same two stops — the camí's «you are here» mark and this «ready for
   you» chip are the same beat. */

/* THE PAGE. The wash rides the composition (see 3 above). */
.practica-hub { position: relative; display: flex; flex-direction: column;
  gap: clamp(20px, 2.6vw, 30px); padding: clamp(4px, 1.4vw, 14px) 0 40px;
  background: radial-gradient(64% 420px at 50% -30px,
    color-mix(in srgb, var(--accent) 7%, transparent),
    color-mix(in srgb, var(--accent) 0.4%, transparent) 72%); }
/* The zones rise in order behind `.is-entrant`, filling BACKWARDS; renderPractica
   drops the class on a settle timer whether or not a frame was ever served (the
   mosaic's rule — this is the eighth surface to pay for it). Only an arrival
   carries the class: climbing back out of la lectura is a return, not a
   landing. The cards keep their own sweep inside the groups zone (the S4
   `entrada` rule further down), which is translate-only and so cannot stack a
   second opacity ramp on top of this one. */
.practica-hub.is-entrant > * { animation: prac-puja var(--dur-3) var(--ease-glass) backwards; }
.practica-hub.is-entrant > .prac-grups { animation-delay: 0.08s; }
.practica-hub.is-entrant > .prac-lema { animation-delay: 0.16s; }
@keyframes prac-puja { from { opacity: 0; transform: translateY(14px); } }
@media (prefers-reduced-motion: reduce) {
  .practica-hub.is-entrant > * { animation: none !important; }
}

/* ---- the hue vocabulary ----
   One hue per door, so the eye learns the room. Two vars, the parallel-ink
   idiom (--family-ink, --voc-hue): the glaze paints, the ink writes (law 1) —
   nothing here ever sets small text in a raw tile hue. */
.prac-hue--blue  { --prac-hue: var(--tile-blue);  --prac-ink: var(--tile-blue-ink); }
.prac-hue--rose  { --prac-hue: var(--tile-rose);  --prac-ink: var(--tile-rose-ink); }
.prac-hue--green { --prac-hue: var(--tile-green); --prac-ink: var(--tile-green-ink); }
.prac-hue--amber { --prac-hue: var(--tile-amber); --prac-ink: var(--tile-amber-ink); }
.prac-hue--terra { --prac-hue: var(--tile-terra); --prac-ink: var(--tile-terra-ink); }

/* ---- Zone 1 · the hero ----
   The view's ONE vidre (elevation 3), and it really does sit over something:
   the page's own accent wash. Radius --radius-l like every other v2 hero
   rather than the handoff's 26px — the rooms open the same way. */
.prac-hero { position: relative; overflow: hidden; isolation: isolate;
  border-radius: var(--radius-l); padding: clamp(24px, 4vw, 46px);
  border: 1px solid color-mix(in srgb, var(--rim) 60%, var(--edge-soft));
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .prac-hero { background: var(--surface); }
}
/* Soft-light for the reason measured on the Progrés, porta and vocab heroes:
   straight alpha over a surface this size lifts the ground enough to push
   --muted ink under AA. */
.prac-hero .hero-lluu { position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; background: var(--sheen); mix-blend-mode: soft-light; }
/* Trencadís scatter: ornament, never information — aria-hidden, inert, and it
   animates nothing. Declared locally rather than shared with the vocab hero's,
   the way `.hero-lluu` is: a shared NAME, a per-hero declaration. */
.prac-scatter { position: absolute; top: -26px; right: -14px;
  width: 240px; height: 190px; pointer-events: none; opacity: 0.5; }
.prac-scatter i { position: absolute; display: block; }
.prac-scatter i:nth-child(1) { top: 44px; right: 36px; width: 30px; height: 30px;
  border-radius: 9px; background: var(--tile-blue); opacity: 0.32; transform: rotate(-6deg); }
.prac-scatter i:nth-child(2) { top: 88px; right: 74px; width: 22px; height: 22px;
  border-radius: 7px; background: var(--tile-amber); opacity: 0.4; transform: rotate(4deg); }
.prac-scatter i:nth-child(3) { top: 36px; right: 96px; width: 17px; height: 17px;
  border-radius: 6px; background: var(--tile-rose); opacity: 0.34; transform: rotate(-3deg); }
.prac-scatter i:nth-child(4) { top: 104px; right: 26px; width: 19px; height: 19px;
  border-radius: 6px; background: var(--tile-green); opacity: 0.3; transform: rotate(7deg); }
.prac-scatter i:nth-child(5) { top: 70px; right: 126px; width: 12px; height: 12px;
  border-radius: 4px; background: var(--accent); opacity: 0.26; transform: rotate(-8deg); }
/* On a phone the hero is ~330px wide and the cluster is 240 of it, so at full
   size it sits under the title and the hint rather than beside them. Scaled
   from its own corner it stays the same ornament, just out of the sentence's
   way. */
@media (max-width: 560px) {
  .prac-scatter { transform: scale(0.72); transform-origin: top right; opacity: 0.4; }
}

.prac-hero-cos { position: relative; display: flex; flex-direction: column; gap: 20px; }
.prac-hero-copia { display: flex; flex-direction: column; gap: 8px; }
/* Two classes so it beats `.dashboard h1`'s 1.5em pin without relying on
   source order. */
.practica-hub .prac-titol { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(38px, 5vw, 54px); line-height: 1.03; letter-spacing: 0.005em; }
.prac-hint { margin: 0; font-size: 15.5px; color: var(--muted);
  max-width: 52ch; text-wrap: pretty; }

/* «Per començar» — the recommendation, promoted into the hero's glass. Still
   exactly ONE move and still subordinate to the doors below: raised, but no
   accent fill, so it never reads as a second primary. It is deliberately not
   `.practica-recomanat` — that class is the porta's quiet row, and this is the
   plaque shape Vocabulari's hero established. */
.prac-plaqueta { display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: 16px; max-width: 640px; padding: 16px 20px;
  border-radius: 16px; background: var(--glass-1);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--edge-soft));
  box-shadow: inset 0 1px 0 var(--edge),
    0 4px 16px -10px color-mix(in srgb, var(--accent) 40%, transparent);
  color: inherit; text-decoration: none; -webkit-tap-highlight-color: transparent;
  transition: border-color 0.16s ease, transform 0.16s ease; }
.prac-plaqueta:hover { border-color: var(--accent); transform: translateY(-1px); }
.prac-plaqueta-copia { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.prac-plaqueta-eyebrow { font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; font-weight: 600; color: var(--accent-ink); }
.prac-plaqueta-frase { font-family: var(--display); font-weight: 700;
  font-size: 21px; line-height: 1.15; text-wrap: pretty; }
.prac-plaqueta-dades { display: flex; flex-direction: column;
  align-items: flex-end; gap: 5px; }
.prac-plaqueta-xifres { font-size: 12px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.prac-plaqueta .prac-fletxa { color: var(--accent); font-size: 18px; }
@media (prefers-reduced-motion: reduce) {
  .prac-plaqueta { transition: none; }
  .prac-plaqueta:hover { transform: none; }
}
/* The plaque's copy is the widest thing in the hero on a phone; the gauge and
   its caption drop below it rather than squeezing the sentence (the vocab
   plaque's rule — same shape, same fix). */
@media (max-width: 560px) {
  .prac-plaqueta { grid-template-columns: minmax(0, 1fr) auto; row-gap: 12px; }
  /* The arrow is pinned beside the copy, not auto-placed: a full-width gauge
     row would otherwise push it onto a third row, where it reads as a stray
     mark rather than «this opens something». */
  .prac-plaqueta > .prac-fletxa { grid-row: 1; grid-column: 2; align-self: center; }
  .prac-plaqueta-dades { grid-row: 2; grid-column: 1 / -1; align-items: flex-start; }
}

/* ---- Zone 2 · the groups row ----
   Two columns at the desktop tier, stacked below it — and the stack point is
   the COLUMNS' own flex-basis, not a breakpoint: dirigida asks for 380px and
   jocs for 320px, so they fall into one column wherever the canvas can no
   longer hold both. `align-items: flex-start` is what makes two lists of
   different lengths read as two lists rather than a broken table. */
.prac-grups { display: flex; flex-wrap: wrap; gap: clamp(20px, 3vw, 32px);
  align-items: flex-start; }
.prac-grup { display: flex; flex-direction: column; gap: 14px; }
.prac-grup:nth-child(1) { flex: 1.45 1 380px; min-width: min(380px, 100%); }
.prac-grup:nth-child(2) { flex: 1 1 320px; min-width: min(320px, 100%); }
.prac-grup-head { display: flex; align-items: center; gap: 14px; }
/* Two classes, again to beat `.dashboard h2`'s 1.5em display pin. */
.practica-hub .prac-grup-titol { margin: 0; font-family: var(--display);
  font-style: italic; font-weight: 600; font-size: 22px; }
.prac-grup-head > .grout-rule { flex: 1; min-width: 40px; }
.prac-cartes { display: flex; flex-direction: column; gap: 14px; }

/* ---- the doors ----
   Alçat: these ARE the actionable things on the surface. The glaze is declared
   here rather than inherited from the shared raised-surface list further down,
   because two of them wash their own ground (the amber repte) or refuse the
   glaze entirely (the locked one). */
.practica-card { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: 18px; padding: 20px 22px; border-radius: 18px;
  color: inherit; text-decoration: none; background: var(--surface);
  border: 1px solid var(--edge-soft);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze);
  -webkit-tap-highlight-color: transparent;
  transition: border-color var(--dur-1) var(--ease-glass),
              transform var(--dur-1) var(--ease-glass); }
/* A card that opens an IN-PLACE descens has no hash to link to, so it is a
   <button> (la lectura). Same glaze, same shape — it just has to be told it is
   a card rather than a form control. */
button.practica-card { font: inherit; text-align: left; width: 100%;
  cursor: pointer; }
/* El delit S1 · the lift. Gated on a fine pointer: a finger IS the highlight,
   and a lift left behind on a tapped card reads as stuck. The selectors name
   the ELEMENTS that are doors — a locked card is a <div> (views/practica.js),
   so it stays flat without a rule of its own. */
@media (hover: hover) and (pointer: fine) {
  a.practica-card:hover, button.practica-card:hover {
    border-color: color-mix(in srgb, var(--prac-hue) 46%, var(--edge-soft));
    transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  .practica-card { transition: none; }
  a.practica-card:hover, button.practica-card:hover { transform: none; }
}
.prac-chip { display: grid; place-items: center; width: 56px; height: 56px;
  border-radius: 16px; color: var(--prac-ink);
  background: color-mix(in srgb, var(--prac-hue) 13%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--prac-hue) 25%, transparent);
  transform: rotate(var(--gir, -2deg)); }
.prac-carta-cos { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.prac-carta-titol { font-family: var(--display); font-weight: 700;
  font-size: 23px; line-height: 1.1; }
/* One line: registre's «avui: …» is the only state that can run long, and it
   ellipsizes rather than reflowing the whole column. */
.prac-carta-estat { font-size: 13.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prac-fletxa { color: var(--prac-ink, var(--accent)); font-size: 17px; }

/* El repte, open. The one card that warms its own ground, and its chip carries
   the pulse — one per view, and only on the thing inviting play. */
.practica-card.is-repte { background: color-mix(in srgb, var(--tile-amber) 7%, var(--surface));
  border-color: color-mix(in srgb, var(--tile-amber) 30%, var(--edge-soft)); }
.practica-card.is-repte .prac-chip {
  background: color-mix(in srgb, var(--tile-amber) 17%, var(--surface));
  border-color: color-mix(in srgb, var(--tile-amber) 34%, transparent);
  animation: cami-batec 2.6s ease-in-out infinite; }
@media (hover: hover) and (pointer: fine) {
  a.practica-card.is-repte:hover {
    border-color: color-mix(in srgb, var(--tile-amber) 62%, var(--edge-soft)); }
}
@media (prefers-reduced-motion: reduce) {
  .practica-card.is-repte .prac-chip { animation: none; }
}
/* AA repair, measured on this card and nowhere else: the 7 % amber wash warms
   the ground just enough to take `--muted` to 4.24:1 in dark (4.84 on the plain
   `--surface` the other cards wear). The two quiet lines standing ON that wash
   take `--muted-firm` instead — 5.34 dark, 6.9 light. Do not thin the wash to
   fix it: the warm ground IS how «open, and waiting for you» is said. */
.practica-card.is-repte .prac-carta-estat,
.practica-card.is-repte .prac-record-cap { color: var(--muted-firm); }
/* The record replaces the arrow — the figure IS the affordance's neighbour, and
   the state line no longer says the number a second time. */
.prac-record { display: flex; flex-direction: column; align-items: flex-end; }
.prac-record-cap { font-size: 10px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--muted); }
.prac-record-xifra { font-family: var(--font-mono); font-weight: 600;
  font-size: 24px; line-height: 1.1; color: var(--tile-amber-ink);
  font-variant-numeric: tabular-nums; }

/* El repte, locked. Law 2: not a door, so no glaze, no hover, no lift — dashed,
   sunk chip, muted title. It still states its condition, and since v2 it also
   draws how far off it is: a padlock that names nothing is a dead end. */
.practica-card.is-tancat { grid-template-columns: auto minmax(0, 1fr);
  box-shadow: none;
  border: 1px dashed color-mix(in srgb, var(--muted) 36%, transparent); }
.practica-card.is-tancat .prac-chip { background: var(--surface-sunk);
  border-color: var(--edge-soft); color: var(--muted); }
.practica-card.is-tancat .prac-carta-cos { gap: 6px; }
.practica-card.is-tancat .prac-carta-titol { color: var(--muted-firm); }
/* The locked state is a whole sentence, not a teaser — it wraps rather than
   ellipsizing, which on a 375px phone is the difference between a condition and
   a hint of one. */
.practica-card.is-tancat .prac-carta-estat { white-space: normal; }
/* A live-model door that is shut says why in a whole sentence, so it wraps for
   the same reason the locked state does: on a 375px phone an ellipsis is the
   difference between a condition and a hint of one. */
.practica-card.is-tancat .prac-carta-nota { white-space: normal;
  color: var(--muted); font-size: 12px; }
.prac-obertura { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.prac-obertura-xifres { font-size: 12px; color: var(--muted);
  font-variant-numeric: tabular-nums; }

/* ---- ceramic tile rows ----
   Two of them, both COMPOSITIONS rather than gauges in the aria sense: they are
   aria-hidden and the caption beside each states the count in words, so the
   reading never depends on counting ceramic. Tilts are seeded in the view
   through the mosaic engine's own hash + RNG. */
.prac-gauge, .prac-obertura-tessella { display: inline-flex; }
.prac-gauge { gap: 4px; }
.prac-obertura-tessella { gap: 3px; }
.prac-tessella { display: block; background: var(--bar-track);
  transform: rotate(var(--gir, 0deg)); }
.prac-gauge .prac-tessella { width: 10px; height: 10px; border-radius: 3px; }
.prac-gauge .prac-tessella.is-laid { background: var(--accent); }
.prac-obertura-tessella .prac-tessella { width: 8px; height: 8px; border-radius: 2.5px; }
.prac-obertura-tessella .prac-tessella.is-laid { background: var(--accent); opacity: 0.75; }

/* ---- La lectura · textos amb glosses (La Paleta S6, 30 Jul 2026) ----
   Two surfaces, and the split is the `raised = actionable` rule doing its job:
   an INDEX row opens a reading, so it wears the glaze every other door wears;
   the reading itself is flat paper, because prose is read, not acted on. The
   only controls inside a text are the
   words. */
.lect-llista { display: grid; gap: 10px; margin-top: 14px;
  /* Reflows on ROW width, not the viewport — la xuleta's and el barri's lesson:
     a title plus its English side is one line and a hard 2-up truncates it
     across the whole 900–1100 band. */
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.lect-fila { display: flex; align-items: center; gap: 12px; width: 100%;
  font: inherit; text-align: left; cursor: pointer; color: inherit;
  padding: 14px 18px; border-radius: var(--radius);
  background: var(--glass-2); border: 1px solid var(--edge-soft);
  box-shadow: var(--glaze); -webkit-tap-highlight-color: transparent; }
.lect-fila-ico { flex-shrink: 0; display: flex; color: var(--accent); }
.lect-fila-cos { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.lect-fila-cos strong { font-family: var(--display); font-size: 17px; }
.lect-fila-cos .hint { font-size: 13px; }
.lect-fila-meta { font-size: 12px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.lect-fila-fletxa { flex-shrink: 0; color: var(--accent); }

.lect-lead, .lect-tanca { max-width: var(--measure); }
.lect-tanca { margin-top: 24px; }
.lect-par { max-width: var(--measure); margin: 16px 0 0;
  font-size: 17px; line-height: 1.8; }
/* A GLOSSED WORD IS MARKED WITH NOTHING AT REST, and that is the finding this
   surface turned up rather than a shortcut. Ink was rejected first (a stained-
   glass window you scan instead of a text you read), then a dotted underline —
   which, because COVERAGE IS THE CONTRACT, landed under nearly every word and
   read as a page full of spell-check squiggles. The mark carries no information
   exactly BECAUSE the contract is total: «which words can I tap» is answered
   once, in the lead line, by «all of them». So the prose stays prose; the
   affordance is the pointer, the hover, and the sentence above it. If coverage
   ever stops being the contract, this is the rule to revisit. */
.lect-mot { font: inherit; color: inherit; background: none; border: 0;
  padding: 0 1px; cursor: pointer; border-radius: 3px;
  -webkit-tap-highlight-color: transparent; }
.lect-mot:hover { background: var(--accent-soft); }
.lect-mot[aria-expanded="true"] { background: var(--accent-soft);
  color: var(--accent-ink); }
/* The revealed meaning: inline, parenthesised in CSS so the JS never builds
   punctuation, and free to break across lines like the words around it. */
.lect-glossa { color: var(--accent-ink); font-size: 0.86em; }
.lect-glossa::before { content: " ("; }
.lect-glossa::after { content: ")"; }

/* «HO HAS ENTÈS?» (26 Sep 2026, docs/COMPRENSIO-PLAN.md). The block sits in the
   page flow under the text, never in a sheet, so the text above stays
   scrollable. Raised = actionable: the LIVE card is esmaltat (the index row's
   recipe); once answered it goes flat, and an answered card from an earlier
   visit is flat paper from the start. */
.lect-comprensio { max-width: var(--measure); margin-top: var(--space-7); }
.lect-comp-cap { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); margin-bottom: var(--space-3); }
.lect-comp-cap h2 { margin: 0; font-family: var(--display); }
.lect-comp-resoltes, .lect-comp-viu { display: grid; gap: var(--space-3); }
.lect-comp-resoltes:not(:empty) { margin-bottom: var(--space-3); }
.lect-comp-card { padding: var(--space-4) 18px; border-radius: var(--radius);
  border: 1px solid var(--edge-soft); }
.lect-comp-card.is-viva { background: var(--glass-2); box-shadow: var(--glaze); }
.lect-comp-card.is-resposta, .lect-comp-card.is-resolta { background: transparent;
  box-shadow: none; }
.lect-comp-enunciat { margin: 0 0 var(--space-3); font-size: 17px; line-height: 1.6;
  font-weight: 600; }
.lect-comp-card.is-resolta .lect-comp-enunciat { margin-bottom: var(--space-2); }
.lect-opcions { display: flex; flex-direction: column; gap: var(--space-2); }
.lect-opcions.is-cf { flex-direction: row; flex-wrap: wrap; }
/* Sized by their words, then grown to fill: «Cert · Fals · No ho diu» sits on
   one row down to a 320 px phone, and wraps rather than squeezes below it. */
.lect-opcions.is-cf .lect-opcio { flex: 1 1 auto; padding-inline: 12px; text-align: center; }
/* 16 px floor, 44 px targets: the tria option's own recipe, one surface over. */
.lect-opcio { min-height: 44px; text-align: left; padding: 10px 16px; font: inherit;
  font-size: 16px; color: var(--text); background: var(--surface);
  border: 1.5px solid var(--bar-track); border-radius: var(--radius-s); cursor: pointer;
  transition: border-color var(--dur-1) var(--ease-glass),
              background var(--dur-1) var(--ease-glass); }
@media (hover: hover) and (pointer: fine) {
  .lect-opcio:not(:disabled):hover { border-color: var(--accent); }
}
.lect-opcio:not(:disabled):active { transform: translateY(1px); }
.lect-opcio:disabled { cursor: default; }
.lect-opcio:disabled:not(.is-correcta):not(.is-errada) { opacity: var(--op-inert); }
.lect-opcio.is-correcta { border-color: var(--ok); background: var(--ok-soft); }
.lect-opcio.is-errada { border-color: var(--miss); background: var(--miss-soft); }
.lect-comp-retro { margin-top: var(--space-3); border-radius: var(--radius-s); }
.lect-comp-retro:focus { outline: none; }
.lect-comp-retro:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; }
.lect-comp-veredicte { margin: 0 0 var(--space-1); font-weight: 700; }
.lect-comp-veredicte.is-encert { color: var(--tile-green-ink); }
.lect-comp-veredicte.is-errada { color: var(--text); }
.lect-comp-resposta { margin: 0 0 var(--space-2); }
.lect-comp-prova-cap { margin: var(--space-2) 0 var(--space-1); font-size: 13px;
  color: var(--muted-firm); }
.lect-comp-prova blockquote { margin: 0; padding: var(--space-1) 0 var(--space-1) var(--space-3);
  border-left: 3px solid var(--accent-soft); font-family: var(--display);
  font-style: italic; line-height: 1.6; }
.lect-comp-seguent { margin-top: var(--space-4); min-height: 44px; }
/* The proof, marked in the text above: a quiet wash, never a flash, and no
   auto-scroll (on a phone, jumping away from the card is worse than the quote
   it already carries). The wash rides the token layer, so it theme-flips with
   no dark rule of its own. It fades in; the reduced-motion twin just appears. */
.lect-frase { border-radius: 3px; -webkit-box-decoration-break: clone;
  box-decoration-break: clone; transition: background-color var(--dur-3) var(--ease-glass); }
.lect-frase.is-prova { background-color: var(--accent-soft); }
@media (prefers-reduced-motion: reduce) {
  .lect-opcio, .lect-frase { transition: none; }
  .lect-opcio:not(:disabled):active { transform: none; }
}

/* El Quiosc (#quiosc) — la premsa adaptada (docs/QUIOSC-PLAN.md). The prose
   wears the .lect-par/.lect-mot family wholesale: one visual language for
   text whose words are one tap from their meaning. Tokens only — the theme
   flip is the token layer's job. */
.quiosc-cos { margin-top: 14px; }
/* The dateline is a masthead band: caps between a printer's double rule and a
   hairline, centred over the columns — the h1 above is the nameplate, this is
   the folio line under it. Same class in both states (today / older edition),
   so the two mounts of the line cannot drift apart. */
.quiosc-data { margin: 20px 0 0; max-width: var(--measure); padding: 7px 0 6px;
  border-top: 3px double color-mix(in srgb, var(--text) 42%, transparent);
  border-bottom: 1px solid var(--edge-soft); text-align: center;
  color: var(--muted-firm); font-size: var(--fs-2); font-weight: 700;
  letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.quiosc-imprimeix { margin: 16px 0 4px; display: flex; flex-direction: column;
  gap: 8px; align-items: flex-start; max-width: var(--measure); }
.quiosc-imprimeix .hint { margin: 0; }
.quiosc-article { margin-top: 40px; max-width: var(--measure); }
/* The kicker sits centred between flanking hairlines — the classic section
   slug, and it doubles as the rule between stories, so no separate divider. */
.quiosc-rubrica { display: flex; align-items: center; gap: 12px;
  margin: 0 0 8px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent-ink); }
.quiosc-rubrica::before, .quiosc-rubrica::after { content: "";
  flex: 1; height: 1px; background: var(--edge-soft); }
.quiosc-titular { margin: 0; font: 700 24px/1.2 var(--display);
  text-align: center; letter-spacing: -.01em; }
.quiosc-titular .lect-mot { font: inherit; }
/* The untouched paragraph is a press CLIPPING now — taped down over the page,
   the one physical object in the room. The tape is an ::before in translucent
   amber (both themes keep it translucent for free) and the box stays FLAT:
   raised is for things you act on, and a clipping is read. */
.quiosc-original { position: relative; margin-top: 30px; padding: 16px 16px 12px;
  background: var(--paper); border: 1px solid var(--edge-soft);
  border-radius: 4px; }
.quiosc-original::before { content: ""; position: absolute; top: -9px;
  left: 50%; width: 84px; height: 20px;
  transform: translateX(-50%) rotate(-2deg);
  background: color-mix(in srgb, var(--tile-amber) 30%, transparent);
  border: 1px solid color-mix(in srgb, var(--tile-amber) 42%, transparent);
  border-radius: 2px; }
.quiosc-original-rub { margin: 0 0 4px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted-firm); }
.quiosc-original .lect-par { margin-top: 6px; font-size: 15px; }
.quiosc-font { margin: 10px 0 0; font-size: 12.5px; color: var(--muted); }
.quiosc-font a { color: var(--accent-ink); }
/* Ràdio la Floresta (#radio) — el programa del barri (docs/RADIO-PLAN.md).
   Elevation honesty draws the whole surface: the player plat is the ONE raised
   thing (it is what you act on); the transcript is flat paper, read and never
   answered. No counters anywhere — an unheard programme is just a programme.
   The plat is the radio SET now: the FM dial (components/radio-art.js) runs
   across its head, the needle parked on the neighbourhood's spot. The dial's
   three signal bars are the surface's one motion — only under `.is-sonant`
   (the audio element really playing), with the stilled twin co-located below;
   at rest they stand at their drawn heights, so nothing depends on the
   animation having run. */
.radio-cos { margin-top: 14px; }
.radio-plat { margin-top: 16px; padding: 16px 20px 16px; max-width: var(--measure);
  background: var(--glass-2); border: 1px solid var(--edge-soft);
  border-radius: var(--radius-l); box-shadow: var(--glaze); }
.radio-dial { margin: 0 0 12px; padding-bottom: 10px;
  border-bottom: 1px solid var(--edge-soft); color: var(--muted); }
.radio-dial svg { display: block; width: 100%; height: auto; }
.radio-dial-linia { stroke: currentColor; stroke-width: 1.4; opacity: .55; }
.radio-dial-tics { stroke: currentColor; stroke-width: 1; opacity: .4; }
.radio-dial-nums text, .radio-dial-fm { fill: var(--muted); stroke: none;
  font-family: inherit; font-size: 9px; font-weight: 700;
  letter-spacing: .08em; }
.radio-dial-agulla path { stroke: var(--accent); stroke-width: 2;
  stroke-linecap: round; }
.radio-dial-agulla circle { fill: var(--accent); }
.radio-dial-emissora { fill: var(--accent-ink); stroke: none;
  font: italic 600 10px var(--display); }
.radio-dial-eq rect { fill: var(--accent);
  transform-box: fill-box; transform-origin: center bottom; }
.radio-plat.is-sonant .radio-dial-eq rect {
  animation: radio-eq .9s var(--ease-glass) infinite alternate; }
.radio-plat.is-sonant .radio-dial-eq rect:nth-child(2) {
  animation-duration: .7s; animation-delay: -.25s; }
.radio-plat.is-sonant .radio-dial-eq rect:nth-child(3) {
  animation-duration: 1.1s; animation-delay: -.6s; }
@keyframes radio-eq {
  from { transform: scaleY(.3); }
  to { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .radio-plat.is-sonant .radio-dial-eq rect { animation: none; }
}
.radio-data { margin: 0; font-family: var(--display); font-size: 15px;
  color: var(--text-secondary); }
.radio-titol { margin: 2px 0 0; font: 700 22px/1.25 var(--display); }
.radio-guio { margin: 6px 0 0; font-size: 13px; color: var(--muted); }
.radio-so { display: block; width: 100%; margin-top: 14px; }
.radio-baixa-linia { margin: 12px 0 0; font-size: 13.5px; }
.radio-baixa { color: var(--accent-ink); font-weight: 600; }
.radio-baixa-linia .hint { display: block; margin-top: 2px; font-size: 12.5px; }
/* The transcript reads as a SCRIPT now: the speaker's name is a small-caps
   line over the words, wearing that voice's colla ink where one exists (the
   thread hues, one surface over) and the neutral default anywhere else — so a
   second learner's cast degrades to exactly the old rendering. Ink, never a
   chip: a chip per line would out-shout the player above it. */
.radio-transcripcio { margin-top: 34px; max-width: var(--measure); }
.radio-transcripcio > .hint { margin-top: 4px; }
.radio-segment { margin-top: 26px; }
.radio-segment h3 { margin: 0 0 2px; font: 700 16px/1.3 var(--display); }
.radio-linia { margin: 14px 0 0; font-size: 15.5px; line-height: 1.6; }
.radio-qui { display: block; margin-bottom: 1px; color: var(--muted-firm);
  font-size: var(--fs-1); font-weight: 700;
  letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.radio-linia[data-veu="nuria"] .radio-qui { color: var(--placa-nuria-ink); }
.radio-linia[data-veu="oriol"] .radio-qui { color: var(--placa-oriol-ink); }
.radio-linia[data-veu="lluis"] .radio-qui { color: var(--placa-lluis-ink); }
.radio-arxiu { margin-top: 34px; max-width: var(--measure); }
.radio-arxiu .radio-arxiu-fila { margin-top: 10px; }
/* ---- Progrés · el vitrall (v2, 5 Aug 2026 — design/handoffs/progres-v2) ----

   THE PAGE IS THREE ZONES IN ONE COLUMN: the hero, el ritme, les xifres, each
   an internal grid, separated by a grout divider. It replaced the two-column
   dashboard whose six blocks were cells of ONE grid (`display: contents` on
   both wrappers, `row dense`, no row-gap). That arrangement existed to stop
   two independent stacks drifting apart across a gutter; zones that own their
   own grid cannot drift, because there is no shared gutter left to drift in.
   `.progres-columns`, `.progres-col-main` and `.progres-col-side` are gone
   with it — the view no longer emits them.

   NOT ONE LITERAL COLOUR ON THE PAGE, THE HERO INCLUDED. The hero is a themed
   surface like every other zone — the same --glass-1 / --edge-soft / --shadow-2
   card the families wear, at a larger radius and with the system --sheen over
   it — and every ink is a token: --text, --muted, --tile-amber-ink for the gold
   accents, --accent-ink for the link. It was briefly a fixed-dark case (the
   handoff's first draft), built by pinning `data-theme="dark"` rather than
   transcribing the three hexes it named; the handoff revised the direction and
   the pin is gone. If a genuinely fixed-dark surface is ever wanted here, pin
   the attribute again — the token layer's dark twin is a bare attribute
   selector — and never transcribe the values.

   Everything animated here fills BACKWARDS behind `.is-entrant`, which the
   view removes on a settle timer — see the sweep's note in progres.js. */

.progres-page .progres-zone { margin: 0; padding: 0; border: 0; border-radius: 0;
  background: transparent; box-shadow: none; }
/* h2 since the A5 repair (zone titles under the Progrés h1); the .overline
   class plus this rule fix size, face and tracking. Margin is 0 because the
   zone is a grid and its row-gap owns the space under the title. */
.progres-page .progres-zone > h2.overline { margin: 0; font-size: 11px;
  letter-spacing: 0.14em; font-weight: 600; font-family: inherit; }
.progres-page .section-link { color: inherit; text-decoration: none; }

/* The divider carries one amber lozenge, so a zone boundary reads as grout
   between two pieces rather than as a rule ruling something off. The halo is
   painted in --bg, which is what parts the line either side of it. Its margin
   is half the 38px zone gap, top and bottom. */
.progres-page > .grout-divider { position: relative; height: 1px; margin: 19px 0;
  border: 0; overflow: visible; background: var(--line); }
.progres-page > .grout-divider::after { content: ""; position: absolute;
  left: 50%; top: 50%; width: 8px; height: 8px; margin: -4px 0 0 -4px;
  border-radius: 1.5px; background: var(--tile-amber); opacity: 0.8;
  transform: rotate(45deg); box-shadow: 0 0 0 6px var(--bg); }
.progres-page > .footnote { margin: 38px 0 0; padding-bottom: 24px; }

/* --- zone 1 · the hero --- */
/* The same alçat targeta the families wear, at a larger radius and with the
   system --sheen laid over it. It carries the h1, so it is the biggest object
   in the room by SCALE, not by being a different species of surface. */
.progres-hero { position: relative; overflow: hidden; border-radius: 22px;
  padding: clamp(28px, 4vw, 48px); border: 1px solid var(--edge-soft);
  background: var(--glass-1); box-shadow: var(--shadow-2);
  /* Confine the sheen's blend to the hero's own backdrop, so what it lightens
     is this card and never the page behind it. */
  isolation: isolate; }
/* THE SHEEN IS BLENDED, NOT LAID FLAT — the app's own idiom for putting --sheen
   over a surface (.hero-frame::after did exactly this before v2). The token is
   `rgba(255,255,255,.5) → .12`, drawn for small glass chips; over the biggest
   surface on the page, straight alpha compositing lifted the DARK card enough
   to push two --muted inks under AA where the gradient is strongest — measured
   4.26 on the epigraph and 4.31 on the figure labels, against a 4.5 bar. Soft
   light keeps the same token and the same --muted ink and buys back the margin.
   Do not swap this for a hand-picked lower alpha: that invents a colour value
   the token layer does not own. */
.progres-hero .hero-lluu { position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; background: var(--sheen); mix-blend-mode: soft-light; }
.hero-cols { position: relative; display: grid; align-items: center;
  gap: clamp(28px, 4vw, 56px);
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); }
.hero-identitat { display: flex; flex-direction: column; align-items: flex-start;
  gap: 18px; min-width: 0; }
.progres-hero .hero-mes { margin: 0; color: var(--tile-amber-ink);
  font-size: 11px; font-weight: 600; letter-spacing: 0.16em; }
.progres-hero h1 { margin: 0; font-family: var(--display); font-style: italic;
  font-weight: 700; font-size: clamp(48px, 6vw, 68px); line-height: 1;
  color: var(--text); }
.progres-hero .hero-cefr { padding: 5px 14px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--tile-amber-ink) 45%, transparent);
  background: transparent; color: var(--tile-amber-ink);
  font-size: 12px; font-weight: 600; letter-spacing: 0.08em; }
/* The volume epigraph: read, not acted on, so it stays a bare line — no card,
   no glaze, no border (raised = actionable; la frase del dia, same reasoning). */
.progres-epigraf { max-width: 44ch; margin: 0; font-family: var(--display);
  font-style: italic; font-size: 20px; line-height: 1.45; color: var(--muted); }
.hero-figures { display: flex; flex-wrap: wrap; gap: 14px 36px; }
.hero-figura { min-width: 0; }
.hero-figura strong { display: block; font-family: var(--display);
  font-weight: 600; font-size: 30px; line-height: 1; color: var(--text);
  font-variant-numeric: tabular-nums; }
.hero-figura-label { display: block; margin-top: 4px; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }

.hero-obra { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.hero-galeria { align-self: flex-end; color: var(--accent-ink);
  font-size: 13px; text-decoration: none; }
.hero-galeria:hover { text-decoration: underline; }
.hero-galeria:focus-visible { outline: 3px solid var(--focus-ring);
  outline-offset: 3px; border-radius: 4px; }

/* --- zones 2 and 3 · the card grids --- */
.progres-ritme, .progres-xifres { display: grid; gap: 20px; align-items: start; }
.progres-ritme > h2.overline, .progres-xifres > h2.overline { grid-column: 1 / -1; }
.progres-ritme { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }

/* The alçat targeta. `.progres-targeta` is what says "this block is raised",
   which is why la biblioteca does not carry it: there is nothing on it to act
   on (law 2), so it wears the head grammar and no card. */
.progres-targeta { display: flex; flex-direction: column; gap: 18px;
  padding: 24px; border: 1px solid var(--edge-soft); border-radius: 16px;
  background: var(--glass-1); box-shadow: var(--shadow-2); }

/* Parallel ink var: tessel·les glaze in the raw --family-hue (a background),
   and every piece of text writes through --family-ink. */
.rhythm-family { --family-hue: var(--accent); --family-ink: var(--accent-ink); }
.rhythm-family--pronouns { --family-hue: var(--tile-blue); --family-ink: var(--tile-blue-ink); }
.rhythm-family--precision { --family-hue: var(--tile-amber); --family-ink: var(--tile-amber-ink); }
.rhythm-family--paraules { --family-hue: var(--tile-terra); --family-ink: var(--tile-terra-ink); }
.rhythm-family--biblioteca { --family-hue: var(--tile-green); --family-ink: var(--tile-green-ink); }

.rhythm-family-head { display: flex; align-items: flex-end;
  justify-content: space-between; gap: 12px; margin: 0; }
/* THE TITLE GIVES WAY, NEVER THE FIGURE. The name column is a flex item, so its
   `min-width: auto` floor is the title's longest WORD — and a one-word title
   («Foundations», «Conversation» in Kathy's pack) is wider at 26px than a card
   in a three-up row, so the mean figure was pushed clean out of its own card,
   and with it the page. The floor is lifted and the title is allowed to break
   inside a word as a last resort — `anywhere` rather than `break-word` because
   it is the one that also lowers the intrinsic width, which is what lets the
   box shrink and the break land evenly instead of orphaning a letter. WebKit
   hyphenates within that width first (the pack's own language is on <html>),
   so where a dictionary exists the break is a proper one. The type is NOT
   reduced: a heading that cannot fit its column wraps, like every other label
   on the card (`.rhythm-skill-label`, same idiom one rule down). */
.rhythm-family-head > div { min-width: 0; }
.rhythm-family h3 { margin: 0; font-family: var(--display); font-weight: 600;
  font-size: 26px; line-height: 1.15; hyphens: auto; overflow-wrap: anywhere; }
.rhythm-status { display: block; margin-top: 4px; color: var(--family-ink);
  font-size: 12px; font-weight: 650; letter-spacing: 0.04em; }
.rhythm-mean { flex: 0 0 auto; text-align: right; }
.rhythm-mean-xifra { display: block; font-family: var(--display);
  font-weight: 600; font-size: 46px; line-height: 1; color: var(--family-ink);
  font-variant-numeric: tabular-nums; }
.rhythm-mean-label { display: block; margin-top: 2px; color: var(--muted);
  font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; }

/* The skills are a plain column inside the card: nesting de-escalates, so they
   lost the inset panel they used to wear when the family was not itself a card. */
.rhythm-skill-grid { display: flex; flex-direction: column; gap: 15px; }
.rhythm-skill { min-width: 0; }
.rhythm-skill-cap { display: flex; align-items: baseline;
  justify-content: space-between; gap: 10px; margin-bottom: 7px; }
.rhythm-skill-label { min-width: 0; color: var(--text-secondary);
  font-size: 14px; line-height: 1.3; overflow-wrap: anywhere; }
.rhythm-skill.weakest .rhythm-skill-label { color: var(--text); font-weight: 700; }
.rhythm-score { flex: 0 0 auto; color: var(--family-ink);
  font: 600 13px/1 var(--font-mono); font-variant-numeric: tabular-nums; }
.rhythm-skill-nota { margin: 6px 0 0; color: var(--muted); font-size: 11.5px;
  font-style: italic; line-height: 1.35; }

/* --- the tessella: one ceramic vocabulary for every bar on the page --- */
.tessella-gauge { display: flex; gap: 3px; }
.tessella { flex: 1 1 0; height: 14px; border-radius: 2px;
  transform: rotate(var(--tilt, 0deg));
  background: color-mix(in srgb, var(--tile-cream) 40%, transparent);
  box-shadow: inset 0 0 0 1px var(--edge-soft); }
.tessella.is-laid { background: color-mix(in srgb, var(--family-hue) 92%, var(--mat));
  box-shadow: inset 0 1px 0 var(--edge),
    inset 0 -3px 4px -2px color-mix(in srgb, var(--text) 22%, transparent); }
/* Glaze jitter, so a run of tiles reads as laid rather than printed. 3 and 5
   are coprime with the 14 segments, so the pattern never stripes (the mosaic
   panel's rule). Scoped to the gauge: the words bar sets its own two hues and
   an unscoped nth-child would repaint every third tile in the family hue. */
.tessella-gauge .tessella.is-laid:nth-child(3n) {
  background: color-mix(in srgb, var(--family-hue) 84%, var(--mat)); }
.tessella-gauge .tessella.is-laid:nth-child(5n + 2) {
  background: var(--family-hue); }

/* --- Paraules: a composition, never a gauge --- */
/* All twenty tiles are always laid; what moves is where the colour changes
   hands. It must never be read as a score, which is why nothing here is empty
   and why the two hues carry matching keys under the figures. */
.words-proportion { display: flex; gap: 3px; }
.words-proportion .tessella { height: 16px; }
.words-proportion .tessella.is-sabuda {
  background: color-mix(in srgb, var(--tile-terra) 92%, var(--mat)); }
.words-proportion .tessella.is-aprenent {
  background: color-mix(in srgb, var(--tile-blue) 88%, var(--mat)); }
.words-proportion .tessella.is-sabuda:nth-child(3n) {
  background: color-mix(in srgb, var(--tile-terra) 80%, var(--mat)); }
.words-proportion .tessella.is-aprenent:nth-child(3n) {
  background: color-mix(in srgb, var(--tile-blue) 74%, var(--mat)); }
.words-figures { display: flex; flex-wrap: wrap; gap: 12px 28px; }
.words-figure { min-width: 0; }
.words-figure strong { display: block; font-family: var(--display);
  font-weight: 600; font-size: 26px; line-height: 1.1;
  font-variant-numeric: tabular-nums; }
.words-figure-label { display: block; margin-top: 2px; color: var(--muted);
  font-size: 12.5px; }
.words-key { display: inline-block; width: 9px; height: 9px; margin-right: 7px;
  border-radius: 2px; }
.words-figure--known .words-key {
  background: color-mix(in srgb, var(--tile-terra) 78%, var(--mat));
  transform: rotate(-3deg); }
.words-figure--learning .words-key {
  background: color-mix(in srgb, var(--tile-blue) 72%, var(--mat));
  transform: rotate(4deg); }
.words-foot { display: flex; justify-content: flex-end; }
.words-link { color: var(--accent); font-size: 13px; text-decoration: none; }
.words-link:hover { text-decoration: underline; }
.words-link:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; }

/* --- La corba --- */
/* This zone answers the one question every other number on the page cannot: is
   he getting better? The curve itself is static SVG, so there is no motion to
   mirror in a prefers-reduced-motion twin. */
.corba-spark-wrap { padding: 14px 16px; border: 1px solid var(--edge-soft);
  border-radius: 12px; background: var(--surface-sunk);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--text) 6%, transparent); }
.corba-spark { display: block; width: 100%; height: 168px; overflow: visible; }
.corba-line { fill: none; stroke: var(--accent); stroke-width: 2.4;
  stroke-linejoin: round; stroke-linecap: round; }
.corba-area { stroke: none; }
/* The month's mean, so a point reads as better or worse than his own average
   rather than against an absolute nobody set. Dashed and faint: context, not
   a target line. */
.corba-mean { stroke: var(--muted); opacity: 0.45; stroke-width: 1;
  stroke-dasharray: 4 5; }
.corba-dot { fill: var(--accent); stroke: var(--surface-sunk); stroke-width: 2; }
.corba .rhythm-status { font-variant-numeric: tabular-nums; }
/* The week-vs-week sentence, below the curve where it has the card's width. */
.corba-delta { margin: 0; color: var(--muted); font-size: 13px;
  font-variant-numeric: tabular-nums; }
.progres-xifres .buit { padding: 18px 0; }

/* --- La biblioteca: named like its neighbours, raised like nothing --- */
.rhythm-family--biblioteca { display: flex; flex-direction: column; gap: 10px;
  padding: 2px 0; }
.rhythm-family--biblioteca h3 { font-size: 21px; }
.rhythm-family--biblioteca .rhythm-mean { color: var(--muted); font-size: 13px;
  font-variant-numeric: tabular-nums; }
.library-note { margin: 0; max-width: var(--measure); font-size: 13px;
  line-height: 1.55; }

/* --- motion --- */
/* Zones rise, tiles land, numerals count. Every one of them fills BACKWARDS,
   so the page must never depend on a frame having been served: `.is-entrant`
   is removed on a timer (progres.js), which returns everything to its plain
   painted state in a hidden tab exactly as in a visible one. */
.progres-page.is-entrant > .progres-zone { animation: progres-puja 0.6s var(--ease-glass) backwards; }
.progres-page.is-entrant > .progres-zone:nth-of-type(2) { animation-delay: 0.1s; }
.progres-page.is-entrant > .progres-zone:nth-of-type(3) { animation-delay: 0.18s; }
.progres-page.is-entrant .tessella.is-laid {
  animation: tessella-cau 0.45s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(0.2s + var(--tessella-retard, 0ms));
  transform-origin: center; }
@keyframes progres-puja { from { opacity: 0; transform: translateY(12px); } }
@keyframes tessella-cau {
  from { opacity: 0; transform: rotate(calc(var(--tilt, 0deg) + 5deg)) scale(0.45); }
}

@media (min-width: 900px) {
  /* El ritme: ONE explicit row of families. `auto-fit` with a 300px floor could
     only fit two of them until the zone reached 940px, so the whole 900–999
     band came out as a 2+1 composition — two cards abreast and the third alone
     under a card-height of empty gutter. The zone is the desktop threshold's
     own row now; the cards narrow instead of wrapping, which is what the
     {480, 900} convention buys (no third breakpoint to express a 2-up tier). */
  .progres-ritme { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* THE HEAD RESERVES TWO TITLE LINES, so the row's cards are cut to one
     pattern: every family's first gauge starts on the same line and two cards
     holding the same number of metrics are the same height. Without it a
     two-line title («Pronoms febles») made its card 19px taller than its
     one-line neighbour («Precisió») — a difference that reads as a data
     difference and is not one. It is a min-height on the h3, so it is derived
     from the type scale rather than from any string: 2 × the h3's own
     line-height, in the h3's own em. Verbs still stands taller than both,
     because it genuinely holds six metrics. Scoped to ≥900 because it is a
     SIDE-BY-SIDE treatment — a stacked phone card must not carry an empty
     line.
     AND THE RESERVED LINE IS AIR ABOVE THE TITLE, NEVER A GAP BELOW IT: the
     text sits on the LAST of the two lines, so a title and its status word stay
     the pair they are, and a title that does wrap (the 900–1010 band, or a
     longer title in another pack) grows UPWARD into the space already held for
     it — the row's gauges never move, and there is no moment where the reserve
     shows as a hole. `align-content` needs a grid or flex box to act on, which
     is the whole reason the h3 is one; nothing else about it changes. */
  .progres-ritme .rhythm-family-head h3 { min-height: calc(2 * 1.15em);
    display: grid; align-content: end; }
  /* Les xifres: Encerts down the left across both rows, Paraules and la
     biblioteca stacked on the right — the handoff's arrangement, placed
     explicitly because auto-fit cannot express a block that spans two rows.
     The empty state carries `.corba-buit` so it lands in the same cell as the
     card it replaces. */
  .progres-xifres { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .progres-xifres > .corba, .progres-xifres > .corba-buit {
    grid-column: 1; grid-row: 2 / span 2; }
  .progres-xifres > .rhythm-family--paraules { grid-column: 2; grid-row: 2; }
  .progres-xifres > .rhythm-family--biblioteca { grid-column: 2; grid-row: 3; }
}

@media (min-width: 640px) {
  .progres-page > .grout-divider { margin: 22px 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* The twin: zones appear in place, tiles render laid, and the numerals never
     counted at all (the view does not start the clock). Motion carries nothing
     here that the static page does not already say. */
  .progres-page.is-entrant > .progres-zone,
  .progres-page.is-entrant .tessella.is-laid { animation: none !important; }
}

/* ── L'Obrador · S3 staging and review ───────────────────────────────────
   A finite actionable queue: the door and rows exist only while drafts do.
   No badge, count, dot, toast or Avui treatment belongs to this block. */
.progres-obrador {
  display: grid;
  gap: var(--space-1);
  margin-top: var(--space-5);
  padding: var(--space-4) var(--space-5);
  color: var(--text);
  text-decoration: none;
  background: var(--glass-1);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}
.progres-obrador strong { font: 700 var(--fs-5)/var(--lh-ui) var(--font-body); }
.progres-obrador > span:last-child { color: var(--muted); font-size: var(--fs-3); }

/* Public course · account-free progress custody. One raised block on Progrés;
   private profiles never render it. Status is text as well as colour. */
.public-progress { margin-top: var(--space-5); }
.public-progress-head { display: flex; align-items: start; justify-content: space-between;
  gap: var(--space-4); }
.public-progress-head h2, .public-progress-head .overline,
.public-progress-intro, .public-progress-preview p { margin: 0; }
.public-progress-head h2 { margin-top: var(--space-1);
  font: 700 var(--fs-6)/1.15 var(--display); }
.public-progress-status { flex: none; margin: 0; padding: var(--space-1) var(--space-3);
  border: 1px solid var(--edge-soft); border-radius: 999px;
  color: var(--muted-firm); font-size: var(--fs-1); font-weight: 700; }
.public-progress-status.is-warning { color: var(--miss); border-color: currentColor; }
.public-progress-intro { max-width: var(--measure); color: var(--text-secondary);
  font-size: var(--fs-3); line-height: 1.55; }
.public-progress-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.public-progress-actions .is-danger { color: var(--miss); }
.public-progress-preview { display: grid; gap: var(--space-2); padding: var(--space-4);
  border: 1px solid var(--edge-soft); border-radius: var(--radius-s);
  background: var(--surface-sunk); }
.public-progress-preview[hidden] { display: none; }
.public-progress-feedback { min-height: 1.4em; color: var(--muted);
  font-size: var(--fs-2); }
@media (max-width: 480px) {
  .public-progress-head { flex-direction: column; }
  .public-progress-status { align-self: start; }
}
.obrador-page { display: grid; gap: var(--space-5); }
.obrador-head { display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: start; gap: var(--space-4); }
.obrador-head h1, .obrador-head p { margin-block: 0 var(--space-2); }
.obrador-list { display: grid; gap: var(--space-4); }
.obrador-row { display: grid; gap: var(--space-3); padding: var(--space-5);
  background: var(--glass-1); border: 1px solid var(--edge-soft);
  border-radius: var(--radius); box-shadow: var(--shadow-2); }
.obrador-row-head { display: flex; align-items: start; justify-content: space-between;
  gap: var(--space-4); }
.obrador-row h2, .obrador-row .overline, .obrador-target, .obrador-error { margin: 0; }
.obrador-row time, .obrador-target { color: var(--muted); font-size: var(--fs-2); }
.obrador-row time { flex: 0 0 auto; }
.obrador-preview { max-height: 20rem; margin: 0; padding: var(--space-4); overflow: auto;
  white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text);
  background: var(--surface-sunk); border: 1px solid var(--line);
  border-radius: var(--radius-s); font: var(--fs-2)/var(--lh-body) var(--font-mono); }
.obrador-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.obrador-row.is-invalid .obrador-preview { color: var(--muted); }
@media (max-width: 480px) {
  .obrador-head { grid-template-columns: 1fr; }
  .obrador-row-head { display: grid; }
}

.scenario-row { display: flex; gap: 12px; align-items: center;
  background: var(--surface); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 14px 18px; margin-top: 12px; }
.scenario-row.locked { opacity: 0.55; }

.scenario-icon { font-size: 22px; }
.scenario-body { flex: 1; }
.scenario-body .bar { margin-top: 6px; }
.practice-mastery-label { display: block; margin-top: 3px; font-variant-numeric: tabular-nums; }
.practice-change { font-size: 14px; color: var(--muted); font-variant-numeric: tabular-nums; }

.seccio-title { margin: 26px 0 2px; }
/* Promoted h3→h2 group titles (A5: Pràctica groups, Escoltar «Al món real»).
   Pin the old h3 look — body face, 1.17em, the class margin — so the semantic
   promotion is visually inert (.dashboard h2 would otherwise impose the display
   serif at 1.5em with its own margins). */
.dashboard h2.seccio-title { font-family: inherit; font-size: 1.17em; margin: 26px 0 2px; }

.listen-card { background: var(--surface); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 14px 18px; margin-top: 12px; }
.listen-card.done { opacity: 0.6; }
.listen-card p { margin: 4px 0; }
.listen-card .btn { margin-top: 8px; margin-right: 8px; }

/* ---- Registre (native register) ---- */
/* tria (pick-the-native) option buttons */
.tria-options { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 2px; }
.tria-option { text-align: left; padding: 12px 16px; font: inherit; font-size: 17px;
  color: var(--text); background: var(--surface);
  border: 1.5px solid var(--bar-track); border-radius: 12px; cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; }
/* Hover is a POINTER state, and on a touch screen it is a STICKY one: WebKit
   leaves it on whatever was tapped last, so an accent border can be found
   standing on an option nobody chose. Guarded to real pointers — la bombolla's
   `(hover: hover) and (pointer: fine)` rule, for the same reason. Wrapped in
   place rather than gathered into one block at the foot: these rules have to
   keep sitting ABOVE their `.is-triat`/`.selected` twin, or hovering a chosen
   option would repaint its accent border as track grey. */
@media (hover: hover) and (pointer: fine) {
  .tria-option:hover { border-color: var(--accent); }
}
/* El delit S1 · the physical half of a pick — RETUNED 31 Jul 2026, and the
   retune is the interesting part. The first cut dipped on `:active` with
   `scale(0.985)` and settled the chosen option with `gap-settle`, the 1.06 pump
   a correct cloze gap makes, on the theory that choosing right and being right
   should speak one language. They do not, because they are not the same OBJECT:
   a cloze gap is a small inline chip, and this is a full-width row of 17px
   prose. Scaling a chip is a gesture; scaling a paragraph resamples every
   glyph in it, so Ryan reported the text going fuzzy the moment he picked —
   which is exactly what a 6 % pump on body copy does, and no amount of tuning
   the curve fixes the fact that a scale is the wrong tool on text.

   THE RULE THIS LEAVES: never scale an element whose content is running text.
   Press it (translate), fade it, ring it — but do not resize it. The app's own
   `.btn:active { scale(0.97) }` is not a counter-example: a button label is one
   or two words on a small control, where the whole thing reads as an object
   rather than as something you are mid-sentence in.

   So the press is la sortida's 1px dip — whole-pixel, no resampling — and the
   selection is left to say itself in colour, which is what the three sibling
   picks (.mot, .calaix-tria, .replica) and the sistema's own `.is-triat`
   pattern already do. Nothing was lost: the dip was always the half that said
   «your finger landed», and the accent fill was always the half that said
   «this one is chosen». Only the pump on top of both is gone. */
.tria-option:active { transform: translateY(1px); }
.tria-option.selected { border-color: var(--accent); background: var(--accent-soft);
  color: var(--accent-ink); font-weight: 600; }
@media (prefers-reduced-motion: reduce) {
  .tria-option { transition: none; }
  .tria-option:active { transform: none; }
}

/* three-band flag chips: natiu / tolerat / evita */
.flag-chip { display: inline-block; font-size: 11px; font-weight: 700;
  letter-spacing: 0.03em; text-transform: uppercase; padding: 2px 8px;
  border-radius: 999px; border: 1px solid transparent; vertical-align: middle; }
.flag-natiu { color: var(--tile-green-ink);
  background: color-mix(in srgb, var(--tile-green) 14%, transparent);
  border-color: color-mix(in srgb, var(--tile-green) 40%, transparent); }
.flag-tolerat { color: var(--tile-amber-ink);
  background: color-mix(in srgb, var(--tile-amber) 16%, transparent);
  border-color: color-mix(in srgb, var(--tile-amber) 45%, transparent); }
/* Clay, NOT `--miss` (23 Jul 2026): `--miss` is the wrong-answer verdict ink,
   and this chip sits on a row whose headline is the CORRECT Catalan form — the
   colour half of the same mistake the «evita» label made. */
.flag-evita { color: var(--tile-terra-ink);
  background: color-mix(in srgb, var(--tile-terra) 12%, transparent);
  border-color: color-mix(in srgb, var(--tile-terra) 40%, transparent); }
/* The two orthogonal axes (22 Jul 2026). Only the MARKED values get a chip.
   They read quieter than the band — the band is still the headline — but they
   are the SAME species of chip: same shape, same caps, same tint-plus-border
   recipe, quieted by hue (neutral ink) rather than by a different form
   (23 Jul 2026 — bare-bordered lowercase read as another kind of object). */
.flag-mode { color: var(--muted-firm);
  background: color-mix(in srgb, var(--muted) 12%, transparent);
  border-color: color-mix(in srgb, var(--muted) 38%, transparent); }
.flag-to-suau { color: var(--tile-blue-ink);
  background: color-mix(in srgb, var(--tile-blue) 14%, transparent);
  border-color: color-mix(in srgb, var(--tile-blue) 40%, transparent); }
/* THE ONE FILL ON THIS SURFACE (v2, 5 Aug 2026): white on `--rose-fill`, the
   seal — law 1's own `-fill` variant, measured 7.4:1 light / 5.4:1 dark. It was
   filled `--miss` with a `--surface` label (the `.placa-danger` idiom), which
   was correct while nothing else on a row was red; v2 puts the castellanisme's
   `mai «tinc que»` in `--miss` INSIDE the same summary, and a `--miss` chip
   beside `--miss` struck text reads as one alarm smeared across two objects.
   It stays FILLED — the one mark that must be impossible to miss — and it stays
   the same species of chip as the bands (same caps, same shape). */
.flag-to-vulgar { color: #fff; background: var(--rose-fill);
  border-color: var(--rose-fill); }

/* ================================================================
   EL REGISTRE v2 — la portada, quatre famílies i el catàleg navegable
   (5 d'agost 2026, design/handoffs/registre-v2/)
   ================================================================
   Three stacked blocks of equal weight became three RANKS: a vidre hero
   (identity + today's expression) ⟩ four alçat family cards ⟩ the catalog as a
   browsable reference behind a filter toolbar. Nothing about the data moved —
   `entryOfDay` still excludes vulgar entries, `catCounts` still decides which
   family cards render, `groupEntries` still orders the catalog.

   What the old surface got wrong, and what replaced it:
     • THE CATALOG WAS A WALL — 79 visually identical collapsed rows, the
       banding invisible until you read chips one at a time. It gets a filter
       toolbar (family tabs · live search · band toggles), and the ≥900 sticky
       `.reg-index` nav is RETIRED: the tabs do its job at every width.
     • THE CONTRAST WAS HIDDEN. The street / book / Spanish triangle — the whole
       reason the page exists — only appeared once a row was open. A collapsed
       row now carries the native form beside its Spanish gloss, and a transfer
       row shows the castellanisme struck through.
     • THE KEY WAS FOLDED AND FORGOTTEN. «Com llegir el catàleg» was the only
       place the app says «tolerat» is not bad Catalan, and it lived inside a
       `<details>` nobody opened. It is a permanently visible card now, and it
       is the ONE source: the desktop index legend went with the index.

   SIX PLACES THE HANDOFF WAS NOT FOLLOWED, each a rule that outranks it:
   (1) The page widens by naming an ARCHETYPE — `lay-browse`, not the handoff's
       raw 1080px. docs/LAYOUT.md outranks a handoff value (Progrés v2,
       la unitat v2 and el vocabulari v2 all ruled the same way), and the page
       was already lay-browse.
   (2) The page wash is a RADIAL fading to transparent, not the handoff's
       `linear-gradient(…, var(--bg) 380px)`: an opaque wash bounded by the
       composition draws two vertical seams down any window wider than the
       column — measured on la porta, plainly visible at 1280.
   (3) The hero takes `--radius-l` like every other v2 hero rather than a
       seventh radius. The rooms open the same way.
   (4) THE TOOLBAR IS STICKY AT ≥900 ONLY. The gimnàs v2 ruling, five days
       older, on exactly this ask: five wrapping family tabs + a search field +
       three band toggles + the count measure ~250px at 375px, which is 37 % of
       a 667px phone standing there permanently. Below 900 it is an ordinary
       block at the head of the catalog; the filters are all still there.
   (5) The three secondary marks stay `flag-chip` — same caps, same shape,
       same tint-plus-border recipe as the bands. The handoff draws them as
       bespoke lowercase pills with no border, which is precisely the "different
       species of object" the 23 Jul 2026 ruling removed: quiet a chip by HUE,
       never by giving it a different shape or casing. Their hues are unchanged
       (mode neutral, suau blue); only the vulgar chip moved, and it moved onto
       law 1's own `-fill` variant — see `.flag-to-vulgar` above.
   (6) The «forma pròpia» band toggle is painted from `--tile-terra`, not the
       handoff's `--miss`. `--miss` is the wrong-answer verdict ink and this
       filter selects rows whose headline form is the CORRECT Catalan one — the
       colour half of the same mistake the «evita» LABEL made (23 Jul 2026).
       It matches `.flag-evita`, which is the chip it filters by.

   One addition on top of the handoff: the veil clears on keyboard FOCUS as well
   as on hover and open, so the reveal is never pointer-only. */

/* THE PAGE. The wash rides the composition (see 2 above). */
.registre-page { position: relative; display: flex; flex-direction: column;
  gap: clamp(20px, 2.6vw, 30px); padding: clamp(4px, 1.4vw, 14px) 0 40px;
  background: radial-gradient(64% 440px at 50% -30px,
    color-mix(in srgb, var(--accent) 7%, transparent),
    color-mix(in srgb, var(--accent) 0.4%, transparent) 72%); }
/* The zones rise behind `.is-entrant`, filling BACKWARDS; renderRegistre drops
   the class on a settle timer whether or not a frame was ever served (the
   mosaic's rule). Only an arrival carries it — climbing out of a drill is a
   return, not a landing. */
.registre-page.is-entrant > * { animation: reg-puja var(--dur-3) var(--ease-glass) backwards; }
.registre-page.is-entrant > .reg-zona:nth-of-type(2) { animation-delay: 0.08s; }
.registre-page.is-entrant > .reg-zona:nth-of-type(3) { animation-delay: 0.14s; }
@keyframes reg-puja { from { opacity: 0; transform: translateY(14px); } }
@media (prefers-reduced-motion: reduce) {
  .registre-page.is-entrant > * { animation: none !important; }
}

/* ---- the hue vocabulary ----
   A family's identity hue rides two vars, the parallel-ink idiom: the glaze
   paints, the ink writes (law 1). Accent IS an identity here (les reaccions),
   which is legal because nothing on this surface uses accent as a state. */
.reg-hue--accent { --reg-hue: var(--accent);     --reg-ink: var(--accent-ink); }
.reg-hue--blue   { --reg-hue: var(--tile-blue);  --reg-ink: var(--tile-blue-ink); }
.reg-hue--amber  { --reg-hue: var(--tile-amber); --reg-ink: var(--tile-amber-ink); }
.reg-hue--rose   { --reg-hue: var(--tile-rose);  --reg-ink: var(--tile-rose-ink); }

/* ---- Zone 1 · the hero ----
   The view's FIRST vidre (elevation 3; the toolbar is the second and last), and
   it really does sit over something: the page's own accent wash. */
.reg-hero { position: relative; overflow: hidden; isolation: isolate;
  border-radius: var(--radius-l); padding: clamp(24px, 4vw, 44px);
  border: 1px solid color-mix(in srgb, var(--rim) 60%, var(--edge-soft));
  background: color-mix(in srgb, var(--surface) 76%, transparent);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .reg-hero { background: var(--surface); }
}
/* Soft-light for the reason measured on the Progrés and porta heroes: straight
   alpha over a surface this size lifts the ground enough to push --muted ink
   under AA. */
.reg-hero .hero-lluu { position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; background: var(--sheen); mix-blend-mode: soft-light; }
/* Trencadís scatter: ornament, never information — aria-hidden, inert, and it
   animates nothing. */
.reg-scatter { position: absolute; top: -26px; right: -14px;
  width: 240px; height: 190px; pointer-events: none; opacity: 0.45; }
.reg-scatter i { position: absolute; display: block; }
.reg-scatter i:nth-child(1) { top: 44px; right: 36px; width: 30px; height: 30px;
  border-radius: 9px; background: var(--tile-blue); opacity: 0.32; transform: rotate(-6deg); }
.reg-scatter i:nth-child(2) { top: 88px; right: 74px; width: 22px; height: 22px;
  border-radius: 7px; background: var(--tile-amber); opacity: 0.4; transform: rotate(4deg); }
.reg-scatter i:nth-child(3) { top: 36px; right: 96px; width: 17px; height: 17px;
  border-radius: 6px; background: var(--tile-rose); opacity: 0.34; transform: rotate(-3deg); }
.reg-scatter i:nth-child(4) { top: 104px; right: 26px; width: 19px; height: 19px;
  border-radius: 6px; background: var(--tile-green); opacity: 0.3; transform: rotate(7deg); }
.reg-scatter i:nth-child(5) { top: 70px; right: 126px; width: 12px; height: 12px;
  border-radius: 4px; background: var(--accent); opacity: 0.26; transform: rotate(-8deg); }

.reg-hero-cos { position: relative; display: flex; flex-wrap: wrap;
  gap: 26px 40px; align-items: stretch; }
.reg-hero-copia { flex: 1 1 380px; min-width: min(380px, 100%);
  display: flex; flex-direction: column; gap: 10px; }
.reg-hero-copia > .sortida { align-self: flex-start; margin: 0 0 6px; }
/* Two classes so it beats `.dashboard h1`'s 1.5em pin without relying on
   source order. */
.reg-hero .reg-titol { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(38px, 5vw, 54px); line-height: 1.03; letter-spacing: 0.005em; }
.reg-hint { margin: 0; font-size: 15.5px; line-height: 1.5; color: var(--muted);
  max-width: 52ch; text-wrap: pretty; }
.reg-stat { margin: 0; font-size: 13px; color: var(--muted);
  font-variant-numeric: tabular-nums; }

/* «Avui al carrer» — esmaltat, nested inside the vidre so it DE-ESCALATES. */
.reg-today { flex: 1 1 300px; min-width: min(300px, 100%); max-width: 440px;
  align-self: center; display: flex; flex-direction: column; gap: 9px;
  padding: 20px 22px; border-radius: 18px;
  background: color-mix(in srgb, var(--accent) 5%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--edge-soft));
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
.reg-today-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.reg-today-label { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 700; color: var(--accent-ink); }
.reg-today-tools { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; }
.reg-native { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(26px, 3vw, 31px); line-height: 1.12; color: var(--text); }
.reg-es { margin: 0; font-size: 13.5px; color: var(--muted); }
.reg-use { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--muted); }

/* ---- the speaker ----
   Bespoke rather than `.btn-audio`: this is a circular icon control on a card,
   not the player's chunky audio button, and the global :focus-visible ring
   covers a bare <button>. */
.reg-veu { display: grid; place-items: center; flex: none;
  width: 34px; height: 34px; border-radius: 999px; padding: 0;
  border: 1px solid var(--edge-soft); background: var(--surface);
  color: var(--accent-ink); cursor: pointer;
  transition: border-color 0.16s ease, color 0.16s ease; }
.reg-veu:hover { border-color: var(--accent); color: var(--accent); }
.reg-veu-l { width: 36px; height: 36px;
  border-color: color-mix(in srgb, var(--accent) 34%, var(--edge-soft)); }
@media (prefers-reduced-motion: reduce) { .reg-veu { transition: none; } }

/* ---- the zone heads ---- */
.reg-zona { display: flex; flex-direction: column; gap: 14px; }
.reg-zona-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.reg-zona-head .reg-zona-titol { margin: 0; font-family: var(--display);
  font-style: italic; font-weight: 600; font-size: 22px; }
.reg-zona-head > .grout-rule { flex: 1; min-width: 60px; }
.reg-zona-nota { margin: 0; font-size: 13px; color: var(--muted); }

/* ---- Zone 2 · la pràctica ----
   Four alçat cards of equal rank — you act on them, so they are raised. They
   render the same quantities the old skinny rows did, in one rendering. */
/* 2 × 2, NOT the handoff's `auto-fit, minmax(250px, 1fr)`, and this is the
   seventh deviation — measured, at the app's own column width. lay-browse caps
   the page at 1000, so a four-across row gives (1000 − 3×16) / 4 = 238px cards:
   the 46px tile takes 58 of the 194px of content, leaving 136 for an overline
   that measures ~184, so every card wraps its own count line AND its title.
   Dropping the floor to fit four just buys the wrap. Four cards at 492 read as
   four peers with room; four at 238 read as the skinny rows this zone replaced.
   Never 3 + 1 either: an orphaned fourth family is a hierarchy among peers. */
.reg-families { display: grid; gap: 16px; grid-template-columns: 1fr; }
@media (min-width: 560px) {
  .reg-families { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.reg-familia { display: flex; flex-direction: column; gap: 12px;
  padding: 20px 22px; border-radius: 18px; background: var(--surface);
  border: 1px solid var(--edge-soft);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
.reg-familia-cap { display: flex; align-items: center; gap: 12px; }
.reg-familia-icona { display: grid; place-items: center; flex: none;
  width: 46px; height: 46px; border-radius: 14px; color: var(--reg-ink);
  background: color-mix(in srgb, var(--reg-hue) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--reg-hue) 25%, transparent);
  transform: rotate(-2deg); }
.reg-familia:nth-child(even) .reg-familia-icona { transform: rotate(2deg); }
.reg-familia-noms { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.reg-overline { font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 600; color: var(--muted); }
.reg-familia-titol { font-family: var(--display); font-weight: 700;
  font-size: 23px; line-height: 1.05; }
.reg-familia-nota { margin: 0; font-size: 13.5px; line-height: 1.5;
  color: var(--muted); text-wrap: pretty; }
.reg-familia-domini { display: flex; flex-direction: column; gap: 6px; margin-top: auto; }
/* A GAUGE, not a composition (the Paraules ruling): ten tessel·les standing for
   one percentage, so the row keeps role=progressbar and its aria-value* and the
   reading never depends on counting ceramic. Tilts are seeded in JS off the
   mosaic engine's own hash. */
.reg-tesselles { display: inline-flex; gap: 4px; flex-wrap: wrap; }
.reg-tessella { display: block; width: 11px; height: 11px; border-radius: 3.5px;
  background: var(--bar-track); transform: rotate(var(--tilt, 0deg)); }
.reg-tessella.is-laid { background: var(--reg-hue);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2); }
.reg-familia-xifres { font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
/* Geometry only — the primary keeps the app's own fill recipe (and its
   theme-split gradient), so nothing here re-declares a colour. */
.reg-primari { align-self: flex-start; display: inline-flex; align-items: center;
  justify-content: center; min-height: 44px; padding: 10px 18px;
  border-radius: 12px; font-weight: 600; font-size: 13.5px; }

/* ---- Zone 3a · «com llegir el catàleg» ----
   Since 6 Aug 2026 the key is the six marks themselves in one row — hover,
   focus or tap a chip and its gloss appears in the line beneath (the previous
   six-paragraph key buried the catalog and repeated the toolbar's own band
   words an inch above it). The CARD stays flat — it is a key, not a control
   panel — but the chips inside are now real buttons and wear the affordance:
   the system `.is-triat` recipe when chosen, which doubles as the hover
   feedback since hovering selects. */
.reg-llegenda { display: flex; flex-direction: column; gap: 8px;
  padding: 14px 20px 12px; border-radius: 16px; background: var(--surface);
  border: 1px solid var(--edge-soft); box-shadow: inset 0 1px 0 var(--edge); }
.reg-llegenda-rubric { font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; font-weight: 700; color: var(--muted); }
.reg-clau { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; }
/* A chip wrapped in a real button. Transparent at rest so the chip reads
   exactly as it does on a row; the ::after inset grows the touch target past
   the visual pill (vertical only — sideways it would overlap the neighbour). */
.reg-clau-boto { position: relative; display: inline-flex; align-items: center;
  margin: 0; padding: 4px 6px; border-radius: 999px;
  border: 1px solid transparent; background: none; cursor: pointer; }
.reg-clau-boto::after { content: ""; position: absolute; inset: -6px 0; }
.reg-clau-boto.is-triat { border-color: var(--accent);
  background: color-mix(in srgb, var(--accent-soft) 68%, var(--surface)); }
.reg-clau-sep { color: var(--muted); opacity: 0.6; padding: 0 3px;
  font-weight: 700; }
/* Every gloss is in the DOM, all six stacked in ONE grid cell: the tallest
   fixes the height, so switching marks can never shift the catalog below. The
   swap is a text swap, no motion — nothing here needs a reduced-motion twin. */
.reg-clau-notes { display: grid; }
.reg-clau-nota { grid-area: 1 / 1; margin: 0; font-size: 13px;
  line-height: 1.5; color: var(--muted); visibility: hidden; }
.reg-clau-nota.is-triada { visibility: visible; }

/* ---- Zone 3b · the filter toolbar ----
   The view's SECOND and last vidre. Sticky at ≥900 only (see 4 above). */
.reg-barra { display: flex; flex-direction: column; gap: 10px;
  padding: 12px 16px; border-radius: 16px; border: 1px solid var(--edge-soft);
  background: color-mix(in srgb, var(--surface) 76%, transparent);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .reg-barra { background: var(--surface); }
}
.reg-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
.reg-tab { padding: 8px 15px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--edge-soft); background: var(--surface);
  color: var(--text); font-weight: 600; font-size: 13.5px;
  transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease; }
.reg-tab:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--edge-soft)); }
/* The system's own selected state, filled: a family tab is a hard either/or and
   the chosen one has to be unmistakable across five pills. White on
   `--accent-fill` is law 1's filled-control recipe. */
.reg-tab.is-triat { border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: var(--accent-fill); color: #fff;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); }
.reg-filtres { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
/* 16px is the iOS zoom law, not a taste call. */
.reg-cerca { flex: 1 1 220px; min-width: min(220px, 100%);
  padding: 10px 14px; border-radius: 12px; border: 1.5px solid var(--line);
  background: var(--surface); color: var(--text); font-size: 16px;
  outline-color: var(--accent); }
.reg-cerca::placeholder { color: var(--muted); opacity: 0.85; }
.reg-marques { display: inline-flex; flex-wrap: wrap; gap: 6px; }
/* A band toggle OFF is dashed and empty — the app's «not written / not on»
   texture — so on-ness is read from the fill, never from the hue alone. */
.reg-marca { padding: 7px 13px; border-radius: 999px; cursor: pointer;
  border: 1px dashed var(--line); background: transparent; color: var(--muted);
  font-weight: 700; font-size: 12px; letter-spacing: 0.02em;
  transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease; }
.reg-marca.is-triat { border-style: solid;
  border-color: color-mix(in srgb, var(--reg-band) 45%, transparent);
  background: color-mix(in srgb, var(--reg-band) 13%, var(--surface));
  color: var(--reg-band-ink); }
.reg-marca--natiu   { --reg-band: var(--tile-green); --reg-band-ink: var(--tile-green-ink); }
.reg-marca--tolerat { --reg-band: var(--tile-amber); --reg-band-ink: var(--tile-amber-ink); }
.reg-marca--evita   { --reg-band: var(--tile-terra); --reg-band-ink: var(--tile-terra-ink); }
.reg-compte { margin-left: auto; font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) {
  .reg-tab, .reg-marca { transition: none; }
}

/* ---- Zone 3c · the groups ---- */
.reg-llista { display: flex; flex-direction: column; gap: 18px; }
.reg-grup { display: flex; flex-direction: column; gap: 10px; }
.reg-grup-head { display: flex; align-items: center; gap: 12px; }
.reg-grup-marca { flex: none; width: 13px; height: 13px; border-radius: 4px;
  background: var(--reg-hue); transform: rotate(-3deg); }
.reg-grup-head .reg-grup-titol { margin: 0; font-family: var(--display);
  font-style: italic; font-weight: 600; font-size: 21px; }
.reg-grup-head > .grout-rule { flex: 1; min-width: 40px; }
.reg-grup-compta { font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.reg-fitxes { display: flex; flex-direction: column; gap: 8px; }
/* Warm, and with the fix at hand — three expressions that are certainly there. */
.reg-buit { margin: 0; padding: 36px 20px; text-align: center;
  border: 1px dashed var(--line); border-radius: 16px;
  color: var(--muted); font-size: 14.5px; }

/* ---- Zone 3d · la fitxa ---- */
.reg-entry { background: var(--surface); border: 1px solid var(--edge-soft);
  border-radius: 14px; box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
/* A COMPLEX SUMMARY WRAPS. A row carries a band chip, the expression object, a
   trap preview, up to two marks, the speaker and the chevron; on a 375px phone
   that cannot be one line, and `flex: 1 1 240px` on the expression is what makes
   it MOVE rather than shrink past its min-content floor and push the controls
   off the right edge. */
.reg-entry > summary { display: flex; flex-wrap: wrap; align-items: center;
  gap: 8px 14px; padding: 13px 18px; cursor: pointer; list-style: none;
  border-radius: 14px; transition: background 0.16s ease; }
.reg-entry > summary::-webkit-details-marker { display: none; }
.reg-entry > summary:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
.reg-entry[open] > summary { border-bottom: 1px dashed var(--line);
  border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.reg-entry > summary > .flag-chip { flex: none; min-width: 52px; text-align: center; }
/* The form and its Spanish gloss ARE the contrast, so they travel as one flex
   item: a gloss that wrapped away from its form would say nothing. */
.reg-expressio { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px;
  flex: 1 1 240px; min-width: min(240px, 100%); }
.reg-form { font-family: var(--display); font-weight: 700; font-size: 21px;
  line-height: 1.1; overflow-wrap: anywhere; }
.reg-gloss { font-size: 13px; color: var(--muted); font-style: italic; }
.reg-trap-mini { flex: none; font-size: 12.5px; color: var(--miss); white-space: nowrap; }
.reg-trap-mini s { text-decoration-thickness: 1.5px; }
.reg-chev { display: inline-flex; flex: none; color: var(--muted);
  transition: transform 0.2s ease; }
.reg-entry[open] .reg-chev { transform: rotate(180deg); }
/* THE VEIL was removed 26 Sep 2026 (Ryan): vulgar forms render like every other row. */
@media (prefers-reduced-motion: reduce) {
  .reg-entry > summary, .reg-chev { transition: none; }
}

.reg-body { display: flex; flex-wrap: wrap; gap: 16px 24px; padding: 16px 18px 18px; }
.reg-body-contrast { flex: 1.3 1 280px; min-width: min(280px, 100%);
  display: flex; flex-direction: column; gap: 9px; }
/* The labelled triangle: al llibre · mai · en castellà, one grid so the three
   forms share a left edge and the eye reads down them. */
.reg-contrast { display: grid; grid-template-columns: 96px minmax(0, 1fr);
  gap: 10px; align-items: baseline; margin: 0; }
.reg-contrast-etiqueta { font-size: 10.5px; letter-spacing: 0.14em;
  text-transform: uppercase; font-weight: 700; color: var(--muted); }
.reg-contrast-valor { font-size: 14.5px; }
.reg-line-trap .reg-contrast-etiqueta, .reg-line-trap .reg-contrast-valor { color: var(--miss); }
.reg-line-trap s { text-decoration-thickness: 1.5px; }
.reg-line-use { margin: 2px 0 0; font-size: 13.5px; line-height: 1.55;
  color: var(--muted); text-wrap: pretty; }
.reg-example { flex: 1 1 250px; min-width: min(250px, 100%); align-self: flex-start;
  display: flex; flex-direction: column; gap: 6px; padding: 14px 16px;
  border-radius: 12px; border: 1px solid var(--edge-soft);
  background: color-mix(in srgb, var(--tile-cream) 26%, var(--surface)); }
.reg-example-rubric { font-size: 10.5px; letter-spacing: 0.14em;
  text-transform: uppercase; font-weight: 700; color: var(--accent-ink); }
.reg-example-ca { margin: 0; font-size: 15px; line-height: 1.5; }
.reg-example-es { margin: 0; font-size: 13px; line-height: 1.5;
  color: var(--muted); font-style: italic; }

@media (min-width: 900px) {
  /* Sticky here and nowhere below — the gimnàs v2 ruling (see 4 above). The
     topbar chrome scrolls with the page, so 12px is clear air, not an offset. */
  .reg-barra { position: sticky; top: 12px; z-index: var(--z-sticky); }
}

/* ================================================================
   LA GRAMÀTICA — el gimnàs, then the ink-on-paper study units
   (--paper is a system token now — the local mix that used to live here is
   gone). The two halves look different on purpose: the gym is a practice
   surface on the page ground, a unit page is a sheet you read.
   ================================================================ */

/* ---- El gimnàs v2 (design/handoffs/gimnas-v2, 5 Aug 2026) ----
   The pane is no longer ONE card holding everything. It is a stack of three
   surfaces — the vidre hero, the controls bar, the roster — parted by the
   system's grout rule, which is what lets each of them be sized and weighted
   for its own job instead of all three sharing one glass-2 box.
   What did NOT move: the tense selection and la gota, l'onada, the three round
   builders, the fantasma peek, the portada gate. See gymBlock's note for the
   three places the handoff was deliberately not followed (the shared subnav,
   the h1, and sticky-on-mobile). */
.gram-gym { display: flex; flex-direction: column; gap: var(--space-5); margin-top: 12px; }
.gram-units > h2 { margin: 2px 0 4px; font-family: var(--display); }
.gram-units > .hint { margin: 0; }

/* The hero: the same vidre card Progrés's hero wears, so the two rooms open the
   same way. It carries the pane's identity, the three derived figures and the
   coverage map — biggest object in the room by SCALE, never by being a
   different species of surface. */
.gym-hero { position: relative; overflow: hidden; isolation: isolate;
  border-radius: var(--radius-l); padding: clamp(26px, 4vw, 46px);
  border: 1px solid var(--edge-soft); background: var(--glass-1);
  box-shadow: var(--shadow-2); }
/* Soft-light, not straight alpha — the token is drawn for small glass chips and
   over a surface this size it lifts the ground enough to push --muted ink under
   AA (measured on the Progrés hero, same fix, same reason). */
.gym-lluu { position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; background: var(--sheen); mix-blend-mode: soft-light; }
.gym-hero .hero-cols { position: relative; }
/* An h2, not the handoff's h1: the lloc's h1 is «El curs» (one h1 per view
   landing). It is also capped well under the handoff's 58px — a pane title
   dwarfing the page title inverts the hierarchy instead of expressing it. */
.gym-hero h2 { margin: 0; font-family: var(--display); font-style: italic;
  font-weight: 700; font-size: clamp(32px, 3.8vw, 44px); line-height: 1.05;
  letter-spacing: 0.01em; color: var(--text); text-wrap: balance; }
.gym-hero .gram-rubric { margin: 0; color: var(--tile-amber-ink);
  letter-spacing: 0.16em; font-weight: 600; }
.gym-hero .hint { margin: 0; max-width: 42ch; font-family: var(--display);
  font-style: italic; font-size: 20px; line-height: 1.45; color: var(--muted); }
.gym-hero .hero-identitat { gap: var(--space-4); }

/* MUX-01: on phones the task precedes its coverage. Flatten only the
   presentational hero wrappers; the existing controls keep their DOM/focus
   order, text sizes and hit areas. Desktop retains the complete vidre hero. */
@media (max-width: 480px) {
  .gram-gym { gap: var(--space-3); }
  .gym-hero,
  .gym-hero .hero-cols,
  .gym-hero .hero-identitat { display: contents; }
  .gym-hero .gym-lluu { display: none; }
  .gram-gym > * { order: 3; }
  .gym-hero .gram-rubric,
  .gym-hero h2,
  .gym-hero .hint { order: 0; }
  .gram-gym > .gram-gym-controls { order: 1; }
  .gym-hero .hero-figures,
  .gym-hero .gym-progres { order: 2; }
  .gym-hero .hero-figures { margin-top: var(--space-3); }
  .gym-hero .gym-progres { padding: var(--space-4);
    border-radius: var(--radius-l); background: var(--glass-1);
    border: 1px solid var(--edge-soft); }
}

/* ---- El gimnàs · the trencadís progress panel (20 Jul 2026) ----
   A live map of the SELECTED tense, so «how much is left» never requires
   scrolling 35 cards. One tessel·la per cued cell in roster order — the panel
   is a mini-map, not a decoration, so a green cluster tells you WHICH stretch
   of the roster you own.
   Not mosaicSVG: that renders authored collectible defs (see gymProgress's
   comment). The kinship is carried here, in CSS, and nowhere in the data. */
.gym-progres { display: flex; flex-direction: column; align-items: center;
  gap: var(--space-3); min-width: 0; }
/* The plinth. The hero is already raised and nesting de-escalates, so the map
   sits in a sunk well — the same well la corba's curve sits in on Progrés. */
.gym-plint { padding: 18px; border-radius: var(--radius); background: var(--surface-sunk);
  border: 1px solid var(--edge-soft); box-shadow: inset 0 1px 2px rgba(42, 32, 25, 0.06); }
/* 15 columns × however many rows the roster needs. Fixed column COUNT, not a
   fixed panel width: the map's shape must stay stable as verbs are added, and
   a 6-wide row would falsely suggest the six persons are the columns.
   v2 takes the shard from 7px to 12px: in the head's leftover whitespace the
   map was texture, and in the hero it has room to be territory. */
.gym-progres-mosaic { display: grid; grid-template-columns: repeat(15, 12px);
  grid-auto-rows: 12px; gap: 3px; }
.gym-tessella { width: 12px; height: 12px; border-radius: 2.5px;
  background: var(--bar-track); }
/* Trencadís jitter — a fired-shard grid, never graph paper. Seeded by position
   via nth-child rather than by JS: deterministic, zero markup cost, and the
   pattern is coprime with 15 columns so it never lines up into stripes. */
.gym-tessella:nth-child(4n+1) { border-radius: 3.5px 2px 2px 3.5px; transform: rotate(-4deg); }
.gym-tessella:nth-child(7n+3) { border-radius: 2px 3.5px 2px 2px; transform: rotate(3.5deg); }
.gym-tessella:nth-child(11n+5) { border-radius: 2px 2px 3.5px 2px; transform: rotate(-2deg); }
.gym-tessella:nth-child(13n+2) { transform: rotate(5deg) scale(0.92); }
/* The three states, and the point of the recolour: FERM is a good thing.
   It used to be the heaviest mark on the surface, so a mastered form read as
   locked-off rather than earned. Green is the app's «done» hue everywhere
   else (--tile-green-ink marks a finished camí step), so mastery joins that
   family here too. */
.gym-tessella.is-ferm { background: var(--tile-green-viu); }
.gym-tessella.is-traçat { background: color-mix(in srgb, var(--tile-amber) 55%, var(--bar-track)); }
/* An un-met cell is an empty SOCKET, not a gap: on the sunk plinth --bar-track
   alone very nearly disappears, and a map with holes in it reads as a map that
   failed to draw. The hairline is what keeps 210 cells countable. */
.gym-tessella.is-fantasma {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--edge-soft) 60%, transparent); }
/* The reading sits at hero scale now: the figure in --text like every other
   hero numeral, its caption in the gold the rubric above it wears — the same
   pairing the Progrés hero's figures use, so the two rooms read alike. */
.gym-progres-lectura { display: flex; align-items: baseline; gap: 10px; }
.gym-progres-xifra { font-family: var(--display); font-weight: 600; font-size: 42px;
  line-height: 1; color: var(--text); font-variant-numeric: tabular-nums; }
.gym-progres-etiqueta { font-size: 11px; letter-spacing: 0.14em; font-weight: 600;
  text-transform: uppercase; color: var(--tile-amber-ink); }
.gym-progres-detall { display: flex; flex-wrap: wrap; justify-content: center;
  gap: 6px var(--space-4); margin: 0; font-size: 12.5px; }
.gym-progres-clau { display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap; }
/* The legend swatches are the same tessel·la, at the size the shape reads at
   inline; `i` carries no italic here, it is just the smallest neutral inline. */
.gym-progres-clau .gym-tessella { width: 9px; height: 9px; flex: 0 0 auto;
  border-radius: 2px; transform: rotate(-4deg); }
.gym-progres-clau .gym-tessella.is-fantasma { box-shadow: inset 0 0 0 1px var(--rim); }
/* ---- L'onada (el delit S3, 31 Jul 2026) ----
   A tense switch rebuilds the whole map, and it used to arrive fully recoloured
   in one frame — the one moment on the surface where something genuinely
   changes, and it snapped. Now a glint travels across it in roster order: the
   panel is a literal map, so a wave crossing it left-to-right says «new
   territory» before the percentage beneath it is read.
   It is a PASS-OVER and carries NO fill mode on purpose. Each tessel·la sits at
   its true colour before its delay and returns to it after, so the sweep can
   only ever add to a map that is already right — and on a throttled timeline
   (the hidden-tab hazard the laying ceremony paid for) nothing happens at all,
   which for a state readout is the correct failure. An entrance stagger would
   need `backwards` and would hold the map invisible in exactly that case.
   `scale`, never `transform`: the jitter above is transform geometry, and
   animating the shorthand would straighten every shard as the wave passed. */
.gym-progres-mosaic.is-onada .gym-tessella {
  animation: gym-onada var(--dur-2) var(--ease-glass) var(--onada, 0ms); }
/* 1.3 is a MEASURED ceiling, not a taste: at 12px on a 15px pitch the crest is
   15.6, so neighbours just kiss and part again. The first cut peaked at 1.55
   and a dense tense (present, 81 % tocat) merged into a moving slab — the
   trencadís read, which is the whole point of the panel, dissolved for a third
   of a second. Raise this only alongside the gap.
   The crest BRIGHTENS rather than dimming (v2): on the enlarged map the old
   opacity dip read as the wave rubbing the colour out of the very cells it was
   celebrating. `filter` and `scale` are separate properties on purpose — the
   jitter above is transform geometry, and animating the `transform` shorthand
   would straighten every shard as the wave passed. */
@keyframes gym-onada { 55% { scale: 1.3; filter: brightness(1.18) saturate(1.5); } }
@media (prefers-reduced-motion: reduce) {
  /* The jitter is static geometry, not motion — nothing to disable. Stated so
     the next reader does not go looking for a transition that isn't here.
     L'onada is belt-and-braces: gymProgress does not even mark the map when
     reducedMotion() is true, so this is the guard for a live OS toggle
     flipping between the tap and the paint. */
  .gym-tessella { transition: none; }
  .gym-progres-mosaic.is-onada .gym-tessella { animation: none; }
}
/* La gota rides this selector, so the chosen tab goes transparent and the chip
   carries its fill and its accent edge across (see the .gota block). The tab's
   own glass fades back in behind it at --dur-1, so the cell the chip is leaving
   fills as it goes rather than snapping. (Until 5 Aug 2026 the chip never
   actually stood here — see gymBlock for the stale `gota()` argument.)
   Every tab the SAME size, whatever its label is long enough to want: `flex: 1
   1 0` from a 104px floor, so «present» and «passat perifràstic» are one row of
   six at canvas width and wrap 3-up on a phone. A grid of three columns put the
   six tenses in two fixed rows and sized a tab by its column rather than by the
   row it shares. */
.gram-tense-selector { --gota-vora: var(--accent); --gota-radi: 12px;
  position: relative; display: flex; flex-wrap: wrap; gap: 8px; margin: 0; }
.gram-tense { position: relative; z-index: 1;
  flex: 1 1 0; min-width: 104px; min-height: 52px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; padding: 8px 10px; border: 1px solid var(--line);
  border-radius: 12px; background: var(--surface);
  color: var(--muted); font: inherit; cursor: pointer;
  transition: color var(--dur-1) var(--ease-glass),
    border-color var(--dur-1) var(--ease-glass),
    background-color var(--dur-1) var(--ease-glass); }
.gram-tense-nom { font-size: 14px; font-weight: 600; line-height: 1.15; text-align: center; }
/* The tab's own coverage. It is the tabs' one piece of new information and the
   reason they had to grow a second line: choosing a tense used to mean picking
   blind and reading the map afterwards. Same figure as the hero's headline —
   `pctTocat`, never a mastery blend (see tenseCoverage).
   The handoff dims it to 0.8 opacity; it writes in full --muted instead.
   Measured, that dim lands at 3.68:1 on --surface against a 4.5 bar, and this
   is a 10.5px numeral somebody reads. The hierarchy it was buying is already
   carried by size and hue. */
.gram-tense-pct { font-size: 10.5px; font-weight: 700; letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums; }
.gram-tense:hover { color: var(--text); border-color: var(--rim); }
.gram-tense.active { color: var(--text); border-color: var(--accent);
  background: color-mix(in srgb, var(--accent-soft) 68%, var(--surface)); }
.gram-tense.active .gram-tense-pct { color: var(--accent-ink); opacity: 1; }
/* The chosen tab's warm cast. It rides the CHIP rather than the tab, because
   the tab goes transparent the moment la gota is standing and a shadow under a
   transparent box is a shadow with nothing casting it. */
.gram-tense-selector .gota { box-shadow: 0 6px 14px -8px
  color-mix(in srgb, var(--accent) 50%, transparent); }
/* …and hands both over to the chip the moment the chip is actually standing.
   The `.active` rule above is the static twin, so a tab still reads as chosen
   in the window before la gota is placed — or if it never is. The hand-over is
   INSTANT (fill and edge drop out of the transition): the chip lands in that
   same frame, and easing the twin out would be a second fill fading where one
   is arriving. Only the ink still eases. A tab LOSING the chip keeps the base
   transition, so its glass floats back in behind the chip. */
.gota.is-viva ~ .gram-tense.active { border-color: transparent; background: transparent;
  transition: color var(--dur-1) var(--ease-glass); }
@media (prefers-reduced-motion: reduce) {
  .gram-tense { transition: none; }
}
/* The three doors are one flex row that WRAPS: the tense round takes the wide
   basis (280px) so it leads the line, and the two cross-tense peers share what
   is left evenly — they stack under it on a phone and sit beside it on the
   canvas. It replaced a 2-column grid with the primary spanning both, which
   could only ever be that one shape.
   The peers are glazed DOORS (.gym-porta), not .btn-ghost: an underlined muted
   label inside a 48px cell read as plain text that happened to be clickable —
   raised = actionable, so a live round door wears the esmaltat glass like every
   other control. Each carries its own quiet meta line (count + what the round
   is), which is what retired the .gram-gym-nota footnote under the row. */
.gram-gym-controls { display: flex; flex-direction: column; gap: var(--space-3); }
/* The tense's own rule of thumb, straight from verbs.json — the one line of
   teaching on a surface that is otherwise all practice, so it is set as prose
   and not as a chip. */
.gym-why { margin: 0; font-size: 13px; font-style: italic; color: var(--muted);
  text-wrap: pretty; }
.gram-gym-modes { display: flex; flex-wrap: wrap; gap: 10px; }
.gram-gym-modes .gram-round-main { flex: 1 1 280px; min-height: 56px;
  justify-content: center; font-size: 15.5px; }
.gram-gym-modes .btn { margin: 0; }
.gram-gym-modes .gym-porta { flex: 1 1 200px; }
.gym-porta { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 2px; min-height: 56px; padding: 9px 12px;
  background: var(--glass-1); border: 1px solid var(--edge-soft);
  box-shadow: inset 0 1px 0 var(--edge), 0 3px 10px -4px rgba(0, 0, 0, 0.12); }
.gym-porta strong { font-size: 15px; line-height: 1.2; }
.gym-porta span { font-size: 12px; color: var(--muted); line-height: 1.2; }
.gym-porta:hover:not(:disabled) {
  border-color: color-mix(in srgb, var(--accent) 42%, var(--edge-soft));
  box-shadow: inset 0 1px 0 var(--edge), 0 4px 12px -5px rgba(0, 0, 0, 0.18); }
.gym-porta:hover:not(:disabled) strong { color: var(--accent-ink); }
/* The grout rule between the controls and the roster: one hairline with a
   single ceramic node, the system ornament (mica-sistema §13). It is the pane's
   only divider — the three surfaces are otherwise parted by air. */
.gram-gym > .grout-rule { margin: var(--space-1) 0; }
.gram-roster-head { display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; margin: 0; }
.gram-roster-head h3 { margin: 0; font-family: var(--display); font-weight: 600;
  font-size: 27px; line-height: 1.1; }
.gram-roster-head .footnote { text-align: right; }
/* The roster sits between its head and its footnote with the stack's own gap,
   so the section reads as one block without a wrapper element. */
.gram-gym-roster { display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); }
.gram-gym > .gym-nota { margin: 0; text-align: center; }
/* The verb card, v2: a --surface card on the page ground, not the unit page's
   ink-on-paper sheet. Those two look different on purpose — the bank inside a
   fitxa belongs to the reading surface it sits in, and this one belongs to the
   gym. */
.gram-gym-roster .gram-grid-card { min-width: 0; width: auto; flex: none; padding: 16px;
  border-radius: var(--radius); border: 1px solid var(--edge-soft);
  background: var(--surface);
  box-shadow: inset 0 1px 0 var(--edge), 0 1px 2px rgba(42, 32, 25, 0.05);
  transition: transform var(--dur-1) var(--ease-glass),
    box-shadow var(--dur-1) var(--ease-glass); }
.gram-gym-roster .gram-grid-card:hover { transform: translateY(-2px);
  box-shadow: inset 0 1px 0 var(--edge), 0 14px 30px -14px rgba(42, 32, 25, 0.28); }
.gram-gym-roster .gram-grid-title { gap: 10px; margin-bottom: 8px; }
.gram-gym-roster .gram-grid-title strong { font-size: 24px; }
.gram-gym-roster .gram-grid-title > span:not(.unit-chip):not(.gram-grid-tally) { display: none; }
.gram-gym-roster .gram-grid-card > .btn { width: 100%; margin-top: 10px;
  padding: 9px; font-size: 13.5px; justify-content: center; }
/* The paradigm cell at roster scale: the person label shrinks to a cap so the
   FORM is what the eye lands on. Scoped to the roster — the unit page's bank
   keeps its own proportions. */
.gram-gym-roster .gram-grid { gap: 6px; }
.gram-gym-roster .gram-cell { min-height: 54px; padding: 6px 4px; border-radius: 10px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; }
/* 10px at full --muted, not the handoff's 9.5px at 0.75 opacity: the type floor
   puts 10px at the very bottom of the scale, and the dim would take a label the
   learner actually reads (which person is this?) to ~3.3:1. */
.gram-gym-roster .gram-cell small { font-size: 10px; letter-spacing: 0.07em;
  text-transform: uppercase; white-space: nowrap; }
.gram-gym-roster .gram-cell-answer { margin: 0; font-size: 15px; line-height: 1.15;
  font-weight: 500; text-align: center; }
.gram-gym-roster .gram-cell.state-traçat .gram-cell-answer { font-weight: 600; }
.gram-gym-roster .gram-cell.state-ferm .gram-cell-answer { font-weight: 700; }
/* Fantasma reads as an empty socket rather than a faint form: sunk ground,
   dashed edge. Dashed and not dotted because the peek is a door — the cell is
   the one state on the card you are invited to tap. */
.gram-gym-roster .gram-cell.state-fantasma { background: var(--surface-sunk);
  border: 1px dashed color-mix(in srgb, var(--muted) 45%, transparent);
  color: var(--muted); }
/* Consulted, not earned — see gridCell's note. The italic is the whole signal,
   and it survives reduced motion and dark theme because it is type, not paint. */
.gram-cell.is-ullada .gram-cell-answer { font-style: italic; }
/* ---- The entrance, and its settle sweep ----
   The three surfaces rise in order. It fills BACKWARDS, so each sits at its
   `from` state until its animation runs — which in a hidden or backgrounded tab
   it never does — and gymBlock drops `.is-entrant` on a timer whether or not a
   frame was served (the mosaic's rule; Progrés and la paret pay for it too).
   A camí↔gimnàs↔camí walk is a RETURN, not an arrival: curs.js already marks
   that on the pane host, and the camí's own wash uses the same signal. */
.gram-gym.is-entrant > .gym-hero,
.gram-gym.is-entrant > .gram-gym-controls,
.gram-gym.is-entrant > .gram-roster-head,
.gram-gym.is-entrant > .gram-gym-roster,
.gram-gym.is-entrant > .gym-nota { animation: gym-puja var(--dur-3) var(--ease-glass) backwards; }
.gram-gym.is-entrant > .gram-gym-controls { animation-delay: 0.08s; }
.gram-gym.is-entrant > .gram-roster-head,
.gram-gym.is-entrant > .gram-gym-roster,
.gram-gym.is-entrant > .gym-nota { animation-delay: 0.14s; }
@keyframes gym-puja { from { opacity: 0; transform: translateY(12px); } }
.curs-pane.is-retorn .gram-gym.is-entrant > * { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .gram-gym.is-entrant > * { animation: none !important; }
  .gram-gym-roster .gram-grid-card { transition: none; }
  .gram-gym-roster .gram-grid-card:hover { transform: none; }
}
.gram-units { margin-top: 34px; padding-top: 20px; border-top: 1px solid var(--bar-track); }
.gram-gym-session { display: grid; gap: 12px; }
.gram-gym-live { position: sticky; top: 4px; z-index: var(--z-sticky); display: flex; gap: 8px;
  padding: 4px 0 8px; overflow-x: auto; scroll-snap-type: x mandatory; }
.gram-gym-live .gram-grid-card { flex: 0 0 min(82vw, 360px); scroll-snap-align: start;
  padding: 10px; box-shadow: var(--shadow); }
.gram-gym-live .gram-grid { grid-template-columns: repeat(6, minmax(42px, 1fr)); }
.gram-gym-live .gram-cell { min-height: 44px; padding: 4px 2px; }
.gram-gym-live .gram-cell-answer { font-size: 11px; overflow-wrap: anywhere; }
/* R5 — the gym as a browse surface at ≥900 (docs/DESKTOP-UX-PLAN.md §2c).
   The tense/mode controls stick under the scroll on --z-sticky (no transformed
   ancestor). The live coverage deck shows 2–3 paradigm cards abreast while the
   player column stays --w-focus. Same specificity as the base rules; inert
   below 900 — and deliberately so for the sticky bar: v2's bar is six tabs, a
   why line and three doors, which on a 667px phone would stand permanently
   across some 300px of it. The handoff asks for sticky everywhere; below 900 it
   stays the plain block it has always been. */
@media (min-width: 900px) {
  /* A floating vidre bar (level 3 — the view's only one, inside the ≤3 budget).
     It used to stick flush at top:0 as an opaque colour patch bleeding past its
     own column, which read as a stray overlay rather than a control that floats
     over the roster. Now: 10px of breathing room, translucent + blurred so the
     verbs visibly pass under it, and no negative-margin bleed so its edges line
     up with the roster columns it floats over. No new motion → no
     reduced-motion rule. It has no fixed descendants, so the containing block
     backdrop-filter creates is harmless.
     The bar is evenly padded on all four sides: 4px at the bottom used to leave
     «Ronda del temps» flush on its own edge, so the mode buttons read as if
     they had fallen out of it (20 Jul 2026). */
  .gram-gym-controls { position: sticky; top: 10px; z-index: var(--z-sticky);
    margin: 0; padding: 14px 16px;
    background: color-mix(in srgb, var(--bg) 80%, transparent);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
    border: 1px solid var(--edge-soft);
    box-shadow: inset 0 1px 0 var(--edge), var(--glaze);
    border-radius: 18px; }
  /* The opaque fallback the elevation law requires wherever backdrop-filter is
     used: the roster scrolls under this bar, so without the blur it must be a
     solid ground, not a tint. */
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .gram-gym-controls { background: var(--bg); }
  }
  .gram-gym-live .gram-grid-card { flex: 0 0 300px; }
  .gram-gym-player { max-width: var(--w-focus); margin-inline: auto; width: 100%; }
  /* Gimnàs on the full canvas (1120): the roster spreads so 35 verbs stop being
     an endless 2-up scroll, while the intro copy, tense/mode controls and roster
     header stay in a readable browse-width column so they don't stretch across
     the whole shell (18 Jul 2026).
     The floor was 250px, which gave 4 columns and ~71px cells — narrower than a
     first-person-plural form, so a card whose fantasmes had all been peeked
     spilled its text (20 Jul 2026). 320px gives 3 columns at ~365px, i.e. ~109px
     cells, which fits the longest forms in the bank (començaríeu) with room. The
     binding constraint is CELL width, not card count: if this floor is ever
     lowered again, check the longest conditional form still fits. */
  .lay-canvas .gram-gym-roster {
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
  /* Every surface in the pane now shares one pair of edges — the hero, the bar
     and the roster are one stack, so nothing is capped to browse width any
     more. The old `.lay-canvas .gram-gym-head` cap existed because the intro
     copy sat in a card WITH the map beside it; in the hero the copy is its own
     column, and the auto-fit grid is what keeps it readable. */
}
.gram-missing { padding: 18px 0; }
.gram-phase { margin-top: 18px; }
.gram-phase.locked .gram-phase-head { opacity: 0.55; }
.gram-phase-head { display: flex; gap: 10px; align-items: flex-start; }
.gram-phase-head h3 { margin: 0; font-family: var(--display); font-size: 18px; }
.gram-phase-head p { margin: 0; }
.gram-unit-row { display: flex; align-items: center; gap: 12px; padding: 13px 15px;
  margin: 8px 0 0 34px; color: inherit; text-decoration: none;
  background: var(--glass-2); border: 1px solid var(--edge-soft); border-radius: 14px;
  box-shadow: var(--shadow);
  transition: border-color var(--dur-1) var(--ease-glass); }
.gram-unit-row:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--edge-soft)); }
@media (prefers-reduced-motion: reduce) { .gram-unit-row { transition: none; } }
.gram-unit-copy { flex: 1; min-width: 0; }
.gram-unit-copy p { margin: 2px 0 6px; }
.gram-vocab-link { display: flex; justify-content: space-between; align-items: center;
  margin-top: 24px; padding: 14px 2px; border-top: 1px dashed var(--bar-track);
  color: var(--accent); text-decoration: none; }
.gram-vocab-link span { color: var(--muted); font-size: 14px; }
/* The Progrés → Galeria descens card (.galeria-card*) was deleted with the v2
   redesign, 5 Aug 2026: inside the hero's dark case a second raised card beside
   the piece read as a competing object, and the door is now one gold link on
   the caption's own column (.hero-galeria). La Galeria itself is unchanged. */
/* The session-header medallion is a door to la Galeria — keep the slot rigid. */
.medallion-link { display: flex; text-decoration: none; color: inherit; }

/* ---- Curs · the CPNL ladder ---- */
/* THE LLOC'S HEAD (design/handoffs/titols-v2, 6 Aug 2026).
   Avui, Pràctica and Progrés already open with a display title over an
   overline; el curs and el món now do too, so the five llocs open the same
   way. The head is ONE flex row — title stack left, the shared subnav standing
   at its right on the title's own baseline — which wraps to two rows on a
   phone, where the subnav keeps the full width and its three even targets.

   THE SUBNAV'S SLOT IS SHARED, so `cursCap` is mounted by el barri too (see
   curs.js): the standing rule is that the three choices never MOVE between the
   surfaces that carry them, and re-slotting the control here alone would make
   it jump the moment you tab across. THE TITLE TREATMENT IS SHARED TOO
   (6 Aug 2026, Ryan's ask): every caller hands an h1.curs-titol plus the
   derived overline, because the subnav stands on the copy stack's baseline —
   el barri's plain title with no rubric left the pills ~50px higher, so
   tabbing across made them jump vertically. What is still NOT shared is the
   control's own styling (the gimnàs v2 ruling — nothing here restyles it).

   The subnav's own margin is dropped here: the row's gap owns the rhythm, and
   its old `margin: 14px 0 6px` was written for a stack.

   THE PILLS STAND ON THE TITLE, NEVER ON THE HINT (6 Aug 2026). The head is a
   grid at every width and `display: contents` lifts the copy stack's children
   into it, because the subnav's position must depend ONLY on the rubric and
   the title — which every caller now renders identically — and never on the
   hint's line count: el barri's hint wraps where el curs's does not, and with
   the subnav standing after the whole copy stack the three choices jumped
   ~25px on every tab across (D3's «the three choices never move», on the
   vertical axis). Below 900 the rows run rubric · title · subnav · hint, the
   subnav at full width with its three even targets; at ≥900 (further down)
   the subnav moves to a second column spanning rubric+title, bottom-aligned
   with the title, and the hint keeps column 1. */
.curs-cap { display: grid; grid-template-columns: minmax(0, 1fr);
  row-gap: 8px; margin: 14px 0 0; }
.curs-cap-copia { display: contents; }
/* Both beat `.dashboard h1`'s `margin: 14px 0 4px` on source order alone —
   they sit later in the file and at equal specificity. */
.curs-cap-copia h1 { margin: 0; grid-row: 2; }
.curs-cap-rubric { grid-row: 1; margin: 0; font-size: 11px; letter-spacing: 0.16em;
  text-transform: uppercase; font-weight: 600; color: var(--accent-ink); }
/* Two classes so the display size beats `.dashboard h1`'s 1.5em pin without
   relying on source order (the prac-titol precedent). Scoped to the cap, not
   to `.dashboard.curs`, since el barri mounts the same head (6 Aug 2026) —
   the treatment travels with the cap, wherever it stands. */
.curs-cap-copia .curs-titol { font-family: var(--display); font-weight: 700;
  font-size: clamp(38px, 5vw, 54px); line-height: 1.03; letter-spacing: 0.005em; }
.curs-cap-linia { grid-row: 4; margin: 0; font-size: 15.5px; color: var(--muted);
  max-width: 52ch; text-wrap: pretty; }
/* Below 900 the subnav takes its own full-width row, directly under the
   title, and keeps the three even targets it has always had; at ≥900 it is
   auto-width and pushed right (further down, with the rest of the desktop
   tier). */
.curs-cap > .curs-segments { grid-row: 3; margin: 0; }
/* The entrance fills BACKWARDS; curs.js drops `is-entrant` on a settle timer
   whether or not a frame was ever served (the mosaic's rule — the camí pane
   below already pays for the same one). */
.curs-cap.is-entrant { animation: cami-puja var(--dur-3) var(--ease-glass) backwards; }
@media (prefers-reduced-motion: reduce) {
  .curs-cap.is-entrant { animation: none !important; }
}

/* Subnav of one lloc (D2; D1 18 Jul 2026): «El camí | El barri | El gimnàs».
   Three 44px choices that fit 320px without wrapping; mirrors the .gram-tense
   selector idiom. El barri is an <a> (routed peer), so the shared rule centres
   content and strips the underline for both elements. */
.curs-segments { --gota-vora: var(--accent); position: relative; display: grid;
  grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 14px 0 6px; }
/* THE BOLD WIDTH IS RESERVED (14 d'agost 2026). `.active` writes at 700 and the
   three columns are 1fr of a content-sized, right-aligned grid, so the selected
   label sized all three and the whole control shifted 14px sideways on every
   tap — measured at 1280, and the same jitter on Ryan's Mica and the demo. The
   label rides `.curs-seg-mot` and a hidden `::after` twin carries it again at
   700 in the SAME grid cell, so a segment is always as wide as its own bold
   rendering. A pseudo-element leaves the accessible name alone (the word is
   still in the DOM once, the tabbar's `.tab-mot` rule), and one cell for both
   keeps the row 44px rather than stacking them. See cursSubnav's `seg`. */
.curs-seg { position: relative; z-index: 1;
  min-height: 44px; padding: 8px 6px; border: 1px solid var(--bar-track);
  border-radius: 10px; background: color-mix(in srgb, var(--surface) 78%, transparent);
  color: var(--muted); font: inherit; font-size: 15px; cursor: pointer;
  display: grid; grid-template-columns: auto; align-items: center; justify-items: center;
  text-decoration: none; text-align: center; white-space: nowrap;
  transition: color var(--dur-1) var(--ease-glass),
    border-color var(--dur-1) var(--ease-glass),
    background-color var(--dur-1) var(--ease-glass); }
.curs-seg-mot { grid-area: 1 / 1; }
.curs-seg::after { content: attr(data-mot); grid-area: 1 / 1;
  font-weight: 700; visibility: hidden; pointer-events: none; }
.curs-seg:hover { color: var(--text); border-color: var(--rim); }
.curs-seg.active { color: var(--text); border-color: var(--accent);
  background: color-mix(in srgb, var(--accent-soft) 68%, var(--surface)); font-weight: 700; }
/* The fill and the edge move to la gota once it is standing; the rule above is
   the static twin, and the hand-over is instant for the reason given on
   .gram-tense. */
.gota.is-viva ~ .curs-seg.active { border-color: transparent; background: transparent;
  transition: color var(--dur-1) var(--ease-glass); }
@media (prefers-reduced-motion: reduce) {
  .curs-seg { transition: none; }
}

/* ---- El camí v2 (design/handoffs/cami-v2, 5 Aug 2026) ----
   The same ladder, re-staged as a ROUTE: an «on ets» hero carrying the current
   unit's own name plus the whole path as a minimap, a rail of CEFR roundels
   with a spine drawn between them, ceramic chips on the rows, and the app's
   motto closing the page. Everything the ladder SAYS is untouched — the states,
   the one ▷, the posture ladder, the locks and the fold are the code they were.
   Five places the handoff was deliberately not followed, each recorded where it
   bites: the vidre hero (see .curs-hero), the fold's pill button (see the fold
   affordance below), law 1 on the minimap's filled tiles and the locked tile's
   opacity (see .cami-fita), and the subnav redraw, which is el barri's and el
   gimnàs's navigation too — the gimnàs v2 ruling, five days old: a handoff that
   looked at one surface may not restyle three. */

/* The pane's entrance: zones rise in order behind `.is-entrant`, filling
   BACKWARDS, and camiPane drops the class on a settle timer whether or not a
   frame was ever served (the mosaic's rule; Progrés, la paret and el gimnàs all
   pay for it). A camí↔gimnàs↔camí walk is a RETURN — the pane host carries
   `is-retorn` for the progress washes already, and the entrance obeys it too. */
.curs-cami.is-entrant > * { animation: cami-puja var(--dur-3) var(--ease-glass) backwards; }
.curs-cami.is-entrant > .grout-rule { animation-delay: 0.06s; }
.curs-cami.is-entrant > .curs-spine { animation-delay: 0.1s; }
.curs-cami.is-entrant > .cami-lema { animation-delay: 0.16s; }
@keyframes cami-puja { from { opacity: 0; transform: translateY(12px); } }
.curs-pane.is-retorn .curs-cami.is-entrant > * { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .curs-cami.is-entrant > * { animation: none !important; }
}

/* ---- Zone 1 · the «on ets» hero ----
   The same ALÇAT card Progrés's and el gimnàs's heroes wear — --glass-1 on
   --edge-soft under --shadow-2, radius 22, the system --sheen blended over it —
   and NOT the handoff's `backdrop-filter: blur(16px) saturate(1.2)` vidre. The
   three rooms must open the same way, and the two sibling redesigns landed on
   this recipe in the same week; on top of that there is nothing behind this
   card to blur (the pane sits on a flat --bg), so the filter would buy a
   compositing layer and a Safari raster — la paret's lesson — for no visible
   difference. Soft-light on the sheen for the reason measured on the Progrés
   hero: straight alpha over a surface this size lifts the ground enough to push
   --muted ink under AA. */
.curs-hero { position: relative; overflow: hidden; isolation: isolate;
  margin: 14px 0 0; border-radius: var(--radius-l); padding: clamp(24px, 3.5vw, 42px);
  border: 1px solid var(--edge-soft); background: var(--glass-1);
  box-shadow: var(--shadow-2); }
.curs-hero .hero-lluu { position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; background: var(--sheen); mix-blend-mode: soft-light; }
.curs-hero .hero-identitat { gap: 14px; }
/* «On ets · Unitat 12 · Elemental B1» — the three facts that place the step,
   composed in one line because none of them means anything alone. */
.curs-hero-rubric { margin: 0; font-size: 11px; letter-spacing: 0.16em;
  text-transform: uppercase; font-weight: 600; color: var(--accent-ink); }
.curs-hero-rubric.is-quiet { color: var(--muted); }
/* The unit's own name at display scale: the pane's h1 is «El curs», so this is
   an h2 and is capped well under it (the gimnàs v2 rule — a lead dwarfing the
   page title inverts the hierarchy instead of expressing it). The big-title
   pass (6 Aug 2026) stepped it down from 32–50 to 30–42: the head's h1 now runs
   to 54px, and at the old cap the two were within four pixels of each other at
   every width — one hierarchy written twice at the same size is no hierarchy. */
.curs-hero-titol { margin: 0; font-family: var(--display); font-style: italic;
  font-weight: 700; font-size: clamp(30px, 3.8vw, 42px); line-height: 1.02;
  letter-spacing: 0.01em; color: var(--text); text-wrap: balance; }
/* Read, not acted on, so it stays a bare line — no card, no glaze (raised =
   actionable; la frase del dia and the Progrés volume epigraph, same rule). */
.curs-hero-epigraf { max-width: 44ch; margin: 0; font-family: var(--display);
  font-style: italic; font-size: 19px; line-height: 1.45; color: var(--muted); }
/* The study flow as tessel·les. The reading beside them is the accessible
   carrier — the tiles are aria-hidden, because a screen reader counting eight
   anonymous shards learns nothing «pas 6 de 8» has not already said. */
.curs-hero-passos { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.curs-passes { display: inline-flex; gap: 5px; }
/* `--passa` is the tile's size, 11px wherever it is a marginal mark (here and
   on the porta's fil) and larger where it is the block's subject — the unit
   page's recorregut sets 18px. The tile itself never changes. */
.curs-passa { width: var(--passa, 11px); height: var(--passa, 11px);
  border-radius: 3px; box-sizing: border-box;
  transform: rotate(var(--tilt, 0deg)); background: var(--bar-track); }
.curs-passa.is-fet { background: var(--accent); }
/* The one you are on: outlined, not filled — a static twin the pulse rides on
   top of, so the mark survives reduced motion and a frozen timeline intact. */
.curs-passa.is-aqui { background: var(--surface); border: 1.5px solid var(--accent);
  animation: cami-batec 2.4s ease-in-out infinite; }
.curs-passes-lectura { font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
/* «You are here», and it appears on exactly two marks in the whole pane: this
   step tile and the minimap tile for the same unit. That scarcity is what makes
   it a signature rather than decoration. */
@keyframes cami-batec {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 34%, transparent); }
  55% { box-shadow: 0 0 0 7px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .curs-passa.is-aqui, .cami-fita.is-aqui { animation: none; }
}

/* ---- The minimap · «el camí sencer» ----
   One tile per unit, in ladder order, one row per level. It is the only place
   the whole route is visible at once, which is the thing a stack of bands can
   never show. One role="img" with a summary label: seventeen unlabelled shards
   walked one by one is noise, and every tile's unit is one real row away. */
.cami-sencer { display: flex; flex-direction: column; align-items: center;
  gap: 14px; min-width: 0; }
/* The hero is already raised and nesting de-escalates, so the panel is a sunk
   well — the same well la corba's curve and the gimnàs's map sit in. */
.cami-mapa { display: flex; flex-direction: column; gap: 8px; max-width: 100%;
  padding: 16px 18px; border-radius: var(--radius); background: var(--surface-sunk);
  border: 1px solid var(--edge-soft); box-shadow: inset 0 1px 2px rgba(42, 32, 25, 0.06); }
/* A level's row must hold its longest spine — nine units plus the code and the
   lozenge — on ONE line, or the wrap orphans the tail of a level under the next
   level's code and the map stops being a map. So the tile scales with the
   viewport: ~19px on a 375px phone, 22px from ~440px up. */
.cami-nivell { display: flex; align-items: center; gap: clamp(4px, 1.2vw, 7px);
  flex-wrap: wrap; --cami-fita: clamp(18px, 5vw, 22px); }
.cami-nivell-codi { flex: none; width: 22px; font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.06em; color: var(--muted); }
.cami-nivell-codi.is-actual { color: var(--accent-ink); }
.cami-nivell-codi.is-superat { color: var(--daurat-ink); }
/* 22px, not the handoff's 19px, and the numeral inside it is 11px rather than
   10px: the ladder position is something the learner READS, so it belongs at
   the type floor, not in the 10px decorative band. */
.cami-fita { flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: var(--cami-fita, 22px); height: var(--cami-fita, 22px);
  border-radius: 5px; box-sizing: border-box;
  transform: rotate(var(--tilt, 0deg));
  background: var(--bar-track); color: var(--muted);
  font-size: 11px; font-weight: 600; font-style: normal;
  font-variant-numeric: tabular-nums; }
/* WHITE ONLY EVER RIDES A -fill (law 1). The handoff paints white numerals on
   raw --tile-green and raw --accent, which passes in light and fails in dark
   where both hues lift; the -fill variants exist for exactly this and carry
   white in both themes. */
.cami-fita.is-fet { background: var(--tile-green-fill); color: #fff; }
.cami-fita.is-aqui { background: var(--accent-fill); color: #fff; font-weight: 700;
  animation: cami-batec 2.4s ease-in-out infinite; }
/* Under way but not the front: the accent family at a lower weight, which is
   the ladder's own answer one surface down — ◐ and ▷ share --accent-ink and
   differ only in what the row around them does. The handoff's amber-over-track
   mix cannot carry a numeral in dark (measured ~2.9:1 against --text), and a
   third hue for a state the ladder already calls accent would be a new word. */
.cami-fita.is-encetada { background: var(--accent-soft); color: var(--accent-ink); }
/* --surface, not the handoff's --tile-cream: cream is a light glaze in BOTH
   themes, so a --muted numeral on it is unreadable in dark. --surface flips
   with the theme, keeps the ink legible either way, and — unlike --surface-sunk
   — is not the panel's own fill, so an open tile is still a tile. */
.cami-fita.is-oberta { background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--edge-soft); }
/* Unauthored: a dashed socket, never a padlock and never a tick (the D3
   invariant at tile size — a `properament` unit is pending text, not a gate). */
.cami-fita.is-propera { background: transparent; border: 1px dashed var(--rim); }
/* Locked fades in the FILL, never in opacity: the handoff dims the whole tile
   to .55/.4, which takes the numeral down with it — measured 2.1:1 on 11px
   text, and the ladder position is something the learner reads. The fill alone
   carries «not yet», and the ink stays at full --muted.
   Beyond the next band the route fades further — the posture ladder's «futura»
   at tile grain, derived from POSITION, never authored. */
.cami-fita.is-tancada { background: color-mix(in srgb, var(--bar-track) 60%, transparent); }
.cami-fita.is-lluny { background: color-mix(in srgb, var(--bar-track) 25%, transparent); }
/* The exam at a level's end: the gold lozenge that belongs to l'obra daurada,
   filled with a check once the level is passed and an empty outline until then.
   The check counter-rotates so it reads upright inside the diamond. */
.cami-llosange { flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; margin: 0 2px; box-sizing: border-box; border-radius: 3px;
  background: transparent; border: 1.5px solid var(--daurat); transform: rotate(45deg); }
/* THE FILLED LOZENGE IS A SEAL, AND A SEAL CARRIES WHITE — so it is the gold
   mixed toward black, the --rose-fill idiom, not the raw hue. --daurat is a
   LIGHT hue in both themes (measured 2.8:1 against white in light, 1.7:1 in
   dark), which is exactly what a -fill exists to fix; there is no --daurat-fill
   in the token layer yet, and one belongs there the day a second surface needs
   it. Mixed at 58 % the check measures 6.9:1 light and 4.8:1 dark. */
.cami-llosange.is-superat { background: color-mix(in srgb, var(--daurat) 58%, #000);
  border-color: color-mix(in srgb, var(--daurat) 58%, #000); }
.cami-llosange > span { display: inline-flex; transform: rotate(-45deg); color: #fff; }
.cami-recompte { margin: 0; font-size: 12.5px; color: var(--muted); text-align: center; }
.cami-recompte-fet { color: var(--tile-green-ink); font-weight: 600; }

/* ---- The closing motto ----
   Shared with la porta (v2, 5 Aug 2026) and the Pràctica hub (v2, same day):
   one rendering — a line of italic display type between two grout rules — so
   every room that closes on a phrase closes the same way. ONE MOTTO, ONE
   RENDERING: all four readers take `S.curs.lema`, «de mica en mica s'omple la
   pica». The hub said a phrase of its own («un racó cada dia») until 16 Aug
   2026 — Ryan's call, and his word for it was «silly»: it stood beside three
   rooms saying the proverb, and a surface-specific motto is one more copy
   waiting to drift. Do not give one of these rooms its own phrase again.
   Vocabulari joins the selector rather than restating it: it carried a
   byte-identical copy of these three declarations, which is exactly the drift
   this comment warns about. Its 6px top margin stays as a per-surface override
   below, the way la porta's already did. */
.cami-lema, .porta-lema, .prac-lema, .voc-lema { display: flex; align-items: center;
  gap: 14px; margin-top: 26px; }
.cami-lema > .grout-rule, .porta-lema > .grout-rule,
.prac-lema > .grout-rule, .voc-lema > .grout-rule { flex: 1; }
.cami-lema p, .porta-lema p, .prac-lema p, .voc-lema p { margin: 0;
  font-family: var(--display); font-style: italic; font-size: 16px;
  color: var(--muted); }

/* ---- Zone 2 · the rail ----
   The CEFR badge left the head for a gutter of its own, with a 2px spine drawn
   between one roundel and the next: that is what turns a stack of bands into a
   route with stations on it. The badge is still a child of .curs-level-head in
   the DOM (one element per level, still named and counted when the band is
   folded) — only the stylesheet moves it, so nothing about the fold, the
   posture ladder or a screen reader's reading order changes.
   The bands carry margin-left rather than the spine carrying padding, so the
   actual band's card can end at the gutter instead of swallowing it.

   The gutter is a clamp on the VIEWPORT, not a breakpoint: on a 375px phone the
   rail costs 32px of a row that is already the tightest thing on the surface
   (the two-line clamp exists because titles were wrapping there), and it opens
   out to 58px where there is room for it. Same rule, one expression. */
.curs-spine { --cami-rail: clamp(32px, 6vw, 58px);
  --cami-roundel: clamp(28px, 5vw, 46px); --cami-gap: 20px;
  --cami-card-pad: clamp(14px, 2.4vw, 24px); }
.curs-level { position: relative; margin-top: var(--cami-gap); margin-left: var(--cami-rail); }
/* The segment from this roundel down to the next one. `bottom` reaches exactly
   one band gap past the box, which is where the next roundel's top edge is —
   so the line is continuous without any element knowing about its neighbour. */
.curs-level::before { content: ""; position: absolute; width: 2px; border-radius: 2px;
  left: calc(-1 * var(--cami-rail) / 2 - 1px);
  top: calc(var(--cami-roundel) + 8px); bottom: calc(-1 * var(--cami-gap));
  background: repeating-linear-gradient(180deg,
    var(--bar-track) 0 6px, transparent 6px 13px); }
/* A walked level's segment is solid and hands over to the accent; the band you
   are in hands over to the track — the route ahead is drawn, never promised. */
.curs-level.is-passada::before, .curs-level.is-superat::before {
  background: linear-gradient(var(--tile-green),
    color-mix(in srgb, var(--tile-green) 40%, var(--accent))); }
.curs-level.is-actual::before {
  background: linear-gradient(color-mix(in srgb, var(--accent) 70%, var(--edge-soft)),
    var(--bar-track)); }
/* …and it has to reach the NEXT roundel, which on the actual band has dropped
   by the card's own padding. `:has()` is the honest way to say «the station
   below me stands on a card»: no element knows its neighbour otherwise, and
   where the selector is unsupported the rule is simply dropped and the line
   falls a padding short — a shortfall, never a break. */
.curs-level:has(+ .curs-level.is-actual)::before {
  bottom: calc(-1 * var(--cami-gap) - var(--cami-card-pad)); }
/* Nothing below the last station, so no line dangling off the end of the page. */
.curs-level:last-child::before { display: none; }
/* El cap d'etapa (S1, «el camí per etapes», 31 Jul 2026): the level's landmark
   — CEFR badge · name · derived meta line — over a rule spanning the band, so
   the boundary between two levels is drawn, not implied by a heading floating
   in a stream of identical cards. flex-wrap lets the meta drop to its own
   right-aligned line on a narrow phone instead of crushing the name. */
.curs-level-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
  min-height: var(--cami-roundel); padding-bottom: 10px;
  border-bottom: 1px solid var(--edge-soft); }
/* h2 since the A5 heading repair (h1 «El curs» → level heads); v2 gives it the
   station's own weight — 25px display, the scale the rail's roundel is drawn
   for — while staying under the hero's lead. */
.curs-level-head h2 { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(21px, 2.4vw, 25px); line-height: 1.1; }
/* The badge is what makes «A» and «B» territory legible at a glance, and since
   v2 it is the RAIL's roundel: a circle standing in the gutter with the spine
   running out of its foot. Absolutely placed, so the head's DOM order (badge ·
   name · meta) is untouched and a folded band still wears it. Its ink follows
   the level's standing, and it is INK tokens all the way down — display letters
   in a raw tile hue is exactly what the core rule forbids. Never a raised
   surface: it is a landmark, not something to tap. */
.curs-insignia { position: absolute; top: 0;
  left: calc(-1 * (var(--cami-rail) + var(--cami-roundel)) / 2);
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--cami-roundel); height: var(--cami-roundel);
  box-sizing: border-box; border: 1.5px solid var(--edge-soft); border-radius: 50%;
  background: var(--surface); box-shadow: inset 0 1px 0 var(--edge), var(--shadow);
  font-family: var(--display); font-size: clamp(13px, 1.5vw, 16px); font-weight: 700;
  letter-spacing: 0.04em; color: var(--muted); }
.curs-level.is-actual .curs-insignia { color: var(--accent-ink);
  background: color-mix(in srgb, var(--accent-soft) 70%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 55%, var(--edge-soft));
  box-shadow: inset 0 1px 0 var(--edge),
    0 0 0 5px color-mix(in srgb, var(--accent) 10%, transparent); }
/* The actual level's head-band tint (§2.2): the rule itself leans accent, so
   the band you are in is a landmark from across the page. */
.curs-level.is-actual .curs-level-head {
  border-bottom-color: color-mix(in srgb, var(--accent) 40%, var(--edge-soft)); }
.curs-level.is-superat .curs-insignia { color: var(--daurat-ink);
  border-color: color-mix(in srgb, var(--daurat) 55%, var(--edge-soft)); }
/* Beyond the route: the roundel loses its fill and its edge breaks, which is
   the same sentence the dashed spine above it is telling. */
.curs-level.is-futura .curs-insignia, .curs-level.is-enlla .curs-insignia {
  background: transparent; border-style: dashed; box-shadow: none;
  opacity: var(--op-locked); }

/* THE BAND YOU ARE IN IS THE ONE CARD ON THE LADDER (law 2, raised =
   actionable). It is a <section> and never a fold — folding away where you are
   is the one thing this pane must not offer — so the card can sit on the band
   itself without a wrapper. Its roundel drops by the card's own padding so the
   badge stays level with the name inside it. */
.curs-level.is-actual { border-radius: 20px; padding: var(--cami-card-pad);
  background: var(--glass-1);
  border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--edge-soft));
  box-shadow: var(--shadow-2); }
.curs-level.is-actual .curs-insignia { top: var(--cami-card-pad); }
.curs-level.is-actual::before { top: calc(var(--cami-card-pad) + var(--cami-roundel) + 8px); }
/* «ets aquí» in words, beside the name: the card says *raised* and this says
   what the raising means. Elevation never carries a state alone. */
.curs-level-aqui { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 600; color: var(--accent-ink); }
.curs-level-meta { margin: 0 0 0 auto; color: var(--muted); font-size: 13px;
  font-weight: 400; text-align: right; }
/* Only the exam clause goes gold (a span) — the unit count beside it stays
   muted, so «superat» reads as the seal, not the whole line shouting. */
.curs-meta-daurat { color: var(--daurat-ink); }
/* The fold affordance (S2, 31 Jul 2026), keyed on the ELEMENT and not on a
   posture: every foldable band is a <details> and every unfoldable one is a
   <section>, so the caret appears exactly where there is something to tap and
   the posture classes stay free to say what they are for. The head renders
   identically open and closed — that is what makes a folded band a station
   rather than a hole: it still names itself, still counts its units, and is
   one tap from them.
   v2 draws that affordance as the handoff's pill, but keeps it a PSEUDO-ELEMENT
   of the head inside <summary> rather than the handoff's nested
   `<button aria-expanded>`. Two reasons: a <summary> already exposes the
   expanded state natively (a button inside it would be interactive content
   inside interactive content, and the whole head would stop being the target),
   and the fold's motion is `plegaBanda`'s — a hard-won invariant with a Safari
   lesson behind it that a second control would have to re-learn. */
details.curs-level > summary { list-style: none; cursor: pointer; }
details.curs-level > summary::-webkit-details-marker { display: none; }
details.curs-level > summary .curs-level-head::after { content: "▸";
  flex: none; align-self: center; padding: 4px 11px; border-radius: 999px;
  border: 1px solid var(--edge-soft);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  font-size: 12.5px; line-height: 1.2; color: var(--muted);
  transition: color var(--dur-1) var(--ease-glass),
    border-color var(--dur-1) var(--ease-glass); }
details.curs-level[open] > summary .curs-level-head::after { content: "▾"; }
details.curs-level > summary:hover .curs-level-head::after,
details.curs-level > summary:focus-visible .curs-level-head::after {
  color: var(--text); border-color: var(--rim); }
@media (prefers-reduced-motion: reduce) {
  details.curs-level > summary .curs-level-head::after { transition: none; }
}
/* …and how it opens: NOT here. The fold's motion lives in `plegaBanda`
   (views/curs.js), and this comment is the signpost so nobody adds a second
   one (S4, corrected 1 Aug 2026).
   It first shipped as pure CSS — `::details-content` transitioning
   grid-template-rows 0fr → 1fr with `content-visibility` on the same
   transition under allow-discrete — on the reasoning that the browser owning
   both directions is worth more than any JS. In Safari, which is what Ryan
   actually reads Mica in, that opened beautifully and **shut instantly, with a
   jitter a beat later**. The mechanism: the whole technique rests on
   `content-visibility` being HELD at `visible` for the duration of a close,
   which is what allow-discrete is supposed to buy. Where that hold does not
   happen the property flips at once, the subtree is skipped, `1fr` resolves
   against nothing and the band is gone in one frame — while the 300 ms
   transition runs on invisibly and settles at the end, which IS the jitter.
   Only the close breaks, because the other direction flips
   `content-visibility` to `visible` at the START.
   No amount of curve-tuning reaches that (an easing pass was tried first and
   changed nothing, which is the tell). So the fold is JS now, in every
   browser: identical behaviour everywhere beats a fast path that is right in
   one engine and silently broken in another. Two things came back for free —
   `.curs-passos` no longer needs a permanent `overflow: hidden`, so the
   outermost rows keep the outer edge of their cast, and both directions are
   clocked by one timeline rather than by discrete-transition semantics that
   differ per engine. Do not reintroduce a `::details-content` transition here:
   it would race the JS animation in the engines that do support it. */
.curs-level.is-enlla .stat-line { margin: 6px 0 0; }
.curs-step { display: flex; align-items: center; gap: 12px; width: 100%;
  min-height: 56px; box-sizing: border-box;
  margin: 8px 0 0; padding: 12px 14px; border-radius: 14px;
  color: inherit; text-decoration: none; text-align: left;
  font: inherit; cursor: pointer;
  background: var(--glass-2); border: 1px solid var(--edge-soft); box-shadow: var(--shadow);
  transition: border-color var(--dur-1) var(--ease-glass); }
.curs-step:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--edge-soft)); }
/* El delit S1 · the ladder's hover eases in. Border only — the wash is a
   progress reading and must not move under the cursor. Inert and locked rows
   inherit the transition and simply have nothing to transition TO (their hover
   rules hold the resting colour), which is the honest result. */
@media (prefers-reduced-motion: reduce) { .curs-step { transition: none; } }
/* The mark, v2: a 30px ceramic CHIP rather than a bare 14px glyph in a 20px
   slot. At row scale the old mark was the quietest thing on the row it leads,
   and the tinted chip gives every state a third carrier — colour AND icon AND
   word — without adding an element to the uniform mark · label · status shape.
   The tint is a low color-mix of the state's own hue with the -ink on top: law
   1, a hue glazes and an ink writes, and a 14px glyph never takes a raw tile. */
.curs-step-mark { flex: 0 0 30px; display: inline-flex; align-items: center;
  justify-content: center; width: 30px; height: 30px; border-radius: 10px;
  text-align: center; color: var(--muted); background: var(--surface-sunk);
  transform: rotate(var(--tilt, 0deg)); }
.curs-step.is-current { border-color: color-mix(in srgb, var(--accent) 55%, var(--edge-soft));
  box-shadow: inset 0 1px 0 var(--edge),
    0 3px 12px -4px color-mix(in srgb, var(--accent) 30%, transparent); }
.curs-step.is-current:hover { border-color: var(--accent); }
.curs-step.is-current .curs-step-mark {
  background: color-mix(in srgb, var(--accent) 14%, transparent); }
.curs-step.is-underway .curs-step-mark {
  background: color-mix(in srgb, var(--accent) 11%, transparent); }
.curs-step.is-done .curs-step-mark {
  background: color-mix(in srgb, var(--tile-green) 14%, transparent); }
/* Two chips carry no tint at all, and each says something by it: a padlock has
   nothing behind it to open, and the exam's gold lozenge belongs to l'obra
   daurada rather than to any of the ladder's own marks. */
.curs-step.is-locked .curs-step-mark, .curs-step.is-examen .curs-step-mark {
  background: transparent; }
.curs-step.is-examen .cami-llosange { width: 15px; height: 15px; margin: 0; }
/* Mark and status say the same thing twice, in the same colour (D3, 18 Jul
   2026): check + «dominat» green, play + «en curs» accent, ring/padlock muted.
   -ink variants, never the raw tile hue — these are small marks and 13px text. */
.curs-step.is-current .curs-step-mark,
.curs-step.is-current .curs-step-status { color: var(--accent-ink); }
/* Started, but not the front of the path (21 Jul 2026): ◐ takes the SAME ink as
   ▷ because it reports the same status — «en curs» — and the status text beside
   it is the same word. What it does not take is .is-current's accent border:
   that edge belongs to the ▷ row alone, and it is what makes one row out of
   nine read as *here*. */
.curs-step.is-underway .curs-step-mark,
.curs-step.is-underway .curs-step-status { color: var(--accent-ink); }
.curs-step.is-done .curs-step-mark,
.curs-step.is-done .curs-step-status { color: var(--tile-green-ink); }
.curs-step.is-locked { opacity: 0.55; cursor: default; box-shadow: none; background: transparent; }
/* Inert: a step whose unit text isn't written yet. Reads as a normal row —
   it is genuinely done or open — but nothing to tap, so no hover affordance. */
.curs-step.is-inert { cursor: default; }
.curs-step.is-inert:hover { border-color: var(--edge-soft); }
/* …and v2 dresses the `properament` case as what it is: a socket in the wall,
   not a tile. Transparent ground, dashed edge, the same dashed ring in its chip
   — pending TEXT, never a gate, so still no padlock and still no tick (D3).
   It keeps the full 76px row and all three slots, which is why this is not the
   §2.4 flattening: `.is-pla` shrinks a row to 48px and that is the thing the
   actual and vinent bands must not mix with full cards. Nothing here shrinks,
   so the rhythm holds in every posture. */
.curs-step.is-open.is-inert:not(.is-examen) { background: transparent; box-shadow: none;
  border-style: dashed; border-color: color-mix(in srgb, var(--muted) 40%, transparent); }
.curs-step.is-open.is-inert:not(.is-examen):hover {
  border-color: color-mix(in srgb, var(--muted) 40%, transparent); }
.curs-step.is-open.is-inert:not(.is-examen) .curs-step-copy { color: var(--muted); }
.curs-step.is-open.is-inert:not(.is-examen) .curs-step-status { font-style: italic; }
/* S3 — elevation honesty inside a futura band (31 Jul 2026, «el camí per
   etapes» §2.4): a step that cannot be acted on — locked past the exam wall,
   or an unauthored properament unit — has no business wearing the same
   raised card as an actionable row. raised = actionable, applied where a
   future band's own unstudied and locked rows were violating it. Same three
   slots (mark · label · status), same alignment; only the elevation and the
   height concede there is nothing to tap. Higher specificity than the base
   .is-locked/.is-inert/height rules below, so it wins regardless of source
   order. The label drops to one line — the tighter box has no room for the
   two-line clamp every full card carries, and a flat row is read at a
   glance, not studied. stepRow (curs.js) is the one place that adds this
   class, gated to unit-kind rows inside an is-futura band; the exam rung
   and every actual/vinent row never carry it. */
.curs-step.is-pla { height: 48px; padding: 6px 14px;
  background: transparent; border-color: transparent; box-shadow: none; }
.curs-step.is-pla:hover { border-color: transparent; }
.curs-step.is-pla .curs-step-copy { -webkit-line-clamp: 1; }
/* La graduació's rung (S4, 24 Jul 2026). The exam is the only step that is not
   a place on the map but a gate at the end of one, so it says so in the one
   hue the rest of the ladder never uses — the same --daurat family as l'obra
   daurada waiting behind it. Tokens only, and -ink for the 13px status beside
   the mark; a raw gold at that size fails AA. It comes AFTER .is-done on
   purpose: a passed exam wears gold, not the units' green. A locked or inert
   rung keeps its own quiet background rather than the tint. */
.curs-step.is-examen { border-color: color-mix(in srgb, var(--daurat) 34%, var(--edge-soft));
  background: color-mix(in srgb, var(--daurat) 7%, var(--glass-2)); }
.curs-step.is-examen:hover { border-color: color-mix(in srgb, var(--daurat) 62%, var(--edge-soft)); }
.curs-step.is-examen .curs-step-mark,
.curs-step.is-examen .curs-step-status { color: var(--daurat-ink); }
.curs-step.is-examen.is-locked, .curs-step.is-examen.is-inert { background: transparent; }
.curs-step.is-examen.is-inert:hover { border-color: color-mix(in srgb, var(--daurat) 34%, var(--edge-soft)); }
/* Progress toward «ferma» as a low-tint wash behind the row's own content —
   a background treatment, not a column, so the uniform mark · label · status
   shape and the desktop columns are untouched. It was first drawn as a 2px
   rule along the bottom edge and rejected (20 Jul 2026): against the row's
   1px border it read as a thick, uneven border — like a stylesheet that had
   half-loaded — and the accent was already spoken for by .is-current's border.
   A wash cannot be mistaken for chrome because it has no edge of its own.
   The fill only paints when stepRow set --curs-progress, so done, locked and
   unauthored rows stay clean rather than showing an empty track. Decorative:
   the status chip states the same fact in text, so the wash never carries
   meaning alone, and the tint stays far below text-contrast weight.

   El delit S3 · it DRAWS IN on the view's first paint (rhythm-draw's idiom one
   surface over), so how far you have walked is something you watch arrive
   rather than something already there. What is animated is the background
   SIZE, not a new child and not the custom property: the gradient's stops are
   percentages of the image box, so growing the box from 0 sweeps the wash's
   leading edge out to its real length with its soft end intact — and the row
   keeps its one uniform shape (an extra element is what broke the desktop
   columns before). Only rows that set --curs-progress animate, which is the
   wash invariant holding by construction — a mark that admits no progress has
   no wash to draw.
   MEASURED, since the laying ceremony's hidden-tab hazard applies here too: a
   hidden tab freezes the animation at currentTime 0 (verified — playState
   «running», time 0, background-size 0%), so the wash is HELD BACK rather than
   drawn, and it draws the moment the tab is actually looked at. That is the
   same thing as playing on arrival, and it is only tolerable because the wash
   is decorative: the status chip beside it states the identical fact in text,
   so a row frozen mid-draw has lost a flourish and not a reading. Never move a
   real reading onto this animation. */
.curs-step[style*="--curs-progress"] {
  background-repeat: no-repeat; background-size: 100% 100%;
  background-image: linear-gradient(to right,
    color-mix(in srgb, var(--accent) 9%, transparent) 0
      calc(var(--curs-progress) * 100% - 6%),
    transparent calc(var(--curs-progress) * 100%));
  animation: curs-wash var(--dur-3) var(--ease-glass); }
@keyframes curs-wash { from { background-size: 0% 100%; } }
/* A pane toggle is not an arrival (2 Aug 2026): camí↔gimnàs rebuilds the rows
   in place, and replaying every wash's sweep on each flip read as aggressive.
   showPane marks a return with `is-retorn`; the view's first paint keeps the
   draw-in. The wash is decorative (the chip states the fact in text), so a
   rest-state paint loses nothing. */
.curs-pane.is-retorn .curs-step[style*="--curs-progress"] { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .curs-step[style*="--curs-progress"] { animation: none; }
}
/* One row shape for every step (18 Jul 2026): mark · label · status, with the
   status always present (see stepRow) and pinned right. It used to be a
   stacked copy block whose second line appeared only for steps that had a
   `detail` — units did, scenarios didn't — so pills alternated one- and
   two-line heights and the two desktop columns never lined up. min-height
   keeps the box stable when a label is short. */
.curs-step-copy { flex: 1; min-width: 0; font-weight: 700; }
/* The ladder number (4 Aug 2026) — the row's position in the course, and the
   thing that tells a 2-up band which of two side-by-side units comes first.
   It leads the label INSIDE .curs-step-copy rather than taking a flex slot of
   its own: the row is one uniform mark · label · status shape and a fourth
   item is what broke the desktop columns before. Fixed min-width + tabular
   figures so 8 and 12 leave their titles on one left edge, and it is rendered
   empty on the rows that have no number (the exam rung), so that edge holds
   down the whole band. Quiet by construction — 400 weight in --muted against
   the label's 700 — because it is an address, not a reading.
   Its width is a token on the row (--curs-num) because a second slot needs it:
   the stacked unlock note below indents to exactly this, so the note lines up
   under the TITLE and not under the number. */
.curs-step { --curs-num: 25px; }
.curs-step-num { display: inline-block; min-width: calc(var(--curs-num) - 3px);
  margin-right: 3px;
  color: var(--muted); font-weight: 400; font-size: 13px;
  font-variant-numeric: tabular-nums; text-align: right; }
/* Phones finish that job (20 Jul 2026). The label column is ~224px there, so
   the longest authored titles wrapped to two lines and three rows stood 80px
   against everyone else's 56 — the same ragged rhythm the uniform row exists
   to prevent, just caused by the label instead of the status.
   Shrinking the type was measured and rejected: fitting a 41-character title
   on one line needs ~10.6px, under the system's 11px floor and well under
   readable for a primary label. So every phone row takes the two-line height
   unconditionally and the label clamps to two lines, centred. Uniform by
   construction — it does not depend on how long the next unit /gramatica
   authors happens to be, which a font tuned to today's titles would.
   Known and accepted UNTIL 10 Sep 2026, when the phone ladder became a list
   and the label column roughly doubled (see the max-width block below —
   measured clean at 320 now): at 320px (iPhone SE 1st gen and nothing newer)
   the longest title ran to a third line and ellipsised. Uniformity is kept, the
   full title is one tap away on the unit page, and the alternatives — a
   smaller face for every phone, or a three-line box for every row — both cost
   the 375px+ majority to serve a width that has effectively disappeared.

   Desktop joined the same rule 23 Jul 2026 — the media query around it is
   gone. The ≥900 two-column spine gives a row ~480px, and after the mark, the
   status and the paddings the label column there is ~330px: the longest
   authored titles («Cara a cara: les tres maneres del passat») wrapped beside
   a long status and that one tile stood taller than its column. Same disease
   the phones had, same cure: every row takes the two-line height and the
   label clamps, so uniformity does not depend on the next title /gramatica
   happens to author. */
.curs-step { height: 76px; min-height: 0; }
/* THE TWO-LINE BOX HAS TO ACTUALLY HOLD TWO LINES (13 Aug 2026). It did not,
   and the arithmetic is the whole bug: the row is 76px with 12px of padding
   top and bottom, so the copy gets 52px — while two lines at the inherited
   1.6 came to 54.4px. Every wrapped title overflowed its own box by 2.4px.
   Chromium hides that (it does not honour -webkit-line-clamp on this box, so
   the copy just clips two pixels of descender and nobody sees it); SAFARI,
   which is what Ryan reads Mica in and where the clamp is live, measures a
   box that cannot hold line two and ellipsises it — so every wrapped row on
   the English ladder ended «introducing yourself…», «asking again…», «for
   word…», with the dots landing after the LAST word of a title that was
   complete. One-line rows were untouched, which is what made it look like
   random text-mangling rather than a height being 2.4px short.
   1.5 makes two lines 51px and the box 52px, so it fits by construction in
   every engine rather than by an engine declining to clamp — the standing
   rule after la paret's zoom and the camí fold's `::details-content`. Keep
   `2 × line-height ≤ row height − 24px` true if either number is ever
   retuned; that inequality is the invariant, not the 1.5. */
.curs-step-copy { display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; overflow: hidden; line-height: 1.5; }
/* …and when it DOES take the second line, that line hangs under the title and
   never under the number (13 Aug 2026). The number is an inline-block at the
   head of the copy, so a wrap returned to the copy box's own left edge — 25px
   to the LEFT of the title it continues — and «yourself» sat beneath «1». Read
   at a glance that is not a wrapped title, it is a second field, and a 2-up
   band of them reads as rubble. The fix is the classic hanging indent, and the
   width it costs is the width alignment costs: the padding narrows the content
   box by --curs-num and the negative indent lets the FIRST line borrow it back,
   which is exactly the space the number is standing in anyway.
   It is deliberately the same --curs-num the stacked unlock note already
   indents by (.curs-step-status.is-nota), so a title's own wrap, a route note
   and a two-digit row all line up on ONE left edge down the whole band. */
.curs-step-copy { padding-left: var(--curs-num);
  text-indent: calc(-1 * var(--curs-num)); }
/* …and the status is capped, so it can never eat the label (4 Aug 2026). It is
   `flex: 0 0 auto`, i.e. it takes exactly its content width and the label — the
   only flexible slot — absorbs whatever is left. That was survivable while every
   status was a single word, and stopped being so the moment a lock started
   naming its own way out («s'obre amb l'imperatiu»). Past the cap the status
   wraps to a second line instead, which the 76px row already has room for, and
   the label keeps a workable column. It fixes an older case of the same disease
   on the way: at 375px «quan acabis les unitats · 4 de 9» left the exam rung's
   own title 76px and ellipsised it. The share is deliberately looser at ≥900
   (see the media block below): a 2-up row is 492px, so the same proportion that
   protects a 375px label there would wrap the exam status a phone has room to
   keep on one line. */
/* The 38 % was measured against a tighter 30 % on 13 Aug 2026 and KEPT. At 30 %
   a two-word status («més endavant», 92px) breaks to two stacked lines and hands
   the title all of 7px — 139px to 144px — which is not a line's worth on any
   title that was short of one. A stacked status for nothing is the worse of the
   two pictures, so the cap stays where it is; the phone's genuinely oversized
   titles are a content length, not a cap. */
.curs-step-status { flex: 0 0 auto; max-width: 38%; margin-left: auto; padding-left: 8px;
  color: var(--muted); font-size: 13px; font-weight: 400; text-align: right;
  font-variant: none; }
/* …and a lock that names its WAY OUT takes a line of its own instead (v2). At
   the 38 % cap «s'obre amb les perífrasis verbals» wrapped to three lines beside
   a title it had already squeezed to about a third of the row. Stacked, the
   title gets the full width on one line and the route reads underneath it,
   indented to the title's own left edge — same information, same element, one
   place further down. The status is NEVER omitted (18 Jul 2026); only its place
   moves, and only for the two locks that carry a route.
   The switch is a CLASS the view adds, not `:has()`: stepRow is the one place
   that knows a row has a note, and deterministic beats engine-dependent (the
   Món spread's rule). */
.curs-step-copy.te-nota { display: flex; flex-direction: column; justify-content: center;
  gap: 2px; -webkit-line-clamp: none; }
.curs-step-titol { display: block; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.curs-step-status.is-nota { max-width: none; margin-left: 0;
  padding-left: var(--curs-num); text-align: left; line-height: 1.3; }
.curs-barri { display: flex; justify-content: space-between; align-items: center;
  margin-top: 24px; padding: 14px 2px; border-top: 1px dashed var(--bar-track);
  color: var(--accent); text-decoration: none; }
.curs-barri span { color: var(--muted); font-size: 14px; }

/* ---- El camí AL TELÈFON és una llista, no una pila de fitxes (10 Sep 2026,
   Ryan's report: «super dense and detailed… the text spills over 2 lines… the
   numbers are way too close to the titles»). Measured on his own ladder at
   393px, and the three complaints are one arithmetic problem with three
   symptoms.

   THE LABEL COLUMN WAS 141px OF A 393px SCREEN. The row is one flex line —
   mark · label · status — and at phone width every fixed slot in it is taken
   out of the only flexible one: 32px rail + 28px band padding + 28px row
   padding + 30px chip + 12px gap + 86px status. Inside that 141px the number's
   own 25px comes off the title again, so a unit's NAME — the one thing a
   ladder row exists to say — was reading at 116px. Twelve of the nineteen
   titles wrapped, and the two longest («El parell del passat: he anat / vaig
   anar», «Cara a cara: les tres maneres del passat») needed a third line inside
   a two-line clamp and were ELLIPSISED. Meanwhile the 86px that truncated them
   was spending itself on «consolidant», printed five times running down one
   band.
   And the number reads as crowding because it IS crowded: `.curs-step-num` is
   a right-aligned box with `margin-right: 3px`, so whatever the digit count,
   the gap to the title is exactly three pixels.

   SO THE STATUS TAKES A LINE OF ITS OWN and the label takes the row. That is
   not a new idiom — it is the one `.is-nota` already uses, and for exactly this
   reason («at the 38 % cap it wrapped to three lines beside a title it had
   already squeezed»); the phone is simply the case where EVERY status is too
   wide for the line it is standing on, so the special case becomes the general
   one and the two shapes converge. The title column goes 116px → 234px, which
   puts fifteen of the nineteen titles on one line and truncates none of them,
   and the number gets ten pixels of air instead of three.

   AND NESTING DE-ESCALATES (§2.4), which it was not doing: nine `--glass-2`
   rows with a border and a cast, stacked inside the `--glass-1` card the actual
   band already wears — a card inside a card, nine times. Flat rows on the
   band's own ground, with THE ROW YOU ARE ON as the one raised thing in it:
   the same sentence the ladder already tells one level up («the band you are in
   is the one card on the ladder»), told again at row grain. Nothing else about
   the row moves — same three slots, same five marks, same inks, same wash, same
   states, same DOM. El barri's ruling, one surface over, is the precedent: the
   atlas said state three times at full volume and was re-drawn as quiet rows.

   Two mechanical notes for whoever touches this next. Rows flatten with
   `background-color`, never the `background` shorthand — the shorthand would
   take `--curs-progress`'s gradient out with it. And `.curs-passos` stays a
   BLOCK: `plegaBanda` folds a band by animating that element's height, and
   flex items in a column would shrink to meet it instead of being clipped, so
   a fold would squash its rows rather than wipe them.

   ≥900 is untouched by construction (this block is max-width): a 2-up row there
   is ~492px, the status has room on the line, and the number is what tells two
   side-by-side units which comes first. */
@media (max-width: 899px) {
  /* mark · title
     mark · status — the mark spans both, so a row with no second line simply
     collapses it (which is how a `.te-nota` row, whose status lives INSIDE the
     copy, lands on the identical shape without a second stylesheet). */
  .curs-step { display: grid; grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "marca titol" "marca meta";
    align-items: center; column-gap: 12px; row-gap: 2px;
    height: auto; min-height: 0; margin: 2px 0 0; padding: 10px;
    background-color: transparent; border-color: transparent; box-shadow: none; }
  /* …and a `.te-nota` row has ONE line, not two: its status is a child of the
     copy, so nothing is ever placed in `meta` and the empty track would still
     hold ~16px open under it. Same `:has()` idiom (and same graceful failure)
     as the rail's `.curs-level:has(+ .curs-level.is-actual)` above — where the
     selector is unsupported the row is simply a touch tall, never broken. */
  .curs-step:has(.te-nota) { grid-template-areas: "marca titol"; }
  /* The chip belongs to the TITLE, not to the row's midpoint: spanning both
     tracks and centring put it beside the gap between a two-line title and its
     status, which reads as a mark for neither. Pinned to the top of the span
     and lifted by half the difference between the chip and one line of title,
     it sits optically centred on line one whatever follows underneath. */
  .curs-step-mark { grid-area: marca; align-self: start;
    margin-top: calc((var(--lh-ui) * 1em - 30px) / 2); }
  .curs-step-copy { grid-area: titol; line-height: var(--lh-ui); }
  /* The status keeps every word it had; it is left-aligned under the TITLE
     (indented past the number, like `.is-nota`) rather than pinned to a right
     edge it was never wide enough for. */
  .curs-step-status { grid-area: meta; max-width: none; margin-left: 0;
    padding-left: var(--curs-num); text-align: left; line-height: var(--lh-ui); }
  /* Ten pixels, not three. The slot still right-aligns its digits, so the gap
     is the same whether the row is 8 or 17 — that is what the fixed slot is
     for — and the hanging indent still hangs a wrapped title under the title. */
  .curs-step { --curs-num: 28px; }
  .curs-step-num { min-width: calc(var(--curs-num) - 10px); margin-right: 10px; }
  /* A locked row that names its way out is now the same shape as everything
     else, so its title may wrap like everything else's instead of ellipsising
     inside a nowrap box that no longer has a reason to be narrow — and the
     hanging indent moves down onto the title with it, or a wrapped second line
     returns to the copy's own edge and sits under the NUMBER (the 13 Aug 2026
     bug, which this shape only avoided by never wrapping at all). */
  .curs-step-copy.te-nota { padding-left: 0; text-indent: 0; }
  .curs-step-titol { display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; white-space: normal;
    padding-left: var(--curs-num); text-indent: calc(-1 * var(--curs-num)); }
  /* THE ONE RAISED ROW. Its border, its glow and its accent ink are the base
     rules' (higher specificity, untouched); only the ground has to come back,
     since the flattening above took it. */
  .curs-step.is-current { background-color: var(--glass-2); }
  /* The gate keeps its gold as a WASH — a band across the list marking the end
     of a level — and drops the edge it no longer needs. A locked or sat rung
     keeps its own quiet ground (base rule): gold is what an open gate says. */
  .curs-step.is-examen { background-color: color-mix(in srgb, var(--daurat) 8%, transparent);
    border-color: transparent; }
  /* `properament` said itself three times over — dotted chip, italic word, and
     a dashed card that on a flat list would be the loudest edge on the page.
     The first two carry it. */
  .curs-step.is-open.is-inert:not(.is-examen) { border-color: transparent; }
  /* Elevation honesty (§2.4) holds by construction here — nothing but the front
     row is raised — so `.is-pla` has no elevation left to concede and stops
     conceding height for it: one row shape down the whole ladder. */
  .curs-step.is-pla { height: auto; padding: 10px; }
  .curs-step.is-pla .curs-step-copy { -webkit-line-clamp: 2; }
  /* …and the station head's meta stops being a right-aligned line floating
     under a left-aligned name (it has wrapped to its own row at this width
     since the day it was written — `margin-left: auto` only ever aligned it to
     an edge nothing else uses). It is the head's subline. */
  .curs-level-meta { flex-basis: 100%; margin: 3px 0 0; text-align: left; order: 2; }
  /* …which leaves the fold's caret stranded on a third line of its own. It
     takes the right end of the NAME's line instead — the place a disclosure
     control belongs, and the place the head's own `margin-left: auto` was
     always reaching for. */
  details.curs-level > summary .curs-level-head::after { order: 1; margin-left: auto; }
}

/* R4 — the camí pane at ≥900, redrawn again for «el camí per etapes» (S1,
   31 Jul 2026): the resume card becomes a full-width "on ets" band (same
   markup as mobile, laid horizontally — the one primary CTA), and the spine
   is a plain vertical stack of full-width level bands. Each band's step list
   (.curs-passos) is a 2-up grid reading left-right then down — columns exist
   only INSIDE a band, never across bands. The old multicol spine (columns: 2)
   balanced every level into one flow by height, so folding a futur <details>
   re-split the flow and the folded level landed wherever the balancer put it;
   a grid inside the band can only shrink in place, so the collapse-jump is
   dead by construction. Inert below 900 — the single column mobile always had. */
@media (min-width: 900px) {
  /* The head stops short of the widened container rather than stretching across
     it. Since the big-title pass it is one row with the subnav pushed to its
     right edge, so the cap decides where that control STANDS — hence
     `--w-browse` and not the old `--w-focus`: the camí below caps itself at
     browse width, so the pills line up with the ladder's right edge instead of
     floating 360px short of it. The outer shell is invariant (A3: always
     lay-canvas, set in curs.js), so this rect is byte-identical on both panes;
     the gimnàs consumes the canvas internally and is deliberately wider than
     its own head. El barri is lay-canvas too and caps its head in the same
     column, so the three choices don't move between the surfaces. */
  .dashboard.curs > .curs-cap,
  .dashboard.finestra-view > .curs-cap { max-width: var(--w-browse);
    margin-inline: 0; }
  /* The head's grid (see the base block) grows a second column: the subnav
     leaves its full-width row 3 and spans rubric+title instead, bottom-aligned
     with the title, while the hint steps up to row 3 in column 1. The pills'
     position still depends only on rubric+title — never on the hint. */
  .curs-cap { grid-template-columns: minmax(0, 1fr) auto; column-gap: 14px; }
  .curs-cap-linia { grid-row: 3; grid-column: 1; }
  /* Auto-width and pushed right, standing on the title's own baseline. The
     inner grid keeps `repeat(3, 1fr)`, so the three targets stay even (D2's
     real invariant) — they are simply sized by the widest label now. */
  .curs-cap > .curs-segments { grid-area: 1 / 2 / 3 / 3; align-self: end;
    margin-bottom: 8px; }
  .curs-cami { max-width: var(--w-browse); }
  .curs-spine { margin-top: var(--space-5); --cami-gap: var(--space-6); }
  .curs-passos { display: grid; grid-template-columns: 1fr 1fr;
    gap: var(--space-2) var(--space-4); margin-top: var(--space-2); }
  /* The exam rung spans both columns (v2): it is not one unit among the level's
     units, it is the gate at the end of them — and its own status is the
     longest on the ladder («quan acabis les unitats · 4 de 9»), which in half a
     row wrapped its title onto two lines while every unit beside it sat on one. */
  .curs-passos > .curs-step.is-examen { grid-column: 1 / -1; }
  /* The grid's gap owns the rhythm here; the mobile rows' own margin-top would
     stack on top of it and skew the first grid row against the head. */
  .curs-step { margin-top: 0; }
  /* A 2-up row is ~492px, so the status may take a larger share and still leave
     the label a workable column — enough for «quan acabis les unitats · 4 de 9»
     to stay on one line, which at the phone's share it would not. Same
     specificity as the base rule, inside the breakpoint, per the layout rule. */
  .curs-step-status { max-width: 50%; }
}

/* ---- la sortida · the one way up ----
   One control for every descens (see js/components/sortida.js). It replaced
   .back-link (a text link here), .xat-back and .placa-back (a 44px ‹ button
   there) — three idioms for one idea, which is how they drifted apart. */
.sortida {
  display: inline-flex; align-items: center; gap: 2px;
  min-height: 44px; padding: 0 10px 0 4px; margin: 0;
  font: inherit; font-size: 14px; background: none; border: none;
  border-radius: 999px; color: var(--muted); text-decoration: none;
  cursor: pointer; flex-shrink: 0;
  transition: color var(--dur-1) var(--ease-glass),
              background var(--dur-1) var(--ease-glass),
              transform var(--dur-1) var(--ease-glass);
}
.sortida:hover { color: var(--accent-ink); background: var(--accent-soft); }
.sortida-chevron { display: grid; place-items: center; width: 22px;
  transition: transform var(--dur-1) var(--ease-pop); }
/* El delit S1 · the most-used control in the app finally answers you. On hover
   the chevron leans the way it will take you — 2px, --ease-pop, so it arrives
   with one soft overshoot rather than sliding. Fine pointers only: on a phone
   there is no hover, and a lean left behind on a tapped control reads as stuck.
   The press dips the WHOLE control 1px, which is the half that works on touch. */
@media (hover: hover) and (pointer: fine) {
  .sortida:hover .sortida-chevron { transform: translateX(-2px); }
}
.sortida:active { transform: translateY(1px); }
@media (prefers-reduced-motion: reduce) {
  .sortida, .sortida-chevron { transition: none; }
  .sortida:hover .sortida-chevron, .sortida:active { transform: none; }
}
/* The label names the destination — the useful half — so it truncates rather
   than shoving the medallion or the counter out of a 375px session header. */
.sortida-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sortida-compact { width: 44px; padding: 0; justify-content: center; gap: 0; }
/* "mobile" compact (A6): the label exists in the DOM; below the desktop
   threshold it hides and the control collapses to the same 44px chevron as
   .sortida-compact, so phones are visually unchanged. At ≥900 the full
   destination label shows — desktop has the room, so it names the way out. */
@media (max-width: 899px) {
  .sortida-compact-mobile { width: 44px; padding: 0; justify-content: center; gap: 0; }
  .sortida-compact-mobile .sortida-label { display: none; }
}
/* Desktop (A6): a centred Sortida floats away from the reading edge — align the
   Dos banner's and the gate screens' exits to the content column's left. */
@media (min-width: 900px) {
  .duo-turn .sortida, .session-end > .sortida {
    display: flex; width: max-content; margin-right: auto;
  }
}
/* ============================================================
   LA UNITAT v2 (5 Aug 2026, design/handoffs/unitat-v2/)
   ============================================================
   The unit page resolved into three strata — head · EL RECORREGUT (the launch
   band, alçat because it is the one actionable block) · the reading spread (la
   fitxa flat beside the xuleta plaque), then the verb bank and the closing
   repeat of the same action. The departures from the handoff are recorded on
   `renderUnit` in views/gramatica.js; the two that bite in CSS are noted where
   they land — the step tile is the app's shared `.curs-passa` (never a fourth
   answer to «you are here»), and the page widens by naming an archetype
   (lay-canvas, docs/LAYOUT.md) rather than by the handoff's raw 1120px. */

.gram-unit { display: flex; flex-direction: column; gap: clamp(24px, 3.5vw, 34px);
  padding-bottom: 40px; }
/* The strata rise in order behind `.is-entrant`, filling BACKWARDS; renderUnit
   drops the class on a settle timer whether or not a frame was ever served (the
   mosaic's rule — Progrés, la paret, el gimnàs, el camí and la porta all pay
   for it). Only an ARRIVAL through the router carries the class: a llengua
   toggle and a study step climbing out are returns, not landings. */
.gram-unit.is-entrant > * { animation: unitat-puja var(--dur-3) var(--ease-glass) backwards; }
.gram-unit.is-entrant > .gram-unit-head { animation-delay: 0.06s; }
.gram-unit.is-entrant > .gram-recorregut { animation-delay: 0.12s; }
.gram-unit.is-entrant > .gram-fitxa { animation-delay: 0.18s; }
.gram-unit.is-entrant > .gram-bank,
.gram-unit.is-entrant > .gram-tancament { animation-delay: 0.24s; }
@keyframes unitat-puja { from { opacity: 0; transform: translateY(12px); } }
@media (prefers-reduced-motion: reduce) {
  .gram-unit.is-entrant > * { animation: none !important; }
}

/* One line of chrome above the page: the way out, and the side being read. */
.gram-unit-top { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.gram-unit-top > .sortida { margin: 0; }
.gram-unit-top > .llengua { margin-left: auto; }

.gram-unit-head { display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: 14px 28px; margin: 0; }
.gram-unit-titol { flex: 1 1 420px; min-width: 0;
  display: flex; flex-direction: column; gap: 10px; }
.gram-unit-eyebrow { margin: 0; font-size: 11px; letter-spacing: 0.16em;
  text-transform: uppercase; font-weight: 600; color: var(--tile-amber-ink); }
/* The page's own h1, so it may run at display scale — capped at 58px, which is
   the app's display ceiling (the porta's greeting). The handoff asks for 62;
   a unit title is not larger than the doorstep's greeting. */
.gram-unit-head h1 { margin: 0; font-family: var(--display); font-style: italic;
  font-weight: 700; font-size: clamp(34px, 5.2vw, 58px); line-height: 1.02;
  letter-spacing: 0.01em; color: var(--text); text-wrap: balance;
  overflow-wrap: anywhere; }
.gram-unit-sub { margin: 0; font-family: var(--display); font-style: italic;
  font-size: 21px; line-height: 1.4; color: var(--muted); text-wrap: pretty; }

.unit-chip { flex: 0 0 auto; display: inline-block; border-radius: 999px;
  padding: 3px 9px; font-size: 11px; line-height: 1.3; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--muted); border: 1px solid var(--bar-track);
  background: color-mix(in srgb, var(--surface) 75%, transparent); }
/* --accent-ink, not raw --accent (law 1: a hue is a glaze, its ink is what
   writes at 11px). --ok is a status hue that is already contrast-tuned in both
   themes, so it stays as it is. */
.state-oberta, .state-en-curs { color: var(--accent-ink); border-color: var(--rim); }
.state-ferma { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 36%, transparent); }
.state-per-revalidar { color: var(--tile-amber-ink); }
.state-de-referència, .state-properament { color: var(--muted); }
/* The head's chip alone takes the v2 pill: bigger, tinted from ITS OWN state
   hue, so «en curs» reads amber-warm and «de referència» stays a quiet grey
   pill — one idiom, every state keeping the ink it already had. Scoped, because
   `.unit-chip` is also the camí fallback map's row chip and the paradigm card's
   «consolidat» mark, and the gym roster wears the latter — surfaces this
   handoff never looked at (the gimnàs v2 ruling). */
.gram-unit-head > .unit-chip { align-self: flex-end; padding: 6px 14px;
  font-size: 12.5px; font-weight: 700; text-transform: none;
  background: color-mix(in srgb, currentColor 13%, var(--surface));
  border-color: color-mix(in srgb, currentColor 34%, transparent); }

/* ---- Stratum 2 · EL RECORREGUT ----
   The unit's action band (21 Jul 2026), which since v2 also DRAWS the flow.
   Raised, because raised is actionable, and it is the one actionable block on a
   page of reference; the fitxa, the xuleta and the bank below it stay flat. */
.gram-recorregut { display: flex; flex-direction: column; gap: 20px;
  padding: clamp(20px, 3vw, 28px); border-radius: 18px;
  background: var(--surface); border: 1px solid var(--edge-soft);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
.gram-recorregut-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.gram-recorregut-head > .gram-rubric { margin: 0; }
.gram-recorregut-pas { margin: 0 0 0 auto; font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
/* One column per flow step. `flex: 1 1 0` with a 70px floor spreads eight steps
   evenly and wraps to a second row on a phone rather than shrinking a label
   below the type floor. */
.gram-passos { display: flex; flex-wrap: wrap; gap: 16px 4px; }
.gram-passa { flex: 1 1 0; min-width: 70px; display: flex; flex-direction: column;
  align-items: center; gap: 9px; }
/* The tile is the app's shared `.curs-passa` (camí hero · porta fil · here), so
   done/here/left looks the same wherever a study flow is drawn — the handoff's
   amber-filled «current» would have been a fourth answer to «you are here».
   `--passa` is the only thing that changes: at 18px it is the centrepiece of
   this band rather than a marginal mark. */
.gram-passa > .curs-passa { --passa: 18px; border-radius: 4px; }
.gram-passa-nom { font-size: 11px; letter-spacing: 0.07em; text-transform: uppercase;
  text-align: center; color: var(--muted); line-height: 1.25; }
/* The one you are on, said in words as well as in the tile — so the pulse is
   never the sole carrier. Everything else keeps full --muted: the handoff fades
   the future labels to 0.65, and a faded 11px micro-cap is exactly the contrast
   failure the camí's minimap refused (the tile beside it carries the state). */
.gram-passa.is-aqui .gram-passa-nom { font-weight: 700; color: var(--accent-ink); }
.gram-recorregut > .gram-estudia { align-self: flex-start; }
@media (max-width: 480px) {
  .gram-recorregut > .gram-estudia { align-self: stretch; }
}

/* ---- The closing repeat ----
   The same action at the foot of the reading spread, so finishing the fitxa
   does not mean scrolling back up. Same label, same handler — a repeat, never
   a second choice. */
.gram-tancament { display: flex; flex-direction: column; align-items: center; gap: 24px; }
.gram-tancament > .grout-rule { width: min(420px, 80%); }

/* ---- Stratum 3 · the reading spread ----
   LA FITXA and LA XULETA are two halves of one sheet, so they wear one idiom:
   a .gram-rubric label above the content, both labels the same size, weight and
   tracking, both columns starting on the same line. The xuleta used to carry
   its own, bolder label INSIDE its box — a second species of heading, which
   also pushed its content down by a line. Do not move a label back inside.

   The spread is plain flex WRAP: the prose column asks for 520px and takes
   everything going (`flex: 999`), the aside asks for 280 and caps at 360, and
   the two drop into one column on their own at about 880px. That retires the
   documented ≥1100 grid exception AND the `#view > .gram-unit` specificity
   override that went with it — the page is `lay-canvas` now, which is how a
   surface widens (docs/LAYOUT.md). */
.gram-fitxa { display: flex; flex-wrap: wrap; align-items: flex-start;
  gap: clamp(28px, 4vw, 48px); }
.gram-rubric { margin: 0 0 8px; color: var(--muted-firm); font-size: 11px;
  letter-spacing: 0.12em; font-weight: 700; }
.gram-fitxa-prose { flex: 999 1 520px; min-width: 0; max-width: 680px; }
.gram-xuleta-col { flex: 1 1 280px; min-width: 0; max-width: 360px;
  position: sticky; top: 26px; z-index: var(--z-sticky); }

/* The portal's first line, doing double duty as the page's lede. Flat and
   serif — read, not acted on. */
.gram-epigraf { margin: 0 0 34px; max-width: 46ch; font-family: var(--display);
  font-style: italic; font-size: 22px; line-height: 1.45; color: var(--muted);
  text-wrap: pretty; }
/* A chapter is an open article now, not a `<details>`: this is a reading page.
   The numeral is ornament (aria-hidden), the h2 is the heading. */
.gram-chapter { margin: 0; }
.gram-chapter-head { display: flex; align-items: baseline; gap: 16px; margin: 0 0 16px; }
.gram-chapter-num { flex: none; font-family: var(--display); font-weight: 600;
  font-size: 46px; line-height: 1; color: var(--accent); opacity: 0.9; }
.gram-chapter-head h2 { margin: 0; font-family: var(--display); font-style: italic;
  font-weight: 700; font-size: 31px; line-height: 1.15; color: var(--text); }
.gram-prose { margin: 0 0 18px; font-size: 17.5px; line-height: 1.75; color: var(--text);
  white-space: pre-line; text-wrap: pretty; }
/* The drop cap is `::first-letter` rather than a floated span the JS injects:
   the browser already knows where a paragraph's first letter ends, punctuation
   and accents included, and no authored prose has to be cut up to find it. */
.gram-chapter > .gram-prose:first-of-type::first-letter {
  float: left; font-family: var(--display); font-weight: 600; font-size: 56px;
  line-height: 0.82; padding: 5px 9px 0 0; color: var(--accent-ink); }
.gram-fitxa .grout-rule { margin: 38px 0; }
/* Examples as ceramic tiles — a quoted sentence with its gloss, warm-tinted so
   the column's one tile-looking thing is still flat (it is a tile, not a card:
   nothing here is actionable). The tilt alternates by position.
   NOT the handoff's `--tile-cream` glaze: cream is a LIGHT glaze in BOTH themes
   (the camí v2 minimap refused it for the same reason), and measured in dark a
   55 % cream ground gives 3.61:1 on the sentence and 1.42:1 on the gloss —
   unreadable. --accent-soft is the token that actually flips, and it is the
   family the example block already wore: 14.0/5.1 light, 11.4/4.5 dark. */
.gram-examples { display: flex; flex-direction: column; gap: 14px; margin: 26px 0 6px; }
.gram-example { margin: 0; padding: 18px 22px; border-radius: 14px;
  background: color-mix(in srgb, var(--accent-soft) 55%, var(--surface));
  border: 1px solid var(--edge-soft); box-shadow: inset 0 1px 0 var(--edge);
  transform: rotate(-0.35deg); }
.gram-example:nth-child(even) { transform: rotate(0.3deg); }
.gram-example blockquote { margin: 0; font-family: var(--display); font-style: italic;
  font-weight: 600; font-size: 21px; line-height: 1.4; color: var(--text); }
.gram-example figcaption { margin-top: 8px; font-size: 13.5px; line-height: 1.5;
  color: var(--muted); }
.gram-note { margin: 18px 0; }
.gram-note > summary { cursor: pointer; font-size: 13.5px; font-weight: 600;
  color: var(--accent-ink); }
.gram-note > p { margin: 8px 0 0; font-size: 15.5px; line-height: 1.6; color: var(--muted); }
.gram-timeline { display: flex; justify-content: space-between; gap: 7px; margin-top: 14px;
  padding-top: 13px; border-top: 2px solid var(--muted); overflow-x: auto; }
.gram-timeline > span { min-width: 78px; display: flex; flex-direction: column; text-align: center; }
.gram-timeline small { color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }

/* ---- LA XULETA · the plaque ----
   The one glazed reference artifact on the page: a card whose 8px of padding
   frames an inner accent hairline, which is what makes it read as a mounted
   plaque rather than as another content box (it replaces the old 2px ink box). */
.gram-xuleta { padding: 8px; border-radius: 16px; background: var(--surface);
  border: 1px solid var(--edge-soft); box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
.gram-xuleta-marc { padding: 16px 18px 12px; border-radius: 11px;
  border: 1.5px solid color-mix(in srgb, var(--accent) 36%, var(--edge-soft)); }
.gram-xuleta-llista { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; }
.gram-xuleta-llista li { display: flex; gap: 12px; padding: 12px 0;
  border-top: 1px solid var(--line); }
.gram-xuleta-llista li:first-child { padding-top: 2px; border-top: 0; }
.gram-xuleta-text { font-size: 15px; line-height: 1.55; color: var(--text); }
/* The numeral tessel·la. Tiles glaze, inks write: the hue is a 12 % glaze (16 %
   for amber, whose light mix is the palest) and the numeral is its own -ink.
   Hue and tilt both cycle on nth-child — five of each, so the JS names no
   class and a reordered xuleta cannot fall out of step with itself.
   The handoff's 16/22 measured 4.42:1 on green and 4.49:1 on terra in dark —
   just under AA for a 12 px numeral. LESS glaze raises contrast in BOTH themes
   (the tile moves toward its own surface, and the ink away from it), so the
   whole cycle came down one step rather than the two hues being special-cased:
   4.65–5.72 dark, 5.20–7.39 light. */
.gram-xuleta-num { flex: none; width: 22px; height: 22px; border-radius: 5px;
  display: grid; place-items: center; font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums; }
.gram-xuleta-llista li:nth-child(5n+1) .gram-xuleta-num { transform: rotate(-3deg);
  background: color-mix(in srgb, var(--tile-terra) 12%, var(--surface)); color: var(--tile-terra-ink); }
.gram-xuleta-llista li:nth-child(5n+2) .gram-xuleta-num { transform: rotate(2deg);
  background: color-mix(in srgb, var(--tile-blue) 12%, var(--surface)); color: var(--tile-blue-ink); }
.gram-xuleta-llista li:nth-child(5n+3) .gram-xuleta-num { transform: rotate(-2deg);
  background: color-mix(in srgb, var(--tile-amber) 16%, var(--surface)); color: var(--tile-amber-ink); }
.gram-xuleta-llista li:nth-child(5n+4) .gram-xuleta-num { transform: rotate(3deg);
  background: color-mix(in srgb, var(--tile-green) 12%, var(--surface)); color: var(--tile-green-ink); }
.gram-xuleta-llista li:nth-child(5n+5) .gram-xuleta-num { transform: rotate(-3deg);
  background: color-mix(in srgb, var(--tile-rose) 12%, var(--surface)); color: var(--tile-rose-ink); }
.gram-xuleta-peu { margin: 2px 0 4px; padding-top: 12px; border-top: 1px solid var(--line);
  font-size: 12.5px; font-style: italic; color: var(--muted); }

.gram-bank { display: flex; flex-direction: column; gap: 14px; }
.gram-bank > .grout-rule { margin-bottom: 8px; }
.gram-bank-head { display: flex; justify-content: space-between; }
.gram-bank-head p { margin: 0; }
.gram-grid-deck { display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory;
  padding: 4px 2px 12px; }
/* The bank stops being a swipe deck once there is no swipe (21 Jul 2026). At
   browse width four 420 px cards ran to 1720 px inside a 1000 px box, so two of
   them sat off the right edge with no scrollbar and no cue — a snap scroller is
   a touch idiom and this tier has a mouse. Co-located with the flex rule above
   and at the same specificity, per the layout convention. It engaged at 1100
   because that is where the unit page used to widen; since v2 the page is
   lay-canvas, so it engages at the app's own desktop threshold instead.
   The 340 px floor is chosen for CELL width, the roster's binding constraint:
   it resolves to two ~490 px cards abreast, i.e. ~150 px paradigm cells, well
   clear of the ~109 px the longest conditional needs. */
@media (min-width: 900px) {
  .gram-grid-deck { display: grid; gap: 14px; overflow-x: visible; padding: 4px 2px;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }
}
/* The bank's cards take the gimnàs roster's shell — --surface on --edge-soft at
   radius 16 with a hover lift — so a paradigm card looks the same wherever it
   is browsed. Scoped to the bank: the same component is also the recital's
   sticky mini and the live coverage deck, where a hover lift means nothing. */
.gram-bank .gram-grid-card { background: var(--surface); border: 1px solid var(--edge-soft);
  border-radius: 16px; box-shadow: inset 0 1px 0 var(--edge), var(--glaze);
  transition: transform var(--dur-1) var(--ease-glass), box-shadow var(--dur-1) var(--ease-glass); }
.gram-bank .gram-grid-card:hover { transform: translateY(-2px);
  box-shadow: inset 0 1px 0 var(--edge), var(--shadow-2); }
@media (prefers-reduced-motion: reduce) {
  .gram-bank .gram-grid-card { transition: none; }
  .gram-bank .gram-grid-card:hover { transform: none; }
}
.gram-grid-card { flex: 0 0 min(100%, 420px); scroll-snap-align: start; padding: 14px;
  border-radius: 14px; border: 1px solid color-mix(in srgb, var(--muted) 25%, transparent);
  background: var(--paper); box-shadow: var(--shadow); }
.gram-grid-card.compact { width: 100%; max-width: 440px; margin: 0 auto; }
.gram-grid-title { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 10px; }
.gram-grid-title strong { font-family: var(--display); font-size: 20px; }
.gram-grid-title > span:not(.unit-chip) { color: var(--muted); }
.gram-grid-title .unit-chip { margin-left: auto; order: 3; }
/* «Què vol dir?» on the lemma. `display: contents` flattens glossReveal's
   wrapper so the button and its panel become flex items of the title itself
   (the .frase-epigraf precedent) — then `order` puts the «?» right after the
   lemma and drops the meaning onto its own full-width line, without the markup
   having to interleave them. */
.gram-grid-title .gloss { display: contents; }
.gram-grid-title .btn-reveal.is-compact { order: 1; }
.gram-grid-title > span:not(.unit-chip) { order: 2; }
/* The gimnàs roster's «4/6» — how many of this verb's six forms have been met
   in the selected tense. It shares the chip's right-hand slot (only one of the
   two is ever present) and stays quiet enough never to compete with the lemma
   it counts. Declared here, at the same specificity as the `> span` rules above
   and after them, because those set every bare span to order 2 and --muted. */
.gram-grid-title > span.gram-grid-tally { margin-left: auto; order: 3; flex: 0 0 auto;
  font-size: 11px; letter-spacing: 0.08em; font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.gram-grid-title .meaning-line { order: 4; flex: 0 0 100%; margin: 2px 0 0;
  font-size: 13px; color: var(--muted); font-style: italic; }
/* La portada — «què vol dir?» before the six persons. Elevation 0 by intent:
   it is a question you read and answer in your head, not a raised, tappable
   thing (raised = actionable). The one raised control on
   the surface is the button that leaves it. */
.gram-portada { max-width: var(--w-focus); margin: 0 auto; padding: var(--space-3) 0; }
.gram-portada h2 { font-family: var(--display); font-size: 28px; line-height: 1.2;
  margin: var(--space-2) 0 var(--space-3); }
.gram-portada .gloss { margin-bottom: var(--space-5); }
.gram-portada .meaning-line { font-family: var(--display); font-style: italic; font-size: 21px; }
.gram-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; }
.gram-cell { min-height: 58px; padding: 6px; border-radius: 9px; color: var(--text);
  border: 1px dotted var(--muted); background: transparent; cursor: pointer; }
.gram-cell small, .gram-cell-answer { display: block; }
.gram-cell small { color: var(--muted); font-size: 10px; }
/* A revealed form must never break out of its cell: the roster's cards are the
   narrowest place a paradigm lands, and long forms (començaríeu, coneixeríem)
   overflowed the card once every fantasma was peeked. `anywhere` is the last
   resort — the roster is sized so it should not fire (20 Jul 2026). */
.gram-cell { min-width: 0; }
.gram-cell-answer { margin-top: 2px; font-size: 14px; font-weight: 650;
  overflow-wrap: anywhere; hyphens: none; }
/* The three cell states read as a POSITIVE ladder (20 Jul 2026). `ferm` used to
   be a black border (`--text`) over a dark inset rule — by far the heaviest mark
   on the surface, which made a mastered form look sealed off rather than won,
   while the un-mastered fantasma sat light and open. Exactly backwards.
   Now the ink warms as you earn it: faint dotted (not yet) → warm solid
   (started) → green glaze + green underline (yours). Green is already the app's
   «done» hue (`--tile-green-ink` marks a finished camí step), so this joins that
   family instead of inventing a third vocabulary for completion.
   The glaze is a background, so the answer keeps writing in `--text`; nothing
   here puts small text on a raw hue. */
.gram-cell.state-traçat { border-style: solid;
  border-color: color-mix(in srgb, var(--tile-amber) 45%, transparent);
  background: color-mix(in srgb, var(--tile-amber) 9%, transparent); }
/* Ferm is the state you WANT, so it has to look it: against the warm amber of
   traçat a pale sage read as the drabbest tile on the card, which is the same
   mistake as the old black border wearing different paint. It wears
   --tile-green-VIU, the brighter «verd llapis», not the camí's moss — see the
   token's note for why the two greens stay separate. */
.gram-cell.state-ferm { border-style: solid;
  border-color: color-mix(in srgb, var(--tile-green-viu) 66%, transparent);
  background: color-mix(in srgb, var(--tile-green-viu) 15%, var(--surface));
  box-shadow: inset 0 -2px 0 var(--tile-green-viu);
  animation: gram-ink 260ms ease-out; }
@keyframes gram-ink { from { opacity: 0.45; filter: blur(1px); } to { opacity: 1; filter: none; } }
.gram-grid-card > .btn { margin-top: 8px; padding-left: 0; }
.gram-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }

.session-stage--estudi { --session-tone: var(--tile-blue); }
.gram-study-card { margin-top: 14px; padding: 20px; border-radius: 14px;
  background: var(--paper); border: 1px solid color-mix(in srgb, var(--session-tone) 24%, var(--edge-soft));
  box-shadow: var(--shadow); }
.gram-study-card h2 { margin-top: 0; font-family: var(--display); }
.gram-study-card > .btn-primary { margin-top: 10px; }
.gram-application { min-height: 130px; resize: vertical; }
.gram-recital { display: grid; gap: 12px; }
.gram-grid-mini { position: sticky; top: 4px; z-index: var(--z-sticky); }
.gram-grid-mini .gram-cell { min-height: 46px; }
/* R5 — the recital opts into lay-browse (§2c); it holds a single paradigm and a
   player, so cap both to --w-focus centred — the widened container just gives
   the sticky deck breathing room, the question never widens. Inert below 900. */
@media (min-width: 900px) {
  .gram-recital .gram-grid-mini,
  .gram-recital-player { max-width: var(--w-focus); margin-inline: auto; width: 100%; }
}

@media (max-width: 480px) {
  /* The tabs already wrap 3-up here on their own 104px floor; the doors stack
     because 200px + 200px + the gap does not fit. Both are the flex rules
     doing their job, so neither needs a rule of its own any more. */
  .gym-hero { padding: 22px 16px; }
  .gram-roster-head h3 { font-size: 22px; }
  .gram-roster-head { display: block; }
  .gram-roster-head .footnote { display: block; margin-top: 3px; text-align: left; }
  .gram-gym-live .gram-grid { grid-template-columns: repeat(3, 1fr); }
  /* The head stacks here, and `flex-basis` follows the direction: the title
     block's 420px basis is a HEIGHT once the row becomes a column, which put
     a 420px hole between the subtitle and the chip. */
  .gram-unit-head { flex-direction: column; align-items: flex-start; }
  .gram-unit-titol { flex: 0 0 auto; }
  .gram-unit-head > .unit-chip { align-self: flex-start; }
  .gram-unit-row { margin-left: 24px; align-items: flex-start; }
  .gram-unit-row .unit-chip { max-width: 120px; text-align: center; }
  .gram-chapter-num { font-size: 34px; }
  .gram-chapter-head h2 { font-size: 25px; }
  .gram-example { padding: 15px 17px; }
  .gram-grid { grid-template-columns: repeat(2, 1fr); }
  .gram-cell { min-height: 52px; }
  .gram-actions .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .gram-cell.state-ferm { animation: none !important; }
  .gram-grid-deck { scroll-behavior: auto; }
}

@media (max-width: 480px) {
  .prompt { font-size: 18px; }
  .skill-row { grid-template-columns: 1fr 1.4fr 30px; }

  /* tighter chrome, safe-area aware */
  .topbar { padding: 12px 14px 6px; gap: 8px; }
  main { padding: 6px 14px calc(78px + env(safe-area-inset-bottom)); }

  /* Five llocs sit comfortably at 375px ("Pràctica" is the longest label);
     the old seven-tab squeeze (11.5px, negative tracking) is gone. */
  .tabbar { gap: 2px; justify-content: space-around;
    padding: 6px max(4px, env(safe-area-inset-left))
             max(8px, env(safe-area-inset-bottom)) max(4px, env(safe-area-inset-right)); }
  .tab { flex: 1 1 0; min-width: 0; min-height: 44px; padding: 8px 4px;
    font-size: 14px; white-space: nowrap; }

  /* ≥44px tap targets */
  .btn { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
  .btn-small { min-height: 40px; }
  .btn-primary, .btn-comenca, .btn-repte { min-height: 48px; }
  .scenario-row .btn, .listen-card .btn { min-height: 44px; }

  .exercise { padding: 18px 16px; }
  .cloze-line { font-size: 18px; line-height: 1.72; }
  /* `.greeting`/`.porta` used to be capped here; both are fluid in v2 (the
     greeting clamps, the porta is the page and carries no card padding). */

}

/* ============ visual rebuild: trencadís system ============ */

/* ---- La porta v2 (design/handoffs/avui-v2, 5 Aug 2026) ----
   The doorstep stops being one narrow stacked card and becomes the PAGE: a
   column of zones on a faint accent wash, with the glass moved down to the one
   zone that earns it. The greeting goes to display scale, el fil leads inside
   the hero, and the living mosaic is promoted out of the card's foot into an
   arch niche beside it. Everything the porta SAYS is untouched — the slot
   logic, the one-primary rule, what el fil may claim, the in-place session
   swap. Four places the handoff was deliberately not followed, each recorded
   where it bites: the niche does not replay the tile-laying (see avui.js
   `mosaicNiche`), la plaça keeps its place above the featured slot (see
   avui.js's zone order), la frase's two marks stay the app's one shared mark
   (see .frase-say below), and the page widens by naming an archetype rather
   than by a raw pixel value (docs/LAYOUT.md outranks a handoff). */

/* THE PAGE. The wash rides the COMPOSITION rather than the viewport: a
   chrome-level background would have to be added and removed around the
   in-place session swap (startSession replaces the porta without routing), and
   that coupling is not worth a 4 % tint. Which is exactly why it is a RADIAL
   fading to transparent rather than the handoff's `linear-gradient(… , var(--bg)
   360px)`: an opaque wash bounded by the composition draws two vertical seams
   down the page wherever the window is wider than the canvas, and measured on a
   1280px window they were plainly visible. Same warmth at the top, no edges
   anywhere. lay-canvas, not the handoff's raw 1080: a surface widens only by
   naming an archetype — Progrés v2's ruling, same week, same reason. */
.porta { position: relative; display: flex; flex-direction: column;
  gap: clamp(18px, 2.5vw, 26px); margin-top: 4px; padding: clamp(8px, 1.6vw, 18px) 0 32px;
  background: radial-gradient(62% 460px at 50% -40px,
    color-mix(in srgb, var(--accent) 8%, transparent),
    color-mix(in srgb, var(--accent) 0.4%, transparent) 70%); }
/* The zones rise in order behind `.is-entrant`, filling BACKWARDS; renderAvui
   drops the class on a settle timer whether or not a frame was ever served (the
   mosaic's rule — Progrés, la paret, el gimnàs and the camí all pay for it). */
.porta.is-entrant > * { animation: porta-puja var(--dur-3) var(--ease-glass) backwards; }
.porta.is-entrant > .frase-epigraf { animation-delay: 0.15s; }
.porta.is-entrant > .porta-feature { animation-delay: 0.24s; }
.porta.is-entrant > .porta-lema { animation-delay: 0.32s; }
@keyframes porta-puja { from { opacity: 0; transform: translateY(14px); } }
@media (prefers-reduced-motion: reduce) {
  .porta.is-entrant > * { animation: none !important; }
}
/* R11 — Avui optical centring at tall viewports (docs/DESKTOP-UX-PLAN.md §4).
   Scoped to .porta ONLY: the in-place session swap (startSession clears .porta
   and mounts the session directly) must stay top-anchored — vertically centring
   the live card is an anti-recommendation (review §11). Inert below 900px wide
   or 800px tall. */
@media (min-width: 900px) and (min-height: 800px) { .porta { margin-top: 4vh; } }
/* The lockup lives in the global topbar, so the porta's own header row is the
   date alone — right-aligned, which is where the handoff's composition puts it
   opposite the mark. */
.porta-head { display: flex; justify-content: flex-end; align-items: center; }
.porta-date { font-size: 13px; color: var(--muted); text-transform: lowercase; }
.porta-pica { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }

/* ---- Zone 1 · the hero ----
   The app's ONE vidre per view (elevation 3), and unlike the camí's and
   Progrés's heroes this one really does sit over something — the page's own
   accent wash — so the blur has a job here it did not have there. Radius is
   --radius-l like all four v2 heroes rather than the handoff's 26px: the rooms
   must open the same way, and 4px is not worth a fifth answer. */
.porta-hero { position: relative; overflow: hidden; isolation: isolate;
  border-radius: var(--radius-l); padding: clamp(26px, 4vw, 48px);
  border: 1px solid color-mix(in srgb, var(--rim) 60%, var(--edge-soft));
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  box-shadow: var(--shadow-2); }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .porta-hero { background: var(--surface); }
}
/* Soft-light for the reason measured on the Progrés hero: straight alpha over a
   surface this size lifts the ground enough to push --muted ink under AA. */
.porta-hero .hero-lluu { position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; background: var(--sheen); mix-blend-mode: soft-light; }
.porta-hero-cols { position: relative; display: flex; flex-wrap: wrap;
  align-items: center; gap: clamp(28px, 4.5vw, 60px); }
.porta-hero-copia { flex: 1.35 1 340px; min-width: min(340px, 100%);
  display: flex; flex-direction: column; gap: 18px; }
.porta-salutacio { display: flex; flex-direction: column; gap: 8px; }
.greeting { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(40px, 5.4vw, 58px); line-height: 1.02; letter-spacing: 0.005em;
  text-wrap: balance; }
/* The signature flourish: the name, and only the name, in italic accent ink. */
.greeting-nom { font-style: italic; color: var(--accent-ink); }
.porta-today { font-size: 15.5px; color: var(--text); margin: 0; }
/* --accent-ink, not raw --accent: law 1 — a hue is a glaze, its ink is what
   writes at this size. */
.porta-today strong { color: var(--accent-ink); font-weight: 600; }
/* ---- Zone 2 · la frase del dia ----
   An EPIGRAPH, never a card (20 Jul 2026): raised is actionable, and a phrase
   is read, not acted on. v2 moves it from a left-indented quote to a centred
   epigraph under its own overline — the same idea, given the width the page now
   has. `.gloss` is display:contents so glossReveal's wrapper doesn't box the
   button and the panel together: they are separate cells of THIS grid. */
.frase-epigraf { display: grid; grid-template-columns: auto auto auto;
  justify-content: center; align-items: center; column-gap: 10px; row-gap: 6px;
  margin: 0; padding: clamp(6px, 1vw, 12px) 0; text-align: center; }
.frase-epigraf .gloss { display: contents; }
.frase-epigraf > .frase-label { grid-column: 1 / -1; letter-spacing: 0.14em; }
.frase-epigraf .frase-ca { grid-column: 1; max-width: 46ch;
  font-size: clamp(19px, 2.3vw, 23px); text-wrap: balance; }
.frase-epigraf .meaning-line { grid-column: 1 / -1; justify-self: center; max-width: 52ch;
  font-size: 13.5px; color: var(--muted); margin: 0; }
.frase-label { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted-firm); }
.frase-ca { font-family: var(--display); font-style: italic; font-size: 19px; margin: 0;
  line-height: 1.35; }
.frase-gloss { font-size: 13px; color: var(--muted); margin: 0; }
/* Both controls are quiet marks beside the phrase, not buttons on it. 32px is
   under the 44px target floor by intent — they sit inside a reading line, and
   the phrase is never the primary action on the porta. */
/* The compact «?» is UNSCOPED (20 Jul 2026): `compact` is an option of the one
   shared glossReveal, so its mark is defined once and every caller wears the
   same one. It shipped scoped to .frase-epigraf when the epigraph was the only
   caller; the gimnàs roster is the second, and restating the look there would
   have made one component render as two different things. .frase-say keeps
   riding the same rule — the two marks are peers on the porta's phrase line.
   THIS IS WHY the v2 handoff's 34px bordered circles were NOT adopted: they
   would have made the epigraph's «?» a different object from the gimnàs
   roster's, on a surface this handoff never looked at (the gimnàs v2 ruling).
   The open state stays the filled accent-soft wash, which is a stronger static
   twin than a border swap anyway. */
.frase-say, .btn-reveal.is-compact {
  display: grid; place-items: center; flex: none; width: 32px; height: 32px; padding: 0;
  border: none; border-radius: 50%; background: none; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 14px; line-height: 1;
  transition: color var(--dur-1) var(--ease-glass), background var(--dur-1) var(--ease-glass); }
.frase-say:hover, .btn-reveal.is-compact:hover {
  color: var(--accent-ink); background: color-mix(in srgb, var(--accent-soft) 70%, transparent); }
.btn-reveal.is-compact[aria-expanded="true"] {
  color: var(--accent-ink); background: var(--accent-soft); }
/* Full width on the phone, auto in the desktop row (6 Aug 2026). v2 made the
   round auto-width for the ≥900 row beside the readiness line — but that rule
   leaked down, and at 375px the pill sat as a lone island with the cost line
   hanging ragged off its left. On one column the round spans it, exactly like
   the fil's own CTA above it, so the two actions read as one aligned stack.
   The ≥900 half lives with .porta-ronda below. Keeps its 48px floor from the
   mobile block. */
.btn-comenca { width: 100%; padding: 14px 30px; font-size: 17px; margin: 0; }
/* ---- El fil (22 Jul 2026) ---- */
/* The porta's lead: where you are on the course, and one tap back into it. It
   is nivell 1 (esmaltat) inside the hero's vidre — nesting de-escalates — with
   an accent-tinted rim and a low accent glow so the eye lands here first
   without a second full-width fill competing with the CTA it holds. The card is
   static: no animation of its own, the hero's zone rise carries it in. */
.porta-fil { display: grid; gap: 10px; margin: 0; box-sizing: border-box;
  padding: clamp(18px, 2.5vw, 26px) clamp(18px, 3vw, 28px);
  border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--edge-soft));
  border-radius: 18px; background: var(--glass-1);
  box-shadow: inset 0 1px 0 var(--edge),
    0 3px 14px -8px color-mix(in srgb, var(--accent) 32%, transparent); }
/* The exam rung (S4, 24 Jul 2026) wears the camí's own gold on the same card
   shape — the frame changes, the furniture does not, so it reads as the last
   step of the thread rather than a different kind of card. The CTA inside it
   stays the app's one accent primary: gold is the level's colour, not a second
   button style, and the porta has exactly ONE primary whatever it says. */
.porta-fil.is-examen { border-color: color-mix(in srgb, var(--daurat) 40%, var(--edge-soft));
  background: color-mix(in srgb, var(--daurat) 9%, var(--glass-1));
  box-shadow: inset 0 1px 0 var(--edge),
    0 3px 14px -8px color-mix(in srgb, var(--daurat) 32%, transparent); }
.porta-fil.is-examen .fil-label { color: var(--daurat-ink); }
/* The overline IS the ladder position now («UNITAT 13 · B1» — 6 Aug 2026,
   Ryan's ask): the card slimmed to overline · title · CTA, so the position
   moved up here from the `.fil-lloc` tail the title used to drag, and «EL FIL»
   survives only as the fallback for a unit on no spine. */
.fil-label { margin: 0; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 600; color: var(--accent-ink); }
.fil-unit { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(24px, 2.6vw, 29px); line-height: 1.1; color: var(--text); }
/* The exam rung's alone since the diet: «Queda la prova» is that card's whole
   claim. The everyday card says its state through the CTA's verb instead. */
.fil-sub { margin: 0; font-size: 14.5px; line-height: var(--lh-ui); color: var(--muted); }
/* The flow bar became the camí hero's step tessel·les (v2) — one component,
   `passosDelFlux`, so the two surfaces cannot draw one position two ways. */
.porta-fil .curs-hero-passos { margin: 2px 0; }
/* An anchor-button: it needs the centring .btn leaves to the flex fallback. It
   stays FULL WIDTH rather than the prototype's inline pill — it is the page's
   one primary, and a button that shrinks to its text reads as an option rather
   than the lead (the 22 Jul rule, unchanged by the restage). */
.fil-cta { display: inline-flex; align-items: center; justify-content: center;
  width: 100%; min-height: 48px; margin-top: 4px; text-align: center; }
/* The demoted round. A real, live control that is simply not the lead — never
   .btn-ghost (an underlined link inside a full-width button reads as an
   afterthought) and never the bare .btn track fill, which is the app's
   INACTIVE-background token and would grey out a button that works. */
.porta-floor { background: var(--glass-1); color: var(--text); font-weight: 600;
  font-size: 15.5px; padding: 12px 24px;
  border: 1px solid var(--edge-soft); box-shadow: var(--shadow-1); }
.porta-floor:hover { border-color: var(--rim); }
.porta-pendent { margin: 0; font-size: var(--fs-2); }
/* ---- the quiet recommendation row ----
   R9's idiom (docs/DESKTOP-UX-PLAN.md §3b), and since Pràctica v2 (5 Aug 2026)
   it belongs to LA PORTA alone, which is why the rules moved here from the
   practica block: a single link on a raised surface (it IS something you act
   on), but no accent fill, so it never reads as a second primary. The hub's own
   recommendation was promoted into its hero and wears the plaque shape instead
   — one recommendation either way, two rooms it can stand in. */
.practica-recomanat { display: flex; align-items: center; gap: 10px;
  margin: 6px 0 10px; padding: 12px 14px; border-radius: 14px; color: inherit;
  background: var(--glass-2); border: 1px solid var(--edge-soft); box-shadow: var(--shadow);
  text-decoration: none; -webkit-tap-highlight-color: transparent;
  transition: border-color var(--dur-1) var(--ease-glass); }
.practica-recomanat:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--edge-soft)); }
@media (prefers-reduced-motion: reduce) { .practica-recomanat { transition: none; } }
.practica-recomanat-eyebrow { flex-shrink: 0; font-size: 11px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted); }
.practica-recomanat-copy { flex: 1; font-size: 14px; min-width: 0; }
.practica-recomanat-arrow { flex-shrink: 0; color: var(--accent); }
/* Nested in the hero: one level quieter on this ground. */
.porta .practica-recomanat { margin: 0; background: var(--glass-1);
  box-shadow: var(--shadow-1); }
/* The round and what it costs. MOBILE-FIRST STACK (6 Aug 2026): on one column
   the button spans it and the cost line centres quietly beneath — the v2 row
   (button beside the line) exists only at ≥900, where the hero's two columns
   give a row a line to sit on. Below that width the row wrapped anyway, and
   what it produced was the island-pill + ragged caption the porta was reported
   for. Same specificity both tiers, co-located (the layout rule). */
.porta-ronda { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
.porta-readiness { display: flex; align-items: center; justify-content: center; gap: 12px;
  font-size: 13.5px; color: var(--muted); margin: 0; }
@media (min-width: 900px) {
  .porta-ronda { flex-direction: row; align-items: center; gap: 14px; flex-wrap: wrap; }
  .porta-readiness { justify-content: flex-start; }
  .btn-comenca { width: auto; }
}
.porta-fet { margin: 0; }
/* The sound toggle left the porta on 7 Aug 2026 — a preference has no place on
   the doorstep, and on a day with due reviews it was the readiness line's only
   occupant. It is chrome now: .so-toggle, beside the theme and the menu
   language. Its block is deleted, not parked. */
/* Zones 4 and 5 — el llindar (la plaça's doorway) and the extra-round door —
   left the porta on 6 Aug 2026: Món's ensemble carries la colla at top billing
   now, and the backlog door went with the library-not-debt framing. Their
   blocks are deleted, not parked. */
.porta-feature { margin: 0; }
.porta-feature > :only-child { margin-top: 0; margin-bottom: 0; }
/* ---- The closing motto (shared with the camí) ---- */
.porta-lema { margin-top: 6px; }

/* ---- Zone 1b · the mosaic niche ----
   The mosaic, promoted out of the card's foot into a Modernisme arch beside the
   greeting. The niche is a SUNK well inside the raised hero — nesting
   de-escalates — so the inner `.mural` plaque flattens: the arch is the frame
   now, and a raised plaque inside a sunk well inside a vidre card is the
   Russian doll the porta has always refused. */
.porta-mosaic { flex: 1 1 300px; min-width: min(300px, 100%);
  display: flex; flex-direction: column; align-items: center; gap: 14px; }
.porta-mosaic-label { margin: 0; text-align: center; color: var(--muted);
  font-size: 11px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; }
.porta-ninxol { position: relative; width: min(340px, 100%); box-sizing: border-box;
  display: flex; flex-direction: column; align-items: center;
  padding: clamp(30px, 4vw, 44px) clamp(24px, 3vw, 34px) 24px;
  border: 1px solid var(--edge-soft); border-radius: 170px 170px var(--radius-l) var(--radius-l);
  background: var(--surface-sunk); box-shadow: inset 0 2px 6px rgba(42, 32, 25, 0.07); }
.ninxol-or { position: absolute; inset: 8px; pointer-events: none;
  border: 1px solid color-mix(in srgb, var(--daurat) 26%, transparent);
  border-radius: 162px 162px 16px 16px; }
.ninxol-llum { position: absolute; top: 14%; left: 50%; transform: translateX(-50%);
  width: 70%; aspect-ratio: 1; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--accent) 9%, transparent), transparent 70%); }
/* The block is the shared `mosaicBlock`, unforked: the completed shelf and the
   door into la Galeria ride with it (that door is one of la Galeria's only two,
   and app.cameFrom routes its sortida back here). `order` sends them to the
   arch's wide foot so the curve holds nothing but the art and its caption. */
.porta-ninxol .mural-wrap { position: relative; display: flex; flex-direction: column;
  align-items: center; gap: 10px; width: 100%; margin: 0; }
.porta-ninxol .mosaic-active { order: 1; display: block; width: min(250px, 84%); }
.porta-ninxol .mural { padding: 0; border: 0; border-radius: 0; background: none;
  box-shadow: none; }
.porta-ninxol .mosaic-svg { width: 100%; height: auto; }
.porta-ninxol .mosaic-cap { order: 2; margin: 0; font-size: 12.5px;
  font-variant-numeric: tabular-nums; }
.porta-ninxol .mosaic-shelf { order: 3; margin: 4px 0 0; gap: 6px; }
.porta-ninxol .mosaic-shelf svg { width: 34px; height: auto; }
.porta-ninxol .mosaic-galeria-link { order: 4; margin: 0; }

/* ---- the mural ---- */
.mural-wrap { text-align: center; margin: 4px 0 18px; }
.mural { display: inline-block; line-height: 0; padding: 10px;
  background: color-mix(in srgb, var(--bar-track) 25%, transparent); border-radius: 14px; }
.mural svg { display: block; }
.tile-laid { animation: tile-in 0.4s cubic-bezier(0.34, 1.4, 0.5, 1) both; }
@keyframes tile-in { from { opacity: 0; transform: scale(0.4); } to { opacity: 1; } }
.mural-cap { font-size: 12px; color: var(--muted); margin: 8px 0 0; font-style: italic; }
/* EL DIA ZERO · the niche and the vitrine with nothing laid in them. `.buit`
   carries the voice; this only fits it into a small box — the pattern's own
   --fs-5 and --space-6 padding are sized for a page-width empty state, and the
   porta's arch is 250px wide. `order: 1` puts it where the art would be: the
   niche lays its children out with `order` (art 1 · cap 2 · shelf 3 · link 4),
   so an unordered child would otherwise jump ahead of all of them. */
.mosaic-buit { margin: 0; padding: var(--space-3) var(--space-2);
  font-size: var(--fs-3); }
/* It stands in the ART'S OWN BOX, not in the box its sentence happens to need.
   The niche and the vitrine are both sized by what they hold, so a bare line
   collapsed the porta's arch to a half-height dome — the surface must not
   change SHAPE on the one visit that has nothing in it yet. Same width as the
   art it replaces, a neutral landscape ratio in place of the def's own, and
   the text centred inside it. */
.porta-ninxol .mosaic-buit { order: 1; width: min(250px, 84%); aspect-ratio: 6 / 5;
  display: flex; align-items: center; justify-content: center; }
/* La Galeria's card gets a FLOOR, not the art's box: there is no shape to
   preserve here (the porta's arch is a drawn niche, this is a plain card), and
   a full 300×250 of held space around one sentence reads as a void rather than
   as a beginning — with `noneYet` saying the same thing again just below it. */
.galeria-current .mosaic-buit { width: min(300px, 100%); min-height: 108px;
  display: flex; align-items: center; justify-content: center; }

/* ---- ink stamp verdict ---- */
.stamp { display: inline-block; font-family: var(--display); font-weight: 700;
  font-size: 26px; letter-spacing: 0.02em; color: var(--tile-terra);
  border: 3px solid var(--tile-terra); border-radius: 10px; padding: 2px 16px;
  transform: rotate(-7deg); opacity: 0.9; margin: 2px 0 4px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tile-terra) 40%, transparent);
  animation: stamp-in 0.28s cubic-bezier(0.3, 1.5, 0.5, 1) both; }
@keyframes stamp-in {
  0% { transform: rotate(-7deg) scale(1.9); opacity: 0; }
  60% { opacity: 0.95; }
  100% { transform: rotate(-7deg) scale(1); opacity: 0.9; }
}
/* ---- l'adhesiu — the die-cut correct-answer sticker (actius de marca 1f) ----
   A sticker, not a control: the tilt, the white die-cut border and the soft
   cast are `.seal`'s family (a mark laid ON the work), which is why it is a
   plain div and carries no hover, focus or pointer. Raised = actionable is not
   contradicted, because nothing here is tappable.

   The ground is a fill or a glaze and the ink is chosen per tone — never white
   on a raw hue. Green and rose deepen to their -fill so white clears AA; amber
   and cream stay glazes and write --pale-ink instead, because both are luminous
   in BOTH themes. Deliberately NOT --tile-amber-ink for the amber one: that
   token is for amber TEXT on an ordinary ground and resolves to raw amber in
   dark, which would paint amber on amber. */
.adhesiu { display: inline-flex; align-items: center; gap: 9px;
  padding: 10px 18px; border-radius: var(--radius-pill, 999px);
  border: 4px solid #fff; font-weight: 700; font-size: 15px; line-height: 1.2;
  box-shadow: 0 6px 14px -8px rgba(42, 32, 25, 0.45);
  transform: rotate(var(--adhesiu-gir, -2deg)); margin: 2px 0 6px;
  animation: adhesiu-in 0.26s cubic-bezier(0.3, 1.5, 0.5, 1) both; }
.adhesiu .ico { flex: 0 0 auto; }
.adhesiu-verd  { background: var(--tile-green-fill); color: #fff; }
.adhesiu-rosa  { background: var(--rose-fill); color: #fff; }
.adhesiu-crema { background: var(--tile-cream); color: var(--pale-ink); }
.adhesiu-ambre { background: var(--tile-amber); color: var(--pale-ink); }
@keyframes adhesiu-in {
  0% { transform: rotate(var(--adhesiu-gir, -2deg)) scale(0.7); opacity: 0; }
  100% { transform: rotate(var(--adhesiu-gir, -2deg)) scale(1); opacity: 1; }
}
/* The form he was reaching for, moved off the headline when it became a
   sticker. Neutral ink: the adhesiu above has already given the verdict, and a
   second coloured line would read as a second one. */
.verdict-forma { margin: 2px 0 0; font-size: 15px; color: var(--text-secondary); }
@media (prefers-reduced-motion: reduce) {
  .adhesiu { animation: none; }
}

.feedback.ok { border-left-color: var(--tile-green); }

/* ---- inline cloze ---- */
/* THE LEADING BELONGS TO THE SENTENCE, NOT TO THE GAP IN IT (9 Sep 2026).
   This ran at 2.1 (2.0 on a phone) for one reason: the gap is an inline input
   with a rule under it and padding round it, and at prose leading a stack of
   them collided. That is a fix at the wrong altitude — it pays for ONE object
   by inflating EVERY line, including the ones with no gap on them. Measured on
   Ryan's own «suficient» card: a four-line prompt stood 171px tall, a third of
   the band a keyboard leaves, and the gaps between the lines were wider than
   the words, so the question read as four fragments rather than one sentence.
   The gap owns its own box now (`line-height` + tight padding below), so its
   margin box lands INSIDE the line box and inflates nothing; the paragraph is
   free to sit at a reading leading. Same card: 171px -> 123px, and it reads as
   prose. If the gap's padding is ever raised, check it still fits the line box
   rather than pushing this number back up. */
.cloze-line { font-size: 20px; line-height: 1.72; }
.cloze-gap { font: inherit; font-size: 20px; line-height: 1.25; color: var(--accent); font-weight: 600;
  text-align: center; background: transparent; border: none;
  border-bottom: 2px solid var(--accent); padding: 2px 5px; margin: 0 2px;
  /* an INLINE underlined gap, not a field: undo the system input box
     (min-height 44 / 10px radius / focus halo would turn it into a pill) */
  min-height: 0; border-radius: 0; box-shadow: none;
  width: 8ch; max-width: 100%; min-width: 0; outline: none; caret-color: var(--accent); }
.cloze-gap:focus, .cloze-gap:focus-visible { outline: none; border-bottom-color: var(--tile-amber); box-shadow: none;
  background: color-mix(in srgb, var(--accent-soft) 60%, transparent); border-radius: 4px 4px 0 0; }

@media (prefers-reduced-motion: reduce) {
  .stamp, .tile-laid { animation: none !important; }
  body { transition: none !important; }
}

/* (the landscape colours — --map-parch/hill/ink — are system tokens now) */

/* ---- fase banner, el repte, mural wall ---- */
.fase-banner { background: linear-gradient(135deg, var(--accent-soft), transparent);
  border: 1px solid var(--accent); border-radius: 14px; padding: 10px 16px;
  margin: 4px 0 10px; animation: rise 0.4s ease-out; }
/* 17px/700 is under the large-text bar, and raw accent on the soft tint is
   4.04:1 — the ink writes (5.15/5.09). The .ico beside it stays raw accent:
   a glyph is non-text UI and passes at 3:1. */
.fase-title { font-family: var(--display); font-weight: 700; color: var(--accent-ink);
  font-size: 17px; margin: 0; }
.fase-opens { font-size: 14px; color: var(--muted); margin: 2px 0 0; }

.btn-repte { width: 100%; margin-top: 10px; background: var(--surface);
  border: 2px solid var(--tile-amber); color: var(--text); font-weight: 600; }
/* El repte reads in the session player's shape: a header ROW (sortida · chip ·
   gauges) and then the question on its own card. It used to be ONE glazed
   .repte card with zero padding wrapping a SECOND glazed head panel — a card
   inside a card, both full-bleed, with the prompt and the answer field sitting
   flat on the outer card's border. .repte is a bare wrapper now (it left the
   raised-surface list); .repte-stage is the card, matching .exercise. */
.repte-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 14px;
  margin: 14px 0 0; }
/* The sprint's one heading is the «EL REPTE» chip (A5): pin the block-label
   look so the h1 promotion is visually inert (UA h1 size/margins never leak). */
.repte-head h1 { margin: 0; font-size: inherit; }
/* Two gauges, the duo scoreboard's shape: a big number over its caption. Bare
   numbers never said which of «58» and «0» was the clock. */
.repte-gauges { display: flex; align-items: flex-start; gap: 22px; margin-left: auto; }
.repte-gauge { text-align: center; min-width: 52px; }
.repte-gauge-label { font-size: 12px; letter-spacing: 0.06em; color: var(--muted);
  margin-top: -2px; }
.repte-clock { font-family: var(--display); font-size: 32px; font-weight: 700;
  line-height: 1.1; color: var(--tile-amber-ink); }
.repte-clock.urgent { color: var(--miss); }
.repte-score { font-family: var(--display); font-size: 32px; font-weight: 700;
  line-height: 1.1; color: var(--accent); }
.repte-stage { border-radius: var(--radius); padding: 24px; margin-top: 14px; }
.repte-stage .prompt { font-size: 21px; margin: 0 0 4px; }
.repte-flash { font-size: 17px; font-weight: 700; color: var(--miss); margin: 12px 0 0; }
/* Small phones: the head still wraps if it must (flex-wrap is the floor), but
   shrinking the two numbers keeps chevron · chip · gauges on one row down to
   ~320px, which is where the sprint is actually played. */
@media (max-width: 480px) {
  .repte-stage { padding: 20px 16px; }
  .repte-clock, .repte-score { font-size: 27px; }
  .repte-gauges { gap: 16px; }
  .repte-gauge { min-width: 44px; }
}

/* ---- L'examen de nivell (docs/GRADUACIO-PLAN.md §4, §7) ----
   The exam borrows the session player's shape — a header ROW, a progress bar,
   then the question on its own .exercise card — and adds exactly two things of
   its own: a countdown, and a results screen whose per-area bars carry the 50 %
   line as a visible mark. Nothing here ever paints a verdict on an answer: the
   loop has no feedback surface at all, by design. */
/* The view's own h1, pinned to the app's landing-title size (the .dashboard h1
   rule, which this surface is not inside): the UA 2em default would otherwise
   leak on the rubric and the results screen. */
.examen h1 { font-family: var(--display); margin: 14px 0 4px; font-size: 1.5em; }
.examen h2 { font-family: var(--display); font-size: 1.17em; }
.examen-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px;
  margin: 14px 0 0; font-size: 13px; color: var(--muted); }
.examen-head .block-label { min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.examen-loop .bar { margin-top: 10px; }
.ex-gauges { display: flex; align-items: baseline; gap: 14px; margin-left: auto; }
.ex-count { font-variant-numeric: tabular-nums; }
/* The clock is the repte gauge's face at a calmer size — this one runs for half
   an hour, not sixty seconds, so it must sit in the chrome rather than lead it. */
.ex-clock { font-family: var(--display); font-size: 24px; font-weight: 700;
  line-height: 1.1; color: var(--tile-amber-ink); font-variant-numeric: tabular-nums; }
.ex-clock.urgent { color: var(--miss); }

/* The rubric: what it covers, what it costs, what the bar is — all of it before
   the clock starts (§4.2). */
.ex-rubric-head { font-family: inherit; font-size: 13px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted); margin: 20px 0 6px; }
.ex-rubric { list-style: none; padding: 0; margin: 0 0 18px; }
.ex-rubric-area { display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line); }
.ex-rubric-name { font-weight: 600; }
.ex-rubric-meta { font-size: 13px; color: var(--muted); white-space: nowrap; }
.ex-rule { font-size: 15px; margin: 6px 0; }

/* A reading passage: paper, not a card — it is the thing being read, and its
   questions come below it on their own card. */
.ex-passage { background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 18px 20px; margin-top: 14px; }
.ex-passage-label { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--muted); font-weight: 600; margin: 0 0 6px; }
.ex-passage-text { margin: 0; line-height: var(--lh-body); white-space: pre-wrap; }

.ex-audio { display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  margin-bottom: 14px; }
.ex-audio .footnote { margin: 0; }

.ex-points-head { font-size: 13px; color: var(--muted); margin: 0 0 4px; }
.ex-points { margin: 0 0 12px; padding-left: 20px; font-size: 15px; }
.ex-points li { margin: 2px 0; }
.ex-write { min-height: 220px; font-size: 17px; line-height: var(--lh-body);
  resize: vertical; }
.ex-words { text-align: right; margin: 4px 0 0; }
.ex-oral-note { font-size: 15px; color: var(--muted); margin: 0; }
.ex-leave h2 { font-family: var(--display); margin: 0 0 6px; }

/* The self-mark interstitial: his text beside the model's, then the task's own
   checks. The checkbox is deliberately plain — the honest control for a yes/no
   he is answering about himself. */
.ex-texts { display: grid; gap: 12px; margin: 4px 0 16px; }
.ex-text { background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius-m); padding: 12px 14px; }
.ex-text-label { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--muted); font-weight: 600; margin: 0 0 4px; }
.ex-text-body { margin: 0; line-height: var(--lh-body); white-space: pre-wrap; }
.ex-checks { list-style: none; padding: 0; margin: 0; }
.ex-check { padding: 8px 0; border-top: 1px solid var(--line); }
.ex-check-label { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
.ex-check-box { width: 20px; height: 20px; margin: 1px 0 0; flex: 0 0 auto; accent-color: var(--accent); }
.ex-check-how { font-size: 13px; color: var(--muted); margin: 4px 0 0 30px; }

/* Results. The verdict is a word, never a colour wall: «Encara no» wears the
   ordinary ink and the AREA MAP carries the information (§10). */
.ex-apte, .ex-encara { font-family: var(--display); margin: 10px 0 2px; }
.ex-apte { color: var(--tile-green-viu-ink); }
.ex-overall { font-size: 17px; font-weight: 600; margin: 0 0 16px; }
.ex-areas { display: grid; gap: 14px; margin-bottom: 18px; }
.ex-area-head { display: flex; justify-content: space-between; align-items: baseline;
  gap: 10px; font-size: 15px; }
.ex-area-name { font-weight: 600; }
.ex-area-pct { color: var(--muted); font-size: 13px; white-space: nowrap; }
.ex-area-track { position: relative; height: 10px; margin-top: 5px; border-radius: 5px;
  background: var(--bar-track); overflow: hidden; }
.ex-area-fill { height: 100%; border-radius: 5px; background: var(--tile-green-viu); }
.is-sota .ex-area-fill { background: var(--miss); }
/* The 50 % line, drawn ON the track: an area figure means nothing without the
   bar it is being measured against. */
.ex-area-min { position: absolute; top: 0; bottom: 0; width: 2px;
  background: color-mix(in srgb, var(--text) 45%, transparent); }
.ex-area-note { font-size: 13px; color: var(--miss); margin: 4px 0 0; }

@media (min-width: 900px) {
  /* Two texts side by side once there is room — comparing them is the task. */
  .ex-texts { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 480px) {
  .ex-clock { font-size: 21px; }
  .ex-gauges { gap: 10px; }
}

.mural-shelf { display: flex; justify-content: center; gap: 10px; margin-bottom: 8px; }
.mural-shelf svg { border-radius: 8px; background: color-mix(in srgb, var(--bar-track) 25%, transparent);
  padding: 3px; opacity: 0.9; }

/* ---- la història (retell card) ---- */
.retell { background: linear-gradient(135deg, color-mix(in srgb, var(--tile-blue) 12%, transparent), transparent);
  border: 1px solid color-mix(in srgb, var(--tile-blue) 40%, transparent);
  border-radius: 14px; padding: 12px 16px; margin: 0 0 14px; }
.retell-label { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--tile-blue-ink); font-weight: 600; margin: 0; }
.retell-title { font-family: var(--display); font-style: italic; font-size: 19px; margin: 2px 0 6px; }
.retell-prompt { font-size: 15px; margin: 0 0 8px; }
.retell-box { width: 100%; font-size: 16px; resize: vertical; line-height: 1.5; }
.retell-lines { margin-top: 10px; padding-left: 12px; border-left: 3px solid var(--accent-soft); }
.retell-lines p { font-size: 15px; font-style: italic; color: var(--muted); margin: 4px 0; }
.retell .btn-ghost { text-decoration: none; }

/* ---- icon system + motion pass (visual overhaul) ---- */
.ico { vertical-align: -3px; }
.btn .ico, .footnote .ico, .capture .ico, .fase-title .ico { margin-right: 3px; }
.scenario-icon .ico { color: var(--accent); }
.capture .ico, .footnote .ico { color: var(--muted); }
.fase-title .ico { color: var(--accent); }

/* audio buttons breathe while Joana speaks */
.btn.speaking .ico { animation: speaking 0.9s ease-in-out infinite; transform-origin: center; }
@keyframes speaking {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.18); opacity: 0.75; }
}

/* view transitions: quick fade-slide between tabs */
.view-enter { animation: view-in 0.18s ease-out; }
@keyframes view-in {
  from { opacity: 0; transform: translateY(5px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .btn.speaking .ico, .view-enter {
    animation: none !important;
  }
}

/* ---- panel-completion shimmer (one-time sweep) ---- */
.mural-complete { position: relative; overflow: hidden; }
.mural-complete::after { content: ""; position: absolute; top: 0; bottom: 0;
  left: -60%; width: 45%; transform: skewX(-18deg);
  background: linear-gradient(90deg, transparent, rgba(255, 252, 240, 0.55), transparent);
  animation: shimmer 1.1s ease-out 0.5s 2; }
@keyframes shimmer { to { left: 130%; } }
.mural-cap-complete { font-family: var(--display); font-style: normal;
  font-weight: 700; color: var(--tile-amber-ink); font-size: 14px;
  letter-spacing: 0.08em; animation: rise 0.4s ease-out 0.3s both; }
@media (prefers-reduced-motion: reduce) {
  .mural-complete::after { animation: none; display: none; }
  .mural-cap-complete { animation: none; }
}


/* ---- V2 · el tacte del joc ---- */
/* rare stamp (5-streak) — bigger, double-ruled */
.stamp-rare { font-size: 30px; padding: 4px 22px; border-width: 4px;
  transform: rotate(-9deg); box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--tile-terra) 45%, transparent),
    0 0 0 3px var(--map-parch), 0 0 0 4px color-mix(in srgb, var(--tile-terra) 30%, transparent); }

/* wax seal for a flawless session */
.seal { width: 84px; height: 84px; margin: 14px auto; display: grid; place-items: center;
  /* The seal's 15px white text sits centred ON the highlight, so the highlight
     itself must clear AA: 95% toward --rose-fill is the lightest sheen that
     passes both themes (6.57 L / 4.84 D) — below ~94% dark fails. */
  border-radius: 50%; background: radial-gradient(circle at 40% 35%,
    color-mix(in srgb, var(--rose-fill) 95%, white), var(--rose-fill));
  color: #fff; font-family: var(--display); font-style: italic; font-size: 15px;
  text-align: center; line-height: 1.05; transform: rotate(-6deg);
  box-shadow: inset 0 2px 6px rgba(0,0,0,0.25), 0 2px 5px rgba(0,0,0,0.2);
  animation: seal-in 0.4s cubic-bezier(0.3, 1.6, 0.5, 1) both; }
@keyframes seal-in { 0% { transform: rotate(-6deg) scale(1.8); opacity: 0; }
  60% { opacity: 1; } 100% { transform: rotate(-6deg) scale(1); opacity: 1; } }

/* gap physics — head-shake on miss, firm settle on hit */
.was-miss .cloze-gap { animation: gap-shake 0.24s ease-in-out; }
.was-correct .cloze-gap { animation: gap-settle 0.18s ease-out; }
@keyframes gap-shake { 0%,100% { transform: translateX(0); }
  25% { transform: translateX(-3px); } 50% { transform: translateX(3px); }
  75% { transform: translateX(-2px); } }
@keyframes gap-settle { 0% { transform: scale(1); } 45% { transform: scale(1.06); }
  100% { transform: scale(1); } }

/* tiles flying into the mural at session end */
/* ---- the session-end laying (see player.js layTiles) ----
   The mosaic paints its new tessel·les invisible on the first frame, so the
   picture opens where the session STARTED; layTiles then removes the holding
   class and each shard is either flown in (.tile-flyer → .tile-set) or popped
   (.tile-laid). The animations carry `both`, so the delay itself holds them
   back — .is-laying only guards that very first paint. */
.mosaic-wrap.is-laying .mosaic-svg [data-laid] { opacity: 0; }

/* A flown shard fades under its arriving ghost — it must not also pop. */
.tile-set { animation: tile-set-in 200ms ease-out both; }
@keyframes tile-set-in { from { opacity: 0; } to { opacity: 1; } }

/* The ghost is the real shard, cloned into an overlay laid over the live SVG:
   at rest it is exactly its counterpart, so the flight is a transform home. It
   is an ABSOLUTE child of the mural (`.mosaic-stage`), not fixed on the body, so
   it moves WITH the picture through any scroll or reflow — the fix for the first
   tessel·la hanging when the end screen reflowed mid-flight. `.mural`/`.mural-wrap`
   are overflow:visible with no transformed ancestor, so nothing clips the ghost's
   rise above the mosaic and `position:absolute` resolves against the stage. */
.mosaic-stage { position: relative; }
/* Duration is set inline from FLIGHT (player.js) — this is only the default. */
.tile-flyer { position: absolute; z-index: var(--z-flyer); pointer-events: none; overflow: visible;
  animation: tile-drop 640ms cubic-bezier(0.34, 1.12, 0.5, 1) both; }
@keyframes tile-drop {
  0% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(0.86) rotate(var(--rot)); }
  18% { opacity: 1; }
  100% { opacity: 1; transform: translate(0, 0) scale(1) rotate(0deg); } }

@media (prefers-reduced-motion: reduce) {
  .stamp-rare, .seal, .was-miss .cloze-gap, .was-correct .cloze-gap { animation: none !important; }
  /* Static twin: the finished picture and the final caption, laid already. */
  .tile-set, .tile-flyer { animation: none !important; }
  .tile-flyer { display: none; }
  .mosaic-wrap.is-laying .mosaic-svg [data-laid] { opacity: 1; }
}

/* ---- F1 · la postal (weekly coach) ----
   GOLD since Avui v2 (5 Aug 2026): the postal is the week's own artefact, and
   the accent belongs to the round and el fil. It takes the same --daurat pair
   the exam rung and the camí's superat marks wear — a hue for a thing you were
   given, not a thing you press. The old accent-washed parchment with its 4px
   left rule read as a second fil card sitting under the real one. */
.postal { position: relative; border-radius: 18px; padding: 20px 24px; margin: 0;
  background: color-mix(in srgb, var(--daurat) 9%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--daurat) 32%, var(--edge-soft));
  box-shadow: inset 0 1px 0 var(--edge), 0 10px 22px -16px rgba(43, 38, 32, 0.35); }
.postal-tag { display: block; font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--daurat-ink); font-weight: 600; margin-bottom: 6px; }
.postal-body h3, .postal h3 { font-family: var(--display); font-weight: 700; font-size: 20px;
  margin: 0 0 6px; color: var(--text); }
.postal-body p { font-size: 15px; max-width: 62ch; margin: 0 0 10px; text-wrap: pretty; }
.postal-body .stat { display: inline-block; margin-right: 14px; font-size: 14px; color: var(--text-secondary, var(--muted)); }
.postal-body .stat strong { color: var(--accent-ink); font-size: 16px; }
.postal-x { position: absolute; top: 10px; right: 10px; background: none; border: none;
  color: var(--muted); cursor: pointer; padding: 6px; line-height: 0; }
.postal-x:hover { color: var(--accent); }

/* ---- F3 · sessió per a dos ---- */
.duo-turn { text-align: center; padding: 24px 0 40px; }
/* The pass-the-device moment sits on ONE card, so the scoreboard and «Som-hi»
   are two things stacked inside a surface rather than two loose objects that
   happened to share a line (.duo-scores is an inline-flex pill and the button
   is inline-block — side by side, the button read as part of the scoreboard). */
.duo-banner { max-width: 380px; margin: 8px auto 0; padding: 26px 24px 24px;
  border-radius: var(--radius); }
/* h1 since the A5 repair (the banner is the surface's landing); same look. */
.duo-turn h1, .duo-turn h2 { font-family: var(--display); font-size: 30px; margin: 4px 0 0; }
.duo-who { font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted); margin: 0; }
.duo-scores { display: flex; justify-content: center; gap: 28px; margin: 18px 0; }
.duo-banner .btn-primary { display: block; width: 100%; }
.duo-score { text-align: center; }
.duo-score .n { font-family: var(--display); font-size: 32px; font-weight: 700; color: var(--accent); }
.duo-score .who { font-size: 13px; color: var(--muted); }
.btn-duo { width: 100%; margin-top: 10px; background: var(--surface);
  border: 2px solid var(--tile-blue); color: var(--text); font-weight: 600; }
/* The teach-back is the point of the whole game, so it reads as la història's
   blue card and not as a grey slab: 12 % of a desaturated blue over TRANSPARENT
   picked up the warm ground and came out flat grey. Mixed into --surface with a
   blue rim instead, the retell card's own recipe. */
.teach-back { background: color-mix(in srgb, var(--tile-blue) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--tile-blue) 32%, transparent);
  border-radius: 12px; padding: 14px 18px; margin: 18px 0 14px; font-size: 15px; }
.teach-back p { margin: 0; }
.teach-back .ico { color: var(--tile-blue); }

/* ============ overhaul · esmalt i vitrall ============ */

/* Procedural depth under the time-of-day sky: a warm top light + fine
   ceramic grain (inline SVG feTurbulence, baked low-alpha). Zero assets. */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(130% 70% at 50% -12%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 58%),
    radial-gradient(90% 60% at 90% 108%, color-mix(in srgb, var(--tile-blue) 7%, transparent), transparent 60%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.5 0 0 0 0 0.45 0 0 0 0 0.4 0 0 0 0.05 0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Light model: raised surfaces read as glazed ceramic — translucent, a bright
   top edge, a soft cast shadow. Blur is reserved for the hero cards only.
   A raised surface is something you ACT on — never add a block here that is
   only read (prose, a note, a status line): the glaze promises an affordance,
   and a surface made of the same glaze as .scenario-row and .exercise will be
   tapped. */
/* `.porta` and `.postal` left this list with Avui v2 (5 Aug 2026): the porta is
   the PAGE now and carries no card at all — its glass moved to `.porta-hero`,
   which declares its own vidre and sheen in the porta block above — and the
   postal became the gold surface specified there, so a generic glaze here would
   simply overwrite it. */
/* `.practica-card` left it with Pràctica v2 (5 Aug 2026), for the same reason
   one door further along: two of the hub's cards do not take the generic
   ground — el repte warms its own with amber, and a LOCKED repte refuses the
   glaze entirely (law 2) — so the card declares its whole surface in the
   practica block above, where those two exceptions live beside it. */
.exercise, .dash-section, .listen-card, .scenario-row,
.retell, .repte-stage, .duo-banner {
  position: relative;
  background: var(--glass-2);
  border: 1px solid var(--edge-soft);
  box-shadow: var(--glaze);
}
.exercise { backdrop-filter: blur(7px) saturate(1.05); }
@supports not (backdrop-filter: blur(1px)) {
  .exercise { background: var(--surface); }
}
/* specular sheen across the hero cards (behind content, soft-light) */
.exercise::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background: var(--sheen); mix-blend-mode: soft-light; pointer-events: none;
}

/* The Avui porta is a doorway, not a Russian doll of glossy cards. */
.porta-feature > .retell { box-shadow: 0 10px 22px -16px rgba(43, 38, 32, 0.35); }

/* Motion — one system. Card entrance gains depth; primary buttons and stamps
   catch a light-sweep. */
/* `.porta`/`.postal` are absent by design: the porta's own zone entrance
   (`porta-puja`, with its settle sweep) carries the whole page in, and a second
   card animation on top of it would double the postal's arrival. */
.exercise, .feedback.ok, .feedback.miss, .session-end {
  animation: glaze-in var(--dur-3) var(--ease-glass) both;
}
@keyframes glaze-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to { opacity: 1; transform: none; }
}
.btn-primary { position: relative; overflow: hidden; }
.btn-primary::after {
  content: ""; position: absolute; top: 0; left: -60%; width: 45%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.35), transparent);
  transform: skewX(-18deg); opacity: 0;
}
.btn-primary:hover::after { animation: sweep 0.6s var(--ease-glass); }
@keyframes sweep { 0% { left: -60%; opacity: 0.9; } 100% { left: 130%; opacity: 0; } }
.stamp { overflow: hidden; }
.stamp::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 40%, rgba(255, 255, 255, 0.45) 50%, transparent 60%);
  transform: translateX(-100%); animation: stamp-sheen 0.7s var(--ease-glass) 0.2s; }
@keyframes stamp-sheen { to { transform: translateX(100%); } }

/* view transition gains gentle depth (main.js toggles .view-enter) */
.view-enter { animation: view-depth var(--dur-2) var(--ease-glass); }
@keyframes view-depth { from { opacity: 0; transform: translateY(6px) scale(0.992); } to { opacity: 1; transform: none; } }

/* El delit S4 · l'entrada — the card grids settle in a sweep instead of all at
   once (Pràctica's hub, la Galeria's shelf). Both are the same shape: a wall of
   equal cards that used to arrive as one flat slab.
   IT IS A SWEEP, NOT AN APPEARANCE, and that is the whole design. The view it
   lands in ALREADY fades in above (`view-depth`), so a second opacity ramp per
   card would be two entrances stacked — mush, and the plan's own risk note for
   this item is «perceived load». Translate only: the view supplies the arrival,
   the grid supplies the sequence, and one 7px differential is enough to read as
   cards being set down one after another.
   Translate-only also buys the honest hidden-tab story. `backwards` is required
   to hold a card through its delay, and a throttled tab never advances the
   timeline (the laying ceremony's lesson) — so the worst case here is a grid
   sitting 7px low, never the blank wall an opacity ramp would leave.
   The window is ≤120ms end to end however many cards there are; the per-card
   delay comes from motion.js's `stagger`, which is also what turns the whole
   thing off under reduced motion (no --entrada is written, and the twin below
   drops the animation itself). */
.practica-card, .galeria-piece {
  animation: entrada var(--dur-2) var(--ease-glass) var(--entrada, 0ms) backwards; }
@keyframes entrada { from { transform: translateY(7px); } }
@media (prefers-reduced-motion: reduce) {
  .practica-card, .galeria-piece { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .exercise, .feedback.ok, .feedback.miss, .session-end,
  .btn-primary::after, .stamp::after, .view-enter { animation: none !important; }
  .btn-primary:hover::after { animation: none !important; }
}

/* ============ richer pass · glass reaches every view ============ */

/* hero cards: more blur, a warm accent rim. `.porta`/`.postal` left this rule
   with Avui v2 — see the porta block; `.porta-hero` owns its own vidre. */
.exercise { backdrop-filter: blur(12px) saturate(1.14); }

/* frosted tab bar — floats over content */
.tabbar {
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  backdrop-filter: blur(16px) saturate(1.1);
  border-top: 1px solid var(--edge-soft);
  box-shadow: 0 -1px 0 var(--edge), 0 -10px 26px -14px rgba(0, 0, 0, 0.22);
}
@supports not (backdrop-filter: blur(1px)) { .tabbar { background: var(--surface); } }
.tab.active { box-shadow: inset 0 1px 0 var(--edge), 0 1px 6px -2px color-mix(in srgb, var(--accent) 40%, transparent); }

/* D2 (18 Jul 2026, replacing the R1 floating dock) — at the desktop threshold
   the five destinations move to the TOP, centred in the topbar band: brand
   left, destinations centre, status right, one --w-canvas shell. Same single
   nav element, same focus order, same PARENT_TAB lighting — only its position
   changes, so mobile keeps the bottom tabbar untouched. The dock is gone
   because it sat ON content (live Progrés evidence, Registre rows, el barri)
   and cost 62px of every viewport; the top pill is position:absolute
   (initial containing block), so it scrolls away WITH the rest of the top
   chrome instead of permanently covering anything. Centred with auto margins,
   NOT translateX — a transform would create a containing block. Stays on
   --z-chrome; mosaic chips (--z-flyer) and the takeover still stack above it.
   Placed AFTER the frosted-bar restyling above so these declarations win at
   equal specificity; inert below 900.

   Sizing (corrected 18 Jul 2026): tabs are CONTENT-sized with symmetric
   padding and a min-width floor, not a fixed 96px flex-basis. The fixed basis
   made every slot far wider than short labels, so the active chip filled its
   whole slot and sat flush against the container's 6px padding while ~40px of
   empty pill trailed after «Món» — the pill read as starting at Avui and
   floating past the last tab, i.e. as if it only wrapped the middle three.
   Content sizing gives each end tab the same inner padding, so the pill hugs
   all five symmetrically. Hit areas stay even (min-width keeps the short
   labels within ~20px of the long ones, well inside the dock's old 65–92px
   spread) and every tab clears the 44px target via .tab's min-height. */
@media (min-width: 900px) {
  .topbar { max-width: var(--w-canvas); min-height: 74px; align-items: center; }
  .tabbar {
    position: absolute; top: 12px; bottom: auto;
    left: 0; right: 0; width: fit-content; margin-inline: auto;
    gap: 2px; padding: 5px 7px;
    border: 1px solid var(--edge-soft); border-radius: 999px;
    background: var(--glass-2); box-shadow: var(--glaze);
    -webkit-backdrop-filter: blur(14px) saturate(1.12);
    backdrop-filter: blur(14px) saturate(1.12);
  }
  .tab { flex: 0 0 auto; min-width: 86px; padding: 8px 20px; }
  /* The pill keeps its words: the icons are the phone's answer to five squeezed
     labels, and there is no squeeze up here. Same specificity as the mobile
     rules they extend, so nothing leaks below the threshold. */
  .tab-ico { display: none; }
  .tab-mot { display: inline; }
  @supports not (backdrop-filter: blur(1px)) {
    .tabbar { background: color-mix(in srgb, var(--surface) 94%, var(--accent-soft)); }
  }
}

/* La bombolla — the pill's liquid-glass hover highlight (components/bombolla.js
   owns the spring; this is only its skin). A glass chip that slides under the
   hovered tab: near-transparent surface tint + a saturating blur lens over the
   pill's own glass, top-lit by --edge like every esmaltat control. Desktop pill
   only — below 900 it stays display:none whatever the JS does. The tabs lift to
   z-index 1 so their labels paint over the chip (a positioned sibling would
   otherwise cover the inline text). JS positions it via transform + width;
   only opacity transitions here (the fade in/out), so reduced motion keeps the
   highlight and loses the travel — the spring is skipped in JS, the fade here. */
.tab-bombolla { display: none; }
@media (min-width: 900px) {
  .tab-bombolla { display: block; position: absolute; top: 5px; bottom: 5px;
    left: 0; width: 90px; border-radius: 999px; pointer-events: none;
    opacity: 0; transform: translate3d(-999px, 0, 0);
    background: color-mix(in srgb, var(--surface) 46%, transparent);
    box-shadow: inset 0 1px 0 var(--edge), inset 0 -1px 1px rgba(0, 0, 0, 0.05),
      0 3px 10px -4px rgba(0, 0, 0, 0.18);
    -webkit-backdrop-filter: blur(6px) saturate(1.35) brightness(1.06);
    backdrop-filter: blur(6px) saturate(1.35) brightness(1.06);
    transition: opacity 0.18s ease; }
  .tab-bombolla.is-viva { opacity: 1; }
  .tabbar .tab { position: relative; z-index: 1; }
  @supports not (backdrop-filter: blur(1px)) {
    .tab-bombolla { background: color-mix(in srgb, var(--surface) 88%, var(--accent-soft)); }
  }
}
@media (prefers-reduced-motion: reduce) {
  .tab-bombolla { transition: none; }
}

/* La gota — the selection chip of every segmented control (components/gota.js
   owns the measuring; this is only its skin). One skin, four consumers (llengua
   toggle · curs subnav · gimnàs tense tabs · the viewer's copy of the toggle),
   each dressing it through the three vars below and nothing else.

   IT DOES NOT MOVE, and that is deliberate (2 Aug 2026). It used to glide
   between choices; in Safari that read as jerky, and the cause was structural —
   every consumer rebuilds a heavy DOM in the same tick as the tap, so the
   glide's opening frames were starved, and it transitioned width/height, which
   is layout every frame. Softening the curve first (--ease-pop → --ease-glass)
   made it worse, which is the tell. La bombolla keeps the app's one piece of
   follow-the-cursor motion; this is its still sibling. Do not add a transition
   back to this element's box — see gota.js for what a revival would need.

   The chip carries the SELECTION FILL, which is why each consumer's own
   `.active`/`.is-triat` fill is dropped by a `.gota.is-viva ~ …` rule: two
   fills would be two selections. The general sibling combinator is
   load-bearing — the chip is PREPENDED, so it precedes every option, and the
   fill only leaves once the chip is really standing. That is the static twin:
   before la gota is placed (or if it never is — an ancient browser, a control
   mounted in a tab so deeply throttled its microtask hasn't run) the control
   keeps the look it always had. Ink and weight never move off the option at
   all. The options are `position: relative` so they paint above the chip (the
   .tabbar .tab idiom). */
.gota { position: absolute; left: 0; top: 0; z-index: 0; width: 0; height: 0;
  pointer-events: none; opacity: 0; transform: translate3d(0, 0, 0);
  border: 1px solid var(--gota-vora, transparent);
  border-radius: var(--gota-radi, 10px);
  background: var(--gota-fill, color-mix(in srgb, var(--accent-soft) 68%, var(--surface))); }
.gota.is-viva { opacity: 1; }
/* No transition here, and no prefers-reduced-motion twin either: there is no
   motion left to reduce. Opacity is a plain state flip, not a fade. */

/* topbar pica/streak get a hairline sheen behind the wordmark */
.topbar { position: relative; }

/* glossy progress + skill bars (enamel channel with a liquid fill) */
.bar { background: color-mix(in srgb, var(--bar-track) 82%, transparent);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.14), inset 0 -1px 0 var(--edge-soft); }
.bar-fill { position: relative; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45); overflow: hidden; }
.bar-fill::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.4), transparent 55%); }

/* session player: block label becomes a glass chip; feedback frosts */
.block-label { display: inline-block; background: var(--glass-1); padding: 3px 11px;
  border-radius: 999px; border: 1px solid var(--edge-soft); box-shadow: inset 0 1px 0 var(--edge); }
.feedback.ok, .feedback.miss { backdrop-filter: blur(6px); box-shadow: var(--glaze); }
.answer-input, .cloze-gap, .retell-box {
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.08), inset 0 1px 0 var(--edge); }

/* buttons: liquid top-light */
.btn-primary { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5),
  0 8px 18px -8px color-mix(in srgb, var(--accent) 65%, transparent); }
.btn-audio, .btn-repte, .btn-duo, .btn-comenca {
  box-shadow: inset 0 1px 0 var(--edge), 0 3px 10px -4px rgba(0, 0, 0, 0.16); }

/* mural + pica frame as glazed ceramic */
.mural { box-shadow: inset 0 1px 0 var(--edge), 0 10px 26px -14px rgba(0, 0, 0, 0.3);
  border: 1px solid var(--edge-soft); }

/* repte + duo: the scoreboard is the lit tile. The repte HEAD is no longer one —
   it is a header row above the card now (the session player's own shape), and a
   glazed row above a glazed card was the Russian doll this pass removed. */
.repte-clock, .repte-score, .duo-score .n { text-shadow: 0 1px 0 var(--edge); }
/* Nesting de-escalates: inside the banner card the scoreboard keeps the lit
   tint and drops the cast shadow. width:max-content + auto margins centre it
   as a BLOCK, so the button below it can never join its line. */
.duo-scores { background: var(--glass-1); border: 1px solid var(--edge-soft);
  border-radius: 16px; padding: 10px 20px; display: flex;
  width: max-content; margin-inline: auto; }
/* In play it rides the session header, where it is a readout and not the
   headline: same tile, two thirds the size. */
.session-progress .duo-scores { padding: 6px 14px; gap: 18px; margin: 0; }
.session-progress .duo-score .n { font-size: 22px; }
.session-progress .duo-score .who { font-size: 11px; }

/* dashboards: cefr as a glass chip */
.cefr { background: var(--glass-1); border: 1px solid var(--edge-soft);
  padding: 3px 11px; border-radius: 999px; box-shadow: inset 0 1px 0 var(--edge); }

/* the pica sink catches light too */
.porta-pica svg { filter: drop-shadow(0 1px 1px rgba(0,0,0,0.15)); }

/* ---- La Plaça · private threads, not chat-app chrome ---- */
.placa-inbox { max-width: none; margin: 0; }
.placa-inbox.is-standalone { position: relative; padding-top: var(--space-2); }
.placa-inbox.is-standalone::before { content: ""; position: absolute; z-index: -1;
  top: 0; right: 0; width: 70%; height: 260px; pointer-events: none;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--accent-soft) 58%, transparent), transparent); }
.placa-portada { position: relative; padding: 0 2px var(--space-5);
  border-bottom: 1px solid var(--edge-soft); }
.placa-portada-bar { display: flex; align-items: center; justify-content: space-between;
  min-height: 48px; gap: var(--space-3); }
.placa-portada-copia { display: flex; max-width: var(--measure); flex-direction: column;
  gap: var(--space-2); margin-top: var(--space-3); }
.placa-portada-rubric { margin: 0; color: var(--accent-ink); font-size: var(--fs-1);
  font-weight: 700; letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.placa-portada h1 { margin: 0; font: 700 var(--fs-9)/1.02 var(--display);
  letter-spacing: .005em; }
.placa-portada-hint { margin: 0; color: var(--muted); font: italic var(--fs-5)/1.45 var(--display); }
.placa-inbox.is-standalone .colla-conjunt { margin-top: var(--space-5); }
.placa-inbox.is-standalone .placa-intro { margin-top: var(--space-4); }
.placa-inbox.is-standalone .placa-pacing { margin-top: var(--space-4); text-align: center; }
.placa-head { display: flex; align-items: center; gap: 10px; min-height: 52px;
  border-bottom: 1px solid var(--edge-soft); padding: 0 2px 8px; }
.placa-head h1, .placa-head h2 { margin: 0; font: 700 23px/1.2 var(--display); text-transform: uppercase;
  letter-spacing: .06em; }
/* …except the inbox's own head, which la colla v3 gives a display title and a
   count pill (see the note beside `.mon-overline`). The thread notice above
   keeps the small uppercase head — it is a notice, not a room. */
/* The pill hugs the title — it is a count OF that thing, not a right-hand
   toolbar. (The handoff's header is a space-between row whose left group is
   title + pill; with nothing on its right, the group IS the row.) */
/* How many conversations are waiting. Hidden at zero — a badge that says «0»
   is a badge asking to be looked at for nothing. */
/* The gap, not a space in the string: the two halves are flex items and
   whitespace between flex items is not rendered. It costs nothing when the
   noun is hidden, because `position: absolute` takes it out of the flow. */
.colla-nou { display: inline-flex; align-items: center; align-self: center;
  gap: .3em; padding: 2px 9px; border-radius: var(--radius-pill);
  background: var(--accent-fill); color: #fff;
  font-size: 12px; font-weight: 600; letter-spacing: .02em; }
/* The noun beside the figure is visible where there is room for it and READ
   ALOUD on both, so a phone's bare «1» is never a number with no noun. */
.colla-nou-mot { position: absolute; width: 1px; height: 1px; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
/* .placa-back / .xat-back are gone — these heads use the shared .sortida. */
.placa-intro { position: relative; margin: 14px 0 8px; padding: 10px 42px 10px 12px;
  border-left: 2px solid var(--accent); color: var(--muted); font: italic 14px/1.45 var(--display); }
.placa-intro p { margin: 0; }
.placa-intro button { position: absolute; top: 2px; right: 2px; width: 40px; height: 40px;
  border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 22px; }
/* ---- LA COLLA v3 · the inbox reads as the messaging service it is ----
   (design/handoffs/colla-v3, 7 Aug 2026; supersedes the Món redesign's ensemble)

   ONE markup tree, two compositions, and the swap is CSS at Món's own 900
   threshold — never a JS breakpoint. Below 900: one phone inbox, the principal
   simply its first row. At and above: a split correspondence pane, the
   principal open on the left with the real incoming line as a bubble and a
   composer AFFORDANCE under it, the rest as a conversation list on the right.

   Mobile-first, so everything here is the phone inbox and the ≥900 block at the
   foot of the section rebuilds it as the pane. Every door is still one
   `.placa-row`, so the roving arrow keys, the unread state and the accessible
   name keep one implementation each. */
.colla-conjunt { display: flex; flex-direction: column; overflow: hidden;
  margin-top: var(--space-3); background: var(--surface);
  border: 1px solid var(--edge-soft); border-radius: var(--radius);
  box-shadow: var(--shadow-1); }
/* On the phone the list is not a card of its own — it is the rest of THIS card,
   so its rows become siblings of the principal and the hairlines fall between
   them without a wrapper in the way. */
.colla-llista { display: contents; }
.placa-row { position: relative; display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 12px;
  width: 100%; min-height: 44px; padding: 13px 14px;
  border: 0; background: transparent; color: var(--text);
  cursor: pointer; font: inherit; text-align: left;
  transition: background var(--dur-1) var(--ease-glass),
              transform var(--dur-1) var(--ease-glass),
              box-shadow var(--dur-1) var(--ease-glass); }
.colla-llista > .placa-row + .placa-row,
.colla-principal + .colla-llista > .placa-row:first-child {
  border-top: 1px solid var(--line); }
/* An INSET focus ring, the tabbar's own idiom and for its reason: the rows lie
   inside a clipped card, so an outward ring is cut off on three sides
   (measured on the middle row of the list, 7 Aug 2026). */
.placa-row:focus-visible { outline-offset: -3px; }
.placa-row:active { background: color-mix(in srgb, var(--accent-soft) 55%, transparent); }
@media (hover: hover) and (pointer: fine) {
  .placa-row:hover { background: color-mix(in srgb, var(--accent-soft) 55%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .placa-row { transition: none; }
  .placa-row:hover { transform: none; }
}

/* The principal, phone side: medallion spanning both rows, name over the last
   line, stamp and count badge down the right. `.colla-cap` and the composer are
   the desktop composition's and are not drawn here at all. */
.colla-principal { grid-template-rows: auto auto; }
.colla-cap { display: contents; }
.colla-principal .placa-medallion, .colla-principal .colla-cares { grid-row: 1 / 3; }
.colla-principal .colla-noms { grid-column: 2; grid-row: 1; }
.colla-principal .colla-bombolla { grid-column: 2; grid-row: 2; }
.colla-principal .colla-marca { grid-column: 3; grid-row: 1 / 3; }
.colla-compositor, .colla-meta { display: none; }

.colla-noms { display: flex; min-width: 0; flex-direction: column; gap: 3px;
  padding-top: 2px; }
.colla-titol { display: flex; align-items: center; gap: 7px; min-width: 0; }
.colla-nom { overflow: hidden; font: 700 18px/1.1 var(--display);
  text-overflow: ellipsis; white-space: nowrap; }
/* La rotllana's own count. 11px, not the handoff's 10.5/10: the type floor is
   11 for anything a reader reads, and a chip naming the group's size is read. */
.colla-veus { flex: 0 0 auto; padding: 2px 8px; border: 1px solid var(--edge-soft);
  border-radius: var(--radius-pill); color: var(--muted); font-size: var(--fs-1);
  font-weight: 600; letter-spacing: .1em; text-transform: uppercase; }

/* The last line. One row, one ellipsis, and whatever marks it needs to be
   honest about who spoke. */
.colla-linia { display: flex; align-items: center; gap: 5px; min-width: 0;
  color: var(--muted); font-size: 14px; line-height: 1.45; }
.colla-linia .colla-dit { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.colla-veu { flex: 0 0 auto; font-weight: 600; }
/* A room's speaker signs in their own ink — never the raw hue (core rule 1);
   the handoff's color-mix(75%, text) is what `--placa-person-ink` already is,
   audited. */
.colla-veu.is-parlant { color: var(--placa-person-ink); }
/* The receipt check: muted = sent, accent = a reading window has had it. */
.colla-tic { flex: 0 0 auto; display: inline-flex; color: var(--muted); }
.colla-tic.is-llegit { color: var(--accent-ink); }
.colla-tic .ico { stroke-width: 2.4; }
.placa-row.is-broken .colla-dit { color: var(--muted); font-style: italic; }

/* The principal's line: the same nodes, given room to be read. On the phone it
   is plain two-line copy in full ink; the bubble arrives with the pane. */
.colla-bombolla { min-width: 0; overflow: hidden; color: var(--text);
  font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
@supports (-webkit-line-clamp: 2) {
  .colla-bombolla { display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; }
}

/* The right column — stamp over receipt on a list row, stamp over count badge
   on the principal. Both stamp lengths are in the DOM and CSS picks one: a
   stamp that reworded itself on a resize listener would be a JS breakpoint. */
.colla-quan, .colla-marca { display: flex; flex: 0 0 auto; flex-direction: column;
  align-items: flex-end; gap: 4px; max-width: 9em; padding-top: 4px;
  text-align: right; }
.colla-marca { gap: 6px; padding-top: 3px; }
.colla-quan time { color: var(--muted); font-size: 12px;
  font-variant-numeric: tabular-nums; }
.colla-quan-mig { display: none; }
.colla-hora { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.colla-principal.is-unread .colla-hora { color: var(--accent-ink); font-weight: 600; }
.colla-rebut { display: inline-flex; align-items: center; gap: 4px;
  color: var(--muted); font-size: var(--fs-1); line-height: 1.3; }
.colla-rebut.is-espera { color: var(--accent-ink); font-weight: 600; }
.colla-mig { display: inline-flex; flex: 0 0 auto; }
.colla-comptador { display: grid; place-items: center; min-width: 19px; height: 19px;
  padding: 0 5px; border-radius: var(--radius-pill); background: var(--accent-fill);
  color: #fff; font-size: 11.5px; font-weight: 700; }

/* The unread mark has two renderings and only ever one at a time: the count
   badge on the phone, this dot on the pane's principal card. */
.placa-unread { display: none; position: absolute; top: 14px; left: 14px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
  animation: placa-dot-in 180ms ease-out both; }
@keyframes placa-dot-in { from { opacity: 0; transform: scale(.55); } }

/* LA ROTLLANA'S FACE — a rounded SQUARE, deliberately not the personal circle,
   the way a messaging app tells a group from a person at a glance. The pieces
   are the cast's own monograms (`facepile` builds them from the roster), and
   `data-veus` places them so two never leave a hole where a third would be. */
.colla-cares { position: relative; width: 48px; height: 48px; flex: 0 0 48px;
  border: 1px solid var(--edge-soft); border-radius: 12px;
  background: var(--surface-sunk); }
.colla-cara { position: absolute; display: grid; place-items: center;
  width: 21px; height: 21px; border-radius: 50%;
  background: color-mix(in srgb, var(--placa-person) 24%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--placa-person) 55%, var(--edge-soft));
  /* The ink is the person's, pushed toward --text: on the 24 % glaze the bare
     -ink measures 3.91:1 for núria at 11px (AA repair, measured 7 Aug 2026).
     Deepening the LETTER rather than thinning the ground keeps the handoff's
     own glaze — 6.2/7.1/6.7 light, 5.9/5.9/5.7 dark. */
  color: color-mix(in srgb, var(--placa-person-ink) 62%, var(--text));
  font: italic 700 11px/1 var(--display); }
.colla-cara:nth-child(1) { top: 4px; left: 4px; }
.colla-cara:nth-child(2) { top: 4px; right: 4px; }
.colla-cara:nth-child(3) { bottom: 4px; left: 13px; }
.colla-cares[data-veus="1"] .colla-cara:nth-child(1) { top: 13px; left: 13px; }
.colla-cares[data-veus="2"] .colla-cara:nth-child(1) { top: 13px; left: 4px; }
.colla-cares[data-veus="2"] .colla-cara:nth-child(2) { top: 13px; right: 4px; }
.colla-cares.is-gran { width: 56px; height: 56px; flex-basis: 56px; }
.colla-cares.is-gran .colla-cara { width: 24px; height: 24px; font-size: 12px; }
.colla-cares.is-gran .colla-cara:nth-child(3) { left: 15px; }

/* ≥900 — THE CORRESPONDENCE PANE. */
@media (min-width: 900px) {
  .colla-nou-mot { position: static; width: auto; height: auto; margin: 0;
    overflow: visible; clip: auto; }
  .colla-conjunt { display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    align-items: stretch; gap: var(--space-4); margin-top: var(--space-4);
    overflow: visible; background: none; border: 0; border-radius: 0;
    box-shadow: none; }
  /* One conversation and nothing to list beside it: a lone card takes the
     column rather than standing next to an empty half (mon.js's rule — the
     count is decided at build time, in JS, and named on the wrapper). */
  .colla-conjunt.is-sol { grid-template-columns: minmax(0, 1fr); }
  .colla-llista { display: flex; flex-direction: column; overflow: hidden;
    background: var(--surface); border: 1px solid var(--edge-soft);
    border-radius: var(--radius); box-shadow: var(--shadow-1); }
  .colla-principal + .colla-llista > .placa-row:first-child { border-top: 0; }

  .colla-principal { display: flex; flex-direction: column; gap: 14px;
    padding: 18px 20px; background: var(--surface);
    border: 1px solid var(--edge-soft); border-radius: var(--radius);
    box-shadow: var(--shadow-1); }
  /* Raised = actionable: the whole card is the door, so it lifts rather than
     washing like a row. */
  .colla-principal:hover { background: var(--surface);
    transform: translateY(-2px); box-shadow: var(--shadow-3); }
  .colla-principal .placa-unread { display: block; }
  .colla-cap { display: flex; align-items: center; gap: 14px; }
  .colla-marca { display: none; }
  .colla-meta { display: block; color: var(--muted); font-size: 13px; }
  .colla-estat.is-nou, .colla-estat.is-espera { color: var(--accent-ink);
    font-weight: 600; }
  .colla-principal .colla-nom { font-size: 21px; line-height: 1.15; }
  .colla-principal .colla-noms { gap: 2px; padding-top: 0; }

  /* The incoming line as a real bubble — rectangular and tail-free, the thread
     surface's own law. It is tinted by whoever SPOKE (a room has three voices
     and no hue of its own), and one he wrote himself is never dressed as an
     arrival: neutral ground, the notch mirrored, aligned to the far side. */
  .colla-bombolla { align-self: start; max-width: 52ch; padding: 12px 16px;
    border: 1px solid color-mix(in srgb, var(--placa-person) 28%, var(--edge-soft));
    border-radius: 4px 14px 14px 14px;
    background: color-mix(in srgb, var(--placa-person) 14%, var(--surface));
    font-size: 15.5px; line-height: 1.55; }
  @supports (-webkit-line-clamp: 3) {
    .colla-bombolla { -webkit-line-clamp: 3; }
  }
  .colla-bombolla.is-meva { align-self: flex-end; border-color: var(--line);
    border-radius: 14px 4px 14px 14px; background: var(--surface-sunk); }

  /* The composer is an AFFORDANCE, not a second place to write: it says «you
     can answer, whenever» and the tap opens the thread, where the real one is. */
  .colla-compositor { display: flex; align-items: center; gap: 10px; margin-top: 2px; }
  .colla-camp { flex: 1; display: flex; align-items: center;
    padding: 9px 16px; border: 1.5px solid var(--line);
    border-radius: var(--radius-pill); background: var(--surface-sunk);
    color: var(--muted); font-size: var(--fs-3); }
  .colla-envia { display: grid; place-items: center; width: 38px; height: 38px;
    flex: 0 0 38px; border-radius: 50%; background: var(--accent-fill);
    color: #fff; box-shadow: var(--shadow-1); }

  .colla-fila { padding: 13px 16px; align-items: center; }
  .colla-fila .colla-nom { font-size: 17.5px; }
  .colla-quan { align-self: start; }
  .colla-quan-mig { display: inline; }
  .colla-quan-curt { display: none; }
}
.placa-pacing { margin: var(--space-3) 2px 0; color: var(--muted);
  font-size: var(--fs-2); }
.placa-empty, .placa-thread-notice { margin: 26px auto 0; max-width: 440px; text-align: center; }
.placa-empty h2, .placa-empty h3 { margin: 0 0 6px; font: 700 22px/1.25 var(--display); }
.placa-empty p, .placa-thread-notice > p { color: var(--muted); font-size: 14px; }
.placa-thread-notice .placa-head { text-align: left; }

/* Ceramic-window monograms: a calm central seal carries the living mark while
   two broad rim pieces nod to the Galeria. Flat colour only — no glaze, cast or
   inset light — and never a portrait. */
.placa-medallion { --placa-person: var(--placa-default); position: relative; isolation: isolate;
  display: grid; place-items: center; width: 48px; height: 48px; flex: 0 0 48px;
  overflow: hidden; border: 1px solid color-mix(in srgb, var(--placa-person) 58%, var(--edge-soft));
  border-radius: 50%; background: color-mix(in srgb, var(--placa-person) 12%, var(--surface));
  color: color-mix(in srgb, var(--placa-person) 84%, var(--text));
  box-shadow: none; font: italic 700 20px/1 var(--display); }
.placa-medallion::before, .placa-medallion::after { content: ""; position: absolute; z-index: -1;
  border: 0; }
.placa-medallion::before { inset: -2px 44% 47% -2px; clip-path: polygon(0 0, 100% 0, 86% 72%, 34% 100%, 0 78%);
  background: color-mix(in srgb, var(--placa-person) 58%, var(--surface)); }
.placa-medallion::after { inset: 53% -2px -2px 42%; clip-path: polygon(22% 0, 100% 18%, 100% 100%, 0 100%, 8% 44%);
  background: color-mix(in srgb, var(--placa-person) 36%, var(--surface)); }
/* ONE hue map for everything outside a thread that wears a person's colour —
   the medallion, la rotllana's facepile pieces and a room line's speaker name.
   `.placa-hue` is the class those elements carry, so a new friend needs one
   token pair and one roster row and never an app edit; the bare default on it
   (and on the medallion itself) is the safety net for a friend activated
   before her tokens land, which would otherwise be an invalid color-mix that
   paints nothing at all. `.xat` keeps its own map: a thread is a different
   surface and sets the hue on the shell and on each bubble's speaker. */
.placa-hue { --placa-person: var(--placa-default);
  --placa-person-ink: var(--placa-default-ink); }
.placa-hue[data-character="nuria"] { --placa-person: var(--placa-nuria); --placa-person-ink: var(--placa-nuria-ink); }
.placa-hue[data-character="oriol"] { --placa-person: var(--placa-oriol); --placa-person-ink: var(--placa-oriol-ink); }
.placa-hue[data-character="lluis"] { --placa-person: var(--placa-lluis); --placa-person-ink: var(--placa-lluis-ink); }
.placa-hue[data-character="carmen"] { --placa-person: var(--placa-carmen); --placa-person-ink: var(--placa-carmen-ink); }
.placa-hue[data-character="mireia"] { --placa-person: var(--placa-mireia); --placa-person-ink: var(--placa-mireia-ink); }
.placa-medallion-mark { position: relative; z-index: 1; display: grid; place-items: center;
  width: 34px; height: 34px; border: 1px solid color-mix(in srgb,
    var(--placa-person) 30%, var(--edge-soft)); border-radius: 50%;
  background: color-mix(in srgb, var(--surface) 91%, transparent); }
.placa-medallion.is-small { width: 36px; height: 36px; flex-basis: 36px; font-size: 15px; }
.placa-medallion.is-small .placa-medallion-mark { width: 26px; height: 26px; }
/* The ensemble's monogram: large, and still never a portrait. */
.placa-medallion.is-gran { width: 56px; height: 56px; flex-basis: 56px; font-size: 23px; }
.placa-medallion.is-gran .placa-medallion-mark { width: 40px; height: 40px; }

/* Thread surface. Bubbles stay rectangular and tail-free; tools sit outside. */
/* --placa-person is the glaze (bubbles, medallion grounds); --placa-person-ink
   writes the small text (sender line, tool hover) — núria measured 3.09 and
   lluís 2.85 in light as raw hues. */
/* Mode immersiu (:root.is-xat — renderThread arms it, route() always disarms):
   a thread takes the whole viewport like a real texting app. One scroll region
   (the feed), chrome hidden below the desktop threshold, composer pinned to
   the bottom edge. --vvh is the visual-viewport height placa.js keeps current
   while the iOS keyboard is up; 100dvh is the no-JS fallback. The ≥900 block
   further down keeps the topbar + pill and brings the ceramic card back. */
:root.is-xat, :root.is-xat body { overflow: hidden; overscroll-behavior: none; }
@media (max-width: 899px) {
  :root.is-xat .topbar, :root.is-xat .tabbar { display: none; }
  /* Share the visual viewport with route-owned prose above the thread. */
  :root.is-xat main { max-width: none; padding: 0; display: flex;
    flex-direction: column; flex: none; height: var(--vvh, 100dvh); }
  :root.is-xat .xat { flex: 1; min-height: 0; }
}
.xat { --placa-person: var(--placa-default); --placa-person-ink: var(--placa-default-ink);
  display: flex; flex-direction: column; gap: 0; height: var(--vvh, 100dvh); }
.xat[data-character="nuria"] { --placa-person: var(--placa-nuria); --placa-person-ink: var(--placa-nuria-ink); }
.xat[data-character="oriol"] { --placa-person: var(--placa-oriol); --placa-person-ink: var(--placa-oriol-ink); }
.xat[data-character="lluis"] { --placa-person: var(--placa-lluis); --placa-person-ink: var(--placa-lluis-ink); }
.xat[data-character="carmen"] { --placa-person: var(--placa-carmen); --placa-person-ink: var(--placa-carmen-ink); }
.xat[data-character="mireia"] { --placa-person: var(--placa-mireia); --placa-person-ink: var(--placa-mireia-ink); }
/* …and per SPEAKER inside a room: la rotllana holds three voices, so the hue
   has to be re-pointed on the bubble or all three glaze and sign identically.
   The shell mapping above still owns a private thread — one sender, no
   attribute, byte-identical rendering. The room itself has no hue of its own
   (--placa-default), deliberately: the colour in it belongs to the voices. */
.xat-bubble[data-sender="nuria"] { --placa-person: var(--placa-nuria); --placa-person-ink: var(--placa-nuria-ink); }
.xat-bubble[data-sender="oriol"] { --placa-person: var(--placa-oriol); --placa-person-ink: var(--placa-oriol-ink); }
.xat-bubble[data-sender="lluis"] { --placa-person: var(--placa-lluis); --placa-person-ink: var(--placa-lluis-ink); }
.xat-bubble[data-sender="carmen"] { --placa-person: var(--placa-carmen); --placa-person-ink: var(--placa-carmen-ink); }
.xat-bubble[data-sender="mireia"] { --placa-person: var(--placa-mireia); --placa-person-ink: var(--placa-mireia-ink); }
.xat-head { position: relative; display: flex; align-items: center; gap: 9px; flex: none;
  min-height: 54px; border-bottom: 1px solid var(--edge-soft);
  padding: max(10px, env(safe-area-inset-top)) 12px 8px; background: var(--surface); }
.xat-who { min-width: 0; }
.xat-who h1 { margin: 0; font: 700 20px/1.2 var(--display); }
.xat-sub { overflow: hidden; margin: 2px 0 0; color: var(--muted); font-size: 11.5px;
  line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }
.xat-actions { position: relative; margin-left: auto; }
.xat-more { display: grid; place-items: center; width: 44px; height: 44px; padding: 0;
  border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 24px; }
.xat-menu[hidden], .placa-confirm[hidden], .entendre-panel[hidden] { display: none; }
.xat-menu { position: absolute; z-index: var(--z-popover); top: 42px; right: 0; width: max-content;
  padding: 5px; border: 1px solid var(--edge-soft); border-radius: 11px;
  background: var(--surface); box-shadow: var(--glaze); }
.xat-menu-delete { min-height: 44px; padding: 8px 12px; border: 0; border-radius: 8px;
  background: transparent; color: var(--miss); cursor: pointer; font: inherit; font-size: 13px; }
.xat-menu-delete:hover { background: var(--miss-soft); }
/* A thread moves on one axis only: contain stops a drag at the feed's own limit
   from chaining out to the ancestor, pan-y refuses the horizontal axis outright.
   pinch-zoom is kept — refusing it would cost real zoom accessibility. */
.xat-feed { display: flex; flex-direction: column; gap: 10px; flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y pinch-zoom;
  padding: 10px 12px; }
.xat-row { display: flex; align-items: flex-end; gap: 4px; }
.xat-row.mine { justify-content: flex-end; }
.xat-bubble { max-width: calc(100% - 54px); padding: 9px 12px 7px; border: 1px solid var(--edge-soft);
  border-radius: 14px; box-shadow: inset 0 1px 0 var(--edge); }
.b-friend { background: color-mix(in srgb, var(--placa-person) 8%, var(--surface)); }
.b-ryan { background: color-mix(in srgb, var(--accent) 11%, var(--surface)); }
.xat-sender { display: block; margin-bottom: 3px; color: var(--placa-person-ink);
  font-size: 10px; font-weight: 750; letter-spacing: .06em; text-transform: uppercase; }
.xat-text { margin: 0; font-size: 15px; line-height: 1.45; white-space: pre-wrap;
  overflow-wrap: break-word; }
.xat-nudge { margin: 6px 0 0; padding-top: 5px; border-top: 1px dashed var(--edge-soft);
  color: var(--accent); font-size: 13px; }
.xat-meta { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; }
.xat-tools { display: flex; flex: 0 0 auto; gap: 1px; }
.xat-tool { display: grid; place-items: center; width: 44px; height: 44px; padding: 0;
  border: 0; border-radius: 50%; background: transparent; color: var(--muted); cursor: pointer;
  font: 700 16px/1 var(--display); }
.xat-tool:hover { background: color-mix(in srgb, var(--placa-person) 10%, transparent);
  color: var(--placa-person-ink); }
.xat-empty { margin: 18px auto; max-width: 38ch; color: var(--muted); font: italic 14px/1.5 var(--display);
  text-align: center; }
.xat-note { flex: none; min-height: 1em; margin: 0; padding: 2px 14px;
  color: var(--muted); font-size: 12.5px; }
.xat-compose { flex: none; display: grid;
  grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 8px;
  padding: 6px 10px max(10px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--edge-soft); background: var(--surface); }
/* While the keyboard is up, the home-indicator inset sits underneath it —
   padding for it would read as a phantom gap. placa.js flags the state. */
.xat.is-teclat .xat-compose { padding-bottom: 8px; }
/* 16px is a hard floor, not a taste call: iOS Safari zooms the page when a focused
   control is under it, and the zoom is what makes the thread pannable sideways.
   Do not drop this to 15px to match .xat-text.
   Height is set inline by autogrow() in placa.js; max-height is the cap it grows into
   (≈5 lines) before handing over to its own scroll. */
.xat-input { min-width: 0; resize: none; max-height: 144px; overflow-y: auto;
  /* the system's generic textarea floor is 96px — this box autogrows from one
     line (placa.js owns the height); keep the floor at the tap target instead */
  min-height: 44px;
  overscroll-behavior: contain; padding: 9px 12px; border: 1px solid var(--edge-soft);
  border-radius: 13px; background: color-mix(in srgb, var(--bg) 70%, transparent);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, .08), inset 0 1px 0 var(--edge);
  color: var(--text); font: inherit; font-size: 16px; }
.xat-input:focus { outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); }
/* Send is a glyph, not a word: the compose box needs every pixel of width it can get. */
.xat-send { display: grid; place-items: center; flex: 0 0 auto; width: 44px; height: 44px;
  padding: 0; border: 0; border-radius: 50%; background: var(--accent-fill); color: #fff;
  box-shadow: var(--glaze); cursor: pointer;
  transition: transform 120ms var(--ease-glass), filter 120ms var(--ease-glass); }
/* Deepens on hover, like every other white-on-fill control — see .btn-primary. */
.xat-send:hover { filter: brightness(0.92) saturate(1.06); }
.xat-send:active { transform: scale(.93); }
/* Held down for the width of a save, so a second tap has nothing to hit. */
.xat-send:disabled { opacity: .5; cursor: default; }
.xat-send:disabled:hover { filter: none; }
.xat-send:disabled:active { transform: none; }

/* ---- desktop thread (≥900) — the ceramic card comes back inside the locked
   shell: topbar + pill stay, the body scrollbar goes, the feed remains the
   only scroll region. Same specificity as the base rules; inert below 900. */
@media (min-width: 900px) {
  :root.is-xat main { display: flex; flex-direction: column; min-height: 0; padding-bottom: 20px; }
  .xat { flex: 1; min-height: 0; height: auto; gap: 10px; padding: 12px;
    border: 1px solid var(--edge-soft); border-radius: 20px;
    background: color-mix(in srgb, var(--surface) 91%, transparent);
    box-shadow: var(--shadow); backdrop-filter: blur(12px) saturate(1.08); }
  @supports not (backdrop-filter: blur(1px)) { .xat { background: var(--surface); } }
  .xat-head { padding: 0 0 9px; background: none; }
  .xat-feed { padding: 5px 2px; }
  .xat-note { padding: 0; }
  .xat-compose { padding: 2px 0 0; border-top: 0;
    background: color-mix(in srgb, var(--surface) 92%, transparent); }
  .xat-bubble { max-width: 78%; }
}

/* Confirm stays inside the thread and names the irreversible boundary plainly. */
.placa-confirm { margin: 10px 12px 0; padding: 14px; border: 1px solid color-mix(in srgb, var(--miss) 35%, var(--edge-soft));
  border-radius: 14px; background: color-mix(in srgb, var(--miss-soft) 54%, var(--surface)); }
.placa-confirm h2 { margin: 0 0 5px; font: 700 18px/1.3 var(--display); }
.placa-confirm p { margin: 0; color: var(--muted); font-size: 13px; }
.placa-confirm-actions { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-top: 12px; }
/* The system has no --miss-fill, and white on the dark theme's lightened miss
   is 2.6:1 — so the label writes in --surface, which flips with the theme
   exactly as the retired --on-accent did (6.34 L / 5.82 D). */
.placa-danger { background: var(--miss); color: var(--surface); font-weight: 700; }
/* The card's own gap + padding space the confirm on desktop; the mobile
   margin above would double it. After the base rule so the reset wins. */
@media (min-width: 900px) { .placa-confirm { margin: 0; } }

/* Entendre: a private sheet, one authored layer open at a time. */
.entendre-overlay { position: fixed; z-index: var(--z-sheet); inset: 0; display: flex; align-items: flex-end;
  justify-content: center; background: color-mix(in srgb, var(--viewer-backdrop) 54%, transparent); }
.entendre-sheet { width: min(100%, 640px); max-height: min(82dvh, 720px); overflow-y: auto;
  padding: 20px max(18px, env(safe-area-inset-right)) max(18px, env(safe-area-inset-bottom))
    max(18px, env(safe-area-inset-left));
  border: 1px solid var(--edge-soft); border-bottom: 0; border-radius: 24px 24px 0 0;
  background: var(--surface); box-shadow: 0 -18px 46px -28px rgba(0, 0, 0, .75);
  animation: entendre-up 260ms var(--ease-glass) both; }
@keyframes entendre-up { from { transform: translateY(100%); } }
.entendre-quote { margin: 0; font: italic 17px/1.45 var(--display); }
.entendre-rule { height: 1px; margin: 16px 0 6px; background: var(--bar-track); }
.entendre-accordion { display: grid; }
.entendre-row { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 48px;
  padding: 8px 4px; border: 0; border-bottom: 1px solid var(--edge-soft); background: transparent;
  color: var(--text); cursor: pointer; font: inherit; font-size: 14px; text-align: left; }
.entendre-row:hover { color: var(--accent); }
/* The chevron turns rather than snapping: the row it sits on is a disclosure,
   and the quarter-turn IS the affordance saying which way it goes. Its
   reduced-motion guard was written when the row shipped and had been sitting
   there for a transition nobody had declared yet (31 Jul 2026). */
.entendre-chevron { color: var(--muted); font: 22px/1 var(--display); transform: rotate(0);
  transition: transform var(--dur-1) var(--ease-glass); }
.entendre-row[aria-expanded="true"] .entendre-chevron { transform: rotate(90deg); }
.entendre-panel { padding: 9px 12px 12px 28px; border-bottom: 1px solid var(--edge-soft);
  background: color-mix(in srgb, var(--accent-soft) 36%, transparent); }
.entendre-panel p { margin: 0; font-size: 14px; line-height: 1.5; }
.entendre-claus { display: grid; gap: 5px; }
.entendre-listen { justify-content: space-between; }
.entendre-listen > span { display: inline-flex; align-items: center; }
.entendre-foot { display: flex; justify-content: flex-end; margin-top: 12px; }
.entendre-close { min-width: 72px; min-height: 44px; }
/* R10 — Entendre as a centred dialog at the desktop threshold
   (docs/DESKTOP-UX-PLAN.md §4). The bottom-sheet is a phone idiom; on desktop
   the overlay centres its sheet, the sheet caps narrower with a full radius, and
   the slide-up entrance becomes a quiet fade/scale. Same content, same JS, same
   focus trap and Escape handling in components/entendre.js — untouched. The
   safe-area paddings stay (harmless at ~0 inset on desktop). Co-located, same
   specificity as the base rules; inert below 900. The new entendre-pop entrance
   is on .entendre-sheet, so the existing prefers-reduced-motion rule below
   (.entendre-sheet { animation: none !important }) already kills it too. */
@media (min-width: 900px) {
  .entendre-overlay { align-items: center; }
  .entendre-sheet { max-width: 480px; border-bottom: 1px solid var(--edge-soft);
    border-radius: 24px; box-shadow: 0 26px 60px -30px rgba(0, 0, 0, .7);
    animation: entendre-pop 220ms var(--ease-glass) both; }
}
@keyframes entendre-pop { from { opacity: 0; transform: scale(0.96); } }
/* The rows' own dark ground went with la colla v3 (7 Aug 2026): a row is
   transparent now and the CARD under it carries --surface, which the token
   layer already flips — so a per-row dark override would paint a second
   ground on top of the one that is right. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .entendre-sheet { background: color-mix(in srgb, var(--surface) 94%, var(--placa-nuria)); }
}
[data-theme="dark"] .entendre-sheet { background: color-mix(in srgb, var(--surface) 94%, var(--placa-nuria)); }
@media (max-width: 560px) {
  .placa-confirm-actions { align-items: stretch; flex-direction: column; }
  .placa-confirm-actions .btn { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .placa-unread, .entendre-sheet { animation: none !important; }
  .entendre-chevron, .xat-send { transition: none !important; }
  .xat-send:active { transform: none; }
}

/* ---- L'Almanac · Món's cultural knowledge base ---- */
.almanac { max-width: 560px; margin: 0 auto; }
.almanac-head { display: flex; align-items: center; gap: 10px; min-height: 52px;
  border-bottom: 1px solid var(--edge-soft); padding: 0 2px 8px; }
.almanac-head h1 { margin: 0; flex: 1; min-width: 0; font: 700 23px/1.2 var(--display);
  text-transform: uppercase; letter-spacing: .06em; }
.almanac > .hint { margin: 10px 2px 0; }

/* One language for the whole surface: the reader's ladder, not a per-line
   reveal. .is-triat carries the selection (segmented, so background not border).
   Shared by l'Almanac and la Gramàtica — components/llengua.js is the only
   thing that emits it, so the two surfaces cannot drift apart. */
.llengua { --gota-radi: 999px; position: relative;
  display: inline-flex; flex: 0 0 auto; overflow: hidden;
  border: 1px solid var(--edge-soft); border-radius: 999px; }
.llengua-opt { position: relative; z-index: 1;
  min-height: 32px; padding: 5px 12px; border: 0;
  background: transparent; color: var(--muted-firm); cursor: pointer; font-size: 12px;
  transition: color var(--dur-1) var(--ease-glass); }
/* The fill passes to la gota, which glides it between the two sides — and the
   two labels are different widths, so the chip animates its width as well as
   its place. The rule below is the static twin: the side reads as chosen with
   no thumb standing (see the .gota block). */
.llengua-opt.is-triat {
  background: color-mix(in srgb, var(--accent-soft) 68%, var(--surface));
  color: var(--text); }
.gota.is-viva ~ .llengua-opt.is-triat { background: transparent;
  transition: color var(--dur-1) var(--ease-glass); }
@media (prefers-reduced-motion: reduce) {
  .llengua-opt { transition: none; }
}

.almanac-prestatge { margin-top: 24px; }
/* Shelf heads carry a filling rule to the right margin — the ledger line that
   gives seven shelves a rhythm the bare labels never had. */
.almanac-prestatge h2 { display: flex; align-items: center; gap: 12px;
  margin: 0 0 8px; color: var(--muted-firm);
  font: 700 13px/1.3 var(--display); text-transform: uppercase; letter-spacing: .08em; }
.almanac-prestatge h2::after { content: ""; flex: 1; height: 1px;
  background: var(--edge-soft); }
/* «Ara mateix» is the index's one featured band: the calendar is what makes an
   almanac an almanac, so the in-season shelf sits on a warm wash the ordinary
   shelves never wear. Flat — the band is a grouping, not a control; the rows
   inside it stay the raised things. */
.almanac-prestatge.is-ara { padding: 14px 14px 12px;
  border: 1px solid color-mix(in srgb, var(--accent) 24%, var(--edge-soft));
  border-radius: var(--radius); background: color-mix(in srgb,
    var(--accent-soft) 42%, var(--bg)); }
.almanac-prestatge.is-ara h2 { color: var(--accent-ink); }
.almanac-prestatge.is-ara h2::after { background: color-mix(in srgb,
  var(--accent) 30%, transparent); }
.almanac-fila { display: grid;
  grid-template-columns: var(--space-8) minmax(0, 1fr) auto;
  align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4);
  border: 1px solid var(--edge-soft);
  border-radius: 14px; background: color-mix(in srgb, var(--surface) 82%, transparent);
  color: var(--text); text-decoration: none; }
.almanac-fila:hover { background: color-mix(in srgb, var(--surface) 70%, var(--accent-soft)); }
.almanac-fila + .almanac-fila { margin-top: 8px; }
.almanac-fila-copia { display: block; min-width: 0; }
.almanac-fila-fletxa { align-self: center; color: var(--muted); font-size: var(--fs-5); }
.almanac-fila:hover .almanac-fila-fletxa { color: var(--accent-ink); }
.almanac-etiqueta { display: block; margin-bottom: 2px; color: var(--accent-ink);
  font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
.almanac-fila-titol { display: block; font: 700 16px/1.25 var(--display); }
.almanac-resum { display: block; margin-top: 3px; color: var(--muted-firm);
  font-size: 14px; line-height: 1.4; }

/* One cultural artefact, three scales. Five broad rim pieces frame one calm
   illustrated window: the chosen middle ground between an ordinary icon and a
   miniature collectible mosaic. The tile owns no title or meaning — adjacent
   authored copy remains the full label. */
.almanac-imatge { --almanac-glassa: var(--tile-terra);
  --almanac-tinta: var(--tile-terra-ink);
  --almanac-junta: color-mix(in srgb, var(--surface) 94%, transparent);
  position: relative; isolation: isolate;
  display: grid; place-items: center; flex: none; overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--almanac-glassa) 34%, var(--edge-soft));
  border-radius: var(--radius-m);
  background: color-mix(in srgb, var(--almanac-glassa) 9%, var(--surface));
  color: var(--almanac-tinta); }
.almanac-imatge svg { display: block; width: 88%; height: 88%; }
.almanac-imatge-marc path { stroke: var(--almanac-junta); stroke-width: 2;
  stroke-linecap: butt; stroke-linejoin: bevel; }
.almanac-imatge-marc .is-a { fill: color-mix(in srgb,
    var(--almanac-glassa) 56%, var(--surface)); }
.almanac-imatge-marc .is-b { fill: color-mix(in srgb,
    var(--almanac-glassa) 33%, var(--surface)); }
.almanac-imatge-marc .is-c { fill: color-mix(in srgb,
    var(--accent) 22%, var(--surface)); }
.almanac-imatge-finestra { fill: color-mix(in srgb, var(--surface) 93%, transparent);
  stroke: color-mix(in srgb, var(--almanac-glassa) 24%, var(--edge-soft));
  stroke-width: 1; }
.almanac-imatge-fons { opacity: .28; }
.almanac-imatge-ple { fill: currentColor; stroke: none; }
.almanac-imatge.is-fila { width: var(--space-8); height: var(--space-8); }
.almanac-imatge.is-cap { width: 100%; height: calc(var(--space-8) * 2 + var(--space-5));
  border-radius: var(--radius-l); }
.almanac-imatge.is-cap svg { width: calc(var(--space-8) + var(--space-7));
  height: calc(var(--space-8) + var(--space-7)); }
.almanac-imatge.is-verd { --almanac-glassa: var(--tile-green);
  --almanac-tinta: var(--tile-green-ink); }
.almanac-imatge.is-rosa { --almanac-glassa: var(--tile-rose);
  --almanac-tinta: var(--tile-rose-ink); }
.almanac-imatge.is-ambre { --almanac-glassa: var(--tile-amber);
  --almanac-tinta: var(--tile-amber-ink); }
.almanac-imatge.is-blau { --almanac-glassa: var(--tile-blue);
  --almanac-tinta: var(--tile-blue-ink); }
.almanac-imatge.is-bosc { --almanac-glassa: var(--tile-green);
  --almanac-tinta: var(--tile-green-ink); }
.almanac-imatge.is-terra { --almanac-glassa: var(--tile-terra);
  --almanac-tinta: var(--tile-terra-ink); }
/* The calendar-leaf segell — a torn-off wall-calendar page pinned over the
   art wherever an entry HAS a calendar (Món's cover, the entry crest). Pure
   decoration and aria-hidden in the DOM: quanLine says the same thing in
   words on the same surface, which is also why its micro-caps may sit at
   10px (decorative floor, not reading floor). The accent strip on top is the
   calendar's binding; the two-degree lean is a pin, not a layout. */
.almanac-segell { position: absolute; top: 10px; right: 10px; z-index: 1;
  display: flex; flex-direction: column; align-items: center;
  min-width: 46px; padding: 7px 9px 6px; text-align: center;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--edge-soft); border-top: 4px solid var(--accent);
  border-radius: 8px; transform: rotate(2deg); }
.almanac-segell-dia { font: 700 22px/1 var(--display); letter-spacing: -.02em; }
.almanac-segell-mes { font-size: 10px; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted-firm); }
.almanac-segell.is-temporada { flex-direction: row; align-items: baseline;
  gap: 3px; padding: 9px 10px 8px; }
.almanac-segell-fins { color: var(--muted); font-weight: 700; }
.almanac-segell.is-epoca { padding: 9px 10px 8px; }
.almanac-segell-anys { font: 600 13px/1.2 var(--display); }
/* Entry page — a calm read at prose measure. */
.almanac-entrada h1 { text-transform: none; letter-spacing: 0; font-size: 24px; }
.almanac-meta { margin: 10px 2px 0; color: var(--muted); font-size: 13px; }
/* The crest: the same plate treatment as Món's cover — the entry hue washed
   deeper, a soft top light, a hairline frame inset like a printed page, and
   the segell pinned in the corner. One look at two scales, on purpose. */
.almanac-entrada-visual { margin-top: var(--space-4); position: relative; }
.almanac-entrada-visual::after { content: ""; position: absolute; inset: 10px;
  pointer-events: none; border-radius: calc(var(--radius-l) - 8px);
  border: 1px solid color-mix(in srgb, var(--text) 16%, transparent); }
.almanac-entrada-visual .almanac-imatge.is-cap { background:
    radial-gradient(120% 90% at 50% 0%,
      color-mix(in srgb, var(--surface) 55%, transparent), transparent 62%),
    color-mix(in srgb, var(--almanac-glassa) 17%, var(--surface)); }
.almanac-cos { margin-top: 6px; }
.almanac-cos p { margin: 14px 0; font-size: 16px; line-height: 1.55; }
/* The illuminated initial — the first paragraph opens on a display capital,
   the almanac-page move. ::first-letter, so it costs no markup and glossless
   prose is the only prose here (the quiosc's glossed paragraphs, whose first
   child is a button, deliberately get none). */
.almanac-cos > p:first-of-type::first-letter { float: left; margin: 5px 9px 0 0;
  font: 700 44px/0.85 var(--display); color: var(--accent-ink); }
.almanac-paraules, .almanac-fils { margin-top: 26px; }
.almanac-paraules h2, .almanac-fils h2 { display: flex; align-items: center;
  gap: 12px; margin: 0 0 6px; color: var(--muted-firm);
  font: 700 13px/1.3 var(--display); text-transform: uppercase; letter-spacing: .08em; }
.almanac-paraules h2::after, .almanac-fils h2::after { content: ""; flex: 1;
  height: 1px; background: var(--edge-soft); }
.almanac-paraules ul { margin: 0; padding: 0; list-style: none; }
/* The glossary is a ruled ledger: term left, meaning right, a dotted rule
   under each pair — a word list that looks like the almanac's own margin
   notes rather than a data table. */
.almanac-paraula { display: flex; gap: 10px; align-items: baseline;
  justify-content: space-between; padding: 6px 0; font-size: 15px;
  border-bottom: 1px dotted color-mix(in srgb, var(--text) 24%, transparent); }
.almanac-paraula:last-child { border-bottom: 0; }
.almanac-paraula strong { font-family: var(--display); }
.almanac-paraula span { color: var(--muted-firm); font-size: 14px;
  text-align: right; }
.almanac-fil { display: block; padding: 5px 0; font-size: 15px; }
.almanac-fil-prefix { margin-right: 6px; color: var(--muted); font-size: 13px;
  font-style: italic; }
.almanac-amic { margin: 24px 0 4px; color: var(--muted-firm); font-size: 14px;
  font-style: italic; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .almanac-fila { background: color-mix(in srgb, var(--surface) 92%, transparent); }
}
[data-theme="dark"] .almanac-fila { background: color-mix(in srgb, var(--surface) 92%, transparent); }

/* Keyboard focus should be as intentional as every other bit of Mica's tactility.
   This :where() rule already covers .tab (it is an <a>) — sessions are driven
   from the keyboard, so that matters. But a tab is a 999px pill flexed against
   its neighbours at 375px, and a 3px ring offset 3px outside it lands ON the
   adjacent tabs. Pull the ring inside for the tabbar only: same colour, same
   intent, no collision. */
:where(a, button, summary, input, textarea, [tabindex]):focus-visible {
  /* 62% alpha measured 2.52:1 against light --bg (under 1.4.11's 3:1);
     --focus-ring is the same hue at legal weight — 3.85 L / 4.48 D. */
  outline: 3px solid var(--focus-ring) !important;
  outline-offset: 3px;
}
.tab:focus-visible { outline-offset: -3px; }
/* Route focus (A1): #view receives programmatic focus after navigation so AT
   announces the new screen, but the [tabindex] rule above painted its 3px ring
   around the whole shell — a page-sized terracotta frame that means nothing.
   Suppress the ring on the view container ONLY; every real control keeps it. */
#view:focus, #view:focus-visible { outline: none !important; }
/* The same move, one level down (25 Sep 2026): a pick card holds focus on the
   CARD, never on an option (player.js render()), so Enter reaches the session
   root without anything looking chosen. The card is a landing spot, not a
   control — Tab from it reaches the options, and each of those rings as usual. */
.session-stage .exercise[tabindex="-1"]:focus,
.session-stage .exercise[tabindex="-1"]:focus-visible { outline: none !important; }

/* ---- La volta ceràmica · session choreography ----
   A session moves through its blocks like handling glazed pieces: each card
   rises into a softly tinted field, the verdict sets into its edge, then the
   whole stage settles away before the next piece arrives. */
.session-stage {
  --session-tone: var(--accent);
  /* the tone's writing ink (block-label): the 72% recipe holds for every stage
     except amber, which takes its own theme-split ink like everywhere else */
  --session-tone-ink: color-mix(in srgb, var(--session-tone) 72%, var(--text));
  position: relative;
  isolation: isolate;
  animation: ceramic-stage-in 360ms var(--ease-glass) both;
}
.session-stage--warm { --session-tone: var(--tile-amber); --session-tone-ink: var(--tile-amber-ink); }
.session-stage--terra { --session-tone: var(--tile-terra); }
.session-stage--rose { --session-tone: var(--tile-rose); }
.session-stage--blue { --session-tone: var(--tile-blue); }
.session-stage--green { --session-tone: var(--tile-green); }
.session-stage::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: -14px -16px -18px;
  border-radius: 28px;
  background: radial-gradient(94% 62% at 50% 0%,
    color-mix(in srgb, var(--session-tone) 14%, transparent), transparent 76%);
  pointer-events: none;
}
/* ≤480 (A2): the aura's negative horizontal inset overruns the small-phone
   gutter (main pads 14px there) and widens the document — scrollWidth 322 at a
   320 viewport in a live session. Clamp the horizontal reach only; the vertical
   glow and reduced-motion behaviour are untouched. */
@media (max-width: 480px) {
  .session-stage::before { inset: -14px 0 -18px; }
}
.session-stage > * { position: relative; z-index: 1; }
.session-stage .block-label {
  color: var(--session-tone-ink);
  background: color-mix(in srgb, var(--session-tone) 10%, var(--glass-1));
  border-color: color-mix(in srgb, var(--session-tone) 25%, var(--edge-soft));
}
.session-stage .bar-fill {
  background: linear-gradient(90deg, var(--session-tone), var(--accent-2));
}
.session-stage .scene-dot.on { background: var(--session-tone); }
.session-stage .scene-dot.done {
  background: color-mix(in srgb, var(--session-tone) 58%, transparent);
}
.session-stage .exercise {
  animation: ceramic-card-in 380ms var(--ease-glass) both;
  border-color: color-mix(in srgb, var(--session-tone) 20%, var(--edge-soft));
}
/* A painted frame inside a scroller moves with its contents in WebKit.
   Feedback itself carries the result; no second border floats over the text. */
.session-stage--resolved .exercise::after { content: none; }
.session-stage--resolved.is-correct { --session-tone: var(--ok); }
.session-stage--resolved.is-miss { --session-tone: var(--miss); }
.session-stage.ceramic-turn-out {
  pointer-events: none;
  animation: ceramic-turn-out 180ms cubic-bezier(0.42, 0, 0.8, 0.42) both;
}
@keyframes ceramic-stage-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}
@keyframes ceramic-card-in {
  from { opacity: 0; transform: translateY(12px) scale(0.987); filter: saturate(0.82); }
  to { opacity: 1; transform: none; filter: none; }
}
@keyframes ceramic-set {
  from { opacity: 0; transform: scale(0.992); }
  58% { opacity: 1; }
  to { opacity: 0.72; transform: none; }
}
@keyframes ceramic-turn-out {
  from { opacity: 1; transform: translateY(0) scale(1); filter: saturate(1); }
  to { opacity: 0; transform: translateY(-7px) scale(0.992); filter: saturate(0.84); }
}
@media (prefers-reduced-motion: reduce) {
  .session-stage, .session-stage .exercise, .session-stage.ceramic-turn-out,
  .session-stage--resolved .exercise::after { animation: none !important; }
  .session-stage .bar-fill { transition: none !important; }
}

/* ============ El Mosaic · collectible trencadís ============
   Successor to the mural. The renderer (components/mosaic-art.js) emits an
   .mosaic-svg with per-mosaic hex fills; sockets and grout come from vars, so
   the empty cells stay theme-reactive. Defs without a paletteDark are
   brightness-adjusted here (the dark fallback). */
.mosaic-svg { display: block; }
.mosaic-svg rect { shape-rendering: geometricPrecision; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .mosaic-svg[data-nodark] { filter: brightness(0.92) saturate(0.95); }
}
[data-theme="dark"] .mosaic-svg[data-nodark] { filter: brightness(0.92) saturate(0.95); }

/* Un-laid sockets must never trace the hidden subject before completion. In
   light, the cream fill sits imperceptibly on the light ground (measured
   1.01:1); in dark, --tile-cream is a light tan that gains real contrast on
   dark grounds, so the filled cells (present only inside the subject) outlined
   the silhouette — the Sagrada Família was legible at 32/224 (21 Jul 2026).
   The first dark fix (--bar-track at 0.55) was tuned against the old .mural
   plaque, whose own ground was --bar-track-tinted; Avui v2 and Progrés v2
   flattened that plaque, so the sockets now sit directly on --surface-sunk
   wells where 0.55 measured 1.17:1 — and a contiguous region at 1.17:1 is a
   readable shape, which Ryan confirmed on the porta (6 Aug 2026: the outline
   gave the artwork away in dark). Even 0.12 (~1.04:1) left a faintly traceable
   mass in an A/B against no sockets at all, so the bar is light's own number:
   0.05 measures 1.011:1 on --surface-sunk and 1.014 on dark --paper — equal to
   light's 1.010, invisible by construction on every ground the socket can sit
   on. The element opacity dims the tracing stroke with the fill. Two-block
   dark pattern; light keeps the cream fill untouched. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .mosaic-svg .mosaic-socket {
    fill: var(--bar-track); opacity: 0.05; }
}
[data-theme="dark"] .mosaic-svg .mosaic-socket {
  fill: var(--bar-track); opacity: 0.05; }

/* the block that replaces muralBlock on Avui/Progrés/session-end */
.mosaic-wrap { text-align: center; }
.mosaic-active { display: inline-block; }
.mosaic-cap { font-style: italic; }
.mosaic-shelf { display: flex; justify-content: center; align-items: center;
  gap: 8px; margin-bottom: 10px; text-decoration: none; flex-wrap: wrap; }
.mosaic-shelf svg { border-radius: 7px; padding: 3px; opacity: 0.92;
  background: color-mix(in srgb, var(--bar-track) 22%, transparent);
  box-shadow: inset 0 1px 0 var(--edge-soft); }
.mosaic-shelf-more { font-size: 12px; color: var(--muted); font-weight: 600; }
.mosaic-galeria-link { display: inline-block; margin-top: 6px; font-size: 13px;
  color: var(--accent); text-decoration: none; }
.mosaic-galeria-link:hover { text-decoration: underline; }

/* L'obra en curs, inside Progrés's hero (v2, 5 Aug 2026). These classes are
   Progrés's alone — Avui's mosaic block wears the .mosaic-* set above. The
   heavy museum frame (6px --frame over --mat, its own --glaze) read as a second
   raised object inside a card that is already raised, so the art sits in a SUNK
   well now — the same well la corba's curve sits in, one zone down — and the
   hero carries the elevation for both. Nesting de-escalates. */
.mosaic-hero { display: block; width: 100%; margin: 0; padding: 0; border: 0;
  background: none; color: var(--text); font: inherit; text-align: left;
  cursor: pointer; transition: transform var(--dur-2) var(--ease-glass); }
.mosaic-hero:hover { transform: translateY(-1px); }
.mosaic-hero:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 5px;
  border-radius: 16px; }
.hero-frame { position: relative; display: block; padding: 16px; overflow: hidden;
  border: 1px solid var(--edge-soft); border-radius: 16px;
  background: var(--surface-sunk);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--text) 6%, transparent); }
/* NO GHOST TREATMENT HERE, DELIBERATELY. The handoff asks for visible ghost
   shards on both grounds (`--tile-cream` 45% over a `--muted` 45% stroke), and
   that is right for the prototype, whose motif is an abstract arch with no
   secret. It is wrong for a real def: `mosaicSVG` draws a socket ONLY for a
   cell that is part of the subject («.» cells are skipped), so the un-laid
   cells' union IS the hidden silhouette — legible ghosts spell out the piece
   before it is finished. That exact bug was fixed on 21 Jul 2026, when the
   Sagrada Família could be read at 32/224; see the .mosaic-socket note further
   down. The intended reading is ~1.01:1 on BOTH grounds: dark first shipped at
   1.18:1 and Ryan could read the silhouette off the porta's niche (6 Aug 2026),
   so the dark socket opacity came down to match light. */
/* THE ART WELL IS FIXED AND THE PIECE FITS INSIDE IT ON BOTH AXES — nothing in
   a case is sized by its own content (the collection viewer's rule, same
   reasoning one surface over). Mosaic defs are not one shape (12×10, 14×16,
   11×18), so a width-only fit hands the case's height to whichever piece
   happens to be active: measured, the active near-square piece made the hero
   757px tall and a portrait one would have run past 900. */
.hero-art { display: flex; align-items: center; justify-content: center;
  height: clamp(200px, 24vw, 300px); line-height: 0; }
.hero-frame svg { width: auto; height: 100%; max-width: 100%; }
/* EL DIA ZERO. With nothing laid the well holds the invitation instead of a
   grid of deliberately-invisible ghost sockets. `.buit` carries the voice; the
   three overrides are the WELL's, not the pattern's — `.hero-art` zeroes
   line-height for the svg it normally holds, its height is a box the text sits
   inside rather than one the text establishes, and `.buit`'s own generous
   padding would push a two-line sentence out of a 200px case on a phone. */
.hero-art.hero-buit { line-height: var(--lh-ui); font-size: var(--fs-4);
  padding: var(--space-4) clamp(16px, 10%, 64px); }
/* One baseline-aligned line that wraps: what the piece is · how far it has got
   · why it grows. Three spans, so the count can carry the display face without
   the sentence around it changing size. */
.hero-caption { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  width: 100%; margin: 0; padding: 0; border: 0; background: none;
  color: var(--text); font: inherit; text-align: left; cursor: pointer; }
.hero-caption:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 4px;
  border-radius: 8px; }
.hero-kicker { color: var(--tile-amber-ink); letter-spacing: 0.14em; font-weight: 600; }
.hero-count { font: 600 21px/1.2 var(--display); font-variant-numeric: tabular-nums; }
.hero-title { font-size: 20px; }
.hero-living { color: var(--muted); font-size: 12.5px; font-style: italic; }
.hero-shelf { display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  width: 100%; margin: 0; }
.hero-mini { flex: 0 0 auto; padding: 4px; border: 1px solid var(--edge-soft);
  border-radius: 8px; background: var(--surface-sunk); color: inherit;
  cursor: pointer; }
.hero-mini svg { width: 38px; height: auto; display: block; }
.hero-mini:hover, .hero-mini:focus-visible {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--edge-soft)); }
.hero-shelf-more { flex: 0 0 auto; color: var(--muted); font-size: 12px;
  font-weight: 700; }

/* the live session-header medallion — a fixed-size slot, no layout shift */
.mosaic-medallion { width: 48px; height: 48px; flex: 0 0 48px;
  display: grid; place-items: center; }
.mosaic-medallion svg { border-radius: 8px;
  background: color-mix(in srgb, var(--bar-track) 22%, transparent); padding: 2px; }

/* ---- La Galeria (#galeria) ---- */
.galeria { max-width: 560px; margin: 0 auto; }
.galeria-head { display: flex; align-items: center; gap: 10px; margin: 6px 0 14px;
  border-bottom: 1px solid var(--edge-soft); padding-bottom: 10px; }
.galeria-head h1 { font-family: var(--display); font-size: 24px; margin: 0; }
.galeria-head-cos { flex: 1; min-width: 0; }
/* `.xat-sub` is la colla's row subtitle, where nowrap + ellipsis is right. Here
   it is a whole sentence in a column the tessera has just narrowed, so on a
   phone it was truncating to «1.389 tessel·les…». Let it wrap; nothing beside
   it depends on a single line. */
.galeria-head .xat-sub { white-space: normal; overflow: visible; }
/* ---- la tessel·la del capçal (actius de marca 1g, reframed) ----
   The count of finished works, laid by hand: the tile is rotated, its contents
   counter-rotated back level. It says ONE thing — how many works — because the
   anti-streak charter forbids the artefact's original tiers and day counts.

   The ground is --accent-fill, not raw --tile-terra: white on the raw hue is
   3.88:1 on the dark theme, and this tile carries an 11px caption. The caption
   is full white rather than the artefact's 85% for the same reason — 85% lands
   at 4.37 L / 3.75 D, under AA at that size. The letter-spacing does the
   quietening the opacity was there for. */
.galeria-tessella { flex: 0 0 auto; width: 116px; height: 116px;
  border-radius: 22px; background: var(--accent-fill); color: #fff;
  display: grid; place-items: center; transform: rotate(-4deg);
  box-shadow: 0 10px 20px -12px rgba(42, 32, 25, 0.5); }
.galeria-tessella-cos { display: flex; flex-direction: column; align-items: center;
  gap: 2px; transform: rotate(4deg); }
.galeria-tessella-cos strong { font-family: var(--display); font-weight: 700;
  font-size: 46px; line-height: 1; }
.galeria-tessella-cos span { font-size: 11px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; }
/* A phone head is a back link, a title and this: at 375 the tile has to give
   way or the title truncates to nothing. */
@media (max-width: 480px) {
  .galeria-tessella { width: 88px; height: 88px; border-radius: 18px; }
  .galeria-tessella-cos strong { font-size: 34px; }
}
.galeria-current { display: flex; width: 100%; flex-direction: column; align-items: center;
  color: var(--text); font: inherit; text-align: center; cursor: pointer; margin: 8px 0 22px;
  background: var(--glass-1); border: 1px solid var(--edge-soft);
  border-radius: 20px; padding: 20px 16px; box-shadow: var(--glaze);
  transition: transform var(--dur-2) var(--ease-glass); }
.galeria-current:hover { transform: translateY(-1px); }
.galeria-current .mural { background: color-mix(in srgb, var(--bar-track) 20%, transparent); }
/* The obra en curs is drawn at a fixed 300px (a generous scale, and the right
   one wherever there is room), but `mosaicSVG` writes that width onto the svg,
   so on a 320px phone the plaque stood 319px wide inside a card whose content
   box is ~258 — the art pressed out past its own card and the document scrolled
   a pixel. It is capped at the card instead: the svg carries a viewBox, so
   `height: auto` keeps the ratio and its own `xMidYMid meet` keeps the fit on
   both axes. It never scales UP — at 375 and on the desktop shelf 300px is
   still inside the cap, so those widths are unchanged. */
.galeria-current .mosaic-active { max-width: 100%; }
.galeria-current .mosaic-active svg { max-width: 100%; height: auto; }
.galeria-current .mosaic-cap { display: block; margin-top: 9px; }
.galeria-tease { font-family: var(--display); font-style: italic; font-size: 15px;
  color: var(--muted); margin: 8px 0 0; }
.galeria-shelf-title { font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 12px; }
.galeria-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 14px; --galeria-well: 128px; }
.galeria-piece { display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  font: inherit; cursor: pointer; padding: 12px; border-radius: 16px;
  background: var(--glass-1); border: 1px solid var(--edge-soft);
  box-shadow: var(--glaze); color: var(--text);
  transition: transform 0.15s var(--ease-glass); }
.galeria-piece:hover { transform: translateY(-2px); }
/* The pieces are not one shape (La Senyera 12×10, La Giralda 11×18) and
   mosaicSVG is sized by WIDTH, so art left to its own height made every card on
   the shelf sit at a different altitude — the same bug the collection viewer
   fixed with a fixed card. The well is a FIXED SQUARE and the art fits inside it
   on BOTH axes (width/height 100% + the svg's own xMidYMid meet), so the frame
   tops and the title baselines line up across the whole wall whatever the
   aspect. Never size this well by its content. */
.galeria-frame { display: block; line-height: 0; padding: 6px; border-radius: 10px;
  height: var(--galeria-well); background: color-mix(in srgb, var(--bar-track) 22%, transparent);
  box-shadow: inset 0 1px 0 var(--edge-soft); }
/* A DEFINITE height, not an aspect-ratio box: an aspect box still takes its
   intrinsic height from the art inside, so a portrait piece (La Sagrada
   Família 14×16) pushed its whole grid row 20 px taller than its neighbours. */
.galeria-frame svg { width: 100%; height: 100%; }
.galeria-piece-title { font-family: var(--display); font-size: 15px; text-align: center;
  line-height: 1.2; min-height: 2.4em; display: flex; align-items: center;
  justify-content: center; }
/* R8 — at ≥900 the wall runs at browse width (lay-browse on .galeria). The grid
   grows its frames and column count; the single in-progress card stays a
   focused object rather than ballooning to the full width. Co-located, same
   specificity as the base rules; inert below 900. */
@media (min-width: 900px) {
  .galeria-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;
    --galeria-well: 150px; }
  .galeria-current { max-width: var(--w-focus); margin-inline: auto; }
}
/* The door to la paret sits beside the shelf's own title — a second way of
   LOOKING at the collection, not a second collection. */
.galeria-shelf-cap { display: flex; align-items: baseline; gap: 12px;
  flex-wrap: wrap; margin: 0 0 12px; }
.galeria-shelf-cap .galeria-shelf-title { margin: 0; }
.galeria-paret-link { margin-left: auto; font-size: var(--fs-2); color: var(--accent-ink); }
.galeria-paret-link:hover { text-decoration: underline; }

/* ---- La paret (#paret) ----
   The collection hung as ONE surface: justified rows of pieces butted against
   each other with a constant grout gap. Three rules govern the look.

   IT DOES NOT ZOOM. The wall is always fitted whole and the pieces are placed
   in PERCENTAGES of it, so the composition is one resolution-independent
   drawing — no transform, no `will-change` layer, nothing rasterised and then
   scaled. The zoomable first cut was smooth in Chromium and blurred-and-jerky in
   Safari for exactly that reason (see views/paret.js). The close-up lives in the
   collection viewer, which draws one piece large and sharp.

   The ground is a CANVAS and therefore flat — raised = actionable, so the pieces
   are the affordance and the wall they hang on is recessed, with no glaze and no
   cast. And nothing here is a frame: a card border around each piece would turn
   the wall back into the shelf it exists to escape, so a piece carries no chrome
   at rest and earns a rim only on hover/focus, drawn inside the grout that is
   already there.

   Ordering inside the wall's own stacking context stays bare small integers —
   the --z-* ladder is for things ordering against the app, and nothing here
   does. The light and the glints are later siblings, so they paint above the
   ceramic without needing a z-index at all. */
.paret-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 6px 0 10px; }
.paret-head h1 { font-family: var(--display); font-size: var(--fs-7); margin: 0; }
.paret-head-cos { flex: 1 1 200px; min-width: 0; }
.paret-head .xat-sub { white-space: normal; overflow: visible; }
.paret-pista { margin: 0 0 12px; font-size: var(--fs-2); color: var(--muted);
  max-width: var(--measure); }

.paret-estenedor { position: relative; display: grid; place-items: center;
  overflow: hidden; height: 56vh; min-height: 300px;
  border-radius: var(--radius); border: 1px solid var(--edge-soft);
  background: color-mix(in srgb, var(--bar-track) 26%, transparent);
  box-shadow: inset 0 1px 0 var(--edge-soft); }
.paret-mur { position: relative; }
.paret-obra { position: absolute; padding: 0; margin: 0; border: 0; line-height: 0;
  background: none; color: inherit; cursor: pointer; border-radius: 3px;
  transition: box-shadow var(--dur-1) var(--ease-glass); }
.paret-obra:hover { box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 60%, transparent); }
.paret-art { position: relative; display: block; width: 100%; height: 100%; }
.paret-art > svg { display: block; width: 100%; height: 100%; }
/* L'obra daurada is unmistakable on the wall too: the art already carries its
   gold from mosaicSVG, and the piece wears a permanent gold rim so it reads as
   the one different object on the surface from across the room. */
.paret-obra.is-daurat { box-shadow: 0 0 0 2px color-mix(in srgb, var(--daurat) 78%, transparent); }
.paret-obra.is-daurat:hover { box-shadow: 0 0 0 3px var(--daurat); }
/* The obra en curs hangs as itself — half-laid, sockets and all. A quiet rim
   says «still being laid» without naming or numbering it. */
.paret-obra.is-encurs { box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--accent) 34%, transparent); }

/* L'aixecada — the wall assembles itself as it mounts, one piece per tick, in
   the order the collection was earned. The animation IS the progressive mount
   (see views/paret.js), so there is no separate stagger to keep in step. */
@keyframes paret-penja {
  from { opacity: 0; transform: scale(0.93); }
  to { opacity: 1; transform: scale(1); }
}
.paret-obra.is-nova { animation: paret-penja var(--dur-2) var(--ease-glass) both; }

/* LA LLUM — the wall at rest, alive. One soft band drifts across the whole
   composition on a long cycle, and the ceramic flares in its wake shard by
   shard. Everything animated is either this ONE overlay or a clone of a real
   shard sitting in a 2-to-5-path sibling svg, so a repaint is confined to one
   piece's box; the 2 273 paths of the art itself never move. Never animate the
   art — there is no engine in which that is affordable at this node count. */
.paret-onada { position: absolute; left: -25%; right: -25%; top: -10%; bottom: -10%;
  pointer-events: none;
  background: linear-gradient(104deg, transparent 38%, var(--paret-llum) 50%, transparent 62%);
  animation: paret-onada 26s linear infinite; }
@keyframes paret-onada {
  0% { transform: translate3d(-16%, 0, 0); opacity: 0; }
  18%, 82% { opacity: 1; }
  100% { transform: translate3d(16%, 0, 0); opacity: 0; }
}
/* Les guspires: the clone lands exactly over its original because the overlay
   shares the piece's viewBox and preserveAspectRatio. A CSS rule beats a
   presentation attribute, which is what lets the flash overwrite the shard's
   own fill without the renderer knowing anything about it. */
.paret-guspires { position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; overflow: visible; }
.paret-guspira { fill: #fff; stroke: none; opacity: 0;
  animation: paret-guspira 13s linear infinite; }
/* A quick catch and a slow release — a glaze glowing and letting go, never a
   blink. The long tail is also what widens the wave front into a band. */
@keyframes paret-guspira {
  0%, 100% { opacity: 0; }
  4% { opacity: calc(var(--guspira) * var(--forca, 1)); }
  24% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* The static twin: the wall simply arrives assembled and stays still. Nothing
     is carried by the motion — every piece is there either way, and the light is
     decoration with no meaning to lose. The JS mirrors this (`reducedMotion()`),
     so the onada is never even built. */
  .paret-obra.is-nova { animation: none; }
  .paret-onada, .paret-guspira { animation: none; opacity: 0; }
}
@media (min-width: 900px) {
  .paret-estenedor { height: clamp(460px, 72vh, 780px); }
  .paret-pista { margin-bottom: 14px; }
}


/* Shared full-screen collection viewer.
   ONE CARD, FIXED GEOMETRY (22 Jul 2026). The old viewer laid a framed picture
   and a plaque out as two separate cream slabs, each sized by its own content:
   two objects instead of one exhibit (loudest in light mode, cream on the dark
   ground), and since the pieces are not one shape — 12x10, 14x16, 11x18 — the
   art resized on every flick and the plaque resized again on every language
   change. So the card has a FIXED size at each tier, the art well fits any
   aspect on BOTH axes, and the plaque scrolls INSIDE the card. The plaque's
   share is a fixed fraction (`flex: 0 0 46%`) rather than `auto` on purpose:
   let it size to its text and the art well breathes with it, which is the same
   bug wearing one card. Nothing here may be sized by its own content. */
.sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.collection-viewer { position: fixed; inset: 0; z-index: var(--z-viewer); color: #f8efe5;
  background: var(--viewer-backdrop); animation: viewer-open var(--dur-2) var(--ease-glass) both; }
/* One soft bloom behind the card so the ground reads as a lit room rather than
   a flat fill. Decorative and click-through — the ground is the exit. */
.collection-viewer::before { content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(118% 76% at 50% 36%, rgba(255, 244, 231, 0.08), transparent 64%); }
.collection-viewer.closing { pointer-events: none; animation: viewer-close 200ms var(--ease-exit) both; }
@keyframes viewer-open { from { opacity: 0; } to { opacity: 1; } }
@keyframes viewer-close { from { opacity: 1; } to { opacity: 0; } }
/* The ground fades, the exhibit RISES — one small scale is the difference
   between "a div appeared" and something being presented to you. On the panel,
   not on `.viewer-card`: cards mount as you reach them (and on idle, ahead of
   you), so animating the card would replay this for every piece, off-screen. */
.viewer-panel { position: relative; display: flex; flex-direction: column;
  width: min(100%, 1120px); height: 100dvh; margin: 0 auto; overflow: hidden;
  animation: viewer-rise var(--dur-2) var(--ease-glass) both; }
@keyframes viewer-rise { from { opacity: 0; transform: scale(0.965); } to { opacity: 1; transform: none; } }
.collection-viewer.closing .viewer-panel { animation: viewer-sink 200ms var(--ease-exit) both; }
@keyframes viewer-sink { from { opacity: 1; transform: none; } to { opacity: 0; transform: scale(0.98); } }
.viewer-top { z-index: 5; display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 10px;
  min-height: 60px; padding: max(10px, env(safe-area-inset-top)) 14px 6px; }
.viewer-close { justify-self: start; }
.viewer-counter { justify-self: center; font-size: 12px; letter-spacing: 0.1em;
  text-transform: lowercase; font-variant-numeric: tabular-nums; color: rgba(248, 239, 229, 0.72); }
.viewer-llengua { justify-self: end; }
/* Hidden in place, never removed: the mystery slide has no second reading side,
   and top chrome that reflows mid-swipe is the jitter this pass removed. */
.viewer-llengua.is-buida { visibility: hidden; }
/* The toggle's own theatre dress: the thumb takes the ground's tint through
   --gota-fill rather than a second .is-triat background (one skin, one fill). */
.viewer-llengua .llengua { border-color: rgba(255, 255, 255, 0.22);
  --gota-fill: rgba(255, 255, 255, 0.15); }
.viewer-llengua .llengua-opt { color: rgba(248, 239, 229, 0.72); }
.viewer-llengua .llengua-opt.is-triat { background: rgba(255, 255, 255, 0.15); color: #f8efe5; }
.viewer-llengua .gota.is-viva ~ .llengua-opt.is-triat { background: transparent;
  transition: color var(--dur-1) var(--ease-glass); }
.viewer-close, .viewer-nav { display: grid; place-items: center; border: 1px solid rgba(255,255,255,0.18);
  background: rgba(255,255,255,0.07); color: #f8efe5; cursor: pointer;
  transition: background var(--dur-1) ease, border-color var(--dur-1) ease,
    transform var(--dur-1) var(--ease-glass); }
.viewer-close:hover, .viewer-nav:not(:disabled):hover {
  background: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.34); }
.viewer-close { width: 44px; height: 44px; border-radius: 999px; font: 26px/1 system-ui, sans-serif; }
/* Arrows are a mouse idiom. On touch the swipe and the dots say it better, and
   two vestigial chevrons over the art were half the "cheap plugin" tell. */
.viewer-controls { display: none; position: absolute; inset: 60px 0 40px; z-index: 4;
  pointer-events: none; }
.viewer-nav { position: absolute; top: 50%; width: 46px; height: 46px; border-radius: 999px;
  font: 30px/1 var(--display); transform: translateY(-50%); pointer-events: auto; }
.viewer-nav:not(:disabled):hover { transform: translateY(-50%) scale(1.06); }
.viewer-prev { left: 10px; }
.viewer-next { right: 10px; }
.viewer-nav:disabled { opacity: 0.16; cursor: default; }
/* The app's --focus-ring is tuned for paper; on the theatre ground it all but
   vanishes, so the viewer's own controls carry the warm ring instead. */
.viewer-close:focus-visible, .viewer-nav:focus-visible,
.viewer-llengua .llengua-opt:focus-visible { outline: 3px solid #f0a77f; outline-offset: 3px; }
.viewer-viewport { flex: 1 1 auto; min-height: 0; overflow: hidden; touch-action: pan-y; }
.viewer-track { display: flex; width: 100%; height: 100%; will-change: transform;
  transition: transform var(--dur-2) var(--ease-glass); }
/* The finger owns the track while it is down; JS sets the settle duration from
   the flick's own velocity on release. */
.viewer-track.is-dragging { transition: none; }
.viewer-slide { flex: 0 0 100%; width: 100%; height: 100%; min-width: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 2px clamp(12px, 4vw, 22px) 6px; }
.viewer-card { display: flex; flex-direction: column; overflow: hidden;
  width: min(100%, 440px); height: 100%; border-radius: 18px;
  background: var(--mat); color: var(--text); text-align: left;
  box-shadow: 0 44px 92px -38px rgba(0, 0, 0, 0.85), 0 0 0 1px var(--frame); }
/* The art well: a shade lighter than the mat in light, a shade darker in dark —
   one card, two zones, no second slab. */
.viewer-stage { flex: 1 1 auto; min-height: 0; background: var(--paper);
  padding: clamp(14px, 3.6vw, 26px); }
.viewer-art { display: grid; place-items: center; width: 100%; height: 100%; line-height: 0; }
.viewer-art svg { display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; }
.viewer-plaque { flex: 0 0 46%; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column;
  padding: clamp(15px, 4vw, 20px) clamp(17px, 5vw, 22px) 20px;
  border-top: 1px solid var(--edge-soft); }
.viewer-plaque > .overline { margin: 0 0 5px; }
.viewer-plaque h2 { margin: 0; font: 700 clamp(22px, 5vw, 29px)/1.2 var(--display);
  text-wrap: balance; }
.viewer-tease { margin: 9px 0 0; color: var(--muted); font-family: var(--display);
  font-style: italic; }
.plaque-en { font-size: 15px; color: var(--muted-firm); font-style: italic; margin: 6px 0 12px; }
.plaque-note { font-size: 15.5px; line-height: 1.62; margin: 10px 0 6px; max-width: 44ch; }
/* The provenance line sits on the card's floor, museum-caption fashion — with a
   fixed card it would otherwise float wherever that piece's prose happened to
   end, which is the "everything moves" complaint in miniature. */
.plaque-meta { font-family: var(--display); font-size: 12.5px; letter-spacing: 0.03em;
  color: var(--muted-firm); margin: 18px 0 0; flex: 0 0 auto;
  border-top: 1px solid var(--edge-soft); padding-top: 10px; }
@media (min-width: 900px) { .plaque-meta { margin-top: auto; } }
.viewer-dots { flex: 0 0 auto; display: flex; justify-content: center; align-items: center;
  gap: 8px; height: 30px; padding-bottom: max(4px, env(safe-area-inset-bottom)); }
.viewer-dot { width: 6px; height: 6px; border-radius: 999px; background: rgba(248,239,229,0.3);
  transition: background var(--dur-1) ease, transform var(--dur-1) var(--ease-glass); }
.viewer-dot.is-current { background: #f8efe5; transform: scale(1.35); }
/* Past eight pieces a dot row stops being readable, so position becomes a rail:
   one thumb, its width the share of the collection one piece is. */
.viewer-rail { position: relative; overflow: hidden; width: min(180px, 46%); height: 3px;
  border-radius: 999px; background: rgba(248,239,229,0.2); }
.viewer-rail-fill { position: absolute; inset: 0 auto 0 0; border-radius: 999px;
  background: #f8efe5; transition: transform var(--dur-2) var(--ease-glass); }

/* Both conditions, deliberately: a mouse, AND room to put the arrows beside the
   card rather than on top of it. A narrow desktop window is still a swipe. */
@media (hover: hover) and (pointer: fine) and (min-width: 900px) {
  .viewer-controls { display: block; }
}

/* A phone turned sideways. This is NOT a third desktop tier — 900px remains the
   one width threshold (docs/LAYOUT.md) — it is an ORIENTATION rule, and it
   exists because the viewer is the rare surface whose card is sized by the
   viewport on both axes: stacked in a 380px-tall window the art gets a sliver
   and the plaque gets less. Same card, same invariant, laid on its side. */
@media (max-width: 899px) and (min-width: 600px) and (max-height: 560px) {
  .viewer-slide { padding: 4px clamp(10px, 3vw, 18px) 4px; }
  .viewer-card { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 44%);
    width: min(100%, 760px); }
  .viewer-plaque { flex: none; padding: 18px 20px 16px;
    border-top: 0; border-left: 1px solid var(--edge-soft); }
  .viewer-stage { padding: 18px; }
}

@media (min-width: 900px) {
  .viewer-top { padding-left: 24px; padding-right: 24px; }
  .viewer-prev { left: 20px; }
  .viewer-next { right: 20px; }
  .viewer-slide { padding: 6px 82px 14px; }
  /* Landscape card: art left, plaque right, both the card's full height. The
     column widths are fixed too — a wide piece and a tall piece get the same
     well, and only the art inside it changes. */
  .viewer-card { display: grid; grid-template-columns: minmax(0, 1fr) minmax(290px, 380px);
    width: min(100%, 960px); height: min(100%, 580px); }
  .viewer-plaque { flex: none; padding: 30px 30px 26px; border-top: 0;
    border-left: 1px solid var(--edge-soft); }
  .viewer-stage { padding: 30px; }
}

@media (max-width: 430px) {
  .hero-shelf { align-items: flex-end; }
  .hero-shelf-pieces { gap: 4px; }
  .hero-mini svg { width: 32px; }
  .viewer-card { border-radius: 16px; }
}

.collection-viewer.reduced, .collection-viewer.reduced.closing { animation: none; }
.collection-viewer.reduced .viewer-track { transition: none; }
.collection-viewer.reduced .viewer-panel,
.collection-viewer.reduced.closing .viewer-panel { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .mosaic-hero, .galeria-current, .galeria-piece { transition: none !important; }
  .mosaic-hero:hover, .galeria-current:hover, .galeria-piece:hover { transform: none; }
  .collection-viewer, .collection-viewer.closing { animation: none !important; }
  .viewer-panel, .collection-viewer.closing .viewer-panel { animation: none !important; }
  .viewer-track { transition: none !important; }
  .viewer-close, .viewer-nav, .viewer-dot { transition: none !important; }
  .viewer-rail-fill { transition: none !important; }
  .viewer-nav:not(:disabled):hover { transform: translateY(-50%); }
  .viewer-dot.is-current { transform: none; }
}

/* ---- El Mosaic · live session feedback (medallion, chip, glint) ---- */
/* fixed-size header slot beside the item counter — never shifts layout */
/* Never shrinks: the counter and the medallion are fixed-meaning chrome, and
   on the study shell this group also carries the llengua toggle. Without the
   floor, the toggle squeezed the counter off the right edge on a 375px phone.
   The block label yields instead — it already ellipses, per the rule above. */
.session-progress-right { display: flex; align-items: center; gap: 12px;
  flex: 0 0 auto; }
.mosaic-medallion.medallion-pulse { animation: medallion-pulse 180ms ease-out; }
@keyframes medallion-pulse {
  0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1); }
}

/* one ceramic chip flying feedback → medallion on a validated correct answer */
.mosaic-chip { position: fixed; width: 13px; height: 13px; border-radius: 3px; z-index: var(--z-flyer);
  background: var(--tile-terra); box-shadow: inset 0 1px 0 rgba(255,255,255,0.4);
  transform: translate(-50%, -50%) rotate(-6deg); opacity: 0; pointer-events: none;
  animation: mosaic-chip-fly 420ms var(--ease-glass) forwards; }
@keyframes mosaic-chip-fly {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(1) rotate(-6deg); }
  12% { opacity: 1; }
  50% { transform: translate(calc(-50% + var(--dx) * 0.5), calc(-50% + var(--dy) * 0.5 + var(--arc))) scale(0.7) rotate(-3deg); }
  100% { opacity: 0.9; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.4) rotate(0deg); }
}

/* session-end: the new tiles catch a one-time specular glint (staggered inline) */
.tile-glint { animation: tile-glint-sweep 500ms ease-out both; transform-box: fill-box;
  transform-origin: center; }
@keyframes tile-glint-sweep {
  0% { filter: brightness(1); } 40% { filter: brightness(1.7) saturate(1.1); } 100% { filter: brightness(1); }
}

@media (prefers-reduced-motion: reduce) {
  .mosaic-chip { display: none; animation: none !important; }
  .mosaic-medallion.medallion-pulse, .tile-glint { animation: none !important; }
}

/* ---- El Mosaic · the completion takeover (ceremony) ---- */
.mosaic-takeover { position: fixed; inset: 0; z-index: var(--z-takeover); display: grid; place-items: center;
  padding: 20px; background: rgba(18, 14, 10, 0); backdrop-filter: blur(0px);
  transition: background 600ms ease-in, backdrop-filter 600ms ease-in;
  /* Safety net, not the plan: the art is height-bounded in JS (fitWidth) so
     the ceremony fits. A very tall piece plus a long note on a short viewport
     can still exceed it, and the continue button must never be unreachable. */
  overflow-y: auto; }
.mosaic-takeover.lit, .mosaic-takeover.reduced { background: rgba(18, 14, 10, 0.72); backdrop-filter: blur(3px); }
.mosaic-takeover.closing { opacity: 0; transition: opacity 260ms ease; }
.takeover-panel { text-align: center; max-width: 92vw; }
.takeover-art { display: inline-block; position: relative; overflow: hidden; line-height: 0;
  padding: 12px; border-radius: 16px; background: rgba(255, 255, 255, 0.05);
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.6);
  transform: scale(0.6); opacity: 0; transition: transform 700ms var(--ease-glass), opacity 500ms ease; }
.lit .takeover-art, .reduced .takeover-art { transform: scale(1); opacity: 1; }
/* one diagonal grout-sheen pass across the finished piece */
.takeover-art::after { content: ""; position: absolute; top: 0; bottom: 0; left: -60%; width: 45%;
  transform: skewX(-18deg); pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(255, 252, 240, 0.5), transparent); }
.lit .takeover-art::after { animation: takeover-sheen 900ms ease-out 700ms 1; }
@keyframes takeover-sheen { from { left: -60%; } to { left: 130%; } }
.takeover-title { font-family: var(--display); font-size: clamp(24px, 6vw, 34px); font-weight: 700;
  color: #fbf3e6; margin: 20px 0 6px; min-height: 1.2em; }
.takeover-letter { display: inline-block; white-space: pre; opacity: 0;
  animation: takeover-letter-in 500ms ease both; }
@keyframes takeover-letter-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.takeover-gloss { font-size: 15px; font-style: italic; color: rgba(251, 243, 230, 0.8);
  margin: 2px 0 10px; opacity: 0; transition: opacity 400ms ease 200ms; }
.takeover-note { font-size: 15px; line-height: 1.6; color: rgba(251, 243, 230, 0.9);
  max-width: 44ch; margin: 0 auto; opacity: 0; transition: opacity 400ms ease 300ms; }
.takeover-cont { margin-top: 18px; opacity: 0; pointer-events: none; transition: opacity 300ms ease 400ms; }
.revealed .takeover-gloss, .revealed .takeover-note, .revealed .takeover-cont { opacity: 1; }
.revealed .takeover-cont { pointer-events: auto; }
.revealed .takeover-letter { opacity: 1; animation: none; } /* skip snaps the title in */

.mosaic-takeover.reduced .takeover-art { transition: none; }
.mosaic-takeover.reduced .takeover-art::after { display: none; }
.mosaic-takeover.reduced .takeover-letter { opacity: 1; animation: none; }
.mosaic-takeover.reduced .takeover-gloss,
.mosaic-takeover.reduced .takeover-note,
.mosaic-takeover.reduced .takeover-cont { opacity: 1; pointer-events: auto; transition: none; }

@media (prefers-reduced-motion: reduce) {
  .mosaic-takeover { transition: none !important; background: rgba(18, 14, 10, 0.72); backdrop-filter: blur(3px); }
  .takeover-art { transition: none !important; transform: none; opacity: 1; }
  .takeover-art::after { display: none; }
  .takeover-letter { opacity: 1 !important; animation: none !important; }
  .takeover-gloss, .takeover-note, .takeover-cont { opacity: 1 !important; transition: none !important; pointer-events: auto; }
}

/* ---- La Graduació · l'obra daurada (the ceremony) ----
   Same overlay, same theatre rung, same skip and cleanup as the completion
   takeover; what differs is the timeline (JS lays the piece over ~10s) and the
   gold. The frame is the only thing gold touches — the art keeps its own
   palette, since the piece is a picture and not a trophy. */
.grad-takeover .takeover-art { padding: 16px; border-radius: 14px;
  background: color-mix(in srgb, var(--daurat) 12%, rgba(255, 255, 255, 0.05));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--daurat) 62%, transparent),
    0 0 0 7px color-mix(in srgb, var(--daurat) 18%, transparent),
    0 26px 70px -22px rgba(0, 0, 0, 0.7); }
/* The piece builds itself shard by shard, so the sheen would sweep an empty
   frame — it belongs to a picture that arrived whole. */
.grad-takeover .takeover-art::after { display: none; }
/* The takeover ground is dark whatever the theme, but only ~0.72 opaque, so on
   a light page behind it raw --daurat measures 2.9:1 — this is 11px uppercase
   and needs 4.5. Lifted toward the same cream the title is written in (4.6:1
   measured against the worst-case composited ground, more in dark). */
.takeover-eyebrow { margin: 20px 0 0;
  color: color-mix(in srgb, var(--daurat) 55%, #fbf3e6);
  font-size: var(--fs-1); font-weight: 800; line-height: var(--lh-ui);
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
  opacity: 0; transition: opacity 400ms ease; }
.grad-takeover .takeover-title { margin-top: 6px; }
/* Its delay is set in JS, so it lands exactly as the last shard does — the
   caption of a picture that has just finished building itself. */
.lit .takeover-eyebrow, .revealed .takeover-eyebrow { opacity: 1; }
.grad-takeover.reduced .takeover-eyebrow { opacity: 1; transition: none; }
@media (prefers-reduced-motion: reduce) {
  .takeover-eyebrow { opacity: 1 !important; transition: none !important; }
}

/* ---- La Graduació · the golden tier on the four collection surfaces ----
   Gold enters as tokens only (--daurat / --daurat-ink, mica-tokens.css); no
   literal hex lives here. Every surface says the same thing in its own scale:
   a warm hairline and a warmer mat around a piece that was earned differently
   from the rest. */
/* 1 · the Avui block's shelf and 3 · the Progrés hero minis — both draw the
   svg directly, and mosaicSVG marks a golden one for them. */
.mosaic-shelf svg.is-daurat, .hero-mini svg.is-daurat {
  background: color-mix(in srgb, var(--daurat) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--daurat) 55%, transparent); }
.hero-mini.is-daurat { border-color: color-mix(in srgb, var(--daurat) 70%, transparent);
  background: color-mix(in srgb, var(--daurat) 10%, var(--mat)); }
.hero-frame.is-daurat { border-color: color-mix(in srgb, var(--daurat) 52%, var(--frame));
  background: color-mix(in srgb, var(--daurat) 9%, var(--mat));
  box-shadow: var(--glaze), inset 0 0 0 1px color-mix(in srgb, var(--daurat) 45%, transparent); }
/* 2 · la Galeria's wall */
.galeria-piece.is-daurat { border-color: color-mix(in srgb, var(--daurat) 45%, var(--edge-soft));
  background: color-mix(in srgb, var(--daurat) 8%, var(--glass-1)); }
.galeria-piece.is-daurat .galeria-frame {
  background: color-mix(in srgb, var(--daurat) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--daurat) 50%, transparent); }
.galeria-piece.is-daurat .galeria-piece-title { color: var(--daurat-ink); }
/* 4 · the collection viewer's card — the plaque IS the diploma */
.viewer-card.is-daurat { box-shadow: 0 44px 92px -38px rgba(0, 0, 0, 0.85),
  0 0 0 1px color-mix(in srgb, var(--daurat) 72%, transparent),
  0 0 0 6px color-mix(in srgb, var(--daurat) 16%, transparent); }
.viewer-card.is-daurat .viewer-stage {
  background: color-mix(in srgb, var(--daurat) 7%, var(--paper)); }
.viewer-card.is-daurat .viewer-plaque > .overline { color: var(--daurat-ink); }

/* ---- El barri · la panoràmica viva i la ruta (v2, 12 Aug 2026) ----
   design/handoffs/barri-v2/. The pencil PNG — Mica's one raster image —
   is retired for a drawing in the app's own hand, and the ten buildings ARE
   the route: each one glazes itself in ceramic as its place is mastered. «De
   mica en mica» made literal, and a drawing that can finally say something.

   What v1 established and this keeps: everything under the drawing speaks the
   app's ordinary card/row language, so el barri reads as a peer of el camí
   rather than a second dialect — one card for the place you are looking at,
   one quiet row per place, and THE INVARIANT, the only mastery bar on the
   surface is the card's.

   Law 1 all the way down: not one raw hex here. Every value is a token or a
   color-mix of tokens, which is what lets the whole scene re-resolve on the
   theme flip for free — the thing a PNG could never do. */
.finestra { width: 100%; margin: var(--space-4) 0 var(--space-3);
  /* The three glazes, declared ONCE for the whole surface (never per element):
     the drawing's buildings and the legend's swatches must be the same green,
     the same accent and the same amber, or the key stops being a key. */
  --glz-fet: var(--tile-green);
  --glz-ara: var(--accent);
  --glz-obert: color-mix(in srgb, var(--tile-amber) 34%, var(--surface)); }

/* Dark softening, in the two-block pattern the system requires (media guarded
   by :not([data-theme="light"]) + an explicit pin twin), or one pin direction
   silently keeps the light glazes. Plus one knob on the whole drawing rather
   than forty per-element overrides: on a dark ground the ceramic reads hot,
   and taking the saturation down is what keeps «glazed» from becoming «neon». */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .finestra {
    --glz-fet: color-mix(in srgb, var(--tile-green) 68%, var(--bg));
    --glz-ara: color-mix(in srgb, var(--accent) 70%, var(--bg));
    --glz-obert: color-mix(in srgb, var(--tile-amber) 24%, var(--surface)); }
  :root:not([data-theme="light"]) .fin-pano { filter: saturate(0.8) brightness(0.97); }
}
[data-theme="dark"] .finestra {
  --glz-fet: color-mix(in srgb, var(--tile-green) 68%, var(--bg));
  --glz-ara: color-mix(in srgb, var(--accent) 70%, var(--bg));
  --glz-obert: color-mix(in srgb, var(--tile-amber) 24%, var(--surface)); }
[data-theme="dark"] .fin-pano { filter: saturate(0.8) brightness(0.97); }

/* The drawing takes the whole canvas and is FLAT (law 2: raised is for what
   you act on, and the card below is what carries the CTA). It is sized by its
   own viewBox — no aspect-ratio, no crop: at 375px the full 1200×440 scene is
   shorter than the 16/7 crop the PNG needed, so the phone gets more drawing
   for less height rather than less. */
.fin-panorama { position: relative; overflow: hidden; margin: 0;
  border: 1px solid var(--rim); border-radius: var(--radius-l);
  background: var(--bg); box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
.fin-pano { display: block; width: 100%; height: auto; }

/* --- the scenery: sky, ridges, the monastery, the viaduct, the meadow --- */
.fin-cel-alt { stop-color: color-mix(in srgb, var(--tile-blue) 11%, var(--bg)); }
.fin-cel-baix { stop-color: var(--bg); }
.fin-sol { fill: none; opacity: 0.55;
  stroke: color-mix(in srgb, var(--accent-2) 70%, var(--text));
  stroke-width: 1.5; stroke-linecap: round; }
.fin-sol > circle { fill: color-mix(in srgb, var(--accent-2) 20%, transparent);
  stroke-width: 1.6; }
.fin-ocells { fill: none; stroke: color-mix(in srgb, var(--text) 32%, transparent);
  stroke-width: 1.4; stroke-linecap: round; }
.fin-serra { stroke-width: 1.5; }
.fin-serra-lluny { fill: color-mix(in srgb, var(--tile-green) 7%, var(--bg));
  stroke: color-mix(in srgb, var(--text) 22%, transparent); }
.fin-serra-mig { fill: color-mix(in srgb, var(--tile-green) 12%, var(--bg));
  stroke: color-mix(in srgb, var(--text) 24%, transparent); }
.fin-prat { fill: color-mix(in srgb, var(--tile-green) 5%, var(--bg));
  stroke: color-mix(in srgb, var(--text) 18%, transparent); }
/* La torre de Collserola and the little church on the far ridge. */
.fin-fita { fill: none; stroke: color-mix(in srgb, var(--text) 40%, transparent);
  stroke-width: 1.5; stroke-linecap: round; }
.fin-monestir { fill: color-mix(in srgb, var(--surface) 88%, transparent);
  stroke: color-mix(in srgb, var(--text) 45%, transparent);
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.fin-teulada { fill: color-mix(in srgb, var(--accent) 22%, transparent); }
.fin-via { fill: none; stroke: color-mix(in srgb, var(--text) 35%, transparent);
  stroke-width: 1.5; stroke-linecap: round; }
.fin-catenaria { stroke-width: 0.8; opacity: 0.5; }
.fin-pins { fill: none; opacity: 0.75; stroke-width: 1.7; stroke-linecap: round;
  stroke: color-mix(in srgb, var(--tile-green) 55%, var(--text)); }
.fin-garlanda { fill: none; stroke-width: 1.2;
  stroke: color-mix(in srgb, var(--text) 35%, transparent); }
/* Trencadís pennants — the one place on the drawing a tile hue is used raw,
   which is exactly what a glaze is for: nothing is written on them. */
.fin-banderoles { stroke: none; opacity: 0.85; }
.fin-banderoles > .is-blau { fill: var(--tile-blue); }
.fin-banderoles > .is-ambre { fill: var(--tile-amber); }
.fin-banderoles > .is-rosa { fill: var(--tile-rose); }
.fin-banderoles > .is-verd { fill: var(--tile-green); }

/* The train crosses left to right and is CLIPPED at the tunnel portal, so it
   visibly enters the hill rather than vanishing; about a third of each loop is
   quiet before the next one. */
.fin-tren { animation: fin-trenpas 34s linear infinite; will-change: transform; }
.fin-tren-cos { fill: color-mix(in srgb, var(--surface) 96%, transparent);
  stroke: color-mix(in srgb, var(--text) 60%, transparent);
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.fin-tren-fin { fill: color-mix(in srgb, var(--text) 16%, transparent); }
.fin-tren-faixa { fill: var(--accent); }
.fin-tunel { stroke: color-mix(in srgb, var(--text) 42%, transparent);
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.fin-tunel-pedra { fill: color-mix(in srgb, var(--tile-green) 13%, var(--bg)); }
.fin-tunel-boca { fill: color-mix(in srgb, var(--text) 38%, var(--bg)); stroke: none; }
@keyframes fin-trenpas { from { transform: translateX(-360px); } to { transform: translateX(1580px); } }

/* The street. The walked stretch re-draws in accent up to the current place's
   DOOR and no further: it is a claim about where you have got to, so it stops
   at a door and never at a guess. */
.fin-carrer { fill: none; stroke-width: 5; stroke-dasharray: 9 7;
  stroke: color-mix(in srgb, var(--text) 16%, transparent); }
.fin-carrer-fet { fill: none; stroke-width: 5; stroke-dasharray: 9 7;
  stroke: color-mix(in srgb, var(--accent) 82%, transparent); }

/* --- the buildings: one per place, five state slots ---
   glaze (roof / awning / cornice) · windows · plaque ink · badge · opacity.
   They are CSS off the group's state class rather than inline fills, which is
   what makes the dark softening three declarations instead of forty — and what
   keeps every colour decision in the token layer where the system says it
   belongs. */
.fin-edificis { fill: none; stroke-width: 1.8; stroke-linecap: round;
  stroke-linejoin: round; stroke: color-mix(in srgb, var(--text) 75%, transparent); }
.fin-edifici { cursor: pointer; outline: none; }
.fin-edifici.is-fora { cursor: default; }
.fin-cos { fill: color-mix(in srgb, var(--surface) 94%, transparent); }
.fin-prim { stroke-width: 1.2; }
.fin-ratlla { stroke-width: 1; opacity: 0.5; }
.fin-columna { stroke-width: 2.4; }
.fin-rellotge { fill: var(--surface); }
.fin-bandera { fill: var(--accent); stroke: none; opacity: 0.9; }
.fin-placa { fill: var(--surface); }
.fin-placa-ico { color: var(--muted); }
/* An invisible slab over each building so the hit target is the whole
   silhouette and not only its strokes — a 1.8px line is not a tap target. */
.fin-toc { fill: transparent; stroke: none; }

.fin-edifici.is-done .fin-glz { fill: var(--glz-fet); }
.fin-edifici.is-current .fin-glz { fill: var(--glz-ara); }
.fin-edifici.is-open .fin-glz { fill: var(--glz-obert); }
.fin-edifici.is-locked .fin-glz { fill: color-mix(in srgb, var(--text) 8%, transparent); }
.fin-edifici.is-locked { opacity: 0.42; }
/* Group opacity multiplies everything inside it, the halo included, so a locked
   place you have selected lifts a little — enough that «you are looking at this
   one» is as visible here as anywhere else, not enough to read as open. */
.fin-edifici.is-locked.is-triat { opacity: 0.62; }

/* The current place is the ONE building with light in its windows — «hi ha
   llum: t'esperen». Every other window is quiet, which is what makes the one
   lit shopfront readable at a glance across the whole panorama. */
.fin-fin { fill: color-mix(in srgb, var(--text) 6%, transparent); }
.fin-edifici.is-current .fin-fin {
  fill: color-mix(in srgb, var(--tile-amber) 62%, var(--surface)); }

.fin-edifici.is-done .fin-placa-ico { color: var(--tile-green-ink); }
.fin-edifici.is-current .fin-placa-ico,
.fin-edifici.is-open .fin-placa-ico { color: var(--accent-ink); }

/* The badge: a seal on a done place, a flag on the one you are at, a padlock
   on one that is not open. Filled marks carry white (law 1's `-fill` rung). */
.fin-medalla > circle { stroke-width: 1.5; stroke: none; }
.fin-medalla-glif { color: #fff; }
/* A glyph from icons.js is drawn at 1.8 on a 24 grid; at 12px inside a filled
   roundel that is a hairline, so the badge thickens its own stroke. */
.fin-medalla-glif .ico { stroke-width: 2.6px; }
.fin-edifici.is-done .fin-medalla > circle { fill: var(--tile-green-fill); }
.fin-edifici.is-current .fin-medalla > circle { fill: var(--accent-fill); }
.fin-edifici.is-locked .fin-medalla > circle { fill: var(--surface);
  stroke: color-mix(in srgb, var(--text) 25%, transparent); }
.fin-edifici.is-locked .fin-medalla-glif { color: var(--muted); }

/* The halo is the selection model made visible: hover or focus shows it at
   half, the selected place at full. Motion never carries the meaning — the
   ring is a state, the fade is only how it arrives. */
.fin-halo { fill: color-mix(in srgb, var(--accent) 7%, transparent);
  stroke: color-mix(in srgb, var(--accent) 65%, transparent); stroke-width: 2;
  opacity: 0; transition: opacity 0.18s ease; }
.fin-edifici.is-sobre .fin-halo { opacity: 0.55; }
.fin-edifici.is-triat .fin-halo,
.fin-edifici:focus-visible .fin-halo { opacity: 1; }
.fin-edifici:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; }

/* Three puffs over the forn's chimney, staggered so they read as one thread of
   smoke rather than three blinks. */
.fin-fum { fill: color-mix(in srgb, var(--text) 25%, transparent); stroke: none;
  opacity: 0.5; }
.fin-fum > circle { transform-box: fill-box;
  animation: fin-fum 5s ease-out infinite; }
.fin-fum > circle:nth-child(2) { animation-delay: 1.6s; }
.fin-fum > circle:nth-child(3) { animation-delay: 3.2s; }
@keyframes fin-fum {
  0% { transform: translateY(0); opacity: 0.55; }
  100% { transform: translateY(-15px); opacity: 0; }
}

/* The static twin, and it is a real one: the train stands on the viaduct, the
   smoke sits over the chimney, and every state the motion carried is still
   said by a glaze, a badge or a word. */
@media (prefers-reduced-motion: reduce) {
  .fin-tren, .fin-fum > circle { animation: none !important; }
  .fin-halo { transition: none; }
}

/* Two vidre pills over the drawing's corners and no more: the count, and the
   colour key the four glazes need (the camí's ●/◉/○ shipped without one — a
   private code with no legend is what that ruling forbids). */
.fin-vidre { position: absolute; z-index: 1; margin: 0;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--edge-soft); border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  backdrop-filter: blur(12px) saturate(1.15);
  -webkit-backdrop-filter: blur(12px) saturate(1.15); }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .fin-vidre { background: var(--surface); }
}
/* The count stays a plain line — its own word space is the gap, so a flex
   `gap` would push the numeral away from the sentence it belongs to. */
.fin-comptador { top: var(--space-3); right: var(--space-3);
  box-shadow: var(--shadow-1); color: var(--text);
  font-size: var(--fs-2); line-height: var(--lh-ui); }
.fin-comptador > strong { color: var(--tile-green-ink); font-variant-numeric: tabular-nums; }
.fin-llegenda { bottom: var(--space-3); right: var(--space-3);
  display: flex; align-items: center; gap: var(--space-4);
  color: var(--muted); font-size: var(--fs-1); line-height: var(--lh-ui); }
.fin-llegenda > span { display: inline-flex; align-items: center; gap: 5px; }
.fin-llegenda i { width: 10px; height: 10px; border-radius: 3px; }
.fin-llegenda i.is-done { background: var(--glz-fet); transform: rotate(-3deg); }
.fin-llegenda i.is-current { background: var(--glz-ara); transform: rotate(2deg); }
.fin-llegenda i.is-open { background: var(--glz-obert); transform: rotate(-2deg); }
.fin-llegenda i.is-locked { transform: rotate(3deg);
  background: color-mix(in srgb, var(--text) 14%, transparent); }

.fin-rule { margin: var(--space-5) 0 0; color: var(--line); }
.fin-ruta { display: grid; gap: var(--space-6); align-items: start;
  margin-top: var(--space-5); }

/* La finestra: one familiar raised card on the place you have chosen. The live
   conversation is a second door beside the drill when the key is available;
   where the place is locked, the gate sentence stands in their place. */
.fin-actual { min-width: 0; padding: var(--space-5);
  border: 1px solid var(--rim); border-radius: var(--radius);
  background: var(--glass-2); box-shadow: var(--shadow-2); }
.fin-actual-cap { margin: 0 0 var(--space-2); color: var(--accent-ink);
  font-size: var(--fs-1); font-weight: 800; line-height: var(--lh-ui);
  letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.fin-actual.is-done .fin-actual-cap { color: var(--tile-green-ink); }
.fin-actual.is-locked .fin-actual-cap { color: var(--muted); }
.fin-actual-nom { margin: 0 0 var(--space-1);
  font: italic 700 var(--fs-7)/var(--lh-display) var(--font-display);
  letter-spacing: 0.01em; text-wrap: balance; }
.fin-actual-lloc { margin: 0 0 var(--space-4); color: var(--muted); font-size: var(--fs-2); }
.fin-actual-frase { margin: 0 0 var(--space-4); color: var(--text);
  font: italic var(--fs-4)/var(--lh-body) var(--font-display); }
.fin-actual-domini { display: flex; justify-content: space-between; gap: var(--space-3);
  margin: 0 0 var(--space-2); color: var(--muted-firm); font-size: var(--fs-2); }
.fin-actual-domini > strong { color: var(--text); font-variant-numeric: tabular-nums; }
.fin-barra { display: block; height: var(--space-1); overflow: hidden;
  margin-bottom: var(--space-5); border-radius: var(--radius-pill);
  background: var(--bar-track); }
.fin-barra > span { display: block; width: var(--fin-mastery); height: 100%;
  border-radius: inherit; background: var(--accent); transition: width 0.35s ease; }
.fin-actual.is-done .fin-barra > span { background: var(--tile-green); }
.fin-actual-cta { display: block; width: 100%; min-height: 44px; text-align: center; }
.fin-actual-cta > span { margin-left: var(--space-1); }
.fin-actual-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3); }
.fin-actual-actions .fin-actual-cta { width: auto; }
.fin-conversa-cta { min-height: 44px; text-align: center; }
/* The gate, where the button would be: a padlock that names nothing is a dead
   end (the camí's ruling, one lloc over). This is what retired the per-row
   popover — the card had a place for it all along. */
.fin-actual-porta { margin: 0; color: var(--muted); font-size: var(--fs-2);
  font-style: italic; }
/* Why «Parla-hi» is dark. Same voice as the gate line above — flat, muted,
   italic — because it answers the same question one state further out: not
   «what opens this place» but «what opens this door». The button itself takes
   the system's own :disabled dimming; nothing here repaints it. */
.fin-conversa-nota { margin: var(--space-2) 0 0; color: var(--muted);
  font-size: var(--fs-2); font-style: italic; }
@media (prefers-reduced-motion: reduce) {
  .fin-barra > span { transition: none; }
}

.fin-llocs { min-width: 0; }
.fin-llocs-cap { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-4); margin-bottom: var(--space-3); }
.fin-llocs-cap h2 { margin: 0; font: 700 var(--fs-6)/var(--lh-display) var(--font-display); }
.fin-llocs-cap p { margin: 0; color: var(--muted); font-size: var(--fs-2); }
.fin-llista { display: grid; gap: var(--space-3); margin: 0; padding: 0; list-style: none; }

/* One quiet row per place: an icon that names the PLACE, the title, its
   position on the route, and one state label with its percentage. Nothing
   else — a row says its state once in ink and once in words, never a third
   time as a graphic. */
.fin-lloc { appearance: none; position: relative; display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
  gap: var(--space-3); width: 100%; min-height: 76px; margin: 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--edge-soft); border-radius: var(--radius-m);
  background: var(--surface); box-shadow: var(--shadow-1);
  color: var(--text); font: inherit; text-align: left;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease; }
.fin-lloc:hover, .fin-lloc.is-sobre {
  border-color: color-mix(in srgb, var(--accent) 42%, var(--edge-soft));
  background: color-mix(in srgb, var(--accent-soft) 26%, var(--surface)); }
.fin-lloc:hover { transform: translateY(-1px); }
.fin-lloc:active { box-shadow: none; transform: none; }
.fin-lloc-icona { display: grid; place-items: center;
  width: var(--space-6); height: var(--space-6);
  border: 1px solid var(--line); border-radius: var(--radius-pill); color: var(--muted); }
.fin-lloc-copy { min-width: 0; }
.fin-lloc-copy > strong { display: block; overflow: hidden; color: inherit;
  font: 700 var(--fs-4)/var(--lh-ui) var(--font-display);
  text-overflow: ellipsis; white-space: nowrap; }
.fin-lloc-copy > span { display: block; overflow: hidden; color: var(--muted);
  font-size: var(--fs-1); line-height: var(--lh-ui);
  text-overflow: ellipsis; white-space: nowrap; }
.fin-lloc-estat { display: flex; flex-direction: column; align-items: flex-end;
  color: var(--muted-firm); font-size: var(--fs-1); font-weight: 750;
  line-height: var(--lh-ui); text-align: right; }

/* States stay in the app's own green / accent / muted families, and each one
   is said once by the ink and once by the label. */
.fin-lloc.is-done .fin-lloc-icona { color: var(--tile-green-ink);
  border-color: color-mix(in srgb, var(--tile-green) 48%, var(--line));
  background: color-mix(in srgb, var(--tile-green) 10%, var(--surface)); }
.fin-lloc.is-done .fin-lloc-estat { color: var(--tile-green-ink); }
.fin-lloc.is-open .fin-lloc-icona,
.fin-lloc.is-current .fin-lloc-icona { color: var(--accent-ink);
  border-color: color-mix(in srgb, var(--accent) 48%, var(--line));
  background: color-mix(in srgb, var(--accent) 9%, var(--surface)); }
.fin-lloc.is-open .fin-lloc-estat,
.fin-lloc.is-current .fin-lloc-estat { color: var(--accent-ink); }
.fin-lloc.is-current { border-color: color-mix(in srgb, var(--accent) 55%, var(--edge-soft)); }
.fin-lloc.is-locked { background: transparent; box-shadow: none; color: var(--muted);
  opacity: 0.68; }
.fin-lloc.is-locked .fin-lloc-estat { color: var(--muted); font-weight: 500; }
/* The chosen row, which is the same fact as the lit building above it. */
.fin-lloc.is-triat { border-color: var(--accent); opacity: 1;
  background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }
@media (prefers-reduced-motion: reduce) {
  .fin-lloc { transition: none; }
  .fin-lloc:hover { transform: none; }
}

.fin-empty { margin: var(--space-6) 0 0; padding: var(--space-5);
  border-radius: var(--radius); background: var(--surface); }
.fin-hint { margin: var(--space-4) 0 0; }

/* The blocks rise in order behind `.is-entrant`, filling BACKWARDS;
   renderEscenaris drops the class on a settle timer whether or not a frame was
   ever served. Only an arrival carries it — climbing out of a street drill is
   a return. Deliberately NOT on the buildings: the drawing arrives whole,
   because a scene that assembles itself every visit would re-stage a ceremony
   that belongs to the session's end. */
.finestra.is-entrant > * { animation: fin-puja var(--dur-3) var(--ease-glass) backwards; }
.finestra.is-entrant > .fin-rule { animation-delay: 0.06s; }
.finestra.is-entrant > .fin-ruta { animation-delay: 0.12s; }
.finestra.is-entrant > .fin-hint { animation-delay: 0.16s; }
@keyframes fin-puja { from { opacity: 0; transform: translateY(12px); } }
@media (prefers-reduced-motion: reduce) {
  .finestra.is-entrant > * { animation: none !important; }
}

@media (min-width: 900px) {
  .finestra { width: min(100%, var(--w-canvas)); margin-inline: auto; }
  /* La finestra on the left in a narrower column, the route beside it: the one
     place the canvas width is actually spent. */
  .fin-ruta { grid-template-columns: minmax(290px, 0.82fr) minmax(0, 1.65fr);
    gap: var(--space-7); margin-top: var(--space-6); }
  /* Two columns when a row can hold its own name, one when it cannot: the
     binding constraint is ROW width, not the viewport (the verb bank's lesson).
     A row's title is one nowrap line, so a hard `repeat(2, 1fr)` truncated
     «A la carnisseria» and «La botiga de música» through the whole 900–1100
     band. 340px is where the longest label still fits beside its icon and
     state, which lands the second column at ~1100. */
  .fin-llista { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
  .finestra-view > :not(.finestra) { max-width: var(--measure); }
}

@media (max-width: 480px) {
  /* The whole scene still fits — a 1200×440 viewBox at 375px is 137px tall,
     shorter than the crop the PNG needed — so the phone loses no drawing. What
     it does lose is the legend: four swatches and four words do not fit over a
     137px band without covering the buildings they explain, and every row below
     says its state in words anyway. */
  .fin-panorama { border-radius: var(--radius); }
  .fin-llegenda { display: none; }
  .fin-comptador { top: var(--space-2); right: var(--space-2);
    padding: var(--space-1) var(--space-2); font-size: var(--fs-1); }
  .fin-actual { padding: var(--space-4); }
  .fin-actual-nom { font-size: var(--fs-6); }
  .fin-actual-actions { grid-template-columns: 1fr; }
  .fin-llocs-cap { align-items: flex-start; flex-direction: column; gap: 0; }
}

/* ---- /conversa · one typed scene inside the selected barri place ---- */
.conversa-page { width: min(100%, var(--w-focus)); margin-inline: auto; }
.conversa-head { display: grid; grid-template-columns: auto auto minmax(0, 1fr);
  align-items: center; gap: var(--space-3); padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--edge-soft); }
.conversa-place-face { display: grid; place-items: center; width: 44px; height: 44px;
  border: 1px solid var(--line); border-radius: var(--radius-pill);
  color: var(--accent-ink); background: var(--surface-sunk); }
.conversa-head-copy { min-width: 0; }
.conversa-head-copy .overline { margin: 0 0 var(--space-1); }
.conversa-head h1 { margin: 0; font: 700 var(--fs-7)/var(--lh-display) var(--display); }
.conversa-head-copy > p:last-child { margin: var(--space-1) 0 0; color: var(--muted);
  font-size: var(--fs-2); }
.conversa-loading { margin: var(--space-6) 0; color: var(--muted);
  font: italic var(--fs-4)/var(--lh-body) var(--display); text-align: center; }
.conversa-fil { display: flex; flex-direction: column; gap: var(--space-3);
  margin: var(--space-5) 0; }
.conversa-stage { max-width: var(--measure); margin: 0 auto var(--space-2);
  color: var(--muted); font: italic var(--fs-3)/var(--lh-body) var(--display);
  text-align: center; }
.conversa-bombolla { max-width: min(84%, 52ch); padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-m); background: var(--surface-sunk); }
.conversa-bombolla p { margin: 0; white-space: pre-wrap; }
.conversa-bombolla.is-character { align-self: flex-start;
  border: 1px solid var(--edge-soft); }
.conversa-bombolla.is-learner { align-self: flex-end;
  background: color-mix(in srgb, var(--accent-soft) 58%, var(--surface)); }
.conversa-nudge { align-self: flex-start; max-width: min(88%, 58ch);
  margin: calc(-1 * var(--space-1)) 0 0 var(--space-3); color: var(--muted-firm);
  font-size: var(--fs-2); }
.conversa-pausa { display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2) var(--space-3); margin: var(--space-2) 0;
  padding: var(--space-4); border-left: 3px solid var(--accent);
  background: var(--surface-sunk); }
.conversa-pausa > strong { color: var(--accent-ink); }
.conversa-pausa p { margin: 0; }
.conversa-pausa .conversa-retry { grid-column: 2; color: var(--muted-firm);
  font-size: var(--fs-2); }
.conversa-form { position: sticky; bottom: var(--space-3); display: grid;
  grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--space-3);
  padding: var(--space-3); border: 1px solid var(--edge-soft);
  border-radius: var(--radius-m); background: var(--surface); box-shadow: var(--shadow-2); }
.conversa-form[hidden] { display: none; }
.conversa-input { min-width: 0; resize: vertical; padding: var(--space-3);
  border: 1px solid var(--line); border-radius: var(--radius-s);
  background: var(--surface-sunk); color: var(--text); font: 16px/1.45 var(--font-body); }
.conversa-form .btn { min-height: 44px; }
.conversa-error { margin-top: var(--space-3); }
.conversa-debrief { margin-top: var(--space-4); padding: var(--space-5);
  border: 1px solid var(--edge-soft); border-radius: var(--radius-m);
  background: var(--surface-sunk); }
.conversa-debrief > p { margin: 0; white-space: pre-wrap; }
.conversa-debrief > p + p { margin-top: var(--space-3); }
.conversa-debrief .conversa-ritual { color: var(--accent-ink);
  font: italic 700 var(--fs-4)/var(--lh-body) var(--display); }
@media (max-width: 480px) {
  .conversa-head { grid-template-columns: auto minmax(0, 1fr); }
  .conversa-place-face { display: none; }
  .conversa-bombolla { max-width: 92%; }
  .conversa-form { grid-template-columns: 1fr; }
}

/* ---- El diari · one written entry, four fixed feedback sections ---- */
.diari-page { width: min(100%, var(--w-focus)); margin-inline: auto; }
.diari-head { display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-4); align-items: start; margin-bottom: var(--space-5); }
.diari-head .overline, .diari-prompt .overline, .diari-estudi .overline { margin: 0 0 var(--space-1); }
.diari-head h1 { margin: 0; font: 700 var(--fs-7)/var(--lh-display) var(--display); }
.diari-head div > p:last-child { max-width: var(--measure); margin: var(--space-1) 0 0;
  color: var(--muted); }
.diari-loading { margin: var(--space-6) 0; color: var(--muted); text-align: center; }
.diari-fil { display: grid; gap: var(--space-4); }
.diari-prompt, .diari-estudi { margin: 0 0 var(--space-3); padding: var(--space-4);
  border: 1px solid var(--edge-soft); border-radius: var(--radius-m); background: var(--surface-sunk); }
.diari-prompt[hidden] { display: none; }
.diari-prompt-text { margin: 0; font: 600 var(--fs-5)/var(--lh-snug) var(--display); }
.diari-estudi > p:last-of-type { margin-bottom: var(--space-3); }
.diari-entry { margin: 0; padding: var(--space-3) var(--space-4);
  border-inline-start: 3px solid var(--tile-terra); color: var(--muted-firm);
  background: color-mix(in srgb, var(--tile-terra) 8%, transparent); }
.diari-form { position: sticky; bottom: var(--space-3); display: grid;
  grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-2); align-items: end;
  margin-top: var(--space-3); padding: var(--space-3); border: 1px solid var(--edge-soft);
  border-radius: var(--radius-m); background: var(--surface); box-shadow: var(--shadow-2); }
.diari-form[hidden] { display: none; }
.diari-input { min-width: 0; resize: vertical; padding: var(--space-3);
  border: 1px solid var(--edge-soft); border-radius: var(--radius-s); background: var(--surface-sunk);
  color: var(--text); font: inherit; }
.diari-form .btn { min-height: 44px; }
.diari-feedback { display: grid; gap: var(--space-4); padding: var(--space-5);
  border: 1px solid var(--edge-soft); border-radius: var(--radius-m);
  background: var(--glass-1); box-shadow: var(--shadow-2); }
.diari-feedback section + section { padding-top: var(--space-3); border-top: 1px solid var(--edge-soft); }
.diari-feedback h2 { margin: 0 0 var(--space-2); font: 650 var(--fs-4)/var(--lh-snug) var(--display); }
.diari-feedback p, .diari-feedback ul { margin: 0; }
.diari-feedback ul { padding-inline-start: 1.25rem; }
.diari-feedback li + li { margin-top: var(--space-1); }
.diari-cap, .diari-closing { color: var(--muted); }
.diari-closing { text-align: center; }
.diari-error { margin-top: var(--space-3); }

@media (max-width: 480px) {
  .diari-head { grid-template-columns: 1fr; }
  .diari-form { grid-template-columns: 1fr; }
}

/* ---- El perfil: chooser + topbar chip (Batch 2, dormant sense registre) ----
   The chooser renders before any learner data exists, so it leans only on
   tokens. Cards are raised (actionable); the medallion reuses the plaça
   monogram idiom in the accent hue — never a portrait. */
.perfil-chooser { padding: 40px 0 24px; text-align: center; }
.perfil-chooser h1 { font-family: var(--display); }
.perfil-hint { color: var(--muted); font-size: 14px; margin: 6px auto 26px; max-width: 40ch; }
.perfil-cards { display: flex; flex-direction: column; gap: 14px; align-items: center; }
.perfil-card { display: flex; align-items: center; gap: 16px; width: min(340px, 100%);
  padding: 14px 18px; text-align: left; cursor: pointer; font: inherit; color: var(--text);
  background: var(--surface); border: 1px solid var(--edge-soft); border-radius: var(--radius-lg, 16px);
  box-shadow: var(--shadow); }
.perfil-card:hover { border-color: var(--accent); }
.perfil-medallion { display: grid; place-items: center; width: 48px; height: 48px;
  flex: 0 0 48px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--accent) 58%, var(--edge-soft));
  background: color-mix(in srgb, var(--accent) 23%, var(--surface));
  color: color-mix(in srgb, var(--accent) 84%, var(--text));
  font: 800 20px/1 var(--display); }
.perfil-cos { display: flex; flex-direction: column; gap: 2px; }
.perfil-nom { font-weight: 700; font-size: 17px; }
.perfil-llengua { color: var(--muted); font-size: 13px; }
/* Chip: quiet chrome beside the theme toggle (the .tema-toggle idiom). */
.perfil-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
  padding: 0 8px; background: none; border: none; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 13px; border-radius: var(--radius-pill); }
.perfil-chip:hover { color: var(--accent-ink); }
/* One profile is not a choice, so the chip is a NAME and not a control: no
   pointer, no hover promise, nothing to press (8 Sep 2026). It keeps the
   strip's geometry exactly — same medallion, same 44px — because the only
   thing that changed is whether it can be tapped. */
.perfil-chip-sol { cursor: default; }
.perfil-chip-sol:hover { color: var(--muted); }
.perfil-chip-medallion { display: grid; place-items: center; width: 20px; height: 20px;
  border-radius: 50%; background: color-mix(in srgb, var(--accent) 23%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 58%, var(--edge-soft));
  color: color-mix(in srgb, var(--accent) 84%, var(--text)); font: 800 11px/1 var(--display); }
@media (max-width: 480px) {
  /* The profile + theme pair exceeded the 320px shell by 13px after the
     profile chip landed. Keep both 44px targets; trim only their side air.
     L'idioma made it three controls (22 Jul 2026) and the row outgrew even
     375: side air tightens to 2px and the chip's NAME folds away — the
     medallion initial still says who is active (R/K in this household) and
     the aria-label keeps the full name. The name returns above 480.
     El so made it four (7 Aug 2026) and cost the least of the four to add:
     a 20px glyph, no word to fold away. */
  .perfil-chip, .tema-toggle { padding-inline: 2px; min-width: 24px; }
  .perfil-chip-nom { display: none; }
  /* …but the button's aria-label is what carried the name through that fold,
     and a plain mark has none (aria-label is prohibited on a generic span).
     So the lone chip HIDES its name instead of removing it: same geometry,
     and a screen reader still reads whose world this is. */
  .perfil-chip-sol .perfil-chip-nom { position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0);
    white-space: nowrap; border: 0; display: inline; }
}

/* ============================================================================
   El Rebost — the pantry reward (docs/REBOST-PLAN.md). Ported from the approved
   comp; every value mapped onto the token layer (§9), so both themes come from
   the tokens flipping — no per-component dark rule is hand-written here. Food
   art (components/rebost-art.js) carries its OWN literal hues and is NEVER
   themed; only these frame/ground/text tokens flip.
   ========================================================================== */
.rebost { max-width: 560px; margin: 0 auto; }
.rebost-head { display: flex; align-items: center; gap: 10px; min-height: 52px;
  border-bottom: 1px solid var(--edge-soft); padding: 0 2px 8px; }
.rebost-head h1 { margin: 0; flex: 1; min-width: 0; font: 700 23px/1.2 var(--display);
  text-transform: uppercase; letter-spacing: .06em; }
.rebost > .hint { margin: 10px 2px 0; }

.reb-eyebrow { margin: 20px 2px 0; color: var(--muted); font-size: 11px; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; }
.reb-acte-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
  margin: 4px 2px 0; }
.reb-dish { margin: 4px 0 0; font: 600 clamp(26px, 5vw, 38px)/1.02 var(--display);
  letter-spacing: -.01em; color: var(--text); }
.reb-season { align-self: center; white-space: nowrap; font-size: 10.5px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--tile-green-ink);
  border: 1px solid color-mix(in srgb, var(--tile-green-viu) 45%, var(--edge-soft));
  border-radius: var(--radius-pill); padding: 5px 11px; }

/* Progress band — a COUNT, never the ingredients (no-spoiler §2). Gathered
   shards fill verd-viu ("yours"), pending stay dashed — the ○→▷→yours ladder. */
.reb-progress { display: flex; align-items: center; gap: 14px; margin: 18px 2px 22px; }
.reb-count { white-space: nowrap; font-size: 12.5px; font-weight: 600; letter-spacing: .04em;
  color: var(--muted-firm); }
.reb-shards { display: flex; gap: 7px; }
.reb-shard { width: 24px; height: 24px; border-radius: 5px 7px 6px 8px; }
.reb-shard.is-on { background: var(--tile-green-viu);
  box-shadow: inset 0 2px 3px color-mix(in srgb, #fff 34%, transparent),
              inset 0 -3px 4px color-mix(in srgb, #000 16%, transparent); }
.reb-shard.is-off { border: 1.6px dashed color-mix(in srgb, var(--muted) 58%, var(--edge-soft)); }
.reb-shard:nth-child(2) { transform: rotate(-4deg); }
.reb-shard:nth-child(3) { transform: rotate(3deg); }
.reb-shard:nth-child(4) { transform: rotate(-2deg); }
.reb-shard:nth-child(5) { transform: rotate(4deg); }

/* Ceramic tesserae shelf — gathering an ingredient glazes a tile (art + name +
   a bare «Collit» stamp). Ungathered = a dashed bisque tile, a `?` enigma, NO
   name and NO art (no-spoiler §2). */
.reb-shelf { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
  gap: 14px; }
.reb-tessella { position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 9px; padding: 18px 14px 15px; border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
  background: linear-gradient(158deg, color-mix(in srgb, #fff 12%, var(--surface)),
    color-mix(in srgb, var(--paper) 60%, var(--surface)));
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 50%, transparent),
              0 10px 22px -14px rgba(0, 0, 0, .28); }
.reb-tessella.is-ghost { justify-content: center; background: transparent; box-shadow: none;
  border: 1.6px dashed color-mix(in srgb, var(--muted) 55%, var(--edge-soft)); }
/* El delit S4 · the enigma's sheen. One faint pass of light across an ungathered
   tile on hover — the mystery answering that you looked at it, and the only
   thing on this shelf that is allowed to tease, since the no-spoiler rule (§2)
   forbids naming or drawing what is behind it.
   THE LINE IT MUST NOT CROSS: an enigma tile is NOT actionable (it is a <div>;
   nothing here is a door), and «raised = actionable» binds motion too. So the
   tile itself never moves, never lifts, never casts, never changes its cursor
   or its border — only light passes over glass. A lift here would promise a tap
   that does not exist, which is the Duolingo line this whole plan is drawn
   against. Fine pointers only: on a phone there is no hover to answer, and a
   sheen left behind on a tapped tile would read as stuck.
   --dur-4 is the token's own «glint» duration; slow is what keeps it a
   reflection rather than a flash.
   IT IS NOT A WHITE BAND, and that was measured, not guessed. The ghost tile is
   transparent, so the sweep runs over --bg — #f7f2ea in light, #201c18 in dark —
   and a fixed white overlay is the one thing that cannot serve both: at 70 % it
   moves cream by five points of red (invisible) and the dark ground by a hundred
   and fifty (a flash). The band is therefore a single soft pass of --muted, the
   ink that is by construction mid-contrast against the ground in EITHER theme:
   a shade travelling over bisque in the light, the same light travelling over it
   in the dark, at the same gentle strength both ways. That is also why there is
   no second rule for dark — the token does the flipping.
   The static twin is the `?` itself: it lifts from its resting 0.5 to 0.72 on
   hover, so the tease still lands under reduced motion (where it simply arrives
   instead of easing) and the motion never carries the meaning alone. */
@media (hover: hover) and (pointer: fine) {
  .reb-tessella.is-ghost { overflow: hidden; }
  .reb-tessella.is-ghost::after { content: ""; position: absolute; inset: 0;
    pointer-events: none; opacity: 0; transform: translateX(-110%);
    background: linear-gradient(102deg, transparent 34%,
      color-mix(in srgb, var(--muted) 15%, transparent) 50%, transparent 66%); }
  .reb-tessella.is-ghost:hover::after { animation: reb-lluor var(--dur-4) var(--ease-glass); }
  .reb-tessella.is-ghost .reb-enigma { transition: opacity var(--dur-2) var(--ease-glass); }
  .reb-tessella.is-ghost:hover .reb-enigma { opacity: 0.72; }
}
@keyframes reb-lluor {
  from { opacity: 0; transform: translateX(-110%); }
  35% { opacity: 1; }
  to { opacity: 0; transform: translateX(110%); }
}
@media (prefers-reduced-motion: reduce) {
  .reb-tessella.is-ghost:hover::after { animation: none; }
  .reb-tessella.is-ghost .reb-enigma { transition: none; }
}
/* Tesserae settle in on load (reduced-motion twin below turns this off). */
@media (prefers-reduced-motion: no-preference) {
  .reb-tessella { opacity: 0; transform: translateY(10px); animation: reb-rise .5s ease forwards; }
  .reb-tessella:nth-child(1) { animation-delay: .05s; }
  .reb-tessella:nth-child(2) { animation-delay: .13s; }
  .reb-tessella:nth-child(3) { animation-delay: .21s; }
  .reb-tessella:nth-child(4) { animation-delay: .29s; }
  .reb-tessella:nth-child(5) { animation-delay: .37s; }
  .reb-tessella:nth-child(n+6) { animation-delay: .45s; }
}
@keyframes reb-rise { to { opacity: 1; transform: none; } }
.reb-art { display: block; width: 64px; height: 64px; }
.reb-art svg { display: block; width: 100%; height: 100%; }
.reb-enigma { display: grid; place-items: center; width: 64px; height: 64px;
  font: 600 38px/1 var(--display); color: var(--muted); opacity: .5; }
.reb-ing-nom { font: 600 16.5px/1.15 var(--display); text-align: center; color: var(--text); }
.reb-stamp { display: inline-flex; align-items: center; gap: 5px; font-size: 9.5px;
  font-weight: 600; letter-spacing: .13em; text-transform: uppercase;
  color: var(--tile-green-viu-ink); }
.reb-stamp::before { content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--tile-green-viu); }
.reb-tessella.is-ghost .reb-stamp { color: var(--muted); }
.reb-tessella.is-ghost .reb-stamp::before { background: none;
  border: 1.4px solid var(--muted); }

/* The anti-streak line, stated out loud (§2). */
.reb-say { margin: 22px 2px 0; max-width: 52ch; font-style: italic; font-size: 15px;
  line-height: 1.6; color: var(--text-secondary); padding-left: 14px;
  border-left: 2px solid color-mix(in srgb, var(--tile-terra) 55%, var(--edge-soft)); }

/* The receptari doorway — cooked dishes, each a link into its fitxa. */
.reb-rubric { margin: 30px 2px 14px; display: flex; align-items: center; gap: 12px;
  color: var(--muted); font: 600 11px/1 var(--display); letter-spacing: .2em;
  text-transform: uppercase; }
.reb-rubric::after { content: ""; height: 1px; flex: 1; background: var(--edge-soft); }
.reb-receptari { margin-top: 8px; }
.reb-recepta-fila { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto;
  align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4);
  border: 1px solid var(--edge-soft); border-radius: 14px;
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  color: var(--text); text-decoration: none;
  transition: background var(--dur-1) var(--ease-glass); }
.reb-recepta-fila:hover { background: color-mix(in srgb, var(--surface) 70%, var(--accent-soft)); }
@media (prefers-reduced-motion: reduce) { .reb-recepta-fila { transition: none; } }
.reb-recepta-fila + .reb-recepta-fila { margin-top: 8px; }
.reb-fila-plat .reb-plat { width: 52px; }
.reb-fila-copia { display: block; min-width: 0; }
.reb-fila-nom { display: block; font: 700 16px/1.25 var(--display); }
.reb-fila-seal { display: block; margin-top: 2px; color: var(--muted-firm); font-size: 12px;
  letter-spacing: .04em; text-transform: uppercase; }
.reb-fila-fletxa { align-self: center; color: var(--muted); font-size: var(--fs-5);
  transition: color var(--dur-1) var(--ease-glass); }
@media (prefers-reduced-motion: reduce) { .reb-fila-fletxa { transition: none; } }
.reb-recepta-fila:hover .reb-fila-fletxa { color: var(--accent-ink); }
.reb-plat { display: block; }
.reb-plat svg { display: block; width: 100%; height: auto; }

/* The cooked dish's fitxa — full colour, no surprise left. */
.rebost-fitxa .rebost-head h1 { text-transform: uppercase; }
.reb-fitxa { position: relative; overflow: hidden; margin-top: var(--space-4);
  padding: clamp(20px, 4vw, 36px); border: 1px solid var(--edge-soft);
  border-radius: var(--radius-l);
  background: linear-gradient(168deg, color-mix(in srgb, #fff 9%, var(--surface)),
    color-mix(in srgb, var(--paper) 55%, var(--surface)));
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 55%, transparent),
              0 40px 70px -46px rgba(0, 0, 0, .5); }
.reb-seal { position: absolute; top: clamp(20px, 4vw, 36px); right: clamp(20px, 4vw, 36px);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  width: 62px; height: 62px; border-radius: 50%; transform: rotate(-7deg);
  border: 1.5px solid color-mix(in srgb, var(--tile-rose) 50%, transparent);
  background: radial-gradient(circle at 40% 35%,
    color-mix(in srgb, var(--tile-rose) 12%, transparent), transparent 70%);
  color: var(--tile-rose-ink); font-size: 9px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; line-height: 1.3; text-align: center; }
.reb-seal b { font-size: 14px; letter-spacing: 0; }
.reb-fitxa > .reb-plat { max-width: 360px; margin: 16px auto 6px; }
.reb-fitxa-titol { margin: 6px 0 4px; text-align: center;
  font: 600 clamp(28px, 6vw, 42px)/1 var(--display); letter-spacing: -.015em; color: var(--text); }
.reb-subtitol { margin: 0 auto 26px; max-width: 46ch; text-align: center; font-style: italic;
  font-size: 16px; line-height: 1.5; color: var(--text-secondary); }
.reb-fitxa .reb-rubric { margin-top: 30px; }
.reb-ribbon { display: flex; flex-wrap: wrap; gap: 10px 20px; margin: 0; padding: 0;
  list-style: none; }
.reb-ribbon li { display: flex; align-items: center; gap: 10px; font-size: 16px; line-height: 1.2; }
.reb-ribbon .reb-art { width: 38px; height: 38px; }
.reb-opt { color: var(--muted); font-size: 13.5px; font-style: italic; }
.reb-metode { margin: 0; padding: 0; list-style: none; counter-reset: pas;
  display: flex; flex-direction: column; gap: 18px; }
.reb-metode li { display: flex; gap: 16px; counter-increment: pas; }
.reb-metode li::before { content: counter(pas); flex: none; display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 50%;
  border: 1.5px solid color-mix(in srgb, var(--tile-terra) 40%, var(--edge-soft));
  background: color-mix(in srgb, var(--tile-terra) 8%, transparent);
  color: var(--tile-terra-ink); font: 600 19px/1 var(--display); }
.reb-metode p { margin: 5px 0 0; font-size: 17px; line-height: 1.5; }
.reb-nota { margin-top: 32px; padding: 20px 22px; border-radius: var(--radius-m);
  background: color-mix(in srgb, var(--tile-amber) 9%, var(--paper));
  border: 1px solid color-mix(in srgb, var(--tile-amber) 22%, var(--edge-soft));
  font-style: italic; font-size: 16px; line-height: 1.62; color: var(--text-secondary); }
.reb-rubric-nota { margin-top: 0; color: var(--tile-amber-ink); }

/* The collit line on the session-end screen — quiet, once per market day. */
.reb-collit { margin: 6px 0 0; color: var(--tile-green-viu-ink); font-size: 14px; }

/* The cook-reveal — the calm counterpart to the mosaic's completionTakeover.
   A dark scrim in both themes (a scrim is theme-independent, like the takeover);
   the plated dish fades up with its name, subtitle and note. Skippable. */
.reb-cook { position: fixed; inset: 0; z-index: var(--z-takeover); display: grid;
  place-items: center; padding: 20px; overflow-y: auto;
  background: rgba(18, 14, 10, 0); backdrop-filter: blur(0);
  transition: background 400ms ease-in, backdrop-filter 400ms ease-in; }
.reb-cook.lit, .reb-cook.reduced { background: rgba(18, 14, 10, .72); backdrop-filter: blur(3px); }
.reb-cook.closing { opacity: 0; transition: opacity 240ms ease; }
.reb-cook-panel { max-width: min(420px, 92vw); text-align: center;
  transform: scale(.7); opacity: 0;
  transition: transform 500ms var(--ease-glass), opacity 400ms ease; }
.reb-cook.lit .reb-cook-panel, .reb-cook.reduced .reb-cook-panel { transform: scale(1); opacity: 1; }
.reb-cook-panel .reb-plat { max-width: 320px; margin: 0 auto; }
.reb-cook-titol { margin: 14px 0 4px; font: 600 clamp(26px, 6vw, 38px)/1 var(--display);
  color: #fbf3e6; }
.reb-cook-subtitol { margin: 0 auto 8px; max-width: 44ch; font-style: italic; font-size: 15px;
  line-height: 1.5; color: rgba(251, 243, 230, .82); }
.reb-cook-nota { margin: 0 auto 6px; max-width: 44ch; font-size: 15px; line-height: 1.6;
  color: rgba(251, 243, 230, .9); }
.reb-cook-cont { margin-top: 18px; }

/* The ingredient reveal — the market-day counterpart to the cook-reveal, and
   lighter: one ceramic ingredient rises with its name and a green «Collit» seal.
   Same dark, theme-independent scrim idiom (a scrim, like the takeover, ignores
   theme); it plays as its own beat before the mosaic lays. Skippable. */
.reb-gather { position: fixed; inset: 0; z-index: var(--z-takeover); display: grid;
  place-items: center; padding: 20px; overflow-y: auto;
  background: rgba(18, 14, 10, 0); backdrop-filter: blur(0);
  transition: background 400ms ease-in, backdrop-filter 400ms ease-in; }
.reb-gather.lit, .reb-gather.reduced { background: rgba(18, 14, 10, .72); backdrop-filter: blur(3px); }
.reb-gather.closing { opacity: 0; transition: opacity 240ms ease; }
.reb-gather-panel { max-width: min(360px, 92vw); text-align: center;
  transform: scale(.7); opacity: 0;
  transition: transform 500ms var(--ease-glass), opacity 400ms ease; }
.reb-gather.lit .reb-gather-panel, .reb-gather.reduced .reb-gather-panel { transform: scale(1); opacity: 1; }
.reb-gather-eyebrow { margin: 0 0 2px; font-size: 11px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: rgba(251, 243, 230, .58); }
.reb-gather-art { display: block; width: 128px; height: 128px; margin: 8px auto 0; }
.reb-gather-art svg { display: block; width: 100%; height: 100%; }
.reb-gather-titol { margin: 8px 0 10px; font: 600 clamp(24px, 5.5vw, 34px)/1 var(--display);
  color: #fbf3e6; }
.reb-gather-stamp { display: inline-flex; align-items: center; gap: 6px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: rgba(251, 243, 230, .9); }
.reb-gather-stamp::before { content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--tile-green-viu); }
.reb-gather-cap { margin: 14px auto 0; max-width: 40ch; font-size: 14px;
  line-height: 1.5; color: rgba(251, 243, 230, .82); }
.reb-gather-cont { margin-top: 20px; }

@media (prefers-reduced-motion: reduce) {
  .reb-tessella { animation: none !important; }
  .reb-cook, .reb-gather { transition: none !important; background: rgba(18, 14, 10, .72);
    backdrop-filter: blur(3px); }
  .reb-cook-panel, .reb-gather-panel { transform: none !important; opacity: 1 !important;
    transition: none !important; }
}

/* ================================================================
   VOCABULARI v2 — el vocabulari en tres grups (5 d'agost 2026,
   design/handoffs/vocabulari-v2/)
   ================================================================
   The flat mastery-sorted list of identical rows became a composition: a vidre
   hero (the view's one) carrying the way out, the title and the live stat line,
   with one quiet plaque that sends him to READ the weakest closed set; then
   three named zones — closed sets as reading-first cards with a per-piece
   mosaic, open domains as a tile grid, the two castellà traps as horizontal
   cards. Nothing about the data moved: `vocabCounts` is still the one predicate
   for what renders, and the never-hollow xuleta rule still decides whether a
   xuleta affordance exists at all.

   Four places the handoff was NOT followed, each a rule that outranks it:
   (1) The page widens by naming an ARCHETYPE — `lay-browse` (--w-browse), not
       the handoff's raw 1080px. docs/LAYOUT.md outranks a handoff value
       (Progrés v2's and la unitat v2's ruling), and the handoff's own "what
       must not change" list keeps lay-browse behaviour anyway.
   (2) The page wash is a RADIAL fading to transparent, not the handoff's
       `linear-gradient(…, var(--bg) 380px)`: an opaque wash bounded by the
       composition draws two vertical seams down any window wider than the
       column — measured on the porta a week earlier, plainly visible at 1280.
   (3) No literal inks anywhere. The teatre scrim takes `--viewer-backdrop` (the
       theme-stable teatre ground the collection viewer already uses) rather
       than the handoff's `#2a2019`, and the panel takes `--shadow-teatre`.
   (4) «La xuleta» is NOT `.btn-ghost`. That control is the app's underlined
       muted link, and beside a filled primary it reads as plain text — the
       gimnàs's «gym-porta» ruling, 31 Jul 2026. It gets a real bordered
       control instead: quieter than the primary, not weaker.

   The closed-set mosaic is EVIDENCE, not a second drawing of the bar: a set's
   drills carry `fam: "<set>.<member>"`, so a member with a reviewed card is a
   member he has met (see workedFlags in views/vocab.js). */

/* THE PAGE. The wash rides the composition (see 2 above); the column and the
   zone rhythm are the handoff's. */
.vocab-page { position: relative; display: flex; flex-direction: column;
  gap: clamp(20px, 2.6vw, 30px); padding: clamp(4px, 1.4vw, 14px) 0 40px;
  background: radial-gradient(64% 440px at 50% -30px,
    color-mix(in srgb, var(--accent) 7%, transparent),
    color-mix(in srgb, var(--accent) 0.4%, transparent) 72%); }
/* The zones rise in order behind `.is-entrant`, filling BACKWARDS; renderVocab
   drops the class on a settle timer whether or not a frame was ever served (the
   mosaic's rule — Progrés, la paret, el gimnàs, el camí, la porta and la unitat
   all pay for it). Only an arrival carries the class: climbing out of a round
   is a return, not a landing. */
.vocab-page.is-entrant > * { animation: voc-puja var(--dur-3) var(--ease-glass) backwards; }
.vocab-page.is-entrant > .voc-zona:nth-of-type(2) { animation-delay: 0.08s; }
.vocab-page.is-entrant > .voc-zona:nth-of-type(3) { animation-delay: 0.16s; }
.vocab-page.is-entrant > .voc-zona:nth-of-type(4) { animation-delay: 0.24s; }
.vocab-page.is-entrant > .voc-lema { animation-delay: 0.3s; }
@keyframes voc-puja { from { opacity: 0; transform: translateY(14px); } }
@media (prefers-reduced-motion: reduce) {
  .vocab-page.is-entrant > * { animation: none !important; }
}

/* ---- the hue vocabulary ----
   A card's identity hue rides two vars, the parallel-ink idiom (--family-ink,
   --session-tone-ink): the glaze paints, the ink writes (law 1). Accent is
   never an identity here — on this surface accent MEANS «the weakest» (the
   plaque, the closed-set wash, the «Nou» card), and a hue that is both an
   identity and a state says neither. */
.voc-hue--blue  { --voc-hue: var(--tile-blue);  --voc-ink: var(--tile-blue-ink); }
.voc-hue--rose  { --voc-hue: var(--tile-rose);  --voc-ink: var(--tile-rose-ink); }
.voc-hue--green { --voc-hue: var(--tile-green); --voc-ink: var(--tile-green-ink); }
.voc-hue--amber { --voc-hue: var(--tile-amber); --voc-ink: var(--tile-amber-ink); }
.voc-hue--terra { --voc-hue: var(--tile-terra); --voc-ink: var(--tile-terra-ink); }
.voc-hue--accent { --voc-hue: var(--accent); --voc-ink: var(--accent-ink); }

/* ---- Zone 1 · the hero ----
   The view's ONE vidre (elevation 3), and it really does sit over something:
   the page's own accent wash. Radius --radius-l like every other v2 hero rather
   than the handoff's 26px — the rooms open the same way. */
.voc-hero { position: relative; overflow: hidden; isolation: isolate;
  border-radius: var(--radius-l); padding: clamp(24px, 4vw, 46px);
  border: 1px solid color-mix(in srgb, var(--rim) 60%, var(--edge-soft));
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .voc-hero { background: var(--surface); }
}
/* Soft-light for the reason measured on the Progrés and porta heroes: straight
   alpha over a surface this size lifts the ground enough to push --muted ink
   under AA. */
.voc-hero .hero-lluu { position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; background: var(--sheen); mix-blend-mode: soft-light; }
/* Trencadís scatter: ornament, never information — aria-hidden, inert, and it
   animates nothing. */
.voc-scatter { position: absolute; top: -26px; right: -14px;
  width: 240px; height: 190px; pointer-events: none; opacity: 0.5; }
.voc-scatter i { position: absolute; display: block; }
.voc-scatter i:nth-child(1) { top: 44px; right: 36px; width: 30px; height: 30px;
  border-radius: 9px; background: var(--tile-blue); opacity: 0.32; transform: rotate(-6deg); }
.voc-scatter i:nth-child(2) { top: 88px; right: 74px; width: 22px; height: 22px;
  border-radius: 7px; background: var(--tile-amber); opacity: 0.4; transform: rotate(4deg); }
.voc-scatter i:nth-child(3) { top: 36px; right: 96px; width: 17px; height: 17px;
  border-radius: 6px; background: var(--tile-rose); opacity: 0.34; transform: rotate(-3deg); }
.voc-scatter i:nth-child(4) { top: 104px; right: 26px; width: 19px; height: 19px;
  border-radius: 6px; background: var(--tile-green); opacity: 0.3; transform: rotate(7deg); }
.voc-scatter i:nth-child(5) { top: 70px; right: 126px; width: 12px; height: 12px;
  border-radius: 4px; background: var(--accent); opacity: 0.26; transform: rotate(-8deg); }

.voc-hero-cos { position: relative; display: flex; flex-direction: column; gap: 18px; }
.voc-hero-cos > .sortida { align-self: flex-start; margin: 0; }
.voc-hero-copia { display: flex; flex-direction: column; gap: 8px; }
/* Two classes so it beats `.dashboard h1`'s 1.5em pin without relying on
   source order. */
.voc-hero .voc-titol { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(38px, 5vw, 54px); line-height: 1.03; letter-spacing: 0.005em; }
.voc-hint { margin: 0; font-size: 15.5px; color: var(--muted);
  max-width: 56ch; text-wrap: pretty; }
.voc-stat { margin: 0; font-size: 13px; color: var(--muted);
  font-variant-numeric: tabular-nums; }

/* «Per començar» — one quiet control inside the hero's glass, never a second
   primary. It opens a xuleta, so it exists only where there is one to open. */
.voc-plaqueta { display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: 16px; max-width: 640px; padding: 16px 20px;
  border-radius: 16px; background: var(--glass-1);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--edge-soft));
  box-shadow: inset 0 1px 0 var(--edge),
    0 4px 16px -10px color-mix(in srgb, var(--accent) 40%, transparent);
  color: inherit; font: inherit; text-align: left; cursor: pointer;
  transition: border-color 0.16s ease, transform 0.16s ease; }
.voc-plaqueta:hover { border-color: var(--accent); transform: translateY(-1px); }
.voc-plaqueta-copia { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.voc-plaqueta-eyebrow { font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; font-weight: 600; color: var(--accent-ink); }
.voc-plaqueta-frase { font-family: var(--display); font-weight: 700;
  font-size: 21px; line-height: 1.15; text-wrap: pretty; }
.voc-plaqueta-dades { display: flex; flex-direction: column;
  align-items: flex-end; gap: 5px; }
.voc-plaqueta-xifres { font-size: 12px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.voc-fletxa { color: var(--accent); font-size: 18px; }
/* The plaque's copy is the widest thing in the hero on a phone; the tile row
   and its figures drop below it rather than squeezing the sentence. */
@media (max-width: 560px) {
  .voc-plaqueta { grid-template-columns: minmax(0, 1fr) auto; row-gap: 12px; }
  /* The arrow is pinned beside the copy, not auto-placed: a full-width figures
     row would otherwise push it onto a third row of its own, where it reads as
     a stray mark rather than «this opens something». */
  .voc-plaqueta > .voc-fletxa { grid-row: 1; grid-column: 2; align-self: center; }
  .voc-plaqueta-dades { grid-row: 2; grid-column: 1 / -1; align-items: flex-start; }
}

/* ---- the zones ---- */
.voc-zona { display: flex; flex-direction: column; gap: 14px; }
.voc-zona-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.voc-zona-head .voc-zona-titol { margin: 0; font-family: var(--display);
  font-style: italic; font-weight: 600; font-size: 22px; }
.voc-zona-head > .grout-rule { flex: 1; min-width: 60px; }
.voc-zona-nota { margin: 0; font-size: 13px; color: var(--muted); }

/* ---- Zone 2 · els conjunts tancats ----
   Reading-first, so this is the one card here that is an article with two
   controls rather than one whole-card button. */
.voc-tancats { display: flex; flex-wrap: wrap; gap: 16px; align-items: stretch; }
.voc-conjunt { flex: 1 1 380px; min-width: min(380px, 100%);
  display: flex; flex-direction: column; gap: 14px; padding: 24px 26px;
  border-radius: 20px; background: var(--surface);
  border: 1px solid var(--edge-soft);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze); }
/* The weakest set wears the accent wash — a STATE, said once in the ground,
   once in the overline and once in the mosaic's hue, never as a fourth mark. */
.voc-conjunt.is-fluix { background: color-mix(in srgb, var(--accent) 4%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 26%, var(--edge-soft)); }
.voc-conjunt-cap { display: flex; align-items: center; gap: 14px; }
.voc-conjunt-noms { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.voc-overline { font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 600; color: var(--muted); }
.voc-fluix { color: var(--accent-ink); }
.voc-conjunt-titol { font-family: var(--display); font-weight: 700;
  font-size: 26px; line-height: 1.05; }
.voc-conjunt-nota { margin: 0; font-size: 14px; line-height: 1.55;
  color: var(--muted); text-wrap: pretty; }
/* ONE quantity, said once in ceramic and once in words (9 Aug 2026): the count
   rides BESIDE the wall rather than under it, so the card lost a whole row
   along with «domini estimat» and «N exercicis», and `.voc-mosaic-bloc` — a
   column wrapper around what is now a single line — went with them. The
   bottom-anchoring moved to `.voc-accions` in the same pass: a set with no
   roster renders no line at all, and the buttons must still sit on the foot.
   It wraps because 13 tessel·les + the figure is ~270px, which is the whole
   card at 320px. */
.voc-mosaic-linia { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.voc-compte { font-size: 12.5px; color: var(--muted); white-space: nowrap;
  font-variant-numeric: tabular-nums; }
.voc-compte b { font-weight: 600; color: var(--text); }
.voc-accions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: auto; }
/* Geometry only — the primary keeps the app's own fill recipe (and its
   theme-split gradient), so nothing here re-declares a colour. */
.voc-primari, .voc-secundari { display: inline-flex; align-items: center;
  justify-content: center; min-height: 44px; border-radius: 12px;
  font-weight: 600; font-size: 14px; }
.voc-primari { padding: 10px 18px; }
.voc-secundari { padding: 10px 16px; background: transparent; color: var(--text);
  border: 1px solid var(--edge-soft); text-decoration: none;
  transition: border-color 0.16s ease; }
.voc-secundari:hover:not(:disabled) { border-color: var(--voc-hue, var(--accent));
  filter: none; box-shadow: none; }

/* ---- Zone 3 · els dominis oberts ----
   One card, one focusable. The bar keeps role=progressbar + barAria, so the
   reading never depends on the mono figure beside it. */
.voc-dominis { display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.voc-domini, .voc-trampa { background: var(--surface);
  border: 1px solid var(--edge-soft);
  box-shadow: inset 0 1px 0 var(--edge), var(--glaze);
  color: inherit; font: inherit; text-align: left; cursor: pointer;
  transition: border-color 0.16s ease, transform 0.16s ease; }
.voc-domini:hover, .voc-trampa:hover {
  border-color: color-mix(in srgb, var(--voc-hue) 48%, var(--edge-soft));
  transform: translateY(-2px); }
.voc-domini { display: flex; flex-direction: column; gap: 10px;
  padding: 20px; border-radius: 18px; }
.voc-domini-cap { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.voc-domini-titol { font-family: var(--display); font-weight: 700;
  font-size: 21px; line-height: 1.1; }
.voc-pct { font-family: var(--font-mono); font-size: 14px; font-weight: 600;
  color: var(--voc-ink); font-variant-numeric: tabular-nums; }
.voc-estat { font-size: 12.5px; color: var(--muted); }
.voc-anar { font-size: 13.5px; font-weight: 600; color: var(--voc-ink); }
/* A practised domain has no state line since 9 Aug 2026 (the percentage is
   already said twice, in mono and in the bar), so the arrow — not the line
   above it — is what has to sit on the card's foot beside its «Nou» siblings. */
.voc-domini > .voc-anar { margin-top: auto; }
/* A domain nobody has opened yet: dashed, unglazed, and NO percentage — 0 % is
   a verdict, and there is nothing yet to pass judgement on. */
.voc-domini.is-nou { box-shadow: none;
  border: 1px dashed color-mix(in srgb, var(--accent) 40%, var(--edge-soft)); }
.voc-domini.is-nou:hover { border-color: var(--accent); }
.voc-pastilla { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 700; padding: 4px 9px; border-radius: 999px;
  background: var(--accent-fill); color: #fff; }

/* ---- Zone 4 · les trampes del castellà ---- */
.voc-trampes { display: flex; flex-wrap: wrap; gap: 14px; }
.voc-trampa { flex: 1 1 340px; min-width: min(340px, 100%);
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: 16px; padding: 18px 20px; border-radius: 18px; }
.voc-trampa-cos { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.voc-trampa-titol { font-family: var(--display); font-weight: 700;
  font-size: 22px; line-height: 1.1; }
.voc-trampa-nota { font-size: 13px; color: var(--muted); }
.voc-trampa-linia { display: flex; align-items: center; gap: 8px; }
.voc-trampa-linia .voc-bar { flex: 1; max-width: 180px; height: 6px; }
.voc-trampa-linia .voc-estat { font-size: 12px; font-variant-numeric: tabular-nums; }
.voc-trampa > .voc-anar { white-space: nowrap; }

/* ---- shared parts ---- */
.voc-chip { display: grid; place-items: center; flex: none;
  width: 44px; height: 44px; border-radius: 13px; color: var(--voc-ink);
  background: color-mix(in srgb, var(--voc-hue) 13%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--voc-hue) 26%, transparent);
  transform: rotate(-2deg); }
.voc-chip-m { width: 50px; height: 50px; border-radius: 14px; transform: rotate(2deg); }
.voc-chip-l { width: 52px; height: 52px; border-radius: 15px; }
.voc-bar { display: block; height: 7px; border-radius: 999px;
  background: var(--bar-track); overflow: hidden; }
.voc-bar > i { display: block; height: 100%; border-radius: 999px;
  background: var(--voc-hue); }
.voc-bar-buida { height: 7px; }
/* One ceramic tile per PIECE — a composition, never a gauge (the Paraules
   ruling): the caption beside it carries the count, and the row itself is
   aria-hidden. Tilts are seeded in JS off the mosaic engine's own hash. */
.voc-mosaic, .voc-mini { display: inline-flex; gap: 4px; flex-wrap: wrap; }
.voc-tessella { display: block; width: 12px; height: 12px; border-radius: 4px;
  background: var(--bar-track); transform: rotate(var(--tilt, 0deg)); }
.voc-tessella.is-laid { background: var(--voc-hue);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2); }
.voc-mini .voc-tessella { width: 10px; height: 10px; border-radius: 3px; }
.voc-mini .voc-tessella.is-laid { background: var(--accent); }

/* The shape is the shared closing-motto rule (§ "The closing motto"); only the
   top margin is Vocabulari's own, exactly as la porta overrides it. */
.voc-lema { margin-top: 6px; }

/* ---- Zone 5 · la xuleta, teatre ----
   Promoted from a page swap to an overlay (desktop dialog / phone sheet): a set
   is now read WITHOUT discarding the list it was read from, which is also why
   it owes no sortida — it never replaced its parent's content. The scrim is a
   real button, so the ground closes by tap and by keyboard alike. */
.voc-teatre { position: fixed; inset: 0; z-index: var(--z-sheet);
  display: grid; align-items: end; justify-items: center; }
.voc-scrim { position: absolute; inset: 0; border: none; padding: 0;
  cursor: pointer; background: color-mix(in srgb, var(--viewer-backdrop) 44%, transparent);
  backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .voc-scrim { background: color-mix(in srgb, var(--viewer-backdrop) 72%, transparent); }
}
.voc-xuleta { position: relative; width: 100%; max-height: 88vh;
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border-radius: var(--radius-l) var(--radius-l) 0 0;
  border: 1px solid color-mix(in srgb, var(--rim) 55%, var(--edge-soft));
  box-shadow: inset 0 1px 0 var(--edge), var(--shadow-teatre);
  animation: voc-sheet-up var(--dur-2) var(--ease-glass); }
@keyframes voc-sheet-up { from { transform: translateY(100%); } }
.voc-x-cap { display: flex; align-items: flex-start; gap: 16px;
  padding: 24px 24px 16px; border-bottom: 1px solid var(--line); }
.voc-x-noms { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.voc-x-noms > .gram-rubric { margin: 0; }
.voc-x-cap .voc-x-titol { margin: 0; font-family: var(--display); font-weight: 700;
  font-size: clamp(26px, 6vw, 32px); line-height: 1.02; }
.voc-x-nota { margin: 0; font-size: 14px; line-height: 1.55; color: var(--muted);
  text-wrap: pretty; max-width: var(--measure); }
.voc-x-compta { margin: 2px 0 0; font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.voc-x-tanca { flex: none; margin-left: auto; display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: 999px; border: 1px solid var(--edge-soft);
  background: transparent; color: var(--muted); cursor: pointer;
  font: 26px/1 system-ui, sans-serif;
  transition: border-color 0.16s ease, color 0.16s ease; }
.voc-x-tanca:hover { border-color: var(--accent); color: var(--accent-ink); }
.voc-x-cos { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain;
  padding: 4px 24px 12px; }
.voc-x-peu { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 14px 24px; border-top: 1px solid var(--line); background: var(--surface); }
.voc-x-resum { margin: 0; flex: 1 1 auto; font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; }

/* A member's row. The forms are the content, so they carry the weight;
   everything under them is apparatus. Deliberately NOT a raised/tappable glaze
   — a xuleta is read, not acted on (the `raised = actionable` rule). */
.xuleta-fila { display: flex; flex-direction: column; gap: 5px;
  padding: 15px 0; border-bottom: 1px solid var(--line); }
.xuleta-fila:last-child { border-bottom: none; }
.xuleta-cap { display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; }
.xuleta-ca { margin: 0; font-family: var(--display); font-weight: 600;
  font-size: 21px; line-height: 1.15; color: var(--text); }
.xuleta-es { margin: 0; font-family: var(--font-mono); font-size: 12px; color: var(--muted); }
.xuleta-en { margin: 0; font-size: 14px; color: var(--muted); }
.xuleta-trampa { margin: 2px 0 0; font-size: 13px; line-height: 1.5;
  display: flex; gap: 8px; align-items: flex-start; color: var(--tile-amber-ink); }
.xuleta-trampa.is-lliure { font-style: italic; color: var(--tile-green-ink); }
.xuleta-punt { flex: none; width: 8px; height: 8px; border-radius: 2.5px;
  margin-top: 5px; background: var(--tile-amber); transform: rotate(-6deg); }
.xuleta-trampa.is-lliure .xuleta-punt { background: var(--tile-green); transform: rotate(5deg); }

/* At ≥900 the sheet becomes a centred dialog. Same panel, different anchor. */
@media (min-width: 900px) {
  .voc-teatre { align-items: center; padding: clamp(12px, 3vw, 32px); }
  .voc-xuleta { width: min(740px, 100%); max-height: min(86vh, 880px);
    border-radius: 22px; animation: voc-teatre-in var(--dur-2) var(--ease-pop); }
  .voc-x-cap { padding: 24px 28px 16px; }
  .voc-x-cos { padding: 4px 28px 16px; }
  .voc-x-peu { padding: 14px 28px; }
  /* The handoff's 36px close button: at this width there is a pointer, and
     below it the 44px touch floor applies instead. */
  .voc-x-tanca { width: 36px; height: 36px; font-size: 22px; }
}
@keyframes voc-teatre-in {
  from { opacity: 0; transform: translateY(16px) scale(0.98); }
}
/* The twins: the sheet arrives in place, cards do not lift, nothing staggers.
   Every one of these carries meaning that the static page already states. */
@media (prefers-reduced-motion: reduce) {
  .voc-xuleta { animation: none !important; }
  .voc-plaqueta:hover, .voc-domini:hover, .voc-trampa:hover { transform: none; }
  .voc-plaqueta, .voc-domini, .voc-trampa, .voc-secundari { transition: none; }
}

/* ---- La marca del taller — rdw (13 Aug 2026) --------------------------------
   design/handoffs/rdw-handoff/. Mica stays the brand; rdw signs the work. Two
   faces of one mark, mounted by the views themselves: the SIGNATURE closes the
   five llocs, the SEAL closes la paret. Which letters each carries is decided
   in components/rdw.js by the brand's scale ladder — never here.

   NOT ONE rdw COLOUR CROSSES THE BORDER, which is the handoff's own rule and
   also Mica's: the mark writes in --text, --muted and (on the seal alone, where
   the handoff allows the accent) --accent-ink, over --surface-sunk. So the
   theme flip costs nothing and there is no dark twin to keep in step.

   Sizes are the brand's ratios, not eyeballed: the mark sits at 35 % of its
   container on both faces. The roundel is 32px rather than the handoff's 26 —
   at 26 its `rdw` would have been 9.5px, under Mica's 11px type floor AND under
   the ladder, which gives a 24–48px container the `r.` shorthand. RODONA and
   RAJOLA in rdw.js are the same two numbers; keep them in step. */
.rdw-peu { display: flex; justify-content: center;
  padding: var(--space-5) 0 var(--space-2); }
.rdw-signatura { display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) var(--space-2); border-radius: var(--radius-pill);
  text-decoration: none; transition: background var(--dur-1) var(--ease-glass); }
.rdw-signatura:hover { background: var(--surface-sunk); }
.rdw-nota { font-size: var(--fs-1); color: var(--muted); }
/* The outline roundel (BRAND variant 5). The border is a mix on --text, so it
   is ink at 25 % in light and cream at 25 % in dark from ONE declaration. */
.rdw-rodona { flex: none; width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center; color: var(--text);
  border: 1px solid color-mix(in srgb, var(--text) 25%, transparent);
  transition: border-color var(--dur-1) var(--ease-glass); }
.rdw-signatura:hover .rdw-rodona {
  border-color: color-mix(in srgb, var(--text) 45%, transparent); }

/* The mark itself. Weight 600 for the wordmark and 700 for the shorthands, and
   never anything else (README rule 2 + BRAND's "Don't" list) — which is why the
   seal's `rdw` is 600 where the handoff's pattern C drew it at 700. */
.rdw-mot { font-family: var(--font-marca); font-weight: 600;
  letter-spacing: -0.05em; line-height: 0.75; }
.rdw-curt { font-weight: 700; letter-spacing: -0.02em; }
.rdw-punt { opacity: 0.45; }
.rdw-rodona .rdw-mot { font-size: 11px; }

/* The colophon seal (pattern C) — the mark pressed into the clay, and the one
   place it takes Mica's accent. Flat by construction: it is READ, not acted on
   beyond its one link, so it wears no card, no glaze and no cast. The tile's
   inset is a literal alpha, exactly as the porta's own niche (.porta-ninxol)
   draws its well — a shadow darkens correctly on both grounds, so no twin. */
.rdw-colofo { display: flex; justify-content: center;
  padding: var(--space-6) 0 var(--space-5); }
.rdw-segell { display: flex; flex-direction: column; align-items: center;
  gap: 18px; text-decoration: none; }
.rdw-rajola { flex: none; width: 56px; height: 56px;
  border-radius: var(--radius-m); display: grid; place-items: center;
  background: var(--surface-sunk); color: var(--accent-ink);
  box-shadow: inset 0 2px 4px rgba(42, 32, 25, 0.12); }
.rdw-rajola .rdw-mot { font-size: 19px; }
.rdw-colofo-cos { display: flex; flex-direction: column; align-items: center;
  gap: 3px; text-align: center; }
.rdw-fet { font-size: var(--fs-1); text-transform: uppercase;
  letter-spacing: 0.18em; color: var(--muted); }
.rdw-lema { font-family: var(--font-display); font-style: italic;
  font-size: var(--fs-2); color: var(--muted); }

/* THE HALLMARK NEVER PERFORMS. Every landing it mounts on rises behind
   `.is-entrant` with a per-child stagger, and a signature joining that parade
   would be the quietest thing on the page asking to be watched — la biblioteca's
   rule, one surface over: a line nobody can act on should not animate. Equal
   specificity to `.porta.is-entrant > *` and later in the file, so it wins; the
   surfaces that name their children individually never reach it at all. */
.is-entrant > .rdw-peu, .is-entrant > .rdw-colofo { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .rdw-signatura, .rdw-rodona { transition: none; }
}

/* Immersive session shell — one viewport, independent content and action.
   All renderers share this structure; tall audio/sorting questions need no
   type-specific height guesses. Desktop retains ordinary document layout. */
.question-content { display: contents; }
.session-mobile-exit { display: none; }
.session-stage--immersive > .exercise { display: flex; flex-direction: column; }
.session-stage--immersive .feedback { order: 2; }
.session-stage--immersive .actions { order: 1; }
.dialeg-pool:empty { display: none; }
@media not all and (min-width: 900px) {
  html:has(.session-stage--immersive),
  body:has(.session-stage--immersive) {
    overflow: hidden; overscroll-behavior: none;
  }
  :root:has(.session-stage--immersive) .topbar,
  :root:has(.session-stage--immersive) .tabbar { display: none; }
  :root:has(.session-stage--immersive) #view {
    position: fixed; top: var(--session-viewport-top, 0px);
    left: 0; right: 0; bottom: auto;
    height: var(--session-viewport-height, 100dvh);
    min-height: 0; max-width: var(--w-focus); margin-inline: auto;
    padding: max(var(--space-2), env(safe-area-inset-top)) var(--space-3)
      max(var(--space-3), env(safe-area-inset-bottom));
    display: flex; flex-direction: column; overflow: hidden;
    animation: none; transform: none; filter: none;
    z-index: var(--z-sticky);
  }
  #view :has(.session-stage--immersive) { display: contents; }
  #view .session-stage--immersive {
    flex: 1 1 0; min-height: 0; width: 100%;
    display: flex; flex-direction: column; padding: 0;
    animation: none; transform: none;
  }
  /* `.bar` is normally allowed to grow when it sits in a horizontal row.
     Here it is a direct child of a full-height COLUMN. Leaving that global
     `flex: 1` in force turns the six-pixel progress track into an empty panel
     that takes half the phone screen (most visibly after Comprova, when the
     keyboard closes). The track is chrome, not session content: keep it at its
     intrinsic height and let the exercise own the remaining band. */
  .session-stage--immersive > .bar { flex: none; width: 100%; }
  .session-mobile-exit { display: contents; }
  .session-stage--immersive .session-progress { margin: 0 0 var(--space-2); }
  .session-stage--immersive .mosaic-medallion {
    width: var(--space-6); height: var(--space-6); flex-basis: var(--space-6);
  }
  #view .session-stage--immersive > .exercise {
    flex: 1 1 0; min-height: 0; max-height: none;
    display: flex; flex-direction: column; overflow: hidden;
    padding: 0; margin-top: var(--space-2);
    animation: none; transform: none; filter: none;
  }
  .session-stage--immersive .question-content {
    display: block; flex: 1 1 0; min-height: 0; overflow: auto;
    overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
    padding: var(--space-4); scroll-padding-block: var(--space-4);
  }
  .session-stage--immersive > .exercise > .actions {
    flex: none; order: 2; position: static; margin: 0;
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--edge-soft); background: var(--surface);
  }
  .session-stage--immersive > .exercise > .actions::before { content: none; }
  .session-stage--immersive > .exercise > .actions > .btn { width: 100%; }
  .session-stage--immersive .feedback { margin-top: var(--space-4); }
  .session-stage--immersive .actions-inline .btn {
    font-size: var(--fs-3); padding: var(--space-2) var(--space-3);
    min-height: calc(var(--space-7) + var(--space-1));
  }
  .is-teclat-obert .session-stage--immersive .answer-input:focus-visible {
    /* The input already has an accent border; avoid a second frame on touch. */
    outline: none !important;
  }
}

/* El terra del boot — the inline reporter in index.html (no module, no import)
   paints here when the module graph never loaded. It rides `.buit`'s warm
   empty-state voice; all this adds is a readable address, which is the one
   fact worth carrying away from a failed boot. `anywhere` because that address
   is an unbroken URL and a phone is 375px wide. */
.boot-terra-detall {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  color: var(--muted-firm);
  overflow-wrap: anywhere;
  margin-top: var(--space-2);
}

/* Offline controls stay hidden until their action is available. */
.public-progress-actions > [hidden] { display: none; }
