/* ============================================================
   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;
  /* The x between the two marks in the phone header's pill, and the same
     0.553 of the lockup's own 14px that sizes the marks beside it. A
     separator glyph rather than text, which is why it is under the floor.
     The pill's caption was 12px here and went with the caption. */
  --size-pill-x:   8px;
  /* 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;
  /* The language switch's labels. Its own primitive rather than
     --size-chrome-sm, which the print view raises to 22 for the sheet's
     scale: the switch is window furniture at a fixed size, the way the
     tooltips are, and it reads at 14 in both outputs. */
  --size-lang:      14px;

  /* --- 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);
  /* A hairline for the phone's own chrome, which sits on the sidebar grey
     rather than on white: same neutral family, one step darker. It drew
     the drag bar's edge when there was one, and now separates the menu's
     header from the contents under it.  */
  --c-hairline:       var(--grey-250);
  /* The pill the phone header's lockup sits in. White on the header's
     grey, with the same hairline for its edge. It carried a caption once
     and the ink for it went when the caption did. */
  --c-pill-bg:        var(--white);
  --c-pill-border:    var(--grey-250);
  /* The dim behind the phone's navigation panel. The same ink as the
     shadows, at the weight that reads as "the page is still there" while
     the panel slides across it. */
  --c-scrim:          rgb(0 17 42 / 0.45);
  --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);
  /* The edition you are reading, in the language switch: the same navy a
     title is set in. The other one takes --c-icon, like the icons it
     stands beside. */
  --c-lang-current:   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 phone's own chrome 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);
  /* The reader takeaway sits a little tighter than the running prose:
     it is a boxed conclusion read in one go rather than a page turned
     through, and at the body's 1.6 the box was taller than what it
     holds. Consumed in layout.css, which is the reader's own
     stylesheet - the sheets set their own leading and are untouched. */
  --t-takeaway-lh: 1.45;
  --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);
  /* The language switch's two labels. Window furniture at a fixed size,
     like the tooltips: it does not follow the sheet's type scale up,
     which is why the primitive is its own and not --size-chrome-sm. */
  --t-lang-size:   var(--size-lang);
  /* The x between the two marks in the phone header's pill. A separator
     inside a lockup rather than a line of prose, which is why it is
     allowed under the type floor. */
  --t-pill-x-size: var(--size-pill-x);

  /* ---------- 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);
  /* The navigation panel's slide, and the dim that comes with it. Out of
     the soft curve rather than the standard one: it leaves the edge fast
     and settles, which is what makes a full-width panel read as arriving
     rather than being dragged. */
  --nav-duration:    var(--speed-medium);
  --nav-ease:        var(--ease-out-soft);
  --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 */
  /* What a table's frame is spaced by, boxed or in running text: the
     paragraph rhythm plus the step above. Written as the sum rather than
     as 42 so the two halves stay the single source of it. */
  --gap-table-air:      calc(var(--gap-para) + var(--gap-block-el));
  --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)
     ============================================================ */
  /* The header's own height was a number here - 188px, with the drag
     strip and the drawer's open height beside it - and all three are gone
     with the drag. The header is a block in a column now: it is as tall as
     34px of air, the title, the pill and 34px more, and the reading pane
     takes what is left. Nothing to keep in step, and app.js no longer
     reads a height at all. */
  --mobile-topbar-h:  64px;
  --mobile-inset:     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; }
/* Every bubble is centred on the icon it names, so the widest label in the
   bar decides how close the group may sit to the window. English clears the
   edge by 2px with "Show as PDF" (80.1px). German's "Als PDF anzeigen" is
   100.4px and lands 8.2px past the window at 1440 and at 1024, so the group
   takes 10px of clearance rather than the label giving up its centre.
   Both editions take it, not only German: scoped to one, the icons sat
   10px further left in the German bar, and clicking DE moved every control
   in the row - the switch the reader had just used most of all. One
   clearance, sized for the wider of the two, and the two bars are the same
   bar. It costs English 10px of a 24px inset and buys 12px of bubble
   clearance where it had 2. */
.header-actions { padding-right: 10px; }
.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; }
/* The file download is a phone control only. On a desktop the sheet view
   is one click away and carries its own download, so a second one in the
   reader's bar would be the same offer twice. */
.action-button--download { display: none; }

/* ============================================================
   PHONE NAVIGATION — the desktop side of it
   ============================================================
   The panel exists in the markup at every width, because the two groups
   inside it are the desktop sidebar's own progress and contents. Here it
   is nothing: `display: contents` takes the wrapper out of the box tree
   and leaves .book-meta and .toc as flex items of .sidebar__inner,
   exactly as they were before the wrapper existed. Its own furniture -
   the close button, the dim, the pill under the title and the menu
   button in the bar - is drawn on a phone alone, in mobile.css.

   Two notes for anyone editing this. The panel's phone rules are all
   inside mobile.css's media query on purpose: the closed panel is
   `visibility: hidden`, which children inherit, so a rule that leaked out
   of the query would blank the desktop sidebar. And build_pdf lifts only
   the .action-button blocks out of this file, so none of this reaches a
   sheet. */
.nav-panel,
.nav-panel__inner,
/* The title's row is a phone arrangement - title on the left, menu button
   on the right. Here the row is nothing and the title is a flex item of
   .book-title-block, which is what it was before the row existed. */
.book-title-row { display: contents; }
.nav-panel__bar,
.nav-scrim,
.powered-pill,
.action-button--menu { display: none; }

/* ============================================================
   READER TAKEAWAY — leading
   ============================================================
   A step tighter than the running prose, on the desktop only.

   It lives here rather than beside the other .takeaway rules in
   components.css because components.css is in the print bundle too, and a
   sheet sets its own leading. layout.css is the reader's alone: build_pdf
   lifts only the .action-button blocks out of it, and it strips at-rules
   on the way, so nothing in this block can reach a sheet.

   `.article .takeaway` is three classes deep, which is what it takes to
   beat `.article p` (two) from a file that loads before it.

   Phones keep the prose leading: the measure there is 40 characters
   narrower and the box has no height to spare. */
@media (min-width: 1024px) {
  .article .takeaway p,
  .article .takeaway li { line-height: var(--t-takeaway-lh); }
}


/* ============================================================
   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); }
/* The reading progress, on its own. The subtitle is back inside the title
   block where it used to live, a --gap-row under the title, so this group
   no longer has to be pulled up by --gap-row less --gap-block to keep the
   pair reading as a pair: it sits a full block under the subtitle, which is
   the same 36px that used to separate the two inside this group. Every gap
   in the desktop column is where it was - measured at 1440px, to the
   hundredth of a pixel. */
.book-meta {
  display: flex;
  flex-direction: column;
  gap: var(--gap-block);
}
.book-title {
  font-family: var(--f-serif);
  /* Medium, not SemiBold, on your instruction. The reader only - the
     printed cover sets its own type and never reads this rule. */
  font-weight: var(--w-medium);
  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
   ============================================================ */
/* 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. */

/* ============================================================
   LANGUAGE SWITCH
   ============================================================
   EN and DE, the rule between them, first in the row of actions - in the
   reader's top bar and on the print view's own bar, the same control in
   both. Each label carries .action-button, so it takes the icons' ground
   and their one-pixel lift on hover and needs no second description of
   either; what it does not take is the bubble, because a label is its own
   name.

   It lives in this file rather than in layout.css because both bundles
   read this one, and build_pdf lifts only the .action-button blocks out
   of layout.css - a switch written there would reach the sheets as bare
   text. */
.lang-switch {
  display: flex;
  align-items: center;
  gap: 2px;
}
.lang-switch__item {
  /* A label needs sides where an icon needed a square, and the square is
     the floor rather than the width. */
  width: auto;
  min-width: var(--btn-size);
  padding: 0 6px;
  font-family: var(--f-sans);
  font-size: var(--t-lang-size);
  line-height: var(--lh-ui);
  letter-spacing: var(--track-ui);
  font-weight: var(--w-medium);
  text-decoration: none;
}
/* The lift is on the label rather than the button, which is how the icons
   do it - the ground stays put and the mark rises inside it. */
.lang-switch__label { transition: transform var(--duration) var(--ease); }
.lang-switch__item:hover .lang-switch__label { transform: translateY(-1px); }
/* The edition you are already reading: the navy a title is set in, and
   nothing to press. */
.lang-switch__item[aria-current] {
  color: var(--c-lang-current);
  cursor: default;
}
.lang-switch__item[aria-current]:hover { background: transparent; }
.lang-switch__item[aria-current]:hover .lang-switch__label { transform: none; }
.lang-switch__rule {
  width: 1px;
  height: 14px;
  background: var(--c-hairline);
}


/* --- 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;
  /* minmax(0, ...) rather than a bare 1fr: a track's automatic floor is its
     min-content width, and a long German noun in one day pushes that track
     past its third and the other two out of the box. Three equal columns is
     the design; the floor is what breaks it. English measures the same
     either way. */
  grid-template-columns: repeat(3, minmax(0, 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; }

/* A German noun can be longer than the box it sits in: "Komponenten-
   bibliothek" (21) in a stacked cell at 390, "Barrierefreiheitspruefung"
   (24) in a calendar column at 1024, "Unterscheidungsmerkmalen" (24) in a
   quote at 360 - 270px against a 248px measure. A word wider than its box
   is not merely tight; the card clips it at its rounded edge and the quote
   runs past its rule. break-word fires only on a word that cannot fit a
   line by itself, so nothing that already fits moves - and it is scoped to
   the German edition regardless, so the English measure is untouched.
   Compounds still break at their hyphen first: this is the last resort for
   a word that has none. */
html[lang="de"] .dtable td,
html[lang="de"] .dtable td .cell__value,
html[lang="de"] .dtable th,
html[lang="de"] .calendar__body p,
html[lang="de"] .quote-box p,
html[lang="de"] .article p,
html[lang="de"] .article li,
html[lang="de"] .figure figcaption { overflow-wrap: break-word; }

/* A hyphenated compound is held whole by `.nb`, and no amount of
   overflow-wrap can break a span that is nowrap. English cells are sized
   to take their compounds - "Marketing-operated" is 139px - but the German
   equivalents are half as wide again: "NIS2-Lieferkettenrisiken" is 179px
   against the 142px a stacked cell leaves at 360, and it is clipped rather
   than tight. Inside a German table cell, therefore, only the tight
   compounds (16 characters or fewer, the limit `typeset.py` already sizes
   for a cell) stay whole; a longer one may break at its own hyphen, which
   is the same trade the 26-character limit makes in prose. Prose, quotes
   and captions are untouched: they have the measure to hold them. */
html[lang="de"] .dtable td .nb:not(.nb--tight),
html[lang="de"] .dtable th .nb:not(.nb--tight) { white-space: normal; }
/* 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 > p + p { margin-top: var(--gap-para); }
/* The table takes the same air inside a box as it does in running text -
   the paragraph rhythm plus the --gap-block-el step every table and figure
   in the book is given. It used to take the paragraph gap alone, which put
   the worksheet tables 20px off their own commentary while Appendix C's
   tables sat 42 off theirs, and the totals box used a third value again.
   One number for a table's air now, wherever the table is. */
.cluster > .point + .card,
.cluster > .card + p { margin-top: var(--gap-table-air); }
/* 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. Only the verdicts: the score table above them
   takes a table's own air from the rule above, which is more than this and
   keeps the first verdict from reading as its caption. */
.cluster--totals > p + p { 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); }


  /* --- Header ---
     No longer a drawer. It was fixed to the top with a height in a token
     and a drag strip at its foot; the contents came up from underneath as
     the reader dragged it open. Navigation is a panel that slides in from
     the right now - see .nav-panel below - so this is a plain block with
     the title and the pill in it.

     The page is a column: the header is as tall as its own content and the
     reading pane takes the rest. That is why the height token is gone
     rather than re-tuned - there is no number to keep in step with the
     type, and no fixed positioning to offset the pane against. */
  .app {
    display: flex;
    flex-direction: column;
    height: 100vh;
  }

  .sidebar {
    flex: none;
    /* The panel and its dim are fixed children, so this stacking context
       is what puts them over the reading pane and its bar. */
    position: relative;
    z-index: 30;
    width: auto;
    height: auto;
    max-height: none;
    /* The two spaces the header shows: 34px above the title's ascenders
       and 34 under the subtitle's last baseline. Neither is the number it
       looks like, because a line box is not its ink.

       27.75 on top rather than 34: the title shares a row with the menu
       button, whose 38px ground is taller than the title's 28px line box,
       so centring the two drops the title 5px inside the row - and the
       line box then holds about a pixel of leading above the ascenders.
       27.75 puts the ink on 34, measured at device scales 1, 2 and 3. It
       sits mid-plateau, too: the raster snaps to whole pixels, so anything
       from 27.5 to 28 lands the type in the same place, and the middle is
       furthest from either edge if a fallback face shifts the metrics.

       34 at the foot, and it is the number it says it is again: the pill
       is the last thing in the header now that the subtitle is not drawn
       here, and a pill's border is its own ink. It was 29 while the
       subtitle ended the block, to allow for the five pixels of descender
       space under its last baseline. */
    padding: 27.75px var(--mobile-inset) 34px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
  /* With the panel up it covers 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 panel's state. */
  .nav-locked .content { overflow: hidden; touch-action: none; }

  /* One in-flow child is left in this column: the title block. The
     progress and the contents are in the panel, which is fixed and so
     contributes nothing here, and the lockup is not drawn at all. */
  .sidebar__inner { flex: none; }

  /* The two marks come off the phone on your instruction. The pill under
     the title carries magier instead, and the sizes that used to be set
     here for the phone lockup went with the rule. */
  .brand-lockup { display: none; }

  .book-title { font-size: 28px; }
  /* The title and the menu button on one line, on your instruction: the
     button is the header's only control now. Centred on each other rather
     than aligned at the top - the button's ground is 38px against the
     title's 28px line box, and centring is what puts the three lines of
     the icon across the title's own ink. The title is allowed to shrink
     and the button is not; at 360px the title's ink is 235px against 312
     of room, so it never has to. */
  .book-title-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
  }
  .book-title-row .book-title { min-width: 0; }
  .action-button--menu { display: grid; flex: none; }
  /* Title to pill. 12px of box gap, which is 16 to the eye: the title's
     baseline sits about four and a half pixels above its box, and the
     pill's own top edge is its ink. Half the block gap, deliberately -
     the pill belongs to the title rather than standing beside it. */
  .book-title-block { gap: 12px; }
  /* No subtitle on a phone, on your instruction. The desktop keeps it -
     this is the only rule that hides it, and it is inside the phone's
     media query, where the sidebar shows the title, the mark and the
     contents behind the menu and nothing else. */
  .book-subtitle { display: none; }
  /* The pixel and a quarter that used to sit here is gone with the lockup:
     it was centring the title's ink between the mark above it and the rule
     below, and the title is the first thing in the header now. The same
     correction is in .sidebar's padding instead, where it belongs when
     there is only one gap above the type to get right. */

  /* --- Powered by magier ---
     The pill is as wide as its content, so it takes flex-start rather than
     the column's stretch. What is in it is the lockup and nothing else -
     the "Powered by" caption is gone on your instruction - which is why
     there are no type rules here: the only text left inside is the x
     between the marks, and it sets its own, the way the sidebar's does.

     The 5px of padding is what keeps the pill 32px tall against the 20px
     mark, and the header's frame is measured to the pill's border, so
     changing it moves the subtitle and the foot with it. */
  .powered-pill {
    display: inline-flex;
    align-self: flex-start;
    align-items: center;
    padding: 5px 10px;
    border: 1px solid var(--c-pill-border);
    border-radius: 999px;
    background: var(--c-pill-bg);
  }
  /* Both marks in the pill now, in the proportion the lockup is drawn in.
     The sidebar's own sizes are 116px on the magier mark and 118 on
     Webflow's, 16px apart with a 14px x between them; 0.553 of each is
     what brought the magier mark to the 20px that seated its wordmark's
     x-height beside the caption that used to sit in front of it. The
     caption is gone and the factor stays: the two marks are still in the
     proportion they were drawn in, and the rest follows from it rather
     than being picked by eye - 11px tall on Webflow's, 9px of gap, an
     8px x. */
  .powered-pill__lockup {
    display: inline-flex;
    align-items: center;
    gap: 9px;
  }
  .powered-pill__mark {
    display: flex;
    text-decoration: none;
    color: inherit;
  }
  .powered-pill__svg { display: block; width: auto; }
  .powered-pill__magier .powered-pill__svg  { height: 20px; }
  .powered-pill__webflow .powered-pill__svg { height: 11px; }
  .powered-pill__x {
    font-family: 'Open Sans', var(--f-sans);
    font-size: var(--t-pill-x-size);
    line-height: 1;
    color: #000;
  }

  /* The subtitle and the progress are a pair, closer to each other than
     either is to anything else, which is what the 16 says - half the
     block gap. Both numbers are the ones they had in the drawer; what
     went is the 26px that used to push the group past the drawer's cut
     line, because there is no cut any more. The rule on top stays: in the
     panel it separates the close button's row from the contents under it,
     which is the job it was drawn for. */
  /* Nothing here now: the group is the progress alone, the rule above it is
     gone on your instruction, and the desktop's pull-up went with the
     subtitle it was correcting for. Left as a comment rather than a rule so
     the next person looking for the phone's version of .book-meta finds the
     answer where they expect it. */
  .toc { gap: 26px; }
  .toc-list { gap: 14px; }
  .dd__inner > * { padding-top: 14px; }
  /* --- Navigation panel ---
     The whole viewport, in from the right, over everything. Transform
     rather than width or right: it is the one property that animates on
     the compositor, so a full-screen panel arrives at frame rate on a
     phone. Closed, it is translated out and hidden - `visibility` is what
     takes it out of the tab order and the a11y tree while the transform
     alone would leave it reachable - and the hiding waits out the slide,
     which is what the second transition is for. */
  .nav-panel {
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0; right: 0; bottom: 0;
    width: 100%;
    z-index: 40;
    background: var(--c-sidebar-bg);
    transform: translateX(100%);
    visibility: hidden;
    transition: transform var(--nav-duration) var(--nav-ease),
                visibility 0s linear var(--nav-duration);
  }
  .nav-panel.is-open {
    transform: none;
    visibility: visible;
    transition: transform var(--nav-duration) var(--nav-ease),
                visibility 0s;
  }
  /* The close button takes the same 38px ground and the same 24px inset
     from the right as the menu button it stands in for, and the panel
     keeps the header's frame: 34px of air above it. The 24 below is the
     rule's clearance - with the icon sitting 8.5px inside its own box that
     reads as the 32 every other block on the phone is spaced by. */
  .nav-panel__bar {
    display: flex;
    justify-content: flex-end;
    flex: none;
    /* 34px of air above it, the header's own frame. The 16 below brings the
       progress up under the button, on your instruction - the icon sits
       8.5px inside its own ground, so 16 of box reads as about 24 of air
       between the two. */
    padding: 34px var(--mobile-inset) 16px;
  }
  /* The contents are taller than a phone, so this is what scrolls - the
     panel itself stays put and the button above stays with it. */
  .nav-panel__inner {
    display: flex;
    flex-direction: column;
    gap: 32px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0 var(--mobile-inset) 34px;
    scrollbar-width: none;
  }
  .nav-panel__inner::-webkit-scrollbar { display: none; }

  /* The dim. It sits under the panel and over everything else, so during
     the slide the page is visibly still there, behind. Pointer-events are
     left off: by the time the animation ends the panel covers the
     viewport, and nothing behind it can be reached anyway. */
  .nav-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 39;
    background: var(--c-scrim);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--nav-duration) var(--nav-ease);
  }
  .nav-scrim.is-open { opacity: 1; }

  /* --- Page shell ---
     Second and last in the column, taking whatever the header leaves.
     min-height: 0 so the pane can be shorter than its own content, which
     is what lets .content scroll instead of the page growing. */
  .main {
    flex: 1 1 auto;
    min-height: 0;
  }
  .top-bar {
    height: var(--mobile-topbar-h);
    padding: 0 var(--mobile-inset);
  }
  .action-button { width: 38px; height: 38px; border-radius: 10px; }
  /* Same reason as on the print bar: the rule above squares every button
     off, and the switch's labels need their own width. */
  .lang-switch__item { width: auto; min-width: 38px; }
  .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; }
  /* Which means this half has to be drawn at both depths here. The
     desktop hides it at depth 1 because the parent crumb is already
     naming the view; with the parent gone, hiding it too left the bar
     blank at the top of every view whose first section has no heading of
     its own - 15 of the 20. app.js puts the view's name in it for exactly
     that case. */
  .breadcrumb[data-depth="1"] .breadcrumb__current { display: block; }
  /* 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;
    /* With the parent crumb hidden, this half *is* the bar's title - so it
       takes the type the desktop sets a title in (--c-crumb-parent, the
       navy, at Medium) rather than the lighter blue and Regular of a
       trailing crumb. Both depths, so the bar does not change weight or
       colour as the reader scrolls out of a view's opening and into a
       headed section. The family is already the desktop's: .breadcrumb
       sets --f-serif for the whole bar and nothing here overrides it. */
    font-weight: var(--w-medium);
    color: var(--c-crumb-parent);
    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. What replaces it is the file itself - Copy Link and
     Download PDF are the two actions in the bar here. */
  .action-button--pdf { display: none; }
  .action-button--download { display: grid; }
  /* Every label in the bar is centred on the icon it names, and "Download
     PDF" is the widest. Centred on the last button in the row, 24px from
     the edge, it is laid out 1.1px past the window - which is enough for a
     sideways scroll on the page and a shaved edge on the bubble. So the
     icon group takes 8px of clearance on the right rather than the label
     giving up its centre: the icons move in by 8, which reads as nothing
     against a 24px inset. The menu button moving into the header put
     Download PDF back at the end of the row, which is why this is back. */
  .header-actions { padding-right: 8px; }
  /* "PDF herunterladen" (107px) against "Download PDF" (88.3px): the German
     bubble is laid out 2.5px past the window at both 390 and 360 with the
     8px above, so the row takes 13px. Both editions take it, for the same
     reason the desktop does: one clearance means one row, and the controls
     do not move when the reader switches edition. It overrides the 8px
     directly above rather than replacing it, so the reasoning for the
     original number stays readable. */
  .header-actions { padding-right: 13px; }

  .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. 8.5em rather than
       the 7em it was, because 7 was measured against the labels that
       happened to be in front of me and two in the book are longer than
       that: "Default Webflow recommendation" in Chapter 7's crawler table
       and "Team-ownership model" in Chapter 10's. Their longest words ran
       13px past the blue block and printed over the value beside it -
       which read as the label being cut off. Measured across all 54
       labels in the book, the widest word is 110px at the label's own
       size, and this track leaves 116px inside the padding. */
    grid-template-columns: 8.5em 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);
    /* A step under the value it names. It is a column heading rather than
       prose, and at the cell's own size the longest of them could not fit
       any track worth giving it. */
    font-size: var(--size-small);
    /* Tighter leading than the prose, on your instruction: a label that
       takes two lines is a heading wrapping, not a paragraph, and 1.6 put
       a visible gap through the middle of the blue block. */
    line-height: var(--lh-ui);
    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;
    /* 1.4 rather than the prose's 1.6. Stacked, a three-line value is a
       block of its own beside a coloured label rather than a paragraph in
       a column of running text, and the looser leading made every row of
       every table taller than it needed to be on a phone. */
    line-height: var(--lh-small);
    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 8.5em label
     track was sized for names like "Webflow Enterprise (3-year)"; here it
     took nearly half a 276px cell and the question could not fit beside
     it at 360px.

     6em, not the 5.5 it was: at 5.5 both "Your answer" and "Score (0-3)"
     wrapped, and the second broke as "Score (0-" / "3)", which reads as a
     typo rather than a wrap. 6em fits each of them on one line at 360px
     and at 390 - and the appendix is *shorter* for it, by 370px at 390 and
     414 at 360, because a one-line name in an empty cell is 18px shorter
     than a two-line one and there are 46 of them. */
  .cluster .dtable tbody td[data-label] {
    grid-template-columns: 6em minmax(0, 1fr);
  }
  /* Everything else about a cluster's cells is the book's own stacked
     table, on your instruction, and two rules that used to sit here are
     gone with it.

     The label was pinned to the top of its cell and the value beside it,
     on the argument that a one-word name like "Question" should not draw a
     blue slab down the side of a five-line question. Every other stacked
     table in the book fills that space - Appendix J's do - so these do
     now: the block takes the height of the cell it names.

     And an empty cell - "Your answer", "Score" - used to collapse to the
     name alone, hugging its own text. That left three chip widths in one
     row (88, 94 and 91 against a 5.5em track) and dropped the empty value
     onto a second grid line, which put 14px of nothing under the block.
     Both are fixed by letting the cell be an ordinary cell: the name keeps
     the track, the blank keeps its place beside it. */
  .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; }
}
