/* ============================================================================
   Plantroom Labs — site design system
   ----------------------------------------------------------------------------
   Layer 1 of this file is the token set. It is the *same* token set that drives
   the bajaux widgets shown in /work/ — the eleven anchor colours, the 4px space
   scale, the two-part elevation model and the motion curves are lifted verbatim
   from the product's tokens.css. The site then adds the handful of roles a
   marketing page needs and a product UI does not: a display type scale, page
   gutters, and a dark band treatment.

   That shared origin is the point. A prospect who looks at the screenshots and
   then at the page around them is looking at one system, not a portfolio site
   that happens to host pictures of something else.

   Nothing below Layer 1 hard-codes a colour, a radius or a duration.
   ========================================================================= */

/* ------------------------------------------------------------------ fonts
   Self-hosted rather than loaded from fonts.googleapis.com. Two reasons: the
   Google stylesheet is a render-blocking request to a third party before any
   text can paint, and it hands a UK visitor's IP address to Google on every
   page load, which is a data-protection question this site does not need to
   answer.

   Both files are the variable font subset to latin plus the punctuation the
   copy actually uses (see assets/fonts/README.md). One 62 KB file covers
   every Inter weight the site asks for; one 39 KB file covers the mono.

   font-display:swap so text is readable in the fallback face immediately and
   reflows once, rather than being invisible while the font downloads. */

@font-face {
  font-family:"Inter"; font-style:normal; font-weight:100 900;
  font-display:swap; src:url("../fonts/inter-var.woff2") format("woff2");
}
@font-face {
  font-family:"JetBrains Mono"; font-style:normal; font-weight:100 800;
  font-display:swap; src:url("../fonts/jetbrains-mono-var.woff2") format("woff2");
}

/* ---------------------------------------------------------------- LAYER 1 */

:root {
  color-scheme: light;

  /* --- product anchors, verbatim ------------------------------------- */

  --pl-anchor-n-00:#ffffff; --pl-anchor-n-05:#fbfcfe; --pl-anchor-n-10:#f7f8fc; --pl-anchor-n-15:#f2f4f9;
  --pl-anchor-n-20:#eef0f5; --pl-anchor-n-25:#e8eaf2; --pl-anchor-n-30:#dde2ec; --pl-anchor-n-35:#d2d8e4;
  --pl-anchor-n-40:#c2cad9; --pl-anchor-n-50:#a5aec1; --pl-anchor-n-60:#7d8798; --pl-anchor-n-70:#5f6b7c;
  --pl-anchor-n-80:#404854; --pl-anchor-n-90:#2f353f; --pl-anchor-n-95:#252a31; --pl-anchor-n-99:#16191e;

  --pl-anchor-ok:#35b15b;   --pl-anchor-ok-tint:#e6f6ec;   --pl-anchor-ok-soft:#c2e8d0;   --pl-anchor-ok-deep:#268345;
  --pl-anchor-bad:#e53935;  --pl-anchor-bad-tint:#fdeaea;  --pl-anchor-bad-soft:#f8c4c3;  --pl-anchor-bad-deep:#b32723;
  --pl-anchor-run:#7b61ff;  --pl-anchor-run-tint:#f0edff;  --pl-anchor-run-soft:#d5ccff;  --pl-anchor-run-deep:#5b41d6;
  --pl-anchor-info:#00b8d9; --pl-anchor-info-tint:#e0f7fc; --pl-anchor-info-soft:#a6e7f4; --pl-anchor-info-deep:#0090aa;
  --pl-anchor-warn:#f29d0b; --pl-anchor-warn-tint:#fff4e0; --pl-anchor-warn-soft:#ffd99e; --pl-anchor-warn-deep:#c47c00;

  /* --- surface roles -------------------------------------------------- */

  --pl-canvas:       var(--pl-anchor-n-10);
  --pl-surface:      var(--pl-anchor-n-00);
  --pl-surface-sunk: var(--pl-anchor-n-15);
  --pl-surface-2:    var(--pl-anchor-n-05);
  --pl-mesh-a: rgba(123, 97, 255, 0.07);
  --pl-mesh-b: rgba(0, 184, 217, 0.07);

  --pl-text:        var(--pl-anchor-n-80);
  --pl-text-strong: var(--pl-anchor-n-95);
  --pl-text-muted:  var(--pl-anchor-n-70);
  /* Not --pl-anchor-n-60: at the 11px these labels are set in (brand tagline,
     eyebrows, table headers, captions) #7d8798 gives 3.3-3.6:1 depending on
     the surface under it, against a 4.5 floor. This is only a shade lighter
     than --pl-text-muted, which is the honest answer — at 11px there is no
     grey that is both faint and legible. The step down from muted is carried
     by size, weight and uppercasing instead of by colour. */
  --pl-text-faint:  #656f7f;   /* 4.62 on the darkest surface it lands on */
  --pl-invert:      var(--pl-anchor-n-00);

  --pl-line:        rgba(37,42,49,0.10);
  --pl-line-strong: rgba(37,42,49,0.17);

  --pl-accent:      var(--pl-anchor-run);
  --pl-accent-deep: var(--pl-anchor-run-deep);
  --pl-accent-tint: var(--pl-anchor-run-tint);
  --pl-accent-2:    var(--pl-anchor-info-deep);

  /* --- contrast-safe variants of the anchors above ---------------------
     The --pl-anchor-* block is the product's palette copied verbatim and stays
     that way. Four of those anchors miss the WCAG AA 4.5:1 floor at the
     12px this site sets chips and buttons in:

       white on --pl-anchor-run          4.20   (primary button)
       --pl-anchor-ok-deep   on its tint 4.25
       --pl-anchor-info-deep on its tint 3.39
       --pl-anchor-warn-deep on its tint 3.09

     The product gets away with it because its chips sit in a dense
     operator UI on a commissioned monitor. A marketing page is read once,
     often on a phone outdoors, and the people reading it are frequently
     doing so in a plant room. Each value below is the same hue dropped in
     lightness until it clears the floor — the largest shift is 8% and none
     is visible side by side. Used only where the colour carries text. */

  --pl-accent-fill: #7257ff;   /* 4.64 with white on top */
  --pl-ok-ink:      #247b41;   /* 4.71 on --pl-anchor-ok-tint   */
  --pl-info-ink:    #00788d;   /* 4.64 on --pl-anchor-info-tint */
  --pl-warn-ink:    #9b6200;   /* 4.65 on --pl-anchor-warn-tint */
  --pl-run-ink:     var(--pl-anchor-run-deep);  /* 5.76, already clears it */

  --pl-focus-ring: 0 0 0 2px var(--pl-canvas), 0 0 0 4px var(--pl-anchor-run);

  /* --- dark band: the product's dark theme, used as a section treatment */

  --pl-dark:         #0e1116;
  --pl-dark-surface: #171d26;
  --pl-dark-raised:  #1c232e;
  --pl-dark-line:    rgba(255,255,255,0.09);
  --pl-dark-text:    #dfe4ec;
  --pl-dark-muted:   #97a2b3;
  /* Tuned against every dark background it can land on, not just the flat one:
     .pl-band paints two radial meshes over --pl-dark, and a breadcrumb link sits
     where mesh-a is ~0.13 alpha. #737f91 was 4.66 on the flat colour but 4.11
     there. This clears 4.5 on --pl-dark (5.70), on --pl-dark-surface (5.10) and
     --pl-dark-raised (4.76), and on either mesh composited at full strength over
     --pl-dark — mesh-a 4.59, mesh-b 4.53. #818d9f, which stood here until
     2026-09-30, missed on mesh-b at 4.47: the claim was written by hand and this
     one value in it was wrong, which is why check.py now recomputes all of them.
     Both meshes at full strength at once would be 3.58, and is not a state that
     exists: they are radial gradients centred at opposite corners (12% -8% and
     96% 104%), each transparent by 62%/58% of its radius. */
  --pl-dark-faint:   #828ea0;
  --pl-dark-accent:  #9b85ff;
  --pl-dark-mesh-a:  rgba(123,97,255,0.20);
  --pl-dark-mesh-b:  rgba(0,184,217,0.15);
  /* Three fills that only exist inside a dark band: a chip, an inline code
     span, and a card icon well. Each is a translucent white or a translucent
     --pl-dark-accent (#9b85ff) rather than a flat colour, so the two radial
     meshes under .pl-band show through instead of being covered by a patch of
     the wrong grey. rgba() cannot take a custom property for its channels
     without splitting every colour into three tokens, so the accent is written
     out here, next to the token it is a tint of. */
  --pl-dark-chip:      rgba(255,255,255,0.05);
  --pl-dark-code:      rgba(255,255,255,0.07);
  --pl-dark-well:      rgba(155,133,255,0.14);
  --pl-dark-well-line: rgba(155,133,255,0.30);

  /* Dark mode needs two roles a band never does. A band has no sunk section and
     no code block, so nothing inside it ever asks for a step *below* --pl-dark;
     a whole page does, for .pl-section--sunk and for <pre>. And the strong
     hairline has no dark twin because a band draws no strong hairlines. */
  --pl-dark-sunk:        #080a0d;
  --pl-dark-line-strong: rgba(255,255,255,0.17);

  /* Status chips in dark mode. The light palette's five tints are near-white,
     so left alone a chip becomes the brightest object on a dark page. Each tint
     below is its anchor hue composited onto --pl-dark-surface at 0.18 and each
     line the same hue at 0.40, flattened to an opaque value so the ink ratio is
     exact wherever the chip lands rather than depending on what is behind it.
     The ink is the anchor's own soft pastel: light mode puts a deep ink on a
     pale tint, and this is that relation turned over rather than a second set
     of invented colours. 9.03-9.50:1, recomputed in check.py like every pair. */
  --pl-dark-ok-tint:#1c3830;   --pl-dark-ok-line:#23583b;   --pl-dark-ok-ink:var(--pl-anchor-ok-soft);
  --pl-dark-bad-tint:#3c2229;  --pl-dark-bad-line:#69282c;  --pl-dark-bad-ink:var(--pl-anchor-bad-soft);
  --pl-dark-run-tint:#29294d;  --pl-dark-run-line:#3f387d;  --pl-dark-run-ink:var(--pl-anchor-run-soft);
  --pl-dark-info-tint:#133946; --pl-dark-info-line:#0e5b6e; --pl-dark-info-ink:var(--pl-anchor-info-soft);
  --pl-dark-warn-tint:#3e3421; --pl-dark-warn-line:#6f501b; --pl-dark-warn-ink:var(--pl-anchor-warn-soft);

  /* Print is a different medium: the paper is white and the ink is black
     whatever the screen palette says, so the dark band inverts to these two
     rather than to a screen role. */
  --pl-paper:#ffffff;
  --pl-ink:#000000;

  /* --- type ------------------------------------------------------------
     Product stack, extended upward. The product tops out at 26px because a
     control interface has no display type; a page needs four more steps. */

  --pl-font: "Inter", "SF Pro Text", -apple-system, BlinkMacSystemFont,
             "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --pl-font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono",
                  Menlo, Consolas, monospace;

  --pl-fs-micro: 12px;
  --pl-fs-xs:    12px;
  --pl-fs-sm:    13.5px;
  --pl-fs-md:    15px;
  --pl-fs-lg:    17px;
  --pl-fs-xl:    20px;
  --pl-fs-2xl:   clamp(22px, 2.4vw, 26px);
  --pl-fs-3xl:   clamp(26px, 3.2vw, 34px);
  --pl-fs-4xl:   clamp(32px, 4.6vw, 46px);
  --pl-fs-5xl:   clamp(36px, 5.6vw, 58px);

  --pl-fw-regular:400; --pl-fw-medium:500; --pl-fw-semi:600; --pl-fw-bold:700;
  --pl-lh-tight:1.14; --pl-lh-snug:1.32; --pl-lh-base:1.62;
  --pl-track-eyebrow:0.11em; --pl-track-title:-0.021em;

  /* --- space: product 4px scale, plus page-level steps ----------------- */

  --pl-s-1:2px;  --pl-s-2:4px;  --pl-s-3:6px;  --pl-s-4:8px;  --pl-s-5:12px;
  --pl-s-6:16px; --pl-s-7:20px; --pl-s-8:24px; --pl-s-9:32px; --pl-s-10:40px;
  --pl-s-11:56px; --pl-s-12:72px; --pl-s-13:96px; --pl-s-14:128px;

  --pl-gutter:24px;
  --pl-max:1120px;
  --pl-max-prose:72ch;
  --pl-nav-h:60px;
  --pl-subnav-h:40px;

  /* --- radii + elevation, verbatim from product ------------------------ */

  --pl-r-xs:4px; --pl-r-sm:6px; --pl-r-md:10px; --pl-r-lg:14px;
  --pl-r-xl:20px; --pl-r-pill:999px;

  --pl-e-1: 0 1px 1px rgba(37,42,49,0.04), 0 1px 3px rgba(37,42,49,0.05);
  --pl-e-2: 0 1px 2px rgba(37,42,49,0.05), 0 4px 12px rgba(37,42,49,0.07);
  --pl-e-3: 0 2px 4px rgba(37,42,49,0.06), 0 10px 28px rgba(37,42,49,0.10);
  --pl-e-4: 0 4px 8px rgba(37,42,49,0.08), 0 20px 48px rgba(37,42,49,0.14);

  /* --- motion, verbatim from product ----------------------------------- */

  --pl-t-fast:120ms; --pl-t-base:180ms; --pl-t-slow:260ms;
  /* Not part of the product's scale: the live-status heartbeat, which is a
     rhythm rather than a response to an interaction, so it is slower than
     anything in it by an order of magnitude. */
  --pl-t-pulse:2400ms;
  --pl-ease:       cubic-bezier(0.2, 0.8, 0.2, 1);
  --pl-ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  :root { --pl-t-fast:1ms; --pl-t-base:1ms; --pl-t-slow:1ms; }
  html { scroll-behavior:auto !important; }
  *, *::before, *::after { animation-duration:1ms !important; transition-duration:1ms !important; }
}

/* ------------------------------------------------------------------ dark mode
   The system preference, and nothing else: no toggle, and therefore no storage,
   no flash-prevention script and no control in the header to explain. A toggle
   is the version of this feature that needs JavaScript before the first paint.

   Almost all of it is this remap. The dark palette was not invented for it — it
   is the product's dark theme, already on the page as the band treatment and
   already measured against every background it can land on. What changes here is
   which semantic role points at which of those values, which is why the whole
   mode costs one block of var() plus a short list of exceptions down in the
   component layer: the things a remap cannot express (a band that inverts, a
   shadow that needs a surface behind it, a near-white tint, a light screenshot).

   --pl-invert, --pl-accent and --pl-accent-fill are deliberately not remapped.
   The first is the ink on a filled accent button and on a dark band, which is
   white in both modes; the other two are the saturated brand hue, which is the
   same hue on either canvas. Neither is a background for body text.

   Every pair this produces is re-measured in check.py against the 4.5:1 floor,
   under the remap rather than as written — 4.53:1 at worst, on the page mesh.
   Forced colours are a separate question and win over this block: that mode
   substitutes the system palette for both palettes, so there is nothing here it
   needs to know about. */
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --pl-canvas:       var(--pl-dark);
    --pl-surface:      var(--pl-dark-surface);
    --pl-surface-sunk: var(--pl-dark-sunk);
    --pl-surface-2:    var(--pl-dark-raised);
    --pl-mesh-a:       var(--pl-dark-mesh-a);
    --pl-mesh-b:       var(--pl-dark-mesh-b);

    --pl-text:         var(--pl-dark-text);
    --pl-text-strong:  var(--pl-anchor-n-00);
    --pl-text-muted:   var(--pl-dark-muted);
    --pl-text-faint:   var(--pl-dark-faint);

    --pl-line:         var(--pl-dark-line);
    --pl-line-strong:  var(--pl-dark-line-strong);

    --pl-accent-deep:  var(--pl-dark-accent);
    --pl-accent-tint:  var(--pl-dark-well);

    /* The ring's inner band is the canvas, so it follows the remap on its own.
       The outer band cannot: --pl-anchor-run is 3.35:1 against --pl-dark, which
       clears the 3:1 a focus indicator is held to but is the dimmest thing on
       the page at the moment it matters most. */
    --pl-focus-ring: 0 0 0 2px var(--pl-canvas), 0 0 0 4px var(--pl-dark-accent);

    /* Elevation. All four steps are black at low alpha, which is a way of
       saying "darken what is behind this", and on a near-black canvas there is
       nothing left to darken — the two small steps are invisible rather than
       subtle. Dark mode carries small elevations on surface lightness and a
       hairline instead, both of which the remap above already provides, so the
       first two steps are dropped. The two large steps stay because what they
       do is different in kind: a sticky header, a menu panel and a demo frame
       have to read as being *in front of* the page, and black at high alpha
       still does that on dark. */
    --pl-e-1: none;
    --pl-e-2: none;
    --pl-e-3: 0 2px 6px rgba(0,0,0,0.50), 0 12px 32px rgba(0,0,0,0.55);
    --pl-e-4: 0 4px 10px rgba(0,0,0,0.55), 0 24px 56px rgba(0,0,0,0.60);
  }
}

/* ---------------------------------------------------------------- LAYER 2
   Components. If a hex value or a pixel radius appears below this line, it
   belongs in Layer 1 instead.
   ========================================================================= */

*, *::before, *::after { box-sizing:border-box; }

html { scroll-behavior:smooth; scroll-padding-top:calc(var(--pl-nav-h) + var(--pl-s-8)); }
/* build.py puts this on <html> for a page that carries the second row. */
html.pl-chrome--two { scroll-padding-top:calc(var(--pl-nav-h) + var(--pl-subnav-h) + var(--pl-s-8)); }

body {
  margin:0;
  font-family:var(--pl-font);
  font-size:var(--pl-fs-md);
  line-height:var(--pl-lh-base);
  color:var(--pl-text);
  background:var(--pl-canvas);
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  font-variant-numeric:tabular-nums;
}

/* The canvas is not flat. Two very low-alpha colour fields sit behind
   everything — the same two hues, at the same angles, as the product canvas. */
body::before {
  content:''; position:fixed; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(900px 620px at 12% -8%,  var(--pl-mesh-a), transparent 62%),
    radial-gradient(820px 560px at 96% 104%, var(--pl-mesh-b), transparent 58%);
}
body > * { position:relative; z-index:1; }

:focus-visible { outline:none; box-shadow:var(--pl-focus-ring); border-radius:var(--pl-r-sm); }

img { max-width:100%; height:auto; }

a { color:var(--pl-accent-deep); text-decoration:none; }
a:hover { text-decoration:underline; text-underline-offset:2px; }

/* Links inside running text are underlined always, not only on hover. The
   accent sits at 1.22:1 against muted body text, so to a reader who does not
   separate those two hues there is nothing marking the link at all, and
   hover does not exist on a phone (WCAG 1.4.1).

   Scoped to prose containers on purpose: nav items, buttons, cards and
   crumbs are recognisable as controls by shape and position, and underlining
   them would be noise. Anything carrying .pl-btn is a button wherever it
   happens to sit.

   .pl-steps li joined that list on 2026-10-03, found by measurement rather
   than by reading: Lighthouse scored
   /notes/niagara-poll-rates-and-tuning-policies/ at 95 on accessibility for
   one link-in-text-block failure, 1.39:1 between #5b41d6 and #404854 where
   axe wants 3:1. A step body is running text that happens to live in a bare
   div rather than a p, so the selector above missed it. Three pages carried
   it. The same run failed /work/decoder-upstream/ at 1.22:1 for a link in a
   table cell, so td joined too. Reverting both selectors on a scratch copy
   and running the gate reports what the old list left unmarked: ten links on
   ten pages, seven in cells and three in step bodies. A hand-rolled sweep
   first reported only the seven, because it did not count a step body as a
   block of its own - which is the argument for the gate rather than the
   sweep. */
:is(p, dd, figcaption, blockquote) a:not(.pl-btn):not(.pl-brand):not(.pl-contact-email),
.pl-prose li a,
.pl-srcs li a,
.pl-steps li a:not(.pl-btn),
td a:not(.pl-btn) {
  text-decoration:underline; text-underline-offset:2px;
  text-decoration-thickness:from-font;
}

code, kbd, samp {
  font-family:var(--pl-font-mono); font-size:0.875em;
  background:var(--pl-surface-sunk); border:1px solid var(--pl-line);
  border-radius:var(--pl-r-xs); padding:1px 5px; color:var(--pl-anchor-n-90);
  overflow-wrap:break-word;
}
/* A block of code keeps the inline look but drops the per-token box: the
   block is the box, and it scrolls sideways rather than wrapping a command. */
pre { background:var(--pl-surface-sunk); border:1px solid var(--pl-line);
  border-radius:var(--pl-r-sm); padding:var(--pl-s-6) var(--pl-s-7);
  overflow-x:auto; line-height:1.5; }
pre code { background:none; border:0; padding:0; font-size:0.85em; }

hr { border:0; border-top:1px solid var(--pl-line); margin:var(--pl-s-11) 0; }

::selection { background:var(--pl-anchor-run-soft); color:var(--pl-anchor-n-99); }

/* ------------------------------------------------------------- utilities */

.pl-wrap { max-width:var(--pl-max); margin-inline:auto; padding-inline:var(--pl-gutter); }
.pl-prose { max-width:var(--pl-max-prose); }
.pl-center { text-align:center; margin-inline:auto; }
.pl-skip {
  position:absolute; left:var(--pl-s-5); top:calc(-1 * var(--pl-s-14));
  background:var(--pl-surface); color:var(--pl-text-strong);
  padding:var(--pl-s-4) var(--pl-s-7); border-radius:var(--pl-r-sm);
  box-shadow:var(--pl-e-3); z-index:100; transition:top var(--pl-t-base) var(--pl-ease);
}
.pl-skip:focus { top:var(--pl-s-5); }


/* ------------------------------------------------------------- type roles */

.pl-eyebrow {
  margin:0 0 var(--pl-s-5); font-size:var(--pl-fs-micro); font-weight:var(--pl-fw-semi);
  letter-spacing:var(--pl-track-eyebrow); text-transform:uppercase; color:var(--pl-accent-deep);
  display:flex; align-items:center; gap:var(--pl-s-4);
}
.pl-eyebrow::before {
  content:''; width:18px; height:2px; border-radius:var(--pl-r-pill);
  background:var(--pl-accent); flex:none;
}

h1,h2,h3,h4 { color:var(--pl-text-strong); letter-spacing:var(--pl-track-title); margin:0; }
h1 { font-size:var(--pl-fs-5xl); line-height:var(--pl-lh-tight); font-weight:var(--pl-fw-bold); }
h2 { font-size:var(--pl-fs-3xl); line-height:var(--pl-lh-snug); font-weight:var(--pl-fw-bold); }
h3 { font-size:var(--pl-fs-xl); line-height:var(--pl-lh-snug); font-weight:var(--pl-fw-semi); letter-spacing:-.012em; }
h4 { font-size:var(--pl-fs-md); line-height:var(--pl-lh-snug); font-weight:var(--pl-fw-semi); letter-spacing:0; }

.pl-lede { font-size:var(--pl-fs-lg); color:var(--pl-text-muted); max-width:var(--pl-max-prose); margin:var(--pl-s-6) 0 0; }
.pl-lede strong { color:var(--pl-text-strong); font-weight:var(--pl-fw-semi); }
.pl-sub { color:var(--pl-text-muted); max-width:var(--pl-max-prose); margin:var(--pl-s-5) 0 0; }

/* A card's last line: the commercial shape of the thing the card describes —
   turnaround, or how it is priced. Quieter and smaller than the body it follows,
   and separated from it, so the card reads as description then terms. */
.pl-fine { color:var(--pl-text-muted); font-size:var(--pl-fs-sm); margin:var(--pl-s-5) 0 0; }

.pl-body p { margin:0 0 var(--pl-s-7); max-width:var(--pl-max-prose); }
.pl-body p:last-child { margin-bottom:0; }
.pl-body ul, .pl-body ol { margin:0 0 var(--pl-s-7); padding-left:var(--pl-s-7); max-width:var(--pl-max-prose); }
.pl-body li { margin-bottom:var(--pl-s-3); }
.pl-body li::marker { color:var(--pl-accent); }

/* ------------------------------------------------------------------- nav */

/* The bar is sticky, so everything on the page scrolls under it. At 88% the
   canvas showed through: headings arrived through the bar as ghosts and the
   brand sat on top of whatever paragraph happened to be passing. A blur is a
   nicety that needs a compositor; opacity is the thing that has to hold. So
   the base is opaque, and the translucency is layered back on only where the
   browser confirms it can actually blur what is behind. */
.pl-nav {
  position:sticky; top:0; z-index:40; height:var(--pl-nav-h);
  background:var(--pl-canvas);
  border-bottom:1px solid var(--pl-line);
}
@supports ((backdrop-filter:blur(12px)) or (-webkit-backdrop-filter:blur(12px))) {
  .pl-nav {
    background:color-mix(in srgb, var(--pl-canvas) 94%, transparent);
    backdrop-filter:saturate(180%) blur(14px);
    -webkit-backdrop-filter:saturate(180%) blur(14px);
  }
}
.pl-nav__inner { display:flex; align-items:center; gap:var(--pl-s-8); height:100%; }
.pl-nav__links {
  display:flex; align-items:center; gap:var(--pl-s-2); margin-left:auto;
  list-style:none; padding:0;
  /* Between the breakpoints the row can come out wider than the space left by
     the brand and the button. Clipped, it silently loses its last item; this
     way it scrolls, and site.js keeps the ink bar with it. */
  max-width:100%; overflow-x:auto; scrollbar-width:none;
}
.pl-nav__links::-webkit-scrollbar { display:none; }
/* nowrap: "Free tools" and "Niagara 5" were breaking across two lines inside a
   row whose height is fixed at 60px, which is how the bar looked at every width
   between 700px and 860px. */
.pl-nav__links a { white-space:nowrap;
  display:block; padding:var(--pl-s-3) var(--pl-s-5); border-radius:var(--pl-r-sm);
  font-size:var(--pl-fs-sm); font-weight:var(--pl-fw-medium); color:var(--pl-text-muted);
  transition:color var(--pl-t-fast) var(--pl-ease), background var(--pl-t-fast) var(--pl-ease);
}
.pl-nav__links a:hover { color:var(--pl-text-strong); background:var(--pl-surface-sunk); text-decoration:none; }
.pl-nav__links a[aria-current="page"] { color:var(--pl-accent-deep); background:var(--pl-accent-tint); }

/* ------------------------------------------------------ narrow-screen menu

   The row above sheds links as the viewport narrows. Below 700px that leaves
   the first item and the quote button, so this is where the rest of the site
   lives on a phone. A <details>, so it opens and closes with no script and
   keeps working if site.js never loads. */

.pl-menu { display:none; position:relative; margin-left:var(--pl-s-4); }
.pl-menu > summary {
  display:inline-flex; align-items:center; gap:var(--pl-s-4);
  min-height:44px; padding:0 var(--pl-s-5); border-radius:var(--pl-r-sm);
  font-size:var(--pl-fs-xs); font-weight:var(--pl-fw-medium);
  color:var(--pl-text-muted); cursor:pointer; list-style:none;
  border:1px solid var(--pl-line-strong); background:var(--pl-surface);
}
.pl-menu > summary::-webkit-details-marker { display:none; }
.pl-menu > summary:focus-visible { outline:2px solid var(--pl-accent); outline-offset:2px; }
.pl-menu[open] > summary { color:var(--pl-text-strong); background:var(--pl-surface-sunk); }

/* Three bars, drawn rather than shipped as an icon font or an extra request. */
.pl-menu__bars, .pl-menu__bars::before, .pl-menu__bars::after {
  display:block; width:15px; height:1.5px; border-radius:var(--pl-r-pill); background:currentColor;
}
.pl-menu__bars { position:relative; }
.pl-menu__bars::before, .pl-menu__bars::after { content:''; position:absolute; left:0; }
.pl-menu__bars::before { top:-5px; }
.pl-menu__bars::after  { top:5px; }

.pl-menu__panel {
  position:absolute; top:calc(100% + 8px); right:0; z-index:50;
  /* Wide enough to read, never wider than the screen: this hangs off the
     right-hand end of a row that already sits inside the page gutter, so an
     unbounded min-width is horizontal page scroll on a phone. */
  min-width:210px; max-width:calc(100vw - 2 * var(--pl-gutter));
  margin:0; padding:var(--pl-s-3); list-style:none;
  background:var(--pl-canvas); border:1px solid var(--pl-line-strong);
  border-radius:var(--pl-r-lg); box-shadow:var(--pl-e-3);
}
.pl-menu__panel a {
  display:block; padding:var(--pl-s-5) var(--pl-s-6); border-radius:var(--pl-r-sm);
  font-size:var(--pl-fs-md); font-weight:var(--pl-fw-medium); color:var(--pl-text);
}
.pl-menu__panel a:hover { background:var(--pl-surface-sunk); text-decoration:none; }
.pl-menu__panel a[aria-current="page"] { color:var(--pl-accent-deep); background:var(--pl-accent-tint); }

/* ----------------------------------------------------------------- brand */

.pl-brand { display:inline-flex; align-items:center; gap:var(--pl-s-5); color:var(--pl-text-strong); }
.pl-brand:hover { text-decoration:none; }
.pl-brand__mark { flex:none; display:block; }
.pl-brand__name { font-size:var(--pl-fs-md); font-weight:var(--pl-fw-semi); letter-spacing:-.015em; line-height:1.1; }
.pl-brand__name span { display:block; font-size:var(--pl-fs-micro); font-weight:var(--pl-fw-medium);
  letter-spacing:var(--pl-track-eyebrow); text-transform:uppercase; color:var(--pl-text-faint); }

/* --------------------------------------------------------------- buttons */

.pl-btn {
  display:inline-flex; align-items:center; justify-content:center; gap:var(--pl-s-4);
  padding:var(--pl-s-5) var(--pl-s-8); border-radius:var(--pl-r-sm);
  font-size:var(--pl-fs-sm); font-weight:var(--pl-fw-semi); font-family:inherit;
  border:1px solid transparent; cursor:pointer; text-decoration:none; white-space:nowrap;
  transition:transform var(--pl-t-fast) var(--pl-ease), box-shadow var(--pl-t-fast) var(--pl-ease),
             background var(--pl-t-fast) var(--pl-ease), border-color var(--pl-t-fast) var(--pl-ease);
}
.pl-btn:hover { text-decoration:none; transform:translateY(-1px); }
.pl-btn:active { transform:translateY(0); }
.pl-btn--primary { background:var(--pl-accent-fill); color:var(--pl-invert); box-shadow:var(--pl-e-2); }
.pl-btn--primary:hover { background:var(--pl-accent-deep); box-shadow:var(--pl-e-3); }
.pl-btn--ghost { background:var(--pl-surface); color:var(--pl-text-strong); border-color:var(--pl-line-strong); box-shadow:var(--pl-e-1); }
.pl-btn--ghost:hover { border-color:var(--pl-accent); color:var(--pl-accent-deep); }
.pl-btn--on-dark { background:var(--pl-surface); color:var(--pl-anchor-n-95); }
.pl-btn--on-dark:hover { background:var(--pl-anchor-n-15); }
.pl-btn--quiet-dark { background:transparent; color:var(--pl-dark-text); border-color:var(--pl-dark-line); }
.pl-btn--quiet-dark:hover { border-color:var(--pl-dark-accent); color:var(--pl-invert); }
.pl-btn-row { display:flex; flex-wrap:wrap; gap:var(--pl-s-5); margin-top:var(--pl-s-9); }

/* ----------------------------------------------------------------- chips */

.pl-chips { display:flex; flex-wrap:wrap; gap:var(--pl-s-4); list-style:none; padding:0; margin:var(--pl-s-9) 0 0; }
.pl-chip {
  font-size:var(--pl-fs-xs); font-weight:var(--pl-fw-medium); color:var(--pl-text-muted);
  background:var(--pl-surface); border:1px solid var(--pl-line);
  border-radius:var(--pl-r-pill); padding:var(--pl-s-3) var(--pl-s-6); box-shadow:var(--pl-e-1);
}
.pl-chip--on-dark { background:var(--pl-dark-chip); border-color:var(--pl-dark-line); color:var(--pl-dark-muted); box-shadow:none; }
.pl-chip--ok   { color:var(--pl-ok-ink);    background:var(--pl-anchor-ok-tint);   border-color:var(--pl-anchor-ok-soft); }
.pl-chip--run  { color:var(--pl-run-ink);   background:var(--pl-anchor-run-tint);  border-color:var(--pl-anchor-run-soft); }
.pl-chip--warn { color:var(--pl-warn-ink);  background:var(--pl-anchor-warn-tint); border-color:var(--pl-anchor-warn-soft); }
.pl-chip--bad  { color:var(--pl-anchor-bad-deep); background:var(--pl-anchor-bad-tint); border-color:var(--pl-anchor-bad-soft); }
.pl-chip--info { color:var(--pl-info-ink);  background:var(--pl-anchor-info-tint); border-color:var(--pl-anchor-info-soft); }

/* ------------------------------------------------- themes per kind of work

   The practice does four different kinds of job and they used to arrive in one
   colour, which made the home page read as one specialism with three footnotes.
   Each kind gets an anchor from the palette already in this file — no new hues,
   so every pairing here is one of the contrast-checked --pl-*-ink combinations
   above rather than a fresh guess. A theme class sets three variables and the
   components downstream read those instead of the global accent. */
.pl-theme--products  { --pl-theme-ink:var(--pl-info-ink); --pl-theme-tint:var(--pl-anchor-info-tint); --pl-theme-soft:var(--pl-anchor-info-soft); }
.pl-theme--ops       { --pl-theme-ink:var(--pl-ok-ink);   --pl-theme-tint:var(--pl-anchor-ok-tint);   --pl-theme-soft:var(--pl-anchor-ok-soft); }
.pl-theme--automation{ --pl-theme-ink:var(--pl-warn-ink); --pl-theme-tint:var(--pl-anchor-warn-tint); --pl-theme-soft:var(--pl-anchor-warn-soft); }
.pl-theme--bas       { --pl-theme-ink:var(--pl-run-ink);  --pl-theme-tint:var(--pl-anchor-run-tint);  --pl-theme-soft:var(--pl-anchor-run-soft); }

/* The pillar card: a themed rule along the top, a themed eyebrow, and chips
   that pick the theme up instead of sitting in default grey. */










/* A themed band: the section eyebrow takes the accent of the work it is about. */
.pl-section.pl-theme--bas .pl-eyebrow,
.pl-section.pl-theme--ops .pl-eyebrow,
.pl-section.pl-theme--products .pl-eyebrow,
.pl-section.pl-theme--automation .pl-eyebrow { color:var(--pl-theme-ink); }

/* -------------------------------------------------------------- sections */

.pl-section { padding-block:var(--pl-s-13); }
.pl-section--tight { padding-block:var(--pl-s-11); }
.pl-section--sunk { background:var(--pl-surface-sunk); border-block:1px solid var(--pl-line); }
.pl-section__head { margin-bottom:var(--pl-s-10); }

/* A dark band. Same palette the widgets use in dark theme — this is how the
   page quotes the product rather than merely linking to pictures of it. */
.pl-band {
  position:relative; background:var(--pl-dark); color:var(--pl-dark-text);
  padding-block:var(--pl-s-13); overflow:hidden;
}
.pl-band::before {
  content:''; position:absolute; inset:0; pointer-events:none;
  background:
    radial-gradient(760px 520px at 12% -8%,  var(--pl-dark-mesh-a), transparent 62%),
    radial-gradient(680px 480px at 96% 104%, var(--pl-dark-mesh-b), transparent 58%);
}
.pl-band > * { position:relative; }
.pl-band h1, .pl-band h2, .pl-band h3, .pl-band h4 { color:var(--pl-invert); }
.pl-band .pl-lede, .pl-band .pl-sub, .pl-band p { color:var(--pl-dark-muted); }
.pl-band .pl-lede strong { color:var(--pl-invert); }
.pl-band .pl-eyebrow { color:var(--pl-dark-accent); }
.pl-band .pl-eyebrow::before { background:var(--pl-dark-accent); }
/* :not(.pl-btn) because a button already carries its own pair. Without it
   this rule won on specificity and painted every .pl-btn--on-dark label
   #9b85ff on its white pill — 2.92:1, the worst contrast on the site, and
   on the primary call to action of three pages. axe does not catch it:
   the button sits over a mesh gradient, so it gives up on the background
   and skips the check rather than failing it. */
.pl-band a:not(.pl-btn) { color:var(--pl-dark-accent); }
.pl-band code { background:var(--pl-dark-code); border-color:var(--pl-dark-line); color:var(--pl-dark-text); }

/* ------------------------------------------------------------------- hero */

.pl-hero { padding-block:var(--pl-s-14) var(--pl-s-13); }
.pl-hero h1 { max-width:19ch; }
.pl-hero--page { padding-block:var(--pl-s-12) var(--pl-s-11); }
.pl-hero--page h1 { font-size:var(--pl-fs-4xl); max-width:24ch; }
.pl-hero__meta { margin-top:var(--pl-s-7); font-size:var(--pl-fs-xs); color:var(--pl-dark-faint); }

/* ------------------------------------------------------------------ chain
   The hero diagram: one job from sensor to interface, in two weights. Filled
   is ours, outlined is another bench's. It only ever renders inside the dark
   hero band, so the fills and strokes are the --pl-dark-* tokens directly
   rather than the mode-switching ones. */

.pl-chain { margin:var(--pl-s-10) 0 0; }
.pl-chain__svg { display:block; width:100%; height:auto; overflow:visible; }
.pl-chain__name { fill:var(--pl-dark-text); font-size:15px; font-weight:var(--pl-fw-semi); }
.pl-chain__sub  { fill:var(--pl-dark-muted); font-size:11px; }
.pl-chain figcaption {
  margin-top:var(--pl-s-7); color:var(--pl-dark-muted);
  font-size:var(--pl-fs-sm); max-width:70ch;
}
/* The plain-text twin, hidden until the diagram stops being legible. */
.pl-chain__text { display:none; }

@media (max-width:760px) {
  /* The viewBox is ~1,090 units wide. Scaled into a phone that puts the 11px
     sub-labels at four pixels — a diagram nobody can read is a decoration, so
     below this width the sentence replaces it rather than sitting beside it. */
  .pl-chain__svg { display:none; }
  .pl-chain__text {
    display:block; margin:0; color:var(--pl-dark-text); font-size:var(--pl-fs-sm);
    line-height:var(--pl-lh-base);
  }
  .pl-chain__text strong { color:var(--pl-invert); font-weight:var(--pl-fw-semi); }
  .pl-chain__key { color:var(--pl-dark-muted); font-size:var(--pl-fs-xs); }
}

/* ------------------------------------------------------------ breadcrumbs */

.pl-crumbs { font-size:var(--pl-fs-xs); color:var(--pl-text-faint); margin-bottom:var(--pl-s-7); }
.pl-crumbs ol { list-style:none; display:flex; flex-wrap:wrap; gap:var(--pl-s-4); margin:0; padding:0; }
.pl-crumbs li + li::before { content:'/'; margin-right:var(--pl-s-4); color:var(--pl-line-strong); }
.pl-crumbs a { color:var(--pl-text-muted); }
.pl-band .pl-crumbs, .pl-band .pl-crumbs a { color:var(--pl-dark-faint); }
.pl-band .pl-crumbs a:hover { color:var(--pl-dark-text); }

/* ------------------------------------------------------------------ grid */

/* min(Npx, 100%) rather than a bare Npx floor. A bare floor is a minimum the
   column keeps even when the container is narrower than it, which is how
   /services/ came to scroll 13px sideways at 320px: a 300px card floor inside a
   269px content box. min() lets the floor collapse to the container on the
   smallest screens and behave as a floor everywhere else. */
.pl-grid { display:grid; gap:var(--pl-s-7); }
.pl-grid--2 { grid-template-columns:repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }
.pl-grid--3 { grid-template-columns:repeat(auto-fit, minmax(min(268px, 100%), 1fr)); }
.pl-grid--4 { grid-template-columns:repeat(auto-fit, minmax(min(212px, 100%), 1fr)); }

/* ------------------------------------------------------------------ card */

.pl-card {
  background:var(--pl-surface); border:1px solid var(--pl-line);
  border-radius:var(--pl-r-lg); padding:var(--pl-s-8);
  box-shadow:var(--pl-e-1); position:relative;
  transition:box-shadow var(--pl-t-base) var(--pl-ease),
             border-color var(--pl-t-base) var(--pl-ease),
             transform var(--pl-t-base) var(--pl-ease);
}
.pl-card p { color:var(--pl-text-muted); font-size:var(--pl-fs-sm); margin:var(--pl-s-5) 0 0; }
/* h2 on /services/, h3 elsewhere — see service_cards() in build.py. */
.pl-card :is(h2,h3) { font-size:var(--pl-fs-lg); font-weight:var(--pl-fw-semi); letter-spacing:-.012em; }
.pl-card--link:hover { border-color:var(--pl-anchor-run-soft); box-shadow:var(--pl-e-3); transform:translateY(-2px); }
/* A card is a link, and until 2026-09-30 the only thing saying so was hover:
   a lift, a border colour and a shadow, none of which exists on a touch screen
   and none of which survives a user stylesheet. Prose links here are always
   underlined for exactly that reason (WCAG 1.4.1 — never colour alone, and by
   extension never hover alone); the cards were missed.
   The cue is a chevron drawn from two borders in currentColor rather than a glyph
   or an image: it is a shape, so it does not depend on colour, it inherits the
   heading's ink in forced-colors mode, and it announces nothing to a screen
   reader, which already has the <a> and does not need to hear "right arrow".
   The 33 cards that say it in words already — "Read more" on a service or note
   card, "Run them" / "Read the method" on a case card on /work/ — do not get a
   second one. In an engine without :has() (pre-Chromium 105) those
   cards show both, which is a duplicate affordance, not a broken card. */
.pl-card--link::after {
  content:''; position:absolute; top:var(--pl-s-8); right:var(--pl-s-8);
  width:.34em; height:.34em; font-size:var(--pl-fs-lg); color:var(--pl-text-muted);
  border-right:2px solid currentColor; border-bottom:2px solid currentColor;
  transform:rotate(-45deg); transition:transform var(--pl-t-base) var(--pl-ease);
}
/* It sits in the corner rather than after the heading text, where an inline
   marker can wrap onto a line of its own and read as a stray tick. The heading
   reserves the width so a long title cannot run under it. */
.pl-card--link :is(h2,h3) { padding-right:1.4em; }
.pl-card--link:has(.pl-card__more)::after { content:none; }
.pl-card--link:has(.pl-card__more) :is(h2,h3) { padding-right:0; }
.pl-card--link:hover::after { transform:translateX(2px) rotate(-45deg); }
.pl-card--link a::after { content:''; position:absolute; inset:0; border-radius:inherit; }
.pl-card--link a { color:inherit; }
.pl-card--link a:hover { text-decoration:none; }
.pl-card__more {
  display:inline-flex; align-items:center; gap:var(--pl-s-3); margin-top:var(--pl-s-7);
  font-size:var(--pl-fs-sm); font-weight:var(--pl-fw-semi); color:var(--pl-accent-deep);
}
.pl-card__more::after { content:'\2192'; transition:transform var(--pl-t-base) var(--pl-ease); }
.pl-card--link:hover .pl-card__more::after { transform:translateX(3px); }
.pl-band .pl-card { background:var(--pl-dark-surface); border-color:var(--pl-dark-line); box-shadow:none; }
.pl-band .pl-card--link::after { color:var(--pl-dark-faint); }
.pl-band .pl-card p { color:var(--pl-dark-muted); }

/* card icon — a 36px tinted tile holding a stroked glyph */
.pl-card__icon {
  width:38px; height:38px; border-radius:var(--pl-r-md); display:grid; place-items:center;
  background:var(--pl-accent-tint); color:var(--pl-accent-deep);
  border:1px solid var(--pl-anchor-run-soft); margin-bottom:var(--pl-s-7);
}
.pl-card__icon svg { width:20px; height:20px; display:block; }
.pl-band .pl-card__icon { background:var(--pl-dark-well); border-color:var(--pl-dark-well-line); color:var(--pl-dark-accent); }

/* ------------------------------------------------------------------ spec
   A definition table. Used for "what you get", scope and readiness matrices —
   the places where a buyer is scanning for a specific answer, not reading. */

.pl-spec { width:100%; border-collapse:collapse; font-size:var(--pl-fs-sm); }
.pl-spec caption { text-align:left; color:var(--pl-text-faint); font-size:var(--pl-fs-xs); padding-bottom:var(--pl-s-5); }
.pl-spec th, .pl-spec td { text-align:left; padding:var(--pl-s-6) var(--pl-s-5); border-bottom:1px solid var(--pl-line); vertical-align:top; }
.pl-spec thead th {
  color:var(--pl-text-faint); font-size:var(--pl-fs-micro); font-weight:var(--pl-fw-semi);
  letter-spacing:var(--pl-track-eyebrow); text-transform:uppercase; padding-block:var(--pl-s-4);
}
.pl-spec tbody th { color:var(--pl-text-strong); font-weight:var(--pl-fw-semi); width:30%; padding-right:var(--pl-s-8); }
.pl-spec td { color:var(--pl-text-muted); }
.pl-spec tbody tr:last-child th, .pl-spec tbody tr:last-child td { border-bottom:0; }
.pl-band .pl-spec th, .pl-band .pl-spec td { border-color:var(--pl-dark-line); }
.pl-band .pl-spec tbody th { color:var(--pl-invert); }
.pl-band .pl-spec td, .pl-band .pl-spec thead th { color:var(--pl-dark-muted); }

/* ------------------------------------------------------------------ note */

.pl-note {
  background:var(--pl-surface); border:1px solid var(--pl-line);
  border-left:3px solid var(--pl-accent); border-radius:0 var(--pl-r-md) var(--pl-r-md) 0;
  padding:var(--pl-s-7) var(--pl-s-8); color:var(--pl-text-muted); font-size:var(--pl-fs-sm);
  box-shadow:var(--pl-e-1);
}
.pl-note > :first-child { margin-top:0; } .pl-note > :last-child { margin-bottom:0; }
.pl-note strong { color:var(--pl-text-strong); }
.pl-note--warn { border-left-color:var(--pl-anchor-warn); }
.pl-note--info { border-left-color:var(--pl-anchor-info); }
.pl-band .pl-note { background:var(--pl-dark-raised); border-color:var(--pl-dark-line); border-left-color:var(--pl-dark-accent); box-shadow:none; }

/* ------------------------------------------------------------- attribution */

/* The "who did what" block on a case study. Visually a note, but a landmark
   rather than an aside: it carries the heading that says which bench built
   which part, and check.py refuses a case study that does not have one. */
.pl-attrib {
  background:var(--pl-surface); border:1px solid var(--pl-line);
  border-left:3px solid var(--pl-anchor-info); border-radius:0 var(--pl-r-md) var(--pl-r-md) 0;
  padding:var(--pl-s-8) var(--pl-s-9); box-shadow:var(--pl-e-1);
  max-width:var(--pl-max-prose);
}
.pl-attrib h2 { font-size:var(--pl-fs-lg); margin:0 0 var(--pl-s-5); }
.pl-attrib p { margin:0; color:var(--pl-text-muted); font-size:var(--pl-fs-sm); }
.pl-attrib code { font-size:0.95em; }

/* ----------------------------------------------------------------- steps */

.pl-steps { list-style:none; counter-reset:step; margin:0; padding:0; display:grid; gap:var(--pl-s-7); }
.pl-steps li { counter-increment:step; display:grid; grid-template-columns:auto 1fr; gap:var(--pl-s-7); align-items:start; }
.pl-steps li::before {
  content:counter(step, decimal-leading-zero);
  font-family:var(--pl-font-mono); font-size:var(--pl-fs-xs); font-weight:var(--pl-fw-semi);
  color:var(--pl-accent-deep); background:var(--pl-accent-tint);
  border:1px solid var(--pl-anchor-run-soft); border-radius:var(--pl-r-sm);
  width:30px; height:30px; display:grid; place-items:center; flex:none;
}
.pl-steps h3 { font-size:var(--pl-fs-md); margin-bottom:var(--pl-s-3); }
.pl-steps p { margin:0; color:var(--pl-text-muted); font-size:var(--pl-fs-sm); max-width:var(--pl-max-prose); }

/* --------------------------------------------------------------- sources */

/* A note's citation list. No numbers: nothing in the prose says "see source
   2", and numbering would imply a footnote mechanism this does not have. The
   rule down the left side is the same device .pl-note uses for an aside. */
.pl-srcs { list-style:none; margin:0; padding:0; display:grid; gap:var(--pl-s-6); max-width:var(--pl-max-prose); }
.pl-srcs li {
  padding-left:var(--pl-s-7); border-left:2px solid var(--pl-line);
  color:var(--pl-text-muted); font-size:var(--pl-fs-sm);
}
.pl-srcs a { color:var(--pl-text-strong); font-weight:var(--pl-fw-semi); }
/* Every one of these leaves the site, and a reader deciding whether to click
   deserves to know that before the click rather than after it. Drawn from the
   character rather than from a border, so it survives forced colours. */
.pl-srcs a::after { content:'\2009\2197'; text-decoration:none; }

/* ---------------------------------------------------------------- figure */

.pl-figure { margin:0; }
/* <picture> is inline by default; the img inside is display:block, so without
   this the wrapper shrink-wraps and --phone's margin-inline:auto stops
   centring. */
.pl-figure picture, .pl-demo__frame picture { display:block; }
.pl-figure img {
  display:block; width:100%; border:1px solid var(--pl-line);
  border-radius:var(--pl-r-md); background:var(--pl-surface); box-shadow:var(--pl-e-2);
}
.pl-figure figcaption { margin-top:var(--pl-s-6); color:var(--pl-text-muted); font-size:var(--pl-fs-sm); max-width:var(--pl-max-prose); }
.pl-figure figcaption strong { color:var(--pl-text-strong); font-weight:var(--pl-fw-semi); }
.pl-band .pl-figure img { border-color:var(--pl-dark-line); background:var(--pl-dark-surface); }
.pl-band .pl-figure figcaption { color:var(--pl-dark-muted); }
.pl-band .pl-figure figcaption strong { color:var(--pl-invert); }
.pl-figure--phone img { max-width:288px; margin-inline:auto; }

/* ------------------------------------------------------------------- CTA */

.pl-cta { text-align:center; }
.pl-cta h2 { max-width:20ch; margin-inline:auto; }
.pl-cta .pl-lede { margin-inline:auto; text-align:center; }
.pl-cta .pl-btn-row { justify-content:center; }

/* --------------------------------------------------------------- contact */

.pl-contact-email {
  font-family:var(--pl-font-mono); font-size:var(--pl-fs-lg); font-weight:var(--pl-fw-medium);
  color:var(--pl-text-strong); background:var(--pl-surface); border:1px solid var(--pl-line-strong);
  border-radius:var(--pl-r-sm); padding:var(--pl-s-5) var(--pl-s-7); display:inline-block;
  box-shadow:var(--pl-e-1);
}
.pl-contact-email:hover { border-color:var(--pl-accent); text-decoration:none; }
.pl-band .pl-contact-email { background:var(--pl-dark-raised); border-color:var(--pl-dark-line); color:var(--pl-invert); box-shadow:none; }

/* ------------------------------------------------------------------- FAQ */

.pl-faq { display:grid; gap:var(--pl-s-5); max-width:var(--pl-max-prose); }
.pl-faq details {
  background:var(--pl-surface); border:1px solid var(--pl-line);
  border-radius:var(--pl-r-md); box-shadow:var(--pl-e-1);
  transition:border-color var(--pl-t-base) var(--pl-ease);
}
.pl-faq details[open] { border-color:var(--pl-anchor-run-soft); }
.pl-faq summary {
  cursor:pointer; list-style:none; padding:var(--pl-s-7) var(--pl-s-8);
  font-weight:var(--pl-fw-semi); font-size:var(--pl-fs-md); color:var(--pl-text-strong);
  display:flex; justify-content:space-between; align-items:center; gap:var(--pl-s-7);
}
.pl-faq summary::-webkit-details-marker { display:none; }
.pl-faq summary::after {
  content:'+'; font-family:var(--pl-font-mono); font-size:var(--pl-fs-xl);
  color:var(--pl-accent); line-height:1; flex:none;
  transition:transform var(--pl-t-base) var(--pl-ease);
}
.pl-faq details[open] summary::after { transform:rotate(45deg); }
.pl-faq__body { padding:0 var(--pl-s-8) var(--pl-s-8); color:var(--pl-text-muted); font-size:var(--pl-fs-sm); }
.pl-faq__body > :first-child { margin-top:0; } .pl-faq__body > :last-child { margin-bottom:0; }
.pl-faq__body p { margin:0 0 var(--pl-s-5); }

/* ---------------------------------------------------------------- footer */

.pl-footer { background:var(--pl-dark); color:var(--pl-dark-muted); padding-block:var(--pl-s-12) var(--pl-s-10); font-size:var(--pl-fs-sm); }
.pl-footer__cols { display:grid; grid-template-columns:2fr 1fr 1fr; gap:var(--pl-s-10); }
/* h2, not h4: the footer's columns are top-level page sections, so an h4
   here skips two levels down from the page h1 and a screen reader announces
   a structure that does not exist. It is set small because it is a label,
   not because it is deep. */
.pl-footer h2 {
  color:var(--pl-dark-faint); font-size:var(--pl-fs-micro); letter-spacing:var(--pl-track-eyebrow);
  font-weight:var(--pl-fw-semi); line-height:var(--pl-lh-snug);
  text-transform:uppercase; margin-bottom:var(--pl-s-6);
}
.pl-footer ul { list-style:none; margin:0; padding:0; display:grid; gap:var(--pl-s-4); }
.pl-footer a { color:var(--pl-dark-muted); }
.pl-footer a:hover { color:var(--pl-invert); }
.pl-footer .pl-brand__name { color:var(--pl-invert); }
.pl-footer .pl-brand__name span { color:var(--pl-dark-faint); }
.pl-footer__legal {
  margin-top:var(--pl-s-11); padding-top:var(--pl-s-8); border-top:1px solid var(--pl-dark-line);
  color:var(--pl-dark-faint); font-size:var(--pl-fs-xs); display:flex; flex-wrap:wrap; gap:var(--pl-s-7);
  justify-content:space-between;
}
.pl-footer__legal p { margin:0; max-width:78ch; }

/* ----------------------------------------------------------- breakpoints */

@media (max-width:940px) {
  /* Small type, no gap between links. Measured: this is the regime in which the
     whole item list plus the call to action still fits, all the way down to the
     menu breakpoint. Above 940px the bar has room for the ordinary type. */
  .pl-nav__links { gap:0; }
  .pl-nav__links a { padding-inline:var(--pl-s-4); font-size:var(--pl-fs-xs); }
}
@media (max-width:860px) {
  .pl-footer__cols { grid-template-columns:1fr 1fr; }
}
@media (max-width:780px) {
  /* The one breakpoint. Above it, every link is in the bar at every width; at
     it and below, the bar keeps only the call to action and the Menu button is
     the complete route through the site. Nothing is hidden in between. */
  .pl-menu { display:block; }

  /* Everything except the call to action now lives in the menu, and leaving a
     stray link or two beside it only pushes the row wider than the screen. */
  .pl-nav__links > li:not(:last-child) { display:none; }

  /* Touch targets. WCAG 2.2 asks for 24px minimum and the platform guidelines
     for 44px; the header controls were 31-33px tall, which is neither. */
  .pl-nav__links a { min-height:40px; display:flex; align-items:center; }
  .pl-footer__cols a { display:inline-block; padding-block:6px; }
}
/* Wordmark, call to action and Menu come to within a pixel or two of the
   available width on a phone, and a header that does not fit is horizontal
   scroll on every page. Buy the room back from the gaps first. */
@media (max-width:430px) {
  .pl-nav__inner { gap:var(--pl-s-5); }
  .pl-menu { margin-left:0; }

  /* A button label that cannot wrap is horizontal scroll on the whole page as
     soon as it is wider than the content box: "How each one is scoped and
     priced" measures 298px in the 269px box a 320px phone has. Outside the
     header these wrap instead. The header's own call to action keeps nowrap -
     it is two short words, and a two-line button in a row whose height is fixed
     at 60px is the worse failure. */
  :where(main, .pl-footer) .pl-btn { white-space:normal; }
}
/* An iPhone SE or a folded phone's cover screen: the gaps are not enough and
   the wordmark has to go. Hidden rather than removed — the mark beside it is
   aria-hidden, so deleting the text leaves the home link with no name. */
@media (max-width:340px) {
  .pl-brand__name {
    position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0; overflow:hidden;
    clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap;
  }
}
@media (max-width:680px) {
  :root { --pl-gutter:18px; }
  .pl-section { padding-block:var(--pl-s-11); }
  .pl-hero { padding-block:var(--pl-s-11) var(--pl-s-10); }
  .pl-footer__cols { grid-template-columns:1fr; gap:var(--pl-s-9); }
  .pl-brand__name span { display:none; }
  .pl-spec tbody th, .pl-spec tbody td { display:block; width:auto; border-bottom:0; padding-block:var(--pl-s-2); }
  .pl-spec tbody tr { display:block; border-bottom:1px solid var(--pl-line); padding-block:var(--pl-s-5); }
  .pl-spec thead { display:none; }
}

/* ------------------------------------------------------ the second row */
/* A page long enough to have sections gets a row under the header pointing
   at them, with one indicator sliding between them as you scroll. site.js
   drives it; with JavaScript off the same links are plain anchors that still
   jump to the right place.

   This used to be the header: on the landing page the nav itself carried the
   section links, on the reasoning that two rows of same-looking links read
   as a mistake. They do, so this row is built not to look like that one -
   smaller type, no brand, no call to action, left-aligned under a
   right-aligned bar, and its own accessible name. What the merged version
   actually read as, reported after use on 2026-10-05, was a header that
   changes when you leave the root page, which is worse than a second row. */

.pl-subnav {
  position:sticky; top:var(--pl-nav-h); z-index:39; height:var(--pl-subnav-h);
  background:var(--pl-canvas);
  border-bottom:1px solid var(--pl-line);
}
@supports ((backdrop-filter:blur(12px)) or (-webkit-backdrop-filter:blur(12px))) {
  .pl-subnav {
    background:color-mix(in srgb, var(--pl-canvas) 94%, transparent);
    backdrop-filter:saturate(180%) blur(14px);
    -webkit-backdrop-filter:saturate(180%) blur(14px);
  }
}
.pl-subnav__inner { position:relative; display:flex; align-items:center; height:100%; }
.pl-subnav__links {
  display:flex; align-items:center; gap:var(--pl-s-2);
  list-style:none; padding:0; margin:0;
  /* Four labels fit a phone only by scrolling, and a clipped row silently
     loses its last section - the same reason .pl-nav__links scrolls. */
  max-width:100%; overflow-x:auto; scrollbar-width:none;
}
.pl-subnav__links::-webkit-scrollbar { display:none; }
.pl-subnav__links a {
  white-space:nowrap; display:block;
  padding:var(--pl-s-2) var(--pl-s-4); border-radius:var(--pl-r-sm);
  font-size:var(--pl-fs-xs); font-weight:var(--pl-fw-medium);
  color:var(--pl-text-muted);
  transition:color var(--pl-t-fast) var(--pl-ease);
}
.pl-subnav__links a:hover { color:var(--pl-text-strong); text-decoration:none; }
.pl-subnav__links a[aria-current="true"] { color:var(--pl-accent-deep); }
.pl-subnav__ink {
  position:absolute; bottom:-1px; left:0; height:2px; width:0;
  background:var(--pl-accent); border-radius:var(--pl-r-pill);
  opacity:0; transform:translateX(0);
  transition:transform var(--pl-t-slow) var(--pl-ease), width var(--pl-t-slow) var(--pl-ease),
             opacity var(--pl-t-base) var(--pl-ease);
}
.pl-subnav__ink.is-on { opacity:1; }

/* html already sets scroll-padding-top for the sticky header, so a tracked
   section only needs a little air above it beyond that. */
.pl-anchor { scroll-margin-top:var(--pl-s-6); }

@media (prefers-reduced-motion: reduce) {
  .pl-subnav__ink { transition:none; }
}

/* Dark mode, part two: the things the token remap in Layer 1 cannot express.
   Four were named when the mode was specified; two more turned up in the sweep
   for colours that are written as anchors rather than as roles, and an anchor is
   the same value in both palettes by definition. */
@media (prefers-color-scheme: dark) {

  /* 1. The band. "Inverted" has no meaning on a dark page — it would be the
     canvas colour. It becomes the one raised surface instead, a step up in
     lightness between two hairlines, and the cards, notes and figures inside it
     move one step further up so they still read as objects sitting on it. Its
     two radial meshes go: the page canvas carries those same two gradients in
     dark mode, and painting them a second time inside the band only muddies the
     single surface distinction the mode has. The footer is the other half of
     this — it is --pl-dark, which is now the canvas, so only a hairline
     separates it from the page. */
  .pl-band { background:var(--pl-surface); border-block:1px solid var(--pl-line); }
  .pl-band::before { display:none; }
  .pl-band .pl-card, .pl-band .pl-note,
  .pl-band .pl-contact-email, .pl-band .pl-figure img { background:var(--pl-surface-2); }
  .pl-footer { border-top:1px solid var(--pl-line); }

  /* 2. Status chips and the demo's error state, onto the dark tints. */
  .pl-chip--ok   { color:var(--pl-dark-ok-ink);   background:var(--pl-dark-ok-tint);   border-color:var(--pl-dark-ok-line); }
  .pl-chip--run  { color:var(--pl-dark-run-ink);  background:var(--pl-dark-run-tint);  border-color:var(--pl-dark-run-line); }
  .pl-chip--warn { color:var(--pl-dark-warn-ink); background:var(--pl-dark-warn-tint); border-color:var(--pl-dark-warn-line); }
  .pl-chip--info { color:var(--pl-dark-info-ink); background:var(--pl-dark-info-tint); border-color:var(--pl-dark-info-line); }
  [data-demo-status].is-error {
    color:var(--pl-dark-bad-ink); background:var(--pl-dark-bad-tint);
    border-color:var(--pl-dark-bad-line);
  }

  /* 3. A light-theme screenshot. Four of the six stills are captures of the
     widgets' dark theme and need nothing; two are the light theme, and a light
     rectangle butted straight against a near-black page is a glare with a hard
     edge. It gets a mat — the same answer a print shop gives, a neutral border
     of its own tone between the image and the page — rather than a filter over
     the image, because the image is evidence and dimming it is retouching it.
     The tag is set in build.py, from the capture, not guessed from the file name.
     The live demos are deliberately not tagged: their theme is a control the
     reader can flip, so a mat fixed at build time would be wrong half the time. */
  .pl-figure--light-ui img {
    background:var(--pl-anchor-n-30); border-color:var(--pl-anchor-n-40);
    padding:var(--pl-s-3);
  }

  /* 4. Inline code and <pre>. Their ink is written as --pl-anchor-n-90, an
     anchor rather than a role, so it stays a near-black on what is now a
     near-black background: the one change in this block that is invisible in a
     screenshot of the light page and unreadable in the dark one. */
  code, kbd, samp { color:var(--pl-text); }

  /* 5. The white pill button that sits on dark bands. Its fill is --pl-surface,
     which the remap turns dark, leaving dark ink on a dark pill. It is the
     primary call to action on three pages, so it is pinned to the anchor it
     always meant — white — rather than to the role. */
  .pl-btn--on-dark { background:var(--pl-anchor-n-00); }

  /* 6. Four hairlines and three glyphs written as light-palette anchors: the
     pale lavender edge reads as a highlight on dark, and --pl-accent at 12px is
     the dimmest text on the page. The accent role and the band's well line are
     the dark-side equivalents, and both are measured. */
  .pl-card__icon, .pl-steps li::before { border-color:var(--pl-dark-well-line); }
  .pl-card--link:hover, .pl-faq details[open] { border-color:var(--pl-accent-deep); }
  .pl-faq summary::after, .pl-body li::marker, .pl-toc li::marker { color:var(--pl-accent-deep); }
}

/* Forced colours — Windows High Contrast and the equivalents. That mode throws
   away every colour in this file and substitutes the user's system palette, and
   it drops box-shadow altogether. The focus ring above IS a box-shadow, so in
   forced colours a keyboard user loses the one affordance they cannot work
   without: focus becomes invisible while everything else still looks fine, which
   is why nobody notices. An outline is not stripped, and Highlight is the colour
   the platform reserves for exactly this.
   The rest of the block is the same argument for shapes that were carried by a
   fill or a shadow rather than by a border: a filled button, a card and a chip
   all flatten into the page background without one. */
@media (forced-colors: active) {
  :focus-visible { outline:3px solid Highlight; outline-offset:2px; }
  .pl-btn, .pl-card, .pl-chip, .pl-menu > summary { border:1px solid ButtonText; }
  .pl-btn--primary { border-width:2px; }
  /* Drawn with borders, so it is already forced to CanvasText and visible. */
  .pl-card--link::after { border-color:CanvasText; }
}

/* One nav, one breakpoint.

   .pl-nav__inner is one non-wrapping row at a fixed 60px height, so the links
   cannot reflow: past a point they run off the right edge and the whole page
   gains a horizontal scrollbar. That used to be handled by hiding one link at
   a time at eight different widths, so the header a visitor saw depended on
   their window: "Free tools" was gone on any laptop under 1081px. Six of the
   eight rules were also dead code, because below 700px a separate rule already
   hid every link in the bar.

   Now there is one rule, below. Above 780px the bar carries the same items at
   every width; at 780px and below the links go and the Menu button carries the
   whole list. Both numbers are measured rather than chosen: at 781px the
   widest header on the site - the landing page, whose links are section names
   - needs 742px of a 766px content box, and the small-type regime that makes
   that fit has to start at 940px rather than 860px, because at 861px the same
   header needed 836px of 846px. Ten pixels is not a margin. */

@media print {
  .pl-nav, .pl-subnav, .pl-skip { display:none; }
  body::before { display:none; }
  .pl-band, .pl-footer { background:var(--pl-paper); color:var(--pl-ink); }
}

/* ------------------------------------------------------------------ demo
   The live widget demos. Each runs in a sandboxed iframe: the widget owns a
   whole viewport, its CSS cannot reach the site chrome, and a widget that
   throws takes down its own frame rather than the page around it. */

.pl-demo { margin-bottom:var(--pl-s-12); }
.pl-demo:last-child { margin-bottom:0; }

.pl-demo__head {
  display:flex; flex-wrap:wrap; gap:var(--pl-s-8); align-items:flex-start;
  justify-content:space-between; margin-bottom:var(--pl-s-7);
}
/* h2 on /work/, h3 on the home page — see demo_block() in build.py. The
   rank is a document-structure decision; the size is not, so pin both. */
.pl-demo__head :is(h2,h3) { font-size:var(--pl-fs-xl); font-weight:var(--pl-fw-semi); letter-spacing:-.012em; }
.pl-demo__head p { margin:var(--pl-s-5) 0 0; color:var(--pl-text-muted); font-size:var(--pl-fs-sm); max-width:62ch; }
.pl-demo__controls { display:flex; flex-wrap:wrap; gap:var(--pl-s-5); align-items:center; flex:none; }
.pl-demo__controls .pl-chip { display:inline-flex; align-items:center; gap:var(--pl-s-4); }

/* A slow pulse on the status chip. It is the one piece of ambient motion on
   the page, and it earns its place: it is the signal that the thing below is
   live rather than an image. */
.pl-demo__pulse {
  width:7px; height:7px; border-radius:var(--pl-r-pill); background:var(--pl-anchor-ok); flex:none;
  box-shadow:0 0 0 0 var(--pl-anchor-ok-soft); animation:pl-pulse var(--pl-t-pulse) var(--pl-ease) infinite;
}
@keyframes pl-pulse {
  0%   { box-shadow:0 0 0 0 var(--pl-anchor-ok-soft); }
  70%  { box-shadow:0 0 0 7px transparent; }
  100% { box-shadow:0 0 0 0 transparent; }
}
[data-demo-status].is-error { color:var(--pl-anchor-bad-deep); background:var(--pl-anchor-bad-tint); border-color:var(--pl-anchor-bad-soft); }
[data-demo-status].is-error .pl-demo__pulse { background:var(--pl-anchor-bad); animation:none; }

.pl-demo__frame {
  position:relative; border:1px solid var(--pl-line-strong); border-radius:var(--pl-r-lg);
  overflow:hidden; box-shadow:var(--pl-e-3); background:var(--pl-surface-sunk);
  height:var(--demo-h, 700px);
}
.pl-demo__frame iframe { display:block; width:100%; height:100%; border:0; }
.pl-demo__frame noscript img { display:block; width:100%; }

/* A browser-chrome hairline across the top, so a dark demo does not read as a
   hole punched in the page. */
.pl-demo__frame::before {
  content:''; position:absolute; top:0; left:0; right:0; height:1px; z-index:2;
  background:linear-gradient(90deg, transparent, var(--pl-anchor-run-soft), transparent);
}

@media (max-width:680px) {
  /* No viewport cap here on purpose. Capping makes the frame shorter than the
     widget, which turns the demo into a scroll region inside a page that is
     already scrolling — on touch, the worst of both. Let it stand at its full
     height and let the page scroll past it. */
  .pl-demo__controls { width:100%; }
}

/* ---------------------------------------------------------------- note toc */

/* A note answers one question, but it gets arrived at in the middle — from a
   search result, or from somebody else's link to a section. The contents list
   is the cheapest way to let that reader land on the part they came for. */
.pl-toc {
  max-width:var(--pl-max-prose); margin:0 0 var(--pl-s-11);
  padding:var(--pl-s-8) var(--pl-s-9);
  background:var(--pl-surface-sunk); border:1px solid var(--pl-line);
  border-radius:var(--pl-r-md);
}
.pl-toc > p { margin:0 0 var(--pl-s-6); }
.pl-toc ol { margin:0; padding-left:var(--pl-s-7); display:grid; gap:var(--pl-s-4); }
.pl-toc li { font-size:var(--pl-fs-sm); color:var(--pl-text-muted); }
.pl-toc li::marker { color:var(--pl-accent); }
.pl-toc a { color:var(--pl-text); text-decoration:none; }
.pl-toc a:hover { color:var(--pl-accent); text-decoration:underline; }

/* The hosted ede-check runner. One block, tokens only: a drop target, the
   controls beside it, and the report in the same mono face the transcripts on
   the rest of the site use, because the lines printed here are the
   command-line tool's lines and reading them as anything else would be a
   second format to keep true. */
.pl-run {
  max-width:var(--pl-max-prose); margin:0 0 var(--pl-s-11);
  display:grid; gap:var(--pl-s-8);
}
.pl-run__drop {
  display:grid; gap:var(--pl-s-5); justify-items:center; text-align:center;
  padding:var(--pl-s-11) var(--pl-s-9);
  background:var(--pl-surface-sunk);
  border:1px dashed var(--pl-line-strong); border-radius:var(--pl-r-md);
  transition:border-color var(--pl-t-base) var(--pl-ease),
             background-color var(--pl-t-base) var(--pl-ease);
}
.pl-run__drop[data-over] { border-color:var(--pl-accent); background:var(--pl-accent-tint); }
.pl-run__drop[data-file]::after {
  content:attr(data-file); font-family:var(--pl-font-mono);
  font-size:var(--pl-fs-xs); color:var(--pl-text-strong);
}
.pl-run__hint { margin:0; font-size:var(--pl-fs-sm); color:var(--pl-text-muted); }
.pl-run__row { display:flex; flex-wrap:wrap; gap:var(--pl-s-6); align-items:end; }
.pl-run__field { display:grid; gap:var(--pl-s-3); font-size:var(--pl-fs-xs); }
.pl-run__field > span { color:var(--pl-text-muted); font-weight:var(--pl-fw-medium); }
.pl-run :is(input,select,textarea) {
  font:inherit; font-size:var(--pl-fs-sm); color:var(--pl-text);
  background:var(--pl-surface); border:1px solid var(--pl-line-strong);
  border-radius:var(--pl-r-sm); padding:var(--pl-s-5) var(--pl-s-6);
}
.pl-run textarea {
  width:100%; min-height:var(--pl-s-14); font-family:var(--pl-font-mono);
  font-size:var(--pl-fs-xs); resize:vertical;
}
.pl-run :is(input,select,textarea):focus-visible { outline:var(--pl-focus-ring); }
.pl-run__out { display:grid; gap:var(--pl-s-6); }
.pl-run__out pre { margin:0; }

/* ---------------------------------------------------- core, fields, teams
   2026-11-07 restructure: the home page leads with the five-step core, the
   field grid and the per-project roles. New components only; every colour is
   an existing token, so the contrast pairs check.py measures still hold. */
.pl-core {
  list-style:none; margin:0; padding:0; display:grid; gap:var(--pl-s-6);
  grid-template-columns:repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  counter-reset:none;
}
.pl-core li {
  position:relative; padding:var(--pl-s-8) var(--pl-s-7) var(--pl-s-7);
  background:var(--pl-surface); border:1px solid var(--pl-line);
  border-radius:var(--pl-r-lg); box-shadow:var(--pl-e-1);
}
.pl-core li::before {
  content:''; position:absolute; inset:0 0 auto 0; height:3px;
  border-radius:var(--pl-r-lg) var(--pl-r-lg) 0 0;
  background:linear-gradient(90deg, var(--pl-anchor-run), var(--pl-anchor-info));
}
.pl-core__n {
  display:inline-grid; place-items:center; width:1.9em; height:1.9em;
  border-radius:var(--pl-r-pill); font-size:var(--pl-fs-sm); font-weight:var(--pl-fw-bold);
  color:var(--pl-run-ink); background:var(--pl-anchor-run-tint); margin-bottom:var(--pl-s-5);
}
.pl-core h3 { font-size:var(--pl-fs-lg); }
.pl-core p { color:var(--pl-text-muted); font-size:var(--pl-fs-sm); margin:var(--pl-s-4) 0 0; }

.pl-field { border-top:3px solid var(--pl-theme-soft); }
.pl-card .pl-field__tag {
  margin:0 0 var(--pl-s-4); font-size:var(--pl-fs-micro); font-weight:var(--pl-fw-semi);
  letter-spacing:var(--pl-track-eyebrow); text-transform:uppercase; color:var(--pl-theme-ink);
}
.pl-field :is(h2,h3) a { color:inherit; text-decoration:none; }
.pl-field :is(h2,h3) a::after { content:''; position:absolute; inset:0; }
.pl-field :is(h2,h3) a:focus-visible { outline:none; }
.pl-field:has(a:focus-visible) { box-shadow:var(--pl-focus-ring); }
.pl-field:hover { border-color:var(--pl-theme-soft); box-shadow:var(--pl-e-3); }
.pl-field__links { margin:0; padding-left:1.1em; }
.pl-field__links li + li { margin-top:var(--pl-s-3); }

.pl-role h3 { font-size:var(--pl-fs-lg); }
.pl-hero__fields { margin-top:var(--pl-s-9); }
.pl-hero__fields a { color:inherit; text-decoration:none; }
.pl-hero__fields a:hover { text-decoration:underline; }

/* Dark mode: the four theme inks were the light-mode deep shades, and on the
   dark sunk band the BAS one measured 2.99:1 (Lighthouse, 2026-11-07, the
   Niagara 5 eyebrow). The dark palette already carries a soft ink per hue. */
@media (prefers-color-scheme: dark) {
  .pl-theme--products  { --pl-theme-ink:var(--pl-dark-info-ink); --pl-theme-tint:var(--pl-dark-info-tint); --pl-theme-soft:var(--pl-dark-info-line); }
  .pl-theme--ops       { --pl-theme-ink:var(--pl-dark-ok-ink);   --pl-theme-tint:var(--pl-dark-ok-tint);   --pl-theme-soft:var(--pl-dark-ok-line); }
  .pl-theme--automation{ --pl-theme-ink:var(--pl-dark-warn-ink); --pl-theme-tint:var(--pl-dark-warn-tint); --pl-theme-soft:var(--pl-dark-warn-line); }
  .pl-theme--bas       { --pl-theme-ink:var(--pl-dark-run-ink);  --pl-theme-tint:var(--pl-dark-run-tint);  --pl-theme-soft:var(--pl-dark-run-line); }
  .pl-core__n { color:var(--pl-dark-run-ink); background:var(--pl-dark-run-tint); }
}
