/* ═══════════════════════════════════════════════════════════════════════════
   KATIB — THE PORTAL'S SHARED DESIGN LANGUAGE                      Phase 27
   ═══════════════════════════════════════════════════════════════════════════

   WHY THIS FILE EXISTS
   Every page carries its own <style> block — between 150 and 1,180 lines of
   it — and they were written months apart against four different ideas of
   what the portal looks like. The result is a product where /audio, /files
   and /quran-ikhtebar each solved "a header with a nav in it" separately, and
   where a tester moving between two pages of the same portal can tell they
   were built by different hands. Phase 12 fixed exactly this for LAYOUT by
   introducing responsive.css. This is the same move for APPEARANCE.

   HOW IT COMPOSES — three sheets, in this order, on every page:

       inline <style>   →  the page's own components (untouched)
       katib.css        →  THE LANGUAGE: tokens, motion, chrome, primitives
       responsive.css   →  layout at every width (still last, still wins)

   Linked AFTER the inline block so its token block wins at equal specificity,
   and BEFORE responsive.css so the layout layer keeps the last word. Anything
   here that fights responsive.css is a bug in this file, not in that one.

   THE LEVER IS THE TOKEN NAMES, NOT THE RULES. index.html, manzoori.html,
   audio.html, files.html, quran-ikhtebar.html and login.html all draw their
   colours from the SAME custom-property names (--bg-page, --gold-500,
   --radius-md, --shadow-sm …). So redefining those names here re-skins six
   pages and every component on them at once, without editing a single one of
   their stylesheets. That is the whole reason this is survivable: the pages
   opt in by linking one file, and nothing else about them changes.

   WHAT THIS DELIBERATELY DOES NOT TOUCH
     · --font-lsd. Kanz-al-Marjaan is the LSD face; invariant #9 and
       test_the_lsd_font_token_is_untouched both say so.
     · --font-ui-lsd's ORDER. Kanz must stay ahead of every generic or the
       declaration silently does nothing (invariant #9, mutation-checked).
     · --font-quran. Amiri Quran is its own token and never an alias.
     · .seg-control's existence. It is the only way off a page and is never
       display:none at any width (pinned since Phase 12). It is RESTYLED into
       a pill nav here, never replaced — a rename would have taken the pin
       with it.
     · admin.css / style.css. /admin and /dashboard run the LeedAB paper-ink
       system and are the last pages in the rollout, not the first.

   ROUNDED CORNERS AND SOFT SHADOWS ARE NOW CORRECT. DESIGN_SYSTEM.md's
   "no rounded corners, no drop shadows except offset" describes the LeedAB
   marketing system and governed this portal until 2026-09-04, when Mohammed
   chose a soft, airy direction for the tester-facing pages. Invariant #8 was
   amended in the same edit that added this file. If you are reading this
   because a review flagged a radius: it is deliberate, and the amendment is
   in CLAUDE.md.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ───────────────────────────────────────────────────────────────────────────
   1 · TOKENS — the re-skin. Redefining these reaches every page at once.
   ─────────────────────────────────────────────────────────────────────────── */

:root {
  /* Grounds. Warmer and lower-contrast than the old paper: the page is a
     surface things float ON, so the panels are what read as white, not the
     background. */
  --bg-page:  #F4F1EB;
  --bg-white: #FFFFFF;
  --bg-cream: #FAF8F4;
  --bg-warm:  #F0ECE4;

  /* Ink. Three steps and a fourth for the barely-there. More brown than the
     old near-black, which read as cold against amber. */
  --text-primary:   #221A12;
  --text-secondary: #6A5B49;
  --text-muted:     #9C8D7B;
  --text-light:     #B6A896;

  /* Lines. One hairline and one that can hold an edge on its own. */
  --border-light:  #E9E2D6;
  --border-medium: #DBD1C1;

  /* AMBER IS THE ONLY ACCENT, and it does three jobs at three weights:
     -600 for text on a light ground (it is the only step that passes
     contrast at body size), -500 for edges and marks, -400/-300 for fills
     and gradients, -100 for the tint behind a selected thing. */
  --gold-600: #8A6117;
  --gold-500: #C08A2E;
  --gold-400: #E0AF55;
  --gold-300: #ECC77F;
  --gold-200: #F5E3BE;
  --gold-100: #FBF1DC;

  /* The brown ramp survives because the logo mark, the active nav pill and
     the LSD bubbles are built from it. Warmed a step so it sits with amber
     rather than under it. */
  --brown-900: #221A12;
  --brown-800: #2F231A;
  --brown-700: #453425;
  --brown-600: #5E4834;
  --brown-500: #7C6146;
  --brown-400: #9C7D5C;
  --brown-300: #BC9E7A;
  --brown-200: #D9C2A2;
  --brown-100: #EFE1CC;
  --brown-50:  #FAF5ED;

  /* SOFT, NOT SHARP. Every radius went up and the scale kept its shape, so a
     page that asked for --radius-sm still gets the smallest one. */
  --radius-sm:   12px;
  --radius-md:   16px;
  --radius-lg:   22px;
  --radius-xl:   28px;
  --radius-full: 999px;

  /* Shadows are DEPTH, not decoration: a tight contact shadow plus a wide,
     very soft ambient one. The old three were a single blur at three sizes,
     which is what made every card look stamped on. */
  --shadow-sm: 0 1px 2px rgba(34, 26, 18, .04), 0 12px 30px -22px rgba(34, 26, 18, .5);
  --shadow-md: 0 2px 4px rgba(34, 26, 18, .05), 0 20px 40px -26px rgba(34, 26, 18, .52);
  --shadow-lg: 0 3px 6px rgba(34, 26, 18, .06), 0 34px 60px -30px rgba(34, 26, 18, .58);

  /* Status colours keep their meaning and lose their fluorescence. */
  --red:   #C0503C;
  --green: #2F8F5B;

  /* MOTION IS A TOKEN SET, so "good animation" is one decision made once
     rather than a number typed at forty call sites. The easing is a strong
     ease-out: things arrive quickly and settle, which reads as responsive
     rather than sluggish. Nothing in this portal eases IN — an element that
     starts slowly reads as lag. */
  --k-ease:     cubic-bezier(.32, .72, 0, 1);
  --k-ease-pop: cubic-bezier(.34, 1.4, .5, 1);
  --k-fast:     .16s;
  --k-base:     .26s;
  --k-slow:     .42s;
  --transition: var(--k-fast) var(--k-ease);

  /* The amber glow behind a focal point. Two stops so it can be dropped into
     a radial-gradient anywhere without restating the colour. */
  --k-glow:     rgba(224, 175, 85, .40);
  --k-glow-out: rgba(224, 175, 85, 0);
}


/* ───────────────────────────────────────────────────────────────────────────
   2 · MOTION — one reveal, one guard.

   REDUCED MOTION IS HANDLED ONCE, HERE, AT THE END OF THE FILE (§ 8) rather
   than beside each animation. Every honest attempt to remember it per-rule in
   this repo has missed at least one.
   ─────────────────────────────────────────────────────────────────────────── */

@keyframes k-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
@keyframes k-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* A page-load stagger. `--k-i` is the item's index; the delay is derived from
   it so a list can be staggered without writing a delay per child.
   IT ANIMATES FROM A VISIBLE RESTING STATE ONWARDS, never parks at opacity 0
   waiting for an observer: an animation that fails to fire must leave the
   content readable, or a JS error becomes a blank page. */
.k-rise {
  animation: k-rise var(--k-base) var(--k-ease) both;
  animation-delay: calc(var(--k-i, 0) * 55ms);
}


/* ───────────────────────────────────────────────────────────────────────────
   3 · CHROME — the header every tester-facing page shares.

   These five selectors exist on /audio, /files, /quran-ikhtebar, /app and
   /manzoori already. Restyling them here is what makes the portal read as one
   product before a single page's structure has been touched.
   ─────────────────────────────────────────────────────────────────────────── */

.header {
  /* The bar stops being a slab: no fill, no rule, no shadow. What separates
     it from the page is the gap under it, not a line across it. */
  background: transparent;
  border-bottom: 0;
  box-shadow: none;
  height: auto;
  min-height: 64px;
  padding-top: 12px;
  padding-bottom: 12px;
  gap: 14px;
}

.logo-mark {
  border-radius: var(--radius-sm);
  background: linear-gradient(150deg, var(--gold-400), var(--gold-500));
  color: #2A1D06;
  box-shadow: 0 6px 14px -8px var(--gold-500);
  transition: transform var(--k-fast) var(--k-ease-pop);
}
.logo-mark:hover { transform: scale(1.06) rotate(-3deg); }

.header-title { font-size: 18px; letter-spacing: -.01em; }

/* THE PILL NAV. Still `.seg-control`, still the only way off the page, still
   never display:none — restyled, not replaced. The active item is a filled
   pill rather than an inverted block, so the control reads as a set of
   choices rather than a toolbar. */
.seg-control {
  background: var(--bg-white);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-full);
  padding: 4px;
  gap: 2px;
  box-shadow: var(--shadow-sm);
  overflow: visible;
}
.seg-control a {
  height: 32px;
  padding: 0 15px;
  border-right: 0;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--text-secondary);
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.seg-control a.active {
  background: var(--brown-800);
  color: var(--gold-200);
}
.seg-control a:not(.active):hover {
  background: var(--bg-warm);
  color: var(--text-primary);
}

/* The secondary header buttons. `.header-link` was a 36px SQUARE holding a
   WORD on two of these shells, which is how ACCOUNT/FEEDBACK/FILES/ADMIN came
   to draw on top of one another. A pill sized by its content cannot do that,
   whatever it is asked to hold. */
.header-link,
.header-btn {
  height: 36px;
  min-width: 36px;
  width: auto;
  padding: 0 14px;
  border-radius: var(--radius-full);
  border-color: var(--border-light);
  font-size: 12px;
  letter-spacing: .02em;
  box-shadow: var(--shadow-sm);
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease),
              border-color var(--k-fast) var(--k-ease), transform var(--k-fast) var(--k-ease);
}
.header-link:hover,
.header-btn:hover { transform: translateY(-1px); }
.header-link.active { background: var(--brown-800); border-color: var(--brown-800); color: var(--gold-200); }

.quota-pill {
  border-radius: var(--radius-full);
  border-color: var(--border-light);
  background: var(--bg-white);
  box-shadow: var(--shadow-sm);
  padding: 6px 14px;
}


/* ───────────────────────────────────────────────────────────────────────────
   4 · THE STUDIO SHELL — rail + canvas.

   The same shape as the conversation rail /app already ships, so the portal
   has ONE idea of "your things are on the left, the work is in the middle".
   ─────────────────────────────────────────────────────────────────────────── */

.k-shell {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  padding: 0 16px 20px;
  align-items: start;
  width: 100%;
  max-width: 1240px;
  margin: 0 auto;
}

.k-rail {
  background: var(--bg-white);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  position: relative;
  overflow: hidden;
}
/* The amber wash at the foot of the rail. Decorative, and the one place in
   the portal where the accent is allowed to be atmospheric rather than
   meaningful — it marks the corner the account block lives in. */
.k-rail::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  height: 190px;
  pointer-events: none;
  background: radial-gradient(120% 84% at 50% 118%, var(--k-glow), var(--k-glow-out) 70%);
}
.k-rail > * { position: relative; z-index: 1; }
.k-rail-grow { flex: 1; }

.k-newbtn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  padding: 11px 14px;
  border: 1px solid var(--border-medium);
  border-radius: var(--radius-full);
  background: var(--bg-white);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease), border-color var(--k-fast) var(--k-ease),
              color var(--k-fast) var(--k-ease);
}
.k-newbtn:hover { background: var(--gold-100); border-color: var(--gold-500); color: var(--gold-600); }
.k-newbtn svg { width: 16px; height: 16px; }

.k-rail-label {
  font-size: 11.5px;
  color: var(--text-muted);
  font-weight: 500;
  padding: 0 6px;
  margin-bottom: 6px;
}

.k-rmenu { display: flex; flex-direction: column; gap: 2px; }
.k-rmenu button {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.k-rmenu button:hover { background: var(--bg-cream); color: var(--text-primary); }
.k-rmenu button[aria-current] { background: var(--bg-warm); color: var(--text-primary); font-weight: 600; }
.k-rmenu svg { width: 17px; height: 17px; flex: none; color: var(--text-muted); }
.k-rmenu button[aria-current] svg { color: var(--gold-500); }

.k-rlist { display: flex; flex-direction: column; gap: 1px; }
.k-rlist button {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-secondary);
  /* INVARIANT #9. A RAIL TITLE IS ALWAYS LSD -- it is the opening words of
     the recording, which is the only title this data has. Under --font-ui
     that text renders in whatever Arabic face the browser picks, because the
     stack ENDS IN GENERICS and both system-ui and sans-serif supply Arabic:
     a Kanz listed after them is never consulted and the declaration silently
     does nothing. The var() fallback restates the stack rather than trusting
     every page that links this file to have declared the token. */
  font-family: var(--font-ui-lsd, 'Inter', 'KanzAlMarjaan', system-ui, -apple-system, sans-serif);
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
  /* One line, ellipsised. A rail item that wraps to three lines turns the
     list into a wall and the rail into a second page. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.k-rlist button:hover { background: var(--bg-cream); color: var(--text-primary); }
.k-rlist button[aria-current] { background: var(--bg-warm); color: var(--text-primary); font-weight: 600; }
/* The row that was just created. It is the only thing on the page the user is
   looking for after a four-minute wait. */
.k-rlist button.is-fresh { color: var(--gold-600); font-weight: 600; }

.k-canvas {
  background: var(--bg-white);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 40px 30px 28px;
  min-height: 520px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}
/* The glow behind the focal point, painted on the canvas rather than on the
   orb, so it stays put while the orb animates. */
.k-canvas::before {
  content: "";
  position: absolute;
  inset: -26% 0 auto;
  height: 440px;
  pointer-events: none;
  opacity: .55;
  background: radial-gradient(52% 60% at 50% 50%, var(--k-glow), var(--k-glow-out) 68%);
}
.k-canvas > * { position: relative; z-index: 1; width: 100%; }

.k-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 640px;
  margin: 0 auto;
}


/* ───────────────────────────────────────────────────────────────────────────
   5 · THE FOCAL POINT — one orb, three states.
   ─────────────────────────────────────────────────────────────────────────── */

.k-orb {
  width: 108px;
  height: 108px;
  display: grid;
  place-items: center;
  position: relative;
  margin-bottom: 24px;
}
.k-orb-ring {
  position: absolute;
  border-radius: 50%;
  border: 1px solid var(--gold-500);
  opacity: .26;
  /* THE RINGS WERE EATING EVERY CLICK ON THE RECORD BUTTON, and nothing said
     so. They are absolutely positioned and `.k-orb-btn` is not, so they paint
     ABOVE it whatever the DOM order -- and r3 is `inset: -22px`, i.e. larger
     than the orb, so it covered the button completely. A transparent element
     with no background still hit-tests, so the tap landed on a decorative
     span with no handler and the page simply did nothing. Found by the
     browser check timing out on a click, which is the ONLY way this shows up:
     it throws nothing, logs nothing, and looks exactly like a page that was
     never wired. Same signal as `.brand-art`: decoration takes no taps. */
  pointer-events: none;
}
.k-orb-ring.r1 { inset:   8px; }
.k-orb-ring.r2 { inset:  -6px; opacity: .16; }
.k-orb-ring.r3 { inset: -22px; opacity: .09; }

.k-orb-btn {
  width: 76px;
  height: 76px;
  border: 0;
  border-radius: 50%;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: #2A1D06;
  background: linear-gradient(155deg, var(--gold-400), var(--gold-500));
  box-shadow: 0 10px 26px -12px var(--gold-500), inset 0 1px 0 rgba(255, 255, 255, .45);
  transition: transform var(--k-fast) var(--k-ease-pop);
}
.k-orb-btn:hover  { transform: scale(1.05); }
.k-orb-btn:active { transform: scale(.97); }
.k-orb-btn svg { width: 30px; height: 30px; }
.k-orb-btn:disabled { cursor: default; }

/* RECORDING. The ripples are the only thing on the page that repeats
   indefinitely, and they are what makes a phone left face-down on a table
   still look alive when it is picked back up. */
.k-orb.is-live .k-orb-btn {
  background: linear-gradient(155deg, #E4796A, var(--red));
  color: #FFF4F0;
  box-shadow: 0 10px 26px -12px var(--red), inset 0 1px 0 rgba(255, 255, 255, .35);
}
.k-orb.is-live .k-orb-ring.r2,
.k-orb.is-live .k-orb-ring.r3 {
  border-color: var(--red);
  animation: k-ripple 2.4s ease-out infinite;
}
.k-orb.is-live .k-orb-ring.r3 { animation-delay: 1.2s; }
@keyframes k-ripple {
  from { transform: scale(.86); opacity: .32; }
  to   { transform: scale(1.22); opacity: 0; }
}

/* WORKING. A slow breath, not a spinner: a spinner says "blocked", a breath
   says "carrying on", and this wait is four minutes long. */
.k-orb.is-busy .k-orb-btn { animation: k-breathe 2.8s ease-in-out infinite; }
@keyframes k-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}


/* ───────────────────────────────────────────────────────────────────────────
   6 · SPEECH, CHOICES, THE COMPOSER — the rest of the studio.
   ─────────────────────────────────────────────────────────────────────────── */

.k-greet {
  font-family: var(--font-display);
  font-size: clamp(24px, 4.2vw, 34px);
  font-weight: 500;
  letter-spacing: -.02em;
  line-height: 1.2;
  text-align: center;
  text-wrap: balance;
  color: var(--text-primary);
}
/* The italic amber span is the design system's signature move and the one
   place the page raises its voice. */
.k-greet em { font-style: italic; color: var(--gold-600); }

.k-sub {
  margin-top: 12px;
  color: var(--text-secondary);
  text-align: center;
  font-size: 15px;
  line-height: 1.6;
  max-width: 46ch;
}

.k-chips {
  display: flex;
  gap: 9px;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 24px;
}
.k-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 15px;
  border: 1px solid var(--border-medium);
  border-radius: var(--radius-full);
  background: var(--bg-white);
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: 13.5px;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease),
              transform var(--k-fast) var(--k-ease), background var(--k-fast) var(--k-ease);
}
.k-chip:hover { border-color: var(--gold-500); color: var(--text-primary); transform: translateY(-1px); }
.k-chip svg { width: 16px; height: 16px; color: var(--text-muted); }
.k-chip[aria-pressed="true"] {
  background: var(--gold-100);
  border-color: var(--gold-500);
  color: var(--gold-600);
  font-weight: 600;
}
.k-chip[aria-pressed="true"] svg { color: var(--gold-500); }

/* The direction chips on /translate. THE ROW, NOT THE CHIP: .k-chip is
   reused as it stands, and this only moves the row out of the centred,
   generously-spaced arrangement it has under a hero and into the
   composer, where it sits directly above the box you type in and must
   line up with its left edge. Below 901px the composer is already tight,
   so the gap closes further rather than the chips wrapping into two rows
   above the keyboard. */
.dir-chips {
  justify-content: flex-start;
  margin-top: 0;
  margin-bottom: 10px;
  gap: 8px;
}
@media (max-width: 900px) {
  .dir-chips { margin-bottom: 8px; gap: 6px; }
}

/* THE DOCUMENT UPLOAD ON /translate, in the row above the composer.

   IT SITS BESIDE THE DIRECTION CHIPS AND WEARS THE SAME .k-chip, because it
   is the same kind of control in the same place: something you set before
   you send, not something that sends. A button styled like the amber send
   button would read as 'upload and translate', which is exactly what it
   does not do -- it fills the box below and stops, so the reader can look at
   what came out of a PDF before spending a prompt on it. */
.upload-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 10px;
}

/* NOT display:none, and not hidden. A file input is the only element that
   can open the picker, so removing it from the layout removes the feature
   from anyone driving the page by keyboard or screen reader. Clipped to a
   pixel is the shape that keeps it operable while taking no space -- the
   same trick a visually-hidden label uses. */
.upload-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.upload-btn[disabled] {
  opacity: .55;
  cursor: progress;
}

/* What was read, in the reader's own terms: the file's name and how many
   words came out of it. The word count is the number that decides whether
   the send will be refused, so it is the one worth showing. */
.upload-note {
  font-family: var(--font-ui);
  font-size: 12px;
  color: var(--text-muted);
  min-width: 0;
  overflow-wrap: anywhere;
}

@media (max-width: 900px) {
  .upload-row { margin-bottom: 8px; gap: 6px; }
  .upload-note { font-size: 11px; }
}

/* THE COMPOSER IS THE RECORDER. Where a chat app puts the message box this
   page puts the recording: choose a file on the left, what comes back in the
   middle, the amber button on the right. The shape is the point — it is the
   one control layout every person on a phone already knows. */
.k-composer {
  margin-top: 24px;
  width: 100%;
  background: var(--bg-white);
  border: 1px solid var(--border-medium);
  border-radius: 24px;
  padding: 16px 16px 12px;
  box-shadow: var(--shadow-md);
  transition: border-color var(--k-base) var(--k-ease), box-shadow var(--k-base) var(--k-ease);
}
.k-composer:hover,
.k-composer.is-hot { border-color: var(--gold-500); box-shadow: var(--shadow-lg); }
.k-composer.is-hot { background: var(--gold-100); }

.k-comp-top { display: flex; align-items: center; gap: 13px; padding: 6px 4px 14px; }
.k-comp-icon {
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: 50%;
  background: var(--bg-warm);
  color: var(--gold-600);
  display: grid;
  place-items: center;
}
.k-comp-icon svg { width: 18px; height: 18px; }
.k-comp-text { min-width: 0; }
.k-comp-text b { display: block; font-size: 15px; font-weight: 600; color: var(--text-primary); }
.k-comp-text small { display: block; font-size: 13px; color: var(--text-muted); }
.k-comp-bot {
  display: flex;
  align-items: center;
  gap: 9px;
  border-top: 1px solid var(--border-light);
  padding-top: 12px;
}

.k-mini {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 13px;
  border: 1px solid var(--border-medium);
  border-radius: var(--radius-full);
  background: var(--bg-white);
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: 13px;
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.k-mini:hover { background: var(--bg-cream); color: var(--text-primary); }
.k-mini svg { width: 15px; height: 15px; }

.k-go {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 10px 20px;
  border: 0;
  border-radius: var(--radius-full);
  background: linear-gradient(150deg, var(--gold-400), var(--gold-500));
  color: #2A1D06;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 8px 20px -10px var(--gold-500);
  transition: transform var(--k-fast) var(--k-ease), filter var(--k-fast) var(--k-ease);
}
.k-go:hover { transform: translateY(-1px); filter: brightness(1.04); }
.k-go:disabled { opacity: .5; cursor: default; transform: none; filter: none; }
.k-go svg { width: 16px; height: 16px; }
.k-go.is-stop {
  background: linear-gradient(150deg, #E4796A, var(--red));
  color: #FFF4F0;
  box-shadow: 0 8px 20px -10px var(--red);
}

.k-foot-note { margin-top: 18px; text-align: center; color: var(--text-muted); font-size: 12.5px; }
.k-formats { margin-top: 10px; display: flex; gap: 7px; justify-content: center; flex-wrap: wrap; }
.k-formats span {
  padding: 4px 11px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-full);
  background: var(--bg-cream);
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* THE WAIT: one line and a clock. Not the six-stage pipeline — that is our
   architecture, not the user's business. The sweep is what says the sentence
   is live rather than stuck. */
.k-readout { margin-top: 4px; display: flex; flex-direction: column; align-items: center; gap: 14px; }
.k-said {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  font-size: clamp(21px, 3.4vw, 29px);
  letter-spacing: -.015em;
  text-align: center;
  background: linear-gradient(96deg, var(--text-secondary) 24%, var(--gold-400) 48%, var(--text-secondary) 72%);
  background-size: 260% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: k-sweep 2.8s ease-in-out infinite;
}
@keyframes k-sweep {
  from { background-position: 140% 0; }
  to   { background-position: -40% 0; }
}
.k-clock {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 15px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-full);
  background: var(--bg-cream);
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 12.5px;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.k-clock i { width: 7px; height: 7px; border-radius: 50%; background: var(--gold-500); animation: k-blink 1.5s ease-in-out infinite; }
.k-clock.is-rec i { background: var(--red); }
@keyframes k-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: .22; }
}
.k-quiet { color: var(--text-muted); font-size: 13px; text-align: center; max-width: 42ch; line-height: 1.6; }
.k-quiet b { color: var(--text-secondary); }

.k-wave { display: flex; align-items: center; gap: 3px; height: 26px; }
.k-wave i {
  width: 3px;
  border-radius: 2px;
  background: var(--red);
  opacity: .75;
  animation: k-bob 1.1s ease-in-out infinite;
}
@keyframes k-bob {
  0%, 100% { height: 5px; }
  50%      { height: 24px; }
}

/* THE FINISHED DOCUMENT. Sized like the thing the user came back for, with
   the miniature sheet on the left — a page carrying the minutes form's own
   red double rule, which is what a reader recognises before reading a word. */
.k-sheetcard {
  display: grid;
  grid-template-columns: 82px 1fr;
  gap: 18px;
  align-items: center;
  width: 100%;
  padding: 18px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-md);
  background: var(--bg-white);
  box-shadow: var(--shadow-md);
  text-align: left;
}
.k-mini-sheet {
  aspect-ratio: 5 / 6.6;
  border: 1px solid var(--border-medium);
  border-radius: 8px;
  background: var(--bg-cream);
  padding: 9px 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.k-mini-sheet .rr { height: 4px; margin-bottom: 3px; border-top: 3px solid var(--red); border-bottom: 1px solid var(--red); }
.k-mini-sheet .ln { height: 3px; border-radius: 2px; background: var(--border-medium); }
.k-sheetcard h3 { font-family: var(--font-display); font-size: 19px; font-weight: 600; margin: 0; }
.k-tags { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 10px; }
.k-tag {
  padding: 3px 10px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-full);
  background: var(--bg-cream);
  font-size: 11.5px;
  color: var(--text-secondary);
}
.k-tag.is-on { background: var(--gold-100); border-color: var(--gold-500); color: var(--gold-600); font-weight: 600; }
.k-tag.is-ok { background: transparent; border-color: var(--green); color: var(--green); }
.k-acts { display: flex; gap: 9px; margin-top: 16px; flex-wrap: wrap; justify-content: center; }

.k-empty { color: var(--text-muted); font-size: 13.5px; text-align: center; padding: 18px 0; }


/* ───────────────────────────────────────────────────────────────────────────
   7 · WIDTH — the same ladder responsive.css uses (481 / 640 / 901), so the
   portal has one set of breakpoints and not two.

   Authored phone-first: the base rules ARE the phone layout, and the rail
   only becomes a column when there is room for one.
   ─────────────────────────────────────────────────────────────────────────── */

@media (max-width: 900px) {
  /* IN A SHEET THESE ARE LISTS AGAIN, NOT SIDEWAYS STRIPS.

     They were horizontal scrollers because the rail was a strip in the flow,
     where a row genuinely cost less height than a list -- the comment here
     used to say "it costs one row instead of a third of the screen", and
     measured, it cost a third of the screen anyway. The rail is a sheet now:
     82dvh, opened deliberately, and scrolling. Swiping sideways through your
     own recordings two cards at a time, with the rest off the right edge and
     nothing to say so, is strictly worse than the vertical list the desktop
     rail already shows. */
  .k-rail-grow { display: none; }
  .k-rlist { flex-direction: column; gap: 2px; }
  .k-rlist button { max-width: none; border: 1px solid var(--border-light); }
  /* The filters WRAP rather than scroll -- there are four and the fourth sat
     off the right edge on every phone. Two rows of chips inside a sheet is a
     row of height nobody above the fold is paying for. */
  .k-rmenu { flex-direction: row; flex-wrap: wrap; gap: 6px; }
  .k-rmenu button { flex: 0 1 auto; }
  .k-canvas { min-height: 0; padding: 32px 18px 24px; }
}

@media (min-width: 901px) {
  .k-shell { grid-template-columns: 262px 1fr; gap: 14px; padding: 0 18px 18px; }
  .k-rail  { min-height: 560px; }
}

@media (max-width: 560px) {
  .k-shell { padding: 0 12px 12px; gap: 10px; }
  .k-canvas { padding: 26px 14px 20px; border-radius: var(--radius-lg); }
  .k-orb { width: 96px; height: 96px; margin-bottom: 18px; }
  .k-orb-btn { width: 68px; height: 68px; }
  .k-composer { border-radius: 20px; padding: 13px 13px 10px; }
  .k-comp-bot { flex-wrap: wrap; }
  /* The primary action goes full width and FIRST on a phone, above the two
     secondary pills — a thumb reaches the bottom of the screen, and this is
     the only control on it that matters. */
  .k-go { width: 100%; margin-left: 0; order: -1; padding: 13px 20px; }
  .k-sheetcard { grid-template-columns: 1fr; }
  .k-mini-sheet { width: 74px; }
  .k-chips { gap: 7px; margin-top: 18px; }
}


/* ───────────────────────────────────────────────────────────────────────────
   8 · THE MOTION GUARD — once, for everything above.

   `prefers-reduced-motion` is a medical setting, not a preference: for a
   vestibular user the ripples and the breathing orb are the difference
   between a usable page and an unusable one. Blanket, because the alternative
   is remembering it at forty call sites and missing one.
   ─────────────────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .k-rise,
  .k-orb-ring,
  .k-orb-btn,
  .k-said,
  .k-clock i,
  .k-wave i,
  /* Section 20's empty-chat screen: the stagger, the breathing mark and its
     two rings. Listed here rather than beside themselves, which is the whole
     point of this block being blanket. */
  .welcome > *,
  .welcome-logo,
  .welcome-logo::before,
  .welcome-logo::after {
    animation: none !important;
  }
  /* The sweep paints the text in a gradient it cannot show without moving, so
     the static form has to put the colour back or the sentence is invisible. */
  .k-said {
    background: none;
    -webkit-text-fill-color: currentColor;
    color: var(--text-secondary);
  }
  .k-wave i { height: 14px; }
}


/* ───────────────────────────────────────────────────────────────────────────
   9 · THE LSD FACE, DECLARED ONCE FOR EVERY PAGE THAT LINKS THIS FILE.

   `@font-face` used to be declared per page, in each page's own <style>. Six
   pages had it; /account, /feedback, /pending and /reset-password did NOT --
   so a name, a role profile or a line of feedback written in LSD on any of
   those four rendered in whatever Arabic face the browser picked. Invariant
   #9 says Arabic is always Kanz, and a page that never declares the face
   cannot keep that promise however carefully its tokens are written.

   A BROWSER ONLY DOWNLOADS A FONT A RULE ACTUALLY USES, so declaring it here
   costs the pages with no Arabic on them nothing.

   The token values are IDENTICAL to the ones the six pages already declare,
   so this changes nothing where they exist and supplies them where they do
   not. Kanz sits AHEAD of every generic in --font-ui-lsd, which is the whole
   point of that token: system-ui and sans-serif both supply Arabic, so a Kanz
   listed after them is never consulted.
   ─────────────────────────────────────────────────────────────────────────── */

@font-face {
  font-family: 'KanzAlMarjaan';
  src: url('/static/fonts/Kanz-al-Marjaan.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: block;
}

:root {
  --font-lsd: 'KanzAlMarjaan', 'Traditional Arabic', 'Scheherazade New', serif;
  --font-ui-lsd: 'Inter', 'KanzAlMarjaan', system-ui, -apple-system, sans-serif;
}

/* Kanz ships OpenType GSUB tables that collapse LSD doubled letters into
   single ligated glyphs, and Chrome does not enable contextual/discretionary
   ligatures for Arabic by default. Without the opt-in you see raw character
   pairs instead of the intended letterforms.

   SCOPED AWAY FROM QURANIC TEXT. `:not(.quran-text)` must stay a SIMPLE
   argument -- `:not(.quran-text *)` needs Selectors 4, and an unsupported
   selector invalidates the whole comma-separated rule, which would drop the
   opt-in everywhere else on older iOS Safari. */
[lang="ar"]:not(.quran-text),
[dir="rtl"]:not(.quran-text) {
  font-feature-settings: "liga" 1, "calt" 1, "rlig" 1, "dlig" 1, "ccmp" 1;
  font-variant-ligatures: contextual common-ligatures discretionary-ligatures;
  text-rendering: optimizeLegibility;
}


/* ───────────────────────────────────────────────────────────────────────────
   10 · THE PORTAL NAV — one collapsible rail, on every page.

   WHY IT IS IN THE RAIL AND NOT THE HEADER. The header had run out of room
   for it once already: `.header-link` was a 36px square holding a WORD, so
   ACCOUNT / FEEDBACK / FILES / ADMIN drew on top of one another (Phase 26.1).
   And four pages -- /account, /feedback, /pending, /reset-password -- had no
   nav at all, only a "back" link, so the way out of them depended on where
   you had come from.

   IT IS THE SAME RAIL THE PAGE OWN LIST LIVES IN. Two zones, nav then the
   list: /audio puts its minutes library under it, /app its conversations,
   /account nothing at all. One rail, one collapse, one account block --
   rather than a nav bar competing with a rail for the same edge.
   ─────────────────────────────────────────────────────────────────────────── */

.k-nav { display: flex; flex-direction: column; gap: 2px; }
.k-nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.k-nav a:hover { background: var(--bg-cream); color: var(--text-primary); }
.k-nav a[aria-current] { background: var(--bg-warm); color: var(--text-primary); font-weight: 600; }
.k-nav svg { width: 17px; height: 17px; flex: none; color: var(--text-muted); }
.k-nav a[aria-current] svg { color: var(--gold-500); }
.k-nav-sep { height: 1px; margin: 8px 4px; background: var(--border-light); }

/* The brand block at the head of the rail, and the control that collapses it. */
.k-rail-head { display: flex; align-items: center; gap: 10px; }
.k-rail-brand { display: flex; align-items: center; gap: 10px; min-width: 0; text-decoration: none; color: inherit; }
/* THE BRAND MARK IS THE LOGO, AND ITS GROUND IS CREAM RATHER THAN GOLD.
   It was a gold tile with `ج` set in it -- a stand-in for artwork that did
   not exist yet. The real mark is TWO-TONE: brown #512D14 and gold #B18D4C,
   and the gold half all but disappears against a --gold-400/500 tile, so
   half the logo would be missing and nothing would look broken. Cream is
   the ground the mark was drawn for; it is what the favicon carries.
   The tile is drawn HERE and not in the file, because an SVG carrying its
   own rounded rect inside a CSS border-radius shows the inner corners
   through the outer clip. One radius, one place. */
.k-rail-brand .mk {
  width: 32px; height: 32px; flex: none;
  border-radius: 10px;
  display: block;
  padding: 3px;
  background: var(--bg-page);
  border: 1px solid var(--border-light);
  box-sizing: border-box;
  object-fit: contain;
}
.k-rail-brand b {
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -.01em;
  white-space: nowrap;
}
.k-collapse {
  margin-left: auto;
  width: 32px; height: 32px;
  flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-sm);
  background: var(--bg-white);
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.k-collapse:hover { background: var(--bg-cream); color: var(--text-primary); }
.k-collapse svg { width: 16px; height: 16px; }

/* The account block at the foot. */
.k-me {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-md);
  background: var(--bg-white);
  box-shadow: var(--shadow-sm);
  width: 100%;
  cursor: pointer;
  text-align: left;
  font-family: var(--font-ui);
  transition: background var(--k-fast) var(--k-ease);
}
.k-me:hover { background: var(--bg-cream); }
.k-me .av {
  width: 32px; height: 32px; flex: none;
  border-radius: 50%;
  background: var(--gold-100);
  color: var(--gold-600);
  display: grid; place-items: center;
  font-weight: 700; font-size: 13px;
}
.k-me .who { min-width: 0; flex: 1; }
.k-me .who b {
  display: block; font-size: 13.5px; font-weight: 600; color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* A display name can be LSD. */
  font-family: var(--font-ui-lsd, 'Inter', 'KanzAlMarjaan', system-ui, sans-serif);
}
.k-me .who small {
  display: block; font-size: 11.5px; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* COLLAPSED IS AN ICON-ONLY RAIL, NOT A HIDDEN ONE — and that is a bug fix,
   not a preference.

   The first cut collapsed the rail to zero width with `visibility: hidden`,
   copying the chat rail from Phase 26.1. That works there because the chat
   page's toggle lives in the HEADER, outside the thing it collapses. Here the
   toggle sits in the rail, so hiding the rail hid the only control that
   brings it back — and the preference is persisted, so it followed the user
   from page to page. A portal with no navigation and no way to restore it.

   An icon-only rail cannot fail that way: every destination stays clickable,
   nothing leaves the tab order, and the control that widens it again is
   exactly where it was. The labels are `display: none`, so they leave the
   accessibility tree with their icons still carrying an accessible name from
   the link's `title`. */
@media (min-width: 901px) {
  .k-rail { transition: width var(--k-base) var(--k-ease), padding var(--k-base) var(--k-ease); }

  .k-shell.is-collapsed { grid-template-columns: 68px 1fr; }
  .k-shell.is-collapsed .k-rail { padding: 12px 10px; }

  /* Everything that is a word rather than a mark. */
  .k-shell.is-collapsed .k-rail-brand b,
  .k-shell.is-collapsed .k-nav-label,
  .k-shell.is-collapsed .k-rail-label,
  .k-shell.is-collapsed .k-rlist,
  .k-shell.is-collapsed .k-newbtn span,
  /* Phase 32 -- the same trap the library filters fell into once already
     (see the BACKSTOP note just below): a bare text node next to an icon
     matches nothing here unless it is named. */
  .k-shell.is-collapsed .ocr-lib-toggle span,
  .k-shell.is-collapsed .ocr-lib-rail-meter,
  .k-shell.is-collapsed .k-me .who,
  .k-shell.is-collapsed .k-me > svg { display: none; }

  /* THE BACKSTOP. Hiding labels by selector only works for labels somebody
     remembered to wrap -- the library filters shipped as bare text nodes and
     wrapped to three lines each inside a 68px column, spilling over the page.
     Clipping the rail means the worst a future unwrapped label can do is be
     invisible, instead of drawing on top of the work. */
  .k-shell.is-collapsed .k-rail { overflow: hidden; }
  .k-shell.is-collapsed .k-rail * { white-space: nowrap; }

  .k-shell.is-collapsed .k-rail-head { flex-direction: column; gap: 8px; }
  .k-shell.is-collapsed .k-collapse { margin-left: 0; }
  .k-shell.is-collapsed .k-nav a,
  .k-shell.is-collapsed .k-rmenu button { justify-content: center; padding: 10px 0; }
  .k-shell.is-collapsed .k-newbtn { padding: 11px 0; }
  .k-shell.is-collapsed .ocr-lib-toggle { justify-content: center; padding: 9px 0; }
  .k-shell.is-collapsed .k-me { justify-content: center; padding: 7px; }
}

/* ───────────────────────────────────────────────────────────────────────────
   ON A PHONE THE RAIL IS A SHEET, AND THE NAV IS A STRIP THAT STAYS.

   It used to stack in the flow -- brand, nav, the page's New button, its
   recents and the account block -- and the comment here claimed that "costs
   one row instead of a third of the screen". MEASURED on /audio at 360x800
   it cost 271px, which is a third of the screen exactly: the canvas started
   at y=293, the mode chips landed at y=554, and the third chip ran to x=495
   against a 360px viewport. Mohammed, 2026-09-10: "the new minute thing and
   recents should [be] in the profile section its the best like we have
   manzoori", and "still the user has to scroll to see the just the word and
   three other options".

   So this is `.conv-rail`'s pattern, deliberately -- same scrim, same
   off-screen transform, same 80dvh cap -- because two different sheets in
   one portal is two sets of bugs to find twice.

   WHAT STAYS OUT OF THE SHEET IS THE CROSS-PAGE NAV, and that is not a
   detail: `.seg-control` is pinned against ever being hidden on the chat
   pages, because a portal whose only way off a page is behind a control is
   the dead end this project already shipped once.
   ─────────────────────────────────────────────────────────────────────────── */

/* The persistent strip. `display: none` here and turned on below 901px --
   authored the opposite way round from the rest of this file because it is
   the ONE element that exists only on a phone. */
.k-topbar { display: none; }
.k-scrim { display: none; }
.k-sheet-close { display: none; }

@media (max-width: 900px) {
  .k-topbar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 4px;
    min-width: 0;
  }
  .k-topbar-brand { display: flex; align-items: center; flex: none; }
  .k-topbar-brand .mk {
    width: 30px;
    height: 30px;
    border-radius: 9px;
    background: var(--bg-page);
    border: 1px solid var(--border-light);
    object-fit: contain;
    padding: 3px;
  }
  /* The nav is the only thing allowed to eat the leftover width, and it
     scrolls rather than wrapping: wrapped, it is two rows at some label
     lengths and one at others, which moves everything under it. */
  .k-topbar .k-nav {
    flex: 1 1 auto;
    min-width: 0;
    flex-direction: row;
    overflow-x: auto;
    gap: 4px;
    scrollbar-width: none;
  }
  .k-topbar .k-nav::-webkit-scrollbar { display: none; }
  .k-topbar .k-nav a { flex: 0 0 auto; padding: 8px 10px; }
  .k-topbar .k-nav a svg { display: none; }
  .k-topbar-me {
    flex: none;
    /* Declared size AND flex-shrink: 0 -- a flex item's automatic minimum
       beats an explicit width, which is how the 44px send button beside the
       composer once measured 38px with nothing in the stylesheet looking
       wrong. */
    width: var(--tap, 44px);
    height: var(--tap, 44px);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
  }
  .k-topbar-me .av {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--gold-100);
    color: var(--gold-600);
    font-weight: 700;
    font-size: 13px;
  }

  .k-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;
  }
  .k-shell.is-sheet-open .k-scrim { opacity: 1; pointer-events: auto; }

  /* `.is-collapsed` is cancelled BY NAME -- (0,2,0) outranks `.k-rail`
     (0,1,0), so somebody who collapsed the rail on a laptop and then opened
     the same browser on a phone would carry `width: 0; visibility: hidden`
     into the sheet and tap a button that opens nothing. The desktop
     preference is deliberately not honoured here: there is no column to
     collapse, only a sheet, and `.is-sheet-open` is the whole story. */
  .k-shell.is-collapsed { grid-template-columns: 1fr; }
  .k-shell.is-collapsed .k-rail,
  .k-rail {
    position: fixed;
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    min-width: 0;
    max-height: 82dvh;
    /* HIDDEN, NOT MERELY OFF SCREEN. `translateY(100%)` moves the sheet out
       of sight and leaves every control in it FOCUSABLE -- New minute, every
       recent, Account, Sign out -- so a keyboard user tabs off the topbar
       and lands on buttons nobody can see, and a screen reader is offered
       two "Portal" navigations. `visibility` removes the subtree from the
       tab order and the accessibility tree at once and, unlike
       `display: none`, keeps the transform animating. It is delayed by the
       length of the slide on the way OUT so the sheet does not vanish
       mid-animation, and immediate on the way in. */
    visibility: hidden;
    border: 1px solid var(--border-medium);
    border-bottom: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: 0 -8px 24px rgba(44, 24, 16, 0.18);
    /* NO BOTTOM PADDING ON THE SHEET ITSELF. A scroll container's
       padding-bottom is part of its scrollport, so a sticky child at
       `bottom: 0` stops ABOVE it and the list goes on scrolling into
       the gap underneath -- a half-row of somebody's transcript
       sliding about below the account block. The safe-area inset moves
       onto the pinned block, where it is doing the same job. */
    padding: 14px 14px 0;
    /* Off-screen rather than display:none, so it animates and so the list
       inside keeps its scroll position between opens. */
    transform: translateY(100%);
    transition: transform 0.22s ease, visibility 0s linear 0.22s;
    overflow-y: auto;
    /* iOS chains a scroll that reaches the end of a container up to the page
       behind it, so flicking to the bottom of the recents drags the canvas
       under the sheet and the sheet appears to come unstuck. */
    overscroll-behavior: contain;
    z-index: 100;
  }
  .k-shell.is-sheet-open .k-rail {
    transform: translateY(0);
    visibility: visible;
    transition: transform 0.22s ease, visibility 0s;
  }

  /* THE ACCOUNT BLOCK STAYS ON THE FLOOR OF THE SHEET. It is the last child,
     so with a real library above it -- ten recordings is nothing -- it sat
     below the fold of the sheet's own scroll, and SIGN OUT HAS NO OTHER
     ROUTE ON A PHONE. Sticky keeps it in view while the list scrolls under
     it and costs nothing when the list is short, which is every page that
     passes no railExtra at all. */
  .k-rail .k-menu-wrap {
    position: sticky;
    bottom: 0;
    z-index: 2;
    margin-top: auto;
    padding-top: 8px;
    /* Opaque, or the rows scrolling underneath show through it. */
    background: var(--bg-white);
    padding-bottom: calc(14px + var(--safe-b, 0px));
    box-shadow: 0 -6px 10px -8px rgba(44, 24, 16, 0.35);
  }

  /* The wash is anchored to the FOOT of the rail and is 190px tall. In a
     sheet that scrolls, `inset: auto 0 0` pins it to the bottom of the
     scrolled box rather than of the visible sheet, so it rides up over the
     recents as you scroll. It is decoration; a sheet does not need it. */
  .k-rail::after { display: none; }
  .k-rail-grow { display: none; }

  /* EXACTLY ONE NAV IS VISIBLE AT ANY WIDTH, which is the whole licence for
     mounting it twice. The topbar's copy is always on screen here, so the
     sheet's copy is pure duplication -- a second identical row of
     destinations directly under the one you can already see, and a second
     "Portal" landmark for anyone listening to the page. */
  .k-rail .k-nav { display: none; }
  .k-nav { flex-direction: row; overflow-x: auto; gap: 6px; }
  .k-nav a { flex: 0 0 auto; }
  .k-nav-sep { display: none; }
  /* The collapse control is meaningless when there is no column to collapse;
     the close beside it is meaningless anywhere else. */
  .k-collapse { display: none; }
  .k-sheet-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--tap, 44px);
    height: var(--tap, 44px);
    flex-shrink: 0;
    margin-left: auto;
    background: none;
    border: 0;
    color: var(--text-muted);
    cursor: pointer;
  }
  .k-sheet-close svg { width: 18px; height: 18px; }
}


/* ───────────────────────────────────────────────────────────────────────────
   11 · THE ACCOUNT MENU, and the document page.
   ─────────────────────────────────────────────────────────────────────────── */

/* Anchored to the account block so the menu opens ABOVE it -- the block sits
   at the foot of the rail, and a menu opening downwards would be off-screen. */
.k-menu-wrap { position: relative; }
.k-menu {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 30;
  background: var(--bg-white);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: 5px;
  display: flex;
  flex-direction: column;
  animation: k-rise var(--k-fast) var(--k-ease) both;
}
.k-menu-item {
  display: block;
  width: 100%;
  padding: 9px 11px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: 13.5px;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.k-menu-item:hover { background: var(--bg-cream); color: var(--text-primary); }
.k-menu-item.is-danger:hover { background: var(--bg-warm); color: var(--red); }
.k-menu-sep { height: 1px; margin: 5px 4px; background: var(--border-light); }

/* THE DOCUMENT PAGE — /account, /feedback and the rest read top-to-bottom and
   are not a studio. Same panel, same edges as `.k-canvas`; it simply does not
   centre its contents or reserve a screen of height for a focal point. */
.k-page {
  background: var(--bg-white);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 30px;
  min-width: 0;
}
.k-page-head { margin-bottom: 22px; }
.k-page-head h1 {
  font-family: var(--font-display);
  font-size: clamp(24px, 3.4vw, 31px);
  font-weight: 500;
  letter-spacing: -.02em;
  margin: 0;
  color: var(--text-primary);
}
.k-page-head p { margin: 8px 0 0; color: var(--text-secondary); font-size: 14.5px; max-width: 62ch; }
.k-page-body { max-width: 680px; }

@media (max-width: 560px) {
  .k-page { padding: 20px 15px; border-radius: var(--radius-lg); }
}


/* ───────────────────────────────────────────────────────────────────────────
   12 · WHY THE CHAT RAIL DOES *NOT* CARRY THE PORTAL NAV.

   It did, briefly, and that was wrong twice over.

   THE NAV WOULD HAVE BEEN ON /app TWICE. The seg-control is still in the chat
   header, so a rail nav beside it is a second copy of the same link list --
   and "the secondary nav is reachable exactly once" is the rule that exists
   because leaving a copy in the header is what made the bar overflow in
   Phase 26.1. Two copies also drift: the rail's list gates FILES on the
   server's verdict and a forgotten header copy would not.

   AND THE HEADER COPY IS THE ONE THAT HAS TO STAY. Below 901px the chat rail
   is a CLOSED SHEET behind the composer's history button, so a nav that lived
   only inside it would be a nav a phone user has to know to go looking for --
   the exact dead end Phase 12 fixed by refusing to hide `.seg-control`.

   /audio can put its nav in the rail precisely because its rail is never a
   sheet: at narrow widths it becomes a visible strip above the work. The two
   pages differ because their rails differ, not by oversight.
   ─────────────────────────────────────────────────────────────────────────── */

/* ───────────────────────────────────────────────────────────────────────────
   13 · THE SHELL FILLS THE SCREEN.

   The first cut capped the shell at 1240px and centred it, which on a desktop
   left the product floating in the middle of a very wide page with a third of
   the screen empty on either side. A tool is not an article: the rail wants
   the edge it is anchored to, and the canvas wants the room.

   `align-items: stretch` (it was `start`) is what makes the rail and the
   canvas the same height, so the rail's account block sits at the BOTTOM OF
   THE SCREEN rather than wherever the nav happened to end.
   ─────────────────────────────────────────────────────────────────────────── */

.k-shell {
  max-width: none;
  align-items: stretch;
  /* The shell IS the page on every surface that uses it -- these pages render
     no header above it -- so it owns the viewport height. dvh tracks the
     Safari URL bar collapsing; the min-height is what stops a short page
     leaving the rail floating half way up the screen. */
  min-height: 100dvh;
  /* Tight to the edge. The rail is anchored furniture, not a card floating in
     the middle of a very wide page. */
  padding: 12px 12px 12px 10px;
}
.k-canvas,
.k-page { min-height: 0; }

@media (max-width: 900px) {
  /* On a phone the rail is a strip and the canvas grows with its content, so
     a forced viewport height would only add a dead band under the fold. */
  .k-shell { min-height: 0; padding: 12px; }
}


/* ───────────────────────────────────────────────────────────────────────────
   14 · /app AND /manzoori IN THE SAME LANGUAGE.

   APPEARANCE ONLY, ON THE CLASSES THEY ALREADY HAVE. The chat rail carries a
   desktop collapse, a phone sheet, a scrim, a rename field and 61 browser
   assertions, and its mechanics are the most-tested surface in the portal.
   Re-plumbing it to share PortalShell would risk all of that to change how it
   LOOKS -- so this restyles `.conv-rail`, `.chat-main` and `.input-row` into
   the studio's panels and leaves every behaviour exactly where it is. Same
   split as Phase 12: structure stays, appearance moves.

   SCOPED TO THE DESKTOP. Below 901px `.conv-rail` is a fixed-position SHEET
   over the transcript (responsive.css), and giving a sheet a floating panel's
   margins and radius would leave a gap down its edge with the scrim showing
   through. The phone keeps the layout that was measured on real devices.
   ─────────────────────────────────────────────────────────────────────────── */

@media (min-width: 901px) {
  .chat-shell {
    gap: 14px;
    padding: 4px 18px 18px;
    background: transparent;
  }

  .conv-rail {
    background: var(--bg-white);
    border: 1px solid var(--border-light);
    border-right: 1px solid var(--border-light);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: 12px;
    overflow: hidden;
    position: relative;
  }
  /* The amber wash at the foot, the same one the katib rail carries. */
  .conv-rail::after {
    content: "";
    position: absolute;
    inset: auto 0 0;
    height: 180px;
    pointer-events: none;
    background: radial-gradient(120% 84% at 50% 118%, var(--k-glow), var(--k-glow-out) 70%);
  }
  .conv-rail > * { position: relative; z-index: 1; }

  /* Collapsed, the rail must take its margins with it or the gap it leaves
     reads as a rendering fault rather than a closed panel. */
  .conv-rail.collapsed { padding-left: 0; padding-right: 0; border-width: 0; }

  .chat-main {
    background: var(--bg-white);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
  }

  .conv-new {
    border-radius: var(--radius-full);
    border: 1px solid var(--border-medium);
    background: var(--bg-white);
    /* THE COLOUR IS NOT OPTIONAL. The page's own sheet sets
       `color: var(--gold-100)` to sit on `--brown-700`; this rule replaced
       the background with white and left the text where it was, so NEW CHAT
       -- the primary action on the page -- was near-white on white. Visible
       only once the header stopped drawing the eye away from it. */
    color: var(--text-primary);
    font-weight: 600;
  }
  .conv-new:hover { background: var(--gold-100); border-color: var(--gold-500); color: var(--gold-600); }

  /* THE COMPOSER, matching the one on /audio: a single rounded surface that
     lifts when it has focus. `:focus-within`, not `:hover` -- this is where
     the user is typing, and a border that only responds to a mouse says
     nothing on a keyboard. */
  .input-row {
    border-radius: 24px;
    border: 1px solid var(--border-medium);
    background: var(--bg-white);
    box-shadow: var(--shadow-md);
    transition: border-color var(--k-base) var(--k-ease), box-shadow var(--k-base) var(--k-ease);
  }
  .input-row:focus-within { border-color: var(--gold-500); box-shadow: var(--shadow-lg); }

  .send-btn {
    border-radius: var(--radius-full);
    background: linear-gradient(150deg, var(--gold-400), var(--gold-500));
    color: #2A1D06;
    border: 0;
    box-shadow: 0 8px 20px -10px var(--gold-500);
    transition: transform var(--k-fast) var(--k-ease), filter var(--k-fast) var(--k-ease);
  }
  .send-btn:hover:not(:disabled) { transform: translateY(-1px); filter: brightness(1.04); }
}


/* ───────────────────────────────────────────────────────────────────────────
   15 · THE MINUTES FORM.

   This is the half of a minute NO MODEL CAN KNOW -- subject, idara, both
   dates, times, place, chairman, minute taker, who attended -- and it is the
   longest stretch of typing in the product. It had the old flat inputs while
   everything around it had been redrawn, which on the one screen that asks
   for sustained attention is exactly where the seam shows.

   THE ONE EXCEPTION TO THE ARABIC THUMB RULE LIVES HERE and is not touched:
   `.mform-input[dir="rtl"]` holds ordinary Arabic typed at a keyboard --
   names, rooms, subjects -- which has NOT been through lsd_text.postprocess,
   and Kanz draws that as different letters. Those fields keep a plain Arabic
   face. The split is composed-vs-typed, and this is the typed side.
   ─────────────────────────────────────────────────────────────────────────── */

.mform-label {
  color: var(--text-secondary);
  font-weight: 600;
  letter-spacing: .01em;
}

.mform-input,
.mform-textarea {
  border: 1px solid var(--border-medium);
  border-radius: var(--radius-sm);
  background: var(--bg-white);
  color: var(--text-primary);
  padding: 10px 12px;
  transition: border-color var(--k-fast) var(--k-ease), box-shadow var(--k-fast) var(--k-ease);
}
.mform-input:hover,
.mform-textarea:hover { border-color: var(--border-medium); }
.mform-input:focus,
.mform-textarea:focus {
  outline: none;
  border-color: var(--gold-500);
  /* A ring rather than a heavier border: a border that thickens on focus
     shifts the field by a pixel and nudges everything beside it. */
  box-shadow: 0 0 0 3px var(--gold-100);
}
.mform-textarea { border-radius: var(--radius-md); }

.mform-online,
.mform-add,
.mform-remove {
  border-radius: var(--radius-full);
  border: 1px solid var(--border-medium);
  background: var(--bg-white);
  color: var(--text-secondary);
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease),
              border-color var(--k-fast) var(--k-ease);
}
.mform-add:hover { background: var(--gold-100); border-color: var(--gold-500); color: var(--gold-600); }
.mform-remove:hover { background: var(--bg-warm); border-color: var(--red); color: var(--red); }
.mform-online:hover { background: var(--bg-cream); color: var(--text-primary); }

.mform-hint { color: var(--text-muted); }
.mform-saved { color: var(--green); font-weight: 600; }

/* Two columns is right for a desk and wrong for a thumb: on a phone a
   half-width "Place" field holds about six characters before it scrolls. */
@media (max-width: 700px) {
  .mform-grid,
  .mform-rolls { grid-template-columns: 1fr; }
}


/* ───────────────────────────────────────────────────────────────────────────
   16 · THE RAIL SCROLLS ITSELF, INSTEAD OF PUSHING THE PRODUCT OFF THE SCREEN.

   Section 13 gave the shell `min-height: 100dvh` and `align-items: stretch`
   so the rail would reach the bottom of the screen. Nothing capped it at the
   TOP of the screen, so the arithmetic ran the other way: the history list is
   as tall as your history, the rail grew to hold all of it, stretch made the
   canvas match, and the whole page became as tall as the rail.

   MEASURED on a real account with a normal amount of history, at 1440x900:
   the shell was 1497px tall inside a 900px window. The canvas centred its
   hero in that 1497px, which put the orb at y=483 -- half a screen of empty
   cream above the one thing on the page -- and the composer at y=840..978,
   i.e. THE RECORD BUTTON WAS BELOW THE FOLD. At 1366x768, the laptop most of
   these testers actually use, the fold lands at 768 and you see the orb, the
   headline, and nothing else: no mode chips, no drop zone, no record button.
   A page whose whole argument is "the composer is the recorder" was shipping
   with the composer off the screen.

   It also quietly falsified section 13's own claim that the account block
   "sits at the bottom of the SCREEN" -- measured, `.k-me` was at y=1442.

   THE FIX IS TO BOUND BOTH COLUMNS AND LET THE LIST SCROLL INSIDE THE RAIL.
   A tool's chrome is fixed and its content moves; only an article scrolls as
   one piece. `.k-rlist` is `flex: 0 1 auto` so it takes its content's height
   when there is room and gives it back when there is not, while
   `.k-rail-grow` (`flex: 1 1 auto`, zero-height) still pushes the account
   block down on the pages that have no list at all -- /files, /account,
   /feedback. Reversing those two flex settings puts the growable spacer in
   competition with the list and leaves a rail that is half blank.

   `min-height: 0` on both is the load-bearing half: a flex item's automatic
   minimum size is its CONTENT, so without it neither will shrink and the cap
   above them does nothing at all. Same rule that put the send button off an
   iPhone SE in Phase 26.1, in the other axis.
   ─────────────────────────────────────────────────────────────────────────── */

@media (min-width: 901px) {
  /* 24px = the shell's 12px top and bottom padding from section 13. */
  .k-rail,
  .k-canvas { max-height: calc(100dvh - 24px); }

  .k-rail { min-height: 0; }
  .k-rail-grow { flex: 1 1 auto; min-height: 0; }

  /* THE SHRINK HAS TO REACH THE FLEX ITEM, AND `.k-rlist` IS NOT ONE.
     MinutesRail groups each caption with the thing it captions, so the rail's
     children are those wrappers -- the first cut styled `.k-rlist` and the
     cap above it did nothing at all: measured, the rail was clipped at 888px
     with its account block still laid out at y=1442, hidden by the rail's own
     `overflow: hidden` rather than moved. The chain has to be unbroken from
     the item down to the scroller, which is what `.k-rail-scroll` names. */
  /* EVERYTHING ELSE IN THE RAIL REFUSES TO SHRINK. The default `flex-shrink:
     1` let the filter block give up height it still had content for, and
     because a flex item's overflow is visible, those four filter rows simply
     DREW ON TOP OF the history beneath them -- the Phase 26.1 `.header-link`
     overlap again, in the other axis. Exactly one child may absorb the
     shortfall, and it is the one that scrolls. */
  .k-rail > * { flex: none; }
  .k-rail-scroll {
    display: flex;
    flex-direction: column;
    flex: 0 1 auto;
    min-height: 0;
  }
  .k-rail > .k-rail-grow { flex: 1 1 auto; }
  .k-rail-scroll .k-rlist {
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* Reaching the end of the history must not then scroll the page behind
       it -- the rail is the one thing on screen that is supposed to stay put. */
    overscroll-behavior: contain;
  }
  /* AND THE ROWS DO NOT SHRINK EITHER. `.k-rlist` is a column flex container,
     so once it was shorter than its content every button shrank a little
     instead of the list scrolling: rows drawn at half height, one title
     bleeding into the next, and no scrollbar to say anything was hidden. A
     list that scrolls needs its items to keep their size -- that IS the
     overflow it is scrolling. */
  .k-rail-scroll .k-rlist > * { flex: none; }

  /* The canvas holds the work, so it is the other scroller: a rendered minute
     runs for pages and must not drag the rail up with it. */
  .k-canvas {
    overflow-y: auto;
    overscroll-behavior: contain;
    /* SAFE CENTRING. `justify-content: center` on a scroll container whose
       content overflows pushes the first child's top out past the scroll
       origin, where no scrollbar can reach it -- the top of a long minute
       becomes unreadable. `margin-block: auto` centres exactly the same way
       when there is room and simply collapses when there is not. */
    justify-content: flex-start;
  }
  .k-canvas > * { flex: none; }
  .k-canvas > .k-stack { margin-block: auto; }
}


/* ───────────────────────────────────────────────────────────────────────────
   17 · THE PHONE'S FIRST SCREEN IS THE RECORDER, NOT THE MENU.

   Measured at 390x844: the rail was 392px -- 46% of the screen -- so the
   canvas began at y=414, the orb at 441, and the composer ran 818..1032,
   entirely below the fold. Someone opening /audio on their phone saw a
   navigation list and had to scroll to find out the page does anything.

   The two captions and the filter strip are what cost that. They are the
   least-used things in the rail and the first two are pure labelling: the
   history strip is recognisably a list of your recordings without a line
   above it saying so. The filters are a refinement of a list you can already
   see -- kept on a desktop, where the rail is a column with room to spare,
   and dropped where the screen is the scarce thing.

   NOT DROPPED: the nav strip (`.seg-control`'s rule -- the way off the page
   is never hidden at any width), New minute, the history itself, or the
   account block, which is the only route to sign out on a phone.
   ─────────────────────────────────────────────────────────────────────────── */

@media (max-width: 900px) {
  /* THE CAPTIONS AND FILTERS ARE BACK, and the reason they went is the
     reason they return. They were dropped because the rail was 392px IN THE
     FLOW and "the user opened /audio and saw a navigation list" -- the cost
     was paid by everyone, on arrival, whether they wanted the library or
     not. The rail is a sheet now: it is 82dvh, it is opened deliberately,
     and a filter is worth its row to somebody who has just asked to see
     their recordings. Nothing above the fold pays for it any more.

     NOTE THE PADDING IS NOT SET HERE. This block sits LATER in the file than
     the sheet, so a `padding` here silently replaced the sheet's
     `calc(14px + var(--safe-b))` and put the last row of the list under the
     home indicator. Whatever the sheet needs, the sheet declares. */
  .k-rail { gap: 10px; }
  .k-canvas { padding: 24px 18px 22px; }
}


/* ───────────────────────────────────────────────────────────────────────────
   18 · AND THE REST OF THE PHONE'S FIRST SCREEN.

   Section 17 got the rail down from 392px to 281px. Measured again at
   390x844 that still put the composer at 705..935 -- the RECORD BUTTON, the
   one control this page is built around, 91px below the fold on the handset
   most testers will open it on. Everything here is spent buying those 91px
   back out of vertical rhythm that a phone cannot afford: the orb is the
   focal point and does not need to be 96px to say so, the three mode chips
   are one scrollable row rather than two wrapped ones, and the hero's copy
   sits closer to the headline it belongs to.

   The chips DO NOT wrap here. A wrapped row is two rows tall and reorders
   itself as the label lengths change; a single row that scrolls is one row
   at every width, and the same gesture the history strip above it already
   uses. `flex-wrap: wrap` is right on a desktop and is the thing being
   overridden, not an oversight.
   ─────────────────────────────────────────────────────────────────────────── */

@media (max-width: 900px) {
  /* THE CHIPS WRAP AGAIN, AND THIS REVERSES A PHASE 27 DECISION ON PURPOSE.
     That decision -- one scrolling row rather than two wrapped ones -- was
     made when the rail above was eating 271px of an 800px screen and every
     vertical pixel was contested. It is not contested any more: the rail is
     a sheet, the canvas starts at y=82 instead of y=293, and the chips sit
     at y=343 with room to spare.

     Meanwhile the cost that decision ACCEPTED is what Mohammed reported on
     2026-09-10 -- "still the user has to scroll to see the just the word and
     three other options". Measured at both 360 and 390: the three chips need
     472px against 336 available, so "The full minute" ran to x=495 and was
     simply off the screen, behind a sideways scroll with no affordance on a
     row that looks complete. A mode nobody can see is a mode nobody picks.

     So: no icons (24px each, and the words already say it), tighter
     padding, and wrap. All three are on screen at every phone width. */
  .k-chips {
    flex-wrap: wrap;
    justify-content: center;
    width: auto;
    max-width: 100%;
    margin-top: 16px;
    gap: 8px;
  }
  .k-chips > * { flex: 0 0 auto; }
  .k-chip { padding: 9px 12px; font-size: 13px; gap: 0; }
  .k-chip svg { display: none; }
  .k-canvas { padding: 20px 16px 20px; }
}

@media (max-width: 560px) {
  .k-orb { width: 80px; height: 80px; margin-bottom: 12px; }
  .k-orb-btn { width: 58px; height: 58px; }
  .k-orb-btn svg { width: 25px; height: 25px; }
  .k-chips { margin-top: 10px; }
  .k-rail { gap: 8px; }
  .k-canvas { padding: 16px 14px 18px; }
}


/* ───────────────────────────────────────────────────────────────────────────
   19 · /app AND /manzoori LOSE THE PORTAL BAR.

   Mohammed, 2026-09-04, on a screenshot of that bar: "i dont need this in
   chat and manzoori page as well". These were the last two pages spending a
   64px strip across the top of the screen on a brand mark, an email address,
   a quota pill and four links -- all four of which the rail already had a
   place for. This is chrome only: the transcript, the composer, the grading
   form, the annotate flow and the rail's own mechanics are untouched.

   THE COLLAPSE HAD TO CHANGE SHAPE, AND THAT IS THE ONE RISKY PART. The
   toggle used to live in the HEADER, outside the column it collapsed, which
   is precisely what licensed `width: 0; visibility: hidden` -- there was
   always a visible control to bring the rail back. With the header gone the
   toggle sits INSIDE the rail, so hiding the rail hides the toggle, and
   `chat.rail.collapsed` is persisted: one click and the page has no
   navigation and no way to restore it, on every future visit. That exact bug
   shipped on /audio in 42aebab. Collapsed is an ICON-ONLY rail here for the
   same reason it is one there: every destination stays clickable, nothing
   leaves the tab order, and the toggle does not move.

   This sheet loads AFTER each page's inline <style>, so the `.conv-rail
   .collapsed` override below wins on order at equal specificity (0,2,0) --
   the inline rule is left in place rather than edited, because it is written
   once in index.html and once in manzoori.html and those two are exactly the
   pair this conversion exists to stop maintaining twice.
   ─────────────────────────────────────────────────────────────────────────── */

.conv-rail-top {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 12px 4px;
  flex-shrink: 0;
}
.conv-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}
/* Same mark, same reasoning as .k-rail-brand .mk above. */
.conv-brand .mk {
  width: 30px;
  height: 30px;
  flex: none;
  display: block;
  padding: 3px;
  border-radius: 9px;
  background: var(--bg-page);
  border: 1px solid var(--border-light);
  box-sizing: border-box;
  object-fit: contain;
}
.conv-brand b {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 600;
  color: var(--brown-900);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.conv-collapse {
  margin-left: auto;
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-sm);
  background: var(--bg-white);
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.conv-collapse:hover { background: var(--bg-cream); color: var(--text-primary); }
.conv-collapse svg { width: 17px; height: 17px; }

.conv-rail-nav { padding: 6px 8px 0; flex-shrink: 0; }
.conv-rail-nav .k-nav a { font-size: 13.5px; padding: 8px 10px; }

.conv-rail-quota { padding: 6px 12px 0; flex-shrink: 0; }
.conv-rail-quota .quota-pill { width: 100%; justify-content: center; }

/* The phone's copy of the nav. Hidden here; shown in the phone block. */
.chat-nav-strip { display: none; }

@media (min-width: 901px) {
  /* ICON-ONLY, NEVER HIDDEN. See the block comment above -- this is the whole
     reason the collapse could not be left as it was. */
  .conv-rail.collapsed {
    width: 68px;
    visibility: visible;
    overflow: hidden;
    border-right: 1px solid var(--border-light);
  }
  .conv-rail.collapsed * { white-space: nowrap; }
  .conv-rail.collapsed .conv-brand b,
  .conv-rail.collapsed .k-nav-label,
  .conv-rail.collapsed .conv-rail-quota,
  .conv-rail.collapsed .rail-user-text,
  .conv-rail.collapsed .rail-user-caret { display: none; }
  /* THE LIST IS HIDDEN, NOT REMOVED, and the difference is where the account
     block ends up. `.conv-list` is `flex: 1` -- it is the thing that pushes
     the account block to the foot of the rail -- so `display: none` collapsed
     it to nothing and the avatar rode up to sit under the nav, half way up a
     900px column. `visibility: hidden` keeps the box, and keeps the promise
     the original collapse was written for: every conversation button leaves
     the tab order, so a keyboard user cannot land on a row nobody can see. */
  .conv-rail.collapsed .conv-list { visibility: hidden; }
  /* NEW CHAT is a word, not an icon, so collapsed it becomes the one glyph it
     can afford. Hiding it outright would take the primary action off the page
     for anyone who left the rail closed. */
  .conv-rail.collapsed .conv-new {
    font-size: 0;
    padding: 9px 0;
  }
  .conv-rail.collapsed .conv-new::after {
    content: "+";
    font-size: 17px;
    font-weight: 700;
  }
  .conv-rail.collapsed .conv-rail-top { flex-direction: column; gap: 8px; padding: 12px 8px 4px; }
  .conv-rail.collapsed .conv-collapse { margin-left: 0; }
  .conv-rail.collapsed .conv-rail-head,
  .conv-rail.collapsed .conv-rail-nav { padding-left: 8px; padding-right: 8px; }
  .conv-rail.collapsed .k-nav a { justify-content: center; padding: 10px 0; }
  .conv-rail.collapsed .rail-user { justify-content: center; padding: 7px; }
}

@media (max-width: 900px) {
  /* THE SHEET DOES NOT CARRY THE NAV, and that is the entire point of the
     second mount. Below 901px `.conv-rail` is a bottom sheet behind the
     composer's history button; a nav in there is the only way off /app
     hidden behind a button, which is the dead end
     test_cross_page_nav_is_never_hidden exists to prevent. */
  .conv-rail-nav { display: none; }
  /* And the collapse toggle collapses a column that does not exist here --
     the sheet has its own close control. */
  .conv-rail-top { display: none; }

  .chat-nav-strip {
    display: block;
    flex: none;
    padding: 8px 12px;
    background: var(--bg-page);
    border-bottom: 1px solid var(--border-light);
  }
  .chat-nav-strip .k-nav {
    flex-direction: row;
    overflow-x: auto;
    gap: 6px;
    /* The row scrolls rather than wrapping: one row at every width, and the
       same gesture the rest of the phone chrome already uses. */
    flex-wrap: nowrap;
  }
  .chat-nav-strip .k-nav a { flex: 0 0 auto; }
  .chat-nav-strip .k-nav-sep { display: none; }
}


/* ───────────────────────────────────────────────────────────────────────────
   20 · THE EMPTY CHAT.

   Mohammed, 2026-09-05: "can we make this attractive as well? like the
   minutes page - this looks a little bland - not too much but a little
   animation or something like that". It is the first thing a tester sees on
   /app and on /manzoori, it is what they look at while deciding whether to
   type anything at all, and it was a mark, two lines and a paragraph pinned
   to the top of a mostly empty column.

   APPEARANCE ONLY, ON THE CLASSES THAT ARE ALREADY THERE. `.welcome` and its
   four children are untouched in ChatPage.tsx, so nothing about the transcript,
   the composer, grading or the annotate flow can be disturbed by any of this.
   The two rings are `::before` and `::after` on the mark for the same reason:
   a ring that needs a <span> needs a JSX edit on the most-tested page pair in
   the portal, to draw a circle.

   THE MARK STAYS DARK. /audio's orb is amber because it is a BUTTON -- the one
   thing on that page you are meant to press. Nothing here is pressable, so
   copying the amber would put the page's loudest object on something inert and
   send every reader to press it. The rings and the halo carry the accent
   instead, which is what makes this the same language rather than the same
   picture.

   AND THE RINGS TAKE NO TAPS. `pointer-events: none`, because that is exactly
   how the record button on /audio spent Phase 27 being dead: decoration
   painted over content, hit-testing normally, swallowing every click in
   silence. Nothing is behind these two -- and they get the rule anyway, since
   the cost is nothing and the failure is invisible.
   ─────────────────────────────────────────────────────────────────────────── */

/* The welcome is CENTRED in the transcript column now, not stacked at the top
   of it. `.messages` is the element with a definite height to measure against
   -- `.chat-container` is the flex item that has one -- so the min-height goes
   there and `.welcome` grows into it. */
.messages { min-height: 100%; }

.welcome {
  flex: 1;
  justify-content: center;
  position: relative;
  /* Was 50vh with 50px of top padding, which is what pinned it high. */
  min-height: 0;
  padding: 24px;
}
/* The ambient wash, the same one `.k-canvas` carries behind its orb. Painted
   on the container rather than the mark so it stays put while the mark moves. */
.welcome::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(38% 42% at 50% 34%, var(--k-glow), var(--k-glow-out) 70%);
  opacity: .5;
}
.welcome > * { position: relative; z-index: 1; }

.welcome-logo {
  position: relative;
  border-radius: 26px;
  box-shadow: 0 18px 40px -18px rgba(44, 24, 16, .55);
  /* The breathing is declared with the stagger further down, because the two
     have to be ONE `animation` shorthand -- `.welcome > *` and `.welcome-logo`
     have equal specificity, so a second declaration up here would simply be
     overwritten by the stagger and the mark would sit still. */
}
.welcome-logo::before,
.welcome-logo::after {
  content: "";
  position: absolute;
  inset: -14px;
  border-radius: 50%;
  border: 1px solid var(--gold-500);
  pointer-events: none;
  animation: k-ripple 5.5s var(--k-ease) infinite;
}
.welcome-logo::after {
  inset: -28px;
  animation-delay: 1.8s;
}

.welcome-heading {
  font-size: clamp(26px, 3.4vw, 34px);
  letter-spacing: -.01em;
  /* A two-word second line under a five-word first is worse than an even
     split, and these headings are configuration -- they change per page. */
  text-wrap: balance;
}
/* THE ARABIC LINE IS THE ACCENT. It is the one piece of this screen that is
   the language the portal is FOR, so it carries the gold rather than sitting
   a shade below the heading in the same brown. Its face is untouched:
   `--font-lsd` is Kanz and invariant #9 is not negotiable. */
.welcome-arabic {
  font-size: 26px;
  color: var(--gold-600);
  margin-bottom: 18px;
}
.welcome-desc {
  max-width: 44ch;
  line-height: 1.72;
  color: var(--text-muted);
}

/* THE STAGGER, and it animates from a visible resting state onwards. `both`
   holds the FROM state before the delay elapses, which is the one thing that
   could leave this screen blank if an animation never fired -- so the delays
   are all under a fifth of a second and the guard at section 8 removes them
   entirely for anyone who has asked for less motion. */
.welcome > * { animation: k-rise var(--k-base) var(--k-ease) both; }
/* Barely there, and deliberately slow: this is ambience, not a status. The
   breathing orb on /audio runs at 2.8s because it IS telling you something is
   happening; nothing is happening here. */
.welcome-logo    { animation: k-breathe 7s ease-in-out infinite, k-rise var(--k-base) var(--k-ease) both; }
.welcome-heading { animation-delay: 60ms; }
.welcome-arabic  { animation-delay: 120ms; }
.welcome-desc    { animation-delay: 180ms; }

@media (max-width: 560px) {
  .welcome-arabic { font-size: 22px; }
  .welcome-logo::after { display: none; }
}


/* ───────────────────────────────────────────────────────────────────────────
   21 · THE OCR LIBRARY — Phase 32.

   EVERY CLASS HERE IS `.ocr-lib-*`, AND ALL OF IT LIVES HERE RATHER THAN IN
   lsd-ocr.html's OWN <style> BLOCK. That is a deliberate departure from that
   file's own rule ("what lives here is only what katib.css has no reason to
   know about: the reader") -- these pieces are new design-language
   components (a meter, a search box, a document list, a per-row menu) in the
   same sense the rail and the account menu already are, not reader-specific
   markup. Nothing here is reachable without `can_access_ocr_library`
   (`usage.enabled`), so a plain OCR user's page never renders a single one
   of these classes -- see History.tsx's own comment on that gate.
   ─────────────────────────────────────────────────────────────────────────── */

/* The rail control and its one-line meter. Same visual family as
   `.k-newbtn`, restated rather than shared: the newbtn is a filled pill for
   the PRIMARY action on the page, and Library is a secondary one -- a ghost
   button reads as the second thing to reach for, which is what it is. */
.ocr-lib-toggle {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 12px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease), color var(--k-fast) var(--k-ease);
}
.ocr-lib-toggle:hover { background: var(--bg-cream); color: var(--text-primary); }
.ocr-lib-toggle svg { width: 16px; height: 16px; flex-shrink: 0; }
.ocr-lib-rail-meter {
  font-size: 11px;
  color: var(--text-light);
  padding: 0 12px;
  margin-top: -8px;
}

/* ── The library canvas itself ──────────────────────────────────────────── */

.ocr-lib { max-width: 860px; width: 100%; }

.ocr-lib-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

/* The meter. A bar rather than a number alone -- "412 of 1024 MB" is a fact,
   a bar that is 40% full is a feeling, and this is the one place in the
   product that exists to let someone judge "am I close to the edge" at a
   glance, the way /audio's quota banner does for minutes. */
.ocr-lib-meter { margin-top: 4px; }
.ocr-lib-meter-bar {
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--bg-warm);
  overflow: hidden;
}
.ocr-lib-meter-fill {
  height: 100%;
  border-radius: var(--radius-full);
  background: linear-gradient(90deg, var(--gold-400), var(--gold-500));
  transition: width var(--k-slow) var(--k-ease);
}
.ocr-lib-meter-text {
  margin-top: 7px;
  font-size: 12.5px;
  color: var(--text-muted);
}
.ocr-lib-cap-note {
  font-size: 12px;
  color: var(--text-light);
  line-height: 1.6;
}
/* The meter's own "ask for more" -- an inline word, not a button, because it
   sits inside a sentence of running text rather than beside a blocking wall
   the way the daily-quota banner's ask does. */
.ocr-lib-ask {
  border: 0;
  background: none;
  padding: 0;
  color: var(--gold-600);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.ocr-lib-ask:hover { color: var(--brown-900); }

/* The search box. Same visual weight as `.ocr-title` (a hairline underline,
   no card) so it reads as part of the composer's own family rather than a
   different control bolted on. */
.ocr-lib-search {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 4px 10px 14px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-full);
  background: var(--bg-white);
  box-shadow: var(--shadow-sm);
  color: var(--text-light);
}
.ocr-lib-search svg { width: 17px; height: 17px; flex-shrink: 0; }
.ocr-lib-search-input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  font-family: var(--font-ui-lsd);
  /* 16px minimum: iOS Safari zooms the whole page on a focused control
     smaller than that and never zooms back. */
  font-size: 16px;
  color: var(--text-primary);
  padding: 4px 8px;
}
.ocr-lib-search-input::placeholder { color: var(--text-light); }
.ocr-lib-search-input:focus { outline: none; }
/* Chromium's own magnifier/clear icons on type="search" clash with the one
   already drawn to its left. */
.ocr-lib-search-input::-webkit-search-cancel-button,
.ocr-lib-search-input::-webkit-search-decoration { -webkit-appearance: none; }

.ocr-lib-results { display: flex; flex-direction: column; gap: 14px; }
.ocr-lib-searching { font-size: 13px; color: var(--text-muted); }
.ocr-lib-empty {
  font-size: 13px;
  color: var(--text-muted);
  padding: 10px 2px;
}

.ocr-lib-group {
  background: var(--bg-white);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 12px;
}
.ocr-lib-group-head {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-light);
  padding: 4px 8px 8px;
}

/* A title/description hit. Whole-row button, same idiom as `.k-rlist`'s
   rows -- the entire card takes the tap, not a smaller link inside it. */
.ocr-lib-hit-doc {
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  text-align: left;
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease);
}
.ocr-lib-hit-doc:hover { background: var(--bg-cream); }
.ocr-lib-hit-title { font-size: 15px; color: var(--brown-900); }
.ocr-lib-hit-desc { font-size: 13px; color: var(--text-secondary); }
.ocr-lib-hit-meta { font-size: 11.5px; color: var(--text-light); }

.ocr-lib-hit-group + .ocr-lib-hit-group { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-light); }
.ocr-lib-hit-doc-name {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 14px;
  color: var(--brown-900);
  padding: 4px 8px;
}
.ocr-lib-hit-count { font-size: 11px; color: var(--text-light); font-family: var(--font-ui); flex-shrink: 0; }
.ocr-lib-hit-page {
  display: block;
  width: 100%;
  padding: 8px 12px;
  margin-top: 2px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  text-align: right;
  direction: rtl;
  cursor: pointer;
  transition: background var(--k-fast) var(--k-ease);
}
.ocr-lib-hit-page:hover { background: var(--bg-cream); }
.ocr-lib-hit-page-no {
  display: block;
  direction: ltr;
  text-align: left;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10.5px;
  letter-spacing: 0.06em;
  color: var(--text-light);
  margin-bottom: 3px;
}
.ocr-lib-hit-snippet {
  display: block;
  font-size: 15px;
  line-height: 1.7;
  color: var(--text-secondary);
}
.ocr-lib-hit-snippet + .ocr-lib-hit-snippet { margin-top: 4px; }

/* The mark itself. A different treatment from `.ocr-unsure` (the model's own
   doubt) on purpose: this is "you found it", not "check this" -- a filled
   chip rather than an underline reads as a positive hit instead of a warning. */
.ocr-hit mark {
  background: var(--gold-300);
  color: var(--brown-900);
  border-radius: 3px;
  padding: 0 2px;
}

.ocr-lib-filters { display: flex; gap: 8px; flex-wrap: wrap; }
.ocr-lib-select {
  padding: 7px 12px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-full);
  background: var(--bg-white);
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: 12.5px;
}

.ocr-lib-list { display: flex; flex-direction: column; gap: 8px; }
.ocr-lib-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: var(--bg-white);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.ocr-lib-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
  padding: 2px;
}
.ocr-lib-row-title {
  font-size: 15px;
  font-weight: 500;
  color: var(--brown-900);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ocr-lib-row-desc {
  font-size: 12.5px;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ocr-lib-row-meta { font-size: 11.5px; color: var(--text-light); }

.ocr-lib-status {
  flex-shrink: 0;
  padding: 3px 10px;
  border-radius: var(--radius-full);
  font-size: 11px;
  font-weight: 500;
  border: 1px solid var(--border-light);
  background: var(--bg-cream);
  color: var(--text-secondary);
  white-space: nowrap;
}
.ocr-lib-status.is-done { color: var(--green); border-color: #cde8d5; background: #f2f9f4; }
.ocr-lib-status.is-failed { color: var(--red); border-color: #f0cfcb; background: #fdf3f2; }
.ocr-lib-status.is-queued,
.ocr-lib-status.is-running { color: var(--gold-600); border-color: var(--gold-200); background: var(--gold-100); }

.ocr-lib-kept {
  flex-shrink: 0;
  font-size: 11px;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-light);
  white-space: nowrap;
}
.ocr-lib-kept.is-kept { color: var(--text-muted); }

.ocr-lib-menu-wrap { position: relative; flex-shrink: 0; }
.ocr-lib-kebab {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
.ocr-lib-kebab:hover { background: var(--bg-cream); color: var(--text-primary); }
.ocr-lib-kebab svg { width: 15px; height: 15px; }
/* `.k-menu` is the shared popover shape (the account menu's own); this only
   repositions it to open BELOW a row near the top of the list rather than
   above it, and gives it a sane width for a kebab menu rather than the
   full-width column it takes in the rail. */
.ocr-lib-menu {
  bottom: auto;
  top: calc(100% + 6px);
  left: auto;
  right: 0;
  min-width: 190px;
}

.ocr-lib-rename {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  min-width: 200px;
}
.ocr-lib-rename-title,
.ocr-lib-rename-desc {
  width: 100%;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-sm);
  padding: 7px 10px;
  font-family: var(--font-ui-lsd);
  font-size: 13.5px;
  color: var(--text-primary);
  background: var(--bg-cream);
  resize: vertical;
}
.ocr-lib-rename-title:focus,
.ocr-lib-rename-desc:focus { outline: none; border-color: var(--gold-500); }
.ocr-lib-rename-actions { display: flex; gap: 6px; }

.ocr-lib-more {
  align-self: center;
  margin-top: 4px;
}

@media (max-width: 640px) {
  .ocr-lib-row { flex-wrap: wrap; }
  .ocr-lib-row-main { flex-basis: 100%; }
}

/* ── Title/description, mandatory everywhere — Phase 32-E ─────────────────
   Mohammed's own first hands-on test: "title and description is mandatory".
   `.ocr-lib-required` is the one inline hint every collection point for
   these two fields shares -- the pending-uploads staging list below, the
   document header's rename form, and the library's own RenameEditor -- so
   the three cannot drift into three different ways of saying the same
   thing. */
.ocr-lib-pending-field { position: relative; margin-bottom: 4px; }
.ocr-lib-required {
  display: block;
  margin-top: 4px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--red);
}

/* ── The pending-uploads staging list ──────────────────────────────────────
   Title and description are collected HERE, after a file is picked and
   before a single byte uploads -- never before picking (nothing to attach
   them to) and never after the read starts (the server would 422 first). */
.ocr-lib-pending-head {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 500;
  color: var(--brown-900);
  margin-bottom: 2px;
}
.ocr-lib-pending-sub { margin-bottom: 14px; }
.ocr-lib-pending-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 100%;
}
.ocr-lib-pending-row {
  padding: 14px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-md);
  background: var(--bg-cream);
}
.ocr-lib-pending-file {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.ocr-lib-pending-name {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11.5px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ocr-lib-pending-remove {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-light);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.ocr-lib-pending-remove:hover { background: var(--bg-warm); color: var(--red); }

/* ── The page image beside the text (PageView.tsx) ──────────────────────── */

/* Mobile-first: no grid at all, so the image is simply the FIRST block in
   normal flow and the text follows it -- "above it on a phone" costs zero
   extra rules. The two-column grid only exists from 860px, where there is
   room for a useful-sized photograph beside a readable column of text. */
.ocr-lib-split.has-image { display: flex; flex-direction: column; gap: 16px; }
@media (min-width: 860px) {
  .ocr-lib-split.has-image {
    display: grid;
    grid-template-columns: minmax(220px, 300px) 1fr;
    align-items: start;
    gap: 22px;
  }
}
.ocr-lib-page-image {
  border: 1px solid var(--border-light);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-cream);
  box-shadow: var(--shadow-sm);
}
.ocr-lib-page-image img {
  display: block;
  width: 100%;
  height: auto;
}
/* The thumbnail IS the "pop the viewer" trigger -- a <button> wrapping it
   rather than an onClick on the <img>, so it stays a real, keyboard-
   reachable control (invariant #: every interactive element keyboard
   accessible) instead of a click handler nothing can Tab to. */
.ocr-lib-page-image-btn {
  display: block;
  width: 100%;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: zoom-in;
}
.ocr-lib-page-image-btn img { display: block; width: 100%; height: auto; }
.ocr-lib-page-image-ph {
  width: 100%;
  aspect-ratio: 3 / 4;
  background: linear-gradient(135deg, var(--bg-cream), var(--bg-warm));
}
.ocr-lib-split-text { min-width: 0; }

/* ── The page viewer — Phase 32-E ─────────────────────────────────────────
   Mohammed: "when i click on the page the page should pop in the viewer —
   im not talking about extracted ones, the one which has been uploaded."
   A DARK GROUND ON PURPOSE, the one deliberate break from this file's warm
   cream language: this is a lightbox for a photograph, and the photograph
   is what has to read as the subject -- a cream panel around it would
   compete with the page it is showing. */
.ocr-viewer-backdrop {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(34, 26, 18, 0.92);
  display: flex;
  /* NOT align-items:center -- the Phase 11.9 trap, restated here rather
     than relearned: centring a flex item taller than its container
     overflows it equally in both directions, so the TOP is clipped and no
     amount of scrolling reaches it. `overflow-y:auto` here plus
     `margin:auto` on `.ocr-viewer` centres it when it fits and lets it
     scroll, top-anchored, the one time it does not. */
  align-items: flex-start;
  justify-content: center;
  overflow-y: auto;
  padding: 16px;
}
.ocr-viewer {
  width: 100%;
  height: 100%;
  max-width: 1400px;
  margin: auto;
  display: flex;
  flex-direction: column;
  background: var(--brown-900);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}
.ocr-viewer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  flex-shrink: 0;
}
.ocr-viewer-count {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--gold-200);
}
.ocr-viewer-tools { display: flex; align-items: center; gap: 10px; }
.ocr-viewer-tools .k-mini {
  background: rgba(255, 255, 255, .08);
  border-color: rgba(255, 255, 255, .18);
  color: var(--gold-100);
}
.ocr-viewer-tools .k-mini:hover { background: rgba(255, 255, 255, .16); }
.ocr-viewer-tools .k-mini[aria-pressed="true"] {
  background: var(--gold-500);
  border-color: var(--gold-500);
  color: var(--brown-900);
}
.ocr-viewer-close {
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 255, 255, .18);
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, .08);
  color: var(--gold-100);
  cursor: pointer;
}
.ocr-viewer-close:hover { background: rgba(255, 255, 255, .18); }
.ocr-viewer-close svg { width: 16px; height: 16px; }

.ocr-viewer-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* "Show text" slides the extracted text in BESIDE the image on a wide
   screen and ABOVE/BELOW it on a phone -- mobile-first, so the narrow case
   costs no extra rule, exactly PageView.tsx's own image-split reasoning. */
@media (min-width: 861px) {
  .ocr-viewer-body.has-text { flex-direction: row; }
}

.ocr-viewer-image-col {
  flex: 1;
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px;
}
.ocr-viewer-nav {
  flex-shrink: 0;
  width: 42px;
  height: 42px;
  border-radius: var(--radius-full);
  border: 1px solid rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .08);
  color: var(--gold-100);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ocr-viewer-nav:hover:not(:disabled) { background: rgba(255, 255, 255, .18); }
.ocr-viewer-nav:disabled { opacity: .3; cursor: default; }

/* FIT-TO-HEIGHT BY DEFAULT: the image column is exactly `object-fit:
   contain` inside whatever room `.ocr-viewer-body` leaves it. `.is-zoomed`
   drops the cap so the image renders at its own natural size and the
   column becomes the scroller -- a toggle between two states, never a
   continuous scale, which is what click / wheel / pinch all set. */
.ocr-viewer-image {
  flex: 1;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: auto;
}
.ocr-viewer-image.is-zoomed { align-items: flex-start; justify-content: flex-start; }
.ocr-viewer-image img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--radius-sm);
  cursor: zoom-in;
}
.ocr-viewer-image.is-zoomed img {
  max-width: none;
  max-height: none;
  width: auto;
  height: auto;
  cursor: zoom-out;
}
.ocr-viewer-loading {
  width: 60%;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-md);
  background: linear-gradient(135deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .13));
}

.ocr-viewer-text {
  flex-shrink: 0;
  padding: 18px 22px;
  overflow-y: auto;
  color: var(--gold-100);
  background: rgba(0, 0, 0, .18);
  max-height: 40vh;
}
@media (min-width: 861px) {
  .ocr-viewer-text {
    max-height: none;
    width: 360px;
    border-left: 1px solid rgba(255, 255, 255, .1);
  }
}
