/* The deck player, the merged meeting page's own frame, and the live-room door
   inside the student area. PUBLIC bundle, like student.css: the student pages
   render outside the admin shell, so this file must never gain an "admin"
   prefix (bundle-css.mjs partitions the two bundles on the literal `/css/admin`,
   and a mis-named file arrives UNSTYLED in production while dev looks fine).

   WHAT THIS FILE OWNS after the 2026-09-04 redesign:
     - `.stu-deck*`     the player, its notes strip, its jump index, the compact
                        modifier used by the homework excerpt;
     - `.stu-meeting*`  the merged meeting page: the tab bar's placement, the
                        panel, the deck-not-open card;
     - `.stu-meetnav*`  previous / next meeting;
     - `.stu-session-head*` and `.stu-attendance__done*`, the two additions the
                        meeting page's own head and attendance block needed;
     - `.stu-room-door*` the live-room door.
   The meeting page's frame lives here rather than in student-pages.css or
   student-materials.css because it exists to hold the player and because those
   two sheets belong to other workstreams; nothing here re-declares a rule from
   either of them. `public/css/student.css` is READ-ONLY and every rule below is
   additive to it.

   Tokens only. The `--stu-*` family is declared once, on `.stu-shell`, in
   student-shell.css; this file uses them and never re-declares one.

   RTL is global, so list indentation is padding-right. Breakpoints are the
   house pair: 1024px and 767px. */

/* ── THE PLAYER ──────────────────────────────────────────────────────────────
   The single OPAQUE surface on a page of translucent ones. That contrast alone
   is the focus signal: no accent bar, no coloured border, no size increase. */

.stu-deck {
    position: relative;
    margin-bottom: var(--space-xl);
    border: 1px solid rgba(98, 98, 234, 0.10);
    border-radius: var(--radius-xl);
    background: var(--bg-surface);
    box-shadow: var(--stu-lift-3, 0 24px 64px rgba(0, 60, 130, 0.13), 0 2px 6px rgba(0, 60, 130, 0.05));
    overflow: clip;
}

/* ── Head. It names the MEETING and the deck. The SLIDE names itself, inside
   the stage: one title per band. */

.stu-deck__head {
    padding: var(--stu-s-5, 20px) var(--space-lg) var(--space-md);
    border-bottom: 1px solid var(--border-light);
    background: linear-gradient(180deg, rgba(239, 246, 255, 0.55), rgba(255, 255, 255, 0));
}

.stu-deck__headmain {
    min-width: 0;
}

.stu-deck__eyebrow {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 9px;
    margin: 0 0 var(--space-xs);
    font-size: var(--stu-t-xs, 12px);
    letter-spacing: 0;
    color: var(--text-secondary);
}

.stu-deck__eyebrow i {
    width: 15px;
    color: var(--text-tertiary);
}

/* The separator is CSS, so no caller has to choose a punctuation mark for it. */
.stu-deck__eyebrow-part + .stu-deck__eyebrow-part::before {
    content: '';
    display: inline-block;
    width: 3px;
    height: 3px;
    margin-inline-end: 9px;
    border-radius: 50%;
    background: var(--border-default);
    vertical-align: middle;
}

.stu-deck__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--stu-t-xl, 21px);
    font-weight: 500;
    letter-spacing: 0;
    line-height: 1.35;
    color: var(--text-color);
}

/* ── Nothing to read yet ────────────────────────────────────────────────── */

.stu-deck__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-xl) var(--space-lg);
    text-align: center;
}

.stu-deck__empty-mascot {
    width: 128px;
    height: 128px;
}

.stu-deck__empty-mascot svg {
    width: 100%;
    height: 100%;
    display: block;
}

.stu-deck__empty .stu-state {
    margin: 0;
}

/* ── Progress. THE BRAND GRADIENT, on the owner's call of 2026-09-08.

   This rail used to be a solid `--primary-color` blue, and the comment that
   stood here said so on purpose: the page's gradient budget was spent on the
   marks. The owner has overridden that. A 2px rail is exactly the shape the
   house rule reserves the ramp FOR - the gradient reads as the brand only drawn
   hairline-thin, never as a fill on a block - and this one earns it twice over,
   because sweeping blue to orange as the reader advances says "how far through"
   in colour as well as in length. The marks keep theirs; there is room for both
   at this weight. The fill is still anchored at the track's start edge and still
   grows toward its end, which under RTL means it grows leftward on its own.

   THE RAMP SPANS THE WHOLE RAIL, NOT THE FILL, and that is the entire difficulty
   here. `<i>`'s width IS the progress, so a background left to size itself would
   squeeze all three stops into whatever the fill currently measures: the reader
   at 10% would get blue, magenta and orange inside a few pixels, which is mush
   rather than the brand. Two declarations undo it:

     background-size       a background percentage resolves against the box it is
                           painted in - the fill - so dividing by the fraction of
                           the rail that box occupies gives back the RAIL's width.
                           `--deck-progress` is that fraction, written by
                           StudentSessionDeck.vue on the track above and inherited
                           down; it can never be 0, which would make this invalid.
     background-position-x the ramp must sit still while the fill uncovers it.
                           Pinned to the fill's RIGHT edge, which under RTL is the
                           one edge of the fill that does not move: `direction:
                           rtl` lays the block out from the right, so the fill's
                           right edge IS the rail's right edge and its LEFT edge is
                           what travels. Pinning left instead would drag the ramp
                           along with the growing edge and every position would
                           show the same colours.

   AND THE RAMP RUNS `to left`, WHICH IS NOT THE DIRECTION global.css WRITES IT.
   `.brand-hairline` uses `90deg` (physically left to right, blue at the left).
   Gradient angles are physical and ignore `direction`, so 90deg here would put
   blue at the rail's LEFT edge - the end a Hebrew reader finishes at - and orange
   under the first slide. Reversed to `to left`, blue lands at the rail's right
   edge, which is the RTL start, and orange at its left, which is the end: the
   deck opens in blue and closes in orange, along the reading direction. The three
   stops and their positions are otherwise verbatim from `.brand-hairline`. */

.stu-deck__progress {
    height: 2px;
    /* The track stays the faint blue it has always been. It reads as an unlit
       rail rather than as a colour, and a greyed-out copy of the ramp behind the
       ramp would only compete with it. */
    background: rgba(0, 113, 227, 0.10);
}

.stu-deck__progress > i {
    display: block;
    height: 100%;
    background-image: linear-gradient(to left, #0894FF 0%, #BD32D6 50%, #FFA304 100%);
    background-size: calc(100% / var(--deck-progress, 1)) 100%;
    background-position-x: right;
    background-repeat: no-repeat;
    border-start-end-radius: 2px;
    border-end-end-radius: 2px;
    transition: width var(--stu-dur-3, 0.28s) var(--stu-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
}

/* ── THE STAGE AND ITS CANVAS ────────────────────────────────────────────────
   THE ONE RULE THIS BLOCK EXISTS FOR: every slide in a deck is the same size,
   and that size is 9/16 of the stage's own width, whatever the slide contains.

   It is the room's geometry, ported. `class-app/src/components/SlideStage.vue`
   plus `class-app/public/css/app.css` (`.cl-stage`, `.cl-stage__canvas`) are the
   originals, and `public/css/deck-slides.css` is the mirrored stylesheet for the
   article that lands inside.

     .stu-deck__stage    an `aspect-ratio: 16 / 9` box. NOTHING scrolls, nothing
                         is capped, and it must NOT declare `container-type` -
                         the canvas is the container, and a second one out here
                         would resolve the deck's `cqw` rules against the page's
                         width instead of against 1920.
     .stu-deck__canvas   exactly 1920px by 1080px, at every screen size, with
                         `container-type: inline-size` and a transform scale the
                         component computes from a ResizeObserver on the box.

   WHAT THIS REPLACED, so nobody rebuilds it: a stage with `max-height:
   min(58vh, 560px)` and `overflow-y: auto` and no height of its own. A title
   card came out about 150px tall and a dense slide clamped at the cap and
   scrolled inside itself, so no two slides in a deck were the same size. With it
   went `.is-overflowing` (a fade mask), `.is-open` (an expand escape), the
   `.stu-deck__more` continuation cue and both of the compact variant's restated
   copies of them. A 16:9 box cannot overflow: there is nothing left to fade.

   PHYSICAL `top` / `left` ON THE CANVAS, NEVER THE LOGICAL PAIR. This area is
   `direction: rtl`, where the logical start edge is the RIGHT one - and anchoring
   a `transform-origin: top left` canvas to the right edge scales it clean off the
   screen. The canvas is a coordinate system, not a text flow. class-app's app.css
   carries the original of this note on `.cl-stage__canvas`; it is the single
   easiest rule in this file to "tidy" into a bug.

   The box's padding lives on `.stu-deck__viewport` rather than here: padding on
   an `aspect-ratio` box is inside the ratio, so it would shrink the slide and
   move the canvas's origin off the corner it is anchored to. */

/* ── THE HOLD: WHY THE CARD STOPS CHANGING HEIGHT SLIDE TO SLIDE ─────────────
   The picture never changes size (the 16:9 box above). Everything under it did:
   the phone rewrite, an interactive slide's summary, the sandbox and embed link
   rows and the student note are each drawn only on the slides that carry them,
   so the control bar and the jump index below them slid up and down by a couple
   of hundred pixels on every press, and the button a reader was aiming at moved
   out from under the cursor.

   `.stu-deck__hold` is the run those blocks share - the viewport and the note
   strip - and it takes a FLOOR: the tallest it has been for this deck at this
   width, measured by the component and handed over in `--deck-hold-floor`. A
   short slide is padded up to the floor and a tall one raises it. It is a floor
   and never a cap, so nothing is clipped and nothing scrolls: this is the same
   refusal the stage's own comment above records, where a `max-height` and an
   `overflow-y: auto` were what made two slides different sizes in the first
   place. StudentSessionDeck.vue carries the argument for measuring rather than
   declaring a reserve; read it before replacing this with a number.

   WHY A VARIABLE AND NOT AN INLINE `min-height`. An inline declaration beats
   every stylesheet rule, and this floor must not reach two places:

     @media screen  the print copy is a separate subtree, but its ancestor
                    `.stu-deck__layer` is `display: none` in the print medium
                    only while `is-printing` is on the card (student-materials.css).
                    A plain Ctrl+P prints the carousel with the layer visible, and
                    a floor measured on a 1440px screen would put a screenful of
                    blank paper under a slide. Fencing the whole rule out of print
                    covers both paths and needs no `!important`.
     :not(.is-full) full screen is a fixed panel with its own scroller. A floor in
                    there is dead space at the bottom of a scroll, and the height
                    it was measured at is not the height the panel has. */
@media screen {
    .stu-deck__layer:not(.is-full) .stu-deck__hold {
        min-height: var(--deck-hold-floor, 0px);
    }
}

.stu-deck__viewport {
    position: relative;
    padding: var(--stu-s-7, 32px) 34px var(--space-xl);
}

.stu-deck__stage {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: var(--radius-md);
    border: 1px solid var(--border-light);
    background: var(--bg-surface);
}

.stu-deck__canvas {
    position: absolute;
    top: 0;
    left: 0;
    width: 1920px;
    height: 1080px;
    transform-origin: top left;
    /* THE reason a student's copy of a slide matches the projector: every rule
       in deck-slides.css and deck-slide-templates.css sizes itself in cqw
       against this box, which is always 1920 wide whatever the screen is. */
    container-type: inline-size;
}

/* THE RING ON A FULL-BLEED CONTROL HAS TO BE DRAWN INSIDE IT.
   `.stu-deck` is `overflow: clip` and the counter fills the card from edge to
   edge, so the area-wide ring in student-shell.css - `outline-offset: 2px` plus
   a 3px outer box-shadow - is cut away on both flanks. All a keyboard reader saw
   was a hairline across its top, which on this card reads as the progress rail
   rather than as focus. (The rail was solid blue when that was measured and is
   the brand gradient now, so the two are no longer the same colour - but the
   confusion was the SHAPE, a 2px line spanning the card's full width, and that
   is unchanged.) A negative offset puts the outline on the control's own
   pixels, inside the clip. The stage keeps the same treatment: it is inset from
   the card's edge now, but it also CLIPS its own canvas, so a +2px ring would be
   drawn half outside the box it belongs to.

   THE CARD CLASS IS IN THE SELECTOR ONLY TO WIN THE CASCADE, and it is why this
   rule works where the bare `.stu-deck__stage:focus-visible` it replaces did
   not. `.stu-shell :is(a, button, summary, input, select, textarea,
   [tabindex]):focus-visible` measures (0,3,0) - `:is()` takes its most specific
   argument and `[tabindex]` is an attribute selector - so the (0,2,0) rule that
   used to sit here lost silently and the offset stayed at +2px. Two classes
   plus the pseudo tie at (0,3,0), and this sheet is linked after
   student-shell.css, so source order decides in its favour. Measured in the
   browser at 1440, not reasoned about. */
.stu-deck .stu-deck__stage:focus-visible,
.stu-deck .stu-deck__counter:focus-visible {
    outline: 2px solid var(--primary-color);
    outline-offset: -3px;
}

/* ── THE SAME SLIDE, IN WORDS, ON A PHONE ONLY ───────────────────────────────
   The picture above is the real designed slide on the projector's 1920x1080
   canvas, scaled to fit, so on a 390px handset its 60px body text paints at
   about 11px. This is the deck author's own `phoneBodyHtml` for the SAME slide,
   set as body copy somebody reads rather than as a slide: a comfortable size, a
   generous line height, and the area's own tokens.

   HIDDEN BY DEFAULT AND SHOWN AT 767px, not the other way round, which is the
   same shape - and the same decision, taken on the same day - as
   `.cl-student__read` in class-app/public/css/app.css. Declaring the hidden
   state first means a browser that never matches the query simply does not draw
   this, which is the right failure: the slide is still there.

   The component renders it unconditionally when the slide carries one. There is
   no width test in the JavaScript at all, on purpose - that is what kept the two
   apps drawing the same thing, and what the old `isPhone` swap broke. */

.stu-deck__read {
    display: none;
}

/* ── The authored body ──────────────────────────────────────────────────────
   `.slide__body` and everything inside it is owned by the three mirrored deck
   sheets (deck-slides.css, deck-kit.css, deck-slide-templates.css), which is
   where the room's own rules for a slide's paragraphs, lists, code and tables
   live. THIS FILE DECLARES NOTHING ABOUT IT, and that is a rule rather than an
   omission: a px size written here would be a px size inside a 1920px canvas
   that is then scaled by 0.19, i.e. a rule that is wrong by construction.

   The block of `.stu-deck__body` rules that used to stand here - a 17px reading
   column, its own h1-h4 scale, its own pre and table treatment - went with the
   reading column itself. So did the `@media (min-width: 768px)` block further
   down that restored the author's column counts on top of the phone branch: the
   display branch declares them, in cqw, and the override would have beaten it. */

/* ── An interactive slide, summarised ─────────────────────────────────────
   Content ABOUT the slide rather than the slide, so it is a recess inside the
   opaque player. */

.stu-deck__ask {
    margin-top: var(--space-lg);
    border-radius: var(--radius-md);
    background: var(--stu-well-tint, rgba(8, 148, 255, 0.055));
    box-shadow: var(--stu-well, inset 0 1px 3px rgba(0, 60, 130, 0.08), inset 0 0 0 1px rgba(98, 98, 234, 0.09));
    padding: var(--space-md);
}

/* Was mono + uppercase + 0.12em tracking on a HEBREW word, which is the least
   legible type the area could produce: uppercase is a no-op on a unicameral
   script and tracked caps is a Latin small-caps convention. Size and colour
   carry the label instead. */
.stu-deck__ask-kind {
    margin: 0 0 var(--space-xs);
    font-family: var(--font-body);
    font-size: var(--stu-t-xs, 12px);
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    color: var(--primary-color);
}

.stu-deck__ask-prompt {
    margin: 0 0 var(--space-md);
    font-size: 17px;
    line-height: 1.6;
    color: var(--text-color);
}

.stu-deck__ask-note {
    margin: var(--space-md) 0 0;
    font-size: 13px;
    color: var(--text-secondary);
}

.stu-deck__options {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

.stu-deck__option {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 4px var(--space-sm);
    background: var(--bg-surface);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    padding: 10px var(--space-md);
    font-size: 15px;
    color: var(--text-color);
}

.stu-deck__option--correct {
    border-color: var(--primary-border);
}

.stu-deck__option-mark {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: var(--primary-color);
}

.stu-deck__option-mark i {
    width: 14px;
}

/* tabular-nums in the BODY font, not the mono stack: this figure sits beside
   Hebrew and a mono face would fall back to Courier for it. */
.stu-deck__option-share {
    font-family: var(--font-body);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    color: var(--text-secondary);
}

/* The bar spans both columns, under the label. Its width is the one thing here
   that is a live figure, so it arrives as a computed :style binding. */
.stu-deck__option-bar {
    grid-column: 1 / -1;
    display: block;
    height: 4px;
    border-radius: var(--radius-full);
    background: rgba(0, 113, 227, 0.10);
    overflow: hidden;
}

.stu-deck__option-fill {
    display: block;
    height: 100%;
    border-radius: var(--radius-full);
    background: var(--primary-color);
}

.stu-deck__answers {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.stu-deck__answers li {
    background: var(--bg-surface);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    padding: 8px var(--space-md);
    font-size: 15px;
    color: var(--text-color);
    overflow-wrap: anywhere;
}

/* ── The outbound links a slide can carry ───────────────────────────────── */

.stu-deck__link-row {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin: var(--space-md) 0 0;
    color: var(--text-secondary);
}

.stu-deck__link-row i {
    width: 16px;
    color: var(--text-tertiary);
}

.stu-deck__link-row .stu-linklike {
    padding: 0;
}

/* ── THE ONE FRAME THIS AREA DRAWS ────────────────────────────────────────
   A YouTube video on an embed slide plays in place; every other embed is still
   the link above. StudentSlideEmbed.vue carries the argument for the exception
   and names the CSP entry that permits it, and it is the only component that
   mounts these classes.

   THE RATIO IS THE BOX'S, NOT THE FRAME'S. `aspect-ratio` on a `width: 100%`
   block, with the iframe absolutely filling it, is what makes the player 16:9
   at every width - from the full-screen layer down to a 390px handset - and
   what makes it impossible for the player to be the thing that scrolls the page
   sideways: the box can only ever be as wide as the column it sits in, and the
   frame can only ever be as wide as the box. An iframe sized directly cannot do
   this: a frame has no intrinsic ratio, so it falls back to the legacy 300x150
   default and letterboxes itself into a strip.

   `inset: 0`, not the logical pair. This area is `direction: rtl` and physical
   offsets are the rule for anything that is a coordinate system rather than a
   text flow - the same note the canvas above carries, for the same reason.
   `inset` is the PHYSICAL shorthand, so it is safe here in a way
   `inset-inline-start` would not be. */

.stu-deck__video {
    margin-top: var(--space-lg);
}

.stu-deck__video-frame {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: var(--radius-md);
    border: 1px solid var(--border-light);
    /* Black, not the surface tint: the player letterboxes a clip that is not
       exactly 16:9, and those bars have to read as part of the picture rather
       than as a gap the card failed to fill. */
    background: #000;
}

.stu-deck__video-iframe {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
}

/* ── THE NOTE WRITTEN FOR THE STUDENT ────────────────────────────────────────
   Rendered ONLY when the slide carries one. '' is the ordinary value and the
   screen renders nothing at all for it.

   A warm RULE, not a second warm surface, and it reuses the `.stu-shell`
   warning trio that already ships with a measured 5.37:1 justification rather
   than inventing a parallel family beside it. Deliberately NOT --accent-warm,
   which is #6935d8 (purple) and belongs to buttons on the marketing site. */

.stu-deck__notes {
    padding: var(--space-md) var(--stu-s-5, 20px) var(--stu-s-5, 20px);
    border-top: 1px solid var(--border-light);
    /* RTL: the physical RIGHT edge, where a Hebrew margin rule belongs. */
    border-inline-start: 2px solid var(--stu-warn-line, rgba(255, 171, 0, 0.45));
    background: linear-gradient(180deg, var(--stu-warn-wash, rgba(255, 171, 0, 0.12)), rgba(255, 255, 255, 0));
}

.stu-deck__notes-label {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin: 0 0 6px;
    font-size: var(--stu-t-xs, 12px);
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    color: var(--stu-warn, #8A5C00);
}

.stu-deck__notes-label i {
    width: 14px;
}

.stu-deck__notes-text {
    margin: 0;
    max-width: 78ch;
    font-size: var(--stu-t-md, 15px);
    line-height: 1.72;
    color: var(--text-secondary);
}

/* ── Moving through the deck ─────────────────────────────────────────────────
   DOM order is previous, spacer, [expand | dots], next. Under RTL the first
   flex child sits on the physical RIGHT, which is where the reading starts, so
   the DOM contract the specs pin and the visual order the design asks for are
   the same order and no CSS reorders anything.

   KEY BINDING, which is genuinely ambiguous in an RTL deck and is therefore
   stated in the component too: ArrowLeft = הבאה, ArrowRight = הקודמת. */

.stu-deck__ctl {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px var(--stu-s-5, 20px);
    border-top: 1px solid var(--border-light);
    background: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(239, 246, 255, 0.5));
}

.stu-deck__ctl-spacer {
    flex: 1 1 auto;
}

.stu-deck__step {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: 40px;
    padding-inline: var(--space-md);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-full);
    background: rgba(255, 255, 255, 0.85);
    font-family: var(--font-body);
    font-size: 14px;
    font-weight: 500;
    letter-spacing: 0;
    color: var(--primary-color);
    cursor: pointer;
    transition: border-color var(--stu-dur-2, 0.22s) ease, background var(--stu-dur-2, 0.22s) ease,
        transform var(--stu-dur-2, 0.22s) ease, box-shadow var(--stu-dur-2, 0.22s) ease;
}

.stu-deck__step i {
    width: 14px;
}

.stu-deck__step:hover:not(:disabled) {
    border-color: var(--primary-color);
    background: var(--primary-light);
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(0, 60, 130, 0.08);
}

/* opacity PLUS the disabled attribute, never a lighter grey: two faint greys
   one step apart do not communicate "there is no previous slide". */
.stu-deck__step:disabled {
    opacity: 0.45;
    pointer-events: none;
}

/* ── WHERE THE READER IS, IN THE BAR THE EYE IS ALREADY ON (owner request).
   Between the two controls that change it, which is the whole point: the same
   figures exist in the jump index's summary at the foot of the card, in the
   full-screen bar and in the stage's screen-reader label, and all three are
   somewhere a reader has to go and look.

   QUIET, and deliberately quieter than either step button. It is a readout, not
   a control, and a counter that competes with the two buttons beside it turns a
   two-item bar into a three-item one. The figures alone take the accent.

   `dir="ltr"` is an ATTRIBUTE in the markup, not `direction` here: only the
   attribute makes the UA sheet's `unicode-bidi: isolate` fire, and without
   isolation the Hebrew around a two-digit number reorders it. `tabular-nums` in
   the BODY font for the same reason `.stu-deck__option-share` uses it - the mono
   stack carries no Hebrew and the word beside the figure would fall to
   Courier. */
.stu-deck__pos {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    margin: 0;
    padding-inline: var(--space-sm);
    font-family: var(--font-body);
    font-size: var(--stu-t-sm, 13px);
    letter-spacing: 0;
    color: var(--text-secondary);
    white-space: nowrap;
}

.stu-deck__pos b {
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    color: var(--primary-color);
}

.stu-deck__expand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: 36px;
    padding-inline: var(--space-sm);
    border: none;
    border-radius: var(--radius-full);
    background: none;
    font-family: var(--font-body);
    font-size: 13px;
    color: var(--text-secondary);
    cursor: pointer;
    transition: color var(--stu-dur-1, 0.16s) ease, background var(--stu-dur-2, 0.22s) ease;
}

.stu-deck__expand:hover {
    background: var(--fsp-hover-bg);
    color: var(--text-color);
}

.stu-deck__expand i {
    width: 13px;
}

/* ── THE JUMP INDEX ──────────────────────────────────────────────────────────
   A native <details> plus a multi-column list, so 37 rows land on one screen
   with no grid of boxes and no JS, and under RTL the columns fill right to left
   on their own. THE SUMMARY IS THE COUNTER: random access is the only reason
   the index exists, and the counter is the one control always in reach. */

.stu-deck__index {
    border-top: 1px solid var(--border-light);
}

.stu-deck__index > summary {
    list-style: none;
    cursor: pointer;
}

.stu-deck__index > summary::-webkit-details-marker {
    display: none;
}

/* Hebrew stays in the body font and the figures are tabular-nums in the SAME
   font, so there is no second face in the string at all. The `dir="ltr"` on the
   figures is an ATTRIBUTE in the markup, so the UA sheet's `unicode-bidi:
   isolate` actually fires; CSS `direction` alone does not isolate. */
.stu-deck__counter {
    display: flex;
    align-items: center;
    gap: 9px;
    margin: 0;
    padding: 12px var(--stu-s-5, 20px);
    font-family: var(--font-body);
    font-size: var(--stu-t-xs, 12px);
    letter-spacing: 0;
    text-transform: none;
    color: var(--text-secondary);
}

.stu-deck__counter i {
    width: 14px;
    color: var(--text-tertiary);
}

.stu-deck__counter b {
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    color: var(--primary-color);
}

.stu-deck__counter:hover {
    color: var(--text-color);
}

.stu-deck__index-panel {
    padding: var(--space-md) var(--space-md) var(--stu-s-5, 20px);
    background: rgba(8, 148, 255, 0.035);
    box-shadow: var(--stu-well, inset 0 1px 3px rgba(0, 60, 130, 0.08), inset 0 0 0 1px rgba(98, 98, 234, 0.09));
}

.stu-deck__index-list {
    list-style: none;
    margin: 0;
    padding: 0;
    columns: 3;
    column-gap: var(--space-xl);
}

.stu-deck__index-list li {
    break-inside: avoid;
    margin: 0;
}

.stu-deck__index-row {
    display: grid;
    grid-template-columns: var(--stu-gutter-num, 22px) 1fr;
    align-items: baseline;
    gap: var(--space-sm);
    width: 100%;
    min-height: 34px;
    padding-inline: var(--space-sm);
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    font-family: var(--font-body);
    font-size: var(--stu-t-sm, 13px);
    letter-spacing: 0;
    color: var(--text-secondary);
    text-align: start;
    cursor: pointer;
    transition: background var(--stu-dur-1, 0.16s) ease, color var(--stu-dur-1, 0.16s) ease;
}

.stu-deck__index-row:hover {
    background: rgba(255, 255, 255, 0.9);
    color: var(--text-color);
}

.stu-deck__index-num {
    direction: ltr;
    font-variant-numeric: tabular-nums;
    font-size: 11px;
    color: var(--text-tertiary);
    text-align: center;
}

.stu-deck__index-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.stu-deck__index-row[aria-current='true'] {
    background: #fff;
    color: var(--primary-color);
    font-weight: 500;
    box-shadow: 0 1px 3px rgba(0, 60, 130, 0.09);
}

.stu-deck__index-row[aria-current='true'] .stu-deck__index-num {
    color: var(--primary-color);
}

/* ── The plan lecture's materials ───────────────────────────────────────── */

.stu-deck__rule {
    margin: var(--space-lg) var(--stu-s-5, 20px) var(--space-md);
}

.stu-deck__materials-title {
    margin: 0 var(--stu-s-5, 20px) var(--space-sm);
    font-size: 16px;
    font-weight: 500;
    color: var(--text-color);
}

.stu-deck__materials {
    list-style: none;
    margin: 0 0 var(--stu-s-5, 20px);
    padding: 0 var(--stu-s-5, 20px);
}

.stu-deck__materials li {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

.stu-deck__materials i {
    width: 16px;
    color: var(--text-tertiary);
}

.stu-deck__materials .stu-linklike {
    padding: 4px 0;
}

/* ── COMPACT VARIANT ─────────────────────────────────────────────────────────
   One modifier on the same component, never a second component: the homework
   excerpt of two or three slides further down the meeting page. */

.stu-deck--compact {
    border-radius: var(--radius-lg);
    box-shadow: var(--stu-lift-2, 0 10px 32px rgba(0, 60, 130, 0.10));
    margin-bottom: 0;
}

.stu-deck--compact .stu-deck__head {
    padding: 14px var(--space-md) var(--stu-s-3, 12px);
}

.stu-deck--compact .stu-deck__title {
    font-size: var(--stu-t-md, 15px);
}

/* JUST THE FRAME'S BREATHING ROOM. The compact player used to cap its stage at
   280px and restate the whole expand escape underneath, because a homework slide
   authored for a projector did not fit in 280px. It does now: the stage is a 16:9
   box at whatever width the panel gives it, so a homework slide is the same
   picture as the full player's, only smaller. The cap, its `.is-open` override
   and the two type rules that went with the reading column are all gone. */
.stu-deck--compact .stu-deck__viewport {
    padding: var(--stu-s-5, 20px) 22px var(--stu-s-7, 32px);
}

.stu-deck--compact .stu-deck__ctl {
    padding: 10px 14px;
}

.stu-deck--compact .stu-deck__step {
    min-height: 36px;
    font-size: var(--stu-t-sm, 13px);
    padding-inline: var(--stu-s-3, 12px);
}

.stu-deck--compact .stu-deck__notes {
    padding: var(--space-md);
}

/* Three slides do not need a three-column index; three dots do. */
.stu-deck__dots {
    display: inline-flex;
    gap: 6px;
    align-items: center;
    padding-inline: 6px;
}

.stu-deck__dots i {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: rgba(0, 60, 130, 0.18);
    transition: background var(--stu-dur-2, 0.22s) ease, transform var(--stu-dur-2, 0.22s) ease;
}

.stu-deck__dots i.is-on {
    background: var(--primary-color);
    transform: scale(1.35);
}

/* ── THE MERGED MEETING PAGE ─────────────────────────────────────────────────
   One meeting, one page: the deck at the top, a tab bar under it, exactly one
   panel. The tab bar wears the house pill group (.stu-tabs / .stu-tab from
   student.css) rather than a second bar, because the courses bar across the top
   of /materials is THE bar in this area and a second one competes with it. */

.stu-meeting {
    display: block;
}

.stu-meeting__material {
    margin-bottom: var(--space-lg);
}

.stu-meeting__tabs {
    display: flex;
    flex-wrap: wrap;
    max-width: 100%;
}

/* Additive to .stu-tab: a 36px pill is a mouse target, and there was no focus
   ring on it at all. The type and the track stay exactly as student.css has
   them. */
.stu-meeting__tab {
    gap: 8px;
    min-height: 40px;
    font-size: var(--stu-t-md, 15px);
    letter-spacing: 0;
}

.stu-meeting__tab i {
    width: 15px;
    color: var(--text-tertiary);
}

.stu-meeting__tab.stu-tab--active i {
    color: var(--primary-color);
}

.stu-meeting__tab:focus-visible {
    outline: 2px solid var(--primary-color);
    outline-offset: 2px;
}

.stu-meeting__panel:focus-visible {
    outline: 2px solid var(--primary-color);
    outline-offset: 4px;
    border-radius: var(--radius-md);
}

.stu-meeting__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-md);
    max-width: 46ch;
    margin-inline: auto;
    padding: var(--space-xl) var(--space-lg);
    text-align: center;
}

.stu-meeting__empty .stu-empty__mascot {
    width: 128px;
    height: 128px;
    flex: none;
}

.stu-meeting__empty .stu-empty__mascot svg {
    width: 100%;
    height: 100%;
    display: block;
}

.stu-meeting__empty .stu-empty__text {
    margin: 0;
    font-size: var(--stu-t-md, 15px);
    line-height: 1.7;
    color: var(--text-secondary);
}

/* The deck is not open yet: a matter of the clock or a staff call, never of
   money. The mascot is asleep rather than searching, because nothing is wrong. */
.stu-meeting__pending {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
}

.stu-meeting__pending-mascot {
    width: 96px;
    height: 96px;
    flex: none;
}

.stu-meeting__pending-mascot svg {
    width: 100%;
    height: 100%;
    display: block;
}

.stu-meeting__pending-main {
    min-width: 0;
}

.stu-meeting__pending-main .stu-state {
    margin: var(--space-xs) 0 0;
}

/* ── WHAT THE STUDENTS SEE, ON THE STAFF READER'S SCREEN ─────────────────────
   Above the player, and only ever drawn for staff: the deck is on the page and
   the gate is still shut, a pair that is impossible for anyone else (the
   argument is in StudentMeetingMaterial.vue, on `deckStaffPreview`).

   A STRIP, NOT A CARD. It is an aside about the player below it, and the player
   is the one opaque surface on a page of translucent ones; a second card here
   would compete with it for exactly the wrong thing. The warm trio it wears is
   `.stu-deck__notes`'s, reused rather than reinvented: that family already ships
   with a measured 5.37:1 justification, and this is the same class of remark
   (something true about this slide deck that is not the deck).

   WRAPS ON PURPOSE. The tag and the sentence are one flex row on a desktop and
   two on a 360px handset. Without the wrap the sentence would push the strip
   past the column and take the page sideways with it. */
.stu-meeting__staffnote {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin: 0 0 var(--space-md);
    padding: 10px var(--space-md);
    border-radius: var(--radius-md);
    /* RTL: the physical RIGHT edge, where a Hebrew margin rule belongs - the
       same logical property, and the same reason, as the note strip's. */
    border-inline-start: 2px solid var(--stu-warn-line, rgba(255, 171, 0, 0.45));
    background: var(--stu-warn-wash, rgba(255, 171, 0, 0.12));
    font-size: var(--stu-t-sm, 13px);
    line-height: 1.6;
    color: var(--text-secondary);
}

.stu-meeting__staffnote i {
    width: 14px;
    flex: none;
    color: var(--stu-warn, #8A5C00);
}

/* 500, and the amber ink solid - never the brand gradient, which only reads
   through light letterforms. */
.stu-meeting__staffnote-tag {
    font-weight: 500;
    color: var(--stu-warn, #8A5C00);
}

/* ── Previous / next meeting ─────────────────────────────────────────────────
   Never wrapping: an arrow with nowhere to go is a control the page does not
   draw at all, rather than one that quietly returns to meeting 1. */

.stu-meetnav {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-bottom: var(--space-lg);
}

.stu-meetnav__spacer {
    flex: 1 1 auto;
}

.stu-meetnav__link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: 40px;
    padding-inline: var(--space-md);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-full);
    background: rgba(255, 255, 255, 0.7);
    font-size: 14px;
    color: var(--primary-color);
    text-decoration: none;
    transition: border-color var(--stu-dur-2, 0.22s) ease, background var(--stu-dur-2, 0.22s) ease;
}

.stu-meetnav__link:hover {
    border-color: var(--primary-color);
    background: var(--primary-light);
}

.stu-meetnav__link i {
    width: 14px;
}

/* ── The meeting page's own head and attendance additions ───────────────── */

.stu-session-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-lg);
}

.stu-session-head__main {
    min-width: 0;
}

.stu-session-head__mascot {
    width: 96px;
    height: 96px;
    flex: none;
}

.stu-session-head__mascot svg {
    width: 100%;
    height: 100%;
    display: block;
}

.stu-attendance__doneblock {
    display: flex;
    align-items: center;
    gap: var(--space-md);
}

.stu-attendance__mascot {
    width: 96px;
    height: 96px;
    flex: none;
}

.stu-attendance__mascot svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* ── The live-room door ─────────────────────────────────────────────────── */

.stu-room-door {
    margin-top: var(--space-md);
}

.stu-room-door__btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
}

.stu-room-door__btn i {
    width: 18px;
}

.stu-room-door__note {
    margin: var(--space-sm) 0 0;
    font-size: 14px;
    line-height: 1.6;
    color: var(--text-secondary);
    max-width: 46ch;
}

.stu-room-door__pay {
    display: inline-block;
    padding-top: 2px;
}

/* ── Reduced motion ─────────────────────────────────────────────────────────
   Nothing here animates layout, and a state that only exists as motion is a
   state a reduced-motion reader never sees, so the colours stay and the
   movement goes. */

/* ── FULL SCREEN ─────────────────────────────────────────────────────────────
   The slide and its note over the whole page, with the area's side menu left
   standing beside it.

   `display: contents` is what makes this one set of nodes rather than two. In
   the ordinary state the wrapper is not in the layout at all and the stage, the
   note, the step controls and the index are the card's own children exactly as
   they were; opening full screen turns the wrapper into a fixed panel around the
   same elements. Nothing is duplicated, so the slide the reader was on and the
   scroll position inside a tall one both survive the press.

   `inset-inline-start` is the rail's width because the area is RTL and the rail
   is the first flex child of `.stu-shell`, which puts it on the PHYSICAL RIGHT -
   the inline start. So this leaves the menu visible and covers everything else.
   The fallback repeats 288px for the one case where the token cannot be reached:
   a layer that ever escapes `.stu-shell`.

   Its own ground rather than a scrim: the layer is a reading surface, and text
   over a darkened page is a dialog. It is the shell's own near-white with the
   same soft radials, so full screen reads as the page having grown rather than
   as something covering it. */
.stu-deck__layer {
    display: contents;
}

.stu-deck__layer.is-full {
    display: flex;
    flex-direction: column;
    position: fixed;
    inset-block: 0;
    inset-inline-start: var(--stu-rail-w, 288px);
    inset-inline-end: 0;
    z-index: 1000;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-lg) clamp(var(--space-md), 4vw, 48px) var(--space-xl);
    gap: var(--space-md);
    background:
        radial-gradient(ellipse 980px 640px at 88% -8%, rgba(8, 148, 255, 0.11), transparent 62%),
        radial-gradient(ellipse 880px 720px at 4% 96%, rgba(105, 53, 216, 0.085), transparent 64%),
        #FAFCFF;
}

/* The card is `overflow: clip`, which does not clip a fixed descendant - but a
   layer whose correctness rests on that staying true across browsers is one
   repaint away from being half-clipped on somebody's phone. Lifted only while
   the layer is open, and the card underneath is covered by then anyway. */
.stu-deck:has(.stu-deck__layer.is-full) {
    overflow: visible;
}

/* THE LAYER'S GAP, RESTATED ONE BOX IN. The slide and its note are one flex
   child of this column now (`.stu-deck__hold`, which exists so the height floor
   has a box to sit on), so the layer's own `gap` no longer falls between them
   and the note ran flush against the stage. Same token, so the full-screen
   column reads exactly as it did before the hold existed. */
.stu-deck__layer.is-full .stu-deck__hold {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

/* THE STAGE IS STILL 16:9 IN HERE, so on a wide screen it has to be told how
   TALL it may be rather than how wide. `.stu-deck__layer.is-full` is a flex
   column and the stage is `width: 100%`, so on a 1600px-wide layer the ratio
   asks for 900px of height and the bar, the note and the step controls are all
   pushed off the bottom of a 900px laptop.

   The 300px is the layer's own furniture, added up rather than guessed:

     layer block padding   24 (--space-lg) + 40 (--space-xl)          =  64
     sticky bar            44px close button + 2 x 8 padding-block    =  60
     viewport padding      32 top + 40 bottom                         =  72
     sticky control row    40px button + 2 x 14 padding + 2 border    =  70
     three x --space-md    the layer's own gap between four children  =  48
                                                                       ----
                                                                        314

   Rounded to 300 on purpose, and not tighter: the note strip is not in that sum
   and neither is the jump index, both of which scroll into view under the slide
   rather than beside it, and a slide that is 14px too tall is a slide whose
   controls a reader has to scroll to find. `min()` hands a narrow screen - where
   the width, not the height, is what binds - straight back to the plain
   full-width box. */
.stu-deck__layer.is-full .stu-deck__stage {
    width: min(100%, calc((100dvh - 300px) * 16 / 9));
    margin-inline: auto;
}

/* The video player takes the STAGE's width in here, from the same expression,
   so the two 16:9 boxes line up on one axis instead of the player running the
   full width of a 1600px layer under a slide that is 300px narrower. It is
   below the slide, so reaching it means scrolling the layer - which is what
   already happens for the note strip and the jump index, and is the honest
   arrangement: a reader who opened full screen to READ a slide has not asked
   for the video to displace it. */
.stu-deck__layer.is-full .stu-deck__video {
    width: min(100%, calc((100dvh - 300px) * 16 / 9));
    margin-inline: auto;
}

/* The bar: which slide this is, and the way out. Sticky, so the X is still in
   reach at the foot of a slide authored for a projector. */
.stu-deck__fsbar {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding-block: var(--space-sm);
    /* The layer's ground, opaque, so a slide scrolling under the bar does not
       show through it. */
    background: #FAFCFF;
}

.stu-deck__fspos {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    font-size: var(--stu-t-md, 15px);
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.stu-deck__fscount {
    font-family: var(--font-mono);
    font-size: var(--stu-t-xs, 12px);
    color: var(--text-tertiary);
}

.stu-deck__fsclose {
    flex: none;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 1px solid var(--stu-glass-line, rgba(98, 98, 234, 0.14));
    border-radius: var(--radius-full);
    background: var(--bg-surface);
    color: var(--text-secondary);
    font-size: 17px;
    cursor: pointer;
}

.stu-deck__fsclose:hover {
    color: var(--primary-color);
    border-color: var(--primary-border);
}

/* The step controls stay in reach at the foot of a slide authored for a
   projector, so stepping through a deck never needs a scroll back down.

   STICKY, NOT PINNED TO THE BOTTOM. `margin-top: auto` would push them there on
   a SHORT slide too, and a title card left half a screen of nothing between the
   note and the controls. Sticky does nothing at all when there is no scrolling
   and everything when there is, which is exactly the difference between those
   two cases. */
.stu-deck__layer.is-full .stu-deck__ctl {
    position: sticky;
    bottom: 0;
    border-radius: var(--radius-lg);
    border: 1px solid var(--stu-glass-line, rgba(98, 98, 234, 0.14));
    background: var(--bg-surface);
}

/* The card's own full-bleed blocks are edge-to-edge because the card clips
   them. Inside the layer there is no card, so they get their own shape. */
.stu-deck__layer.is-full .stu-deck__notes,
.stu-deck__layer.is-full .stu-deck__index {
    border-radius: var(--radius-lg);
    border: 1px solid var(--stu-glass-line, rgba(98, 98, 234, 0.14));
}

@media (prefers-reduced-motion: reduce) {
    .stu-deck__progress > i,
    .stu-deck__step,
    .stu-deck__dots i {
        transition: none;
    }

    .stu-deck__step:hover:not(:disabled) {
        transform: none;
    }
}

/* ── Coarse pointers pay the space; a mouse keeps the tight desktop rhythm. */

@media (pointer: coarse) {
    /* EVERY control in this file, not a selection of them. The full-screen
       button (which still wears `.stu-deck__expand`) and the two meeting arrows
       were left out of the first version of this block and measured 36px and
       40px on a phone, which is the one surface a student reads the material on.
       Measured with `pointer: coarse` actually matching, not assumed. */
    .stu-deck__index-row,
    .stu-deck__step,
    .stu-deck__expand,
    .stu-meeting__tab,
    .stu-meetnav__link {
        min-height: 44px;
    }

    .stu-deck__index-row {
        padding-inline: var(--stu-s-3, 12px);
    }

    .stu-deck__dots i {
        width: 8px;
        height: 8px;
    }
}

/* ── Tablet ─────────────────────────────────────────────────────────────── */

@media (max-width: 1024px) {
    /* The frame gives the slide back some of the card's width. The stage itself
       needs nothing here: a 16:9 box is already responsive, and a `max-height`
       on it would be the one thing that could break the ratio. */
    .stu-deck__viewport {
        padding: var(--space-lg);
    }

    /* The side menu is a drawer below this width, so there is nothing on screen
       to leave room for and the layer takes the whole viewport. The drawer sits
       at z-index 1002 and still opens over it. */
    .stu-deck__layer.is-full {
        inset-inline: 0;
        padding-inline: var(--space-md);
    }

    .stu-deck__index-list {
        columns: 2;
    }
}

/* ── Phone ──────────────────────────────────────────────────────────────── */

@media (max-width: 767px) {
    .stu-deck__head {
        padding: var(--space-md) var(--space-md) var(--space-sm);
    }

    .stu-deck__viewport {
        padding: var(--space-md);
    }

    /* ── THE SLIDE IN WORDS, and this query is the only thing that draws it.
       See `.stu-deck__read` above for why the hidden state is the default. Sized
       as body copy on a phone - a student READS this, they do not project it -
       and it inherits the page's RTL, so nothing here restates a direction.

       These values are `.cl-student__read`'s, from class-app/public/css/app.css,
       because the two surfaces show the same rewrite of the same slide to the
       same person on the same handset. Change one, change the other. */
    .stu-deck__read {
        display: block;
        margin-top: var(--space-lg);
        font-size: 17px;
        line-height: 1.75;
        color: var(--text-color);
    }

    /* The gap between blocks is what makes this read as copy rather than as a
       wall. MATCHED AT ANY DEPTH, not with a child combinator: the authored
       prose arrives as one `<div dir="rtl">` wrapping its paragraphs, so
       `> * + *` sees a single child and sets nothing - which is exactly what it
       did in the room, and the two paragraphs ran together with no space at all.
       A list is indented with `padding-right`, per the repo's RTL rule. */
    .stu-deck__read :is(p, ul, ol, h2, h3, blockquote) + :is(p, ul, ol, h2, h3, blockquote) {
        margin-top: var(--space-md);
    }

    .stu-deck__read ul,
    .stu-deck__read ol {
        padding-right: 1.25em;
    }

    .stu-deck__read li + li {
        margin-top: var(--space-xs);
    }

    .stu-deck__read strong {
        font-weight: 600;
    }

    .stu-deck__read code {
        font-family: var(--font-mono);
        font-size: 0.92em;
        padding: 1px 5px;
        border-radius: var(--radius-sm);
        background: var(--primary-light);
        color: var(--primary-color);
    }

    /* Wrapped, and each step takes half the row, so neither label is squeezed
       into a column a few characters wide. */
    .stu-deck__ctl {
        flex-wrap: wrap;
        padding: var(--space-sm) var(--space-md);
    }

    .stu-deck__step {
        flex: 1 1 0;
        justify-content: center;
        min-height: 44px;
    }

    .stu-deck__ctl-spacer {
        display: none;
    }

    /* Its own centred row between the two steps and the full-screen control.
       In the row with them it would take its width off `flex: 1 1 0` buttons
       whose labels are already two Hebrew words, and "השקופית הקודמת" wrapped to
       three lines. `order: 2` keeps it above the full-screen control at 3. */
    .stu-deck__pos {
        flex: 1 1 100%;
        order: 2;
        justify-content: center;
        padding-block: var(--space-xs);
    }

    .stu-deck__expand {
        flex: 1 1 100%;
        order: 3;
        justify-content: center;
    }

    .stu-deck__dots {
        flex: 1 1 100%;
        order: 3;
        justify-content: center;
    }

    .stu-deck__counter {
        padding: 12px var(--space-md);
    }

    .stu-deck__index-list {
        columns: 1;
    }

    .stu-deck__index-panel {
        max-height: 50vh;
        overflow-y: auto;
    }

    .stu-deck__notes {
        padding: var(--space-md);
    }

    .stu-deck__rule,
    .stu-deck__materials-title,
    .stu-deck__materials {
        margin-inline: var(--space-md);
    }

    .stu-deck__materials {
        padding-inline: 0;
    }

    /* The head stacks rather than shrinking the robot: 96px is the artwork's
       floor (StudentMascot.vue), below which the 3-unit stroke aliases into
       fuzz, so a narrow screen gives it a row of its own instead. */
    .stu-session-head {
        flex-direction: column-reverse;
        align-items: flex-start;
        gap: var(--space-sm);
    }

    .stu-meeting__pending {
        flex-direction: column;
        align-items: flex-start;
    }

    .stu-attendance__doneblock {
        flex-direction: column;
        align-items: flex-start;
    }

    .stu-room-door__btn {
        width: 100%;
        justify-content: center;
    }
}

/* ── The recording panel ─────────────────────────────────────────
   A link out to YouTube, never an embed: the video is private and a private
   video will not play in an iframe on this origin, so the panel is a short
   note plus one button rather than a player.

   Centred and narrow like `.stu-meeting__empty` beside it, because the panel
   holds two elements and a full-width button on a desktop column reads as an
   error state rather than an action. */

.stu-recording {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-lg);
    max-width: 46ch;
    margin-inline: auto;
    padding: var(--space-xl) var(--space-lg);
    text-align: center;
}

.stu-recording__note {
    margin: 0;
    color: var(--text-secondary);
    font-size: 0.95rem;
    line-height: 1.7;
}

.stu-recording__link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
}

/* The icon reads as a play affordance, so it keeps the button's text colour
   rather than the brand gradient - a gradient on a glyph this small bands. */
.stu-recording__link svg {
    width: 1.1em;
    height: 1.1em;
}

@media (max-width: 767px) {
    .stu-recording {
        padding: var(--space-lg) var(--space-md);
    }

    .stu-recording__link {
        width: 100%;
        justify-content: center;
    }
}

/* ── PAPER HAS NO PLAYER ─────────────────────────────────────────────────────
   A student who presses Ctrl+P on a meeting page prints the carousel as it
   stands (student-materials.css explains why that path keeps the open slide),
   and an iframe on paper is a black rectangle the size of the slide above it.
   The frame goes; the link under it stays, and the print sheet's
   `content: attr(href)` writes the address after it, so the printed page still
   says where the clip is.

   The deck's own PDF button never renders a player at all - its print copy is a
   second run of elements that draws a plain link - so this rule exists purely
   for the reader who prints the page themselves. */
@media print {
    .stu-deck__video-frame {
        display: none !important;
    }
}
