/* ═══════════════════════════════════════════════════════════════════════════
   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;
    /* STAYS SCROLLABLE, and reverting it to `visible` here is what put the
       sign-out button beyond reach on a tablet. The phone tier above already
       says why the row scrolls: "rather than clipping the logout button off
       the end". Turning that off at 901px assumes everything fits from there
       up, and on a TOUCH tablet it does not -- the coarse-pointer block below
       inflates every control in this row to 44px, so a 912px iPad or Surface
       in portrait overflows the header on /audio and /quran-ikhtebar and
       `body { overflow-x: hidden }` cuts the button off with no way to scroll
       it back. Found by adding a 912px device to the matrix; the old list
       jumped straight from 820 (phone rules) to 1180 (plenty of room) and
       never sampled the band where the header is on its own. */
    overflow-x: auto;
  }
}


/* ─── § 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,
  .conv-rename,
  .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; }

/* AND THE OTHER HALF OF THE SAME TRAP, which cost the send button entirely.
   `flex-shrink: 0` stops the BUTTONS being squeezed; it does nothing about the
   textarea beside them refusing to shrink. A flex item's automatic minimum
   size is its MIN-CONTENT, and a <textarea> carries an intrinsic width from
   its default column count -- so `.input-box` bottomed out at 210px however
   narrow the screen got. Measured on a 375px iPhone SE: the row wanted 372px
   inside a 343px box, and the send button's right edge landed at 388 against
   a viewport of 375. `body.page-chat` sets `overflow-x: hidden`, so those 13px
   were not merely off-screen, they were UNREACHABLE -- no scroll, no error,
   just a chat page you cannot send a message from on the smallest handset
   still in use. Phase 26 put the history button in that row and pushed it over
   the edge; nothing measured it because the device matrix had been auditing
   the login page since Phase 23. */
.input-box, .input-field { min-width: 0; }

/* Below 400px the four controls plus three 10px gaps leave the composer under
   150px of typing room. The gap is the only thing here that may give: the
   buttons are at the 44px tap minimum and shrinking those trades a layout bug
   for an accessibility one. */
@media (max-width: 400px) {
  .page-chat .input-row { gap: 7px; }
}

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



/* ─── § 5b · CHAT HISTORY RAIL (/app, /manzoori) ──────────────────────────
   Phase 26. Desktop: a 268px rail in the flow beside the transcript, always
   present. Phone: the same markup as a BOTTOM SHEET, opened by the composer's
   history button.

   Pinned left/right/bottom rather than `inset: 0` — the same idiom as the
   annotate modal above, and for the same two reasons: it keeps the sheet out
   of the fixed-flex-overlay trap in this file's header comment, and a sheet
   opens under the thumb instead of being centred off the top of a short
   viewport.

   `.seg-control` IS NOT TOUCHED HERE. Making room for the history button by
   hiding the cross-page nav is exactly the dead end these two pages already
   shipped once, and it is pinned against by
   test_cross_page_nav_is_never_hidden. */

@media (max-width: 900px) {
  /* The desktop collapse is NEUTRALISED here, and it has to be done by name.
     `.conv-rail.collapsed` (0,2,0) outranks the `.conv-rail` block below
     (0,1,0), so without this the sheet would keep `width: 0` and
     `visibility: hidden` from the shell's stylesheet and simply never appear
     - a history button that opens nothing, on the width where the button is
     the ONLY way in. The desktop preference is deliberately not honoured on a
     phone: there is no column to collapse, only a sheet, and `.open` is the
     whole story. */
  .conv-rail.collapsed {
    width: auto;
    visibility: visible;
    border-right: none;
  }

  /* One control per width, each meaning exactly one thing. The header toggle
     collapses a column that does not exist here; the composer's button opens
     the sheet. Showing both would give a phone two buttons driving two
     different pieces of state that look identical. */
  .rail-toggle { display: none; }

  /* The button that opens the sheet exists only where the rail does not. */
  .conv-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Declared size AND flex-shrink: 0. A flex item's default shrink beats an
       explicit width, which is how the 44px send button beside it once
       measured 38px on a 344px fold with nothing in the stylesheet looking
       wrong. */
    width: var(--tap);
    height: var(--tap);
    flex-shrink: 0;
    background: var(--bg-cream);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    font-size: 16px;
    cursor: pointer;
  }

  .conv-scrim {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(44, 24, 16, 0.4);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s;
    z-index: 99;
  }
  .conv-scrim.show { opacity: 1; pointer-events: auto; }

  .conv-rail {
    position: fixed;
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    max-height: 80dvh;
    border-right: none;
    border-top: 1px solid var(--border-medium);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: 0 -8px 24px rgba(44, 24, 16, 0.18);
    /* The sheet clears the home indicator itself; without this the last row of
       the list sits under the gesture bar. */
    padding-bottom: var(--safe-b);
    /* Off-screen rather than display:none, so opening it animates and so the
       list inside keeps its scroll position between opens. */
    transform: translateY(100%);
    transition: transform 0.22s ease;
    z-index: 100;
  }
  .conv-rail.open { transform: translateY(0); }

  .conv-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    flex-shrink: 0;
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 16px;
    cursor: pointer;
  }
}

@media (pointer: coarse) {
  /* Every row in the list is a tap target, and a 9px-padded two-line row is
     already close to the minimum — this makes it explicit rather than
     incidental. */
  .conv-open { min-height: var(--tap); }
  .conv-actions button { min-width: var(--tap); min-height: 30px; }
  .conv-new, .conv-more { min-height: var(--tap); }
  /* The account menu is reached with a thumb on the sheet, so its rows
     are tap targets too — and it is the only route to Account,
     Feedback and sign-out now that those have left the header. */
  .rail-user, .rail-menu-item { min-height: var(--tap); }
  /* The rail toggle is 34px, which is fine for a mouse and under the
     minimum for a thumb. It is hidden below 900px, so this catches the
     case that falls between the two rules: a TOUCH TABLET wide enough
     to get the desktop column. Found on an iPad in landscape. */
  .rail-toggle { width: var(--tap); height: var(--tap); }
}

/* The account menu is a POPOVER and is allowed to extend past the sheet that
   anchors it -- measured on an iPhone in landscape the sheet is 217px tall and
   the menu is 243px, so it necessarily overlaps the transcript behind the
   scrim. What it must never do is leave the VIEWPORT: five items fit today and
   a sixth would start pushing the first one off the top of the screen, where
   there is no scroll and no indication anything is missing. The cap is the
   guard; the scroll is what makes the cap usable rather than a silent trim. */
.rail-menu {
  max-height: calc(100dvh - 96px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* iOS chains a scroll that reaches the end of a container up to the page
   behind it, so flicking to the bottom of the history list drags the whole
   page under the sheet -- the sheet appears to come unstuck. */
.conv-list { overscroll-behavior: contain; }

/* ─── § 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: 0; 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) {
  /* NO SPLIT ANY MORE. This block used to restore the two-column layout
     above 901px; the page is one light panel at every width now, so all
     that is left here is the desktop padding and a bigger star tile. */
  .brand-art { background-size: 200px 200px; }
  .form-panel { padding: 56px; }
  .form-wrap { max-width: 380px; }
  .form-heading { font-size: 30px; }
}


/* ─── § 9b · LANDING (landing.html) ───────────────────────────────────────
   The public page at `/`. It has no app shell, no nav and no form, so almost
   nothing in §§ 3-8 applies to it — what it needs is the opposite of the rest
   of this file: a single-column phone layout that OPENS OUT on a desktop,
   rather than a desktop layout that folds down.

   THE BASE IS THE PHONE, and it lives in landing.html's own <style>. Only the
   widening happens here. Numbered 9b rather than 12 so it sits with § 9,
   the other unauthenticated page, and so nothing below it had to be
   renumbered — the same reason § 5b is called 5b.
   ───────────────────────────────────────────────────────────────────────── */

/* Tablet: the four tiles pair up. Two columns is the first width where a
   tile is still wide enough to hold its heading on one line. */
@media (min-width: 601px) {
  .page-landing .jsr-tiles { grid-template-columns: 1fr 1fr; gap: 16px; }
  .page-landing .jsr-rules { grid-template-columns: 1fr 1fr 1fr; }
  .page-landing .jsr-band { padding: 64px 28px; }
  .page-landing .jsr-hero-logo { width: min(330px, 74vw); }
  .page-landing .jsr-latin { font-size: 25px; }
  .page-landing .jsr-hero-line { font-size: 17.5px; }
  .page-landing h2.jsr-h { font-size: 31px; }
  .page-landing .jsr-close h2 { font-size: 26px; }
  .page-landing .jsr-close .lsd { font-size: 34px; }
  .page-landing .jsr-close { padding: 52px 32px; }
}

/* Desktop: the four tiles in one row, and each demo becomes copy beside its
   screen. */
@media (min-width: 901px) {
  .page-landing .jsr-head { padding: 16px 32px; }

  /* Trimmed when the farmaan moved above the demonstration: that panel is
     250px of hero, and the workbench below it is what shows the product
     working. The top margin is the cheapest place to buy some of it back. */
  .page-landing .jsr-hero { padding: 46px 32px 68px; }
  .page-landing .jsr-hero-logo { width: 420px; }
  .page-landing .jsr-latin { font-size: 28px; }
  .page-landing .jsr-hero-line { font-size: 18.5px; }
  .page-landing .jsr-stage { min-height: 392px; --phone-w: 186px; }
  .page-landing .jsr-hand { padding: 22px; min-height: 230px; }
  .page-landing .h-lsd { font-size: 23px; }

  .page-landing .jsr-band { padding: 88px 32px; }
  .page-landing h2.jsr-h { font-size: 38px; }
  .page-landing .jsr-sub { font-size: 16.5px; }
  .page-landing .jsr-say { font-size: 16.5px; }

  .page-landing .jsr-tiles { grid-template-columns: repeat(4, 1fr); gap: 18px; }

  /* The minutes, under the transcript. The copy is the wider half here --
     the opposite of a demo band -- because what is being sold at this point
     is the sentence, and the document beside it is the evidence. */
  .page-landing .jsr-under {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
    gap: 40px;
  }
  .page-landing h3.jsr-h3 { font-size: 23px; }

  /* THE DEMOS ALTERNATE, and the alternation is done with `order` rather
     than by writing the columns the other way round in the markup. Reading
     order stays copy-then-screen for a screen reader and for anybody on a
     phone, where the explanation must arrive before the picture of it.

     AN EXPLICIT CLASS, NEVER :nth-of-type. The demos do not all share a
     parent — two sit together in one band and the other two have a section
     each — so nth-of-type restarts counting at every band and produced
     normal, flipped, normal, normal. A rhythm that is wrong in one place
     reads as a mistake rather than as a rhythm. */
  .page-landing .jsr-demo {
    grid-template-columns: minmax(0, .84fr) minmax(0, 1.16fr);
    gap: 48px;
  }
  .page-landing .jsr-demo + .jsr-demo { margin-top: 104px; }
  .page-landing .jsr-demo-flip > *:first-child { order: 2; }

  .page-landing .jsr-screen-body { padding: 26px; min-height: 264px; }
  .page-landing .jsr-lsd { font-size: 25px; }

  .page-landing .jsr-close { padding: 64px 40px; }
  .page-landing .jsr-close h2 { font-size: 30px; }
}

/* The two side-by-side stages inside a demo screen -- the manzoori's brief
   and letter, and the OCR's photo and text -- are only side by side where
   there is room for both. Below 601px each half would be about 150px wide,
   which is narrower than the words in it. */
@media (max-width: 600px) {
  .page-landing .mz-stage,
  .page-landing .ocr-stage { grid-template-columns: 1fr; }
  .page-landing .ocr-photo { min-height: 150px; }
  /* Stacked, the crossing has to turn too -- and be sized in the axis it
     ends up in, or `width: 100%` becomes 300px of arc drawn down through
     the photograph. Same rule, same reason, as the hero's. */
  .page-landing .ocr-cross { width: 100%; height: 44px; }
  .page-landing .ocr-cross svg { width: 40px; height: 30px; transform: rotate(90deg); }
  .page-landing .ocr-cross i { display: none; }

  /* The hero's two cards stack, and the arc between them turns to point
     DOWN rather than across — an arrow still pointing sideways at a card
     that is now underneath is worse than no arrow. */
  .page-landing .jsr-slide {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
    gap: 6px;
  }
  /* THE CROSSING TURNS TO POINT DOWN, AND IT MUST BE SIZED BEFORE IT IS
     TURNED. The arc is `width: 100%` so it can span whatever column it is
     given on a desktop; rotated 90deg that same 100% becomes the element's
     HEIGHT -- 350px of arc drawn straight down through the handset and the
     notes inside it, on the one layout where the two cards are stacked and
     there is something behind it to ruin. A rotated box has to be sized in
     the axis it will END UP in. */
  .page-landing .jsr-cross { width: 100%; height: 60px; }
  .page-landing .jsr-cross svg { width: 54px; height: 40px; transform: rotate(90deg); }
  /* The spark is not rotated with it -- it is a sibling, positioned in
     percentages of the full-width container -- so stacked it would fly
     sideways across a bridge that now points down. The arc alone says
     "and then this" perfectly well. */
  .page-landing .jsr-cross i { display: none; }
  .page-landing .jsr-hand { min-height: 0; padding: 14px 16px; }
  /* The handset shrinks rather than the hero growing. A phone mock on a
     phone is still the most legible object on the screen, but it may not
     cost so much height that the promise beneath it falls off the fold. */
  .page-landing .jsr-stage { min-height: 430px; --phone-w: 124px; }
  .page-landing .h-device { gap: 7px; }
  /* Three bullets set for a 186px screen overflow a 124px one, and the
     screen clips with no scrollbar -- so the third line of the brief just
     is not there, on the slide whose whole point is that three lines
     become a letter. Measured at 390x844: 11px over. */
  .page-landing .h-screen { padding: 22px 9px 10px; }
  /* The same eighteen lines, set for a 124px viewfinder. */
  .page-landing .h-page { padding: 9px 8px; }
  .page-landing .h-page i { height: 3px; }
  .page-landing .h-brief { gap: 6px; margin-top: 2px; }
  .page-landing .h-brief div { font-size: 10px; line-height: 1.4; }
  .page-landing .h-brief div::before { margin-top: 5px; }
}


/* Wide desktop: hold the measure. Past ~1240px the hero sentence starts
   running to a width nobody reads comfortably. */
@media (min-width: 1240px) {
  .page-landing .jsr-band { padding-left: 40px; padding-right: 40px; }
}


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