/* attract layout: this station used to top-align its overlay at 22vh while
   the other five centred theirs, which put its action button about 40px
   higher than every neighbour. The attract screen is one screen across
   the gallery, so the layout is the framework's now too. */
/* attract screen: title, subtitle and the rule are defined ONCE in
   framework/kiosk.css now. The override that sat here made this station
   look different from its five neighbours for no reason anyone chose.
   Only the Devanagari line-height tuning stays local, because the strings
   differ. See the attract block in kiosk.css. */
/* Archival Deep Dive app styles.
   Portrait 1080x1920 on 22" ViewSonic TD2230. Single-column flows, 2-col grids.
   Framework (kiosk.css) supplies palette, type, .screen/.grid/.chip/.btn etc.

   ── the August 2026 refinement pass ──────────────────────────────────────────
   The complaint was "too basic, big buttons". Measured on the real panel the
   three faults behind it were: every target was a filled rectangle with a
   border and a separate caption bar under it (the default card component, not
   a considered surface for a textile gallery); the top and bottom thirds of a
   1920-tall panel were empty while three cards sat stranded in the middle; and
   the only surface with any imagery on it was the one home tile that happened
   to have a photograph.

   The answer everywhere below is the same one: THE PICTURE IS THE CARD. Where a
   scanned page exists it is shown edge to edge with nothing drawn round it;
   where none exists (the introduction, the four stories, every film and
   recording the content team has not delivered) the surface is a piece of
   authentic woven cloth from framework/weave-swatch.js, keyed so no two are
   alike. Captions sit in a gradient at the foot of the picture instead of in a
   filled bar bolted underneath it.

   TARGETS DID NOT SHRINK. Every control here still answers a fingertip at 60px
   or more on its short side, and the test suite measures it on all seven
   screens. The size is bought with padding, which is invisible, rather than
   with visible bulk, which is what "big buttons" was actually complaining
   about. */

/* portrait 1080px wide: 1.75vw ≈ 19px reading size */
html, body { font-size: clamp(15px, 1.75vw, 22px); }

/* plain <button> reset for our card-buttons (framework .btn/.chip/.key unaffected) */
button { font: inherit; color: inherit; background: none; border: 0; text-align: left; padding: 0; }

/* CRITICAL. An inactive screen must not be a hit target.
   kiosk.css sets `.screen { pointer-events: none }`, but pointer-events is NOT
   sealed for the subtree: any descendant that re-enables it (kiosk.css does so
   for `button`, `.touchable`, `.grid`, `.chip`, `.card`, `.seek`, `.pbtn`,
   `.osk .key`; this file does so for the scroll columns and the cards) becomes
   a live hit target even while its screen is faded out. Every `.screen` is
   `position:absolute; inset:0`, so the LAST such screen in the DOM sits on top
   of the whole viewport and swallows every touch. The kiosk looked alive but
   no button on it could ever be pressed.
   Kill hit-testing across the whole inactive subtree, immediately (no
   transition delay, so a tap during a screen fade cannot reach the outgoing
   screen), while leaving the opacity crossfade untouched. */
.screen:not(.active),
.screen:not(.active) * { pointer-events: none !important; }

/* The 64px floor now comes from kiosk.css (--touch), one number for every
   small target in all seven stations. Only the reading size is local. */
.chip { font-size: 1.05em; }

/* The screen is 1080px wide with `overflow:hidden` on html/body, so there is
   no horizontal scroll to rescue us. A single unbreakable string from the
   manifest (a long compound name, a pasted URL, a mis-keyed caption) would
   otherwise blow the layout sideways and push content off the right edge for
   good. `anywhere` (not `break-word`) is deliberate: it also shrinks the
   min-content width, so over-long flex/grid items such as a filter chip stop
   forcing their row wider than the screen. */
#saver, #saver *, .screen, .screen * { overflow-wrap: anywhere; }

/* The ONLY clamp left in this app, and it never fires on shipped copy: every
   screen heading here is two or three words ("Four stories", "Four exhibition
   books"). It is a guard against a hand-edited manifest title long enough to
   push the whole grid off the panel, which testing/test_archival_deep_dive.py
   injects deliberately. Visitor-facing text is never truncated: the captions
   that used to be clamped are now short at the source instead.
   No padding on the h2, so the clip cannot bleed a sliced third line. */
.topbar .title h2 {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ---------- Home pill clearance ----------
   kiosk.js builds a fixed "⌂ Home" pill top-right (measured x 918-1059 at
   1080 wide). Every non-home screen here opens with a .topbar whose flex
   title box runs to the right edge (measured right = 1050), so a long
   manifest title would paint underneath the pill. The pill stays in the
   framework corner, one control in the same place in all six apps, and the
   topbar reserves that corner instead: 9.6em ≈ 181px ends the title box at
   ≈899px, a ~19px gutter clear of the pill. The viewer has no topbar; its back
   control is top-LEFT, so the pill floats clear over the photograph there. */
.topbar { padding-right: 9.6em; }

/* ---------- screensaver ----------
   Both languages are painted at once (data-i18n-both), so the title block is
   twice as tall as it was and the type comes down to pay for it. The venue
   line that used to sit above it is gone: it told a visitor standing in the
   room the name of the room. */
#saver .overlay { display: flex; flex-direction: column; align-items: center; gap: .9em; }
/* The framework centres the overlay. On a 1920-tall portrait panel that put the
   title almost 40% down, in the dead band a standing adult reads past before
   they notice it. Anchor the block near the top of the reachable range so the
   title meets the eye at about a fifth of the way down, and let the woven
   pattern fill the glass below it. The weave canvas and the corner credit are
   position:absolute, so only the overlay moves. */
/* .saver-title font-size removed: it existed to cancel this station's 22px
   root so the attract title landed on the same 68px as the other five.
   The framework now sizes that title in pixels, so the compensation is
   not needed and would fight it. */
.saver-rule { width: 46%; margin: .2em 0; }
/* Devanagari sets longer and taller than Latin at the same size; the Hindi
   half of each pair is trimmed so the pair keeps its shape at 1080 wide. */
.saver-title [lang="hi"] { font-size: .88em; }
.saver-sub [lang="hi"],
#saver .pulse [lang="hi"] { font-size: .92em; }

/* ==========================================================================
   HOME
   Portrait 1080x1920 on a wall. Before this pass the header sat at 12vh, the
   three targets ended at about 66% and the bottom third of the glass (the
   third a child or a shorter adult can actually reach) was empty, while the
   middle target was a 400px-tall empty rectangle with two lines of type
   floating in the centre of it.

   Now the column runs the full height and the two large targets are pictures
   rather than boxes: the photograph, then the cloth. They flex 5:3, so the
   invitation to LOOK is always the dominant object no matter what the type
   size control is set to, and neither can collapse to a caption strip.
   ========================================================================== */
/* The header starts below the framework settings cluster whatever size that
   cluster measures, rather than at a hardcoded number that was only ever
   right for the language pill on its own. 7.6vh (146px) is what the
   composition wants and normally wins; the calc is the floor that keeps the
   kicker clear of the chrome if the cluster ever grows. */
.home-head { padding: max(7.6vh, calc(var(--kiosk-chrome-bottom, 104px) + 1.2em)) 2.2em .2em; }
/* The screen title has to outrank the tiles it introduces. The framework drops
   h1 from 3.4em to 2.6em in portrait, which on this station landed the title at
   49px while .tile-title below it sits at 2.7em (51px) in the same face, the
   same weight and the same colour: the captions were reading LOUDER than the
   thing they hang under. Restoring the landscape proportion here puts roughly a
   third more size between the two so the order is read, not guessed. */
.home-head h1 { font-size: 3.5em; }
.home-kicker { margin-bottom: .5em; }
.home-head .weave-rule { width: 62%; margin: .8em 0 0; }

.entry-tiles {
  display: flex; flex-direction: column; gap: 1.4em;
  padding: 1.4em 2.2em 4.5vh; flex: 1; min-height: 0;
}

/* A tile is a picture with type in it, so it has no fill, no outline and no
   caption bar. What separates it from the wall is the same thing that
   separates a mounted textile panel from the wall it hangs on: a shadow. */
.tile {
  position: relative; display: flex; flex-direction: column; justify-content: flex-end;
  overflow: hidden; border-radius: var(--radius);
  background: #0d0b08; box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  pointer-events: auto;
}
/* Press feedback has to survive losing the border, so it is now a gold ring
   drawn INSIDE the tile (an outline would be clipped by overflow:hidden and a
   border would move the picture by a pixel on every press). */
.tile:active { transform: scale(.99); }
.tile:active::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 2px var(--gold); pointer-events: none;
}

/* The picture layer. `inset: 0` rather than a flow-level image, so the caption
   gradient can sit over its foot instead of below its edge. */
.tile-art {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; border: 0;
}
.tile-cloth { background-color: #2c4a48; }   /* the ground while the cloth encodes */

/* The caption. A gradient, not a bar: the picture runs behind the words and
   fades into them, which is how a gallery label sits on an image. Measured on
   the two worst cases in this app (the cream margin of a scanned page, and the
   pale figuring of jamdani) the top stop has to reach .92 for --ink to hold
   AA, so it does. */
.tile-body {
  position: relative; z-index: 2;
  display: flex; flex-direction: column; gap: .3em;
  padding: 3.4em 1.9em 1.5em;
  background: linear-gradient(to top,
              rgba(13, 11, 8, .96) 0%, rgba(13, 11, 8, .92) 42%, rgba(13, 11, 8, 0) 100%);
}
/* Title and chevron on one baseline. The chevron used to float in the middle
   of the right edge as a separate object; there it read as a second control. */
.tile-line { display: flex; align-items: baseline; justify-content: space-between; gap: .8em; }
.tile-title { font-family: var(--display); font-weight: 400; font-size: 2.7em; line-height: 1.08; color: var(--ink); }
.tile-go { font-family: var(--display); font-size: 2.1em; color: var(--gold); flex: 0 0 auto; line-height: 1; }
/* Secondary line floor: at 1080px wide 1em lands near 19px, under the gallery's
   standing-reading floor. Hold it at the framework's shared --meta-size so it
   is legible on the 15.6-inch panel, not only the 32-inch. */
.tile-sub { color: var(--ink-dim); font-weight: 300; font-size: var(--meta-size);
            line-height: 1.5; max-width: 32em; }
#tile-photos-sub { color: var(--gold); font-weight: 600; letter-spacing: .1em;
                   text-transform: uppercase; font-size: max(18px, .78em); }

/* The one dominant choice on the screen. A standing visitor looks before they
   read, so the largest target opens the scanned volumes and carries a real
   page from one of them, full bleed. */
/* The catalogue pages ship with the kiosk, so the hero photograph loads. If it
   ever failed, app.js weaves cloth into the same <img> and the tile is still a
   picture; nothing on screen would mention a missing file. */
.tile-hero { flex: 5 1 0; min-height: 0; }
/* The second target has no photograph and never had one, so it is cloth by
   design rather than by fallback. */
.tile-woven { flex: 3 1 0; min-height: 0; }

/* The third target is a line, not a card: a hairline above it, a small woven
   marker, the label, and the chevron. Its touch height is bought with padding
   (1.5em top and bottom around a 4.2em marker is 132px measured), so it is an
   easy target without being a big button. */
.tile.slim {
  flex: 0 0 auto; flex-direction: row; align-items: center;
  justify-content: flex-start; gap: 1.2em;
  padding: 1.5em .4em 1.5em .2em;
  background: none; box-shadow: none; border-radius: 0;
  border-top: 1px solid rgba(237, 229, 216, .14);
}
.tile.slim:active { transform: none; background: rgba(201, 162, 75, .07); }
.tile.slim:active::after { box-shadow: none; }
.tile-chip {
  flex: 0 0 auto; width: 4.2em; height: 4.2em; border-radius: 10px;
  background-color: #5e2545; display: block;
}
.tile.slim .tile-body {
  flex: 1; background: none; padding: 0; gap: .15em;
}
.slim-title { font-size: 1.62em; }
.slim-sub { font-size: max(18px, .82em); }
.tile.slim .tile-go { font-size: 2.1em; padding-right: .3em; }

.search-ico { width: 1.9em; height: 1.9em; color: var(--gold); flex: 0 0 auto; }

/* ---------- shared scroll columns ---------- */
.intro-scroll, .detail-scroll, .search-scroll, .list {
  flex: 1; overflow-y: auto; overscroll-behavior: contain; pointer-events: auto;
}
.intro-scroll::-webkit-scrollbar, .detail-scroll::-webkit-scrollbar,
.search-scroll::-webkit-scrollbar, .list::-webkit-scrollbar { width: 0; }

/* ---------- introduction ----------
   The gallery photograph this screen was designed around was never delivered,
   so for a year it opened on the holding texture inside a bordered box. It now
   opens on a full-bleed piece of cloth with the title set into it, which is
   what the box was standing in for. */
.intro-scroll { padding: 0 2.2em 3em; }
/* One grid cell holding two layers, rather than an absolutely positioned
   picture inside a fixed-ratio box. The ratio still sets the height in every
   normal case, but if the gallery title ever runs long enough to make the
   caption taller than the picture, the row grows and the picture stretches to
   meet it (object-fit keeps it honest) instead of the title being clipped off
   the top. The title comes from the manifest and is hand-edited on site. */
.intro-hero {
  position: relative; margin: 0 -2.2em 1.8em; display: grid;
  overflow: hidden; background: #2a251d;
}
.intro-img {
  grid-area: 1 / 1; width: 100%; height: 100%; aspect-ratio: 4/3;
  object-fit: cover; display: block;
}
.intro-hero-cap {
  grid-area: 1 / 1; align-self: end;
  position: relative; z-index: 2; padding: 3.6em 2.2em 1.6em;
  background: linear-gradient(to top,
              rgba(13, 11, 8, .96) 0%, rgba(13, 11, 8, .9) 45%, rgba(13, 11, 8, 0) 100%);
}
.intro-hero-cap .eyebrow { margin-bottom: .5em; }
.intro-title { font-size: 2.5em; }
.intro-body p { font-weight: 300; line-height: 1.75; color: var(--ink-dim); font-size: 1.12em; margin-bottom: 1.1em; }
.intro-body p:first-of-type::first-letter {
  font-family: var(--display); font-size: 3.1em; line-height: .85;
  float: left; padding: .06em .12em 0 0; color: var(--gold);
}
.intro-more { display: flex; gap: 1em; margin-top: 1.6em; flex-wrap: wrap; }

/* ---------- the four exhibition books ---------- */
/* The search tool lives in the header, not on a row of its own: it is for a
   visitor who is already browsing, and a row above the grid costs book page. */
.cat-search { flex: 0 0 auto; font-size: 1em; padding: .7em 1.1em; gap: .6em; }
.cat-search .search-ico { width: 1.3em; height: 1.3em; color: currentColor; }

/* One honest line, Hindi only. Empty in English, and an empty box must not
   leave a gap where a line would have been. */
.books-lang { padding: 0 1.6em .6em; color: var(--ink); font-weight: 300; }
.books-lang:empty { display: none; }

/* The volume's own provenance line, printed once above its pages instead of
   164 times underneath them. */
.cat-note { padding: 0 1.6em .8em; color: var(--ink-dim); font-weight: 300; line-height: 1.5; }
.cat-note:empty { display: none; }

#chip-groups { padding: 0 .4em .6em; }
.chip-group { margin-bottom: .7em; }
.chip-label { padding: 0 1.6em .35em; }
.chip-group .chips { row-gap: .6em; }

/* `.grid` (kiosk.css) is `flex:1; overflow-y:auto`, so it has a DEFINITE height,
   and the card used to be `overflow:hidden`, which made each row's min-content
   contribution 0. Grid's "maximize tracks" step then grew all four auto rows
   equally to exactly fill the viewport (304px) instead of to their content
   (432px): every card was squashed, the caption was pushed past the card's
   clipped bottom edge, and the grid never scrolled.
   Pin the rows to their content height so cards keep their captions and the
   grid scrolls normally. */
#cat-grid { align-content: start; grid-auto-rows: max-content; gap: 1.6em 1.4em; }
/* A contact sheet, not a card grid. The scanned page IS the card: it carries
   its own paper edge and its own shadow, so a filled panel with a 1px outline
   round it added nothing except the look of an unfinished component. The
   caption is plain type on the gallery ground beneath it. */
.photo-card {
  background: none; border: 0; padding: 0;
  display: flex; flex-direction: column; pointer-events: auto;
}
.photo-thumb {
  width: 100%; aspect-ratio: 4/3; object-fit: cover; display: block;
  background: #0d0b08; border-radius: 8px;
  box-shadow: 0 6px 16px rgba(0, 0, 0, .5);
}
.photo-card:active { transform: scale(.985); }
.photo-card:active .photo-thumb { box-shadow: 0 0 0 2px var(--gold), 0 6px 16px rgba(0, 0, 0, .5); }
/* No line clamp. The clamp that used to sit here cut every caption off
   mid-sentence with an ellipsis and gave the visitor no way to read the rest.
   It is not needed any more either: the caption is now the page reference
   alone, because the volume's provenance line is printed once above the set
   rather than repeated on all 164 of its pages. Nothing here is truncated. */
.photo-cap {
  padding: .7em .2em 0; font-weight: 300; color: var(--ink-dim); line-height: 1.45;
}

/* ---------- zoom viewer ---------- */
#viewer { background: #000; }
.zoomstage { flex: 1; position: relative; overflow: hidden; touch-action: none; pointer-events: auto; }
.zoomframe { position: absolute; inset: 0; transform-origin: center center; will-change: transform; }
.zoomframe img { width: 100%; height: 100%; object-fit: contain; display: block; }
.viewer-back { position: absolute; top: var(--pillsafe); left: 1em; z-index: 5; background: rgba(22, 19, 15, .72); }
/* No filled panel and no rule across the glass. The photograph is letterboxed
   on black already, so the caption simply sits on that black, which is what
   makes the picture the whole screen rather than the top two thirds of it.

   THE max-height IS LOAD-BEARING, not styling. .zoomstage is `flex: 1` and a
   scroll container, so it can shrink to nothing, while this panel is a plain
   flex item that cannot shrink below its own text. A caption long enough (a
   curator pasting a paragraph into the manifest, which the test suite injects)
   therefore grew the panel past the bottom of the screen and, because a
   `.screen` is `position:absolute; inset:0` with no clipping of its own, past
   the bottom of the DOCUMENT: measured at 2258px on a 1920px panel. From then
   on any element.scrollIntoView() that could not be satisfied inside an inner
   scroller scrolled the VIEWPORT instead (html{overflow:hidden} stops a finger,
   not a script), every screen sat 338px too high, and the back button was off
   the top of the glass for the rest of the session.
   Bounded here, and the caption scrolls on its own instead. Nothing is
   truncated: a long caption is still readable in full, it is just read by
   moving it rather than by moving the kiosk. */
.viewer-cap {
  padding: 1.4em 1.9em 1.8em; display: flex; flex-direction: column; gap: .45em;
  max-height: 34vh; overflow-y: auto; overscroll-behavior: contain;
}
.viewer-cap::-webkit-scrollbar { width: 0; }
.viewer-cap p { font-family: var(--serif); font-weight: 400; line-height: 1.45;
                color: var(--ink); font-size: 1.5em; }
.zoom-hint { color: var(--gold); opacity: .75; }

/* ==========================================================================
   THE FOUR STORIES
   Four cards, each a different cloth chosen for the story it carries (app.js
   TOPIC_CLOTH). They flex to fill the column, so the screen has no dead band
   at the foot and no row of small boxes stranded in the middle.
   ========================================================================== */
.list { display: flex; flex-direction: column; gap: 1.3em; padding: 1.3em 2em 2.4em; }
.topic-card {
  position: relative; overflow: hidden; border-radius: var(--radius);
  background: #2a251d; box-shadow: 0 10px 28px rgba(0, 0, 0, .42);
  display: flex; flex-direction: column; justify-content: flex-end;
  min-height: 13em; pointer-events: auto;
}
/* Grow to fill the column, but NEVER shrink below the card's own content.
   flex-shrink:1 here would let four cards share a fixed column height and clip
   their own titles off the top the moment a story's standfirst ran long
   (justify-content is flex-end, so what overflows is the top). The list is a
   scroller; a long story is allowed to push the column past the fold, and
   nothing on a gallery wall is cut off. */
#topics-list .topic-card { flex: 1 0 auto; }
.topic-art { position: absolute; inset: 0; display: block; }
.topic-card:active { transform: scale(.99); }
.topic-card:active::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 2px var(--gold); pointer-events: none;
}
.topic-card-body {
  position: relative; z-index: 2; padding: 3em 1.7em 1.4em;
  display: flex; flex-direction: column; gap: .4em;
  background: linear-gradient(to top,
              rgba(13, 11, 8, .96) 0%, rgba(13, 11, 8, .9) 46%, rgba(13, 11, 8, 0) 100%);
}
.topic-card-title { font-family: var(--display); font-weight: 400; font-size: 2.15em; color: var(--ink); line-height: 1.1; }
.topic-card-sum { font-weight: 300; color: var(--ink-dim); line-height: 1.5; font-size: var(--meta-size); }
.topic-card-more { color: var(--gold); letter-spacing: .1em; margin-top: .1em; }

/* ---------- topic detail ---------- */
.detail-scroll { padding: 0 2.2em 3em; }
/* The standfirst sits in the story's own cloth, full bleed, so a reader meets
   a woven surface before a column of body text. */
.topic-banner {
  position: relative; margin: 0 -2.2em 1.6em; min-height: 19em;
  display: flex; flex-direction: column; justify-content: flex-end;
  overflow: hidden; background: #2a251d;
}
.topic-summary {
  position: relative; z-index: 2; padding: 4em 2.2em 1.7em;
  font-family: var(--serif); font-style: italic; font-size: 1.4em;
  color: var(--gold); line-height: 1.45;
  background: linear-gradient(to top,
              rgba(13, 11, 8, .96) 0%, rgba(13, 11, 8, .9) 48%, rgba(13, 11, 8, 0) 100%);
}
/* Shown only when a story has no Hindi text yet: names the language the
   visitor is about to read, immediately above the English they will get. */
.lang-note { color: var(--ink); font-weight: 300; margin-bottom: 1em; }
.lang-note:empty { display: none; }
.topic-body p { font-weight: 300; line-height: 1.75; color: var(--ink-dim); font-size: 1.1em; margin-bottom: 1.05em; }
.media-head { color: var(--gold); margin: 1.6em 0 .9em; letter-spacing: .04em; }
.media-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.6em 1.4em; }
/* Same contact-sheet treatment as the book pages: the picture is the card. */
.media-card {
  background: none; border: 0; padding: 0;
  display: flex; flex-direction: column; pointer-events: auto;
}
.media-card:active { transform: scale(.985); }
.media-thumb, .media-face {
  width: 100%; aspect-ratio: 16/10; display: block; background: #2a251d;
  border-radius: 8px; box-shadow: 0 6px 16px rgba(0, 0, 0, .5); overflow: hidden;
}
.media-thumb { object-fit: cover; }
.media-card:active .media-thumb, .media-card:active .media-face {
  box-shadow: 0 0 0 2px var(--gold), 0 6px 16px rgba(0, 0, 0, .5);
}
/* A film or a recording has no still to show, so it gets its own cloth with
   the transport mark set into it. It reads as a piece of the collection
   rather than as a slot waiting for one. */
.media-face {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .6em;
}
/* The two marks on a woven face carry their own legibility rather than making
   the whole card go dark for them: a near-solid ink disc under the gold
   transport glyph (7.4:1 measured, against 3.9:1 over bare cloth), and the
   framework's solid gold badge for "Coming soon". That is what lets a film
   card be as vivid a piece of cloth as the photograph card beside it, instead
   of the two reading as different kinds of object. */
.media-icon {
  position: relative; z-index: 2;
  width: 2.4em; height: 2.4em; border: 1.5px solid var(--gold); border-radius: 50%;
  background: rgba(13, 11, 8, .88); color: var(--gold); font-size: 1.6em;
  display: flex; align-items: center; justify-content: center; padding-left: .12em;
  box-shadow: 0 0 18px 10px rgba(13, 11, 8, .5);
}
.media-face.audio .media-icon { padding-left: 0; }
.media-cap { padding: .55em .2em 0; line-height: 1.4; color: var(--ink-dim); font-weight: 300; }
.media-kind { padding: .7em .2em 0; }
/* The card is dimmed, but the label that stops the dead tap is not: it is the
   framework's solid .badge-state (gold ground, --bg ink, 7.7:1), so it is the
   loudest small thing on a card that does nothing. */
.media-card.coming .media-face { opacity: .8; }
.media-card.coming .media-cap, .media-card.coming .media-kind { opacity: .75; }
.media-card.coming .media-icon { border-style: dashed; color: var(--ink-faint); }
.coming-tag { position: relative; z-index: 2; }

/* ---------- search ---------- */
.query-box {
  margin: .2em 1.6em .9em; min-height: 2.6em; border-bottom: 2px solid var(--gold);
  display: flex; align-items: center; padding: .3em .4em;
  font-family: var(--serif); font-size: 1.7em; color: var(--ink); letter-spacing: .04em;
}
.caret { width: 2px; height: 1.15em; background: var(--gold); margin-left: 3px; animation: caret 1.1s steps(1) infinite; }
@keyframes caret { 0%, 60% { opacity: 1; } 61%, 100% { opacity: 0; } }
.search-scroll { padding: 0 1.6em; }
.search-hint { color: var(--ink-faint); font-weight: 300; padding: 1em .2em; }
/* A section label above a set of results, with a hairline running out from it
   so the two sets read as two sets without either needing a box. */
.result-head {
  color: var(--gold); margin: 1.4em 0 .9em; padding-bottom: .5em;
  border-bottom: 1px solid rgba(201, 162, 75, .28);
}
.result-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.6em 1.4em; }
.result-list { display: flex; flex-direction: column; gap: 1.2em; }
/* A result is a hit, not a destination screen, so the cards do not need the
   full height they take on the stories screen. */
.result-list .topic-card { min-height: 11em; }
#osk-host { background: var(--bg-raised); border-top: 1px solid #3a332a; }

/* The local scrub-bar correction that used to live here has been removed, as
   its own note asked: kiosk.css `.seek` is now content-box with an explicit
   8px rail inside a 64px target, so the framework gets it right for every app
   and a local height here would only stack onto that padding. */

/* ---------- shared empty state ---------- */
.empty-note {
  grid-column: 1 / -1; text-align: center; color: var(--ink-faint); font-weight: 300;
  border: 1px dashed #3a332a; border-radius: var(--radius); padding: 2.2em 1.4em;
}

/* ---------- transient message ----------
   Shown when a film or recording that IS on disk turns out to be unplayable, so
   a visitor whose player just closed is told why instead of being dropped back
   with no explanation. Must clear the player overlay, hence the high z-index. */
.add-toast {
  position: fixed; left: 50%; bottom: 6vh; transform: translateX(-50%);
  z-index: 90; pointer-events: none;
  max-width: 80vw; text-align: center;
  background: rgba(22, 19, 15, .96); color: var(--ink);
  border: 1px solid var(--gold); border-radius: var(--radius);
  padding: 1em 1.8em; font-size: 1.05em; line-height: 1.5;
  opacity: 0; transition: opacity .35s ease;
}
.add-toast.show { opacity: 1; }


/* ── 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; }
.grid, .list, .intro-scroll, .detail-scroll, .search-scroll,
.viewer-cap { touch-action: pan-y; }
/* Second line of defence behind the .viewer-cap bound above. A `.screen` is
   position:absolute inset:0 with no clipping, so ANY future unbounded string on
   ANY screen would extend the document's scroll height and reintroduce the
   viewport-scroll failure described there. Clipping one screen's overflow is a
   local, visible fault that a curator can see and fix in the manifest; a
   viewport scrolled 338px up is a kiosk that has to be rebooted, and it looks
   identical to a dead touchscreen. */
.screen { overflow: hidden; }
/* .zoomstage keeps touch-action:none (below): its pinch/pan is the app's own. */

/* ==========================================================================
   GALLERY LEGIBILITY AUDIT (app-local corrections)
   Panel: 22" 1080x1920 portrait. Root font here is 18.9px, so the framework's
   .82em .caption lands at 15.5px, well under the ~18px gallery floor, and
   --ink-faint measures 3.84:1 on --bg, 3.48:1 on --bg-raised, 3.15:1 on
   --bg-card, all under AA body 4.5:1. Every selector below is app-owned; the
   framework-wide .caption fix is reported separately.
   ========================================================================== */
/* ==========================================================================
   SETTINGS CLUSTER CLEARANCE
   kiosk.js builds a fixed cluster top-LEFT on every screen including the
   screensaver: the language toggle, and since Aug 2026 the type-size control
   beside it. Three app controls sat under the language pill on its own (the
   .topbar back button, the viewer's own back button, and the home kicker), and
   the cluster is now roughly twice as wide and a little taller.
   The framework measures its own chrome and publishes the real number, which
   is the only value that stays true when the Devanagari faces load or the type
   size changes, so --pillsafe is now that measurement rather than a constant
   this app guessed. The 5.4em fallback is the previously measured value and
   only applies for the single frame before kiosk.js measures.
   ========================================================================== */
:root { --pillsafe: var(--kiosk-chrome-bottom, 5.4em); }

.topbar        { padding-top: var(--pillsafe); }
/* .viewer-back takes --pillsafe where it is declared, above. The home screen
   needs no rule of its own: .home-head already floors its own top padding on
   the same measurement. */

/* ==========================================================================
   HINDI TYPOGRAPHY
   Devanagari is taller than Latin (matras above, conjuncts and nukta below)
   and its glyphs must not be tracked apart: letter-spacing pulls a matra off
   its consonant and reads as a spelling error, not as styling. Every tracked
   run in this app is therefore reset for Hindi, and the tight display
   line-heights are opened up so ि/ी and ृ/ॢ are not clipped by the line box.
   ========================================================================== */
/* NAME THE BUNDLED FACE, do not assume it is inherited.
   kiosk.css sets the Devanagari face on `body` and on h1/h2/h3, .btn and
   .homepill. Any class that names a face of ITS OWN overrides that inheritance,
   and every one below does: .tile-title and .topic-card-title name var(--display)
   (Cormorant Garamond) and .eyebrow names var(--sans) (Lato). Neither family
   contains a single Devanagari glyph, so on a Hindi kiosk these nine runs fell
   through to whatever the machine happened to supply.

   This is invisible on the build box, which is why it survived a passing Hindi
   walk: Linux here has dozens of system Devanagari fonts, so every glyph renders
   and every screenshot looks right. The gallery panels are sealed Windows boxes.
   Windows ships Nirmala UI, which does cover Devanagari, so the likely result
   there is not empty boxes but the WRONG face: one line of Hindi in Nirmala UI
   beside another in the bundled Noto, on the same screen, at the same size.

   Measured on the shipped folder in Hindi: .topic-card-title x4, .tile-title x3,
   .eyebrow x2 on home, topics and intro, plus .topic-summary and .viewer-cap p
   on the two screens a visitor only reaches at depth. Serif for everything set
   in a serif so the design's serif-against-sans distinction survives the
   language switch, sans for the eyebrow so it matches its own Latin.

   Note that the first pass at this fix MISSED the last two, because the probe
   that found the first seven walked the five screens reachable from home and
   not the two below them. The suite assertion added alongside this walks all
   seven and is what caught them. A partial audit of a whole-app property reads
   exactly like a clean one.

   Each selector keeps its OWN Latin fallback behind the Devanagari face rather
   than sharing one. Latin still appears on these elements in Hindi (the
   publication titles are printed forms and stay Latin by design), so collapsing
   .tile-title onto var(--serif) would quietly restyle those runs from Cormorant
   to EB Garamond. The added family is the only intended change. */
html[lang="hi"] .tile-title,
html[lang="hi"] .topic-card-title { font-family: 'Noto Serif Devanagari', var(--display); }
html[lang="hi"] .topic-summary,
html[lang="hi"] .viewer-cap p     { font-family: 'Noto Serif Devanagari', var(--serif); }
html[lang="hi"] .eyebrow          { font-family: 'Noto Sans Devanagari', var(--sans); }

html[lang="hi"] .chip-label,
html[lang="hi"] .eyebrow,
html[lang="hi"] #tile-photos-sub,
html[lang="hi"] .media-kind,
html[lang="hi"] .topic-card-more,
html[lang="hi"] .coming-tag,
html[lang="hi"] .zoom-hint,
html[lang="hi"] .query-box,
html[lang="hi"] .media-head,
html[lang="hi"] #viewer-meta,
html[lang="hi"] .screen .btn,
html[lang="hi"] .caption { letter-spacing: normal; }
/* The screensaver paints both scripts at once, so its Devanagari cannot hang
   off html[lang]: the pair is styled by its own [lang="hi"] span above. */
.saver-title [lang="hi"], .saver-sub [lang="hi"],
#saver .pulse [lang="hi"] { letter-spacing: normal; line-height: 1.4; }

/* line boxes that were sized for Latin */
html[lang="hi"] h1,
html[lang="hi"] .intro-title   { line-height: 1.4; }
html[lang="hi"] .topbar .title h2 { line-height: 1.45; }
html[lang="hi"] .tile-title    { line-height: 1.35; }
html[lang="hi"] .topic-card-title { line-height: 1.4; }
html[lang="hi"] .topic-summary { line-height: 1.6; }
html[lang="hi"] .photo-cap     { line-height: 1.6; }
html[lang="hi"] .media-cap     { line-height: 1.55; }
html[lang="hi"] .intro-body p,
html[lang="hi"] .topic-body p  { line-height: 1.9; }
html[lang="hi"] .tile-sub      { line-height: 1.65; }
/* A baseline-aligned row of Latin puts the chevron on the title's baseline.
   Devanagari hangs from the shirorekha, so the same row drops the chevron
   below the letterforms; centring the row is what reads level in Hindi. */
html[lang="hi"] .tile-line     { align-items: center; }

/* Devanagari runs longer than English for the same sentence; the display
   sizes are trimmed a little so the home tiles hold their shape at 1080px
   wide. */
html[lang="hi"] .tile-title    { font-size: 2.4em; }
html[lang="hi"] .slim-title    { font-size: 1.48em; }
html[lang="hi"] .topic-card-title { font-size: 1.95em; }
html[lang="hi"] .intro-title   { font-size: 2.2em; }

/* A drop cap is a Latin convention. On Devanagari ::first-letter takes the
   bare consonant and leaves its matra behind at body size, which reads as a
   broken word, and no Delhi museum sets Hindi that way. */
html[lang="hi"] .intro-body p:first-of-type::first-letter {
  font-family: inherit; font-size: inherit; line-height: inherit;
  float: none; padding: 0; color: inherit;
}

/* The right-hand gutter that clears the ⌂ Home pill (9.6em, set above) is
   NOT reduced for Hindi: "⌂ होम" sets no narrower than "⌂ Home" once the
   Devanagari serif's side bearings are counted, and the test suite measures
   the real overlap in both languages rather than trusting either guess. */

/* the chevron kept its own span so data-i18n can swap only the word */
.chev { line-height: 1; }

/* Small lines held at or above the gallery's 18px standing-reading floor.
   --meta-size is the framework's shared number for a card's secondary line.
   The em part is .86 rather than the framework .eyebrow's .72 so these lines
   still GROW when a visitor presses the large type step. At .72 the floor wins
   at every step and the one control a partially-sighted visitor came for does
   nothing to the labels, which is the opposite of what it is for. */
.eyebrow,
#tile-photos-sub { font-size: max(18px, .86em); }
.chip-label    { font-size: max(18px, .86em); letter-spacing: .14em;
                 text-transform: uppercase; color: var(--gold); font-weight: 600; }
.zoom-hint,
#viewer-meta   { font-size: max(18px, .82em); }
/* NOT uppercased, unlike the other tracked cap lines. This one cites a
   publication, and a publication keeps the form it was printed in: "The Master
   Weavers" is a title, not a label. */
#viewer-meta   { color: var(--gold); text-transform: none; }
.zoom-hint     { color: var(--gold); opacity: .9; }
.topic-card-more { font-size: max(18px, .8em); }
.search-hint   { color: var(--ink-dim); }
.photo-cap     { font-size: max(18px, .88em); }
.media-kind    { font-size: max(18px, .86em); letter-spacing: .14em;
                 text-transform: uppercase; color: var(--gold); font-weight: 600; }
.media-cap     { font-size: max(18px, .88em); }

/* ==== HH LOGO STYLE, PROPAGATED FROM THE QUIZ (Vivek 16 Aug 21:52) =========
   The quiz's approved logo treatment, applied verbatim (see apps/quiz/app.css
   revisions 11-14 for the full reasoning):
   1. Attract screen: stacked lockup top right - the REAL standalone emblem
      (hh-emblem.png, two-tone, rendered directly, never a single-ink mask)
      over "Handloom Haat" in the credit's voice, upright, small caps.
   2. The framework's horizontal .saver-mark img is hidden; the lockup stands
      in its place.
   3. In-app screens: NO top-right HH mark at all; the framework's bottom-left
      .wm-hh watermark (untouched) is the only in-app HH presence.
   FRAMEWORK CANDIDATE: this block is now duplicated across all six apps and
   belongs in kiosk.css/weave-saver.js on the next framework pass. */
.saver-mark img { display: none; }
.brand-watermarks .wm-hh-top { display: none; }
.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;
}
.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));
}
.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);
}

/* v2.2.1, 17 Aug, VIVEK'S ITEM 3: "logos OUT of the weave and motif areas
   entirely", read together with his item 4, the khadi border bands. The marks
   have MOVED rather than been deleted: framework/weave-saver.js now weaves a
   khadi border between weave sections carrying one institution's emblem
   repeated saree-border style, cycling per border. So the attract screen's
   top-right lockup is retired here, because the identity it carried is now IN
   the cloth where he asked for it, and leaving both would show the same marks
   twice on one screen.
   HIDDEN, NOT DELETED, and deliberately so: this block is Vivek-approved work
   from 16 Aug 21:52 and the rules below it (.saver-hh-sym/-word geometry) are
   the record of how that lockup was built. One line restores it if he wants it
   back after seeing the borders. Scope: this is the ONLY change made in these
   five directories, per Bob's 11:07 extension.
   NOT TOUCHED: the framework's bottom-left .wm-hh watermark, and the quiz's
   own bottom-right cluster, which is separately Vivek-approved. */
.saver-hh-row { display: none; }
