/* ==========================================================================
   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; } }
   @keyframes sqAway { from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
                       to   { opacity: 0; transform: translate(-50%, -50%) scale(.5); } }
   @keyframes sqBack { from { opacity: 0; transform: translate(-50%, -50%) scale(.5); }
                       to   { opacity: 1; transform: translate(-50%, -50%) scale(1); } }
   @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 .42s cubic-bezier(.45, .12, .5, .94) both;
      animation-delay: calc(var(--sd, 0s) * .5);
   }

   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: sqBack .5s cubic-bezier(.16, 1, .3, 1) both;
      animation-delay: calc(var(--sd, 0s) * .75 + .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;
      width: min(46ch, 42vw);
      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;
   }

   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.board-out #cp-card,
   body.board-out #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; } }
   @keyframes sqAway { from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
                       to   { opacity: 0; transform: translate(-50%, -50%) scale(.28); } }
   @keyframes sqBack { from { opacity: 0; transform: translate(-50%, -50%) scale(.28); }
                       to   { opacity: 1; transform: translate(-50%, -50%) scale(1); } }
   @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
   @keyframes fadeIn  { from { opacity: 0; } to { opacity: 1; } }

   body.board-out .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. */
   body.card-hold:has(#intro):not(.story-film) #cp-card.on,
   body.card-hold:has(#intro):not(.story-film) #cp-ring.on {
      width: 40px !important;
      height: 40px !important;
      padding: 0 !important;
   }

   body.card-hold:has(#intro):not(.story-film) #cp-card .cp-body {
      opacity: 0 !important;
      transition: opacity .18s ease !important;
   }

   /* the compositor is told a frame early, on the pointer */
   body.card-hold #intro,
   body.card-hold #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; } }

   body.board-out #film-ground  { animation: groundIn .4s cubic-bezier(.4, 0, .5, 1) both; }
   body.story-film #film-ground { opacity: 1; animation: none; }
   body.film-closing #film-ground { animation: groundOut .5s ease .2s both; }

   /* the held mark is the CLOSED mark: production's open state grows the
      photo to 48px, which a 40px card with overflow:hidden then cropped into
      a grey blob */
   body.card-hold:has(#intro):not(.story-film) #cp-card .cp-avwrap {
      width: 31px !important;
      height: 31px !important;
   }

   body.card-hold:has(#intro):not(.story-film) #cp-card img {
      width: 31px !important;
      height: 31px !important;
   }

   body.board-out #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.board-out #intro.is-gone #intro-stage,
   body.board-out #intro.is-gone .intro-rail,
   body.board-out #intro.is-gone #intro-skip {
      opacity: 0 !important;
      transition: none !important;
   }

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

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

   body.board-out #dotfield .ssq {
      animation: sqAway .3s cubic-bezier(.45, .12, .5, .94) both;
      animation-delay: calc(var(--sd, 0s) * .3);
   }

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

   body.dash-landing #dotfield .ssq {
      animation: sqBack .46s cubic-bezier(.16, 1, .3, 1) both;
      animation-delay: calc(var(--sd, 0s) * .5);
   }

   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: dropIn .56s cubic-bezier(.16, 1, .3, 1) both .36s; }
   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;
   }

   @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.card-hold #cp-ring,
   body.board-out #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; }

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

   #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); }

   #story-colophon .sc-line i {
      display: block;
      width: 100%;
      height: 1px;
      margin: 7px 0;
      background: rgba(245, 243, 238, .22);
   }

   #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.card-hold #cp-scrim,
   body.board-out #cp-scrim,
   body.colophon-out #cp-scrim,
   body.film-closing #cp-scrim {
      opacity: 0 !important;
      transition: opacity .28s ease !important;
   }
}
