/* THE REEL FEED — the surface, shared by every family.
 *
 * One stylesheet for every reel feed: TNPSC, CTET and whatever comes next all
 * render the same cards through reel-feed.js. Nothing exam-specific belongs
 * here — the exam's name, its subject labels and its links arrive as data
 * (data/reel-families.json), which is what stops a second feed from becoming a
 * second stylesheet that drifts from this one.
 *
 * The block marked STAGE below is lifted from explainer-journey-studio.html and
 * must stay in step with it: journey-engine.js draws into these exact ids and
 * every screen-space number in the engine assumes them. It is duplicated rather
 * than shared because converging the two means editing the studio, and the studio
 * is the input to a live film render — that convergence is worth doing, on a day
 * when nothing is mid-render. Until then: CHANGE BOTH.
 *
 * The one deliberate difference is the fit. The studio CONTAINS the 1080x1920
 * stage in the window (min of the two ratios, letterboxed, so a director can see
 * the whole frame). A feed reel must COVER the viewport edge to edge with no bars,
 * so reel-feed.js overrides __fit() to use max() and centres the overflow.
 */

:root {
  --bgr-ink: #05070f;
  --bgr-gold: #fac410;
  --bgr-rail: 62px;
}

* { box-sizing: border-box; margin: 0; padding: 0 }
html, body {
  background: var(--bgr-ink); color: #fff; height: 100%;
  font-family: "Noto Sans Tamil", "Nirmala UI", "Latha", "Segoe UI", system-ui, sans-serif;
  overscroll-behavior-y: none;         /* no pull-to-refresh mid-reel */
}

/* ============================ STAGE (mirror of the studio) ============================ */
#stage { position: relative; width: 1080px; height: 1920px; overflow: hidden; background: #150c36 }
body.fit #stage { transform: scale(var(--fit, .42)); transform-origin: 0 0 }
#world { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform }
.obj { position: absolute }
.obj svg { display: block; width: 100%; height: auto }
#vessels { position: absolute; left: 0; top: 0 }

#labels { position: absolute; inset: 0; pointer-events: none }
#leads { position: absolute; inset: 0 }
.chip {
  position: absolute; left: 0; top: 0; display: inline-block; padding: 10px 22px; border-radius: 14px;
  font-weight: 800; font-size: 34px; line-height: 1.15; white-space: nowrap;
  box-shadow: 0 8px 0 rgba(0, 0, 0, .22); opacity: 0; will-change: transform;
}
.chip.sm { font-size: 29px; padding: 8px 18px }

#hud { position: absolute; inset: 0; pointer-events: none }
#keyscrim {
  position: absolute; left: 0; right: 0; top: 940px; height: 560px;
  background: linear-gradient(180deg, transparent, rgba(6,8,20,.58) 34%, rgba(6,8,20,.58) 78%, transparent);
}
/* The keyline lives inside the SAFE part of a cover-fit stage. --bgr-crop is how
   much of each side the viewport is cutting off and --bgr-rail-stage is the width
   of the action rail, both measured in stage pixels by reel-feed.js. Unset (the
   studio, an MP4 render) they are 0 and this is the original 70/70 caption box.
   The right inset is the larger of the two on purpose: the text shifts left of
   centre rather than running under the ♥. */
#key {
  /* Anchored by its BASELINE, not its top. A three-line Tamil or Hindi keyline
     is taller than the two-line English one it was designed around, and growing
     downward walked it straight into the caption block (measured: key ended at
     y648, the caption began at y600). Growing upward spends the artwork instead,
     which is what the artwork is there for. */
  position: absolute; text-align: center; top: auto; bottom: 580px;
  left: calc(70px + var(--bgr-crop, 0px));
  right: calc(70px + var(--bgr-crop, 0px) + var(--bgr-rail-stage, 0px));
  font-size: 62px; font-weight: 800; line-height: 1.24; text-shadow: 0 6px 26px rgba(0,0,0,.55);
}
#key b { color: var(--bgr-gold) }
#key .l2 { display: block; font-size: 46px; opacity: .92; margin-top: 10px }
#cite {
  position: absolute; left: 60px; bottom: 356px; max-width: 620px; font-size: 23px; font-weight: 700;
  letter-spacing: .02em; opacity: 0; text-align: left;
  background: rgba(6,8,20,.55); padding: 7px 14px; border-radius: 10px;
}
#brand {
  position: absolute; left: 56px; top: 64px; display: flex; align-items: center; gap: 13px; opacity: 0;
  background: rgba(6,8,20,.74); padding: 10px 18px 10px 12px; border-radius: 16px;
}
#brand i { display: block; width: 32px; height: 32px; border-radius: 10px; background: var(--bgr-gold); flex: none }
#brand b { font-size: 25px; font-weight: 800; letter-spacing: .04em; display: block; line-height: 1.1 }
#brand u { font-size: 19px; font-weight: 700; opacity: .8; text-decoration: none; display: block; letter-spacing: .02em }
#mascot { position: absolute; width: 190px; right: 176px; bottom: 352px; opacity: 0 }
#mascot svg { width: 100%; height: auto; filter: drop-shadow(0 10px 24px rgba(0,0,0,.45)) }
#bloom { position: absolute; inset: 0; opacity: 0 }
#vig { position: absolute; inset: 0; opacity: .5; background: radial-gradient(ellipse at 50% 45%, transparent 46%, rgba(0,0,0,.55) 100%) }
#stats {
  position: absolute; left: 46px; top: 300px; width: 346px; opacity: 0;
  background: rgba(6,8,20,.72); border-radius: 26px; padding: 26px 24px 28px;
}
#stats div { display: inline-block; font-weight: 800; border-radius: 16px; padding: 11px 20px; font-size: 44px; line-height: 1.1; white-space: nowrap; margin-bottom: 12px }
#stats .c { margin-bottom: 0; font-size: 33px; white-space: normal }
#stats .rule { display: block; height: 5px; border-radius: 99px; background: rgba(255,255,255,.30); margin: 2px 0 16px; padding: 0; width: auto }

/* The engine's own end card is suppressed in the feed: the feed draws its own,
   because here it is interactive (an optional question) rather than a still. */
#end { display: none !important }
#cover { display: none !important }

/* ============================ the player ============================ */
#player {
  position: fixed; inset: 0; overflow: hidden; z-index: 0;
  background: #150c36; display: flex; align-items: center; justify-content: center;
}
#player .holder { position: absolute; left: 0; top: 0 }
/* while the feed is between two reels the player dims, so a half-scrolled card
   never shows the previous topic's art under the next topic's title */
#player.between { opacity: .28; transition: opacity .18s ease }
/* on a checkpoint there is no reel to show: the stage clears rather than freezing
   on the last frame of a topic the learner has already finished */
#player.hidden { opacity: 0; transition: opacity .22s ease }

/* ============================ the scroll stack ============================ */
#feed {
  position: relative; z-index: 1; height: 100%; overflow-y: auto;
  scroll-snap-type: y mandatory; -webkit-overflow-scrolling: touch;
}
.card {
  position: relative; height: 100%; scroll-snap-align: start; scroll-snap-stop: always;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 0 calc(var(--bgr-rail) + 14px) calc(26px + var(--bg-bottom-inset, 0px)) 16px;
}
/* the card itself must not eat taps meant for the player (pause/resume); only its
   controls do */
.card > * { pointer-events: auto }
.card { pointer-events: none }

/* progress: one segment per beat, story-style */
/* Sits just under the real chrome, whatever height that is in this lane — the
   old constant put it 46px below a 56px guess and landed it inside the filter
   row the day a third row of chrome appeared. */
.prog { position: absolute; left: 12px; right: 12px; top: calc(var(--bgr-chrome, 104px) + 8px); display: flex; gap: 4px; height: 3px }
.prog i { flex: 1; background: rgba(255,255,255,.28); border-radius: 99px; overflow: hidden }
.prog i span { display: block; height: 100%; width: 0; background: #fff; border-radius: 99px }

/* the caption block the feed owns (topic + title), bottom-left */
.cmeta { max-width: 100%; text-shadow: 0 2px 14px rgba(0,0,0,.6) }
.cmeta .topic {
  display: inline-block; background: rgba(147,51,234,.92); color: #fff; font-weight: 800;
  font-size: .68rem; letter-spacing: .04em; padding: 4px 10px; border-radius: 8px; margin-bottom: 8px;
}
.cmeta .ttl { font-size: 1.12rem; font-weight: 800; line-height: 1.3 }
.cmeta .sub { font-size: .8rem; font-weight: 700; opacity: .8; margin-top: 4px }

/* ============================ the action rail ============================ */
/* Five buttons, not four, since the ⋯ sheet landed here. The rail was re-tightened
   to keep its TOP where four buttons put it (~520px at 844) — the engine draws the
   keyline across the full stage width and does not know the rail exists, so a taller
   rail walks straight up into the sentence the reel is built around. */
.rail {
  position: absolute; right: 6px; bottom: 80px; width: var(--bgr-rail);
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.rail button {
  width: 40px; height: 40px; border-radius: 50%; border: 0; cursor: pointer;
  background: rgba(8,10,24,.62); color: #fff; font-size: 1.04rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  -webkit-tap-highlight-color: transparent;
}
.rail button:active { transform: scale(.92) }
.rail b { display: block; font-size: .62rem; font-weight: 800; opacity: .85; margin-top: -10px }
.rail .on { background: var(--bgr-gold); color: #141018 }
.rail .on.like { background: #ed3945; color: #fff }

/* ============================ the end question ============================ */
.qwrap {
  position: absolute; inset: 0 0 var(--bg-bottom-inset, 0px) 0; background: rgba(5,7,15,.90); z-index: 3;
  display: none; flex-direction: column; justify-content: center; gap: 12px; padding: 18px 20px;
  overflow-y: auto;
}
.card.asking .qwrap { display: flex }
.qwrap .qskip { position: absolute; top: 14px; right: 14px; background: none; border: 0; color: rgba(255,255,255,.6); font-size: .78rem; font-weight: 800; cursor: pointer }
.qwrap .qq { font-size: 1.02rem; font-weight: 800; line-height: 1.4 }
.qwrap .qopt {
  display: flex; gap: 10px; align-items: flex-start; width: 100%; text-align: left; cursor: pointer;
  background: rgba(255,255,255,.07); border: 2px solid rgba(255,255,255,.16); color: #fff;
  border-radius: 12px; padding: 11px 13px; font-size: .88rem; font-weight: 700; line-height: 1.34;
  font-family: inherit;
}
.qwrap .qopt u { text-decoration: none; font-weight: 800; opacity: .6; flex: none }
.qwrap .qopt.right { border-color: #22c55e; background: rgba(34,197,94,.18) }
.qwrap .qopt.wrong { border-color: #ed3945; background: rgba(237,57,69,.18) }
.qwrap .qwhy { font-size: .82rem; line-height: 1.5; opacity: .9; background: rgba(255,255,255,.07); border-radius: 12px; padding: 11px 13px; display: none }
.qwrap.done .qwhy { display: block }
.qwrap .qnext { align-self: flex-start; background: var(--bgr-gold); color: #141018; border: 0; border-radius: 12px; padding: 11px 20px; font-weight: 800; font-size: .88rem; cursor: pointer; display: none; font-family: inherit }
.qwrap.done .qnext { display: block }

/* ============================ the checkpoint card ============================
   The one card in the stack that is not a reel. It reads as a pause in the feed
   rather than as a page: same snap, same scroll, its own solid ground so the
   dimmed player behind it never competes with the question text.

   It scrolls INSIDE itself. Six questions with four bilingual options each will
   not fit on a 390x844 phone, and the arena already learned what happens when a
   list is silently taller than the screen — so the card owns its own overflow
   and the padding at the foot guarantees the last option clears the bottom. */
.chk { justify-content: stretch; padding: 0 }
.chkin {
  height: calc(100% - var(--bg-bottom-inset, 0px)); overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: linear-gradient(180deg, #10142a 0%, #070a17 100%);
  padding: calc(var(--bgr-chrome, 104px) + 18px) 18px calc(env(safe-area-inset-bottom, 0px) + 30px);
}
.chkhead { text-align: center; margin-bottom: 14px }
.chkkick {
  display: inline-block; background: var(--bgr-gold); color: #141018; font-weight: 800;
  font-size: .66rem; letter-spacing: .08em; text-transform: uppercase;
  padding: 4px 12px; border-radius: 99px;
}
.chkhead h2 { font-size: 1.16rem; font-weight: 800; line-height: 1.3; margin: 10px 0 0 }
.chkhead p { font-size: .84rem; line-height: 1.5; opacity: .78; margin: 6px 0 0 }
.chkscore {
  text-align: center; font-size: .76rem; font-weight: 800; letter-spacing: .02em;
  background: rgba(255,255,255,.09); border-radius: 99px; padding: 7px 14px;
  margin: 0 auto 16px; width: -webkit-fit-content; width: fit-content;
}
.chkscore[data-done="1"] { background: var(--bgr-gold); color: #141018 }

.chkq {
  background: rgba(255,255,255,.055); border: 1px solid rgba(255,255,255,.1);
  border-radius: 16px; padding: 14px; margin-bottom: 12px;
}
.chkq.ok { border-color: rgba(34,197,94,.55) }
.chkq.no { border-color: rgba(237,57,69,.55) }
.chkqq { font-size: .95rem; font-weight: 800; line-height: 1.45; display: flex; gap: 9px }
.chkqq i {
  flex: none; font-style: normal; width: 21px; height: 21px; border-radius: 50%;
  background: rgba(255,255,255,.16); font-size: .7rem; font-weight: 800;
  display: flex; align-items: center; justify-content: center; margin-top: 2px;
}
.chkopt {
  display: flex; gap: 9px; align-items: flex-start; width: 100%; text-align: left;
  margin-top: 8px; background: rgba(255,255,255,.06); color: #fff; font-family: inherit;
  border: 1px solid rgba(255,255,255,.14); border-radius: 12px; padding: 10px 12px;
  font-size: .86rem; font-weight: 600; line-height: 1.4; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.chkopt u { text-decoration: none; font-weight: 800; opacity: .6; flex: none }
.chkopt[disabled] { cursor: default }
.chkopt.right { border-color: #22c55e; background: rgba(34,197,94,.18) }
.chkopt.wrong { border-color: #ed3945; background: rgba(237,57,69,.18) }
/* The explanation is the point of getting it wrong, so it only exists after an
   answer — shown before, it is just the answer key sitting next to the question. */
.chkwhy {
  display: none; font-size: .8rem; line-height: 1.5; opacity: .9;
  background: rgba(255,255,255,.07); border-radius: 11px; padding: 10px 12px; margin-top: 9px;
}
.chkq.done .chkwhy { display: block }
.chknext {
  display: block; width: 100%; margin-top: 4px; background: var(--bgr-gold); color: #141018;
  border: 0; border-radius: 13px; padding: 13px 20px; font-weight: 800; font-size: .9rem;
  cursor: pointer; font-family: inherit;
}

/* ============================ the picture card ============================
   The second texture in the feed: twenty figure questions where the picture IS the
   question. Same snap, same scroll, its own opaque ground — the figures are drawn on
   cream paper and would fight the reel artwork behind them.

   It scrolls INSIDE itself for the same reason the checkpoint does: a 640x420 chart
   plus a stem plus four options does not fit a 390x844 phone, and a list silently
   taller than the screen is a bug this repo has already paid for. */
.fig { justify-content: stretch; padding: 0 }
.figin {
  height: calc(100% - var(--bg-bottom-inset, 0px)); overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: linear-gradient(180deg, #141029 0%, #070a17 100%);
  padding: calc(var(--bgr-chrome, 104px) + 18px) 16px calc(env(safe-area-inset-bottom, 0px) + 30px);
}
.fighead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px }
.figkick {
  display: inline-block; background: #38bdf8; color: #06121c; font-weight: 800;
  font-size: .66rem; letter-spacing: .04em; padding: 4px 11px; border-radius: 99px;
}
.figtopic { font-size: .72rem; font-weight: 800; opacity: .74; letter-spacing: .02em }
/* The frame keeps the figure's own paper. An SVG of a chart inverted onto a dark
   card loses its printed values, and those values are what the question is asked
   about. */
.figframe {
  background: #fbf8f2; border-radius: 14px; padding: 8px; overflow: hidden;
  box-shadow: 0 10px 30px rgba(0,0,0,.4);
}
.figframe img { display: block; width: 100%; height: auto; border-radius: 8px }
.figcap { font-size: .68rem; font-weight: 700; opacity: .7; margin-top: 7px; line-height: 1.4 }
.figq { font-size: .98rem; font-weight: 800; line-height: 1.45; margin: 14px 0 4px }
.figopt {
  display: flex; gap: 9px; align-items: flex-start; width: 100%; text-align: left;
  margin-top: 8px; background: rgba(255,255,255,.06); color: #fff; font-family: inherit;
  border: 1px solid rgba(255,255,255,.14); border-radius: 12px; padding: 11px 12px;
  font-size: .88rem; font-weight: 600; line-height: 1.4; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.figopt u { text-decoration: none; font-weight: 800; opacity: .6; flex: none }
.figopt[disabled] { cursor: default }
.figopt.right { border-color: #22c55e; background: rgba(34,197,94,.18) }
.figopt.wrong { border-color: #ed3945; background: rgba(237,57,69,.18) }
/* the explanation is the point of getting it wrong — before an answer it is just
   the answer key sitting under the question */
.figwhy {
  display: none; font-size: .82rem; line-height: 1.55; opacity: .92;
  background: rgba(255,255,255,.07); border-radius: 12px; padding: 11px 13px; margin-top: 10px;
}
.fig.done .figwhy { display: block }
.fignext {
  display: block; width: 100%; margin-top: 12px; background: var(--bgr-gold); color: #141018;
  border: 0; border-radius: 13px; padding: 13px 20px; font-weight: 800; font-size: .9rem;
  cursor: pointer; font-family: inherit;
}

/* ============================ the question card ============================
   The drill and the mock ride this one card. It is the picture card's twin by
   design — opaque ground, its own scroll, options that clear the bottom inset —
   because a learner should not have to re-learn what a question looks like when
   they change lane.

   THE TOP PADDING IS MEASURED, not guessed: --bgr-chrome is written by
   measureChrome() from the real height of #top, which is one row taller in the
   lanes that carry a subject filter. The old constant was right for exactly one
   chrome shape and silently hid the top of the stem in the others. */
.qb { justify-content: stretch; padding: 0 }
.qbin {
  height: calc(100% - var(--bg-bottom-inset, 0px)); overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: linear-gradient(180deg, #101a2e 0%, #070a17 100%);
  padding: calc(var(--bgr-chrome, 104px) + 18px) 16px calc(env(safe-area-inset-bottom, 0px) + 30px);
}
.qbhead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px }
.qbkick {
  display: inline-block; background: var(--bgr-gold); color: #141018; font-weight: 800;
  font-size: .66rem; letter-spacing: .06em; text-transform: uppercase;
  padding: 4px 11px; border-radius: 99px;
}
.qbtopic { font-size: .72rem; font-weight: 800; opacity: .74; letter-spacing: .02em }
/* The syllabus line is the orientation a question pulled out of a bank needs and
   the largest thing it is allowed to be: bigger than this it competes with the
   stem for the eye that is supposed to be reading the stem. */
.qbsyl { font-size: .74rem; font-weight: 700; opacity: .6; margin-bottom: 6px }
.qbq { font-size: 1.02rem; font-weight: 800; line-height: 1.45; margin: 4px 0 6px }
.qbopt {
  display: flex; gap: 9px; align-items: flex-start; width: 100%; text-align: left;
  margin-top: 8px; background: rgba(255,255,255,.06); color: #fff; font-family: inherit;
  border: 1px solid rgba(255,255,255,.14); border-radius: 12px; padding: 11px 12px;
  font-size: .88rem; font-weight: 600; line-height: 1.4; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.qbopt u { text-decoration: none; font-weight: 800; opacity: .6; flex: none }
.qbopt[disabled] { cursor: default }
.qbopt.right { border-color: #22c55e; background: rgba(34,197,94,.18) }
.qbopt.wrong { border-color: #ed3945; background: rgba(237,57,69,.18) }
/* the explanation is the point of getting it wrong — before an answer it is just
   the answer key sitting under the question */
.qbwhy {
  display: none; font-size: .82rem; line-height: 1.55; opacity: .92;
  background: rgba(255,255,255,.07); border-radius: 12px; padding: 11px 13px; margin-top: 10px;
}
.qb.done .qbwhy { display: block }
.qbnext {
  display: none; width: 100%; margin-top: 12px; background: var(--bgr-gold); color: #141018;
  border: 0; border-radius: 13px; padding: 13px 20px; font-weight: 800; font-size: .9rem;
  cursor: pointer; font-family: inherit;
}
.qb.done .qbnext { display: block }

/* ============================ the hand-off card ============================
   What replaced the dead end. It closes every lane, and the mock's result card
   is the same object with a score on top — the two were separate layouts for
   one job in the first draft, which is exactly the duplication the engine was
   unified to stop. */
.endc { justify-content: stretch; padding: 0 }
.endin {
  height: calc(100% - var(--bg-bottom-inset, 0px)); overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: linear-gradient(180deg, #131a33 0%, #070a17 100%);
  padding: calc(var(--bgr-chrome, 104px) + 22px) 20px calc(env(safe-area-inset-bottom, 0px) + 30px);
  text-align: center;
}
.endkick { display: inline-block; font-size: 2rem; line-height: 1 }
.endh { font-size: 1.2rem; font-weight: 800; line-height: 1.3; margin: 12px 0 0 }
.endbig { font-size: 3rem; font-weight: 900; line-height: 1; margin: 10px 0 0; color: var(--bgr-gold) }
.endbig small { font-size: 1.3rem; font-weight: 800; opacity: .6 }
.endbar {
  display: block; height: 7px; border-radius: 99px; background: rgba(255,255,255,.16);
  overflow: hidden; margin: 14px auto 0; max-width: 240px;
}
.endbar i { display: block; height: 100%; width: 0; border-radius: 99px; background: var(--bgr-gold) }
.endsub { font-size: .85rem; line-height: 1.5; opacity: .8; margin: 10px 0 0 }
.endgo { display: flex; flex-direction: column; gap: 9px; margin-top: 20px; text-align: left }
.endbtn {
  display: block; width: 100%; background: rgba(255,255,255,.08); color: #fff; text-decoration: none;
  border: 1px solid rgba(255,255,255,.14); border-radius: 13px; padding: 13px 15px;
  font-weight: 800; font-size: .88rem; cursor: pointer; font-family: inherit; text-align: left;
}
/* The other lanes are the primary offer — they are one tap and stay inside the
   feed — so they carry the accent and the family's pages sit quieter beneath. */
.endbtn.go { background: var(--bgr-gold); color: #141018; border-color: transparent }
.endbtn.quiet { background: none; border-color: rgba(255,255,255,.16); opacity: .75; text-align: center }

/* the centred single-message card: all-seen, empty filter, loading, bank failure */
.card.note {
  pointer-events: auto; justify-content: center; text-align: center; padding: 0 28px;
}

/* ============================ the double-tap heart ============================ */
#burst {
  position: fixed; z-index: 8; margin: -26px 0 0 -22px; font-size: 2.6rem; color: #ed3945;
  opacity: 0; pointer-events: none; text-shadow: 0 6px 22px rgba(0,0,0,.5);
}
#burst.pop { animation: bgrpop .72s ease-out forwards }
@keyframes bgrpop {
  0%   { opacity: 0; transform: scale(.4) }
  22%  { opacity: 1; transform: scale(1.22) }
  46%  { opacity: 1; transform: scale(1) }
  100% { opacity: 0; transform: scale(1.5) translateY(-38px) }
}

/* ============================ the first-visit hint ============================ */
/* It sits clear of the caption block, not over it: the caption is the reel's title
   and the hint arriving on top of it hides the one thing that says what is playing. */
#hint {
  position: absolute; left: 50%; top: calc(var(--bgr-top, 56px) + 96px); bottom: auto;
  transform: translateX(-50%); z-index: 6; display: flex; align-items: center; gap: 8px;
  background: rgba(8,10,24,.82); border-radius: 99px; padding: 8px 15px;
  font-size: .78rem; font-weight: 800; pointer-events: none; transition: opacity .3s ease;
  /* left:50% caps an absolutely positioned box's shrink-to-fit width at HALF the
     card, which wrapped six words onto two lines. The transform above re-centres it,
     so it is free to be as wide as its one line needs. */
  white-space: nowrap;
}
#hint i { font-style: normal; font-size: .72rem; animation: bgrup 1.5s ease-in-out infinite }
#hint.gone { opacity: 0 }
@keyframes bgrup {
  0%, 100% { transform: translateY(2px); opacity: .5 }
  50%      { transform: translateY(-3px); opacity: 1 }
}

/* ============================ the sheet ============================
   Where the rest of TNPSC lives now that a TNPSC tap lands on the feed. A sheet, not
   a nav bar: the bottom of the screen belongs to the reel. */
#sheet {
  position: absolute; inset: 0; z-index: 7; background: rgba(4,6,14,.62);
  display: none; align-items: flex-end;
}
#sheet.open { display: flex }
#sheet .sh {
  width: 100%; max-height: 82%; overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: #10142a; border-radius: 20px 20px 0 0;
  padding: 10px 16px calc(env(safe-area-inset-bottom, 0px) + var(--bg-bottom-inset, 0px) + 18px);
  animation: bgrsheet .22s ease-out;
}
@keyframes bgrsheet { from { transform: translateY(26px) } to { transform: translateY(0) } }
#sheet .shbar { width: 42px; height: 4px; border-radius: 99px; background: rgba(255,255,255,.3); margin: 0 auto 12px }
#sheet h2 { font-size: .96rem; font-weight: 800; margin-bottom: 10px }
#sheet a {
  display: block; text-decoration: none; color: #fff; background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,255,255,.11); border-radius: 13px; padding: 11px 13px; margin-bottom: 8px;
}
#sheet a b { display: block; font-size: .88rem; font-weight: 800 }
#sheet a small { display: block; font-size: .74rem; opacity: .72; margin-top: 2px; line-height: 1.4 }
#sheet button {
  width: 100%; background: rgba(255,255,255,.14); color: #fff; border: 0; border-radius: 13px;
  padding: 12px; font-weight: 800; font-size: .86rem; cursor: pointer; font-family: inherit; margin-top: 4px;
}

/* ============================ top chrome: filters ============================ */
#top {
  position: fixed; left: 0; right: 0; top: 0; z-index: 5;
  padding: calc(env(safe-area-inset-top, 0px) + 8px) 10px 8px;
  background: linear-gradient(180deg, rgba(5,7,15,.92) 0%, rgba(5,7,15,.72) 62%, rgba(5,7,15,0) 100%);
}
/* A checkpoint is the one card that SCROLLS ITS OWN CONTENT under this chrome.
   Behind a reel the gradient sits on opaque artwork and reads as a scrim; behind
   a scrolling question it let option text run straight through the filter pills
   and the coverage bar. So while a checkpoint is the active card the chrome goes
   solid — a scrim is only a scrim when there is something opaque beneath it. */
#top.solid {
  background: #0a0d1a;
  border-bottom: 1px solid rgba(255,255,255,.09);
}
/* While a reel plays the chrome steps back — a coverage bar and a filter row pinned
   over a playing reel is the difference between a feed and a widget in a page. It
   returns on any tap or scroll, and never dims on a question card (see .solid). */
#top { transition: opacity .35s ease }
#top.dim { opacity: 0; pointer-events: none }
#top .row1 { display: flex; align-items: center; gap: 8px }
#top .home { color: #fff; text-decoration: none; font-weight: 800; font-size: 1.05rem; flex: none; padding: 2px 4px }
#top .cov { flex: 1; min-width: 0; padding-right: 6px }
#top .cov .bar { height: 5px; border-radius: 99px; background: rgba(255,255,255,.2); overflow: hidden }
#top .cov .bar span { display: block; height: 100%; width: 0; background: var(--bgr-gold); border-radius: 99px; transition: width .3s ease }
/* One line, truncated on purpose. A long persona name (a Tamil exam label runs
   half again the width of its English twin) used to run right up to the sound
   button with no gap; the padding on .cov holds that gap open and the ellipsis
   gives the line somewhere to end that is not the button. */
#top .cov .lab { font-size: .64rem; font-weight: 800; opacity: .82; margin-top: 3px; letter-spacing: .02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
#top .lang { flex: none; background: rgba(255,255,255,.14); color: #fff; border: 0; border-radius: 9px; padding: 6px 10px; font-weight: 800; font-size: .72rem; cursor: pointer; font-family: inherit }
/* A family with one language has nothing to switch to. Unlike the sound chip
   this is display:none rather than hidden: it is decided once at boot and never
   flickers, so reserving its width would leave a hole in the row forever. */
#top .lang.gone { display: none }

/* The sound chip sits next to the language chip and is the same object at the
   same size — muting and switching language are the same class of control. It is
   square because it holds a glyph, not a word. `gone` (not display:none) keeps
   the row from reflowing on every mount: a chip that appears and disappears as
   narrated and un-narrated reels scroll past would shove the language button
   sideways under the reader's thumb. */
#top .snd {
  flex: none; background: rgba(255,255,255,.14); color: #fff; border: 0; border-radius: 9px;
  width: 30px; height: 28px; padding: 0; font-size: .78rem; line-height: 1; cursor: pointer;
  font-family: inherit; margin-right: 6px;
}
#top .snd.off { background: rgba(255,255,255,.07); opacity: .65 }
#top .snd.gone { visibility: hidden }

/* ============================ the mode row ============================
   Mode and subject are two different axes, so they get two different controls:
   a fixed segmented control for the lane you are in, a scrolling pill row for
   what you are filtering it by. Folding both into one row was tried and is how
   you get a learner reading nine pills to find out which two do different jobs.

   It is full-width and equal-thirds rather than scrollable: three items that
   never change do not need to scroll, and a segmented control that can hide one
   of its segments off the right edge is a segmented control with a bug in it. */
#modes { display: flex; gap: 5px; margin-top: 8px }
#modes.gone { display: none }
#modes button {
  flex: 1; min-width: 0; background: rgba(255,255,255,.10); color: #fff; border: 0;
  border-radius: 10px; padding: 7px 6px; font-size: .74rem; font-weight: 800;
  cursor: pointer; font-family: inherit; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
#modes button.on { background: #fff; color: #141018 }

/* The rail scrolls, but at 390px it always cuts the last chip mid-glyph and a
   hard cut reads as truncation, not as "there is more to the right". The mask
   fades the final 26px so the cut is a peek; unsupported on the oldest
   WebViews, where it degrades to today's hard edge. */
#filters { display: flex; gap: 6px; overflow-x: auto; margin-top: 8px; padding-bottom: 2px; padding-right: 4px; scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 26px), rgba(0,0,0,0));
  mask-image: linear-gradient(to right, #000 calc(100% - 26px), rgba(0,0,0,0)) }
/* A mock is a mixed set by definition, so it carries no subject row — and the
   row is removed rather than emptied so the chrome gives its height back to the
   card underneath. */
#filters.gone { display: none }
#filters::-webkit-scrollbar { display: none }
#filters button {
  flex: none; background: rgba(255,255,255,.12); color: #fff; border: 0; border-radius: 99px;
  padding: 6px 12px; font-size: .72rem; font-weight: 800; cursor: pointer; white-space: nowrap; font-family: inherit;
}
#filters button.on { background: var(--bgr-gold); color: #141018 }

/* The phone default comes FIRST, so the desktop block below can override it.
   Sitting after the media query it won at equal specificity on source order, which
   pinned the shell to 0,0 on a wide screen: body's flex centring cannot move a
   fixed element, so the column sat flush left with 860px of black beside it while
   the rounded corners and shadow it was still being given implied otherwise. */
#shell { position: fixed; inset: 0 }

/* ============================ desktop ============================
   A feed is a phone shape. On a wide screen it is centred in a phone-width column
   rather than stretched — a 1080x1920 reel scaled to a 1440px-wide window would be
   a 2500px-tall stage with the caption off both ends of the screen. */
@media (min-width: 620px) {
  body { display: flex; align-items: center; justify-content: center; background: #0a0d1a }
  #shell {
    position: relative; width: 420px; height: min(92vh, 900px); max-height: 900px;
    border-radius: 22px; overflow: hidden; box-shadow: 0 24px 70px rgba(0,0,0,.6);
  }
  #player { position: absolute }
  #top { position: absolute }
}

/* the toast that confirms a share/save without stealing the screen */
#toast {
  position: fixed; left: 50%; bottom: 78px; transform: translateX(-50%) translateY(10px); z-index: 9;
  background: rgba(8,10,24,.94); color: #fff; font-size: .78rem; font-weight: 800;
  padding: 9px 16px; border-radius: 99px; opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0) }

/* ============================ the XP burst ============================
   The number flying out of the coverage bar when a reel is finished or an answer
   is right. It is deliberately part of the TOP chrome — the bar it animates out
   of is the same bar it just moved — rather than a floating toast somewhere else
   on screen, which reads as two unrelated events.

   The engine's own bottom XP strip is suppressed here: this page is a full-screen
   player and a second, permanent bar of chrome across the artwork is exactly what
   the feed was built to get rid of. The coverage bar IS this page's progress
   display; /my-progress is the long version, one tap away in the ⋯ sheet. */
.rlxp {
  position: absolute; right: 12px; top: calc(env(safe-area-inset-top, 0px) + 34px);
  font-weight: 900; font-size: .95rem; color: #4ade80; pointer-events: none;
  text-shadow: 0 2px 10px rgba(0,0,0,.5); animation: rlxpFly 1.35s ease-out forwards;
}
@keyframes rlxpFly {
  0%   { opacity: 0; transform: translateY(6px) scale(.9) }
  16%  { opacity: 1; transform: translateY(0) scale(1.14) }
  70%  { opacity: 1; transform: translateY(-12px) scale(1) }
  100% { opacity: 0; transform: translateY(-30px) scale(.94) }
}
.xp-hud { display: none !important }
