/* ============================================================
   SERAC — Shared UI primitives
   ============================================================
   Reusable, low-level component classes built on top of
   `shared/tokens.css`. This file owns *primitives* only (buttons,
   fields, badges, tabs, lists, empty states, toasts, modals) — app
   shells, page layout and domain-specific components (planning grid,
   exercise catalogue, moderation cards, etc.) stay local to each app's
   own stylesheet. See docs/TRAINER_FRONTEND_CURRENT_STATE.md
   §33/34 for the rationale.

   Currently consumed by the trainer backoffice, trainer login and the
   client app. Written with generic, app-agnostic class names precisely
   so a second app can adopt the same primitives without a rename.
   Link this file *after* tokens.css and *before* the app's own
   styles.css, so the app can still override where genuinely needed.
   ============================================================ */

/* ── Icons (shared/icons.js) ─────────────────────────────────────
   Exactly two sizes app-wide — never a one-off inline width/height on an
   icon <svg>. Stroke-based by default (a filled glyph, e.g. the nav bolt,
   sets fill="currentColor" on its own paths inside the sprite instead).
   Color always follows the surrounding text color via currentColor, so an
   icon never needs its own color rule at the call site. */
.icon {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    /* Sensible default when an icon sits inline with text (e.g. a hint
       paragraph) rather than inside a flex row (nav item, button) — flex
       containers use align-items instead and simply ignore this. */
    vertical-align: -3px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.75;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.icon-sm { width: 16px; height: 16px; }

/* Pair with the `spinner` icon for a loading indicator. */
@keyframes sr-icon-spin { to { transform: rotate(360deg); } }
.icon-spin { animation: sr-icon-spin 0.8s linear infinite; }

/* ── Base reset shared by anything that opts in ─────────────────── */
.btn, .field input, .field select, .field textarea {
    font-family: var(--font-sans);
}

/* Consistent, visible keyboard focus everywhere (spec §32) instead of
   relying on each component to remember to set it. */
:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* Containers and headings that only take PROGRAMMATIC focus (tabindex="-1": onboarding title, dialog
   panels, <main> after the skip link) are not controls. Without keyboard use (shared/ui.js sets
   html[data-input]) they draw no ring, so a mouse user never sees a focus box around a heading;
   once the user presses a key the standard ring above applies again. */
html:not([data-input="keyboard"]) :is(h1, h2, h3, main, [role="dialog"])[tabindex="-1"]:focus-visible {
    outline: none;
    box-shadow: none;
}

/* Respect a user's OS-level motion preference globally, once, instead of
   relying on every future transition/animation to check it individually. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ── Buttons ──────────────────────────────────────────────────────
   Hierarchy: primary (one obvious main action) → secondary (bordered,
   alternative action) → ghost/text (low-priority) → danger
   (destructive). */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 16px;
    font-size: var(--text-md);
    font-weight: var(--font-semibold);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
    line-height: var(--leading-tight);
}

/* An <a> styled as a button (WhatsApp / Email share links, "mailto:" actions) is a button first:
   never underlined, whatever container it sits in or whatever the browser does on hover/focus. */
a.btn, a.btn:hover, a.btn:focus-visible { text-decoration: none; }

/* Primary: dark neutral fill, not brand blue — see tokens.css'
   --action/--primary split (spec §1/§5: brand blue is an accent, not
   the default color of every interaction). */
.btn-primary { background: var(--action); color: #fff; }
.btn-primary:hover { background: var(--action-hover); }
.btn-primary:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; pointer-events: none; }

/* Secondary: same weight as primary, bordered instead of filled — for
   an alternative action that shouldn't compete visually with the one
   primary action on screen. */
.btn-secondary { background: var(--bg-card); color: var(--text); border-color: var(--border); }
.btn-secondary:hover { background: var(--bg-hover); border-color: var(--text-light); }
.btn-secondary:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; pointer-events: none; }

.btn-ghost { background: transparent; color: var(--text-secondary); }
.btn-ghost:hover { background: var(--bg-hover); color: var(--text); }

.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { background: var(--danger-hover); }

/* Low-priority destructive action (e.g. "Eliminar" in a list row) —
   ghost by default, only reads as danger on hover/focus. */
.btn-danger-ghost { background: transparent; color: var(--text-secondary); }
.btn-danger-ghost:hover, .btn-danger-ghost:focus-visible { background: var(--danger-bg); color: var(--danger-text); }

.btn-small { padding: 5px 10px; font-size: var(--text-sm); min-height: var(--target-min); }
/* Full-width primary action, icon on the left, anchored at the bottom of a
   mobile screen (client "Empezar", "Salir a correr" pattern) — opt-in so
   desktop contexts (modal footers, inline forms) aren't affected. */
.btn-block { display: flex; width: 100%; justify-content: center; padding-top: 13px; padding-bottom: 13px; }

.btn-icon {
    width: 32px; height: 32px;
    display: flex; align-items: center; justify-content: center;
    background: transparent; border: none; color: var(--text-secondary);
    font-size: 18px; cursor: pointer; border-radius: var(--radius-sm);
}
.btn-icon:hover { background: var(--bg-hover); }

/* ── Form fields ──────────────────────────────────────────────────── */
.field { margin-bottom: 14px; }
.field label, .field .field-label {
    display: block;
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    color: var(--text-secondary);
    margin-bottom: 6px;
}

.field input, .field select, .field textarea {
    width: 100%;
    padding: 10px 12px;
    background: var(--bg);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: var(--text-md);
    font-family: inherit;
}

.field input:focus, .field select:focus, .field textarea:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: var(--focus-ring);
}

.field input:disabled, .field select:disabled, .field textarea:disabled {
    opacity: var(--disabled-opacity);
    cursor: not-allowed;
}

.field-row { display: flex; gap: 12px; }
.field-row .field { flex: 1; }
.field-hint { margin-top: 6px; font-size: var(--text-sm); color: var(--text-secondary); }

/* Training baseline "días/semana" rows (shared/baseline-field.js) — one compact row per
   SELECTED discipline chip, appearing right below the chip grid. */
.baseline-freq-rows { display: flex; flex-direction: column; gap: 6px; }
.baseline-freq-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 6px 0;
}
.baseline-freq-label { font-size: var(--text-sm); color: var(--text); }
.baseline-freq-input {
    width: 56px;
    padding: 6px 8px;
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text);
    font-size: var(--text-sm);
    text-align: center;
}
.baseline-freq-input:focus { outline: none; border-color: var(--primary); }

/* A <select> styled to match .field input/textarea, for the many filter
   bars that need a dropdown outside a labelled .field (ownership/
   discipline filters, category filters). Previously every one of these
   carried its own inline style="..." with slightly different values at
   each call site — this is that pattern, named once. */
.select {
    padding: 8px 10px;
    border: var(--border-width) solid var(--border-control);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text);
    font-size: var(--text-base);
    font-family: inherit;
    cursor: pointer;
}
.select:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: var(--focus-ring);
}

/* A bare text <input> styled like .field input but without the .field
   wrapper/label/margin — for a list screen's own search box, which sits
   above the list rather than inside a form. */
.search-input {
    display: block;
    width: 100%;
    padding: 10px 14px;
    margin-bottom: var(--space-4);
    font-size: var(--text-md);
    border: var(--border-width) solid var(--border-control);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text);
    font-family: inherit;
    outline: none;
    transition: border-color var(--duration-base), box-shadow var(--duration-base);
}
.search-input:focus { border-color: var(--primary); box-shadow: var(--focus-ring); }

/* Row that holds a .search-input plus one or more .select filters above a
   list — handles the wrap-on-narrow-viewport case once instead of at
   every call site. */
.filter-bar {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 16px;
}
.filter-bar { align-items: center; }
.filter-bar .search-input { flex: 1; min-width: 200px; margin: 0; }
/* Quiet "30 de 988" count, pushed to the end of the row (Ejercicios). */
.filter-bar .filter-count { margin-left: auto; font-variant-numeric: tabular-nums; white-space: nowrap; }


/* Browser autofill (Chrome/Safari force a yellow/blue tinted surface via an
   internal UA style that ignores `background`) — override with a long
   transition delay trick so autofilled fields keep the same flat surface
   and text color as every other input instead of suddenly looking like a
   different, inconsistent component. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--text);
    -webkit-box-shadow: 0 0 0 1000px var(--bg) inset;
    box-shadow: 0 0 0 1000px var(--bg) inset;
    transition: background-color 9999s ease-in-out 0s;
}

/* ── Password field (auth forms) ──────────────────────────────────
   One password-field structure used everywhere a password is entered
   (trainer login/register, client login, invite registration):
   <div class="password-field"><input type="password">
   <button type="button" class="password-toggle" aria-label="Mostrar
   contraseña"></button></div>. The eye glyph is injected
   by password-toggle.js (inline SVG, never an emoji). Wiring (click → toggle type/label)
   lives in shared/password-toggle.js, delegated from one listener —
   no per-field onclick/id needed. */
.password-field { position: relative; }
.password-field input { padding-right: 44px; }

.password-toggle {
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--radius-sm);
    cursor: pointer;
    background: transparent;
    color: var(--text-secondary);
    font-size: 15px;
    line-height: 1;
}
.password-toggle:hover { background: var(--bg-hover); color: var(--text); }

/* ── Auth form shell (login/register) ─────────────────────────────
   One shared shell for every login/register screen — Trainer
   login/register and Client login/invite registration — so they read
   as one product instead of two designs. Combine with `.card` above
   for border/background/radius; this only adds auth-specific
   sizing/typography/spacing. Page-specific wrapper/centering layout
   (full-viewport grid vs a top-anchored mobile screen) stays local to
   each app's own styles.css. */
.auth-card { width: 100%; max-width: 400px; padding: var(--space-6); }
.auth-card h1 {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--display-sm);
    font-weight: var(--font-bold);
    letter-spacing: var(--tracking-tight);
    color: var(--text);
}
.auth-card .subtitle { margin: 6px 0 var(--space-5); color: var(--text-secondary); font-size: var(--text-base); }
.auth-card .btn { width: 100%; justify-content: center; }
.auth-card .error { min-height: 18px; color: var(--danger); font-size: var(--text-base); margin: var(--space-3) 0 0; }
/* Longhand (not `padding` shorthand) so this never clobbers
   `.password-field input`'s reserved right padding for the eye button. */
.auth-card .field input { padding-top: 10px; padding-bottom: 10px; padding-left: 12px; font-size: var(--text-md); }

.auth-hint { margin: -4px 0 var(--space-4); color: var(--text-secondary); font-size: var(--text-sm); }

.auth-links {
    margin-top: var(--space-5);
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}
.auth-links a {
    color: var(--text-secondary);
    text-decoration: none;
    font-size: var(--text-base);
    padding-top: 10px;
    margin-top: 2px;
    border-top: 1px solid var(--border);
    width: 100%;
}
.auth-links a:hover { color: var(--text); }

/* ── Badges / status pills ────────────────────────────────────────
   Generic pill for status language (spec §30): one semantic modifier,
   never stacked with decorative extras.

   `.tag` is the one actually consumed anywhere in the app today
   (exercise/session/routine/user status pills — `tag-public`,
   `-pending`, `-rejected`, `-global`, `-private`). `.badge`/`.badge-*`
   and `.tag-active`/`-inactive`/`-inactive-danger` are aliases with
   zero call sites in any of the four apps (verified 2026-09, Phase 1
   of the trainer UI/UX pass) — kept, not deleted, in case an app ever
   wants Bootstrap-ish `.badge` naming instead of `.tag`, or an
   active/inactive status distinct from the pending/rejected set above.
   Both names read from the same tokens, so whichever is used stays
   visually identical — never duplicate a hex value to add a variant. */
.badge, .tag {
    display: inline-block;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    font-size: var(--text-xs);
    font-weight: var(--font-semibold);
    background: var(--neutral-bg);
    color: var(--neutral-text);
}

.badge-success, .tag-public, .tag-active   { background: var(--success-bg); color: var(--success-text); }
.badge-warning, .tag-pending               { background: var(--warning-bg); color: var(--warning-text); }
.badge-danger,  .tag-rejected, .tag-inactive-danger { background: var(--danger-bg);  color: var(--danger-text); }
.badge-info,    .tag-global                { background: var(--info-bg);    color: var(--info-text); }
.badge-neutral, .tag-private, .tag-inactive { background: var(--neutral-bg); color: var(--neutral-text); }

/* ── Page header ──────────────────────────────────────────────────
   Title (+ optional inline count) and a single primary action, used by
   every top-level screen. Promoted from the trainer app's .view-header
   (2026-09, Phase 1 of the trainer UI/UX pass) — same definition, now
   available to a second app without copying it; .view-header itself no
   longer exists. Client Detail (Phase 6) now also builds on this — its
   side-by-side back+title row stays local as .user-layout-topbar since
   it has no context line to stack under, unlike session/routine detail. */
.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-5);
    margin-bottom: var(--space-5);
}
/* DS.1 — a header that stacks BackLink / eyebrow / h1 / meta is taller
   than its actions, so the actions anchor to the top (level with the
   BackLink) instead of floating mid-block. Markup:
     <div class="page-header page-header-stacked">
       <div>
         <button class="back-link">‹ Sesiones</button>
         <span class="eyebrow page-header-eyebrow">Plantilla</span>
         <h1>Torso — Fuerza A</h1>
         <p class="page-header-meta">Gimnasio · 3 bloques · 6 ejercicios</p>
       </div>
       <div class="page-header-actions">…one .btn-primary + "···"…</div>
     </div>
   Context ("Plantilla reutilizable", "Sesión de este cliente") is an
   eyebrow or a meta line — never a pill; pills are for ownership only. */
.page-header-stacked { align-items: flex-start; }
.page-header-eyebrow { margin-bottom: 6px; }
/* Quick links riding on the eyebrow ("Planificación · Perfil" next to a
   client session's context) keep normal case and the text-action style. */
/* Quick links riding on the meta line ("Planificación · Perfil" next to a
   client session's scope fact) — quiet text actions, first one set off
   from the meta text with a little breathing room. */
.page-header-meta .btn-link { margin-left: var(--space-3); }
.page-header-meta .btn-link + .btn-link { margin-left: var(--space-2); }
.page-header-meta {
    margin: 6px 0 0;
    font-size: var(--text-md);
    color: var(--text-secondary);
}
/* BackLink (D3 .d-back): plain text, no button chrome — it is orientation,
   not an action. */
.back-link {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-bottom: 12px;
    padding: 0;
    background: none;
    border: none;
    font: var(--font-medium) var(--text-base) var(--font-sans);
    color: var(--text-secondary);
    cursor: pointer;
    min-height: var(--target-min);
}
.back-link:hover { color: var(--text); }
.back-link .icon { width: 16px; height: 16px; }
/* Phase 1 of TRAINER_UI_UX_MIGRATION_PLAN.md — bumped from --text-xl (20px)
   to --display-sm (28px), the ceiling tokens.css already reserves for this
   exact use ("the trainer backoffice reaches for --display-sm at most" —
   still a dense tool, not a consumer surface, just no longer capped at the
   same size as a section title). Real hierarchy was one of the audit's
   named problems ("h1 demasiado pequeño") across all 14 top-level views
   that use this class. */
.page-header h1, .page-header h2 { font-family: var(--font-display); font-size: var(--display-sm); font-weight: var(--font-semibold); letter-spacing: var(--tracking-tight); }
.page-header-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
/* Inline-editable page title (Phase 6, Routines) — a click swaps the h1
   for a text input with Guardar/Cancelar, never a permanent name field on
   the page. `.inline-title-editable` is the affordance hint (cursor +
   hover), applied only when the title is actually editable (never on a
   read-only Global Training Catalog item). */
.inline-title-editable { cursor: pointer; border-radius: var(--radius-sm); }
.inline-title-editable:hover { color: var(--text-secondary); }
.inline-title-form { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.inline-title-input {
    font-family: var(--font-display);
    font-size: var(--display-sm);
    font-weight: var(--font-semibold);
    letter-spacing: var(--tracking-tight);
    color: var(--text);
    border: none;
    border-bottom: 2px solid var(--border-strong);
    background: none;
    padding: 0 0 2px;
    min-width: 220px;
}
.inline-title-input:focus { outline: none; border-bottom-color: var(--action); }
.inline-title-actions { display: flex; align-items: center; gap: var(--space-2); }
/* Mobile: back to the pre-Phase-1 size — 28px reads fine as a page anchor
   on desktop but crowds a dense list's own title row on a phone (same
   reduction D3/Planning already applies to its own oversized headings
   under 768px). No new token: this is exactly --text-xl, the value every
   page-header h1 used before Phase 1 bumped it. */
@media (max-width: 640px) {
    .page-header h1, .page-header h2 { font-size: var(--text-xl); }
    /* D3 .d-header on a phone: the title owns the full width, actions
       drop below it (a wrapped 3-line title next to a button was the
       alternative). */
    .page-header-stacked { flex-direction: column; align-items: stretch; gap: var(--space-3); }
}

/* ── Tabs ─────────────────────────────────────────────────────────
   One visual language for "a row of tabs with an active state",
   reused by every tab-like control in the app (catalogue filters,
   client-detail tabs, exercise-detail tabs) even where each keeps its
   own class name for layout reasons. Selected tab: obvious but subtle
   — text weight + a short centered underline, no heavy container. */
.tabs-underline {
    display: flex;
    border-bottom: 1px solid var(--border);
    gap: 4px;
    /* A strip wider than its container scrolls instead of cutting tabs, with no visible bar; the
       cut edge fades (SR_UI.tabsOverflow toggles the has-overflow-* classes). */
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
}
.tabs-underline::-webkit-scrollbar { display: none; }
.tabs-underline.has-overflow-end { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent); }
.tabs-underline.has-overflow-start { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(to left, #000 calc(100% - 40px), transparent); }
.tabs-underline.has-overflow-start.has-overflow-end { -webkit-mask-image: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent); }
.tabs-underline .tab {
    position: relative;
    padding: 10px 16px;
    background: transparent;
    border: none;
    color: var(--text-secondary);
    font-size: var(--text-md);
    font-weight: var(--font-medium);
    cursor: pointer;
    transition: color var(--duration-fast);
}
.tabs-underline .tab { white-space: nowrap; flex: none; }
/* Keyboard focus: inset ring (an outline outside the tab would be clipped by the scroll container). */
.tabs-underline .tab:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; border-radius: var(--radius-xs); }
/* "Rutinas semanales" is one tab: on a phone the " semanales" suffix drops so four tabs fit (#42). */
@media (max-width: 640px) { .tabs-underline .tab { padding: 10px 12px; } .tab-suffix { display: none; } }
.tabs-underline .tab:hover { color: var(--text); }
/* Ink, not brand blue (2026-09 "Menos Azul" pass): a coloured fill/label
   for "this one is selected" is the pattern that dates an interface —
   --primary now stays reserved for branding/focus/marker use. The
   indicator is a short 20px underline centered under the label (2026-09
   Oysho-style pass) rather than a full-width border, so it reads as a
   mark under the word instead of a bar under the whole tab's hit area. */
.tabs-underline .tab::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: -1px;
    width: 0;
    height: 2px;
    background: var(--action);
    transform: translateX(-50%);
    transition: width var(--duration-fast) var(--ease-standard);
}
.tabs-underline .tab.active { color: var(--text); font-weight: var(--font-semibold); }
.tabs-underline .tab.active::after { width: 20px; }

/* ── Card ─────────────────────────────────────────────────────────
   Use only when a section is an independent object/interaction (spec
   §6) — not as a default wrapper for every block of content. */
.card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-4);
}

/* ── List ─────────────────────────────────────────────────────────
   Flat row list used by every catalogue/collection screen (clients,
   exercises, sessions, routines). `.item-*` is the canonical inner
   naming (used by users.js, dashboard.js and others); `.list-item-*`
   is an equivalent convention used only by the two library renderers
   this pass didn't touch — app.js's session list and
   routine-plans.js's routine list, which Phase 4 of the trainer
   UI/UX pass unifies into one `LibraryRow` component. Aliased here to
   the same declarations so both render identically until that lands,
   rather than silently diverging in the meantime. */
.list {
    display: flex;
    flex-direction: column;
    gap: 1px;
    background: var(--border);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    overflow: hidden;
}
/* A list whose only child is an empty/loading state is not a list: the
   --border fill above exists to draw the hairlines BETWEEN rows, so with
   no rows it showed through as one solid beige box around "Sin
   programas…". An empty state sits on the page, never on its own fill —
   drop the container chrome entirely (`:has()`), and paint the state
   itself page-white as the fallback for engines without `:has()`, where
   the hairline frame then still shows but the beige block never does. */
.list > .empty-state:only-child,
.list > .empty-list:only-child,
.list > .empty-hint:only-child { background: var(--bg); }
.list:has(> .empty-state:only-child),
.list:has(> .empty-list:only-child),
.list:has(> .empty-hint:only-child) { background: transparent; border-color: transparent; }

.list-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    background: var(--bg-card);
    cursor: pointer;
    transition: background var(--duration-fast);
}
.empty-state.empty-state-page { padding-top: 96px; }  /* a list page that is empty: sit a third of the way down, not stuck under the header */
.list-group-label { background: var(--bg); padding: var(--space-4) var(--space-4) var(--space-2); }
.list-item:hover { background: var(--surface-day-hover); }  /* one step past paper: #43 */

/* Openable row with trailing actions (Sessions, Routines, Programs, Exercises,
   Imports — #33). The row can't be one <button> (it holds the "···" menu), so
   the TITLE is the real <button class="list-item-open"> and its ::after
   stretches over the whole row: a click anywhere opens it, Tab/Enter/Space
   work natively, and the look is unchanged. The row itself shows the focus
   ring (`:has(:focus-visible)`); trailing actions sit above the stretch. */
.list-item-open {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}
.list-item-open::after { content: ''; position: absolute; inset: 0; }
.list-item-open:focus-visible { outline: none; }
.list-item:has(.list-item-open:focus-visible) { outline: 2px solid var(--primary); outline-offset: -2px; }
.list-item:has(.list-item-open) .item-actions,
.list-item:has(.list-item-open) .list-item-actions { position: relative; z-index: 1; }

.item-icon {
    width: 40px; height: 40px;
    background: var(--bg);
    border-radius: var(--radius-sm);
    display: flex; align-items: center; justify-content: center;
    font-size: 18px;
    flex-shrink: 0;
}

/* ListRow (DS.1) — the one shape for repeated content: [media] · title +
   meta · trailing (status / "···" / chevron). Title 16/600, Meta 13px
   secondary (plan §6.8 — was 14/600 + 12px, the values every list
   already-in-flight was written against; bumped once here for all of
   them rather than per screen). Semantics decide the CONTAINER, not the
   row: `.list` (boxed) when the list is the page's own object — a roster,
   a catalogue; `.hairline-rows` (flat, below) when the rows live inside a
   Section or a Surface that already frames them — a session's exercises,
   a day's entries. */
.item-info, .list-item-main { flex: 1; min-width: 0; }
.item-title, .list-item-name, .list-item-title { font-size: var(--text-lg); font-weight: var(--font-medium); color: var(--text); line-height: var(--leading-tight); }
.item-meta, .list-item-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    font-size: var(--text-base);
    color: var(--text-secondary);
    margin-top: 2px;
}
.item-actions, .list-item-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
/* Media slot — a 48px square (photo or placeholder glyph), the size D3
   gives an exercise in a session. Photos are content: --radius-xs at most,
   never a circle (that is .avatar, a person). */
.item-media {
    width: 48px;
    height: 48px;
    flex-shrink: 0;
    border-radius: var(--radius-xs);
    /* --surface-media, not --surface (DS.2A) — a thumbnail is neutral
       chrome around a photo, not a content surface; see the token's own
       comment in tokens.css. */
    background: var(--surface-media) center/cover no-repeat;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary);
    overflow: hidden;
}
.item-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.item-media .icon { width: 20px; height: 20px; opacity: 0.6; }
/* On a warm surface the placeholder square is white (a photo frame on
   paper), otherwise it would vanish into the surface it sits on. */
.surface .item-media, .surface-selected .item-media, .surface-quiet .item-media,
.hairline-rows > .list-item:hover .item-media, .hairline-rows > .list-item.is-selected .item-media { background-color: var(--bg-card); }

/* ── Empty states (spec §29) ──────────────────────────────────────
   Small icon, short message, optional action — never a large
   decorative illustration. `.empty-state` is the canonical shape;
   `.empty-list`/`.empty-hint` are lighter-weight equivalents used
   inline (e.g. inside a panel) where a full icon block would be too
   heavy — normalized to the same muted typography so neither looks
   unstyled. */
.empty-state {
    text-align: center;
    padding: var(--space-6) var(--space-4);
    color: var(--text-secondary);
}
/* 2026-09 visual maturity pass: was 40px/opacity 0.8 — read as a large
   decorative sticker rather than the "small icon" this section's own spec
   already called for. Desaturated + dimmed further so it reads as a quiet
   placeholder glyph, consistent with .exercise-thumb-emoji. */
.empty-state span { font-size: 24px; display: block; margin-bottom: var(--space-2); opacity: 0.5; filter: grayscale(1); }
/* Icon variant of the line above (2026-09, Phase 2 of the trainer UI/UX
   pass) — an empty/error state now renders a real <svg class="icon">
   (shared/icons.js) instead of an emoji in a bare <span>; sized up from
   the default 20px and dimmed the same way the emoji version was. */
.empty-state .icon { width: 24px; height: 24px; display: block; margin: 0 auto var(--space-2); opacity: 0.5; }
.empty-state strong { display: block; font-size: var(--text-md); font-weight: var(--font-semibold); color: var(--text-secondary); margin-bottom: 4px; }
.empty-state p { font-size: var(--text-base); margin: 0 auto; max-width: 32em; }
.empty-state .btn { margin-top: var(--space-3); }

.empty-list, .empty-hint {
    padding: var(--space-4);
    text-align: center;
    color: var(--text-secondary);
    font-size: var(--text-base);
}

/* Muted text — promoted here (Phase 4 of the trainer UI/UX pass) once a second real screen
   (Clients/invite dialog) needed the exact same one-property rule Import already had scoped to
   itself (.import-muted, styles.css) — see that class's own comment for why it stayed local at
   the time. Genuinely generic (colour only, no layout), so it belongs in the shared layer now
   rather than being redefined a third time. */
.sr-muted { color: var(--text-secondary); }

/* Segmented control — a row of mutually-exclusive filter/view buttons on one pill-shaped track
   (Phase 4 of the trainer UI/UX pass, Clients' Todos/Atención/Sin rutina/Inactivos). Promoted
   here on its second real consumer: Planning's own 1-semana/4-semanas toggle
   (.planning-range-toggle, dashboard/trainers/backoffice-routines/styles.css) is the same idiom
   but stays on its own class — it's already shipped and tested against Planning's D3 surface
   tokens, and consolidating it into this one isn't worth the regression risk for a visual-only
   rename. New call sites should use this one instead of inventing a third variant. */
.segmented-control {
    display: inline-flex;
    gap: 2px;
    /* Track = --surface-quiet, not --surface (DS.2A) — a segmented track
       is chrome the active pill sits on, not a content surface. */
    background: var(--surface-quiet);
    border-radius: var(--radius-sm);
    padding: 2px;
}
.segmented-control button {
    font-size: var(--text-base);
    font-weight: var(--font-medium);
    padding: 5px 12px;
    min-height: var(--target-min);
    border: none;
    border-radius: var(--radius-xs);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
}
.segmented-control button:focus-visible { outline-offset: 0; }
.segmented-control button.active { background: var(--action); color: var(--text-on-inverse); font-weight: var(--font-semibold); }
.segmented-control button:not(.active):hover { color: var(--text); }

/* ── Skeleton loading rows ────────────────────────────────────────
   A handful of lists (dashboard attention/activity, the client roster,
   Sessions/Routines/Programs/Exercises) showed either a generic
   "Cargando..." line or nothing at all while the first fetch was in
   flight. Use via SR_SKELETON_ROWS(n) (app.js), wrapped in
   SR_UI.loading.run (shared/ui.js) so it only actually appears once a
   load is genuinely slow (delay) and never for less time than it takes
   to register (minVisible) — see DESIGN_SYSTEM.md's Loading/Refreshing/
   Empty/Error pattern.
   A quiet surface pulse, not a bright shimmer sweep — a sweeping
   highlight reads as "app chrome" more than the rest of this editorial,
   low-motion system; a slow opacity breathe fits it better and is
   trivially disabled for reduced motion. */
.list-sentinel { min-height: 1px; margin-top: var(--space-3); }
.list-sentinel .meta { text-align: center; margin: var(--space-2) 0; }
.skeleton-list { display: flex; flex-direction: column; gap: var(--space-2); }
.skeleton-row {
    height: 52px;
    border-radius: var(--radius-md);
    background: var(--surface-quiet);
    animation: skeleton-pulse 1.8s ease-in-out infinite;
}
@keyframes skeleton-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
}
@media (prefers-reduced-motion: reduce) {
    .skeleton-row { animation: none; opacity: 0.8; }
}

/* ── Toast v2 (Phase 0, TRAINER_UI_UX_MIGRATION_PLAN.md §7/§13) ──────
   Stacking host created lazily by shared/ui.js (SR_UI.toast) — replaces
   the old single `#toast` element (spec §31) both apps used to hardcode
   into index.html. Bottom-center column, newest at the bottom, each
   toast closable and pausable on hover; aria-live lives on the host so
   screen readers announce new toasts without re-reading the whole list. */
.sr-toast-host {
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    width: max-content;
    max-width: calc(100vw - 32px);
}
.sr-toast {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    background: var(--text);
    color: #fff;
    padding: 10px 12px 10px 16px;
    border-radius: var(--radius-sm);
    font-size: var(--text-md);
    box-shadow: var(--shadow-md);
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 0.2s, transform 0.2s;
}
.sr-toast.active { opacity: 1; transform: translateY(0); }
.sr-toast-error { background: var(--danger); }
.sr-toast-success { background: var(--success); }
.sr-toast-warning { background: var(--warning); }
.sr-toast-message { flex: 1; min-width: 0; }
.sr-toast-action {
    flex-shrink: 0;
    background: none;
    border: none;
    color: inherit;
    font-weight: var(--font-semibold);
    text-decoration: underline;
    cursor: pointer;
    padding: 0;
}
.sr-toast-close {
    flex-shrink: 0;
    display: flex;
    background: none;
    border: none;
    color: inherit;
    opacity: 0.8;
    cursor: pointer;
    padding: 2px;
}
.sr-toast-close:hover { opacity: 1; }
@media (max-width: 640px) {
    .sr-toast-host { left: 16px; right: 16px; transform: none; width: auto; max-width: none; }
}

/* ── Dropdown menu ────────────────────────────────────────────────
   Every "···" overflow menu and secondary action menu in the app (spec:
   list rows never carry standalone action buttons). Moved here from the
   trainer backoffice's own stylesheet (2026-09, Phase 1 of the trainer
   UI/UX pass) — the class names and behaviour are unchanged, so its ~24
   existing call sites needed no edits. Renders in normal document flow by
   default (.dropdown-wrapper is a relatively-positioned inline flex
   container, .dropdown-menu an absolutely-positioned panel under it); a
   caller that needs to escape an overflow-clipped ancestor instead
   reparents the menu to document.body and adds .dropdown-portal, which
   resets the two properties that positioning relied on (trainer
   app.js: openPortalDropdown/closePortalDropdown). Toggled open with
   .is-open, not the .active convention used elsewhere in this file,
   since it predates that convention and ~24 call sites already depend on
   it. */
.dropdown-wrapper {
    position: relative;
    display: inline-flex;
}
.dropdown-menu {
    display: none;
    position: absolute;
    top: 100%;
    right: 0;
    z-index: var(--z-dropdown);
    margin-top: 4px;
    min-width: 180px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    padding: 4px;
    flex-direction: column;
}
.dropdown-menu.is-open {
    display: flex;
}
.dropdown-menu.dropdown-portal {
    margin-top: 0 !important;
    right: auto !important;
}
.dropdown-menu-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: 8px 10px;
    border: none;
    background: transparent;
    border-radius: var(--radius-sm);
    font-size: var(--text-base);
    color: var(--text);
    cursor: pointer;
}
.dropdown-menu-item:hover {
    background: var(--bg-hover);
}

/* ── Status label (Phase 2, shared/status-label.js) — glyph + word, never
   color alone; color reserved for the two states that need it. ────── */
.sr-status { display: inline-flex; align-items: center; gap: 6px; font-weight: var(--font-semibold); color: var(--text); }
.sr-status-glyph { line-height: 1; }
.sr-status-in_progress { color: var(--status-progress); }
.sr-status-completed { color: var(--status-done); }
.sr-status-skipped { color: var(--status-skipped); }
/* Derived "No realizada": quiet — word + dotted glyph carry it, no alarm colour (G9). */
.sr-status-not_done { color: var(--text-secondary); }

/* ── Modal ────────────────────────────────────────────────────────── */
.modal {
    display: none;
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    z-index: var(--z-modal);
    align-items: center;
    justify-content: center;
}
.modal.active { display: flex; }

.modal-overlay {
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0, 0, 0, 0.4);
}

.modal-panel {
    position: relative;
    background: var(--bg-card);
    border-radius: var(--radius-lg);
    border: 1px solid var(--border);
    width: 480px;
    max-height: 85vh;
    overflow-y: auto;
    padding: var(--space-5);
    box-shadow: var(--shadow-md);
}
.modal-panel.modal-small { width: 380px; }
.modal-panel.modal-large { width: 560px; }
.modal-panel-wide { max-width: 640px; }

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-4);
}
.modal-header h2, .modal-header h3 { font-size: var(--text-lg); font-weight: var(--font-semibold); }

.modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    margin-top: var(--space-4);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
}
/* Only dynamically-built dialogs (shared/ui.js, SR_UI.confirm/alert/prompt/
   dialog) use `.modal-body` — the existing static modals in index.html put
   their content directly after `.modal-header` and stay exactly as they
   are; this just gives paragraph spacing when a dialog body is more than
   one element. */
.modal-body > * + * { margin-top: var(--space-3); }

/* ── Drawer (Phase 0) — "consult without losing context": a detail view
   opened from a picker/list, or filters on a narrow viewport (plan §6.6).
   Slides in from the right; full-screen under 768px so it never becomes a
   cramped sliver on a phone. Built by shared/ui.js (SR_UI.drawer). ──── */
.sr-drawer-root { position: fixed; inset: 0; z-index: var(--z-modal); }
.sr-drawer-veil {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    opacity: 0;
    transition: opacity 0.2s;
}
.sr-drawer-root.active .sr-drawer-veil { opacity: 1; }
.sr-drawer {
    position: absolute;
    top: 0; right: 0; bottom: 0;
    width: 480px;
    max-width: 100vw;
    background: var(--bg-card);
    box-shadow: var(--shadow-md);
    display: flex;
    flex-direction: column;
    transform: translateX(100%);
    transition: transform 0.25s ease;
}
.sr-drawer-root.active .sr-drawer { transform: translateX(0); }
.sr-drawer-sm { width: 360px; }
.sr-drawer-lg { width: 560px; }
.sr-drawer-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}
.sr-drawer-header h3 { font-size: var(--text-lg); font-weight: var(--font-semibold); }
.sr-drawer-body { flex: 1; overflow-y: auto; padding: var(--space-5); }
@media (max-width: 767px) {
    .sr-drawer, .sr-drawer-sm, .sr-drawer-lg { width: 100vw; }
    .sr-drawer { transform: translateY(100%); }
    .sr-drawer-root.active .sr-drawer { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
    .sr-drawer, .sr-drawer-veil { transition: none; }
}

/* ── Exercise Detail (spec: shared/exercise-detail.js) ──────────────
   One visual system for "what is this exercise", reused as-is by the
   trainer preview modal and the client session overlay — see that file's
   header for the product rule. Self-contained (no reliance on either
   host's ambient page padding) so it looks the same in both containers. */
.exdetail-name { margin: 0 0 8px; font-size: var(--text-lg); font-weight: var(--font-semibold); color: var(--text); }
.exdetail-name-secondary { font-size: var(--text-sm); font-weight: var(--font-regular); color: var(--text-secondary); }
/* Phase 7: plain text joined by " · " (category/difficulty, muscles,
   equipment), never a row of pills — same rule as every list row's meta
   line since the color/typography closing pass (§23). */
.exdetail-meta { margin: 0 0 10px; font-size: var(--text-base); color: var(--text-secondary); }
.exdetail-media { margin-bottom: 12px; }
.exdetail-media img { width: 100%; border-radius: var(--radius-md); display: block; }
.exdetail-video-thumb {
    position: relative;
    cursor: pointer;
    background: #000;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: var(--radius-md);
}
.exdetail-video-thumb img { width: 100%; height: 100%; object-fit: cover; opacity: 0.85; }
.exdetail-play-icon {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28px;
    color: #fff;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.exdetail-video-nothumb { margin-bottom: 0; }

/* Read-only media THUMBNAILS — shared/exercise-detail.js's mediaBlock/galleryThumbHtml. Client
   and trainer both only ever get this row, never upload/delete/set-primary controls (those stay
   in the trainer's own admin media manager, a completely separate component). No large image/
   video, no arrows, no "selected" state here by design (spec: the detail shows thumbnails only —
   the large viewer only exists inside the lightbox below, opened by clicking a thumbnail). */
.exdetail-gallery-thumbs {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    /* Wraps on wide screens (room to spare) but scrolls on narrow ones rather than shrinking
       tap targets below a usable size — spec: "scroll horizontal o wrap responsive". */
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
.exdetail-gallery-thumb {
    position: relative;
    flex-shrink: 0;
    width: 56px;
    height: 56px;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--bg-hover);
    cursor: pointer;
}
.exdetail-gallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.exdetail-gallery-thumb-badge {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #fff;
    background: rgba(0, 0, 0, 0.25);
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}
/* Video with no thumbnailUrl yet — never <img src="video.m3u8">, just a neutral generic-video
   icon (the play badge above still overlays on top of it, same as it would over a real frame). */
.exdetail-gallery-thumb-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font-size: 20px;
    background: var(--bg-hover);
}

/* Large viewer / lightbox (spec §4/§5) — the ONLY place a big image/video-thumb + arrows ever
   appear; a body-level singleton created once by ensureLightboxEl() and reused across renders.
   Fixed/full-viewport so it overlays correctly regardless of which host container (trainer modal,
   client overlay) the thumbnails themselves are sitting inside. */
.exdetail-lightbox {
    display: none;
    position: fixed;
    inset: 0;
    z-index: var(--z-lightbox);
    align-items: center;
    justify-content: center;
}
.exdetail-lightbox.active { display: flex; }
.exdetail-lightbox-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.82);
}
.exdetail-lightbox-main {
    position: relative;
    max-width: min(90vw, 900px);
    max-height: 85vh;
    display: flex;
    align-items: center;
    justify-content: center;
}
.exdetail-lightbox-main .exdetail-gallery-image {
    max-width: min(90vw, 900px);
    max-height: 85vh;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--radius-md);
    display: block;
}
.exdetail-lightbox-main .exdetail-video-thumb {
    width: min(90vw, 900px);
    max-height: 85vh;
}
.exdetail-lightbox-main .exdetail-yt-frame {
    width: min(90vw, 900px);
    aspect-ratio: 16 / 9;
    border: 0;
    border-radius: var(--radius-md);
    background: #000;
}
/* "YouTube" provenance badge over a video thumbnail (gallery, lightbox, media manager). */
.exdetail-yt-badge {
    position: absolute;
    left: 6px;
    bottom: 6px;
    padding: 1px 6px;
    border-radius: var(--radius-sm);
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.02em;
    line-height: 1.5;
    pointer-events: none;
}
.exdetail-yt-badge--sm { left: 2px; bottom: 2px; padding: 0 4px; font-size: 9px; }
.exdetail-lightbox-close {
    position: absolute;
    top: 16px;
    right: 16px;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-full);
    border: none;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    font-size: 18px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: var(--z-base);
}
.exdetail-lightbox-close:hover { background: rgba(255, 255, 255, 0.22); }
.exdetail-gallery-arrow {
    position: relative;
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-full);
    border: none;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: var(--z-base);
}
.exdetail-gallery-arrow:hover { background: rgba(255, 255, 255, 0.22); }
.exdetail-lightbox-prev { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); }
.exdetail-lightbox-next { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); }

@media (max-width: 640px) {
    .exdetail-lightbox-main,
    .exdetail-lightbox-main .exdetail-gallery-image,
    .exdetail-lightbox-main .exdetail-video-thumb {
        max-width: 94vw;
    }
    .exdetail-lightbox-prev { left: 6px; }
    .exdetail-lightbox-next { right: 6px; }
}
.exdetail-section { padding: 10px 0; border-top: 1px solid var(--border); }
.exdetail-section:first-of-type { border-top: none; padding-top: 0; }
/* Same eyebrow grammar as every other Section heading in the system
   (uppercase + tracking carries the emphasis, not weight — DS.3) — was
   plain-case, predating that convention. */
.exdetail-section-title { font-size: var(--text-xs); font-weight: var(--font-semibold); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--text-secondary); margin-bottom: 4px; }
.exdetail-section p { margin: 0; font-size: var(--text-base); line-height: var(--leading-normal); color: var(--text); white-space: pre-line; }
.exdetail-subgroup { margin-top: 6px; }
.exdetail-subgroup:first-of-type { margin-top: 0; }
.exdetail-subgroup-title { font-size: var(--text-xs); color: var(--text-secondary); margin-bottom: 4px; }
.exdetail-variants { display: flex; flex-direction: column; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; margin-top: 4px; }
.exdetail-variant { padding: 10px 12px; background: var(--bg-card); }
/* A clickable row is a real <button> now (Phase 8 of the trainer UI/UX
   pass — it used to be a div with role="button" tabindex="0" and no
   keyboard activation) — reset the native button chrome so it still reads
   as a plain row, same pattern as .list-item-button. */
.exdetail-variant[onclick] {
    display: block;
    width: 100%;
    border: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    transition: background var(--duration-fast);
}
.exdetail-variant[onclick]:hover { background: var(--bg-hover); }
.exdetail-variant[onclick]:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.exdetail-variant-name { font-size: var(--text-base); font-weight: var(--font-medium); color: var(--text); }
.exdetail-variant-meta { font-size: var(--text-sm); color: var(--text-secondary); margin-top: 2px; }
.exdetail-variant-reason { font-style: italic; color: var(--text-secondary); }

/* Client-only performed-exercise status/action (Fase 6) — status reads as informational (same
   tint as .badge-info), the reset stays a plain ghost button so it never competes visually with
   whatever primary action the host screen already has. */
.exdetail-performed-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 8px;
    padding: 10px 12px;
    margin-bottom: 12px;
    background: var(--info-bg);
    color: var(--info-text);
    border-radius: var(--radius-md);
    font-size: var(--text-sm);
}
.exdetail-performed-cta { margin-bottom: 12px; }

/* ── Avatar ──────────────────────────────────────────────────────────
   One avatar atom for both apps (Trainer Profile MVP, 2026-09). Image
   when there is one, initials on a neutral fill otherwise — never a
   brand-colour fill (tokens.css: avatars are explicitly not a place for
   --primary). Three sizes; the default is the 36px list size the trainer
   app's former .user-avatar-circle used (that class is kept as an alias
   in the trainer stylesheet so its ~10 call sites needed no edits).
   Markup: <span class="avatar avatar-lg"><img src="…" alt=""></span> or
   <span class="avatar">CR</span>. The <img> is `object-fit: cover` so a
   non-square legacy image (OAuth) is centre-cropped, not squashed. */
.avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    /* Avatar placeholder = --surface-media + --text initials (T2b owner
       decision, #16/#38): ink is reserved for the primary action, so a
       person's disc is the neutral media frame, not an ink block. The inset
       hairline (--border-strong) keeps it distinguishable on --surface-day rows (#f3f3f1 vs
       #e9e9e6 alone is ~1.1:1). Initials: --text on --surface-media ≈ 13:1.
       Never affects a real photo (the <img> below covers this fill). */
    background: var(--surface-media);
    color: var(--text);
    box-shadow: inset 0 0 0 1px var(--border-strong);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: var(--font-semibold);
    font-size: var(--text-md);
    line-height: 1;
    flex-shrink: 0;
    overflow: hidden;
    user-select: none;
}
.avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.avatar-sm { width: 24px; height: 24px; font-size: var(--text-xs); }
.avatar-md { width: 48px; height: 48px; font-size: var(--text-lg); }
.avatar-lg { width: 96px; height: 96px; font-size: var(--text-2xl, 28px); }

/* ── Chip group (multi-select) ───────────────────────────────────────
   Toggle chips backed by real checkboxes — keyboard-accessible with no
   JS (the input is visually hidden, the label is the chip; focus ring
   follows :focus-visible on the input). Selected = ink border + ink
   text, never a brand-colour wash (tokens.css "Menos Azul"). Consolidates
   the client app's ad-hoc sports grid (`chip-selected`) and the trainer
   profile's disciplines picker into one primitive.
   Markup: <div class="chip-group"><label class="chip-option">
             <input type="checkbox" value="gym"><span>Gimnasio</span>
           </label>…</div> */
.chip-group {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
.chip-option {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 12px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-card);
    color: var(--text-secondary);
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    cursor: pointer;
    transition: border-color var(--duration-fast), color var(--duration-fast), background var(--duration-fast);
}
.chip-option:hover { border-color: var(--text-light); color: var(--text); }
.chip-option input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    pointer-events: none;
}
.chip-option:has(input:checked) {
    border-color: var(--action);
    color: var(--text);
    background: var(--bg-hover);
    font-weight: var(--font-semibold);
}
.chip-option:has(input:focus-visible) {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
.chip-option:has(input:disabled) {
    opacity: var(--disabled-opacity);
    cursor: not-allowed;
}
/* Touch-friendly size variant (Client Onboarding/Profile) — 44px minimum touch
   target, mobile being a first-class viewport there. The trainer app's denser
   desktop chip pickers stay on the default size above. */
.chip-option-lg {
    padding: 12px 16px;
    font-size: var(--text-base);
    min-height: 44px;
}
/* Optional accent-dot marker (2026-09 Oysho-style pass) — for a *filter/
   category* chip group (one active choice, e.g. catalogue discipline
   filters) rather than a plain multi-select. Opt-in: add
   <span class="chip-dot"></span> as the first child inside a
   .chip-option's label content; it stays invisible until that option's
   checkbox is checked. This is the brand accent's one legitimate use
   inside a chip — a marker shape, never the chip's fill or border. */
.chip-option .chip-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--primary);
    flex-shrink: 0;
    opacity: 0;
    transform: scale(0.4);
    transition: opacity var(--duration-fast), transform var(--duration-fast);
}
.chip-option:has(input:checked) .chip-dot { opacity: 1; transform: scale(1); }
/* Inline validation/error line under a .field — shared so both apps
   show field-level errors the same way (Trainer Profile MVP). */
.field-error { margin-top: 6px; font-size: var(--text-sm); color: var(--danger); }
/* Small "opcional" label suffix — promoted here from the trainer's own styles.css
   (Client Onboarding, Phase 4) since the client's onboarding form needs it too. */
.field-optional {
    font-weight: var(--font-regular);
    color: var(--text-secondary);
    margin-left: 4px;
}
.field.has-error input, .field.has-error textarea, .field.has-error select { border-color: var(--danger); }

/* ── Display type + eyebrow (2026-09 Oysho-style pass) ───────────────
   Two small text utilities so a screen's one headline moment (client
   hero, an empty state's title, a modal's confirmation) and the small
   uppercase label that can sit above it are defined once, consistently,
   rather than reinvented per screen. Font size is deliberately NOT set
   here — pick one of tokens.css's --display-sm/md/lg (or --text-3xl for
   a big number) at the call site, since the right size is screen-
   dependent. */
.display-heading {
    font-family: var(--font-display);
    font-weight: var(--font-semibold);
    letter-spacing: var(--tracking-tight);
    line-height: 1.05;
    text-wrap: balance;
    color: var(--text);
}
.eyebrow {
    display: block;
    font-size: var(--text-xs);
    /* 700 (DS.1, matching Planning D3's own local eyebrow) → 600 (final
       color/typography pass) — Carlos's own explicit weight table puts
       Eyebrow at 600; uppercase + letter-spacing already carry the
       emphasis at this size, so 700 read as one more bold element in a
       screen asking for less of that. Planning's own eyebrows are a
       separate local style (.planning-panel-label etc., untouched by this
       pass) and keep 700, matching D3. */
    font-weight: var(--font-semibold);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--text-secondary);
}

/* ── Visually hidden — text for assistive tech only (a count read aloud next to its compact
   visual form, "Planificar · 5" → "Planificar, 5 clientes"). Never for content a sighted user
   needs. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* ── Meta (DS.1, plan §6.8) — the secondary line under a title: facts
   joined by " · ". 13px secondary, never italic, never a row of pills. */
.meta { font-size: var(--text-base); color: var(--text-secondary); }
.meta-lg { font-size: var(--text-md); color: var(--text-secondary); }

/* ── Text action (DS.1, D3 .d-link) — an action that reads as a word, not
   a button: the secondary actions next to one primary ("Cambiar sesión",
   "+ Ejercicio", "+ Añadir nota"). Replaces three local overrides that
   each un-styled .btn-ghost/.btn-secondary to get here. `-quiet` for the
   "add" affordance that must not compete with content. */
.btn-link {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 0;
    min-height: var(--target-min);
    background: none;
    border: none;
    border-radius: 0;
    font: var(--font-medium) var(--text-md) var(--font-sans);
    color: var(--text);
    cursor: pointer;
}
.btn-link:hover { background: none; color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.btn-link-quiet { color: var(--text-secondary); font-size: var(--text-base); }
.btn-link-quiet:hover { color: var(--text); }
/* Disclosure toggle: a quiet link with a chevron that turns when its panel is open (#32). */
.btn-disclosure { display: inline-flex; align-items: center; gap: var(--space-1); }
.btn-disclosure .icon { transition: transform var(--duration-fast) var(--ease-standard); }
.btn-disclosure[aria-expanded="true"] .icon { transform: rotate(90deg); }
.btn-link:disabled { color: var(--text-disabled); text-decoration: none; cursor: default; }

/* ── Surface (DS.1) — warm paper a group WITH MEANING sits on: the
   selected day's detail, a session block, a notes panel. No border, no
   shadow; radius-md; --surface-pad inside. Four roles, one per meaning —
   never pick by taste:
     .surface           content that exists (a day with training, a block)
     .surface-quiet     absence / rest / empty — almost the page, a hairline
                        keeps the geometry
     .surface-selected  the one focused thing on screen (+ optional 2px dark-
                        neutral rule at the top: .surface-selected.has-rule
                        — the surface + bold type carry selection; the rule
                        marks it, never a thick ink bar)
     .surface-inverse   ink — one small strong mark (a "today" badge, the
                        active segment), never a large block */
.surface, .surface-quiet, .surface-selected, .surface-inverse {
    border-radius: var(--radius-md);
    padding: var(--surface-pad);
}
.surface { background: var(--surface-day); }
.surface-quiet { background: var(--surface-quiet); box-shadow: inset 0 0 0 1px var(--surface-quiet-line); }
.surface-selected { background: var(--surface-selected); }
.surface-selected.has-rule { border-top: 2px solid var(--text-secondary); }
.surface-inverse { background: var(--surface-inverse); color: var(--text-on-inverse); }
.surface-inverse .eyebrow, .surface-inverse .meta { color: var(--text-on-inverse-secondary); }

/* ── Section (DS.1) — how content is grouped WITHOUT a card: eyebrow +
   title (+ meta) + actions in a head row, then the body. On the page,
   sections are separated by spacing only (--section-gap). Inside a
   surface, siblings divide with a hairline (the D3 detail). Markup:
     <section class="section">
       <div class="section-head">
         <div>
           <span class="eyebrow">Bloque 1 · Calentamiento</span>
           <h2 class="section-title">Serie principal</h2>
           <p class="meta">Sube peso si las últimas 2 reps salen fáciles.</p>
         </div>
         <div class="section-actions"><button class="btn-link btn-link-quiet">+ Ejercicio</button> …</div>
       </div>
       <div class="hairline-rows">…ListRows…</div>
     </section> */
.section { margin: 0; }
.section + .section { margin-top: var(--section-gap); }
.section-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-3);
}
.section-head > :first-child { min-width: 0; }
.section-head .eyebrow + .section-title { margin-top: 4px; }
.section-title {
    margin: 0;
    font-family: var(--font-display);
    font-size: 18px;
    font-weight: var(--font-semibold);
    letter-spacing: -0.01em;
    line-height: var(--leading-tight);
    color: var(--text);
}
.section-title-lg { font-size: var(--display-sm); font-weight: var(--font-semibold); letter-spacing: var(--tracking-tight); }
.section-head .meta { margin: 4px 0 0; }
.section-actions { display: flex; align-items: center; gap: var(--space-4); flex-shrink: 0; }
/* Sub-sections inside a surface: hairline + --space-5, never above the first. */
.surface > .section + .section,
.surface-selected > .section + .section,
.surface-quiet > .section + .section {
    margin-top: var(--space-5);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}
/* Footer line of a section — a quiet fact that closes the group
   ("1 min de descanso tras el bloque"). */
.section-foot { margin-top: var(--space-3); }
/* Ordinal sections (Phase 5.1): a numbered group in a sequence — the
   blocks of a session, the weeks of a programme. The number is set in
   display type and muted, so it has presence (it is what the eye scans)
   without competing with the title. `.section-lead` places it beside the
   eyebrow/title stack. */
.section-lead { display: flex; align-items: flex-start; gap: var(--space-4); min-width: 0; }
.section-ordinal {
    flex-shrink: 0;
    width: 40px;
    font-family: var(--font-display);
    font-size: var(--display-sm);
    font-weight: var(--font-semibold);
    letter-spacing: var(--tracking-tight);
    line-height: 1;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
    margin-top: -3px;
}
/* Reading measure (Phase 5.1) — the editorial body of a page: blocks,
   rows, prescriptions. The header above it keeps the shell's full width. */
.page-reading { max-width: var(--reading-max-width); }

/* ── Media tile / media hero (2026-09 Oysho-style pass) ──────────────
   Photography is the one place color is allowed to be loud — the tile
   itself is never rounded (--radius-media) and never wrapped in a card;
   the label sits below it as plain text, the way a catalogue caption
   does, not inside a bordered container. Both come with a `-placeholder`
   state that is a first-class, deliberately-designed default — NOT a
   degraded fallback — because most exercises/disciplines will not have
   campaign-quality photography (see Project/docs/DESIGN_SYSTEM.md,
   "riesgo 1"). Never conditionally render a smaller/plainer layout for
   the no-photo case; only the fill inside the tile changes.

   Markup — grid tile:
     <div class="media-tile">
       <div class="media-tile-image"><img src="…" alt=""></div>
       <!-- no photo yet: -->
       <div class="media-tile-image media-tile-placeholder"><svg class="icon">…</svg></div>
       <div class="media-tile-label">Fitness</div>
     </div>
   Wrap several in <div class="media-tile-grid"> for an auto-filling
   responsive grid (catalogue, disciplines). */
.media-tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: var(--space-4);
}
.media-tile {
    display: flex;
    flex-direction: column;
    gap: 10px;
    cursor: pointer;
    text-align: left;
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: inherit;
}
.media-tile-image {
    position: relative;
    aspect-ratio: 3 / 4;
    border-radius: var(--radius-media);
    overflow: hidden;
    /* --surface-media (DS.2A) — same reasoning as .item-media above. */
    background: var(--surface-media);
}
.media-tile-image img { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-tile-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary);
}
.media-tile-placeholder .icon { width: 28px; height: 28px; opacity: 0.6; }
.media-tile-label { font-size: var(--text-md); font-weight: var(--font-medium); color: var(--text); }
.media-tile-sub { margin-top: -6px; font-size: var(--text-sm); color: var(--text-secondary); }

/* Full-bleed hero banner — a screen's one big photographic moment
   (client "Hoy", a session/challenge detail, an invite cover). The
   overlay gradient only exists to keep the eyebrow/title legible over a
   photo; `.media-hero-empty` drops it and fills with solid ink instead,
   so a trainer/exercise with no cover photo still reads as an
   intentional, designed hero rather than a broken image slot. */
.media-hero {
    position: relative;
    border-radius: var(--radius-media);
    overflow: hidden;
    /* --surface-media (DS.2A) — same reasoning as .item-media above. */
    background: var(--surface-media);
    aspect-ratio: 4 / 5;
}
.media-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-hero-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: var(--space-5);
    background: linear-gradient(180deg, rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, 0.55) 100%);
}
.media-hero-eyebrow { color: #fff; opacity: 0.85; }
.media-hero-title {
    margin-top: 4px;
    font-size: var(--display-sm);
    color: #fff;
}
.media-hero.media-hero-empty {
    background: var(--action);
    display: flex;
}
.media-hero.media-hero-empty .media-hero-overlay { background: none; }

/* ── Stat display (2026-09 Oysho-style pass) ─────────────────────────
   A number that matters more than anything around it — client execution
   (peso × reps), a running stat, an activity summary. Label sits small,
   above the number, never beside it. `.stat-row` divides several side by
   side with a hairline, never a card per stat. */
.stat-display { display: flex; flex-direction: column; gap: 4px; }
.stat-display-label { font-size: var(--text-sm); color: var(--text-secondary); }
.stat-display-value {
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: var(--font-semibold);
    letter-spacing: var(--tracking-tight);
    line-height: 1;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.stat-display-value small {
    margin-left: 4px;
    font-size: 0.5em;
    font-weight: var(--font-semibold);
    letter-spacing: 0;
    color: var(--text-secondary);
}
.stat-row { display: flex; }
.stat-row .stat-display { flex: 1; padding: 0 var(--space-4); text-align: center; }
.stat-row .stat-display:first-child { padding-left: 0; }
.stat-row .stat-display:not(:first-child) { border-left: 1px solid var(--border); }

/* ── Distribution bar (V1, 2026-10) ──────────────────────────────────
   A share of a whole as one quiet row: label · hairline track with an
   ink fill proportional to the share · the value at the end of the row
   (never a number floating over the bar). Ink on paper only — the fill
   is --text, the track is --border; a bar shows proportion, not state,
   so it never takes a status colour or a tint. Consumers: the Actividad
   tab's "Volumen por grupo muscular" and the session editor's
   "Distribución planificada" (client-activity.js, srDistributionBarsHtml).
   The value is nowrap + tabular so a column of bars lines up and never
   wraps on a phone; the label ellipsises instead of pushing the track.
   The CONTAINER is the grid and each row is `display: contents`, so every
   track starts and ends on the same column whatever its label/value. */
.distribution-bars {
    display: grid;
    grid-template-columns: minmax(0, 7rem) minmax(0, 1fr) auto;
    align-items: center;
    column-gap: var(--space-3);
    row-gap: var(--space-2);
    font-size: var(--text-base);
}
.distribution-bar { display: contents; }
.distribution-bar-label { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.distribution-bar-track { display: block; height: 4px; background: var(--border); }
.distribution-bar-fill { display: block; height: 100%; max-width: 100%; background: var(--text); }
.distribution-bar-value {
    white-space: nowrap;
    text-align: right;
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    color: var(--text-secondary);
}
.distribution-bars-note { margin: var(--space-2) 0 0; font-size: var(--text-sm); color: var(--text-secondary); }
@media (max-width: 640px) {
    .distribution-bars { grid-template-columns: minmax(0, 5.5rem) minmax(0, 1fr) auto; }
}

/* ── Surface panel (2026-09 Oysho-style pass) ────────────────────────
   The warm, filled metadata block (session duration/type/difficulty; a
   challenge's "en curso · 783 usuarios" / "finaliza en 28 días") — a
   block of related facts, not an independent floating object, so it
   uses --surface + an internal hairline rather than --bg-card + border. */
.surface-panel { background: var(--surface); border-radius: var(--radius-md); overflow: hidden; }
.surface-panel-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: var(--space-4);
    font-size: var(--text-base);
    color: var(--text);
}
.surface-panel-row .icon { color: var(--text-secondary); }
.surface-panel-row + .surface-panel-row { border-top: 1px solid var(--border); }

/* ── Hairline rows (2026-09 Oysho-style pass) ────────────────────────
   A flatter alternative to `.list` above — full-bleed rows separated by
   a hairline only, no enclosing border/radius/background. Use where the
   surrounding screen already supplies the page background (an agenda, a
   notifications feed, a calendar's list view) and a boxed `.list` would
   read as one more nested container. */
.hairline-rows { display: flex; flex-direction: column; }
.hairline-row { padding: var(--space-4) 0; border-bottom: 1px solid var(--border); }
.hairline-row:last-child { border-bottom: none; }

/* DS.1 — a ListRow (.list-item) inside .hairline-rows is the flat variant:
   no background, --row-pad vertical, hairline below, nothing after the
   last one. Hover = the title underlines (D3 .d-session), not a fill:
   inside a warm surface a fill would fight the surface. `.is-selected`
   (a chosen row in a picker) is the only fill, and it is the warm
   selected paper, not a tint. */
.hairline-rows > .list-item {
    padding: var(--row-pad) 0;
    background: none;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
}
.hairline-rows > .list-item:last-child { border-bottom: none; }
/* Hover/selected on the PAGE (white): the row takes the warm paper,
   bleeding 12px past the text edge so the fill frames the row instead of
   hugging it (Phase 5.1 — a purposeful surface, the D3 "day hover"). */
.hairline-rows > .list-item:hover,
.hairline-rows > .list-item:focus-within,
.hairline-rows > .list-item.is-selected {
    margin: 0 calc(-1 * var(--space-3));
    padding-left: var(--space-3);
    padding-right: var(--space-3);
    border-radius: var(--radius-sm);
}
.hairline-rows > .list-item:hover { background: var(--surface-day-hover); }
.hairline-rows > .list-item:focus-within { background: var(--surface-day); }
.hairline-rows > .list-item.is-selected { background: var(--surface-selected); }
.hairline-rows > .list-item:hover .item-title,
.hairline-rows > .list-item:hover .list-item-name { text-decoration: underline; text-underline-offset: 3px; }
/* Inside a warm surface: no hover fill (it would fight the surface — the
   title underline is the feedback) and a hairline one step softer. */
.surface .hairline-rows > .list-item, .surface-selected .hairline-rows > .list-item { border-bottom-color: var(--surface-quiet-line); }
.surface .hairline-rows > .list-item:last-child, .surface-selected .hairline-rows > .list-item:last-child { border-bottom: none; }
.surface .hairline-rows > .list-item:hover, .surface-selected .hairline-rows > .list-item:hover,
.surface .hairline-rows > .list-item:focus-within, .surface-selected .hairline-rows > .list-item:focus-within { background: none; margin: 0; padding-left: 0; padding-right: 0; }

/* ── Touch targets (#36) ──────────────────────────────────────────
   Text-only controls (.btn-link, .back-link, .btn-small, segmented buttons) are at least
   --target-min (32px) tall on any pointer; on touch an invisible ::after grows the tap area to
   44px without changing the layout. Applies to every control that opts in with this list. */
@media (pointer: coarse) {
    .btn-link, .back-link, .btn-small, .segmented-control button { position: relative; }
    .btn-link::after, .back-link::after, .btn-small::after, .segmented-control button::after {
        content: ''; position: absolute; inset: -6px -4px;
    }
}


/* Brand lockup (symbol + SERAC, shared/brand/lockup.svg). The only place the logo is used as an <img>
   in product chrome. Sized by context, never recoloured, never given a margin of its own. */
.brand-lockup { display: block; width: auto; height: 20px; }
.brand-lockup--sm { height: 16px; }
.brand-lockup--md { height: 20px; }
.brand-lockup--lg { height: 28px; margin-bottom: var(--space-6, 24px); }
