/* ═══════════════════════════════════════════════════════════════════════════
   LSD PORTAL — SHARED RESPONSIVE LAYER                            Phase 12
   ═══════════════════════════════════════════════════════════════════════════

   WHY THIS FILE EXISTS
   Responsive work was done page-by-page as each phase shipped, so six pages
   ended up with five different breakpoint sets (1024/720/420 · 900/720/480 ·
   880/420 · 720/520 · 860/700/600/480) and wildly uneven quality. index.html
   and manzoori.html are a copy-paste pair differing by ~127 lines; audio.html
   and quran-ikhtebar.html independently reinvented the same two-row-header
   recipe. Editing six inline <style> blocks guarantees they drift again.

   HOW IT COMPOSES
   Linked AFTER each page's inline <style> block, so at equal specificity this
   file wins. The division of labour is deliberate:

       inline <style>   →  APPEARANCE (colors, borders, type, brand look)
       responsive.css   →  LAYOUT at every width

   That split is what makes "responsive only, keep both looks" safe: the two
   token systems (paper-ink-amber on admin, brown-gold here) are never touched.

   MOBILE-FIRST, HONESTLY SCOPED
     · Universal hygiene (touch, text-size-adjust, overscroll, 16px controls)
       is unprefixed — it is correct at every width.
     · The SHELL is authored phone-first: base rules are the phone layout and
       `min-width: 901px` restores the single-row desktop header. This is only
       possible because all five app-page `.header` rules are structurally
       identical (64px tall, 24px gutter, flex, space-between).
     · Page-specific component tiers use `max-width`. Deliberate: those pages'
       desktop CSS lives in untouched inline blocks, and flipping them to
       min-width would mean rewriting all six inline stylesheets — exactly the
       re-skin risk the "responsive only" decision rules out.

   BREAKPOINT LADDER — the single ladder, replacing all five old sets
     base            phone portrait   iPhone SE 375 · 17 402 · 17 Pro Max 440
                                      Pixel 9 Pro 412 · S25 Ultra 412
     min-width: 481  large phone / phone landscape (667–956)
     min-width: 640  foldable unfolded / small tablet
                     (Pixel 9 Pro Fold inner ~674, Galaxy Z Fold inner ~653-700)
     min-width: 901  desktop, single-row header

   901px, not 721px: the header does not overflow at iPad-portrait widths, it
   SQUASHES — clipping the active nav tab behind the FILES link and wrapping
   the quota pill. The breakpoint has to clear the content's real minimum, not
   the point where a scrollbar finally appears. (Inherited from audio.html.)

   Capability queries do the rest, because they ask the right question:
   `pointer: coarse` gives a touch laptop 44px targets while a narrow desktop
   window stays compact — width can never tell you that.

   TEST CONSTRAINT — READ BEFORE ADDING AN OVERLAY
   `test_fullscreen_flex_overlays_can_scroll` is parametrised over every
   frontend/*.css, so it polices this file. Any rule carrying all three of
   `position: fixed`, `display: flex` and `inset: 0` must also have
   `overflow-y: auto` and must NOT have `align-items: center` — a flex item
   taller than its container overflows BOTH ways, putting its top out of
   scroll reach. Use `align-items: flex-start` + `margin: auto` on the child.
   The bottom sheets below sidestep this by pinning left/right/bottom rather
   than using `inset: 0`.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ─── § 1 · LAYOUT TOKENS ─────────────────────────────────────────────────
   Mobile-first: the bare :root values are the phone's. Each tier raises them.
   Everything downstream reads the token, so a gutter change happens once. */

:root {
  --shell-gutter: 14px;
  --header-h: 56px;

  /* WCAG 2.5.5 / Apple HIG minimum. */
  --tap: 44px;

  /* Landscape on a notched iPhone takes ~59px on the notch side, so the
     inset can exceed the gutter — max() keeps whichever is larger. */
  --safe-l: max(var(--shell-gutter), env(safe-area-inset-left, 0px));
  --safe-r: max(var(--shell-gutter), env(safe-area-inset-right, 0px));
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);

  /* Height the iOS soft keyboard is covering. Published by viewport.js from
     window.visualViewport; stays 0px everywhere else, including Android,
     where `interactive-widget=resizes-content` shrinks the layout viewport
     and dvh already does the right thing. */
  --kb-inset: 0px;

  /* Tells the UA to render form controls, scrollbars and the canvas in light
     colors. Without it iOS will happily auto-darken a <select> out from under
     the brand palette. */
  color-scheme: light;
}

@media (min-width: 481px) { :root { --shell-gutter: 16px; } }
@media (min-width: 640px) { :root { --shell-gutter: 20px; } }
@media (min-width: 901px) { :root { --shell-gutter: 24px; --header-h: 64px; } }


/* ─── § 2 · UNIVERSAL HYGIENE ─────────────────────────────────────────────
   Correct at every width, so no media query. */

html {
  /* iOS inflates text when a page rotates to landscape unless told not to.
     100% (not `none`) so user-initiated zoom still works. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* Nothing may exceed the viewport. Note we do NOT set `overflow-x: hidden` on
   body — that hides overflow bugs rather than fixing them, and would make the
   no-horizontal-overflow assertion in the device matrix pass falsely. */
img, svg, video, canvas { max-width: 100%; height: auto; }
pre, code { overflow-x: auto; }

/* A long unbroken token (a URL, an un-spaced Arabic string) is the other way
   a page gains a horizontal scrollbar. */
.msg-bubble, .message-bubble, .lsd-render, .preview, .exam-verse-card {
  overflow-wrap: anywhere;
}

/* Removes the ~300ms delay browsers reserve for double-tap-to-zoom, and stops
   a double tap on a control zooming the page instead of firing twice. */
a, button, summary, label,
[role="button"], input[type="submit"], input[type="button"],
.header-link, .header-btn, .seg-control a, .pick-btn, .drop-zone {
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(92, 58, 40, 0.14);
}

/* Long-pressing a button on iOS raises the copy/share callout and starts a
   text selection. Controls only — never links or content, where selecting
   text is the whole point. */
button, summary,
.header-link, .header-btn, .seg-control a,
.mic-btn-large, .stop-btn, .retry-btn, .play-mini, .pick-btn {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Scroll chaining: without `contain`, flinging past the end of any of these
   scrolls the page behind it — and on Android Chrome, fires pull-to-refresh
   mid-chat, discarding whatever was typed. */
.chat-container, .page-body, .messages,
.audio-history, .upload-queue,
.annot-modal, .header-right {
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* Every control keeps a visible focus ring for keyboard and switch-control
   users. `:focus-visible` so a tap does not leave a ring behind. */
a:focus-visible, button:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--gold-600, #A67C3D);
  outline-offset: 2px;
}


/* ─── § 3 · APP SHELL: HEADER + CROSS-PAGE NAV ────────────────────────────
   Phone-first. Base = the two-row header; min-width:901px puts it back on one
   row. Shared by index / manzoori / audio / quran-ikhtebar / files, whose
   .header rules are structurally identical.

   THE BUG THIS FIXES: index.html and manzoori.html had
   `@media (max-width: 420px) { .seg-control { display: none } }` — which left
   a phone user with NO way to reach Chat, Manzoori or Audio. audio.html had
   already spotted this and kept the nav in a scrollable row instead. That
   pattern is now portal-wide, and `.seg-control` is never hidden anywhere.
   Pinned by test_mobile_readiness.py::test_cross_page_nav_is_never_hidden. */

.header {
  height: auto;
  min-height: var(--header-h);
  flex-wrap: wrap;
  align-items: center;
  row-gap: 6px;
  padding: calc(6px + var(--safe-t)) var(--safe-r) 0 var(--safe-l);
}

/* min-width:0 is what lets a long title actually ellipsis instead of forcing
   the flex row wider than the screen — the default `min-width: auto` on a
   flex item refuses to shrink below its content. */
.header-left { flex: 1 1 auto; min-width: 0; }
.header-left > div { min-width: 0; }
.header-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The email line is the first thing to go — it is recoverable from /files or
   the admin page, the nav is not. */
.header-subtitle { display: none; }

.header-right {
  order: 3;
  flex: 1 0 100%;
  justify-content: flex-start;
  gap: 6px;
  padding-bottom: 8px;
  /* Even wrapped onto its own row, quota + 3-way nav + FILES + ADMIN + logout
     can exceed a 320px screen. Scroll it sideways in place rather than
     clipping the logout button off the end. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.header-right::-webkit-scrollbar { display: none; }
/* Scrollable flex children must not shrink, or they compress instead. */
.header-right > * { flex-shrink: 0; }

.seg-control { flex-shrink: 0; }

/* ── Tablet portrait and up: the email line fits again ── */
@media (min-width: 721px) {
  .header-subtitle { display: block; }
}

/* ── Desktop: back to one row ── */
@media (min-width: 901px) {
  .header {
    height: var(--header-h);
    min-height: 0;
    flex-wrap: nowrap;
    row-gap: 0;
    padding: 0 var(--safe-r) 0 var(--safe-l);
  }
  .header-left { flex: 0 1 auto; }
  .header-right {
    order: 0;
    flex: 0 1 auto;
    justify-content: flex-end;
    gap: 8px;
    padding-bottom: 0;
    overflow-x: visible;
  }
}


/* ─── § 4 · TOUCH TARGETS ─────────────────────────────────────────────────
   Keyed on the pointer, not the width. A touch laptop or an iPad Pro at
   1024px gets 44px targets; a narrow desktop window does not get needlessly
   chunky ones. */

@media (pointer: coarse) {
  .header-link, .header-btn { height: var(--tap); min-width: var(--tap); }
  .seg-control a { height: var(--tap); padding: 0 14px; }
  .audio-history-row .play-mini,
  .upload-row .status-icon {
    min-width: var(--tap);
    min-height: var(--tap);
  }
  .audio-history-row { padding: 12px; }
  .word-timing-chip { padding: 6px 10px; font-size: 12px; }
  .ww-add, .na-btn, .grade-submit-row .btn { min-height: var(--tap); }
  /* The composer collapses to 40px when empty, which is under the minimum and
     is the single most-tapped element on the phone. It still grows to its
     140px max as the user types. */
  .input-field { min-height: var(--tap); }
  .stars .star { min-width: var(--tap); min-height: var(--tap); font-size: 24px; }
  .form-pwd-toggle { min-height: var(--tap); min-width: var(--tap); }
  .exam-mistake-group summary { min-height: var(--tap); }
  .submit-btn, .pick-btn, .exam-start-btn { min-height: var(--tap); }
  /* The minutes form. `.mform-remove` is a 32px × that DELETES a line someone
     just typed, which is the worst possible combination of small and
     destructive; `.btn-ghost` carries SAVE DETAILS and both export buttons.
     The checkbox gets its target from the label wrapping it, so the whole
     "Online" control is tappable rather than just the 13px box. */
  .mform-remove, .mform-add, .mform-online, .btn-ghost {
    min-height: var(--tap);
    min-width: var(--tap);
  }

  /* NOT resized: `.logo-mark` stays 38x38. It is a link to /app, and the CHAT
     entry in .seg-control is a >=44px control to the same destination on the
     same screen — which is WCAG 2.5.5's explicit equivalent-control exception.
     Inflating the brand mark to 44px to satisfy a checker that the criterion
     already exempts would be the wrong trade. Recorded here so the next person
     to run the tap-target audit knows it was a decision, not an oversight. */

  /* iOS zooms the whole page when a control smaller than 16px takes focus,
     then leaves it zoomed. This is a floor, not a restyle — desktop keeps its
     designed 13/15px sizes because this block never applies there.
     The class selectors are needed: a bare `textarea` (0,0,1) loses to
     `.input-field` (0,1,0) no matter which file it is in. */
  input, select, textarea,
  .input-field, .form-input, .modal-input, .modal-select,
  .ww-row input, .annot-modal input, .annot-modal select, .annot-modal textarea {
    font-size: 16px;
  }
}

/* Hover affordances must never be the only way to see something. Anything
   that appears purely on :hover gets pinned visible where hover is faked. */
@media (hover: none) {
  .copy-btn, .annot-action-btn, .msg-actions { opacity: 1; }
}


/* ─── § 5 · CHAT PAGES (index.html · manzoori.html) ───────────────────────
   The 1611/1660-line copy-paste twins. Every rule here lands on both at once,
   which is the entire point of this file. */

/* `height: 100%` under the iOS dynamic toolbar measures the LARGE viewport,
   so the composer sits behind the toolbar until the user scrolls. dvh tracks
   it. The kb-inset subtraction lifts the whole shell above the iOS keyboard —
   Android needs no equivalent because interactive-widget=resizes-content
   already shrinks the layout viewport. */
.page-chat {
  height: 100vh;                                  /* fallback */
  height: calc(100dvh - var(--kb-inset, 0px));
  /* Kills Chrome Android's pull-to-refresh on the app shell. Only the chat
     pages: on a document-scroll page like /files, refresh-on-pull is the
     behaviour a user expects. */
  overscroll-behavior-y: none;
}

/* The composer is the one element that must clear the home indicator, at every
   width — an iPhone in landscape has a notch inset too. Written as longhand
   max() against the DESIGNED values (14px 16px 18px) rather than as a new
   shorthand, so on hardware with no insets the computed padding is byte-for-
   byte what the page already had. This is the pattern to copy anywhere a
   designed value must survive contact with a safe area. */
.page-chat .input-area {
  padding-left: max(16px, env(safe-area-inset-left, 0px));
  padding-right: max(16px, env(safe-area-inset-right, 0px));
  padding-bottom: max(18px, calc(18px + env(safe-area-inset-bottom, 0px)));
}

/* The composer row is `display: flex` with the textarea on `flex: 1`. On a
   344px fold the send button was being SQUEEZED from its declared 44px to 38px
   — a flex item's default `flex-shrink: 1` overrides an explicit width when
   the row runs out of room, so a control can measure smaller than its own CSS
   says and nothing in the stylesheet looks wrong. Never conditional: a narrow
   desktop window squeezes it exactly the same way. */
.send-btn, .mic-btn-inline { flex-shrink: 0; }

/* Toast: additive margin rather than a new `bottom`, so the designed offset
   is preserved and the inset is simply added on notched hardware. */
.toast { margin-bottom: var(--safe-b); }

/* ── Phone and tablet-portrait: the chat surface itself ──
   Scoped to max-width rather than left unprefixed because these override the
   page's designed desktop values, which the "responsive only" decision says
   must not change. Values carried over from the twins' old 720px block. */
@media (max-width: 900px) {
  .page-chat .messages { padding: 16px var(--shell-gutter) 20px; gap: 10px; }
  .page-chat .msg-body { max-width: 92%; }
  .page-chat .msg-bubble { font-size: 14px; padding: 10px 14px; }
  .page-chat .msg-bubble.lsd { font-size: 19px; line-height: 1.7; }
  .page-chat .msg-avatar { width: 30px; height: 30px; font-size: 14px; }
  .page-chat .msg-time { font-size: 10px; flex-wrap: wrap; }
  .page-chat .copy-btn { padding: 4px 8px; font-size: 10px; }
  .page-chat .welcome-heading { font-size: 22px; }
  .page-chat .welcome-arabic { font-size: 56px; letter-spacing: 2px; }
  .page-chat .gate-hint {
    margin-left: var(--shell-gutter);
    margin-right: var(--shell-gutter);
  }

  .page-chat .input-area { padding-top: 8px; }
  .page-chat .input-box { padding: 4px 12px; }
  .page-chat .input-meta {
    font-size: 10px;
    flex-direction: column;
    gap: 4px;
    align-items: flex-start;
  }
}

@media (max-width: 480px) {
  .page-chat .msg-body { max-width: 94%; }
  .page-chat .msg-bubble.lsd { font-size: 17px; }
  .page-chat .welcome-arabic { font-size: 48px; }
  .page-chat .welcome-heading { font-size: 20px; }
  .page-chat .grade-form { padding: 12px; }
}

/* ── Grade form: the wrong-words editor ──
   A 4-column grid (word / correction / note / delete) is unusable at 360px.
   Each row becomes a card with the header row dropped, since a stacked field
   carries its own placeholder. */
@media (max-width: 720px) {
  .ww-head { display: none; }
  .ww-row {
    grid-template-columns: 1fr;
    gap: 6px !important;
    padding: 8px;
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    background: var(--bg-white);
    margin-bottom: 8px;
    position: relative;
  }
  .ww-row input { padding: 8px 10px; }
  .ww-row .ww-del { position: absolute; top: 6px; right: 6px; width: 32px; height: 32px; }
  .ww-add { padding: 12px; font-size: 12px; }

  .grade-form { padding: 14px; margin-top: 12px; }
  .grade-header .title { font-size: 13px; }
  .dim-row { margin-bottom: 10px; }
  .dim-row .dim-label { font-size: 13px; }
  .dim-row .dim-help { font-size: 11px; }
  .stars { gap: 2px; flex-wrap: wrap; }
  .grade-submit-row { flex-direction: column; gap: 10px; align-items: stretch; }
}

/* ── Annotate modal → bottom sheet ──
   Pinned left/right/bottom rather than `inset: 0`, which keeps it out of the
   fixed-flex-overlay trap described in the header comment. A sheet is also
   simply the right shape on a phone: it opens under the thumb, and it cannot
   be centred off the top of a short viewport. */
@media (max-width: 720px) {
  .annot-modal {
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    transform: none;
    width: auto;
    max-width: none;
    max-height: 85dvh;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: 20px var(--safe-r) calc(20px + var(--safe-b)) var(--safe-l);
  }
}


/* ─── § 6 · AUDIO (audio.html) ────────────────────────────────────────────
   Carried over from the page's own inline block, which was the portal's
   reference implementation before this file existed. */

@media (max-width: 900px) {
  .audio-page-grid { max-width: none; padding: 20px; }
}

@media (max-width: 720px) {
  .audio-page-grid {
    padding: 16px var(--safe-r) calc(24px + var(--safe-b)) var(--safe-l);
    gap: 16px;
  }
  .audio-page-title { font-size: 22px; }
  .audio-result { padding: 16px; }
}

@media (max-width: 720px) {
  /* The minutes form (Phase 19). Two columns of text boxes fit a laptop and
     nothing else: at 720px the Arabic name fields are narrower than the names
     that go in them, and the attendance roll — name + idara + Online + remove
     on one line — has four controls competing for half a phone. Both grids
     go to one column together, because a one-column header above a
     two-column roll reads as a broken layout rather than a dense one. */
  .mform-grid, .mform-rolls { grid-template-columns: 1fr; }
}

@media (max-width: 700px) {
  /* dropzone | recorder side by side needs both to be usable; below 700 the
     recorder's mic button starts colliding with the dropzone copy. */
  .audio-input-card { grid-template-columns: 1fr; }
}

@media (max-width: 420px) {
  /* Below this the roll's own row is the problem, not the grid: four controls
     on one line leaves the two text boxes at ~70px each. The name and idara
     boxes take a line each and the checkbox drops to a third — which is why
     the "Online" label STAYS: there is a whole line for it down here, and a
     bare checkbox beside a × is a control nobody can read. */
  .mform-roll-row { flex-wrap: wrap; }
  .mform-roll-row .mform-input { flex: 1 1 100%; }
}

@media (max-width: 480px) {
  .audio-page-title { font-size: 20px; }
  .audio-dropzone { padding: 24px 12px; }
  .audio-recorder { padding: 20px 12px; }
  .audio-result { padding: 14px; gap: 14px; }
  /* 1.4em of an already-large LSD face overflows a 360px screen. */
  .audio-result .message-bubble { font-size: 1.25em; padding: 10px 12px; }
  .audio-result .translation { font-size: 14px; }
  /* Stack the date under the transcript preview so neither is squeezed. */
  .audio-history-row { flex-wrap: wrap; row-gap: 4px; }
  .audio-history-row .preview { flex: 1 1 auto; font-size: 16px; }
  .audio-history-row .date { flex: 1 0 100%; text-align: right; }
}

@media (pointer: coarse) {
  .mode-toggle button { min-height: var(--tap); }
}


/* ─── § 7 · QURAN IKHTEBAR (quran-ikhtebar.html) ──────────────────────────*/

@media (max-width: 900px) {
  .exam-page-grid { max-width: none; padding: 20px; }
}

@media (max-width: 720px) {
  .exam-page-grid {
    padding: 16px var(--safe-r) calc(24px + var(--safe-b)) var(--safe-l);
    gap: 16px;
  }
  .exam-page-title { font-size: 22px; }
  .exam-verse-card { padding: 16px 14px; }
}

@media (max-width: 480px) {
  .exam-page-title { font-size: 20px; }
  .exam-score-tiles { gap: 8px; }
  .exam-score-tile { padding: 12px; flex-basis: 100px; }
  .exam-score-value { font-size: 22px; }
  .exam-stepper { flex-wrap: wrap; row-gap: 6px; }
}

/* The rubric USED to be a 4-column table with a 520px min-width that scrolled
   sideways on a phone, behind an edge-fade meant to advertise the scroll. It
   was the wrong trade: a student contesting a grade had to drag the marks
   into view, and the fade only made a bad affordance discoverable. It is now
   a list of criteria (.exam-rubric-list) that reflows — the criterion column
   is minmax(0, 1fr) and the descriptor spans the full width beneath, so the
   whole block fits a 320px screen with nothing off-canvas.

   Nothing here may reintroduce a width, a min-width or an overflow on the
   rubric. Pinned by test_the_rubric_never_scrolls_sideways. */

/* Stacked, the criterion column absorbs all the slack — which is right on a
   phone and wrong on anything wider, where it leaves a growing void between
   the criterion and its marks and pushes the descriptor onto a line of its
   own below. From 640px there is room to put the descriptor back in that
   void: four columns, the descriptor taking what is left.

   The criterion column is a FIXED 10rem here, not max-content. Each item is
   its own grid, so a content-sized column would resolve differently per row
   and the marks would stop lining up down the block — the one thing the old
   table was actually buying. 10rem holds every criterion in the seeded
   rubric, and a longer one wraps rather than widening anything.

   This is a reflow, NOT the old table: no min-width, so it collapses back to
   the stacked layout below 640px instead of scrolling. */
@media (min-width: 640px) {
  .exam-rubric-item {
    grid-template-columns: 10rem 2.5rem 4.5rem minmax(0, 1fr);
    column-gap: 14px;
  }
  .exam-rubric-desc { grid-column: auto; }
}

/* ── Phase 14: the marked passage on the results screen ──
   Same rule as the rubric above: it reflows and must NEVER scroll sideways,
   or a student reads Quran through a letterbox. Nothing here sets a width. */

@media (max-width: 480px) {
  .exam-passage-block { padding: 12px 10px; }
  .exam-passage-legend { gap: 10px; row-gap: 6px; }
}

/* Marked words are the only tappable thing inside a block of text, so they
   need real target height without opening gaps between the unmarked words
   around them. Vertical padding plus inline-block gets that; the generous
   line-height on .exam-passage is what stops the padded boxes colliding
   between lines. Keyed on the pointer, not the width — a touch laptop needs
   this and a narrow desktop window does not. */
@media (pointer: coarse) {
  .exam-passage-word.marked {
    display: inline-block;
    padding: 6px 4px;
    margin: 0 -2px;
  }
}


/* ─── § 8 · FILES (files.html) ────────────────────────────────────────────*/

@media (max-width: 720px) {
  .page-inner { padding: 32px var(--shell-gutter) calc(48px + var(--safe-b)); }
  .drop-zone { padding: 36px 20px; }
  .welcome-heading { font-size: 24px; }
}

/* The progress bar used to be `display: none` below 520px. Hiding the only
   feedback an upload gives is not a responsive strategy — a user on a slow
   phone connection is exactly who needs it. It reflows to a full-width bar
   beneath the filename instead. */
@media (max-width: 620px) {
  .upload-row { flex-wrap: wrap; row-gap: 8px; }
  .upload-row .file-info { flex: 1 1 auto; min-width: 0; }
  .upload-row .progress-wrap {
    order: 5;
    flex: 1 0 100%;
    width: auto;
  }
  .upload-row .progress-label { text-align: left; }
}


/* ─── § 9 · LOGIN (login.html) ────────────────────────────────────────────
   Phone-first: one column, brand panel demoted to a compact band. The old
   `min-height: 40vh` on the brand half pushed the form below the fold on an
   iPhone SE — you landed on a sign-in page with no sign-in form in sight. */

.page-login { overflow: auto; }

.auth-layout {
  grid-template-columns: 1fr;
  height: auto;
  min-height: 100svh;
}

.brand-panel {
  padding: 20px var(--safe-r) 20px var(--safe-l);
  min-height: 0;
}
.brand-center { min-height: 96px; }
.brand-arabic { font-size: 52px; letter-spacing: 2px; }
.brand-art { background-size: 140px 140px; }

.form-panel {
  padding: 28px var(--safe-r) calc(32px + var(--safe-b)) var(--safe-l);
}
.form-wrap { max-width: 100%; }
.form-heading { font-size: 26px; }

@media (min-width: 481px) {
  .brand-arabic { font-size: 72px; letter-spacing: 3px; }
  .brand-center { min-height: 130px; }
}

@media (min-width: 640px) {
  .brand-panel { padding: 36px 28px; }
  .brand-arabic { font-size: 92px; letter-spacing: 4px; }
  .form-panel { padding: 40px 32px; }
}

@media (min-width: 901px) {
  .page-login { overflow: hidden; }
  .auth-layout {
    grid-template-columns: 1fr 1fr;
    height: 100vh;
    height: 100dvh;
    min-height: 0;
  }
  .brand-panel { padding: 56px; }
  .brand-center { min-height: 0; }
  .brand-art { background-size: 200px 200px; }
  .form-panel { padding: 56px; }
  .form-wrap { max-width: 380px; }
  .form-heading { font-size: 30px; }
}


/* ─── § 10 · SHORT VIEWPORTS (phone in landscape) ─────────────────────────
   ~380px of height once the browser chrome is gone. Vertical space is the
   scarce resource here, not width — which is why this is a height query. */

@media (max-height: 480px) and (orientation: landscape) {
  .header { position: static; }
  .mic-btn-large { width: 56px; height: 56px; }
  .audio-recorder { padding: 14px 12px; gap: 8px; }
  .audio-page-grid, .exam-page-grid { gap: 12px; }
  .page-chat .welcome { min-height: 0; padding: 20px 24px 12px; }
  .page-chat .welcome-arabic { font-size: 40px; }
  .brand-center { min-height: 0; }
  .brand-arabic { font-size: 44px; }
}


/* ─── § 11 · REDUCED MOTION ───────────────────────────────────────────────*/

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  /* The record button's pulse is a live status cue, not decoration — keep a
     static ring so "recording" stays visually distinct from "idle". */
  .mic-btn-large.recording { box-shadow: 0 0 0 4px rgba(196, 148, 74, 0.35); }
}
