/* =============================================================================
   TALBOTIQ — homepage
   -----------------------------------------------------------------------------
   The design is mockup-homepage.html: one display face doing all the talking,
   one or two coloured words per heading, flat band edges, and one asymmetric
   corner. (The mockup's hand-drawn marks were removed by request — §26 is what
   replaced them.) Structure follows odoo.com — a product grid high on the
   page, then capability, then a closing offer.

   The mockup curved the white page into the grey product band with a 96px SVG
   arc. That is now a STRAIGHT edge — see the note on `.band` in §4. The cut
   corner on `.caps` is a different device and is still there.

   THE DISPLAY FACE IS MESHED DISPLAY, a high-contrast serif, self-hosted in
   §0. The mockup used Caveat Brush, a brush script. The swap is why §0 carries
   a derivation note; the marks that also had to be re-derived against this face
   no longer exist.

   Everything from §1 to §12 is the mockup's own CSS, kept at its own values so
   the page renders as designed. §13 onward is what a real page needs and a
   mockup does not: the three nav panels, a menu that works on a phone, focus
   states, and the markers that show which content is still a placeholder.
   ========================================================================== */

/* ---- 0 · THE DISPLAY FACE ----------------------------------------------
   MESHED Display by Rajesh Rajput, self-hosted. Free for commercial use; the
   licence forbids MODIFYING the files, so this is the vendor's own .woff2
   copied verbatim out of the package — never subset or re-compressed. See
   assets/fonts/README.md.

   Only Bold is shipped because only Bold is used, and the @font-face declares
   700 so the browser matches the real file instead of synthesising a bold from
   something lighter. */
@font-face {
  font-family: "Meshed Display";
  src: url("../fonts/MeshedDisplay-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --teal: #02A885;
  --green: #027A5C;
  --yellow: #F3E202;
  --amber: #C48A00;
  --blue: #1B7FC4;

  --grey: #5D5B5B;
  --ink: #1F2430;
  --band: #F3F4F6;
  --border: #E3E4E4;
  --footer: #3A3A3A;

  --dim: #6E6C6C;
  --quiet: #8B8989;

  /* The fallbacks are SERIFS now, not handwriting scripts: MESHED Display is a
     high-contrast display serif, so if it fails to load a Didone-ish serif is
     the near miss and Comic Sans would be a disaster. */
  --f-hand: "Meshed Display", "Playfair Display", "Didot", "Bodoni MT", Georgia, serif;
  --f-text: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

  /* ---- THE KEYWORD PAIR --------------------------------------------------
     One or two words per heading carry a colour; everything else is ink. These
     are the two roles, and they are TOKENS rather than the raw brand hexes
     because the brand yellow cannot be used as text: #F3E202 on white is
     1.34:1, which is invisible. --key-y is that yellow taken down until it
     passes on BOTH light grounds (4.03:1 on white, 3.66:1 on the grey band) —
     still gold, now readable. Green needs no adjustment at 5.33:1.

     On a dark or teal ground these two invert; see §26. */
  --key-g: #027A5C;
  --key-y: #A87500;

  --hdr-h: 72px;
  --r: 5px;
  --ease: cubic-bezier(.22, .61, .36, 1);
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* the header is sticky, so anything linked to by #id has to clear it */
:target { scroll-margin-top: calc(var(--hdr-h) + 16px); }

body {
  margin: 0;
  background: #fff;
  color: var(--grey);
  font-family: var(--f-text);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; }

.hand {
  font-family: var(--f-hand);
  /* matches the one @font-face weight shipped, so nothing is synthesised */
  font-weight: 700;
  letter-spacing: 0;
  line-height: 1.08;
  color: var(--ink);
}

.wrap { max-width: 1200px; margin: 0 auto; padding: 0 32px; }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--ink); color: #fff; padding: 12px 18px;
  font-size: 15px; text-decoration: none; border-radius: 0 0 var(--r) 0;
}
.skip:focus { left: 0; }

/* ---- 1 · HEADER --------------------------------------------------------- */
header {
  position: sticky; top: 0; z-index: 50;
  background: #fff; height: var(--hdr-h);
  display: flex; align-items: center;
  border-bottom: 1px solid transparent;
  transition: border-color .2s;
}
header.stuck { border-bottom-color: var(--border); }

.hdr {
  max-width: 1320px; margin: 0 auto; padding: 0 32px; width: 100%;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 20px;
}
.logo { display: block; }
.logo img { height: 40px; width: auto; display: block; }

nav.mid { display: flex; gap: 34px; justify-content: center; align-items: center; }
nav.mid > a,
nav.mid > .navitem > button {
  color: var(--ink); text-decoration: none;
  font-family: var(--f-text); font-size: 15.5px; font-weight: 500;
  white-space: nowrap;
  background: none; border: 0; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; gap: 4px;
}
nav.mid > a:hover,
nav.mid > .navitem > button:hover { color: var(--teal); }
.car { font-size: 10px; opacity: .55; display: inline-block; vertical-align: 2px; }

.hdr-right { display: flex; gap: 18px; justify-content: flex-end; align-items: center; }
.hdr-right a.si {
  color: var(--ink); text-decoration: none; font-size: 15.5px; font-weight: 500;
  white-space: nowrap;
}
.hdr-right a.si:hover { color: var(--teal); }

/* ---- 2 · BUTTONS -------------------------------------------------------- */
.btn {
  display: inline-block; border: 0; cursor: pointer; text-decoration: none;
  font-family: var(--f-text); font-size: 15.5px; font-weight: 600;
  padding: 11px 22px; border-radius: var(--r); white-space: nowrap;
  transition: background .16s var(--ease), transform .16s var(--ease);
}
.btn-primary { background: var(--teal); color: #fff; }
.btn-primary:hover { background: var(--green); }
.btn-ghost { background: var(--band); color: var(--ink); }
.btn-ghost:hover { background: #E7E9EC; }
.btn-white { background: #fff; color: var(--green); }
.btn-outline-white {
  background: transparent; color: #fff;
  box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / .75);
}
.btn-outline-white:hover { background: rgb(255 255 255 / .1); }
.btn-lg { padding: 15px 30px; font-size: 17px; }

/* ---- 3 · HERO ----------------------------------------------------------- */
/* The bottom padding is NOT the mockup's. It was `74px 0 0` there, because the
   subnote sat under the buttons and its own margin held the buttons clear of
   the curve into the product band. With the subnote removed the buttons ended
   up flush against that curve — measured at a 1px overlap — so the space the
   subnote used to occupy is now stated directly. */
.hero { text-align: center; padding: 74px 0 46px; position: relative; overflow: hidden; }
h1.hand { font-size: 76px; margin: 0 0 26px; letter-spacing: -.01em; }

/* The highlighter. The stroke is drawn BEHIND the words, so the text keeps its
   own contrast and the mark reads as ink on top of paper.

   THE VERTICAL NUMBERS ARE DERIVED, NOT EYEBALLED. A marker covers the
   lowercase body of a word, not its whole line box, and where that body sits
   depends entirely on the face. For MESHED Display Bold, measured off the
   font itself:

       ascent .724em   descent .197em   x-height .513em
       line-height 1.08  ->  half-leading .079em
       baseline      = .079 + .724 = .803em below the line-box top
       x-height top  = .803 - .513 = .290em

   so a stroke that starts a hair above the x-height and finishes a hair below
   the baseline is top .24em, height .62em (ending at .86em). Both are in em,
   so they hold at every size the hero uses — 76px, 62px and 44px — with no
   per-breakpoint overrides.

   REMOVED, by request: the highlighter and its three siblings. This block used
   to position a filled marker sweep behind the hero's last clause, and carried
   the derivation notes for re-aiming it at MESHED Display. The emphasis is now
   colour on the words themselves — see §26. */

.lede { font-size: 22px; line-height: 1.5; max-width: 23ch; margin: 0 auto 34px; color: var(--grey); }
.lede b { color: var(--ink); font-weight: 600; }

.cta-pair { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }

/* The hero used to carry two more things under the buttons: a "Live in under
   two weeks. See how" subnote, and a rotated pencilled aside out to the right
   reading "8 products. 1 login." with an arrow pointing at it. Both were
   removed, so `.subnote`, `.aside-note` and the arrow SVG went with them —
   the hero is now the line, the lede and the two buttons, and nothing else.

   The hero keeps `position: relative` and `overflow: hidden`: the curve into
   the product band below still needs them. */

/* ---- 4 · CURVED DIVIDER + PRODUCT BAND ---------------------------------- */
/* A STRAIGHT EDGE. The mockup ran a 96px-tall SVG here that curved the white
   page into this grey band — two stacked paths, one grey and one white, with
   `preserveAspectRatio="none"` so the arc stretched to any width. It is gone,
   and with it the only reason this band had no top padding: the curve WAS the
   top padding, and the tiles sat under it. So the space is stated directly.

   The arc is still in `design/mockup-homepage.html` if it is ever wanted back:
   restore the two paths, put `.curve` first inside `.band`, and set this
   padding back to `0 0 96px`.

   The cut corner on the capability band further down (`.caps`) is a separate
   device and was left alone. */
.band { background: var(--band); position: relative; padding: 84px 0 96px; }

/* ---- the product grid: ten tiles in three labelled bands ----------------- */
/* Design: mockup-homepage-product-grid.html. Three groups, because the grid
   answers three different questions and a reader arrives holding one of them.
   Nothing here moves — see the note in §18 for what used to. */

.phead { text-align: center; max-width: 66ch; margin: 0 auto 8px; position: relative; }
.phead h2.hand { font-size: 46px; margin: 0 0 16px; }
/* The global .lede is 22px in a 23ch column, which is a hero measure. This one
   sits over a grid and needs the width, so only the three properties that
   differ are restated — `.lede b` still does the emphasis. */
.phead .lede { font-size: 18px; line-height: 1.62; max-width: none; margin: 0; }

/* Nothing is absolutely positioned in here any more — the connector SVG was
   the only thing this was a containing block for. */
.pblock { }

/* The label over each band: the name, a rule that fades out, and the count.
   `currentColor` on the rule is what lets one rule serve all three tones. */
.glabel {
  display: flex; align-items: center; gap: 18px;
  margin: 42px 0 6px;
}
/* The 64px that used to sit above every band was runway for the connectors to
   travel through, not breathing room — with the traces gone it reads as a gap
   rather than as a grouping. 42px between a row and the next band's label, and
   the label keeps its 6px to the row it belongs to, so each heading still binds
   downward. The first one keeps more, because it is separating the grid from
   the section's own headline rather than one band from another. */
.pblock > .glabel:first-child { margin-top: 58px; }
.glabel .t {
  font-size: 11.5px; letter-spacing: .15em; text-transform: uppercase;
  font-weight: 700; white-space: nowrap;
}
.glabel .r { flex: 1; height: 1px; background: linear-gradient(90deg, currentColor, transparent); opacity: .35; }
/* The count is the band's own tally, so it is quiet — the label is what the
   reader is scanning for, not the number beside it. */
.glabel .c { font-size: 12px; color: var(--quiet); white-space: nowrap; }
.glabel.g1 { color: var(--teal); }
.glabel.g2 { color: var(--green); }
.glabel.g3 { color: var(--amber); }

.prow { display: grid; gap: 8px; }
.prow-4 { grid-template-columns: repeat(4, 1fr); }
.prow-5 { grid-template-columns: repeat(5, 1fr); }

.tile {
  text-align: center; text-decoration: none; display: block;
  padding: 14px 10px 8px; border-radius: 18px;
  transition: transform .22s var(--ease);
}
.tile:hover { transform: translateY(-6px); }
.tile .card {
  width: 132px; height: 132px; margin: 0 auto 22px;
  background: #fff; border-radius: 28px;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 14px rgb(31 36 48 / .055), 0 1px 3px rgb(31 36 48 / .05);
  transition: box-shadow .22s var(--ease);
}
.tile:hover .card { box-shadow: 0 14px 34px rgb(2 168 133 / .16), 0 2px 6px rgb(31 36 48 / .06); }
/* overflow is visible because several icons are drawn to the very edge of their
   own viewBox, and a stroke on the edge is half outside it */
.tile .card svg { display: block; overflow: visible; }
/* TWO LINES' WORTH OF SPACE WHETHER THE NAME NEEDS IT OR NOT. The products are
   named in full now — "Intelligent Recruitment Software", "Task & Productivity
   Manager" — so a row mixes one-line and two-line names, and without a floor
   here each caption below starts at a different height and the row reads
   ragged. Reserving two lines levels every row except the names that run to
   three, which is a smaller irregularity than five different baselines.

   BELOW 460px THAT REMAINDER IS FIXED PROPERLY — see the three-line band in the
   460px block. It is not fixed here because above 460px no name exceeds two
   lines, so a three-line band would only add a blank line to every tile. */
.tile .nm {
  display: block; font-size: 20px; font-weight: 700; color: var(--ink);
  margin-bottom: 9px; letter-spacing: -.01em; line-height: 1.2;
  min-height: calc(2 * 1.2em);
}
.tile .ds {
  display: block; font-size: 15px; line-height: 1.5;
  color: var(--quiet); max-width: 20ch; margin: 0 auto;
}

/* THE BADGE SAYS THE ONE THING THE NAME AND THE CAPTION CANNOT: whether the
   round happens live or on the candidate's own time. It appears on hover
   because it is a detail for someone already interested in that tile. */
.kin {
  display: inline-block; margin-top: 11px;
  font-size: 10px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 20px;
  opacity: 0; transition: opacity .22s var(--ease);
}
.tile:hover .kin, .tile:focus-visible .kin { opacity: 1; }
.kin-async  { background: #EAF1F8; color: #1B5F94; }
.kin-live   { background: #EAF7F3; color: var(--green); }
.kin-2-modes { background: #FFF7DB; color: #8A6200; }

/* ---- a tile that has sub-modes: the pill, and the branch it opens ------- */
/* A video round and a chat round each come in three shapes, and each shape is
   its own page. The pill says how many; the branch is where they are. It opens
   on hover for a pointer, on focus for a keyboard, and sits inline below 1080px
   where there is neither.

   `.cell` is the frame the branch hangs off. Every tile has one so the grid's
   children are all the same box, and only the two with modes put anything in
   it — see productTile() in build.js. */
.cell { position: relative; }

/* A <button>, not a span, and a sibling of the tile rather than a child of its
   link — see productTile() in build.js for why. So it needs the UA button
   styling reset, and its own centring: it used to inherit `text-align: center`
   from inside `.tile`. */
.modes {
  display: flex; align-items: center; gap: 6px;
  width: -moz-fit-content; width: fit-content; margin: 4px auto 0;
  font-family: inherit;
  font-size: 11.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--teal); background: #E7F7F2; border: 0; padding: 4px 11px; border-radius: 20px;
  cursor: pointer;
  transition: background .2s var(--ease), color .2s var(--ease);
}
/* A CLICK PINS IT OPEN AT EVERY WIDTH, so the button is never a control that
   does nothing. On a pointer the hover rule above has usually opened the panel
   already; this is what makes the same button work for touch and keyboard. */
.cell.is-open .branch { opacity: 1; visibility: visible; transform: none; }
.cell.is-open .modes { background: var(--teal); color: #fff; }
.cell.is-open .modes svg { transform: rotate(180deg); }
.cell:hover .modes, .cell:focus-within .modes { background: var(--teal); color: #fff; }
.modes svg { transition: transform .2s var(--ease); }
.cell:hover .modes svg, .cell:focus-within .modes svg { transform: rotate(180deg); }

/* 352px IS THE DRAWING. The tree's three drops are placed at the centres of
   three equal columns inside a panel this wide, so the width and the connector
   agree by construction and neither can be nudged on its own. */
.branch {
  position: absolute; top: calc(100% - 4px); z-index: 40; width: 352px;
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: opacity .24s var(--ease), transform .24s var(--ease), visibility .24s;
}
.cell:hover .branch, .cell:focus-within .branch { opacity: 1; visibility: visible; transform: none; }
/* The first column anchors its panel to its own left edge and the others centre
   theirs — which is what keeps a panel off the page's left margin. */
.branch.b-left { left: -16px; }
.branch.b-mid { left: 50%; margin-left: -176px; }
.branch .tree { display: block; width: 100%; height: 44px; overflow: visible; }

.minis {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
  background: #fff; border: 1px solid var(--border); border-radius: 16px;
  padding: 14px 10px 12px;
  box-shadow: 0 18px 42px rgb(31 36 48 / .14);
}
/* Display only - no href, so no hover lift and no pointer. The panel names the
   three modes; the tile above it is what opens anything. */
.mini {
  display: block; text-align: center; cursor: default;
  padding: 8px 4px 6px; border-radius: 10px;
}
.mini svg { display: block; margin: 0 auto 9px; overflow: visible; }
.mini b { display: block; font-size: 12.5px; font-weight: 600; color: var(--ink); line-height: 1.25; }

/* a tile with no application to open yet is still a tile, but it does not
   pretend to be a door */
.tile[aria-disabled="true"] { cursor: default; }
.tile[aria-disabled="true"]:hover { transform: none; }
.tile[aria-disabled="true"]:hover .card {
  box-shadow: 0 4px 14px rgb(31 36 48 / .055), 0 1px 3px rgb(31 36 48 / .05);
}

/* The engine is not a row of one, it is the floor the other nine stand on — so
   it is centred and drawn larger. It used to carry the section's only sentence
   as well; `.enote` styled it and both went when the claim was withdrawn. */
.pengine { margin-top: 44px; }
.pengine .tile { max-width: 420px; margin: 0 auto; }
.pengine .card { width: 148px; height: 148px; border-radius: 32px; margin-bottom: 24px; }
.pengine .nm { font-size: 23px; }
.pengine .ds { font-size: 15.5px; max-width: 30ch; }
.band .allp { text-align: right; max-width: 1060px; margin: 44px auto 0; padding: 0 4px; }
.band .allp a { color: var(--green); font-weight: 600; text-decoration: none; font-size: 16.5px; }
.band .allp a:hover { text-decoration: underline; }

/* ---- 5 · GENERIC SECTIONS ----------------------------------------------- */
section { padding: 96px 0; }
h2.hand { font-size: 52px; margin: 0 0 22px; text-align: center; }
h2.hand.left { text-align: left; }
.sec-lede { font-size: 19px; max-width: 66ch; margin: 0 auto; text-align: center; line-height: 1.6; }
.sec-lede b { color: var(--ink); font-weight: 600; }

/* REMOVED, by request: the ruled underline, the squiggle and the lasso. §26 is
   what marks a heading's important words now. */

/* ---- 6 · ECOSYSTEM BAND ------------------------------------------------- */
.eco {
  background: #EAF7F3;
  border-top: 1px solid #B9E4D8; border-bottom: 1px solid #B9E4D8;
  padding: 74px 0;
}
.eco .eyebrow {
  text-align: center; font-size: 12.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--green); font-weight: 600; margin-bottom: 14px;
}
.eco p { max-width: 74ch; margin: 0 auto; text-align: center; font-size: 18px; line-height: 1.62; }

/* ---- 8 · CAPABILITY CARDS ----------------------------------------------- */
/* The one asymmetric corner on the page. It is the reason this band reads as
   a card the page sits on rather than another full-bleed stripe. */
.caps { background: var(--band); border-radius: 0 90px 0 0; padding: 88px 0 96px; }
.capgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 44px; }
.cap { background: #fff; border-radius: 12px; padding: 30px 30px 32px; position: relative; }
.cap h3 { font-size: 20px; font-weight: 600; color: var(--ink); margin: 0 0 10px; padding-right: 44px; }
.cap p { margin: 0; font-size: 15.5px; line-height: 1.6; }
.cap.wide { grid-column: span 2; }
.cap .star { position: absolute; right: 22px; top: 20px; }
.cap .star .glow {
  position: absolute; right: -8px; top: -8px; width: 52px; height: 52px;
  border-radius: 50%; background: #EDEEF0;
}
.cap .star svg { position: relative; }

/* ---- 9 · WHY LEAD WITH TALBOTIQ ---------------------------------------- */
/* This replaced the mockup's testimonial slot — a dashed box that said out
   loud it had no quote to show. The three claims that sit here now each run
   the photograph the live site runs above them.

   Three columns of centred text rather than the cards used further up. The
   capability band above is already a grid of white cards on grey, and a second
   card grid one section later would read as the same idea twice. */
.why { padding: 92px 0; }
.whygrid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 44px 30px; margin: 54px auto 0; max-width: 1060px;
  text-align: center;
}
/* THE THREE SOURCE PHOTOS ARE NOT THE SAME SHAPE — one 16:9 and two 3:2 — so
   the box sets the ratio and the picture is cropped into it. Left to their own
   proportions they would be three different heights and the row of headings
   underneath would step up and down, which is the thing the eye notices before
   it notices any of the photographs. */
.whyitem .shot { display: block; margin: 0 0 22px; border-radius: 12px; overflow: clip; }
.whyshot {
  display: block;
  width: 100%;
  /* `height: auto` is load-bearing, not tidiness. The width/height attributes on
     the <img> map to real CSS width and height in the UA stylesheet, and
     `aspect-ratio` is ignored the moment BOTH dimensions are definite — so
     without this the pictures rendered at their attribute height of 506px in a
     333px column, i.e. portrait, and the 16/9 below did nothing at all. */
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  object-position: center;
}

.whyitem h3 {
  font-size: 22px; font-weight: 600; color: var(--ink);
  margin: 0 0 12px; line-height: 1.3; letter-spacing: -.01em;
}
.whyitem p { margin: 0; font-size: 16px; line-height: 1.65; }

/* ---- 10 · BLOG ---------------------------------------------------------- */
/* The cards carry real published articles now, so the thumbnail is a real
   photograph rather than the mockup's gradient block. Everything else keeps
   the mockup's measurements. */
/* The editorial introduction above the grid. `.sec-lede` is centred and
   66ch wide because every other section that uses it sits under a centred
   heading; this one sits under `h2.hand.left`, so it re-anchors left and drops
   the auto margins. Kept at --grey and a notch below the base lede size so the
   heading stays the strongest thing in the section, which is the whole point
   of a standfirst. Same override shape as `.demoside .sec-lede`. */
.blog-lede {
  text-align: left; margin: 0; max-width: 74ch;
  font-size: 18px; color: var(--grey);
  /* THE GOLD RULE. A border on the paragraph itself rather than a spacer
     element beside it, so the bar can only ever be exactly as tall as the text
     it marks — which is the single thing asked of it, and the thing a separate
     element gets wrong the moment the copy rewraps.

     --amber is the logo's gold and not a new colour in the system. The brand
     yellow it sits beside, #F3E202, is a 92%-luminance lemon: a 3px rule in it
     is invisible on the #FBFCFE ground, and widening it until it shows turns a
     standfirst mark into a highlighter. The teal is deliberately not used —
     teal is the product's colour, and this paragraph is about a masthead. */
  border-left: 3px solid var(--amber);
  padding-left: 22px;
}

/* ---- the featured episode ------------------------------------------------ */
/* One media item, above the grid rather than in it. Not a ninth card: the
   columns below are read and this is watched, and a 16:9 player dropped into a
   row of 3:2 thumbnails would claim to be the same kind of thing.

   Video left, editorial right, centred against each other. Top-aligning them
   is the obvious choice and the wrong one — the prose runs about 250px against
   a 395px player, so it would hang a fifth of the block's height of nothing
   under the last line. The left column is the wider of the two because the
   player is the feature; the words only explain it. */
.tlfeat {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: 44px;
  align-items: center;
  margin: 46px 0 0;
}

/* The ratio is held by the FRAME, not by the iframe, so the box is the right
   shape before YouTube has answered and the page does not jump when it does.
   The well-coloured ground underneath means the gap reads as the player's own
   letterbox rather than as a hole. */
.tlfeat-media {
  aspect-ratio: 16 / 9;
  border-radius: 14px;
  overflow: hidden;
  background: var(--well);
  box-shadow: 0 1px 2px rgb(31 36 48 / .06), 0 14px 36px rgb(31 36 48 / .10);
}
.tlfeat-media iframe { display: block; width: 100%; height: 100%; border: 0; }

/* THE PLAYER IS A PICTURE UNTIL IT IS ASKED FOR. A lazy iframe still means a
   reader who scrolls here waits on YouTube's bundle while looking at a black
   rectangle. This is the poster and a play control; app.js swaps in the real
   iframe on the click, which is when the megabyte is actually worth paying.
   The button inherits the frame's own rounding and shadow, so nothing about
   the block moves when the swap happens. */
.ytlite {
  display: block; position: relative; width: 100%; height: 100%;
  padding: 0; border: 0; background: none; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.ytlite img { display: block; width: 100%; height: 100%; object-fit: cover; }
.ytplay {
  position: absolute; inset: 0; display: grid; place-items: center;
  /* a wash under the badge, so a white triangle holds on a pale frame */
  background: linear-gradient(180deg, rgb(14 22 32 / .04), rgb(14 22 32 / .22));
  transition: background .2s var(--ease);
}
.ytplay svg {
  /* the triangle is drawn slightly right of centre in its own box, which is
     what makes an optically centred play mark rather than a mathematical one */
  display: block; margin-left: 4px;
  /* ONE TRIANGLE SIZE FOR BOTH BADGES, SET HERE RATHER THAN IN THE MARKUP. The
     two SVGs carry different width/height attributes — 26×30 on the episode,
     20×24 on the press still — because the episode's badge used to be 76px and
     needed the bigger glyph to fill it. Both badges are 66px now, so the same
     attributes would leave the episode's mark at 45% of its circle against the
     other's 36%: same button, visibly chunkier. Stating it once in CSS keeps
     them identical and overrides both attributes. */
  width: 20px; height: 24px;
  /* the badge below is absolutely positioned, so it would otherwise paint OVER
     this non-positioned sibling and swallow the triangle */
  position: relative; z-index: 1;
  filter: drop-shadow(0 1px 2px rgb(14 22 32 / .35));
}
/* the badge itself, drawn on the span rather than a second element */
.ytplay::before {
  content: ""; position: absolute; width: 66px; height: 66px; border-radius: 50%;
  background: var(--teal); box-shadow: 0 6px 20px rgb(14 22 32 / .28);
  transition: transform .2s var(--ease), background .2s var(--ease);
}
.ytlite:hover .ytplay { background: linear-gradient(180deg, rgb(14 22 32 / .1), rgb(14 22 32 / .3)); }
.ytlite:hover .ytplay::before { transform: scale(1.07); background: var(--green); }
.ytlite:focus-visible { outline: 3px solid var(--teal); outline-offset: 3px; }

/* The show's name is the strongest line in the block, so it takes the display
   face — the same move every other section makes for its own loudest line, and
   what keeps this from reading as an embed someone pasted in. The episode title
   under it is the body face at card-headline size: secondary by face as well as
   by size, which is a clearer step down than a couple of points would be. */
.tlfeat-show {
  font-family: var(--f-hand);
  font-size: 34px; line-height: 1.12; font-weight: 400;
  color: var(--ink); margin: 0 0 14px;
}
.tlfeat-ep {
  font-size: 19px; font-weight: 600; line-height: 1.4;
  color: var(--ink); margin: 0 0 12px;
}
.tlfeat-desc {
  font-size: 15.5px; line-height: 1.62; color: var(--dim);
  margin: 0; max-width: 46ch;
}

/* ---- the second item: a broadcast appearance, linked out ---------------- */
/* Same block, three differences. The still keeps its own 3:2 ratio because its
   bottom edge is the programme's own ticker and a 16/9 crop would take it. The
   show line comes down from 34px: this title runs to nine words where the
   episode's ran to three, and at display size it would tower over the item
   above it rather than reading as its peer. And the block says out loud that it
   leaves the site, because the play glyph is baked into the still and cannot be
   asked to stop implying playback. */
.tlfeat--press { margin-top: 40px; }
.tlfeat--press .tlfeat-show { font-size: 27px; }
.tlfeat-media--photo { aspect-ratio: auto; display: block; position: relative; }
.tlfeat-media--photo img { display: block; width: 100%; height: auto; }
.tlfeat-media--photo:focus-visible { outline: 3px solid var(--teal); outline-offset: 3px; }
/* NO SIZE OVERRIDE HERE ANY MORE, and the deletion is the point. This carried
   `width: max(44px, 10.5%)` back when the episode's badge was a fixed 76px: a
   percentage kept this one proportional to a column that changes width, and it
   landed near 66px on a desktop. Both badges are 66px flat now, so the
   percentage stopped holding them in proportion and started pulling them apart
   — 88px against 66 once the block stacks at 900, 44 against 66 on a phone.
   One fixed size in the base rule is what actually makes them the same button.

   If this still ever wants its own size again, it is one rule here, not a
   percentage: the two frames are different shapes and only a number stays put
   across the stacked layout. */

.tlfeat-out { margin: 16px 0 0; font-size: 15.5px; }
.tlfeat-out a {
  color: var(--green); font-weight: 600; text-decoration: none;
  border-bottom: 1px solid rgb(2 122 92 / .3);
}
.tlfeat-out a:hover { border-bottom-color: currentColor; }

/* 56 rather than the 44 this used to carry: the grid now follows a feature
   instead of a paragraph, and the gap has to say "different kind of thing"
   without a rule drawn across the page to say it. */
/* THREE PLACARDS, WIDELY SPACED. 56px between columns rather than the 26px a
   row of eight needed — with three cards the gap is what tells you they are
   three separate pieces rather than one block of thumbnails. */
/* There is no unscoped `.eyebrow` rule in this stylesheet — every section
   styles its own — so this is set here rather than inherited. It matches the
   product band's category labels (`.glabel .t`) on purpose: both are small
   labels naming a group of cards, and they should look like the same device. */
.blogsel {
  margin: 54px 0 24px;
  font-size: 11.5px; letter-spacing: .15em; text-transform: uppercase;
  /* Not --quiet: at 11.5px this is small text and needs 4.5:1, and #8B8989 on
     the page ground measures 3.40. #6B7280 is the slate the pipeline labels
     already use and lands at 4.73. */
  font-weight: 700; color: #6B7280;
}
.blog {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 48px 56px; margin-top: 0;
}
.post { text-decoration: none; display: flex; flex-direction: column; }

/* THE COVER IS THE CARD. Every one is forced to the same 16:9 box and cropped
   to fill it, so the three line up exactly whatever their source turns out to
   be — the branded editorial artwork is 16:9, the publisher's fallback
   photographs are not, and a row where one image is a different height reads
   as broken rather than as varied. */
/* THE RATIO GOES ON THE IMAGE, NOT ON THE WRAPPER. `aspect-ratio` on the
   wrapper loses to its own content: the fallback photographs are 3:2, so at
   `height: 100%` the picture's own min-content height pushed the 16:9 box back
   out to 1.5 and the "identical" covers were not identical at all. On the image
   itself, with an auto height, the ratio holds and object-fit does the crop. */
.post .thumb {
  width: 100%; height: auto; display: block;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--band);
  transition: transform .2s var(--ease);
}
.post .thumb--none { aspect-ratio: 16 / 9; background: linear-gradient(135deg, #0FBF98, var(--green)); }
a.post:hover .thumb { transform: scale(1.02); }

/* The one metadata line, and the only thing between the cover and the summary.
   Small and quiet: it names the author, it is not a headline. The headline is
   set into the artwork above it and is deliberately not repeated here. */
.post .pby {
  margin: 16px 0 10px; font-size: 12.5px; line-height: 1.45;
  color: var(--quiet); letter-spacing: .01em; font-weight: 500;
}

/* Four lines, and always four lines' worth of space. The clamp alone would let
   a short summary pull its "Read more" up while a long one pushed the next
   card's down, so the box is given the height of four lines whether it needs
   them or not — that is what keeps the three links on one baseline. The full
   text the publisher wrote stays in the markup; only the display is capped. */
.post .psum {
  margin: 0; font-size: 15px; line-height: 1.55; color: var(--dim);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
  min-height: calc(4 * 1.55em);
}

/* A text link, not a button — and a span rather than an <a>, because it is
   already inside the card's own link. `margin-top: auto` pins it to the bottom
   of whichever card is tallest, which is the last thing holding the row level. */
.post .pmore {
  margin-top: auto; padding-top: 16px;
  font-size: 14.5px; font-weight: 600; color: var(--green);
}
a.post:hover .pmore { text-decoration: underline; }

/* the blog's "all articles" link sits left, under the grid, unlike the
   product band's which is right-aligned against the tile block */
.allp--blog { text-align: left; margin-top: 44px; max-width: none; }
/* The product band's version of this link is styled by `.band .allp a`, and
   this section is a <section> rather than a .band — so until now it rendered
   as an unstyled browser-default blue underline. A text link with an arrow, at
   the same weight as "Read more" so the two read as the same kind of action. */
.allp--blog a {
  color: var(--green); font-weight: 600; font-size: 15.5px;
  text-decoration: none; letter-spacing: .01em;
}
.allp--blog a:hover { text-decoration: underline; }

/* THE TAIL OF THE GRID, and the class that hides it sits on the GRID rather
   than on the cards. One class toggled in one place, and — more to the point —
   the cards are visible until something removes them, so a script that never
   runs cannot swallow five articles.

   `display: none` and not a height or opacity animation: these are grid items,
   and a collapsing row that still occupies a track leaves a hole in the layout.
   The arrival is animated instead — the five cards keep the .rv-card entrance
   every other card on the page has, and because a display:none element never
   intersects, their observer has not fired yet. Un-hiding them IS their
   entrance, staggered, with no new animation written for it. */
.blog--collapsed .post--rest { display: none; }

/* The disclosure, matched to the anchor beside it so the swap is invisible.
   A button carries none of an anchor's styling, so the reset is the price of
   the right element. The [hidden] guard is not optional: `display` set here
   would beat the UA's [hidden] rule and the button would show before app.js
   has decided anything. */
.blogall { display: inline-block; }
.blogall[hidden] { display: none; }
button.blogall {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; cursor: pointer;
  color: var(--green); font-weight: 600; font-size: 15.5px;
  letter-spacing: .01em;
}
button.blogall:hover { text-decoration: underline; }

/* ---- 11 · CLOSING CTA --------------------------------------------------- */
.cta { background: var(--teal); color: #fff; text-align: center; padding: 82px 0; }
.cta h2 { color: #fff; font-size: 50px; margin: 0 0 28px; }
/* This line was fine print in the mockup — 15px at 85% white, sized for
   "Malaysia-based team · Response within one business day". It now carries the
   company's creed, which is a statement rather than a footnote, so it is a
   little larger and fully opaque. Still set in the body face, not the display
   one: the mission heading upstairs already says this sentence loudly, and
   saying it loudly twice would be shouting. Revert to `15px` and `/ .85` for
   the mockup's weighting. */
.cta .fine {
  margin-top: 26px; font-size: 18px; line-height: 1.5; color: #fff;
  max-width: 34ch; margin-inline: auto;
}

/* ---- 12 · FOOTER -------------------------------------------------------- */
footer { background: var(--footer); color: #C9C7C7; padding: 60px 0 0; font-size: 15px; }
footer img { height: 38px; width: auto; margin-bottom: 26px; }
/* The footer logo is wrapped in a link now. The margin has to move to the
   anchor, or the inline-level <a> keeps its own baseline gap and the columns
   below shift down by a few pixels. */
footer .flogo { display: inline-block; margin-bottom: 26px; }
footer .flogo img { margin-bottom: 0; display: block; }
.fgrid { display: grid; grid-template-columns: 1.15fr 1fr 1fr .9fr 1.05fr; gap: 36px; }
.fcol h4 {
  font-size: 12.5px; letter-spacing: .14em; text-transform: uppercase;
  margin: 0 0 16px; font-weight: 600; padding-bottom: 9px;
}
/* five columns, five different rules — the device that stops the footer
   reading as one undifferentiated block of links */
.fcol:nth-child(1) h4 { color: #3FD4B0; border-bottom: 2px solid #3FD4B0; }
.fcol:nth-child(2) h4 { color: #8FD6C4; border-bottom: 2px solid #8FD6C4; }
.fcol:nth-child(3) h4 { color: #E8D86A; border-bottom: 2px solid #E8D86A; }
.fcol:nth-child(4) h4 { color: #9FB8CC; border-bottom: 2px solid #9FB8CC; }
.fcol:nth-child(5) h4 { color: #D8C9A0; border-bottom: 2px solid #D8C9A0; }
.fcol a, .fcol span.soon { display: block; color: #C9C7C7; text-decoration: none; padding: 5px 0; font-size: 15px; }
.fcol a:hover { color: #fff; }
.fcol .book { color: #3FD4B0; font-weight: 600; }

.news {
  border-top: 1px solid #4E4C4C; margin-top: 50px; padding: 34px 0;
  display: flex; gap: 28px; align-items: center; justify-content: space-between; flex-wrap: wrap;
}
.news p { margin: 0; max-width: 46ch; font-size: 15px; }
.news p b { color: #fff; display: block; font-size: 16.5px; margin-bottom: 4px; font-weight: 600; }
.newsform { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.newsform input {
  border: 0; border-radius: var(--r); padding: 12px 16px; width: 280px;
  font-family: var(--f-text); font-size: 15px;
}
.newsform input:disabled { background: #575555; color: #9E9C9C; }
.newsform .btn[aria-disabled="true"] { background: #575555; color: #9E9C9C; cursor: default; }

.legal {
  border-top: 1px solid #4E4C4C; padding: 24px 0 40px; font-size: 14px; color: #9E9C9C;
  display: flex; gap: 20px; justify-content: space-between; flex-wrap: wrap;
}
.legal a { color: #9E9C9C; text-decoration: none; }
.legal a:hover { color: #fff; }

/* =============================================================================
   BEYOND THE MOCKUP
   The mockup drew a caret on three nav items but had nowhere for them to go,
   had no menu below 820px, and had no focus states. A shipped page needs all
   three.
   ========================================================================== */

/* ---- 13 · NAV PANELS ---------------------------------------------------- */
.navitem { position: relative; display: flex; align-items: center; }
nav.mid > .navitem > button[aria-expanded="true"] { color: var(--teal); }
nav.mid > .navitem > button[aria-expanded="true"] .car { transform: rotate(180deg); }
.car { transition: transform .18s var(--ease); }

/* Anchored under the whole header rather than under the button, so all three
   panels open on the same line and the bar reads as one surface. */
.panel {
  position: fixed; left: 0; right: 0; top: var(--hdr-h);
  background: #fff; border-bottom: 1px solid var(--border);
  box-shadow: 0 22px 44px -26px rgb(31 36 48 / .26);
  z-index: 49;
}
.panel[hidden] { display: none; }
.panel > .wrap { padding-block: 30px 26px; }

.pgrid { display: grid; gap: 8px 26px; }
.pgrid--products { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.pgrid--solutions { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.pgrid--company { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.pitem {
  display: flex; gap: 13px; align-items: flex-start;
  padding: 11px 12px; border-radius: 9px; text-decoration: none;
  transition: background .14s var(--ease);
}
a.pitem:hover { background: var(--band); }
.pitem .pico {
  flex: 0 0 38px; height: 38px; border-radius: 10px; background: var(--band);
  display: flex; align-items: center; justify-content: center;
}
.pitem .ptx { min-width: 0; }
.pitem .pn { display: block; font-size: 15.5px; font-weight: 600; color: var(--ink); line-height: 1.3; }
.pitem .pd { display: block; font-size: 13.5px; line-height: 1.45; color: var(--dim); margin-top: 3px; }

.pfoot {
  border-top: 1px solid var(--border); background: var(--band);
}
.pfoot > .wrap {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding-block: 13px; font-size: 14px; color: var(--dim); flex-wrap: wrap;
}
.pfoot a { color: var(--green); font-weight: 600; text-decoration: none; }
.pfoot a:hover { text-decoration: underline; }

/* ---- 14 · PLACEHOLDER + UNLINKED MARKERS -------------------------------- */
/* A destination that does not exist yet renders as text with this marker
   rather than as a link that 404s. It is deliberately quiet — the reader who
   does not care never notices it. */
.soon { color: inherit; }
.soon::after {
  content: "soon"; margin-left: 7px;
  font-size: 10.5px; letter-spacing: .07em; text-transform: uppercase;
  color: var(--quiet); vertical-align: 1.5px;
}
.panel .soon { display: block; }

/* A product that IS live but has no public front door — the AI Engine, whose
   console is an internal surface. It reads as ordinary text: no marker,
   because there is nothing pending about it, and no link, because there is
   nowhere public to send anyone. */
.nolink { color: inherit; }
.fcol .nolink { display: block; padding: 5px 0; font-size: 15px; }
.drawer .nolink { display: block; padding: 9px 0; font-size: 16.5px; font-weight: 500; color: var(--ink); }

/* ---- 15 · MOBILE MENU --------------------------------------------------- */
.burger {
  display: none; width: 42px; height: 42px; margin-right: -8px;
  background: none; border: 0; cursor: pointer;
  place-items: center; padding: 0;
}
.burger i, .burger i::before, .burger i::after {
  display: block; width: 20px; height: 2px; background: var(--ink); border-radius: 2px;
  transition: transform .2s var(--ease), opacity .16s;
}
.burger i::before, .burger i::after { content: ""; position: absolute; }
.burger i::before { transform: translateY(-6px); }
.burger i::after { transform: translateY(6px); }
.burger[aria-expanded="true"] i { transform: rotate(45deg); }
.burger[aria-expanded="true"] i::before { opacity: 0; }
.burger[aria-expanded="true"] i::after { transform: rotate(-90deg); }

.drawer {
  position: fixed; inset: var(--hdr-h) 0 0; z-index: 48;
  background: #fff; overflow-y: auto;
  padding: 18px 32px 48px;
  display: none;
}
.drawer[data-open="true"] { display: block; }
.dgrp { border-bottom: 1px solid var(--border); padding: 6px 0 14px; }
.dgrp > h4 {
  font-size: 12.5px; letter-spacing: .13em; text-transform: uppercase;
  color: var(--quiet); font-weight: 600; margin: 14px 0 6px;
}
.drawer a, .drawer .soon {
  display: block; padding: 9px 0; color: var(--ink);
  text-decoration: none; font-size: 16.5px; font-weight: 500;
}
.drawer .dcta { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 22px; }
.drawer .dcta .btn { flex: 1 1 auto; text-align: center; }

/* ---- 16 · FOCUS + MOTION ------------------------------------------------ */
:focus-visible {
  outline: 2.5px solid var(--teal);
  outline-offset: 3px;
  border-radius: 3px;
}
.btn-white:focus-visible, .btn-outline-white:focus-visible, .cta :focus-visible { outline-color: #fff; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}

/* ---- and a finger gets the micro-transitions too ------------------------
   THIRTY-SIX :hover RULES AND ONE :active MEANS A TAP GOT NO ANSWER. Every
   micro-transition on this page hangs off :hover, which a finger does not
   have. Touch browsers fake it — :hover latches on tap and stays latched
   until you touch something else — so the tile you pressed either did
   nothing at the moment you pressed it, or lifted and stayed lifted after
   you left. Both read as an unresponsive page, and it was half of what was
   reported as "no micro transitions on mobile".

   MIRRORED, NOT INVENTED. Each rule below repeats the exact declaration its
   :hover twin already uses, and the transition that drives it is already on
   the element (.tile carries transform .22s, .post .thumb carries .2s). So a
   tap moves the same distance on the same curve over the same duration as a
   pointer — the same feel, which is the whole requirement, rather than a
   second animation language for phones.

   (hover: none) is the query for "no hovering pointer", so desktop never
   sees any of this. The tap-highlight reset goes first because the browser's
   own grey flash arrives before our feedback and fights it. */
@media (hover: none) {
  a, button, .tile, .post, .modes, summary { -webkit-tap-highlight-color: transparent; }

  .tile:active { transform: translateY(-6px); }
  .tile:active .card { box-shadow: 0 14px 34px rgb(2 168 133 / .16), 0 2px 6px rgb(31 36 48 / .06); }
  /* a tile that is not offering anything must not answer the press */
  .tile[aria-disabled="true"]:active { transform: none; }
  a.post:active .thumb { transform: scale(1.02); }
  .ytlite:active .ytplay::before { transform: scale(1.07); background: var(--green); }
  .btn-primary:active { background: var(--green); }
  .btn-ghost:active { background: #E7E9EC; }
}
/* The reduced-motion block above kills the duration, not the property, so the
   press would still jump. Under both, a tap changes colour and nothing moves. */
@media (hover: none) and (prefers-reduced-motion: reduce) {
  .tile:active, a.post:active .thumb, .ytlite:active .ytplay::before { transform: none; }
}

/* ---- 17 · RESPONSIVE ---------------------------------------------------- */
@media (max-width: 1080px) {
  /* The rows reflow here: five across becomes three, four becomes two. */
  .prow-5 { grid-template-columns: repeat(3, 1fr); gap: 24px; }
  .prow-4 { grid-template-columns: repeat(2, 1fr); gap: 24px; }
  /* NOTHING TO HOVER WITH, SO THE PILL IS THE CONTROL. From here down the panel
     is a disclosure: closed until the pill is tapped, in the flow under its own
     tile rather than floating over the grid. It used to be forced permanently
     open here, which made every tile with modes twice as tall and pushed the
     rest of the grid down for people who had not asked to see the modes.

     `display` and not just opacity: a closed panel has to leave the flow, or
     the gap it holds open is the same defect in a quieter form.

     The connector goes with the hover — a tree drawn to a panel that never
     moved is a diagram of nothing. */
  /* NAME BOTH VARIANTS OR THE RESET SILENTLY LOSES. `.branch.b-mid` carries the
     desktop centring offset at two-class specificity, so a `.branch` reset here
     is outranked no matter how late it comes — the panel kept `margin-left:
     -176px`, which on a static block both shifts it 154px off the left edge of
     a 390px screen AND widens it to 343px, so it landed on the next column. */
  .branch, .branch.b-left, .branch.b-mid {
    position: static; left: auto; width: auto; margin: 0 6px 8px;
    opacity: 1; visibility: visible; transform: none;
  }
  /* SCOPED TO html.js, WHICH IS THE WHOLE POINT. The panel only collapses when
     script is actually present to reopen it; with JS off or app.js broken the
     rule never applies and the modes stay visible, as they were before this was
     a disclosure. Nothing is hidden behind script — see the note on the inline
     flag in build.js. */
  html.js .branch { display: none; }
  html.js .cell.is-open .branch { display: block; }
  .branch .tree { display: none; }
  .minis { box-shadow: none; padding: 10px 6px 8px; }
  /* the chevron is the affordance again: it points down when closed and turns
     over when open, which is the only thing telling a thumb the pill does
     something. It was hidden here while the panel was always open. */
  .modes svg { display: block; }
  /* hover is meaningless on a touch screen, and leaving it on means the pill
     keeps the open styling after a tap-then-close on hybrid devices */
  .cell:hover .modes { background: #E7F7F2; color: var(--teal); }
  .cell:hover .modes svg { transform: none; }
  .cell.is-open .modes { background: var(--teal); color: #fff; }
  .cell.is-open .modes svg { transform: rotate(180deg); }
  .capgrid { grid-template-columns: repeat(2, 1fr); }
  .fgrid { grid-template-columns: repeat(3, 1fr); }
  .pgrid--products { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  h1.hand { font-size: 62px; }
}

@media (max-width: 900px) {
  .pgrid--products, .pgrid--solutions, .pgrid--company { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The featured episode stacks a breakpoint EARLIER than the rest of the
     content, which stacks at 820. A 16:9 player beside a paragraph is the one
     two-column block on the page with a hard minimum: below about 900 the
     player is under 450px and the prose column is down to ~330, and both are
     worse than either would be full width. Video first is DOM order already,
     so nothing needs reordering — the two-column rule just stops applying. */
  .tlfeat { grid-template-columns: 1fr; gap: 26px; align-items: start; }
  .tlfeat-show { font-size: 30px; }
  .tlfeat-desc { max-width: none; }
}

/* Below this the centre nav is gone, so the header becomes logo + actions and
   the panels are replaced by one drawer holding all of them.

   The mockup put this at 820px, but it was drawn at ~1900px and never tested
   in between: five nav items at 34px gaps, plus "Sign in" and a filled
   button, start wrapping the header at about 900px. So the drawer takes over
   at 1000px, while there is still room for it to look deliberate. */
/* THE HEADER ONLY. The nav has to give up before the CONTENT does — five nav
   items plus two actions run out of room around 900px, while a three-column
   card grid is still perfectly comfortable there. So this block swaps the nav
   for the drawer and touches nothing else; the content keeps its desktop
   layout down to 820px in the next block.

   (These were one block at 820px, which is how the mockup had it. Raising it
   to 1000px for the nav dragged the phone content layout up with it and gave
   tablets a single-column blog. Two concerns, two breakpoints.) */
@media (max-width: 1000px) {
  nav.mid { display: none; }
  .panel { display: none !important; }
  .burger { display: grid; }
  .hdr { grid-template-columns: 1fr auto; }
  .hdr-right { gap: 12px; }
  .hdr-right a.si { display: none; }
}

/* THE CONTENT. Everything stacks here. */
@media (max-width: 820px) {
  h1.hand { font-size: 44px; }
  h2.hand { font-size: 36px; }
  .cta h2 { font-size: 36px; }
  .phead h2.hand { font-size: 33px; }
  /* minmax(0,…), not 1fr. A bare `1fr` is `minmax(auto, 1fr)`, so the track
     cannot go below its content's min-content width — "Intelligent Recruitment
     Software" held a 124px floor and two of them plus the gap pushed the page
     to 381px wide, which is why the homepage scrolled sideways on a 360px
     phone. Same idiom as .pgrid--products above. */
  .prow-4, .prow-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
  .tile .card { width: 104px; height: 104px; border-radius: 22px; margin-bottom: 16px; }
  .tile .nm { font-size: 17px; }
  .tile .ds { font-size: 13.5px; }
  .glabel { margin: 34px 0 4px; }
  /* Two tiles across, so a mini's column is down to about 100px — narrower than
     "Conversational Chat Interview" wants. Stacked, the icon sits beside the name and
     the whole name gets the width. */
  .minis { grid-template-columns: 1fr; }
  .mini { display: flex; align-items: center; gap: 10px; text-align: left; }
  .mini svg { margin: 0; }
  .mini b { font-size: 13px; }
  .pblock > .glabel:first-child { margin-top: 44px; }
  .pengine { margin-top: 34px; }
  .capgrid { grid-template-columns: 1fr; }
  .blog { grid-template-columns: 1fr; gap: 40px; }
  /* the wrap's own padding drops to 22px here, so the bar's indent comes in
     with it — 22 inside 22 reads as a margin error */
  .blog-lede { padding-left: 16px; }
  .whygrid { grid-template-columns: 1fr; gap: 34px; margin-top: 38px; }
  .why { padding: 68px 0; }
  .cap.wide { grid-column: span 1; }
  /* same reason as .prow above: "AI Strategy & Consulting" set a 148px floor on
     a track that had 120px to give, and the footer overhung the page */
  .fgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .caps { border-radius: 0 50px 0 0; }
  section { padding: 68px 0; }
  .band { padding: 56px 0 68px; }
  .wrap { padding: 0 22px; }
  .drawer { padding-inline: 22px; }
  .newsform input { width: 100%; }
  .newsform { width: 100%; }
}

/* THREE, TWO, ONE. The old window here was 640-820px, written when the grid
   held eight small cards; with three editorial placards a 900px screen was
   still being given three columns and each cover came out 238px wide. The
   ladder now turns at 1080 and at 640, so a card is never narrower than about
   300px. Ordered after the 1080 block so it wins inside its own range. */
@media (min-width: 640px) and (max-width: 1079px) {
  .blog { grid-template-columns: repeat(2, 1fr); gap: 44px 36px; }
}

@media (max-width: 460px) {
  h1.hand { font-size: 36px; }
  .hdr-right .btn { padding: 10px 15px; font-size: 14.5px; }
  .prow-4, .prow-5 { gap: 16px; }

  /* ---- THE TWO-COLUMN GRID LINES UP, INCLUDING THE THREE-LINE NAMES ----
     The base rule reserves two lines for a product name, which levels most
     rows. Below 460px a column is under 200px and four names wrap to THREE —
     "Intelligent Recruitment Software", "Business Management System", "Task &
     Productivity Manager", "Intelligent Document Management" — and each of
     those pushed its own description 21px below the one in the column beside
     it. Measured at 390px: Chat Interview's caption started at y1406 and
     Intelligent Recruitment Software's at y1427.

     THREE IS MEASURED, NOT PICKED. Three lines is the most any name takes at
     every width from 300px to 455px; at 460px and above the longest is two,
     which is why this is scoped to the phone range and the base rule is left
     alone. One band for every tile, so nothing is nudged individually: the
     card is a fixed 104px square, the name band is a fixed three lines, and
     the caption therefore starts on the same baseline in both columns whether
     the name above it took one line or three.

     Scoped to `.prow` on purpose. The Private AI Engine tile sits alone in
     `.pengine` with nothing beside it to line up with, so a band bought for
     alignment there would be blank space bought for nothing.

     EVERY NAME IS TWO LINES, AND THAT IS WHY THE BAND IS TWO. The halves come
     from `nameBreak` in home.js, split there rather than here because no CSS
     rule produces the editorial breaks the design asks for — "Sales / CRM" is
     two lines even though it fits on one. With every title exactly two lines
     the band needs no slack, so nothing carries a reserved blank line: the
     captions line up because the titles are the same height, not because the
     titles were padded to the tallest one. */
  .prow .tile .nm-a, .prow .tile .nm-b { display: block; }
  /* THE NAME TAKES THE WHOLE CELL, NOT THE PADDED BOX. `.tile` carries 10px of
     side padding for its hover lift, which the title does not need: it is
     centred text with nothing beside it to collide with. Cancelling that
     padding for the name alone hands back 20px, and 20px is the difference
     between "Recruitment Software" (182px at 17px) fitting on its line and
     spilling onto a third. Nothing else in the tile moves. */
  .prow .tile .nm { margin-inline: -10px; min-height: calc(2 * 1.2em); }
  /* THE BAND LABEL IS THREE THINGS THAT CANNOT SHRINK. A nowrap name, a rule
     that gives back at most its own flex, and a nowrap count — so below about
     380px "5 products" hung 21px off the right edge and took the whole
     homepage sideways with it on a 360px phone. The name is the only part with
     somewhere to go, so it is the part allowed to wrap; the count stays on one
     line because "5 / products" would read as two facts. */
  .glabel { gap: 12px; }
  .glabel .t { white-space: normal; }
  /* 36px of gutter between two 120px columns is 11% of a 320px screen, and it
     was the last 6px of sideways scroll on the narrowest phones — the tracks
     are already minmax(0,…), so the gap is the part with room to give. */
  .fgrid { gap: 24px; }
}

/* ---- NARROWER THAN 416px, ONE NAME WILL NOT FIT ON TWO LINES ----------
   Every product name is split into two lines above, and at 416px and wider
   both halves fit — the captions then line up with no reserved blank space,
   which is the intended result. 416 is measured, not chosen: it is the width
   at which "Recruitment Software", the longest half in the set, still fits a
   cell (the cell is (W - 44 wrap - 16 gap) / 2, and that half needs the lot).

   BELOW IT, THE LONGEST HALVES TAKE A THIRD LINE, so the band has to allow
   for three or the captions go back to being 20px apart. This is the same
   trade as the base rule and the reason it is a band at all: a name that runs
   long is levelled up to the tallest rather than every caption finding its own
   height. Only the narrowest phones pay the blank line, and only on the two
   or three names that actually run over.

   Type size is deliberately NOT reduced to make them fit. It would need to go
   from 17px to about 14px to buy that, at which point the fix has changed the
   typography it was asked to leave alone. */
@media (max-width: 415px) {
  .prow .tile .nm { min-height: calc(3 * 1.2em); }
}


/* ---- 18 · THE DRAWING --------------------------------------------------- */
/* The page is a drawing of the suite that assembles itself as you read it.
   Construction rules register the grid, the eight products resolve into their
   cells, and the wiring that is actually there draws itself between them.

   THREE RULES, AND THEY ARE WHY THIS IS NOT A SHOW.
   1. Nothing is ever covered. There is no overlay, no fixed layer, no canvas
      over the page. Every drawn thing lives inside the section it belongs to
      and stops at that section's edges. The header is never obscured.
   2. Nothing is hidden by default. Every rule that hides something is scoped to
      `html.fx`, a class scroll.js adds only once it has decided to animate. No
      JS, reduced motion, or a phone, and nothing here applies — the page is the
      page, complete, with the drawing already finished.
   3. Only opacity, translate, scale, filter and stroke-dashoffset move. All are
      compositor- or paint-cheap, and none of them cause layout. */

:root {
  /* Instant departure, long settle. This is the curve that makes motion read as
     mechanical rather than bouncy, and it is doing most of the work here. */
  --ease-out: cubic-bezier(0, 0, 0, 1);
  --ease-soft: cubic-bezier(0.2, 0, 0, 1);

  /* Hairlines are far fainter than they feel like they should be. At 5.5% of
     the brand ink a rule reads as a construction guide; at 15% it reads as a
     border, and the drawing turns into a table. */
  --rule: rgb(31 36 48 / .055);
  --rule-firm: rgb(31 36 48 / .10);

  /* the dark chapter's ground — the brand ink, taken most of the way down */
  --well: #0C1014;
}

/* ---- the reveal ---------------------------------------------------------- */
/* The single treatment that used to live here — 12px, 3px of blur, 400ms, on
   everything — has been replaced by the four-treatment kit in §25. What stays is
   the contract: nothing is hidden unless html.fx is set, and .in is what unhides
   it, so a page with no script showing has nothing to unhide. */

/* ---- the band's drawing surface ------------------------------------------ */
/* REMOVED, by request: the plate, the construction rules, the registration
   marks, the wiring and the pin. This band used to become 152vh tall, stick
   under the header, and assemble itself against scroll position — `--draw`,
   `--tick`, `--tile`, `--wire`. The section is a plain section now; §19's
   connectors are static and the only thing left in the drawing. If it is ever
   wanted back it is in git, together with the geometry code in scroll.js that
   re-derived it from the real cards at every width. */

/* ---- the core pipeline ---------------------------------------------------- */
/* The engine's four stages on one rail, each with the plain sentence for what it
   does underneath. Every stage is drawn identically and permanently: there is no
   active stage, no progression and no state, because the reader should not have
   to scroll to be told what a word means.

   THE GEOMETRY IS DERIVED FROM ONE SET OF NUMBERS. The rail has to cross the
   nodes exactly, and the nodes sit below a line of type — so rather than
   hand-tuning a `top` until it looks right, the label's line-height, the gap
   and the node's own size are variables and the rail's offset is calculated
   from them. Change the type size and the rail follows it. */
.core {
  --stageLH: 15px;
  --nodeGap: 11px;
  --nodeSize: 7px;
  --nodeY: calc(var(--stageLH) + var(--nodeGap) + var(--nodeSize) / 2);
  margin: 40px 0 0;
}
/* The list's own box, and the only thing `.flow` positions against. It has the
   list's width and starts at the list's top, so an offset measured from here is
   the same offset measured from the first row of type. */
.pipe { position: relative; max-width: 640px; }
.pipe .arc { width: 100%; }
/* A div, not a p — the section sets `.eco .ecotext p` at body size and it would
   win on specificity, which is how this first rendered at 19px looking like a
   second heading. */
.core-eyebrow {
  margin: 0 0 26px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: #7E8B95;
}
.arc {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}
/* ONE LINE UNDER ALL FOUR, which is the entire claim: these are one continuous
   path through one engine, not four things that happen to be adjacent. It runs
   between the outer nodes only — 12.5% is the centre of the first column. */
.arc::before {
  content: "";
  position: absolute;
  left: 12.5%; right: 12.5%; top: var(--nodeY);
  height: 1px;
  background: var(--rule-firm);
}
.arc li {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.arc .stage {
  font-size: 12.5px;
  line-height: var(--stageLH);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #4B5563;
}
.arc .node {
  width: var(--nodeSize);
  height: var(--nodeSize);
  margin-top: var(--nodeGap);
  border-radius: 50%;
  background: var(--teal);
  box-shadow: 0 0 0 3px var(--band);
}
/* The tick down to the plain label, so the label is visibly tied to its own
   node rather than floating under the row. */
.arc .plain {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-size: 10.5px;
  line-height: 1.4;
  font-weight: 500;
  letter-spacing: .03em;
  color: #7E8B95;
  max-width: 13ch;
}
.arc .plain::before {
  content: "";
  width: 1px;
  height: 15px;
  margin-bottom: 11px;
  background: var(--rule-firm);
}

/* THE ONE MOVING PART, and it is on a clock rather than on the scroll. A small
   light crosses the rail every nine seconds and that is all it does: it never
   fills, never stops on a stage and never leaves anything behind, so it reads
   as a live line rather than as a progress bar. `.flow` spans exactly the rail,
   its child is inset to the same width, and the child translates by 100% of
   ITSELF — which is therefore 100% of the rail, with no JS measuring anything. */
.pipe .flow {
  position: absolute;
  left: 12.5%; right: 12.5%;
  top: calc(var(--nodeY) - 1.5px);
  height: 3px;
  overflow: clip;
  pointer-events: none;
}
.pipe .flow::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(4px 1.5px at 0 50%, rgb(2 168 133 / .95), rgb(2 168 133 / 0) 100%);
  animation: coreflow 9s linear infinite;
}
@keyframes coreflow {
  0%   { transform: translateX(0);    opacity: 0; }
  8%   { opacity: 1; }
  92%  { opacity: 1; }
  100% { transform: translateX(100%); opacity: 0; }
}

@media (max-width: 700px) {
  .pipe { max-width: 100%; }
  .arc { grid-template-columns: repeat(2, 1fr); row-gap: 30px; }
  /* At two columns a single rail across four nodes would be a lie — the third
     node is on the next row. The line goes, the nodes stay. */
  .arc::before, .pipe .flow { display: none; }
  .arc .plain::before { height: 11px; margin-bottom: 9px; }
}
@media (max-width: 360px) {
  .arc { grid-template-columns: 1fr; row-gap: 24px; }
}

@media (prefers-reduced-motion: reduce) {
  html.fx [data-reveal] { transition-duration: .01ms; transition-delay: 0ms; }
  /* The one ambient loop in the page stops. Nothing else here moves. */
  .pipe .flow { display: none; }
}

/* ---- 19 · THE SURFACE ---------------------------------------------------- */
/* A 1px line every 8px across the product band. It is `::after` and the wash in
   §20 is `::before` SPECIFICALLY so this paints on top: the wash is made of
   opaque colours, and drawn second it would cover the texture completely. White
   lines over a colour field is also exactly the order attio.com uses.

   A 1px line every 8px across the product band. Borrowed straight from attio.com,
   which runs the same period over both its light and dark sections — it is most
   of why their surfaces read as engineered rather than as flat fills.

   LIGHTER THAN THE GROUND, NOT DARKER. On the grey band an ink pinstripe at any
   visible alpha reads as dirt or a print artefact; white at 55% reads as woven
   paper. Attio does the same thing in the other direction, laying white over a
   saturated gradient rather than black over a pale one.

   It is behind everything the band draws, including the connectors, so the two
   never compete: 8px is far below the connectors' spacing, which keeps it as
   texture rather than as a second grid to read. */
.band { position: relative; }
.band::after {
  content: "";
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    90deg,
    rgb(255 255 255 / .55) 0 1px,
    transparent 1px 8px
  );
  /* Fade it out at both ends so the band does not start and stop with a hard
     edge of texture — the pattern should feel like the surface the section is
     printed on, and a surface does not have a seam in the middle of the page. */
  -webkit-mask-image: linear-gradient(transparent, #000 12% 88%, transparent);
  mask-image: linear-gradient(transparent, #000 12% 88%, transparent);
  pointer-events: none;
  z-index: 0;
}
.band > * { position: relative; z-index: 1; }

/* ---- 20 · THE ATMOSPHERE ------------------------------------------------- */
/* A cool tint gathering toward the bottom of the product band. It used to run
   all the way to a mint, because the section underneath was mint; that section
   is now the dark chapter, so the band stays light and clean and hands the
   dimming over at its own edge. §21 does the going-dark.

   The signature is measured, from Attio's hero and six sibling gradients on
   Stripe's homepage: an oversized ellipse, origin at or below the bottom edge so
   the light source is off-canvas, six to eight stops widening toward the end,
   and a terminal stop that is EXACTLY the section's own background. Terminating
   in the page is what leaves the wash with no boundary anywhere. */
.band::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(94% 46% at 50% 106%,
    #BBD9D6   0%,   /* off-canvas — never rendered */
    #CBE1DE  14%,
    #D9E8E6  28%,
    #E4EEEC  42%,
    #EBF1F0  57%,
    #EFF3F3  74%,
    var(--band) 100%);
  pointer-events: none;
  z-index: 0;
}

/* ---- 21 · THE DARK CHAPTER ----------------------------------------------- */
/* The page runs light from the hero down to here, goes dark for one section,
   and comes back up. That is the eclipse — not a disc crossing the viewport,
   but the page itself dimming for one chapter and returning.

   IT HAS TO BE DARK, AND THAT IS THE WHOLE POINT. A wireframe well, five
   falling signals and four lit steps are all low-contrast marks. On a white
   ground a low-contrast mark is invisible, which is precisely why the first
   attempt at this — a pale iris on the mint band — read as a smudge no matter
   how it was tuned. The identical figure on near-black reads as an instrument.
   Attio's own version is on a dark section; on their light page they drop it
   entirely, because it does not transfer.

   NO RAMP AT EITHER END, by request. This used to ease in from the product
   band's grey and back out to the page's ground, so the dark started a third
   of the way down the section and the top third rendered as a pale wash. The
   section is one flat well now: the black meets the product band on a hard
   edge and leaves on one. */
.eco {
  position: relative;
  overflow: clip;
  border-top: 0;
  border-bottom: 0;
  color: rgb(255 255 255 / .72);
  background: var(--well);
}

/* NO PIN. This section used to set `min-height: 188vh` and hold `.ecopin`
   sticky under the header, so the reader spent nearly two viewports of scroll
   watching four words light up one at a time. Removed by request. The section
   is now exactly as tall as its content, it scrolls past like any other, and
   `.ecoinner` is a plain box — it exists only to carry the vertical padding. */
.ecoinner { padding: 104px 0 112px; }
@media (max-width: 720px) { .ecoinner { padding: 76px 0 84px; } }

/* The claim on the left, the surface it describes on the right, a hairline
   between them — the split Attio uses for exactly this moment, and the reason
   it works is that the diagram is never asked to sit behind the words. */
/* IT STACKS NOW, AND THAT IS THE WHOLE CHANGE. This was a two-column split —
   the claim and the pipeline on the left, the diagram squeezed into the right
   half — which capped the figure at 663px on a 1200px screen and 810px at the
   very widest. The figure is 1920 units of three-column diagram carrying
   twenty-odd labels, so at that width its type landed around 9px and the note
   in §21 had to argue for every pixel it could get from the split.

   Stacked, the figure gets the whole measure. At 1560px that is 1464px against
   810px before: the same labels at nearly twice the size, and the reason the
   split existed — keeping the diagram out from behind the words — is satisfied
   better by putting it under them than beside them.

   THE SECTION ALSO STOPS OBEYING .wrap. Every other section on this page is
   1136px because it is reading width, and prose past about 75 characters is
   hard to track. This one is a diagram, which has the opposite constraint, so
   it takes min(1560px, the viewport less a margin) instead. */
.ecogrid { display: grid; grid-template-columns: 1fr; gap: 48px; }
.eco .wrap.ecogrid { max-width: min(1560px, 100% - 80px); }
.ecotext { position: relative; z-index: 1; }

/* The pipeline was 640px because it sat in a half-width column. It has the
   full measure now, so it takes it — the four stages spread out rather than
   crowding into the left third of a wide screen. */
.eco .core { margin: 8px 0 0; }
.eco .pipe { max-width: none; }

/* ---- and below 900px it is a different layout, not a squeezed one -------
   THE FOUR STAGES GO TWO-UP, THEN ONE-UP. Four columns on a phone gives each
   stage about 78px, which breaks "Intelligent path" over three lines and turns
   a row of four short labels into a wall. The connecting rail is dropped with
   the row: a horizontal line under a vertical stack is not the same claim, it
   is just a stray rule. */
@media (max-width: 900px) {
  .eco .wrap.ecogrid { max-width: min(1560px, 100% - 40px); }
  .arc { grid-template-columns: repeat(2, 1fr); gap: 34px 20px; }
  .arc::before { display: none; }
  .pipe .flow { display: none; }
}
/* TWO-UP HOLDS DOWN TO A NARROW PHONE, which is what the reference shows and
   what the measurements allow: at 390px each stage still gets about 165px, and
   the longest caption under one — "Intelligent path" — needs well under that.
   Stacking all four instead would triple the height of the block for no gain
   in legibility. One column is kept for the genuinely narrow end only. */
@media (max-width: 379px) {
  .arc { grid-template-columns: 1fr; gap: 26px; }
}

/* THE DIAGRAM SCROLLS SIDEWAYS ON A PHONE RATHER THAN SHRINKING TO NOTHING.
   It is a 16:9 figure holding three columns of labels; at 360px wide the type
   would be under 4px, which is not small, it is absent. So below 900px it
   keeps a legible minimum width and the reader swipes it — the same bargain
   every wide table on the web makes, and the alternative is a decorative
   smudge that says nothing.

   The overscroll-behavior-x rule stops a swipe that reaches the end of the
   figure from turning into a browser back-gesture, which is the one thing that
   makes a horizontal scroller feel broken. */

@media (max-width: 900px) {
  .ecowell {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 10px;
  }
  .ecowell .nloop { min-width: 720px; }
  /* a hairline the scrollbar can sit on, so the overflow is visible as an
     affordance rather than discovered by accident */
  .ecowell::-webkit-scrollbar { height: 5px; }
  .ecowell::-webkit-scrollbar-thumb { background: rgb(255 255 255 / .22); border-radius: 3px; }
}

/* ---- PHONES: THE FIGURE FITS, AND THAT IS A TRADE THAT WAS CHOSEN ------
   REQUESTED, AND IT REVERSES THE ARGUMENT DIRECTLY ABOVE. The swipe above
   keeps the type legible at the cost of showing about 40% of the figure at a
   time; on a phone that read as a cut-off diagram rather than a scrollable
   one, so the choice here is the other side of the same bargain: the whole
   figure, on one screen, edge to edge.

   BE HONEST ABOUT WHAT THAT COSTS. 1920 units into 390px is a scale of about
   0.20, which puts the column headings near 3.7px and the node labels near
   3.2px. Nobody reads those. Below 520px this figure is therefore DECORATION —
   it carries the shape of the architecture, not its words, and every label in
   it is also written in prose elsewhere in the section. That is the only
   reason it is acceptable to shrink: nothing is only here.

   Scoped to 520px, not the 900px above it, because the tablet band still has
   enough width for the swipe to be worth taking — this is strictly the phone
   layout, as asked.

   `aria-hidden` is NOT set here even though the figure is decorative at this
   size: the svg carries a real aria-label describing the flow, and a screen
   reader is not affected by how many pixels wide it is. */
@media (max-width: 520px) {
  /* the scroller goes with the min-width; without both, the figure is simply
     clipped instead of fitted */
  .ecowell {
    overflow-x: visible;
    padding-bottom: 0;
  }
  .ecowell .nloop { min-width: 0; }
  /* FILL THE WIDTH. The grid gives up its 20px outer margin and the figure
     escapes `.wrap`'s 22px padding, so the drawing spans the viewport instead
     of sitting in a 306px column inside a 390px screen — a 27% bigger figure
     for free, which at this scale is the difference between 3.2px type and
     4.1px. No `vw` anywhere: `100vw` would include a classic scrollbar and
     reintroduce the sideways scroll this file fixed elsewhere. */
  .eco .wrap.ecogrid { max-width: 100%; }
  .ecowell { margin-inline: -22px; }
}

/* The closing line under the figure. Same treatment as the core eyebrow above
   it, so the chapter opens and closes on the same quiet voice. */
.ecotag {
  margin: 34px 0 0;
  text-align: center;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: rgb(255 255 255 / .34);
}
@media (max-width: 560px) { .ecotag { letter-spacing: .18em; } }

/* Type for a dark ground. The eyebrow lifts to a light teal because the brand
   teal at #02A885 is too dark to read on near-black; the body drops to 72%
   white, which is the value that reads as considered rather than as low
   contrast. Both are checked against the chapter's darkest stop. */
.eco .eyebrow { color: #6FD8C0; }
.eco .ecotext p { color: rgb(255 255 255 / .72); }

/* ---- the well ------------------------------------------------------------ */
/* min-width:0 IS LOAD-BEARING. A grid item defaults to min-width:auto, so it
   refuses to shrink below its content's min-content width — and the mobile
   scroller above gives its child a 720px minimum. Without this the item takes
   720px, blows the grid past the viewport and puts a horizontal scrollbar on
   the whole PAGE: header clipped, every paragraph clipped, on every phone.
   The overflow has to happen inside the scroller, which cannot start until the
   item is allowed to be narrower than its own contents. */
.ecowell { position: relative; z-index: 0; min-width: 0; }
/* THE FIGURE ITSELF is one 16:9 exhibit that loops for ever: every duration
   divides 9s, so the cycle closes exactly where it began and there is no seam.
   It was authored as a standalone page, so its class, id and keyframe names
   are all generic — .node, .flow, .eyebrow, .cap and .well already mean
   something else here. Everything is nl- prefixed and scoped under .nloop so
   neither side can reach into the other, and the exhibit's own opaque ground
   was dropped: the section is flat black now and a grey panel would put the
   boundary back. Labels inherit Inter, which the page already loads. */
.nloop {
  display: block; width: 100%; height: auto; overflow: visible;
  font-family: var(--f-text); -webkit-font-smoothing: antialiased;
}

/* every duration divides 9s, so the loop closes exactly at t=9s */
  @keyframes nl-breathe {0%,100%{opacity:.42} 50%{opacity:1}}
  @keyframes nl-shimmer {0%,100%{opacity:.62} 50%{opacity:1}}
  @keyframes nl-travel  {to{stroke-dashoffset:-1000}}
  @keyframes nl-travelm {to{stroke-dashoffset:-952}}
  @keyframes nl-travelg {to{stroke-dashoffset:-872}}
  @keyframes nl-ring    {0%{transform:scale(.22);opacity:.55} 100%{transform:scale(2.05);opacity:0}}
  @keyframes nl-hubpulse{0%,100%{opacity:.55} 50%{opacity:1}}

  .nloop .nl-node{animation:nl-breathe 3s ease-in-out infinite}
  .nloop .nl-node.nl-dim{opacity:.6}
  .nloop .nl-wires g{animation:nl-shimmer 4.5s ease-in-out infinite}
  .nloop .nl-wires g:nth-child(2){animation-delay:-1.4s}
  .nloop .nl-wires g:nth-child(3){animation-delay:-2.5s}
  .nloop .nl-wires g:nth-child(4){animation-delay:-3.6s}

  .nloop .nl-flow path{fill:none;stroke-linecap:round;
              animation-duration:4.5s;animation-timing-function:linear;
              animation-iteration-count:infinite;animation-delay:inherit}
  .nloop .nl-flow .nl-fg{stroke:#26CCA0;stroke-width:9;opacity:.15;stroke-dasharray:150 350;
             stroke-dashoffset:128;animation-name:nl-travelg}
  .nloop .nl-flow .nl-fm{stroke:#3FE8C0;stroke-width:3;opacity:.5;stroke-dasharray:70 430;
             stroke-dashoffset:48;animation-name:nl-travelm}
  .nloop .nl-flow .nl-fh{stroke:#DCFFF5;stroke-width:3.2;opacity:.95;stroke-dasharray:22 478;
             stroke-dashoffset:0;animation-name:nl-travel}

  .nloop .nl-ring{fill:none;stroke:#3FE8C0;stroke-width:1.6;vector-effect:non-scaling-stroke;
         animation:nl-ring 2.25s linear infinite}
  .nloop .nl-hubhalo{animation:nl-hubpulse 2.25s ease-in-out infinite}

  .nloop .nl-pill{fill:#F2F3F5;font-weight:700;letter-spacing:.2px;dominant-baseline:middle}
  .nloop .nl-agent{fill:#F2F3F5;font-weight:700;letter-spacing:.2px;dominant-baseline:middle}
  .nloop .nl-rn{fill:#4ACA9A;font-weight:700;font-size:18px;letter-spacing:1.4px;
       text-anchor:end;dominant-baseline:middle}
  .nloop .nl-eyebrow{fill:#34AE80;font-weight:700;font-size:15px;letter-spacing:5.2px;dominant-baseline:middle}
  .nloop .nl-colhead{fill:#F2F3F5;font-weight:700;font-size:18px;letter-spacing:4.4px;dominant-baseline:middle}
  .nloop .nl-colnum{fill:#5CE4B0;font-weight:700;font-size:18px;text-anchor:middle;dominant-baseline:middle}
  .nloop .nl-coretitle{fill:#5CE4B0;font-weight:700;font-size:23px;letter-spacing:2.4px;text-anchor:middle;dominant-baseline:middle}
  .nloop .nl-layer{fill:#769C92;font-weight:700;font-size:14px;letter-spacing:4px;text-anchor:middle;dominant-baseline:middle}
  .nloop .nl-cap{fill:#6C7682;font-weight:400;font-size:16px;letter-spacing:.6px;text-anchor:middle;dominant-baseline:middle}
  .nloop .nl-ic{fill:none;stroke:#5CE4B0;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;
       color:#5CE4B0}

  @media (prefers-reduced-motion:reduce){.nloop *{animation:none!important}}

/* ---- the video, when there is one ---------------------------------------- */
/* Ambient, not media. `pointer-events: none` is the belt to the missing
   `controls` attribute's braces: with no controls there is no UI to click, and
   with no pointer events a click cannot reach the element to summon one. */
.ecovid {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 14px;
  pointer-events: none;
  background: var(--well);
}
.ecovid::-webkit-media-controls { display: none !important; }
.ecovid::-webkit-media-controls-enclosure { display: none !important; }

/* ---- 22 · THE GROUND ----------------------------------------------------- */
/* The body stops being pure white. Measured on the references: Stripe #F8FAFD,
   Clerk #F7F7F8, Vercel #FAFAFA, Family #FBFAF9 — three of four light B2B pages
   sit 1.5-2.5% off white with a real cast. #FBFCFE is 1.6% down, on the same
   temperature as the ink, so no new hue enters the system. It leaves a 3.1% gap
   to the grey band, which still reads as a band.

   For seventy bytes, every white surface on the page — the sticky header, the
   capability cards, the eight product tiles, the article thumbnails — is
   genuinely lifted off the ground instead of dissolving into it. */
:root { --ground: #FBFCFE; }
body { background: var(--ground); }

/* ---- 23 · THE CLOSING FLOOR ---------------------------------------------- */
/* The CTA is the only large uninterrupted digital fill left on the page, and it
   gets the same grammar as everything else: origin below the edge so the source
   is off-canvas, terminal stop EXACTLY the section's own colour so the wash has
   no boundary.

   IT DARKENS TOWARD THE FLOOR, AND THAT DIRECTION IS AN ACCESSIBILITY
   DECISION, not a taste one. White on #02A885 is 3.02:1, and `.cta .fine` is
   18px — not large text, so it fails AA today. Lightening the floor, which is
   the more obvious way to do this, would take that to 2.36:1 and make a real
   failure materially worse. Deepening it puts the darkest tone exactly where
   the smallest type sits: ~#029A7B, 3.55:1. Never worse than shipped, better
   where it matters. Still short of 4.5:1 — that is a pre-existing problem with
   white on this teal and it is not this rule's to fix. */
.cta {
  background: radial-gradient(110% 92% at 50% 104%,
    #018E70   0%,   /* off-canvas: never rendered */
    #029A7B  16%,   /* the deepest pixel actually seen */
    #02A180  34%,
    var(--teal) 100%);
}

/* ---- 24 · (was: THE MARKS DRAW THEMSELVES) ------------------------------- */
/* REMOVED, by request. Four hand-drawn pen marks used to be made here rather
   than merely be present — a highlighter wiped behind the hero's last clause, a
   lasso looped around "Intelligence", a ruled underline pulled under the
   capability heading, a squiggle under the blog heading, each stroked by walking
   a dash offset along its own length and each timed to the kind of stroke it
   was. The whole device is gone: no shapes, no strokes, nothing to draw. What
   used to be said with a pen is now said with colour, in §26, and colour needs
   no animation to arrive. scroll.js lost its `watchMarks` observer with it. */

/* ---- 25 · THE ENTRANCE KIT ------------------------------------------------ */
/* §18 gave every element on the page the same entrance: opacity, 12px of rise,
   3px of blur, 400ms, staggered 55ms — applied to twenty-eight things across
   seven sections. One idea, repeated, which is what makes a page read as a
   template no matter how good the idea is.

   This replaces it with FOUR treatments chosen by what the content IS. Four, not
   seven: a page where every section invents its own effect is not sophisticated,
   it is noisy, and the reader stops trusting that the motion means anything.

     .rv-head   display headings  — uncovered, not faded
     .rv-text   body copy         — a short rise, and NO blur
     .rv-card   cards and columns — rise with a little scale, staggered
     .rv-media  images            — a wipe, with the picture moving inside it

   Every curve and duration here is Attio's own, measured off the live site:
   cubic-bezier(0,0,0,1) and cubic-bezier(0.2,0,0,1), nothing longer than 700ms. */

:root { --rv: cubic-bezier(0.2, 0, 0, 1); }

/* ---- 1 · DISPLAY HEADINGS: uncovered ------------------------------------- */
/* A Didone at 40-76px is the most expensive thing on the page and it should not
   arrive by fading, which is what a heading does when nobody decided how it
   should arrive. Clipping instead means the letterforms are revealed at full
   contrast from the first frame — the type is uncovered rather than switched on.

   The inset is negative top and bottom so the clip never crosses a descender or
   one of the hand-drawn marks, which sit outside the text box. */
html.fx .rv-head {
  clip-path: inset(-35% 0 100% 0);
  translate: 0 22px;
  transition: clip-path .66s var(--rv), translate .66s var(--rv);
}
html.fx .rv-head.in { clip-path: inset(-35% 0 -35% 0); translate: 0 0; }

/* ---- 2 · BODY COPY: a short rise, no blur -------------------------------- */
/* THE BLUR COMES OFF THE TEXT. Blurring body copy on the way in is the single
   clearest tell of an amateur scroll animation: for 400ms the reader's own eyes
   are told they are out of focus, and on a 15px line it reads as a rendering
   fault rather than as motion. Blur stays, but only on cards and images, where
   there is a shape to soften rather than a word to read. 10px, not 12 — the
   text should settle before the eye reaches it. */
html.fx .rv-text {
  opacity: 0;
  translate: 0 10px;
  transition: opacity .42s var(--rv), translate .42s var(--rv);
  transition-delay: calc(var(--i, 0) * 45ms);
}
html.fx .rv-text.in { opacity: 1; translate: 0 0; }

/* ---- 3 · CARDS: rise, with a little scale -------------------------------- */
/* A card is an object, so it may arrive like one — slightly under size and
   slightly low, settling into place. 0.985 is deliberately almost nothing: at
   0.95 the card visibly grows and the whole grid pulses, which is the other
   common tell. The stagger is 70ms rather than 55: cards are large enough that
   a faster cascade reads as a single blur of movement rather than as a sequence. */
html.fx .rv-card {
  opacity: 0;
  translate: 0 18px;
  scale: .985;
  filter: blur(2px);
  transition: opacity .5s var(--rv), translate .5s var(--rv),
              scale .5s var(--rv), filter .5s var(--rv);
  transition-delay: calc(var(--i, 0) * 70ms);
}
html.fx .rv-card.in { opacity: 1; translate: 0 0; scale: 1; filter: blur(0); }

/* The star badge lands after its card, with the only overshoot on the page.
   One bouncy thing, on the smallest object, is a punctuation mark; two would be
   a personality. */
html.fx .rv-card .star {
  opacity: 0;
  scale: .3;
  transition: opacity .3s var(--rv), scale .44s cubic-bezier(.2, 1.5, .4, 1);
  transition-delay: calc(var(--i, 0) * 70ms + 200ms);
}
html.fx .rv-card.in .star { opacity: 1; scale: 1; }

/* ---- 4 · IMAGES: a wipe, with the picture moving inside it ---------------- */
/* The editorial image reveal, and the reason it beats a fade: the frame opens
   while the photograph drifts from 1.07 to 1 inside it, so the two edges move at
   different rates and the picture reads as being uncovered by something in front
   of it rather than as an element whose opacity changed. It needs two elements —
   on one, the clip edge scales with the image and it slides instead of wiping. */
.post .shot { display: block; overflow: clip; border-radius: 10px; }
html.fx .rv-card .shot {
  clip-path: inset(0 0 100% 0);
  transition: clip-path .72s var(--rv);
  transition-delay: calc(var(--i, 0) * 70ms + 80ms);
}
html.fx .rv-card.in .shot { clip-path: inset(0 0 0 0); }
html.fx .rv-card .shot > * {
  scale: 1.07;
  transition: scale 1s var(--rv);
  transition-delay: calc(var(--i, 0) * 70ms + 80ms);
}
html.fx .rv-card.in .shot > * { scale: 1; }

@media (prefers-reduced-motion: reduce) {
  html.fx .rv-head, html.fx .rv-text, html.fx .rv-card, html.fx .rv-media,
  html.fx .rv-card .star, html.fx .rv-media img {
    transition-duration: .01ms; transition-delay: 0ms;
  }
}

/* ---- 5 · THE FIRST SCREEN ANIMATES ITSELF -------------------------------- */
/* THE HERO IS EXEMPT FROM §25 AND HAS TO BE. Everything above hides an element
   and waits for scroll.js to un-hide it, which is three bets in a row: that the
   script runs, that IntersectionObserver reports, and that the compositor
   repaints the clip. Below the fold a lost bet is an element that forgot to
   animate. On the first screen it is a page with nothing on it — and the bet
   WAS lost, on Windows: the headline kept `clip-path: inset(... 100% ...)`,
   the lede and the buttons resolved normally, and the hero rendered as an
   empty band above them. Same Blink, same markup, same CSS; a driver that will
   not repaint an animated clip is enough.

   So the entrance here is a CSS animation and not a JS-toggled transition, and
   the whole difference is in the resting state:

     backwards fill   holds the opening frame before the animation starts, so
                      there is no unstyled flash to pay for this
     NO forwards fill so the instant it ends the element is back to its plain
                      self — no clip-path, no translate, no opacity left on it

   An animation that never runs leaves that same plain self. There is no longer
   a failure mode in which the words are not on the page: the worst case is a
   headline that arrives without ceremony. scroll.js keeps the reveals for
   everything below the fold, where the reader is not staring at the result. */
@keyframes rv-uncover { from { clip-path: inset(-35% 0 100% 0); translate: 0 22px; } }
@keyframes rv-rise    { from { opacity: 0; translate: 0 10px; } }

html.fx .hero h1.hand   { animation: rv-uncover .66s var(--rv) backwards; }
html.fx .hero .lede     { animation: rv-rise .42s var(--rv) .09s backwards; }
html.fx .hero .cta-pair { animation: rv-rise .42s var(--rv) .16s backwards; }

/* `.fx` is never set under reduced motion, so these never run there. This is
   for the reader who turns it on mid-session, when `.fx` is already on. */
@media (prefers-reduced-motion: reduce) {
  html.fx .hero h1.hand, html.fx .hero .lede, html.fx .hero .cta-pair {
    animation-duration: .01ms; animation-delay: 0ms;
  }
}

/* The pipeline, re-set for the dark chapter. The rail and the tick are white at
   low alpha rather than the ink hairline they are on mint; the nodes keep the
   brand teal, which is bright enough to carry on black.

   THE ALPHAS ARE MEASURED, NOT PICKED. Both label rows are small text and need
   4.5:1 against the chapter's darkest stop, #0C1014. 42% white — where this
   started — computes to 4.15:1 and fails. The stage word sits at 92% and its
   plain line at 66%, which keeps the hierarchy while leaving both comfortably
   readable: there is no "inactive" stage here to justify dimming one. */
.eco .core-eyebrow { color: rgb(255 255 255 / .52); }
.eco .arc::before { background: rgb(255 255 255 / .16); }
.eco .arc .stage { color: rgb(255 255 255 / .92); }
.eco .arc .plain { color: rgb(255 255 255 / .66); }
.eco .arc .plain::before { background: rgb(255 255 255 / .16); }
.eco .arc .node {
  background: var(--teal);
  box-shadow: 0 0 0 3px var(--well), 0 0 10px rgb(2 168 133 / .5);
}

/* The chapter's text column, CENTRED OVER THE FIGURE.

   THIS USED TO BE A LEFT-ALIGNED HALF-COLUMN, and that is the whole bug. When
   the section was a two-up split — claim on the left, diagram on the right —
   this block turned off §6's centring and capped the paragraph at 46ch so the
   measure stayed readable in half a screen. The split is gone (see the note
   above .ecogrid: the figure is stacked under the words now and takes the
   whole 1560px), but the override outlived it. The result was 522px of ragged
   copy jammed against the left edge of a 1496px band, with 974px of black to
   its right — reported as "the text is congested on the left side".

   Nothing replaces it, because §6 already had the right answer: centred, and
   sharing the figure's axis. Two things do change from §6's defaults.

   THE MEASURE IS THE FIGURE'S, NOT PROSE'S. §6 caps at 74ch, which is the
   reading-width rule and would break this into four short lines floating in
   the middle of a very wide band. This paragraph is a section standfirst
   sitting directly above a 1560px diagram, so it takes a measure wide enough
   to read as the same object — three lines that span the figure — rather
   than a column that ignores it. min() keeps every narrower screen on 100%,
   so nothing below ~1290px is touched.

   AND THE EYEBROW IS THE SECTION'S HEADING. It is the only heading this
   chapter has — there is no h2 under it — so at 12.5px it read as a caption
   for the paragraph rather than the title of the section. Same face, colour,
   caps and tracking as before; it just gets display size, and clamp() hands
   the small end back to phones untouched. */
.eco .ecotext p { max-width: min(108ch, 100%); }
.eco .ecotext .core { text-align: center; }
@media (min-width: 900px) {
  .eco .ecotext .eyebrow { font-size: clamp(22px, 2.6vw, 38px); margin-bottom: 20px; }
}

/* ---- 26 · THE DEMO PAGE -------------------------------------------------- */
/* The request-a-demo form, rebuilt from the one on the old site so this site
   owns its own most important conversion. Two columns: what the reader is
   asking for on the left, the asking on the right.

   THE SCENE IS THE OLD SITE'S, BY REQUEST. The gradient ground, the
   photograph and the dark translucent form panel are lifted from
   talbotiq.com's closing section — measured, not eyeballed: the background is
   its own bg-talbotiq.jpg, the panel its own teal-to-slate gradient at 12px,
   the button its own #FFCD57. Both images are served from assets/demo/ so the
   page owns them.

   ONE MEASURED DEPARTURE. The old panel is translucent enough that its white
   labels sit on whatever the gradient happens to be doing underneath, and over
   the bright yellow-green corner they fall to about 2.4:1 — unreadable. The
   stops here are the same two colours in the same direction, carried at a
   higher alpha, which holds every label above 4.5:1 without changing what the
   panel looks like. */
/* The top padding tracks the overlay, which is sized by viewport width: the
   band is 531/2560 of however wide it is drawn, and the lower orange dot sits
   near its bottom edge. Anything less and the dot lands in the headline. */
/* THE BLOOM IS LOAD-BEARING. `cover` crops a 1366x768 landscape differently at
   every viewport, so which part of the artwork ends up under the left column
   is not a constant — at 1440 the eyebrow sits on 4.9:1, at 1024 on 4.33, at
   390 on 2.48. No single text colour is legible across all of them, so the
   floor has to come from the ground rather than the type: a soft white bloom
   under the column, which is the same white the artwork already carries in its
   own bottom-left corner. Gentle here, stronger below 940px where the crop
   keeps nothing but the saturated core. Measured at 390/768/1024/1280/1440/1920. */
/* THE CONTENT SITS HIGH, and the overlay is what decides how high it can. The
   band is 531/2560 of however wide it is drawn and the lower orange dot sits
   near its bottom edge, so the padding has to clear the dot or the dot lands in
   the headline. Rather than push the column down to meet a full-width band —
   which left most of a laptop screen as empty gradient before anything was
   readable — the band is capped at 1150px and lifted 40px under the header, and
   the padding tracks what is left: `19.5vw - 24px` is the dot's own bottom edge
   plus a margin, settling at 196px once the band stops growing. The heading now
   lands in the top third at every width rather than on the halfway line. */
.demo {
  position: relative;
  overflow: clip;
  padding: clamp(96px, calc(19.5vw - 24px), 196px) 0 88px;
  background-color: #DDEBDA;
  background-image:
    radial-gradient(104% 60% at 25% 46%, rgb(255 255 255 / .50) 0%, rgb(255 255 255 / .26) 48%, rgb(255 255 255 / 0) 78%),
    url(../demo/bg-talbotiq.jpg);
  background-size: auto, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
}
/* The wave lines, the orange dot and the spheres, exactly as the old section
   layers them: one transparent overlay, contained to the width and pinned to
   the top, at .91. Two encodings of the same picture — AVIF where it is
   understood, WebP everywhere else; the plain url() below is what a browser
   too old for image-set() gets. Purely decorative, so a browser that loads
   neither simply sees the gradient. */
.demo::before {
  content: "";
  position: absolute;
  inset: -40px 0 auto;
  max-width: 1150px;
  margin-inline: auto;
  aspect-ratio: 2560 / 531;
  background: url(../demo/hero-overlay.webp) 50% 0 / contain no-repeat;
  background-image: image-set(url(../demo/hero-overlay.avif) type("image/avif"), url(../demo/hero-overlay.webp) type("image/webp"));
  opacity: .91;
  pointer-events: none;
}
.demo .demogrid { position: relative; z-index: 1; }

/* One column, and `cover` now crops to nothing but the saturated core: the
   eyebrow reads 2.48:1 against it, the lede 2.78, the contact links 2.55. The
   bloom has to carry much further here. */
@media (max-width: 939px) {
  .demo {
    background-image:
      radial-gradient(128% 62% at 34% 40%, rgb(255 255 255 / .84) 0%, rgb(255 255 255 / .60) 46%, rgb(255 255 255 / .18) 82%),
      url(../demo/bg-talbotiq.jpg);
  }
}
.demogrid { display: grid; grid-template-columns: 1fr; gap: 40px; }
@media (min-width: 940px) {
  .demogrid { grid-template-columns: minmax(0, .82fr) minmax(0, 1fr); gap: 0 64px; align-items: center; }
}
.demoside h1.hand { font-size: 42px; line-height: 1.08; margin: 8px 0 16px; }
/* Both of these are darker than the page's defaults, and measured rather than
   guessed: the column sits on the brightest cyan corner of the gradient, where
   the usual body grey lands at 4.16:1. */
.demoside .sec-lede { text-align: left; margin: 0 0 26px; max-width: 40ch; color: #4F4D4D; }
/* Orange, as on the old page — it is the one warm note in the section and it
   is what tells you where the block begins. Deepened from the old #D95F03,
   which reads 2.56:1 against the cyan behind it and is genuinely hard to make
   out; this is the same hue carried far enough down to clear 4.5:1 against the
   brightest point the column actually sits on, measured off the render. */
.demoside .eyebrow { text-align: left; color: #7E3500; letter-spacing: .02em; }
.demoshot { margin: 0 0 26px; max-width: 548px; border-radius: 12px; box-shadow: 0 10px 30px rgb(16 44 42 / .16); }
.demoshot img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; }

/* The three that work today, set as a definition list rather than prose so the
   eye can find the one it wants without reading a sentence. */
.demoreach { list-style: none; margin: 0; padding: 22px 0 0; border-top: 1px solid rgb(16 44 42 / .16); max-width: 548px; }
.demoreach li { display: flex; gap: 16px; align-items: baseline; padding: 8px 0; font-size: 15.5px; }
.demoreach span { flex: 0 0 62px; color: #4C5A57; font-size: 12.5px; text-transform: uppercase; letter-spacing: .1em; }
.demoreach a { color: #054C39; text-decoration: none; font-weight: 600; }
.demoreach a:hover { text-decoration: underline; }

.dform {
  background: linear-gradient(rgb(10 61 63 / .92) 0%, rgb(52 78 96 / .89) 100%);
  border-radius: 12px;
  padding: 26px;
  box-shadow: 0 0 4px 1px rgb(52 66 64 / .38), 0 18px 44px rgb(16 44 42 / .22);
}
@supports (backdrop-filter: blur(2px)) { .dform { backdrop-filter: blur(2px); } }
.ffgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px 16px; }
@media (max-width: 560px) { .ffgrid { grid-template-columns: 1fr; } }
.ff { margin: 0; display: flex; flex-direction: column; gap: 6px; }
.ff--wide { grid-column: 1 / -1; }
.ff label { font-size: 12.5px; font-weight: 700; color: #FAFAFA; letter-spacing: .015em; }
/* `.req` IS NOT THIS PAGE'S CLASS. contact.html, signin.html and the four
   solutions pages mark their required fields with it too, on a white ground,
   where this red is right — so the red stays the default and the dark panel
   takes an override rather than the other way round. On the panel that red
   measures 2.1:1 and cannot be read at all; the coral clears 4.5:1. */
.req { color: #C0392B; }
.dform .req { color: #FFC2B5; }

.ff input, .ff select, .ff textarea {
  font: inherit; font-size: 15.5px; color: #3A3A3A;
  padding: 10px 12px;
  border: 1px solid #9E9E9E;
  border-radius: 6px;
  background: #fff;
  width: 100%;
  transition: border-color .18s var(--ease-out), box-shadow .18s var(--ease-out);
}
.ff textarea { resize: vertical; min-height: 132px; line-height: 1.55; }
.ff input::placeholder, .ff textarea::placeholder { color: #8A8A8A; }
.ff input:focus, .ff select:focus, .ff textarea:focus {
  outline: none;
  border-color: #FFCD57;
  box-shadow: 0 0 0 3px rgb(255 205 87 / .45);
}
/* The browser's own invalid styling only after a submit has been attempted, so
   nobody is told their email is wrong while they are still typing it. */
.dform.tried :invalid { border-color: #C0392B; box-shadow: 0 0 0 3px rgb(192 57 43 / .22); }

/* The old site's yellow, and it earns its place: it is the only yellow in the
   panel, so the one thing you can do here is the one thing that is warm. */
.dsubmit, .dsubmit:link, .dsubmit:visited {
  display: block; width: 100%; margin-top: 20px; text-align: center;
  background: #FFCD57; color: #1E293B; border: 0; border-radius: 12px;
  font-size: 19px; font-weight: 700; letter-spacing: .01em; padding: 12px 20px;
}
.dsubmit:hover { background: #FFD97C; color: #1E293B; }
.dsubmit:active { background: #F3BE43; }
.dnote { margin: 13px 0 0; font-size: 13px; line-height: 1.55; color: rgb(255 255 255 / .82); text-align: center; }
.dnote code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }

/* The honeypot. Off-screen rather than display:none, because some bots skip
   anything that is not rendered — and hidden from assistive tech, which would
   otherwise read out a field nobody should fill. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---- 26 · THE KEYWORD ----------------------------------------------------- */
/* WHAT REPLACED THE PEN. Every major heading now carries its emphasis in the
   words themselves: one or two of them take a colour, the rest stay ink. There
   is no shape, no stroke and nothing overlaid, so a heading is exactly as tall
   and as wide as its own type — which is the whole reason this is cheaper than
   the marks were, and why it needs no animation to arrive.

   TWO ROLES, NOT A PALETTE. `.k-g` is the subject — the technology, the thing
   being named. `.k-y` is the consequence — the outcome, the differentiator.
   Two words in one heading may take one role each; a single-keyword heading
   takes whichever role is true. Nothing takes three.

   THE PAIR INVERTS ON A DARK GROUND, because neither light-ground value
   survives there: --key-g is 2.91:1 on near-black and --key-y is worse. The
   brand's own bright teal and yellow are 5.13:1 and 11.59:1 on the same
   ground, so the dark chapter gets those instead. This is the same two roles
   in the only values that read, not a second colour scheme. */
.k-g { color: var(--key-g); }
.k-y { color: var(--key-y); }

/* The dark chapter. */
.eco .k-g { color: var(--teal); }
.eco .k-y { color: var(--yellow); }

/* THE TEAL PANEL IS THE ONE PLACE A KEYWORD CANNOT BE BRAND-COLOURED. Against
   #02A885 the brand yellow is 2.26:1 and the greens are ~1.0:1 — a keyword
   there would be less legible than the white it is meant to stand out from. So
   on teal the emphasis inverts to ink, which is 5.13:1 on that ground and
   reads as deliberate rather than as a colour that failed. */
.cta .k-g, .cta .k-y { color: var(--ink); }

/* =============================================================================
   §30 · THE SEO PASS — self-hosted Inter, the two hub pages, breadcrumbs
   -----------------------------------------------------------------------------
   INTER IS SELF-HOSTED NOW. It came from Google Fonts, which put a render-
   blocking stylesheet from a third origin in front of every first paint —
   1.3–2.0 s of the mobile LCP in Lighthouse. Same face, same four weights:
   Google serves Inter as one variable file per script, and the latin file is
   all an English site uses. Declared as a 400–700 range so nothing is
   synthesised. tools/seo-pass.js writes the identical rule into the 24 hand-
   written pages.

   THE HUB PAGES (/products, /solutions) are generated by build.js from this
   same shell, so they use this stylesheet rather than an inline copy. The
   comparison table is a real <table> — the one element on the site that is,
   because a grid faking a table is not parsed as one by anything that reads
   structured data. It scrolls inside .tblwrap on narrow screens rather than
   letting the page scroll sideways.
   ========================================================================== */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/inter/Inter-latin.woff2") format("woff2");
}

.crumb { font-size: 14px; color: var(--dim); margin: 0 0 18px; }
.crumb a { color: var(--green); text-decoration: none; font-weight: 600; }
.crumb a:hover { text-decoration: underline; }

.hub { padding: 64px 0 96px; }
.hub .wrap { max-width: 1060px; }
.hub h1.hand { font-size: 56px; margin: 0 0 22px; }
.hub h2.hand { font-size: 38px; margin: 72px 0 14px; }
.hub .intro { font-size: 19px; line-height: 1.6; max-width: 68ch; margin: 0 0 18px; color: var(--grey); }
.hub .intro b { color: var(--ink); font-weight: 600; }
.hub .intro a, .hub .tldr a, .hub .ab a { color: var(--green); font-weight: 600; text-decoration: none; }
.hub .intro a:hover, .hub .tldr a:hover, .hub .ab a:hover { text-decoration: underline; }
.tldr { margin: 22px 0 0; padding: 22px 26px; background: var(--band); border-radius: 12px; list-style: none; }
.tldr li { position: relative; padding-left: 22px; margin: 8px 0; font-size: 16.5px; line-height: 1.55; color: var(--grey); }
.tldr li::before { content: ""; position: absolute; left: 0; top: 10px; width: 9px; height: 9px; border-radius: 50%; background: var(--teal); }
.tldr b { color: var(--ink); font-weight: 600; }

.tblwrap { overflow-x: auto; margin: 26px 0 0; -webkit-overflow-scrolling: touch; border: 1px solid var(--border); border-radius: 12px; }
table.cmp { width: 100%; border-collapse: collapse; font-size: 15px; line-height: 1.5; min-width: 820px; }
table.cmp th, table.cmp td { text-align: left; vertical-align: top; padding: 13px 16px; border-bottom: 1px solid var(--border); color: var(--grey); }
table.cmp thead th { font-size: 12.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--dim); font-weight: 600; background: var(--band); }
table.cmp tbody th { color: var(--ink); font-weight: 600; white-space: nowrap; }
table.cmp tbody tr:last-child th, table.cmp tbody tr:last-child td { border-bottom: 0; }
table.cmp a { color: var(--green); text-decoration: none; }
table.cmp a:hover { text-decoration: underline; }
.tblnote { font-size: 13.5px; color: var(--dim); margin: 12px 0 0; }

.hub .capgrid { margin-top: 26px; }
.hub .cap { border: 1px solid var(--border); }
.hub .cap h3 { padding-right: 0; }
.hub .cap h3 a { color: var(--ink); text-decoration: none; }
.hub .cap h3 a:hover { color: var(--green); }
.hub .cap .more { display: inline-block; margin-top: 12px; color: var(--green); font-weight: 600; text-decoration: none; font-size: 15px; }
.hub .cap .more:hover { text-decoration: underline; }
.hub .cap.pending { opacity: .75; }
.hub .cap.pending h3 .soon { color: var(--ink); }

.acc { max-width: 920px; margin: 22px 0 0; border-top: 1px solid var(--border); }
.acc details { border-bottom: 1px solid var(--border); }
.acc summary { list-style: none; cursor: pointer; padding: 18px 4px; font-size: 17.5px; font-weight: 600; color: var(--ink); display: flex; align-items: center; justify-content: space-between; }
.acc summary::-webkit-details-marker { display: none; }
.acc summary:hover, .acc details[open] summary { color: var(--green); }
.acc summary .pm { color: var(--teal); font-size: 21px; font-weight: 400; flex: none; margin-left: 16px; transition: transform .2s; }
.acc details[open] summary .pm { transform: rotate(45deg); }
.acc .ab { padding: 0 4px 21px; font-size: 16px; line-height: 1.62; max-width: 76ch; color: var(--grey); }

.stamp { font-size: 13px; color: var(--dim); margin: 28px 0 0; }

@media (max-width: 820px) {
  .hub { padding: 40px 0 72px; }
  .hub h1.hand { font-size: 38px; }
  .hub h2.hand { font-size: 30px; margin-top: 52px; }
  .hub .capgrid { grid-template-columns: 1fr; }
  .hub .cap.wide { grid-column: auto; }
}
