blog.git

blog

~ Personal research notes. blog.sqzer.com
git clone https://github.com/debuman/blog.git

src/styles/tokens.css (25277B)

      1 /* Design tokens. Type, spacing and grid values are measured from two reference sites,
      2    The Harvard Gazette and nicholas.carlini.com; the colour ladder is re-derived around a
      3    crimson anchor. Single light theme — neither
      4    reference site ships a dark mode. */
      5 
      6 :root {
      7   color-scheme: light;
      8 
      9   /* Colour: five families x four steps. base sits at OKLCH L=.4701 with chroma at .901 of
     10      the gamut maximum, both taken from the crimson anchor #a51c30.
     11        dark   -> --ppc-text  (kickers)
     12        base   -> --ppc-ui    (rules, quote bars, focus rings)
     13        bright -> on black only
     14        wash   -> emphasis backgrounds
     15      All 20 steps are contrast-checked against --page-bg, bright against black: dark >= 4.5,
     16      base >= 3.0, bright >= 4.5 on black, wash <= 1.25. */
     17   --crimson-dark:   #771120;  --crimson:   #a51c30;  --crimson-bright: #faa2a2;  --crimson-wash: #f7ebeb;
     18 
     19   /* Hover step for a link in running text, and the one colour here that is not a rung of
     20      the ladder above — it sits between --crimson and --crimson-bright, which is why it is
     21      declared apart rather than pretending to be a fifth step.
     22 
     23      Derived from the reference rather than picked: its body links go rgb(160,32,32) at
     24      rest to rgb(192,48,48) on hover, measured over CDP on its own article page, which is
     25      a luminance increase of x1.572. The same factor applied to --crimson lands here.
     26      Contrast: 5.30:1 on --page-bg, so it stays over the 4.5:1 normal text needs while
     27      spending the headroom the way the reference spends it. Against the black body text
     28      beside it this reads 3.84:1, where --crimson at rest is 2.81:1 — so the hovered link
     29      is the one state that clears the 3:1 a colour-only link is asked for. */
     30   --crimson-lift:   #cb233b;
     31   --blue-dark:      #0e455f;  --blue:      #186285;  --blue-bright:    #70cafa;  --blue-wash:    #e6f0f7;
     32   --ochre-dark:     #553a0c;  --ochre:     #775315;  --ochre-bright:   #f6ae30;  --ochre-wash:   #f7ede0;
     33   --purple-dark:    #5e127b;  --purple:    #831daa;  --purple-bright:  #dda3fa;  --purple-wash:  #f3ebf7;
     34   --slate-dark:     #453f34;  --slate:     #605a4e;  --slate-bright:   #c4bdb0;  --slate-wash:   #f5eee0;
     35 
     36   /* Neutrals */
     37   --black: #000000;
     38   --grey: #b9b8ad;            /* hairlines only */
     39   --sand: #faedda;            /* surface: code plate, inline code, kbd, mark */
     40   --sand-light: #fffbf4;      /* page background */
     41   --sand-dark: #eed6aa;       /* borders */
     42 
     43   /* Page roles. A section overrides the --ppc-* trio. */
     44   --page-bg: var(--sand-light);
     45   --page-fg: var(--black);
     46   --page-meta: var(--slate);
     47   --page-hairline: var(--grey);
     48   --ppc-text: var(--crimson-dark);
     49   --ppc-ui: var(--crimson);
     50   --ppc-wash: var(--crimson-wash);
     51 
     52   /* ── Type scale ────────────────────────────────────────────────────────────
     53      Every fluid step is written as clamp(MIN, A vw + B rem, MAX) with
     54 
     55          A = (MAX - MIN) x 1600 / 1168   (rem -> vw)      B = MIN - 0.2 A
     56 
     57      so the middle term equals MIN at exactly 320px and MAX at exactly 1488px,
     58      rather than merely approaching them. Rounded to four decimals the residual at
     59      either endpoint is under 0.002px on every row. Not every clamp in this file shares
     60      these ends: --fs-code runs from 375 to 1440 and --root-pad has its own, each
     61      explained at the token, and the spacing clamps (--sp-32 to --sp-120) keep theirs.
     62 
     63      Derived from Lora's outlines rather than scaled by a constant off the
     64      Newsreader/Inter ladder it replaced. Measured on the shipped subset
     65      (public/fonts/latin-9bba8f25.woff2, wght 400, upem 1000): x-height .500em,
     66      cap .700em, b-ascender .755em, p-descender -.255em. Inter's x-height is
     67      .5459em, so an unchanged px size already draws 8.4% less lowercase on Lora;
     68      a constant-factor rescale would bury that instead of spending it.
     69 
     70      The anchor is apparent size, not px. The reference this design was pointed at
     71      sets the same face at 14pt = 18.667px, which is 9.333px of x-height. The ladder
     72      is cut against a 19px body = 9.500px of x-height, 1.8% above the reference and
     73      13.0% below a 20px Inter (10.92px) - a 13% cut in what the eye measures out of a
     74      5% cut in px. Running text itself is a flat 16px (see --fs-body); the headings
     75      keep the 19px anchor.
     76 
     77      The ladder is then a multiple of that anchor. The reference renders nothing above
     78      1.50x body anywhere; ours has to carry a real page title on pages with no image
     79      and no other display element, so at 1488 h1 is 2.0x the anchor (38px), h2 the
     80      reference's own 1.50x (28.5px), and the rest interpolates. Against the 16px body
     81      that ships:
     82 
     83                     h1     h2     h3     h4     h5     h6    body
     84          1488px   38.0   28.5   24.5   22.0   20.0   17.0   16.0
     85                  2.38x  1.78x  1.53x  1.38x  1.25x  1.06x
     86           320px   27.0   23.0   21.0   19.5   18.0   17.0   16.0
     87                  1.69x  1.44x  1.31x  1.22x  1.13x  1.06x
     88 
     89      h6 is 17, not 18, so the ladder stays strictly monotone at 320px, where h5's
     90      floor is 18. No post writes ##### or ######, but a post that contains an h1 has
     91      every heading moved down one level (satteriProse), so its #### renders as h5.
     92      .entry__title on the Writing index is --fs-h5 as well, so h5 is both the deepest
     93      article heading and the index entry title: 20px at 1488 against the 16px body,
     94      where the reference sets its own index entries at exactly body size.
     95 
     96      --fs-small and --fs-medium sit outside the ladder. --fs-small sets the small print
     97      (footer, byline, labels, the contents rail) and --fs-medium the secondary text
     98      (captions, tables, footnotes, the inline contents list). Both already land at or
     99      below body size, and cutting --fs-small would push the small print under 11px. They
    100      keep the same 320-1488 shape, 11->14 and 12->16. The nav and the page name have their
    101      own token, --fs-nav, and --fs-code has its own note at the token. */
    102   --fs-small:    clamp(0.6875rem, 0.2568vw + 0.6361rem, 0.8750rem);  /* 11→14 */
    103   --fs-medium:   clamp(0.7500rem, 0.3425vw + 0.6815rem, 1.0000rem);  /* 12→16 */
    104 
    105   /* Body is a flat 16px, not a clamp, because that is what the reference is.
    106      Measured on nicholas.carlini.com/writing/2025/regex-chess.html at a 1440px viewport:
    107      body 16px, line-height 27.2px, paragraph box 928px — the same measure this site
    108      uses. It never declares a font-size at all, so its body is the browser default and
    109      does not move with the viewport. A body that grows with the viewport reads as too
    110      large overall: a 16→19px ramp on the 320-1488 shape is already 18.9px at 1440, 18%
    111      over the reference's 16.
    112 
    113      Growth with the viewport is the thing avoided, so this is not a shallower clamp
    114      either. The heading ladder above keeps its own clamps, which this line does not
    115      touch; only its ratios to body differ from the 19px anchor it was cut against — h1
    116      is 2.375x at 1488 rather than 2.00x — and the table above gives them against this
    117      16px. */
    118   --fs-body:     1rem;                                               /* 16, fixed */
    119 
    120   /* The header is measured from the reference's own header, not from body.
    121      It sets its title at 150% and its nav at 120% of a 16px root: 24px and 19.2px. Hung
    122      off --fs-body instead, with the nav at body size and the name at 1.25x of it, the
    123      bar would read 20/16 against the flat 16px body. This token carries the nav size and .page-name takes 1.25x of it, which is the
    124      reference's own 24:19.2 ratio.
    125 
    126      It is a clamp where body is not, because the reference collapses its header into a
    127      hamburger on a phone and this one wraps instead: a flat 19.2px nav wraps to a third
    128      row at 320px, where 16px keeps the bar to two. */
    129   --fs-nav:      clamp(1.0000rem, 0.2740vw + 0.9452rem, 1.2000rem);  /* 16→19.2 */
    130   --fs-h6:       1.0625rem;                                          /* 17, fixed */
    131   --fs-h5:       clamp(1.1250rem, 0.1712vw + 1.0908rem, 1.2500rem);  /* 18→20 */
    132   --fs-h4:       clamp(1.2188rem, 0.2140vw + 1.1759rem, 1.3750rem);  /* 19.5→22 */
    133   --fs-h3:       clamp(1.3125rem, 0.2997vw + 1.2526rem, 1.5313rem);  /* 21→24.5 */
    134   --fs-h2:       clamp(1.4375rem, 0.4709vw + 1.3433rem, 1.7813rem);  /* 23→28.5 */
    135   --fs-h1:       clamp(1.6875rem, 0.9418vw + 1.4991rem, 2.3750rem);  /* 27→38 */
    136 
    137   /* Unreferenced by any rule in src/. Kept because the names are part of the token
    138      contract, and sized on the ladder's own 320-1488 shape so that anything which later
    139      picks one up lands near the scale rather than on a display step far above it.
    140      --fs-h1-large is also named in src/pages/writing/[year]/[slug].astro, where the
    141      article title's note says why the title does not use it. */
    142   --fs-xlarge:   clamp(1.2500rem, 0.5137vw + 1.1473rem, 1.6250rem);  /* 20→26 */
    143   --fs-2xlarge:  clamp(1.6250rem, 0.5137vw + 1.5223rem, 2.0000rem);  /* 26→32 */
    144   --fs-h1-large: clamp(2.0000rem, 1.1986vw + 1.7603rem, 2.8750rem);  /* 32→46 */
    145 
    146   /* Hooks for src/pages/writing/index.astro, which is written as a portable
    147      component: it re-declares --fs-h1 / --fs-h5 / --fs-large from
    148      var(--wp-*, <its own literal clamp>), and those literals are a larger ladder than
    149      this one (34→64, 20→24 and 16→20). Without --wp-h5 the entry titles would set at
    150      20→24 while article headings follow the ladder above - and every entry title on
    151      the site is on that one page. --wp-h1 sizes only the index's .sr-only page title,
    152      which is never drawn, and --wp-large only the empty-state line, which a build with
    153      any post never renders. This is the same mechanism fonts.css uses to put the index
    154      on the shipped face through --font-sans / --font-serif.
    155 
    156      Written as literal clamps, not var(--fs-h1). Substitution happens at the
    157      declaring element, so :root { --wp-h1: var(--fs-h1) } would in fact compute to
    158      :root's --fs-h1 and inherit down cleanly - but the index assigns the result
    159      back into --fs-h1 on .index, and handing a property to itself through an alias
    160      is not a thing to be clever about when the failure mode is every entry title
    161      losing its font-size. Keep --wp-h1 and --wp-h5 in step with --fs-h1 and --fs-h5.
    162      --wp-large is not --fs-body: it is a 16→19 ramp, not the flat 16px. */
    163   --wp-h1:       clamp(1.6875rem, 0.9418vw + 1.4991rem, 2.3750rem);  /* = --fs-h1 */
    164   --wp-h5:       clamp(1.1250rem, 0.1712vw + 1.0908rem, 1.2500rem);  /* = --fs-h5 */
    165   --wp-large:    clamp(1.0000rem, 0.2568vw + 0.9486rem, 1.1875rem);  /* 16→19, not --fs-body */
    166 
    167   /* Line heights. Cut for Newsreader and kept for Lora on measurement, not by
    168      oversight - the tempting number to react to is the wrong one.
    169 
    170      Lora's metrics box is sTypoAscender + sTypoDescender = 1.006 + 0.274 = 1.280em,
    171      against exactly 1.000em for the Newsreader this ladder was cut for, which looks
    172      like every step below 1.28 has become an overlap. It has not. Lines collide when
    173      ink meets ink, and ink is what barely moved: measured on the shipped subset,
    174      Lora's lowercase and parentheses run -0.271em to +0.760em, a 1.031em band, against
    175      1.033em for Newsreader, the face the ladder was set against (its 0.9745em band at the
    176      106% size-adjust it used): the band is 0.2% narrower than the one these steps assume.
    177 
    178      What the larger metrics box does is shift the half-leading. At --lh-tight the
    179      half-leading is (1.1 - 1.280) / 2 = -0.090em, so the line box spans +0.916em to
    180      -0.364em around the baseline and the ink sits inside it with room at both ends:
    181      nothing clips, and consecutive lines clear by 1.1 - 1.031 = 0.069em. That is
    182      2.6px on the 38px h1 - the clearance is a ratio, so a smaller size shrinks it
    183      in px but never closes it. Korean headings are looser still - Hangul ink spans
    184      at most 0.840em once KoBody's 94.29% is applied.
    185 
    186      The one glyph that would break this is $, which tops out at 0.833em; it appears
    187      only inside code, which sets --lh-base. */
    188   --lh-tight: 1.1;   /* h1 */
    189   --lh-snug: 1.2;    /* h2, h3 */
    190   --lh-close: 1.25;  /* h4 */
    191   --lh-medium: 1.4;  /* h5, h6 */
    192   --lh-base: 1.5;
    193   --lh-large: 1.6;   /* body */
    194 
    195   /* Spacing */
    196   --sp-20: 0.44rem;  --sp-30: 0.67rem;  --sp-40: 1rem;     --sp-50: 1.5rem;
    197   --sp-60: 2.25rem;  --sp-70: 3.38rem;
    198   --sp-24: 1.5rem;                                          /* heading-adjacent rhythm */
    199   /* Between paragraphs, and the one spacing token deliberately written in em rather than
    200      rem. The reference sets `p { margin-top: 0; margin-bottom: .7em }`, which is 11.2px
    201      against its 16px body; --sp-24 is 24px, 2.1x that, and reads as too much air. A
    202      custom property substitutes as a token stream, so the em resolves against the font
    203      size of whichever element consumes it — which is what keeps the gap proportional to
    204      the type instead of frozen at one pixel value. Read by three rules and no others:
    205      `p + p` in base.css, `.prose > p + p` and `.prose blockquote p + p` in the article
    206      template. It is NOT the paragraph-adjacent rhythm: a heading followed by a paragraph
    207      keeps --sp-24, because the heading's job is to open a section rather than to continue
    208      one. Korean check, where the risk is that the gap disappears into the leading: at
    209      --lh-ko 1.75 the line box is 28px and Hangul's ink band fills 0.84em of it, so the
    210      white between two lines is 14.6px and the white across a paragraph boundary is
    211      25.8px — still 1.77x, which still reads as a boundary. */
    212   --sp-para: 0.7em;                                         /* between paragraphs */
    213   --sp-32: clamp(1.5rem, 0.5vw + 1.4rem, 2rem);
    214   --sp-48: clamp(2rem, 1vw + 1.8rem, 3rem);                 /* block rhythm */
    215   --sp-64: clamp(2.5rem, 2.2vw + 2rem, 4rem);
    216   --sp-80: clamp(2.5rem, 2.5vw + 2rem, 5rem);
    217   --sp-120: clamp(4rem, 3.5vw + 3.3rem, 7.5rem);
    218 
    219   /* ── Grid ──────────────────────────────────────────────────────────────────
    220      --w-content is 928px: Carlini parity, chosen deliberately over a 728px column,
    221      with the cost measured first and accepted.
    222 
    223      Where the side whitespace comes from. Above the width at which the shell stops
    224      growing (1024 + 2 x --root-pad, which --root-pad's own ramp puts at vw = 1063),
    225      the distance from the viewport edge to the first prose character is
    226 
    227          left  = (vw - 96 - --w-content) / 2 + 96      right = (vw - 96 - --w-content) / 2
    228 
    229      --root-pad cancels out of both: it is inside the shell's own max-width, so it
    230      moves only the edge of main's own box, which paints nothing, never where the text
    231      starts. At 1488px that is 328px left and 232px right whatever the padding does,
    232      and --w-content is the only token on the page that can change it. A 728px column
    233      would put it at 428 / 332, so 928 takes exactly 100px from each side.
    234 
    235      What it costs. Measured over the paragraphs of the built articles, greedy-wrapped
    236      on spaces only because word-break is keep-all, with Lora's advances and its 2,439
    237      GPOS kern pairs, and Pretendard's uniform 1770/2048 Hangul advance at KoBody's
    238      94.29%. cpl is the mean of the lines that actually wrap; the syllable figure is
    239      what one full line holds:
    240 
    241          728px / 16px   English  94.5 cpl   Korean 66.2 cpl   55.8 syllables
    242          928px / 16px   English 122.2 cpl   Korean 85.4 cpl   71.2 syllables
    243 
    244      The usual bands are 45-75 characters for English and 41-45 syllables for
    245      Korean. Either width is past both at the 16px body; 928px holds more than half
    246      again the Korean band's top, and English runs past 120 characters a line. The
    247      column was chosen at a 19px body, where 928px held 59.9 syllables and about 100
    248      English characters; the flat 16px body (see --fs-body) puts more on every line.
    249      Article prose is about four fifths Hangul by letter count (see the KoBody note in
    250      fonts.css), so that is the cost on most of the site, not an edge case. This block
    251      records the trade. It is not an argument to reverse it, and no narrower fallback
    252      is hiding below: 928px is the measure at every viewport wide enough to reach it.
    253 
    254      Both pages hang off this token, and they do not share it. The Writing index
    255      keeps its own --content-w, capped at min(100%, rail + gap + --content-w), so
    256      the 100% branch follows a narrowing but nothing follows a widening. Were the
    257      index to restate the width instead, moving this line alone would part the two:
    258      with the index at 824px and the article at 1024px, the article title lands 100px
    259      to the LEFT of the entry it was clicked from at 1488px and 1920px, and 81px left
    260      at 1024px - the wider box is centred, so its left edge moves outward, not inward.
    261      --content-w in src/pages/writing/index.astro therefore reads this token through
    262      var() rather than re-stating it: a rem literal there matches only at a 16px root,
    263      and at a 12px root the article <h1> sits 116px left of the entry title. Bound, the
    264      two left offsets were compared in a browser at 320 / 768 / 1024 / 1488 / 1920 and
    265      at 12 / 14 / 16 / 20 / 24px roots: identical to the pixel wherever the rail's band,
    266      below, does not apply.
    267 
    268      One bounded exception to that, named here because this is where a reader will
    269      come looking for it. From 1440px, where the contents rail is shown, every article
    270      pushes its page box right wherever a centred gutter cannot seat the rail's 240px
    271      floor; the gutter is reserved whether or not the post has a rail - see the
    272      ── the rail ── block in src/pages/writing/[year]/[slug].astro. Inside that band
    273      the article title sits right of the index entry instead of matching it: measured
    274      at a 16px root, 72px at 1440, 48 at 1488, 24 at 1536, and 0.00px from 1584 up and
    275      below 1440. The band tracks the root size, because the floor is expressed in rem
    276      along with the type it exists to fit - measured 0.00px at every width at a 12px
    277      root, and at a 24px root it runs from 1440 to about 1912. Its worst case there is
    278      195.91px at 1520, not the 156.80px it reads at the foot of the band: below 1521
    279      the third, room term of --toc-rail-w is what binds, so --page-left tracks the
    280      viewport and the divergence RISES with it until the 15rem floor takes over.
    281      Sampling only the usual 1440 / 1488 / 1536 / 1584 columns misses that peak by 8px
    282      and the foot of the band by 39px. That is the whole of the divergence: no width,
    283      root size or article overflows horizontally, and the page box never loses its full
    284      --w-page - measured 928px of measure and 0 horizontal overflow at 12 / 14 / 16 /
    285      20 / 24px roots across 1400-2000px. */
    286   --w-content: 928px;
    287   /* Page box: the 72px date rail, its 24px gutter, and the measure. The shell, the
    288      header and footer padding and the rail geometry read it; the index and the article
    289      compute the same sum from --w-content, which is what puts their text on one left
    290      edge. */
    291   --w-page: calc(var(--w-content) + 4.5rem + 1.5rem);   /* 1024px */
    292   --w-wide: 1488px;
    293   /* Outer gutter, 16 -> 24px. The reference site runs a flat 1em at every width;
    294      this starts there and grows only to 24px rather than going fixed.
    295 
    296      The shape is deliberate and differs from the type scale above: the middle term
    297      is pinned at the 1rem floor for every viewport up to exactly 768px (1vw =
    298      0.48rem there, so the bracket is zero and negative below it), and only then
    299      ramps. A phone gutter must not ride a desktop ramp - the canonical
    300      A vw + B rem form would put 19.1px of padding on a 768px screen and cost the
    301      prose 6px it does not have. The factor sets where the cap lands: at 1.1111 the
    302      term is exactly 1.5rem at 1488 ((0.93 - 0.48) x 1.1111 = 0.5), and the floor
    303      holds 1rem at 320.
    304 
    305      Above the width where the shell stops growing, the pad reaches neither the prose
    306      nor the header's full-bleed rule: it sets the width of main's own box, 1024 + 2 x
    307      --root-pad = 1072px at 1488, which nothing paints. The prose column does not move
    308      at all - see the cancellation above. */
    309   --root-pad: clamp(1rem, 1rem + ((1vw - 0.48rem) * 1.1111), 1.5rem);  /* 16→24 */
    310 
    311   /* ── Syntax highlighting ───────────────────────────────────────────────────
    312      Astro builds the css-variables theme with `variablePrefix: '--astro-code-'`
    313      and NO `variableDefaults` (@astrojs/internal-helpers/dist/shiki.js), and
    314      @shikijs/core only emits a fallback when defaults are supplied
    315      (dist/index.mjs: `return \`var(${prefix}${name})\``). So every token colour
    316      ships as a bare var() with no fallback: leave one of these undefined and the
    317      declaration is invalid at computed-value time, `color` falls back to the
    318      inherited value, and the block renders as flat body text. They are not
    319      optional — they ARE the palette.
    320 
    321      Placement measured on the built DOM (dist, 52 plates, 2,230 token spans):
    322        foreground 24.3% · string 21.3% · string-expression 14.8% · keyword 14.1%
    323        punctuation 12.3% · function 6.3% · comment 3.9% · constant 3.1% · parameter 0.04%
    324      Contrast recomputed against both surfaces, sand #faedda first (the real plate):
    325        black 18.19/20.36 · purple-dark 9.84/11.01 · blue-dark 8.94/10.00
    326        crimson-dark 9.65/10.80 · ochre-dark 9.11/10.19 · slate 5.93/6.63. */
    327   --astro-code-background:              transparent;         /* figure.code paints the plate */
    328   --astro-code-foreground:              var(--black);
    329   --astro-code-token-string:            var(--black);        /* bash: unquoted argument, not a string */
    330   --astro-code-token-keyword:           var(--purple-dark);
    331   --astro-code-token-string-expression: var(--blue-dark);
    332   --astro-code-token-punctuation:       var(--slate);
    333   --astro-code-token-function:          var(--crimson-dark);
    334   --astro-code-token-comment:           var(--ochre-dark);
    335   --astro-code-token-constant:          var(--blue-dark);
    336   --astro-code-token-parameter:         var(--black);
    337   --astro-code-token-link:              var(--blue-dark);
    338 
    339   /* Code body. The top of the ramp is 0.9 x the fixed --fs-body, not 1.0: a
    340      monospace face at the same em reads larger than the text face beside it, because
    341      every glyph carries the width of the widest one, so code set at body size looks
    342      like the loudest thing in the article. 14.4px is the correction, and it is a
    343      correction rather than a redesign - one notch, still above the 13px the reference's
    344      own <pre> inherits from the UA monospace default against its 16px body.
    345 
    346      It buys line width as well as quiet. The plate's inner box is 928 - 2*16 = 896px,
    347      and a line's width is linear in the size, so 14.4 rather than 16 is +10% of column.
    348      Measured over the built corpus - 52 plates, longest rendered line each, Consolas:
    349        at 16.0px   4 plates overflow 896px
    350        at 14.4px   3 plates overflow 896px
    351      The one that stops scrolling is the widest line any author here would want kept
    352      whole, a 110-column jsonc route table: 955px at 16, 859px at 14.4, 37px of slack.
    353      Of the three that remain, one is a 132-column wget command that would need 12.4px,
    354      under the 13px floor, and two are dumps, not code: a 701-column line of sysmon XML
    355      config (2.3px) and a 1,639-column syslog line holding a raw sysmon XML event
    356      (1.0px). They keep the scroller, or take ```<lang> wrap.
    357 
    358      How many characters 896px is still depends on the face: 104 at 0.600em (JetBrains
    359      Mono, Menlo), 113 at 0.5498em (Consolas, measured off the built page - a 132-column
    360      line renders 72.57px per 1px of font-size). No single character count holds until
    361      the site ships a monospace face of its own.
    362 
    363      The ramp runs 13px at 375 to 14.4px at 1440, and the floor is 13px because a phone
    364      gains little below it: at 375 the plate's inner box is about 300px (311 on a phone,
    365      296 beside a classic scrollbar), some 40 columns at 13px, and half the plates run
    366      wider than that, most of them far wider, so a smaller size would not stop them
    367      scrolling. */
    368   --fs-code:  clamp(0.8125rem, 0.1315vw + 0.7817rem, 0.9rem);  /* 13→14.4 */
    369   --code-pad: var(--sp-40);                                  /* 16px */
    370 }
    371 
    372 /* Per-section palette, swapped by a class on <body>. */
    373 .is-writing { --ppc-text: var(--crimson-dark); --ppc-ui: var(--crimson); --ppc-wash: var(--crimson-wash); }
    374 .is-code    { --ppc-text: var(--blue-dark);    --ppc-ui: var(--blue);    --ppc-wash: var(--blue-wash); }
    375 .is-talk    { --ppc-text: var(--ochre-dark);   --ppc-ui: var(--ochre);   --ppc-wash: var(--ochre-wash); }
    376 .is-papers  { --ppc-text: var(--purple-dark);  --ppc-ui: var(--purple);  --ppc-wash: var(--purple-wash); }
    377 .is-slate   { --ppc-text: var(--slate-dark);   --ppc-ui: var(--slate);   --ppc-wash: var(--slate-wash); }