/*
 * Aloud (v2) foundations — docs/DA.md tokens, fonts, shell and the shared
 * component classes. Screen-specific layout goes in dedicated files.
 *
 * Every rule is scoped under body.ui; @font-face and @keyframes are the
 * exception, since neither can be scoped.
 */

/* ------------------------------------------------------------------- */
/* Fonts — Fustat (body/UI) + Fraunces (titles, English), self-hosted   */
/* variable fonts. Fraunces is subset to basic latin, SOFT/WONK pinned  */
/* to 0, wght limited to 400-600 (docs/DA.md: no heavier weights).      */
/* ------------------------------------------------------------------- */

@font-face {
    font-family: 'Fustat';
    src: url("../fonts/fustat-latin-zsJONjL.woff2") format('woff2-variations'),
         url("../fonts/fustat-latin-zsJONjL.woff2") format('woff2');
    font-weight: 300 800;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Fustat';
    src: url("../fonts/fustat-latin-ext-WysWm1C.woff2") format('woff2-variations'),
         url("../fonts/fustat-latin-ext-WysWm1C.woff2") format('woff2');
    font-weight: 300 800;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Fraunces';
    src: url("../fonts/fraunces-latin-tlFM0ng.woff2") format('woff2-variations'),
         url("../fonts/fraunces-latin-tlFM0ng.woff2") format('woff2');
    font-weight: 400 600;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+2010-2027, U+2030-205E;
}

@font-face {
    font-family: 'Fraunces';
    src: url("../fonts/fraunces-italic-latin-eIY8VIs.woff2") format('woff2-variations'),
         url("../fonts/fraunces-italic-latin-eIY8VIs.woff2") format('woff2');
    font-weight: 400 600;
    font-style: italic;
    font-display: swap;
    unicode-range: U+0000-00FF, U+2010-2027, U+2030-205E;
}

/* ------------------------------------------------------------------- */
/* Reset — needed on fresh v2 page loads, where app.css is absent       */
/* ------------------------------------------------------------------- */

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* Must win over any `display` a component class sets on the same element. */
[hidden] {
    display: none !important;
}

html, body {
    height: 100%;
}

/* ------------------------------------------------------------------- */
/* Tokens (docs/DA.md, dark-first)                                      */
/* ------------------------------------------------------------------- */

body.ui {
    --bg: #14110F;
    --surface: #1C1916;
    --surface-2: #262220;

    --ink: #F3EAD8;
    --ink-2: rgba(243, 234, 216, .64);
    --ink-3: rgba(243, 234, 216, .40);
    --ink-disabled: rgba(243, 234, 216, .40);

    --brand: #E8A24A;
    --brand-soft: rgba(232, 162, 74, .18);
    /* Text set in the brand color on a --brand-soft fill, or directly on a
       scene, needs to darken in light mode (bright amber on a pale tint or
       a bright photo falls well under 4.5:1) — --brand itself stays THE one
       accent color (docs/DA.md) and never changes. */
    --brand-ink: var(--brand);
    /* Icon/text sitting directly on a --brand fill (buttons, the mic): a
       fixed dark ink works in both themes because --brand itself doesn't
       change, unlike --bg which used to be (wrongly) reused for this. */
    --ink-on-brand: #14110F;

    --ok: #8FBF9A;
    --warn: #D9A15F;
    --err: #C9605B;

    --glass-bg: rgba(28, 25, 22, .60);
    --glass-blur: 18px;
    --glass-border: rgba(243, 234, 216, .14);

    --scene-overlay: linear-gradient(180deg, rgba(20, 17, 15, 0) 35%, #14110F 100%);
    /* Halo behind text set over a scene, reinforcing --scene-overlay locally;
       flips with the overlay in light mode (docs/DA.md "Variante jour"). */
    --scene-text-shadow: 0 1px 12px rgba(0, 0, 0, .45);

    /* Dims whatever is behind a sheet/modal — stays dark in both themes,
       like any scrim (it's not the page's own surface). */
    --scrim: rgba(20, 17, 15, .55);

    --elev-1: 0 2px 8px rgba(0, 0, 0, .25);
    --elev-2: 0 12px 32px rgba(0, 0, 0, .35);
    --elev-3: 0 24px 64px rgba(0, 0, 0, .45);

    --t-display-size: 40px; --t-display-line: 1.05;
    --t-1-size: 28px; --t-1-line: 1.1;
    --t-2-size: 22px; --t-2-line: 1.2;
    --t-3-size: 18px; --t-3-line: 1.3;
    --t-4-size: 16px; --t-4-line: 1.45;
    --t-5-size: 13px; --t-5-line: 1.4;

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;
    --space-7: 48px;
    --space-8: 64px;

    --dur-fast: 160ms;
    --dur: 320ms;
    --dur-slow: 600ms;

    --radius-card: 24px;
    --radius-tile: 16px;
    --radius-sheet: 28px;
    --radius-pill: 999px;
    --ease: cubic-bezier(.4, 0, .2, 1);

    /* Floating tab bar height + breathing room. */
    --tabbar-clearance: 104px;
}

/* ------------------------------------------------------------------- */
/* Light theme (docs/DA.md "Variante jour", E4.3) — token overrides only,  */
/* no new component rules. Automatic: driven by the scene's daylight flag  */
/* (base.html.twig sets <html data-theme>), never a user-facing switch. */
/*                                                                          */
/* Selector note: light-theme overrides target body.ui, not :root — the */
/* dark tokens above are also declared on body.ui, and a custom property */
/* set there always wins over one merely inherited from <html>, regardless  */
/* of selector specificity. [data-theme="light"] lives on <html>, hence the */
/* compound selector to reach through it.                                  */
/* ------------------------------------------------------------------- */

[data-theme="light"] body.ui {
    color-scheme: light;

    --bg: #F5EFE6;
    --surface: #FAF5EC;
    --surface-2: #FFFBF3;

    --ink: #1B1714;
    --ink-2: rgba(27, 23, 20, .64);
    --ink-3: rgba(27, 23, 20, .52);
    --ink-disabled: rgba(27, 23, 20, .52);

    /* --brand stays #E8A24A — see --brand-ink above for where its text needs
       to darken instead of following --brand directly. */
    --brand-ink: #8F5A15;

    --ok: #356A44;
    --warn: #8A5A1E;
    --err: #B23B36;

    --glass-bg: rgba(255, 251, 243, .60);
    --glass-border: rgba(27, 23, 20, .14);

    /* Lightens toward --bg instead of darkening toward it, and starts veiling
       from the top: a high-key photograph is busy everywhere, so dark ink
       needs a scrim well before the bottom third where the night scenes
       could rely on their own darkness. */
    --scene-overlay: linear-gradient(180deg, rgba(245, 239, 230, .30) 0%, rgba(245, 239, 230, .52) 28%, rgba(245, 239, 230, .90) 62%, #F5EFE6 88%);
    --scene-text-shadow: 0 1px 12px rgba(255, 251, 243, .65);

    --elev-1: 0 2px 8px rgba(27, 23, 20, .08);
    --elev-2: 0 12px 32px rgba(27, 23, 20, .12);
    --elev-3: 0 24px 64px rgba(27, 23, 20, .16);
}

/* ------------------------------------------------------------------- */
/* Base                                                                 */
/* ------------------------------------------------------------------- */

body.ui {
    background: var(--bg);
    color: var(--ink);
    font-family: 'Fustat', system-ui, sans-serif;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    color-scheme: dark;
}

.ui a {
    color: inherit;
    text-decoration: none;
}

.ui button, .ui input, .ui select, .ui textarea {
    font-family: inherit;
    color: inherit;
}

.ui button {
    background: none;
    border: none;
    cursor: pointer;
}

.ui ul, .ui ol {
    list-style: none;
}

.ui img, .ui svg {
    display: block;
    max-width: 100%;
}

/* ------------------------------------------------------------------- */
/* Focus — one ring for every control, both themes (E4.3 a11y pass).    */
/* --brand-ink rather than --brand: a plain amber ring falls under 3:1  */
/* on the light bg (amber-on-pale-cream), same reason buttons use it.   */
/* ------------------------------------------------------------------- */

.ui :focus-visible {
    outline: 2px solid var(--brand-ink);
    outline-offset: 2px;
}

/* ------------------------------------------------------------------- */
/* Typography                                                           */
/* ------------------------------------------------------------------- */

.ui h1, .ui h2, .ui .screen-title {
    font-family: 'Fraunces', Georgia, serif;
    font-weight: 600;
}

.ui h1, .ui .screen-title {
    font-size: var(--t-1-size);
    line-height: var(--t-1-line);
}

.ui h2 {
    font-size: var(--t-2-size);
    line-height: var(--t-2-line);
}

.ui h3 {
    font-size: var(--t-3-size);
    line-height: var(--t-3-line);
    font-weight: 600;
}

.ui p {
    line-height: 1.5;
}

.ui .text-body {
    font-size: 15px;
    line-height: 1.5;
}

.ui .text-secondary {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .text-muted {
    color: var(--ink-3);
}

.ui .section-label {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ink-3);
}

/* Bilingual content marker (docs/DA.md): English is always set in true
   italic Fraunces, French/UI stays in Fustat. */
.ui .en {
    font-family: 'Fraunces', Georgia, serif;
    font-style: italic;
    font-weight: 500;
}

.ui .tabular-nums {
    font-variant-numeric: tabular-nums;
}

/* Visually hidden but still announced — names icon-only controls, since
   <twig:Icon> always renders aria-hidden. */
.ui .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
}

/* ------------------------------------------------------------------- */
/* Buttons                                                              */
/* ------------------------------------------------------------------- */

.ui .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 52px;
    padding: 0 24px;
    border-radius: var(--radius-pill);
    font-size: 16px;
    font-weight: 600;
    border: none;
    white-space: nowrap;
    transition: transform var(--dur-fast) var(--ease);
}

.ui .btn:active {
    transform: scale(.97);
}

.ui .btn:disabled,
.ui .btn.is-disabled {
    opacity: .35;
    pointer-events: none;
}

/* Primary: one per screen — the amber lamp on night ink. */
.ui .btn-primary {
    background: var(--brand);
    color: var(--ink-on-brand);
}

/* Secondary: night glass. */
.ui .btn-secondary {
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(var(--glass-blur));
    backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
    color: var(--ink);
    height: 48px;
    font-size: 15px;
}

/* Tertiary: bare text. */
.ui .btn-tertiary {
    background: transparent;
    color: var(--ink-2);
    height: 48px;
    font-size: 15px;
}

.ui .btn-small {
    height: 40px;
    padding: 0 18px;
    font-size: 14px;
}

/* The screen's closing actions: a full-width primary, optionally a tertiary
   under it. The screen container owns the outer edges (≥ 20px sides, bottom
   safe-area); the bar owns the breathing room above and between buttons. */
.ui .cta-bar {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3);
    margin-top: var(--space-5);
}

.ui .cta-bar .btn {
    width: 100%;
}

/* Round 48px icon button — used standalone (back/close), not inside .btn. */
.ui .btn-icon {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-pill);
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(var(--glass-blur));
    backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
    color: var(--ink);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: transform var(--dur-fast) var(--ease);
}

.ui .btn-icon:active {
    transform: scale(.94);
}

/* ------------------------------------------------------------------- */
/* Cards & tiles                                                        */
/* ------------------------------------------------------------------- */

.ui .card {
    background: var(--surface);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-card);
    padding: 20px;
    box-shadow: var(--elev-1);
}

.ui .tile {
    background: var(--surface-2);
    border-radius: var(--radius-tile);
    padding: 14px 16px;
}

/* ------------------------------------------------------------------- */
/* Day tile — week strip (components/DayTile.html.twig, v2 variant)     */
/* Blame-free vocabulary (§3.3): full ring = done, ring + dot = done    */
/* plus an extra, soft dash = the week's free pause, moon = rest. An    */
/* empty day stays a quiet number — no red, no reproach.                */
/* ------------------------------------------------------------------- */

.ui .day-tile-v2 {
    width: 40px;
    height: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    background: none;
    border: 0;
}

.ui .day-tile-v2 .day-tile-dow {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--ink-3);
}

.ui .day-tile-ring {
    position: relative;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1.5px solid var(--glass-border);
    color: var(--ink-3);
}

.ui .day-tile-v2 .day-tile-num {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: inherit;
}

.ui .day-tile-v2.is-done .day-tile-ring,
.ui .day-tile-v2.is-done-plus .day-tile-ring {
    border-color: var(--brand);
    background: var(--brand-soft);
    color: var(--brand-ink);
}

/* The "extra done" dot, riding the bottom edge of the ring. */
.ui .day-tile-plus {
    position: absolute;
    bottom: -7px;
    left: 50%;
    width: 5px;
    height: 5px;
    margin-left: -2.5px;
    border-radius: 50%;
    background: var(--brand);
}

.ui .day-tile-v2.is-pause .day-tile-ring {
    border-style: dashed;
}

.ui .day-tile-dash {
    width: 10px;
    height: 2px;
    border-radius: 1px;
    background: var(--ink-3);
}

.ui .day-tile-v2.is-rest .day-tile-ring {
    border-color: transparent;
    background: var(--surface);
}

.ui .day-tile-v2.is-today .day-tile-ring {
    border-color: var(--ink-2);
    color: var(--ink);
}

.ui .day-tile-v2.is-today .day-tile-dow {
    color: var(--ink-2);
}

/* Today's session already completed: amber halo on top of the done ring. */
.ui .day-tile-v2.is-today-done .day-tile-ring {
    box-shadow: 0 0 0 3px var(--brand-soft);
}

.ui .day-tile-v2.is-future {
    opacity: .55;
}

/* ------------------------------------------------------------------- */
/* Chips                                                                */
/* ------------------------------------------------------------------- */

.ui .chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 28px;
    padding: 0 12px;
    border-radius: var(--radius-pill);
    font-size: var(--t-5-size);
    font-weight: 600;
    background: var(--surface-2);
    color: var(--ink-2);
    white-space: nowrap;
}

.ui .chip.is-active,
.ui .chip-neutral.is-active {
    background: var(--brand-soft);
    color: var(--brand-ink);
}

/* English word/phrase chip — Fraunces italic like all English content. */
.ui .chip-en {
    font-family: 'Fraunces', Georgia, serif;
    font-style: italic;
    font-weight: 500;
    color: var(--ink);
    border: 1px solid var(--glass-border);
}

/* ------------------------------------------------------------------- */
/* Badges — 6-state AI pipeline pill (desaturated, discreet)            */
/* ------------------------------------------------------------------- */

.ui .badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 26px;
    padding: 0 12px;
    border-radius: var(--radius-pill);
    font-size: 12.5px;
    font-weight: 600;
    white-space: nowrap;
}

.ui .badge-pending {
    background: var(--surface-2);
    color: var(--ink-3);
}

.ui .badge-transcribing,
.ui .badge-analyzing {
    background: var(--brand-soft);
    color: var(--brand-ink);
    animation: v2Blink 1.6s ease-in-out infinite;
}

.ui .badge-completed {
    background: color-mix(in srgb, var(--ok) 16%, transparent);
    color: var(--ok);
}

.ui .badge-failed {
    background: color-mix(in srgb, var(--err) 16%, transparent);
    color: var(--err);
}

.ui .badge-queued-budget {
    background: var(--surface-2);
    color: var(--warn);
}

/* ------------------------------------------------------------------- */
/* Track — plain amber progress fill (no hachures in the night UI)      */
/* ------------------------------------------------------------------- */

.ui .track {
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    border: 1px solid var(--glass-border);
    overflow: hidden;
}

.ui .track-fill {
    height: 100%;
    border-radius: var(--radius-pill);
    background: var(--brand);
    transition: width .4s var(--ease);
}

/* ------------------------------------------------------------------- */
/* AudioPlayer — pill player (Recorder preview, listen-back history)    */
/* ------------------------------------------------------------------- */

.ui .audio-player {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    background: var(--surface);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-pill);
    padding: 6px 14px 6px 6px;
}

.ui .audio-player-toggle {
    width: 44px;
    height: 44px;
    min-width: 44px;
    border-radius: var(--radius-pill);
    background: var(--brand);
    border: none;
    color: var(--ink-on-brand);
    display: flex;
    align-items: center;
    justify-content: center;
}

.ui .audio-player-toggle:active {
    transform: scale(.97);
}

.ui .audio-player-icon {
    display: flex;
}

/* Same UA-stylesheet override as `.recorder-panel[hidden]` below: an
   author-origin `display` rule always wins over `[hidden] { display: none }`
   regardless of specificity, so without this both play/pause icons would
   render at once. */
.ui .audio-player-icon[hidden] {
    display: none;
}

.ui .audio-player-track {
    flex: 1;
    height: 10px;
}

.ui .audio-player-time {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--ink-2);
    flex-shrink: 0;
}

/* ------------------------------------------------------------------- */
/* Forms                                                                */
/* ------------------------------------------------------------------- */

.ui .field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.ui .field label {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--ink-2);
}

.ui .input {
    height: 52px;
    border-radius: var(--radius-tile);
    border: 1px solid var(--glass-border);
    background: var(--surface);
    padding: 0 16px;
    /* 16px is a hard floor, not a style choice: Safari iOS zooms the viewport
       when a focused field is under 16px. */
    font-size: 16px;
    color: var(--ink);
    transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}

.ui .input:focus {
    outline: none;
    border-color: var(--brand-ink);
    box-shadow: 0 0 0 3px var(--brand-soft);
}

.ui .input[aria-invalid="true"] {
    border-color: var(--err);
}

.ui .input[aria-invalid="true"]:focus {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--err) 25%, transparent);
}

.ui .field-error {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--err);
}

/* Segmented control (e.g. level picker A2/B1/B2). */
.ui .segmented {
    display: flex;
    gap: 8px;
}

.ui .segmented-option {
    flex: 1;
    height: 48px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--glass-border);
    background: var(--surface);
    color: var(--ink-2);
    font-size: 15px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
}

.ui .segmented-option.is-active {
    background: var(--brand-soft);
    border-color: var(--brand-ink);
    color: var(--brand-ink);
}

.ui .segmented-stack {
    flex-direction: column;
}

.ui .segmented-stack .segmented-option {
    flex: none;
    width: 100%;
    height: auto;
    min-height: 48px;
    justify-content: flex-start;
    text-align: left;
    padding: 12px 18px;
}

/* ------------------------------------------------------------------- */
/* Bottom sheet                                                         */
/* ------------------------------------------------------------------- */

/* Set on <html> while a sheet is open — body carries ui, html doesn't,
   so this can't be scoped under .ui. */
.is-sheet-open {
    overflow: hidden;
}

/* A sheet opens inside .stage-content, which is its own stacking context: the
   tab bar's z-index is weighed against the stage, not against the overlay, so
   it paints over the panel and swallows its last row however high the overlay
   goes. Under a modal it is unreachable anyway. */
.is-sheet-open .ui .tabbar {
    display: none;
}

.ui .sheet-overlay {
    position: fixed;
    inset: 0;
    background: var(--scrim);
    z-index: 60;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    overscroll-behavior: contain;
}

.ui .sheet-overlay[hidden] {
    display: none;
}

.ui .sheet {
    width: 100%;
    max-width: 430px;
    max-height: 85dvh;
    overflow-y: auto;
    background: var(--surface);
    border-top: 1px solid var(--glass-border);
    border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
    padding: 14px 20px calc(34px + env(safe-area-inset-bottom, 0px));
    display: flex;
    flex-direction: column;
    gap: 14px;
    box-shadow: var(--elev-3);
    animation: v2SlideUp var(--dur) var(--ease) both;
}

.ui .sheet-handle {
    width: 40px;
    height: 5px;
    border-radius: var(--radius-pill);
    background: var(--ink-3);
    margin: 0 auto;
}

.ui .sheet-title {
    font-family: 'Fraunces', Georgia, serif;
    font-size: var(--t-3-size);
    font-weight: 600;
}

/* ------------------------------------------------------------------- */
/* App shell                                                            */
/* ------------------------------------------------------------------- */

.ui .app-viewport {
    max-width: 430px;
    margin: 0 auto;
    min-height: 100dvh;
    background: var(--bg);
    position: relative;
    display: flex;
    flex-direction: column;
}

.ui .app-content {
    flex: 1;
    /* Top inset clears the notch/Dynamic Island on iPhones (0 elsewhere). */
    padding: calc(20px + env(safe-area-inset-top, 0px)) 20px calc(var(--tabbar-clearance) + env(safe-area-inset-bottom, 0px));
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.ui .app-content.is-focus {
    padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
}

/* ------------------------------------------------------------------- */
/* Flash messages                                                       */
/* ------------------------------------------------------------------- */

.ui .flash-messages {
    position: fixed;
    top: calc(12px + env(safe-area-inset-top, 0px));
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    z-index: 100;
    pointer-events: none;
    padding: 0 20px;
}

.ui .flash {
    max-width: 390px;
    width: 100%;
    border-radius: var(--radius-tile);
    padding: 12px 16px;
    font-size: 14px;
    font-weight: 600;
    text-align: center;
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(180%);
    backdrop-filter: blur(var(--glass-blur)) saturate(180%);
    color: var(--ink);
    border: 1px solid var(--glass-border);
    box-shadow: var(--elev-2);
    animation: v2Pop var(--dur) var(--ease) both;
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.ui .flash-text {
    flex: 1;
    min-width: 0;
}

.ui .flash-close {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin: -6px -8px -6px 0;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: currentColor;
    opacity: .5;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: opacity var(--dur-fast) var(--ease);
}

.ui .flash-close:hover,
.ui .flash-close:focus-visible {
    opacity: .85;
}

.ui .flash-success {
    border-color: var(--ok);
    /* Auto-dismiss: appear, wait 4s, fade out. The exit keyframe MUST be
       named flashDismiss — flash_controller.js removes the element when an
       animation with exactly that name ends. */
    animation: v2Pop var(--dur) var(--ease) both, flashDismiss 250ms var(--ease) 4s both;
}

.ui .flash-error {
    border-color: var(--err);
    color: var(--err);
}

.ui .flash.is-dismissing {
    animation: flashDismiss 250ms var(--ease) both;
}

@keyframes v2Pop {
    from {
        opacity: 0;
        transform: scale(.92) translateY(-6px);
    }
}

@keyframes flashDismiss {
    to {
        opacity: 0;
        transform: scale(.92) translateY(-6px);
    }
}

/* ------------------------------------------------------------------- */
/* Tab bar — same glass pill as v1, tinted with the v2 night palette    */
/* (reskinned to 3 tabs in E0.2)                                        */
/* ------------------------------------------------------------------- */

.ui .tabbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(20px + env(safe-area-inset-bottom, 0px));
    display: flex;
    justify-content: center;
    gap: 4px;
    width: max-content;
    margin: 0 auto;
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(180%);
    backdrop-filter: blur(var(--glass-blur)) saturate(180%);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-pill);
    padding: 7px;
    box-shadow: var(--elev-2);
    z-index: 50;
}

.ui .tabbar-tab {
    position: relative;
    overflow: hidden;
    height: 46px;
    border-radius: var(--radius-pill);
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 0 13px;
    color: var(--ink-3);
    transition: all .2s var(--ease);
}

.ui .tabbar-tab.is-active {
    background: var(--brand-soft);
    color: var(--brand-ink);
    padding: 0 16px;
}

.ui .tabbar-tab:active {
    transform: scale(.9);
}

.ui .tabbar-glass-ripple {
    position: absolute;
    border-radius: 50%;
    pointer-events: none;
    transform: translate(-50%, -50%) scale(0);
    background: radial-gradient(circle closest-side, var(--brand-soft), color-mix(in srgb, var(--brand) 0%, transparent));
    animation: v2GlassRipple .55s var(--ease) forwards;
}

@keyframes v2GlassRipple {
    to { transform: translate(-50%, -50%) scale(1); opacity: 0; }
}

.ui .tabbar-label {
    font-size: 13px;
    font-weight: 600;
    color: inherit;
}

/* ------------------------------------------------------------------- */
/* Recorder — signature 6-state widget                                  */
/* ------------------------------------------------------------------- */

.ui .recorder {
    background: var(--surface);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-card);
    padding: 24px 20px;
    box-shadow: var(--elev-1);
}

.ui .recorder-panel {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    text-align: center;
}

/* recorder_controller.js toggles panels via the native `hidden` property;
   without this override the flex rule above would win over the UA
   stylesheet's `[hidden] { display: none }` and every panel would show. */
.ui .recorder-panel[hidden] {
    display: none;
}

.ui .recorder-panel-idle {
    padding: 12px 0;
}

/* The mic is the amber lamp: warm glow instead of a drop shadow. */
.ui .recorder-mic {
    width: 84px;
    height: 84px;
    border-radius: 50%;
    background: var(--brand);
    color: var(--ink-on-brand);
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 0 32px color-mix(in srgb, var(--brand) 35%, transparent), var(--elev-1);
}

.ui .recorder-mic:active {
    transform: scale(.96);
}

.ui .recorder-hint {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .recorder-waves {
    display: flex;
    align-items: center;
    gap: 3px;
    height: 36px;
}

.ui .recorder-waves span {
    display: inline-block;
    width: 3px;
    height: 100%;
    border-radius: var(--radius-pill);
    background: var(--ink-3);
    animation: v2Wave .9s ease-in-out infinite;
}

.ui .recorder-waves span.is-accent {
    background: var(--brand);
}

.ui .recorder-timer {
    font-size: 24px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.ui .recorder-timer small {
    font-size: 14px;
    font-weight: 600;
    color: var(--ink-2);
    margin-left: 4px;
}

.ui .recorder-stop {
    width: 68px;
    height: 68px;
    border-radius: 50%;
    background: var(--brand-soft);
    display: flex;
    align-items: center;
    justify-content: center;
    animation: v2Pulse 1.4s ease-in-out infinite;
}

.ui .recorder-stop-square {
    width: 22px;
    height: 22px;
    border-radius: 6px;
    background: var(--brand);
}

.ui .recorder-recorded-actions {
    display: flex;
    gap: 10px;
    width: 100%;
}

.ui .recorder-recorded-actions .btn {
    flex: 1;
}

.ui .recorder-spinner {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 3.5px solid var(--brand-soft);
    border-top-color: var(--brand);
    animation: v2Spin 1s linear infinite;
}

.ui .recorder-uploading-title {
    font-size: 15px;
    font-weight: 600;
}

.ui .recorder-uploading-sub {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .recorder-error-card {
    background: color-mix(in srgb, var(--err) 16%, transparent);
    color: var(--err);
    border-radius: var(--radius-tile);
    padding: 14px 16px;
    width: 100%;
}

.ui .recorder-error-message {
    font-size: 14px;
    font-weight: 600;
}

.ui .recorder-offline-card {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--surface-2);
    border: 1px solid var(--glass-border);
    color: var(--ink);
    border-radius: var(--radius-tile);
    padding: 14px 16px;
    width: 100%;
    text-align: left;
}

.ui .recorder-tries {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 16px;
}

/* ------------------------------------------------------------------- */
/* Stage — full-frame cinematic background (components/Stage.html.twig) */
/* ------------------------------------------------------------------- */

/* The blur data-URI (inline style) paints over --bg until the image loads. */
.ui .stage {
    position: relative;
    height: 100%;
    overflow: hidden;
    background-color: var(--bg);
    background-size: cover;
    background-position: center;
}

.ui .stage-media {
    display: block;
    position: absolute;
    inset: 0;
}

.ui .stage-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.ui .stage-overlay {
    position: absolute;
    inset: 0;
    background: var(--scene-overlay);
    pointer-events: none;
}

.ui .stage-content {
    position: relative;
    z-index: 1;
    height: 100%;
}

/* Stage chrome shared by every screen drawn over a place photo: the top
   row (close button, place chip), the glass place chip and Nora's bust. */
.ui .scene-top {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
}

.ui .scene-top-right {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.ui .scene-place {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 40px;
    padding: 0 16px;
    border-radius: var(--radius-pill);
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(var(--glass-blur));
    backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
    font-size: 14px;
    font-weight: 600;
    color: var(--ink);
    white-space: nowrap;
    min-width: 0;
}

.ui .scene-place-sm {
    height: 32px;
    padding: 0 14px;
    font-size: var(--t-5-size);
}

.ui .scene-place-label {
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Nora in bust, 4:5, blended into the scene's grade: the portrait fades out
   towards the bottom instead of sitting in a square tile. */
.ui .scene-nora {
    position: relative;
    width: 150px;
    aspect-ratio: 4 / 5;
    flex-shrink: 0;
    -webkit-mask-image: radial-gradient(ellipse 50% 62% at 58% 42%, #000 40%, transparent 74%);
    mask-image: radial-gradient(ellipse 50% 62% at 58% 42%, #000 40%, transparent 74%);
    filter: saturate(.85) sepia(.15) contrast(1.05);
}

/* Shot inside the place itself (same room, same lamps): no grade to match,
   only the bottom edge to dissolve, like the guest. */
.ui .scene-nora.is-in-place {
    -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 58%, transparent 100%), linear-gradient(90deg, transparent 0%, #000 26%, #000 74%, transparent 100%);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 58%, transparent 100%), linear-gradient(90deg, transparent 0%, #000 26%, #000 74%, transparent 100%);
    mask-composite: intersect;
    filter: none;
}

.ui .scene-nora-poster,
.ui .scene-nora-video {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* ------------------------------------------------------------------- */
/* Coach — Nora poster + clip layers (components/Coach.html.twig)       */
/* ------------------------------------------------------------------- */

.ui .coach {
    position: relative;
    aspect-ratio: 4 / 5;
    overflow: hidden;
    border-radius: var(--radius-tile);
    background: var(--surface);
    box-shadow: var(--elev-1);
}

.ui .coach-sm { width: 88px; }
.ui .coach-md { width: 140px; }
.ui .coach-lg { width: 220px; }

.ui .coach-poster,
.ui .coach-video {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Duration must match CROSSFADE_MS in coach_controller.js. */
.ui .coach-video {
    opacity: 0;
    transition: opacity 300ms var(--ease);
}

.ui .coach-video.is-visible {
    opacity: 1;
}

/* DA: reduced motion never plays Nora's clips — the JS guard skips loading
   them, this rule guarantees nothing flashes even if one were mid-play. */
@media (prefers-reduced-motion: reduce) {
    .ui .coach-video {
        display: none;
    }
}

/* ------------------------------------------------------------------- */
/* Motion — the four DA gestures (docs/DA.md §Motion; the 5th, Nora's   */
/* clip, is a <video>, not a keyframe)                                  */
/* ------------------------------------------------------------------- */

/* 1. Push-in: slow scale on any full-frame image. */
.ui .anim-push-in {
    animation: v2PushIn 8s linear both;
}

@keyframes v2PushIn {
    from { transform: scale(1); }
    to { transform: scale(1.04); }
}

/* 2. Fade between full-frame screens. */
.ui .anim-fade-in {
    animation: v2FadeIn var(--dur-slow) var(--ease) both;
}

@keyframes v2FadeIn {
    from { opacity: 0; }
}

/* 3. ON AIR: the amber light turns on in 3 steps when the mic opens
   (fade back out over 1s by removing the class with a transition). */
.ui .on-air {
    animation: v2OnAirOn 400ms steps(3, jump-start) both;
}

@keyframes v2OnAirOn {
    from { opacity: .25; }
    to { opacity: 1; }
}

/* 4. Dim: while Nora speaks, UI outside her bubble drops to 60%. */
.ui .is-dimmed {
    animation: v2Dim var(--dur) var(--ease) both;
}

@keyframes v2Dim {
    to { opacity: .6; }
}

/* Component keyframes */

@keyframes v2Pulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.12); }
}

@keyframes v2Wave {
    0%, 100% { transform: scaleY(.35); }
    50% { transform: scaleY(1); }
}

@keyframes v2Spin {
    to { transform: rotate(360deg); }
}

@keyframes v2Blink {
    0%, 100% { opacity: 1; }
    50% { opacity: .45; }
}

@keyframes v2SlideUp {
    from { transform: translateY(24px); opacity: 0; }
    to { transform: translateY(0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    /* DA: fixed poster, no push-in, no fade. */
    .ui .anim-push-in,
    .ui .anim-fade-in {
        animation: none;
    }

    .ui .flash,
    .ui .flash-success,
    .ui .tabbar-glass-ripple,
    .ui .sheet,
    .ui .on-air,
    .ui .is-dimmed,
    .ui .recorder-waves span,
    .ui .recorder-stop,
    .ui .badge-transcribing,
    .ui .badge-analyzing {
        animation-duration: 1ms;
        animation-iteration-count: 1;
    }

    .ui .tabbar-tab:active,
    .ui .btn:active,
    .ui .btn-icon:active,
    .ui .recorder-mic:active {
        transform: none;
    }
}

/* ------------------------------------------------------------------- */
/* Home v2 — l'écran de parole (home/index.html.twig)               */
/* ------------------------------------------------------------------- */

.ui .home-v2 {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.ui .home-v2 .stage {
    flex: 1;
}

.ui .home-scene {
    min-height: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: calc(20px + env(safe-area-inset-top, 0px)) 20px calc(var(--tabbar-clearance) + env(safe-area-inset-bottom, 0px));
}

/* Nora's slot (decision 12): every non-scene screen opens with the same
   top-left bust — this shared rule is the only one that places her. */
.ui .screen-top {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
}

.ui .home-main {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: var(--space-5);
}

/* ON AIR lamp: visible but unlit by default; recording adds .is-on + the
   .on-air 3-step keyframe, removal fades it back out over 1 s. */
.ui .home-onair {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .14em;
    color: var(--brand-ink);
    text-shadow: var(--scene-text-shadow);
    opacity: .25;
    transition: opacity 1s var(--ease);
}

.ui .home-onair-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 12px color-mix(in srgb, var(--brand) 80%, transparent);
}

.ui .home-onair.is-on {
    opacity: 1;
    transition: none;
}

/* The scene darkens while recording; base transition so the release is soft. */
.ui .home-v2 .stage-img {
    transition: filter var(--dur) var(--ease);
}

.ui .home-v2.is-recording .stage-img {
    filter: brightness(.8);
}

/* The question, in Nora's voice: Fraunces italic, revealed word by word. */
.ui .home-question-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
}

.ui .home-question {
    flex: 1;
    font-size: var(--t-1-size);
    line-height: var(--t-1-line);
    text-shadow: var(--scene-text-shadow);
}

.ui .home-question-word {
    opacity: 0;
    transition: opacity 240ms var(--ease);
}

.ui .home-question-word.is-shown {
    opacity: 1;
}

.ui .home-question-replay {
    flex-shrink: 0;
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
}

.ui .home-ask {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.ui .home-words {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
}

/* The recorder floats on the scene: no card chrome, bigger lamp-mic. */
.ui .home-v2 .recorder {
    background: none;
    border: 0;
    box-shadow: none;
    padding: 0;
}

.ui .home-v2 .recorder-mic {
    width: 88px;
    height: 88px;
}

/* After the take left for Nora, the ask zone gives way to "C'est fait". */
.ui .home-v2.is-sent .home-question-row,
.ui .home-v2.is-sent .home-words,
.ui .home-v2.is-sent .home-scene-card,
.ui .home-v2.is-sent .home-extra-before,
.ui .home-v2.is-sent .recorder-panel-idle {
    display: none;
}

.ui .recorder-retake {
    display: flex;
    justify-content: center;
    margin-top: var(--space-2);
}

.ui .recorder-target-hint {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .recorder-ring {
    display: inline-flex;
    margin-right: 6px;
}

.ui .recorder-ring svg {
    display: block;
    transform: rotate(-90deg);
}

.ui .recorder-ring-track {
    fill: none;
    stroke: var(--ink-3);
    stroke-width: 3.5;
}

.ui .recorder-ring-fill {
    fill: none;
    stroke: var(--brand);
    stroke-width: 3.5;
    stroke-linecap: round;
    stroke-dasharray: 100;
    stroke-dashoffset: 100;
    transition: stroke-dashoffset 1s linear;
}

.ui .recorder-ring.is-reached .recorder-ring-fill {
    filter: drop-shadow(0 0 6px color-mix(in srgb, var(--brand) 60%, transparent));
}

/* Real waveform: the JS meter drives the bars, the CSS loop steps aside. */
.ui .recorder-waves.is-live span {
    animation: none;
    transform: scaleY(.15);
}

/* "C'est fait. Jour N." — the reward, held, no confetti. */
.ui .home-fait,
.ui .home-done {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
}

.ui .home-done-line {
    font-size: var(--t-1-size);
    line-height: var(--t-1-line);
    font-weight: 600;
    color: var(--brand-ink);
    text-shadow: var(--scene-text-shadow);
}

.ui .home-done-sub {
    color: var(--ink-2);
}

.ui .home-extra-link {
    margin-top: var(--space-2);
}

/* Before the day's take: night glass, small, so it reads as a control without
   taking the amber the microphone owns. */
.ui .home-extra-before {
    display: flex;
    width: fit-content;
    margin: var(--space-3) auto 0;
}

.ui .home-queued-note,
.ui .home-budget-note {
    font-size: var(--t-5-size);
    color: var(--ink-2);
    text-align: center;
}

/* Wednesday: the day's core is a scene, said as an invitation, not a block. */
.ui .home-scene-card {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 14px 16px;
    border-radius: var(--radius-tile);
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
}

.ui .home-scene-card-label {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .home-scene-card-title {
    font-weight: 600;
}

.ui .home-scene-card-cta {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--brand-ink);
}

/* Sunday — sober, no mic (the real weekly review lands in E7). */
.ui .home-sunday {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.ui .home-sunday-title {
    font-size: var(--t-display-size);
    line-height: var(--t-display-line);
}

.ui .home-sunday-text {
    color: var(--ink-2);
    max-width: 30ch;
}

/* Bottom strip: day line, week tiles, tomorrow's teaser. */
.ui .home-bottom {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    border-top: 1px solid var(--glass-border);
    padding-top: var(--space-4);
}

.ui .home-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

.ui .home-dayline {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--brand-ink);
    white-space: nowrap;
}

.ui .home-week {
    display: flex;
    gap: 4px;
    max-width: 100%;
}

.ui .home-teaser {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

/* The day's tries over the scene: glass rows, readable feedback. */
.ui .home-tries {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-top: var(--space-4);
    text-align: left;
}

.ui .home-tries:empty {
    display: none;
}

.ui .home-tries .speaking-try {
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-tile);
    padding: 12px 14px;
}

.ui .speaking-try-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-bottom: 6px;
}

.ui .speaking-try-time {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .speaking-progress {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .speaking-spinner {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid var(--brand-soft);
    border-top-color: var(--brand);
    animation: v2Spin 1s linear infinite;
    flex-shrink: 0;
}

.ui .speaking-queued p,
.ui .speaking-failed p {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .speaking-failed {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    align-items: flex-start;
}

.ui .speaking-feedback-details summary {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--ink-2);
    cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
    /* Fixed poster world: words appear at once, the lamp snaps, no dim ease. */
    .ui .home-question-word,
    .ui .home-onair,
    .ui .home-v2 .stage-img,
    .ui .recorder-ring-fill {
        transition: none;
    }
}

/* ------------------------------------------------------------------- */
/* Clôture (ritual/done.html.twig) — même squelette que la home     */
/* ------------------------------------------------------------------- */

.ui .ritual-done {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.ui .ritual-done .stage {
    flex: 1;
}

/* Definite height for the scene (min-height: 100% resolves against the
   absolute box), and room to scroll once the extras unfold. */
.ui .ritual-done .stage-content {
    position: absolute;
    inset: 0;
    overflow-y: auto;
}

/* No tab bar on the closing screen: the scene runs to the bottom edge. */
.ui .ritual-done .home-scene {
    padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px));
}

.ui .done-keeper {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.ui .done-keeper-label {
    color: var(--ink-2);
}

.ui .done-keeper-word {
    font-size: var(--t-2-size);
    line-height: var(--t-2-line);
    color: var(--ink);
}

.ui .done-more {
    background: none;
    border: 0;
    color: var(--ink-2);
    font: inherit;
    font-size: var(--t-4-size);
    padding: 10px;
    cursor: pointer;
    align-self: center;
}

.ui .done-extras {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.ui .done-extra {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: 14px 16px;
    border-radius: var(--radius-tile);
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
    color: var(--ink);
    text-decoration: none;
}

.ui .done-extra.is-done {
    opacity: .55;
}

.ui .done-extra-min {
    font-size: var(--t-5-size);
    color: var(--ink-2);
    white-space: nowrap;
}

/* Milestone moment on the closing screen: held, no confetti. */
.ui .milestone {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

.ui .milestone-kicker {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--brand-ink);
}

.ui .milestone-phrase {
    font-size: var(--t-2-size);
    line-height: var(--t-2-line);
    color: var(--ink);
}

/* ------------------------------------------------------------------- */
/* Take feedback (take/feedback.html.twig)                          */
/* ------------------------------------------------------------------- */

.ui .take-feedback {
    gap: var(--space-5);
}

.ui .take-opener {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.ui .take-opener-text {
    font-size: var(--t-2-size);
    line-height: var(--t-2-line);
    color: var(--ink);
}

.ui .take-star {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.ui .take-star-said {
    position: relative;
    display: inline-block;
    color: var(--ink-2);
    font-size: var(--t-3-size);
}

/* The strike is a pseudo-element bar rather than text-decoration: a solid
   background scales more reliably across browsers than animating the width
   of a line-through. .is-struck is added by the controller after mount. */
.ui .take-star-said::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 2px;
    background: var(--err);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--dur-slow) var(--ease);
}

.ui .take-star-said.is-struck::after {
    transform: scaleX(1);
}

.ui .take-star-better {
    font-size: var(--t-2-size);
    line-height: var(--t-2-line);
}

.ui .take-star-better mark,
.ui .take-fold-better mark,
.ui .take-fold-vocab-item mark {
    background: var(--brand-soft);
    color: var(--brand-ink);
    padding: 0 4px;
    border-radius: 6px;
}

.ui .take-star-why {
    color: var(--ink-2);
    font-size: var(--t-4-size);
}

.ui .take-fold {
    border-top: 1px solid var(--glass-border);
    padding-top: var(--space-3);
}

.ui .take-fold .fold-toggle {
    justify-content: flex-start;
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--ink-2);
}

.ui .take-fold-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin-top: var(--space-3);
}

.ui .take-fold-item {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.ui .take-fold-said {
    color: var(--ink-2);
}

.ui .take-fold-why {
    color: var(--ink-2);
    font-size: var(--t-5-size);
}

.ui .take-fold-vocab-item {
    font-size: var(--t-4-size);
}

.ui .take-keeper {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-4);
    border-radius: var(--radius-card);
    background: var(--surface);
    border: 1px solid var(--glass-border);
}

.ui .take-keeper-text {
    font-size: var(--t-3-size);
    color: var(--ink);
}

.ui .take-keeper-btn.is-kept {
    opacity: .6;
    pointer-events: none;
}

.ui .take-keeper-error {
    font-size: var(--t-5-size);
    color: var(--err);
}

.ui .take-revanche {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-4);
    border-radius: var(--radius-card);
    background: var(--brand-soft);
}

.ui .take-revanche-kicker {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--brand-ink);
}

.ui .take-revanche-text {
    color: var(--ink);
}

.ui .take-revanche-cta {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--brand-ink);
}

@media (prefers-reduced-motion: reduce) {
    .ui .take-star-said::after {
        transition: none;
    }
}

/* ------------------------------------------------------------------- */
/* Install / push ask — day-1 closing screen (E4.2, ritual/done.html.twig) */
/* ------------------------------------------------------------------- */

.ui .done-install {
    position: relative;
    display: flex;
    padding: 14px 16px;
    border-radius: var(--radius-tile);
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
}

.ui .done-install-dismiss {
    position: absolute;
    top: 8px;
    right: 8px;
    color: var(--ink-3);
}

.ui .done-install-ask,
.ui .done-install-push {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
    width: 100%;
    padding-right: 24px;
}

.ui .done-install-title {
    font-size: var(--t-4-size);
    line-height: var(--t-4-line);
    color: var(--ink);
}

.ui .done-install-ios {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .done-install-btn {
    align-self: flex-start;
}

.ui .done-install-push-label {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.ui .done-install-push-message {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .done-install-push .toggle {
    align-self: flex-start;
}

/* Toggle switch (components/Toggle.html.twig) has no v2 styling yet — only
   this screen uses it under the dark theme so far. */
.ui .toggle {
    width: 52px;
    height: 32px;
    border-radius: var(--radius-pill);
    background: var(--ok);
    position: relative;
    flex-shrink: 0;
    transition: background var(--dur) var(--ease);
}

.ui .toggle.is-off {
    background: var(--glass-border);
}

.ui .toggle-knob {
    position: absolute;
    top: 3px;
    left: 3px;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: #fff;
    transition: transform var(--dur) var(--ease);
}

.ui .toggle:not(.is-off) .toggle-knob {
    transform: translateX(20px);
}

@media (prefers-reduced-motion: reduce) {
    .ui .toggle,
    .ui .toggle-knob {
        transition: none;
    }
}

/* ------------------------------------------------------------------- */
/* Sunday "Ta semaine" (E7.2, home/sunday.html.twig)                 */
/* ------------------------------------------------------------------- */

.ui .home-sunday-review {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
}

.ui .home-sunday-noraline {
    font-size: var(--t-2-size);
    line-height: var(--t-2-line);
    text-shadow: var(--scene-text-shadow);
}

.ui .home-sunday-numbers {
    color: var(--ink-2);
}

.ui .home-sunday-recordings {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.ui .home-sunday-recording {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.ui .home-sunday-recording-label {
    font-size: var(--t-5-size);
    font-weight: 600;
    color: var(--ink-2);
}

.ui .home-sunday-focus {
    color: var(--ink-2);
}

.ui .home-sunday-closing {
    font-size: var(--t-5-size);
    color: var(--ink-3);
}

/* Monday: last week's focus, once, as a quiet line under the question. */
.ui .home-monday-focus {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

/* ------------------------------------------------------------------- */
/* Invitation door — the public "porte" (invitation/welcome.html.twig) */
/* ------------------------------------------------------------------- */

.ui .door-screen {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.ui .door-screen .stage {
    flex: 1;
}

.ui .door-scene {
    min-height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-6);
    padding: calc(var(--space-8) + env(safe-area-inset-top, 0px)) var(--space-5) calc(var(--space-7) + env(safe-area-inset-bottom, 0px));
    text-align: center;
}

.ui .door-bottom {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-4);
    width: 100%;
    max-width: 320px;
}

.ui .door-subtitle {
    font-size: var(--t-1-size);
    line-height: var(--t-1-line);
    font-weight: 600;
    color: var(--brand-ink);
    text-shadow: var(--scene-text-shadow);
}

.ui .door-subtitle-calm {
    font-size: var(--t-3-size);
    line-height: var(--t-3-line);
    font-weight: 500;
    color: var(--ink-2);
    text-shadow: none;
}

.ui .door-cta {
    width: 100%;
}

/* A nudge, not a wall: the address can be confirmed whenever (E5.2). */
.ui .verify-nudge {
    display: block;
    margin: 8px 16px 0;
    padding: 10px 14px;
    border-radius: var(--radius-tile);
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    border: 1px solid var(--glass-border);
    color: var(--ink-2);
    font-size: var(--t-5-size);
}

/* A failed take offers the microphone first; relaunching the same audio is the quiet second choice. */
.ui .speaking-failed-actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.ui .speaking-failed-relaunch {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    font-size: var(--t-5-size);
    color: var(--ink-2);
    text-decoration: underline;
    cursor: pointer;
}

/* ------------------------------------------------------------------- */
/* Fold — animated disclosure (components/Fold.html.twig)               */
/* ------------------------------------------------------------------- */

/* grid-template-rows 0fr→1fr animates to the content's real height without
   any JS measurement (a measured height goes stale when the content reflows);
   the delayed visibility flip keeps the closed region out of the a11y tree
   and tab order — [hidden] would kill the transition. */
.ui .fold {
    --fold-dur: 280ms;
    display: flex;
    flex-direction: column;
}

.ui .fold-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

.ui .fold-chevron {
    flex-shrink: 0;
    transition: transform var(--fold-dur) var(--ease);
}

.ui .fold.is-open .fold-chevron {
    transform: rotate(180deg);
}

.ui .fold-region {
    display: grid;
    grid-template-rows: 0fr;
    visibility: hidden;
    transition: grid-template-rows var(--fold-dur) var(--ease), visibility 0s var(--fold-dur);
}

.ui .fold.is-open .fold-region {
    grid-template-rows: 1fr;
    visibility: visible;
    transition: grid-template-rows var(--fold-dur) var(--ease), visibility 0s;
}

.ui .fold-content {
    min-height: 0;
    overflow: hidden;
    opacity: 0;
    transform: translateY(6px);
    transition: opacity var(--fold-dur) var(--ease), transform var(--fold-dur) var(--ease);
}

.ui .fold.is-open .fold-content {
    opacity: 1;
    transform: none;
}

@media (prefers-reduced-motion: reduce) {
    .ui .fold-region,
    .ui .fold-content,
    .ui .fold-chevron {
        transition: none;
    }
}

/* ------------------------------------------------------------------- */
/* Stage pinned to the viewport (<twig:Stage :fixed="true">)            */
/* ------------------------------------------------------------------- */

/* Scrolling screens drawn over a place photo: the photo must not resize when
   the page grows (a fold opening). iOS Safari ignores background-attachment:
   fixed, so the media itself goes position: fixed; max-width + auto margins
   re-center it in the 430px app column — a translateX would be overridden by
   the anim-push-in transform animation. */
.ui .stage-fixed .stage-media,
.ui .stage-fixed .stage-overlay {
    position: fixed;
    max-width: 430px;
    margin: 0 auto;
}

/* ------------------------------------------------------------------- */
/* Feuille Extras (chantier "feuille Extras + Chauffe", 2026-08-26)     */
/* components/ExtrasSheet.html.twig — reuses .sheet/.sheet-overlay/     */
/* .sheet-handle/.sheet-title, glass panel and item rows here.          */
/* ------------------------------------------------------------------- */

/* The wrapping div carries the controller only — it must not add a box to
   whichever flex row (.home-done-actions, .cta-bar) the trigger sits in. */
.ui .extras-sheet {
    display: contents;
}

/* .sheet is shared with other sheets (conversation scene/configure) and
   stays opaque there — only this panel is glass. */
.ui .extras-sheet-panel {
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
}

.ui .extras-sheet-panel .sheet-handle {
    touch-action: none;
    cursor: grab;
}

.ui .extras-sheet-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.ui .extras-sheet-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.ui .extras-sheet-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 14px 16px;
    border-radius: var(--radius-tile);
    background: var(--surface-2);
    border: 1px solid var(--glass-border);
    color: var(--ink);
    text-decoration: none;
}

.ui .extras-sheet-item.is-done {
    opacity: .55;
}

.ui .extras-sheet-item-icon {
    flex-shrink: 0;
    display: flex;
    color: var(--brand-ink);
}

.ui .extras-sheet-item-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.ui .extras-sheet-item-name {
    font-weight: 600;
}

.ui .extras-sheet-item-phrase {
    font-size: var(--t-5-size);
    color: var(--ink-2);
}

.ui .extras-sheet-item-min {
    flex-shrink: 0;
    font-size: var(--t-5-size);
    color: var(--ink-2);
    white-space: nowrap;
}
