/* Handloom Quiz app styles. Vishwakarma Gallery, 32" landscape 1920x1080.
   Framework (kiosk.css) supplies fonts, colours, .btn, .screen, .osk, #saver. */

/* ---------- ghost-tap guard ----------
   kiosk.css sets a blanket `button, .touchable { pointer-events: auto }`, which
   beats the `pointer-events: none` that .screen carries while it is inactive.
   Result: every button on a hidden screen stays hit-testable and steals taps.
   The centre of quiz option C, for instance, sits on top of #thanks-done and
   would throw the visitor back to the screensaver mid-game. Re-assert the block
   here with higher specificity. Remove once kiosk.css scopes its rule. */
.screen:not(.active) button,
.screen:not(.active) .touchable,
.screen:not(.active) [data-k],
.screen:not(.active) [data-act] { pointer-events: none; }
#saver.hidden button, #saver.hidden .touchable { pointer-events: none; }

/* ---------- arriving-screen guard ----------
   A screen the visitor did not ask for at that instant (results, after the last
   answer; the thank-you card, after Send) can land under a finger that is
   already coming down. The centre of quiz option C sits exactly on "Play again"
   and the e/b keys sit on "Done", so a carried-over tap would wipe a finished
   score or bounce the visitor to the attract loop. Deaden the new screen's
   controls briefly and dim them, so the pause reads as the screen settling in
   rather than as a tap that did nothing. */
.screen.arriving .btn,
.screen.arriving .key,
.screen.arriving .lb-tab,
.screen.arriving [data-act] { pointer-events: none; opacity: .45; }
.screen .btn, .screen .key, .screen .lb-tab { transition: opacity .3s ease; }

.exit-x { position: absolute; top: 1em; right: 1.2em; z-index: 40; }

/* ---------- Home pill clearance ----------
   kiosk.js pins its "⌂ Home" pill top-right (x 1748-1898 at 1920x1080,
   measured). It is the one control visitors learn once across all seven
   gallery stations, so it stays where the framework puts it and this app's own
   top-right controls move clear instead. There is only one left: #email's
   "✕ Cancel" (.exit-x), which sat under the pill. Cancel is a step BACK to the
   results, so it belongs at the top-LEFT position the framework uses for back
   controls. See the language pill note below for where it finally landed,
   since the top-left is now shared.
   The quiz screen's own "✕ Exit" is gone: a screen that already carries the
   Home pill does not get a second way out. */

/* ---------- settings cluster clearance ----------
   kiosk.js pins a settings cluster top-LEFT on every screen including the
   screensaver: the "English | हिंदी" toggle and, since the type-size control
   was added, a second pill beside it. The cluster is about 427px wide where
   the language pill alone used to be about 209px, so a 12em reservation that
   used to clear it no longer does, in either language.

   Two of this app's controls sat under it and are now measured off the number
   the framework publishes and keeps current on boot, after the Devanagari
   faces load, and on every language or type-size change. Never hardcode it.
   - #quiz's progress counter has left the top-left corner altogether. It is
     now the eyebrow over the question in the right-hand column, which is both
     where a reader looks for it and clear of the cluster by construction.
   - #email's "✕ Cancel" had already been moved from top-right to top-left to
     escape the Home pill, which landed it on the language pill and then under
     the whole cluster. Park it immediately to the RIGHT of the cluster, so the
     top-left still reads as one control row. */
#email-cancel { right: auto; left: var(--kiosk-chrome-clear, 12.5em); }
/* The name and certificate screens carry the same kind of control and hit the
   same wall. Measured: #cert-close landed at x 1732-1892, y 23-87, directly
   under the framework's Home pill, and elementFromPoint at its centre returned
   the pill. A JS click worked and a real finger did not, which is the same
   shape as every other defect found today: the mechanism was fine and the
   person could not reach it. Both are a step BACK, so they belong beside the
   Cancel that already moved. */
#cert-close,
#name-cancel { right: auto; left: var(--kiosk-chrome-clear, 12.5em); }

/* ---------- attract screen ----------
   The empty-bank message stands in for the touch invitation, so only one of the
   two is ever in the flow. display:none rather than visibility, because a
   reserved gap under the title would read as something failing to load. */
#saver .hidden { display: none; }
#saver .notready { font-family: var(--serif); font-size: max(30px, 1.2em);
                   color: var(--ink-dim); margin-top: 1.5em; }

/* ---------- entry screens (email / thanks) ---------- */
.entry-wrap { flex: 1; display: flex; flex-direction: column; align-items: center;
              justify-content: center; gap: .9em; padding: 1.5em; text-align: center; }
.crest { width: 110px; height: auto; opacity: .9; }
.entry-display { min-width: 560px; max-width: 900px; min-height: 74px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 2em; letter-spacing: .12em; color: var(--ink);
  border-bottom: 2px solid var(--gold); padding: .1em .5em; }
.entry-display .ph { color: var(--ink-faint); }
.entry-display.email { font-size: 1.5em; letter-spacing: .04em; }
.ehint { min-height: 1.5em; color: #d97a63; font-size: 1.05em; }

/* ---------- privacy notice (email screen) ----------
   Participant records are kept for the run of the exhibition, so the visitor
   is told what is taken, what it is for, how long it is held and by whom.
   It sits above the address rather than behind a dismiss button: a notice the
   visitor has to tap past is a notice nobody reads, and this one stays in view
   while they type. Three lines, because a fourth would not be read either.
   Ruled top and bottom rather than boxed: a filled bordered card was the same
   chrome as the answer slabs, and a legal notice is the last thing on the
   screen that should be the loudest object on it. */
.privacy { max-width: 46em; display: flex; flex-direction: column; gap: .35em;
  padding: 1.1em 1.6em; border-radius: 0;
  border-top: 1px solid #3a332a; border-bottom: 1px solid #3a332a;
  background: transparent;
  text-align: center; font-size: .95em; color: var(--ink-dim); line-height: 1.45; }
.privacy .pv-title { font-family: var(--serif); font-size: 1.1em; color: var(--gold); }

/* ==========================================================================
   QUIZ SCREEN
   The client's note was "too basic with big buttons", and on a 1920x1080 panel
   it was three separate faults reinforcing each other: no imagery at all (the
   media column was `display: none` for 13 of 21 questions and an outlined
   placeholder box for the rest), the top and bottom thirds of the panel empty,
   and four filled bordered slabs in the middle carrying the answers.

   The answer to all three is the same one: give the question real cloth and
   let it hold the left of the panel edge to edge, then set the question and
   its answers as type down the right. Nothing here shrinks a touch target.
   ========================================================================== */

/* Progress as a hairline along the very top edge of the panel, above the
   framework's settings cluster rather than beside it. The counter that used to
   share its row has moved into the question column, which is both where a
   reader looks for it and the one part of this screen guaranteed to be clear
   of the cluster in every language at every type size. */
.qtrack { position: absolute; top: 0; left: 0; right: 0; height: 5px;
          background: #2a251d; z-index: 5; }
.qfill { height: 100%; width: 0; background: var(--gold); transition: width .5s ease; }

.qbody { flex: 1; display: flex; align-items: stretch;
         min-height: 0; overflow: hidden; }

/* ---------- the plate ----------
   Full bleed to the left, top and bottom edges: this is a piece of cloth the
   screen is laid on, not a thumbnail in a frame, so it carries no border, no
   radius and no caption bar. app.js paints it for EVERY question, including
   the thirteen with no media of their own, which is what stops any question
   coming up as an empty box. The ground colour is only what shows in the frame
   before the weave lands. */
.qmedia { position: relative; flex: 0 0 40%; min-width: 0; min-height: 0;
          overflow: hidden; background: var(--bg-raised);
          display: flex; align-items: center; justify-content: center; }
/* A real photograph, when the content team has delivered one, covers the cloth
   completely: it is the better image and it should win. Placeholder artwork
   does not get this far; see isPlaceholderArt in app.js. */
.q-img { position: absolute; inset: 0; width: 100%; height: 100%;
         object-fit: cover; border: 0; border-radius: 0; }

/* The caption is set ON the cloth over its own gradient, not in a filled bar
   bolted underneath it. The gradient is doubled with the swatch renderer's
   'bottom' scrim so the type clears AA over any of the ten weaves.

   Two elements, and the split is not cosmetic. `position: absolute` BLOCKIFIES
   display, so a -webkit-box that is also absolutely positioned computes to
   flow-root and its -webkit-line-clamp is silently dropped: measured
   display:flow-root, scrollHeight 323 in a 147px box, which sheared an
   eight-times-repeated caption through the middle of a letter and ran it off
   the foot of the panel. The band does the positioning and the gradient; the
   caption inside it stays in normal flow, where the clamp works and ends the
   line with an ellipsis that tells the visitor there is more. */
.q-capband { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
             padding: 3.4em 2.4em 2.1em;
             background: linear-gradient(to top, rgba(9,7,5,.92) 0%,
                                         rgba(9,7,5,.72) 42%, rgba(9,7,5,0) 100%); }
.q-cap { text-align: left; max-width: 100%; overflow-wrap: anywhere;
         font-family: var(--sans); font-weight: 600; line-height: 1.35;
         letter-spacing: .12em; text-transform: uppercase;
         display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
         overflow: hidden; }

/* ---------- the question column ----------
   Eyebrow, question, answer list and verdict, set as ONE optically centred
   block rather than pinned top and bottom with the answers stretched between.
   Stretching was tried first and left a 170px hole between the question and
   the first answer, which reads as something failing to load; the same 170px
   as a top and bottom margin reads as a set page. The panel is not empty
   either way, because the cloth beside it runs floor to ceiling. */
.qmain { flex: 1 1 auto; display: flex; flex-direction: column;
         justify-content: center;
         min-width: 0; min-height: 0; padding: 2.5em 3.2em 1.8em 3.9em; }
.qbar { flex: 0 0 auto; display: flex; align-items: baseline; gap: 1.2em; }
.qprogress { flex: 0 0 auto; }
.qplayer { flex: 1 1 auto; min-width: 0; text-align: right; letter-spacing: .1em; }

/* Content-driven strings must never be able to push a control off a screen the
   visitor cannot scroll. Everything the manifest/questions file can lengthen is
   width-bounded and line-clamped. Clamps are generous: a normally worded
   question or option is nowhere near them, which the layout audit checks by
   failing on any box whose content is actually sheared. */
.qtext { flex: 0 0 auto; text-align: left; margin: .55em 0 0;
         font-size: 2.5em; font-weight: 400; line-height: 1.2;
         max-width: 18em; overflow-wrap: anywhere;
         display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical;
         overflow: hidden; }

/* [CLAMP-1] Four pixels of bottom padding on every clamped box.
   A -webkit-box that shrink-wraps to n line boxes reports a scrollHeight 2-3px
   taller than its clientHeight whenever n x line-height is fractional, which it
   is at every type step in both scripts: the root is clamp(14px,1.2vw,20px)
   scaled by 0.88/1/1.24, so no font-size in this app lands on a whole pixel.
   The layout audit reads that as text SHEARED inside its own box and it is
   right to: the difference is real, the last line's descender is genuinely
   clipped. Measured across all 21 questions and all 84 options, in English and
   Hindi at small, medium and large: 2-3px of overshoot without this, 0 with it.
   The clamp itself is not the cause (an unclamped block with the same
   line-height overshoots identically), so this is not a workaround for the
   clamp, it is the rounding slack the line box needs. */
.qtext, .opt-text, .q-cap, .media-missing .mm-cap { padding-bottom: 4px; }

/* ---------- the answers ----------
   Four lines in a list, not four slabs. The visible affordance is the lettered
   disc and the type; the 100 px of hit area around them is padding, which costs
   nothing on screen. That is the whole trick the old build missed, and it is
   why "refined" and "easy to hit" are not in tension: the target is larger
   than the thing you can see. Framework .card chrome (fill, border, radius) is
   deliberately absent. */
/* Ruled top and bottom as well as between, so the four answers read as one
   bounded list under the question rather than as loose lines on the page. */
.opts { flex: 0 0 auto; min-height: 0; display: flex; flex-direction: column;
        width: 100%; margin-top: 1.5em;
        border-top: 1px solid #3a3327; border-bottom: 1px solid #3a3327; }
/* Fixed row height rather than letting the four grow into the column. Rows that
   stretch to fill 1080px end up 180px apart with a hairline floating halfway
   between each pair, which stops reading as a list of four answers and starts
   reading as four stranded lines. The list is a tight block; the slack around
   it is margin, and the cloth beside it is what stops that margin reading as
   emptiness. 100px of row for 27px of type is all padding, so the target is
   well over the 60px floor while nothing on screen looks bulky. */
/* NOTE on the em here and on .opt-letter: both resolve against this rule's own
   1.36em font-size, not against the root, so 3.7em is 100px at the medium step
   and not 74px. Getting that wrong is what made the rows 131px on the first
   pass. */
.opt { position: relative; flex: 0 0 auto; min-height: max(100px, 3.7em);
  min-width: 0;
  border: 0; border-radius: 0; background: transparent; color: var(--ink);
  font-family: var(--serif); font-weight: 500; font-size: 1.36em;
  text-align: left; display: flex; align-items: center; gap: 1em;
  padding: .7em 1em .7em .2em; }
.opt + .opt { border-top: 1px solid #3a3327; }
.opt:active { background: rgba(201,162,75,.10); }
/* brief settle-in while option taps are guarded, so the guard reads as the
   question arriving rather than as an unresponsive screen */
.opts .opt { transition: opacity .28s ease, background-color .18s ease; }
.opts.settling .opt { opacity: .35; }
/* 999px rather than 50%: identical on a square box, but the fleet writes round
   things as 999px and one spelling is the point. HOUSE-STYLE.md item 2. */
.opt-letter { flex: 0 0 max(54px, 2.7em); height: max(54px, 2.7em); border-radius: 999px;
  border: 1.5px solid #4c4536; color: var(--ink-dim);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--sans); font-weight: 600; font-size: .72em; letter-spacing: .05em; }
.opt-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
  overflow: hidden; }
/* Marked answers get a wash and an edge marker rather than a solid fill. Three
   signals carry each state (ground, marker, disc) so it still reads from the
   far side of the gallery without any of them having to shout. box-shadow
   rather than a border, because a border would shift the line 4 px sideways at
   the moment the visitor is reading it. */
.opt.correct { background: rgba(201,162,75,.16); box-shadow: inset 4px 0 0 var(--gold); }
.opt.correct .opt-letter { background: var(--gold); border-color: var(--gold); color: var(--bg); }
.opt.correct .opt-text { color: var(--gold); }
.opt.wrong { background: rgba(168,68,47,.24); box-shadow: inset 4px 0 0 var(--madder); }
.opt.wrong .opt-letter { background: var(--madder-deep); border-color: var(--madder-deep); color: var(--ink); }
.opt.wrong .opt-text { color: var(--madder); }
.opt.dim { opacity: .34; }

/* One row of reserved height for both the verdict and the mash-guard button,
   so nothing above moves when either appears. */
.qfoot { flex: 0 0 auto; display: flex; align-items: center; gap: 1.6em;
         min-height: max(84px, 4.2em); padding-left: .55em; }
/* min-height on the LINE as well as on the row. .qfoot reserving the height is
   what stops the layout jumping, but an empty #q-feedback with no height of its
   own has a zero-size box, and a zero-size box is indistinguishable from a
   missing element to anything measuring the screen: the overflow test reads it
   as the feedback line having vanished, and it is right to. */
.qfeedback { flex: 1 1 auto; min-width: 0; min-height: 1.6em; text-align: left;
             font-family: var(--serif); font-size: 1.35em; }
/* shown only when a visitor is hammering the screen: auto-advance stops and one
   deliberate tap, well clear of the answer list, is required to continue */
.qnext-wrap { flex: 0 0 auto; display: flex; }
.qnext-wrap #q-next { display: none; }
.qnext-wrap.show #q-next { display: inline-flex; min-height: 64px; align-items: center; }
.qfeedback.good { color: var(--gold); }
.qfeedback.bad { color: #d97a63; }

/* ---------- question media, sitting on the cloth ----------
   Both controls take the framework pill's shell (solid dark, gold edge) rather
   than a dashed outline: they now sit on woven cloth, where a dashed line
   disappears and a solid shell is the thing that stays findable. */
.media-play { position: relative; z-index: 2;
  min-height: max(96px, 4.6em); min-width: min(72%, 300px); max-width: 84%;
  border-radius: 999px; border: 1.5px solid var(--gold);
  background: rgba(16,13,10,.86); color: var(--gold);
  font-family: var(--serif); font-size: 1.2em;
  display: flex; align-items: center; justify-content: center;
  gap: .7em; padding: 1em 1.5em; }
.media-play:active { background: var(--gold); color: var(--bg); }
.media-play .mp-icon { font-size: 1.5em; }
.media-missing { position: relative; z-index: 2;
  border: 1.5px solid #4c4536; border-radius: var(--radius);
  background: rgba(16,13,10,.86);
  padding: 1.5em 1.8em; text-align: center; color: var(--ink-dim);
  max-width: 82%; overflow: hidden; }
.media-missing .mm-title { font-family: var(--serif); font-size: 1.2em;
  color: var(--ink-dim); margin-bottom: .4em; }
.media-missing .mm-cap { overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden; }
.media-play span:not(.mp-icon) { min-width: 0; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; }

/* ==========================================================================
   RESULTS / LEADERBOARD
   The same composition as the quiz screen: cloth on the left, content on the
   right. It replaces a layout that centred a crest, a headline and three
   stacked buttons in one empty half and a board in the other, leaving the top
   and bottom thirds of a 1920x1080 panel doing nothing.
   ========================================================================== */
.res-wrap { flex: 1; display: flex; align-items: stretch; min-height: 0; }

/* The result IS a piece of cloth. Seeded on the score band, so the four
   outcomes are four different weaves and a visitor who plays twice can see
   they did better; see paintResultPlate in app.js. */
.res-plate { position: relative; flex: 0 0 40%; min-width: 0; overflow: hidden;
             background: var(--bg-raised);
             display: flex; align-items: center; justify-content: center; }
.res-plate-in { position: relative; z-index: 2; max-width: 100%;
                display: flex; flex-direction: column; align-items: center;
                gap: .35em; text-align: center; padding: 2em 2.4em; }
.crest { width: max(88px, 4.4em); height: auto; opacity: .92; }
.res-plate .crest { margin-bottom: .7em; }
.res-plate .eyebrow { margin-bottom: .2em; }
/* Score first and largest: it is the one thing the visitor came back to the
   screen for. The headline is the sentence about it, so it sits under it and
   a size down, and the elapsed time is metadata and set as such. */
/* The text serif, NOT the display face, and lining figures asked for by name.
   Cormorant Garamond sets oldstyle figures by default, which turned a 4/10
   into something that reads as "4 / IO" at 75px on the wall. A score is the one
   string in this app that has to be unmistakable as numerals. */
.res-score { display: flex; flex-direction: column; align-items: center; gap: .25em;
             font-family: var(--serif); font-size: 1.5em; color: var(--ink-dim);
             font-variant-numeric: lining-nums; }
.res-score b { font-weight: 400; font-size: 2.5em; line-height: 1;
               color: var(--gold); letter-spacing: .01em; }
.res-score .rs-who { font-family: var(--serif); font-size: .8em; letter-spacing: .06em; }
.res-score .rs-time { font-family: var(--sans); font-size: max(18px, .58em);
                      letter-spacing: .16em; color: var(--ink-dim); }
#res-headline { font-size: 2.3em; line-height: 1.14; margin-top: .1em;
                overflow-wrap: anywhere; }

/* min-height:0 so the board is the part that gives: the header row, the note
   row, the retention line and the actions all share this column, and without
   it a flex item refuses to shrink under its content and the last rows push
   out of the panel instead of scrolling.
   The top padding clears the framework chrome. The Home pill and the settings
   cluster both sit in that band, and the leaderboard heading and its four
   window tabs would otherwise run under them. Never hardcode the number:
   kiosk.js measures and republishes it on every language and type change. */
.res-right { flex: 1 1 auto; display: flex; flex-direction: column; gap: .7em;
             min-width: 0; min-height: 0;
             padding: calc(var(--kiosk-chrome-bottom, 108px) + 1.4em) 3.2em 2.2em; }
/* Heading and windows on one line. h2 is flex: 0 0 auto so its box hugs its
   own text rather than spanning the column, which keeps it clear of the Home
   pill by construction and not by luck. */
.lb-top { flex: 0 0 auto; display: flex; align-items: center; gap: 1.4em; flex-wrap: wrap; }
.lb-top h2 { flex: 0 0 auto; }

/* ---------- leaderboard window tabs ----------
   min-width keeps the four on one row at 1920 wide in both languages;
   flex-wrap is the safety net rather than the plan, so a longer translation
   drops a tab to a second row instead of squeezing the labels. The house touch
   floor is --touch (64px, HOUSE-STYLE.md #5); these shipped at 60 and were the
   one control under it. The padding does the work, not the fill: the tabs are
   hairline pills now rather than filled blocks. */
.lb-tabs { flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: .5em; }
.lb-tab { flex: 1 1 auto; min-width: 148px; min-height: var(--touch, 64px); padding: .3em 1em;
  border-radius: 999px; border: 1px solid #3a332a;
  background: transparent; color: var(--ink-dim);
  font-family: var(--sans); font-size: 1em;
  display: flex; align-items: center; justify-content: center; text-align: center; }
.lb-tab.on { border-color: var(--gold); color: var(--gold);
             background: rgba(201,162,75,.14); font-weight: 600; }
.lb-tab:active { border-color: var(--gold); }

/* The board label and the visitor's own place share one line: the label is the
   state of the panel, the place is the one number they came for. */
.lb-head { flex: 0 0 auto; display: flex; align-items: baseline; gap: 1em; min-width: 0; }
#lb-note { flex: 1 1 auto; min-width: 0;
           overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#lb-rank { flex: 0 0 auto; color: var(--gold); font-size: .95em; }
/* Gallery reading floor and AA contrast, same correction as [VIS-2] below. */
#lb-partial { flex: 0 0 auto; color: var(--ink-dim); font-size: .95em; }
#lb-partial:empty { display: none; }

/* Rows are ruled, not boxed: ten filled rounded rectangles stacked up the
   right of the panel were the same chrome as the answer slabs and read the
   same way. The visitor's own row keeps a wash and an edge marker so it is
   still the one row that finds you from across the gallery. */
/* flex: 0 1 auto, so the board takes the height its rows need and scrolls when
   there are more than fit. Letting it GROW put the retention line at the foot
   of the panel with 600px of nothing above it on the first score of the day. */
.lb { display: flex; flex-direction: column; flex: 0 1 auto; min-height: 0;
      overflow-y: auto; overscroll-behavior: contain; }
.lb::-webkit-scrollbar { width: 0; }
.lb-row { display: flex; align-items: center; gap: 1.2em;
  min-height: max(58px, 2.9em);
  background: transparent; border: 0; border-radius: 0;
  padding: .5em .9em; font-size: 1.05em; }
.lb-row + .lb-row { border-top: 1px solid #2f2a20; }
.lb-row.me { background: rgba(201,162,75,.14); box-shadow: inset 4px 0 0 var(--gold); }
.lb-rank { flex: 0 0 2em; text-align: center;
           font-family: var(--serif); font-size: 1.15em; color: var(--gold); }
.lb-name { flex: 1; letter-spacing: .06em;
           overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-score { flex: 0 0 4.5em; text-align: right; color: var(--ink-dim); }
.lb-sep { text-align: center; color: var(--ink-faint); }
.lb-loading { color: var(--ink-faint); text-align: center; padding: 2em;
              font-family: var(--serif); font-size: 1.15em; }

/* The three ways on, along the foot of the board. Play again keeps the filled
   treatment: the emphasised path on a museum kiosk must not be a keyboard. */
.res-actions { flex: 0 0 auto; display: flex; gap: .9em; margin-top: auto;
               padding-top: 1.2em; }
.res-actions .btn { flex: 1 1 auto; min-height: 72px; justify-content: center; }
/* The framework's .btn sets display:inline-flex, which BEATS the browser's own
   [hidden] rule. Without this line, hiding the certificate button leaves it on
   screen and fully tappable. The same trap already cost this app once, when a
   .whypanel class rule with display:flex left an invisible panel swallowing
   every touch. Anything given display by a class needs its hidden case spelled
   out. */
.res-actions .btn[hidden] { display: none; }

/* ---------- thanks ---------- */
.center { text-align: center; }
#thanks .entry-wrap { justify-content: center; }
#thanks .body.big { font-size: 1.25em; max-width: 34em; }
/* Cloth rising out of the ground along the foot, the way the loom does on the
   attract screen. The gradient is what stops it reading as a bar bolted to the
   bottom of the screen: there is no edge, the weave simply appears. */
.cloth-band { flex: 0 0 30%; min-height: 0; position: relative; overflow: hidden;
              background: var(--bg-raised); }
.cloth-band::after { content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to bottom, var(--bg) 0%, rgba(22,19,15,.62) 30%,
                              rgba(22,19,15,0) 72%); }

/* The "Leave the quiz?" modal went with the ✕ Exit button that was the only
   thing that opened it. Nothing on a kiosk should ask a visitor to confirm a
   decision they made by walking up to the screen. */

/* ---------- email keyboard accent ---------- */
.email-osk .key.send { color: var(--bg); background: var(--gold); border-color: var(--gold); }


/* ── escape & abuse hardening (Aug 2026 abuse pass) ───────────────────────────
   Gestures must stay inside the app. `touch-action: manipulation` (kiosk.css)
   still lets Chromium claim a two-finger pinch as a browser zoom: reproduced at
   visualViewport.scale = 4 in testing/test_escape.py, and NOT stopped by the
   --disable-pinch launch flag. Pinning the full-screen layers to
   `touch-action: none` and re-granting only the axis each scroller needs is what
   actually holds. Chrome stops the touch-action walk at the scroller, so
   `none` on the .screen does not disable `pan-y` inside it (measured).
   `overscroll-behavior: none` stops the two-finger overscroll back/forward
   navigation without relying on --overscroll-history-navigation=0. */
html, body { overscroll-behavior: none; }
img, video, svg { -webkit-user-drag: none; user-drag: none; }
#saver, .screen, .player { touch-action: none; }
.lb { touch-action: pan-y; }

/* ==========================================================================
   GALLERY LEGIBILITY AUDIT: app-local corrections
   Panel: 32" 1920x1080 landscape, visitors standing 40-80 cm away.
   ========================================================================== */

/* [VIS-1] The email keyboard's bottom row is six `.key.wide` at
   `flex: 2 1 140px` inside a 730px row, so each settles at ~111px of content
   box while "@gmail.com" needs 121px: the label was clipped 10px on the
   right and ran into the pill edge (measured scrollWidth 121 vs clientWidth
   111). Let that row wrap and let the keys take their content width. */
.email-osk .row:last-child { flex-wrap: wrap; row-gap: .5em; }
.email-osk .key.wide { flex: 0 1 auto; min-width: 110px; padding: 0 1.1em; }

/* ==========================================================================
   DEVANAGARI (html[lang="hi"]): app-local corrections
   Noto Sans/Serif Devanagari sets matras above the headline and below the
   baseline, so a line box sized for Latin cuts them off. Everything below is
   scoped to lang="hi" so the English layout is byte-for-byte unchanged.
   ========================================================================== */

/* [HI-1] #q-text is a -webkit-box with `overflow: hidden` and a line clamp, so
   its height is exactly n x line-height and anything taller is SHEARED rather
   than scrolled. At a Latin line-height a two-line Hindi question measured
   scrollHeight 101 in a 95px box: the top matras of "ज्यामितीय" and
   "प्रसिद्ध" were visibly cut. 1.45 clears the tallest stacks in the bank with
   room to spare against the 1.2 the question is now set at. The clamp stays
   generous: Devanagari sets shorter than Latin for the same sentence, so no
   question in the bank comes near it in either script. */
html[lang="hi"] .qtext { line-height: 1.45; }

/* [HI-2] Same shear risk in every other clamped block: .opt-text in a 4-line
   clamp, .q-cap in a 2-line clamp, .mm-cap in a 3-line clamp. */
html[lang="hi"] .opt-text,
html[lang="hi"] .q-cap,
html[lang="hi"] .media-missing .mm-cap,
html[lang="hi"] .media-play span:not(.mp-icon) { line-height: 1.5; }

/* [HI-3] The answer rows are single lines of Devanagari over a min-height
   floor. They are no longer a fixed-height two-column grid, so the taller line
   box simply makes the row taller, which is fine and which the column absorbs.
   The padding stays trimmed so four Devanagari rows and four Latin rows come
   out close to the same total height and the composition does not shift when a
   visitor switches language mid-question. */
html[lang="hi"] .opt { padding-top: .55em; padding-bottom: .55em; }

/* [HI-4] The feedback line is unclamped but sits in a min-height box sized for
   Latin, both on the line itself and on the .qfoot row around it. */
html[lang="hi"] .qfeedback { min-height: 2em; line-height: 1.5; }

/* [HI-5] Tracking is a Latin display convention: applied to Devanagari it
   pushes matras away from the consonant they belong to and the word stops
   reading as one unit. Drop it wherever this app added it.
   The two eyebrows and the plate caption are new to this pass and are the worst
   offenders, because the framework's .eyebrow tracks at .14em and the caption
   at .12em: "आपके अंक" came out as four floating syllables over the score.
   Only this app's own eyebrows are touched, not the framework class, since the
   other five stations set their own. */
html[lang="hi"] .entry-display,
html[lang="hi"] .qplayer,
html[lang="hi"] .qprogress,
html[lang="hi"] .res-plate .eyebrow,
html[lang="hi"] .q-cap,
html[lang="hi"] .lb-name { letter-spacing: normal; }

/* [HI-6] The email keyboard is Latin either way (the address is), but
   "मिटाएँ" and "भेजें ✓" are taller than "Clear" and "Send ✓" and were riding
   the top edge of their keys. */
html[lang="hi"] .osk .key { line-height: 1.5; }

/* [HI-7] Same again for the two blocks added with the certificate and the
   leaderboard windows: the tab labels are single centred lines in a 60px pill,
   and the privacy notice is three stacked lines of running Devanagari where a
   Latin line-height closes the gap between one line's descenders and the next
   line's matras. */
html[lang="hi"] .lb-tab { line-height: 1.5; }
html[lang="hi"] .privacy { line-height: 1.6; }

/* [VIS-2] Gallery reading floor (~18px) and AA body contrast: --ink-faint
   measures 3.84:1 on --bg. App-owned selectors only; the framework-wide
   .caption fix is reported separately.
   .q-cap has left this list because it no longer sits on --bg: it is set on
   woven cloth over a scrim, where --ink-dim is the wrong way round. It takes
   full --ink over a doubled gradient instead, and its own floor, in the .q-cap
   rule above. */
.qplayer,
#lb-note,
#email .caption { font-size: .95em; color: var(--ink-dim); }
#saver .overlay .caption { font-size: .95em; color: var(--ink-dim); }

/* [VIS-3] Type set on cloth rather than on the gallery ground. The scrim under
   each of these is opaque enough that the pair behaves like ink on --bg, and
   each keeps the 18px gallery floor of its own. */
.q-cap { font-size: max(18px, .84em); color: var(--ink); }
.media-missing .mm-cap { font-size: max(18px, .9em); color: var(--ink-dim); }

/* ---------- see the answer ----------
   Offered on every question, right or wrong. Quiet at rest so the quiz still
   moves for a visitor who does not want it, and a full plate once opened so
   what they came to read is not competing with the answer list behind it. */
/* Sits in the reserved footer row alongside the feedback line and the
   mash-guard Next button. flex: 0 0 auto matters: without it this button
   competed for the row's width and squeezed #q-next down to a zero-size box,
   which the suite reads, correctly, as the Next button having vanished. */
.qfoot .qwhy {
  flex: 0 0 auto;
  /* 64, not 56. Every control a visitor can touch in this gallery clears 60px,
     and the audit is right to fail anything that does not: a standing adult
     aiming at a 56px target on a 32 inch panel misses. */
  /* max(18px, ...) not a bare em. At the 20px root .82em is 16.4px, under the
     gallery's 18px floor, and this one sits over a busy woven panel where it
     needs every pixel. */
  font-size: max(18px, .82em); padding: .5em 1.1em; min-height: 64px;
  opacity: .8; letter-spacing: .02em; white-space: nowrap;
}
.qfoot .qwhy[hidden] { display: none; }
.qfoot .qwhy:active { opacity: 1; }

.whypanel {
  position: absolute; inset: 0; z-index: 40;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10, 8, 6, .82);
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
  opacity: 0; transition: opacity .28s ease;
}
/* A class rule with display:flex beats the browser's own [hidden] rule, so
   without this the panel stayed laid out at full screen size while invisible
   and swallowed every touch on the quiz behind it. Taps dispatched straight
   at an element still worked, which made it look intermittent rather than
   broken. Anything given display by a class needs its hidden case spelled
   out. */
.whypanel[hidden] { display: none; }
.whypanel.on { opacity: 1; }
.whypanel .wp-inner {
  max-width: 46ch; margin: 0 2em; text-align: left;
  border-left: 3px solid var(--gold); padding: 1.4em 1.7em;
  /* var(--radius), not the .4em this used to carry. The explanation panel is a
     panel, and the fleet now uses one radius for cards and panels.
     HOUSE-STYLE.md item 2. */
  background: rgba(22, 19, 15, .96); border-radius: var(--radius);
}
.whypanel .wp-head {
  /* 18px floor, like every other label in the gallery. It was 15px, which no
     root this panel ever has could lift over the floor. */
  font-family: var(--sans); font-weight: 600; font-size: max(18px, .68em);
  letter-spacing: .16em; text-transform: uppercase; color: var(--gold);
}
html[lang="hi"] .whypanel .wp-head { letter-spacing: .05em; text-transform: none; }
.whypanel .wp-answer {
  font-family: var(--display); font-size: 1.7em; line-height: 1.2;
  color: var(--ink); margin: .35em 0 .5em;
}
.whypanel .wp-body {
  font-family: var(--serif); font-size: 1.02em; line-height: 1.55;
  color: var(--ink-dim);
}
.whypanel .wp-close { margin-top: 1.3em; }

/* ---------- name on the board ----------
   The same entry furniture as the email screen, one size up. A name is short,
   so it can afford to be large and legible from standing distance. */
.entry-display.name { font-size: 1.9em; letter-spacing: .03em; text-transform: none; }
.name-osk .key { text-transform: none; }

/* ---------- the certificate ----------
   Free, drawn on the panel, and photographed rather than printed or posted.
   It is laid out as a SHEET on the cloth, with a fixed max width, because the
   thing a visitor points a camera at should read as a document and not as
   another screen of the app. Sized so the whole sheet clears a 1080 panel with
   the close button and the "photograph this" line still on screen. */
#cert { display: flex; flex-direction: column; align-items: center;
        justify-content: center; gap: 1.1em; }
.cert-sheet {
  width: min(78ch, 76vw); max-height: 78vh; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: .5em; padding: 2.2em 2.6em;
  background: rgba(22, 19, 15, .97);
  border: 1.5px solid var(--gold); border-radius: var(--radius);
  box-shadow: 0 0 60px rgba(0, 0, 0, .55);
}
.cert-sheet .crest { width: max(72px, 3.4em); margin-bottom: .2em; }
.cert-eyebrow { font-family: var(--sans); font-size: max(18px, .72em);
  letter-spacing: .18em; text-transform: uppercase; color: var(--gold); margin: 0; }
.cert-title { font-family: var(--serif); font-size: 2.1em; line-height: 1.15;
  color: var(--ink); margin: .1em 0 .3em; }
.cert-awarded { font-family: var(--serif); font-size: max(18px, .95em);
  color: var(--ink-dim); margin: 0; }
/* The name is the reason the sheet exists, so it is the largest thing on it.
   It wraps rather than scrolls: 18 characters cannot overflow this box, and a
   name that somehow does is better wrapped than clipped. */
.cert-name { font-family: var(--serif); font-weight: 600; font-size: 2.6em;
  line-height: 1.15; color: var(--gold); margin: .15em 0 .25em;
  overflow-wrap: anywhere; }
.cert-body { font-family: var(--serif); font-size: max(18px, 1.05em);
  line-height: 1.5; color: var(--ink); max-width: 52ch; margin: 0; }
.cert-foot { display: flex; gap: 2.4em; align-items: baseline; margin-top: 1em;
  font-family: var(--sans); font-size: max(18px, .7em); letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-dim); }
.cert-keep { font-family: var(--sans); font-size: max(18px, .7em);
  letter-spacing: .1em; color: var(--ink-dim); }

/* Devanagari sets taller, so the sheet needs the same breathing room the rest
   of the app gives it or the certificate crowds its own border. */
html[lang="hi"] .cert-title { font-size: 1.9em; line-height: 1.3; }
html[lang="hi"] .cert-name { font-size: 2.3em; line-height: 1.35; }
html[lang="hi"] .cert-body { line-height: 1.7; }
