/* The slide renderer's stylesheet: the token defaults every layout reads, and the layout rules
   themselves. Lifted out of engine/index.html so that the LMS's deck editor can
   render a slide exactly as the tablet does — see engine/render.js for why that matters.

   Nothing here is client-specific and nothing may become so: every colour is a token defined in
   :root below and overwritten at boot from brand.tokens.json. build.js's guardEngine() fails the
   build on a hex colour anywhere outside that :root block, which is the rule that keeps this
   stylesheet renderable in anyone's palette.

   What did NOT come with it: the rail, the picker, the landing screen, the debug panel. Those are
   the presenter application, not the slide. */

  :root {
    --canvas:#14181C; --surface:#F2F2F0; --primary:#3A3A3A; --ink:#1A1A1A;
    /* THE GROUND RAMP: two tones on the light side and two on the dark, so a plate can always be
       told from whatever it sits on. Tokens and not computed tints, because a client whose
       surface is already dark needs to CHOOSE the step rather than have one derived from a
       brightness rule that only works in one direction.

       --panel was #E8E8E4 and was widened. Measured against every surface any
       brand.tokens.json sets, plus this default, it came to between 1.049:1 and 1.132:1 -- so
       the zebra stripe on .l-rows.boxed was, in plain terms, not there, and the `boxed` adjective
       was decorative. Roughly 1.2:1 is where a tone step stops being deniable on a screen;
       #D9D9D5 is the lightest value clearing that against all of them. test/roles.test.js
       re-derives this from the token files, so a new palette that breaks it fails the suite. */
    --panel:#D9D9D5;
    /* The same step on the dark side, and the reason it has to exist: a deck with a navy page and
       a brighter navy callout has TWO dark grounds, and a single dark token flattens them into
       each other. On the training corpus that is 10 plates across 10 slides -- among them a cover
       whose two-tone split disappeared and a stat row whose callout panel dissolved into the
       page. Kept under lum 0.3 so the text map still treats it as a dark ground. */
    --panel-dark:#2A2A2C;
    /* A quiet tier in each direction, so a footnote stays a footnote instead of being promoted to
       body text -- 324 runs of footnote grey in the training corpus depend on it. Each is the
       lightest (resp. darkest) neutral clearing 4.5:1 against EVERY ground on its side of the
       ramp, --panel and --panel-dark included. Measuring them one at a time against the ground
       they usually land on produced values that both failed the moment another ground moved. */
    --muted-ink:#5F5F5B;
    --muted-light:#91918E;
    /* RED, WHICH THE IMPORTER NEVER PRODUCES. Every other token on this ramp exists because the
       recognisers have to resolve a source colour onto something; these two exist because an
       author asks for them. A source #FF0000 still resolves to a dark ground, as it always did --
       nothing about importing a deck changed.

       Two of them for the same reason there are two quiet tiers: one value cannot clear 4.5:1
       against a near-white page AND a near-black one. --alert is the darkest side, measured
       against every surface any brand.tokens.json ships plus this default (worst 4.58:1), and it
       doubles as a red plate with --surface on it. --alert-light is for text on a dark ground
       (worst 4.68:1) and is TEXT ONLY: at that lightness it lands in the middle of the ramp where
       neither ink separates from it, which is the same trap the mid band was removed for.
       test/roles.test.js re-derives both, so a new client surface that breaks them fails. */
    --alert:#C00000;
    --alert-light:#FF5A5A;
    /* A highlighter band laid over text, always drawn translucent (its alpha travels on the node).
       The deck's palette overrides it when the deck uses its own. The theme accents have no
       default here: an accent a palette doesn't name is drawn as --primary (render.js tone()). */
    --highlight:#FFC000;
    --rail-bg:#22262A; --rail-rule:#4A4D50; --rail-muted:#9C9D9F; --rail-active:#F2F2F0;
    --hot-border:rgba(26,26,26,.55); --hot-fill:rgba(26,26,26,.06);
    --hot-fill-seen:rgba(26,26,26,.14);
    /* A drop shadow under the before/after handle. rgba, like the two above it and for the same
       reason: a degree of darkness is not a colour a brand chose, and tinting it with a role
       would make the handle disappear on the palettes whose ink is pale. */
    --hot-scrim:rgba(26,26,26,.38);
    --rail-h: 74px;
    /* engine chrome — belongs to nobody, not client-overridable */
    --scrim:rgba(0,0,0,.94);
    --dbg-bg:#0F1216; --dbg-fg:#D6DADE; --dbg-btn:#1E242B; --dbg-btn-edge:#323A43;
    --warn-bg:#8A1C1C; --warn-fg:#FFFFFF;
    /* Engine type and space scale. Layout code never writes a literal size or gap; it uses
       these. They belong to the engine, not a client -- a client controls colour and font
       family, not the rhythm of the grid. Sized for a landscape iPad, which is the only
       target: 1180x820 CSS px. */
    --sp-1:4px; --sp-2:8px; --sp-3:14px; --sp-4:22px; --sp-5:34px; --sp-6:48px;
    --fs-kicker:13px; --fs-title:34px; --fs-lead:19px; --fs-label:18px;
    --fs-detail:16px; --fs-statement:30px; --fs-attrib:15px; --fs-caption:13px;
    --lh-tight:1.2; --lh-body:1.5;
  }

  /* ---------- layout scaffold ----------
     Every layout fills the card and reads tokens for all of it. No layout rule below names a
     colour, a font or a spacing value: they come from :root above, which is the single place
     the engine's defaults live and the single place brand.tokens.json overwrites. */
  .l { position:absolute; inset:0; display:flex; flex-direction:column;
       padding:var(--sp-5); overflow:hidden;
       color:var(--ink); background:var(--surface);
       font-family:var(--font, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif); }

  /* FRAME layouts are the takeover slides: they sit on the dark canvas rather than the light
     card, which is what makes a cover read as a cover and not as a content slide. */
  .l.frame { background:var(--canvas); color:var(--surface);
             align-items:center; justify-content:center; text-align:center; }

  .l .kicker { font-size:var(--fs-kicker); letter-spacing:.14em; text-transform:uppercase;
               font-weight:bold; color:var(--primary); margin-bottom:var(--sp-2); }
  .l.frame .kicker { color:var(--rail-active); }
  .l .title { font-size:var(--fs-title); line-height:var(--lh-tight); font-weight:bold;
              margin:0 0 var(--sp-3); }
  .l .lead { font-size:var(--fs-lead); line-height:var(--lh-body); margin:0 0 var(--sp-4);
             opacity:.78; }
  .l .sub { font-size:var(--fs-lead); line-height:var(--lh-body); margin:var(--sp-2) 0 0;
            opacity:.72; }

  /* cover may carry a background image; the text sits over a scrim so it stays legible
     whatever the photograph is doing underneath. */
  .l-cover .bgimg { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
  .l-cover .scrim { position:absolute; inset:0; background:var(--scrim); opacity:.55; }
  .l-cover .inner { position:relative; max-width:22em; }

  .l-pull .quote { font-size:var(--fs-statement); line-height:var(--lh-tight); font-weight:bold;
                   margin:0; max-width:20em; }
  .l-pull .attrib { font-size:var(--fs-attrib); margin-top:var(--sp-4); opacity:.7; }
  .l-close .statement { font-size:var(--fs-statement); line-height:var(--lh-tight);
                        font-weight:bold; margin:0; max-width:20em; }

  /* labeled-rows: the workhorse. A bold label and an optional detail beside it. */
  .l-rows .rows { display:flex; flex-direction:column; gap:var(--sp-3);
                  overflow:hidden; flex:1; }
  .l-rows .row { display:grid; grid-template-columns:minmax(8em,15em) 1fr; gap:var(--sp-4);
                 align-items:baseline; padding-bottom:var(--sp-3);
                 border-bottom:1px solid var(--border, var(--rail-rule)); }
  /* The last row has no rule under it, so its padding is just dead space at the bottom of
     the list: 14px that 23 of the corpus's slides needed and did not have. */
  .l-rows .row:last-child { border-bottom:0; padding-bottom:0; }
  .l-rows .row .label { font-size:var(--fs-label); font-weight:bold; color:var(--primary); }
  .l-rows .row .detail { font-size:var(--fs-detail); line-height:var(--lh-body); }
  .l-rows .row.nodetail { grid-template-columns:1fr; }

  /* THE ADJECTIVES. Every one of these is drawn from a token, never from a value on
     the slide — that is what keeps one restyle covering the whole deck. 69 corpus slides are
     built as plates and arrived as bare text, because nothing read a shape's fill until now. */
  .l-rows.boxed .rows { gap:0; }
  .l-rows.boxed .row { padding:var(--sp-3) var(--sp-4); align-items:center; }
  /* The stripe. Two tones, both from the palette, so a greyscale deck stripes in greys. */
  .l-rows.boxed .row:nth-child(odd) { background:var(--panel, var(--surface)); }
  .l-rows.boxed .row:nth-child(even) { background:var(--surface); }
  .l-rows.boxed .row + .row { border-top:1px solid var(--border, var(--rail-rule)); }
  /* The label in a badge. A fixed column so the badges line up down the slide however long the
     labels are, which is the whole reason an author draws them. */
  .l-rows.badged .row { grid-template-columns:minmax(7em,11em) 1fr; gap:var(--sp-3); }
  .l-rows.badged .row .label {
    background:var(--primary); color:var(--surface);
    padding:var(--sp-2) var(--sp-3); border-radius:var(--sp-1);
    align-self:stretch; display:flex; align-items:center;
  }

  /* prose: paragraphs as paragraphs, with an optional framed image beside them. */
  .l-prose .wrap { display:flex; gap:var(--sp-5); flex:1; overflow:hidden; }
  .l-prose .body { display:flex; flex-direction:column; gap:var(--sp-3); flex:1;
                   font-size:var(--fs-detail); line-height:var(--lh-body); }
  .l-prose .body p { margin:0; }
  .l-prose figure { margin:0; flex:0 0 38%; align-self:flex-start; }
  .l-prose figure img { width:100%; height:auto; display:block; border-radius:var(--sp-1);
                        border:1px solid var(--border, var(--rail-rule)); }

  /* A PANEL, not an inset. 62 corpus slides put the picture down one whole side and this layout
     drew every one of them at 38% with its natural height — a commission table 43% wide and 77%
     tall arriving as a thumbnail beside the prose that was meant to annotate it.
     45% and full height, contained rather than cropped, because a document that is cropped is a
     document you cannot read. */
  .l-prose.paneled figure { flex:0 0 45%; align-self:stretch; min-height:0;
                            display:flex; align-items:flex-start; }
  .l-prose.paneled figure img { height:100%; width:100%; object-fit:contain; object-position:top;
                                border:0; }
  /* The ones the author drew a border on: 19 of the 78, every one a screenshot of a table or an
     application window. The hairline is the deck's own border token. */
  .l-prose.paneled figure.document img { border:1px solid var(--border, var(--rail-rule)); }

  /* ---- tranche 3: ARGUMENT ---- */

  /* The neutral glyph set is drawn with currentColor, so an icon is whatever colour its
     context already is. No icon anywhere carries a literal value. */
  .gl { width:1.4em; height:1.4em; flex:0 0 auto; display:block;
        fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round;
        stroke-linejoin:round; }
  img.gl { stroke:none; object-fit:contain; }

  /* icon-checklist: labeled-rows with a leading mark. The icon column is fixed so the labels
     line up down the slide however wide the individual glyphs are. */
  .l-icons .rows { display:flex; flex-direction:column; gap:var(--sp-3); overflow:hidden; flex:1; }
  .l-icons .row { display:grid; grid-template-columns:auto minmax(8em,15em) 1fr;
                  gap:var(--sp-3) var(--sp-4); align-items:baseline;
                  padding-bottom:var(--sp-3);
                  border-bottom:1px solid var(--border, var(--rail-rule)); }
  .l-icons .row:last-child { border-bottom:0; padding-bottom:0; }
  .l-icons .row .gl { color:var(--primary); align-self:center; }
  .l-icons .row .label { font-size:var(--fs-label); font-weight:bold; color:var(--primary); }
  .l-icons .row .detail { font-size:var(--fs-detail); line-height:var(--lh-body); }
  .l-icons .row.nodetail { grid-template-columns:auto 1fr; }

  /* two-column-compare: exactly two columns, because that is the layout rather than a cap. */
  /* flex:0 1 auto, not flex:1 — see the note above .l-stats .stats. Measured at 1180x820, a
     bordered column holding 277px of content was drawn 697px tall. The default stretch inside
     the row stays, so the two columns still match each other; it is the ROW that no longer
     claims the whole stage. */
  .l-compare .cols { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-5);
                     flex:0 1 auto; overflow:hidden; min-height:0; }
  .l-compare .col { display:flex; flex-direction:column; gap:var(--sp-3); min-height:0;
                    overflow:hidden; padding:var(--sp-4);
                    border:1px solid var(--border, var(--rail-rule));
                    border-radius:var(--sp-1); }
  .l-compare .col.emph { border-width:2px; border-color:var(--primary); }
  .l-compare .col .heading { font-size:var(--fs-label); font-weight:bold; color:var(--primary);
                             padding-bottom:var(--sp-2);
                             border-bottom:1px solid var(--border, var(--rail-rule)); }
  .l-compare .col ul { margin:0; padding:0 0 0 1.1em; display:flex; flex-direction:column;
                       gap:var(--sp-2); font-size:var(--fs-detail); line-height:var(--lh-body); }
  .l-compare .band { margin-top:var(--sp-4); padding:var(--sp-3) var(--sp-4);
                     background:var(--rail-bg); color:var(--surface);
                     border-radius:var(--sp-1); font-size:var(--fs-detail); }

  /* card-grid: 2x2 or 2x3. Three columns only once there are more than four cards, so four
     cards read as a square rather than as a row of three and an orphan. */
  .l-cards .grid { display:grid; grid-template-columns:repeat(2, 1fr); gap:var(--sp-4);
                   flex:1; overflow:hidden; min-height:0; align-content:start; }
  .l-cards .grid.three { grid-template-columns:repeat(3, 1fr); }
  .l-cards .card { display:flex; flex-direction:column; gap:var(--sp-2); min-height:0;
                   overflow:hidden; padding:var(--sp-4);
                   border:1px solid var(--border, var(--rail-rule));
                   border-radius:var(--sp-1); }
  .l-cards .card .gl { color:var(--primary); }
  .l-cards .card .heading { font-size:var(--fs-label); font-weight:bold; color:var(--primary); }
  .l-cards .card .body { font-size:var(--fs-detail); line-height:var(--lh-body); }

  /* ---- tranche 4: SHOW ---- */

  /* A treated asset is a figure, not a bare img: tint and overlayText are siblings drawn over
     the image, never filters applied to its pixels. That distinction is what makes the
     verbatim rule enforceable -- a verbatim asset simply has no siblings. */
  .asset { position:relative; overflow:hidden; display:block; }
  .asset img { display:block; width:100%; height:100%; object-fit:cover; }
  .asset .tint { position:absolute; inset:0; pointer-events:none; opacity:.42; }
  .asset .tint.primary { background:var(--primary); }
  .asset .tint.canvas  { background:var(--canvas); }
  .asset .tint.ink     { background:var(--ink); }
  .asset .ovl { position:absolute; left:0; right:0; bottom:0; padding:var(--sp-3) var(--sp-4);
                background:var(--scrim); color:var(--surface);
                font-size:var(--fs-caption); line-height:var(--lh-body); }
  .asset.f-card { border-radius:var(--sp-1); box-shadow:0 1px 6px rgba(0,0,0,.28); }
  .asset.f-hairline { border:1px solid var(--border, var(--rail-rule)); }

  /* photo-grid: 2x2 up to four images, 3x2 beyond. Cells are a fixed ratio so a portrait and
     a landscape photograph do not make neighbouring rows different heights. */
  .l-pgrid .grid { display:grid; grid-template-columns:repeat(2, 1fr); gap:var(--sp-3);
                   flex:1; min-height:0; overflow:hidden; }
  .l-pgrid .grid.three { grid-template-columns:repeat(3, 1fr); }
  .l-pgrid figure { margin:0; display:flex; flex-direction:column; gap:var(--sp-2);
                    min-height:0; overflow:hidden; }
  .l-pgrid figure .asset { flex:1; min-height:0; }
  .l-pgrid figcaption { font-size:var(--fs-caption); line-height:var(--lh-body); opacity:.8; }

  /* photo-row-caption: 2-3 images in a row, one caption band under all of them. */
  .l-prow .row { display:flex; gap:var(--sp-3); flex:1; min-height:0; overflow:hidden; }
  .l-prow .row .asset { flex:1 1 0; min-width:0; }
  .l-prow .caption { margin-top:var(--sp-3); font-size:var(--fs-detail);
                     line-height:var(--lh-body); }

  /* panel-notes: a solid panel one side, headed blocks the other. */
  .l-panel { padding:0; flex-direction:row; }
  .l-panel.right { flex-direction:row-reverse; }
  .l-panel .panel { flex:0 0 38%; background:var(--canvas); color:var(--surface);
                    position:relative; overflow:hidden; }
  .l-panel .panel .asset { position:absolute; inset:0; }
  .l-panel .notes { flex:1; min-width:0; display:flex; flex-direction:column;
                    gap:var(--sp-3); padding:var(--sp-5); overflow:hidden; }
  .l-panel .blocks { display:flex; flex-direction:column; gap:var(--sp-3);
                     flex:1; min-height:0; overflow:hidden; }
  .l-panel .block .heading { font-size:var(--fs-label); font-weight:bold; color:var(--primary); }
  .l-panel .block .body { font-size:var(--fs-detail); line-height:var(--lh-body); }

  /* process-chain: steps left to right, connected. The connector is a pseudo-element so it
     never takes a grid cell and never appears after the last step. */
  /* Same flex:0 1 auto as stat-row and compare. Nothing visible stretches here today because
     .step draws no border, so this is the latent case rather than a defect — it would become one
     the moment a step got a box. Caught in the same pass; process-chain is not yet in use. */
  .l-chain .steps { display:flex; align-items:stretch; flex:0 1 auto; min-height:0;
                    overflow:hidden; gap:0; }
  .l-chain .step { flex:1 1 0; min-width:0; display:flex; flex-direction:column;
                   align-items:center; text-align:center; gap:var(--sp-2);
                   padding:var(--sp-3) var(--sp-2); position:relative; }
  .l-chain .step + .step::before { content:''; position:absolute; left:0; top:var(--sp-5);
                   width:100%; height:2px; background:var(--border, var(--rail-rule));
                   transform:translateX(-50%); z-index:0; }
  .l-chain .step .gl { color:var(--primary); position:relative; z-index:1;
                       background:var(--surface); padding:var(--sp-1); box-sizing:content-box; }
  .l-chain .step .dot { width:10px; height:10px; border-radius:50%; background:var(--primary);
                        position:relative; z-index:1; box-shadow:0 0 0 5px var(--surface); }
  .l-chain .step .label { font-size:var(--fs-caption); font-weight:bold; color:var(--primary);
                          line-height:var(--lh-tight); position:relative; z-index:1; }
  .l-chain .step .detail { font-size:var(--fs-caption); line-height:var(--lh-body); opacity:.8; }

  /* ---- tranche 5: EVIDENCE ---- */

  /* verbatim-exhibit: the document is the slide. It gets the room, the chrome stays at the
     edges, and nothing is drawn over the asset itself. */
  .l-exhibit .frameBox { flex:1; min-height:0; position:relative; display:flex;
                         border:1px solid var(--border, var(--rail-rule));
                         border-radius:var(--sp-1); overflow:hidden;
                         background:var(--rail-bg); }
  .l-exhibit .frameBox .asset { flex:1; min-width:0; }
  .l-exhibit .caption { margin-top:var(--sp-2); font-size:var(--fs-caption);
                        line-height:var(--lh-body); }
  .l-exhibit .source { margin-top:var(--sp-1); font-size:var(--fs-caption);
                       opacity:.65; font-style:italic; }

  /* table */
  .l-table .scroll { flex:1; min-height:0; overflow:hidden; }
  .l-table table { width:100%; border-collapse:collapse; font-size:var(--fs-detail); }
  .l-table th, .l-table td { text-align:left; padding:var(--sp-2) var(--sp-3);
                             border-bottom:1px solid var(--border, var(--rail-rule));
                             vertical-align:top; line-height:var(--lh-body); }
  .l-table th { font-weight:bold; color:var(--primary); font-size:var(--fs-caption);
                letter-spacing:.06em; text-transform:uppercase; }
  .l-table tbody tr:last-child td { border-bottom:0; }
  .l-table .sourceNote { margin-top:var(--sp-3); font-size:var(--fs-caption); opacity:.65; }

  /* stat-row */
  /* A BOX SIZES TO ITS CONTENT, NOT TO THE STAGE. This read flex:1, which claims
     every pixel left under the title, and align-items:stretch then passed that height straight
     into a bordered card. Measured at 1180x820: a card holding "20+" and its label was drawn
     697px tall for 82px of content.
     flex:0 1 auto is the fix — no growing, still allowed to shrink, sized by content. The stretch
     stays, because it is what keeps the three cards level with each other; card-grid has always
     done exactly this and is the precedent. The leftover height sits BELOW the row rather than
     centring it: a three-stat slide and a five-stat slide must start at the same place, or paging
     through the deck reads as the layout moving. */
  .l-stats .stats { display:flex; gap:var(--sp-3); flex:0 1 auto; min-height:0; align-items:stretch; }
  .l-stats .stat { flex:1 1 0; min-width:0; display:flex; flex-direction:column;
                   justify-content:center; align-items:center; text-align:center;
                   gap:var(--sp-2); padding:var(--sp-4) var(--sp-2);
                   border:1px solid var(--border, var(--rail-rule));
                   border-radius:var(--sp-1); }
  .l-stats .stat.emph { background:var(--primary); color:var(--surface); border-color:var(--primary); }
  .l-stats .stat .value { font-size:var(--fs-statement); font-weight:bold; line-height:var(--lh-tight);
                          color:var(--primary); }
  .l-stats .stat.emph .value { color:var(--surface); }
  .l-stats .stat .slabel { font-size:var(--fs-caption); line-height:var(--lh-body); opacity:.85; }
  .l-stats .band { margin-top:var(--sp-3); padding:var(--sp-3) var(--sp-4);
                   background:var(--rail-bg); color:var(--surface);
                   border-radius:var(--sp-1); font-size:var(--fs-detail); }
  .l-stats .sourceNote { margin-top:var(--sp-2); font-size:var(--fs-caption); opacity:.65; }

  /* chart: drawn as SVG from brand tokens, so it recolours per client like everything else. */
  .l-chart .plot { flex:1; min-height:0; display:flex; gap:var(--sp-4); }
  .l-chart svg { flex:1; min-width:0; height:100%; display:block; overflow:visible; }
  .l-chart .axis { stroke:var(--border, var(--rail-rule)); stroke-width:1; }
  .l-chart .tick { font-size:11px; fill:var(--ink); opacity:.65; }
  .l-chart .legend { display:flex; flex-direction:column; gap:var(--sp-2);
                     justify-content:center; font-size:var(--fs-caption); }
  .l-chart .legend .key { display:flex; align-items:center; gap:var(--sp-2); }
  .l-chart .legend .sw { width:12px; height:12px; border-radius:2px; flex:0 0 auto; }
  .l-chart .sourceNote { margin-top:var(--sp-2); font-size:var(--fs-caption); opacity:.65; }

  /* ---- tranche 6: EMPHASIS ---- */

  /* centered-callout: one boxed statement. Not a FRAME layout -- it keeps the card and the
     chapter rail, because it is a point inside an argument rather than a break between two. */
  .l-callout { align-items:center; justify-content:center; text-align:center; }
  .l-callout .box { max-width:26em; padding:var(--sp-5) var(--sp-6);
                    border:2px solid var(--primary); border-radius:var(--sp-2); }
  .l-callout .statement { font-size:var(--fs-statement); line-height:var(--lh-tight);
                          font-weight:bold; margin:0; color:var(--primary); }
  .l-callout .supporting { font-size:var(--fs-lead); line-height:var(--lh-body);
                           margin:var(--sp-4) 0 0; opacity:.78; }

  /* ---- tranche 7: DEPRECATED ---- */

  /* bullets-image: a bullet list with an unframed image floating beside it. Never offered in
     the editor. Rendered only so imported Product Demo material can be shown at all. */
  .l-bullets .wrap { display:flex; gap:var(--sp-5); flex:1; min-height:0; overflow:hidden; }
  .l-bullets ul { margin:0; padding:0 0 0 1.2em; flex:1; min-width:0;
                  display:flex; flex-direction:column; gap:var(--sp-3);
                  font-size:var(--fs-detail); line-height:var(--lh-body); overflow:hidden; }
  .l-bullets .side { flex:0 0 38%; align-self:flex-start; }

  /* full-bleed-image: one image filling the card, hit areas over it. This is what every
     pre-layout v2 slide becomes, so it must keep looking exactly as it did.

     contain, NOT the .asset default of cover. v2's #card img.slide was contain on a surface
     background, and these are whole slides exported from PowerPoint -- cropping one to fill
     the card would cut content off a slide that already reads correctly. An author who sets
     fit explicitly still wins, because assetFrom writes that inline. */
  .l-fbi { padding:0; }
  .l-fbi .asset { position:absolute; inset:0; background:var(--surface); }
  .l-fbi .asset img { width:100%; height:100%; object-fit:contain; }

  /* actions: the explicit affordance on layouts with no single image to draw a box over. */
  .l .actions { display:flex; gap:var(--sp-2); margin-top:var(--sp-4); flex-wrap:wrap; }
  .l .actions button { font:inherit; font-size:var(--fs-caption); font-weight:bold;
                       min-height:44px; padding:var(--sp-2) var(--sp-4);
                       border-radius:var(--sp-1);
                       border:1px solid var(--primary); background:transparent;
                       color:var(--primary); }
  .l .actions button.seen { background:var(--primary); color:var(--surface); }

  /* tap targets on a slide -> logged as opened */
  .hot { position:absolute; border:2px dashed var(--hot-border);
    border-radius:4px; background:var(--hot-fill); pointer-events:auto; }
  .hot.seen { border-style:solid; background:var(--hot-fill-seen); }

  /* ---------- verbatim ----------
     A slide kept in its own arrangement rather than re-derived into one of the layouts above.
     Every box is absolutely positioned in per-cent of the card, and every length in this block is
     either a per-cent or a cqh, because the card has no fixed size: #stage is inset:0 on whatever
     screen it finds, and the LMS draws the same slide again at thumbnail size.

     container-type:size is what makes cqh mean anything here. It is the one modern CSS feature the
     engine depends on (Safari 16, Chrome 105); without it a verbatim slide would need its card
     measured in JS on every resize, which the renderer does not do.

     No colour is named below. The renderer writes var(--token) inline from the token NAME the
     importer resolved, which is what keeps guardEngine() satisfied and what makes a verbatim slide
     restyle from brand.tokens.json exactly as a catalogue layout does. */
  .l-verbatim { padding:0; overflow:hidden;
                font-family:var(--font, Arial, 'Liberation Sans', Helvetica, sans-serif); }
  /* The slide at its own shape, centred in whatever card it is given. container-type lives HERE
     and not on .l-verbatim, so 1cqh is one per cent of the SLIDE's height rather than of a card
     that may be a different shape entirely. margin:auto against inset:0 is what centres it. */
  .l-verbatim .vb-page { position:absolute; inset:0; margin:auto;
                         max-width:100%; max-height:100%; container-type:size; }
  .l-verbatim .vb-shape, .l-verbatim .vb-text,
  .l-verbatim .vb-img, .l-verbatim .vb-table, .l-verbatim .vb-chart {
    position:absolute; box-sizing:border-box; }
  /* The text box is a flex column so a:bodyPr's anchor (top, middle, bottom) is one property. */
  .l-verbatim .vb-text { display:flex; flex-direction:column; }
  .l-verbatim .vb-img { object-fit:fill; }
  .l-verbatim .vb-crop { overflow:hidden; }
  .l-verbatim .vb-crop img { position:absolute; }
  .l-verbatim .vb-p { margin:0; line-height:1.2; }
  /* An empty line holds its height (render.js sets it from the line's own size). flex-shrink:0
     because a text box is a flex column, and when its lines run past the box the browser shrinks
     whatever has no content first: every blank line collapsed to nothing, where PowerPoint keeps
     them and lets the text run on. */
  .l-verbatim .vb-p.vb-gap { height:1em; flex-shrink:0; }
  /* The bullet and its hanging indent take the size and color of the text they lead: render.js
     sets both from the first run. Runs carry their own sizes, so the paragraph has none, and at
     1em of it the bullet was a speck and the indent didn't scale with the slide. */
  .l-verbatim .vb-bullet { position:relative; padding-left:calc(var(--vb-bullet-size, 1em) * 1.1); }
  .l-verbatim .vb-bullet::before { content:"\2022"; position:absolute; left:calc(var(--vb-bullet-size, 1em) * .2);
    font-size:var(--vb-bullet-size, 1em); color:var(--vb-bullet-color, inherit); }

  /* A NUMBERED PARAGRAPH counts within its own text box, which is what an author means by "a
     numbered list": a counter reset per .vb-text rather than per page, so two lists on one
     slide each start at 1. CSS counters rather than an <ol>, because paragraphs here are a
     flat run of <p> and wrapping some of them in a list would change the structure the editor
     addresses by index. */
  .l-verbatim .vb-text { counter-reset: vb-num; }
  .l-verbatim .vb-num::before {
    counter-increment: vb-num;
    content: counter(vb-num) ".";
    font-size:var(--vb-bullet-size, 1em); color:var(--vb-bullet-color, inherit);
    position:absolute; left:0; width:1em; text-align:right;
  }

  /* ---------- the named text styles ----------
     Four names an author can apply, each defined once. The VALUES come from the brand through
     custom properties, so what "Heading" means is a brand decision rather than a number in
     this file, and the fallbacks below are what an unconfigured deck gets — a real scale, not
     placeholders, so a deck with no brand styles still reads correctly.

     Sizes are cqh, like every other measurement here: one cqh is one per cent of the card's
     height, so a style scales with the card instead of needing a breakpoint.

     A run's own size, weight or colour is written inline by the renderer and therefore wins
     over these, which is exactly what an override is and why the editor can offer "Reset to
     style" by simply removing the inline value. */
  .l-verbatim .vb-ts-title {
    font-size:var(--ts-title-size, 7.2cqh); font-weight:var(--ts-title-weight, 800);
    line-height:var(--ts-title-lh, 1.08); color:var(--ts-title-color, var(--ink));
  }
  .l-verbatim .vb-ts-heading {
    font-size:var(--ts-heading-size, 4.6cqh); font-weight:var(--ts-heading-weight, 700);
    line-height:var(--ts-heading-lh, 1.18); color:var(--ts-heading-color, var(--ink));
  }
  .l-verbatim .vb-ts-body {
    font-size:var(--ts-body-size, 3cqh); font-weight:var(--ts-body-weight, 400);
    line-height:var(--ts-body-lh, 1.42); color:var(--ts-body-color, var(--ink));
  }
  .l-verbatim .vb-ts-caption {
    font-size:var(--ts-caption-size, 2.1cqh); font-weight:var(--ts-caption-weight, 400);
    line-height:var(--ts-caption-lh, 1.35); color:var(--ts-caption-color, var(--muted-ink));
  }
  /* FINE PRINT: the line under a price. Smaller than a caption and the same muted role — in full
     ink it would read as a point being made rather than as a disclosure. */
  .l-verbatim .vb-ts-finePrint {
    font-size:var(--ts-finePrint-size, 1.6cqh); font-weight:var(--ts-finePrint-weight, 400);
    line-height:var(--ts-finePrint-lh, 1.3); color:var(--ts-finePrint-color, var(--muted-ink));
  }

  /* Preset geometries the corpus actually draws. Anything else is a rectangle, which is what it
     looks like in the source too -- prstGeom names far more shapes than this deck uses. */
  .l-verbatim .g-roundRect, .l-verbatim .g-round1Rect,
  .l-verbatim .g-round2SameRect { border-radius:1.6cqh; }
  .l-verbatim .g-round1Rect, .l-verbatim .g-round2SameRect {
    border-bottom-left-radius:0; border-bottom-right-radius:0; }
  .l-verbatim .g-ellipse, .l-verbatim .g-pie, .l-verbatim .g-chord { border-radius:50%; }
  /* THE OUTLINES A ROUNDED CORNER CANNOT EXPRESS.
     Until now anything that was not a rounded rectangle or an ellipse was drawn as a plain
     rectangle -- silently, so an arrow in a source deck arrived as a box and looked like a
     design decision. Fourteen nodes in the training corpus are one of these. PowerPoint names
     them and the importer passes the name through, so styling them here fixes what has already
     been imported as well as giving the editor something to offer.
     clip-path and not SVG because these are still ordinary boxes: they take a fill, a shadow and
     text exactly as the rectangles do. It does clip a border to the same outline, which is the
     behaviour you want, and clips a drop shadow away, which is the one thing lost. */
  .l-verbatim .g-triangle   { clip-path:polygon(50% 0%, 100% 100%, 0% 100%); }
  .l-verbatim .g-diamond    { clip-path:polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }
  .l-verbatim .g-octagon    { clip-path:polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%,
                                                70% 100%, 30% 100%, 0% 70%, 0% 30%); }
  .l-verbatim .g-rightArrow { clip-path:polygon(0% 25%, 60% 25%, 60% 0%, 100% 50%,
                                                60% 100%, 60% 75%, 0% 75%); }
  .l-verbatim .g-leftArrow  { clip-path:polygon(100% 25%, 40% 25%, 40% 0%, 0% 50%,
                                                40% 100%, 40% 75%, 100% 75%); }
  .l-verbatim .g-upArrow    { clip-path:polygon(25% 100%, 25% 40%, 0% 40%, 50% 0%,
                                                100% 40%, 75% 40%, 75% 100%); }
  .l-verbatim .g-downArrow  { clip-path:polygon(25% 0%, 25% 60%, 0% 60%, 50% 100%,
                                                100% 60%, 75% 60%, 75% 0%); }

  .l-verbatim .vb-table table { width:100%; height:100%; border-collapse:collapse;
                                table-layout:fixed; }
  .l-verbatim .vb-table td { vertical-align:middle; padding:.25em .5em; }

  .l-verbatim .vb-chart { display:flex; align-items:flex-end; gap:2%; padding:4% 2% 0; }

  /* A CHART DRAWN AS ITS SOURCE DREW IT (render.js drawnChart): title, then a grid of the value
     axis, the category axis and the plot, then the legend. Every position along an axis is a
     per cent of the plot, so the axes' cells span exactly the plot's. */
  .l-verbatim .vb-chart.vb-drawn { display:flex; flex-direction:column; align-items:stretch; gap:0;
                                   padding:0; }
  .l-verbatim .vb-ch-title { text-align:center; line-height:1.2; padding:.3em 0 .4em; }
  .l-verbatim .vb-ch-grid { flex:1; min-height:0; display:grid; position:relative;
                            grid-template-columns:auto 1fr; grid-template-rows:1fr auto;
                            grid-template-areas:"val plot" ". cat"; }
  .l-verbatim .vb-ch-grid.is-h { grid-template-areas:"cat plot" ". val"; }
  .l-verbatim .vb-ch-plot { grid-area:plot; position:relative; }
  .l-verbatim .vb-ch-val { grid-area:val; position:relative; min-width:2.6em; min-height:1.6em; }
  .l-verbatim .vb-ch-cat { grid-area:cat; position:relative; min-width:4.5em; min-height:1.6em; }
  .l-verbatim .vb-ch-val span, .l-verbatim .vb-ch-cat span { position:absolute; white-space:nowrap; line-height:1; }
  /* Beside a vertical plot, the value labels are right-aligned against it and centered on their
     line; under a horizontal one, centered on their mark. Category labels the other way round. */
  .l-verbatim .vb-ch-grid:not(.is-h) .vb-ch-val span { right:.5em; transform:translateY(50%); }
  .l-verbatim .vb-ch-grid:not(.is-h) .vb-ch-cat span { top:.4em; transform:translateX(-50%); }
  .l-verbatim .vb-ch-grid.is-h .vb-ch-val span { top:.4em; transform:translateX(-50%); }
  .l-verbatim .vb-ch-grid.is-h .vb-ch-cat span { right:.6em; transform:translateY(50%); }
  .l-verbatim .vb-ch-gridline { position:absolute; background:color-mix(in srgb, var(--ink) 12%, transparent); }
  .l-verbatim .vb-ch-grid:not(.is-h) .vb-ch-gridline { left:0; right:0; height:1px; }
  .l-verbatim .vb-ch-grid.is-h .vb-ch-gridline { top:0; bottom:0; width:1px; }
  .l-verbatim .vb-ch-bar { position:absolute; }
  /* A value label inside or beside its bar, where the source put it. */
  .l-verbatim .vb-ch-lbl { position:absolute; line-height:1; white-space:nowrap; }
  .l-verbatim .vb-ch-lbl.is-ctr { left:50%; top:50%; transform:translate(-50%, -50%); }
  .l-verbatim .vb-ch-lbl:not(.is-h).is-inEnd { top:.35em; left:50%; transform:translateX(-50%); }
  .l-verbatim .vb-ch-lbl:not(.is-h).is-outEnd { bottom:100%; left:50%; transform:translate(-50%, -.3em); }
  .l-verbatim .vb-ch-lbl:not(.is-h).is-inBase { bottom:.35em; left:50%; transform:translateX(-50%); }
  .l-verbatim .vb-ch-lbl.is-h.is-inEnd { right:.4em; top:50%; transform:translateY(-50%); }
  .l-verbatim .vb-ch-lbl.is-h.is-outEnd { left:100%; top:50%; transform:translate(.3em, -50%); }
  .l-verbatim .vb-ch-lbl.is-h.is-inBase { left:.4em; top:50%; transform:translateY(-50%); }
  .l-verbatim .vb-ch-legend { display:flex; justify-content:center; flex-wrap:wrap; gap:.4em 1.2em;
                              padding:.5em 0 .2em; line-height:1.2; }
  .l-verbatim .vb-ch-key { display:inline-flex; align-items:center; gap:.35em; }
  .l-verbatim .vb-ch-key i { display:inline-block; width:.7em; height:.7em; }
  .l-verbatim .vb-cat { flex:1; display:flex; flex-direction:column; height:100%; }
  .l-verbatim .vb-bars { flex:1; display:flex; align-items:flex-end; gap:6%;
                         justify-content:center; }
  .l-verbatim .vb-bar { flex:1; max-width:46%; position:relative; }
  .l-verbatim .vb-bar span { position:absolute; top:-1.5em; left:0; right:0; text-align:center;
                             font-size:1.9cqh; color:var(--ink); }
  .l-verbatim .vb-catlabel { text-align:center; font-size:2cqh; padding-top:.4em;
                             color:var(--ink); }

  /* ================= node-level actions =================
     Every colour below is a token from :root, and there is no exception: build.js's guardEngine()
     fails the build on a hex outside that block, which is what keeps an overlay renderable in a
     client's own palette rather than in a designer's. The one raw rgba is --hot-scrim, declared up
     there beside the two the hit-area boxes already use and for the same reason — a scrim is a
     degree of darkness, not a colour anybody's brand chose.

     WHAT IS HERE AND WHY IT IS HERE: all of it is the SLIDE, or a full-screen surface the slide
     opens. The tablet, the author's Preview and the live view all read this file, so a pop-up looks
     the same in all three. The editor's own chrome -- the badge on a node that carries an action --
     is NOT here: it lives in the LMS's deckStudio.css, because a badge on a tablet would be a
     defect and the surest way to prevent one is for the rule not to exist in the shared file. */

  /* A node that does something says so with a cursor and, on a device with no cursor, with the
     44px minimum target the hit areas are already measured against. Nothing else: an outline or a
     glow would be the renderer deciding how an author's slide looks.

     pointer-events:auto IS NOT DECORATION, AND IT IS WHY THIS RULE EXISTS AT ALL.

     Both hosts put a full-screen tap layer UNDER the slide and make the slide itself transparent to
     pointers, so that a tap anywhere turns the page: the presenter's #stage is pointer-events:none
     over #nav, and the LMS viewer's .dv-stage is the same over .dv-nav. That was complete while the
     only thing on a slide you could tap was a hit area — and .hot has carried pointer-events:auto
     since the beginning for exactly this reason.

     A node-level action is the second thing, and without this line every one of them is untappable:
     the tap passes straight through to the nav layer and the rep gets the NEXT SLIDE instead of the
     pop-up. It opts the whole subtree back in, which is what a gallery grid's cells and a
     before/after's divider need too.

     FOUND BY A REAL POINTER, not by a test. Every assertion in the suite clicks with element.click(),
     which dispatches an event at a node and does no hit-testing at all — so all of it passed. The
     browser drive dragged the before/after divider with genuine mouse events, the slide advanced
     instead, and that is the only reason this is here. */
  .l-verbatim .vb-act { cursor:pointer; pointer-events:auto;
                        -webkit-tap-highlight-color:transparent; }
  .l-verbatim .vb-act:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }
  /* Already opened in this appointment. Painted at draw time from buttonsSeen, so it appears on
     the next entry to the slide -- the same moment a reveal resets. */
  .l-verbatim .vb-act.vb-seen { opacity:.92; }

  /* DRAW ATTENTION TO THIS ONE.

     A ring that grows out of the affordance and fades, once every two seconds. Not a flash and not
     a colour change: this has to be on the screen for four minutes while somebody talks over it,
     and anything sharper becomes the thing the customer is looking at instead of the rep.

     ON A PSEUDO-ELEMENT, so it costs the element itself nothing — no transform that would fight
     the drag on a compare slider, no opacity that would fade the label with it. outline rather
     than a border, because a border would move the box.

     currentColor, so it is whatever the affordance already is. No colour is named here. */
  @keyframes vb-pulse {
    0%   { transform:scale(1);    opacity:.55; }
    70%  { transform:scale(1.16); opacity:0; }
    100% { transform:scale(1.16); opacity:0; }
  }
  .l-verbatim .vb-pulse { position:relative; }
  .l-verbatim .vb-pulse::after {
    content:""; position:absolute; inset:0; pointer-events:none;
    border-radius:inherit; outline:0.35cqh solid currentColor; outline-offset:0.2cqh;
    animation:vb-pulse 2s ease-out infinite; }
  /* Somebody who has asked their system not to animate things is not asking for an exception on a
     slide. The ring stays, once, so the affordance is still marked — it simply stops moving. */
  @media (prefers-reduced-motion: reduce) {
    .l-verbatim .vb-pulse::after { animation:none; opacity:.5; transform:scale(1.06); }
  }

  /* HIDDEN UNTIL REVEALED. visibility rather than display, so the box keeps its place in the paint
     order and revealing it cannot shift anything around it; and it leaves the accessibility tree
     while hidden, which display:none would also do but at the cost of a layout pass. */
  .l-verbatim .vb-hidden { visibility:hidden; }
  /* The editor draws the same node VISIBLE and marked, because a box nobody can see is a box
     nobody can move, resize or un-hide. Dashed rather than tinted: a tint would change the colour
     the author is judging. */
  .l-verbatim .vb-hidden-auth { outline:1px dashed var(--primary); outline-offset:-1px; }

  /* ---------- a hotspot ---------- */
  .l-verbatim .vb-hot { position:absolute; box-sizing:border-box; border-radius:50%;
                        display:flex; align-items:center; justify-content:center; }
  /* currentColor, so the plus is one declaration away from being any role at all -- and --surface
     is the ground-safe pairing for the default --primary fill. The editor offers this node the
     ground-safe swatches for exactly this reason. */
  /* THE PLUS FILLS MORE OF THE MARKER. At 62% on a marker the author had shrunk to fit between two
     roof shingles, the glyph was a few pixels of stroke and the whole thing read as a dot. */
  .l-verbatim .vb-hot-gl { width:74%; height:74%; color:var(--surface);
                           fill:none; stroke:currentColor; stroke-width:2.6;
                           stroke-linecap:round; }

  /* ---------- before and after ---------- */
  /* ROUNDED, because every other picture on a slide is. A hard-cornered frame in the middle of a
     deck of rounded cards reads as something that has not been finished. */
  .l-verbatim .vb-ba { position:absolute; box-sizing:border-box; overflow:hidden;
                       border-radius:1.6cqh; touch-action:none; }
  .l-verbatim .vb-ba img { position:absolute; inset:0; width:100%; height:100%;
                           object-fit:cover; }
  /* THE FRONT IMAGE IS CLIPPED, NOT SIZED. Both pictures are the same box at the same object-fit,
     so they crop their subjects identically and the divider reads as one scene changing rather
     than as a seam between two photographs. A wrapper with a width would have made the front
     image's own width depend on the wrapper's, which is the version that looks wrong. */
  /* WHICH IS UNDERNEATH, SAID OUT LOUD. It was true by append order, which is the kind of
     invariant that survives until somebody reorders two lines for a good reason -- and the
     symptom would be a divider that reveals the wrong picture. */
  .l-verbatim .vb-ba-after { z-index:0; }
  .l-verbatim .vb-ba-before { z-index:1;
    clip-path:inset(0 calc(100% - var(--vb-ba, 50%)) 0 0); }
  .l-verbatim .vb-ba-handle { position:absolute; top:0; bottom:0;
                              left:var(--vb-ba, 50%); width:2px; margin-left:-1px;
                              background:var(--surface); pointer-events:none; }
  .l-verbatim .vb-ba-handle::after {
    content:""; position:absolute; top:50%; left:50%; width:44px; height:44px;
    transform:translate(-50%, -50%); border-radius:50%;
    background:var(--surface); box-shadow:0 1px 8px var(--hot-scrim); }
  /* TWO ARROWS ON THE GRIP. A bare line down the middle of a photograph reads as part of the
     picture; this is the part that says it moves. 44px is the same minimum every hit area here is
     measured against, and the arrows sit inside it. */
  .l-verbatim .vb-ba-grip {
    position:absolute; top:50%; left:50%; width:26px; height:26px;
    transform:translate(-50%, -50%); z-index:1;
    fill:none; stroke:var(--ink); stroke-width:2;
    stroke-linecap:round; stroke-linejoin:round; }
  .l-verbatim .vb-ba:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }

  /* ---------- a gallery grid ---------- */
  .l-verbatim .vb-gg { position:absolute; box-sizing:border-box; display:grid; gap:1.2cqh;
                       grid-template-columns:repeat(auto-fill, minmax(18%, 1fr));
                       align-content:start; overflow:hidden; }
  .l-verbatim .vb-gg-cell { padding:0; border:0; background:none; overflow:hidden;
                            position:relative; aspect-ratio:4 / 3; cursor:pointer;
                            border-radius:.6cqh; }
  .l-verbatim .vb-gg-cell img { width:100%; height:100%; object-fit:cover; display:block; }
  .l-verbatim .vb-gg-cell:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }

  /* ================= the overlays =================
     Fixed to the viewport rather than to the card: an action opens over the whole screen, which is
     what makes a spec sheet readable and a video worth playing.

     z-index 90, WHICH IS ABOVE EVERYTHING IN BOTH HOSTS, and the first number here was 60 with a
     comment reasoning that the LMS viewer's 80 was "deliberately above this, because in the LMS the
     overlay belongs inside the preview". That was wrong in the one way that matters: the overlay is a
     child of document.body, not of the viewer, so it does not belong inside anything -- it simply
     opened BEHIND the preview and an author saw nothing.

     It passed the test that checks a pop-up opens, because the element existed. Existence is not
     visibility, and a fake DOM cannot tell the two apart. Found by looking at the screenshot.

     The two stacks it has to clear, written down so the next number is not a guess:
       presenter   #nav 1, #stage 2, #lightbox 40, #notice 45, #debug 50, #offlineBadge 60,
                   .screen 65, #landing 70
       LMS         Deck Studio shell 40, the section overview 50, the editor 60, the viewer 80

     An overlay is a modal: while one is open it is the thing being looked at, in either host. It is
     closed by closeSlide() on every navigation and by the viewer's own teardown, so it cannot outlive
     the surface underneath it and sit over a picker. */
  /* THE PADDING IS ONE NUMBER, because "how much of the screen does an overlay take" is one
     decision and five overlay types would otherwise each answer it. 5vmin leaves ~90% of the
     smaller dimension for the content, which is what a picture on a tablet is for. */
  .vb-ov { --vb-ov-pad:5vmin;
           position:fixed; inset:0; z-index:90; display:flex;
           align-items:center; justify-content:center;
           padding:var(--vb-ov-pad);
           background:var(--scrim);
           color:var(--ink);
           font-family:var(--font, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif); }
  /* AN OVERLAY IS AS BIG AS THE SCREEN ALLOWS, and this is the line that decides it.

     It was max-width/max-height, which CAPS a thing and never grows one — so a 900px photograph
     opened as a 900px photograph in the middle of a 2360px tablet, and a rep in a customer's
     kitchen was shown a thumbnail with a black border round it. The picture was always the full
     upload; nothing was ever resized on the way out. It simply had no reason to get bigger.

     A real width and height, with object-fit:contain on the media, is the version that fills:
     the box is the viewport less its padding, and the picture grows to the larger of the two
     dimensions it can have while keeping its shape. Every overlay type shares it — see the note
     on --vb-ov-pad. */
  .vb-ov-body { width:100%; height:100%; display:flex;
                align-items:center; justify-content:center; }
  /* The close button is 44px, the same minimum every hit area is measured against, and it is in
     the corner of the SCREEN rather than of the content -- so it does not move when the content
     is a portrait document one moment and a landscape photograph the next. */
  .vb-ov-x { position:absolute; top:clamp(8px, 2vmin, 20px); right:clamp(8px, 2vmin, 20px);
             width:44px; height:44px; border-radius:50%; border:0; cursor:pointer;
             background:var(--surface); color:var(--ink);
             font-size:19px; line-height:1; z-index:2; }
  .vb-ov-x:focus-visible { outline:2px solid var(--surface); outline-offset:3px; }

  /* width AND height, not max-*: see the note on .vb-ov-body. contain keeps the shape and fills
     whichever dimension runs out first, which is the whole of "large and consistent". */
  .vb-ov-pic { width:100%; height:100%; object-fit:contain; display:block; }
  .vb-ov-video { width:100%; height:100%; object-fit:contain; background:var(--canvas); }

  /* ---------- a pop-up ---------- */
  /* A POP-UP IS AS BIG AS EVERY OTHER OVERLAY. It was capped at 60em, which on a 2360px tablet is
     a card in the middle of a dark screen; a pop-up is the thing the rep is talking about, not a
     tooltip. The words still stop at a readable measure — that is what .vb-ov-pop-txt's max-width
     is for — so filling the screen widens the picture rather than the paragraph. */
  .vb-ov-pop { display:flex; gap:var(--sp-5); align-items:center;
               background:var(--surface); border-radius:var(--sp-2);
               padding:var(--sp-5); width:100%; height:100%;
               box-sizing:border-box; overflow:auto; }
  .vb-ov-pop-img { flex:1 1 52%; min-width:0; align-self:stretch;
                   display:flex; align-items:center; justify-content:center; }
  .vb-ov-pop-img img { width:100%; height:100%; object-fit:contain;
                       border-radius:var(--sp-1); display:block; }
  .vb-ov-pop-txt { flex:1 1 40%; min-width:0; max-width:34em; }

  /* THE FIVE ARRANGEMENTS. Same two pieces, same DOM order — the heading is always before the
     caption in the markup, so what a screen reader hears does not depend on which of these an
     author picked — and the placing is done here. */
  .vb-ov-pop.pop-beside-flip { flex-direction:row-reverse; }
  .vb-ov-pop.pop-above { flex-direction:column; align-items:stretch; }
  .vb-ov-pop.pop-above .vb-ov-pop-img { flex:1 1 auto; min-height:0; }
  .vb-ov-pop.pop-above .vb-ov-pop-txt { flex:none; max-width:none; }
  .vb-ov-pop.pop-words, .vb-ov-pop.no-img { display:block; }
  .vb-ov-pop.pop-words .vb-ov-pop-txt, .vb-ov-pop.no-img .vb-ov-pop-txt {
    max-width:40em; margin:0 auto; }

  /* THE PICTURE WITH THE WORDS OVER IT, on a band dark enough to read on. The band is a scrim
     rather than an opacity, for the reason every other scrim here is: opacity would fade the
     text with the ground it is sitting on. */
  .vb-ov-pop.pop-over { display:block; position:relative; padding:0; overflow:hidden; }
  .vb-ov-pop.pop-over .vb-ov-pop-img { position:absolute; inset:0; }
  .vb-ov-pop.pop-over .vb-ov-pop-img img { object-fit:cover; border-radius:0; }
  .vb-ov-pop.pop-over .vb-ov-pop-txt {
    position:absolute; left:0; right:0; bottom:0; max-width:none;
    padding:var(--sp-5); background:var(--hot-scrim);
    backdrop-filter:blur(2px); }
  .vb-ov-pop.pop-over .vb-ov-pop-txt h2 { color:var(--surface); }
  .vb-ov-pop.pop-over .vb-ov-pop-txt p { color:var(--surface); }
  .vb-ov-pop-txt h2 { margin:0 0 var(--sp-3); font-size:var(--fs-title);
                      line-height:var(--lh-tight); color:var(--ink); }
  /* A POP-UP'S PARAGRAPH IN ONE OF THE BRAND'S OWN TEXT STYLES.

     Read from the same --ts-* properties .vb-ts-* reads, which is the point: "how big is a
     Caption" is answered once, by brand.tokens.json, for a paragraph on a slide and for the same
     words in a pop-up. The fallbacks are the engine's own defaults, identical to the ones in the
     .vb-ts-* block above, so a deck whose tokens say nothing about text styles still draws all
     five distinctly.

     cqh with no container resolves against the small viewport, and an overlay IS the viewport —
     so a 3cqh body in a pop-up is the same share of the screen that a 3cqh body is of a slide.

     No colour. A pop-up's words are already coloured for the surface they are on, and pop-over
     paints them --surface over a darkened photograph; taking a role from the text style here
     would undo that and put muted ink on a dark band. Size, weight and line-height are what an
     author is choosing. */
  .vb-ov-pop-txt p.vb-ov-ts-title {
    font-size:var(--ts-title-size, 7.2cqh); font-weight:var(--ts-title-weight, 800);
    line-height:var(--ts-title-lh, 1.08);
  }
  .vb-ov-pop-txt p.vb-ov-ts-heading {
    font-size:var(--ts-heading-size, 4.6cqh); font-weight:var(--ts-heading-weight, 700);
    line-height:var(--ts-heading-lh, 1.18);
  }
  .vb-ov-pop-txt p.vb-ov-ts-body {
    font-size:var(--ts-body-size, 3cqh); font-weight:var(--ts-body-weight, 400);
    line-height:var(--ts-body-lh, 1.42);
  }
  .vb-ov-pop-txt p.vb-ov-ts-caption {
    font-size:var(--ts-caption-size, 2.1cqh); font-weight:var(--ts-caption-weight, 400);
    line-height:var(--ts-caption-lh, 1.35);
  }
  .vb-ov-pop-txt p.vb-ov-ts-finePrint {
    font-size:var(--ts-finePrint-size, 1.6cqh); font-weight:var(--ts-finePrint-weight, 400);
    line-height:var(--ts-finePrint-lh, 1.3);
  }
  .vb-ov-pop-txt p { margin:0; font-size:var(--fs-lead); line-height:var(--lh-body);
                     color:var(--muted-ink); white-space:pre-wrap; }
  /* A pop-up that has no picture YET and one that is meant to have none draw the same, which is
     right: both are words on a card. The difference is a thing the author can see in the panel. */
  /* A pop-up on a tablet held upright, or on a phone: the image above the words rather than a
     42% column beside them, which at that width is a thumbnail next to a newspaper column. */
  @media (max-width: 760px), (orientation: portrait) {
    .vb-ov-pop.pop-beside, .vb-ov-pop.pop-beside-flip {
      flex-direction:column; align-items:stretch; gap:var(--sp-4); }
    .vb-ov-pop.pop-beside .vb-ov-pop-img, .vb-ov-pop.pop-beside-flip .vb-ov-pop-img {
      flex:1 1 auto; min-height:0; }
    .vb-ov-pop.pop-beside .vb-ov-pop-txt, .vb-ov-pop.pop-beside-flip .vb-ov-pop-txt {
      flex:none; max-width:none; }
  }

  /* ---------- the gallery viewer ---------- */
  .vb-ov-gal { display:flex; flex-direction:column; align-items:center; gap:var(--sp-4);
               max-width:100%; max-height:100%; }
  .vb-ov-gal-frame { flex:1; min-height:0; display:flex; align-items:center;
                     justify-content:center; touch-action:pan-y; }
  .vb-ov-dots { display:flex; gap:var(--sp-2); flex-wrap:wrap; justify-content:center; }
  /* 44px of target around a 9px dot: the dot says where you are and the button is what a finger
     hits. The two being different sizes is the point -- twelve 44px dots would be a toolbar. */
  .vb-ov-dot { width:44px; height:24px; border:0; background:none; padding:0; cursor:pointer;
               position:relative; }
  .vb-ov-dot::after { content:""; position:absolute; top:50%; left:50%; width:9px; height:9px;
                      transform:translate(-50%, -50%); border-radius:50%;
                      background:var(--rail-muted); }
  .vb-ov-dot.on::after { background:var(--rail-active); }
  .vb-ov-dot:focus-visible { outline:2px solid var(--surface); outline-offset:0; }

  /* ---------- the PDF viewer ---------- */
  .vb-ov-pdf { display:flex; flex-direction:column; gap:var(--sp-3);
               width:min(96vw, 60em); height:88vh; }
  .vb-ov-pdf-bar { display:flex; align-items:center; justify-content:center; gap:var(--sp-3);
                   color:var(--rail-active); font-size:var(--fs-caption); flex:none; }
  .vb-ov-pdf-z { width:44px; height:44px; border-radius:50%; border:0; cursor:pointer;
                 background:var(--surface); color:var(--ink); font-size:19px; line-height:1; }
  .vb-ov-pdf-z:focus-visible { outline:2px solid var(--surface); outline-offset:3px; }
  .vb-ov-pdf-note { min-width:6em; text-align:center; }
  /* pan-x as well as pan-y, because a page zoomed past the frame's width is a page you scroll
     sideways; and pinch-zoom on top, so the platform's own gesture still works over ours. */
  .vb-ov-pdf-pages { flex:1; min-height:0; overflow:auto; background:var(--panel);
                     border-radius:var(--sp-1); padding:var(--sp-2);
                     touch-action:pan-x pan-y pinch-zoom; }
  .vb-ov-pdf-page { margin:0 auto var(--sp-3); background:var(--surface);
                    width:calc(100% * var(--vb-pdf-zoom, 1)); max-width:none; }
  .vb-ov-pdf-page canvas { display:block; width:100%; height:auto; }
  .vb-ov-pdf-fail { color:var(--ink); background:var(--surface); margin:0;
                    padding:var(--sp-4); border-radius:var(--sp-1);
                    font-size:var(--fs-detail); line-height:var(--lh-body); }


/* ============================== THE SECTION RAIL ==============================
   Here, in the renderer's own stylesheet, for the same reason paintRail is in the renderer: the
   presenter, the author's Preview and the library's live view all show this rail, and the brief is
   that the three are identical. Two stylesheets with the same intent are two stylesheets that
   diverge, and a rail that looks different in the preview than on the tablet makes the preview a
   thing you have to double-check.

   Each host still owns the BAR — its height, its background, where it sits — because that is the
   host's chrome. What is below is the inside of the rail: the labels, the marker, and how the
   marker moves.

   Colours are the --rail-* roles both hosts already define. No value is named. */
.rail-list {
  position: relative;
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  gap: 6px;
  overflow-x: auto;
  overflow-y: hidden;
  /* The rail scrolls to keep the live label in view, and a visible scrollbar inside a 74px bar
     would eat a third of the label's height. paintRail does the scrolling. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.rail-list::-webkit-scrollbar { display: none; }

/* A LABEL IS AS WIDE AS ITS TITLE, PLUS AN EQUAL SHARE OF WHAT IS LEFT. flex-grow with a content
   basis, so a rail of two sections still fills the bar and a label with more to say is wider than one
   with less; a min-width that does not shrink, so a rail of twelve overflows and scrolls rather than
   ellipsising every one of them into uselessness.

   MEASURED: an earlier version capped this at 16em, and on a 1180px tablet every label grew to
   exactly that cap — three labels, three identical 256px boxes, and a marker whose width never
   changed. The cap was doing the opposite of what it was for. */
.rail-item {
  flex: 1 0 auto;
  min-width: 5.5em;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  text-align: center;
  /* Not interactive unless the host asked for it: see .can-jump below. */
  pointer-events: none;
  cursor: default;
}
.rail-item span {
  display: block;
  font-size: 11px;
  letter-spacing: .4px;
  color: var(--rail-muted);
  padding-bottom: 7px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Active section: WEIGHT carries the state and colour only reinforces it. A monochrome client
   palette has no accent to spend here, so the live section must stay identifiable with the colour
   difference removed. Do not make colour load-bearing. */
.rail-item.on span { color: var(--rail-active); font-weight: bold; letter-spacing: .6px; }
/* A title the author has not written. sectionRail warns about it; this stops it reading as the
   live one by accident. */
.rail-item[data-untitled] span { font-style: italic; opacity: .75; }

/* The rule under every label, and the one marker that moves along it. The rule is drawn by the
   list so there is exactly one of it, rather than one per label with gaps between. */
.rail-list::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--rail-rule);
}
.rail-marker {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 0;
  height: 4px;
  border-radius: 2px;
  background: var(--rail-active);
  /* Placed by paintRail before this class arrives, so the first paint does not glide in from the
     left edge. Position AND width: the labels are not all the same width. */
  transform: translateX(0);
}
.rail-list.is-live .rail-marker {
  transition: transform 300ms cubic-bezier(.4, 0, .2, 1), width 300ms cubic-bezier(.4, 0, .2, 1);
}
/* Somebody who has asked their system not to animate things is not asking for an exception here. */
@media (prefers-reduced-motion: reduce) {
  .rail-list.is-live .rail-marker { transition: none; }
}

/* THE HOST DECIDES WHETHER A LABEL IS A TARGET. The presenter's rail is pass-through by design
   during a presentation; the LMS's viewer is not. paintRail adds this class when it is given
   something to do with a tap. */
.rail-list.can-jump .rail-item { pointer-events: auto; cursor: pointer; }
.rail-list.can-jump .rail-item:hover span { color: var(--rail-active); }
.rail-list.can-jump .rail-item:focus-visible { outline: 2px solid var(--rail-active); outline-offset: 2px; }
