/* ═══════════════════════════════════════════════════════════════════════════
   Tienescancha — plain build (Fase 3) · CERAMIC design system
   Copied from docs/design-directions/direction-12/styles.css and extended.
   Base (tokens, mesh, .surface, materials, type scale, dock, motion) is carried
   over unchanged; everything after "§ EXTENSIONS" is new for this phase.
   Amendments vs direction-12 are marked [AMENDED] and are recorded in
   docs/design-directions/chosen-dna.md.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Type family ────────────────────────────────────────────────────────────
   RESOLVED 2026-08-13: Inter, self-hosted, is the product's face.

   The build previously led with Segoe UI Variable and kept Inter as a fallback.
   Two things killed that: Segoe is Windows-only, so the club's front desk and
   the player's phone rendered two different-looking products; and it is
   Microsoft proprietary, so it can be neither redistributed nor web-embedded —
   it could never live in this folder. Inter is SIL OFL, ships here as a single
   48KB variable file (weight 100–900), and renders identically everywhere.

   Self-hosted rather than CDN-linked for two reasons that both still hold: the
   plain build runs from file://, and a booking app should not hand a third
   party a request log of every page view.

   The three optical-size tokens below are kept as the system's API even though
   they now resolve to one family — every screen still reads through them, so
   changing the face later remains an edit to three lines. */
@font-face {
  font-family: "Inter";
  src: url("fonts/inter-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* The Fase 4 font test (Roboto Mono · Noto Sans · Open Sans · DxRuiga) is
   resolved: the system stack won, and it ships as Inter above. The candidate
   faces and their DEV switcher were removed rather than left as dead weight in
   a folder that has to stay self-contained. */

:root {
  /* ── Surface ──────────────────────────────────────────────────────────────
     Two surfaces only. --ceramic is the raised card; --bone is everything
     else — the page canvas AND every recessed area inside a card (inputs,
     row hover, ticket foot, muted chips). Unified 2026-08-14: --ceramic-2
     was a third, near-identical tone that earned nothing. */
  --ceramic: #FFFFFF;
  --bone: #F4F1EC;

  /* ── Ink ──────────────────────────────────────────────────────────────────
     Two text tones only. --ink is primary text AND the text that sits on a
     lime fill (unified from --lime-ink). --ink-2 is every secondary tone:
     body copy, lede, labels, captions, disabled figures (unified from
     --faint, taking the darker of the two, so every label gained contrast). */
  --ink: #171420;
  --ink-2: #4A4657;

  /* ── Accent ─────────────────────────────────────────────────────────────── */
  --lime: #C9FF4A;
  --peach: #FFC9A8;
  --lilac: #D9C8FF;
  --mint: #B9F2DC;
  --rose: #F2748D;
  --rose-ink: #B03A55;

  /* ── Elevation ──────────────────────────────────────────────────────────── */
  --shadow: 0 20px 44px rgba(38, 30, 55, .09);
  --shadow-lift: 0 30px 60px rgba(38, 30, 55, .16);

  /* ── Radius scale ─────────────────────────────────────────────────────────
     The four steps the DNA already described in prose, now addressable. Every
     rounded thing resolves to one of these; 50% (the slot circle) is the one
     deliberate exception, because it is a circle rather than a radius. */
  --r: 34px;            /* panels, cards, modals, the ticket */
  --r-soft: 26px;       /* soft panels, insets, nested blocks */
  --r-field: 18px;      /* inputs, selects, small code blocks */
  --r-pill: 99px;       /* anything pill-shaped: chips, buttons, pills, dock */

  /* ── Gradient ─────────────────────────────────────────────────────────────
     --grad is decorative + the peak-tier ring; --grad-35 is the peak-tier
     fill. Mint only ever appears inside these two and the mesh. */
  --grad: linear-gradient(90deg, var(--peach), var(--lilac) 52%, var(--mint));
  --grad-35: linear-gradient(35deg, var(--peach), var(--lilac) 52%, var(--mint));

  /* ── Motion scale ──────────────────────────────────────────────────────────
     Fase 4 motion layer. Three durations and two easings, sourced from the
     Fase 3 DNA (everything interactive already eased on --ease at 250ms) —
     this block makes them addressable as tokens rather than scattering raw
     ms values. GSAP reads the same numbers; CSS transitions keep using them.
       --t-fast  taps, presses, state flips    140ms
       --t-base  the Fase 3 standard           250ms
       --t-slow  entrances, reveals, sequences 420ms
       --ease-out  the decel curve the whole system uses */
  --t-fast: 140ms;
  --t-base: 250ms;
  --t-slow: 420ms;
  /* One easing name, not two: --ease and --ease-out held the same curve and
     drifted apart in usage. --ease-out is the survivor (it is the one the
     motion scale documents). */
  --ease-out: cubic-bezier(.2, .8, .2, 1);
  /* No CSS spring token: the only spring in the system is the slot-select
     snap, and that is a GSAP elastic.out. A token nothing paints with is a
     claim the stylesheet cannot keep. */

  /* Focus ring paint. Swapped wholesale by .focus-lime for A/B comparison. */
  --focus-ring: linear-gradient(35deg, var(--peach), var(--lilac) 52%, var(--mint));

  /* Three optical sizes of one family. Changing typeface = changing these. */
  /* RESOLVED 2026-08-13: Inter, self-hosted, is THE face — not a fallback.
     Segoe UI Variable was the previous first choice, but it is Microsoft
     proprietary: it cannot be redistributed or web-embedded, so it could never
     live in the project folder, and it only ever rendered on Windows anyway.
     Making Inter primary ships one identical face on the club's front desk and
     the player's phone, which is what the old stack could not promise. The
     three optical-size tokens are kept as the system's API — everything
     downstream still resolves through them, so swapping the face later is
     still an edit to these three lines. */
  --font:         Inter, system-ui, -apple-system, sans-serif;
  --font-display: Inter, system-ui, -apple-system, sans-serif;
  --font-small:   Inter, system-ui, -apple-system, sans-serif;
  --font-mono:    ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* ── Type scale ───────────────────────────────────────────────────────────
     Every font-size in the system resolves to one of these. Changing the
     scale is changing this block — nothing downstream carries a raw px. */
  --fs-h1:       clamp(52px, 10.5vw, 110px);   /* 300, <b> 700 */
  --fs-h2:       clamp(28px, 4vw, 42px);       /* 300, <b> 600 */
  --fs-hero:     clamp(78px, 17vw, 140px);     /* 350 - between 300 and 400 */
  --fs-stat:     40px;                         /* 200 */
  --fs-lede:     20px;                         /* 300 */
  --fs-body:     16px;                         /* 300 */
  --fs-btn:      17px;                         /* 600 — primary button */
  --fs-ui:       15px;                         /* 600 — cell, day pill, dock, small button */
  --fs-ui-sm:    14px;                         /* 600 — filter pill, lang pill */
  --fs-chip:     13px;                         /* 600 — status chip */
  --fs-chip-sub: 11px;                         /* 500 — chip subtext */
  --fs-label:    10px;                         /* 500 — uppercase label */
  --fs-label-sm:  9px;                         /* 500 — uppercase label, dense */

  /* Body-family tiers. These already existed as raw px across ~30 rules;
     tokenised 2026-08-14 at their existing values (no visual change) so the
     whole scale lives in this block and a future "+1px to body" is one edit. */
  --fs-h3:       22px;                         /* 300 — card / section sub-heading */
  --fs-hello:    clamp(38px, 8.6vw, 56px);     /* 300 — the greeting, top of Reservar */
  /* Three steps that previously sat in the 22→40 gap as raw px, one per
     component. Named by role so the next component in that range picks a step
     instead of inventing a fourth. */
  --fs-lead:     23px;                         /* 500 — the grid's instruction line */
  --fs-modal:    26px;                         /* 300 — modal title */
  --fs-price:    30px;                         /* 300 — the figure in panel + ticket */
  --fs-value:    15px;                         /* 500 — the value opposite a label */
  --fs-body-sm:  14px;                         /* 300 — table rows, help text */
  --fs-body-xs:  13px;                         /* 300 — dense secondary copy */
  --fs-meta:     12px;                         /* 300 — finest secondary copy */
}

* { box-sizing: border-box; }

/* Kills the blue flash mobile browsers paint over a tapped element. The system
   already answers every tap with its own lift/press, so the platform highlight
   was a second, off-palette response to the same gesture. */
html { -webkit-tap-highlight-color: transparent; }


body {
  margin: 0; background: var(--bone); color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-body); line-height: 1.55; -webkit-font-smoothing: antialiased;
  font-variant-numeric: tabular-nums; overflow-x: hidden; padding-bottom: 120px;
}
/* [ADDED — Fase 4b mobile pass] Notch/home-indicator clearance for the fixed
   dock. On devices with a safe-area inset the dock's 26px offset sits inside
   the gesture bar region without this; dvh-unaware pages scroll content under
   it too. No visual change where the inset is 0 (desktop, most test frames). */
@supports (padding: max(0px)) {
  body { padding-bottom: max(120px, calc(106px + env(safe-area-inset-bottom))); }
}

/* ── Daylight mesh ─────────────────────────────────────────────────────────
   Blob size is sized against the LARGER viewport axis, not width alone.
   Sizing purely in vw meant that on a tall narrow phone (412 × 914) three
   small circles floated in a mostly-empty column and the 80px blur smeared
   them into one flat tint — the gradient disappeared exactly where the
   product is mostly used. max(vw, vh) keeps each blob large enough to read as
   a distinct body of colour at any aspect ratio. */
.mesh { position: fixed; inset: -15%; z-index: 0; pointer-events: none; filter: blur(80px); }
.mesh i { position: absolute; display: block; border-radius: 50%; }
.m1 { width: max(54vw, 42vh); height: max(54vw, 42vh); background: var(--peach); opacity: .58; top: -4%; left: -6%; animation: d1 30s ease-in-out infinite alternate; }
.m2 { width: max(48vw, 38vh); height: max(48vw, 38vh); background: var(--lilac); opacity: .55; top: 30%; right: -8%; animation: d2 35s ease-in-out infinite alternate; }
.m3 { width: max(42vw, 34vh); height: max(42vw, 34vh); background: var(--mint); opacity: .48; bottom: 2%; left: 18%; animation: d3 41s ease-in-out infinite alternate; }
@keyframes d1 { to { transform: translate3d(10vw, 7vh, 0) scale(1.15); } }
@keyframes d2 { to { transform: translate3d(-9vw, -8vh, 0) scale(1.1); } }
@keyframes d3 { to { transform: translate3d(7vw, -10vh, 0) scale(1.18); } }

.surface { position: relative; z-index: 2; max-width: 900px; margin: 0 auto; padding: 60px 24px; }
/* [WIDENED — Fase 5 desktop pass] 1100 → 1320. Admin/super-admin are desktop-
   first surfaces (Fase 15 validates them at desktop/tablet, not phone) — the
   extra width is what makes the two-column card/chart grids below possible
   without cramming. Collapses with everything else under 1024px. */
.surface--wide { max-width: 1320px; }

h1, h2 { font-family: var(--font-display); margin: 0; letter-spacing: -.045em; }
h1 { font-size: var(--fs-h1); line-height: .94; font-weight: 300; }
h1 b { font-weight: 700; }
h2 { font-size: var(--fs-h2); font-weight: 300; }
h2 b { font-weight: 600; }
b { font-weight: 600; }
i { font-style: normal; }
.dim { color: var(--ink-2); }

.tag { font-family: var(--font-small); font-size: var(--fs-label); letter-spacing: .22em; text-transform: uppercase; color: var(--ink-2); margin: 0 0 18px; font-weight: 500; }
.tag.center { text-align: center; }

/* ── Materials ── */
.card { background: var(--ceramic); border-radius: var(--r); box-shadow: var(--shadow); }
.card--soft { background: var(--bone); border-radius: var(--r-soft); box-shadow: 0 10px 26px rgba(38, 30, 55, .06); }

/* ── Landing ── */
.topbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 60px; }
.brand { font-family: var(--font-display); font-size: 20px; font-weight: 600; letter-spacing: -.03em; }
.brand em { font-style: normal; color: var(--rose); }
.langpill { display: flex; gap: 2px; padding: 5px; border-radius: var(--r-pill); background: var(--ceramic); box-shadow: var(--shadow); font-size: var(--fs-ui-sm); font-weight: 600; }
.langpill span { padding: 7px 15px; border-radius: var(--r-pill); color: var(--ink-2); }
/* [REVISED 2026-08-18] Gradient, not lime — see the `.mode-pills button.on`
   note below for the full reasoning. This brings the language pill in line
   with the dock's own active-tab treatment, which was already gradient. */
.langpill span.on { background: var(--grad-35); color: var(--ink); --focus-ring: linear-gradient(var(--ink), var(--ink)); }

.lede { font-family: var(--font-display); font-size: var(--fs-lede); color: var(--ink-2); margin: 26px 0 36px; max-width: 34ch; font-weight: 300; }
.cta { display: flex; gap: 12px; flex-wrap: wrap; }
.btn {
  padding: 19px 34px; border-radius: var(--r-pill); text-decoration: none; font-size: var(--fs-btn); font-weight: 600;
  display: inline-flex; align-items: center; justify-content: center; gap: 12px; letter-spacing: -.01em;
  border: 0; font-family: inherit; cursor: pointer; position: relative;
  transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base);
}
/* ── Primary · the committing action [ADOPTED Fase 4 review] ──────────────
   The gradient, not lime. See §Color in chosen-dna.md for the split this
   creates: the GRADIENT now carries actions you can take, LIME carries state
   that already stands (Activa, Pagada, the dock item you are on).

   The focus ring is overridden to solid ink here, and that override is
   load-bearing: `--focus-ring` is itself the peach→lilac→mint gradient, so a
   gradient ring around a gradient button is invisible. Painting it ink keeps
   keyboard focus legible on the one control that commits. */
.btn-a {
  background: var(--grad-35); color: var(--ink);
  box-shadow: 0 14px 34px rgba(38, 30, 55, .18);
  --focus-ring: linear-gradient(var(--ink), var(--ink));
}
.btn-a:hover { transform: translateY(-3px) scale(1.02); box-shadow: 0 22px 50px rgba(38, 30, 55, .26); }
.btn-a.is-loading::after { border-color: rgba(23, 20, 32, .2); border-top-color: var(--ink); }
.btn-b { background: var(--ceramic); color: var(--ink); box-shadow: var(--shadow); }
.btn-b:hover { transform: translateY(-3px); box-shadow: var(--shadow-lift); }

.orbit { display: flex; gap: 10px; margin-top: 48px; flex-wrap: wrap; }
.orb { padding: 20px 26px; border-radius: var(--r-soft); display: flex; flex-direction: column; gap: 2px; min-width: 132px; }
.orb b { font-family: var(--font-display); font-size: 34px; font-weight: 300; letter-spacing: -.05em; line-height: 1.05; }
.orb b em { font-style: normal; font-size: .45em; color: var(--ink-2); }
.orb span { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }
.orb--lime { background: var(--lime); color: var(--ink); box-shadow: 0 16px 40px rgba(150, 190, 40, .28); }
.orb--lime b { font-weight: 500; }
.orb--lime span { color: rgba(18, 23, 5, .58); }

/* ── Section head ── */
.sec-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 22px; gap: 16px; flex-wrap: wrap; }
.sec-head--admin .res-head { margin-bottom: 0; }
.btn--create { font-size: calc(var(--fs-btn) + 2px); font-weight: 700; }
.config-actions { display: flex; justify-content: flex-end; margin-top: 26px; }
.config-surface .field-row { grid-template-columns: repeat(auto-fit, minmax(180px, 240px)); justify-content: start; }
.config-surface .field:not(.field--wide) { max-width: 240px; }
.config-surface .cfg-limits .field { max-width: 280px; }
.config-surface .hsec--soft .hsec-day { font-size: calc(var(--fs-h3) + 2px); font-weight: 400; }
.config-surface .cgroup-name, .config-surface .cgroup-n { font-size: calc(var(--fs-label-sm) + 2px); font-weight: 600; }
.config-surface .field > label { font-size: calc(var(--fs-label-sm) + 4px); font-weight: 500; }
.config-surface .hsec .sec-note, .config-surface .field .help { max-width: 60ch; font-size: var(--fs-ui); font-weight: 400; line-height: 1.45; }
.config-surface .field .cfg-limit-help { font-size: calc(var(--fs-ui) + 2px); }
.config-surface .field .cfg-policy-help { font-size: calc(var(--fs-ui) + 2px); }
.config-surface .field--refund-until { grid-column: span 2; max-width: 400px !important; }
.config-surface .btn-remove { padding: 7px 13px; font-size: calc(var(--fs-chip-sub) + 2px); font-weight: 700; color: var(--ink-2); }
.config-surface .btn-remove:hover { background: rgba(255, 122, 148, .14); color: var(--rose-ink); box-shadow: none; transform: none; }
.chip { font-family: var(--font-small); padding: 10px 18px; border-radius: var(--r-pill); font-size: var(--fs-label); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2); background: var(--ceramic); box-shadow: var(--shadow); font-weight: 500; }
/* Pills stay on one line — the row extends to fit them rather than wrapping
   to a second line. overflow-x is a silent fallback, not a visible scrollbar. */
.days { display: flex; flex-wrap: nowrap; gap: 4px; padding: 5px; border-radius: var(--r-pill); background: var(--ceramic); box-shadow: var(--shadow); overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; }
.days::-webkit-scrollbar { display: none; }
.day { flex-shrink: 0; }
.day { border: 0; background: transparent; color: var(--ink-2); padding: 9px 18px; border-radius: var(--r-pill); font-size: var(--fs-ui); font-weight: 600; cursor: pointer; font-family: inherit; transition: color var(--t-fast), background var(--t-fast); }
.day:hover { color: var(--ink); }
.day.is-active { background: var(--ink); color: var(--bone); }
.day.is-out { color: var(--ink-2); }
.day.is-out::after { content: ""; width: 5px; height: 5px; border-radius: 50%; background: rgba(23, 20, 32, .18); display: inline-block; margin-left: 7px; vertical-align: middle; }
.day.is-out.is-active { background: rgba(23, 20, 32, .07); color: var(--ink); }

/* The "PRÓXIMAS 36 H" lime badge and its pulse dot were removed in Fase 4 —
   the rule reads as a sentence now (.window-note), not a badge. */

/* ── Court grid ── */
.grid-card { padding: 26px 22px; }
.grid-card + .grid-card { margin-top: 14px; }
/* No scroll: past hours are dropped from the data before render, so the
   remaining columns are meant to fit. auto stays only as a safety net for
   a full two-segment future day on very narrow phones. */
.grid-scroll { overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; }
.grid-scroll::-webkit-scrollbar { display: none; }
/* No grid-template-columns here on purpose. This rule used to carry
   `repeat(var(--am, 7), …) 18px repeat(var(--pm, 7), …)`, which is the exact
   declaration that shipped the collapsed-grid bug: `--am`/`--pm` were never
   actually set by anything, so every render fell through to the literal
   `repeat(7, …)` fallback and only looked right because Grid.render overrides
   the property inline. A stale fallback that silently produces a *plausible
   but wrong* 7+7 layout is worse than none — if the inline style is ever
   missing the grid should collapse visibly, not lie. The template is computed
   per render in Grid.render (see the comment there). */
.ghead, .grow {
  display: grid;
  min-width: max-content;
}
.ghead { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .14em; color: var(--ink-2); padding-bottom: 14px; font-weight: 500; }
.ghead span { text-align: center; }
.ghead .gh-c { text-align: left; position: sticky; left: 0; background: var(--ceramic); z-index: 3; }
.ghead .now { color: var(--ink); font-weight: 700; }
.ghead .mid { color: transparent; }

.grow { align-items: center; padding: 3px 0; }
.gc { display: flex; flex-direction: column; position: sticky; left: 0; background: var(--ceramic); z-index: 3; padding-right: 8px; }
.gc b { font-size: var(--fs-value); font-weight: 500; letter-spacing: -.02em; }
.gc i { font-size: var(--fs-label-sm); color: var(--ink-2); letter-spacing: .12em; text-transform: uppercase; }

/* ── Slot lozenge → CIRCLE [AMENDED 2026-08-13] ───────────────────────────
   A slot is a fixed 52px circle, never stretched into an oval by its column.
   Availability is carried by fill vs outline; price tier by neutral vs
   gradient. Filled = taken, outline = free. */
.cell {
  position: relative; overflow: hidden;
  margin: 5px 0; width: 52px; height: 52px; border-radius: 50%;
  justify-self: center; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-ui); font-weight: 600; cursor: pointer;
  border: 0; font-family: inherit; padding: 0; color: var(--ink);
  background: transparent;
  transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base), background var(--t-fast);
}

/* Libre · tarifa valle — green outline, the number in ink. [REVISED 08-14] */
.cell.free { box-shadow: inset 0 0 0 2px rgba(150, 190, 40, .65); }
.cell.free:hover { transform: translateY(-4px) scale(1.06); box-shadow: inset 0 0 0 2px rgba(150, 190, 40, .95), 0 12px 26px rgba(38, 30, 55, .16); }
.cell.free:active { transform: translateY(-1px) scale(1.02); }

/* ── Ambient motion [ADDED 2026-08-17] — a bookable grid that never moves on
   its own reads as dead on a phone with no cursor to hover. A very small
   breathing scale says "this is live and waiting", not "loading". Paused on
   hover/active so it never fights the tap feedback below it, and skipped for
   the selected/held cell so it doesn't compete with the spring snap. */
@keyframes cellBreathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.015); } }
.cell.free:not(.is-sel), .cell.peak:not(.is-sel) { animation: cellBreathe 3.6s ease-in-out infinite; }
.cell.free:hover, .cell.free:active, .cell.peak:hover, .cell.peak:active,
.cell.free:focus-visible, .cell.peak:focus-visible { animation-play-state: paused; }

/* Tap ripple — a small radial burst from the touch point on any bookable
   cell. Spawned in JS (needs the tap coordinates), pure CSS to animate. */
.tap-ripple {
  position: absolute; top: 0; left: 0; width: 14px; height: 14px; margin: -7px 0 0 -7px;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(23, 20, 32, .22), transparent 70%);
  animation: tapRipple .5s ease-out forwards;
}
@keyframes tapRipple { to { transform: scale(8); opacity: 0; } }

/* A tap on a cell that cannot be booked (full, past) still gets an answer
   instead of doing nothing — a short shake says "not this one" without a
   toast or an alert. */
@keyframes cellShake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-3px); }
  40%, 60% { transform: translateX(3px); }
}
.cell.is-shake { animation: cellShake .4s; }

/* Libre - hora pico: a true gradient ring with a genuinely transparent
   centre. The old padding-box/border-box sandwich needed a solid inner
   colour, which painted a white disc that read as a fill on any non-white
   ground. Masking the ring instead means no fill exists at all. */
.cell.peak { border: 0; background: transparent; }
.cell.peak::before {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: var(--grad-35); padding: 2.5px;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  pointer-events: none;
  /* A slow hue drift — the ring is "the thing worth a second look", so it
     gets the one piece of truly ambient (not tap-triggered) motion. */
  animation: ringGlow 5s ease-in-out infinite;
}
@keyframes ringGlow { 0%, 100% { filter: hue-rotate(0deg); } 50% { filter: hue-rotate(16deg); } }
.cell.peak:hover { transform: translateY(-4px) scale(1.06); box-shadow: 0 12px 26px rgba(38, 30, 55, .18); }
.cell.peak:active { transform: translateY(-1px) scale(1.02); }

/* Ocupada · tarifa valle — solid lime fill. Fill always means taken. */
.cell.taken { background: var(--lime); color: var(--ink); cursor: not-allowed; }
/* Ocupada · hora pico — the same gradient, at 35°, filled. [REVISED 08-14] */
.cell.taken-peak { background: var(--grad-35); color: var(--ink); cursor: not-allowed; }


/* ── full · PLAYER GRID ONLY [Fase 4 review] ───────────────────────────────
   No courts left at this hour, whatever the tariff. Grey and inert: a dead end
   should look like one, and a lime or gradient fill made the hours the player
   CANNOT have the loudest objects on screen. This does not touch the admin
   matrix, where a fill still means "this specific court is booked". */
.cell.full {
  background: rgba(23, 20, 32, .06); color: var(--ink-2);
  cursor: not-allowed; font-weight: 500;
}

/* Pasada — never rendered live (the grid drops past hours), kept for the
   styleguide and for any future reuse. Light grey ring, no number. */
.cell.past { box-shadow: inset 0 0 0 1.5px rgba(23, 20, 32, .09); color: transparent; cursor: not-allowed; }
/* Fuera de la ventana — light grey dashed ring, number still visible so it
   never reads as unbookable-forever, just not yet. */
.cell.outside { border: 1.5px dashed rgba(23, 20, 32, .13); color: var(--ink-2); cursor: not-allowed; font-weight: 500; }

.cell.mid { background: transparent; cursor: default; width: auto; box-shadow: none; border: 0; }
/* Held = the player already has a reservation, so nothing here is bookable.
   [REVISED Fase 4 review] It no longer greys the grid out. Dimming to .45 and
   stripping the ring destroyed the very information the screen exists to show:
   a player with a booking still needs to read today's real availability. The
   cells keep their true state and only lose interactivity. */
.cell.is-held { cursor: not-allowed; }

/* The slot the player has picked but not yet confirmed.
   [REVISED Fase 4 review] The double halo (a ceramic gap ring plus an ink
   ring) was doing the job of a focus indicator on something that is a
   selection, and it read as heavy chrome bolted onto a circle. Selection is
   now carried by size alone: the picked slot simply stays scaled up. Held,
   not animated-and-released, so it is unambiguous which slot is chosen. */
/* [REVISED 2026-08-17, twice] First pass made every selection the gradient —
   read wrong for a plain valley hour, which had never worn the gradient
   anywhere else in the grid. Selection now keeps the tier's own fill: lime
   for valley, gradient for peak. Number weight jumps +200 (600 → 800), same
   logic as the hour itself carrying weight in the active card. */
.cell.is-sel {
  color: var(--ink); border: 0;
  box-shadow: 0 14px 30px rgba(23, 20, 32, .28);
  transform: scale(1.18);
  z-index: 2;
  font-weight: 800;
  --focus-ring: linear-gradient(var(--ink), var(--ink));
}
.cell.free.is-sel { background: var(--lime); }
.cell.peak.is-sel { background: var(--grad-35); }
/* The peak ring is redundant once the cell itself fills with the same
   gradient — without this it double-paints at the edge. */
.cell.peak.is-sel::before { display: none; }
.cell.is-sel:hover { transform: scale(1.18); }

.subhead { font-family: var(--font-display); font-size: 18px; font-weight: 300; margin: 34px 0 12px; letter-spacing: -.03em; }
/* +4px on both the label and its swatch [REVISED 2026-08-17] — at the base
   --fs-label-sm size the grid's own convention key was harder to read than
   the grid it explains. */
/* Sentence case, not tracked uppercase [REVISED 2026-08-17] — COPY.slot is
   already written "Libre", "Ocupado · hora pico"; forcing full caps fought
   the copy instead of using it. */
.legend { font-family: var(--font-small); display: flex; flex-wrap: wrap; gap: 20px; margin-top: 22px; font-size: calc(var(--fs-label-sm) + 4px); letter-spacing: .02em; color: var(--ink-2); align-items: center; font-weight: 500; }
/* Swatches are circles now, and each one is the real treatment of its state. */
.legend i { width: 19px; height: 19px; border-radius: 50%; display: inline-block; margin-right: 9px; vertical-align: -4px; }
.lg.free { box-shadow: inset 0 0 0 2px rgba(150, 190, 40, .65); }
.lg.peak { border: 2px solid transparent; background: linear-gradient(var(--ceramic), var(--ceramic)) padding-box, var(--grad) border-box; }
.lg.taken { background: var(--lime); }
.lg.taken-peak { background: var(--grad-35); }
.lg.full { background: rgba(23, 20, 32, .06); }
.lg.past { box-shadow: inset 0 0 0 1.5px rgba(23, 20, 32, .09); }
.lg.outside { border: 1.5px dashed rgba(23, 20, 32, .13); }

/* ── Day board · admin Reservas [Fase 5] ──────────────────────────────────
   Transposed against the player's court × hour circle grid: courts across
   the top, hours down the side, and an occupied cell prints WHO holds it.
   A name can't live inside a 52px circle, so the slot mark here is a
   lozenge — an interactive control, so --r-field per §4, not the card's
   --r-soft. The state vocabulary is unchanged: same six words slotState
   speaks, same fills and outlines the circles use, different shape. */
/* No scroll wrapper on purpose. The court grid of §6 may scroll because its
   circles are a fixed 52px and enough hours will always outrun a phone; this
   board must be readable whole, so the columns divide the card instead of the
   card chasing the columns. Nothing here is allowed a `max-content` floor. */
.brow { display: grid; gap: 8px; align-items: stretch; padding: 3px 0; }
/* Court headers run 6px above the dense-label tier — they name the columns an
   operator scans first, and 9px was reading as a caption of the board rather
   than its own axis. Tracking loosens off proportionally: .14em is calibrated
   for 9px uppercase and turns into gaps at 15px. */
.bhead { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 6px); letter-spacing: .04em; color: var(--ink-2); font-weight: 500; padding-bottom: 12px; }
.bhead span { text-align: center; align-self: end; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bhead .bh-c { text-align: left; }
.bh { display: flex; align-items: center; padding-right: 8px; min-width: 0; }
.bh b { font-size: var(--fs-value); font-weight: 600; letter-spacing: -.02em; white-space: nowrap; }
.brow.is-now .bh b { font-weight: 700; }

/* A fixed box — same height in every row, same width in every column, whatever
   the name inside is. Text never sizes the cell; the cell sizes the text. */
.bcell {
  position: relative;
  height: 46px; min-width: 0; border-radius: var(--r-field);
  display: flex; align-items: center; justify-content: center;
  padding: 0 12px; text-align: center;
  font-family: inherit; font-size: var(--fs-board); font-weight: 600; color: var(--ink);
  border: 0; background: transparent; cursor: default;
  transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base);
}
.bc-name, .bc-first { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Full name at admin width, first name once the columns get tight (≤760px).
   Both are rendered; only one is ever displayed. */
.bc-first { display: none; }

/* Libre · tarifa valle — the same green outline .cell.free carries. */
.bcell.free { box-shadow: inset 0 0 0 2px rgba(150, 190, 40, .65); color: var(--ink-2); font-weight: 500; }
/* Libre · hora pico — gradient border, painted the padding-box/border-box
   way because a lozenge can't fake a ring with a ::before circle. */
.bcell.peak {
  color: var(--ink-2); font-weight: 500; border: 2px solid transparent;
  background: linear-gradient(var(--ceramic), var(--ceramic)) padding-box, var(--grad) border-box;
}
.bcell.taken { background: var(--lime); }
.bcell.taken-peak { background: var(--grad-35); }
.bcell.outside { border: 1.5px dashed rgba(23, 20, 32, .13); color: var(--ink-2); font-weight: 500; }
.bcell.past { box-shadow: inset 0 0 0 1.5px rgba(23, 20, 32, .09); color: var(--ink-2); font-weight: 500; }

/* Occupied cells jump to their row in the table below — the only clickable
   mark on the board, so the only one that lifts (§5). Free cells are inert
   here: creating a reservation is the "Crear reserva" button's job. */
.bcell.is-link { cursor: pointer; }
.bcell.is-link:hover { transform: translateY(-3px); box-shadow: 0 12px 26px rgba(38, 30, 55, .18); }
.bcell.is-link:active { transform: translateY(-1px); }
.bcell:focus-visible { outline: none; }
.bcell:focus-visible::after {
  content: ''; position: absolute; inset: -5px;
  border-radius: calc(var(--r-field) + 5px); border: 2px solid var(--ink); pointer-events: none;
}

/* Legend swatches follow the marks: lozenges here, circles under the grid. */
.legend--board i.lgb { width: 26px; height: 16px; border-radius: 6px; display: inline-block; margin-right: 9px; vertical-align: -3px; }
.lgb.free { box-shadow: inset 0 0 0 2px rgba(150, 190, 40, .65); }
.lgb.peak { border: 2px solid transparent; background: linear-gradient(var(--ceramic), var(--ceramic)) padding-box, var(--grad) border-box; }
.lgb.taken { background: var(--lime); }
.lgb.taken-peak { background: var(--grad-35); }
.lgb.outside { border: 1.5px dashed rgba(23, 20, 32, .13); }

/* Board sizing tokens. The two things that must give when the surface narrows
   are the hour column and the type inside a cell — the column count can't
   change (it IS the court list) and horizontal scroll is off the table.
   `--fs-board` sits 2px under the UI tier: a name is read, not scanned, so it
   wants less weight on the page than a button label of the same box. */
:root { --bh-w: 104px; --fs-board: calc(var(--fs-ui) - 2px); }
@media (max-width: 1024px) {
  :root { --bh-w: 84px; --fs-board: 11px; }
  .brow { gap: 6px; }
  .bcell { padding: 0 8px; }
}
/* Where a column stops fitting "Mariana Gómez" — measured, not guessed: at
   760px the four-court board gives each cell ~140px, and the full name wants
   more than that at this size. Below here the first name is the whole label. */
@media (max-width: 760px) {
  .bc-name { display: none; }
  .bc-first { display: block; }
}
@media (max-width: 560px) {
  :root { --bh-w: 62px; --fs-board: 10px; }
  .brow { gap: 4px; }
  .bcell { height: 42px; padding: 0 5px; }
  .bhead { letter-spacing: .06em; }
}


/* ── Admin ── */
/* [REVISED — Fase 5 Resumen pass] Resumen's "at a glance" zone was four
   full-width blocks (occupancy, two stats, a meter), then two matched
   cards, then occupancy-with-income-in-the-corner — each pass folding more
   of it into one `.hero-stat` card. Revenue no longer gets any special
   treatment at all: it's the last fact in `.hs-insights` below, same size
   as everything else. One hierarchy — the big figure is occupancy, every
   other number on this card is a same-weight supporting fact. */

/* Same-day distribution facts inside the occupancy card — reservas today,
   court count, operating hours, and up to three conditional hour-facts
   (emptiest / fullest / busiest). Whitespace separates this row from the
   bar above it, not a rule — §4 rules out decorative borders. */
.hs-insights { display: flex; flex-wrap: wrap; gap: 22px 28px; margin-top: 4px; }
.hs-insight { display: flex; flex-direction: column; gap: 3px; }
.hs-i-label { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 4px); letter-spacing: .01em; color: var(--ink-2); font-weight: 500; }
.hs-i-val { font-size: var(--fs-value); font-weight: 500; color: var(--ink); }

/* Upcoming reservations, sectioned by start hour — not folded, every group
   stays visible; this only breaks a long flat wall of cards into "who's in
   at 17:00, who's in at 18:00…". */
.upcoming-groups { margin-bottom: 26px; }
.hour-group + .hour-group { margin-top: 22px; }
.hour-group-label { font-family: var(--font-display); font-size: calc(var(--fs-ui) + 4px); font-weight: 700; color: var(--ink-2); letter-spacing: -.01em; margin: 0 0 10px; }
/* Upcoming reservations — fixed 380px cards (three-up at the 1320px admin
   container), never stretched to fill a short row. `auto-fill` + a flat
   380px track (not `auto-fit, minmax(380px, 1fr)`) is the difference: a
   lone card in the last row, or a group with only one reservation, stays
   the same standard size instead of growing to eat the leftover width. */
.upcoming-grid { display: grid; grid-template-columns: repeat(auto-fill, 380px); gap: 10px; margin-bottom: 0; }
.upcoming-grid .active-card { margin-bottom: 0; }
/* Status/payment chips carry less hierarchy here than everywhere else they
   appear — the card's story is who's playing and when, not the chip. Same
   fill/text recipe (still instantly recognisable as the system's status
   chip), just scaled down, the same move `.rtr .row-act .btn-s` already
   makes for a denser context. */
.upcoming-grid .st { padding: 5px 11px; font-size: var(--fs-chip-sub); }
/* Pills sit on the right, stacked vertically, and never force the name/time
   block to wrap onto a second line to make room — the card stays one row,
   the chip column just narrows to its own content. */
.upcoming-grid .active-card { align-items: flex-start; flex-wrap: nowrap; }
.upcoming-grid .ac-main { min-width: 0; }
.upcoming-grid .ac-side { flex-direction: column; align-items: flex-end; flex-shrink: 0; flex-wrap: nowrap; gap: 6px; }
.upcoming-grid .active-card .ac-when { font-size: calc(var(--fs-h3) + 1px); font-weight: 400; }
.upcoming-grid .active-card .ac-label { font-size: calc(var(--fs-label-sm) + 4px); }

.hero-stat { padding: 34px; display: flex; flex-direction: column; gap: 18px; margin-bottom: 14px; transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base); }
.hero-stat:hover { transform: translateY(-4px); box-shadow: var(--shadow-lift); }
/* [REVISED — Fase 5 desktop pass] Sentence case, +100 weight (500 → 600) —
   requested; matches the lighter tracking every other sentence-case label in
   the system already uses (`.ac-label`, `.cs-flabel`) instead of the heavy
   uppercase tracking, which reads oddly once the caps are gone. */
.hero-stat .h-label { font-family: var(--font-small); font-size: calc(var(--fs-label) + 4px); letter-spacing: .01em; text-transform: none; color: var(--ink-2); font-weight: 600; }
.hero-stat .h-num { font-family: var(--font-display); font-size: var(--fs-hero); font-weight: 350; line-height: .82; letter-spacing: -.07em; }
.hero-stat .h-num small { font-size: .28em; color: var(--ink-2); font-weight: 400; }
/* ── Meter ─────────────────────────────────────────────────────────────────
   The one place the decorative gradient is allowed to resolve into lime,
   because it is a filled measure and the lime end reads as "this much is
   committed". Reusable for any 0-100 figure: occupancy, collection progress. */
.meter, .h-bar { display: block; height: 14px; background: rgba(23, 20, 32, .07); border-radius: var(--r-pill); overflow: hidden; }
/* Centered 50% reference tick — `.h-bar` only (the occupancy figure), not
   `.meter` generally: a "half full" mark is meaningful for occupancy, not
   for every 0–100 measure `.meter` gets reused for. Bleeds past the bar's
   own edge, so it needs a wrapper: `.h-bar` itself keeps `overflow: hidden`
   (load-bearing — the fill `<i>` carries its own `--r-pill` radius on all
   four corners, and without the parent clipping it, a partial fill would
   show rounded corners at its flush-left edge too). The tick lives on the
   wrapper instead, outside that clip. */
/* `display: block` is load-bearing on both — `.h-bar` was a direct flex
   child of `.hero-stat` before, which blockifies a bare <span> for free;
   nested one level inside this wrapper, it reverts to inline and its own
   `height: 14px` gets silently ignored, collapsing the bar to nothing. */
.h-bar-wrap { position: relative; display: block; }
.h-bar-wrap::after { content: ""; position: absolute; top: -3px; left: 50%; width: 1px; height: calc(100% + 6px); background: rgba(23, 20, 32, .22); transform: translateX(-.5px); z-index: 2; pointer-events: none; }
.meter i, .h-bar i { display: block; height: 100%; border-radius: var(--r-pill); background: linear-gradient(90deg, var(--peach), var(--lilac) 45%, var(--mint) 72%, var(--lime)); transition: width .5s var(--ease-out); }
.meter--sm { height: 8px; }
.meter-row { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 8px; }
.meter-row b { font-size: var(--fs-value); font-weight: 500; }
.meter-row span { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }

.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 26px; }
.stats--2 { grid-template-columns: repeat(2, 1fr); }
.stat { padding: 26px; display: flex; flex-direction: column; gap: 8px; transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base); }
.stat:hover { transform: translateY(-4px); box-shadow: var(--shadow-lift); }
.stat b { font-family: var(--font-display); font-size: var(--fs-stat); font-weight: 200; letter-spacing: -.055em; line-height: 1; }
/* A unit suffix on a stat figure ("21%") — same stepped-back treatment
   `.h-num small` already gives the hero stat's "%", so any figure that
   needs a bare unit (not a "x / y" denominator, that's `.of`) reads the
   same way wherever it appears. */
.stat b small { font-size: .28em; color: var(--ink-2); font-weight: 300; }
.stat span { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }

.table { padding: 10px 0; overflow: hidden; }
.th, .tr { display: grid; grid-template-columns: 1fr 1.3fr .9fr 1.3fr .9fr; padding: 16px 26px; align-items: center; font-size: var(--fs-body-sm); gap: 10px; }
.th { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }
.tr { transition: background var(--t-fast); }
.tr:hover { background: var(--bone); }
.st { font-family: var(--font-small); font-size: var(--fs-chip); letter-spacing: .1em; text-transform: uppercase; padding: 6px 13px; border-radius: var(--r-pill); justify-self: start; font-weight: 600; white-space: nowrap; display: inline-flex; align-items: baseline; gap: 7px; }
/* Solid fill + text, always - the same recipe as the toasts. A pale wash
   carrying a darker tint of its own hue as text is the generic pattern this
   system deliberately avoids. Cancelada and Pago fallido share rose because
   they are the same idea in two different columns; they never collide.
   Text is ink on the pale fills (lime, lilac) and ceramic (white) on rose,
   the one fill dark enough to read better with light text than dark
   [REQUESTED 2026-08-18]. Contrast note, not a blocker: ceramic-on-rose
   measures ~2.7:1, under WCAG AA's 4.5:1 for text this size — ink-on-rose
   was ~6.6:1. Flagging it here since Fase 15 (the accessibility pass) will
   need to either accept the tradeoff or revisit it. */
.st.ok { background: var(--lime); color: var(--ink); }
.st.canc, .st.err { background: var(--rose); color: var(--ceramic); }
.st.part { background: var(--lilac); color: var(--ink); }
.st.mute { background: var(--bone); color: var(--ink-2); box-shadow: inset 0 0 0 1.5px rgba(23, 20, 32, .07); }
/* Inline, never stacked - a chip is one line. */
.st sub { display: inline; font-size: var(--fs-chip-sub); letter-spacing: .06em; opacity: .68; font-weight: 500; vertical-align: baseline; }
/* Partial-payment chip: the already-charged figure is -100 off the chip's
   base 600, so the eye lands on the total first and the paid-so-far reads
   as the supporting figure. */
.st .amt-paid { font-weight: 500; }

/* ── Confirmation ticket ── */
.confirm { text-align: center; }
/* The ticket is its own object: it carries the card surface itself rather than
   sitting inside a second framing container. */
.ticket { max-width: 440px; margin: 0 auto; text-align: left; overflow: hidden; border-radius: var(--r); box-shadow: var(--shadow); }
/* Header hierarchy: label → the hour you play → the day → the court. The
   hour is the reason anyone re-opens a ticket, so it gets the figure tier;
   day and court get readable rows instead of one cramped uppercase line. */
/* One continuous gradient runs the full height of the ticket (set inline, at a
   per-reservation angle) — the same treatment as the page ground. It used to
   be three stacked bands (gradient header, white body, bone foot), which read
   as three cards taped together instead of one object. Every section below is
   therefore transparent. */
.ticket-top { padding: 30px 30px 26px; position: relative; overflow: visible; }
/* Commit payoff [ADDED 2026-08-17] — a handful of gradient-toned dots burst
   from the ticket header on open, spawned + animated in JS (M.commitBurst)
   since each needs its own random direction. Pure CSS couldn't vary that. */
.confetti-bit { position: absolute; top: 24px; left: 30px; width: 7px; height: 7px; border-radius: 50%; pointer-events: none; z-index: 2; }
/* [REVISED 2026-08-18 — consistency pass vs. the active card and the
   selection step]
   - Sentence case, not tracked uppercase: the same call already made on
     .ac-label — "Reserva confirmada" is a status sentence, not a form-field
     label, so it follows that precedent rather than .sp-flabel's.
   - rgba(23,20,32,.58/.74) replaced with var(--ink-2): every other secondary
     tone in the system reads through that token; these two were the only
     hand-typed ink tints left, invisible until you diff them side by side.
   - .t-when now carries weight (300 → 500): both other cards land on "the
     hour is the one figure that gets weight" — the ticket's time was the
     only one of the three still flat.
   - .t-court lightens (600 → 300) to match .ac-court: a court name is
     context everywhere else in the app; only here it was bold enough to
     compete with the hour above it. */
.ticket-top .t-label { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 2px); letter-spacing: .01em; color: var(--ink-2); font-weight: 500; display: block; margin-bottom: 12px; }
.ticket-top .t-when { font-family: var(--font-display); font-size: var(--fs-stat); font-weight: 500; letter-spacing: -.05em; line-height: 1.02; }
.ticket-top .t-day { font-family: var(--font-display); font-size: var(--fs-lede); font-weight: 300; color: var(--ink-2); margin-top: 4px; }
.ticket-top .t-court { font-family: var(--font-small); font-size: var(--fs-value); font-weight: 300; margin-top: 10px; color: var(--ink-2); }
/* Same line order as the selection panel, and the same label-free treatment:
   the draft the player confirmed and the ticket they get read identically. */
.ticket-lines { padding: 22px 30px 18px; display: flex; flex-direction: column; gap: 3px; }
/* The ticket used to render this figure at 28px while the panel it mirrors
   used 30px — the same number in two sizes across one flow. Unified. */

/* The QR is the reason the ticket exists, so it gets the room — centred and
   large enough to scan off a phone held at arm's length across a counter.
   The white box is its quiet zone: a QR needs clear margin or scanners fail. */
.ticket-qr { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; padding: 24px 30px 28px; }
/* No frame. The QR sits straight on the ticket ground — the white rounded card
   it used to live in read as a sticker pasted onto the ticket, and its square
   corners fought the box's radius. The SVG's own white plate is gone too
   (qr.js); the pale ground supplies both the contrast and the quiet zone. */
.qr-box { flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.qr-box .qr { display: block; }
.qr-code { font-size: var(--fs-h3); font-weight: 600; letter-spacing: .06em; font-family: var(--font-mono); }
.ticket-qr p { font-size: var(--fs-body-xs); color: var(--ink-2); margin: 0; font-weight: 300; max-width: 30ch; }

/* ── Floating dock ── */
.dock {
  position: fixed; z-index: 6; left: 50%; transform: translateX(-50%); bottom: 26px;
  /* [ADDED — Fase 4b mobile pass] Rides above the home indicator on notch
     devices; identical to 26px wherever the inset is 0. */
  bottom: max(26px, calc(14px + env(safe-area-inset-bottom, 0px)));
  display: flex; gap: 9px; padding: 12px; border-radius: var(--r-pill);
  background: var(--ceramic); box-shadow: 0 20px 50px rgba(38, 30, 55, .20);
}
/* +4px across the board — the dock reads as the app's one fixed, always-on
   control and was sized too close to a footnote for that job. */
.dock a { text-decoration: none; color: var(--ink-2); font-size: calc(var(--fs-ui) + 4px); font-weight: 600; padding: 17px 26px; border-radius: var(--r-pill); transition: color var(--t-fast), background var(--t-fast), font-weight var(--t-fast); }
.dock a:hover { color: var(--ink); background: var(--bone); }
/* The gradient does verbs elsewhere, but the current tab is the one place a
   fixed, ever-present element also reads as a claim on attention — gradient
   here, not lime. [REVISED — see chat 2026-08-17] Ring is set to ink locally:
   --focus-ring is itself the gradient, so a gradient ring on a gradient fill
   is invisible otherwise. */
.dock a.is-active { background: var(--grad-35); color: var(--ink); font-weight: 700; --focus-ring: linear-gradient(var(--ink), var(--ink)); }
#admin-format-switch { position: fixed; z-index: 40; top: 16px; right: 74px; }
.admin-format { display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--r-pill); background: var(--bone); }
.admin-format button { border: 0; border-radius: var(--r-pill); background: transparent; color: var(--ink-2); font: inherit; font-size: var(--fs-meta); font-weight: 600; padding: 7px 9px; cursor: pointer; }
.admin-format button.on { background: var(--lime); color: var(--ink); }

/* [ADDED — Fase 5 desktop pass] Admin/super-admin dock, scaled to 65% (a
   35% reduction) of the player dock's size — requested once the admin
   contexts stopped sharing the player's touch-target sizing rationale.
   Every dimension scales off the same tokens the full-size dock reads, so a
   future change to those tokens keeps the two proportional automatically.
   Toggled by `app.js` per context; the player dock is untouched.

   [REVISED] The factor is now one number, `--dock-k`, instead of being
   repeated on four properties — and it went .65 → .7475, a requested 15%
   increase on everything in the bar (padding, gap, type; the pill radius is
   `--r-pill`, already shape-not-size).

   Desktop only, by cascade: the ≤760px block near the end of this file sets
   `.dock a` at the same specificity and later, so on a phone BOTH docks take
   the player's touch sizing. Left as-is deliberately — shrinking admin's
   mobile tab targets to ~47px to honour a desktop density factor would trade
   a real accessibility floor for consistency nobody asked for. */
.dock--compact { --dock-k: .7475; padding: calc(12px * var(--dock-k)); gap: calc(9px * var(--dock-k)); }
.dock--compact a { padding: calc(17px * var(--dock-k)) calc(26px * var(--dock-k)); font-size: calc((var(--fs-ui) + 4px) * var(--dock-k)); }


/* ═══════════════════════════════════════════════════════════════════════════
   § EXTENSIONS — new for Fase 3
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Focus ─────────────────────────────────────────────────────────────────
   A gradient halo, not a hard black outline. Painted as a masked ring on a
   pseudo-element so the centre stays transparent and the ring reads on any
   ground - lime fill, ink fill, ceramic or bone. Pill-family uses ::before;
   .cell uses ::after because its ::before already paints the peak-tier ring.
   Swap --focus-ring (see .focus-lime) to repaint every ring at once. */
@keyframes focusIn { from { opacity: 0; transform: scale(.82); } }

:where(a, button, input, select, textarea, [tabindex]):focus-visible { outline: none; }

:where(.btn, .btn-s, .btn-x, .day, .dock a, .mode-pills button, .fgroup button, .hist-row,
       .toggle, .drawer > summary, .sg-toc a, .cs-fact--do, .devswitch button, .active-card) {
  position: relative;
}
/* [FOUND — Fase 5 desktop pass] `.active-card` was keyboard-focusable
   (`tabindex="0"`, and now a real `<button>` in admin's clickable variant)
   but missing from both lists below — it shipped with no focus indicator at
   all, the exact failure mode this comment block warns about. Added here,
   fixing it for the player's long-press card too, not just the new admin
   one — same class, same gap. */
:where(.btn, .btn-s, .btn-x, .day, .dock a, .mode-pills button, .fgroup button, .hist-row,
       .toggle, .drawer > summary, .sg-toc a, .cs-fact--do, .active-card):focus-visible::before,
.cell:focus-visible::after,
.demo-focus::before {
  content: ""; position: absolute; inset: -6px; border-radius: inherit;
  background: var(--focus-ring); padding: 3px;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  filter: drop-shadow(0 2px 6px rgba(38, 30, 55, .28));
  animation: focusIn .22s var(--ease-out);
  pointer-events: none; z-index: 2;
}
.cell:focus-visible::after, .cell.demo-focus::before { inset: -5px; border-radius: 50%; }
/* Alternative paint, shown side by side on the styleguide. */
.focus-lime { --focus-ring: linear-gradient(35deg, #9FD400, var(--lime) 55%, #EAFFBA); }
[disabled], .is-disabled, [aria-disabled="true"] {
  cursor: not-allowed !important;
  box-shadow: none !important;
  transform: none !important;
  opacity: .55;
  color: var(--ink-2);
}
.btn[disabled], .btn.is-disabled { pointer-events: none; }

/* Button loading — label hidden, width held, spinner centred. */
.btn.is-loading { pointer-events: none; color: transparent !important; }
.btn.is-loading::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid rgba(23, 20, 32, .2); border-top-color: var(--ink);
  animation: spin .7s linear infinite;
}
.btn-b.is-loading::after { border-color: rgba(23, 20, 32, .12); border-top-color: #8CB320; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Skeleton — loading state for the grid. */
.skeleton { background: var(--bone); border-radius: var(--r-pill); position: relative; overflow: hidden; }
/* Shimmer runs on the same 35deg axis as the peak-tier fill, so loading
   reads as part of the same system rather than a borrowed effect. */
.skeleton::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(35deg, transparent 28%, rgba(217, 200, 255, .55), transparent 72%);
  animation: sweep 1.4s linear infinite;
}
@keyframes sweep { to { transform: translateX(100%); } }
.cell.skeleton { cursor: progress; }

/* Empty state — faint label, one helper line, the CTA. No illustration. */
.empty { padding: 54px 26px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 14px; }
.empty .e-label { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .18em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }
.empty p { margin: 0; color: var(--ink-2); font-weight: 300; max-width: 38ch; }

/* ── Section heading + its one-line note ───────────────────────────────────
   The pattern every screen kept re-declaring inline. A heading, optionally
   followed by a single dim line explaining what the block below contains. */
.sec-h { font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 300; letter-spacing: -.04em; margin: 0 0 4px; }
/* Local, scoped bump — not a change to `.sec-h` itself, which Reservas'
   grid heading and others still use at the base size/weight. */
.sec-h--lg { font-size: calc(var(--fs-h3) + 2px); font-weight: 400; margin-bottom: 18px; }
.sec-h-sub { font-weight: 200; }
.sec-note { font-size: var(--fs-body-xs); color: var(--ink-2); font-weight: 300; margin: 0 0 18px; }
.sec-h + .sec-note { margin-top: 0; }
/* A figure's denominator — "32 / 56". Same tier, stepped back. */
.of { font-size: .5em; color: var(--ink-2); font-weight: 300; }

/* ── Disclosure drawer — for content that must exist without competing ── */
.drawer { margin-top: 34px; }
.drawer > summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 10px;
  padding: 13px 24px; border-radius: var(--r-pill); background: var(--ceramic); box-shadow: var(--shadow);
  font-size: var(--fs-body-xs); font-weight: 600; color: var(--ink);
  transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base);
}
.drawer > summary::-webkit-details-marker { display: none; }
.drawer > summary::after { content: "▾"; font-size: 11px; color: var(--ink-2); transition: transform var(--t-base) var(--ease-out); }
.drawer[open] > summary::after { transform: rotate(180deg); }
.drawer > summary:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
/* [FIXED — Fase 5 audit] This hard ink outline fought the masked-gradient ring
   §7 requires ("never a hard black outline") — `.drawer > summary` is already
   in both focus-ring selector lists above, so the ring painted on top of this
   outline instead of replacing it. Removed rather than kept as a second
   indicator: one focus treatment, system-wide. */
.drawer > summary i { color: var(--ink-2); font-weight: 500; }
.drawer-body { margin-top: 14px; }

/* ── Decorative gradient accent (the motif) ── */
.accent { height: 4px; border-radius: var(--r-pill); background: var(--grad); margin: 0 0 26px; }
.accent--thin { height: 3px; }
.accent--full { border-radius: 0; margin: 0; }

/* ── One-active-reservation card ──
   Gradient ring instead of a flat card edge — the same --grad-35 axis the
   grid uses to mark a peak-tier circle, so "this is the thing that matters
   right now" reads as one visual grammar across the surface. */
.active-card { padding: 26px 28px; display: flex; align-items: center; gap: 24px; flex-wrap: wrap; margin-bottom: 22px; border: 3px solid transparent; background: linear-gradient(var(--ceramic), var(--ceramic)) padding-box, var(--grad-35) border-box; }
.active-card .ac-main { flex: 1 1 240px; display: flex; flex-direction: column; gap: 6px; }
/* Sentence case, not tracked uppercase — an all-caps label with heavy
   tracking on a short phrase reads as generated boilerplate. The message
   already says "activa"; the label just names what the block is. */
.active-card .ac-label { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 2px); letter-spacing: .01em; color: var(--ink-2); font-weight: 500; }
/* The line stays light; the HOUR inside it carries +200 (300 → 500). Weighting
   the whole line made the date shout as loudly as the time, which is not what
   anyone reopens the card for. Date and time also split size: the date is a
   frame, the time is the answer. */
.active-card .ac-when { font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 300; letter-spacing: -.035em; line-height: 1.15; }
.active-card .ac-when .ac-date { font-size: calc(var(--fs-h3) - 2px); }
.active-card .ac-when b, .active-card .ac-when .ac-time { font-weight: 500; font-size: calc(var(--fs-h3) + 2px); }
.active-card .ac-court { font-size: calc(var(--fs-body-xs) + 2px); color: var(--ink-2); font-weight: 300; }
/* [REVISED Fase 4] Chips + action now WRAP instead of scrolling.
   The Fase 3 rule kept this row on one line and let it scroll; at 412px that
   silently clipped the "Cancelar reserva" button off the right edge — the one
   control on the card. A container that hides its own action is worse than a
   ragged second row, so the card grows in height instead. The no-wrap rule
   still holds for the day/filter/mode pill groups, which do fit. */
.active-card .ac-side { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.active-card .ac-side > * { flex-shrink: 0; }

/* ── Long-press to cancel ─────────────────────────────────────────────────
   Replaces the standing "Cancelar reserva" button. The card is the target;
   the hint makes the gesture discoverable, and the sweep gives the press a
   visible duration so nothing fires without warning. */
.active-card { position: relative; cursor: pointer; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base); }
/* [ADDED — Fase 5 desktop pass] The card was already interactive (long-press
   to cancel on player, a click-through on admin) but never lifted on hover —
   the one card in the system that didn't answer the universal "everything
   interactive lifts" rule (§5). */
.active-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lift); }
.active-card.is-holding:hover { transform: scale(.97); }
/* Admin's Resumen card is a real <button> (click-through to Reservas,
   filtered to that day) rather than the player's long-press div — reset the
   native button chrome its markup now carries. */
button.active-card { border: 0; background: linear-gradient(var(--ceramic), var(--ceramic)) padding-box, var(--grad-35) border-box; font: inherit; width: 100%; text-align: left; }
/* [REVISED 2026-08-17] Moved outside the card — it now sits as its own line
   right below, so the gradient-ringed card stays purely informative and the
   gesture hint reads as a caption rather than card content. */
.ac-hint { display: block; margin: -14px 0 22px; font-size: calc(var(--fs-meta) + 1px); color: var(--ink-2); font-weight: 300; opacity: .75; }
/* Held: the card presses in. The progress bar it replaced was a second thing
   to read during a gesture whose whole point is that you are already holding
   the thing — the press itself is the feedback. */
.active-card.is-holding { transform: scale(.97); box-shadow: 0 8px 20px rgba(38, 30, 55, .10); }

/* Flagged when the player taps an hour they cannot book — the card is the
   answer to "why not?", so it raises its hand rather than letting the tap
   fall on the floor. */
@keyframes flagCard {
  0%   { box-shadow: var(--shadow); }
  30%  { box-shadow: var(--shadow-lift), 0 0 0 3px var(--lime); }
  100% { box-shadow: var(--shadow); }
}
.active-card.is-flag { animation: flagCard .9s var(--ease-out); }
@media (prefers-reduced-motion: reduce) {
  .active-card.is-flag { animation: none; box-shadow: var(--shadow-lift), 0 0 0 3px var(--lime); }
}

/* Keeps a proper noun (a club's name) from breaking mid-phrase — it moves to
   the next line whole instead of splitting across two. */
.nowrap { white-space: nowrap; }

.held-note { display: flex; align-items: center; gap: 12px; padding: 18px 24px; border-radius: var(--r-soft); background: var(--bone); color: var(--ink-2); font-size: var(--fs-body-sm); font-weight: 300; margin-bottom: 14px; }
.held-note b { font-weight: 600; color: var(--ink); }

/* Small secondary/ghost button used inside cards and rows. */
.btn-s { padding: 11px 20px; border-radius: var(--r-pill); font-size: var(--fs-ui); font-weight: 600; border: 0; font-family: inherit; cursor: pointer; background: var(--bone); color: var(--ink); transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base), background var(--t-fast); }
.btn-s:hover { background: var(--ceramic); box-shadow: var(--shadow); transform: translateY(-2px); }
.btn-s:active { transform: translateY(0); }
/* The small-button twin of .btn-a — same material, same meaning. */
.btn-s--lime { background: var(--grad-35); color: var(--ink); --focus-ring: linear-gradient(var(--ink), var(--ink)); }
.btn-s--lime:hover { background: var(--grad-35); box-shadow: 0 12px 26px rgba(38, 30, 55, .22); }

/* ── Reservar · heading hierarchy [REVISED Fase 4] ────────────────────────
   Everything ABOVE the gradient accent is informative and descends in this
   order: greeting → date → club. Everything actionable (day pills, grid,
   selection) lives BELOW the line. The greeting takes the top tier because
   it is the only line that addresses the player rather than the data. */
.res-head { margin-bottom: 20px; }
/* The greeting is the largest text on the screen — it is the only line that
   addresses the player rather than describing data. */
.res-hello { font-family: var(--font-display); font-size: var(--fs-hello); font-weight: 300; letter-spacing: -.045em; margin: 0; line-height: 1.02; }
.res-hello b { font-weight: 600; }
/* One rotating line, picked per page load from COPY.greetingPool. */
.res-sub { font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 300; letter-spacing: -.02em; color: var(--ink-2); margin: 10px 0 0; line-height: 1.25; }
/* `.res-head-row` (the heading/language-pill pairing) is gone [2026-08-18]:
   the language preference moved into Perfil's settings form, beside Formato
   de hora, so no player header carries a right-aligned control any more and
   all three open identically — heading, quiet line, accent. */

/* Instruction above the grid — says what the circles are for. */
.grid-lead { margin: 0 0 16px; }
/* +8px over the body tier (15 → 23): this is the one line that tells the
   player what the field of circles below is for, so it outranks the copy
   around it. */
.grid-lead-t { font-family: var(--font-display); font-size: var(--fs-lead); font-weight: 500; letter-spacing: -.03em; color: var(--ink); margin: 0; line-height: 1.2; }
.grid-lead-s { font-size: var(--fs-body-xs); color: var(--ink-2); font-weight: 300; margin: 3px 0 0; }

/* ── Player grid · the whole 36 h window [REVISED Fase 4 review] ───────────
   The court × hour matrix is the ADMIN's view. The player picks an hour (the
   court is chosen in the panel), and the grid now spans the entire window
   rather than one day at a time — the day tabs are gone, replaced by section
   labels. auto-fill fits as many 52px circles per row as the width allows —
   5 at 412px — and wraps rather than scrolling. */
.hgrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
  gap: 12px 10px; justify-items: center; align-items: center;
}
.hgrid .cell { margin: 0; }

/* Day sections inside the grid card. The label replaces the old day tab. */
.hsec + .hsec { margin-top: 22px; padding-top: 20px; border-top: 1px solid rgba(23, 20, 32, .07); }
.hsec-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.hsec-day { font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 600; letter-spacing: -.03em; }
/* A card-title section (Club's "Canchas" / "Reglas de la casa") rather than a
   day section ("Hoy" / "Mañana") — same shape, same hairline, but the title
   itself drops to the weight `.cfg-card h3` uses everywhere else a card is
   titled. 600 is earned by a day's worth of attention; a card title isn't. */
.hsec--soft .hsec-day { font-weight: 300; }
/* The middot is the same resource character every other day/time pairing in
   the app uses (e.g. the active-card's "Jue 13 · 18:00"); the head just
   never had it between the label and the date. */
.hsec-dot { color: var(--ink-2); font-weight: 500; }
.hsec-date { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 4px); letter-spacing: .04em; color: var(--ink-2); font-weight: 500; }

/* ── Selection step — the grid card's second state [REVISED 2026-08-17] ────
   Was a separate .selpanel card below the legend; is now the grid card
   itself, swapped in place, with a back button in place of the old
   "Cambiar" button beside confirm. */
.selstep { display: flex; flex-direction: column; gap: 18px; }
.sp-back {
  align-self: flex-start; border: 0; background: transparent; cursor: pointer;
  font-family: inherit; font-size: var(--fs-ui-sm); font-weight: 600; color: var(--ink-2);
  padding: 7px 12px 7px 8px; margin: -8px 0 -4px -8px; border-radius: var(--r-pill);
  transition: color var(--t-fast), background var(--t-fast);
}
.sp-back:hover { color: var(--ink); background: var(--bone); }

/* No "TURNO SELECCIONADO" label — the panel only ever appears because a slot
   was just tapped, so the label described something already obvious.
   Date/time split mirrors the active card: the date is context, the time is
   the answer, so only the time carries the extra weight. */
.sp-when { font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 300; letter-spacing: -.03em; }
.sp-when .sp-time { font-weight: 600; }

/* Name, value and payment terms carry no uppercase labels: at this point in
   the flow there is nothing else they could be, and the labels were louder
   than the values they described. All left-aligned. */
.sp-lines { display: flex; flex-direction: column; gap: 4px; }
.sp-name { font-size: var(--fs-value); font-weight: 500; margin: 0; }
.sp-value { font-family: var(--font-display); font-size: var(--fs-price); font-weight: 300; letter-spacing: -.045em; margin: 2px 0 0; }
.sp-value i { font-size: var(--fs-body-xs); font-weight: 300; letter-spacing: 0; }

.selstep .sp-field { display: flex; flex-direction: column; gap: 10px; }
.selstep .sp-flabel { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 2px); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }
/* Court chips wrap onto as many rows as they need — the panel grows in height
   rather than scrolling sideways. */
.court-picks { display: flex; flex-wrap: wrap; gap: 8px; }
.court-pick {
  border: 0; font-family: inherit; font-size: var(--fs-ui-sm); font-weight: 600; cursor: pointer;
  padding: 11px 18px; border-radius: var(--r-field); background: var(--bone); color: var(--ink-2);
  position: relative; transition: transform var(--t-base) var(--ease-out), background var(--t-fast), color var(--t-fast);
}
.court-pick:hover { color: var(--ink); transform: translateY(-2px); }
/* [REVISED 2026-08-17] Was a flat ink fill, unrelated to the tier of the hour
   it belongs to. Now matches the circle it came from: lime for a valley
   hour, the gradient for peak — same material, same convention. */
.court-pick.on { background: var(--lime); color: var(--ink); }
.court-pick.on.peak { background: var(--grad-35); color: var(--ink); --focus-ring: linear-gradient(var(--ink), var(--ink)); }
/* Taken at this hour — always shown, never removed. Grey and inert, so the
   player reads what the club HAS, not only what is left of it. */
.court-pick.is-off {
  background: transparent; color: var(--ink-2); opacity: .5; cursor: not-allowed;
  box-shadow: inset 0 0 0 1.5px rgba(23, 20, 32, .10); text-decoration: line-through;
}

/* De-scoped from `.selstep` [2026-08-18]. Three surfaces render `.sp-charge` —
   the selection panel, the ticket and now the club summary — but only the
   panel's was ever styled, so the ticket's own terms line had been falling
   back to plain body text while the panel it deliberately mirrors set it at
   the small secondary step. Found by comparing the two card sets, not by
   looking at the ticket. */
.sp-charge { font-size: var(--fs-body-xs); color: var(--ink-2); font-weight: 300; margin: 4px 0 0; }
/* Inline confirm failure, inside the panel — rose, never an alert. */
.sp-err { font-size: var(--fs-body-xs); color: var(--rose-ink); font-weight: 500; background: rgba(242, 116, 141, .12); padding: 12px 16px; border-radius: var(--r-field); margin: 0; }
/* One full-width confirm. "Cambiar" was removed — the grid stays on screen, so
   another hour is always one tap away and a second button earned nothing. */
.sp-confirm { width: 100%; padding: 18px 24px; }

/* ── Forms ── */
.field { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
/* [REVISED 2026-08-18] Sentence case, matching `.ac-label`/`.cs-flabel` —
   the same correction applied to Club's fact labels now reaches the system's
   actual form labels too. Tracked uppercase across every field of a form
   (Nombre completo, Correo, Teléfono, Género, Formato de hora) is the same
   "eight shouts in a row" problem the Club card had, just spread down a form
   instead of across a grid. */
.field > label { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 2px); letter-spacing: .01em; color: var(--ink-2); font-weight: 500; }
/* [REVISED 2026-08-18] Ceramic, not bone. A bone-filled field on a ceramic
   card read as a grey box sitting on the card rather than a field belonging
   to it — the muddiness was the two surfaces disagreeing, not the ring.
   Definition now comes entirely from the ring (bumped .06 → .10, the same
   weight `.cell.past` and `.court-pick.is-off` already use), which is a
   functional border marking an editable area — not the decorative kind §4
   rules out. `.input[readonly]` keeps its bone fill below: that's a
   deliberately flatter, non-editable state, a different signal on purpose. */
.input, .select {
  font-family: inherit; font-size: var(--fs-value); font-weight: 400; color: var(--ink);
  background: var(--ceramic); border: 0; border-radius: var(--r-field); padding: 15px 18px;
  box-shadow: inset 0 0 0 1.5px rgba(23, 20, 32, .10);
  transition: box-shadow var(--t-fast); width: 100%;
}
.input:hover, .select:hover { box-shadow: inset 0 0 0 1.5px rgba(23, 20, 32, .18); }
/* Focus is the same shape as error - an inset ring on the field itself, not
   an outline floating at an offset around it. Only the colour differs. */
.input:focus, .select:focus, .input:focus-visible, .select:focus-visible {
  outline: none; box-shadow: inset 0 0 0 2px var(--ink);
}
.input::placeholder { color: var(--ink-2); opacity: .7; }
.field.has-error .input { box-shadow: inset 0 0 0 1.5px var(--rose); background: rgba(255, 122, 148, .05); }
.field .help { font-size: var(--fs-meta); color: var(--ink-2); font-weight: 300; }
.field .err { font-size: var(--fs-meta); color: var(--rose-ink); font-weight: 500; }
.input[readonly] { background: var(--bone); color: var(--ink-2); box-shadow: none; cursor: default; }
.select { appearance: none; cursor: pointer; padding-right: 42px;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-2) 50%), linear-gradient(135deg, var(--ink-2) 50%, transparent 50%);
  background-position: calc(100% - 22px) 22px, calc(100% - 16px) 22px;
  background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; }

/* Read-only identity line — replaces the name input in the confirm modal. */
.readonly-line { display: flex; flex-direction: column; gap: 4px; padding: 15px 18px; border-radius: var(--r-field); background: var(--bone); }
.readonly-line span { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }
.readonly-line b { font-size: var(--fs-value); font-weight: 500; }

.field-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0 16px; }

/* Price confirmation inside the create-reservation modal [Fase 5] — the hour
   <select> names a price per option, but once one is picked there was
   nothing readable to check it against. Reuses the player draft panel's own
   .sp-value/.sp-charge rather than inventing a new price treatment. */
.cr-price { margin: -6px 0 18px; }
.cr-price .sp-value { font-size: var(--fs-value); }

/* ── Toggle ── */
.toggle { position: relative; display: inline-flex; align-items: center; gap: 12px; cursor: pointer; font-size: var(--fs-ui); font-weight: 600; border: 0; background: transparent; font-family: inherit; color: var(--ink); padding: 0; }
.toggle .tg { width: 46px; height: 27px; border-radius: var(--r-pill); background: rgba(23, 20, 32, .12); position: relative; transition: background var(--t-base) var(--ease-out); flex-shrink: 0; }
.toggle .tg::after { content: ""; position: absolute; top: 3px; left: 3px; width: 21px; height: 21px; border-radius: 50%; background: var(--ceramic); box-shadow: 0 3px 8px rgba(23, 20, 32, .2); transition: transform var(--t-base) var(--ease-out); }
.toggle.is-on .tg { background: var(--lime); }
.toggle.is-on .tg::after { transform: translateX(19px); }

/* ── Modal ── */
/* The confirm-modal internals (.modal-rows, .price-line, .charge-line) and the
   ink destructive button were removed with the modal itself: the selection
   panel commits now, and confirming buttons are always the gradient. */
.modal-back {
  position: fixed; inset: 0; z-index: 40; background: rgba(38, 30, 55, .34);
  backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center;
  padding: 24px; animation: fade var(--t-base) var(--ease-out);
}
@keyframes fade { from { opacity: 0; } }
.modal {
  background: var(--ceramic); border-radius: var(--r); box-shadow: var(--shadow-lift);
  width: 100%; max-width: 460px; max-height: calc(100vh - 48px); overflow-y: auto;
  /* [ADDED — Fase 4b mobile pass] 100vh is the LARGEST viewport on mobile
     browsers with a dynamic toolbar — a tall modal could overflow the visible
     area while the toolbar is up. dvh tracks the visible viewport; the 100vh
     line above stays as the fallback for older engines. */
  max-height: calc(100dvh - 48px);
  padding: 34px 32px 30px;
  /* Card entrance is driven by GSAP (M.modalIn). The CSS keyframe that used to
     run here was removed so the two do not double-animate; under reduced motion
     / no-gsap the card simply appears. */
}
.modal--wide { max-width: 560px; }
/* Bare modal — no chrome, the body IS the card. Used by the confirmed ticket,
   which is its own object and should not sit inside a titled dialog box. */
.modal--bare { background: transparent; box-shadow: none; padding: 0; max-width: 440px; overflow: visible; }
.modal--bare .ticket { margin: 0; }
/* [REVISED 2026-08-18] "Gracias" was the one full-width primary button in
   the app still running the base .btn padding (19px 34px, tuned for an
   inline-width label). Every other full-width commit button — .sp-confirm —
   is repadded to 18px 24px specifically for that stretched shape; without
   it, the same text/pill ratio that reads right at intrinsic width reads
   loose once flex stretches the box open. Not a weight or size mismatch —
   the two buttons share the exact same font rules — it was proportion. */
.ticket-close { margin-top: 14px; display: flex; }
.ticket-close .btn { flex: 1; padding: 18px 24px; }
.ticket-close .btn { flex: 1; }
.modal h3 { font-family: var(--font-display); margin: 0 0 6px; font-size: var(--fs-modal); font-weight: 500; letter-spacing: -.04em; }
.modal .m-sub { margin: 0 0 22px; color: var(--ink-2); font-size: calc(var(--fs-body-xs) + 2px); font-weight: 300; }
.modal-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.modal-actions .btn { flex: 1 1 140px; padding: 17px 26px; }
/* Confirm failure lives in the selection panel now (.sp-err), not a modal —
   there is no booking modal left to fail inside. */

/* ── Toast ── */
.toast-host { position: fixed; top: 22px; left: 50%; transform: translateX(-50%); z-index: 60; display: flex; flex-direction: column; gap: 10px; align-items: center; pointer-events: none; width: calc(100% - 40px); max-width: 420px; }
.toast {
  padding: 15px 24px; border-radius: var(--r-pill); font-size: var(--fs-body-sm); font-weight: 600;
  box-shadow: var(--shadow-lift); pointer-events: auto;
  display: flex; align-items: center; gap: 10px; text-align: center;
  /* Entrance driven by GSAP (M.toastIn); the drop keyframe was removed to avoid
     a double animation. Reduced motion / no-gsap: toast appears in place. */
}
.toast.is-out { animation: lift .3s var(--ease-out) forwards; }
@keyframes lift { to { opacity: 0; transform: translateY(-14px); } }
.toast--ok { background: var(--lime); color: var(--ink); }
.toast--err { background: var(--rose); color: #2A0912; }

/* ── Dev switcher (plain build only) ──────────────────────────────────────
   Collapsed to a single chip by default. Five rows of dev chrome were sitting
   on top of the screens they exist to review, which made the tool the thing
   blocking the review. */
.devswitch {
  position: fixed; top: 16px; right: 16px; z-index: 50;
  display: flex; flex-direction: column; gap: 6px; align-items: flex-end;
}
.ds-toggle {
  border: 0; font-family: var(--font-small); font-size: 10px; font-weight: 700;
  letter-spacing: .18em; color: var(--ink-2); background: var(--ceramic);
  padding: 9px 13px; border-radius: var(--r-pill); cursor: pointer; opacity: .55;
  box-shadow: var(--shadow); transition: opacity var(--t-fast), color var(--t-fast);
}
.ds-toggle:hover { opacity: 1; color: var(--ink); }
/* Open: capped in height and scrollable, so it can never exceed the viewport. */
.devswitch.is-open { max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); overflow-y: auto; scrollbar-width: none; }
.devswitch.is-open::-webkit-scrollbar { display: none; }
.ds-close { font-size: 15px !important; line-height: 1; padding: 5px 9px !important; }
.devswitch .ds-row { display: flex; gap: 3px; padding: 5px; border-radius: var(--r-pill); background: var(--ceramic); box-shadow: var(--shadow-lift); }
.devswitch button { border: 0; background: transparent; font-family: inherit; font-size: 11px; font-weight: 600; letter-spacing: .04em; color: var(--ink-2); padding: 8px 13px; border-radius: var(--r-pill); cursor: pointer; transition: color var(--t-fast), background var(--t-fast); white-space: nowrap; }
.devswitch button:hover { color: var(--ink); background: var(--bone); }
.devswitch button.on { background: var(--ink); color: var(--bone); }
.devswitch .ds-tag { font-family: var(--font-small); font-size: 9px; letter-spacing: .22em; text-transform: uppercase; color: var(--ink-2); font-weight: 600; padding-right: 8px; align-self: center; }

/* ── Card with a title and a quiet subtitle ──────────────────────────────
   Was Configuración's own card; Estadísticas borrowed it for its four chart
   cards, and after Configuración moved onto `.grid-card` + `.hsec` it is
   Estadísticas that owns it. Left under its old name rather than renamed:
   the class is what `admin/analytics.js` writes, and a rename with no
   behaviour behind it is churn. The `.cfg` grid and `.cfg-card--full` that
   sat here went with the screen that used them. */
.cfg-card { padding: 30px 28px; }
.cfg-card h3 { font-family: var(--font-display); margin: 0 0 4px; font-size: var(--fs-h3); font-weight: 300; letter-spacing: -.04em; }
.cfg-card .c-sub { margin: 0 0 22px; font-size: var(--fs-body-xs); color: var(--ink-2); font-weight: 300; }

/* ── Configuración (admin) ───────────────────────────────────────────────
   [REVISED 2026-08-20] The `.cfg` two-column grid and the `.cfg-card` box are
   gone. The screen runs Perfil's structure now — `.grid-card` cards sectioned
   by `.hsec.hsec--soft`, `card--split` for the pairs that sit side by side —
   so the only CSS left here is the four shapes that structure doesn't cover:
   a sport group, a court row, a schedule row, and the time picker. `.c-sub`
   went with `.cfg-card`; `.sec-note` was already doing the same job on every
   other screen. */

/* A sport group — heading, its court rows, its own add button. The heading is
   the `sport` field each row used to carry a copy of. */
.cgroup + .cgroup { margin-top: 26px; padding-top: 22px; border-top: 1px solid rgba(23, 20, 32, .07); }
.cgroup-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; }
.cgroup-name { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }
.cgroup-n { font-family: var(--font-small); font-size: var(--fs-label-sm); color: var(--ink-2); font-weight: 500; font-variant-numeric: tabular-nums; }
.cgroup-empty { margin: 12px 0 16px; }

/* Nombre · ubicación · disponibilidad · borrar. */
.court-row { display: grid; grid-template-columns: minmax(140px, 160px) minmax(150px, 190px) auto auto; gap: 12px; align-items: end; justify-content: start; padding: 10px 0; }
.court-row + .court-row { border-top: 1px solid rgba(23, 20, 32, .06); }
.court-row .field { margin-bottom: 0; }
.court-row .input, .court-row .select { padding: 10px 14px; }
.court-row .select { padding-right: 38px; background-position: calc(100% - 20px) 17px, calc(100% - 14px) 17px; }
.court-row--head .field > label { font-size: calc(var(--fs-label-sm) + 4px); }
.cfg-courts .hsec-day { font-size: calc(var(--fs-h3) + 2px); font-weight: 400; }
.court-row .toggle { padding-bottom: 10px; }
/* A court in maintenance is quieted, not greyed out: it is not a disabled
   row — the operator still renames and relocates it while it sits out. */
.court-row.is-off .input, .court-row.is-off .select { color: var(--ink-2); }
.court-row.is-off .field > label { opacity: .7; }

/* Tariff bands use the same dense row language as courts and opening shifts. */
.tariff-row { display: grid; grid-template-columns: minmax(140px, 180px) minmax(130px, 160px) minmax(190px, 230px) minmax(190px, 230px) auto; gap: 12px; align-items: end; justify-content: start; padding: 10px 0; }
.tariff-row + .tariff-row { border-top: 1px solid rgba(23, 20, 32, .06); }
.tariff-row .field { margin-bottom: 0; max-width: none !important; }
.tariff-row .input, .tariff-row .select { padding: 10px 14px; }
.tariff-row .select { padding-right: 38px; background-position: calc(100% - 20px) 17px, calc(100% - 14px) 17px; }
.tariff-row--head .field > label { font-size: calc(var(--fs-label-sm) + 4px); }

.seg-row { display: grid; grid-template-columns: minmax(200px, 240px) minmax(200px, 240px) auto; gap: 12px; align-items: end; justify-content: start; padding: 10px 0; }
.seg-row + .seg-row { border-top: 1px solid rgba(23, 20, 32, .06); }
.seg-row .field { margin-bottom: 0; }
.seg-row .select { padding-top: 10px; padding-bottom: 10px; }

/* Time picker — two selects reading as one control. The colon between them is
   what makes the pair a time rather than two unrelated dropdowns. */
.tpick { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 6px; align-items: center; }
.tpick .select { padding-right: 34px; }
.tpick-sep { font-weight: 600; color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* A stack of switches — Cobro's payment options, the policy's two rules.
   Roomier than a field stack: every row in it is its own decision. `--row`
   lays the same stack across where the card is full width and the switches
   are siblings rather than a sequence. */
.tstack { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; margin-bottom: 18px; }
.tstack--row { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 14px 40px; margin-bottom: 24px; }
.tg-lab { min-width: 132px; text-align: left; }
.toggle[disabled] { opacity: .55; }

/* A `card--split` column holding more than one section. The split rule strips
   the hairline between sections because side-by-side columns don't need it;
   inside a column they are stacked again, so it comes back. */
.card--split .cfg-col .hsec + .hsec { margin-top: 22px; padding-top: 20px; border-top: 1px solid rgba(23, 20, 32, .07); }

/* A field that needs more than a `.field-row` track but not the whole card —
   the refund rule's select, whose longest option is a full sentence. */
.field--wide { max-width: 460px; }
/* An hour or a percentage — the box is sized to the value, not to the
   column it happens to sit in. The help line under it keeps the column. */
.field--num .input { max-width: 200px; }

/* Court rows print their column labels once per group, so only the first row
   carries the label line and the rows under it close up against it. */
.court-row--head + .court-row { padding-top: 10px; }

/* The fields a mode reveals under its pills, and the quiet closing line a
   section ends on — a total, a default, a rule. */
.cfg-fields { margin-top: 18px; }
.cfg-total { margin: 4px 0 0; }

@media (max-width: 720px) {
  .config-surface .field-row { grid-template-columns: minmax(0, 1fr); }
  .config-surface .field:not(.field--wide) { max-width: none; }
  .config-surface .field--refund-until { grid-column: auto; }
  .court-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
  .tariff-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
  .court-row .toggle { padding-bottom: 0; }
  .court-row .btn-x, .seg-row .btn-x { justify-self: end; }
  .seg-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
}
.btn-x { width: 42px; height: 42px; border-radius: var(--r-pill); border: 0; background: var(--bone); color: var(--ink-2); font-size: 18px; cursor: pointer; font-family: inherit; transition: background var(--t-fast), color var(--t-fast); flex-shrink: 0; }
.btn-x:hover { background: rgba(255, 122, 148, .14); color: var(--rose-ink); }
/* [REVISED 2026-08-18] Ceramic + ring, not a bone fill — same correction as
   `.input`/`.select` just above, and the same reasoning: a filled grey track
   on a ceramic card read as a box sitting on the card, not part of it.
   Radius moved from `--r-pill` to `--r-field` in the same pass, requested
   after the fully-round track sat directly under the field-radius inputs in
   Datos and the two corner languages disagreed on one form. This is a
   deliberate, local exception: `.mode-pills` reads as a field-adjacent
   control (a settings row, right below a text input) rather than a pill
   chip like the day selector or the dock, so it borrows the field's radius
   instead of the system's default pill shape. */
.mode-pills { display: inline-flex; flex-wrap: nowrap; gap: 4px; padding: 5px; border-radius: var(--r-field); background: var(--ceramic); box-shadow: inset 0 0 0 1.5px rgba(23, 20, 32, .10); max-width: 100%; overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; }
.mode-pills::-webkit-scrollbar { display: none; }
.mode-pills button { flex-shrink: 0; }
.mode-pills button { border: 0; background: transparent; font-family: inherit; font-size: var(--fs-ui); font-weight: 600; color: var(--ink-2); padding: 10px 18px; border-radius: calc(var(--r-field) - 5px); cursor: pointer; transition: color var(--t-fast), background var(--t-fast); }
.mode-pills button:hover { color: var(--ink); }
/* [REVISED 2026-08-18] Gradient, not a solid ink fill — that flat black was a
   third material for "selected," alongside the language pill's lime and the
   dock's already-gradient active tab. All three are the same shape (pick one
   of a small set of mutually exclusive options), so they now share the same
   fill. The dock's `.dock a.is-active` had already made this call; this pass
   catches the two pill groups that hadn't. Focus ring overridden to ink for
   the same reason every gradient-filled control needs it — `--focus-ring` is
   itself the gradient, so a gradient ring on a gradient fill is invisible. */
.mode-pills button.on { background: var(--grad-35); color: var(--ink); --focus-ring: linear-gradient(var(--ink), var(--ink)); }

/* ── Filter pills (reservation management) ── */
.filters { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; align-items: center; }
/* Was an inline style="margin-top:30px" on Reservas — the gap the legend
   above needs before a filter row starts, formalized as a real modifier
   instead of a one-off magic number on the caller's side. */
.filters--table { margin-top: 30px; }
/* The day filter sits above the board it drives — tighter to the card below
   than .filters--table is to the legend above it, so the pair reads as one
   block rather than two things that happen to be adjacent. */
.filters--board { margin: 0 0 16px; }
/* The result-count line between the filters and the table — pulled up
   against the filters it describes rather than reading as its own block. */
.sec-note--count { margin: -8px 0 16px; }
.fgroup { display: inline-flex; flex-wrap: nowrap; gap: 3px; padding: 5px; border-radius: var(--r-pill); background: var(--ceramic); box-shadow: var(--shadow); max-width: 100%; overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; }
.fgroup::-webkit-scrollbar { display: none; }
.fgroup button { flex-shrink: 0; border: 0; background: transparent; font-family: inherit; font-size: var(--fs-ui-sm); font-weight: 600; color: var(--ink-2); padding: 9px 16px; border-radius: var(--r-pill); cursor: pointer; transition: color var(--t-fast), background var(--t-fast); white-space: nowrap; }
.fgroup button:hover { color: var(--ink); }
.fgroup button.on { background: var(--lime); color: var(--ink); --focus-ring: linear-gradient(var(--ink), var(--ink)); }

/* ── Management + tenants tables ── */
.rtable { padding: 10px 0; overflow: hidden; }
.rth, .rtr { display: grid; grid-template-columns: 1fr 1.2fr 1.1fr 1.1fr 1fr .9fr; gap: 10px; padding: 15px 26px; align-items: center; font-size: var(--fs-body-sm); }
.rth { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 2px); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }
.rtr { transition: background var(--t-fast); }
.rtr:hover { background: var(--bone); }
.rtr .row-act { display: flex; gap: 6px; justify-self: end; }

/* ── Row hierarchy [Fase 5] ───────────────────────────────────────────────
   Six fields used to render at one size, so the row read as a list of equal
   facts. It isn't: an operator scanning this table is looking for a person,
   then where and when, then whether they paid. The ladder — Jugador → Cancha
   → Cuándo → Pago → Estado → Cancelar — is set here rather than in the markup
   so the column ORDER (which puts Pago before Estado, next to the action that
   changes it) stays free to differ from the visual weight. Every rule is
   scoped to `.rtr`: `.st` is shared with Clubes and the cards, and must not
   move. */
.rtr .rc-name  { font-size: var(--fs-body); font-weight: 600; letter-spacing: -.01em; }
.rtr .rc-court { font-size: var(--fs-value); font-weight: 500; }
.rtr .rc-when  { font-size: var(--fs-body-sm); font-weight: 500; color: var(--ink); }
.rtr .rc-pay .st,
.rtr .rc-status .st,
.rtr .row-act .btn-s { font-size: var(--fs-chip-sub); font-weight: 600; padding: 5px 11px; }
.rtr .rc-status .st { letter-spacing: .08em; }
/* Last rung: still a real 44px target on touch (that floor lives in the
   ≤760px block), just the quietest thing in the row at desktop density. */
.rtr .row-act .btn-s { padding: 7px 13px; font-size: calc(var(--fs-chip-sub) + 2px); font-weight: 700; color: var(--ink-2); }

.ttable { padding: 10px 0; overflow: hidden; }
.tth, .ttr { display: grid; grid-template-columns: 1.3fr 1.4fr 1fr .9fr auto; gap: 10px; padding: 15px 26px; align-items: center; font-size: var(--fs-body-sm); }
.tth { font-family: var(--font-small); font-size: var(--fs-label-sm); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 500; }
.ttr { transition: background var(--t-fast); }
.ttr:hover { background: var(--bone); }
.ttr code { font-size: var(--fs-meta); color: var(--ink-2); background: var(--bone); padding: 5px 11px; border-radius: var(--r-pill); font-family: var(--font-mono); justify-self: start; }

/* ── Dense table row states [Fase 5] ──────────────────────────────────────
   The two states `.rtable`/`.ttable` never had: loading and error. Rows
   carry no dividers (§3) and don't lift on hover — a table row isn't a
   discrete object the way a card is, so the universal "everything
   interactive lifts" rule (§5) has its one deliberate exception here; the
   bone hover wash is the row's whole feedback. Documented in
   chosen-dna.md §4a. */
.skel-bar { display: block; height: 13px; border-radius: var(--r-pill); }
.rtr.is-skel, .ttr.is-skel { pointer-events: none; }
/* A table-level failure — reuses `.empty`'s shape (label + line + action) so
   a failed fetch reads with the same recipe as a genuinely empty result,
   distinguished only by the rose accent §7 reserves for error. */
.empty--err .e-label { color: var(--rose-ink); }

/* ── Analytics (Fase 5) ──────────────────────────────────────────────────
   A read-only KPI shell — Fase 12 swaps the numbers, not this layout. Chart
   marks stay inside the system's existing neutral-ink vocabulary rather than
   introducing new hues: every distribution here is a single series (one
   metric per court, one metric per hour), which per the dataviz method takes
   one uniform fill and no legend — the only genuinely categorical figure is
   the two-way sport split, which stays inside ink/ink-2 and carries its
   labels directly rather than reaching for peach/lilac/mint (reserved,
   system-wide, for "blended only, never solo" — see chosen-dna.md §1). */
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 26px; }
/* Chart cards are `.cfg-card` — same title/subtitle recipe Configuración
   already uses, no second card dialect invented for one new screen. Two per
   row on desktop [Fase 5 desktop pass] — four charts read as one long
   scroll otherwise, when the canvas has room for a 2×2 layout. */
.chart-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; align-items: start; }
@media (max-width: 980px) { .chart-grid { grid-template-columns: 1fr; } }

/* Peak hours — a vertical column per hour, magnitude in height. Columns cap
   at 4px per the mark spec; square at the baseline. */
.vchart { display: flex; align-items: flex-end; gap: 6px; height: 140px; margin: 18px 0 4px; }
.vchart .vcol { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; min-width: 0; }
.vchart .vcol i { display: block; width: 100%; max-width: 22px; border-radius: 4px 4px 0 0; background: var(--ink-2); opacity: .2; transition: opacity var(--t-fast), background var(--t-fast); }
.vchart .vcol.is-peak i { opacity: 1; background: var(--ink); }
.vchart .vcol span { font-family: var(--font-small); font-size: 10px; color: var(--ink-2); margin-top: 8px; font-weight: 500; }

/* Reservations / revenue per court — a horizontal bar per court. Pill radius,
   matching `.meter`'s own shape rather than the generic 4px bar-cap spec:
   these are single-value-of-a-max marks, the same job `.meter` already does,
   so they read as the same family of component. */
.hchart { display: flex; flex-direction: column; gap: 12px; margin-top: 4px; }
/* Label column sizes to its own content (auto) rather than a fixed width —
   a long court name wraps or widens the column instead of being clipped,
   per rule 15 (no container clips its own content). */
.hbar-row { display: grid; grid-template-columns: minmax(64px, auto) 1fr 48px; align-items: center; gap: 12px; }
.hbar-row .hb-label { font-size: var(--fs-body-sm); color: var(--ink-2); font-weight: 500; }
.hbar-track { height: 13px; background: rgba(23, 20, 32, .06); border-radius: var(--r-pill); overflow: hidden; }
.hbar-track i { display: block; height: 100%; border-radius: var(--r-pill); background: var(--ink-2); transition: width .5s var(--ease-out); }
.hbar-row .hb-val { font-size: var(--fs-body-sm); font-weight: 600; text-align: right; }

/* Sport mix — one segmented bar, ≤2 slices, each carrying its own printed
   label. Two neutral ink tones, not new hues: identity here rides the label,
   not the color, which the dataviz method allows for a directly-labeled,
   low-cardinality split. */
.mix-bar { display: flex; height: 32px; border-radius: var(--r-pill); overflow: hidden; margin-top: 6px; }
.mix-bar span { display: flex; align-items: center; justify-content: center; font-size: var(--fs-chip); font-weight: 600; color: var(--ceramic); white-space: nowrap; overflow: hidden; }
.mix-bar .mx-a { background: var(--ink); }
.mix-bar .mx-b { background: var(--ink-2); }
.mix-legend { display: flex; gap: 18px; margin-top: 12px; font-size: var(--fs-body-xs); color: var(--ink-2); font-weight: 300; }
.mix-legend i { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 7px; vertical-align: -1px; }
.mix-legend i.mx-a { background: var(--ink); }
.mix-legend i.mx-b { background: var(--ink-2); }

/* ── Player: club page + profile ── */
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
/* The club page carries no card dialect of its own [REVISED 2026-08-18]. Its
   sections are `.grid-card` + `.hsec`, exactly as Reservar's grid card holds
   its day sections, and its money block is `.sp-lines`. `.info-card` /
   `.i-label` and `.cfg-card` on this screen are both gone: a third and fourth
   way to draw a card is how a system stops being one. */
.club-rules { margin: 0; color: var(--ink-2); font-weight: 300; }
/* ── Club summary ─────────────────────────────────────────────────────────
   Same ground recipe as the page and the ticket — bone with three pale mesh
   blobs — so the three surfaces read as one family. Padding matches
   `.grid-card`: the summary and the card below it are two surfaces of one
   page, and they used to sit 4px apart on every edge.

   The club's name left this block for the page heading, taking `.cs-head`,
   `.cs-name` and the uppercase sport pill with it. */
.club-summary {
  position: relative;
  border-radius: var(--r); box-shadow: var(--shadow); padding: 26px 22px; margin-bottom: 14px;
  background:
    radial-gradient(62% 55% at 6% 4%, rgba(255, 201, 168, .58), transparent 70%),
    radial-gradient(56% 50% at 97% 34%, rgba(217, 200, 255, .55), transparent 70%),
    radial-gradient(52% 46% at 28% 99%, rgba(185, 242, 220, .48), transparent 70%),
    var(--bone);
}
/* ── Long-press to share [ADDED 2026-08-18] ─────────────────────────────────
   Same gesture as the active card's long-press-to-cancel, aimed at a
   different outcome. `cursor: pointer` + `user-select: none` on the whole
   card, exactly as `.active-card` does — the four contact links inside keep
   their own cursor and their own tap/long-press behaviour; the JS guard that
   arms this gesture explicitly steps aside when the touch starts on an `<a>`. */
.club-summary { cursor: pointer; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base); }
.club-summary.is-holding { transform: scale(.97); box-shadow: 0 8px 20px rgba(38, 30, 55, .10); }
/* Same negative-margin caption trick as `.ac-hint`, re-tuned: the active card
   sits on a margin-bottom of 22px, this card on 14px, so pulling by the same
   -14px here would leave the hint touching the card. -6px nets the same ~8px
   breathing room `.ac-hint` gets after the active card. */
.club-summary + .ac-hint { margin: -6px 0 22px; }
/* The money block, borrowed whole from the selection panel — same label,
   same figure, same terms line. Space below it separates it from the fact
   grid; a rule there would be a border doing a job space already does. `.
   sp-charge` inside it is bumped +2px, scoped to this card only — see the
   card-wide bump note below `.cs-fact b`. */
.cs-money { margin-bottom: 24px; }
.cs-money .sp-value { margin-top: 4px; font-weight: 500; }
.cs-money .sp-charge { font-size: calc(var(--fs-body-xs) + 2px); }
/* Two columns where there is room, one where there is not. Each fact stacks
   its label over its value so a long address never shares a line with it. */
.cs-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 18px 20px; }
.cs-fact { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* Sentence case, following `.ac-label`. This page used to set every label in
   tracked uppercase — eight of them in one grid, each shouting a word like
   "TELÉFONO" louder than the number underneath it.
   [+2px, requested 2026-08-18] `.cs-flabel` and `.cs-fact b` are unique to
   this card (no other screen reuses them), so the bump lives directly in
   their own rule rather than a scoped override. */
.cs-flabel { font-family: var(--font-small); font-size: calc(var(--fs-label-sm) + 4px); letter-spacing: .01em; color: var(--ink-2); font-weight: 500; }
.cs-fact b { font-size: calc(var(--fs-value) + 2px); font-weight: 500; overflow-wrap: anywhere; }
/* An actionable fact: the whole cell is the target, not just the value line.
   A phone number rendered as text is a number the player copies by hand, and
   a bare value line is a 20px tap area on the device this is built for. */
/* Radius is here only so the focus ring (`border-radius: inherit`) rounds —
   the cell itself paints nothing. Without the `.cs-fact--do` entry added to
   the focus-ring selector lists above, these links would have had no keyboard
   focus indicator at all: `:where(a…):focus-visible` removes the browser's. */
.cs-fact--do { text-decoration: none; color: inherit; padding: 4px 0; min-height: 44px; justify-content: center;
               border-radius: var(--r-field); transition: transform var(--t-base) var(--ease-out); }
.cs-fact--do b { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px;
                 text-decoration-color: rgba(23, 20, 32, .22); transition: text-decoration-color var(--t-fast); }
.cs-fact--do:hover { transform: translateX(2px); }
.cs-fact--do:hover b { text-decoration-color: var(--ink); }
/* Court groups [REVISED 2026-08-18] — grouped by sport instead of listed one
   row per court. A player choosing whether to play doesn't need to know a
   court is Indoor vs Outdoor before they've even picked an hour (that detail
   still lives on the selection panel, where it might matter); they need to
   know the club has padel, or tennis, or both, and how many. Reuses `.cs-fact`
   — the exact label-over-value block the summary above already runs — rather
   than inventing a second layout for the same shape of information. */
.court-groups { display: flex; flex-direction: column; gap: 16px; }

/* History rows — same fix: the three-column grid squeezed the date, court and
   chip together at 412px. The mobile block below stacks them. */
.hist { display: flex; flex-direction: column; }
/* [REVISED 2026-08-18] The row is a button now — it opens the ticket modal.
   Button reset first (font/colour inherit), then the hover wash the admin
   tables already use for a tappable row: bone, quiet, no border appearing.
   The DATE leads with the weight; the hour is secondary — a log is scanned
   by when, not by hour. No divider between rows, same as ever: separation
   is whitespace (§3). */
.hist-row {
  display: grid; grid-template-columns: auto 1fr auto auto; gap: 6px 14px; align-items: center;
  padding: 16px 0; font-size: var(--fs-body-sm); width: 100%; text-align: left;
  border: 0; background: transparent; font-family: inherit; color: inherit; cursor: pointer;
  border-radius: var(--r-field);
  transition: background var(--t-fast), color var(--t-fast);
}
.hist-row:hover { background: var(--bone); }
.hist-row .h-date { font-weight: 500; min-width: 0; }
.hist-row .h-time { color: var(--ink-2); font-weight: 300; min-width: 0; }
.hist-row .h-court { color: var(--ink-2); font-weight: 300; min-width: 0; }
.hist-row .st { justify-self: end; }

/* ── Auth screens ── */
.auth-wrap { max-width: 440px; margin: 0 auto; }
.auth-card { padding: 38px 34px 34px; }
.auth-card h2 { margin-bottom: 6px; }
.auth-card .a-sub { font-family: var(--font-small); margin: 0 0 26px; color: var(--ink-2); font-size: var(--fs-body-sm); font-weight: 300; }
.auth-foot { margin: 20px 0 0; text-align: center; font-size: var(--fs-body-xs); color: var(--ink-2); font-weight: 300; }
/* [ADDED — Fase 4b mobile pass] "Crear cuenta" / "Entrar" measured 16px tall as
   bare inline links — the smallest tap targets on any player surface. Padding
   grows the hit area to ~44px without changing the visual line it sits on. */
.auth-foot a { display: inline-block; padding: 13px 10px; color: var(--ink); font-weight: 600; text-decoration: none; }
.auth-foot a:hover { text-decoration: underline; }
.auth-alt { margin-top: 22px; padding-top: 22px; border-top: 1px solid rgba(23, 20, 32, .07); text-align: center; }

/* ── Styleguide ── */
.sg-sec { margin-bottom: 54px; }
.sg-sec > h2 { margin-bottom: 6px; }
.sg-sec > .sg-note { margin: 0 0 22px; color: var(--ink-2); font-size: var(--fs-body-xs); font-weight: 300; max-width: 60ch; }
.sg-panel { padding: 28px 26px; }
.sg-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.sg-row + .sg-row { margin-top: 22px; padding-top: 22px; border-top: 1px solid rgba(23, 20, 32, .06); }
.sg-lab { font-family: var(--font-small); font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-2); font-weight: 600; width: 100%; margin-bottom: -4px; }
.sw-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 12px; }
.sw { border-radius: var(--r-soft); overflow: hidden; box-shadow: var(--shadow); }
.sw .sw-chip { height: 76px; }
.sw .sw-meta { padding: 12px 14px; background: var(--ceramic); display: flex; flex-direction: column; gap: 2px; }
.sw .sw-meta b { font-size: var(--fs-meta); font-weight: 600; }
.sw .sw-meta span { font-size: 11px; color: var(--ink-2); font-family: var(--font-mono); }
.type-row { display: flex; align-items: baseline; gap: 20px; padding: 16px 0; flex-wrap: wrap; }
.type-row + .type-row { border-top: 1px solid rgba(23, 20, 32, .06); }
.type-row .t-meta { font-family: var(--font-small); font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 600; width: 168px; flex-shrink: 0; }
.sg-cellrow { display: flex; gap: 18px; flex-wrap: wrap; }
.sg-cellrow .cell { margin: 0; }
.sg-cellrow figure { margin: 0; display: flex; flex-direction: column; gap: 10px; align-items: center; width: 76px; }
.sg-cellrow figcaption { font-family: var(--font-small); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-2); font-weight: 600; }
.sg-ground { background: var(--bone); border-radius: var(--r-soft); padding: 22px; }
.sg-toc { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 40px; }
.sg-toc a { text-decoration: none; font-size: var(--fs-meta); font-weight: 600; color: var(--ink-2); background: var(--ceramic); padding: 9px 16px; border-radius: var(--r-pill); box-shadow: var(--shadow); transition: transform var(--t-fast) var(--ease-out); }
.sg-toc a:hover { transform: translateY(-2px); color: var(--ink); }
.sg-rule { display: flex; gap: 12px; padding: 14px 0; font-size: var(--fs-body-sm); color: var(--ink-2); font-weight: 300; }
.sg-rule + .sg-rule { border-top: 1px solid rgba(23, 20, 32, .06); }
.sg-rule b { color: var(--ink); flex: 0 0 190px; font-weight: 600; }
.sg-copy { display: grid; grid-template-columns: 190px 1fr; gap: 12px 18px; align-items: baseline; }
.sg-copy dt { font-family: var(--font-small); font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-2); font-weight: 600; }
.sg-copy dd { margin: 0; font-size: var(--fs-body-sm); color: var(--ink-2); font-weight: 300; }

/* Static replicas of the pseudo-class states, so hover / focus / active can be
   compared side by side. They are INERT — pointer-events off — because a
   replica that also responds to your mouse shows two states at once and the
   "Reposo" swatch stops being reposo the moment you point at it. One live
   control sits beside them for the real feel. */
.demo-hover, .demo-active, .demo-focus, .demo-rest { pointer-events: none; }
.demo-hover.btn-a { transform: translateY(-3px) scale(1.02); box-shadow: 0 22px 50px rgba(150, 190, 40, .44); }
.demo-hover.btn-b { transform: translateY(-3px); box-shadow: var(--shadow-lift); }
.demo-hover.cell { transform: translateY(-4px) scale(1.06); }
.demo-hover.cell.free { box-shadow: inset 0 0 0 2px rgba(150, 190, 40, .95), 0 12px 26px rgba(38, 30, 55, .16); }
.demo-hover.cell.peak { box-shadow: 0 12px 26px rgba(38, 30, 55, .18); }
/* Pressed: the lift collapses BELOW resting and the shadow tightens, so it
   reads as depressed rather than as an identical twin of reposo. */
.demo-active { transform: translateY(1px) scale(.985) !important; }
.demo-active.btn-a { box-shadow: 0 4px 12px rgba(150, 190, 40, .34) !important; }
.demo-active.btn-b { box-shadow: 0 3px 10px rgba(38, 30, 55, .12) !important; }
.demo-live { position: relative; }


/* ═══════════════════════════════════════════════════════════════════════════
   § RESPONSIVE — verified at 360 / 390 / 412 / 768 / 1024 / 1440 / 1920
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {
  .rth, .rtr { grid-template-columns: 1fr 1.1fr 1fr 1fr .9fr; }
  .rth .rh-pay, .rtr .rc-pay { display: none; }
  .tth, .ttr { grid-template-columns: 1.3fr 1.4fr .9fr auto; }
  .tth .th-sport, .ttr .tc-sport { display: none; }
}

@media (max-width: 760px) {
  .stats { grid-template-columns: 1fr; }
  .th, .tr { grid-template-columns: 1fr 1fr; gap: 8px; }
  .dock a { padding: 17px 19px; font-size: calc(var(--fs-ui-sm) + 4px); }
  .surface { padding: 44px 20px; }

  /* Tables collapse to a two-column card per row. */
  .th, .rth, .tth { display: none; }
  .tr, .rtr, .ttr {
    grid-template-columns: 1fr 1fr; gap: 6px 12px;
    padding: 18px 22px; border-radius: var(--r-soft); background: var(--bone); margin: 8px 16px;
  }
  .rth .rh-pay, .rtr .rc-pay, .tth .th-sport, .ttr .tc-sport { display: block; }
  .rtr .row-act, .ttr .tc-act { grid-column: 1 / -1; justify-self: start; margin-top: 6px; }

  .court-row { grid-template-columns: 1fr 1fr; }
  .court-row .btn-x { grid-column: 1 / -1; justify-self: start; }
  .seg-row { grid-template-columns: 1fr 1fr; }
  .seg-row .btn-x { grid-column: 1 / -1; justify-self: start; }

  .active-card { padding: 22px; gap: 16px; }
  .active-card .ac-side { width: 100%; }

  /* ── Touch-target floor [Fase 4b mobile pass] ─────────────────────────────
     Two player controls measured under 44px at 360–412: the mode-pills
     segmented controls (39px — Perfil's time-format and language rows, the
     auth gender row) and .btn-s (41px). Desktop keeps the denser sizing. */
  .mode-pills button { padding-top: 12px; padding-bottom: 12px; }
  .btn-s { min-height: 44px; }

  /* ── Player, 412px-first fixes [Fase 4] ─────────────────────────────────
     Rule for this block: a container grows in height, it never scrolls
     sideways and it never clips its own content. */

  /* History rows stack: date+court on one line, chip on its own. The
     three-column grid ran the three values together at this width. */
  .hist-row { grid-template-columns: 1fr auto; }
  .hist-row .h-time { grid-column: 1 / -1; }
  .hist-row .h-court { grid-column: 1 / -1; }
  .hist-row .st { grid-row: 1; grid-column: 2; }

  .info-grid { gap: 12px; }
  /* Matches .grid-card's mobile padding, same as at full width. */
  .club-summary { padding: 20px 16px; }
  /* One column means the facts are a stack of tap targets, so the gap tightens
     — each cell already carries its own 44px height. */
  .cs-grid { grid-template-columns: 1fr; gap: 8px; }

  /* Profile — the time-format group is a real pill row at this width, and
     the form never goes edge-to-edge tight. */
  .cfg-card { padding: 24px 20px; }
  .mode-pills { width: 100%; }
  .mode-pills button { flex: 1 1 auto; text-align: center; }

  /* Selection panel + grid keep their breathing room without overflowing. */
  .hgrid { gap: 12px 8px; }

  .modal { padding: 28px 24px 26px; border-radius: var(--r-soft); }
  .modal--bare { padding: 0; }
  .devswitch { top: 10px; right: 10px; }
  .devswitch button { padding: 7px 10px; font-size: 10px; }
  .ticket-lines { padding: 20px 22px 16px; }
  .ticket-qr { padding: 20px 22px 24px; }

  .sg-rule { flex-direction: column; gap: 4px; }
  .sg-rule b { flex: none; }
  .sg-copy { grid-template-columns: 1fr; gap: 4px; }
  .sg-copy dd { margin-bottom: 12px; }
  .type-row .t-meta { width: 100%; }
}

@media (max-width: 460px) {
  .cta .btn { flex: 1 1 100%; }
  .orb { flex: 1 1 100%; }
  .modal-actions .btn { flex: 1 1 100%; }
  .grid-card { padding: 20px 16px; }
  .hero-stat { padding: 26px 22px; }
}

@media (prefers-reduced-motion: reduce) {
  .mesh i { animation: none; }
  .pulse, .skeleton::after, .btn.is-loading::after { animation: none; }
  .toast, .modal, .modal-back { animation: none; }
  .cell.free, .cell.peak, .cell.peak::before, .cell.is-shake, .tap-ripple, .confetti-bit { animation: none; }
  /* [FIXED — Fase 4b] The ambient cell breathing survived reduced motion: its
     base selector `.cell.free:not(.is-sel)` out-specifies `.cell.free` above,
     so `animation: none` never won. Matched specificity here closes it —
     measured live: animationName stayed "cellBreathe" under emulate-reduce. */
  .cell.free:not(.is-sel), .cell.peak:not(.is-sel) { animation: none; }
  * { transition-duration: .01ms !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   § PLAYER AT DESKTOP WIDTH — Fase 4b web pass
   The player surface stays a 900px column (the generous rhythm is the design,
   not a defect to fill), but width earns two things it could not have on a
   phone: the grid stays visible while the draft panel is open, and the
   "one card, two sections" screens run their sections side by side. Below
   981px (the same breakpoint value the admin collapse uses) every screen is
   byte-for-byte the mobile layout. minmax(0,1fr) throughout — the Fase 5
   grid-blowout lesson applies here too.
   ═══════════════════════════════════════════════════════════════════════════ */

.book-two { display: block; }
/* The duplicated grid is desktop-only content; mobile keeps the panel-swap
   flow the Fase 4 reviews settled on. */
.book-two .book-two-grid { display: none; }
.book-two .grid-card + .grid-card { margin-top: 0; }

/* Canchas/Recomendaciones (Club) and Datos/Historial (Perfil). */
.card--split { display: block; }

@media (min-width: 981px) {
  .book-two {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(340px, 400px);
    gap: 14px; align-items: start;
  }
  .book-two .book-two-grid { display: block; }
  /* The grid is on screen beside the draft — "Cambiar hora" was the answer
     to the grid disappearing, which no longer happens here. The button stays
     in the DOM (and on mobile) so the back path never depends on width. */
  .book-two .sp-back { display: none; }

  .card--split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 46px; }
  /* The hairline between sections separates stacked sections; side by side,
     whitespace between columns does that job and the border would hang from
     the shorter column's top edge. */
  .card--split .hsec + .hsec { margin-top: 0; padding-top: 0; border-top: none; }
}
