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;
}

/* Attempt rows are review surfaces rather than task-input surfaces: give them
   the full practice rail so transcripts, recordings, and score summaries can
   use the available desktop width. The inner history component remains 100%,
   so expanded and compact cards follow the same geometry. */
practice-page-frame:not([chrome="rail"]) > .attempt-history-region:has(> .speaking-attempt-history) {
    width: min(
        calc(var(--practice-rail-width) + 240px),
        calc(100vw - (2 * var(--practice-gutter)))
    );
    margin-inline: auto;
    grid-column: 1 / -1;
}

/* Submit is the hand-off into review. Scrolling it to the top leaves the new
   attempt directly beneath it, while preserving a small breathing edge. */
practice-page-frame:not([chrome="rail"]) > .nav-buttons {
    scroll-margin-top: 16px;
}

/* No recession here, deliberately. A submission used to fade and blur the
   header and workspace so the arriving attempt would read as the live thing;
   the page stayed clickable throughout, so the only thing the treatment did
   was make a legible prompt illegible — a rewrite of how the prompt reads, on
   the submit that ADR-0090 says must not rewrite it.

   What points at the new attempt instead is what already did: the scroll that
   carries Submit to the top, and the card's own 280ms arrival. Attention is
   directed to the arriving thing rather than taken from everything else.

   If that ever reads too flat, the sanctioned upgrade is a transient dip on
   `opacity` alone — never `filter`. Opacity is a compositor property and costs
   effectively nothing to animate; blur is re-convolved every frame over the
   largest subtree on the page, so a slower, prettier fade is a strictly more
   expensive one. See ADR-0091. */

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 whole bar is the way into the Questions panel (ADR-0034), so the trigger
   spans the card rather than wrapping the name inside it. It wears no button
   furniture: the card is the affordance and the chevron is the cue. */
practice-page-frame .task-bar-trigger {
    display: flex;
    width: 100%;
    min-width: 0;
    align-items: center;
    padding: 0;
    border: 0;
    margin: 0;
    background: none;
    color: inherit;
    cursor: pointer;
    gap: clamp(8px, 1.2cqi, 14px);
    font: inherit;
    text-align: left;
}

/* The trigger's two rows: the naming line, then the instruction line. */
practice-page-frame .task-bar-body {
    display: flex;
    min-width: 0;
    flex: 1 1 auto;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
}

/* Which Exercise, in the accent: `#n/N` at the trailing edge of the bar, sized
   to be read across the row rather than leaned into. The `#` is not a label
   for the number, it is part of it, so it takes the number's size and colour
   by inheritance and states nothing of its own. */
practice-page-frame .task-bar-count {
    display: inline-flex;
    flex: none;
    align-items: baseline;
    color: var(--chrome-accent);
    font-size: clamp(24px, 2.8cqi, 28px);
    font-weight: 800;
    white-space: nowrap;
}

/* The denominator is context for the number, not a second number: it drops to
   roughly half the size, so `#1/30` reads as one figure with a scale behind
   it. It keeps the full accent because the soft token lacks text contrast. */
practice-page-frame .task-bar-count .task-bar-total {
    color: var(--chrome-accent);
    font-size: clamp(14px, 1.6cqi, 16px);
}

/* The name and the Skill badge read as one answer to "which Task", so they
   share a group and the ellipsis falls on the name rather than between them. */
practice-page-frame .task-bar-heading {
    display: flex;
    min-width: 0;
    flex: 1 1 auto;
    align-items: center;
    gap: 8px;
}

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

/* The cue gets a seat (#617): the bare glyph read as furniture rather than as
   a control, so it sits in a pill sized like a button even though the whole
   card is still the target (ADR-0034). The card stays unstyled; only the cue
   says "this opens" — and it says it filled rather than tinted, because a tint
   that only turns solid on hover states the affordance to a pointer and to
   nothing else. The resting state is the one every learner sees, so the
   resting state is the one that carries the cue; hover lifts the fill away
   rather than adding it. */
practice-page-frame .task-bar-chevron {
    display: inline-flex;
    flex: none;
    width: 40px;
    height: 40px;
    align-items: center;
    justify-content: center;
    border-radius: var(--chrome-radius-pill);
    background: var(--chrome-accent-soft);
    color: var(--surface);
    transition: background-color 0.15s ease-out;
}

practice-page-frame .task-bar-chevron svg {
    transition: transform 0.15s ease-out;
}

practice-page-frame .task-bar-trigger:hover .task-bar-chevron {
    background: var(--chrome-accent-tint);
    color: var(--chrome-accent);
}

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

practice-page-frame .task-bar-trigger[aria-expanded="true"] .task-bar-chevron svg {
    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 {
    display: block;
    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;
}

/* The page supplies the line as a <p>, and a paragraph's own margins are what
   stood between this bar and the prototype's 64px (#617): the slot is one
   line of chrome, not prose. */
practice-page-frame .task-bar .instructions p {
    margin: 0;
}

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;
    box-sizing: border-box;
    width: clamp(132px, 18cqi, 220px);
    min-width: 96px;
    flex: 0 1 auto;
    align-items: center;
    padding: 3px 10px;
    border: 1.5px solid var(--chrome-line);
    border-radius: var(--pill);
    background: var(--surface);
    gap: 6px;
}

practice-page-frame .questions-panel-go-to:focus-within {
    border-color: var(--chrome-accent);
}

practice-page-frame .questions-panel-go-to input {
    box-sizing: border-box;
    width: 100%;
    min-width: 0;
    height: 26px;
    min-height: 0;
    margin: 0;
    padding: 0;
    border: 0;
    outline: 0;
    background: none;
    color: var(--chrome-ink);
    font: inherit;
    font-size: 12px;
}

practice-page-frame .questions-panel-search-affordance {
    display: grid;
    width: 18px;
    flex: none;
    color: var(--chrome-ink-soft);
    font-size: 18px;
    line-height: 1;
    pointer-events: none;
    place-items: center;
    transform: scale(-2, 2);
}

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

/* Shared deliberately: Questions and Practice are sibling overlays and their
   identical close action should carry the same unmistakable affordance. */
:is(practice-page-frame, practice-chrome) .chrome-overlay-close {
    display: grid;
    width: 32px;
    height: 32px;
    border: 1.5px solid color-mix(in srgb, var(--chrome-alert) 35%, var(--chrome-line));
    border-radius: var(--pill);
    background: color-mix(in srgb, var(--chrome-alert) 9%, var(--surface));
    color: color-mix(in srgb, var(--chrome-alert) 62%, var(--chrome-ink-soft));
    cursor: pointer;
    font-size: 17px;
    line-height: 1;
    margin-inline-start: auto;
    place-items: center;
}

:is(practice-page-frame, practice-chrome) .chrome-overlay-close:is(:hover, :focus-visible) {
    border-color: var(--chrome-alert);
    color: var(--chrome-alert);
}

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-family: var(--font-mono);
    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"] {
    --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"] {
    --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"] {
    --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"] {
    --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-family: var(--font-mono);
    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 .questions-panel-controls,
    practice-page-frame .questions-panel-filters {
        flex-wrap: nowrap;
        gap: 4px;
    }

    practice-page-frame .questions-filter {
        padding-inline: 8px;
    }

    practice-page-frame .questions-panel-go-to {
        width: auto;
        min-width: 62px;
        flex: 1 1 auto;
        padding-inline: 8px;
    }

    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.

       box-sizing, because without it this rule asked for 44px and drew 62px:
       the row is content-box by default and carries 9px of block padding on
       each side. Across 23 rows that was 414px of catalogue — most of the
       reason showing all four Skills at once looked impossible (ADR-0052).
       The target does not shrink; the floor is the same 44px it always
       said. */
    :is(practice-page-frame, practice-chrome) .practice-sheet-task {
        box-sizing: border-box;
        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;
    }

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

}

/* The Skill badge keeps its dot and gives up its word, but only below 400px of
   Frame (#618). Above it the badge's sixty pixels are there for the asking:
   at 410px of Frame — a 430px phone, the widest in portrait — the naming line
   is 330px and every Task label but one fits beside the word, so the Skill is
   named rather than merely coloured.

   The one that does not is "Multiple Choice, Multiple Answers (Reading)" at
   302px, and it is the exception that argues for the word rather than against
   it: what the ellipsis takes is the trailing "(Reading)", which is precisely
   what the badge beside it has just said. There is no width below ~500px
   where both that label and the word fit whole, so the choice is which of the
   two says the Skill, and the badge says it in sixty pixels rather than a
   hundred.

   Below 400px the line is ~290px and the word would eat into the label itself
   rather than its parenthetical, so it goes — leaving the Skill said three
   ways still: the dot's colour, the accent body[data-skill] tints the whole
   chrome with, and the Task's own name. It is clipped rather than deleted, so
   the badge still reads "Speaking" to a screen reader, which is not competing
   for the width.

   Scoped to the bar: the same badge in the Practice sheet is a row with room,
   and there the word is the label. */
@container practice-frame (max-width: 399px) {
    practice-page-frame .task-bar .task-skill-name {
        position: absolute;
        overflow: hidden;
        width: 1px;
        height: 1px;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}

/* 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);
}

/* Preparation Yield: a countdown the learner cannot see is a countdown they
   cannot trust, so a held clock rides the top of the viewport — and only then.
   Pinning for the whole take would put a moving element in peripheral vision
   while recording, churn the visual baselines on six pages (ADR-0051) and eat
   viewport at high zoom (WCAG 1.4.10 reflow), all to repeat news the learner
   can already read in place.

   The header sticks, not the chip: a sticky element only travels inside its
   containing block, and the chip's parent is a badge row exactly one chip
   tall, so sticking the chip itself moves it precisely nowhere.

   Paused only, never waiting: a waiting clock has not begun, so there is no
   held countdown to keep in sight — and `waiting` is painted by every mode,
   so matching it here would hand a Writing page the Speaking behaviour. */
practice-page-frame:not([chrome="rail"]) > .exercise-header:has(.task-timer[data-timer-state="paused"]) {
    position: sticky;
    z-index: 3;
    top: 0;
    background: var(--surface);
}

/* 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;
}

/* The chip's transient message wears the chrome the chip does: the same card
   surface, line and ink-family shadow, not the content layer's neutral box. */
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: 5px 10px;
    border: 1px solid var(--chrome-line);
    border-radius: var(--chrome-radius-pill);
    background: var(--chrome-panel);
    box-shadow: var(--chrome-shadow-bar);
    color: var(--chrome-ink);
    font-size: 13px;
    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(--chrome-accent);
    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-family: var(--font-mono);
    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 the zoomed 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. Only the Surface a page names with data-text-zoom scales: the pill
    sits beside the text it resizes and touches nothing else.
*/
practice-page-frame:not([chrome="rail"]) [data-task-surface][data-text-zoom] {
    font-size: calc(1em * var(--task-text-scale, 1));
}

/* The Surface's footer row: a page's own control at the leading edge, the
   zoom pill at the trailing one where the prototype draws it. Its size is
   pinned back to the root so the control does not grow with the passage it
   grows. It wraps rather than overflowing, which is the escape hatch and not
   the arrangement — on a phone the two fit, and two stacked rows is what a
   missing rule looks like rather than a design. */
practice-page-frame .task-surface-tools {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    margin-top: clamp(10px, 1.5cqi, 16px);
    gap: 12px;
    font-size: 1rem;
}

/* With nothing at the leading edge the pill keeps the trailing one it has
   always had, rather than sliding to the middle of an empty row. */
practice-page-frame .task-surface-tools > .text-size-zoom:only-child {
    margin-left: auto;
}

practice-page-frame .task-surface-tools > [data-surface-tool] {
    min-width: 0;
    flex: 0 1 auto;
}

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-family: var(--font-mono);
    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;
}

/* The legacy Retry pill used a negative margin to tuck into a taller control
   row. In the Frame that same margin protrudes into Attempt History, which is
   the next grid row. The Frame owns this spacing, so keep the button in its
   layout box here. */
practice-page-frame:not([chrome="rail"]) > .nav-buttons #redoBtn {
    margin-bottom: 0;
}

/* 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;
    }

    /* Two columns rather than one row (#618). The bar's four parts do not fit
       across a phone, but they do fit in an L: the naming line and the
       instructions take a column of their own, and the count sits over the
       chevron in a narrow trailing one.

       This replaces a wrap-and-hide arrangement that spent the name's line on
       the count and then dropped the chevron to win it back — which cost the
       card its only "this opens" cue and still let "Reorder Paragraphs" wrap
       to two lines. Stacking the trailing pair costs 70px of a column nothing
       else wants, and buys the name a line to itself at every phone width. */
    practice-page-frame .task-bar-trigger {
        display: grid;
        align-items: center;
        column-gap: 8px;
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-rows: auto auto;
        row-gap: 4px;
    }

    practice-page-frame .task-bar-body {
        grid-row: 1 / span 2;
        grid-column: 1;
    }

    /* The number keeps its prominence but gives up its desktop floor: `#1/500`
       at 28px and a Task's name are competing for the same three hundred
       pixels here, and the name is what the card cannot do without. */
    practice-page-frame .task-bar-count {
        justify-self: center;
        grid-row: 1;
        grid-column: 2;
        font-size: 20px;
    }

    practice-page-frame .task-bar-count .task-bar-total {
        font-size: 13px;
    }

    /* The cue keeps its seat here. It sits under the count rather than beside
       it, which is the whole point of the column: together they are the
       trailing edge the desktop bar gives them, and the glossary's "the count
       and a chevron together at its trailing edge" is true at every width
       again rather than only above 768px. */
    practice-page-frame .task-bar-chevron {
        justify-self: center;
        grid-row: 2;
        grid-column: 2;
    }

    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 and the timer's
   label; the Practice button's word stays, because the ☰ beside it goes
   instead (see the 519px tier) and the word is the better sixty pixels.

   The ceiling is 479 rather than #605's 429 because 430 is a shipping phone
   width — every iPhone Pro Max — and at 430 the tier not firing was not a
   cosmetic miss: the timer stayed wide, the centred Practice pill overlapped
   the wordmark and the timer by 19px on each side, and Speaking pages, which
   carry a "Waiting" state chip the other Skills lack, overlapped worst. The
   harness could never have caught it: headless Chromium reserves a 15px
   scrollbar, so a 430px device renders a 415px container here. */
@container practice-frame-host (max-width: 479px) {
    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%);
    }
}

/* 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 — and so is the ☰.

   The glyph and the word were saying the same thing twice. Given a row that
   can afford one of them, the word is what a control should be: "PRACTICE"
   names where it goes, ☰ names a shape. The chevron stays at every width,
   because that is the part saying something neither of the others does — that
   this opens rather than navigates. Dropping the glyph buys 21px and lets the
   word stay un-clipped down to 390, where the header still has 13px of
   clearance on each side of the pill. */
@container practice-frame-host (max-width: 519px) {
    :is(practice-page-frame, practice-chrome) .chrome-identity-divider {
        display: none;
    }

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

/* The dots were the wordmark's ornament and used to be dropped here, on their
   own, to buy the name some width. They are inside the mark now (ADR-0046), so
   what a narrow row spends instead is scale: the whole lockup goes smaller
   rather than half of it going away. */
@container practice-frame-host (max-width: 399px) {
    :is(practice-page-frame, practice-chrome) .chrome-lockup-logo {
        height: 22px;
    }
}

/* ── 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, a
   smaller lockup, 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) .chrome-lockup-logo {
        height: 22px;
    }

    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 stacked Practice sheet (ADR-0052) ───────────────────────────────
   Under 640px four columns of Tasks is four columns nobody can read. What
   #605 put there instead was four tabs showing one Skill at a time; what
   goes there now is the whole catalogue in one column, scrolled.

   The sheet was always built to scroll — `overflow-y: auto` against a cap
   measured from --chrome-sheet-top — and tabbed it never reached it: 605px
   of content inside a 765px box, with three Skills out of four hidden to
   keep it there. Flat and with the row bug below fixed it is ~1180px, which
   is 1.4 phone screens and one thumb-flick per Skill.

   The switch is still made in practice-sheet.js, which now has only a shape
   to name: with no tabs there is no ARIA for a stylesheet to be unable to
   write. This file draws whichever shape it left behind, keyed on the
   sheet's own `data-layout`. */
:is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="stacked"] {
    /* One column, so there is no second one for the ✕ to sit clear of. */
    padding-right: clamp(12px, 1.8cqi, 22px);
}

:is(practice-page-frame, practice-chrome) .practice-sheet[data-layout="stacked"] .practice-sheet-columns {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* ── 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: grid;
    grid-template-columns:
        minmax(0, auto)
        minmax(0, var(--practice-chrome-column))
        minmax(0, auto);
    align-items: center;
    justify-content: center;
    container-name: practice-frame practice-frame-host;
    container-type: inline-size;
    column-gap: var(--practice-chrome-gap);
    padding: 14px var(--practice-chrome-edge);
}

/* The row is the Frame's flanked tier with the Task-shaped things absent, and
   absence has a width (#617): the right flank holds the room the Task Timer
   takes on a practice page, so the ☰ PRACTICE button lands where it does
   there instead of drifting to the window edge. The measure is the timer
   chip's resting footprint, stated once here because no timer exists on Home
   to measure. */
practice-chrome::after {
    content: "";
    /* The Remaining-mode chip's rendered width, and the Task bar's resting
       height: the flank holds the timer's room and the row holds the bar's,
       so the identity cluster and the button sit at the height they do on a
       practice page, not hugging a shorter row's top. Measured, not derived —
       both size themselves from content this row does not have. */
    min-width: var(--practice-chrome-timer-flank, 171px);
    height: var(--practice-chrome-bar-height, 61px);
}

/* The lone button keeps the Task bar's trailing edge: on a practice page it
   sits after the bar's flex-1 stretch, and here the empty stretch is stated
   directly. */
practice-chrome .task-bar-column {
    justify-content: flex-end;
}

/* The stacked tier, as the Frame states it (#605): identity keeps the leading
   edge and the button the trailing one. The Frame centres the button here
   because a practice page's Task Timer holds the trailing cell; Home has no
   timer, so centred left it aligned to nothing. The sheet is already fixed on
   Home, so the column can dissolve the way it does there.

   A media query rather than the host's container query, because here the row
   IS the host and a container cannot answer its own query. On Home the chrome
   spans the viewport, so the two measures differ only by the scrollbar —
   pseudo-elements are queried against their originating element's own
   container, so ::after is stated here too rather than splitting the tier
   across two measures. */
@media (max-width: 1274px) {
    practice-chrome {
        grid-template-columns: auto minmax(0, 1fr) auto;
        /* No gap: ::after is explicitly zero-wide below, so the row was still
           paying the column gap to reach it and the button stopped 18px short
           of the trailing edge it is aligned to.
           The edge itself is Home's content gutter rather than the chrome's
           own, because the button is aligned to Home's content and the two
           measures grow differently — the gutter with the window, this edge
           fixed. The 18px gap had been standing in for that divergence, which
           is why it only looked right around 844-1000. The fallback keeps the
           rule honest for any host that has no Home gutter to read. */
        column-gap: 0;
        padding: 10px var(--home-gutter, var(--practice-chrome-edge-stacked));
    }

    practice-chrome .task-bar-column {
        display: contents;
    }

    practice-chrome .practice-trigger {
        position: relative;
        z-index: 45;
        justify-self: end;
    }

    practice-chrome::after {
        min-width: 0;
        /* The stacked tier drops the bar to its own row on a practice page,
           so there is no bar height for this row to hold any more. */
        height: auto;
    }
}

/* 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);
}

/* A media query for the same reason as the stacked tier above: the row is the
   host, and a container cannot answer its own query — as a container query
   this rule could never match. */
@media (max-width: 429px) {
    practice-chrome {
        --practice-chrome-edge: 10px;
        --practice-chrome-edge-stacked: 10px;
    }
}
