/* ==========================================================================
   CANDIDATE E — THE CONTACT SHEET, on the live dashboard

   This file is loaded LAST (Dash sorts assets, and "zz-" sorts last), so it
   overrides styles.css without touching it. Production keeps every one of
   its behaviours; only the indicators' reveal is swapped.

   The sheet that lives in production is stood down, the card's own
   collapsible is given back its job, the seven measures fold away into a
   spine line to pay for the room, and the seals DEVELOP like a print coming
   up in the tray: paper first, then the tint, then the text.

   Desktop only. The phone's block in styles.css is untouched, and every
   rule here is fenced to 641+.
   ========================================================================== */
@media (min-width: 641px) {

   /* ---- stand the production sheet down ------------------------------- */
   #ind-sheet, #ind-scrim2 { display: none !important; }

   body.sheet-up .app {
      transform: none !important;
      filter: none !important;
      border-radius: 0 !important;
      overflow: visible !important;
   }

   /* ---- give the card's own room its job back ------------------------- */
   #modal-indicators-content {
      display: grid !important;
      grid-template-rows: 0fr;
      transition: grid-template-rows .95s cubic-bezier(.7, 0, .4, 1);
   }

   #modal-indicators-content[style*="1fr"] {
      grid-template-rows: 1fr;
      transition: grid-template-rows 1.3s cubic-bezier(.45, .12, .5, .94);
   }

   /* ---- the fold: the seven measures pay for the room ------------------ */
   #modal-summary-content {
      display: grid;
      grid-template-rows: 1fr;
      transition: grid-template-rows .8s cubic-bezier(.7, 0, .4, 1),
         opacity .4s ease .3s;
   }

   #modal-summary-content > * { overflow: hidden; min-height: 0; }

   body:has(.expandbtn.is-open) #modal-summary-content {
      grid-template-rows: 0fr;
      opacity: 0;
      transition: grid-template-rows .8s cubic-bezier(.45, .12, .5, .94),
         opacity .3s ease;
   }

   /* ---- the develop: paper, then tint, then text ---------------------- */
   @keyframes eDevelop {
      0% { opacity: 0; background: #FFFFFF; border-color: rgba(22, 19, 14, .05);
           color: transparent; transform: rotate(var(--r, 0deg)) scale(.96); }
      35% { opacity: 1; background: #FFFFFF; color: transparent; }
      100% { opacity: 1; transform: rotate(var(--r, 0deg)) scale(1); }
   }

   #modal-indicators-content[style*="1fr"] .pstamp {
      animation: eDevelop .8s cubic-bezier(.3, 0, .2, 1) both !important;
      animation-delay: calc(.30s + var(--n, 0) * .05s) !important;
   }

   /* ---- TWO SEALS TO A ROW, ALWAYS ------------------------------------
      Flex-wrap laid them out by their own widths, and since the labels run
      from "Viability ban" to "Medication abortion restricted" most rows took
      a single chip — fourteen rows of one, a card twice as tall as it needed
      to be, and a column so long it pushed the state's name up under the top
      bar. A two-track grid ends the argument: seven rows, every row full,
      and the chips size to the track instead of the track sizing to them. */
   #modal-indicators-content .stampwrap {
      display: grid !important;
      grid-template-columns: 1fr 1fr;
      gap: 7px;
      align-items: start;
   }

   /* NOT display:flex — that turns the label into an anonymous flex item and
      it stops wrapping, so "Private insurance restricted" ran straight off
      the card's edge and was clipped. Block, and let the text wrap as
      production always intended it to. */
   #modal-indicators-content .pstamp {
      max-width: none !important;
      width: 100%;
      /* production has no global border-box reset, so a content-box chip at
         width:100% came out 22px wider than its track — every right-hand
         chip ran past the card and had its last letters shaved off */
      box-sizing: border-box;
      display: block;
      min-height: 34px;          /* a one-line chip still fills its row */
      overflow-wrap: break-word;
      /* Half the hand's tilt. Production's ±3.2° is lovely when the chips
         float at their own widths; in a two-track grid the corners of the
         right-hand column swing into the card's edge and get shaved. The
         character survives at 45% of the angle — it still reads as set by
         hand, and nothing touches the wall. */
      transform: rotate(calc(var(--r, 0deg) * .45));
   }

   #modal-indicators-content .stampwrap { padding-right: 4px; }

   /* ---- and the column can never overflow its own window --------------
      The column is vertically centred, so anything it cannot fit pushes out
      of BOTH ends — the title upward, the reading downward. The card is
      capped to the room the window actually has, and on a short window the
      seals scroll inside it rather than shoving the name off the top. */
   #state-modal.statecard {
      /* the script measures the real budget and writes --e-cardmax; the
         fallback is only for the frame before it runs */
      max-height: var(--e-cardmax, min(calc(100vh - 380px), 560px));
      transition: max-height .6s cubic-bezier(.45, .12, .5, .94);
   }

   #state-modal .statecard-body { overflow-y: auto; overflow-x: hidden; }

   /* ---- THE TOGGLE'S ZOOM MUST NOT COST ANYTHING ----------------------
      A transformed box still contributes to SCROLLABLE OVERFLOW, so a
      full-width button scaling to 1.08 did two things at once: its ends ran
      past the card's clip (the left one visibly shaved, the right one hidden
      under the scrollbar gutter) and the extra height summoned a vertical
      scrollbar the moment the pointer arrived.

      The fix is arithmetic rather than restraint: the button rests at
      1/1.08 of the width, centred, so the hover scales it to EXACTLY the
      content width — it grows into the space it was already given instead of
      out of the card. A few pixels of slack under it absorb the same 8% in
      height, so the scrollbar never appears either. */
   /* At rest the button is exactly as wide as the rows above it — the
      measure tracks and the seals all share one left and right edge, and the
      control should not be the odd one out.

      The room for its zoom comes from moving padding rather than shrinking
      the button. The clip that shaved its left end is the SCROLL CONTAINER's
      padding box (.statecard-body owns overflow-y, so it clips there, and
      its padding-left was 0). Sixteen pixels of padding move from the card
      onto the body: the content lands in exactly the same place on screen,
      the rows keep their edges, and the button now has 16px of bleed on each
      side — comfortably more than the 14.4 that a 1.08 scale needs. Padding
      is inside the scrollable area, so no bar appears either.

      !important throughout: the width is an inline style written by app.py's
      btn_style dict, and a class rule never had a chance against it. */
   #state-modal.statecard {
      padding-left: 12px !important;
      padding-right: 12px !important;
   }

   #state-modal .statecard-body {
      padding-left: 16px;
      padding-right: 16px;
      margin-right: 0;
      padding-bottom: 14px;
   }

   #state-modal .expandbtn {
      width: 100% !important;
      margin-left: 0 !important;
      margin-right: 0 !important;
   }

   /* ---- THE TOUR IS THE BUTTON'S, AND NOBODY ELSE'S -------------------
      Production arms the walkthrough 2.4s after the dashboard's entrance,
      so a reader arriving got the tour AND the field's own demonstration at
      the same time — two teachers talking over each other. The card is now
      invisible unless the reader ASKED for it (body.tour-invited, set by the
      Tour button), and zz-variant.js stops any uninvited run on the frame it
      begins. Hidden first, stopped second: that order means no flash. */
   /* Not keyed on `tour-on`: production drops that class BEFORE the card
      has finished leaving, so the card became visible during its own
      fade-out — a flash of the walkthrough nobody asked for. Un-invited
      means hidden, whatever the tour thinks it is doing. */
   body:not(.tour-invited) #tourcard { display: none !important; }
   body.tour-on:not(.tour-invited) .fieldlegend { opacity: 1 !important; }

   /* ---- THE STORY'S FIRST CHART TEACHES ITSELF -------------------------
      The dashboard's field performs one hover so the reader learns what a
      square is. The story's opening chart earns the same courtesy — it is
      the first chart anyone meets. */
   .story-hint {
      position: fixed;
      z-index: 3600;
      font-family: var(--mono, 'JetBrains Mono', monospace);
      font-size: 9px;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: rgba(245, 243, 238, .62);
      opacity: 0;
      transform: translateY(6px);
      pointer-events: none;
      transition: opacity .5s ease, transform .7s cubic-bezier(.16, 1, .3, 1);
   }

   body.story-hint-on .story-hint { opacity: 1; transform: none; }

   /* ---- THE STANDFIRST, ON THE PAGE AS WELL AS THE FILM ---------------
      The mark's film carries the project's name, a rule and its sources.
      The dashboard itself was carrying none of that, so the page and the
      overlay disagreed about what this thing is called. The same line now
      sits at the foot of the page in ink, and the RULE IS CENTRED on the
      text in both places — set as a baseline-aligned flex item, a 1px rule
      lands on the text's baseline, which reads as sitting under the words
      rather than between them. */
   /* .ms-line is the film overlay's own class (production's mark stage);
      .filmline was this file's earlier guess and matched nothing. */
   #mark-stage .ms-line i,
   .filmline i,
   .dash-standfirst i {
      align-self: center !important;
   }

   .dash-standfirst {
      position: fixed;
      left: 40px;
      right: 40px;
      bottom: 22px;
      z-index: 26;
      display: flex;
      align-items: baseline;
      gap: 16px;
      font-family: var(--mono, 'JetBrains Mono', monospace);
      font-size: 9.5px;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: #A79E92;
      pointer-events: none;
      opacity: 0;
      transform: translateY(6px);
      transition: opacity .6s ease, transform .8s cubic-bezier(.16, 1, .3, 1);
   }

   body:has(#intro.is-gone) .dash-standfirst {
      opacity: 1;
      transform: none;
      transition-delay: .5s;
   }

   /* One tone for the whole line. In ink at 9.5px the darker weight read
      as a different, louder typeface than every other label on the page
      (the axis ends, the legend, the column numbers); the film can keep
      its contrast because it is printing on video. */
   .dash-standfirst b { font-weight: 400; color: inherit; }

   .dash-standfirst i {
      font-style: normal;
      flex: 1;
      height: 1px;
      background: rgba(22, 19, 14, .16);
   }

   /* The key's caption now lives in the strip directly below it, so the
      legend keeps only its moving demonstration — the words are not
      printed twice, 23px apart, in the same corner. */
   .fieldlegend { bottom: 58px !important; }
   .fieldlegend::after { display: none !important; }

   /* ---- THE FIELD STANDS WITH THE COLUMN -------------------------------
      Production centres the field in the WINDOW (top:50%, height:66vh), so
      its feet landed wherever the viewport happened to put them — never on
      the same line as the national-averages card beside it. The box now
      takes its top and its height from the column itself (zz-variant.js
      measures and publishes them), so the two panels start and finish
      together — and the box is dropped until the chart's own foot-line lands
      on the card's bottom rule. Measured once, at rest, then held: see
      zz-variant.js. The squares are positioned by percentage inside this box,
      so they spread to fill it rather than resize. */
   #dotfield {
      top: var(--fld-top, 50%) !important;
      height: var(--fld-h, 66vh) !important;
      transform: translate(-50%, 0) !important;
      /* deliberately no transition: the frame is written once, and a
         transition would play that first write as a visible settle */
   }

   /* the solver sizes the box; --sq only exists once it has committed, so the
      squares keep their server-rendered size until then */
   #dotfield { width: var(--fld-w, 62vw) !important; }

   /* solved: an absolute seat, so the centring translate steps aside */
   html.fld-sized #dotfield {
      left: var(--fld-left) !important;
      transform: none !important;
   }

   html.fld-sized #dotfield .ssq,
   html.fld-sized #dotfield .ghostsq {
      width: var(--sq) !important;
      height: var(--sq) !important;
   }

   /* ---- PAPER TO FILM: ONE LINE, ONE CLAUSE CHANGING -------------------
      The page's strip and the film's standfirst say the same thing in the
      same corner, so hovering the mark swapped one for the other and the eye
      caught nothing — two near-identical lines trading places 12px apart.

      They now share a seat exactly: same left, same right, same baseline,
      same size. The name and the rule therefore SURVIVE the cut — paper
      turns to film underneath them — and the only thing that moves is the
      clause on the right, which lifts out and is replaced by the sources,
      then hands the invitation back on the way out. The rule no longer wipes
      itself in from zero either: it was already drawn on the page, and
      re-drawing it announced that the line had been rebuilt. */
   body:has(.dash-standfirst) #mark-stage .ms-line {
      left: 40px;
      right: 40px;
      bottom: 22px;
      gap: 16px;
      font-size: 9.5px;
      letter-spacing: .2em;
      transform: none;
      transition: opacity .34s ease;
   }

   body.mark-stage #mark-stage .ms-line { transition-delay: .1s; }

   body:has(.dash-standfirst) #mark-stage .ms-line i {
      transform: scaleX(1);
      transition: none;
   }

   /* The clause arrives after the picture has settled, so it reads as a
      change of words rather than part of the fade. An animation, not a
      transition: the stage is BUILT on the first hover, so a fresh node
      never gets a frame in its start state and a transition would simply
      not run the first time — which is the one time it matters most. */
   @keyframes clauseIn {
      from { opacity: 0; transform: translateY(7px); }
      to   { opacity: 1; transform: none; }
   }

   body:has(.dash-standfirst) #mark-stage .ms-line span {
      display: inline-block;
   }

   body.mark-stage #mark-stage .ms-line span {
      animation: clauseIn .55s cubic-bezier(.16, 1, .3, 1) .3s both;
   }

   /* and the page's own clause steps up out of the slot as the film lands,
      dropping back into it once the picture has gone */
   .dash-standfirst span {
      display: inline-block;
      transition: opacity .3s ease .34s,
                  transform .55s cubic-bezier(.16, 1, .3, 1) .34s;
   }

   body.mark-stage .dash-standfirst span {
      opacity: 0;
      transform: translateY(-7px);
      transition-delay: 0s, 0s;
   }

   /* ---- THE CARD'S BOTTOM LINE -----------------------------------------
      Two rows, 32px each — exactly the 64px that stood between this card and
      the state profile card beside it, so the column no longer changes height
      when a state is picked. Ink only: the label in the card's own kicker
      tone, the measure a shade stronger, the figure last. Nothing coloured,
      nothing striped — six strips is already the card's whole voice. */
   .kpisum {
      display: block;
   }


   .kpisum-row {
      box-sizing: border-box;
      height: 28px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 14px;
   }

   /* the gap between the last strip and these two says "summary" on its
      own; a rule as well was one mark more than the card needed */
   .kpisum { padding-top: 8px; }

   .kpisum-row .kicker { color: #9A9186; }

   .kpisum-val {
      font-family: var(--mono);
      font-size: 9.5px;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: #5E574D;
      white-space: nowrap;
   }

   .kpisum-val b { font-weight: 500; color: #3A352E; }

   .kpisum-val i {
      font-style: normal;
      color: #CFC7BB;
      margin: 0 7px;
   }

   /* The legend's tick was reading as a mark in its own right — taller and
      darker than the thing it stands for. It is a caption, not a datum: the
      ticks inside the tracks keep their weight, this one steps back. */
   .kpikey .kk--tick {
      width: 1px;
      height: 7px;
      background: rgba(22, 19, 14, .26);
      vertical-align: -1px;
   }

   /* ---- THE BOARD BREAKS UP, PIECE BY PIECE -----------------------------
      Fading the whole page as one block is a flash, not a departure. Every
      element leaves on its own gesture and its own beat, and returns the same
      way: the bar lifts, the headline slides off to the left, the tiles drop,
      the card falls, and the field dissolves square by square in the order it
      originally settled — production stamps each square with its own `--sd`,
      so that order already exists and is simply replayed.

      Nothing here animates a transform that something else owns. `.leftcol`
      is translated by the column anchor and the field's transform is
      !important once solved, so both are left alone and their CHILDREN carry
      the motion. The squares keep their translate(-50%, -50%) in every frame. */
   @keyframes upOut   { from { opacity: 1; transform: none; }
                        to   { opacity: 0; transform: translateY(-9px); } }
   @keyframes upIn    { from { opacity: 0; transform: translateY(-11px); }
                        to   { opacity: 1; transform: none; } }
   @keyframes leftOut { from { opacity: 1; transform: none; }
                        to   { opacity: 0; transform: translateX(-24px); } }
   @keyframes leftIn  { from { opacity: 0; transform: translateX(-28px); }
                        to   { opacity: 1; transform: none; } }
   @keyframes dropOut { from { opacity: 1; transform: none; }
                        to   { opacity: 0; transform: translateY(18px) scale(.972); } }
   @keyframes dropIn  { from { opacity: 0; transform: translateY(24px) scale(.966); }
                        to   { opacity: 1; transform: none; } }
   @keyframes slideOut { from { opacity: 1; transform: none; }
                         to   { opacity: 0; transform: translateX(14px); } }
   @keyframes slideIn  { from { opacity: 0; transform: translateX(16px); }
                         to   { opacity: 1; transform: none; } }
   /* The field leaves as ONE THING — but it does leave. A plain fade was so
      quiet the chart looked like it had simply been switched off while
      everything around it moved. Every square carries the SAME drift, so the
      field falls as a single sheet: the tiles' own gesture, at chart scale,
      with no stagger and no per-square scatter to make it busy. */
   /* THE FIELD'S OWN ENTRANCE, RUN BACKWARDS AND FORWARDS.
      Production scatters each square in from a random offset, rotation and
      blur when the dashboard is first entered — every square still carries
      its own --sx/--sy/--sr/--sd. Going out is that same motion reversed;
      coming back is `fieldSettle` itself, the identical animation you see on
      first entry. Nothing invented: the chart leaves the way it arrived. */
   @keyframes sqAway {
      from { opacity: 1; filter: blur(0);
             transform: translate(-50%, -50%) rotate(0deg) scale(1); }
      70%  { opacity: 1; }
      to   { opacity: 0; filter: blur(5px);
             transform: translate(calc(-50% + var(--sx, 0px)),
                                  calc(-50% + var(--sy, 0px)))
                        rotate(var(--sr, 0deg)) scale(.85); }
   }

   /* The profile button carries a 1px border, and scaling a bordered box
      lands its edge on half a pixel — which is the bottom edge going missing.
      It travels, it never scales. */
   @keyframes ctaOut { from { opacity: 1; transform: none; }
                       to   { opacity: 0; transform: translateY(22px); } }
   @keyframes ctaIn  { from { opacity: 0; transform: translateY(26px); }
                       to   { opacity: 1; transform: none; } }
   @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
   @keyframes fadeIn  { from { opacity: 0; } to { opacity: 1; } }

   body.mark-stage .app { pointer-events: none; }

   body.mark-stage #intro-replay { animation: upOut .26s ease both; }
   body.mark-stage #tour-btn     { animation: upOut .26s ease both .04s; }
   body.mark-stage .searchwrap   { animation: upOut .28s ease both .08s; }
   body.mark-stage .topright     { animation: upOut .28s ease both .11s; }
   body.mark-stage .segment      { animation: slideOut .3s ease both .14s; }

   body.mark-stage #hero-title   { animation: leftOut .4s cubic-bezier(.45, .12, .5, .94) both .06s; }
   body.mark-stage #hero-sub     { animation: leftOut .4s cubic-bezier(.45, .12, .5, .94) both .12s; }
   body.mark-stage #score-panel  { animation: dropOut .42s cubic-bezier(.45, .12, .5, .94) both .18s; }
   body.mark-stage #profile-cta  { animation: fadeOut .3s ease both .22s; }
   body.mark-stage #kpis-wrap,
   body.mark-stage #card-wrap    { animation: dropOut .44s cubic-bezier(.45, .12, .5, .94) both .24s; }

   body.mark-stage #dotfield .ssq {
      animation: sqAway .5s cubic-bezier(.45, .12, .5, .94) both;
      animation-delay: calc(var(--sd, .1s) * .55);
   }

   body.mark-stage #dotfield .ghostsq  { animation: fadeOut .3s ease both; }
   body.mark-stage #dotfield .histaxis { animation: fadeOut .34s ease both .16s; }
   body.mark-stage .fieldlegend        { animation: fadeOut .3s ease both .2s; }
   body.mark-stage .dash-standfirst    { animation: upOut .34s ease both .26s; }

   body.film-landing #dotfield .ssq {
      animation: fieldSettle .8s cubic-bezier(.16, 1, .3, 1) both;
      animation-delay: var(--sd, .1s);
   }

   body.film-landing #dotfield .ghostsq  { animation: fadeIn .4s ease both .12s; }
   body.film-landing #dotfield .histaxis { animation: fadeIn .4s ease both .2s; }

   body.film-landing #hero-title  { animation: leftIn .52s cubic-bezier(.16, 1, .3, 1) both .1s; }
   body.film-landing #hero-sub    { animation: leftIn .52s cubic-bezier(.16, 1, .3, 1) both .16s; }
   body.film-landing #score-panel { animation: dropIn .56s cubic-bezier(.16, 1, .3, 1) both .22s; }
   body.film-landing #profile-cta { animation: fadeIn .36s ease both .3s; }
   body.film-landing #kpis-wrap,
   body.film-landing #card-wrap   { animation: dropIn .58s cubic-bezier(.16, 1, .3, 1) both .28s; }

   body.film-landing #intro-replay { animation: upIn .42s cubic-bezier(.16, 1, .3, 1) both .3s; }
   body.film-landing #tour-btn     { animation: upIn .42s cubic-bezier(.16, 1, .3, 1) both .34s; }
   body.film-landing .searchwrap   { animation: upIn .44s cubic-bezier(.16, 1, .3, 1) both .38s; }
   body.film-landing .topright     { animation: upIn .44s cubic-bezier(.16, 1, .3, 1) both .42s; }
   body.film-landing .segment      { animation: slideIn .46s cubic-bezier(.16, 1, .3, 1) both .46s; }
   body.film-landing .fieldlegend  { animation: fadeIn .4s ease both .46s; }
   body.film-landing .dash-standfirst { animation: upIn .46s cubic-bezier(.16, 1, .3, 1) both .5s; }

   /* The picture waits for the board to go, rather than landing on top of
      it. An ANIMATION, not a transition: production builds the stage with
      `mark-stage` already set, so on the first hover there is no start state
      to move from and the film snapped straight to full — hiding the very
      cascade it is meant to follow. */
   @keyframes deskFilmIn { from { opacity: 0; } to { opacity: 1; } }

   body.mark-stage #mark-stage {
      animation: deskFilmIn .5s ease .18s both;
   }

   body:not(.mark-stage) #mark-stage { transition: opacity .45s ease !important; }

   /* ---- THE COLOPHON ----------------------------------------------------
      The film's own type: the method where the page's words were, the
      standfirst underneath it. On the story the picture is already the
      page's background, so the story simply stands down and leaves it. */
   body.story-film #intro-stage,
   body.story-film .intro-rail,
   body.story-film #intro-skip {
      opacity: 0;
      transition: opacity .5s ease;
      pointer-events: none;
   }

   #intro-stage, .intro-rail {
      transition: opacity .5s ease;
   }

   #story-colophon {
      position: absolute;
      inset: 0;
      z-index: 40;
      opacity: 0;
      pointer-events: none;
      transition: opacity .5s ease .12s;
   }

   body.story-film #story-colophon { opacity: 1; }
   body.mark-pin #story-colophon { pointer-events: auto; }

   /* the method block — same seat on both films */
   .ms-method {
      position: absolute;
      left: 40px;
      bottom: 84px;
      /* wider, so the lines can run. At 46ch the copy was stacked into a
         narrow column and read as a paragraph squeezed rather than set. */
      width: min(64ch, 46vw);
      color: rgba(245, 243, 238, .74);
      font-family: var(--ui, system-ui), sans-serif;
      font-size: 13.5px;
      line-height: 1.62;
      opacity: 0;
      transform: translateY(8px);
      transition: opacity .5s ease .18s,
                  transform .9s cubic-bezier(.16, 1, .3, 1) .18s;
   }

   body.mark-stage .ms-method,
   body.story-film .ms-method { opacity: 1; transform: none; }

   .ms-method .msm-k {
      font-family: var(--mono);
      font-size: 9.5px;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: rgba(245, 243, 238, .5);
      margin-bottom: 12px;
   }

   .ms-method p { margin: 0 0 11px 0; }
   .ms-method p:last-child { margin-bottom: 0; }
   .ms-method b { font-weight: 500; color: rgba(245, 243, 238, .95); }

   .ms-method .msm-caveat {
      color: rgba(245, 243, 238, .56);
      font-size: 12.5px;
      padding-top: 10px;
      border-top: 1px solid rgba(245, 243, 238, .16);
   }

   /* the story's own standfirst, in the film's voice */
   #story-colophon .sc-line {
      position: absolute;
      left: 40px;
      right: 40px;
      bottom: 22px;
      display: flex;
      align-items: baseline;
      gap: 16px;
      font-family: var(--mono);
      font-size: 9.5px;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: rgba(245, 243, 238, .58);
      opacity: 1;
      transform: none;
   }

   #story-colophon .sc-line b {
      font-weight: 500;
      color: rgba(245, 243, 238, .92);
   }

   #story-colophon .sc-line i {
      font-style: normal;
      flex: 1;
      height: 1px;
      align-self: center;
      background: rgba(245, 243, 238, .22);
      transform: none;
   }

   /* the sources are links now — reachable once the film is pinned */
   .ms-line a, #story-colophon .sc-line a {
      color: inherit;
      text-decoration: none;
      border-bottom: 1px solid rgba(245, 243, 238, .28);
      padding-bottom: 1px;
      transition: color .25s ease, border-color .25s ease;
   }

   body.mark-pin .ms-line a,
   body.mark-pin #story-colophon .sc-line a { pointer-events: auto; }

   .ms-line a:hover, #story-colophon .sc-line a:hover {
      color: #F7F5F1;
      border-bottom-color: rgba(245, 243, 238, .8);
   }

   .ms-line em, #story-colophon .sc-line em {
      font-style: normal;
      color: rgba(245, 243, 238, .34);
      margin: 0 7px;
   }


   /* ======================================================================
      THE FILM LEAVES IN THREE BEATS — the phone's sequence, restated here

      On the way out the desktop did none of what the phone does. Production
      drops `mark-stage` on the pointer's own leave, and `deskFilmIn` went
      with it: removing an animation returns the value on the frame, so the
      picture did not fade — it VANISHED, one frame from film to white paper.
      Left standing on that white was the card still open, its glass back to
      paper with the full shadow under it, and the green ring drawn around
      the whole rectangle, across the headline.

      The phone's three beats, at the phone's times:
         0ms     the type leaves while the picture holds     (colophon-out)
         260ms   the picture dissolves off a landing board   (film-closing)
         1160ms  the ring returns last, a face inside it     (ring-back)
      ====================================================================== */

   /* 1. the picture is held up after production has let go of it */
   body.film-hold #mark-stage {
      animation: none !important;
      opacity: 1 !important;
   }

   /* 2. and then dissolves off the board, which has already begun landing —
         .18s of overlap, the phone's own number, so no white shows between */
   @keyframes deskFilmOut { from { opacity: 1; } to { opacity: 0; } }

   body.film-closing #mark-stage {
      animation: deskFilmOut .6s ease .18s both !important;
   }

   /* the type goes first and not all at once: the method drops back the way
      it rose, the standfirst follows a beat later, and its rule closes to
      the left it opened from */
   body.colophon-out .ms-method {
      opacity: 0 !important;
      transform: translateY(8px) !important;
      transition: opacity .28s ease,
                  transform .34s cubic-bezier(.45, .12, .5, .94) !important;
   }

   body.colophon-out #mark-stage .ms-line {
      opacity: 0 !important;
      transform: translateY(6px) !important;
      transition: opacity .3s ease .06s,
                  transform .36s cubic-bezier(.45, .12, .5, .94) .06s !important;
   }

   body.colophon-out #mark-stage .ms-line i {
      transform: scaleX(0) !important;
      transition: transform .34s cubic-bezier(.45, .12, .5, .94) !important;
   }

   /* the story's film is one block and leaves as one, exactly as on the
      phone — its own .34s fade, while the picture behind it runs on */
   body.colophon-out #story-colophon {
      opacity: 0 !important;
      transition: opacity .34s ease !important;
   }

   body.colophon-out #intro-stage,
   body.colophon-out .intro-rail,
   body.colophon-out #intro-skip { opacity: 0 !important; }

   /* 3. NO SHRINKING BOX — ONLY THE PICTURE GOES HOME. The card's glass was
         collapsing from the open panel down to the avatar's circle: a grey
         box minimising across the film with the photograph riding inside it.
         The panel now dissolves where it stands, early, and the box that
         carries on folding is invisible. What you watch travel back into
         place is the picture, and nothing else.

         It has to LEAVE rather than be dropped — cutting the glass on the
         frame the pointer left is a hard edge — so what it fades FROM is
         still the film's own skin, production's values for
         `body.mark-stage #cp-card.on`. */
   body.film-hold #cp-card,
   body.film-hold #cp-card.on,
   body.film-closing #cp-card,
   body.film-closing #cp-card.on {
      background: transparent !important;
      -webkit-backdrop-filter: none !important;
      backdrop-filter: none !important;
      border: 0 !important;
      box-shadow: none !important;
   }

   /* AND THE PHOTOGRAPH IS NOT CUT WHILE IT TRAVELS.

      The card clips to its own box, and on the desktop that clip is the ONLY
      thing taking the copy away: `.cp-body` is pinned at full opacity, so the
      lines are not faded out, they are cut off by a box that is shrinking
      past them. The picture sits in that same box, and at the end of the fold
      the box is smaller than the circle, which is the half moon.

      So the copy is faded on purpose first, quickly, and only once it is gone
      does the box stop clipping. Unclipping it any earlier would let the name
      and the two links spill across the board on their way out, which is a
      worse thing than a chipped photograph. */
   body.film-hold #cp-card .cp-body,
   body.film-closing #cp-card .cp-body {
      opacity: 0 !important;
      transition: opacity .14s ease !important;
   }

   /* NOTHING HERE TOUCHES `transition`. That declaration is what made the
      whole card jump. Production is mid-close when this class lands, with
      `left` and `bottom` transitioning on its own curve; replacing the
      transition list cancels those two in flight, and a cancelled transition
      does not stop where it is, it snaps to its end value. Measured at the
      exact frame the class arrived: top 20 to 30, left 22 to 28, with the
      box still at full size. The picture, the name, the subtitle and both
      links all moved 10px down and 6px across in one frame, which is the
      glitch. Every transition on this card belongs to production. */
   body.photo-free #cp-card {
      overflow: visible !important;
   }

   body.film-hold #cp-card .cp-kicker,
   body.film-closing #cp-card .cp-kicker { color: rgba(245, 243, 238, .62) !important; }
   body.film-hold #cp-card .cp-fullname,
   body.film-closing #cp-card .cp-fullname { color: #F7F5F1 !important; }
   body.film-hold #cp-card .cp-row,
   body.film-hold #cp-card .cp-row span,
   body.film-closing #cp-card .cp-row,
   body.film-closing #cp-card .cp-row span { color: rgba(245, 243, 238, .88) !important; }
   body.film-hold #cp-card .cp-row i,
   body.film-closing #cp-card .cp-row i { color: rgba(245, 243, 238, .62) !important; }
   body.film-hold #cp-card .cp-row + .cp-row::before,
   body.film-closing #cp-card .cp-row + .cp-row::before {
      background: rgba(245, 243, 238, .18) !important;
   }

   /* 4. THE RING IS LAST — down for the whole sequence, back only once there
         is a photograph inside it again. It had been arriving on the frame
         the pointer left: green, still drawn at the open card's full size,
         standing across the hero. */
   /* ARMED ON THE POINTER, not on production's class. Under a fast enough
      hammering of the mark the card was seen opening in its paper skin with
      the green ring drawn round it, ahead of the picture — production owns
      `mark-stage`, and anything keyed on it can only be as early as its own
      handler. This one is set by ours, in the same breath as the film is
      called for, so there is no frame in which the card can be open and
      still dressed for the board. Dropped again the moment the close
      begins, which is where the rules below take over. */
   body.film-arming #cp-ring,
   body.mark-stage #cp-ring,
   body.film-hold #cp-ring,
   body.film-closing #cp-ring { opacity: 0 !important; }

   body.film-arming #cp-card,
   body.film-arming #cp-card.on {
      background: rgba(16, 13, 10, .40) !important;
      -webkit-backdrop-filter: blur(18px) saturate(185%) !important;
      backdrop-filter: blur(18px) saturate(185%) !important;
      border: 0 !important;
      box-shadow: 0 18px 44px rgba(0, 0, 0, .38) !important;
   }

   body.film-arming #cp-card .cp-kicker { color: rgba(245, 243, 238, .62) !important; }
   body.film-arming #cp-card .cp-fullname { color: #F7F5F1 !important; }
   body.film-arming #cp-card .cp-row,
   body.film-arming #cp-card .cp-row span { color: rgba(245, 243, 238, .88) !important; }
   body.film-arming #cp-card .cp-row i { color: rgba(245, 243, 238, .62) !important; }
   body.film-arming #cp-card .cp-row + .cp-row::before {
      background: rgba(245, 243, 238, .18) !important;
   }

   /* to .9, not to 1: that is where production's breath sits at 0%, so when
      the breath takes back over there is no step */
   @keyframes ringBack { from { opacity: 0; } to { opacity: .9; } }

   /* the breath is carried along — naming an animation here replaces the
      list, and the ring would come back holding its breath. ringBack goes
      LAST: with both animating opacity, the later one wins, and first time
      round the breath overrode the fade and the ring simply appeared. */
   body.ring-back #cp-ring {
      animation: ringBreath 2.4s cubic-bezier(.4, 0, .6, 1) infinite,
                 ringBack .34s ease both;
   }


   /* ---- THE LINK ROWS' WASH KEEPS OFF THE RULE ------------------------
      The hover plate was inset 1px top and bottom of a 31px row, so its
      lower edge landed all but on the hairline between the two links — the
      wash and the rule read as one thick smudge.

      IT COSTS NO LAYOUT. Standing the rows 8px apart did give the plate its
      full height and the rule its air, but `.cp-body` is anchored to the
      card's bottom, so a taller block of copy is a block that sits higher —
      and while production brings the three lines in on a stagger, that
      height is still settling after the box has stopped growing. Measured:
      the name and the links carried on drifting 6 to 7px for a fifth of a
      second after the card was full size. That is the whole block twitching
      as it opens.

      So the rows are left exactly where production puts them and the plate
      gives up three pixels at each end instead. 25px around 11.5px type,
      and three of clear air on either side of the rule. Both rows, both
      pages; the phone's card is untouched. */
   #cp-card .cp-row::after {
      top: 3px !important;
      bottom: 3px !important;
   }


   /* ---- ONE LANDING, NOT TWO -------------------------------------------
      Dropping `story-film` returns the stage on its own half-second fade,
      and the entrance replayed 80ms behind it — so the page came back, and
      then came back again. The stage is held down through that gap and let
      go in the very frame its own animations are restarted, with no
      transition of its own to run. What you see is the entrance, once. */
   body.story-closing #intro:not(.is-gone) #intro-stage {
      opacity: 0 !important;
      transition: none !important;
   }

   body.mark-pin #mark-stage { pointer-events: auto; }
   body.mark-pin #mark-stage .ms-line { pointer-events: auto; }
}

/* The summary rows are a desktop addition and the card's markup is shared:
   the phone keeps the card it already had, to the pixel. */
@media (max-width: 640px) {
   .kpisum { display: none !important; }
}

/* ---- THE PHONE'S FIRST LANDING ------------------------------------------
   The tour is invited, never self-starting — the same rule the desktop was
   given — and one square introduces the field instead. */
@media (max-width: 640px) {
   /* Not keyed on `tour-on`: production drops that class BEFORE the card
      has finished leaving, so the card became visible during its own
      fade-out — a flash of the walkthrough nobody asked for. Un-invited
      means hidden, whatever the tour thinks it is doing. */
   body:not(.tour-invited) #tourcard { display: none !important; }

   .tap-hint {
      position: fixed;
      z-index: 3600;
      padding: 7px 12px;
      border-radius: 999px;
      background: rgba(22, 19, 14, .82);
      -webkit-backdrop-filter: blur(10px);
      backdrop-filter: blur(10px);
      color: #F7F5F1;
      font-family: var(--mono);
      font-size: 9.5px;
      letter-spacing: .14em;
      text-transform: uppercase;
      white-space: nowrap;
      pointer-events: none;
      opacity: 0;
      transform: translateY(6px);
      transition: opacity .4s ease, transform .6s cubic-bezier(.16, 1, .3, 1);
   }

   body.tap-hint-on .tap-hint { opacity: 1; transform: none; }

   /* the pair in play lifts, the way a touched one does */
   #dotfield .ssq.is-hinted {
      transform: translate(-50%, -50%) scale(1.35);
      z-index: 30;
      box-shadow: 0 6px 18px rgba(28, 20, 14, .3);
      transition: transform .5s cubic-bezier(.16, 1, .3, 1), box-shadow .5s ease;
   }

   #dotfield .ghostsq.is-hinted {
      border-color: rgba(22, 19, 14, .55) !important;
      z-index: 29;
   }

   /* ---- THE CARD OVER FILM, ON A PHONE, IS THE DESKTOP'S CARD -----------
      The dashboard's card is white glass because the dashboard is paper. Over
      the film it read as a frosted slab across the top of the picture. It now
      wears exactly what the desktop wears over the same footage — and the
      ring goes with it: open, the glass IS the edge, so the 2px outline is
      one line too many. (Desktop already drops it; the phone did not.) */
   /* No transition on the way in: production animates the card's background,
      so the dark glass arrived over ~300ms and you watched it pass through
      paper on the way — that is the pale slab at the start of the open. */
   body.film-arming #cp-card,
   body.film-arming #cp-card.on,
   body.story-film #cp-card,
   body.story-film #cp-card.on,
   body.story-film #cp-card,
   body.story-film #cp-card.on,
   body.colophon-out #cp-card,
   body.colophon-out #cp-card.on,
   body.film-closing #cp-card,
   body.film-closing #cp-card.on {
      background: rgba(16, 13, 10, .4) !important;
      -webkit-backdrop-filter: blur(18px) saturate(185%) !important;
      backdrop-filter: blur(18px) saturate(185%) !important;
      box-shadow: 0 18px 44px rgba(0, 0, 0, .38) !important;
      border: 0 !important;
   }

   /* only the arming frame suspends the transitions */
   body.film-arming #cp-card { transition: none !important; }

   body.story-film #cp-ring { opacity: 0 !important; }

   body.film-arming #cp-card .cp-kicker,
   body.story-film #cp-card .cp-kicker,
   body.colophon-out #cp-card .cp-kicker,
   body.film-closing #cp-card .cp-kicker { color: rgba(245, 243, 238, .62) !important; }
   body.film-arming #cp-card .cp-fullname,
   body.story-film #cp-card .cp-fullname,
   body.colophon-out #cp-card .cp-fullname,
   body.film-closing #cp-card .cp-fullname { color: #F7F5F1 !important; }
   body.film-arming #cp-card .cp-row,
   body.film-arming #cp-card .cp-row span,
   body.story-film #cp-card .cp-row,
   body.story-film #cp-card .cp-row span,
   body.colophon-out #cp-card .cp-row,
   body.colophon-out #cp-card .cp-row span,
   body.film-closing #cp-card .cp-row,
   body.film-closing #cp-card .cp-row span { color: rgba(245, 243, 238, .88) !important; }
   body.film-arming #cp-card .cp-row i,
   body.story-film #cp-card .cp-row i,
   body.colophon-out #cp-card .cp-row i,
   body.film-closing #cp-card .cp-row i { color: rgba(245, 243, 238, .62) !important; }
   body.story-film #cp-card .cp-row + .cp-row::before {
      background: rgba(245, 243, 238, .18) !important;
   }

   /* ---- THE FILM ARRIVES AND LEAVES, IT DOES NOT CUT ---------------------
      On the story the page fades and the picture is already there. On the
      dashboard the picture was simply switched on over a page that never
      moved, and switched off again the instant you tapped away — no exit for
      the film, no return for the page. Both sides now cross in the same
      half-second, and `film-closing` holds the picture on screen long enough
      to leave properly instead of vanishing with the class that summoned it. */
   /* ---- THE BOARD BREAKS UP HERE TOO ------------------------------------
      The same treatment the desktop gets, at phone distances: every element
      leaves on its own gesture and its own beat rather than the whole page
      fading as one block. Keyframes are declared inside this query so each
      side keeps its own travel — the names are shared, the numbers are not. */
   /* Travel far enough to be read as a gesture. At 8-20px on a 390px screen
      five different moves all looked like one wash; these are roughly double
      and the beats below are spread wider apart. */
   @keyframes upOut   { from { opacity: 1; transform: none; }
                        to   { opacity: 0; transform: translateY(-18px); } }
   @keyframes upIn    { from { opacity: 0; transform: translateY(-22px); }
                        to   { opacity: 1; transform: none; } }
   @keyframes leftOut { from { opacity: 1; transform: none; }
                        to   { opacity: 0; transform: translateX(-40px); } }
   @keyframes leftIn  { from { opacity: 0; transform: translateX(-46px); }
                        to   { opacity: 1; transform: none; } }
   @keyframes dropOut { from { opacity: 1; transform: none; }
                        to   { opacity: 0; transform: translateY(34px) scale(.94); } }
   @keyframes dropIn  { from { opacity: 0; transform: translateY(40px) scale(.93); }
                        to   { opacity: 1; transform: none; } }
   @keyframes slideOut { from { opacity: 1; transform: none; }
                         to   { opacity: 0; transform: translateX(30px); } }
   @keyframes slideIn  { from { opacity: 0; transform: translateX(34px); }
                         to   { opacity: 1; transform: none; } }
   /* THE FIELD'S OWN ENTRANCE, RUN BACKWARDS AND FORWARDS.
      Production scatters each square in from a random offset, rotation and
      blur when the dashboard is first entered — every square still carries
      its own --sx/--sy/--sr/--sd. Going out is that same motion reversed;
      coming back is `fieldSettle` itself, the identical animation you see on
      first entry. Nothing invented: the chart leaves the way it arrived. */
   @keyframes sqAway {
      from { opacity: 1; filter: blur(0);
             transform: translate(-50%, -50%) rotate(0deg) scale(1); }
      70%  { opacity: 1; }
      to   { opacity: 0; filter: blur(5px);
             transform: translate(calc(-50% + var(--sx, 0px)),
                                  calc(-50% + var(--sy, 0px)))
                        rotate(var(--sr, 0deg)) scale(.85); }
   }

   /* The profile button carries a 1px border, and scaling a bordered box
      lands its edge on half a pixel — which is the bottom edge going missing.
      It travels, it never scales. */
   @keyframes ctaOut { from { opacity: 1; transform: none; }
                       to   { opacity: 0; transform: translateY(22px); } }
   @keyframes ctaIn  { from { opacity: 0; transform: translateY(26px); }
                       to   { opacity: 1; transform: none; } }
   @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
   @keyframes fadeIn  { from { opacity: 0; } to { opacity: 1; } }

   body.story-film .app { pointer-events: none; }

   /* The picture is DISPLAYED from the start of the open, still invisible.
      An animation cannot run on a display:none element, so when the film's
      class landed 420ms later the browser had nothing to start and jumped
      straight to full — a cut where `filmIn` was declared. Given a frame of
      existence first, the dissolve actually plays. */
   /* The card does not expand over a page that is still leaving. It is held
      at the mark's own size until the picture has arrived, then released —
      production's transition carries it open from there. */
   /* `:has(#intro…)` in production's own rule counts as an ID, so its
      `width: calc(100vw - 24px) !important` scored (2,1,1) against this one's
      (1,3,1) and won outright. Matching its ID count puts this ahead. */
   /* Out of sight while the board leaves — production morphs it open behind
      this, so by the time it is shown the morph is over and there is nothing
      to stutter. It fades up with the picture, already open. */
   body.film-arming:not(.story-film) #cp-card,
   body.film-arming:not(.story-film) #cp-ring {
      opacity: 0 !important;
      transition: none !important;
   }

   @keyframes cardIn { from { opacity: 0; transform: translateY(-6px); }
                       to   { opacity: 1; transform: none; } }

   body.story-film #cp-card { animation: cardIn .42s cubic-bezier(.16, 1, .3, 1) both; }

   /* the compositor is told a frame early, on the pointer */
   body.film-arming #intro,
   body.film-arming #film-ground { will-change: opacity; }

   #film-ground {
      position: fixed;
      inset: 0;
      z-index: 2998;
      background: #0C0A08;
      opacity: 0;
      pointer-events: none;
   }

   @keyframes groundIn  { from { opacity: 0; } to { opacity: 1; } }
   @keyframes groundOut { from { opacity: 1; } to { opacity: 0; } }

   /* ONE rule. Collapsing the state machine left two of these both matching
      `story-film`, and the second — a leftover that pinned it opaque — won.
      The ground snapped from nothing to solid black on a single frame, which
      is the hard cut at the start of the open: 226 to 52 in one frame. */
   /* The ground existed to cover a gap that no longer exists: the picture now
      rises on the same frame the page starts leaving. Ramped, it merely got
      there first and put a black dip in the middle of a cross-dissolve. It
      stays in the document as a backstop but is never brought up. */
   body.story-film #film-ground,
   body.film-closing #film-ground { opacity: 0; animation: none; }

   body.story-film #intro.is-gone {
      display: flex !important;
      opacity: 0;
      pointer-events: none !important;
   }

   /* The dashboard's picture is the STORY's markup. Its stage was still
      transitioning out while the film faded in, so the story's own headline
      ghosted across the dashboard for half a second. Down before the picture
      is shown, not during it. */
   body.story-film #intro.is-gone #intro-stage,
   body.story-film #intro.is-gone .intro-rail,
   body.story-film #intro.is-gone #intro-skip {
      opacity: 0 !important;
      transition: none !important;
   }

   body.story-film #intro-replay { animation: upOut .22s ease both; }
   body.story-film #tour-btn     { animation: upOut .22s ease both .03s; }
   body.story-film .segment      { animation: slideOut .26s ease both .07s; }
   body.story-film .searchwrap   { animation: upOut .26s ease both .1s; }
   body.story-film .topright     { animation: upOut .26s ease both .13s; }

   body.story-film #hero-title   { animation: leftOut .34s cubic-bezier(.45, .12, .5, .94) both .02s; }
   body.story-film #hero-sub     { animation: leftOut .34s cubic-bezier(.45, .12, .5, .94) both .07s; }
   body.story-film #score-panel  { animation: dropOut .36s cubic-bezier(.45, .12, .5, .94) both .12s; }
   body.story-film #profile-cta  { animation: ctaOut .32s cubic-bezier(.45, .12, .5, .94) both .18s; }
   body.story-film #kpis-wrap,
   body.story-film #card-wrap    { animation: dropOut .34s cubic-bezier(.45, .12, .5, .94) both .16s; }

   body.story-film #dotfield .ssq {
      animation: sqAway .46s cubic-bezier(.45, .12, .5, .94) both;
      animation-delay: calc(var(--sd, .1s) * .5);
   }

   body.story-film #dotfield .ghostsq  { animation: fadeOut .22s ease both; }
   body.story-film #dotfield .histaxis { animation: fadeOut .24s ease both .08s; }
   body.story-film .fieldlegend        { animation: fadeOut .24s ease both .1s; }

   body.dash-landing #dotfield .ssq {
      animation: fieldSettle .76s cubic-bezier(.16, 1, .3, 1) both;
      animation-delay: var(--sd, .1s);
   }

   body.dash-landing #dotfield .ghostsq  { animation: fadeIn .36s ease both .1s; }
   body.dash-landing #dotfield .histaxis { animation: fadeIn .36s ease both .18s; }

   body.dash-landing #hero-title  { animation: leftIn .6s cubic-bezier(.16, 1, .3, 1) both .12s; }
   body.dash-landing #hero-sub    { animation: leftIn .6s cubic-bezier(.16, 1, .3, 1) both .2s; }
   body.dash-landing #score-panel { animation: dropIn .64s cubic-bezier(.16, 1, .3, 1) both .28s; }
   body.dash-landing #profile-cta { animation: ctaIn .5s cubic-bezier(.16, 1, .3, 1) both .3s; }
   body.dash-landing #kpis-wrap,
   body.dash-landing #card-wrap   { animation: dropIn .54s cubic-bezier(.16, 1, .3, 1) both .25s; }

   body.dash-landing #intro-replay { animation: upIn .4s cubic-bezier(.16, 1, .3, 1) both .28s; }
   body.dash-landing #tour-btn     { animation: upIn .4s cubic-bezier(.16, 1, .3, 1) both .32s; }
   body.dash-landing .segment      { animation: slideIn .42s cubic-bezier(.16, 1, .3, 1) both .36s; }
   body.dash-landing .searchwrap   { animation: upIn .42s cubic-bezier(.16, 1, .3, 1) both .4s; }
   body.dash-landing .topright     { animation: upIn .42s cubic-bezier(.16, 1, .3, 1) both .44s; }
   body.dash-landing .fieldlegend  { animation: fadeIn .38s ease both .42s; }

   /* only the DASHBOARD's re-shown intro fades — on the story #intro is the
      page itself, and fading it would take the story with it */
   body.story-film #intro.is-gone,
   body.film-closing #intro.is-gone {
      display: flex !important;
      pointer-events: auto !important;
      transition: opacity .45s ease;
   }

   /* THE PICTURE IS ALWAYS LIVE. A <video> inside a display:none subtree is
      not rendering, so the film could fade in showing #intro's own dark
      ground with the footage arriving a frame or two later — a dark dip, then
      a snap as the picture appeared. Kept displayed at zero opacity it is
      already painting when the fade begins, so the fade shows the film. */
   /* ---- THE TIER PICKER ------------------------------------------------
      The bar's own insets and gap are production's again: taking them to 8px
      and 5px to win width also pulled every control closer together, which is
      why the ring ended up against the Story button. The picker keeps a
      little more than production's 168 so the longest option still reads,
      and the search takes the rest. */
   .topright {
      flex: 0 0 178px !important;
      width: 178px !important;
   }

   .searchwrap { flex: 1 1 auto !important; }

   /* ---- THE SHEET'S LEGEND TICK, AS ON DESKTOP -------------------------
      It was reading as a mark in its own right — taller and darker than the
      thing it stands for. It is a caption, not a datum. */
   .kpikey .kk--tick {
      width: 1px !important;
      height: 7px !important;
      background: rgba(22, 19, 14, .26) !important;
      vertical-align: -1px;
   }

   /* ---- THE COLUMN MAY NOT SCROLL WHILE THE FILM IS UP ------------------
      The cascade translates the column's children, and a transformed box
      still contributes to scrollable overflow: `.leftcol` went from 277px of
      content to 1242 the moment the film opened, turning a fixed panel into
      a scroller. A thumb moving it there leaves the last element — the
      profile button — bisected by the scroll edge when everything comes
      back, which is its bottom border going missing. */
   body.story-film .leftcol,
   body.colophon-out .leftcol,
   body.film-closing .leftcol {
      overflow: hidden !important;
   }

   /* ---- ONE CARD, TWO PAGES -------------------------------------------
      Production gives the dashboard's card its own phone metrics: 32px of top
      padding against the story's 26, no bottom padding against its 25, and an
      avatar two pixels larger. The result was the same card with everything
      sitting six pixels lower on one page than the other. The story's
      measurements win on both. */
   /* `.on` comes off the instant you tap to close, so these stopped applying
      for the whole fold and the contents snapped back to production's own
      dashboard metrics for a beat before disappearing. `credit-closing` —
      production's own class for the fold — carries them to the end. */
   body:has(#intro.is-gone) #cp-card.on .cp-body,
   body:has(#intro.is-gone).credit-closing #cp-card .cp-body {
      padding: 26px 15px 25px !important;
   }

   /* OPEN ONLY. Applying this to the closed card as well put a 42px photo
      inside a 40px mark — the picture overflowing its own ring. The closed
      state keeps production's 30px. */
   body:has(#intro.is-gone) #cp-card.on .cp-avwrap {
      width: 42px !important;
      height: 42px !important;
   }

   /* The photo is absolutely placed, and the dashboard sat it 11px lower when
      open. ONLY when open: during the fold it has to travel 26px -> 5px and
      shrink 42 -> 30 to land concentric in the mark, exactly as it does on the
      story. Pinning it through `credit-closing` froze that journey and left
      the picture stranded at the top-left while the card closed around it. */
   body:has(#intro.is-gone) #cp-card.on .cp-avwrap {
      top: 26px !important;
   }

   /* ---- FIRST-FRAME CORRECTNESS, WITHOUT ANY SCRIPT --------------------
      Keyed on production's own `credit-open`, so the scrim's 14px blur never
      paints, the card never shows its paper glass, and the ring never shows
      at all — from the very first frame of the open, with no timing to lose. */
   body.credit-open #cp-scrim { opacity: 0 !important; }

   body.credit-open #cp-card,
   body.credit-open #cp-card.on {
      background: rgba(16, 13, 10, .4) !important;
      -webkit-backdrop-filter: blur(18px) saturate(185%) !important;
      backdrop-filter: blur(18px) saturate(185%) !important;
      box-shadow: 0 18px 44px rgba(0, 0, 0, .38) !important;
      border: 0 !important;
   }

   body.credit-open #cp-ring { opacity: 0 !important; }

   body.credit-open #cp-card .cp-kicker { color: rgba(245, 243, 238, .62) !important; }
   body.credit-open #cp-card .cp-fullname { color: #F7F5F1 !important; }
   body.credit-open #cp-card .cp-row,
   body.credit-open #cp-card .cp-row span { color: rgba(245, 243, 238, .88) !important; }
   body.credit-open #cp-card .cp-row i { color: rgba(245, 243, 238, .62) !important; }
   body.credit-open #cp-card .cp-row + .cp-row::before {
      background: rgba(245, 243, 238, .18) !important;
   }

   /* THE VIGNETTE ARRIVES WITH THE PICTURE. `#promo-scrim` runs production's
      own intro-entrance animation, so on the dashboard the film opened as raw
      footage and the vignette faded up behind it a second later — measured at
      0.00 on open, 0.50 at 700ms, 0.94 at 1100ms. Held at full for the whole
      film, so the first frame is already the signature look. */
   body.story-film #intro.is-gone #promo-scrim,
   body.film-closing #intro.is-gone #promo-scrim {
      opacity: 1 !important;
      animation: none !important;
   }

   @keyframes filmIn { from { opacity: 0; } to { opacity: 1; } }

   /* display:none to display:flex gives a transition nothing to move from,
      so the picture arrives on an animation instead of snapping on */
   /* No `opacity: 1 !important` here. !important beats an animation, so the
      picture jumped straight to full instead of playing `filmIn` — a hard cut
      where a dissolve was declared. The animation's fill holds it at 1. */
   body.story-film #intro.is-gone {
      /* fast attack: the previous curve sat near zero for its first 150ms,
         which put 175ms of flat black between the board leaving and the
         picture registering — the stall that read as chop */
      animation: filmIn .62s cubic-bezier(0, .45, .35, 1) both;
   }
   /* An ANIMATION out, not a plain switch. The picture came in on `filmIn`
      with fill:both; dropping that animation returns the value on the frame,
      so the picture vanished instantly and left the page — whose own landing
      is spread across staggered elements — showing white underneath. */
   @keyframes filmOut { from { opacity: 1; } to { opacity: 0; } }

   body.film-closing #intro.is-gone {
      animation: filmOut .6s ease .18s both;
   }

   /* No white flash: the board lands UNDERNEATH the picture and the picture
      dissolves off it. Cross-fading the two left the page's own white showing
      through the middle of the dissolve. (This used to force opacity with
      !important, which would now beat the landing animation outright.) */

   /* And it is the STORY's markup being used as the picture — its title and
      subtitle live inside it. Releasing them at step two flashed the story's
      words across the dashboard for a beat. They stay down until the picture
      has gone; on the story itself (#intro without `is-gone`) this does not
      apply, because there the page returning IS the point. */
   body.film-closing #intro.is-gone #intro-stage,
   body.film-closing #intro.is-gone .intro-rail,
   body.film-closing #intro.is-gone #intro-skip { opacity: 0 !important; }

   /* The ring is last. It stays out for the whole sequence — through the
      colophon leaving, the page returning and the card folding — and only
      then fades up, by which time there is a photograph inside it. It used
      to reappear as a bare circle with nothing in it. */
   body.film-arming #cp-ring,
   body.story-film #cp-ring,
   body.story-film #cp-ring,
   body.colophon-out #cp-ring,
   body.film-closing #cp-ring { opacity: 0 !important; }

   @keyframes ringBack { from { opacity: 0; } to { opacity: 1; } }

   body.ring-back #cp-ring { animation: ringBack .34s ease both; }

   /* step one: the colophon leaves while the picture is still running and
      the story is still standing down */
   body.colophon-out #story-colophon {
      opacity: 0 !important;
      transition: opacity .34s ease !important;
   }

   body.colophon-out #intro-stage,
   body.colophon-out .intro-rail,
   body.colophon-out #intro-skip { opacity: 0 !important; }

   /* The dashboard's green ring stays: it is only ever seen CLOSED now —
      the moment the card opens the ring drops to nothing — so it reads on
      paper exactly as it does on desktop. */

   body.story-film #cp-ring,
   body.film-closing #cp-ring {
      border-color: rgba(245, 243, 238, .55) !important;
   }

   /* the clue belongs to the page, not to the film: over film it painted
      above the card and bled through its glass */
   body.film-arming .tap-hint,
   body.film-arming .hl-read,
   body.film-arming #hint-layer,
   body.story-film .tap-hint,
   body.story-film .hl-read,
   body.story-film #hint-layer,
   body.colophon-out .tap-hint,
   body.colophon-out .hl-read,
   body.colophon-out #hint-layer,
   body.film-closing .tap-hint,
   body.film-closing .hl-read,
   body.film-closing #hint-layer {
      opacity: 0 !important;
      transition: opacity .2s ease !important;
   }

   /* ---- THE CLUE, ON LANDING -------------------------------------------
      One state that actually MOVED, its journey drawn, its scores read out —
      and the words under the profile button where the thumb already is. */
   #hint-layer {
      position: fixed;
      inset: 0;
      z-index: 3550;
      pointer-events: none;
      opacity: 0;
      transition: opacity .45s ease;
   }

   body.tap-hint-on #hint-layer { opacity: 1; }

   #hint-layer .hl-run {
      fill: none;
      stroke: rgba(22, 19, 14, .5);
      stroke-width: 1.5;
      stroke-linecap: round;
      stroke-dasharray: 1 5;
   }

   #hint-layer .hl-head { fill: rgba(22, 19, 14, .62); }

   .hl-read {
      position: fixed;
      z-index: 3560;
      display: flex;
      align-items: center;
      gap: 9px;
      padding: 7px 11px;
      border-radius: 9px;
      background: rgba(255, 255, 255, .52);
      -webkit-backdrop-filter: blur(18px) saturate(185%);
      backdrop-filter: blur(18px) saturate(185%);
      border: 1px solid rgba(255, 255, 255, .6);
      box-shadow: 0 8px 22px rgba(28, 20, 14, .14);
      font-family: var(--mono);
      font-size: 8.5px;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: #6B645A;
      white-space: nowrap;
      pointer-events: none;
      opacity: 0;
      transform: translateY(5px);
      transition: opacity .4s ease .12s,
                  transform .6s cubic-bezier(.16, 1, .3, 1) .12s;
   }

   .hl-read b { font-size: 13px; letter-spacing: 0; color: #2F2A22; font-weight: 600; }
   .hl-read i { font-style: normal; color: #B9B2A6; }
   .hl-read u {
      text-decoration: none;
      padding: 2px 5px;
      border-radius: 5px;
      background: rgba(197, 106, 68, .13);
      color: #B0552F;
   }

   body.tap-hint-on .hl-read { opacity: 1; transform: none; }

   /* the words sit under the profile button, not over the chart */
   .tap-hint {
      left: 50%;
      transform: translate(-50%, 6px);
   }

   body.tap-hint-on .tap-hint { transform: translate(-50%, 0); }
}

/* ---- THE PHONE'S COLOPHON ------------------------------------------------
   The same film the desktop raises, sized for 390px. On the dashboard the
   intro is shown again purely as the picture: everything it used to carry is
   already stood down by the rules below. */
@media (max-width: 640px) {

   /* ---- THE CHART COMES DOWN TO MEET THE TIMELINE -----------------------
      Measured at 390x844: the caption's foot sat at 633 and the timeline
      blocks began at 763 — 130px of nothing between the chart and the rail,
      while the chart itself was the smallest thing on the screen. The copy,
      the headline and the years stay exactly where they are; the chart grows
      into the space below it and is pushed down to close the gap. */
   .intro-chart {
      width: 89vw !important;
      height: 30vh !important;
      min-height: 176px !important;
      margin-top: 26px !important;
   }

   /* The STORY's intro is the page and needs forcing visible. The DASHBOARD's
      re-shown intro must NOT be forced: `opacity: 1 !important` beats an
      animation outright, so `filmIn` ran — playState running, currentTime
      advancing — while the computed value stayed pinned at 1, and the picture
      cut in instead of dissolving. Third time this rule has bitten tonight. */
   body.story-film #intro:not(.is-gone) {
      display: flex !important;
      opacity: 1 !important;
      pointer-events: auto !important;
   }

   body.story-film #intro.is-gone {
      display: flex !important;
      pointer-events: auto !important;
   }

   body.story-film #intro-stage,
   body.story-film .intro-rail,
   body.story-film #intro-skip {
      opacity: 0 !important;
      pointer-events: none;
      transition: opacity .5s ease;
   }

   /* leaving is unhurried, returning is not */
   body.film-closing #intro-stage,
   body.film-closing .intro-rail,
   body.film-closing #intro-skip { transition: opacity .3s ease; }

   /* LEAVING IS PROMPT, RETURNING WAITS FOR THE GLASS.
      The story's headline is large and white, and it was coming back while
      the card was still at full width — so it sat behind the glass, which is
      the one place white type has nowhere to hide.

      An ANIMATION, not a delayed transition: the delay was declared and
      measured correct at rest (0.36s) and simply not honoured when the class
      came off — the title started 21ms after the removal. An animation on
      `film-closing` cannot be skipped. Only the story's own intro is
      addressed, so the dashboard's sequence is untouched. */
   #intro-stage, .intro-rail, #intro-skip {
      transition: opacity .34s ease;
   }

   /* THE STORY COMES BACK IN ITS OWN LANDING.
      Production already animates the scene's contents with distinct, staggered
      gestures — `fxTrack` on the year, `fxRise` on the headline, the copy and
      the figure, 0.1s apart. Overriding those with my own was what made the
      page animate TWICE: dropping my class returned production's animation
      names, and a changed animation-name restarts the animation, so the
      landing played a second time a beat later.

      So nothing here touches them. They are simply replayed, once, at the
      moment the glass is folding (zz-variant.js). Only the two elements
      production does NOT animate get gestures of their own. */
   @keyframes sbDown { from { opacity: 0; transform: translateY(-10px); }
                       to   { opacity: 1; transform: none; } }
   @keyframes sbUp   { from { opacity: 0; transform: translateY(16px); }
                       to   { opacity: 1; transform: none; } }

   /* The stage stays down until the replay actually begins. Shown at step two
      it was visible for 340ms with its contents already at full opacity —
      the white headline back on the glass, which is the thing this whole
      sequence exists to avoid. `story-back` is set by the same timer that
      replays the landing. */
   body.film-closing #intro:not(.is-gone) #intro-stage {
      opacity: 0 !important;
      animation: none !important;
   }

   body.story-back #intro:not(.is-gone) #intro-stage {
      opacity: 1 !important;
   }

   body.film-closing #intro:not(.is-gone) #intro-skip {
      animation: sbDown .5s cubic-bezier(.16, 1, .3, 1) .42s both;
   }

   body.film-closing #intro:not(.is-gone) .intro-rail {
      animation: sbUp .58s cubic-bezier(.16, 1, .3, 1) .62s both;
   }

   #story-colophon {
      position: absolute;
      inset: 0;
      z-index: 40;
      opacity: 0;
      pointer-events: none;
      /* the page needs its half-second to leave before this arrives —
         at .1s the two were crossing over each other */
      transition: opacity .5s ease .42s;
   }

   body.story-film #story-colophon { opacity: 1; pointer-events: auto; }

   #story-colophon .ms-method {
      position: absolute;
      left: 20px;
      right: 20px;
      bottom: 78px;
      width: auto;
      color: rgba(245, 243, 238, .74);
      font-family: var(--ui, system-ui), sans-serif;
      font-size: 12.5px;
      line-height: 1.55;
      opacity: 0;
      transform: translateY(8px);
      transition: opacity .5s ease .52s,
                  transform .9s cubic-bezier(.16, 1, .3, 1) .52s;
   }

   body.story-film #story-colophon .ms-method { opacity: 1; transform: none; }

   #story-colophon .msm-k {
      font-family: var(--mono);
      font-size: 8.5px;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: rgba(245, 243, 238, .5);
      margin-bottom: 9px;
   }

   #story-colophon .ms-method p { margin: 0 0 9px 0; }
   #story-colophon .ms-method p:last-child { margin-bottom: 0; }
   #story-colophon .ms-method b { font-weight: 500; color: rgba(245, 243, 238, .95); }

   #story-colophon .msm-caveat {
      color: rgba(245, 243, 238, .56);
      font-size: 11.5px;
      padding-top: 8px;
      border-top: 1px solid rgba(245, 243, 238, .16);
   }

   /* 350px cannot hold the name, a rule and three sources on one line — it
      wrapped both halves into each other. The strip stands up instead: name,
      rule, sources, in the order the eye reads them. */
   #story-colophon .sc-line {
      position: absolute;
      left: 20px;
      right: 20px;
      bottom: 18px;
      display: block;
      font-family: var(--mono);
      font-size: 8px;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: rgba(245, 243, 238, .58);
   }

   #story-colophon .sc-line b,
   #story-colophon .sc-line span { display: block; }

   #story-colophon .sc-line b { font-weight: 500; color: rgba(245, 243, 238, .92); }

   /* No rule between them on the phone: stacked, the two lines already read
      as two lines, and a full-width hairline across a 350px screen was the
      loudest thing in the frame. The spacing carries the separation. */
   #story-colophon .sc-line i {
      display: block;
      height: 0;
      margin: 6px 0 0;
      background: none;
   }

   #story-colophon .sc-line a {
      color: inherit;
      text-decoration: none;
      border-bottom: 1px solid rgba(245, 243, 238, .3);
   }

   #story-colophon .sc-line em {
      font-style: normal;
      color: rgba(245, 243, 238, .34);
      margin: 0 4px;
   }

   /* over the picture the mark wears the story's ring on BOTH pages — the
      dashboard's green belongs to paper, not to film */
   body.story-film #cp-ring {
      border-color: rgba(245, 243, 238, .55) !important;
   }

   /* The card's own scrim carries a 14px backdrop blur. Over film it is
      merely redundant — the picture is dark enough — but on the way OUT it
      was the real damage: production holds it at full strength until the
      card closes, so the moment the colophon left, a full-screen blur came
      up over the page that was trying to return, and stayed for a second.
      It is suppressed over film and taken off entirely for the close. */
   body.story-film #cp-scrim { opacity: .22 !important; }

   body.film-arming #cp-scrim,
   body.story-film #cp-scrim,
   body.colophon-out #cp-scrim,
   body.film-closing #cp-scrim {
      opacity: 0 !important;
      transition: opacity .28s ease !important;
   }
}

/* the blank-story guard's second lever: nothing may hold the story down */
body.story-heal #intro-stage,
body.story-heal .intro-rail,
body.story-heal #intro-skip,
body.story-heal .intro-scene.on {
   opacity: 1 !important;
   visibility: visible !important;
}

/* ---- SHORT PHONE VIEWPORTS: THE BUTTON MUST NOT BE CLIPPED --------------
   With Safari's chrome the usable height drops to roughly 620-660, and the
   column — which is `overflow: auto` — was overflowing by 7px. The profile
   button, being last, had its bottom border sitting exactly on the clip edge
   or just past it. That is the "cut off" button, and no z-index can fix it:
   clipping is not stacking.

   Seven pixels is all it needs, so the spacing gives it back rather than the
   page gaining a scrollbar. */
@media (max-width: 640px) and (max-height: 720px) {
   #score-panel { margin-top: 6px !important; }
   #profile-cta { margin-top: 6px !important; }
}

/* Squeezing the tiles made them TALLER — production sizes them from the
   inside, and overriding the padding let the content expand. The height comes
   from the field instead, which has plenty: the column simply reaches further
   down the screen, and the chart takes what is left. */
@media (max-width: 640px) and (max-height: 700px) {
   .leftcol { bottom: 42% !important; }
}

@media (max-width: 640px) and (max-height: 620px) {
   .leftcol { bottom: 36% !important; }
}
