/* ============================================================
   FONTS
   ------------------------------------------------------------
   Both families are on Google Fonts; they are served from
   src/fonts/ instead so the reader and the print sheet render
   the same with no network, and so the PDF export embeds real
   outlines rather than whatever the machine falls back to.

   woff2 only - every browser this book targets supports it.
   latin plus latin-ext, because the German legal terms in the
   text reach past the latin subset.

   To add a weight: drop the file in src/fonts/ and add a block
   here. Nothing else refers to font files.
   ============================================================ */

/* --- Roboto Serif — display, headings, footnote markers ---- */
@font-face {
  font-family: 'Roboto Serif';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/roboto-serif-latin-400-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Roboto Serif';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/roboto-serif-latin-ext-400-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Roboto Serif';
  font-style: normal;
  font-weight: 500;
  font-display: block;
  src: url('fonts/roboto-serif-latin-500-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Roboto Serif';
  font-style: normal;
  font-weight: 500;
  font-display: block;
  src: url('fonts/roboto-serif-latin-ext-500-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Roboto Serif';
  font-style: normal;
  font-weight: 600;
  font-display: block;
  src: url('fonts/roboto-serif-latin-600-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Roboto Serif';
  font-style: normal;
  font-weight: 600;
  font-display: block;
  src: url('fonts/roboto-serif-latin-ext-600-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --- TASA Orbiter — running text, UI, labels --------------- */
@font-face {
  font-family: 'TASA Orbiter';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/tasa-orbiter-latin-400-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'TASA Orbiter';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url('fonts/tasa-orbiter-latin-ext-400-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'TASA Orbiter';
  font-style: normal;
  font-weight: 500;
  font-display: block;
  src: url('fonts/tasa-orbiter-latin-500-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'TASA Orbiter';
  font-style: normal;
  font-weight: 500;
  font-display: block;
  src: url('fonts/tasa-orbiter-latin-ext-500-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'TASA Orbiter';
  font-style: normal;
  font-weight: 600;
  font-display: block;
  src: url('fonts/tasa-orbiter-latin-600-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'TASA Orbiter';
  font-style: normal;
  font-weight: 600;
  font-display: block;
  src: url('fonts/tasa-orbiter-latin-ext-600-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}


/* ============================================================
   BRAND
   ------------------------------------------------------------
   Primitives only: the raw palette, the typefaces, the scale.
   Nothing here refers to a place in the UI.

   To rebrand the book, change this file and nothing else.
   tokens.css maps these primitives onto roles ("sidebar
   background", "table header"), and the component styles only
   ever read the roles. That indirection is what makes a colour
   or type change safe.
   ============================================================ */
:root {

  /* --- Blue ramp ------------------------------------------ */
  --blue-50:   #EDF4FF;   /* sidebar, table header, highlight fill */
  --blue-75:   #E0EDFF;   /* chip fill                             */
  --blue-100:  #D4E5FF;   /* hover fill, box border                */
  --blue-150:  #C9DFFF;   /* progress track                        */
  --blue-200:  #BDD7FF;   /* mobile drag bar                       */
  --blue-500:  #146EF5;   /* the brand blue                        */
  --blue-700:  #385D94;   /* icon stroke                           */
  --blue-900:  #002D70;   /* section labels                        */
  --blue-950:  #002660;   /* book title, breadcrumb parent         */
  --blue-ink:  #00112A;   /* sidebar item text                     */

  /* --- Neutral ramp --------------------------------------- */
  --white:     #FFFFFF;
  --grey-25:   #FCFCFC;   /* page background          */
  --grey-50:   #FAFBFC;   /* code block, zebra list    */
  --grey-100:  #F5F6F8;   /* card footer              */
  --grey-150:  #F0F0F1;   /* sidebar, bar, table row  */
  --grey-250:  #DEDEE0;   /* hairline on grey-150     */
  --grey-200:  #E4E7EC;   /* container border         */
  --grey-225:  #D2D5DB;   /* container stroke         */
  --grey-300:  #C8C8C8;   /* footnote rule            */
  --grey-400:  #8F97A6;   /* eyebrow                  */
  --grey-500:  #5B6B85;   /* small labels             */
  --grey-900:  #1C1C1C;   /* body copy                */

  /* Chart fills. Light enough that the value sits on them in navy
     rather than needing reversed type. */
  --blue-125:  #C7DDFF;   /* cost bar, chart 5        */
  --blue-300:  #6FA5FA;   /* chart 4                  */
  --red-125:   #FFD4CE;   /* lost-media bar           */
  --red-700:   #B3261E;   /* figures on a chart fill  */

  /* Status colours. Each solid carries white text at 5:1 or better, and
     each tint carries its own solid at 6:1 or better. */
  --green-800:  #145F30;   /* verdict: full switch     */
  --green-700:  #1E7B3C;   /* verdict: federated       */
  --green-50:   #E9F7EE;   /* green cluster note       */
  --amber-700:  #A05A00;   /* verdict: partial switch  */
  --amber-800:  #8A4B00;   /* amber note text          */
  --amber-50:   #FFF4E5;   /* amber cluster note       */
  --red-800:    #98201A;   /* red note text            */
  --red-50:     #FDECEA;   /* red cluster note         */
  --black:     #000000;

  /* --- Typefaces ------------------------------------------
     Both load from Google Fonts in template.html. Swapping a
     family means changing it in both places.                 */
  --font-serif: 'Roboto Serif', 'PT Serif', Georgia, serif;
  --font-sans:  'TASA Orbiter', 'Inter', system-ui, -apple-system, sans-serif;
  --font-mono:  ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;

  --weight-regular:  400;
  --weight-medium:   500;
  --weight-semibold: 600;

  /* --- Type scale -----------------------------------------
     14px is a hard floor; nothing in the book renders smaller.
     Each step carries its own line height and tracking, because
     tighter tracking on display sizes is part of the design,
     not an accident.

     The whole scale sits four points above where it started: the
     reader is read at arm's length on a wide screen, and every step
     moved together so the relationships between them are unchanged.
     Table cells are the exception - see --size-table below.       */
  --size-display: 48px;  --lh-display: 1.1;  --track-display: -0.05em;
  --size-title:   36px;  --lh-title:   1.0;  --track-title:   -0.05em;
  --size-heading: 28px;  --lh-heading: 1.3;  --track-heading: -0.04em;
  --size-lead:    24px;  --lh-lead:    1.25; --track-lead:    -0.04em;
  --size-body:    20px;  --lh-body:    1.6;  --track-body:    -0.03em;
  --size-small:   18px;  --lh-small:   1.4;  --track-small:   -0.03em;
  --size-ui:      18px;  --lh-ui:      1.3;  --track-ui:      -0.05em;
  --size-min:     18px;
  /* Footnote markers only — the in-text superscript and the numeral in
     the footnote list. Deliberately below --size-min: they are reference
     glyphs, not text to be read. */
  --size-marker:  12px;
  /* Smaller again on a phone, where the marker sits in 16px prose. */
  --size-marker-sm: 10px;
  /* Footnote text, also below the floor by choice: sources and links
     are apparatus, read on purpose rather than in the flow. */
  --size-footnote: 16px;
  --size-caption: 18px;
  /* The figure in a stat square, and the same figure in the stacked
     variant where the row is tighter. */
  /* Held back from the step with the tables and the chrome: a figure
     column has to hold its longest value on one line, and four points
     put 'Up to 94%' onto two and the headcount sentence onto three. */
  --size-stat:    30px;
  --size-stat-sm: 28px;
  /* --- Table cells ----------------------------------------
     A cell reads at the same size as the prose around it. Holding them
     back kept the columns inside the text measure, and the price was a
     table set two steps under the paragraph that introduces it; the
     tables are let out past the measure instead - see .article__wide in
     article.css - and the type follows the body.
     --size-table-cap is the grade table's own header row.           */
  --size-table:     20px;
  --size-table-sm:  20px;
  /* The comparison matrix runs eight columns wide and sets its header row
     a step under the cells - that is what this size is for. */
  --size-table-cap: 16px;
  /* --- Chrome ---------------------------------------------
     The sidebar and the bar across the top, also held back from the
     step. They are not the read: they are the furniture around it, and
     furniture that grows with the text stops being furniture. The
     contents list has to hold thirty rows in a fixed column without
     wrapping every one of them, and a label in the bar has a pill to
     stay inside. Same values the scale had before it moved.        */
  --size-chrome:    16px;
  --size-chrome-sm: 14px;
  --size-chrome-lg: 32px;
  /* The label that appears under an icon on hover. Below the floor by
     choice, and for the same reason as the footnote apparatus: it is the
     icon's name, read for a second and then gone, not text in the page.
     Small also keeps it inside the window - the icons sit at the end of
     the bar and the label is centred on the one it belongs to. */
  --size-tip:       12px;

  /* --- Shape and depth ------------------------------------ */
  --radius-sm:  6px;
  --radius-md:  8px;
  --radius-pill: 100px;

  --shadow-raised: 0 4px 4px rgba(201, 201, 201, 0.25);
  --shadow-float:  0 4px 19.7px rgba(157, 157, 157, 0.24);

  /* --- Motion ---------------------------------------------
     All of it is suppressed under prefers-reduced-motion.     */
  --speed-fast:   120ms;
  /* Long enough for a state under the cursor to be seen arriving,
     short enough that it never delays the pointer. */
  --speed-hover:  200ms;
  --speed-medium: 340ms;
  --speed-slow:   720ms;
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ============================================================
   THE SCALE ON A PHONE
   One place, one set of numbers. Every component in the book is
   built from these primitives, so a phone gets a coherent scale
   by restating the scale rather than by patching thirty
   components one at a time - which is how the reader ended up
   with 16px prose under a 24px component header.

   The steps keep their desktop relationships and all move down
   together: body 16, the blue component header a step above it,
   the small print a step below.

   The breakpoint is the reader's own, the one mobile.css uses.
   The print bundle restates every one of these in pdf.css after
   this file, so a sheet is never touched by this block.
   ============================================================ */
/* Between the phone layout and a full desktop the sidebar takes a fixed
   slice of the window, so the reading pane - which is what bounds how far
   a table may reach - is only a little wider than the text measure.
   Body-sized cells do not fit there, so in this band the cells stay where
   they were. The step comes in at 1440, where the pane finally has the
   room for it. */
@media (min-width: 1024px) and (max-width: 1439px) {
  :root {
    --size-table:     16px;
    --size-table-sm:  16px;
    --size-table-cap: 14px;
  }
}

@media (max-width: 1023px) {
  :root {
    --size-display: 34px;
    --size-title:   28px;
    --size-heading: 22px;
    --size-lead:    18px;
    --size-body:    16px;
    --size-small:   14px;
    --size-ui:      14px;
    --size-min:     14px;
    --size-caption: 14px;
    --size-footnote: 14px;
    --size-stat:    24px;
    --size-stat-sm: 22px;
    /* A stacked row is a block of its own with the whole width to use,
       so a cell reads at the body size here too. */
    --size-table:     16px;
    --size-table-sm:  16px;
    --size-table-cap: 16px;
  }
}


/* ============================================================
   TOKENS
   ------------------------------------------------------------
   Roles, not values. Every component reads from here; nothing
   reads a hex code directly.

   Geometry originates in Figma (layout-desktop, node 569:884).
   ============================================================ */
:root {

  /* ---------- Colour: surfaces ---------- */
  --c-page-bg:        var(--grey-25);
  --c-sidebar-bg:     var(--grey-150);
  --c-topbar-bg:      var(--white);
  --c-foot-bg:        var(--grey-100);
  /* grey-50 sits ~2% off white, which reads as no stripe at all once a
     table is more than a few rows deep. */
  --c-row-alt:        var(--grey-150);
  --c-bar-track:      var(--grey-100);
  --c-bar-cost:       var(--blue-125);
  --c-bar-lost:       var(--red-125);
  --c-bar-text-cost:  var(--blue-500);
  --c-bar-text-lost:  var(--red-700);
  --c-case-bg:        var(--blue-50);
  --c-case-rule:      var(--blue-125);
  --c-cal-head-bg:    var(--blue-50);
  --c-cal-num-bg:     var(--blue-500);
  --c-cal-num-fg:     var(--white);
  --c-mark-bg:        var(--blue-100);
  --c-logo-tip-bg:    var(--blue-950);
  --c-logo-tip-fg:    var(--white);
  --c-quote-from:     var(--blue-500);
  --c-quote-to:       var(--blue-950);
  --c-quote-text:     var(--white);
  --c-check:          var(--blue-500);
  --c-perm-lock:      var(--grey-400);
  --c-grade-weak:     var(--red-700);

  /* Cluster notes: a tint with its own text colour. */
  --c-note-green-bg:  var(--green-50);
  --c-note-green-fg:  var(--green-800);
  --c-note-amber-bg:  var(--amber-50);
  --c-note-amber-fg:  var(--amber-800);
  --c-note-red-bg:    var(--red-50);
  --c-note-red-fg:    var(--red-800);
  /* Verdicts: a solid with white text. */
  --c-verdict-full:   var(--green-800);
  --c-verdict-fed:    var(--green-700);
  --c-verdict-part:   var(--amber-700);
  --c-verdict-stay:   var(--red-700);
  --c-verdict-fg:     var(--white);
  --c-cluster-bg:     var(--blue-50);
  --c-rule-soft:      var(--grey-200);

  /* Partner tiers: one ladder, read as a cascade from the entry rung to
     the premium one. Each step keeps its own text colour so the light
     rungs stay as readable as the dark ones. */
  /* Five steps, brightest first, so the biggest share reads first. */
  --c-slice-1:        var(--blue-500);
  --c-slice-2:        var(--blue-950);
  --c-slice-3:        var(--blue-700);
  --c-slice-4:        var(--blue-300);
  --c-slice-5:        var(--blue-125);
  --c-stat-solid-bg:  var(--blue-500);
  --c-stat-solid-fg:  var(--white);
  --c-box-bg:         var(--blue-50);
  --c-table-head:     var(--blue-500);
  /* The drag bar sits on a sidebar of the same colour, so the hairline
     is what separates them. Same neutral family, one step darker. */
  --c-handle:         var(--grey-150);
  --c-handle-line:    var(--grey-250);
  --c-chip-bg:        var(--blue-75);
  --c-code-bg:        var(--grey-50);

  /* ---------- Colour: text ---------- */
  --c-brand:          var(--blue-500);
  --c-prose:          var(--grey-900);
  --c-level-1:        var(--blue-900);   /* sidebar section labels */
  --c-level-2:        var(--blue-500);   /* sidebar chapter labels */
  --c-level-3:        var(--blue-ink);   /* sidebar item labels    */
  --c-book-title:     var(--blue-950);
  --c-chapter-num:    var(--blue-500);
  --c-chapter-name:   var(--black);
  --c-chapter-title:  var(--black);
  --c-crumb-parent:   var(--blue-950);
  --c-crumb-current:  var(--blue-500);
  --c-eyebrow:        var(--grey-400);
  --c-label:          var(--grey-500);
  --c-deep:           var(--blue-950);

  /* ---------- Colour: lines and edges ---------- */
  --c-rule:           var(--grey-300);   /* footnote separator */
  --c-card-border:    var(--grey-225);   /* cards and images   */
  --c-border:         var(--grey-200);
  --c-box-border:     var(--blue-100);
  --c-scrollbar:      var(--blue-150);
  --c-progress-track: var(--blue-150);
  --c-progress-fill:  var(--blue-500);
  --c-zebra:          var(--grey-50);
  --c-meta:           var(--grey-400);

  /* ---------- Colour: controls ---------- */
  --c-icon:           var(--blue-700);
  --c-btn-hover:      var(--blue-100);
  --c-focus:          var(--blue-500);
  --c-bubble-bg:      var(--blue-950);   /* dark navy, not brand blue */
  /* A footnote tooltip is a link, so it keeps the brand blue that says
     so, while the icon tooltips stay navy. */
  /* The panel a footnote opens on hover. Light blue with the ink of the
     sidebar's own labels on it, and a dark shadow held at a low opacity:
     enough depth to lift the panel off the page, not enough to read as a
     drop shadow in its own right. */
  --c-fn-tip-bg:      var(--blue-50);
  --c-fn-tip-text:    var(--blue-ink);
  --c-fn-tip-border:  var(--blue-100);
  --shadow-fn-tip:    0 6px 18px rgb(0 17 42 / 0.14),
                      0 1px 3px rgb(0 17 42 / 0.10);
  --c-takeaway-bg:    var(--blue-950);
  --c-takeaway-text:  var(--white);
  --c-pull-quote:     var(--blue-500);
  --c-bubble-text:    var(--white);

  /* Expressed as channels so the gradient can vary alpha. Matched to
     the pane so the ramp dies with no seam; use 255 255 255 for a
     pure-white fade. */
  --c-fade-rgb: 252 252 252;

  /* ---------- Print-view viewer chrome ---------- */
  --c-viewer-bar-bg:    var(--grey-150);
  /* Same hairline the mobile drag bar uses - the pill sits on the same
     grey, so it takes the same edge. */
  --c-viewer-bar-line:  var(--grey-250);
  --c-viewer-btn-bg:    var(--blue-500);
  --c-viewer-btn-hover: var(--blue-700);
  --c-viewer-btn-text:  var(--white);
  --c-viewer-card-bg:   var(--white);
  --c-viewer-card-line: var(--blue-150);
  /* One surface behind the sheets, and the fade ramps to exactly it.
     base.css paints the body --c-page-bg (#FCFCFC) for the reader; the
     print viewer needs its own, darker surface so a white sheet reads
     as a sheet, and the two must not disagree or the backdrop shows a
     seam where the band ends. Keep these two in step. */
  /* Same surface as the reader's sidebar, so the two views read as one
     product. Keep the fade channels in step with it. */
  --c-viewer-surface:  var(--grey-150);
  --c-viewer-fade-rgb: 240 240 241;   /* = --grey-150 */

  /* ---------- Typefaces ---------- */
  --f-serif: var(--font-serif);
  --f-sans:  var(--font-sans);
  --f-mono:  var(--font-mono);
  --w-regular:  var(--weight-regular);
  --w-medium:   var(--weight-medium);
  --w-semibold: var(--weight-semibold);
  --t-min-size: var(--size-min);
  --t-marker-size: var(--size-marker);   /* footnote reference marker */
  --t-footnote-size: var(--size-footnote);
  --t-caption-size: var(--size-caption);  /* figure captions */
  --t-lead-size:    var(--size-lead);     /* blue component headers */
  --t-pull-size:     var(--size-lead);    /* mobile.css steps it down */

  /* ---------- Type: table cells ----------
     The one place the scale's four-point step does not reach: a cell's
     width is its column's width, and the tables were balanced at these
     sizes. --t-table-size is a cell that reads as prose, --t-table-sm-size
     the data table's own tighter cell, --t-table-cap-size the grade
     table's small header row. */
  --t-table-size:     var(--size-table);
  --t-table-sm-size:  var(--size-table-sm);
  --t-table-cap-size: var(--size-table-cap);

  /* ---------- Type: article ---------- */
  /* The chapter h1 sits on the title step, not the display step above
     it. It keeps the display line height: --lh-title is 1.0, which
     collides the chapter number with the title on a two-line heading. */
  --t-h1-size:   var(--size-title);   --t-h1-lh:   var(--lh-display); --t-h1-ls:   var(--track-display);
  --t-h2-size:   var(--size-heading); --t-h2-lh:   var(--lh-heading); --t-h2-ls:   var(--track-heading);
  --t-body-size: var(--size-body);    --t-body-lh: var(--lh-body);    --t-body-ls: var(--track-body);
  --t-card-size: var(--size-small);
  --t-figure-size: var(--size-title);          /* number highlights */
  --t-byline-size: var(--size-lead);   --t-byline-lh: var(--lh-lead);   /* author bylines */

  /* ---------- Type: sidebar ----------
     Built from the chrome sizes, not the article's: the contents list
     and the bar stay where they were when the reading type stepped up.
     Line heights and tracking are shared with the rest of the scale -
     only the sizes are held back. */
  --t-l1-size:   var(--size-chrome);    --t-l1-lh:   var(--lh-ui);    --t-l1-ls:   var(--track-ui);
  --t-l2-size:   var(--size-chrome-sm); --t-l2-lh:   var(--lh-ui);    --t-l2-ls:   var(--track-ui);
  --t-l3-size:   var(--size-chrome-sm); --t-l3-lh:   var(--lh-small); --t-l3-ls:   var(--track-small);
  --t-book-size: var(--size-chrome-lg); --t-book-lh: var(--lh-title); --t-book-ls: var(--track-title);
  --t-sub-size:  var(--size-chrome-sm); --t-sub-lh:  var(--lh-ui);    --t-sub-ls:  var(--track-small);
  --t-prog-size: var(--size-chrome-sm); --t-prog-lh: var(--lh-ui);    --t-prog-ls: var(--track-ui);

  /* ---------- Type: chrome ----------
     The breadcrumb, and every tooltip bubble - the one under an action
     button, the footnote's, the logo wall's. One component, one size. */
  --t-crumb-size:  var(--size-chrome-sm); --t-crumb-lh:  var(--lh-ui); --t-crumb-ls:  var(--track-heading);
  --t-bubble-size: var(--size-chrome-sm); --t-bubble-lh: var(--lh-ui); --t-bubble-ls: var(--track-heading);
  /* An action icon's own label is set smaller than the tooltips that
     carry a sentence - the footnote's and the logo wall's keep
     --t-bubble-size. */
  --t-tip-size:    var(--size-tip);

  /* ---------- Shape and depth ---------- */
  --radius:        var(--radius-md);
  --btn-radius:    var(--radius-md);
  --bubble-radius: var(--radius-sm);
  /* Temporarily flat. Restore with var(--shadow-raised). */
  --shadow-topbar: none;
  --shadow-bubble: var(--shadow-float);


  /* ---------- The contents rule ----------
     The line that passes under a row in the contents on hover. A section
     draws it in its own colour; a chapter draws it as the brand gradient,
     left to right, so the two levels read apart at a glance. */
  /* The lit colour: what a contents row goes when it is pointed at or is
     the row the reader is on. The rule tokens that used to sit beside it
     went with the underline. */
  --toc-lit:          var(--c-brand);

  /* ---------- Motion ---------- */
  --duration:        var(--speed-fast);
  --hover-duration:  var(--speed-hover);
  --dd-duration:     var(--speed-medium);
  --ease:            var(--ease-standard);
  --reveal-duration: var(--speed-slow);
  --reveal-stagger:  90ms;
  --reveal-ease:     var(--ease-out-soft);

  /* ============================================================
     LAYOUT
     The sidebar is fluid and the reading column is fixed, so the
     window gains or loses width on the left, never in the measure.
     ============================================================ */
  --sidebar-w:   clamp(360px, 27vw, 400px);
  --article-w:   744px;
  /* Prose fills the column. It used to stop at 616px inside a 744px
     column, which left running text ragged against the full-width
     cards and tables sitting above and below it. */
  --measure-w:   var(--article-w);
  --article-top: 122px;
  --inset-x:      24px;
  --inset-top:    29px;

  --topbar-h:        62px;
  --topbar-inset-x:  26px;
  --btn-size:        32px;
  --btn-gap:         12px;
  --crumb-gap:       10px;
  --bubble-pad:       6px;
  --bubble-offset:    8px;
  --scrollbar-w:      6px;
  --chevron-size:    24px;

  --logo-webflow-w: 118px;
  --logo-magier-w:  116px;

  /* ---------- Rhythm ---------- */
  --gap-block:          36px;  /* sidebar blocks                   */
  --gap-row:            20px;  /* rows within a sidebar group      */
  --gap-section-group:  32px;  /* between Introduction / Sections  */
  --gap-section:        46px;  /* between article sections         */
  --gap-heading:        29px;  /* heading to body                  */
  --gap-para:           20px;  /* paragraph rhythm                 */
  --gap-block-el:       22px;  /* extra air around tables, figures */
  --gap-verdict:        32px;  /* between the verdicts in a totals box */
  --gap-standfirst:     36px;  /* chapter title to standfirst      */
  --gap-chapter-line:    4px;  /* chapter number to chapter title  */
  --indent-item:        12px;  /* items under a section label      */
  --chapter-num-w:      17px;  /* reserved for "01".."13"          */
  --chapter-num-gap:    14px;
  --section-clearance:  72px;  /* read by app.js for scroll offset */

  /* ---------- Reading pane fades ---------- */
  /* How far a table's frame reaches past the text measure on each side.
     Enough that the columns breathe at body size, close enough that the
     table still reads as belonging to the column it interrupts. */
  --table-bleed: 140px;

  --fade-top:     56px;
  --fade-bottom: 104px;

  /* ============================================================
     MOBILE  (< 1024px)
     --mobile-header-h is read by app.js for the drawer's collapsed
     height. Change it here, not in the script.
     ============================================================ */
  /* 194px puts the drag bar's top edge just above the book title, which
     sits at 173px in the collapsed drawer. At 210px the title's first
     13px showed under the progress bar. */
  --mobile-header-h: 194px;
  /* The drawer's open height. The stylesheet is the only place it is
     decided; app.js measures the result rather than recomputing it. */
  --mobile-drawer-h: min(88vh, 760px);
  --mobile-topbar-h:  64px;
  --mobile-inset:     24px;
  --handle-h:         24px;
}


/* ============================================================
   RESET
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--c-page-bg);
  font-family: var(--f-sans);
  /* Caveat: never hyphenate, never break words mid-word. */
  hyphens: none;
  -webkit-hyphens: none;
  overflow-wrap: normal;
  word-break: normal;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
ul { list-style: none; margin: 0; padding: 0; }
h1, h2, h3, p { margin: 0; }
a { text-decoration: none; color: inherit; }

/* A word that arrives hyphenated is marked at build time rather than
   left to the browser, which would treat the hyphen as a legal break and
   produce something that reads exactly like hyphenation. */
/* A table cell used to be exempted, on the grounds that a column has no
   width to hold a long compound whole. It reads exactly like the
   hyphenation being avoided, though, wherever it happens - so cells keep
   their compounds whole too, and the columns are sized to take them. */
.nb { white-space: nowrap; }

/* Text that is there for a screen reader and not for the page. */
.u-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* ============================================================
   APP SHELL
   ============================================================ */
.app { display: flex; height: 100vh; }
.main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; position: relative; }

/* Softens both ends of the reading pane. Purely decorative, so it never
   intercepts the pointer. Multi-stop rather than a two-stop ramp — a
   linear fade leaves a visible band where the gradient terminates.

   Shorter and thinner than it was: at 88px and 160px, opaque for most of
   the way, it read as the page closing in on the column. The band now
   gives up its opacity in the first quarter, so a line entering or
   leaving the pane is legible for longer and the reading area feels the
   size it actually is. */
.main::before,
.main::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
  z-index: 2;
}
.main::before {
  top: var(--topbar-h);
  height: var(--fade-top);
  background: linear-gradient(
    to bottom,
    rgb(var(--c-fade-rgb) / 1)    0%,
    rgb(var(--c-fade-rgb) / 0.78) 18%,
    rgb(var(--c-fade-rgb) / 0.48) 38%,
    rgb(var(--c-fade-rgb) / 0.24) 58%,
    rgb(var(--c-fade-rgb) / 0.08) 78%,
    rgb(var(--c-fade-rgb) / 0)   100%
  );
}
.main::after {
  bottom: 0;
  height: var(--fade-bottom);
  background: linear-gradient(
    to top,
    rgb(var(--c-fade-rgb) / 1)    0%,
    rgb(var(--c-fade-rgb) / 0.78) 18%,
    rgb(var(--c-fade-rgb) / 0.48) 38%,
    rgb(var(--c-fade-rgb) / 0.24) 58%,
    rgb(var(--c-fade-rgb) / 0.08) 78%,
    rgb(var(--c-fade-rgb) / 0)   100%
  );
}
.content { flex: 1; overflow-y: auto; }

/* ============================================================
   TOP BAR
   ============================================================ */
.top-bar {
  flex: none;
  height: var(--topbar-h);
  background: var(--c-topbar-bg);
  padding: 0 var(--topbar-inset-x);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  box-shadow: var(--shadow-topbar);
  /* Above the content pane so the shadow - when there is one - falls on
     the page, and above the top fade so the hover tooltips, which hang
     below the bar inside the fade band, stay fully legible. The stacking
     is load-bearing for the tooltips on its own. */
  position: relative;
  z-index: 3;
}

/* --- Breadcrumb --- */
.breadcrumb {
  display: flex;
  align-items: baseline;
  gap: var(--crumb-gap);
  min-width: 0;
  font-family: var(--f-serif);
  font-size: var(--t-crumb-size);
  line-height: var(--t-crumb-lh);
  letter-spacing: var(--t-crumb-ls);
  white-space: nowrap;
}
.breadcrumb__parent {
  font-weight: var(--w-medium);
  color: var(--c-crumb-parent);
  overflow: hidden;
  text-overflow: ellipsis;
}
.breadcrumb__sep {
  font-weight: var(--w-medium);
  color: var(--c-crumb-parent);
  flex: none;
}
.breadcrumb__current {
  font-weight: var(--w-regular);
  color: var(--c-crumb-current);
  flex: none;
}
/* When only one crumb applies, the separator and trailing crumb collapse
   rather than leaving a dangling slash. */
.breadcrumb[data-depth="1"] .breadcrumb__sep,
.breadcrumb[data-depth="1"] .breadcrumb__current { display: none; }

/* --- Action buttons ---
   Neutral and hover only. There is deliberately no latched pressed
   state: :active releases the moment the pointer lifts, so a click
   returns the button to neutral immediately. */
.header-actions { display: flex; align-items: center; gap: var(--btn-gap); flex: none; }
.action-button {
  width: var(--btn-size);
  height: var(--btn-size);
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--btn-radius);
  background: transparent;
  color: var(--c-icon);
  cursor: pointer;
  transition: background-color var(--duration) var(--ease);
}
.action-button:hover { background: var(--c-btn-hover); }
/* Subtle lift on hover — the icon rises a hair as the bubble drops in. */
.action-button svg { transition: transform var(--duration) var(--ease); }
.action-button:hover svg { transform: translateY(-1px); }

/* --- Tooltip bubble ---
   Lives below the button, revealed on hover and keyboard focus, and
   retracted the moment either ends. aria-hidden because the accessible
   name already sits on the button itself. */
.action-button { position: relative; }
.action-button__bubble {
  position: absolute;
  top: calc(100% + var(--bubble-offset));
  left: 50%;
  padding: var(--bubble-pad);
  border-radius: var(--bubble-radius);
  background: var(--c-bubble-bg);
  color: var(--c-bubble-text);
  box-shadow: var(--shadow-bubble);
  font-family: var(--f-sans);
  font-size: var(--t-tip-size);
  line-height: var(--t-bubble-lh);
  letter-spacing: var(--t-bubble-ls);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -4px);
  transition: opacity 140ms var(--ease), transform 140ms var(--ease);
}
.action-button:hover .action-button__bubble,
.action-button:focus-visible .action-button__bubble,
.action-button.is-tipped .action-button__bubble {
  opacity: 1;
  transform: translate(-50%, 0);
}
/* Every label is centred on the icon it names, at every width - the same
   as the print viewer's bar. The last icon sits against the header's
   right edge, so the label is only inside the window because it is set
   at --size-tip; the bar keeps a little clearance for it below. */

/* On touch there is no hover to reveal the label, so a tap does it. */
@media (hover: none) {
  .action-button:hover .action-button__bubble { opacity: 0; }
  .action-button.is-tipped .action-button__bubble { opacity: 1; }
}
.action-button:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
}
.action-button svg { display: block; width: 18px; height: 18px; }
.action-button svg path { stroke: currentColor; }


/* ============================================================
   SIDEBAR SHELL
   ============================================================ */
.sidebar {
  /* Fluid width; the reading column on the right stays fixed. */
  flex: 0 0 auto;
  width: var(--sidebar-w);
  min-width: 0;
  height: 100vh;
  background: var(--c-sidebar-bg);
  padding: var(--inset-top) var(--inset-x) 64px;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  /* Scrollable, but the bar itself is never drawn. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.sidebar::-webkit-scrollbar { width: 0; height: 0; display: none; }

.sidebar__inner {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--gap-block);
}

/* ============================================================
   BRAND LOCKUP
   ============================================================ */
.brand-lockup { display: flex; align-items: center; gap: 16px; }
/* Widths drive the scale; height auto keeps each mark's ratio. Keyed to
   a class per mark, not nth-of-type: the order is a design decision and
   positional selectors quietly swap the two scales when it changes. */
.brand-lockup svg { display: block; height: auto; }
.brand-lockup__mark { display: flex; text-decoration: none; color: inherit; }
.brand-lockup__webflow svg { width: var(--logo-webflow-w); }
.brand-lockup__magier svg  { width: var(--logo-magier-w); }
.brand-lockup__x {
  font-family: 'Open Sans', var(--f-sans);
  font-size: 14px;
  line-height: 1.5;
  color: #000;
}

/* ============================================================
   BOOK TITLE
   ============================================================ */
.book-title-block { display: flex; flex-direction: column; gap: var(--gap-row); }
.book-title {
  font-family: var(--f-serif);
  font-weight: var(--w-semibold);
  font-size: var(--t-book-size);
  line-height: var(--t-book-lh);
  letter-spacing: var(--t-book-ls);
  color: var(--c-book-title);
}
.book-subtitle {
  font-family: var(--f-sans);
  font-weight: var(--w-regular);
  font-size: var(--t-sub-size);
  line-height: var(--t-sub-lh);
  letter-spacing: var(--t-sub-ls);
  color: var(--c-level-3);
  text-wrap: pretty;
}

/* ============================================================
   READING PROGRESS
   ============================================================ */
.progress { display: flex; flex-direction: column; gap: 16px; }
.progress__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 32px;
  font-family: var(--f-sans);
  font-weight: var(--w-medium);
  font-size: var(--t-prog-size);
  line-height: var(--t-prog-lh);
  letter-spacing: var(--t-prog-ls);
}
.progress__label { color: #000; }
.progress__value { color: var(--c-progress-fill); flex: none; }
.progress__track {
  height: 8px;
  border-radius: 100px;
  background: var(--c-progress-track);
  overflow: hidden;
}
.progress__fill {
  height: 100%;
  width: 0%;
  border-radius: 100px;
  background: var(--c-progress-fill);
  transition: width 120ms linear;
}

/* ============================================================
   TABLE OF CONTENTS — nested dropdowns
   ============================================================ */
/* The drag handle belongs to the mobile drawer only. */
.nav-drag-handle { display: none; }

/* Sections sit further apart than the rows inside them, so the four
   top-level groups read as distinct blocks. */
.toc { display: flex; flex-direction: column; gap: var(--gap-section-group); }

/* --- Toggle rows ---
   space-between is what makes the sidebar feel responsive: the label
   sits left, the chevron pins right, and the gap between them grows
   with the column. */
.dd__toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-row);
  min-height: var(--chevron-size);
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  color: inherit;
}
.dd__toggle:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Section rows and chapter rows must size identically: the label takes
   all free space, the chevron is a fixed box pinned to the right edge.
   Without the explicit flex basis the label collapses toward min-content
   and the chevron floats in beside it, which is what made the four
   section rows disagree with every chapter row. */
.dd__row {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-row);
  min-height: var(--chevron-size);
}
.dd__label--section {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
}
/* Chapter labels sit in the same row shape as section labels. */
.dd--chapter > .dd__row { min-height: var(--chevron-size); }
.dd__toggle--chevron {
  flex: 0 0 var(--chevron-size);
  width: var(--chevron-size);
  height: var(--chevron-size);
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: inherit;
}
.dd__toggle--chevron:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
  border-radius: 2px;
}
.dd__label--section:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
  border-radius: 2px;
}
/* The PDF view's front-matter row is a button, not a link - it jumps
   within the document rather than navigating. A button arrives with a UA
   background, border, centred text and its own font, none of which
   belong on a row that has to sit flush with the labels around it. */
button.dd__label--section,
button.dd__label--chapter {
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  /* No font or colour reset here: `button.dd__label--section` outranks
     the plain-class rule that sets them further down, so resetting
     either would win and the row would inherit the bar's sans instead
     of the serif every other row uses. */
}
a.dd__label--section,
a.dd__label--chapter,
button.dd__label--section,
button.dd__label--chapter { cursor: pointer; }
/* --- The colour arriving from the left ------------------------------
   Pointing at a row turns its words blue from the left, over 200ms, and
   that is the whole signal - there is no underline any more. The line
   that used to pass under a row was a second mark saying the same thing,
   and two marks for one state read as two things happening. The
   mechanism is the one in the ICS Media piece - a gradient twice the width of the box, the two colours meeting
   dead on the half, clipped to the glyphs, and the whole thing slid one
   box-width to the left. At rest the right half is showing, which is the
   row's own ink; lit, the left half is, which is the brand blue. Nothing
   fades and no second copy of the text is involved, so there is no
   cross-fade to go muddy in the middle - each letter changes colour as
   the edge passes through it.

   The wipe is on a span of its own inside the text box rather than on the
   text box itself, because a chapter row must light its name and not its
   numeral - a span around the name alone is what buys that. It also kept
   the wipe off the same background as the underline while there still was
   one: two layers of one background, and Safari has never reliably
   honoured a per-layer `background-clip`.

   `--toc-ink` is the rest colour, declared on the row so every tier
   brings its own: navy for a section, black for a chapter name, the
   sidebar's ink for an item. `--toc-lit` is the blue, and the rule reads
   from the same token, so the two halves of the gesture cannot disagree.

   Both fragments of a wrapped label wipe from their own left edge rather
   than the second lagging behind the first, which is what
   `box-decoration-break: clone` buys: each line carries a gradient the
   width of its own words.

   Behind @supports because the fallback for an unsupported clip is
   invisible text, and behind any-hover for the pointer states so a tap
   on a phone does not leave a row half lit. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .toc-wipe {
    display: inline;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
    background-image: linear-gradient(to right,
        var(--toc-lit) 50%, var(--toc-ink, currentColor) 50%);
    background-repeat: no-repeat;
    background-size: 200% 100%;
    background-position: 100% 0;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    /* The stroke is what carries the weight - see the note below. It is
       declared here at zero so it has something to interpolate from. */
    -webkit-text-stroke-color: transparent;
    -webkit-text-stroke-width: 0;
    transition: background-position var(--hover-duration) var(--ease),
                -webkit-text-stroke-width var(--hover-duration) var(--ease);
  }

  @media (any-hover: hover) {
    a.dd__label--section:hover .toc-wipe,
    button.dd__label--section:hover .toc-wipe,
    a.dd__label--chapter:hover .toc-wipe,
    button.dd__label--chapter:hover .toc-wipe,
    a.toc-link--item:hover .toc-wipe,
    button.toc-link--item:hover .toc-wipe { background-position: 0 0; }
  }

  a.dd__label--section:focus-visible .toc-wipe,
  a.dd__label--chapter:focus-visible .toc-wipe,
  a.toc-link--item:focus-visible .toc-wipe,
  button.dd__label--section:focus-visible .toc-wipe,
  button.dd__label--chapter:focus-visible .toc-wipe,
  button.toc-link--item:focus-visible .toc-wipe,
  .dd__label--section[aria-current="true"] .toc-wipe,
  .dd__label--chapter[aria-current="true"] .toc-wipe,
  .toc-link[aria-current="true"] .toc-wipe { background-position: 0 0; }

  /* A page printed from the reader gets no background images, so the
     glyphs would come out of the printer blank. */
  @media print {
    .toc-wipe {
      background-image: none;
      color: var(--toc-ink, currentColor);
      -webkit-text-fill-color: currentColor;
    }
  }
}

/* --- The weight the current row settles at --------------------------
   The row the reader is on is heavier as well as blue, and the weight
   arrives over the same 200ms rather than snapping at the end of it.

   It cannot be done with `font-weight`. The faces are static instances -
   400, 500 and 600, with nothing in between - so there is no axis to
   interpolate along, and a variable font's `wght` transition would have
   nothing to move through. Worse, the 600 cut of both families is 6 to 8%
   wider than the 500, which is enough to push a label onto another line:
   measured across the contents, 18 rows at 1440px and 28 at 1280 gain a
   line when set in SemiBold, and an item row goes current on scroll -
   so the column would shuffle under the reader as they read. That is the
   same wall the weight transition hit the first time, and why there was
   no weight change here at all.

   What does work is inking the glyph rather than changing the face:
   `-webkit-text-stroke-width` interpolates smoothly, and because the
   stroke is transparent and the letters are painted by the wipe's
   gradient clipped to them, thickening the stroke thickens the mask - so
   the glyph grows and the gradient fills the larger shape. The reader
   sees the weight come up as the colour comes across. It costs no width
   at all: a stroke is drawn on the outline and takes part in no
   measurement, so not one row re-wraps and the column never moves.
   Per tier, because the distance to cover is not the same: a section
   label starts at 500 in the serif, a chapter name at 500 in the sans,
   an item at 400.

   Hovering does not touch the weight - only the row you are on is
   heavier, or every row the cursor passed would twitch. */
.dd__label--section { --toc-bold: 0.35px; }
.dd__label--chapter { --toc-bold: 0.4px; }
.toc-link--item     { --toc-bold: 0.5px; }

.dd__label--section[aria-current="true"] .toc-wipe,
.dd__label--chapter[aria-current="true"] .toc-wipe,
.toc-link[aria-current="true"] .toc-wipe {
  -webkit-text-stroke-width: var(--toc-bold, 0.4px);
}

.dd__label--chapter:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
  border-radius: 2px;
}

.dd__label--section {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--t-l1-size);
  line-height: var(--t-l1-lh);
  letter-spacing: var(--t-l1-ls);
  color: var(--c-level-1);
  --toc-ink: var(--c-level-1);
}

.dd__label--chapter {
  display: flex;
  flex: 1 1 auto;
  align-items: baseline;
  gap: var(--chapter-num-gap);
  min-width: 0;
  font-family: var(--f-sans);
  font-weight: var(--w-medium);
  font-size: var(--t-l2-size);
  line-height: var(--t-l2-lh);
  letter-spacing: var(--t-l2-ls);
}
.chapter-num  {
  color: var(--c-chapter-num);
  flex: none;
  min-width: var(--chapter-num-w);
}
.chapter-name { color: var(--c-chapter-name); }
/* Only the name is lit and only the name carries the rule: the numeral
   is already the brand blue and stays exactly as it is. */
.dd__label--chapter { --toc-ink: var(--c-chapter-name); }
/* An appendix row names itself in the numeral's blue and carries its title
   in the prose ink on the same line: "Appendix A: What Webflow Enterprise
   Offers…". Inline, so a title long enough to wrap runs under the whole
   label rather than into a column of its own. */
.toc-num { color: var(--c-chapter-num); }

/* --- Chevron ---
   Points down when closed, flips up when the panel opens. */
.chevron {
  flex: 0 0 var(--chevron-size);
  width: var(--chevron-size);
  height: var(--chevron-size);
  color: var(--c-level-1);
  transition: transform var(--dd-duration) var(--ease);
}
.dd[data-open="true"] > .dd__toggle .chevron,
.dd[data-open="true"] > .dd__row .chevron { transform: rotate(180deg); }

/* --- Panel ---
   grid-template-rows 0fr -> 1fr animates to the content's natural
   height without hard-coding one, and the overflow:hidden child is the
   mask the rows are revealed through. */
.dd__panel {
  display: grid;
  grid-template-rows: 0fr;
  /* Clipping alone leaves the contents in the accessibility tree and in
     the tab order, so a collapsed chapter could still be reached with
     the keyboard. visibility removes them properly; the delay holds it
     visible for the length of the collapse animation. */
  visibility: hidden;
  transition: grid-template-rows var(--dd-duration) var(--ease),
              visibility 0s linear var(--dd-duration);
}
.dd[data-open="true"] > .dd__panel {
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows var(--dd-duration) var(--ease),
              visibility 0s linear 0s;
}
.dd__inner { overflow: hidden; min-height: 0; }

.dd__inner > * { padding-top: var(--gap-row); }
.dd__inner .toc-list,
.dd__inner .dd { transition: none; }
/* The panel is revealed purely by its mask. Rows carry no transform of
   their own, so no text slides or nudges while a panel animates — the
   only motion in the sidebar is the mask edge and the chevron. */

/* Item lists sit 12px in from their chapter row, which is what makes the
   section / chapter / item tiers read as distinct without relying on
   type size alone. Bullets are markers on the li, so the marker lands in
   that indent and wrapped lines hang flush with the first line. */
.toc-list {
  display: flex;
  flex-direction: column;
  gap: var(--gap-row);
  padding-left: var(--indent-item);
}
/* Items belonging to a chapter align with the chapter's title rather
   than its number, so the text below "The €140K Landing Page" starts on
   the same vertical as the title itself. The number box is a fixed
   width, so this stays exact for every chapter from 01 to 13. */
.dd--chapter .toc-list {
  padding-left: calc(var(--chapter-num-w) + var(--chapter-num-gap));
}
/* No markers — the tiers are separated by indentation alone. */
.toc-item { list-style: none; }
.dd--chapter { display: block; }
.dd__inner > .dd + .dd { padding-top: var(--gap-row); }

/* --- Item links --- */
.toc-link {
  cursor: pointer;
  text-wrap: pretty;
}
/* A block, not a grid. The grid existed to stack the old SemiBold ghost
   on top of the label; with the ghost gone its only remaining effect was
   to blockify the text box, and an inline box is what lets the passing
   rule follow each line of a label that wraps.

   Not for a chapter row, though. In the print picker a chapter row is a
   .toc-link and a .dd__label--chapter at once - which is what makes its
   hover and current states the reader's - and the two rules set `display`
   at the same weight, so whichever came last won. This one came last, and
   the numeral's gutter went with it: "01The €140K Landing Page". Written
   as an exception so the row keeps the flex the numeral column needs. */
.toc-link:not(.dd__label--chapter) { display: block; }
/* The row used to slide five pixels towards the reader on hover. It is
   the colour wipe now and nothing moves: a label that shifts under the
   cursor takes the eye off the word it is trying to read, and in a column
   of thirty rows the movement was the loudest thing in the sidebar. */

.toc-link--item {
  font-family: var(--f-sans);
  font-size: var(--t-l3-size);
  line-height: var(--t-l3-lh);
  letter-spacing: var(--t-l3-ls);
  color: var(--c-level-3);
  font-weight: var(--w-regular);
  --toc-ink: var(--c-level-3);
}
/* Hover does not change weight here either - the colour is the whole
   signal. What the current row does take is set further up, and it is
   inked rather than a change of face, because the faces are static
   instances and cannot be interpolated. */
.toc-link:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
  border-radius: 2px;
}
/* Sub-headers previously carried an extra offset. All items now share
   the same indentation, so the modifier is intentionally inert. */
.toc-item--nested { padding-left: 0; }


/* ============================================================
   ARTICLE
   ============================================================ */
.content {
  padding: var(--article-top) 0 160px;
  overscroll-behavior: contain;
}

.article {
  width: var(--article-w);
  max-width: calc(100% - 96px);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--gap-section);
}

.chapter {
  display: flex;
  flex-direction: column;
  gap: var(--gap-section);
  scroll-margin-top: var(--section-clearance);
  /* Explicit, so a short view cannot collapse narrower than the column. */
  width: 100%;
  align-self: stretch;
  min-width: 0;
}
.chapter > *,
.chapter-intro,
.section__body { width: 100%; min-width: 0; }
/* Chapters are all in the document; only the current one is rendered. */
.chapter[hidden] { display: none; }
/* A section opener is a short front page for the chapters beneath it. */
.chapter--section .chapter-heading__num { color: var(--c-chapter-num); }

.article section {
  display: flex;
  flex-direction: column;
  gap: var(--gap-heading);
  /* Clearance so a targeted heading never lands against the top bar. */
  scroll-margin-top: var(--section-clearance);
}

/* Prose runs the full column width, same as the tables and figures. */
.article p,
.article ul,
.article ol,
.footnotes__list { max-width: var(--measure-w); }
/* Except inside a box that has stepped outside the column: the box set
   its own width, and prose capped at the measure left a quarter of it
   empty on the right. What is in there is reference text read a line at a
   time - a verdict, a score band - not a page of running prose. */
.cluster p,
.cluster ul,
.cluster ol { max-width: none; }

/* --- Level 1 --- */
.chapter-intro {
  display: flex;
  flex-direction: column;
  gap: var(--gap-standfirst);
}
.chapter-heading {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--t-h1-size);
  line-height: var(--t-h1-lh);
  letter-spacing: var(--t-h1-ls);
  text-wrap: balance;
}
.chapter-heading__num   { display: block; color: var(--c-chapter-num); }
.chapter-heading__title { display: block; color: var(--c-chapter-title); }

.chapter-standfirst {
  font-family: var(--f-sans);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
  text-wrap: pretty;
}

/* --- Level 2 --- */
.section-heading {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--t-h2-size);
  line-height: var(--t-h2-lh);
  letter-spacing: var(--t-h2-ls);
  color: var(--c-prose);
  text-wrap: balance;
}

/* --- Level 3 --- */
.article p,
.article li,
.article td,
.article th,
.figure figcaption,
.footnotes__list {
  font-family: var(--f-sans);
  font-weight: var(--w-regular);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
}
/* A cell is set a step below the prose: its width is its column's width,
   so type that grows takes the measure with it and the balance the
   columns were set to comes apart. Written after the rule above rather
   than lifted out of it, so a cell still inherits everything else. */
.article td,
.article th { font-size: var(--t-table-size); }
.article p { text-wrap: pretty; }
.section__body { display: flex; flex-direction: column; gap: var(--gap-para); }
/* Block elements need more separation from running text than paragraphs
   need from each other. */
/* --- A table takes the room it needs ---------------------------------
   The measure is set for prose - 744px is about the number of words an
   eye tracks comfortably across a line - and a table is not read that
   way. Now that cells are set at body size, holding a seven-column
   matrix inside the paragraph column would squeeze every one of them.
   A table's frame steps outside the measure instead - by a set amount
   either side rather than filling the pane, which on a wide window left
   a three-column table stretched across a metre of screen.

   The step is capped by the pane less 24px, so a narrow window keeps its
   margin. --pane-w is written onto the article by app.js, because the
   pane is the scroll container and its width is not something CSS in
   here can reach; without it the rule falls back to the measure and
   nothing moves. The negative margins are percentages of the column, so
   the frame stays centred on it however wide it grows.

   A section divider is a short front page rather than a chapter, and its
   two-column table reads with the text: it keeps the measure. */
.chapter:not(.chapter--section) .section__body > .card:has(.dtable),
.chapter:not(.chapter--section) .section__body > .chart-box:has(.dtable),
.chapter--appendix .section__body > .card:has(.dtable),
.chapter--appendix .section__body > .chart-box:has(.dtable),
/* A cluster box holds a worksheet table, so the box is what steps out and
   the table fills it. Widening the table inside a fixed-width box would
   only have pushed it past the box's own edge. */
.chapter--appendix .section__body > .cluster:has(.dtable) {
  --wide-w: max(100%, min(calc(var(--measure-w) + var(--table-bleed) * 2),
                          calc(var(--pane-w, 100%) - 48px)));
  width: var(--wide-w);
  /* The frame carries the measure as its own ceiling, which is the thing
     being lifted here. */
  max-width: var(--wide-w);
  margin-inline: calc((100% - var(--wide-w)) / 2);
}

.section__body > .card,
.section__body > .figure,
.section__body > .highlights,
.section__body > .file-card,
.section__body > .numbered,
.section__body > .byline,
.section__body > .logo-set,
.section__body > .code-card { margin-block: var(--gap-block-el); }
/* Two of them in a row would stack both margins - flex margins do not
   collapse - and 22 above plus 22 below on top of the flow gap reads as a
   section break between two blocks that belong together. The second drops
   its own. One rule rather than every pair spelled out: `:is()` takes its
   specificity from a single class, so this still outranks the list above. */
.section__body
  > :is(.card, .figure, .highlights, .file-card, .numbered, .byline,
        .logo-set, .code-card)
  + :is(.card, .figure, .highlights, .file-card, .numbered, .byline,
        .logo-set, .code-card) { margin-block-start: 0; }
/* A second foreword opens inside the same section, so its heading has a
   paragraph above it rather than a section boundary. The flow gap alone
   reads as though the heading belonged to that paragraph; this gives it
   the same clearance a section heading gets, the flow gap included. */
.section__body > * + .section-heading {
  margin-block-start: calc(var(--gap-section) - var(--gap-para));
}
.section__body > :first-child { margin-block-start: 0; }
.section__body > :last-child { margin-block-end: 0; }
.article strong { font-weight: var(--w-semibold); }
/* Italics are not used in this design. */
.article em { font-style: normal; }


/* First-pass table styles removed — superseded by .card / .dtable. */

/* --- Image placeholders --- */
.figure { display: flex; flex-direction: column; gap: 12px; margin: 0; }
.figure__placeholder {
  border: 2px dotted var(--c-rule);
  border-radius: 4px;
  aspect-ratio: 768 / 546;
  display: grid;
  place-items: center;
  color: var(--c-rule);
  font-family: var(--f-sans);
  font-size: var(--size-small);
  letter-spacing: var(--t-body-ls);
}
.figure__image {
  display: block;
  /* Every figure fills the measure. An export narrower than the column
     is drawn up to it rather than sitting short of the right margin: an
     empty half-inch beside a picture reads as a mistake where a slightly
     soft edge does not. --fig-max still carries each asset's own pixel
     width for the print build to work from. The width stays definite, so
     a lazy image reserves its box before it loads. */
  width: 100%;
  height: auto;
}
.figure figcaption { max-width: var(--measure-w); }
/* Captions are apparatus, not prose: they sit below the 14px floor on
   purpose, the same exemption the footnote list has. */
.figure figcaption,
.figure--chart figcaption { font-size: var(--t-caption-size); }

/* --- Footnotes ---
   Footnote numbering, not ordinary list numbering: the marker is a
   superscript numeral matching the reference in the running text, and
   the text hangs clear of it. */
.footnotes { display: flex; flex-direction: column; gap: 16px; margin-top: 8px; }
.footnotes__rule {
  border: 0;
  border-top: 1px solid var(--c-rule);
  margin: 0;
  width: 100%;
}
.footnotes__list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-left: 0;
  list-style: none;
  counter-reset: footnote;
}
.footnotes__list li {
  counter-increment: footnote;
  /* Hanging indent sized to the marker, not to body type. */
  padding-left: 20px;
  position: relative;
}
.footnotes__list li::before {
  content: counter(footnote);
  position: absolute;
  left: 0;
  /* Matches the in-text marker: a reference glyph at --t-marker-size,
     raised like a superscript. nowrap keeps two-digit numbers whole
     inside the narrow hanging indent. */
  font-size: var(--t-marker-size);
  font-weight: var(--w-semibold);
  /* line-height 1 plus a fixed top offset, so the marker keeps its
     optical position against the first text line if the size changes. */
  line-height: 1;
  top: 4px;
  white-space: nowrap;
  color: var(--c-brand);
}
/* --- Pull quote ---
   A line lifted out of the argument. Not the boxed .highlight--quote:
   this one stays in the flow and just carries weight. */
.article .pull-quote {
  font-weight: var(--w-semibold);
  font-size: var(--t-pull-size);
  line-height: 1.4;
  color: var(--c-pull-quote);
}

/* Footnote text is apparatus, not running prose, so it sits below the
   14px floor. Two classes deep on purpose: mobile.css steps
   `.article li` to 14px, and without the extra weight that rule would
   win here. */
.article .footnotes__list,
.article .footnotes__list li { font-size: var(--t-footnote-size); }

.footnotes a { color: var(--c-brand); text-decoration: underline; }
.footnotes__backref { text-decoration: none; }

/* The marker is a reference glyph, not prose: it sits at --t-marker-size,
   below the 14px floor by design, and never wraps away from the word or
   the punctuation it annotates. */
.footnote-ref {
  font-size: var(--t-marker-size);
  font-weight: var(--w-semibold);
  vertical-align: super;
  line-height: 0;
  white-space: nowrap;
}
.footnote-ref a { color: var(--c-brand); text-decoration: none; }
/* An explanatory note cites no source, so its marker is not a link -
   same numeral, but it carries the note in the tooltip and reaches the
   keyboard through tabindex rather than through an href. */
.footnote-ref__note {
  color: var(--c-brand);
  cursor: help;
  outline-offset: 2px;
}

/* --- Footnote tooltip ---
   The reference links straight to its source now, so the tooltip says
   what that source is before the reader commits to opening it. It lives
   inside the anchor: hovering it keeps it open, and clicking it follows
   the same link as the numeral.

   Same reveal as the top bar's buttons, opening upward - a marker sits
   mid-paragraph, and a tooltip dropping down would cover the line being
   read. */
.footnote-ref { position: relative; }
.footnote-tip {
  position: absolute;
  bottom: calc(100% + var(--bubble-offset));
  left: 50%;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: max-content;
  max-width: 320px;
  padding: 8px 10px;
  border: 1px solid var(--c-fn-tip-border);
  border-radius: var(--bubble-radius);
  background: var(--c-fn-tip-bg);
  color: var(--c-fn-tip-text);
  box-shadow: var(--shadow-fn-tip);
  font-family: var(--f-sans);
  font-size: var(--t-bubble-size);
  line-height: var(--t-bubble-lh);
  letter-spacing: var(--t-bubble-ls);
  text-align: left;
  white-space: normal;
  /* The marker is superscript with no line box of its own; the tooltip
     must not inherit that. */
  vertical-align: baseline;
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 4px);
  transition: opacity 140ms var(--ease), transform 140ms var(--ease),
              visibility 0s linear 140ms;
}
.footnote-ref a:hover .footnote-tip,
.footnote-ref a:focus-visible .footnote-tip,
.footnote-ref__note:hover .footnote-tip,
.footnote-ref__note:focus-visible .footnote-tip {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
  transition: opacity 140ms var(--ease), transform 140ms var(--ease),
              visibility 0s linear 0s;
}
/* The tooltip floats 8px clear of the marker, so the cursor would cross
   dead space on its way up and drop the hover before arriving. This pad
   joins the two hit areas; the tooltip sits inside the <a>, so hovering
   it keeps the link hovered and the tooltip open long enough to click. */
.footnote-tip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: var(--bubble-offset);
}
.footnote-tip__title { font-weight: var(--w-medium); }
/* A note runs to a few sentences where a citation is one line, so it is
   given room to be read rather than squeezed into a citation's width. */
.footnote-tip--note { max-width: 360px; }
.footnote-tip--note .footnote-tip__title { font-weight: var(--w-regular); }
/* Underlined because the whole tooltip is part of the link. On the light
   panel the address takes the brand blue rather than a dimmed ink: at 0.9
   opacity on white it read as a link, on pale blue it reads as faded. */
.footnote-tip__url { color: var(--c-brand); text-decoration: underline; }


/* ============================================================
   EDITORIAL COMPONENTS
   ============================================================ */

/* No borders anywhere in this system — separation comes from fills and
   row tints. The only 1px border in the article is on the image. */

/* --- Card shell --- */
.card {
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  background: var(--c-topbar-bg);
  overflow: hidden;          /* clips the header band to the radius */
}
/* Except where the card holds a table. A footnote in a cell hangs its
   tooltip above the row, and a card that clips cuts the tooltip off at
   its own frame - the reference is then unreadable in exactly the place
   a reference is most needed. The corner cells take the radius instead,
   which is all the clipping was ever doing. */
.card:has(> .dtable),
.card:has(> .dtable--fixed) { overflow: visible; }
.card > .dtable thead th:first-child { border-top-left-radius: var(--radius); }
.card > .dtable thead th:last-child  { border-top-right-radius: var(--radius); }
.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  padding: 14px 16px;
}
.card__title {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-body);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-prose);
}
.card__eyebrow {
  font-family: var(--f-sans);
  font-size: var(--size-small);
  letter-spacing: var(--t-body-ls);
  color: var(--c-eyebrow);
  white-space: nowrap;
  flex: none;
}
.card__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  padding: 12px 16px;
  background: var(--c-foot-bg);
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  line-height: 1.5;
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
}
.card__foot strong { font-weight: var(--w-semibold); white-space: nowrap; }

/* --- Data table ---
   Everything is left-aligned. Tight padding so short cells don't sit in
   a field of empty space. */
.dtable { width: 100%; border-collapse: collapse; }
.dtable th,
.dtable td { text-align: left; }
.dtable thead th {
  background: var(--c-brand);
  padding: 9px 16px;
  font-family: var(--f-sans);
  font-weight: var(--w-medium);
  font-size: var(--t-table-sm-size);
  letter-spacing: var(--t-body-ls);
  color: var(--c-bubble-text);
}
.dtable tbody td {
  padding: 10px 16px;
  vertical-align: top;
  font-family: var(--f-sans);
  font-size: var(--t-table-sm-size);
  line-height: 1.5;
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
}
.dtable tbody tr:nth-child(even) td { background: var(--c-row-alt); }

/* Column sizing lives with the table, not with the cell class.
   A fixed width and `nowrap` on a helper class was fine while every
   labelled cell held two words; once a cell holds a sentence, the width
   squeezes it into a stack of three-word lines while the neighbouring
   column runs half empty. Tables that need a specific balance carry a
   colgroup and `dtable--fixed`; the rest are sized by their content. */
.dtable--fixed { table-layout: fixed; }
.dtable__key    { color: var(--c-brand); white-space: nowrap; }
.dtable__label  { font-weight: var(--w-semibold); }
.dtable__result { color: var(--c-brand); }
/* For the short figures that genuinely must not break. */
.dtable__nowrap { white-space: nowrap; }
.dtable__note {
  display: block;
  margin-top: 2px;
  font-size: var(--t-table-sm-size);
  color: var(--c-eyebrow);
}

/* --- Matrix card ---
   A comparison grid wider than the 744px column. The card scrolls
   sideways rather than dropping or stacking columns, because in a matrix
   the columns are the argument. The axis column wraps instead of forcing
   the table wider still. */
.card--matrix { overflow: hidden; }
/* Only the table scrolls; the head band and the tally foot stay put. */
.card__scroll { overflow-x: auto; }
/* Wide enough that the platform names sit on two lines rather than
   three and the team column holds "Marketing-operated" whole. */
.card--matrix .dtable { min-width: 920px; }
.card--matrix .dtable__label { white-space: normal; }

/* The axis column stays put while the platforms scroll past it, so a
   rating is never read without the row it belongs to. */
.card--matrix .dtable thead th:first-child,
.card--matrix .dtable tbody td.dtable__label { position: sticky; left: 0; z-index: 1; }
.card--matrix .dtable thead th:first-child { background: var(--c-brand); }
.card--matrix .dtable tbody td.dtable__label { background: var(--c-topbar-bg); }
.card--matrix .dtable tbody tr:nth-child(even) td.dtable__label { background: var(--c-row-alt); }

/* --- Numbered points --- */
.numbered { list-style: none; padding: 0; margin: 0; max-width: var(--measure-w); }
.numbered > li {
  display: grid;
  grid-template-columns: 30px 1fr;
  gap: 14px;
  padding: 14px 0;
}
.numbered > li:first-child { padding-top: 0; }
.numbered__badge {
  width: 30px;
  height: 24px;
  border-radius: 6px;          /* rounded rectangle, not a circle */
  background: var(--c-box-bg);
  color: var(--c-brand);
  display: grid;
  place-items: center;
  font-family: var(--f-sans);
  font-weight: var(--w-medium);
  font-size: var(--size-small);
  line-height: 1;
}
/* Block flow, deliberately not flex. A flex container blockifies every
   inline child, which put each bold lead-in and each footnote marker on a
   line of its own. Sibling blocks still get the paragraph rhythm. */
.numbered__body {
  font-family: var(--f-sans);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  letter-spacing: var(--t-body-ls);
}
.numbered__body > * + * { margin-top: var(--gap-para); }

/* --- Highlight boxes --- */
.highlights {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 12px;
}
.highlight {
  background: var(--c-box-bg);
  border-radius: var(--radius);
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.highlight__value {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--t-figure-size);
  line-height: 1.15;
  letter-spacing: -0.04em;
  color: var(--c-brand);
}
.highlight__label {
  font-family: var(--f-sans);
  font-size: var(--size-small);
  line-height: 1.45;
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
.highlight--quote { gap: 10px; grid-column: 1 / -1; }
.highlight--quote p {
  font-family: var(--f-serif);
  /* Medium, not regular: at 20px on a tinted panel the regular weight
     reads lighter than the prose around it. */
  font-weight: var(--w-medium);
  font-size: var(--size-lead);
  line-height: 1.4;
  letter-spacing: -0.03em;
  color: var(--c-deep);
  max-width: none;
}

/* --- Reader takeaway ---
   The chapter's conclusion, boxed so it reads as the thing to remember
   rather than one more section. Dark navy, so everything inside it has
   to invert. */
.takeaway {
  padding: 24px;
  border-radius: 8px;
  background: var(--c-takeaway-bg);
  color: var(--c-takeaway-text);
}
.takeaway .section-heading,
.takeaway p,
.takeaway li,
.takeaway strong { color: var(--c-takeaway-text); }
.takeaway a { color: var(--c-takeaway-text); text-decoration: underline; }
/* Its own padding stands in for the section gap. */
.article .takeaway { scroll-margin-top: var(--section-clearance); }

/* --- Company file --- */
.file-card {
  background: var(--c-box-bg);
  border-radius: var(--radius);
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.file-card__name {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-heading);
  line-height: 1.2;
  letter-spacing: -0.04em;
  color: var(--c-prose);
}
.file-card__meta {
  font-family: var(--f-sans);
  font-size: var(--size-small);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
.stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.stat-box {
  background: var(--c-topbar-bg);
  border-radius: var(--radius);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.stat-box__value {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--t-figure-size);
  line-height: 1.1;
  letter-spacing: -0.04em;
  color: var(--c-brand);
}
.stat-box__label {
  font-family: var(--f-sans);
  font-size: var(--size-small);
  line-height: 1.4;
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
.chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--c-chip-bg);
  color: var(--c-brand);
  font-family: var(--f-sans);
  font-size: var(--size-small);
  letter-spacing: var(--t-body-ls);
  vertical-align: middle;
}
.file-card__subhead {
  font-family: var(--f-sans);
  font-weight: var(--w-semibold);
  font-size: var(--t-card-size);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
  display: flex;
  align-items: center;
  gap: 10px;
}

/* The image is the one place a border is kept. */
.figure__image {
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  /* Fills the column; the frame is the column's width, not the asset's. */
  max-width: 100%;
  height: auto;
}
/* An export that runs edge to edge needs air between the artwork and the
   frame, or the border reads as part of the drawing. */
.article .figure__image--pad,
.figure__image--pad { padding: 20px; }

/* --- Previous / next chapter ---
   Plain type and an arrow, no container. */
.section-nav {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 32px;
  padding-top: 8px;
}
/* The label wraps rather than truncating, so a long chapter title is
   always readable in full. The narrower box is what buys the wrap. */
.section-nav__btn {
  display: flex;
  /* A button arrives with the UA's own 13.333px Arial. Both lines inside
     it set their own type, so nothing showed - but the box was being
     measured against a font the book does not use. */
  font: inherit;
  /* Centred against the label block. It used to be flex-start plus a
     hard margin on the arrow, which only lined up at one label size -
     changing the type broke it. This holds at any size or line count. */
  align-items: center;
  gap: 12px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-brand);
  text-align: left;
  cursor: pointer;
  flex: 0 1 auto;
  max-width: 42%;
  transition: opacity var(--duration) var(--ease);
}
.section-nav__btn:hover { opacity: 0.7; }
.section-nav__btn:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 4px; border-radius: 2px; }
.section-nav__btn[disabled] { opacity: 0.35; cursor: default; }
.section-nav__btn svg { flex: none; width: 20px; height: 20px; }
.section-nav__meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.section-nav__kicker {
  font-family: var(--f-sans);
  font-size: var(--size-small);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
.section-nav__label {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-body);
  line-height: 1.25;
  letter-spacing: -0.04em;
  color: var(--c-brand);
  /* Up to three lines, no ellipsis: nothing is cut off. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: normal;
  text-wrap: pretty;
}
.section-nav__btn--next { margin-left: auto; text-align: right; }
.section-nav__btn--next .section-nav__meta { align-items: flex-end; }

/* --- Code card ---
   For structured-data and configuration snippets. Same shell as .card, so
   a snippet reads as a document exhibit rather than as running text. The
   body scrolls sideways instead of wrapping, because a wrapped JSON line
   stops being readable as code. */
.code-card {
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  background: var(--c-topbar-bg);
  overflow: hidden;
}
.code-card__body {
  margin: 0;
  padding: 16px;
  background: var(--c-code-bg);
  overflow-x: auto;
}
.code-card__body code {
  display: block;
  font-family: var(--f-mono);
  font-size: var(--t-min-size);
  line-height: 1.55;
  letter-spacing: 0;
  color: var(--c-prose);
  white-space: pre;
}
/* Inline code in running prose — element names, paths, file names. */
.article code {
  font-family: var(--f-mono);
  font-size: var(--t-min-size);
  letter-spacing: 0;
  color: var(--c-deep);
}

/* --- Author byline ---
   A name, a role, and a portrait. The portrait frame is a dotted
   placeholder until a headshot exists; drop an img element inside it and the
   placeholder treatment removes itself. */
.byline { display: flex; align-items: center; gap: 16px; }
.byline__portrait {
  width: 88px;
  height: 88px;
  flex: none;
  border-radius: var(--radius);
  background: var(--c-box-bg);
  border: 2px dotted var(--c-rule);
  overflow: hidden;
}
.byline__portrait:has(img) { border: 0; background: none; }
.byline__portrait img { display: block; width: 100%; height: 100%; object-fit: cover; }
.byline__name {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--t-byline-size);
  line-height: var(--t-byline-lh);
  letter-spacing: var(--track-lead);
  color: var(--c-prose);
}
.byline__role {
  margin-top: 4px;
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  line-height: var(--lh-small);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}

/* No block ends on a word by itself where the browser can avoid it.
   `pretty` costs a little layout time and reflows the last few lines to
   balance them, which is exactly the trade we want in a book. */
.square__label,
.stat-box__label,
.ruled-list > li,
.calendar__body p,
.barviz__title,
.casebox p,
.figure figcaption,
.dtable td { text-wrap: pretty; }

/* --- Chart box ---
   The frame every figure drawn in the document sits in, so a chart, a
   diagram, or a boxed list all carry the same edge. */
.chart-box {
  padding: 24px;
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  max-width: var(--measure-w);
}
/* A table reaches the frame itself: padding around it would double the
   cell padding already inside it. The box used to clip, so that the
   header band took the corner radius - but a footnote in a cell hangs its
   tooltip above the row, and clipping cut the tooltip off at the frame.
   The radius is put on the corner cells instead and the box lets its
   contents out. */
.chart-box--flush {
  padding: 0;
  overflow: visible;
}
.chart-box--flush > .dtable thead th:first-child { border-top-left-radius: var(--radius); }
.chart-box--flush > .dtable thead th:last-child  { border-top-right-radius: var(--radius); }
.chart-box--flush > .perm-legend { margin: 0; padding: 16px 20px; }

/* --- Bar chart ---
   The figure this replaces was a flat export: two horizontal bars, the
   value sitting inside the fill, a right-aligned note above each track.
   Same shape, drawn in the document so the numbers can move.

   The two fills share one scale - 105,000 to the full track - so the
   comparison the figure is making is true to the numbers rather than
   decorative. Widths are set per bar with --pct in the markup. */
.barviz {
  display: flex;
  flex-direction: column;
  gap: 28px;
  width: 100%;
}
.barviz__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}
.barviz__title {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-body);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-prose);
}
.barviz__note {
  justify-self: end;
  font-family: var(--f-sans);
  font-size: var(--size-small);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
.barviz__track {
  height: 46px;
  border-radius: 23px;
  background: var(--c-bar-track);
  overflow: hidden;
}
.barviz__fill {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: 100%;
  min-width: 6.5em;                /* the value never outgrows its fill */
  padding-inline: 20px;
  border-radius: 23px;
  width: var(--pct);
}
/* Each bar carries its own figure colour: what was spent is stated in
   the brand blue, what was lost in red. */
.barviz__fill--cost { background: var(--c-bar-cost); }
.barviz__fill--lost { background: var(--c-bar-lost); }
.barviz__fill--cost .barviz__value { color: var(--c-bar-text-cost); }
.barviz__fill--lost .barviz__value { color: var(--c-bar-text-lost); }
.barviz__value {
  font-family: var(--f-sans);
  font-weight: var(--w-semibold);
  font-size: var(--t-card-size);
  letter-spacing: var(--t-body-ls);
  font-variant-numeric: tabular-nums;   /* no reflow while it counts */
  white-space: nowrap;
}

/* The animated state. Kept behind a class set by script so the bars are
   drawn at full width for anything that never runs it - the print
   sheets, a reader with JS off, a saved page. */
.barviz.is-armed .barviz__fill {
  width: 0;
  transition: width 900ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* The min-width that keeps the value inside its fill would leave a stub
   of bar sitting there before the chart runs. It only matters once the
   value is visible. */
.barviz.is-armed:not(.is-shown) .barviz__fill {
  min-width: 0;
  padding-inline: 0;      /* width:0 plus padding is still 40px of bar */
}
.barviz.is-armed.is-shown .barviz__fill { width: var(--pct); }
.barviz.is-armed .barviz__value { opacity: 0; }
.barviz.is-armed.is-shown .barviz__value {
  opacity: 1;
  transition: opacity 200ms var(--ease) 250ms;
}
@media (prefers-reduced-motion: reduce) {
  .barviz.is-armed .barviz__fill { width: var(--pct); transition: none; }
  .barviz.is-armed .barviz__value { opacity: 1; }
}

/* --- Square stat containers ---
   For a list that is really a set of headline numbers. Square so the
   figure carries the block and the words sit under it. */
.squares {
  display: grid;
  /* Five across on the full column, so the set reads as one row of
     figures rather than three and a gap. */
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  gap: 10px;
  max-width: var(--measure-w);
}
.square {
  /* Square by default. A box whose words will not fit in a square grows
     taller instead of clipping them - aspect-ratio yields to the content's
     own minimum height. */
  aspect-ratio: 1;
  /* Square is the floor, not the ceiling: the row is sized by whichever
     box needs the most height and the rest stretch up to match, so a
     long label never leaves one box taller than its neighbours. */
  min-height: 100%;
  display: flex;
  flex-direction: column;
  /* Figure and words sit on the floor of the square. Centred, a short
     label and a long one never line up with each other. */
  justify-content: flex-end;
  gap: 8px;
  padding: 16px;
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  background: var(--c-topbar-bg);
}
.square__value {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-stat);
  line-height: 1.05;
  letter-spacing: -0.04em;
  color: var(--c-brand);
}
/* A statement that is all words rather than a figure - same block, but
   set at a size the sentence can actually hold. */
.square__value--text {
  font-size: var(--size-lead);
  line-height: 1.25;
  letter-spacing: -0.03em;
}
.square__label {
  font-family: var(--f-sans);
  font-size: var(--size-small);
  line-height: 1.4;
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}

/* --- Case study box ---
   A whole case study lifted out of the run of the chapter: light blue
   ground, the company's mark at the top, its own sub-headings. */
.casebox {
  padding: 32px;
  border-radius: var(--radius);
  background: var(--c-case-bg);
}
.casebox > * + * { margin-top: var(--gap-para); }
/* Only the mark and the name live here, no loose text, so a flex column
   is safe: nothing inline can be turned into a track of its own. */
.casebox__logo {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 24px;
}
.casebox__wordmark {
  font-family: var(--f-serif);
  font-weight: var(--w-semibold);
  font-size: var(--size-heading);
  line-height: 1.2;
  letter-spacing: -0.03em;
  color: var(--c-book-title);
}
.casebox__logo img { display: block; width: 170px; max-width: 45%; height: auto; }
.casebox__heading {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-lead);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-prose);
}
.casebox > .casebox__heading { margin-top: var(--gap-section); }
/* Both sub-headings sit the same distance above what they label. The
   ruled list resets its own margin, so this has to outrank that. */
.casebox > .casebox__heading + * { margin-top: 14px; }
.casebox .stat-grid { grid-template-columns: repeat(2, 1fr); }

/* --- Ruled list ---
   Bullets that need a line between them rather than only white space. */
.ruled-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
/* The marker is positioned, not laid out: a flex row would make every
   inline child of the item its own flex track, so a marked-up word
   inside the sentence would pick up the row's gap and open a hole
   before the next comma. The offset matches the row's padding, which is
   14px everywhere except the first row. */
.ruled-list > li {
  position: relative;
  padding: 14px 0 14px 32px;
  border-top: 1px solid var(--c-rule);
  font-family: var(--f-sans);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
}
.ruled-list > li:first-child { border-top: 0; padding-top: 0; }
.ruled-list > li:last-child { padding-bottom: 0; }
.ruled-list > li::before {
  content: '';
  position: absolute;
  left: 4px;
  top: 14px;
  width: 5px;
  height: 5px;
  margin-top: 0.6em;
  border-radius: 50%;
  background: var(--c-brand);
}
.ruled-list > li:first-child::before { top: 0; }
/* On the case box's blue ground a grey hairline reads as dirt. */
.casebox .ruled-list > li { border-top-color: var(--c-case-rule); }

/* A stat box that is the emphasis rather than a quiet panel. */
.stat-box--solid {
  background: var(--c-stat-solid-bg);
  color: var(--c-stat-solid-fg);
  padding: 18px;
}
.stat-box--solid .stat-box__value,
.stat-box--solid .stat-box__label { color: var(--c-stat-solid-fg); }

/* --- Squares, stacked ---
   Five figures across leaves each label about twenty characters wide,
   which breaks short phrases into three lines. Stacked, the figure sits
   beside its words and each row has the whole column to read across. */
.squares--stacked {
  grid-template-columns: 1fr;
  gap: 16px;
}
.squares--stacked .square {
  aspect-ratio: auto;
  min-height: 0;
  flex-direction: row;
  align-items: baseline;
  gap: 16px;
  padding: 18px 20px;
}
.squares--stacked .square__value {
  /* The figure column has to hold the longest value on one line, so its
     width is set in the figure's own type and follows it up or down.
     5.2em clears 'Up to 94%', the longest of them, with a little room. */
  flex: 0 0 var(--stat-basis, 5.2em);
  font-size: var(--size-stat-sm);
}
/* A statement-shaped value keeps the figure size - one size across the
   set - and takes the whole row instead of a fixed figure column. */
.squares--stacked .square__value--text { flex: 1 1 auto; }
.squares--stacked .square__label { flex: 1 1 auto; }

/* --- Calendar ---
   The three days of the launch laid out as a week strip: a header band
   with the day and its number, the agenda in the cell below it, one
   bordered panel around the set. Static by design - there is nothing to
   choose here, so nothing responds to a pointer. */
.calendar {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--measure-w);
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  overflow: hidden;                /* clips the header band to the radius */
  background: var(--c-topbar-bg);
}
.calendar__day {
  display: flex;
  flex-direction: column;
}
.calendar__day + .calendar__day { border-left: 1px solid var(--c-card-border); }
.calendar__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  background: var(--c-cal-head-bg);
  border-bottom: 1px solid var(--c-card-border);
}
.calendar__num {
  flex: none;
  /* Sized in its own type so the digit never outgrows the circle. */
  width: 1.7em;
  height: 1.7em;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--c-cal-num-bg);
  color: var(--c-cal-num-fg);
  font-family: var(--f-sans);
  font-weight: var(--w-semibold);
  font-size: var(--size-ui);
  letter-spacing: var(--t-body-ls);
}
.calendar__name {
  font-family: var(--f-serif);
  font-weight: var(--w-semibold);
  font-size: var(--size-body);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-prose);
}
.calendar__body { padding: 16px; }
.calendar__body p { margin: 0; }

/* --- Ruled list, numbered ---
   Numbers rather than dots, set at the size of the text they label so
   the row reads as one line. */
.ruled-list--numbered {
  list-style: none;
  /* --list-offset lets a list continued on the next print sheet resume
     its numbering instead of starting again at one. */
  counter-reset: ruled var(--list-offset, 0);
}
/* The inset scales with the type: a two-digit marker at the sheet's
   larger size runs past a fixed 32px and collides with its own text. */
.ruled-list--numbered > li { counter-increment: ruled; padding-left: 2em; }
.ruled-list--numbered > li::before {
  content: counter(ruled) ".";
  left: 0;
  width: auto;
  height: auto;
  min-width: 1.2em;
  margin-top: 0;
  border-radius: 0;
  background: none;
  color: var(--c-brand);
  font-family: var(--f-sans);
  font-weight: var(--w-semibold);
  font-size: inherit;
  line-height: inherit;
}

/* --- Customer grids ---
   Three by three, one cell per company. The cells hold the name for now
   and are sized for the logo that will replace it. Hovering a cell
   names the site it opens, the same reveal the top-bar buttons use;
   clicking follows it. */
.logo-set__title {
  font-family: var(--f-serif);
  font-weight: var(--w-semibold);
  font-size: var(--size-body);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-prose);
  margin-bottom: 10px;
}
.logo-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--measure-w);
}
.logo-cell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 128px;
  padding: 24px 20px;
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  background: var(--c-topbar-bg);
  text-align: center;
  text-decoration: none;
  transition: background var(--duration) var(--ease),
              border-color var(--duration) var(--ease);
}
.logo-cell:hover { background: var(--c-btn-hover); border-color: var(--c-brand); }
.logo-cell:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
/* Every mark gets the same band of height and is contained inside it, so
   a tall wordmark and a wide one read as the same size on the wall and
   neither is ever scaled past its own artwork. */
/* --mark-h is set per logo in the markup, measured rather than guessed:
   each file's ink was rendered and its bounding box read, and the height
   is the one that gives every mark the same ink area. Holding them all
   to one height instead makes a square badge read half the size of a
   long wordmark, and a file with padding inside its own box read smaller
   again. */
.logo-cell__mark {
  display: block;
  width: 100%;
  /* --mark-scale trims the whole wall at once and leaves the measured
     relationship between the marks untouched. */
  height: calc(var(--mark-h, 34px) * var(--mark-scale, 0.75));
  object-fit: contain;
  /* Each mark gets a raster of its own to be composited from.

     An SVG referenced by <img> is re-rasterised whenever the region it
     sits in is repainted, and it is not cached between repaints: on the
     way past eighteen of them the scroll dropped a frame at nearly every
     logo. Measured over a 2.5s scroll through the wall, 1440x1000:
     15 frames over 20ms before, none after - the same clean 16.7ms as a
     page of plain prose. Promoting the grid alone only got it to 11; the
     raster has to belong to each image.

     `will-change` on eighteen elements is deliberate rather than a
     precaution: the wall is static, so the layers are made once and then
     only ever composited, and each is a few hundred pixels square. */
  will-change: transform;
}
.logo-cell__name {
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  line-height: 1.35;
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
}

/* --- Ruled list, no marker ---
   Where the separator is the whole point and a bullet would just be
   noise; the subject of each row is named in the brand colour instead. */
.ruled-list--plain > li { padding-left: 0; }
.ruled-list--plain > li::before { content: none; }
.ruled-list__name {
  font-weight: var(--w-semibold);
  color: var(--c-brand);
}

/* --- Boxed list ---
   One point per box, spaced apart, for a set that reads as six separate
   findings rather than a run of bullets. */
.boxed-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 0;
  padding: 0;
  max-width: var(--measure-w);
}
.boxed-list__name {
  font-weight: var(--w-semibold);
  color: var(--c-brand);
}
.boxed-list > li {
  padding: 14px 18px;
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  background: var(--c-topbar-bg);
  font-family: var(--f-sans);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
  text-wrap: pretty;
}

/* --- Checklist ---
   Reads as a list of things already cleared, so the marks are drawn, not
   offered: there is no input here and nothing to toggle. */
.checklist {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--measure-w);
}
.checklist > li {
  position: relative;
  padding: 0 0 0 34px;
  font-family: var(--f-sans);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
  text-wrap: pretty;
}
.checklist > li + li { margin-top: 12px; }
/* The box */
.checklist > li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.28em;
  width: 18px;
  height: 18px;
  border-radius: 4px;
  background: var(--c-check);
}
/* The tick, drawn with two borders on a rotated box */
.checklist > li::after {
  content: '';
  position: absolute;
  left: 5px;
  top: calc(0.28em + 5px);
  width: 8px;
  height: 4px;
  border-left: 2px solid var(--c-stat-solid-fg);
  border-bottom: 2px solid var(--c-stat-solid-fg);
  transform: rotate(-45deg);
}

/* --- Highlighted phrase ---
   A sentence the reader should not be able to skim past. */
.mark-blue {
  background: var(--c-mark-bg);
  color: inherit;
  font-weight: var(--w-semibold);
  padding: 0.08em 0.18em;
  border-radius: 3px;
  /* A highlight that breaks across lines should carry its ground with
     it rather than leaving the second line bare. */
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* --- Quote box ---
   A pull quote with its attribution, on a blue gradient running left to
   right so it reads as a panel rather than another card. */
.quote-box {
  margin: 0;
  padding: 28px 32px;
  border-radius: var(--radius);
  background: linear-gradient(90deg, var(--c-quote-from), var(--c-quote-to));
  color: var(--c-quote-text);
  max-width: var(--measure-w);
}
.quote-box p {
  margin: 0;
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-lead);
  line-height: 1.4;
  letter-spacing: -0.02em;
  color: var(--c-quote-text);
  text-wrap: pretty;
}
.quote-box__source {
  margin-top: 16px;
  font-family: var(--f-sans);
  font-size: var(--size-small);
  line-height: 1.5;
  letter-spacing: var(--t-body-ls);
  color: var(--c-quote-text);
  opacity: 0.92;
}
.quote-box__source strong { font-weight: var(--w-semibold); }
/* A brand-blue marker vanishes on the panel's own blue. */
.quote-box .footnote-ref a,
.quote-box .footnote-ref__note { color: var(--c-quote-text); }


/* --- Point ---
   A main point under a heading: the lead on its own line, semibold and
   in the brand colour, with the body underneath in ordinary prose, so it
   is findable when the reader is scanning a section rather than buried
   at the head of a paragraph. No numeral and no marker - the lead is the
   whole signal. */
.article .point__lead,
.point__lead {
  font-weight: var(--w-semibold);
  font-size: var(--size-lead);
  line-height: 1.35;
  letter-spacing: -0.02em;
  color: var(--c-brand);
  margin-bottom: 8px;
}
/* Only the bottom margin goes: `margin: 0` here also killed the top
   margin, and a point with three paragraphs in it - the appendix is full
   of them - had its last one sitting flush against the one above. The
   rule is more specific than `p + p`, so it won that fight silently. */
/* --- Run-in lead ---
   A label that stays on the line it introduces: the appendix's glossary
   terms, the platform teardown labels, the cluster verdicts. It takes the
   lead's colour and weight and nothing else - no size of its own, because
   it is part of the sentence it opens, and it keeps the full stop that
   separates it from that sentence. The blue lead on its own line is the
   other form (.point__lead); this one is for a reference entry, where a
   heading per term would be a page of headings. */
.article strong.inline-lead,
strong.inline-lead {
  font-weight: var(--w-semibold);
  /* The prose ink, not the brand blue: on the line it opens, a coloured
     label reads as a link, and a reference page of them reads as a page of
     links. The weight is the whole signal. Set explicitly rather than
     left to inherit, so the class names the treatment in one place. */
  color: var(--c-prose);
}

.point > p:last-child { margin-bottom: 0; }
/* Spacing between a point's own paragraphs: the section's flex gap does
   not reach inside it. */
.point > p + p { margin-top: var(--gap-para); }
.point > .point__lead + p { margin-top: 0; }
/* A list inside a point is a block like any other and takes the same
   rhythm; without this it sat flush against the paragraph introducing it. */
.point > p:not(.point__lead) + .ruled-list,
.point > p:not(.point__lead) + .boxed-list,
.point > .ruled-list + p,
.point > .boxed-list + p { margin-top: var(--gap-para); }
.point > .ruled-list:last-child,
.point > .boxed-list:last-child { margin-bottom: 0; }
/* The lead keeps its own tighter gap to whatever it introduces, list or
   paragraph: it is a heading for the block under it, not a sibling. */
.point > .point__lead + .ruled-list,
.point > .point__lead + .boxed-list { margin-top: 0; }

/* The same shape inside an ordered list. The list stays ordered so the
   sequence is still carried in the markup, but nothing numeric is
   drawn. */
.point-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--measure-w);
}
.point-list > li + li { margin-top: var(--gap-para); }

/* --- Pie chart ---
   Drawn as one circle per slice: radius 25 with a 50-wide stroke fills
   the whole disc, so a dash length is an arc and the slice can be swept
   on rather than faded in. Each slice starts where the last one ended,
   from twelve o'clock, and arrives in turn. */
.pie-chart {
  display: grid;
  grid-template-columns: 220px 1fr;
  align-items: center;
  gap: 32px;
}
.pie {
  width: 100%;
  max-width: 220px;
  aspect-ratio: 1;
  /* The stroke is drawn from the middle of the path outwards, so the
     disc is complete without a fill. */
  overflow: visible;
}
.pie__seg {
  fill: none;
  stroke-width: 50;
  transform-box: view-box;
  transform-origin: 50% 50%;
  transform: rotate(var(--rot));
  stroke-dasharray: var(--len) 157.08;
}
.pie__seg--1 { stroke: var(--c-slice-1); }
.pie__seg--2 { stroke: var(--c-slice-2); }
.pie__seg--3 { stroke: var(--c-slice-3); }
.pie__seg--4 { stroke: var(--c-slice-4); }
.pie__seg--5 { stroke: var(--c-slice-5); }

/* Armed by script, so a page with no script keeps the whole pie. */
.pie-chart.is-armed .pie__seg {
  stroke-dasharray: 0 157.08;
  transition: stroke-dasharray 620ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--i) * 260ms);
}
.pie-chart.is-armed.is-shown .pie__seg { stroke-dasharray: var(--len) 157.08; }
.pie-chart.is-armed .pie-legend__row {
  opacity: 0;
  transition: opacity 300ms var(--ease);
  transition-delay: calc(var(--i) * 260ms);
}
.pie-chart.is-armed.is-shown .pie-legend__row { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .pie-chart.is-armed .pie__seg {
    stroke-dasharray: var(--len) 157.08;
    transition: none;
  }
  .pie-chart.is-armed .pie-legend__row { opacity: 1; transition: none; }
}

/* --- Pie legend ---
   Number, then a rectangle carrying the slice colour. The rectangle is
   also where a logo will go, so it is sized for one rather than drawn as
   a bullet. */
.pie-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.pie-legend__row {
  display: grid;
  grid-template-columns: 18px 44px 1fr auto;
  align-items: center;
  gap: 12px;
}
.article .pie-legend__num,
.pie-legend__num {
  font-family: var(--f-sans);
  font-weight: var(--w-semibold);
  font-size: var(--size-small);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
.pie-legend__swatch {
  width: 44px;
  height: 24px;
  border-radius: 4px;
}
.pie-legend__swatch--1 { background: var(--c-slice-1); }
.pie-legend__swatch--2 { background: var(--c-slice-2); }
.pie-legend__swatch--3 { background: var(--c-slice-3); }
.pie-legend__swatch--4 { background: var(--c-slice-4); }
.pie-legend__swatch--5 { background: var(--c-slice-5); }
.article .pie-legend__name,
.pie-legend__name {
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  line-height: 1.4;
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
}
.article .pie-legend__value,
.pie-legend__value {
  font-family: var(--f-sans);
  font-weight: var(--w-semibold);
  font-size: var(--t-card-size);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- Checklist, blank ---
   A list of things to go and do, so the boxes are empty: an outline and
   no tick, and still nothing to click. */
.checklist--blank > li::before {
  background: none;
  border: 1.5px solid var(--c-check);
}
.checklist--blank > li::after { content: none; }
/* Inside the checklist the lead and its body are separate blocks, so the
   box lines up with the lead rather than floating between them. */
.checklist.point-list > li { padding-left: 34px; }
.checklist.point-list > li + li { margin-top: 18px; }

/* --- Permission matrix ---
   Was a flat export; now a real table, so the roles are readable at any
   size and a screen reader can say what each cell means. The state is
   carried by an icon with a text equivalent beside it. */
.dtable--perm thead th {
  vertical-align: top;
  line-height: 1.25;
}
.dtable--perm thead th:not(:first-child) { text-align: center; }
.perm-head__role { display: block; font-weight: var(--w-semibold); }
.perm-head__note {
  display: block;
  margin-top: 2px;
  font-size: var(--t-caption-size);
  font-weight: var(--w-regular);
  opacity: 0.85;
}
.dtable--perm tbody th {
  padding: 12px 16px;
  vertical-align: top;
  font-weight: var(--w-regular);
}
.article .perm-row__name,
.perm-row__name {
  display: block;
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  line-height: 1.4;
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
}
.perm-row__note {
  display: block;
  margin-top: 2px;
  font-family: var(--f-sans);
  font-size: var(--t-caption-size);
  line-height: 1.35;
  color: var(--c-label);
}
.dtable--perm tbody td.perm { text-align: center; vertical-align: middle; }
.perm__wrap { display: inline-flex; align-items: center; justify-content: center; }
.perm__icon { width: 22px; height: 22px; display: block; }
/* Allowed and toggleable are a filled disc with a white tick; the two
   differ only in the fill, which is what the legend explains. */
.perm--yes .perm__icon circle { fill: var(--c-brand); }
.perm--maybe .perm__icon circle { fill: var(--c-deep); }
.perm__mark {
  fill: none;
  stroke: var(--white);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Not allowed is a lock, drawn rather than filled. */
.perm--no .perm__icon { width: 20px; height: 20px; }
.perm--no .perm__stroke {
  fill: none;
  stroke: var(--c-perm-lock);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.perm-legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 20px;
  margin: 16px 0 0;
  padding: 0;
}
.article .perm-legend__item,
.perm-legend__item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--f-sans);
  font-size: var(--t-caption-size);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
/* The legend states the meaning in words, so the icon's own text
   equivalent would be read twice. */
.perm-legend .u-sr { display: none; }

/* --- Range chart ---
   A row per platform: the cost band drawn as a line with its mid-point
   marked, the band's figures repeated on the right. The two halves are
   separate spans so the band can open outward from the mid-point rather
   than sweeping in from one end. */
.range-chart { display: block; }
.range-chart__head,
.range-row {
  display: grid;
  grid-template-columns: 150px 1fr 132px;
  align-items: center;
  gap: 12px;
}
.range-chart__ticks {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  font-family: var(--f-sans);
  font-size: var(--t-caption-size);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
/* The ticks label the gridlines, so each sits at the start of its own
   share of the plot rather than centred in it. */
.range-chart__ticks > span { text-align: left; }
.range-chart__unit {
  font-family: var(--f-sans);
  font-size: var(--t-caption-size);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
  text-align: right;
}
.range-rows {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  border-top: 1px solid var(--c-card-border);
}
.range-row {
  padding: 14px 0;
  border-bottom: 1px solid var(--c-card-border);
}
.range-row.is-marked { background: var(--c-case-bg); }
.range-row__name {
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  line-height: 1.35;
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
}
.range-row__value {
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
  text-align: right;
  white-space: nowrap;
}
.range-row.is-marked .range-row__name,
.range-row.is-marked .range-row__value { color: var(--c-brand); }

/* The plot. Gridlines match the six ticks above it. */
.range-row__plot {
  position: relative;
  display: block;
  height: 22px;
  /* One gridline per tile: the gradient's period has to be the whole
     tile, not a fraction of it, or it repeats inside every tile. */
  background-image: repeating-linear-gradient(
    to right, var(--c-card-border) 0 1px, transparent 1px 100%);
  background-size: calc(100% / 6) 100%;
  background-repeat: repeat-x;
}
.range-row__left,
.range-row__right {
  position: absolute;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  background: var(--c-deep);
}
.range-row__left  { left: var(--lo); width: calc(var(--mid) - var(--lo)); transform-origin: right center; }
.range-row__right { left: var(--mid); width: calc(var(--hi) - var(--mid)); transform-origin: left center; }
.range-row__cap {
  position: absolute;
  top: 50%;
  width: 2px;
  height: 12px;
  margin-top: -6px;
  background: var(--c-deep);
}
.range-row__cap--lo { left: var(--lo); }
.range-row__cap--hi { left: calc(var(--hi) - 2px); }
.range-row__dot {
  position: absolute;
  top: 50%;
  left: var(--mid);
  width: 11px;
  height: 11px;
  margin: -5.5px 0 0 -5.5px;
  border-radius: 50%;
  border: 2px solid var(--c-deep);
  background: var(--c-topbar-bg);
}
.range-row.is-marked .range-row__left,
.range-row.is-marked .range-row__right,
.range-row.is-marked .range-row__cap { background: var(--c-brand); }
.range-row.is-marked .range-row__dot { border-color: var(--c-brand); }

/* Armed by script; without it every band is drawn in full. */
.range-chart.is-armed .range-row__left,
.range-chart.is-armed .range-row__right {
  transform: scaleX(0);
  transition: transform 560ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--i) * 200ms);
}
.range-chart.is-armed .range-row__cap,
.range-chart.is-armed .range-row__dot {
  opacity: 0;
  transition: opacity 240ms var(--ease);
  transition-delay: calc(var(--i) * 200ms + 220ms);
}
.range-chart.is-armed.is-shown .range-row__left,
.range-chart.is-armed.is-shown .range-row__right { transform: scaleX(1); }
.range-chart.is-armed.is-shown .range-row__cap,
.range-chart.is-armed.is-shown .range-row__dot { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .range-chart.is-armed .range-row__left,
  .range-chart.is-armed .range-row__right { transform: scaleX(1); transition: none; }
  .range-chart.is-armed .range-row__cap,
  .range-chart.is-armed .range-row__dot { opacity: 1; transition: none; }
}

.range-legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  margin: 16px 0 0;
  padding: 0;
}
.article .range-legend__item,
.range-legend__item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--f-sans);
  font-size: var(--t-caption-size);
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}
.range-legend__line { width: 22px; height: 2px; background: var(--c-deep); }
.range-legend__line--brand { background: var(--c-brand); }
.range-legend__dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 2px solid var(--c-deep);
}
.article .range-chart__source,
.range-chart__source {
  margin: 16px 0 0;
  font-family: var(--f-sans);
  font-size: var(--t-caption-size);
  line-height: 1.45;
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
}

/* --- Chart heading ---
   A chart that carries its own title reads as a standalone figure, the
   way it does in the design source. */
.article .chart-box__title,
.chart-box__title {
  font-family: var(--f-serif);
  font-weight: var(--w-semibold);
  font-size: var(--size-lead);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-prose);
  margin: 0 0 4px;
}
.article .chart-box__note,
.chart-box__note {
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  line-height: 1.45;
  letter-spacing: var(--t-body-ls);
  color: var(--c-label);
  margin: 0 0 20px;
}

/* The figure number carries the weight; the description stays prose. */
.article .figure figcaption .figcaption__label,
.figure figcaption .figcaption__label { font-weight: var(--w-semibold); }

/* --- Verdict box ---
   A point the source itself calls a box: same lead-and-body shape, with
   a frame so each verdict reads as its own answer. */
.point--boxed {
  padding: 18px 20px;
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  background: var(--c-topbar-bg);
  max-width: var(--measure-w);
}
.point--boxed + .point--boxed { margin-top: 10px; }

/* --- Grade matrix ---
   Single-letter ratings, centred, with the letter itself carrying the
   reading so the row scans without a legend lookup. */
/* Seven platform columns in one measure: the cell's own padding is what
   squeezes a name like "SitecoreAI", so the matrix runs tighter than the
   book's other tables and sets its heads at caption size. */
/* Written against `thead th` and `tbody td` rather than the bare
   elements: `.dtable thead th` carries the same weight and one more
   element, so a shorter selector here loses and the cells kept the
   book's standard 16px - which left a platform name wider than its own
   column and 'Contentful' crowding the table's right edge. */
.dtable--grade thead th,
.dtable--grade tbody td { padding-inline: 4px; }
/* The axis column is the one that holds words rather than a letter, and
   it sits against the frame: it keeps the book's standard inset on that
   side. Taken out of the column's own width, not the ratings' - a fixed
   layout sizes the column from the colgroup and padding eats into the
   text, so the seven rating columns are untouched. */
.dtable--grade thead th:first-child,
.dtable--grade tbody td:first-child { padding-left: 16px; }
.dtable--grade thead th { font-size: var(--t-table-cap-size); }
.dtable--grade tbody td:not(.dtable__label) { text-align: center; }
.dtable--grade thead th:not(:first-child) { text-align: center; }
.article .grade,
.grade { font-weight: var(--w-semibold); }
.grade--w { color: var(--c-brand); }
.grade--t { color: var(--c-prose); }
.grade--l { color: var(--c-grade-weak); }
/* An address column breaks where it must. A 60-character URL has no
   space to break at, and the alternative to breaking mid-token is a cell
   that runs past its own table - the same trade the footnote apparatus
   already makes. Scoped to the column that holds addresses, so ordinary
   prose cells keep their words whole. */
.dtable td.cell--url,
.dtable td.cell--url .cell__value { overflow-wrap: anywhere; }
/* An address is a link, and in the source list it is the only thing in
   its cell - so it is marked as one: the brand blue, semibold, and
   underlined. The book's links are otherwise unmarked in running prose,
   where the words carry the sense; a column of fifteen addresses is a
   list of places to go, and every one of them should look like it. The
   underline is offset clear of the descenders and thinned, because these
   break mid-token and a heavy rule under a wrapped URL reads as two. */
.dtable td.cell--url a {
  color: var(--c-brand);
  font-weight: var(--w-semibold);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.dtable__total td { background: var(--c-foot-bg); }
.article .grade-tally,
.grade-tally {
  font-weight: var(--w-semibold);
  text-align: right;
  white-space: nowrap;
}

/* --- Cluster ---
   A block of diagnostic questions on its own tinted ground, with the
   three outcome notes underneath it. The heading names the cluster; no
   marker is added, because the cluster's own number is in its title. */
.cluster {
  padding: 28px;
  border-radius: var(--radius);
  background: var(--c-cluster-bg);
  max-width: var(--measure-w);
}
.cluster + .cluster { margin-top: var(--gap-para); }
/* Inside a box the line breaks greedily rather than prettily. `pretty`
   spends the last few lines of a paragraph evening out the rag, which in a
   box this wide left several lines ending well short of the right edge;
   greedy filling puts every word it can on each line. Widows are still
   handled - the last two words are joined at build time - and nothing
   hyphenates, because a compound inside the limit is held whole. */
.cluster p,
.cluster li { text-wrap: wrap; }
/* The appendix's clusters are the worksheet form of the same six: a lead,
   the question table, the score line, the verdict. The section's flex gap
   stops at the box, so the box spaces its own children. Written against
   what is inside rather than against the appendix's class, so the prose
   clusters in Chapter 10 - a title, a question list, three notes, each
   carrying its own bottom margin - are left exactly as they were. */
.cluster > .point + .card,
.cluster > .card + p,
.cluster > p + p { margin-top: var(--gap-para); }
/* The totals box reads as four verdicts to choose between, not as four
   paragraphs of prose, so they sit further apart than the rhythm inside a
   cluster's own commentary. The score table above them takes the same
   step, which is what stops the first verdict reading as its caption. */
.cluster--totals > .card + p,
.cluster--totals > p + p,
.cluster--totals > .point + .card { margin-top: var(--gap-verdict); }
.cluster > :last-child { margin-bottom: 0; }
.article .cluster__title,
.cluster__title {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-lead);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-brand);
  margin: 0 0 16px;
}
.cluster__questions {
  /* Ordered for the outline, unnumbered on the page: the questions are
     a set, not a sequence the reader has to count. */
  list-style: none;
  margin: 0 0 24px;
  padding: 0;
}
.article .cluster__questions > li,
.cluster__questions > li {
  font-family: var(--f-sans);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  letter-spacing: var(--t-body-ls);
  color: var(--c-prose);
  text-wrap: pretty;
}
.cluster__questions > li + li {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--c-case-rule);
}

/* The three outcomes, each on its own tint. */
.article .note,
.note {
  margin: 0;
  padding: 12px 16px;
  border-radius: var(--radius-sm);
  font-family: var(--f-sans);
  font-size: var(--t-card-size);
  line-height: 1.5;
  letter-spacing: var(--t-body-ls);
  text-wrap: pretty;
}
.note + .note { margin-top: 8px; }
.note strong { font-weight: var(--w-semibold); }
/* The modifier carries the colour, so it needs to outrank `.article p`
   the same way the base rule does. */
.article .note--green,
.note--green { background: var(--c-note-green-bg); color: var(--c-note-green-fg); }
.article .note--amber,
.note--amber { background: var(--c-note-amber-bg); color: var(--c-note-amber-fg); }
.article .note--red,
.note--red   { background: var(--c-note-red-bg);   color: var(--c-note-red-fg); }

/* --- Verdict ---
   Four outcomes, each on its own solid so the shape of the answer is
   readable before the words are. */
.verdicts {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: var(--measure-w);
}
.verdict {
  padding: 18px 20px;
  border-radius: var(--radius);
  color: var(--c-verdict-fg);
}
.verdict--full { background: var(--c-verdict-full); }
.verdict--fed  { background: var(--c-verdict-fed); }
.verdict--part { background: var(--c-verdict-part); }
.verdict--stay { background: var(--c-verdict-stay); }
.article .verdict__lead,
.verdict__lead {
  font-family: var(--f-serif);
  font-weight: var(--w-semibold);
  font-size: var(--size-lead);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-verdict-fg);
  margin: 0 0 6px;
}
.article .verdict p,
.verdict p { color: var(--c-verdict-fg); margin: 0; }

/* --- Stakeholder panel ---
   One panel per role: what they care about, then the objections they
   raise, ruled apart. White ground and a grey edge, so a run of nine
   panels reads as a list of people rather than a wall of tint. */
.panel {
  padding: 28px;
  border: 1px solid var(--c-card-border);
  border-radius: var(--radius);
  background: var(--c-topbar-bg);
  max-width: var(--measure-w);
}
.panel + .panel { margin-top: var(--gap-para); }
.article .panel__title,
.panel__title {
  font-family: var(--f-serif);
  font-weight: var(--w-medium);
  font-size: var(--size-lead);
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--c-brand);
  margin: 0 0 12px;
}
.article .panel__cares,
.panel__cares { margin: 0 0 8px; }
.article .panel__label,
.panel__label {
  font-weight: var(--w-semibold);
  color: var(--c-prose);
  margin: 20px 0 0;
}
.panel__items {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
}
.panel__items > li + li {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--c-rule-soft);
}
.article .panel__q,
.panel__q {
  font-style: italic;
  color: var(--c-prose);
  margin: 0 0 8px;
}
.panel__items > li > p:last-child { margin: 0; }
/* A panel that opens with a line of instruction before its list needs
   the gap the list's own first row deliberately does not carry. */
.panel > p + .ruled-list { margin-top: 18px; }


/* --- Partner badge ---
   The four rungs of the partner ladder, drawn as the badges Webflow
   issues rather than as chips of our own. They are the same artwork at
   four widths - the Enterprise pair carry a second lockup - so they are
   set by height and left to take whatever width their own ratio asks
   for. Capped at the column: the widest is nearly six times as wide as
   it is tall, and on a narrow column the height gives way rather than
   the badge running into the next cell. The height is twice the body
   size at every scale - 40 against the reader's 20, 30 against the
   phone's 16, 48 against a sheet's 24 - so the ladder reads at one
   weight wherever it appears. */
.article .tier-badge,
.tier-badge {
  display: block;
  width: auto;
  height: var(--badge-h, 40px);
  max-width: 100%;
  object-fit: contain;
  object-position: left center;
}


/* ============================================================
   MOBILE  (< 1024px)
   ============================================================ */
@media (max-width: 1023px) {
  /* The reference numeral sits in 16px prose here rather than 20px. */
  :root { --t-marker-size: var(--size-marker-sm); }


  /* --- Drawer ---
     Fixed to the top and overlaying the article, so opening it never
     pushes the reading column down. The contents are already in the
     DOM; dragging only changes the height it is clipped to. */
  .app { display: block; }

  .sidebar {
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 30;
    width: auto;
    height: var(--mobile-header-h);
    max-height: var(--mobile-drawer-h);
    padding: 30px var(--mobile-inset) var(--handle-h);
    overflow: hidden;                 /* the inner column does the scrolling */
    display: flex;
    flex-direction: column;
    touch-action: none;
  }
  .sidebar.is-open { height: var(--mobile-drawer-h); }
  .sidebar.is-dragging { transition: none; }
  /* With the drawer up it covers most of the viewport, so scrolling
     the sheet behind it just loses the reader's place. The class goes
     on <html> from app.js, which owns the drawer's state. */
  .nav-locked .content { overflow: hidden; touch-action: none; }
  .sidebar:not(.is-dragging) { transition: height var(--dd-duration) var(--ease); }

  /* Collapsed, the drawer is a 210px bar showing the lockup and the
     progress. Its contents are far taller than that, so leaving it
     scrollable let the bar be scrolled internally - the lockup sliding
     away to reveal half a table of contents inside a closed bar. It
     only scrolls once the drawer is actually open, and then only if the
     contents overflow, which is what `auto` already decides. */
  .sidebar__inner {
    gap: 20px;
    flex: 1;
    min-height: 0;
    overflow-y: hidden;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }
  .sidebar.is-open .sidebar__inner { overflow-y: auto; }
  .sidebar__inner::-webkit-scrollbar { display: none; }

  /* Collapsed, the header shows the lockup and the progress block, so
     the book title moves below the fold rather than into that space. */
  .brand-lockup     { order: 1; }
  .progress         { order: 2; }
  .book-title-block { order: 3; }
  .toc              { order: 4; }

  .book-title { font-size: 28px; }
  .book-title-block { gap: 12px; }
  /* padding-bottom keeps Section III clear of the drag bar. */
  .toc { gap: 26px; padding-bottom: 24px; }
  .toc-list { gap: 14px; }
  .dd__inner > * { padding-top: 14px; }
  .brand-lockup { gap: 14px; }
  /* Just enough to clear the collapsed cut line — the header height is
     what hides the title, not this margin. */
  .book-title-block { margin-top: 26px; }
  .brand-lockup svg:nth-of-type(1) { width: 112px; }
  .brand-lockup svg:nth-of-type(2) { width: 110px; }

  /* --- Drag handle --- */
  .nav-drag-handle {
    display: grid;
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: var(--handle-h);
    place-items: center;
    /* Line spacing now comes from the SVG itself. */
    padding: 0;
    border: 0;
    /* box-sizing is border-box globally, so this does not grow --handle-h. */
    border-top: 1px solid var(--c-handle-line);
    background: var(--c-handle);
    cursor: grab;
    touch-action: none;
  }
  .sidebar.is-dragging .nav-drag-handle { cursor: grabbing; }
  .nav-drag-handle__grip {
    display: block;
    width: 29px;
    height: 7px;
    color: var(--c-level-1);   /* drives both strokes */
  }

  /* --- Page shell --- */
  .main {
    margin-top: var(--mobile-header-h);
    height: calc(100vh - var(--mobile-header-h));
  }
  .top-bar {
    height: var(--mobile-topbar-h);
    padding: 0 var(--mobile-inset);
  }
  .action-button { width: 38px; height: 38px; border-radius: 10px; }
  .action-button svg { width: 21px; height: 21px; }
  .header-actions { gap: 10px; }
  /* Only the current section shows on mobile — the chapter crumb has no
     room next to three actions. */
  .breadcrumb__parent,
  .breadcrumb__sep { display: none; }
  /* The bar is the shrinking element and the actions are fixed, so a
     long section title truncates instead of sliding under the icons. */
  .breadcrumb { flex: 1 1 auto; min-width: 0; }
  .header-actions { flex: 0 0 auto; }
  .breadcrumb__current {
    font-size: 14px;
    flex: 1 1 auto;
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .main::before { top: var(--mobile-topbar-h); height: 64px; }
  .main::after  { height: 129px; }

  /* --- Article --- */
  .content { padding: 64px 0 120px; }
  .article {
    width: auto;
    max-width: none;
    margin: 0;
    padding: 0 var(--mobile-inset);
    gap: 39px;
  }
  .article p, .article ul, .article ol, .footnotes__list,
  .numbered, .figure figcaption { max-width: none; }

  /* A hidden tooltip is still laid out, and a 320px bubble centred on a
     marker in a 354px measure pushes the document wider than the screen.
     On a phone it is taken out of the flow until it opens - there is no
     hover to fade in from anyway - and clamped to the viewport. */
  .footnote-tip { display: none; max-width: min(320px, calc(100vw - 2 * var(--mobile-inset))); }
  .footnote-ref a:hover .footnote-tip,
  .footnote-ref a:focus-visible .footnote-tip,
  .footnote-ref__note:hover .footnote-tip,
  .footnote-ref__note:focus-visible .footnote-tip { display: flex; }

  /* The sheet view does not come along below this width: a sheet is an
     A2 page, and there is no size it can be drawn at here that is worth
     reading. Copy Link is left as the only action in the bar. */
  .action-button--pdf { display: none; }

  .chapter-intro     { gap: 24px; }
  .chapter-standfirst{ line-height: 1.4; }
  /* Code comes down with the prose, and wraps rather than scrolling: a
     JSON-LD block is six hundred pixels wide at any size that can be
     read, and a sideways swipe inside a vertical read loses the reader's
     place. pre-wrap keeps the indentation. */
  .code-card__body { overflow-x: visible; }
  .code-card__body code { white-space: pre-wrap; overflow-wrap: anywhere; }
  .article section   { gap: 24px; scroll-margin-top: 24px; }

  /* --- Tables become stacked rows ---
     A three-column table cannot hold its shape at 354px, and forcing it
     to would mean a horizontal scroll inside a vertical read. So each row
     becomes a block of its own: the row's name as its heading, then one
     line per cell.

     The header band cannot come along - it is a row, and the rows are no
     longer beside each other - so the column names travel on the cells
     instead. `label_cells()` in typeset.py copies each header onto the
     body cells beneath it at build time, and the label is set in the
     header's blue in front of its value. Without it a stacked rating
     table is a column of bare letters: the column was the only thing
     saying which platform each one belonged to. */
  .card__head { flex-direction: column; align-items: flex-start; gap: 4px; }
  .dtable thead { display: none; }
  .dtable, .dtable tbody, .dtable tr, .dtable td { display: block; width: auto; }
  .dtable tbody tr { padding: 14px 16px; }
  .dtable tbody tr:nth-child(even) { background: var(--c-row-alt); }

  /* Every cell is a row of its own: the column's name in a block on the
     left, its value beside it. The label block takes the header band's
     colours, because that is what it is - the band turned on its side, one
     cell at a time.

     A two-column grid, which needs the value wrapped in an element of its
     own: a grid makes a separate item of every child, so a cell holding an
     `.nb` span (a compound that must not break) would have thrown that
     span into the next grid cell. `label_cells()` in typeset.py supplies
     the wrapper. The value's track is `minmax(0, 1fr)` - a plain `1fr`
     refuses to go under its own min-content, and one long compound would
     push the row past the edge of the block.

     Both parts are centred against each other, so a value on one line
     sits in the middle of a label that took two, and both read on the
     cell's own line height - no part of the pair sets its own leading. */
  .dtable tbody td[data-label] {
    display: grid;
    /* In em, so the column tracks the cell's own type - and set narrower
       now the cells read at the body size, or the label would take back
       every pixel the larger type just gained on the value. */
    grid-template-columns: 7em minmax(0, 1fr);
    align-items: stretch;
  }
  .dtable tbody td[data-label]::before {
    content: attr(data-label);
    display: flex;
    align-items: center;
    padding: 7px 10px;
    /* The rounding belongs to the coloured block, so the cell itself
       needs no `overflow: hidden` to clip a corner - and nothing that
       overruns the value column is ever cut off. */
    border-radius: var(--radius-sm);
    background: var(--c-brand);
    color: var(--c-bubble-text);
    font-weight: var(--w-medium);
    line-height: var(--t-body-lh);
    text-wrap: pretty;
  }
  /* Centred with `align-self`, not by being a flex box of its own: the
     value is a sentence, and a flex container would make a separate item
     of the `.nb` span inside it and set the two side by side. A plain
     block keeps the sentence a sentence. */
  .dtable tbody td[data-label] > .cell__value {
    align-self: center;
    padding: 7px 12px;
    line-height: var(--t-body-lh);
    text-wrap: pretty;
  }
  /* Weight and colour are the desktop's. The row's own name is regular
     there, so it is regular here: the label block is what marks it. */
  /* A cell that spans the row is a total: it belongs to no column, so it
     carries the row's name from the first cell instead of a label. */
  .dtable tbody td[colspan] { font-weight: var(--w-semibold); }
  /* A rating is centred in its column in the real table. Stacked there
     are no columns to centre in, so the pair is read from the left. */
  .dtable tbody td[data-label],
  .dtable--grade tbody td:not(.dtable__label),
  .dtable--grade thead th:not(:first-child) { text-align: left; }
  /* One gap between the rows of a block, and none inside a row. */
  .dtable tbody td + td { margin-top: 8px; }
  /* A badge is set shorter on a phone: the widest is six times as wide as
     it is tall, and the column is a third of 390px. */
  .article .tier-badge { --badge-h: 30px; }

  /* Two squares to a row on a phone; five would be thumbnails. Their
     figures come down with them - at desktop size a two-word value
     breaks across three lines in a 165px box. */
  .squares { grid-template-columns: repeat(2, 1fr); }
  /* The stacked set keeps its single column here - two of them would put
     a four-word label in a 60px measure - and the figure sits above its
     words rather than beside them. */
  .squares--stacked { grid-template-columns: 1fr; }
  .squares--stacked .square {
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
  }
  .squares--stacked .square__value { flex: none; }

  /* Three day columns in 390px would be four words wide, so the strip
     becomes a stack and the dividers turn horizontal. */
  .calendar { grid-template-columns: 1fr; }
  .calendar__day + .calendar__day {
    border-left: 0;
    border-top: 1px solid var(--c-card-border);
  }
  .casebox { padding: 20px; }
  .casebox .stat-grid { grid-template-columns: 1fr; }
  .dtable tbody tr:nth-child(even) td { background: none; }
  .dtable tbody td { padding: 0; }
  /* The matrix's axis inset is a desktop thing: it exists because that
     first column sits against the table's frame. Stacked there is no
     frame and no column, so the inset only set one label block 16px in
     from the seven under it. It needs its own selector because
     `.dtable--grade tbody td:first-child` outranks the reset above. */
  .dtable--grade tbody td:first-child,
  .dtable--grade thead th:first-child { padding-left: 0; }
  /* The fixed widths these carry on the desktop are the table's, and
     there is no table here. Their colours are the desktop's own: the
     blue that used to be forced onto every one of them was standing in
     for the missing header, which the labels now supply. */
  .dtable__key,
  .dtable__label,
  .dtable__num,
  .dtable__result { width: auto; }

  /* A matrix used to be exempted from the stack and left to scroll
     sideways, because a column of bare letters says nothing about what is
     being rated. The labels say it, so the matrix stacks like everything
     else and nothing on a phone has to be swiped.  */

  .card__foot { flex-direction: column; align-items: flex-start; gap: 4px; }

  /* --- Blocks --- */
  .highlights { grid-template-columns: 1fr; }
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
  .file-card { padding: 16px; }
  /* A cluster gives its padding back to what it holds. 28px each side is
     right beside a question list; around the appendix's worksheet table it
     left the stacked label column nothing to sit in, and the cells ran
     past the box by up to 16px at 360. */
  .cluster { padding: 16px; }
  /* Inside a cluster the column names are short - #, Question, Your
     answer, Score - and the values are whole questions. The 7em label
     track was sized for names like "Webflow Enterprise (3-year)"; here it
     took nearly half a 246px cell and the question could not fit beside
     it at 360px. */
  .cluster .dtable tbody td[data-label] {
    grid-template-columns: 5.5em minmax(0, 1fr);
    /* The label sits at the top of its cell rather than being stretched
       down it. Centred against a five-line question, a one-word name like
       "Question" drew a blue slab the height of the whole cell. */
    align-items: start;
  }
  .cluster .dtable tbody td[data-label] > .cell__value { align-self: start; }
  /* A worksheet cell is empty because it is there to be written in. The
     pair collapses to the name alone, so "Your answer" reads as the field
     it is instead of a blue block beside a blank column. */
  .cluster .dtable tbody td:has(> .cell__value:empty) {
    grid-template-columns: auto;
    /* and the name hugs its own text rather than stretching across the
       row: a full-width blue bar reads as a heading, not as a field. */
    justify-items: start;
  }
  .numbered > li { grid-template-columns: 30px 1fr; gap: 12px; }
  .byline { gap: 12px; }
  .byline__portrait { width: 72px; height: 72px; }

  /* Kept left-and-right rather than stacked; a long chapter name wraps
     to two lines instead of forcing the pair into a column. */
  .section-nav { flex-direction: row; align-items: flex-start; gap: 16px; }
  .section-nav__btn { max-width: 45%; }
  /* Size only - the centring comes from the desktop rule and needs no
     per-breakpoint offset. */
  .section-nav__btn svg { width: 18px; height: 18px; }
  .section-nav__label { -webkit-line-clamp: 3; }
}

@media (prefers-reduced-motion: reduce) {
  .toc-link, .action-button, .action-button svg,
  .action-button__bubble { transition: none; }
  .reveal-line > span { transform: none !important; transition: none !important; }
}
