body:has(main > practice-page-frame) {
    display: flex;
    min-height: 100vh;
    flex-direction: column;
    /* Set on body, not html, so the tint also paints the body margin the
       practice pages still carry from main.css. */
    background: var(--surface-page);
}

/*
    A practice page has no sidebar (ADR-0033), so it also has no inset to be
    pushed by: the margin main.css and header.css set on every body is dropped
    here, and the Frame's own edge padding is the page's only gutter. The
    standalone tool keeps the old tint and the old margin, because it keeps the
    old chrome.
*/
body:has(main > practice-page-frame:not([chrome="rail"])) {
    margin: 0;
    background: var(--chrome-canvas);
}

/* Also a container: the Frame element cannot answer its own @container
   query, so rules that must restyle the Frame element itself (the
   chrome="rail" stacking below) query this host instead. */
main:has(> practice-page-frame) {
    display: flex;
    flex: 1;
    flex-direction: column;
    container-name: practice-frame-host;
    container-type: inline-size;
}

/* On :root rather than on the element, so page furniture that sits outside the
   Frame can line up with the rail without copying its measurements. The Frame
   inherits these unchanged; it is still the only thing that decides how the
   rail is applied. */
:root {
    --practice-rail-width: 1440px;
    --practice-gutter: clamp(16px, 3vw, 48px);
    /* The flanked tier's middle column and the room around it. The cap is
       generous on purpose: what actually decides the column's width is how
       much the identity cluster and the timer take from the window. */
    --practice-chrome-column: 1700px;
    --practice-chrome-gap: 18px;
    --practice-chrome-edge: 18px;
    /* The stacked tier's edge, which the very narrow tier halves again. */
    --practice-chrome-edge-stacked: 16px;
    /* chrome="rail" only: the narrow tool sidebar, not the centered column
       above ("rail" predates the sidebar variant and means the max-width). */
    --practice-tool-rail-width: 112px;
}

practice-page-frame {
    box-sizing: border-box;
    container-name: practice-frame;
    container-type: inline-size;
    display: flex;
    flex: 1;
    flex-direction: column;
    width: 100%;
}

practice-page-frame > [data-frame-region] {
    box-sizing: border-box;
    width: min(var(--practice-rail-width), calc(100% - (2 * var(--practice-gutter))));
    margin-right: auto;
    margin-left: auto;
}

practice-page-frame > .exercise-header {
    display: flex;
    min-height: 34px;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    padding: 0 0 14px;
}

practice-page-frame .exercise-header-badges {
    display: flex;
    align-items: stretch;
    gap: 12px;
}

/* "Which Task, which exercise" is one question, so the chooser and the
   exercise selector sit together at the leading edge and the timer keeps the
   trailing edge to itself. */
practice-page-frame .exercise-header-lead {
    display: flex;
    min-width: 0;
    align-items: center;
    gap: 12px;
}

/* ── The flanked tier (ADR-0033) ──────────────────────────────────────────
   The header row and the content are one grid, not two stacked ones, because
   they have to agree on where the middle column's edges are: the identity
   cluster and the Task Timer flank a capped content column, and the Task bar
   has to line up with the Task Surfaces beneath it to the pixel. A separate
   header grid could only guess at a width the flanks have already taken from
   it, so the header is a subgrid of the Frame's own three columns.

   This is the widest of four tiers; the other three are at the bottom of this
   file, in the order a window narrows through them (#605). */
practice-page-frame:not([chrome="rail"]) {
    display: grid;
    grid-template-columns:
        minmax(0, auto)
        minmax(0, var(--practice-chrome-column))
        minmax(0, auto);
    align-content: start;
    justify-content: center;
    column-gap: var(--practice-chrome-gap);
    row-gap: 18px;
    padding: 14px var(--practice-chrome-edge) 56px;
}

/* The grid's own row gap is the space between the bar and the content, so the
   workspace stops carrying one of its own. */
practice-page-frame:not([chrome="rail"]) > .task-workspace {
    margin-top: 0;
    margin-bottom: 0;
}

practice-page-frame:not([chrome="rail"]) > [data-frame-region] {
    width: auto;
    margin: 0;
    grid-column: 2;
}

practice-page-frame:not([chrome="rail"]) > .exercise-header {
    display: grid;
    min-height: 0;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    align-items: center;
    /* Restated, not inherited: a subgrid uses its OWN gap, and the 24px the
       old header row carried left the Task bar 3px inside the column its
       content sits in — near enough to look right and far enough to be
       wrong. */
    gap: 0 var(--practice-chrome-gap);
    padding: 0;
}

:is(practice-page-frame, practice-chrome) .practice-identity {
    display: flex;
    min-width: 0;
    align-items: center;
    gap: clamp(8px, 1.2cqi, 14px);
}

/* The middle column of the header row: the Task bar, and beside it the ☰
   PRACTICE button the prototype draws between the bar and the timer. Both
   overlays hang from this box, which is why it is the positioned one — the
   sheet spans the bar and the button together, as drawn.

   Above the scrim (40), because the column carries both overlays' triggers:
   underneath it, the only thing a click on the other trigger could reach is
   the scrim, and switching between the Questions panel and the Practice sheet
   would take two clicks and a dismissal — leaving the rule that only one is
   open with nothing to arbitrate (#601). The button moving out of the bar does
   not change that: it moved into this box, which is what carries the z-index
   now. */
:is(practice-page-frame, practice-chrome) .task-bar-column {
    position: relative;
    z-index: 45;
    display: flex;
    min-width: 0;
    align-items: center;
    gap: clamp(8px, 1.2cqi, 14px);
}

/* The Task bar. One card carrying the four things a learner needs before
   answering — which Exercise, which Task, which Skill, what is asked — with
   the instruction line on its own row beneath the rest. Positioned, because
   the Questions panel drops out of it (ADR-0033). */
practice-page-frame .task-bar {
    box-sizing: border-box;
    position: relative;
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    flex-direction: column;
    gap: 2px;
    padding: 8px clamp(11px, 1.4cqi, 18px);
    border: 1px solid var(--chrome-line);
    border-radius: var(--chrome-radius-bar);
    background: var(--surface);
    box-shadow: var(--chrome-shadow-bar);
}

/* The bar's own line is the way into the Questions panel, so the Task name is
   inside a control rather than beside one. It wears no button furniture: the
   card is the affordance and the chevron is the cue. */
practice-page-frame .task-bar-trigger {
    display: flex;
    min-width: 0;
    flex: 0 1 auto;
    align-items: center;
    padding: 0;
    border: 0;
    margin: 0;
    background: none;
    color: inherit;
    cursor: pointer;
    gap: 8px;
    font: inherit;
    text-align: left;
}

practice-page-frame .task-bar-trigger:hover .task-bar-name {
    color: var(--chrome-accent-deep);
}

practice-page-frame .task-bar-chevron {
    display: inline-flex;
    flex: none;
    color: var(--chrome-line-strong);
    transition: transform 0.15s ease-out;
}

practice-page-frame .task-bar-trigger[aria-expanded="true"] .task-bar-chevron {
    color: var(--chrome-accent);
    transform: rotate(180deg);
}

practice-page-frame .task-bar-line {
    display: flex;
    min-width: 0;
    align-items: center;
    gap: clamp(8px, 1.2cqi, 14px);
}

practice-page-frame .task-bar-name {
    overflow: hidden;
    /* Shrinks to an ellipsis rather than growing: the Skill badge belongs
       beside the name it qualifies, not at the far end of the bar. */
    min-width: 0;
    flex: 0 1 auto;
    margin: 0;
    color: var(--chrome-ink);
    font-size: clamp(15px, 2cqi, 20px);
    font-weight: 700;
    letter-spacing: -0.4px;
    line-height: 1.15;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Inside the bar the instruction line is a line, not an aside: the leading
   rule and the block padding the standalone region wears would draw a second
   box inside the one it is already in. */
practice-page-frame .task-bar > .instructions {
    width: auto;
    margin: 0;
    padding: 0;
    border: 0;
    color: var(--chrome-ink-muted);
    font-size: clamp(12px, 1.3cqi, 13px);
    font-weight: 700;
    line-height: 1.4;
    text-wrap: pretty;
}

practice-page-frame:not([chrome="rail"]) .exercise-header-badges {
    justify-self: end;
}

/* ── The overlay layer (ADR-0033) ─────────────────────────────────────────
   One scrim, and surfaces anchored to the Task bar's column rather than to
   the window: an overlay that spans the viewport re-teaches the learner where
   the content edge is every time it opens. The Questions panel is the first;
   the Practice sheet (#601) joins it above the same scrim. */
.chrome-scrim {
    position: fixed;
    z-index: 40;
    background: var(--chrome-scrim);
    inset: 0;
}

practice-page-frame .questions-panel {
    position: absolute;
    z-index: 50;
    top: calc(100% + 8px);
    right: 0;
    left: 0;
    box-sizing: border-box;
    padding: 12px clamp(11px, 1.4cqi, 16px) 14px;
    border: 1px solid var(--chrome-accent);
    border-radius: var(--chrome-radius-bar);
    background: var(--chrome-panel);
    box-shadow: var(--chrome-shadow-overlay);
}

/* Chips, then "Go to #", then the way out — one wrapping row, so the panel's
   controls read left to right in the order a learner reaches for them. */
practice-page-frame .questions-panel-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: 8px;
    gap: 6px;
}

practice-page-frame .questions-panel-filters {
    display: flex;
    min-width: 0;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}

practice-page-frame .questions-filter {
    padding: 7px 12px;
    border: 1.5px solid var(--chrome-line);
    border-radius: var(--pill);
    background: var(--surface);
    color: var(--chrome-ink-soft);
    cursor: pointer;
    font-size: 11px;
    font-weight: 700;
    white-space: nowrap;
}

practice-page-frame .questions-filter[data-filter="doing"] {
    border-style: dashed;
    border-color: var(--chrome-line-muted);
    color: var(--chrome-accent-deep);
}

practice-page-frame .questions-filter[data-filter="done"] {
    border-color: var(--score-pill-strong-line);
    color: var(--score-pill-strong-ink);
}

/* The chosen filter is the one the grid is currently obeying, so it is filled
   rather than merely outlined, and keeps its own family's colour. */
practice-page-frame .questions-filter[aria-pressed="true"] {
    border-color: var(--chrome-accent);
    background: var(--chrome-accent);
    color: var(--surface);
}

practice-page-frame .questions-filter[data-filter="doing"][aria-pressed="true"] {
    border-color: var(--chrome-accent);
    background: var(--chrome-accent-tint);
    color: var(--chrome-accent-deep);
}

practice-page-frame .questions-filter[data-filter="not-done"][aria-pressed="true"] {
    border-color: var(--chrome-line-strong);
    background: var(--surface);
    color: var(--chrome-ink);
}

practice-page-frame .questions-filter[data-filter="done"][aria-pressed="true"] {
    border-color: var(--score-pill-strong-line);
    background: var(--score-pill-strong-bg);
    color: var(--score-pill-strong-ink);
}

practice-page-frame .questions-panel-go-to {
    display: flex;
    flex: none;
    align-items: center;
    padding: 4px 12px;
    border: 1.5px solid var(--chrome-line);
    border-radius: var(--pill);
    background: var(--surface);
    gap: 6px;
}

practice-page-frame .questions-panel-go-to input {
    width: 72px;
    min-height: 28px;
    border: 0;
    background: none;
    color: var(--chrome-ink);
    font: inherit;
    font-size: 12px;
}

practice-page-frame .questions-panel-go-to input[aria-invalid="true"] {
    color: var(--chrome-alert);
}

:is(practice-page-frame, practice-chrome) .chrome-overlay-close {
    display: grid;
    width: 28px;
    height: 28px;
    border: 1.5px solid var(--chrome-line-strong);
    border-radius: var(--pill);
    background: var(--surface);
    color: var(--chrome-ink-soft);
    cursor: pointer;
    font-size: 13px;
    line-height: 1;
    margin-inline-start: auto;
    place-items: center;
}

practice-page-frame .questions-panel-grid {
    display: flex;
    /* Viewport height, deliberately: the chrome measures widths in container
       units (STYLE_GUIDE), and what caps this box is how much screen there is
       below the bar, which no container knows — the Frame is an inline-size
       container and has no block size to ask about. */
    max-height: 40vh;
    flex-wrap: wrap;
    padding: 8px;
    border: 1px solid var(--chrome-line);
    border-radius: var(--chrome-radius-card);
    background: var(--surface);
    gap: 5px;
    overflow-y: auto;
}

practice-page-frame .question-pill {
    min-width: 42px;
    min-height: 34px;
    padding: 6px 8px;
    border: 1.5px solid var(--chrome-line);
    border-radius: var(--chrome-radius-pill);
    background: var(--surface);
    color: var(--chrome-ink-muted);
    cursor: pointer;
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    text-align: center;
}

practice-page-frame .question-pill[data-state="doing"] {
    border-color: var(--chrome-accent);
    border-style: dashed;
    color: var(--chrome-accent-deep);
}

practice-page-frame .question-pill[data-state="done-strong"] {
    border-color: var(--score-pill-strong-line);
    background: var(--score-pill-strong-bg);
    color: var(--score-pill-strong-ink);
}

practice-page-frame .question-pill[data-state="done-weak"] {
    border-color: var(--score-pill-weak-line);
    background: var(--score-pill-weak-bg);
    color: var(--score-pill-weak-ink);
}

/* Where the learner is now, whatever it also is. */
practice-page-frame .question-pill[data-current="true"] {
    border-color: var(--chrome-accent);
    border-style: solid;
    background: var(--chrome-accent);
    color: var(--surface);
}

practice-page-frame .questions-panel-status {
    margin: 8px 0 0;
    color: var(--chrome-ink-muted);
    font-size: 12px;
    font-weight: 600;
}

/* A number the bank does not have is said in the same line the filter count
   uses, so it is read where the learner is already looking. */
practice-page-frame .questions-panel:has([aria-invalid="true"]) .questions-panel-status {
    color: var(--chrome-alert);
}

/* ── The Practice sheet (#601) ────────────────────────────────────────────
   The second surface on the overlay layer: the whole Task catalogue, dropping
   out of the ☰ PRACTICE button and anchored to the same column as the
   Questions panel, so the two open in the same place and never at once. The
   columns are the flanked tier's layout; the tabbed one, and the sheet that
   fixes itself to the window once its column stops being a box, are at the
   bottom of this file with the other tiers (#605). */
:is(practice-page-frame, practice-chrome) .practice-sheet {
    position: absolute;
    z-index: 50;
    top: calc(100% + 8px);
    right: 0;
    left: 0;
    box-sizing: border-box;
    /* Viewport height, for the reason the Questions panel's grid states: the
       chrome measures widths in container units, and what caps this box is how
       much screen there is below the bar — which the Frame, an inline-size
       container, has no block size to answer with. */
    max-height: 66vh;
    /* Right padding clears the ✕ that floats in the corner rather than sitting
       in a row of its own. */
    padding: 14px 48px 16px clamp(12px, 1.8cqi, 22px);
    border: 1px solid var(--chrome-accent);
    border-radius: var(--chrome-radius-bar);
    background: var(--chrome-sheet);
    box-shadow: var(--chrome-shadow-overlay);
    overflow-y: auto;
}

/* The way out floats in the sheet's corner instead of ending a title row: the
   prototype's sheet is one flat card of columns and nothing else, and the
   dialog is named by the overlay layer's aria-label. */
:is(practice-page-frame, practice-chrome) .practice-sheet-close {
    position: absolute;
    z-index: 2;
    top: 10px;
    right: 10px;
    margin-inline-start: 0;
}

/* One column per Skill, four across: the catalogue's widest shape, and the one
   the prototype holds from the flanked tier all the way down to 1100px. The
   count is stated rather than left to `auto-fit`, which was the drift the
   prototype comparison caught — auto-fit packs by whatever fits, so it drew
   three across on a 760px screen and put Listening under Speaking, which is
   neither the prototype's shape nor the order the Skills are read in. The two
   narrower counts are stated the same way, tier by tier, near the other
   #605 tiers at the bottom of this file. */
:is(practice-page-frame, practice-chrome) .practice-sheet-columns {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px clamp(12px, 1.6cqi, 22px);
}

/* No card of its own: the sheet is the card, and the columns are told apart by
   the whitespace between them and by the coloured rule under each name. Four
   boxes inside a box is the drift the prototype comparison caught. */
:is(practice-page-frame, practice-chrome) .practice-sheet-group {
    min-width: 0;
}

/* Each Skill wears its own colour twice — the dot before the name and the rule
   under it — from the accessible skill palette the Task bar's badge uses. The
   tab that stands for a column below 640px is the same claim in another shape,
   so it reads the same pair (#605). */
:is(practice-page-frame, practice-chrome) .practice-sheet-group[data-skill="speaking"],
:is(practice-page-frame, practice-chrome) .practice-sheet-tab[data-skill="speaking"] {
    --practice-sheet-skill: var(--skill-dot-speaking);
    --practice-sheet-skill-ink: var(--skill-label-speaking);
}

:is(practice-page-frame, practice-chrome) .practice-sheet-group[data-skill="writing"],
:is(practice-page-frame, practice-chrome) .practice-sheet-tab[data-skill="writing"] {
    --practice-sheet-skill: var(--skill-dot-writing);
    --practice-sheet-skill-ink: var(--skill-label-writing);
}

:is(practice-page-frame, practice-chrome) .practice-sheet-group[data-skill="reading"],
:is(practice-page-frame, practice-chrome) .practice-sheet-tab[data-skill="reading"] {
    --practice-sheet-skill: var(--skill-dot-reading);
    --practice-sheet-skill-ink: var(--skill-label-reading);
}

:is(practice-page-frame, practice-chrome) .practice-sheet-group[data-skill="listening"],
:is(practice-page-frame, practice-chrome) .practice-sheet-tab[data-skill="listening"] {
    --practice-sheet-skill: var(--skill-dot-listening);
    --practice-sheet-skill-ink: var(--skill-label-listening);
}

:is(practice-page-frame, practice-chrome) .practice-sheet-group-name {
    display: flex;
    align-items: center;
    padding: 0 0 6px;
    border-bottom: 2px solid var(--practice-sheet-skill, var(--chrome-line-muted));
    margin: 0 0 5px;
    color: var(--practice-sheet-skill-ink, var(--chrome-ink));
    font-size: 11.5px;
    font-weight: 800;
    gap: 7px;
    letter-spacing: 0.09em;
    text-transform: uppercase;
}

:is(practice-page-frame, practice-chrome) .practice-sheet-group-name .task-skill-dot {
    width: 8px;
    height: 8px;
    background: var(--practice-sheet-skill, var(--chrome-line-muted));
}

:is(practice-page-frame, practice-chrome) .practice-sheet-tasks {
    display: flex;
    flex-direction: column;
    padding: 0;
    margin: 0;
    gap: 2px;
    list-style: none;
}

:is(practice-page-frame, practice-chrome) .practice-sheet-task {
    display: flex;
    align-items: center;
    padding: 9px 10px;
    border-radius: var(--chrome-radius-pill);
    color: var(--chrome-ink-soft);
    font-size: 12.5px;
    font-weight: 600;
    gap: 8px;
    text-decoration: none;
}

:is(practice-page-frame, practice-chrome) .practice-sheet-task:hover {
    background: var(--chrome-accent-tint);
    color: var(--chrome-accent-deep);
}

/* The Task you are already on: named, not hidden. Choosing it again is a
   reload, which is a harmless thing to allow and a confusing thing to refuse
   silently.

   Outlined, not filled (the prototype): a filled row is how this sheet says
   "hover", and a solid purple block in the middle of a column reads as a
   selection being offered rather than as where the learner already is. The
   ring is an inset shadow so it costs no layout and the row does not shift by
   two pixels when it becomes the current one. */
:is(practice-page-frame, practice-chrome) .practice-sheet-task[aria-current="page"] {
    background: var(--surface);
    box-shadow: inset 0 0 0 2px var(--practice-sheet-skill, var(--chrome-accent));
    color: var(--chrome-ink);
}

:is(practice-page-frame, practice-chrome) .practice-sheet-task-name {
    min-width: 0;
    flex: 1 1 auto;
}

:is(practice-page-frame, practice-chrome) .practice-sheet-task-count {
    flex: none;
    padding: 2px 7px;
    border-radius: var(--pill);
    background: var(--chrome-canvas);
    color: var(--chrome-ink-muted);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
}

:is(practice-page-frame, practice-chrome) .practice-sheet-task[aria-current="page"] .practice-sheet-task-count {
    background: var(--chrome-accent-deep);
    color: var(--surface);
}

/* The way into the sheet: the standalone purple button the prototype draws
   between the Task bar and the timer (#597, #601). Filled rather than
   outlined, because it is the one control in the header row that leaves the
   Task you are on — the Skill badge beside it names a Skill and says nothing
   about where to go. */
:is(practice-page-frame, practice-chrome) .practice-trigger {
    display: flex;
    flex: none;
    align-items: center;
    padding: 13px 18px;
    border: 0;
    border-radius: var(--chrome-radius-bar);
    background: var(--chrome-accent);
    /* --surface is what the chrome already uses for ink on a filled purple
       (the pressed Questions filter, the current question pill). */
    color: var(--surface);
    cursor: pointer;
    font: inherit;
    font-size: 12px;
    font-weight: 800;
    gap: 8px;
    letter-spacing: 0.125em;
    line-height: 1;
    text-transform: uppercase;
}

:is(practice-page-frame, practice-chrome) .practice-trigger:hover,
:is(practice-page-frame, practice-chrome) .practice-trigger[aria-expanded="true"] {
    background: var(--chrome-accent-deep);
}

:is(practice-page-frame, practice-chrome) .practice-trigger-glyph {
    font-size: 14px;
    letter-spacing: 0;
}

:is(practice-page-frame, practice-chrome) .practice-trigger-chevron {
    display: inline-flex;
    flex: none;
    transition: transform 0.15s ease-out;
}

:is(practice-page-frame, practice-chrome) .practice-trigger[aria-expanded="true"] .practice-trigger-chevron {
    transform: rotate(180deg);
}

/* Phone portrait: the panel is a grid of small targets, and small is what a
   thumb cannot hit. The chrome keeps the prototype's scale where a pointer is
   assumed and gives it up where one is not. */
@container practice-frame (max-width: 480px) {
    practice-page-frame .question-pill {
        min-width: 44px;
        min-height: 44px;
    }

    practice-page-frame .questions-filter,
    :is(practice-page-frame, practice-chrome) .chrome-overlay-close {
        min-height: 44px;
    }

    /* A Task in the sheet is a row a thumb has to land on, and it is inside an
       overlay rather than in the bar — so it takes the 44px the trigger that
       opened it deliberately does not. */
    :is(practice-page-frame, practice-chrome) .practice-sheet-task {
        min-height: 44px;
    }

    /* The ☰ PRACTICE button stands on its own outside the bar, so unlike the
       bar's own furniture it can take the floor's full 44px without making the
       row it shares any taller than the card beside it already is. */
    :is(practice-page-frame, practice-chrome) .practice-trigger {
        min-height: 44px;
    }

    /* The tab that stands for a whole Skill's column below 640px is a thumb
       target for the same reason the Task rows under it are (#605). */
    :is(practice-page-frame, practice-chrome) .practice-sheet-tab {
        min-height: 44px;
    }

    :is(practice-page-frame, practice-chrome) .chrome-overlay-close {
        width: 44px;
    }
}

/* The card the prototype draws around a Task's content is the Task Surface
   itself; only its line, corner and lift change. */
practice-page-frame:not([chrome="rail"]) [data-task-surface] {
    border-color: var(--chrome-line);
    border-radius: var(--chrome-radius-card);
    box-shadow: var(--chrome-shadow-card);
}

practice-page-frame .task-timer {
    position: relative;
    box-sizing: border-box;
    display: grid;
    min-width: 112px;
    min-height: var(--header-chip-height);
    grid-template-columns: auto auto 5ch;
    align-items: center;
    gap: 8px;
    font-weight: 700;
    /* Border, radius, background and ink come from the shared header-chip
       family in header.css, which the timer wears in both chromes. */
}

practice-page-frame .task-timer:not(:has(.task-timer-state)) {
    grid-template-columns: auto 5ch;
}

/*
    The flanked tier's timer is a pill with a ring: the arc is the same
    --task-timer-progress the strip below draws, in the one place a glance
    already goes. What the arc means is settled in #602: a countdown depletes
    it, an elapsed clock sweeps it through the minute, and only a countdown
    can turn it red.
*/
practice-page-frame:not([chrome="rail"]) .task-timer,
practice-page-frame:not([chrome="rail"]) .task-timer:not(:has(.task-timer-state)),
practice-page-frame:not([chrome="rail"]) .task-timer:has(.task-timer-state) {
    min-width: 0;
    grid-template-areas:
        "ring label label"
        "ring time state";
    grid-template-columns: auto auto minmax(0, 1fr);
    align-items: center;
    gap: 0 9px;
    padding: 6px clamp(11px, 1.2cqi, 16px) 6px 8px;
    border: 1.5px solid var(--chrome-line);
    border-radius: var(--pill);
}

practice-page-frame .task-timer-ring {
    display: grid;
    width: 32px;
    height: 32px;
    align-self: center;
    border-radius: 50%;
    /* Both timer modes write --task-timer-progress: a countdown's is what is
       left of its whole, an elapsed clock's is how far through the current
       minute it is. The fallback is for a chip nothing has ticked at all.
       The arc's colour is a variable so the critical rule below re-colours
       the ring without restating how the ring is drawn. */
    background: conic-gradient(
        var(--task-timer-ring-colour, var(--chrome-accent))
            calc(var(--task-timer-progress, 1) * 360deg),
        var(--chrome-track) 0
    );
    grid-area: ring;
    place-items: center;
}

practice-page-frame .task-timer-ring::after {
    content: "";
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--surface);
}

practice-page-frame:not([chrome="rail"]) .task-timer-label {
    color: var(--chrome-ink-muted);
    font-size: 9px;
    letter-spacing: 0.12em;
    grid-area: label;
}

/* Fixed width for the same reason the old chip gave it one: the word changes
   as the take does — Waiting, then nothing, then Stopped — and a pill that
   resized under it would move the digits beside it mid-count. */
practice-page-frame:not([chrome="rail"]) .task-timer-state {
    width: 7ch;
    color: var(--chrome-ink-muted);
    font-size: 11px;
    grid-area: state;
}

practice-page-frame:not([chrome="rail"]) .exercise-header .task-timer #taskTimer {
    width: auto;
    color: var(--chrome-ink);
    font-size: clamp(16px, 1.9cqi, 18px);
    grid-area: time;
    text-align: left;
}

/* The strip under the digits and the ring say the same thing; the pill wears
   the ring, so the strip stands down rather than drawing a second gauge. */
practice-page-frame:not([chrome="rail"]) .task-timer:has(.task-timer-state)::before,
practice-page-frame:not([chrome="rail"]) .task-timer:has(.task-timer-state)::after {
    content: none;
}

practice-page-frame:not([chrome="rail"]) .task-timer[data-timer-urgency="critical"],
practice-page-frame:not([chrome="rail"]) .task-timer[data-timer-state="expired"] {
    border-color: var(--chrome-alert);
}

/* Ring, digits and border go red together: one alarm read three ways in the
   same glance, rather than a detail the eye has to hunt for. */
practice-page-frame:not([chrome="rail"]) .task-timer[data-timer-urgency="critical"] {
    --task-timer-ring-colour: var(--chrome-alert);
}

/* The digits need saying again here: the pill's own colour rule above
   outranks the shared critical rule further down, so without this the chip
   would alarm everywhere except the one place the time is actually read. */
practice-page-frame:not([chrome="rail"]) .exercise-header .task-timer[data-timer-urgency="critical"] #taskTimer,
practice-page-frame:not([chrome="rail"]) .exercise-header .task-timer[data-timer-state="expired"] #taskTimer {
    color: var(--chrome-alert);
}

/* Quieter than the digits it labels: the chip family's small caps, not a
   second bold headline competing with the time itself. */
practice-page-frame .task-timer-label {
    color: var(--ink-tertiary);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

practice-page-frame .task-timer-state {
    width: 7ch;
    color: var(--ink-tertiary);
    font-size: 12px;
    font-weight: 600;
}

practice-page-frame .exercise-header .task-timer #taskTimer {
    font-size: 15px;
}

practice-page-frame .task-timer-notice {
    position: absolute;
    z-index: 2;
    top: calc(100% + 8px);
    right: 0;
    width: max-content;
    max-width: min(320px, 80vw);
    padding: 4px 8px;
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
    box-shadow: var(--shadow-sm);
    font-size: 14px;
    font-weight: 600;
}

practice-page-frame .task-timer-notice:empty {
    display: none;
}

practice-page-frame .task-timer-fast-forward {
    position: absolute;
    right: 12px;
    bottom: -4px;
    color: var(--rubric);
    font-size: 14px;
    letter-spacing: -2px;
    animation: task-timer-fast-forward var(--task-timer-fast-forward-duration) ease-out both;
}

@keyframes task-timer-fast-forward {
    from { opacity: 1; transform: translateX(-8px); }
    to { opacity: 0; transform: translateX(4px); }
}

practice-page-frame .task-timer #taskTimer {
    order: initial;
    box-sizing: border-box;
    width: 5ch;
    min-width: 0;
    min-height: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    font-size: inherit;
    font-variant-numeric: tabular-nums;
    text-align: right;
    transition: color 0.3s ease;
}

/*
    Remaining mode wears a depleting strip under the digits: the digits say
    how much is left, the strip says how much that is out of — the part a
    glance cannot get from "00:46" alone. Elapsed chips have no whole to
    deplete, so the strip is gated on the state element only remaining chips
    render. --task-timer-progress is written by the frame's own
    updateTaskTimer, the same tick that writes the digits — never a second
    timer.
*/
practice-page-frame .task-timer:has(.task-timer-state) {
    /* Same 34px silhouette as the chips beside it: the strip lives in a
       slightly deeper bottom padding instead of making the chip taller. */
    padding: 5px 14px 9px;
}

practice-page-frame .task-timer:has(.task-timer-state)::before,
practice-page-frame .task-timer:has(.task-timer-state)::after {
    content: "";
    position: absolute;
    bottom: 4px;
    left: 12px;
    height: 3px;
    border-radius: var(--pill);
    pointer-events: none;
}

practice-page-frame .task-timer:has(.task-timer-state)::before {
    right: 12px;
    background: var(--brand-purple-tint-strong);
}

practice-page-frame .task-timer:has(.task-timer-state)::after {
    /* Width, not transform: a scaled pill squashes its rounded ends. The
       1s linear tween matches the tick, so depletion reads as continuous
       motion rather than a once-a-second jump. */
    width: calc((100% - 24px) * var(--task-timer-progress, 1));
    background: var(--brand-purple);
    transition: width 1s linear, background-color 0.3s ease, opacity 0.3s ease;
}

/*
    Running carries no state word (the moving digits say it), so the empty
    state slot shows a breathing dot instead — the one mark that says "live"
    at a glance. Decorative only: an empty content box never reaches the
    aria-live region's announcement.
*/
practice-page-frame .task-timer[data-timer-state="running"] .task-timer-state::before {
    content: "";
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--brand-purple);
    animation: task-timer-live 1.8s ease-in-out infinite;
}

@keyframes task-timer-live {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

/* A paused countdown holds its place but stops claiming attention. */
practice-page-frame .task-timer[data-timer-state="paused"]::after {
    opacity: 0.35;
}

practice-page-frame .task-timer[data-timer-state="paused"] #taskTimer,
practice-page-frame .task-timer[data-timer-state="stopped"] #taskTimer {
    color: var(--ink-tertiary);
}

/* Stopped keeps whatever was left on the strip as a neutral record: the
   take ended by hand, so the leftover time is history, not urgency. */
practice-page-frame .task-timer[data-timer-state="stopped"]::after {
    background: var(--ink-placeholder);
}

/*
    Colour temperature for the endgame. The mixes keep the palette in
    tokens: --rubric carries the alarm, darkened toward --ink-strongest for
    the digits so 15px bold text stays above AA contrast on the white chip.
*/
practice-page-frame .task-timer[data-timer-urgency="critical"] {
    border-color: color-mix(in srgb, var(--rubric) 45%, var(--border-tinted));
    animation: task-timer-critical-alert 0.9s ease-out 2;
}

practice-page-frame .task-timer[data-timer-urgency="critical"]::before {
    background: color-mix(in srgb, var(--rubric) 18%, var(--surface));
}

practice-page-frame .task-timer[data-timer-urgency="critical"]::after,
practice-page-frame .task-timer[data-timer-urgency="critical"] .task-timer-state::before {
    background: var(--rubric);
}

practice-page-frame .task-timer[data-timer-urgency="critical"] #taskTimer,
practice-page-frame .task-timer[data-timer-state="expired"] #taskTimer,
practice-page-frame .task-timer[data-timer-state="expired"] .task-timer-state {
    color: color-mix(in srgb, var(--rubric) 80%, var(--ink-strongest));
}

/* Two knocks as the countdown crosses into its final stretch, then still:
   enough to catch a wandering eye without becoming a siren for the rest of
   the take. */
@keyframes task-timer-critical-alert {
    0%, 100% { box-shadow: var(--header-chip-shadow, none); }
    50% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--rubric) 25%, transparent); }
}

/* Colour still shifts under reduced motion — it is state, not decoration —
   but the strip steps instead of sliding and nothing breathes or knocks.
   The fast-forward cue keeps its 1.2s stay and loses only its slide. */
@media (prefers-reduced-motion: reduce) {
    practice-page-frame .task-timer:has(.task-timer-state)::after {
        transition: none;
    }

    practice-page-frame .task-timer[data-timer-urgency="critical"],
    practice-page-frame .task-timer[data-timer-state="running"] .task-timer-state::before,
    practice-page-frame .task-timer-fast-forward {
        animation: none;
    }
}

/* A rule down the leading edge instead of one underneath: it marks the
   instruction as an aside to the task rather than drawing a divider across the
   page, and it lets the block sit tighter to the header above it. */
practice-page-frame > .instructions {
    margin-bottom: 0;
    padding: 0 0 0 12px;
    border-left: 2px solid var(--brand-purple-tint-strong);
    /* Standing orders: bold enough to be read once and obeyed, but set below
       body size so it does not compete with the passage underneath. */
    color: var(--ink);
    font-size: 0.9em;
    font-weight: 700;
    line-height: 1.55;
}

practice-page-frame > .instructions > [data-frame-instructions] {
    margin: 0;
}

/* instructions="assistive". main.css's .visually-hidden supplies the
   clipping, but the region rules above out-specify its width and would
   otherwise leave a full-width strip of padding and border behind. */
practice-page-frame > .instructions.visually-hidden {
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    border: 0;
}

practice-page-frame > .score-panel {
    flex: none;
}

/*
    Also answers to `task-surface` (ADR-0024): a score panel is mounted either
    in a Task Surface or here, and the orb row inside it has to measure the box
    it is actually in either way. The Marks outcome region is deliberately NOT
    a container — `.score-panel` there is width: min-content (css/main.css) and
    inline-size containment would stop it widening to its own row.
*/
practice-page-frame > .outcome-surface {
    display: grid;
    justify-items: center;
    gap: 8px;
    container-name: outcome-surface task-surface;
    container-type: inline-size;
}

practice-page-frame > .outcome-surface:not(:has([data-primary-outcome] > *)):not(:has([data-primary-outcome]:not(:empty))):not(:has([data-outcome-notices] > *)):not(:has([data-outcome-notices]:not(:empty))) {
    display: none;
}

practice-page-frame > .outcome-surface:has([data-primary-outcome] > .score-panel:empty:not([aria-label])):not(:has([data-outcome-notices] > *)):not(:has([data-outcome-notices]:not(:empty))) {
    display: none;
}

practice-page-frame [data-primary-outcome] {
    width: 100%;
    min-height: 0;
    font-weight: 700;
    text-align: center;
}

/* A LONE orb in the Outcome Surface — the Overall slot the open-ended Speaking
   pages lift out of their panel — has no row to be centred by, so it centres
   itself. `:only-child` is what keeps that to the lone case: written as a plain
   descendant it also caught the slots of a whole multi-orb row hosted here
   (Read_Aloud, Write_Essay et al keep their score panel inside this host), and
   out-specified the Overall stand-off in css/ai-feedback.css — the deliberate
   70px gap collapsed to the row's 28px while the separator kept drawing at
   left:-49px, adrift outside the gap it was meant to split.

   Alone, the slot is also :last-child, so ai-feedback's stand-off and separator
   would still apply to it — a rule separating it from nothing. Both are dropped
   here for the same reason the narrow-container step drops them. */
practice-page-frame [data-primary-outcome] .ai-score-orb-slot:only-child {
    margin: 0 auto;
}

practice-page-frame [data-primary-outcome] .ai-score-orb-slot:only-child::before {
    content: none;
}

practice-page-frame [data-outcome-notices] {
    width: min(100%, 680px);
    color: var(--ink-muted);
    font-size: 14px;
    text-align: center;
}

practice-page-frame [data-outcome-notices] > * {
    margin: 0;
}

practice-page-frame .outcome-notice {
    padding-top: 8px;
    border-top: 1px solid var(--border);
    font-weight: 400;
    line-height: 1.45;
}

/* The disclosure's summary line, whether the notice IS the <details> or
   merely holds one — a notice that carries actions keeps them outside the
   collapsible region, so its detail is a nested disclosure. */
practice-page-frame details.outcome-notice > summary,
practice-page-frame .outcome-notice > details > summary {
    cursor: pointer;
    font-weight: 600;
}

/* The notice line inside a notice that carries actions. Its container takes
   the margin reset [data-outcome-notices] > * gives the notices themselves. */
practice-page-frame .outcome-notice > p {
    margin: 0;
}

practice-page-frame .outcome-notice-detail {
    margin-top: 8px;
    padding: 8px 12px;
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
    color: var(--ink-tertiary);
}

/* The actions an Outcome Notice offers — a Pending Save's "Try saving
   again" is the first. They sit beneath the notice line, in the button
   voice, so the offer reads as the thing to do about the fact above it. */
practice-page-frame .outcome-notice-actions {
    margin-top: 8px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
}

/* The interactive-target floor from the style guide: at least 44px tall and
   44px wide on phone portrait, so the tap area does not shrink with the
   label. The min-height carries the height; the horizontal padding carries
   the width beside the label's own. */
practice-page-frame .outcome-notice-action {
    min-width: 44px;
    min-height: 44px;
    padding: 0 16px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
    color: var(--ink);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

practice-page-frame .outcome-notice-action:hover {
    /* The page's skill colour when one is declared; the brand blue elsewhere. */
    border-color: var(--skill-color, var(--brand-blue));
}

practice-page-frame > .task-workspace {
    display: grid;
    min-width: 0;
    gap: clamp(16px, 2.5vw, 32px);
    margin-top: 12px;
    margin-bottom: 32px;
}

/*
    The two surfaces stretch to a common height so a short response card does
    not leave a lopsided void beside a tall stimulus. `align-items: start` was
    the earlier choice only so a surface would not grow taller than its
    content; the content itself stays top-aligned via the align-content rule
    below, so nothing centres inside the taller card.
*/
practice-page-frame > .task-workspace--spatial {
    grid-template-columns: minmax(0, 3fr) minmax(320px, 2fr);
    align-items: stretch;
}

/*
    workspace="spatial-compact": two EQUAL columns, for the Tasks whose
    stimulus is a single fixed-height widget (an audio player) rather than a
    passage or an image. The 3fr/2fr split above exists to give a tall
    stimulus the room it needs; a player needs none of it, and the lopsided
    split only strands the options list in a narrow gutter.
*/
practice-page-frame > .task-workspace--spatial-compact {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: stretch;
}

practice-page-frame > .task-workspace--spatial > [data-task-surface],
practice-page-frame > .task-workspace--spatial-compact > [data-task-surface] {
    align-content: start;
}

/*
    A Task Surface is the box a component inside it actually has to fit
    (ADR-0024): component queries name `task-surface` instead of measuring the
    Frame, which is the whole page wide.
*/
practice-page-frame [data-task-surface] {
    box-sizing: border-box;
    min-width: 0;
    container-name: task-surface;
    container-type: inline-size;
    margin: 0;
    padding: clamp(16px, 2vw, 32px);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: none;
}

/*
    ── Text-size zoom (#604) ────────────────────────────────────────────────
    The Frame writes --task-text-scale; this is the one rule that spends it.
    It multiplies the size a Surface would otherwise inherit (1em, not 1rem),
    so at 100% the passage is drawn exactly as it was before the control
    existed, and everything sized in em or inherited inside it comes along.
*/
practice-page-frame:not([chrome="rail"]) [data-task-surface] {
    font-size: calc(1em * var(--task-text-scale, 1));
}

/* The strip under the content for what a learner does to the Task's
   presentation rather than to their answer. It has one tenant so far, held to
   the trailing edge where the prototype draws it. */
practice-page-frame > .task-bottom-bar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 12px;
}

practice-page-frame .text-size-zoom {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 3px;
    border: 1px solid var(--chrome-line);
    border-radius: var(--pill);
    background: var(--chrome-panel);
}

practice-page-frame .text-size-button {
    display: grid;
    min-width: 34px;
    height: 34px;
    box-sizing: border-box;
    align-items: center;
    justify-items: center;
    padding: 0;
    border: none;
    border-radius: var(--pill);
    background: none;
    color: var(--chrome-accent-deep);
    font: inherit;
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
}

practice-page-frame .text-size-button[data-text-size="larger"] {
    font-size: 17px;
}

/* The readout is also the reset target, so it is sized to be read rather than
   to shout, and its digits do not shuffle as the percentage changes. */
practice-page-frame .text-size-button[data-text-size="reset"] {
    min-width: 44px;
    color: var(--chrome-ink-muted);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

practice-page-frame .text-size-button:hover {
    background: var(--chrome-accent-tint);
}

practice-page-frame .text-size-button:focus-visible {
    outline: 2px solid var(--brand-blue);
    outline-offset: 2px;
}

/* Spent, not gone: the button keeps its place in the tab order and says no. */
practice-page-frame .text-size-button[aria-disabled="true"] {
    background: none;
    cursor: default;
    opacity: 0.4;
}

/* The chrome keeps the prototype's 34px scale where there is a pointer and
   room; a phone-width column gets the 44px target the style guide requires. */
@container practice-frame (max-width: 768px) {
    practice-page-frame .text-size-button {
        min-width: 44px;
        height: 44px;
    }
}

/* The controls follow the workspace in normal flow. They used to be pushed to
   the bottom of the viewport (margin-top: auto), which opened a tall void
   under short tasks. The rail variant keeps its own control layout below. */
practice-page-frame > .nav-buttons {
    margin-top: 24px;
    margin-bottom: 24px;
}

/* Tasks without prev/next arrows (Personal Introduction) have a single action
   group, which space-between would strand at the left edge. */
practice-page-frame > .nav-buttons:not(:has(#prevBtn)):not(:has(#nextBtn)) {
    justify-content: center;
}

/*
    chrome="rail": the Frame's timer badges and controls move into a narrow
    left sidebar, together with the page-supplied [data-frame-brand] and
    [data-rail-extra]. The content regions keep their identity and order to
    the right; only their centering changes, because the rail already fixes
    the left edge. grid-template-rows ends in 1fr so the rail — spanning
    1 / -1 — always reaches the bottom of the Frame even when the content
    is shorter than the viewport.
*/
practice-page-frame[chrome="rail"] {
    display: grid;
    grid-template-columns: minmax(var(--practice-tool-rail-width), max-content) minmax(0, 1fr);
    grid-template-rows: repeat(7, max-content) 1fr;
    column-gap: var(--practice-gutter);
    padding-right: var(--practice-gutter);
}

practice-page-frame[chrome="rail"] > [data-frame-region] {
    width: auto;
    margin: 0;
    grid-column: 2;
}

practice-page-frame[chrome="rail"] > .task-rail {
    grid-column: 1;
    grid-row: 1 / -1;
}

/* The header keeps its hidden title and app-shell slot but loses its badges
   to the rail, so it should not reserve header height. */
practice-page-frame[chrome="rail"] > .exercise-header {
    min-height: 0;
    padding: 0;
}

practice-page-frame > .task-rail {
    box-sizing: border-box;
    position: sticky;
    top: 0;
    display: flex;
    max-height: 100dvh;
    flex-direction: column;
    align-items: stretch;
    gap: 16px;
    padding: 12px 10px 16px;
    border-right: 1px solid var(--border);
    background: var(--surface);
    overflow-y: auto;
}

practice-page-frame .task-rail .exercise-header-badges {
    flex-direction: column;
    align-items: stretch;
}

practice-page-frame .task-rail .task-timer,
practice-page-frame .task-rail .task-timer:not(:has(.task-timer-state)) {
    min-width: 0;
    grid-template-columns: 1fr;
    justify-items: center;
    gap: 2px;
    /* Stacked label-over-digits needs less vertical padding than the header's
       single row to settle on the family's 44px. */
    padding: 4px 6px;
    text-align: center;
}

practice-page-frame .task-rail .task-timer-label {
    font-size: 11px;
}

practice-page-frame .task-rail .task-timer #taskTimer {
    width: auto;
    text-align: center;
}

/* The rail chip stacks label over digits, so the strip gets its own band
   below the stack rather than borrowing a single row's bottom padding. */
practice-page-frame .task-rail .task-timer:has(.task-timer-state) {
    padding-bottom: 12px;
}

practice-page-frame .task-rail .task-timer:has(.task-timer-state)::before,
practice-page-frame .task-rail .task-timer:has(.task-timer-state)::after {
    bottom: 5px;
    left: 8px;
}

practice-page-frame .task-rail .task-timer:has(.task-timer-state)::before {
    right: 8px;
}

practice-page-frame .task-rail .task-timer:has(.task-timer-state)::after {
    width: calc((100% - 16px) * var(--task-timer-progress, 1));
}

/*
    In the header the notice is a flyout and costs no layout. In the rail it
    sits in the flow under the chip, where its text would otherwise size the
    rail's max-content column — a message arriving mid-task visibly widened
    the whole sidebar and shoved the workspace sideways. Inline-size
    containment makes the notice size to the rail rather than the rail to the
    notice, so a rail cue should be a word or two; anything longer belongs on
    the page's own surface, where there is room for it.
*/
practice-page-frame .task-rail .task-timer-notice {
    position: static;
    /* The chip centres its items, so the contained notice — which no longer
       has an intrinsic width to be centred at — has to be told to fill the
       chip, or it collapses to its padding and spills its text. */
    justify-self: stretch;
    width: auto;
    max-width: 100%;
    contain: inline-size;
    white-space: normal;
    overflow-wrap: break-word;
    text-align: center;
}

/*
    Rail controls read top-down: the action controls (retry, submit) first at
    full width, then previous and next side by side beneath — the arrangement
    a tool sidebar had before it wore the Frame. The buttons are addressed by
    the ids the Frame itself generates, because buttons.css sizes them the
    same way and a class-only rule would lose to it.
*/
practice-page-frame .task-rail .nav-buttons {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin: 0;
}

practice-page-frame .task-rail .nav-buttons > div {
    flex-direction: row;
    grid-row: 1;
    grid-column: 1 / -1;
    align-items: stretch;
    gap: 8px;
}

practice-page-frame .task-rail .nav-buttons > button {
    grid-row: 2;
}

practice-page-frame .task-rail #prevBtn,
practice-page-frame .task-rail #nextBtn {
    width: 44px;
    height: 44px;
    min-width: 44px;
    justify-self: center;
}

practice-page-frame .task-rail #prevBtn span.nav-icon,
practice-page-frame .task-rail #nextBtn span.nav-icon {
    top: -2px;
    font-size: 3em;
}

/* By id for the same reason as the arrows above: buttons.css sizes these by
   id, and #redoBtn's negative bottom margin — which lifts it into a taller
   control row the rail does not have — would otherwise pull it over the
   arrows beneath. */
practice-page-frame .task-rail #redoBtn,
practice-page-frame .task-rail #submitBtn {
    width: 100%;
    min-width: 0;
    margin-bottom: 0;
    padding: 7px 8px 6px;
    font-size: 0.78em;
}

.practice-frame-failure {
    margin: 16px;
    padding: 12px 16px;
    border: 1px solid var(--incorrect-border);
    border-radius: var(--radius);
    background: var(--incorrect-bg);
    color: var(--rubric);
    font-weight: 700;
}

/* The app rail can leave less room than the viewport width suggests, notably
   on a phone in landscape. Stack from the Frame's real available width. */
@container practice-frame (max-width: 768px) {
    practice-page-frame .exercise-header-badges > * {
        min-height: 44px;
    }

    /* One line has no room for a Task's full name beside its number and its
       Skill, and an ellipsis where the name should be is worse than a wrap. */
    practice-page-frame .task-bar-line {
        flex-wrap: wrap;
    }

    /* The chevron goes where the row is already wrapping. Sixteen pixels is
       enough to push a Task's name onto a line of its own here, which costs a
       whole row of the header to say something the card's own affordance and
       the control's aria-expanded already say. */
    practice-page-frame .task-bar-chevron {
        display: none;
    }

    practice-page-frame .task-bar-name {
        white-space: normal;
    }

    practice-page-frame > .task-workspace--spatial,
    practice-page-frame > .task-workspace--spatial-compact {
        grid-template-columns: minmax(0, 1fr);
    }

    practice-page-frame[chrome="rail"] > [data-frame-region]:not(.task-rail) {
        width: min(var(--practice-rail-width), calc(100% - (2 * var(--practice-gutter))));
        margin-right: auto;
        margin-left: auto;
    }

    practice-page-frame[chrome="rail"] > .task-rail {
        position: static;
        width: 100%;
        max-height: none;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        justify-content: space-between;
        gap: 8px 16px;
        padding: 8px var(--practice-gutter);
        border-right: 0;
        border-bottom: 1px solid var(--border);
    }

    /* Stacked into a top bar, the rail has width to spare, so the controls
       go back to one row. */
    practice-page-frame[chrome="rail"] .task-rail .nav-buttons {
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 8px;
    }

    practice-page-frame[chrome="rail"] .task-rail .exercise-header-badges {
        flex-direction: row;
        align-items: center;
    }

    practice-page-frame[chrome="rail"] .task-rail .nav-buttons > div button {
        width: auto;
    }

    practice-page-frame details.outcome-notice > summary {
        box-sizing: border-box;
        min-width: 44px;
        min-height: 44px;
        padding: 12px 8px;
    }
}

/* The rail stacks above the content as a full-width bar at the same width
   the Frame's other chrome stacks. This queries the host container because
   the Frame — being the container the inner rules answer to — cannot match
   its own @container query. */
@container practice-frame-host (max-width: 768px) {
    practice-page-frame[chrome="rail"] {
        display: flex;
        flex-direction: column;
        padding-right: 0;
    }
}

/* ── The stacked tier (#605) ──────────────────────────────────────────────
    Below the flanked tier the flanks have nowhere to go without eating the
    measure, so the row stops flanking: the identity cluster, the ☰ PRACTICE
    button and the Task Timer keep one line between them and the Task bar drops
    to a full-width row beneath — which is the shape the content takes too. It
    is the prototype's own boundary, and it queries the host rather than the
    Frame because a container cannot answer its own query.

    The Task bar and the button change rows without changing parents:
    `.task-bar-column` becomes `display: contents` and its two children become
    grid items of the header in their own right. That is the only way an L is
    laid out on a grid — a grid area is a rectangle, and "the button in the
    middle cell of row 1, the bar across all of row 2" is not one. What the
    column carried, its two children carry here: each takes the `z-index: 45`
    that keeps a trigger clickable above the scrim (ADR-0033), so the rule that
    only one overlay is open still has two reachable triggers to arbitrate.
*/
@container practice-frame-host (max-width: 1274px) {
    practice-page-frame:not([chrome="rail"]) {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 13px;
        padding: 10px var(--practice-chrome-edge-stacked) 40px;
    }

    practice-page-frame:not([chrome="rail"]) > [data-frame-region] {
        grid-column: 1;
    }

    practice-page-frame:not([chrome="rail"]) > .exercise-header {
        grid-template-columns: auto minmax(0, 1fr) auto;
        gap: 11px 10px;
    }

    practice-page-frame:not([chrome="rail"]) .task-bar-column {
        display: contents;
    }

    practice-page-frame:not([chrome="rail"]) .practice-identity {
        grid-row: 1;
        grid-column: 1;
    }

    /* Centred in the middle cell, as the prototype draws it: the button is the
       row's own control rather than something leaning on the bar. */
    practice-page-frame:not([chrome="rail"]) .practice-trigger {
        position: relative;
        z-index: 45;
        grid-row: 1;
        grid-column: 2;
        justify-self: center;
    }

    practice-page-frame:not([chrome="rail"]) .exercise-header-badges {
        grid-row: 1;
        grid-column: 3;
    }

    practice-page-frame:not([chrome="rail"]) .task-bar {
        z-index: 45;
        grid-row: 2;
        grid-column: 1 / -1;
    }

    /*
        The Practice sheet is the one surface that has to leave the flow here.
        Its host box is gone — `display: contents` leaves nothing to be
        absolutely positioned against — and the button it drops out of is a
        centred cell rather than a full-width one, so an anchored sheet would
        hang under the button instead of spanning the page. Fixed to the window
        it does what the prototype does: edge to edge, starting just under the
        button, and capped by the screen it has left rather than by a share of
        it. --chrome-sheet-top is measured by practice-sheet.js when the sheet
        opens, which is the one fact on this page no stylesheet can state.

        The Questions panel needs none of this: it hangs from the Task bar,
        which in this tier already spans the content, so it lands edge to edge
        exactly where it did before.
    */
    :is(practice-page-frame, practice-chrome) .practice-sheet {
        position: fixed;
        top: var(--chrome-sheet-top, 64px);
        right: var(--practice-chrome-edge-stacked);
        left: var(--practice-chrome-edge-stacked);
        max-height: calc(100vh - var(--chrome-sheet-top, 64px) - 16px);
    }

    practice-page-frame .questions-panel-grid {
        max-height: 44vh;
    }
}

/* ── The very narrow tier (#605) ──────────────────────────────────────────
   A phone in portrait. The chrome gives up its edge padding first and the
   Practice button's word second: the glyph and the chevron still say "a menu
   opens here", and the word survives for a screen reader because it is clipped
   rather than deleted. */
@container practice-frame-host (max-width: 429px) {
    practice-page-frame:not([chrome="rail"]) {
        --practice-chrome-edge-stacked: 10px;
        /* The timer's mini shape, stated as the short tier states it. */
        --task-timer-areas: "ring time";
        --task-timer-columns: auto minmax(0, 1fr);
        --task-timer-aside-display: none;
        --task-timer-aside-position: absolute;
        --task-timer-aside-overflow: hidden;
        --task-timer-aside-width: 1px;
        --task-timer-aside-height: 1px;
        --task-timer-aside-clip: inset(50%);
    }

    :is(practice-page-frame, practice-chrome) .practice-trigger-label {
        position: absolute;
        overflow: hidden;
        width: 1px;
        height: 1px;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}

/* The divider is a mark between two things that are still side by side; below
   this the identity cluster is tight enough that the rule is one more thing in
   a row with none to spare. */
@container practice-frame-host (max-width: 519px) {
    :is(practice-page-frame, practice-chrome) .chrome-identity-divider {
        display: none;
    }
}

/* The four Skill dots are the wordmark's ornament, not its name. */
@container practice-frame-host (max-width: 399px) {
    :is(practice-page-frame, practice-chrome) .wordmark-dots {
        display: none;
    }
}

/* ── The short tier (#605) ────────────────────────────────────────────────
   A landscape phone: width is fine and height is the scarce thing, so this
   one is a media query rather than a container query on purpose — the Frame
   is an inline-size container and has no block size to be asked about, and
   ADR-0033's "the header row's own space" has no height to read. The chrome
   spends less of what there is: tighter page padding, a tighter header row,
   no wordmark ornament, and a shorter Questions grid. */
@media (max-height: 559px) {
    practice-page-frame:not([chrome="rail"]) {
        row-gap: 10px;
        padding-top: 6px;
        padding-bottom: 24px;
    }

    practice-page-frame:not([chrome="rail"]) > .exercise-header {
        row-gap: 8px;
    }

    :is(practice-page-frame, practice-chrome) .wordmark-dots {
        display: none;
    }

    practice-page-frame .questions-panel-grid {
        max-height: 38vh;
    }
}

/*
    The timer, down to what it is for. The word over the digits and the state
    word beside them are the first things a short or a very narrow window can
    spare — the ring already says how much is left, and the moving digits
    already say the clock is live. Neither is deleted: the state is a live
    region that still announces "Waiting" and "Stopped", so it is clipped
    rather than removed, and the chip's own aria-label carries the word the
    visible label dropped.

    Two tiers want the same chip and a height and a width cannot be asked in
    the same breath, so the shape is declared once here against defaults and
    each query below states only what it wants different. The alternative —
    the whole block written twice, under a media query and under a container
    query — is the same drawing kept in two places, which is how two chips
    that were meant to be one drift apart.
*/
practice-page-frame:not([chrome="rail"]) .task-timer,
practice-page-frame:not([chrome="rail"]) .task-timer:not(:has(.task-timer-state)),
practice-page-frame:not([chrome="rail"]) .task-timer:has(.task-timer-state) {
    grid-template-areas: var(--task-timer-areas, "ring label label" "ring time state");
    grid-template-columns: var(--task-timer-columns, auto auto minmax(0, 1fr));
}

practice-page-frame:not([chrome="rail"]) .task-timer-label {
    display: var(--task-timer-aside-display, block);
}

practice-page-frame:not([chrome="rail"]) .task-timer-state {
    position: var(--task-timer-aside-position, static);
    overflow: var(--task-timer-aside-overflow, visible);
    width: var(--task-timer-aside-width, 7ch);
    height: var(--task-timer-aside-height, auto);
    clip-path: var(--task-timer-aside-clip, none);
}

@media (max-height: 559px) {
    practice-page-frame:not([chrome="rail"]) {
        --task-timer-areas: "ring time";
        --task-timer-columns: auto minmax(0, 1fr);
        --task-timer-aside-display: none;
        --task-timer-aside-position: absolute;
        --task-timer-aside-overflow: hidden;
        --task-timer-aside-width: 1px;
        --task-timer-aside-height: 1px;
        --task-timer-aside-clip: inset(50%);
    }
}

/* ── The Practice sheet's column counts (#605) ────────────────────────────
   Measured off prototypes/practice-page-layout.html with the sheet open: four
   across down to 1100px, a 2x2 block from 1100px to 760px, and one column
   below that until the tabbed tier takes over at 640px. The counts are keyed
   on the host rather than on the sheet's own inline size on purpose — above
   1274px the flanked tier hands the sheet a narrower column than a 1024px
   window does, and the prototype still draws four across there, so the sheet's
   own width is not the fact these tiers turn on.

   Two across rather than "as many as fit" is what keeps the reading order
   intact: Speaking | Writing on the first row and Reading | Listening on the
   second is the catalogue's order in rows, and the two rows balance. */
@container practice-frame-host (max-width: 1099px) {
    :is(practice-page-frame, practice-chrome) .practice-sheet-columns {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@container practice-frame-host (max-width: 759px) {
    :is(practice-page-frame, practice-chrome) .practice-sheet-columns {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ── The tabbed Practice sheet (#605) ─────────────────────────────────────
   Under 640px four columns of Tasks is four columns nobody can read, so the
   catalogue becomes four tabs and one list. The switch is made in
   practice-sheet.js — real tab semantics are ARIA no stylesheet can write, and
   a tablist that is only sometimes a tablist is worse than one built when it
   is one — and this file draws whichever shape it left behind, keyed on the
   sheet's own `data-layout`. */
:is(practice-page-frame, practice-chrome) .practice-sheet-tabs {
    display: none;
}

:is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] {
    container-name: practice-sheet;
    container-type: inline-size;
    /* One list, so there is no second column for the ✕ to sit clear of. */
    padding-right: clamp(12px, 1.8cqi, 22px);
}

:is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] .practice-sheet-body {
    display: flex;
    gap: 10px;
}

:is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] .practice-sheet-tabs {
    display: flex;
    flex: none;
    gap: 2px;
}

:is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] .practice-sheet-columns {
    display: block;
    min-width: 0;
    flex: 1 1 auto;
}

/* The tab is the panel's heading, so the column's own name would be the same
   word twice. It stays in the tree as the panel's name. */
:is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] .practice-sheet-group-name {
    position: absolute;
    overflow: hidden;
    width: 1px;
    height: 1px;
    padding: 0;
    border: 0;
    margin: 0;
    clip-path: inset(50%);
    white-space: nowrap;
}

:is(practice-page-frame, practice-chrome) .practice-sheet-tab {
    display: flex;
    align-items: center;
    padding: 9px 12px;
    border: 0;
    border-radius: var(--chrome-radius-pill);
    background: none;
    color: var(--chrome-ink-soft);
    cursor: pointer;
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
    gap: 7px;
    text-align: left;
    white-space: nowrap;
}

:is(practice-page-frame, practice-chrome) .practice-sheet-tab[aria-selected="true"] {
    background: var(--chrome-accent-tint);
    color: var(--chrome-accent-deep);
    font-weight: 800;
}

:is(practice-page-frame, practice-chrome) .practice-sheet-tab .task-skill-dot {
    width: 8px;
    height: 8px;
    flex: none;
    background: var(--practice-sheet-skill, var(--chrome-line-muted));
}

/* Wide enough for a column of names beside the list, the tabs stand down the
   sheet's leading edge; below that they lie across its top, because 150px of
   tabs on a 390px screen is most of the sheet. */
@container practice-sheet (min-width: 520px) {
    :is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] .practice-sheet-body {
        flex-direction: row;
    }

    :is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] .practice-sheet-tabs {
        width: 150px;
        flex-direction: column;
        padding-right: 10px;
        border-right: 1px solid var(--chrome-line);
    }
}

@container practice-sheet (max-width: 519px) {
    :is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] .practice-sheet-body {
        flex-direction: column;
    }

    :is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="tabs"] .practice-sheet-tabs {
        overflow-x: auto;
        width: 100%;
        flex-direction: row;
        padding-bottom: 8px;
        border-bottom: 1px solid var(--chrome-line);
    }
}


/* ── The chrome without a Frame: the Profile Home (#606) ──────────────────
   Home is the last page the sidebar was on and the only page with no Task, so
   it wears the chrome with the two Task-shaped things taken out: no Task bar,
   no Task Timer. What is left — the identity cluster and the ☰ PRACTICE
   button — was never the Task's, so it is drawn by the rules above, which the
   whole file states for both hosts rather than twice.

   The element is the app shell's own slot (`_app/index.html`), which makes it
   the box the chrome needs: the container its container queries answer to, and
   the positioned ancestor the sheet is fixed to. Both container names, because
   above they are two different things and here they are the same box — the
   Frame cannot answer its own query and so needs a host, and Home's chrome has
   no separate host to give it. */
practice-chrome {
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    container-name: practice-frame practice-frame-host;
    container-type: inline-size;
    gap: var(--practice-chrome-gap);
    padding: 14px var(--practice-chrome-edge);
}

/* The sheet is fixed on Home at every width, where a practice page only fixes
   it below the flanked tier. The reason the two differ is the reason the
   stacked tier gives: an anchored sheet hangs from the box that carries it,
   and here that box is the button alone — the Task bar that made it the width
   of the content column is what Home does not have. "Anchored to the content
   edge" is therefore the page's own edge, which is what fixed draws.

   --chrome-sheet-top is written by practice-sheet.js on open, as it is there;
   the fallback matches this row's own height rather than the Frame's. */
practice-chrome .practice-sheet {
    position: fixed;
    top: var(--chrome-sheet-top, 72px);
    right: var(--practice-chrome-edge);
    left: var(--practice-chrome-edge);
    max-height: calc(100vh - var(--chrome-sheet-top, 72px) - 16px);
}

@container practice-frame-host (max-width: 429px) {
    practice-chrome {
        --practice-chrome-edge: 10px;
    }
}
