/* 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; }
.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: var(--madder); 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 var(--hairline); border-bottom: 1px solid var(--hairline);
  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: var(--bg-card); 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; }

/* ---------- per-question difficulty + countdown, 16 Aug 2026 ----------
   Vivek's placement spec exactly: ONE LINE in the existing eyebrow,
   "QUESTION 1 OF 10 · EASY · 00:30", no separate widget, no badge shape, no
   border, no pill. The difficulty word takes colour inline within .qprogress
   itself; the word is always present, colour is never the only carrier
   (his words: "no gaming-style neon badges or cartoonish UI").
   Museum-grade, restrained: house tokens where they exist (--gold for medium,
   --madder for hard), and one new value for easy since no green token exists
   yet in the palette - #7a9b6e, chosen to sit near --madder's own measured
   weight (5.94:1 vs 4.26:1 on --bg) rather than matching --gold's brighter
   7.72:1, so the line does not visibly brighten or dim as the level changes
   question to question. Not proposed as a framework token here: this is a
   quiz-local value pending Vivek's eye, same as everything else this phase. */
.qdiff { font-weight: inherit; }
.qdiff-easy { color: #7a9b6e; }
.qdiff-medium { color: var(--gold); }
.qdiff-hard { color: var(--madder); }
/* Numerals, not prose: lining figures so they never wobble between oldstyle
   heights as the seconds change, the same reason scores use them elsewhere. */
.qcountdown { font-variant-numeric: lining-nums; font-weight: inherit; }
/* Two warning steps, colour and weight only - nothing moves, nothing pulses.
   His words: "a colour and weight shift on the numerals, nothing that moves
   or pulses hard enough to pull the eye off the question." */
.qcountdown.warn { color: var(--gold); font-weight: 600; }
.qcountdown.urgent { color: var(--madder); font-weight: 700; }

/* ---------- PRIMARY countdown on the cloth (Vivek 07:53) ----------
   The eyebrow figure above stays as metadata; THIS is the timer a visitor
   reads. Seated on the upper cloth column: measured at 1920x1080, the zone
   x~60-380 / y~140-270 is clear of the settings cluster (ends y87), the
   question column (starts x846), the HH mark and Home pill (both top-right)
   and the Cultre watermark (bottom-right). Absolute against the screen, so a
   long question cannot push it.
   A quiet plate rather than bare glyphs: the cloth cycles through nine bands
   and no single ink clears them all (the credit line learned this first), so
   the numerals sit on a near-opaque dark ground with a hairline rule, the
   same technique at the same restraint. Serif lining figures at 3.2em: the
   register of the results score, not an exam clock. Warning steps reuse the
   eyebrow's classes, colour and weight only, nothing moves. */
.qtimer {
  position: absolute; left: 3em; top: 7em; z-index: 6; pointer-events: none;
  display: flex; flex-direction: column; align-items: flex-start; gap: .1em;
  padding: .55em .9em .5em;
  background: rgba(8, 6, 4, .74);
  border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline);
}
.qtimer-label {
  font-family: var(--sans); font-size: max(18px, .8em); color: var(--ink-dim);
  letter-spacing: .14em; text-transform: uppercase;
}
.qtimer-clock {
  font-family: var(--serif); font-size: 3.2em; line-height: 1;
  font-variant-numeric: lining-nums; color: var(--ink);
}
.qtimer-clock.warn { color: var(--gold); }
.qtimer-clock.urgent { color: var(--madder); font-weight: 700; }

/* 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 var(--hairline); border-bottom: 1px solid var(--hairline); }
/* 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 var(--hairline); }
.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;
  /* #4c4536, not var(--hairline): a lighter CONTROL EDGE on dim ground, not a rule
     line between rows. Kept distinct on purpose (DESIGN-LANGUAGE section 1). */
  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: var(--madder); }
/* A timeout is neither: the visitor did not get it wrong, they ran out of
   time, so it earns its own neutral tone rather than borrowing the alarm of
   "bad" or the credit of "good". */
.qfeedback.skip { color: var(--ink-dim); }

/* ---------- 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;
  /* #4c4536, same deliberate exception as .opt-letter above: a control edge, not
     the row hairline. */
  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; }
/* The ONE bare .crest rule: an earlier 110px rule in the entry-screens section
   (email/thanks) was dead, since this rule and that one share a selector and
   equal specificity, so source order alone decided and this one always won.
   Applies to every plain class="crest" image (results plate, the thanks
   screen); .cert-sheet .crest below stays a separate, more specific rule. */
.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;
  /* #3a332a here happens to equal --hairline's value, but this is a CONTROL EDGE
     (the pill's own outline, same family as .opt-letter), not a divider rule
     between two elements, so it is deliberately not wired to the token. */
  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 var(--hairline); }
.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; }
/* v2.3.1, 16 Aug 08:00, VIVEK: "weaved text and logos at top left and right
   corner on dull white khadi like fabric with indigo weaved text in fine
   silk". Three decisions carry the whole treatment:
   KHADI GROUND: layered repeating gradients, a fine 3px warp and weft in two
   ecrus over an unbleached base, plus two wide, faint slub bands, because
   khadi's tell is the slight unevenness of handspun yarn; a perfectly regular
   grid would read as machine cloth.
   WOVEN TEXT: the ::after overlay lays the weft threads OVER everything on
   the sheet, ink and logos alike, exactly the trick the loom uses on the
   attract cloth: letterforms crossed by picks read as woven in, not printed
   on. Kept faint enough that 600-weight indigo stays comfortably legible.
   INDIGO IN SILK: the inks below are one indigo dye lot (#27406b family), and
   the name takes a subtle vertical gradient, dye depth, not gloss, which is
   how fine silk thread actually differs from cotton: sheen in the fibre, not
   highlights painted on. */
.cert-sheet {
  position: relative;
  width: min(78ch, 76vw); max-height: 78vh; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: .5em; padding: 3.2em 2.6em 2.2em;
  background:
    repeating-linear-gradient(0deg,   rgba(214, 204, 182, .30) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg,  rgba(221, 212, 190, .28) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(0deg,   transparent 0 34px, rgba(206, 196, 172, .16) 34px 37px),
    repeating-linear-gradient(90deg,  transparent 0 41px, rgba(226, 218, 198, .18) 41px 44px),
    #efe9da;
  border: 1px solid #2e4666;
  outline: 1px solid rgba(46, 70, 102, .35); outline-offset: -7px; /* selvedge rule */
  border-radius: 2px;   /* cloth, not a card */
  box-shadow: 0 0 60px rgba(0, 0, 0, .55);
}
/* The weft passing over everything: what makes the sheet WOVEN rather than
   printed. pointer-events none; the certificate has no controls inside it. */
.cert-sheet::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(0deg,
    rgba(239, 233, 218, .34) 0 1px, transparent 1px 4px);
}
/* 16 Aug redesign: the raw white crest is hidden ([hidden] attribute in the
   markup) and replaced by .cert-hh-mark below, the same quiz-local mask +
   colour technique as the rest of tonight's logo work, sized for THIS
   sheet's own pairing.
   MEASUREMENT: Cultre's x-height (lowercase-dominant, "cultre") against HH's
   cap-height ("HANDLOOM HAAT"), the same two fractions measured earlier
   tonight - 0.1349 of the Cultre asset's height, 0.0411 of HH's - applied to
   a Cultre reference chosen FOR THIS SHEET rather than reused from the
   attract screen (3.1em) or the in-app ghost (2.8em), both solved for a
   different context. Here Cultre is a small FOOTER credit (1.5em, clearly
   below cert-title's 2.1em and cert-name's 2.6em, "subordinate" as asked),
   which the same ratio (fixed at ~3.28x by the two logos' own proportions,
   not by this sheet) puts HH at 4.923em - up from the old 3.4em, genuinely
   more legible, without outscaling the name that is the actual subject of
   the document. */
.cert-sheet .crest { width: max(72px, 3.4em); margin-bottom: .2em; }
/* The two marks move to the TOP CORNERS (Vivek 08:00), both dyed the sheet's
   own indigo so they read as woven into the khadi rather than applied to it.
   HH top-left, Cultre top-right, both absolute so the centred column of text
   flows between them untouched. The Cultre img is recoloured by masking a div
   with its own asset's alpha, the same technique as every mark tonight; its
   red stays right on dark grounds elsewhere, but red thread on this cloth
   would be the one non-indigo dye on the sheet. */
/* 16 Aug 21:32 (Vivek, "correct the HH logos inside the quiz also"): the
   indigo dye is dropped with the rest of tonight's mask treatments; the
   certificate carries the real mark in its real construction, same footprint. */
.cert-hh-mark {
  position: absolute; top: 1.2em; left: 1.4em;
  width: calc(3.4em * 1.4141); height: 3.4em; margin: 0;
  background: url(../../framework/assets/hh-logo-white.svg) left center / contain no-repeat;
}
.cert-cultre-mark { display: none; }   /* the red img retires; replaced below */
.cert-cultre-corner {
  position: absolute; top: 1.2em; right: 1.4em;
  height: 2.6em; width: calc(2.6em * 0.743);   /* asset 870x1171 */
  background-color: #2e4666;
  -webkit-mask: url(../../framework/assets/cultre-lockup-red.png) right center / contain no-repeat;
          mask: url(../../framework/assets/cultre-lockup-red.png) right center / contain no-repeat;
}
/* One indigo dye lot for every thread of text on the cloth: a deep master
   tone for the headline layers, a paler washed indigo for the secondary
   lines, exactly the two depths a natural dye bath gives. Gold and cream are
   gone from this sheet; they belong to the dark screens. */
.cert-eyebrow { font-family: var(--sans); font-size: max(18px, .72em);
  letter-spacing: .18em; text-transform: uppercase; color: #51648a; margin: 0; }
.cert-title { font-family: var(--serif); font-size: 2.1em; line-height: 1.15;
  color: #27406b; margin: .1em 0 .3em; }
.cert-awarded { font-family: var(--serif); font-size: max(18px, .95em);
  color: #51648a; margin: 0; }
/* The name is the reason the sheet exists, so it is the largest thing on it,
   and it is the one run of SILK on the cotton: a vertical dye-depth gradient,
   darker at the foot the way hanging silk holds its dye, not a gloss. */
.cert-name { font-family: var(--serif); font-weight: 600; font-size: 2.6em;
  line-height: 1.15; margin: .15em 0 .25em;
  background: linear-gradient(180deg, #35507f 0%, #1e3357 78%);
  -webkit-background-clip: text; background-clip: text;
  color: #27406b; -webkit-text-fill-color: transparent;
  overflow-wrap: anywhere; }
.cert-body { font-family: var(--serif); font-size: max(18px, 1.05em);
  line-height: 1.5; color: #27406b; max-width: 52ch; margin: 0; }
/* The descriptor, the SAME headline() the results screen already shows
   ("A master weaver!" etc.), so this is a reused honest label, not a grade
   invented for the certificate. Set between the citation and the numbers, a
   size down from cert-body: an accent, not a second headline competing with
   the name. */
.cert-descriptor { font-family: var(--serif); font-style: italic;
  font-size: max(18px, .95em); color: #51648a; margin: .3em 0 0; }
/* The quiet summary block Vivek asked for: one line for the score, one line
   for the breakdown, NOT a table of labelled fields. The score line gets the
   marks number bold and a size up, since it is the number that answers "how
   did they do"; the breakdown is the same weight as cert-awarded, present
   but not competing with anything above it. */
.cert-summary { display: flex; flex-direction: column; align-items: center;
  gap: .3em; margin-top: .6em; }
.cert-score-line { font-family: var(--serif); font-size: max(18px, 1.15em);
  color: #27406b; }
.cert-score-line b { color: #1e3357; font-weight: 600; font-size: 1.3em; }
.cert-breakdown-line { font-family: var(--sans); font-size: max(18px, .78em);
  letter-spacing: .04em; color: #51648a; }
.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: #51648a; }
.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; }

/* ==========================================================================
   TOP-RIGHT PROGRAMME MARK, QUIZ-LOCAL OVERRIDE (Vivek, 16 Aug 2026)
   His instruction, verbatim in effect: match the Handloom Haat lockup's colour
   to the Cultre lockup's live colour, and match their PERCEIVED typographic
   scale rather than an arbitrary px value; preserve proportions and clarity;
   do not alter the underlying marks unnecessarily. This is quiz-only, per the
   quiz-first design lane; the framework candidate is recorded in
   ~/Cultre/fleet/quiz-first/FRAMEWORK-CANDIDATES.md, pending his approval to
   promote it to the other five apps. TWO PAIRS exist and each needed its own
   number: the ATTRACT screen (.saver-mark vs .saver-credit, both 3.1em there)
   and the IN-APP ghost watermark (.wm-hh-top vs .wm-cultre, which is 2.8em,
   NOT 3.1em). An earlier pass on the in-app pair used 3.1em for both, which
   is the attract screen's Cultre size, not this one's; caught on a second
   look and corrected below rather than left to ship silently wrong.

   COLOUR: read from the live asset, not a remembered hex. Sampled directly
   from framework/assets/cultre-lockup-red.png's rendered ink (2286 pixels
   across the wordmark, brightest-decile estimate identical to the mean):
   #A85C48. That is what the Cultre mark actually renders with right now, and
   it is the SAME ink both places, because the asset itself carries one flat
   colour regardless of which screen displays it.

   SCALE: "cultre" is lowercase-dominant, so its X-HEIGHT is the metric doing
   the visual work; "HANDLOOM HAAT" is uppercase, so its CAP-HEIGHT is the
   comparable metric. Both measured from the rendered glyphs, not assumed:
     Cultre lockup (cultre-lockup-red.png, 1171px tall): the word's two
       ascender-free letters, 'c' and 'e', independently agree EXACTLY on
       158px tall -> x-height fraction 0.1349 of the asset.
     Handloom Haat lockup (hh-logo-white.svg, viewBox 841.89x595.28, aspect
       1.4141:1; verified its raster export hh-logo-white.png shares the same
       proportions): the wordmark's cap height across its full span measures
       102px of a 2481px-tall render -> cap-height fraction 0.0411.
     Required em = (cultre_em x 0.1349) / 0.0411, evaluated PER SCREEN at
     that screen's own Cultre em, since the two Cultre instances are sized
     differently by the framework already:
       ATTRACT  (.saver-credit is 3.1em)  -> 10.174em, box 203.5 x 287.7px
       IN-APP   (.wm-cultre    is 2.8em)  ->  9.189em, box 183.8 x 259.9px

   METHOD: the SAME source artwork (framework/assets/hh-logo-white.svg),
   referenced by a CSS mask, not redrawn or re-laid-out; only its presentation
   colour and size change. No framework file is edited. Mask-image already
   has precedent in this codebase (apps/documentary-selector/app.css), so it
   renders reliably on the WebView floor this fleet already targets. */

/* ---- IN-APP ghost mark (.wm-hh-top / .wm-cultre pairing, 2.8em) ---- */
/* .brand-watermarks .wm-hh-top, not the bare class: kiosk.css's own selector
   carries two classes (specificity 0,0,2,0), which beats a bare .wm-hh-top
   (0,0,1,0) regardless of app.css loading after kiosk.css. Caught by
   inspecting computed styles after the first render still showed the ghost
   mark faintly behind the new one: display:block, not none as intended. */
.brand-watermarks .wm-hh-top { display: none; }
/* 16 Aug 21:39 (Vivek, final): NO top-right HH mark inside the quiz at all.
   The in-app screens keep only the framework's own bottom-left .wm-hh
   watermark (the "older one"), which this app has never touched. The
   .quiz-hh-mark element stays in the markup but renders nothing; the
   .wm-hh-top hide above also stays, so the top-right corner is clean on
   every question screen. (Supersedes 21:32's real-artwork render and the
   whole top-right ghost series.) */
.quiz-hh-mark { display: none; }
/* Same two hide conditions .wm-hh-top already had, mirrored here since this
   element lives outside .brand-watermarks: the attract screen shows its own
   large mark in this corner (weave-saver.js toggles .wm-hidden the same way),
   and nothing should show over a playing clip. States are mutually exclusive
   in practice (the saver refuses to run while the player is busy), so the two
   rules below never need to arbitrate between each other. */
#saver:not(.hidden) ~ .quiz-hh-mark { display: none; }
body.hh-player-open .quiz-hh-mark { display: none; }

/* ---- ATTRACT-SCREEN mark (.saver-mark / .saver-credit pairing, 3.1em) ----
   This is the pair Vivek was actually looking at in his screenshot: the
   in-app fix above never reaches the attract screen, since it is explicitly
   hidden while the saver is up.
   PLACED AS A CHILD OF #saver IN THE MARKUP, not a sibling: #saver is
   position:absolute, z-index:100, covering the full viewport, so a sibling
   at any lower z-index paints BEHIND it regardless of DOM order. Found this
   empirically: a plain background-colour box, no mask at all, at the right
   position and size, painted NOTHING, and elementsFromPoint showed #saver
   itself sitting on top the whole time. As a child it shares #saver's own
   stacking context and inherits #saver's hidden state (opacity/visibility)
   automatically, so no extra show/hide toggle is needed at all. */
.saver-mark img { display: none; }
/* v2.2.1 FOURTH REVISION (Vivek 07:23): one coordinated bottom-right cluster,
   three storeys sharing the credit's own 1.4em right rule:
       woven HH symbol          (top)
       HANDLOOM HAAT            (middle)
       Powered by · cultre      (the framework's .saver-credit, untouched)
   THE TWO PARTS ARE WINDOWS ONTO ONE ARTWORK, not separate assets and not
   redrawn: hh-logo-white.png is a horizontal lockup, so each element scales the
   whole image up with mask-size and slides the wanted region into view with
   mask-position. The window fractions are MEASURED off the asset's real alpha
   bounds (symbol x 0.146-0.4658, y 0.2765-0.7231; wordmark x 0.5088-0.8532,
   y 0.4813-0.5216 of the 3508x2481 image), so the crops cannot clip a stroke.
   Percent mask-position p maps to offset p*(box-mask overflow), hence
   pos = f0/(1-span) for a window starting at fraction f0 spanning `span`.
   COLOUR: var(--ink), the same cream the "Powered by" line is set in
   (kiosk.css .saver-credit span), so the whole cluster reads as one unit; the
   credit's Cultre mark stays its brand red, being the one brand-colour element.
   Element sizes come from each window's own aspect: symbol 1123x1108 px is
   1.0126:1, wordmark 1208x100 px is 12.08:1.
   BOTTOM OFFSETS derive from the credit: its top is 1.1em + 3.1em = 4.2em.
   Wordmark sits at 5.0em (0.8em air), symbol at 6.7em (0.7em above the word).
   Cluster top ~11.3em; its left edge (~12.1em wide wordmark) stays right of
   the fact band's 0.80 cutoff, so the cloth text cannot reach it. */
/* SEVENTH REVISION (Vivek 07:42): the corner is TWO MATCHED CREDIT ROWS.
       [HH symbol]  Handloom Haat
       Designed by  [cultre mark]
   The upper row is built with the framework credit's OWN construction, copied
   from kiosk.css .saver-credit: flex, align-items center, .7em gap, right
   1.4em. That flex centring is exactly how "Powered by" centres on the cultre
   mark, so the two rows are the same mechanism, not a resemblance. Together
   they read as one statement: Handloom Haat, designed by cultre. The credit's
   own wording changes quiz-locally through Kiosk.i18n ('ui.poweredBy' ->
   "Designed by" / "roopankan"), no framework file touched.
   BOTTOM 4.55em = the credit's 1.1em + its 3.1em lockup + .35em air: close
   enough to pair as two lines of one block, far enough not to crowd. */
/* ELEVENTH REVISION (Vivek 16 Aug 21:08): the lockup returns to the TOP RIGHT
   and stacks the way the cultre credit stacks: mark above, name below, both
   centred on one axis. The construction is unchanged, only the flex axis
   turns vertical and the anchor moves from bottom to top. The credit row
   ("Designed by · cultre") stays alone at bottom right; the two corners now
   mirror each other: programme lockup top, designer lockup bottom.
   TOP 1.1em = the same breathing room the credit takes from its edge, and the
   same offset the language pill takes at top left, so the three corner
   elements sit on one margin rhythm. GAP .35em ~ the optical gap between the
   cultre mark and its own word in the credit lockup. */
.quiz-saver-hh-row {
  position: absolute; z-index: 4; pointer-events: none;
  right: 1.4em; top: 1.1em;
  display: flex; flex-direction: column; align-items: center; gap: .35em;
}
/* The symbol: a measured window onto the brand artwork, cut at the asset's
   real alpha bounds (x 0.146-0.4658, y 0.2765-0.7231 of 3508x2481), scaled to
   sit as the mark of its row the way the 3.1em cultre lockup is the mark of
   the row below. pos = f0/(1-span) is the percent-position mapping. */
/* NINTH REVISION (Vivek 07:50): the whole HH row scales UNIFORMLY so the
   wordmark's letter height equals the "Designed by" line's letter height.
   Everything is derived from the credit's own font-size formula,
   max(18px, .46em), so the two rows cannot drift apart at any type size:
   Lato's cap height runs ~0.72 of font size, the wordmark crop is exactly
   cap-high, so wordmark height = credit-size x .72; width follows the
   artwork's 12.08:1; and the symbol keeps the row's previous 3.94:1 ratio to
   the wordmark, shrinking with it rather than staying large over a smaller
   name. */
/* THIRTEENTH REVISION (Vivek 16 Aug 21:31, "use the real logo"): the symbol
   stops being a measured crop-window into the big lockup PNG and becomes the
   STANDALONE BRAND ASSET, hh-emblem.png (1123x1110, alpha bbox = the full
   canvas, so the artwork defines its own edges). A hand-measured window can
   only approximate the mark; the dedicated asset IS the mark. Whole-image
   mask, center/contain, no size/position arithmetic left to drift. Width
   ratio 1.0117 = the asset's own 1123/1110. */
/* FOURTEENTH REVISION (Vivek 16 Aug 21:32, "correct the HH logos inside the
   quiz also", read together with 21:31's "use the real logo"): every mask is
   gone. A single-ink mask floods the emblem's WHITE WEAVE BANDS with the same
   colour as its grey bars, which is precisely the "slightly changed" look -
   the logo's own construction is two-tone and a mask cannot say two colours.
   The framework's .saver-mark has always rendered the artwork as a plain
   image; the quiz now does the same. background-image, real colours, scrim
   shadows kept for contrast over the cloth. */
.quiz-saver-hh-sym {
  height: calc(max(18px, .46em) * 2.84);
  width:  calc(max(18px, .46em) * 2.84 * 1.0117);
  background: url(../../framework/assets/hh-emblem.png) center / contain no-repeat;
  filter: drop-shadow(0 1px 4px rgba(8,6,4,.98))
          drop-shadow(0 0 10px rgba(8,6,4,.9))
          drop-shadow(0 0 22px rgba(8,6,4,.7));  /* matches .saver-mark img's own scrim */
}
/* TENTH REVISION (Vivek 17:31, supersedes the 07:46 brand-letterform revert):
   the name is LIVE TEXT in the credit line's exact voice again, every value
   copied from kiosk.css .saver-credit span so the two rows share one
   typography: sans 400 italic, capitalize + small-caps (H-andloom H-aat as
   P-owered B-y), .06em tracking, the same cream, the same four-layer scrim
   carrying contrast over the cloth. Subordination to the symbol holds: a
   credit-scale line beside a 2.84x mark, the same ratio the cultre block has
   between its mark and its words. Row layout and centring unchanged. */
/* TWELFTH REVISION (Vivek 16 Aug 21:29): the wordmark drops the italic. The
   italic belonged to the credit-row voice ("Powered by"); now that the lockup
   stands alone at top right it reads as a nameplate, and nameplates stand
   upright. Everything else of the credit voice stays: weight, small caps,
   tracking, cream, scrim. */
.quiz-saver-hh-word {
  font-family: var(--sans); font-weight: 400; font-style: normal;
  font-size: max(18px, .46em);
  letter-spacing: .06em; text-transform: capitalize;
  font-variant-caps: small-caps; font-variant: small-caps;
  color: var(--ink);
  text-shadow: 0 0 3px rgba(8,6,4,.98), 0 1px 5px rgba(8,6,4,.98),
               0 0 18px rgba(8,6,4,.92), 0 0 34px rgba(8,6,4,.75);
}
