/* ============================================================
   iamtec.ai — site styles
   Tokens are the canonical ones from docs/BRAND.md. Brand and
   status colours are deliberately separate namespaces: --brand-*
   is themeable, --status-* is never. See BRAND.md §4.
   ============================================================ */

:root {
  /* brand — themeable */
  --brand-sand:   #F3EEE5;
  --brand-sand-2: #FBF8F3;
  /* One step DARKER than sand, for alternating section grounds. Darker rather
     than lighter on purpose: cards are --brand-sand-2, so a lighter alternate
     ground would put light cards on a lighter ground and flip them from raised
     to flush halfway down the page. Against this they stay raised, and the
     card-to-ground relationship is the same in every section. */
  --brand-sand-3: #EBE4D6;
  /* The ground the current section is painted in. Sections override it. */
  --ground: #F3EEE5;
  --brand-ink:    #1D1C21;
  --brand-accent: #E5218C;
  /* One step deeper, for anything white text sits on. White on #E5218C is
     4.26:1, just under AA; on this it is 4.79:1. Same hue family. */
  --brand-accent-deep:  #D81B80;   /* white on this is 4.79:1 */
  --brand-accent-hover: #BD1670;   /* 5.98:1 */
  --brand-accent-ink:   #8E1054;
  --brand-accent-ink-2: #B31468;   /* 5.65:1 on sand — ghost button text */
  --brand-accent-lift:  #EE4BA2;   /* 4.97:1 on ink — accent as SMALL text on dark.
                                      #E5218C is only 3.97:1 there: fine for display
                                      sizes, not for a 12px label. */   /* raspberry as readable text on tint */
  /* Large fills only. #E5218C at panel scale is too bright and fails text
     contrast either way; this is deeper, still raspberry, and readable. */
  --brand-accent-panel: #BD1670;
  --brand-accent-soft:  #F58BC2;   /* large text over the water panel's brightest crest: 4.56:1 (needs 3:1) */
  /* The light end of the ramp. Used sparingly: a wash of it across every
     panel made the whole site read pink rather than confident. */
  --brand-tint:   #FBE7F1;
  --brand-tint-2: #F6D5E7;

  /* supporting neutrals, derived from ink */
  /* Darkened in Madurai_2: at #5E5B66 / #6A6673 body text passed contrast but
     read as faint, low-ink type on a 1x screen ("seems like low ink quality").
     Same weight, so no line re-wraps. */
  /* Small labels on ink. Was #8F8A99 at 5.05:1 — which passes AA for normal
     text and was still unreadable at 11px, uppercase and tracked .1em, because
     at that size contrast is only one of three levers (Naveen: "it is not even
     visible"). This is the same value the footer already uses successfully for
     13px text on the same ground. */
  /* ---- text on a dark ground ----
     Five muted greys were in use on ink — #9891A1, #A49EAF, #B4AFBD, #B7B2BC
     and #C9C3BA, from 5.56:1 to 9.67:1. Each passed contrast; together they
     read as haze rather than as hierarchy, and Naveen went through them one
     screenshot at a time: "it is not even visible", "not bold enough", then
     "they need to really white — bring sharp contrast".

     Two values now, not five. Supporting text on ink is the SAME sand as the
     headings, because on a dark ground a muted tier costs far more legibility
     than the hierarchy is worth; hierarchy on ink comes from size and weight
     instead. The second value exists only for genuinely tertiary fine print
     and is still far sharper than anything it replaces. */
  --on-ink:   #F3EEE5;   /* 14.65:1 — supporting text, same as headings */
  /* The accent doing duty as a 13px uppercase label on ink. --brand-accent-lift
     is 4.97:1 there, which passes and still reads dim at that size next to
     14.65:1 sand beside it. Same hue family, two steps lighter. Large accent
     text and accent fills are untouched. */
  --accent-label-on-ink: #F58BC2;   /* 7.54:1 on ink */
  --on-ink-2: #DAD4C8;   /* 11.48:1 — fine print only */
  --label-on-ink: #DAD4C8;   /* 11.48:1 on ink; was #B7B2BC at 8.15:1 */
  --ink-2: #45424C;   /* 8.4:1 on sand */
  --ink-3: #57535F;   /* 6.5:1 on sand. Was #8A8693 at 3.07:1 before Madurai_1,
                       which failed every eyebrow label, pill and footer line. */
  --hair:  #DFD7C9;
  /* The divider weight. Referenced in eleven places — section dividers, the
     ticker's top and bottom edges, and every card outline in the demo blocks —
     but never declared until now, so all eleven computed to `0px none`: no
     border rendered at all, and the doubled space around the /studio section
     joins had no line in the middle to justify it. A shade softer than --hair
     because a full-width rule carries more visual weight than a card edge of
     the same value. */
  --rule:  #E6DFD2;

  /* status — NEVER themeable, never brand */
  --status-critical: #DC2626;
  --status-warning:  #D97706;
  --status-healthy:  #059669;

  --measure: 66ch;

  /* ---- type scale ----
     Twenty distinct literal sizes were in use between 10.5px and 25px, half of
     them half-pixel values — 11.5, 12.5, 13.5, 14.5, 15.5, 16.5. That is not a
     scale, it is each element having been eyeballed on its own, and it is the
     kind of inconsistency a visitor feels without being able to name it.
     Display type stays fluid via clamp(); everything else sits on these. */
  --fs-xs:   11px;   /* eyebrows, pills, captions  */
  --fs-sm:   13px;   /* notes, fine print          */
  --fs-base: 15px;   /* UI, nav, list copy         */
  --fs-md:   17px;   /* body                       */
  --fs-lg:   19px;   /* lede, standfirst           */

  /* Display steps. These were being written inline in the markup — the same
     clamp() pasted into three files, plus two one-offs — so a change meant
     finding every copy. */
  --fs-h2:     clamp(23px, 3.2vw, 32px);   /* section headings   */
  --fs-h2-sm:  clamp(21px, 2.7vw, 26px);   /* quieter headings   */
  --fs-feature: clamp(30px, 4.6vw, 54px);  /* a panel's statement */

  /* ---- display weight ----
     Every heading on the site was 800, declared separately in eleven rules.
     Naveen brought a reference he rated as high quality (Brentwood School) and
     the difference was not layout or colour: their headings run to 72px at
     weight 400. Heavy weight at large size is the loudest template signal
     there is, and it was why these pages read as loud rather than considered —
     the body text I enlarged first was the wrong lever.

     Two values, not one. Light type on the ink bands is optically thinner than
     the same weight on sand, and those grounds also carry
     -webkit-font-smoothing: antialiased (see the rule below), which thins the
     strokes again — so dark grounds take one step up instead of looking
     fragile. Bricolage is already served 400..800, so none of this downloads
     anything new.

     Two things deliberately keep 800 and are NOT on these tokens: the logo
     lockup, which is the mark (BRAND.md §2.3), and the 404 keycaps, where the
     glyph is printed on a physical-looking keycap. */
  --wt-display:      400;
  --wt-display-dark: 500;   /* the same type, on ink */
  /* 600, not 500. Raised after Naveen flagged small type as "not bold enough"
     four separate times, on labels and short lines rather than on headlines.
     The principle behind the split is optical sizing, not taste: large display
     type carries at 400 because the strokes are large, and the same weight at
     15-23px on a dark ground reads thin. So the big end stays where he approved
     it and only the small end moves. */
  --wt-display-sm:   600;   /* under ~28px, where 400 goes weak */

  /* ---- the gap between a prose run and an object ----
     One value, because the alternative was proven: .dims carried 26px above
     and 0 below, .ladder 8px above, .roles and .figs nothing, so every
     junction on the page had a different measure and the paragraph after the
     eight-dimension grid sat on its bottom edge. Spacing set per block cannot
     be made constant by tuning the numbers — each new block invents its own.
     The container owns it instead (.wrap.flow), and blocks carry no margins. */
  --blk: clamp(32px, 3.8vw, 56px);
  /* Section padding for prose-only sections — see .sec-doc. Deliberately close
     to --blk rather than to --sec-y: a section that is two paragraphs needs
     separating, not framing. */
  --sec-y-doc: clamp(34px, 4.2vw, 62px);

  /* ---- vertical rhythm ----
     Every full-width band derives its padding from this. Before it there were
     six different values in play (112, 104, 100, 88, 64, 128), and `section`
     and `.hero` were each declared three or four times across different passes
     of this file, so which value won was an accident of source order. */
  /* The floor used to be 68px. On a phone that is the same absolute air as a
     laptop gets, between blocks that are a third of the height — 112px of
     nothing between two 180px blocks, which reads as the page having lost
     something rather than as space. The vw term still governs from ~640px up,
     so nothing above a phone changed. */
  --sec-y: clamp(46px, 7.2vw, 104px);
}

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

/* The hidden attribute must win. An author `display: flex` beats the UA
   stylesheet's `display: none`, which silently leaves a "hidden" panel on
   screen and makes its close button look broken. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  /* The canvas behind the page. Rubber-banding past the top or bottom reveals
     THIS colour, not the body's — so the overscroll reads as raspberry rather
     than as a blank sand gap, and the accent rule at the top of the header
     appears to stretch rather than run out. */
  background: var(--brand-accent);
  /* clip, not hidden: `overflow-x: hidden` turns <html> into a scroll
     container, which silently breaks position: sticky on descendants.
     `clip` prevents sideways scrolling without that side effect. */
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--brand-sand);
  /* The html canvas behind this is raspberry, for the overscroll. Without a
     floor here a page shorter than the window lets that canvas fill everything
     below the footer, which on a short page and a tall screen is a block of
     raspberry rather than a rubber-band. */
  min-height: 100vh;
  color: var(--brand-ink);
  font-family: "Manrope", ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: var(--fs-md);
  line-height: 1.62;
  /* No -webkit-font-smoothing: antialiased here. On macOS it switches off the
     stroke thickening dark-on-light text gets, and paragraphs came out thin. */
}
/* Light text on a dark or magenta ground is the opposite case: there the
   thickening makes type heavier and softer, so those grounds keep it off.
   Checked by a sweep of every page for light-on-dark text without it. */
.skip-link, .band, .site-foot, .b-ink, .b-accent, .pin-band, .netband, .sweep-band,
.cta-band, .aih, .gp-card, .chip[aria-pressed="true"], .btn-primary, .nf-key:not(.nf-ghost),
.layer-now, .shot-hint, .assist .assist-fab,
/* Added after release QA F-2. Every one of these is light-on-dark and was
   missing from the list — .quote-band is new in this release, but
   .nav-toggle-label (16 pages) and .gp-seq-next predate it, which is the point:
   this list is maintained by hand and a new dark ground will keep being added
   without it. RT-32 exists to catch exactly that. */
.quote-band, .nav-toggle-label, .gp-seq-next {
  -webkit-font-smoothing: antialiased;
}

/* ---- body weight on a dark ground ----
   The same weight is not the same colour on ink as it is on sand. Light type on
   a dark ground loses apparent stroke — and these grounds also run
   -webkit-font-smoothing: antialiased (the rule above), which thins it again.
   At 13-15px the result is text that measures 8.15:1 and still reads as grey
   haze: the footer's legal line and policy links, on all fifteen pages.

   Naveen, 2026-09-18: "lots of fonts or letters are not bold enough", after
   "it is not even visible" about an 11px label that passed contrast at 5.05:1.
   Contrast was never the missing lever; weight was.

   Half a step, inherited, so it reaches body copy without touching anything
   that sets its own weight — headings take the display tokens, .tag takes 700,
   buttons and pills set their own. Large display type is unaffected because it
   never inherits from here. */
.band, .site-foot, .b-ink, .b-accent, .pin-band, .netband, .sweep-band,
.cta-band, .aih, .quote-band, .layer-now {
  font-weight: 600;
}


/* Tracking and leading move WITH the weight, or the change reads as a mistake.
   -.032em was set to stop 800-weight glyphs colliding; at 400 the same value
   pinches the counters shut. Likewise 1.03 leading: heavy lines can sit that
   close because each one is a solid mass, light lines need air between them. */
h1, h2, h3, .display {
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-variation-settings: "wdth" 94, "opsz" 48;
  font-weight: var(--wt-display);
  letter-spacing: -.018em;
  line-height: 1.06;
  text-wrap: balance;
  margin: 0;
}
h1 { font-size: clamp(38px, 6.4vw, 68px); }
h2 { font-size: clamp(27px, 3.8vw, 40px); }
h3 { font-size: clamp(18px, 2.1vw, 21px); font-weight: var(--wt-display-sm); letter-spacing: -.012em; }

p  { margin: 0; max-width: var(--measure); }
a  { color: inherit; }

.wrap { max-width: 1080px; margin: 0 auto; padding: 0 28px; }
/* A wrap that owns the rhythm of what it holds. Every gap between the blocks
   inside it is --blk, with no exceptions and nothing to tune per block, so the
   spacing is equal by construction rather than by care. */
.wrap.flow { display: flex; flex-direction: column; gap: var(--blk); }
/* And nothing inside it brings its own. Margins ADD to a flex gap rather than
   collapsing into it, so one leftover `margin: 0 0 4px` on .section-intro made
   three of the six junctions 59px against the other three at 55px — invisible
   to the eye, which is exactly why it has to be a rule and not a sweep. */
.wrap.flow > * { margin: 0; }
.stack   { display: flex; flex-direction: column; gap: 20px; }
.stack-l { display: flex; flex-direction: column; gap: 40px; }
section  { padding: 72px 0; }

:where(a, button, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: 3px;
  border-radius: 2px;
}
/* On controls that already have a border, an offset ring reads as a second
   box around the first. Sit it on the edge instead. */
.nav-toggle:focus-visible, .btn:focus-visible, .chip:focus-visible {
  outline-offset: 1px;
}

/* ---------- the logotype ---------- */
/* font-size MUST sit on .lockup: the svg height is in em and resolves
   against THIS element, not its child. BRAND.md §2.3. */
.lockup {
  display: inline-flex; align-items: flex-end; gap: 0; line-height: 1;
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-variation-settings: "wdth" 90, "opsz" 48;
  /* Stays 800 while every other heading went light. This is the mark, not a
     heading — its weight is specified in BRAND.md §2.3, and a logo that drifts
     with a type decision is no longer a logo. */
  font-weight: 800; letter-spacing: -.042em;
  color: var(--brand-ink); text-decoration: none;
}
/* margin-right is negative on purpose — the tittle overhangs the stem and
   must sit ABOVE the following 'a', not push it away. BRAND.md §2.3. */
.lockup svg { height: 1.16em; width: auto; display: block;
              margin-bottom: -.062em; margin-right: -.355em; }
.lockup .sfx { color: var(--brand-accent); font-weight: 400; }

/* ---------- skip link ---------- */
.skip-link {
  position: absolute; left: 12px; top: -60px; z-index: 100;
  background: var(--brand-ink); color: var(--brand-sand);
  padding: 11px 16px; border-radius: 0 0 4px 4px; font-weight: 700;
  font-size: var(--fs-base); text-decoration: none;
  transition: top .12s ease-out;
}
.skip-link:focus { top: 0; }

/* ---------- header ---------- */
.site-head {
  border-bottom: 1px solid var(--hair);
  background: var(--brand-sand);
  position: sticky; top: 0; z-index: 10;
}
.site-head .wrap {
  display: flex; align-items: center; gap: 20px; min-height: 72px;
}
.nav-wrap { margin-left: auto; }
.nav-toggle { display: none; }
.nav-cta { font-size: var(--fs-base); padding: 10px 18px; min-height: 40px; flex: none; }
.site-head .lockup { font-size: 25px; }
.nav { display: flex; align-items: center; gap: 4px; }
@media (min-width: 901px) {
  /* Two overrides are needed, not one. Older engines hide a closed details'
     children with display:none, which the first rule beats. Current Chrome
     hides them via ::details-content { content-visibility: hidden }, which
     display cannot reach at all: without the second rule the desktop nav
     disappears completely. */
  .nav-wrap:not([open]) > .nav { display: flex; }
  .nav-wrap::details-content { content-visibility: visible; block-size: auto; }
}
/* The current page is marked by a filled box rather than a rule under the
   word. The gap shrinks to match: at 26px apart the boxes float, and the bar
   stops reading as one group. */
.nav a {
  font-size: var(--fs-base); font-weight: 500; color: var(--ink-2);
  text-decoration: none; padding: 8px 12px; border-radius: 4px;
  transition: background-color .16s ease, color .16s ease;
}
.nav a:hover { background: rgba(29,28,33,.06); color: var(--brand-ink); }
.nav a[aria-current="page"] {
  background: var(--brand-tint); color: var(--brand-accent-ink); font-weight: 700;
}
/* Desktop only. The phone menu already sets its links 600 in ink; the bar stayed
   500 in grey and read faint beside the bold current page (G4-4). Dropdown panel
   links keep the lighter weight, a step below the bar, as the phone menu does.
   The rule above is (0,2,1) and keeps the current page at 700 and its tint —
   AT DESKTOP ONLY. At phone width `.site-head .nav a` (below, ≤900px) ties it
   at the same specificity and wins on source order, so the phone current-page
   link is 600 too, not 700 (measured 375px, BACKLOG 133). Its own aria-current
   override only sets background and colour, not weight, so it does not win
   weight back. */
@media (min-width: 901px) {
  .nav > a, .nav-item > a { font-weight: 600; color: var(--brand-ink); }
}
/* The current page, on desktop: a dot under the word, the same off-axis tittle
   as the i in the mark (Madurai_2, Naveen's pick). The tinted box it replaces was
   a different height from "Talk to us", and hover drew a grey box, so the bar
   read as three kinds of box. Hover now turns the word magenta; nothing is
   boxed except the call to action. The phone menu keeps its tinted row. */
@media (min-width: 901px) {
  .nav { gap: 10px; }
  .nav > a, .nav-item > a { position: relative; padding: 8px 10px; }
  .nav > a:hover, .nav-item > a:hover { background: none; color: var(--brand-accent-ink); }
  .nav > a::after, .nav-item > a::after {
    content: ""; position: absolute; left: 50%; bottom: -3px;
    width: 6px; height: 6px; margin-left: -3px; border-radius: 50%;
    background: var(--brand-accent); transform: scale(0);
    transition: transform .2s cubic-bezier(.3,1.5,.5,1);
  }
  /* Centred under the word, not the word plus its chevron. */
  .nav-item > a::after { margin-left: -10px; }
  .nav > a[aria-current="page"], .nav-item > a[aria-current="page"] {
    background: none; color: var(--brand-ink); font-weight: 700;
  }
  .nav > a[aria-current="page"]::after, .nav-item > a[aria-current="page"]::after { transform: scale(1); }
  /* Standing on a page inside a menu (/timesheet under Products): the menu name
     takes a hollow ring, the quieter form of the same mark, instead of the
     underline it had before the dot. Filled dot = this page; ring = in here. */
  /* .nav prefix: the underline rule is later in the file at equal specificity. */
  .nav .nav-item:has(.nav-sub a[aria-current="page"]) > a { box-shadow: none; }
  .nav-item:has(.nav-sub a[aria-current="page"]) > a::after {
    transform: scale(1); background: transparent;
    box-shadow: inset 0 0 0 1.5px var(--brand-accent);
  }
}
@media (min-width: 901px) and (prefers-reduced-motion: reduce) {
  .nav > a::after, .nav-item > a::after { transition: none; }
}
@media (max-width: 900px) {
  /* Six links wrapped onto two rows made a 184px header. On a phone the links
     go behind a menu button instead: logo left, menu and call to action right. */
  .site-head .wrap {
    flex-wrap: wrap; align-items: center; gap: 8px 10px;
    padding-top: 12px; padding-bottom: 12px; min-height: 0;
  }
  /* Call to action first, menu last, both packed to the right. Only the
     first of the pair may claim the free space: with margin-left:auto on
     both they split it and the menu strands itself mid-bar. */
  .nav-cta  { margin-left: auto; order: 2; }
  .nav-wrap { margin-left: 0; order: 3; }
  .nav-toggle {
    display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
    list-style: none; padding: 9px 14px; min-height: 38px;
    border: 1px solid var(--brand-ink); border-radius: 3px;
    font-size: var(--fs-sm); font-weight: 700; color: var(--brand-ink);
  }
  .nav-toggle::-webkit-details-marker { display: none; }
  .nav-toggle-bars {
    width: 15px; height: 2px; background: currentColor; position: relative;
  }
  .nav-toggle-bars::before, .nav-toggle-bars::after {
    content: ""; position: absolute; left: 0; width: 15px; height: 2px; background: currentColor;
  }
  .nav-toggle-bars::before { top: -5px; }
  .nav-toggle-bars::after  { top:  5px; }
  .nav-wrap[open] .nav-toggle { background: var(--brand-ink); color: var(--brand-sand); }

  /* The open menu is a panel under the whole header, not a column beside the
     button. It was styled as if .nav were a direct child of the header's
     .wrap — it is not, it sits inside <details class="nav-wrap"> — so
     flex-basis: 100% was landing on an element that is not in that flex row
     at all, and the menu opened as a narrow strip pinned under the toggle on
     the right, with the call to action stranded in the middle of it. */
  .site-head .nav {
    position: absolute; top: 100%; left: 0; right: 0; z-index: 40;
    flex-direction: column; align-items: stretch;
    gap: 0; margin: 0; padding: 6px 22px 14px;
    background: var(--brand-sand);
    border-top: 1px solid var(--hair);
    box-shadow: 0 22px 34px -24px rgba(29,28,33,.75);
  }
  .site-head .nav a {
    padding: 13px 2px; border-bottom: 1px solid var(--hair); border-top: 0;
    font-size: var(--fs-md); font-weight: 600; color: var(--brand-ink);
  }
  .site-head .nav a:last-child { border-bottom: 0; }
  .site-head .nav a[aria-current="page"] { background: var(--brand-tint); color: var(--brand-accent-ink); }
  /* Keep the logotype and the call to action in proportion. A 44px button
     beside a 21px logotype reads as a button with a logo attached. */
  .site-head .lockup { font-size: 24px; }
  .nav-cta {
    font-size: var(--fs-sm); padding: 9px 15px; min-height: 38px;
  }

  .site-head .wrap { row-gap: 4px; position: relative; }
  /* Not sticky on a phone. Wrapped onto three lines the header is 184px, and
     permanently occupying 23% of the screen costs more than scrolling back up
     does. The contact button in the corner stays reachable regardless. */
  .site-head { position: static; }
}

/* ---------- buttons ---------- */
/* Primary is INK, never magenta — a magenta button beside a destructive
   red one is a real hazard. BRAND.md §3.5. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  min-height: 44px;
  font: inherit; font-size: var(--fs-base); font-weight: 700;
  padding: 12px 22px; border-radius: 4px; text-decoration: none;
  border: 1px solid transparent; cursor: pointer;
}

/* Depth comes from edges and cast shadow, never from a gradient across the
   fill. The light end of the accent ramp puts white text at 3.42:1, so a
   button that fades toward it fails contrast across its own top half — the
   fill stays flat and measured, and the dimension is a lit top edge, a solid
   seated edge beneath, and the shadow the object casts on the page.
   --btn-seat is how far the button stands off the page; :active drops it to
   nothing, so pressing one actually puts it down. */
.btn-primary {
  background: var(--brand-accent-deep); color: #fff;   /* 4.79:1 */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.28),
    inset 0 -1px 0 rgba(0,0,0,.18),
    0 2px 0 var(--brand-accent-ink),
    0 4px 9px -3px rgba(142,16,84,.38),
    0 12px 22px -14px rgba(142,16,84,.55);
}
.btn-primary:hover {
  background: var(--brand-accent-hover);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.3),
    inset 0 -1px 0 rgba(0,0,0,.2),
    0 3px 0 var(--brand-accent-ink),
    0 7px 14px -4px rgba(142,16,84,.42),
    0 18px 30px -16px rgba(142,16,84,.6);
}
.btn-primary:active {
  box-shadow:
    inset 0 2px 5px rgba(0,0,0,.3),
    inset 0 1px 0 rgba(0,0,0,.14),
    0 0 0 var(--brand-accent-ink),
    0 1px 2px rgba(142,16,84,.3);
}

.btn-ghost {
  border-color: var(--brand-accent-deep); color: var(--brand-accent-ink-2); /* 5.65:1 */
  background: var(--brand-sand-2);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.9),
    0 2px 0 rgba(142,16,84,.2),
    0 4px 9px -4px rgba(29,28,33,.16),
    0 12px 20px -14px rgba(29,28,33,.4);
}
.btn-ghost:hover {
  background: #fff;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    0 3px 0 rgba(142,16,84,.26),
    0 7px 14px -5px rgba(29,28,33,.2),
    0 18px 28px -18px rgba(29,28,33,.45);
}
.btn-ghost:active {
  background: var(--brand-tint);
  box-shadow:
    inset 0 2px 5px rgba(142,16,84,.18),
    0 0 0 rgba(142,16,84,0),
    0 1px 2px rgba(29,28,33,.16);
}

.btn-row { display: flex; flex-wrap: wrap; gap: 12px; }
/* A button row that follows the no-JS note needs its own space. Every other
   .btn-row on the site sits inside a block whose siblings give it 12-40px; the
   one added to /studio's <noscript> on 2026-09-20 measured 0.0px above it at all
   six widths, the only one of 37 sitewide (QA re-pass, F-R1). Scoped to this
   pairing rather than given to .btn-row generally, which would shift the other 36. */
.gp-note + .btn-row { margin-top: 16px; }

/* ---------- hero ---------- */
.hero { padding: var(--sec-y) 0 calc(var(--sec-y) * .92); }
.hero h1 { max-width: 17ch; }
.hero .lede { font-size: clamp(18px, 2.1vw, 21px); color: var(--ink-2); max-width: 54ch; }
.hero .lede b { color: var(--brand-ink); font-weight: 600; }

/* ---------- audience split ---------- */
/* Two paths, not one universal pitch: enterprise and small-business
   buyers weigh genuinely different things. */
.paths { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }
.path {
  background: var(--brand-sand-2); border-radius: 4px; padding: 30px 28px;
  display: flex; flex-direction: column; gap: 14px;
}
.path h3 { color: var(--brand-ink); }
.path p { font-size: var(--fs-base); color: var(--ink-2); }
.path .go {
  margin-top: auto; padding-top: 6px;
  font-size: var(--fs-base); font-weight: 700; color: var(--brand-accent);
  text-decoration: none; align-self: flex-start;
}
.path .go:hover { text-decoration: underline; text-underline-offset: 3px; }

/* ---------- products ---------- */
.products { display: flex; flex-direction: column; gap: 16px; }
.product {
  display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 24px;
  align-items: start;
  background: var(--brand-sand-2); border-radius: 4px; padding: 30px 28px;
}
@media (max-width: 720px) { .product { grid-template-columns: 1fr; } }
.product .body { display: flex; flex-direction: column; gap: 11px; }
.product p { font-size: var(--fs-base); color: var(--ink-2); }
.product .meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pill {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: 4px 10px; border-radius: 99px;
}
.pill-live  { background: rgba(5,150,105,.13); color: #047955; }
.pill-build { background: rgba(29,28,33,.08); color: var(--ink-2); }

/* ---------- ethos band ---------- */
.band {
  background: var(--brand-ink); color: var(--brand-sand);
  border-radius: 4px; padding: 58px 44px;
  display: flex; flex-direction: column; gap: 20px;
}
.band h2 { color: var(--brand-sand); font-weight: var(--wt-display-dark); }
.band .big {
  font-family: "Bricolage Grotesque", sans-serif; font-variation-settings: "wdth" 94;
  font-size: clamp(23px, 3.2vw, 34px); font-weight: var(--wt-display-dark); line-height: 1.22;
  letter-spacing: -.014em; max-width: 21ch;
}
/* The emphasis still has to out-weigh the line it sits in. Two steps above the
   line, not a brick: at 800 against a 500 line it reads as a different font. */
.band .big b { color: var(--brand-accent); font-weight: 700; }
.band p { color: var(--on-ink); }

/* ---------- footer ---------- */
/* Black, not sand (G4-5). On sand the footer's grey text read
   thin however well it measured; on ink the same copy sits at 8:1 or better and
   every page gets a firm end. No top margin: it left a strip of sand between the
   last section and the black. /websites and /responsible-ai already end on a dark
   .cta-band, so there the hairline is what separates the two. */
.site-foot {
  background: var(--brand-ink); color: var(--brand-sand);
  border-top: 1px solid rgba(243,238,229,.14);
  padding: 48px 0 60px; margin-top: 0;
}
.site-foot .cols {
  display: flex; flex-wrap: wrap; gap: 40px; justify-content: space-between;
  align-items: flex-start;
}
.site-foot .lockup { font-size: 23px; }
.site-foot nav { display: flex; flex-wrap: wrap; gap: 22px; }
.site-foot nav a { font-size: var(--fs-base); color: var(--ink-2); text-decoration: none; }
.site-foot nav a:hover { color: var(--brand-ink); }
.legal {
  margin-top: 34px; padding-top: 20px; border-top: 1px solid var(--hair);
  font-size: var(--fs-sm); color: var(--ink-3); max-width: none;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}


/* ============================================================
   HERO VISUAL — the product doing its job, not a stock photo.
   One-shot entrance on load: everything is at its resting state
   within 2.4s and stays there, so a thumbnail or a late reader
   sees the finished frame, never an empty one.
   ============================================================ */
.hero-grid {
  display: grid; grid-template-columns: minmax(0,1fr) minmax(0,400px);
  gap: 56px; align-items: center;
}
@media (max-width: 880px) { .hero-grid { grid-template-columns: 1fr; gap: 40px; } }

.hv { display: flex; flex-direction: column; }
.hv-card {
  background: #fff; border-radius: 4px; padding: 16px 18px;
  box-shadow: 0 1px 2px rgba(29,28,33,.05), 0 10px 26px -18px rgba(29,28,33,.4);
}
.hv-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink-2); margin-bottom: 14px;
}
.hv-head b { color: var(--brand-ink); font-weight: 700; }
.hv-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
.hv-day { aspect-ratio: 1 / 1; border-radius: 3px; background: rgba(29,28,33,.07); }
.hv-day.on { background: var(--brand-accent); animation: hvDay .34s cubic-bezier(.2,.8,.3,1) both; }
@keyframes hvDay { from { opacity: 0; transform: scale(.55); } to { opacity: 1; transform: scale(1); } }

.hv-flow {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  padding: 13px 0; color: var(--ink-3); font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .03em;
  animation: hvFade .3s ease-out .42s both;
}
.hv-flow svg { display: block; }

.hv-inv { animation: hvRise .42s cubic-bezier(.2,.8,.3,1) .52s both; }
@keyframes hvRise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes hvFade { from { opacity: 0; } to { opacity: 1; } }

.hv-inv-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.hv-inv-top b { font-size: var(--fs-base); font-weight: 800; letter-spacing: -.01em; }
.hv-pdf {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  background: rgba(5,150,105,.13); color: #047955; padding: 4px 9px; border-radius: 99px;
}
.hv-line {
  display: flex; justify-content: space-between; gap: 14px;
  font-size: var(--fs-sm); color: var(--ink-2); padding: 8px 0;
  border-bottom: 1px solid var(--hair);
  font-variant-numeric: tabular-nums;
  animation: hvFade .4s ease-out both;
}
.hv-line:nth-of-type(1) { animation-delay: .66s; }
.hv-line:nth-of-type(2) { animation-delay: .74s; }
.hv-total {
  display: flex; justify-content: space-between; gap: 14px;
  font-size: var(--fs-md); font-weight: 800; padding-top: 12px;
  font-variant-numeric: tabular-nums;
  animation: hvFade .34s ease-out .84s both;
}

@media (prefers-reduced-motion: reduce) {
  .hv-day.on, .hv-flow, .hv-inv, .hv-line, .hv-total {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
}

/* ============================================================
   BENTO — panels of unequal weight. The composition carries the
   page so the copy does not have to; every panel is one idea in
   one or two sentences.
   ============================================================ */
.bento { display: grid; grid-template-columns: repeat(12, 1fr); gap: 14px; }
.b {
  border-radius: 6px; padding: 30px 30px 32px;
  display: flex; flex-direction: column; gap: 12px;
  min-width: 0;
}
.b h2, .b h3 { margin: 0; }
.b h2 { font-size: clamp(18px, 2.1vw, 21px); letter-spacing: -.02em; }
.b p { font-size: var(--fs-base); color: var(--ink-2); max-width: 44ch; }

.b-quiet  { background: var(--brand-sand-2); }
.b-ink    { background: var(--brand-ink); }
.b-ink h2, .b-ink h3 { color: var(--brand-sand); font-weight: var(--wt-display-dark); }
.b-ink p  { color: var(--on-ink); }
.b-accent { background: var(--brand-accent-panel); }
.b-accent h2, .b-accent h3 { color: var(--brand-sand); font-weight: var(--wt-display-dark); }  /* 5.17:1 */
.b-accent p  { color: #F6DCEA; }                                 /* 4.65:1 */
.b-accent .btn-primary { background: var(--brand-ink); color: var(--brand-sand); }
.b-accent .btn-primary:hover { background: #2C2A31; }
.b-open   { background: none; padding-left: 0; padding-right: 0; }

.s4  { grid-column: span 4; }
.s5  { grid-column: span 5; }
.s6  { grid-column: span 6; }
.s7  { grid-column: span 7; }
.s8  { grid-column: span 8; }
.s12 { grid-column: span 12; }
@media (max-width: 900px) {
  .s4, .s5, .s6, .s7, .s8 { grid-column: span 6; }
}
@media (max-width: 620px) {
  .s4, .s5, .s6, .s7, .s8 { grid-column: span 12; }
  .b { padding: 26px 24px 28px; }
}

/* a number/label that gives a panel a quiet anchor */
.b .tag {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3);
}
.b-ink .tag { color: var(--label-on-ink); }
/* Same reasoning as --label-on-ink: an 11px uppercase tracked label needs more
     than a passing ratio. #F6DCEA was 4.65:1 on the panel; white is 5.98:1 and
     is what the buttons on this ground already use, so it adds no new value to
     the palette. Was #EFC2DB at 3.82:1 before that. */
  .b-accent .tag { color: #fff; }   /* 5.98:1 on --brand-accent-panel */
.b .spacer { flex: 1; min-height: 6px; }

/* the one big statement, inside a panel */
.b .big {
  font-family: "Bricolage Grotesque", sans-serif; font-variation-settings: "wdth" 94;
  font-size: clamp(22px, 2.9vw, 31px); font-weight: var(--wt-display-sm); line-height: 1.2;
  letter-spacing: -.014em; max-width: 20ch; color: var(--brand-ink);
}
.b .big b { color: var(--brand-accent); font-weight: 700; }

/* Theme overrides MUST sit after the base .big rule above. They carry the same
   specificity (0,2,0), so source order decides, and when these came first the
   base painted ink text onto an ink panel. */
.b-ink .big    { color: var(--brand-sand); }
.b-ink .big b  { color: var(--brand-accent); }
.b-accent .big { color: var(--brand-sand); }
/* The emphasis inside .big is accent-coloured, which on the accent panel put
   raspberry on raspberry at 1.40:1 — "strongest" was invisible. Ink is no use
   here either (2.83:1); white is the only tone that carries, at 5.98:1. */
.b-accent .big b { color: #fff; }
/* Ghost buttons inherit accent-deep text and border, which on these panels is
   3.54:1 on ink and worse on raspberry. Both take the panel's own text tone. */
.b-ink .btn-ghost, .b-accent .btn-ghost {
  border-color: rgba(243,238,229,.45); color: var(--brand-sand);
  background: rgba(243,238,229,.07);
  /* On a dark ground the seated edge is a shadow, not a darker ink: there is
     nothing darker to sit against, so the object is read from the lit top
     edge and the black it casts into the panel below it. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.16),
    0 2px 0 rgba(0,0,0,.42),
    0 5px 12px -4px rgba(0,0,0,.5);
}
.b-ink .btn-ghost:hover, .b-accent .btn-ghost:hover {
  background: rgba(243,238,229,.14); border-color: var(--brand-sand);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.24),
    0 3px 0 rgba(0,0,0,.46),
    0 9px 18px -5px rgba(0,0,0,.55);
}
.b-ink .btn-ghost:active, .b-accent .btn-ghost:active {
  box-shadow:
    inset 0 2px 6px rgba(0,0,0,.45),
    0 0 0 rgba(0,0,0,0);
}

/* ---------- profession picker ---------- */
/* A grid of real choices, not a tag cloud. Nineteen identical pills gave the
   eye nothing to land on and buried the one choice that is different in kind.
   Even columns, each carrying the object that job actually involves. */
/* Two groups with a labelled break between them, rather than one wall of
   nineteen. The break is not decoration: it separates "a business like yours"
   from "a team inside a bigger one", which are different questions with
   different answers. */
.picker { display: flex; flex-direction: column; gap: 30px; }
.pick-group { display: flex; flex-direction: column; gap: 12px; }
.pick-label {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-3); margin: 0;
}

.chips {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
  /* Every row the same height. Without this a row holding a two-line label
     ("Veterinary practice") is taller than the rows around it, and a grid whose
     rows are three different heights reads as ragged rather than as a set. */
  grid-auto-rows: 1fr;
  gap: 10px;
}
/* Scoped to this grid on purpose. `height: 100%` on .chip itself also hit
   .hp-chip, which is the same component inside a flex row on the homepage,
   and stretched every tile to the height of its container — tiles 260px tall
   with the results panel printing straight over the top of them. */
.chips > .chip { height: 100%; }

/* Fifteen businesses fill five columns exactly — three full rows, no orphan.
   auto-fit gave six and left three stranded on a fourth row, which is the
   ragged edge that made the whole block read as a dump rather than a set. */
@media (min-width: 960px) {
  .chips      { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .chips-wide { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.chip {
  font: inherit; font-size: var(--fs-base); font-weight: 600;
  display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
  min-height: 96px; padding: 16px 16px 15px;
  border-radius: 5px; cursor: pointer; text-align: left;
  background: var(--brand-sand-2); color: var(--brand-ink);
  border: 1px solid var(--hair);
  transition: border-color .16s ease, background-color .16s ease,
              transform .16s ease-out, box-shadow .16s ease;
}
.chip span { line-height: 1.25; margin-top: auto; }

/* Selecting a tile fills it, like a level rising.
 *
 * The homepage panel is a water surface, so this is the same idea one page
 * later rather than a new effect invented for this grid: the tile does not
 * switch colour, it fills. ::before is the body of it and ::after is the
 * surface — a bright line that rides up on top of the level, because the thing
 * that makes liquid read as liquid is that its top edge is a lit edge and not
 * simply where the colour stops.
 *
 * The easing overshoots slightly past 1 before settling. That is the slosh:
 * water arriving at a level goes a touch beyond it and comes back, and without
 * that the fill reads as a bar chart growing.
 *
 * Everything the tile paints sits above both layers, which is what .chip > *
 * is for — without it the fill covers its own label. */
.chip { position: relative; }
/* Every chip's content must be an ELEMENT, never a bare text node. A
   positioned pseudo-element paints above its parent's own inline text
   regardless of z-index, so a label written straight onto the button with
   textContent vanishes under the fill the instant the tile is selected. */
.chip > * { position: relative; z-index: 2; }
.chip::before, .chip::after {
  content: ''; position: absolute; left: 0; right: 0; z-index: 0;
  pointer-events: none;
}
.chip::before {
  top: 0; bottom: 0; border-radius: inherit;
  background: var(--brand-ink);
  transform: scaleY(0); transform-origin: 50% 100%;
  transition: transform .46s cubic-bezier(.2,.9,.25,1.06);
}
.chip::after {
  height: 2px; bottom: 0; z-index: 1;
  background: var(--brand-accent-lift); opacity: 0;
  transition: transform .46s cubic-bezier(.2,.9,.25,1.06), opacity .2s ease .34s;
}
.chip[aria-pressed="true"]::before { transform: scaleY(1); }
.chip[aria-pressed="true"]::after  { transform: translateY(-100%); opacity: 0; }
/* The surface line only exists while the level is moving: once the tile is
   full there is no surface to light, and a bright rule pinned under the top
   edge of a filled tile just looks like a mistake. */
.chip[aria-pressed="true"]::after { animation: chipSurface .46s ease-out; }
@keyframes chipSurface {
  0%   { opacity: 0; }
  18%  { opacity: .85; }
  86%  { opacity: .55; }
  100% { opacity: 0; }
}
/* The count sits under the label, quiet on purpose: it is the reason to click
   this tile rather than read all fifteen, but the trade is still the thing
   being chosen, so the number must not outrank its own label. */
.chip-n {
  font-style: normal; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .01em; color: var(--ink-3); line-height: 1.2;
  transition: color .16s ease;
}
.chip:hover .chip-n { color: var(--brand-accent-ink-2); }
.chip[aria-pressed="true"] .chip-n { color: var(--brand-tint-2); }
.chip-i {
  /* Madurai_2 redraw ("these icons are losing pixel in big screen"). Drawn on
     a 32 grid with 2px lines on whole-pixel coordinates and shown at 32px, so
     every line lands on the pixel grid at 1x and 2x. The old set was a 100
     grid at 26px: 0.68px lines at 55% opacity, which a 1x screen smears grey.
     Each icon is two paths: .f, a soft fill, under the line drawing. */
  width: 32px; height: 32px; flex: none;
  fill: none; stroke: var(--brand-accent-deep); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  /* Pivot low on the glyph, so it hinges up off the tile like an object being
     picked up rather than scaling from its middle like a zoom. */
  transform-origin: 50% 100%;
  transition: stroke .18s ease, transform .22s cubic-bezier(.22,.9,.3,1.12);
}
.chip-i .f { fill: var(--brand-tint-2); stroke: none; transition: fill .18s ease; }

/* The icon stands up when you point at the tile.
 *
 * Hover-only, and only where hovering is a real thing: a touch screen has no
 * hover, it has a tap that LEAVES the hover state stuck on afterwards, so a
 * phone would be left holding a popped icon on the last tile touched until
 * something else was tapped. (hover: hover) and (pointer: fine) together mean
 * a mouse or a trackpad and nothing else. Touch gets the same motion from
 * :active below, which is correct for touch because it lasts exactly as long
 * as the finger does.
 *
 * The drop-shadow is what sells it. A transform alone is an object changing
 * shape; a transform with a shadow that falls away beneath it is an object
 * leaving the surface. Because it is a filter and not a box-shadow it follows
 * the drawn stroke rather than the icon's square box, so the shadow is the
 * shape of the thing itself. */
@media (hover: hover) and (pointer: fine) {
  /* Flat: a 3D transform with a drop-shadow filter made the browser paint the
     icon small and stretch the bitmap, which was the blur on a big screen. */
  .chip:hover .chip-i { transform: translateY(-3px) rotate(-5deg); }
  .chip:hover .chip-i .f { fill: #F2BFDB; }
  .chip:hover {
    transform: translateY(-2px);
    border-color: var(--brand-tint-2);
    box-shadow: 0 10px 20px -12px rgba(29,28,33,.4);
  }
}

/* Pressing it puts the object back down — the icon settles and the tile takes
   the press, which is the same 2px the buttons use across the site. */
.chip:active { transform: translateY(1px); }
.chip:active .chip-i { transform: translateY(-1px); }

@media (prefers-reduced-motion: reduce) {
  .chip:hover .chip-i, .chip:active .chip-i,
  .pick-other:hover .chip-i {
    transform: none;
  }
  .chip:hover, .chip:active { transform: none; }
}
.chip[aria-pressed="true"] .chip-i { stroke: var(--brand-accent-lift); }
.chip[aria-pressed="true"] .chip-i .f { fill: rgba(238,75,162,.2); }
/* The one choice that is a different kind of choice, given the shape of one.
   It was a dashed tile sitting alone on a fourth row: a dashed box in a grid
   of solid ones does not read as "deliberately different", it reads as the
   one that failed to load. A full-width bar with its own sentence cannot be
   mistaken for a tile at all. */
.pick-other {
  display: flex; flex-direction: row; align-items: center; gap: 16px;
  width: 100%; min-height: 0; padding: 20px 22px; text-align: left;
  /* The same hair border as every chip above it. It carried a magenta outline
     until now, which was redundant twice over: the bar is already full width
     with its own sentence and an arrow, so it cannot be mistaken for a tile —
     and in a grid where the SELECTED chip is the solid dark one, an outlined
     box reads as a third state, either "already chosen" or "something is
     wrong". It is neither. It is one more option, and it should sit in the set
     rather than shout across it. The accent stays on the pin and the arrow,
     which is where it says "this one goes somewhere different" without
     claiming a state. */
  background: var(--brand-sand-2); border: 1px solid var(--hair);
  border-radius: 5px; cursor: pointer; font: inherit;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.9),
    0 2px 0 rgba(142,16,84,.16),
    0 5px 12px -6px rgba(29,28,33,.2);
  transition: transform .16s ease-out, box-shadow .16s ease, background-color .16s ease;
}
.pick-other:hover {
  transform: translateY(-1px); background: #fff;
  border-color: var(--brand-accent-deep);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    0 3px 0 rgba(142,16,84,.22),
    0 10px 20px -8px rgba(29,28,33,.28);
}
.pick-other:active { transform: translateY(2px); box-shadow: inset 0 2px 5px rgba(142,16,84,.16); }
.pick-other .chip-i { margin: 0; }
@media (hover: hover) and (pointer: fine) {
  .pick-other:hover .chip-i { transform: translateY(-3px) rotate(-5deg); }
  .pick-other:hover .chip-i .f { fill: #F2BFDB; }
}
.pick-other-t { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.pick-other-t b { font-size: var(--fs-md); font-weight: 700; color: var(--brand-ink); }
.pick-other-t em {
  font-style: normal; font-size: var(--fs-base); font-weight: 500; color: var(--ink-2);
}
.pick-other-go {
  width: 22px; height: 22px; flex: none; margin-left: auto;
  color: var(--brand-accent-ink-2);
}
.pick-other[aria-pressed="true"] {
  background: var(--brand-ink); border-color: var(--brand-ink);
}
.pick-other[aria-pressed="true"] .chip-i { stroke: var(--brand-accent-lift); }
.pick-other[aria-pressed="true"] .chip-i .f { fill: rgba(238,75,162,.2); }
.pick-other[aria-pressed="true"] .pick-other-t b { color: var(--brand-sand); }
.pick-other[aria-pressed="true"] .pick-other-t em { color: #B7B2BC; }
.pick-other[aria-pressed="true"] .pick-other-go { color: var(--brand-accent-lift); }


@media (max-width: 560px) {
  .chips { grid-template-columns: repeat(auto-fit, minmax(136px, 1fr)); gap: 8px; }
  .chip { min-height: 84px; padding: 13px 13px 12px; font-size: var(--fs-sm); }
  .chip-i { width: 24px; height: 24px; }
}
/* No background here any more: it used to snap straight to ink, which meant
   the fill above rose behind a tile that was already the colour it was rising
   to. The colour now comes from ::before; only the text and the border change
   on the element itself, timed to the level passing them. */
.chip[aria-pressed="true"] {
  color: var(--brand-sand); border-color: var(--brand-ink);
  transition: color .3s ease .1s, border-color .3s ease .12s;
}

@media (prefers-reduced-motion: reduce) {
  .chip::before, .chip::after { transition: none; animation: none; }
  .chip[aria-pressed="true"] { background: var(--brand-ink); transition: none; }
}

.picks { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.picks label {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 10px 12px; border-radius: 4px; cursor: pointer;
  font-size: var(--fs-base); line-height: 1.45; color: var(--ink-2);
}
.picks label:hover { background: var(--brand-sand-2); color: var(--brand-ink); }
.picks input { margin: 4px 0 0; width: 17px; height: 17px; accent-color: var(--brand-accent); flex: none; }
.picks input:checked + span { color: var(--brand-ink); font-weight: 600; }

textarea {
  font: inherit; font-size: max(16px, var(--fs-base)); width: 100%;   /* iOS zoom floor, BACKLOG 172: the /build picker's own textarea (#own) takes this rule, not .field */
  padding: 12px 14px; border-radius: 4px; resize: vertical;
  border: 1px solid var(--hair); background: var(--brand-sand);
  color: var(--brand-ink);
}
textarea:focus { outline: 2px solid var(--brand-accent); outline-offset: 1px; border-color: transparent; }
.fineprint { font-size: var(--fs-sm); color: var(--ink-3); margin-top: 10px; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------- contact form ---------- */
.form { display: flex; flex-direction: column; gap: 16px; margin-top: 4px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: var(--fs-base); font-weight: 600; color: var(--brand-ink); }
.field .opt { font-weight: 400; color: var(--ink-3); }
/* 16px is the floor, not 15. iOS Safari zooms the page in when a text field under
   16px takes focus, and does not zoom back out, so on an iPhone the contact form
   and the assistant's call-back and message forms (BACKLOG 172, site-assistant
   prerequisite I13) all lurched on the first tap. max() leaves anything the
   scale ever makes larger exactly as it was. */
.field input,
.field textarea {
  font: inherit; font-size: max(16px, var(--fs-base)); width: 100%;
  padding: 11px 13px; border-radius: 4px;
  border: 1px solid var(--hair); background: var(--brand-sand);
  color: var(--brand-ink);
}
.field textarea { resize: vertical; min-height: 120px; }
.field select { font-size: max(16px, var(--fs-base)); }
/* One line, not two. The ring used to sit 1px OUTSIDE the field's own hairline
   border, so a focused field showed a magenta ring with a pale line inside it —
   about 4px of decoration on an empty required field at the top of a form,
   which reads as "something is wrong here" rather than "you are here". Flush
   with the edge and in the deeper accent, it is still an unmissable 2px
   indicator without looking like an error. */
.field input:focus, .field textarea:focus, .field select:focus {
  outline: 2px solid var(--brand-accent-deep); outline-offset: 0; border-color: transparent;
}
/* Only flag a field the visitor has actually left, never on first paint. */
/* :user-invalid matches only after the field has been interacted with or the
   form submitted, so a pristine form never opens covered in red.

   The old guard was `:not(:placeholder-shown):invalid`, which silently depends
   on the field HAVING a placeholder: with none, :placeholder-shown never
   matches, the :not() is always true, and an empty required field is flagged
   before the visitor has typed a character. "Your name" and "Email" have
   labels rather than placeholders, so both opened in red. */
.field input:user-invalid,
.field textarea:user-invalid { border-color: var(--status-critical); }

@supports not selector(:user-invalid) {
  /* Older engines: the placeholder-dependent guard is the best available, so
     fields without a placeholder get one that is never shown to anyone. */
  .field input:not(:placeholder-shown):invalid,
  .field textarea:not(:placeholder-shown):invalid { border-color: var(--status-critical); }
}
.form .btn { align-self: flex-start; }
.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ============================================================
   ASSIST DOCK — the assistant.
   The rules now live in the site-assistant component and are
   served as /assets/css/assist.css; the build copies them.

   What stays here is the mapping. The component owns its own
   --sa-* tokens precisely so it does not ship one brand's
   token names to applications under another, and this is
   where iamtec.ai says which of its own tokens fill them.
   Every --sa-* also has a literal fallback in the component,
   so a missed mapping is a plain panel rather than an
   invisible one — but every one of the fourteen is mapped
   here, and none of those fallbacks is ever reached on this
   site.
   ============================================================ */
.assist {
  --sa-accent:      var(--brand-accent);
  --sa-accent-deep: var(--brand-accent-deep);
  --sa-accent-hover:var(--brand-accent-hover);
  --sa-accent-ink:  var(--brand-accent-ink-2);
  --sa-ink:         var(--brand-ink);
  --sa-ink-2:       var(--ink-2);
  --sa-ink-3:       var(--ink-3);
  --sa-surface:     var(--brand-sand);
  --sa-surface-2:   var(--brand-sand-2);
  --sa-hair:        var(--hair);
  --sa-fs-base:     var(--fs-base);
  --sa-fs-md:       var(--fs-md);
  --sa-fs-sm:       var(--fs-sm);
  --sa-fs-xs:       var(--fs-xs);
}

/* Back to top. Site furniture, not the assistant — it is driven by
   scroll-fx.js and only SITS in .assist-dock, which is a column the
   component leaves open for exactly this. It came out of the component
   during the extraction: a shared container is not shared ownership.
   The --sa-* tokens it uses are the ones mapped just above; they inherit
   down from .assist. */
.to-top {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: none; cursor: pointer;
  border-radius: 99px; border: 1px solid var(--sa-hair, #e0e0e0);
  background: var(--sa-surface-2, #f5f5f5); color: var(--sa-ink, #1b1b1f);
  box-shadow: 0 2px 0 rgba(29,28,33,.08), 0 10px 18px -12px rgba(29,28,33,.5);
  transition: background-color .16s ease, border-color .16s ease,
              color .16s ease, transform .16s ease-out;
}
.to-top:hover {
  background: #fff; border-color: var(--sa-accent-deep, #3a5a87);
  color: var(--sa-accent-ink, #2f4c73); transform: translateY(-2px);
}
.to-top:active { transform: translateY(1px); }
/* On phones the "Ask us" button is taken out of the dock's column
   (position:absolute, bottom:0), so the column stopped stacking the two and the
   chevron sat entirely underneath it: tapping it opened the assistant (DEF-029).
   It is taken OUT of the dock's flow as well, not just nudged up. While it sat
   in flow it gave the dock — and so the fixed .assist box, which spans the
   full width on phones — a height of 40px, and that invisible strip across the
   bottom of the screen swallowed taps meant for the page: a tap at the
   bottom-left landed on the assistant container. Absolutely positioned, the
   dock has no in-flow height and the strip is gone. bottom:55px is the 47px
   round button plus the dock's 8px gap; right:4px centres a 40px button over
   a 47px one. Only .to-top changes here — the dock
   and the button belong to the shared assistant component. */
@media (max-width: 560px) {
  .to-top { position: absolute; right: 4px; bottom: 55px; }
}
/* Confirmation beside the mailto copy fallback (website DEF-044). Site-owned; the
   assistant rules that shared this hunk live in the site-assistant component. */
.copy-note { font-size: var(--fs-sm); font-weight: 600; color: var(--brand-accent-ink-2); }

/* ---------- prose with a working aside ----------
   Beside long prose (.doc), a narrow card that does something related to what
   is being read. Sticky on desktop so it stays beside the reading; stacked
   under the prose below 960px, where there is no side to put it on. */
.doc-split { display: grid; gap: 36px; }
@media (min-width: 960px) {
  .doc-split { grid-template-columns: minmax(0, 1fr) 300px; gap: 60px; align-items: start; }
  .doc-aside { position: sticky; top: 96px; }
}
.doc-aside .b { padding: 26px 24px 24px; }
.doc-aside h3 { margin: 10px 0 8px; font-size: var(--fs-md); line-height: 1.25; }
.doc-aside p { margin: 0 0 16px; }
.doc-aside .aside-note { font-size: var(--fs-sm); margin: 14px 0 0; opacity: .82; }


/* ---------- inline booking ---------- */
#cal-inline { width: 100%; min-height: 560px; margin-top: 4px; border-radius: 5px; overflow: hidden; }
/* The card that stands in for the calendar until someone asks for it. Sized so
   the section does not jump much when the calendar replaces it. */
.cal-gate {
  display: flex; flex-direction: column; align-items: flex-start; gap: 16px;
  margin-top: 8px; padding: 28px 26px; border: 1px solid var(--hair); border-radius: 6px;
  background: var(--brand-sand);
}
.cal-gate p { margin: 0; max-width: 60ch; color: var(--ink-2); }
.cal-loading { margin: 14px 0 0; color: var(--ink-3); font-size: var(--fs-sm); }

/* ---------- FAQ ---------- */
/* Native <details>: keyboard accessible, findable by browser in-page search,
   and works with no JavaScript at all. */
.faq { display: flex; flex-direction: column; gap: 0; }
.faq-h {
  font-size: clamp(20px, 2.4vw, 25px); margin-bottom: 10px;
  padding-bottom: 10px; border-bottom: 2px solid var(--brand-accent);
}
.qa-item { border-bottom: 1px solid var(--hair); }
.qa-item summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: flex-start; gap: 14px;
  padding: 17px 4px; min-height: 44px;
  font-size: var(--fs-md); font-weight: 600; color: var(--brand-ink);
}
.qa-item summary::-webkit-details-marker { display: none; }
.qa-item summary::after {
  content: ""; flex: none; margin-left: auto; margin-top: 7px;
  width: 10px; height: 10px;
  border-right: 2px solid var(--brand-accent-deep);
  border-bottom: 2px solid var(--brand-accent-deep);
  transform: rotate(45deg); transition: transform .15s ease-out;
}
.qa-item[open] summary::after { transform: rotate(-135deg); margin-top: 11px; }
.qa-item summary:hover { color: var(--brand-accent-deep); }
.qa-item summary:focus-visible { outline: 2px solid var(--brand-accent); outline-offset: 2px; border-radius: 3px; }
.qa-a { padding: 0 4px 20px; display: flex; flex-direction: column; gap: 10px; }
.qa-a p { font-size: var(--fs-base); color: var(--ink-2); max-width: 68ch; }
@media (prefers-reduced-motion: reduce) { .qa-item summary::after { transition: none; } }

/* ============================================================
   MOTION
   Two rules throughout: nothing is parked invisible waiting on a
   script, and everything is off under prefers-reduced-motion.
   Scroll-linked work uses native view()/scroll() timelines, so
   there is no observer, no scroll listener and no JS at all.
   ============================================================ */

/* --- the mark answers you: the tittle jumps and settles --- */
/* On every page load, and again on every control pressed — one class, applied
   by script in both cases. Without it the mark is simply there, which stays
   the correct resting state: with scripting off, or reduced motion on,
   nothing moves and nothing is missing.
   It starts and ends in position and never fades: the dot is part of the
   name, and a logo that assembles from nothing reads as a page still
   loading. Jumping is something a thing that is already there does. */
.site-head .lockup svg circle.jump {
  transform-box: fill-box; transform-origin: center bottom;
  animation: tittleJump .82s cubic-bezier(.3,.7,.4,1) both;
}
@keyframes tittleJump {
  0%   { transform: none; }
  10%  { transform: translateY(1px) scaleY(.86) scaleX(1.1); }  /* crouch */
  34%  { transform: translateY(-15px) scaleY(1.08) scaleX(.94); }
  56%  { transform: translateY(0) scaleY(.9) scaleX(1.07); }    /* landing squash */
  72%  { transform: translateY(-5px) scaleY(1.03) scaleX(.98); }
  88%  { transform: translateY(0) scaleY(.97) scaleX(1.02); }
  100% { transform: none; }
}

/* --- hero content arrives in sequence, settled inside 0.8s --- */
.hero .b-open > * { animation: heroIn .5s cubic-bezier(.2,.8,.3,1) both; }
.hero .b-open > *:nth-child(1) { animation-delay: .06s; }
.hero .b-open > *:nth-child(2) { animation-delay: .14s; }
.hero .b-open > *:nth-child(3) { animation-delay: .20s; }
.hero .b-open > *:nth-child(4) { animation-delay: .26s; }
@keyframes heroIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* --- controls respond --- */
.btn {
  transition: transform .12s ease-out, background-color .12s ease-out,
              border-color .12s ease-out, box-shadow .12s ease-out;
}
.btn:hover { transform: translateY(-1px); }
/* Down past its resting position, so the travel matches the edge collapsing
   underneath it. A button that only changes colour on press feels drawn; one
   that moves the same distance its shadow loses feels like an object. */
.btn:active { transform: translateY(2px); }

@keyframes headLift {
  from { box-shadow: 0 0 0 rgba(29,28,33,0); }
  to   { box-shadow: 0 10px 26px -22px rgba(29,28,33,.85); }
}

@media (prefers-reduced-motion: reduce) {
  .site-head .lockup svg circle,
  .hero .b-open > *,
  section:not(.hero) .b,
  .faq-h, .qa-item, .site-head,
  .net-card, .net-card-rec span, .gp-panel {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  /* .assist-fab is the component's own; it disables its transition itself. */
  .btn, .chip, .b-quiet, .path, .cut { transition: none !important; }
}

/* A month-view calendar needs width. On a phone the panel's own padding was
   leaving it 216px, so the booking panel gives its gutters back. */
@media (max-width: 560px) {
  #book .b { padding-left: 12px; padding-right: 12px; }
  #cal-inline { min-height: 620px; }
}

/* ---------- homepage picker ---------- */
.hp { display: flex; flex-direction: column; gap: 14px; }
.hp-chips { display: flex; flex-wrap: wrap; gap: 10px; }
/* The answer under the chips: heading, list, button. It had no spacing of its
   own (#hpOut is not the flex parent the .hp gap applies to), so the three
   stacked edge to edge and read as congested (Naveen, Madurai_2). */
#hpOut { display: flex; flex-direction: column; gap: 14px; margin-top: 14px; }
#hpOut:empty { display: none; }
.hp-chip { font-size: var(--fs-sm); min-height: 38px; padding: 8px 14px; }
.hp-lead {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3); margin: 0;
}
.hp-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.hp-list li {
  display: flex; gap: 11px; font-size: var(--fs-base); line-height: 1.45; color: var(--ink-2);
}
.hp-list li::before {
  content: ""; flex: none; width: 7px; height: 7px; margin-top: 8px; border-radius: 2px;
  background: var(--brand-accent);
}
.hp-cta { align-self: flex-start; margin-top: 10px; }
@media (prefers-reduced-motion: no-preference) {
  .hp-list li { animation: rise .32s cubic-bezier(.2,.8,.3,1) both; }
  .hp-list li:nth-child(2) { animation-delay: .05s; }
  .hp-list li:nth-child(3) { animation-delay: .1s; }
}

/* ---------- footer ---------- */
.foot-grid {
  display: grid; grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr));
  gap: 40px 28px; align-items: start;
}
@media (max-width: 860px) { .foot-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (max-width: 480px) { .foot-grid { grid-template-columns: 1fr; gap: 30px; } }

.foot-brand { display: flex; flex-direction: column; gap: 12px; align-items: flex-start; }
/* The logotype's stem is fill="currentColor", so this alone turns the mark sand;
   the tittle and ".ai" keep their magenta. */
.foot-brand .lockup { font-size: 24px; color: var(--brand-sand); }
.foot-tag { font-weight: 600; font-size: var(--fs-base); max-width: 30ch; color: var(--brand-sand); }
.foot-note { font-size: var(--fs-base); color: #B7B2BC; max-width: 34ch; }   /* 8.15:1 on ink */
/* Naveen, 2026-09-20: bold, and no underline — "keep them bold as other text links
   in footer". So it takes the footer's own link treatment: sand on ink at 14.65:1,
   no underline, and the accent-lift hover the columns use.

   Weight 700 rather than the columns' 500, because this one sits INSIDE a sentence
   rather than in a list, and it is the only thing marking where the link starts.
   Dropping the underline without changing the colour would have left weight alone
   to do that — 700 against the paragraph's 600 — which is too fine a distinction to
   rest on (WCAG 1.4.1 wants more than colour, and this now has colour AND weight). */
.foot-note a {
  font-weight: 700; color: var(--brand-sand);   /* 14.65:1 on ink */
  text-decoration: none;
}
.foot-note a:hover { color: var(--brand-accent-lift); }   /* 4.97:1 */
.foot-btn { margin-top: 4px; font-size: var(--fs-base); padding: 10px 16px; min-height: 40px; }
/* The ghost button is drawn for sand: its accent-ink-2 text is 2.6:1 on ink, and
   its hover is a white fill that flashes on black. */
.site-foot .foot-btn {
  color: var(--brand-accent-lift); border-color: var(--brand-accent-lift);   /* 4.97:1 */
  background: transparent; box-shadow: none;
}
.site-foot .foot-btn:hover {
  background: var(--brand-accent-deep); border-color: var(--brand-accent-deep);
  color: #fff; box-shadow: none;   /* 4.79:1 */
}
.site-foot .foot-btn:active {
  background: var(--brand-accent-hover); border-color: var(--brand-accent-hover);
  color: #fff; box-shadow: none;
}

.foot-col { display: flex; flex-direction: column; gap: 9px; }
/* Sizes stay on the type scale (11px, 13px); the contrast does the work. */
.foot-col h2 {
  font-family: "Manrope", sans-serif; font-weight: 700;
  font-size: var(--fs-xs); letter-spacing: .1em; text-transform: uppercase;
  color: #B7B2BC; margin-bottom: 3px;   /* 8.15:1 on ink */
}
.foot-col a {
  font-size: var(--fs-base); font-weight: 500; color: var(--brand-sand);   /* 14.65:1 */
  text-decoration: none; width: fit-content; padding: 2px 0;
}
.foot-col a:hover { color: var(--brand-accent-lift); }   /* 4.97:1 */

.foot-bottom {
  margin-top: 42px; padding-top: 20px; border-top: 1px solid rgba(243,238,229,.14);
  display: flex; flex-wrap: wrap; gap: 8px 24px; justify-content: space-between;
  font-size: var(--fs-sm); color: #B7B2BC;   /* 8.15:1 on ink */
}
.foot-legal { max-width: 62ch; }
/* The small print stays out of the floating buttons' corner. At the end of the
   page back-to-top and "Ask us" sit on the bottom right, and on a phone they
   covered the company number at every scroll position (F19-7, V19-2). 140px of
   bottom padding cleared them but left a dead black band (Gate 4, round 2), so
   the room is taken from the right instead, and only as much as overlaps:
   the dock reaches 131px in from the window edge (20px offset + the labelled
   button), 61px on a phone (14px + the round one); the wrap's right edge sits
   40px in, or 22px at 720px and below, and further in once the window is wider
   than 1140px. 16px of clearance, which also covers a classic scrollbar. */
.foot-bottom { padding-right: max(0px, calc(107px - max(0px, (100vw - var(--wrap)) / 2))); }
@media (max-width: 720px) { .foot-bottom { padding-right: 125px; } }
@media (max-width: 560px) { .foot-bottom { padding-right: 55px; } }

/* ============================================================
   TOUCH TARGETS
   Nav and footer links are text links, comfortable with a mouse
   and too small for a thumb. Rather than inflating them for
   everyone, grow them only where the pointer is coarse, which is
   exactly where the 44px guidance applies.
   ============================================================ */
.nav a      { padding-block: 9px; }
.foot-col a { padding: 7px 0; }

@media (pointer: coarse) {
  .nav a      { padding-block: 13px; }
  .foot-col a { padding: 11px 0; }
  .qa-item summary { padding-top: 19px; padding-bottom: 19px; }
  .hp-chip    { min-height: 44px; }
}


/* ---------- prose documents ---------- */
.doc { max-width: 68ch; display: flex; flex-direction: column; gap: 16px; }
.doc h2 { font-size: clamp(20px, 2.5vw, 25px); margin-top: 22px; padding-bottom: 8px; border-bottom: 1px solid var(--hair); }
.doc h2:first-child { margin-top: 0; }
.doc p  { font-size: var(--fs-md); color: var(--ink-2); }
.doc ul { margin: 0; padding-left: 0; list-style: none; display: flex; flex-direction: column; gap: 12px; }
/* NOT display:flex. Flex would promote every inline child (strong, code, em)
   to a flex item and scatter them across the row instead of letting the
   sentence flow. Bullet is absolutely positioned instead. */
.doc li { position: relative; padding-left: 20px; font-size: var(--fs-md); color: var(--ink-2); line-height: 1.6; }
.doc li::before {
  content: ""; position: absolute; left: 0; top: 11px;
  width: 7px; height: 7px; border-radius: 2px; background: var(--brand-accent);
}
.doc strong { color: var(--brand-ink); }
.doc code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em; background: var(--brand-sand-2); padding: 2px 6px; border-radius: 3px; }
.doc a { color: var(--brand-accent-ink-2); font-weight: 600; }  /* 5.65:1; accent-deep was 4.14:1 */
/* ...but a BUTTON inside .doc is still a button. `.doc a` (0,1,1) outranked `.btn`
   (0,1,0), so the two CTAs added to /responsible-ai on 2026-09-20 rendered at 600
   while every other .btn on all 17 pages renders at 700 — including an identical
   "How we build" button further down the same page. Found by the QA re-pass, F-R2.
   `.doc .btn` is 0,2,0 and wins without touching the link rule beside it. */
.doc .btn { font-weight: 700; }
.foot-links { display: flex; flex-wrap: wrap; gap: 18px; }
.foot-links a { color: #B7B2BC; text-decoration: none; }   /* 8.15:1 on ink */
.foot-links a:hover { color: var(--brand-accent-lift); }

/* ============================================================
   SCROLL ASSEMBLY
   Six scattered pieces converge into the mark across the length
   of the page, scrubbed by scroll position rather than played on
   a timer. Scrolling back up takes it apart again, because a
   scroll timeline is a position, not a trigger.
   ============================================================ */
.asm {
  position: fixed; left: 30px; bottom: 34px;
  width: 140px; height: 170px; pointer-events: none; z-index: 0;
}
.asm-p, .asm-dot { position: absolute; display: block; background: var(--brand-ink); opacity: .13; }

/* assembled state: five blocks stack into the stem, one dot becomes the tittle */
.asm-1 { left: 44px; top:  66px; width: 24px; height: 18px; border-radius: 3px 3px 0 0; }
.asm-2 { left: 44px; top:  86px; width: 24px; height: 18px; }
.asm-3 { left: 44px; top: 106px; width: 24px; height: 18px; }
.asm-4 { left: 44px; top: 126px; width: 24px; height: 18px; }
.asm-5 { left: 44px; top: 146px; width: 24px; height: 18px; border-radius: 0 0 3px 3px; }
.asm-6 {
  left: 80px; top: 20px; width: 26px; height: 26px; border-radius: 50%;
  background: var(--brand-accent); opacity: .3;
}

/* each piece arrives from somewhere different, so it reads as assembly
   rather than a group slide */
@keyframes asmA { from { opacity: 0; transform: translate(-96px,  54px) rotate(-42deg); } to { opacity: .13; transform: none; } }
@keyframes asmB { from { opacity: 0; transform: translate( 118px, 96px) rotate( 30deg); } to { opacity: .13; transform: none; } }
@keyframes asmC { from { opacity: 0; transform: translate(-140px,-70px) rotate( 62deg); } to { opacity: .13; transform: none; } }
@keyframes asmD { from { opacity: 0; transform: translate(  90px,-120px) rotate(-24deg); } to { opacity: .13; transform: none; } }
@keyframes asmE { from { opacity: 0; transform: translate(-70px, 150px) rotate( 18deg); } to { opacity: .13; transform: none; } }
@keyframes asmF { from { opacity: 0; transform: translate( 150px,-40px) scale(.3); }      to { opacity: .3;  transform: none; } }

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .asm-p, .asm-dot {
      animation-timeline: scroll(root block);
      animation-duration: auto;
      animation-timing-function: linear;
      animation-fill-mode: both;
    }
    /* staggered ranges: the stem builds bottom-up, the tittle lands last */
    .asm-5 { animation-name: asmA; animation-range: 0%  38%; }
    .asm-4 { animation-name: asmB; animation-range: 6%  48%; }
    .asm-3 { animation-name: asmC; animation-range: 14% 58%; }
    .asm-2 { animation-name: asmD; animation-range: 22% 68%; }
    .asm-1 { animation-name: asmE; animation-range: 30% 78%; }
    .asm-6 { animation-name: asmF; animation-range: 44% 92%; }
  }
}

/* Without a scroll timeline the pieces simply sit assembled, which is a
   perfectly good resting state rather than a broken one. */
@media (prefers-reduced-motion: reduce) {
  .asm-p, .asm-dot { animation: none !important; transform: none !important; }
}
/* It competes with the content on anything narrow, and there is no room for
   it beside a single column. */
@media (max-width: 1100px) { .asm { display: none; } }

/* ---------- status pills ---------- */
/* flex:none so a pill never stretches to fill the row it sits in, and
   align-self:center so it sits on the heading's optical centre. */
.pill {
  display: inline-flex; align-items: center; gap: 7px;
  flex: none; align-self: center; white-space: nowrap;
  line-height: 1; padding: 6px 12px;
}
.pill-live::before {
  content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: #059669;
  box-shadow: 0 0 0 0 rgba(5,150,105,.55);
  animation: livePulse 2.4s cubic-bezier(.4,0,.6,1) infinite;
}
@keyframes livePulse {
  0%   { box-shadow: 0 0 0 0 rgba(5,150,105,.55); }
  65%  { box-shadow: 0 0 0 8px rgba(5,150,105,0); }
  100% { box-shadow: 0 0 0 0 rgba(5,150,105,0); }
}
.pill-build::before {
  content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: var(--ink-3);
}
@media (prefers-reduced-motion: reduce) {
  .pill-live::before { animation: none; box-shadow: 0 0 0 3px rgba(5,150,105,.2); }
}


/* A heading given tabindex="-1" is focused programmatically to move screen
   reader position after a swap. It is not a control, so it should not flash
   a focus ring at sighted users. */
[tabindex="-1"]:focus { outline: none; }

/* ============================================================
   SCROLL-LINKED WORD REVEAL
   Words brighten one after another as the block moves through
   the viewport. Scrubbed by scroll position, so it runs backwards
   when you scroll up.

   The words are split at BUILD time, so there is no JavaScript,
   no layout shift, and the text exists in the served HTML.

   Important: the resting colour is legible, not invisible. If the
   timeline never resolves the headline reads as muted grey rather
   than disappearing, which is the mistake made once already.
   ============================================================ */
/* Word colours, dim and lit, declared once per ground (BACKLOG 118). A light
   section takes the defaults written into the rules below; a dark band sets
   its own pair here, and every rule reads the pair. A new dark band carrying
   reveal words is added to this list and nowhere else.

   Five rules used to do this by hand, naming the bands or painting every band
   alike, and they disagreed. The reduced-motion rule for a page already running
   the effect painted every word ink, so switching Reduce Motion on mid-visit
   left all 122 words on dark grounds ink on ink, in Chromium and WebKit alike. */
.b-ink, .cta-band, .quote-band, .netband, .gp { --word-dim: #6B6676; --word-lit: var(--brand-sand); }
/* Dimmer over the footage, and still legible at its dimmest: this is emphasis
   arriving, not text appearing. */
.pin-band { --word-dim: #4A4553; --word-lit: var(--brand-sand); }

.reveal-words .w {
  /* Inline, not inline-block: the words only change colour, and a row of
     inline-blocks under text-wrap: balance could be broken once with the
     fallback font and not re-balanced when the real font arrived, leaving
     "It / takes three shapes" (Naveen, Madurai_2). */
  display: inline;
  /* BACKLOG 135: this is the RESTING state, in effect whenever .fx (added
     only by scroll-fx.js) is absent — not just before the script runs, but
     permanently for a visitor with no JavaScript, where it never runs at
     all. As --word-dim it was a starting point a script would brighten; with
     no script, it is what ships. Measured: 17 homepage band words at 1.83:1,
     77 more in ink cards at 3.05:1, both below their WCAG floor (QA
     Madurai_12, F-5). The reduced-motion rule below solved the identical
     problem the identical way already — resting colour is the LIT one when
     nothing will ever light it — so this mirrors it. Once .fx is present,
     the more specific `.fx .reveal-words .w` rule (below) wins as always,
     driving colour from --lit; this only changes what a no-JavaScript
     visitor, and only such a visitor, sees. */
  color: var(--word-lit, var(--brand-ink));
  transition: none;
}
/* There was a `.reveal-words .w::after { content: " " }` here (DEF-050), and
   removing it is the fix for a defect that was almost invisible on screen.

   It arrived with the original word reveal, when .w was display:inline-block
   and the real spaces between the spans therefore did not paint. Madurai_2
   changed .w to display:inline (see the note above) and left this behind. The
   markup has always had real space text nodes between the words; with this rule
   present, the generated space painted the gap and CSS whitespace collapsing
   then dropped the real one — measured at exactly 0px wide.

   Generated content is not text. It is absent from innerText, from the
   selection, from the clipboard and from find-in-page. So every reveal headline
   and paragraph on the site rendered nearly correctly, read correctly to a
   crawler (which sees the raw markup), and copied as "Itdoestheboringwork".
   Ctrl+F for a phrase inside one found nothing. 54 of 56 reveal elements were
   affected, on the 8 pages that carry any.

   Deleting it restores the text on all 54. It also corrects three renderings:
   /websites was showing "front of the shop ." with a space before the full
   stop at every width, and two headings re-wrap (/products h1 across
   384-608px, /cybersecurity h2 at 320-328px) to the break the plain sentence
   takes under text-wrap: balance.

   This paragraph replaces one that claimed the change "moves nothing … identical
   to the pixel", that the affected pages numbered 15, and that the 2 unaffected
   elements were .sweep-words. All three were wrong — there are no .sweep-words
   in the built site at all — and all three were withdrawn in DEFECTS.md and in
   docs/IMPACT-madurai-3-def-050.md while surviving HERE, in a comment that
   ships inside dist/assets/css/site.css, through two separate correction passes
   and a sweep that reported nothing left standing. Both of those passes looked
   only at markdown. The lesson is not about this defect: when a claim is
   withdrawn, grep the whole repository for it, including code, and including
   what gets published.

   Found while investigating BACKLOG 40, which is about the assistant's indexer
   joining words. The indexer was innocent: the page itself was serving joined
   text, and the indexer reads raw HTML, so it never saw the fault at all. */

@keyframes wordLight { to { color: var(--brand-ink); } }

/* On the ink panel the same effect runs between two light tones. */
@keyframes wordLightInk { to { color: var(--brand-sand); } }

@media (prefers-reduced-motion: reduce) {
  .reveal-words .w { animation: none !important; color: var(--word-lit, var(--brand-ink)); }
}

/* ============================================================
   FILL PANEL
   The panel fills from the bottom as it moves through the
   viewport, like liquid rising, and drains again on the way up.

   Built as a full-height block translated up from below rather
   than an animated height: it stays on the compositor, and the
   "surface" line rides the top edge without being distorted by
   a scaling transform.

   Colour is chosen so the text is legible at EVERY fill level,
   including half-filled. Sand reads 14.65:1 on the ink ground
   and 5.17:1 on the raspberry fill, so a word straddling the
   waterline is readable either side of it.
   ============================================================ */
.fill-box { position: relative; overflow: hidden; isolation: isolate; }
.fill-box > * { position: relative; z-index: 1; }
/* Body text inside the rising water is sand, not the ink panel's grey (DEF-039).
   The water's comment below says every tone keeps white text above 4.5:1, and
   for white it does (5.13:1 on the lightest layer). But a .b-ink paragraph is
   var(--on-ink), not white, and on rgb(189,22,112) that is 2.79:1 wherever the
   waterline has risen past the words. A moving background is judged at its
   worst frame. Sand holds about 5.1:1 on the lightest water and far more on ink. */
.fill-box p:not(.big) { color: var(--brand-sand); }
/* Every other colour over moving water, measured at its worst frame (DEF-041,
   DEF-042). The first fix checked only the body grey and left the tag and the
   pink emphasis in the same panels failing. Worst cases, computed from the
   shipped constants:
     rising water (lightest wave #BD1670)   tag #8F8A99 1.78 -> sand 5.17
                                            pink #E5218C 1.40 -> tint-2 4.44 (large text, needs 3)
     ripple crest (white at 0.16 over ink)  tag #8F8A99 3.05 -> #B4AFBD 4.78
                                            pink #E5218C 2.40 -> accent-soft 4.56 (large text)
   On magenta water nothing more saturated than tint-2 can pass, so the
   emphasis there is pale pink rather than pink. */
.fill-box .tag      { color: var(--brand-sand); }
.fill-box .big b    { color: var(--brand-tint-2); }
[data-ripple] .tag  { color: var(--on-ink); }
[data-ripple] .big b { color: var(--brand-accent-soft); }
.fill-box::before {
  content: "";
  position: absolute; left: 0; right: 0; top: 0; bottom: 0;
  z-index: 0;
  /* Three waves rather than one. A single curve reads as a decorative squiggle
     however well drawn; what makes it read as sea is parallax — layers at
     different wavelengths moving at different speeds, two of them in opposite
     directions, so crests overtake one another and the surface never shows the
     same shape twice within a cycle. The darker layers sit behind and show
     through above the front crest, which is what gives the water depth.

     All three tiles are 80 high and the solid body starts at exactly 80. They
     were three different heights first, and where the tallest ended it drew a
     hard dark line straight across the panel.

     Every tone is at or darker than the panel colour, so white text stays
     above 4.5:1 wherever the waterline crosses it. */
  background:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 420 80' preserveAspectRatio='none'><path d='M0,50 C32,34 64,28 105,38 C141,47 162,62 202,58 C242,54 266,32 311,36 C347,39 372,56 420,50 L420,80 L0,80 Z' fill='rgb(189,22,112)'/></svg>") 0 0 / 420px 80px repeat-x,
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 560 80' preserveAspectRatio='none'><path d='M0,40 C48,26 88,22 144,32 C200,42 232,56 288,50 C344,44 376,24 436,30 C488,35 528,46 560,40 L560,80 L0,80 Z' fill='rgb(166,19,98)'/></svg>") 0 0 / 560px 80px repeat-x,
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 760 80' preserveAspectRatio='none'><path d='M0,30 C76,40 135,38 211,28 C287,18 363,14 447,24 C531,34 608,36 692,28 C726,25 747,26 760,29 L760,80 L0,80 Z' fill='rgb(142,16,84)'/></svg>") 0 0 / 760px 80px repeat-x,
    linear-gradient(var(--brand-accent-panel), var(--brand-accent-panel)) 0 80px / 100% calc(100% - 80px) no-repeat;
  will-change: transform;
  animation: oceanDrift 26s linear infinite;
}

@keyframes fillUp { to { transform: translateY(0); } }

/* Unfilled is the resting state: the panel is its normal self, never blank. */
@media (prefers-reduced-motion: reduce) {
  .fill-box::before { animation: none; transform: translateY(100%); }
}

/* ============================================================
   DEPTH PASS
   Clean and premium are not the same thing. Flat panels on a
   flat ground read as tidy; what reads as considered is depth,
   weight and air. None of this is decoration for its own sake:
   each rule below gives an edge, a shadow or a breath that the
   eye reads as craft.
   ============================================================ */

/* Panels sit ON the page rather than being drawn on it: a hairline
   catches the light at the top, a long soft shadow grounds them. */
.b-quiet, .path, .cut {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.7),
    0 1px 2px rgba(29,28,33,.04),
    0 14px 32px -26px rgba(29,28,33,.5);
}
.b-quiet:hover, .cut:hover {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.85),
    0 2px 4px rgba(29,28,33,.05),
    0 20px 40px -26px rgba(29,28,33,.6);
}

/* The dark panels get real weight: a top edge that reads as light
   falling on it, and a deeper shadow beneath. */
.b-ink {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.09),
    0 24px 56px -34px rgba(29,28,33,.85);
}

/* More air. Cramped sections read as a brochure; space reads as confidence.
   The section and hero padding itself is declared once, further down with the
   rest of the layout scale; it used to be repeated here with identical values. */
.bento { gap: 18px; }

/* The headline carries the page, so let it. */
h1 { font-size: clamp(40px, 7.2vw, 78px); letter-spacing: -.022em; line-height: 1.02; }
.hero .lede { font-size: clamp(18px, 2.2vw, 22px); line-height: 1.48; }

/* A hairline above each major section, so the page has structure you can feel
   rather than just gaps. The line itself is drawn by the section (border-top:
   1px solid var(--rule)); this only establishes the containing block. */
section + section > .wrap { position: relative; }

/* ---------- product screenshots ---------- */
/* aspect-ratio is declared up front so the space is reserved before the image
   loads: no layout shift, and no jump as you scroll past. */
.shot {
  margin: 0; border-radius: 6px; overflow: hidden;
  background: var(--brand-sand-2);
  aspect-ratio: var(--ratio, 16 / 10);
  box-shadow:
    inset 0 0 0 1px rgba(29,28,33,.08),
    0 2px 6px rgba(29,28,33,.05),
    0 26px 50px -30px rgba(29,28,33,.55);
}
.shot img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top center; }
.b-ink .shot { box-shadow: inset 0 0 0 1px rgba(255,255,255,.1), 0 26px 50px -28px rgba(0,0,0,.8); }

/* A captioned screenshot, and a step laid out beside it (/timesheet). The
   screenshot is the one thing in these rows with a frame: it is the product,
   and the rest of the page is type on sand. */
.shot-fig { margin: 0; display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.shot-fig figcaption { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-3); }
.shot-row { align-items: start; }
.shot-row .shot-text p { max-width: 38ch; }
/* Steps alternate sides on wider screens. On a phone each step reads text
   first, then its screen, because that is the order in the markup. */
@media (min-width: 621px) { .shot-row .shot-flip { order: 2; } }
/* The portrait invoice would be taller than a screen at full column width. */
.shot-row .shot-portrait { max-width: 440px; }

.shot-zoom {
  all: unset; box-sizing: border-box; display: block; position: relative;
  width: 100%; height: 100%; cursor: zoom-in;
}
.shot-zoom:focus-visible { outline: 2px solid var(--brand-accent-deep); outline-offset: 3px; border-radius: 6px; }
/* Bottom left: the site's chat button and back-to-top chevron are fixed at
   bottom right, and on a phone they sat on top of this label. */
.shot-hint {
  position: absolute; left: 10px; bottom: 10px;
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .04em;
  padding: 6px 10px; border-radius: 3px;
  background: rgba(29,28,33,.86); color: var(--brand-sand);
  opacity: 0; transform: translateY(4px); transition: opacity .18s ease, transform .18s ease;
}
.shot-zoom:hover .shot-hint, .shot-zoom:focus-visible .shot-hint { opacity: 1; transform: none; }
/* No hover on touch, so the hint is simply there. */
@media (hover: none) { .shot-hint { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .shot-hint { transition: none; } }

.shot-dialog {
  width: min(1640px, calc(100vw - 32px)); max-width: none; max-height: calc(100vh - 32px);
  padding: 0; border: 0; border-radius: 4px;
  background: var(--brand-sand); color: var(--brand-ink);
  overflow: hidden;
}
.shot-dialog[open] { display: flex; flex-direction: column; }
.shot-dialog::backdrop { background: rgba(29,28,33,.78); }
.shot-dialog-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 10px 12px 10px 18px; border-bottom: 1px solid var(--hair);
}
.shot-dialog-cap { margin: 0; font-size: var(--fs-sm); color: var(--ink-2); }
.shot-dialog-view { overflow: auto; overscroll-behavior: contain; }
.shot-dialog-view img { display: block; width: 100%; height: auto; }
/* On a phone the screen opens at a width its text can be read at, and scrolls. */
@media (max-width: 720px) { .shot-dialog-view img { width: 1000px; max-width: none; } }
/* shot-zoom.js opens it on the app's content rather than its empty left edge,
   and a mouse can drag it as well as a finger (Gate 4, round 2: "we are not able
   to move"). The line saying so shows only where there is something to move. */
.shot-dialog-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.shot-dialog-hint { display: none; margin: 0; font-size: var(--fs-xs); font-weight: 700; color: var(--ink-2); }
/* Only the screenshot dialog drags; the /studio sample invoice shares
   .shot-dialog-view but cannot be moved, so it must not offer a grab cursor (F21-3). */
@media (max-width: 720px) {
  .shot-dialog-hint { display: block; }
  .shot-dialog:not(.inv-dialog) .shot-dialog-view { cursor: grab; touch-action: pan-x pan-y; }
  .shot-dialog:not(.inv-dialog) .shot-dialog-view.is-dragging { cursor: grabbing; user-select: none; }
}
/* Focus goes to Close when a dialog opens, so a mouse user never sees a ring on
   the frame; the browser's default one was an orange line round the whole
   dialog. But a keyboard can still reach the frame itself (WebKit with
   Option+Tab, Firefox with Shift+Tab), and removing the ring outright left
   nothing to see there (F21-1, WCAG 2.4.7). So: nothing for a mouse, a sand
   ring for a keyboard. Sand, not magenta: the ring sits outside the frame on
   the dimmed backdrop, where magenta measured about 2:1 and sand holds. Shared
   with the /studio sample invoice. */
.shot-dialog:focus:not(:focus-visible) { outline: none; }
.shot-dialog:focus-visible { outline: 2px solid var(--brand-sand); outline-offset: 3px; }
html.shot-open { overflow: hidden; }

/* ---------- fallback hooks ----------
   Scoped to browsers WITHOUT scroll timelines, and that scoping matters: these
   rules set the resting colour to fully lit, so when they applied everywhere
   the native animation ran from lit to lit and nothing appeared to happen.
   Driven by assets/js/scroll-fx.js. */

/* ---------- live invoice demo ---------- */
.demo { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1.15fr); gap: 22px; align-items: start; }
@media (max-width: 820px) { .demo { grid-template-columns: 1fr; } }

.demo-controls { display: flex; flex-direction: column; gap: 18px; }
.demo-field { display: flex; flex-direction: column; gap: 7px; }
.demo-field label { font-size: var(--fs-base); font-weight: 600; }
.demo-field .val { color: var(--brand-accent-ink-2); font-variant-numeric: tabular-nums; }  /* 5.65:1 */
.demo-field input[type="range"] { width: 100%; accent-color: var(--brand-accent-deep); height: 28px; }
.demo-check { display: flex; align-items: center; gap: 10px; font-size: var(--fs-base); font-weight: 600; cursor: pointer; }
.demo-check input { width: 18px; height: 18px; accent-color: var(--brand-accent-deep); }

.demo-card {
  background: #fff; border-radius: 6px; padding: 22px 24px;
  box-shadow: inset 0 0 0 1px rgba(29,28,33,.08), 0 20px 44px -28px rgba(29,28,33,.6);
  display: flex; flex-direction: column; gap: 4px;
}
.demo-card.issued { animation: issued .45s cubic-bezier(.2,.8,.3,1); }
@keyframes issued { 0% { transform: scale(.985); } 60% { transform: scale(1.006); } 100% { transform: none; } }

.demo-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px; }
.demo-head b { font-size: var(--fs-md); font-weight: 800; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.demo-head span { font-size: var(--fs-xs); color: var(--ink-3); }
.demo-row {
  display: flex; justify-content: space-between; gap: 16px;
  font-size: var(--fs-base); color: var(--ink-2); padding: 9px 0;
  border-bottom: 1px solid var(--hair); font-variant-numeric: tabular-nums;
}
.demo-total {
  display: flex; justify-content: space-between; gap: 16px;
  font-size: var(--fs-lg); font-weight: 800; padding-top: 14px;
  font-variant-numeric: tabular-nums;
}
.demo-note { font-size: var(--fs-sm); color: var(--ink-3); margin-top: 12px; }
@media (prefers-reduced-motion: reduce) { .demo-card.issued { animation: none; } }


/* ---------- section intros ----------
   A heading introducing a group of panels is not itself a panel. Previously
   these were bento cells with the background switched off inline, which left
   the panel shadow drawing an empty frame around a bare heading. */
.section-intro {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; gap: 8px;
  margin: 14px 2px 2px;
  background: none; box-shadow: none; padding: 0;
}
.section-intro h2 { font-size: clamp(24px, 3.4vw, 34px); letter-spacing: -.018em; }
.section-intro p { color: var(--ink-2); font-size: var(--fs-md); max-width: 56ch; }
.bento > .section-intro:first-child { margin-top: 0; }

/* ============================================================
   EDITORIAL PASS
   Everything below deliberately overrides the card treatment
   above. The page is carried by type, space and one rule, not
   by borders and shadows.

   When every block is a card, nothing has hierarchy. Fill,
   border, radius and shadow are spent on the two blocks that
   are genuinely different, and nowhere else.
   ============================================================ */

/* Panels stop being cards. */
.b-quiet, .cut, .path, .product {
  background: none !important;
  box-shadow: none !important;
  border-radius: 0;
  padding: 0;
}
.b-quiet:hover, .cut:hover { box-shadow: none !important; }

/* Structure comes from a hairline and space, not a container. */
.bento { gap: 0 40px; }
.b { padding: 38px 0 34px; }
.b-quiet { border-top: 1px solid var(--hair); }
/* A section heading is not a card, so it does not take the card rhythm.
 *
 * .bento is `row-gap: 0` on purpose — cards butt together and a hairline
 * separates them. With `margin: 0 0 4px` the .section-intro inherited that,
 * leaving FOUR pixels between a section's standfirst and the first card's
 * top hairline. The hairline then reads as underlining the standfirst rather
 * than starting the group beneath it, which is why Naveen kept seeing "the
 * start of the new section immediately after the first section" on
 * /responsible-ai — and on the six other pages built the same way.
 *
 * --blk, so it is the same gap the rest of the site puts between a prose run
 * and the object it introduces, rather than a seventh number. */
.section-intro { border-top: 0; padding-top: 8px; margin: 0 0 var(--blk); }
.section-intro + .b-quiet { border-top: 1px solid var(--hair); }
/* The dark feature panels carry `margin: 28px 0` so they breathe inside a
   row-gap: 0 grid. After a section heading that ADDS to --blk — margins do not
   collapse in a grid — and /products measured 82.7px where every other section
   heading on the site measured 54.7px. The heading's gap is already the right
   one, so the panel contributes nothing on top of it. */
.section-intro + .b-ink,
.section-intro + .b-accent { margin-top: 0; }
/* ...and so does the panel BESIDE it. The rule above is an adjacent-sibling rule,
   so it zeroes the top margin of the panel that follows the heading IN THE DOM.
   Two half-width panels sit side by side in the same row, but only the first is
   the heading's DOM sibling — so the second kept its own 28px, started 28px
   lower and, because an auto-height grid row sizes to the tallest item, ended up
   28px shorter as well. Both bottoms aligned and both tops did not, which is
   exactly what a reader sees as "uneven boxes" (Naveen, 2026-09-20, on /products).

   Restricted to panels narrower than the full grid: a full-width panel following
   another is on a NEW row and genuinely does want its 28px. That is the case on
   the homepage, which is why this cannot simply zero every following panel.

   And restricted to the widths where the two panels actually SHARE a row. The
   selector describes DOM order, which is true at every width; the defect it
   fixes is a grid-row effect, which is only true while .bento has two columns.
   .bento collapses to one column at max-width: 620px, so below 621px the two
   panels are stacked, the second one's 18px is the gap BETWEEN them rather than
   a mismatch beside them, and zeroing it left /products with 18px where every
   other pair of stacked dark panels on the site has 36px. Found by QA scope A,
   F-A1, 2026-09-20 — a regression introduced by the fix above, four hours after
   it shipped, and the reason a selector that reasons about rows has to name the
   width at which rows exist. */
@media (min-width: 621px) {
  .section-intro + :is(.b-ink, .b-accent):not(.s12)
                 + :is(.b-ink, .b-accent):not(.s12) { margin-top: 0; }
}
.b-open { border-top: 0; padding-top: 0; }

/* The two blocks that ARE different keep their weight, and gain room. */
.b-ink, .b-accent {
  border-radius: 3px;
  padding: 56px 44px 52px;
  margin: 28px 0;
}
/* Declared after the .b padding rules in every breakpoint, so the two blocks
   that keep their fill also keep their inset. Between 620 and 720 the generic
   .b rule was winning and the text sat flush against the dark edge. */
@media (max-width: 720px) {
  .b-ink, .b-accent { padding: 44px 30px 40px; margin: 22px 0; }
}
@media (max-width: 620px) {
  .b-ink, .b-accent { padding: 38px 24px 34px; margin: 18px 0; }
}

/* Type does the work. */
h1 {
  font-size: clamp(42px, 7.4vw, 88px); letter-spacing: -.024em; line-height: 1.0;
  /* NOT overflow-wrap: break-word. With text-wrap: balance the balancer is
     free to break a word mid-way to even the lines out, which produced
     "ourselve / s." It cured an overflow nobody could reproduce and caused a
     worse one that was plainly visible. Headroom in the type scale and
     min-width on the cell do the job instead. */
  overflow-wrap: normal;
  word-break: normal;
}
.hero .b { min-width: 0; }
h2 { font-size: clamp(24px, 2.9vw, 32px); letter-spacing: -.016em; }
.b h2 { font-size: clamp(19px, 2.2vw, 23px); }
.section-intro h2 { font-size: clamp(28px, 4.2vw, 46px); letter-spacing: -.02em; }
.hero .lede { font-size: clamp(19px, 2.3vw, 24px); line-height: 1.45; color: var(--ink-2); }
.b p { font-size: var(--fs-md); line-height: 1.6; }
/* Every 11px label on the site is 700. Two sat at 600 — .hv-flow's "INVOICE
   PRODUCED" and .chip-n's "5 problems" — which release QA pass 6 flagged (P6-3)
   as off the family. At this size the difference is not a hierarchy signal,
   it is one label looking slightly weaker than its neighbours for no reason.

   font-weight and text-transform belong HERE, not only on `.b .tag`.
   They were set only inside a card, so the fourteen eyebrow labels in cards
   rendered at 700 while the five sitting directly on a band inherited 400 —
   11px, weight 400, tracked .13em, on ink. Naveen: "it is not even visible",
   of "What we actually do" on the homepage. It passed contrast at 5.05:1 and
   was still unreadable, which is the point: contrast is not the only thing
   that makes small tracked type legible. Weight and tracking are the rest. */
.tag {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase;
}

/* Air. */
section { padding: var(--sec-y) 0; }
/* A ground with warmth, not a pattern (Madurai_2, Naveen: "some nice background
   … just to add some feel", and "we may not" need to overdo it). Two low blooms —
   the accent behind the picker, a warmer one under the headline — over a sand
   gradient, with fine grain so the gradient never bands on a wide screen. No
   pattern to read past, and the network language stays where it means something,
   in the two dark bands. Pure CSS: nothing to download, and the headline keeps
   its contrast. Every page's hero gets it, not only the homepage. */
.hero {
  padding: var(--sec-y) 0 calc(var(--sec-y) * .92);
  position: relative; isolation: isolate; overflow: hidden;
}
.hero::before, .hero::after { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.hero::before {
  /* No accent blooms. This carried three of them at 16%, 9% and 10% — a rosy
     tint across the whole opening screen, which is what made the site read as
     pink rather than as sand with a magenta accent. Naveen, 2026-09-18, looking
     at it beside a version without: "that basically giving a feel that it is
     too pinkish… like a japanese doll".

     Worth recording that he also asked for the blooms, on 2026-09-17 — "can we
     have some nice background here… think like a top notch UI", then "we may
     need to overdo there but just to add some feel" and, immediately after,
     "we may not". Seeing it in context is what settled it; this is a decision
     changed on evidence, not a mistake corrected.

     It also nearly cost the wrong fix. The complaint was "is raspberry too
     pinkish, can we make it teal", and the answer looked like a hue change to
     a brand still in DRAFT — 112 token uses, three SVG marks, and every
     contrast measurement in this release taken against magenta grounds. The
     hue was never the problem: a full-screen 16% tint was. */
  background: linear-gradient(170deg, var(--brand-sand-2) 0%, var(--brand-sand) 52%, #F1EBE0 100%);
}
.hero::after {
  opacity: .5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
}
.wrap { max-width: var(--wrap); padding: 0 40px; }
/* Big screens (Madurai_2, option B: "we need to widen it", from a 32-inch
   2560 screen). The content column grows from 1140px at a 1440 window to
   1520px, and above 1800px the whole page is zoomed so type, buttons and
   spacing grow with it rather than sitting laptop-sized in a wider column.
   Reading widths are set in ch, so paragraphs stay a comfortable length.
   Below 1440 nothing changes. Browsers without CSS zoom (Firefox < 126) get
   the wider column only. */
:root { --wrap: clamp(1140px, calc(1140px + (100vw - 1440px) * .7), 1520px); }
/* --vh is the window height in the page's own (zoomed) pixels. 100vh ignores
   the zoom, so a pane sized or centred with it overshot: on a 2560 screen the
   pinned bands sat 454px below the header and 92px above the bottom. */
:root { --zoom: 1; --vh: calc(100vh / var(--zoom)); }
@media (min-width: 1800px) { html { zoom: 1.12; } :root { --wrap: 1360px; --zoom: 1.12; } }
@media (min-width: 2300px) { html { zoom: 1.25; } :root { --zoom: 1.25; } }
@media (max-width: 720px) {
  section { padding: var(--sec-y) 0; }
  .hero { padding: var(--sec-y) 0 calc(var(--sec-y) * .92); }
  .wrap { padding: 0 22px; }
  .bento { gap: 0 24px; }
  .b { padding: 24px 0 20px; }
  /* A block that paints its own ground needs its own gutter. The shared rule
     above drops the side padding so plain blocks align to the page gutter —
     correct for them, wrong for a coloured panel, whose text then runs to the
     cut edge of the colour with nothing between the last glyph and the end of
     the box. Applies to every painted variant, not just the one it was
     noticed on. */
  .b-ink, .b-accent { padding-left: 22px; padding-right: 22px; }
}

/* One accent, used sparingly. Buttons keep it; nothing else competes. */
.pill { border-radius: 0; background: none !important; padding: 0; }
.pill-live  { color: #047955; }
.pill-build { color: var(--ink-3); }

/* The demo is the centrepiece, so it keeps its frame. */
.demo-card { border-radius: 3px; }

/* ---------- photography ----------
   Treated so it sits with the palette rather than shouting against it: a
   slight desaturation and a warm ground behind, so a photo reads as part of
   the page rather than pasted onto it. */
.shot-photo {
  border-radius: 3px; overflow: hidden; background: var(--brand-sand-2);
  box-shadow: none;
}
.shot-photo img {
  filter: saturate(.82) contrast(1.03);
  width: 100%; height: 100%; object-fit: cover; display: block;
}
/* A full-width quiet band between sections. */
.shot-band { border-radius: 0; margin: 0; }
.shot-band img { filter: saturate(.55) contrast(1.02) brightness(1.02); }


/* ============================================================
   REVEAL ON SCROLL
   Driven by scroll position through --in, --lit and --fill, set
   by assets/js/scroll-fx.js. Every property defaults to the
   finished state, so without JavaScript nothing is hidden and
   the page is complete, just static.

   Position-driven rather than triggered, so a block fades out
   again on the way back up instead of revealing once and staying.
   ============================================================ */
.fx section:not(.hero) .b:not(.b-open),
.fx .section-intro {
  --lift: 80px;
  opacity: var(--in, 1);
  /* 80px, not 30: tied to scroll, 30px over a third of a window is a section
     moving 10% faster than the page, which reads as no movement at all. Naveen
     at Gate 4 of Madurai_12, 2026-09-25: "they fade but they do not move". */
  transform: translateY(calc((1 - var(--in, 1)) * var(--lift)));
}
/* Less below 1120px, where a section's own bottom padding is smaller: at 80px,
   a card still rising is drawn past its section at every width from 375 up to
   1100 (QA round 3, Madurai_12 Gate 4 re-check) — up to 34px at 375, down to
   0.81px at 1100, and 0 from 1120px up. 44px is the most that stays inside
   every section at every width below that, and is still half as much again as
   the old 30px. */
@media (max-width: 1120px) {
  .fx section:not(.hero) .b:not(.b-open),
  .fx .section-intro { --lift: 44px; }
}

/* BACKLOG 136: at phone width, Tab can land inside a form or button while its
   section is still fading (92% to 58% of the window, Naveen's ruling on
   DEF-053 — forms and buttons fade like everything else). A visitor scrolling
   with a mouse sees the fade finish before they'd act on it; a keyboard user
   moving by Tab does not scroll first, so focus can land on a control at
   ~0.21-0.26 opacity, focus outline included, which /accessibility promises
   is always visible. A section holding keyboard focus shows at full strength;
   the fade still runs for everyone else. Higher specificity than the reveal
   rule above (one more pseudo-class on the same chain), so no !important. */
.fx section:not(.hero) .b:not(.b-open):focus-within {
  opacity: 1;
  transform: none;
}

.fx .reveal-words .w {
  color: color-mix(in srgb, var(--word-lit, var(--brand-ink)) calc(var(--lit, 1) * 100%), var(--word-dim, var(--ink-3)));
}

/* Scroll-tracked, so deliberately no transition: the level must follow the
   scroll exactly, up as well as down. A transition would lag behind it. */
.fill-box::before { transform: translateY(calc((1 - var(--fill, 0)) * 100%)); }

@media (prefers-reduced-motion: reduce) {
  .fx section:not(.hero) .b, .fx .section-intro {
    opacity: 1 !important; transform: none !important; color: var(--brand-ink) !important;
  }
  /* Each word in its own ground's lit colour. The words were in the rule above,
     and its ink turned every dark band's words ink on ink whenever Reduce Motion
     came on mid-visit (118). */
  .fx .reveal-words .w {
    opacity: 1 !important; transform: none !important; color: var(--word-lit, var(--brand-ink)) !important;
  }
}

/* ============================================================
   RESTORED after a stylesheet rewrite truncated everything past
   the reveal marker. Keep new blocks ABOVE that section, or
   append only.
   ============================================================ */

/* The waterline drifts, so the surface reads as liquid, not a shape. */
/* Each layer covers a whole number of its OWN tiles in one cycle — 3, 2 and 1
   — so every layer loops seamlessly while their apparent periods (8.7s, 13s,
   26s) stay out of step with one another. */
@keyframes oceanDrift {
  from { background-position-x: 0, 0, 0, 0; }
  to   { background-position-x: -1260px, 1120px, -760px, 0; }
}
@media (prefers-reduced-motion: reduce) { .fill-box::before { animation: none; } }

/* ---------- SWEEP ----------
   A highlight travelling through a sentence, one word at a time. Each word
   runs the same loop offset by its index, so a bright band moves across.
   Colour only: the sentence is fully legible at its dimmest, so this is
   emphasis rather than concealment. */
.sweep-band { background: var(--brand-ink); padding: var(--sec-y) 0; margin: 0; }
.sweep-band .wrap { max-width: var(--wrap); }
.sweep-inner { display: flex; flex-direction: column; gap: 26px; align-items: flex-start; }

.sweep-words {
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-variation-settings: "wdth" 94, "opsz" 72;
  font-weight: var(--wt-display-dark);
  font-size: clamp(32px, 6.2vw, 76px);
  line-height: 1.07;
  letter-spacing: -.02em;
  max-width: 20ch;
  margin: 0;
  color: #5C5766;
}
.sweep-words .w {
  display: inline-block;
  color: #5C5766;
  animation: sweep 5.5s linear infinite;
  animation-delay: calc(var(--i) * -0.34s);
}
@keyframes sweep {
  0%   { color: #5C5766; }
  6%   { color: var(--brand-sand); }
  13%  { color: #5C5766; }
  100% { color: #5C5766; }
}
.sweep-band .lede { color: var(--on-ink); font-size: clamp(17px, 1.9vw, 20px); max-width: 52ch; }
.sweep-band .tag  { color: var(--label-on-ink); }
.sweep-band .btn-primary { background: var(--brand-accent-deep); color: #fff; }

@media (prefers-reduced-motion: reduce) {
  .sweep-words, .sweep-words .w { animation: none !important; color: var(--brand-sand); }
}
@media (max-width: 620px) { .sweep-band { padding: 64px 0; } }


/* ============================================================
   PINNED BAND
   A tall section whose contents stay put while you scroll past,
   with the sentence lighting one word at a time as you travel
   through it. Scroll back up and it unlights: the lighting is
   tied to position, not to a trigger.
   ============================================================ */
.pin-band {
  background: var(--brand-ink);
  /* Height comes from its content: see "Not pinned either" below. */
  position: relative;
}
.pin-inner {
  position: sticky; top: 0;
  height: 100vh; min-height: 420px;
  display: flex; align-items: center;
}
.pin-content { display: flex; flex-direction: column; gap: 26px; align-items: flex-start; }
/* The wrap is a flex item of .pin-inner, so it shrank to its text and sat centred:
   a 930px column in the middle of a 2560 screen, with nothing either side
   ("too much vacant space"). Full width, it lines up with every other section,
   and the right-hand column holds the clip (Madurai_2). */
.pin-inner > .wrap { width: 100%; }
/* Holds, and fills the screen while it holds (Madurai_2, Naveen's pick after "the
   stop and load on those blocks dont work"). The history, so nobody re-walks it:
   a full-screen pane with small centred content left bare dark; a capped pane
   moved that dark outside it; unpinning made it tight but lost the hold. Now the
   pane is the window under the header, the clip fills its right half top to
   bottom, the sentence is set to the window's height, and the hold is short:
   1.3 screens of scroll, not the 2.4 it was. */
.pin-band { height: 230vh; padding: 0; }
.pin-band .pin-inner {
  position: sticky; top: 76px; height: calc(var(--vh) - 76px); min-height: 480px;
  display: flex; align-items: center; overflow: hidden;
}
@media (max-width: 900px) {
  /* The phone header scrolls away, so the pane takes the whole window. */
  .pin-band .pin-inner { top: 0; height: var(--vh); }
}
@media (min-width: 901px) {
  .pin-band .pin-words { font-size: clamp(34px, min(5vw, 7.6vh), 88px); }
  .pin-band .pin-lede { font-size: clamp(16px, min(1.5vw, 2.4vh), 21px); }
}
@media (min-width: 901px) {
  /* Split: the words on the site's column on the left, the clip filling the right
     half of the pane edge to edge ("stretch to the full block"). Positioned
     against .pin-inner, not the wrap, so it reaches the window edge. The words
     never sit over the footage: a fade on its left edge joins it to the ink. */

  .pin-content { max-width: 50%; }
  /* Above the band's scrim (z 1), which is for the dot field, not the footage.
     50%, not 48%: the words' block ends at exactly 50% of the window (measured,
     not assumed — .pin-content is max-width 50% of .wrap, which lands on 50.0%
     of the viewport at 1440). So the clip starts precisely where the text stops.
     At 48% it started at 52%, leaving a 2% strip of ink that belonged to
     neither half (Naveen: "put this 50-50"). */
  .pin-media { position: absolute; top: 0; right: 0; bottom: 0; width: 50%; z-index: 2; }
  .pin-content { padding-block: clamp(8px, 2vw, 32px); }
  /* The footage fades itself out, not a dark overlay over it: an overlay left a
     1px seam where the video's own anti-aliased edge showed past it ("can this
     line be removed, can it fade organically"). With a mask the edge pixels are
     transparent, so there is no edge to see. Eased stops, so it dissolves rather
     than ramps. */
  .pin-video {
    width: 100%; height: 100%; object-fit: cover; object-position: 62% 50%; background: none;
    /* The left edge dissolves into the ink. The bottom gets a 1.5% softening only:
       a longer bottom fade (88%) emptied the last ~160px of the block and read as
       a dark strip under the footage.

       The 90deg ramp is SHORT on purpose. It used to run to 56% of the clip's
       own width, which put full strength at 78.9% of the window — measured, and
       it is why the band read as mostly shading with a sliver of video rather
       than as two halves ("right now it shades too much"). The fade is a join,
       not a treatment: its whole job is to stop the clip's anti-aliased left
       edge showing as a 1px seam, which an opaque overlay could not do. It
       needs to be soft, not long. Now full strength at 58% of the window, with
       the footage already readable by ~54.5% — a ~115px dissolve at 1440 rather
       than a ~390px one. */
    -webkit-mask-image:
      linear-gradient(90deg, transparent 0%, rgba(0,0,0,.08) 2%, rgba(0,0,0,.32) 5%, rgba(0,0,0,.68) 9%, rgba(0,0,0,.92) 12.5%, #000 16%),
      linear-gradient(180deg, #000 0%, #000 98.5%, transparent 100%);
    -webkit-mask-composite: source-in;
            mask-image:
      linear-gradient(90deg, transparent 0%, rgba(0,0,0,.08) 2%, rgba(0,0,0,.32) 5%, rgba(0,0,0,.68) 9%, rgba(0,0,0,.92) 12.5%, #000 16%),
      linear-gradient(180deg, #000 0%, #000 98.5%, transparent 100%);
            mask-composite: intersect;
  }
}
.pin-video { display: block; background: #26252B; }
.pin-video-toggle {
  position: absolute; right: 16px; top: 16px; z-index: 2;
  width: 40px; height: 40px; display: grid; place-items: center;
  border-radius: 50%; border: 1px solid rgba(243,238,229,.35); cursor: pointer;
  background: rgba(29,28,33,.72); color: var(--brand-sand);
  -webkit-font-smoothing: antialiased;
}
.pin-video-toggle svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pin-video-toggle .i-play { display: none; fill: currentColor; stroke: none; }
.pin-video-toggle.is-paused .i-play { display: block; }
.pin-video-toggle.is-paused .i-pause { display: none; }
.pin-video-toggle:hover { background: rgba(29,28,33,.9); border-color: var(--brand-sand); }
.pin-video-toggle:focus-visible { outline: 2px solid var(--brand-sand); outline-offset: 3px; }
/* The wrap is the positioned box by default, which pinned the clip to the text's
   height (378px of an 844px pane on a phone). The clip measures the pane instead. */
.pin-band .pin-inner > .wrap { position: static; }
/* Above the clip at every width: this lived in the desktop block, so on a phone
   the full-bleed clip painted over the words. */
.pin-content { position: relative; z-index: 3; }
@media (max-width: 900px) {
  /* The clip fills the block and the words sit on it, the arrangement deloitte.com
     uses on a phone. Their footage is nearly black where the type sits; ours is a
     lit workshop, so the grade does that work: near-solid ink behind the words,
     opening up below them where the hands are. */
  .pin-band .pin-inner { align-items: flex-start; }
  .pin-content { padding-top: clamp(24px, 7vh, 72px); }
  .pin-media { position: absolute; inset: 0; width: 100%; z-index: 0; }
  .pin-video { width: 100%; height: 100%; object-fit: cover; object-position: 50% 62%; border-radius: 0; }
  /* A lighter scrim over the phone video band (BACKLOG 66).
   *
   * Naveen, on the live site: "the video is too dark", then "too much overlay".
   * The footage is not dark — raw frame luminance runs 74-104 of 255 — and at
   * .97 this gradient was covering almost all of it.
   *
   * These alphas are 60% of what they were. That is the whole change.
   *
   * WHAT I FIRST WROTE HERE WAS WRONG, and it is worth the space because the
   * error is the useful part. I claimed two scrims were stacking — this one and
   * `.pin-inner::after` — compositing to 0.9964, and I added a suppression rule
   * with a note calling its selector "load-bearing". QA could not reproduce any
   * of it: with this gradient removed entirely, adding or removing the inner
   * scrim leaves the painted ground identical to four decimal places. I
   * re-measured and got a delta of exactly 0.00000. `.pin-inner::after` never
   * painted over this band. The suppression rule was a no-op describing a
   * mechanism that was not operating, so it has been deleted rather than left
   * to mislead the next reader.
   *
   * How I got it wrong: I read both rules' computed values, saw .97 and .88,
   * and multiplied. Computed style says what a rule WOULD paint, not that it
   * paints here. The lesson is the one this codebase keeps teaching — measure
   * the result, not the inputs.
   *
   * The value, measured by QA against the worst text run over the brightest
   * ground, which is stricter than my own sweep and supersedes it: shipped
   * 12.205:1, at 60% **7.592:1**, at 45% it already fails (8 runs of 72 below
   * the bar at 899px, worst 4.222:1), at 30% 4.550:1. The floor crosses 4.5
   * between 45% and 50%, so 60% sits about 1.2x above failing — NOT the 2.3x I
   * claimed from a median-ground measurement that also missed the `.tag` run.
   *
   * Note this block is `max-width: 900px`, so it reaches tablet portrait, where
   * the margin is thinnest. The scrim is heavy for a reason — see the note
   * below on text over footage — so it stays; it is simply no longer opaque.
   *
   * BACKLOG 126(b), Madurai_13: at short tablet-HEIGHT windows (this block is
   * width-only, and never named a height), Madurai_10 QA measured three real
   * failures at 700px tall: 768x700 4.280:1, 899x700 4.320:1, 900x700
   * 4.307:1 — all below the 4.5 bar, all from .pin-lede, the second text run.
   * Re-measured with a different method (whole-box brightest pixel, not
   * per-glyph — close to, not identical to, those figures: 4.73/4.83/4.83:1
   * in my own harness, same run, same near-threshold band). Scaled every stop
   * below by 1.2x. Pushes the worst known failure to 6.6-6.8:1 in my harness
   * (QA re-measures with its own established method); a normal-height control
   * (899x1024) moves from ~7:1 to 8.9-9.3:1, still short of the "too dark"
   * complaint this scrim exists to avoid (shipped level there was 7.592:1).
   * Screenshotted before and after: the footage stays clearly visible. */
  .pin-media::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(180deg,
      rgba(20,19,23,.698) 0%, rgba(20,19,23,.691) 40%, rgba(20,19,23,.670) 58%,
      rgba(20,19,23,.446) 78%, rgba(20,19,23,.245) 100%);
  }
  .pin-video-toggle { top: auto; bottom: 16px; right: 16px; }
  /* Lit from the start on a phone, and larger. The word-by-word reveal belongs to
     the desktop hold; over footage its dim grey measured 1.1:1 against the graded
     ground, which is unreadable. Sand from the first frame instead. */
  .pin-band .pin-words { font-size: clamp(34px, 8.6vw, 46px); max-width: 15ch; letter-spacing: -.018em; }
  .pin-words .w, .fx .pin-words .w { color: var(--word-lit); }
  .pin-band .pin-lede { color: #E8E3DA; font-size: 17px; }
}

.pin-content .tag { color: var(--label-on-ink); }

.pin-words {
  /* Sized by the window's height as well as its width, so a held band on a tall
     screen is filled by the sentence rather than by space around it. */
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-variation-settings: "wdth" 94, "opsz" 72;
  font-weight: var(--wt-display-dark);
  font-size: clamp(30px, 5.6vw, 68px);
  line-height: 1.09;
  letter-spacing: -.02em;
  max-width: 19ch;
  margin: 0;
}
.pin-lede { color: var(--on-ink); font-size: clamp(16px, 1.8vw, 19px); max-width: 50ch; }

@media (prefers-reduced-motion: reduce) {
  .pin-band { height: auto; }
  .pin-band .pin-inner { position: static; height: auto; min-height: 0; padding: var(--sec-y) 0; }
}

/* ---------- closing call to action ---------- */
.cta-band { background: var(--brand-ink); padding: var(--sec-y) 0; }
.cta-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 56px; align-items: start;
}
@media (max-width: 860px) { .cta-grid { grid-template-columns: 1fr; gap: 40px; } }

.cta-lead { display: flex; flex-direction: column; gap: 22px; align-items: flex-start; }
.cta-lead h2 {
  color: var(--brand-sand); font-weight: var(--wt-display-dark);
  font-size: clamp(30px, 4.4vw, 52px); letter-spacing: -.02em; line-height: 1.08;
  max-width: 16ch;
}
.cta-lede { color: var(--on-ink); font-size: clamp(16px, 1.8vw, 19px); max-width: 46ch; }

.cta-points { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 30px; }
.cta-points li { display: grid; grid-template-columns: 26px 1fr; gap: 16px; align-items: start; }
.cta-points svg { color: var(--brand-accent); margin-top: 3px; }
.cta-points h3 { color: var(--brand-sand); font-size: var(--fs-md); letter-spacing: -.015em; margin: 0 0 5px; }
.cta-points p { color: var(--on-ink); font-size: var(--fs-base); line-height: 1.55; margin: 0; max-width: 42ch; }

@media (max-width: 620px) { .cta-band { padding: 64px 0; } }

/* ============================================================
   NAV BUTTON PAIR
   Declared last, so the two controls that sit side by side are
   guaranteed the same height whatever else the sheet says. They
   had drifted to 50px and 42px, which reads as a mistake because
   it is one.
   ============================================================ */
.site-head .nav-cta,
.site-head .nav-toggle {
  min-height: 42px;
  padding: 10px 18px;
  font-size: var(--fs-base);
  font-weight: 700;
  border-radius: 3px;
  box-sizing: border-box;
  line-height: 1.15;
}
.site-head .nav-toggle { gap: 10px; }

/* ---------- cycling examples ----------
   One line that keeps changing what it is an example of. Shows the
   breadth of trades we build for without a wall of logos, and is honest:
   these are problem shapes, not claimed clients. */
.ticker {
  padding: var(--sec-y) 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
.ticker .wrap {
  display: flex; flex-direction: column; align-items: center;
  gap: 16px; text-align: center;
}
/* The shared 10.5px eyebrow disappears above a 54px display line. This band
   is one sentence and its label, so the label has to hold its own. */
.ticker .tag {
  font-size: clamp(13px, 1.5vw, 16px);
  letter-spacing: .16em;
  font-weight: 700;
  color: var(--ink-2);
}
.cycler {
  margin: 0;
  /* Always two lines. On one line the pair plus "for" ran past the column and
     wrapped, stranding "for" on its own at the end of the first line. */
  display: flex; flex-direction: column; align-items: center; gap: 0;
  font-size: clamp(30px, 4.4vw, 54px);
  font-weight: var(--wt-display); letter-spacing: -.02em; line-height: 1.1;
  color: var(--brand-accent-deep);
}
.cycler .line { display: flex; align-items: baseline; justify-content: center; gap: .26em; }
.cycler .lead { color: var(--ink-3); font-weight: 400; }
.cycler .slot { position: relative; display: inline-block; white-space: nowrap; }
.cycler .sizer { visibility: hidden; }
.cycler .word {
  position: absolute; left: 0; right: 0; top: 0;
  white-space: nowrap;
  transition: opacity .38s ease, transform .38s cubic-bezier(.22,.7,.28,1);
}
.cycler .word.in  { opacity: 0; transform: translateY(.3em); }
.cycler .word.out { opacity: 0; transform: translateY(-.3em); }
@media (prefers-reduced-motion: reduce) {
  .cycler .word { transition: none; }
}


/* ---------- the living network (Concept 3) ----------
   Same sticky pattern as .pin-band, which is proven to hold: a tall section
   whose inner pane sticks for the duration, so the scroll distance becomes
   the timeline the network is built over. */
.netband {
  position: relative;
  height: 360vh;
  background: var(--brand-ink);
  color: var(--brand-sand);
}
.net-inner {
  position: sticky; top: 0;
  height: 100vh; min-height: 460px;
  display: flex; align-items: center;
  overflow: hidden;
}
.net-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* By the end the network is dense enough to run lines straight through the
   copy. A veil pushes it behind the words without hiding it: heaviest on the
   left where the headline sits, lightest on the right where it can breathe. */
.net-inner::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(100deg,
    rgba(29,28,33,.90) 0%,
    rgba(29,28,33,.74) 38%,
    rgba(29,28,33,.58) 66%,
    rgba(29,28,33,.30) 100%);
}
.net-content {
  position: relative; z-index: 2;
  display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  gap: clamp(30px, 5vw, 76px); align-items: center;
}
.net-head { display: flex; flex-direction: column; gap: 18px; align-items: flex-start; }
.net-head .tag { color: var(--label-on-ink); }
.net-h {
  font-size: clamp(30px, 4.4vw, 54px); font-weight: var(--wt-display-dark);
  letter-spacing: -.02em; line-height: 1.08;
  max-width: 15ch; color: var(--brand-sand);
}
.net-lede { color: var(--on-ink); max-width: 46ch; font-size: clamp(15px, 1.6vw, 17px); }
.net-foot {
  /* Full strength, not the label tier: this is a sentence to read, not an
     eyebrow. Naveen pointed at it directly. */
  color: var(--on-ink); max-width: 40ch; font-size: var(--fs-base);
  padding-top: 14px; border-top: 1px solid rgba(243,238,229,.14);
}

.net-stages { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.net-stages li {
  display: flex; flex-direction: column; gap: 3px;
  padding: 13px 0 13px 22px;
  border-left: 2px solid rgba(243,238,229,.16);
  /* Nudged with padding rather than transform: a translate on the active row
     pushed it 5px past the container and got clipped at the right edge. */
  /* Every stage stays fully opaque. Dimming them with opacity was the first
     attempt: at .34 the inactive stages measured 1.94:1, so four fifths of the
     section was unreadable until you had scrolled far enough to light it. The
     active one is signalled by colour and its rail instead, which costs the
     other four nothing. */
  transition: color .45s ease, border-color .45s ease, padding-left .45s cubic-bezier(.22,.7,.28,1);
}
.net-stages li.on { border-left-color: rgba(243,238,229,.45); }
.net-stages li.now { border-left-color: var(--brand-accent); padding-left: 27px; }
.net-k {
  font-size: clamp(17px, 2vw, 23px); font-weight: var(--wt-display-sm);
  letter-spacing: .01em; text-transform: uppercase;
  color: var(--on-ink);                                 /* 14.65:1 on ink */
  transition: color .45s ease;
}
.net-stages li.on  .net-k { color: var(--on-ink); }     /* 14.65:1 */
/* Not --brand-accent: at 3.97:1 on ink it fails for text at this size. */
.net-stages li.now .net-k { color: var(--brand-accent-lift); }  /* 4.97:1 */
.net-d { font-size: var(--fs-base); color: var(--on-ink); max-width: 42ch; line-height: 1.45; }

/* The stage examples (one step of the /timesheet sample story each). In the
   plain list — no JavaScript, or reduced motion — every stage shows its example
   and record line. .net-motion, set on the root by motion-on.js before the page
   paints (not by deferred network.js, which flashed the plain list), turns the list into a rail and
   draws the current stage as a card instead. */
.net-x { margin: 8px 0 0; font-size: var(--fs-base); line-height: 1.5; color: #E8E3DA; max-width: 46ch; }
.net-x b, .net-card-ex b {
  display: block; margin-bottom: 4px;
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--on-ink);
}
.net-rec, .net-card-rec { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.net-rec span, .net-card-rec span {
  font: 500 13px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color: #E8E3DA;
  background: rgba(243,238,229,.07); border: 1px solid rgba(243,238,229,.12); border-radius: 3px; padding: 7px 9px;
}
.net-rec span:last-child, .net-card-rec span:last-child { border-color: rgba(238,75,162,.55); color: var(--brand-sand); }
/* Naveen, 2026-09-29: bring the chip-level rise back alongside the whole
   card's (site.css:2678) - both animate, not one or the other. Delays start
   after the card itself is mostly settled (card: 0s-0.5s; chips: 0.2s
   onward) so the chips read as a second, deliberate beat rather than racing
   the card's own motion. Same heroIn keyframe as the card and the hero. */
.net-card-rec span { animation: heroIn .42s cubic-bezier(.2,.8,.3,1) both; }
.net-card-rec span:nth-child(1) { animation-delay: .20s; }
.net-card-rec span:nth-child(2) { animation-delay: .27s; }
.net-card-rec span:nth-child(3) { animation-delay: .34s; }
.net-card-rec span:nth-child(4) { animation-delay: .41s; }
.net-sample { margin: 18px 0 0; font-size: var(--fs-sm); color: var(--on-ink); }
/* The plain list is one column: the headline, then the five steps under it.
   It does not pin: with the examples in, the list is taller than a screen, and
   a 100vh pane with overflow hidden cut the last two stages off. */
html:not(.net-motion) .netband .net-content { grid-template-columns: minmax(0, 1fr); }
html:not(.net-motion) .netband { height: auto; }
html:not(.net-motion) .netband .net-inner { position: static; height: auto; padding: 90px 0; }

@media (max-width: 900px) {
  .net-content { grid-template-columns: minmax(0, 1fr); gap: 26px; }
  .net-h { font-size: clamp(27px, 6.4vw, 38px); }
  .net-lede, .net-foot { display: none; }
  .net-d { font-size: var(--fs-sm); }
  .netband { height: 300vh; }
}

/* ---------- the stage card (Gate 4 of Madurai_2 option B) ----------
   Before: the densest part of the network ran under the stage list, because the
   veil was lightest on the right where the list sat ("This looks crap"). Now the
   network stays behind the headline side, the right side is solid, and one card
   carries the current stage with its Timesheet example. The pane is capped at
   820px and centred below the sticky header, so on a tall screen the section's
   words arrive in the first view instead of an empty black band. */
/* Holds while the network builds and the card steps through the stages, and fills
   the window while it holds (Madurai_2; see .pin-band for the history: unpinning
   made it tight but "without the scroll and hold, there is no point of this").
   A short hold, 1.3 screens of scroll, and the headline and card are sized to the
   window's height so the held pane is filled rather than centred in dark. */
.net-motion .netband { height: 230vh; padding: 0; }
.net-motion .net-inner {
  position: sticky; top: 76px; height: calc(var(--vh) - 76px); min-height: 520px;
  display: flex; align-items: center; overflow: hidden;
}
@media (max-width: 900px) {
  .net-motion .net-inner { top: 0; height: var(--vh); }
}
@media (min-width: 901px) {
  .net-motion .net-h { font-size: clamp(34px, min(4.6vw, 8vh), 72px); }
  .net-motion .net-lede { font-size: clamp(16px, min(1.5vw, 2.4vh), 21px); }
  .net-motion .net-card { min-height: min(62vh, 640px); }
  .net-motion .net-card-k { font-size: clamp(26px, min(2.8vw, 5vh), 44px); }
  .net-motion .net-card-ex p { font-size: clamp(18px, min(1.7vw, 3vh), 28px); }
}
.net-motion .net-inner::after {
  background: linear-gradient(100deg,
    rgba(29,28,33,.62) 0%, rgba(29,28,33,.72) 44%,
    rgba(29,28,33,.97) 58%, rgba(29,28,33,.98) 100%);
}
.net-motion .net-stages { flex-direction: row; flex-wrap: wrap; gap: 6px 16px; margin-top: 10px; }
.net-motion .net-stages li {
  padding: 6px 0; border-left: 0;
  border-bottom: 2px solid rgba(243,238,229,.16);
}
.net-motion .net-stages li.on { border-bottom-color: rgba(243,238,229,.45); }
.net-motion .net-stages li.now { border-bottom-color: var(--brand-accent); padding-left: 0; }
.net-motion .net-stages .net-k { font-size: var(--fs-sm); letter-spacing: .08em; }
/* Hidden visually, not from screen readers: the card is aria-hidden, so the
   list is still where the words live for anyone not looking at the card. */
.net-motion .net-stages :is(.net-d, .net-x, .net-rec),
.net-motion .net-sample {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* Naveen, 2026-09-28: the whole card should rise into place each time a
   stage changes, not just the chips - one block, no exit animation for the
   card it replaces. Reuses the hero's own entrance exactly (heroIn,
   site.css:1250) rather than a new one. paintCard() mutates this element's
   children in place (only .net-card-rec's spans are ever replaced outright),
   so nothing re-inserts the card itself to retrigger a CSS animation on its
   own - network.js clears then restores card.style.animation on every
   paintCard() call to force a fresh play. */
.net-card {
  display: flex; flex-direction: column; gap: 14px; min-height: 430px;
  background: #26252B; border: 1px solid rgba(243,238,229,.12); border-radius: 6px;
  padding: clamp(24px, 3vw, 36px);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.8);
  animation: heroIn .5s cubic-bezier(.2,.8,.3,1) both;
}
.net-card-top { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.net-card-step { font-size: var(--fs-xs); letter-spacing: .1em; text-transform: uppercase; color: var(--on-ink); }
.net-card-sample {
  font-size: var(--fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--on-ink);
  border: 1px solid rgba(243,238,229,.18); border-radius: 3px; padding: 2px 6px;
}
.net-card-k {
  margin: 0; font-size: clamp(24px, 2.6vw, 32px); text-transform: uppercase; letter-spacing: .01em;
  color: var(--brand-accent-lift);
}
.net-card-d { margin: 0; color: var(--on-ink); font-size: var(--fs-base); line-height: 1.5; }
.net-card-ex {
  display: flex; flex-direction: column; gap: 10px; flex: 1;
  border-top: 1px solid rgba(243,238,229,.14); padding-top: 16px;
}
.net-card-ex p { margin: 0; color: var(--brand-sand); font-size: clamp(17px, 1.7vw, 20px); line-height: 1.45; }
.net-card-rec { margin-top: auto; }
.net-card-bar { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
.net-card-bar i { height: 4px; border-radius: 2px; background: rgba(243,238,229,.16); }
.net-card-bar i.on { background: rgba(243,238,229,.5); }
.net-card-bar i.now { background: var(--brand-accent); }
@media (max-width: 900px) {
  .net-motion .net-stages {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  }
  .net-card { min-height: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* No pinning at all: the section becomes an ordinary block with the
     finished network behind it. */
  .netband { height: auto; }
  .net-inner { position: static; height: auto; padding: 90px 0; }
  .net-stages li, .net-stages li.now { transition: none; padding-left: 22px; }
}

/* ---------- the 404 keys (Madurai_2) ----------
   4, 0, 4 as keys built from the button styles: the same fill, edge and cast
   shadow, only bigger. They drop in one after another and shake, then the
   headline rises and shakes. Once only — a loop would be motion nobody can
   stop (WCAG 2.2.2) — and a key shakes again when pointed at or tapped. */
.nf-keys { display: flex; gap: clamp(12px, 2vw, 22px); margin: 0 0 clamp(26px, 4vw, 40px); }
.nf-key {
  display: grid; place-items: center;
  width: clamp(78px, 14vw, 138px); aspect-ratio: 1 / 1.08; border-radius: 10px;
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-weight: 800; font-size: clamp(54px, 10vw, 100px); line-height: 1; letter-spacing: -.04em;
  background: var(--brand-accent-deep); color: #fff; border: 1px solid transparent;
  box-shadow:
    inset 0 2px 0 rgba(255,255,255,.28), inset 0 -2px 0 rgba(0,0,0,.18),
    0 6px 0 var(--brand-accent-ink), 0 12px 22px -8px rgba(142,16,84,.45),
    0 26px 40px -24px rgba(142,16,84,.6);
  user-select: none;
  animation: nfDrop .55s cubic-bezier(.3,1.5,.5,1) both, nfShake .6s ease-in-out 1.05s both;
}
.nf-key.nf-ghost {
  background: var(--brand-sand-2); color: var(--brand-accent-ink-2); border-color: var(--brand-accent-deep);
  box-shadow:
    inset 0 2px 0 rgba(255,255,255,.9), 0 6px 0 rgba(142,16,84,.24),
    0 12px 22px -10px rgba(29,28,33,.22), 0 26px 40px -24px rgba(29,28,33,.45);
}
/* The middle key tips the other way, so the three do not move as one block. */
.nf-key:nth-child(2) { animation-delay: .14s, 1.05s; --tilt: -1; }
.nf-key:nth-child(3) { animation-delay: .28s, 1.05s; }
/* nfShakeAgain, not nfShake: an animation is matched by name, so naming the
   one that has already finished does not play it again. */
.nf-key:hover, .nf-key:active { animation: nfShakeAgain .5s ease-in-out; }
/* .hero .b-open > * gives every hero child the site's heroIn entrance, which
   outranks a single class: without the matching selector the headline never
   shook and the keys' row slid up underneath its own drop. */
.hero .b-open > .nf-keys { animation: none; }
.hero .b-open > .nf-h { animation: nfRise .5s ease-out 1.5s both, nfNudge .5s ease-in-out 2.05s both; }
@keyframes nfDrop {
  from { opacity: 0; transform: translateY(-70px) rotate(-8deg); }
  to   { opacity: 1; transform: none; }
}
@keyframes nfShake {
  0%, 100% { transform: none; }
  15% { transform: translateX(-7px) rotate(calc(-7deg * var(--tilt, 1))); }
  35% { transform: translateX(6px)  rotate(calc(6deg * var(--tilt, 1))); }
  55% { transform: translateX(-4px) rotate(calc(-4deg * var(--tilt, 1))); }
  75% { transform: translateX(2px)  rotate(calc(2deg * var(--tilt, 1))); }
}
@keyframes nfShakeAgain {
  0%, 100% { transform: none; }
  15% { transform: translateX(-7px) rotate(calc(-7deg * var(--tilt, 1))); }
  35% { transform: translateX(6px)  rotate(calc(6deg * var(--tilt, 1))); }
  55% { transform: translateX(-4px) rotate(calc(-4deg * var(--tilt, 1))); }
  75% { transform: translateX(2px)  rotate(calc(2deg * var(--tilt, 1))); }
}
@keyframes nfRise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes nfNudge {
  0%, 100% { transform: none; }
  20% { transform: translateX(-10px); } 40% { transform: translateX(9px); }
  60% { transform: translateX(-6px); }  80% { transform: translateX(3px); }
}
@media (prefers-reduced-motion: reduce) {
  .nf-key, .nf-key:hover, .nf-key:active, .hero .b-open > .nf-h { animation: none; }
}

/* ---------- the proof screenshots ----------
   Two real screenshots, cross-fading with a slow push in. Driven entirely by
   CSS: a JavaScript timer here would be a third thing that can bank frames
   behind a hidden tab, which is exactly how the cycler broke. */
.proof { display: flex; flex-direction: column; }
.proof-stage {
  position: relative; margin-top: auto; overflow: hidden;
  border-radius: 4px; aspect-ratio: 1120 / 752;
  /* Visible while the images are still coming down the wire. */
  background:
    linear-gradient(100deg, rgba(255,255,255,.05) 30%, rgba(255,255,255,.18) 50%, rgba(255,255,255,.05) 70%)
    rgba(0,0,0,.12);
  background-size: 220% 100%;
  animation: proofWait 1.6s linear infinite;
  box-shadow: 0 18px 40px rgba(0,0,0,.22);
}
@keyframes proofWait { to { background-position: -220% 0; } }

.proof-shot { position: absolute; inset: 0; margin: 0; opacity: 0; animation: proofFade 13s infinite; }
.proof-shot:nth-child(2) { animation-delay: 6.5s; }
.proof-shot img {
  width: 100%; height: 100%; object-fit: cover; object-position: top center;
  display: block; transform-origin: 50% 22%;
  animation: proofZoom 13s infinite;
}
.proof-shot:nth-child(2) img { animation-delay: 6.5s; }
.proof-shot figcaption {
  position: absolute; left: 10px; bottom: 9px;
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: #fff; background: rgba(29,28,33,.72);
  padding: 5px 9px; border-radius: 3px;
}
@keyframes proofFade {
  0%, 50%, 100% { opacity: 0; }
  4%, 46%       { opacity: 1; }
}
@keyframes proofZoom {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.13); }
  51%, 100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  /* One screenshot, held still. */
  .proof-stage { animation: none; }
  .proof-shot, .proof-shot img { animation: none; }
  .proof-shot:first-child { opacity: 1; }
  .proof-shot:nth-child(2) { display: none; }
}

/* ---------- Concept 1: the page answers ---------- */
.aih { background: var(--brand-ink); color: var(--brand-sand); padding: var(--sec-y) 0; }
.aih-stage { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; }
.aih .tag { color: var(--label-on-ink); }
.aih-greet {
  font-size: clamp(30px, 4.6vw, 56px); font-weight: var(--wt-display-dark); letter-spacing: -.02em;
  color: var(--brand-sand); margin: 0; line-height: 1.09;
}
.aih-ask {
  font-size: clamp(26px, 3.7vw, 44px); letter-spacing: -.02em; line-height: 1.12;
  color: #A8A2AC; max-width: 20ch; margin: 0;
}
/* Blinks while the page is typing, and then keeps blinking after the question
   lands — that waiting caret is the invitation to answer it. */
.aih-greet.typing::after, .aih-ask.typing::after, .aih-ask.waiting::after {
  content: ""; display: inline-block; width: .07em; height: .82em;
  margin-left: .06em; vertical-align: -.06em;
  background: var(--brand-accent); animation: aihBlink 1s steps(1) infinite;
}
@keyframes aihBlink { 50% { opacity: 0; } }

.aih-form { width: 100%; max-width: 640px; margin-top: 10px; }
.aih-field { display: flex; gap: 10px; align-items: stretch; }
.aih-field input {
  flex: 1; min-width: 0; background: transparent; color: var(--brand-sand);
  border: 0; border-bottom: 2px solid rgba(243,238,229,.28);
  padding: 12px 2px; font: inherit; font-size: clamp(16px, 1.8vw, 19px);
  border-radius: 0;
}
.aih-field input::placeholder { color: #6F6A78; }
.aih-field input:focus { outline: none; border-bottom-color: var(--brand-accent); }
.aih-go {
  background: var(--brand-accent-deep); color: #fff; border: 0; border-radius: 3px;
  padding: 0 22px; min-height: 46px; font: inherit; font-weight: 700; cursor: pointer;
}
.aih-go:hover { background: var(--brand-accent-hover); }

.aih-seeds { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.aih-seeds button {
  background: rgba(243,238,229,.07); color: var(--on-ink);
  border: 1px solid rgba(243,238,229,.16); border-radius: 999px;
  padding: 9px 15px; min-height: 40px; font: inherit; font-size: var(--fs-sm); cursor: pointer;
  transition: background .2s ease, color .2s ease, border-color .2s ease;
}
.aih-seeds button:hover { background: rgba(243,238,229,.14); color: var(--brand-sand); border-color: rgba(243,238,229,.34); }

.aih-out { width: 100%; margin-top: 26px; }
.aih-reply {
  font-size: clamp(22px, 2.8vw, 32px); font-weight: var(--wt-display-sm); letter-spacing: -.015em;
  color: var(--brand-accent); margin: 0 0 16px;
}
.aih-ws {
  border: 1px solid rgba(243,238,229,.18); border-radius: 5px;
  background: rgba(243,238,229,.04); overflow: hidden;
  animation: aihOpen .5s cubic-bezier(.2,.8,.3,1) both;
}
@keyframes aihOpen { from { opacity: 0; transform: translateY(14px); } }
.aih-ws-head { padding: 18px 22px; border-bottom: 1px solid rgba(243,238,229,.14); }
.aih-echo { margin: 6px 0 0; font-size: clamp(17px, 2vw, 21px); color: var(--brand-sand); }
.aih-ws-body {
  display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
  gap: 0;
}
.aih-col { padding: 20px 22px; }
.aih-col + .aih-col { border-left: 1px solid rgba(243,238,229,.14); }
.aih-chip {
  display: inline-block; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--label-on-ink);
}
.aih-read { margin: 8px 0 0; font-size: var(--fs-md); color: var(--brand-sand); line-height: 1.35; }
.aih-steps { margin: 10px 0 0; padding-left: 1.15em; display: flex; flex-direction: column; gap: 8px; }
.aih-steps li { color: var(--on-ink); font-size: var(--fs-base); line-height: 1.45; }
.aih-steps li::marker { color: var(--brand-accent); font-weight: 700; }
.aih-ws-foot {
  padding: 20px 22px; border-top: 1px solid rgba(243,238,229,.14);
  display: flex; flex-wrap: wrap; gap: 18px 28px; align-items: center; justify-content: space-between;
}
.aih-gives { margin: 8px 0 0; color: var(--brand-sand); font-size: var(--fs-base); max-width: 44ch; }
.aih-note { margin: 0; padding: 14px 22px; font-size: var(--fs-sm); color: #8F8A99; background: rgba(0,0,0,.18); }

@media (max-width: 760px) {
  .aih-ws-body { grid-template-columns: minmax(0, 1fr); }
  .aih-col + .aih-col { border-left: 0; border-top: 1px solid rgba(243,238,229,.14); }
  .aih-ask { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .aih-ws { animation: none; }
  .aih-greet.typing::after, .aih-ask.typing::after, .aih-ask.waiting::after { animation: none; }
}

/* No price on the site yet, so say so where a price would be rather than
   leaving people to guess or assume it is hidden. */
.price-note {
  margin: 4px 0 0; font-size: var(--fs-sm); color: var(--ink-3);
  max-width: 42ch;
}
.b-ink .price-note, .b-accent .price-note { color: rgba(243,238,229,.72); }

/* ---------- Concept 4: give it a problem ---------- */
/* Naveen, 2026-09-29: the whole section reads as one dark block, the same
   full-bleed-ink treatment as .cta-band/.pin-band elsewhere - not a rounded
   panel floating on the page, the section itself. gp-stage (the demo
   content) keeps its own separate var(--brand-sand-2) card regardless, so
   none of create/think/act's own styling (built for a light card) is
   affected by the section going dark around it. */
.gp { padding: var(--sec-y) 0; background: var(--brand-ink); color: var(--brand-sand); }
/* QA round 4: .gp inherited full --brand-sand for its eyebrow tag, brighter
   than every other ink-section tag on the site - .b-ink and .sweep-band
   both dim theirs to --label-on-ink. Same fix, same reason: an uppercase
   tracked label needs less contrast than body text to still read as a
   label rather than a second heading. */
.gp .tag { color: var(--label-on-ink); }
.gp-head { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; margin-bottom: 30px; }
.gp-head h2 { color: var(--brand-sand); font-size: clamp(28px, 4vw, 48px); letter-spacing: -.02em; line-height: 1.08; max-width: 19ch; }
.gp-lede { color: var(--on-ink); max-width: 56ch; font-size: clamp(16px, 1.8vw, 18px); }

.gp-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
/* The cards used to be the one dark thing on a light section, var(--brand-ink)
   on sand. On a now-ink section that colour is invisible - same fix as any
   other control that has to read on ink, the .b-ink .btn-ghost treatment:
   a sand-tinted panel and edge, not a solid fill. */
.gp-card {
  display: flex; flex-direction: column; gap: 9px; text-align: left;
  padding: 24px 22px 20px; min-height: 190px;
  background: rgba(243,238,229,.07); color: var(--brand-sand);
  border: 1px solid rgba(243,238,229,.32); border-radius: 4px; cursor: pointer; font: inherit;
  transition: background .22s ease, border-color .22s ease, transform .18s ease-out, box-shadow .22s ease;
}
.gp-card:hover {
  transform: translateY(-2px); background: rgba(243,238,229,.13); border-color: rgba(243,238,229,.5);
  box-shadow: 0 16px 34px rgba(0,0,0,.35);
}
.gp-card.on { background: var(--brand-accent-panel); border-color: var(--brand-accent-panel); }
.gp-k {
  font-size: clamp(24px, 3.2vw, 38px); font-weight: var(--wt-display-sm);
  letter-spacing: .005em; text-transform: uppercase; line-height: 1;
}
.gp-d { color: var(--on-ink); font-size: var(--fs-base); line-height: 1.45; }
.gp-go {
  margin-top: auto; font-size: var(--fs-sm); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--accent-label-on-ink);
}
/* Full strength: at 0.9 alpha this measured 4.41:1 on the card (release QA F-6); sand is 5.17:1. */
/* White, not sand: on the magenta panel sand is 5.17:1 and white is 5.98:1,
   and the buttons on this ground already use white. */
.gp-card.on .gp-go, .gp-card.on .gp-d { color: #fff; }
.gp-card.on .gp-go::after { content: " ·"; }

/* BACKLOG (studio carousel v2), Madurai_14: gp-scroll/gp-pin reuse the
   netband's own position:sticky pin pattern (.net-inner) rather than a new
   one - and reuse .net-motion itself, not a new studio-specific class.
   motion-on.js (SHARED, render-blocking) already sets .net-motion sitewide
   whenever motion is allowed, and already gates the unrelated .pin-band the
   same way, so it is a real "motion is allowed" flag, not a netband-only
   one - reusing it here needs no shared-file edit. gp-scroll's height is
   set by demos.js from real measured content, so there is enough scroll
   room to progress through all three demos and, at narrow widths, pan any
   demo taller than the pinned viewport. Without .net-motion - reduced
   motion or no JS - everything below falls back to plain static flow, and
   .gp-stage keeps ITS OWN older min-height reserve (further down), because
   that fallback path still needs to stop the page growing as a demo's
   content fills in, the original BACKLOG 36/92 problem the pin path does
   not have (its own height is fixed, not driven by flow). */
.net-motion .gp-scroll { position: relative; }
.net-motion .gp-pin {
  position: sticky; top: 76px;
  max-height: calc(var(--vh) - 76px);
  overflow: hidden;
}
@media (max-width: 900px) {
  /* No sitewide header to leave room for here (.site-head is deliberately
     not sticky below 900px - a 184px, three-line phone header eating 23%
     of the screen permanently costs more than scrolling back up does).
     Without it, top: 0 pinned the tabs flush against the bare screen edge
     - Naveen, seeing it live: "looks bit odd". A little padding, not the
     heading itself (tried and reverted - it shrank gp-stage's own room
     enough to need panning at desktop widths too, QA round 6), gives the
     pin some breathing room instead. Automatically part of the budget
     layout() computes: it measures outerStage's real rendered top against
     gp-pin's own, so this padding is counted the same way any other
     spacing already is, not a separate figure to keep in sync by hand. */
  .net-motion .gp-pin { top: 0; max-height: var(--vh); padding-top: 20px; }
}
/* Only the stage portion pans internally when a demo is taller than the
   pinned viewport - the cards (tabs) stay fixed at the top of the pin the
   whole time, never scrolled out of reach. demos.js sets height and
   scrollTop; overflow is clipped, not a visitor-facing scrollbar drag.
   min-height:0 overrides the fallback path's own 620px/1070px below,
   which would otherwise force gp-stage taller than its computed height on
   a short window and break the fixed-height assumption this depends on:
   min-height always wins over a smaller height in a genuine conflict. */
.net-motion .gp-stage { overflow-y: hidden; min-height: 0; }

html:not(.net-motion) .gp-scroll { height: auto !important; }
html:not(.net-motion) .gp-pin { position: static; max-height: none; }

.gp-stage {
  margin-top: 16px; padding: 22px;
  background: var(--brand-sand-2); border: 1px solid var(--rule); border-radius: 4px;
  /* The height the first demo will need, reserved from the start: the page used
     to grow 654px as the stage filled, which moved everything under it while
     somebody was reading (Madurai_2, BACKLOG 36). demos.js sets the figure for
     each demo as it is chosen; these two match its Create values, so the page
     does not move at load either. Only applies without .net-motion (above) -
     the pin path fixes gp-stage's own height instead, from demos.js. */
  min-height: 620px;
  /* Naveen, 2026-09-29: a real transition between demos, not an instant
     swap - the outgoing panel slides up and off, the incoming one slides up
     from below to take its place. This is the viewport both panels move
     inside; overflow:hidden is what makes the slide read as "into a slot"
     rather than spilling past the card's own edges. */
  position: relative; overflow: hidden;
}
@media (max-width: 720px) { .gp-stage { min-height: 1070px; } }

.gp-panel { animation: gpPanelIn .45s cubic-bezier(.2,.8,.3,1) both; }
/* Absolutely positioned to sit exactly where the incoming panel starts (the
   stage's own padding box), so the handoff has no seam - inset matches
   .gp-stage's padding above, not a separate figure chosen to look right. */
.gp-panel-leaving {
  position: absolute; inset: 22px;
  animation: gpPanelOut .45s cubic-bezier(.4,0,.2,1) both;
  pointer-events: none;
}
@keyframes gpPanelIn { from { opacity: 0; transform: translateY(60px); } to { opacity: 1; transform: none; } }
@keyframes gpPanelOut { to { opacity: 0; transform: translateY(-60px); } }

/* What to watch for, said before anything moves: the first version reached its
   point only at the end ("hard to follow"). */
.gp-watch { margin: 6px 0 0; font-size: var(--fs-base); color: var(--ink-2); max-width: 64ch; }

/* In on the left, out on the right, so the demo reads as one thing becoming
   another rather than as a list that happens to end in a total. */
.gp-io { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 18px; align-items: start; }
@media (max-width: 720px) { .gp-io { grid-template-columns: minmax(0, 1fr); } }
.gp-in, .gp-out {
  background: #fff; border: 1px solid var(--rule); border-radius: 3px; padding: 14px;
}
.gp-io-t { margin: 6px 0 12px; font-size: var(--fs-base); font-weight: 700; }
.gp-month { display: flex; flex-wrap: wrap; gap: 6px; }
.gp-day {
  display: grid; place-items: center; width: 34px; height: 34px; border-radius: 3px;
  font-size: var(--fs-sm); font-weight: 600; color: var(--brand-ink);
  background: var(--brand-tint); border: 1px solid transparent;
  opacity: 0; transform: translateY(4px);
}
.gp-day.seen { opacity: 1; transform: none; transition: opacity .18s ease, transform .18s ease; }
/* The day that is not billed: greyed, and outlined once the demo names it. */
.gp-day-off { background: #EDE7DC; color: var(--ink-3); }
.gp-day-off.flagged { border-color: var(--brand-accent-deep); color: var(--brand-accent-ink-2); }
.gp-trap-line { margin: 12px 0 0; font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-2); }
.gp-trap-line b { color: var(--brand-ink); }
/* The hard part, named on the line it belongs to. */
.gp-trap {
  grid-column: 1 / -1; font-style: normal; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--brand-accent-ink-2);
}
.gp-out .gp-sums { border-top: 0; }
.gp-out .gp-sum { padding-left: 0; padding-right: 0; }
.gp-s-head { margin-bottom: 8px; }
.gp-s-title { margin: 6px 0 0; font-size: clamp(18px, 2.2vw, 23px); font-weight: 700; letter-spacing: -.02em; }
.gp-chip {
  display: inline-block; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3);
}
.gp-dim { color: var(--ink-3); }
.gp-num { font-variant-numeric: tabular-nums; text-align: right; }
.gp-note { margin: 10px 0 0; font-size: var(--fs-sm); color: var(--ink-3); max-width: 62ch; }

/* Each new row arrives rather than appearing, so you can see it being built. */
.gp-rows > *, .gp-sums > *, .gp-find, .gp-done, .gp-audit {
  animation: gpIn .3s cubic-bezier(.2,.8,.3,1) both;
}
@keyframes gpIn { from { opacity: 0; transform: translateY(6px); } }

.gp-sheet { background: #fff; border: 1px solid var(--rule); border-radius: 3px; overflow: hidden; }
.gp-row, .gp-sum {
  display: grid; grid-template-columns: 1fr auto auto; gap: 16px;
  padding: 9px 16px; font-size: var(--fs-base); align-items: baseline;
}
.gp-row + .gp-row { border-top: 1px solid #F0EAE0; }
.gp-sums { border-top: 2px solid var(--brand-ink); }
.gp-sum { grid-template-columns: 1fr auto; font-weight: 600; }
.gp-sum.gp-total { font-size: var(--fs-lg); font-weight: 800; border-top: 1px solid var(--rule); }
.gp-done p { margin: 12px 0 0; font-size: var(--fs-base); }

.gp-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.gp-col { background: #fff; border: 1px solid var(--rule); border-radius: 3px; padding: 9px 14px; }
.gp-rec {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 4px 0; font-size: var(--fs-base); opacity: .35;
  transition: opacity .2s ease;
}
.gp-rec.seen { opacity: 1; }
.gp-rec + .gp-rec { border-top: 1px solid #F4EFE6; }

.gp-findings { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.gp-find { padding: 6px 14px; border-radius: 3px; border-left: 3px solid var(--ink-3); background: #fff; }
.gp-find.gp-critical { border-left-color: var(--status-critical); }
.gp-find.gp-warning  { border-left-color: var(--status-warning); }
.gp-find-t { margin: 0; font-weight: 700; font-size: var(--fs-base); }
.gp-find-e { margin: 4px 0 0; font-size: var(--fs-sm); color: var(--ink-2); line-height: 1.4; }

.gp-prop { background: #fff; border: 1px solid var(--rule); border-radius: 3px; padding: 18px 20px; }
.gp-prop-t { margin: 0; font-size: clamp(16px, 2vw, 19px); }
.gp-ev { margin: 12px 0 0; padding-left: 1.1em; }
.gp-ev li { font-size: var(--fs-sm); color: var(--ink-2); padding: 2px 0; }
.gp-approve { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.gp-result:empty { display: none; }
.gp-res-t { margin: 16px 0 0; font-weight: 700; font-size: var(--fs-md); }
.gp-res-t.gp-ok { color: var(--status-healthy); }
.gp-res-t.gp-working { color: var(--ink-3); }
.gp-audit {
  margin-top: 12px; padding: 12px 16px;
  background: var(--brand-tint); border-radius: 3px;
}
.gp-audit p { margin: 5px 0 0; font-size: var(--fs-sm); color: var(--brand-accent-ink); }

@media (max-width: 860px) {
  .gp-cards { grid-template-columns: minmax(0, 1fr); }
  .gp-card { min-height: 0; }
  .gp-cols { grid-template-columns: minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .gp-rows > *, .gp-sums > *, .gp-find, .gp-done, .gp-audit { animation: none; }
  .gp-card:hover { transform: none; }
  .gp-rec { opacity: 1; }
}

/* ---------- the ambient field behind the pinned sentence ----------
   Same particle language as the network band, mounted at a fraction of the
   volume. The sentence is the thing being read here, so the field sits well
   back: fewer points, thinner lines, and a scrim between it and the words. */

.pin-band .net-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.pin-inner::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(96deg,
    rgba(29,28,33,.88) 0%,
    rgba(29,28,33,.76) 46%,
    rgba(29,28,33,.52) 100%);
}
.pin-band .wrap { position: relative; z-index: 2; }

/* ---------- the accent rule across the very top ----------
   Sits on the sticky header, so it holds at the top of the viewport rather
   than scrolling away with the page. */
.site-head { border-top: 3px solid var(--brand-accent); }

/* ---------- Concept 7: the lab index ----------
   Four shelves in the order things move through them: a question becomes an
   experiment, an experiment becomes a build, a build becomes a product. */
/* On the shared rhythm, not its own. This had gap: clamp(34px,4.6vw,62px) and
   .lab-lead added margin-bottom: 26px on top of it — 88px where every other
   junction on the site is --blk. Naveen spotted it on /products. */
.lab .wrap { display: flex; flex-direction: column; gap: var(--blk); }

/* ---------- the lab track ----------
   Four headed lists say "here are four categories". A spine with the shelves
   hung off it says the thing that is actually true: these are positions on one
   route, and everything below where you are reading is further from being real.
   The rail carries the certainty as well as the order — solid where things
   ship, dashed where the question is open, dotted where there is only a
   question. That is structure encoding something true about the content rather
   than decorating it. */
.lab-lead { max-width: 64ch; margin: 0; }
.lab-lead h2 { margin: 0 0 8px; }
.lab-lead p { color: var(--ink-2); margin: 0; }

.lab-track { list-style: none; margin: 0; padding: 0; }
.lab-stage {
  display: grid; grid-template-columns: 26px 1fr; gap: 0 22px;
  position: relative; padding-bottom: 38px;
}
/* The rail runs from under this stage's ring to the next one. The last stage
   has nothing below it, so it has no rail — a line trailing off the bottom of
   a list implies a fifth stage that does not exist. */
.lab-stage::before {
  content: ''; position: absolute; left: 12px; top: 32px; bottom: 0;
  /* Ink at 24%, not --rule. --rule is #E6DFD2 on a #F3EEE5 ground — about a
     point and a half of contrast, which is right for a hairline between blocks
     and far too faint for a line that is carrying the meaning. The spine IS
     the idea here; a spine you cannot see is four stacked lists again. */
  border-left: 2px solid rgba(29, 28, 33, .24);
}
.lab-track > .lab-stage:last-child::before { display: none; }
.s-exp::before { border-left-style: dashed; }
.s-res::before { border-left-style: dotted; }

.lab-ring {
  width: 26px; height: 26px; border-radius: 50%;
  border: 2px solid rgba(29, 28, 33, .28); background: var(--brand-sand);
  display: grid; place-items: center; margin-top: 3px;
}
.lab-ring::after { content: ''; width: 9px; height: 9px; border-radius: 50%; background: var(--ink-3); }
.s-live  .lab-ring { border-color: var(--brand-ink); }
.s-live  .lab-ring::after { background: var(--status-healthy); }
.s-build .lab-ring::after { background: var(--status-warning); }
.s-exp   .lab-ring { border-style: dashed; }
.s-exp   .lab-ring::after { background: var(--brand-accent); }
.s-res   .lab-ring { border-style: dotted; }
.s-res   .lab-ring::after { width: 5px; height: 5px; }

/* The entry rule. Small, and it is the most load-bearing sentence on the
   shelf: a roadmap without one is a wish list, and "Released" quietly comes to
   mean "deployed for ourselves" the moment nobody has written down that it
   does not. */
.lab-gate {
  margin: 14px 0 0; font-size: var(--fs-sm); line-height: 1.5;
  color: var(--ink-3); max-width: 64ch;
}
.lab-gate b { color: var(--ink-2); font-weight: 700; }

@media (max-width: 560px) {
  .lab-stage { grid-template-columns: 20px 1fr; gap: 0 14px; padding-bottom: 30px; }
  .lab-stage::before { left: 9px; top: 26px; }
  .lab-ring { width: 20px; height: 20px; }
  .lab-ring::after { width: 7px; height: 7px; }
}

.shelf { border-top: 2px solid var(--brand-ink); padding-top: 20px; }
.shelf-h {
  display: grid; grid-template-columns: auto auto 1fr;
  gap: 0 14px; align-items: baseline; margin-bottom: 6px;
}
.shelf-k {
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(19px, 2.4vw, 27px); font-weight: var(--wt-display-sm);
  letter-spacing: .02em; text-transform: uppercase; line-height: 1;
}
.shelf-n {
  font-size: var(--fs-sm); font-weight: 700; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.shelf-d { grid-column: 1 / -1; margin: 8px 0 0; color: var(--ink-2); max-width: 66ch; font-size: var(--fs-base); }

.shelf-list { list-style: none; margin: 14px 0 0; padding: 0; }
.item {
  display: grid; grid-template-columns: 16px minmax(0, 1fr) auto;
  gap: 4px 16px; align-items: start;
  padding: 20px 0; border-top: 1px solid var(--rule);
}
.item-t { display: flex; align-items: baseline; gap: 11px; flex-wrap: wrap; }
.item h2 { font-size: clamp(17px, 2vw, 21px); letter-spacing: -.02em; margin: 0; }
.item-b p { margin: 6px 0 0; color: var(--ink-2); font-size: var(--fs-base); max-width: 64ch; }
.item-b .price-note { margin-top: 9px; }
.item-a { display: flex; align-items: center; gap: 10px; }
/* Rows with no action still align with the ones that have one. */
.item > .item-b:last-child { grid-column: 2 / -1; }

/* The dot carries the colour so the label can stay high-contrast text. */
.dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 7px; display: block; }
.dot-live  { background: var(--status-healthy); }
.dot-build { background: var(--status-warning); }
.dot-exp   { background: var(--brand-accent); }
.dot-res   { background: var(--ink-3); }

.state {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  padding: 5px 10px; border-radius: 3px; white-space: nowrap;
  /* Solid fills, not alpha over the page: a translucent pill's real contrast
     depends on whatever it happens to be sitting on. */
  background: #E7E1D6; color: #4C4954;
}
.state-live  { background: #DCEFE8; color: #045C40; }
.state-build { background: #F7E7CF; color: #6F3E05; }
.state-exp   { background: var(--brand-tint); color: var(--brand-accent-ink); }
.state-held  { background: #E7E1D6; color: #4C4954; }
.state-res   { background: transparent; color: var(--ink-3); border: 1px solid var(--rule); }

@media (max-width: 720px) {
  .item { grid-template-columns: 16px minmax(0, 1fr); }
  .item-a { grid-column: 2; flex-direction: row; align-items: center; justify-content: flex-start; margin-top: 12px; }
}

/* ---------- older engines ----------
   The scroll-lit sentences interpolate their colour with color-mix(), which
   Safari only got in 16.2 and Firefox in 113. Such an engine does not drop
   those declarations: each contains var(), so it keeps them, they fail when
   the colour is computed, and the word takes its sentence's own colour. Every
   word measured legible that way (118), but only because each sentence happens
   to be coloured for its ground. This makes it deliberate: where color-mix is
   missing, skip the effect and show every word lit, in its ground's colour.

   To simulate such an engine, rename the function to one that does not exist.
   Never delete the declarations: that measures an engine nobody ships, and it
   reported 20 words ink on ink that no real engine shows. */
@supports not (color: color-mix(in srgb, red 50%, blue)) {
  .fx .reveal-words .w { color: var(--word-lit, var(--brand-ink)); }
}

/* The row that needed a decision, marked so the eye lands on it. */
.gp-row-odd { background: var(--brand-tint); font-weight: 600; }
.gp-seq { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
.gp-seq-prev, .gp-seq-next {
  font-weight: 800; font-variant-numeric: tabular-nums;
  padding: 6px 11px; border-radius: 3px; font-size: var(--fs-base);
}
.gp-seq-prev { background: rgba(29,28,33,.07); color: var(--ink-2); }
.gp-seq-next { background: var(--brand-accent-panel); color: #fff; }
.gp-seq-arrow { color: var(--ink-3); }

/* The water behind the problem statement. Sits under the text and above the
   panel's own ground; the text keeps its full contrast because the rings never
   exceed 20% alpha and the left half is where the drops do not land. */
[data-ripple] { position: relative; isolation: isolate; overflow: hidden; }
[data-ripple] > * { position: relative; z-index: 1; }
.ripple-cv {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; pointer-events: none;
}


/* ---------- Products dropdown ----------
   No JavaScript, because the rest of the nav has none either. The trigger
   lives INSIDE .nav-item, which is what makes the keyboard path work: focusing
   it opens :focus-within, and only then can the panel's own links be tabbed
   to. A panel hidden until something inside it is focused can never be
   reached at all. */
.nav-item { position: relative; display: flex; align-items: center; }
.nav-item > a { display: inline-flex; align-items: center; gap: 6px; }
.chev { width: 9px; height: 6px; flex: none; transition: transform .18s ease; }
/* When the page you are on is INSIDE a menu, the trigger has to say so.
   Without this, standing on /timesheet lights a box that is folded away
   behind a closed dropdown, and the bar reads as though you are nowhere. The
   parent takes a quieter mark than the page's own, so the two are never
   confused for each other. */
.nav-item:has(.nav-sub a[aria-current="page"]) > a {
  color: var(--brand-ink); font-weight: 700;
  box-shadow: inset 0 -2px 0 var(--brand-accent);
}
.nav-item:hover .chev, .nav-item:focus-within .chev { transform: rotate(180deg); }

@media (min-width: 901px) {
  .nav-sub {
    position: absolute; top: 100%; left: -12px; z-index: 30;
    display: flex; flex-direction: column; gap: 2px;
    min-width: 186px; padding: 8px; margin-top: 12px;
    background: var(--brand-sand-2);
    border: 1px solid var(--hair); border-radius: 4px;
    box-shadow: 0 14px 34px rgba(29,28,33,.15);
    opacity: 0; visibility: hidden; transform: translateY(-6px);
    transition: opacity .16s ease, transform .16s ease, visibility .16s;
  }
  /* Bridges the gap between trigger and panel, so the pointer can travel
     between them without the menu closing underneath it. */
  .nav-item::after {
    content: ""; position: absolute; top: 100%; left: 0; right: 0; height: 14px;
  }
  .nav-item:hover .nav-sub, .nav-item:focus-within .nav-sub {
    opacity: 1; visibility: visible; transform: none;
  }
  /* .nav a is (0,1,1); these are (0,2,1) so they actually win. Panel links
     styled at equal specificity is a trap this codebase has hit before. */
  .nav .nav-sub a {
    padding: 9px 10px; font-size: var(--fs-base); white-space: nowrap;
    border-bottom: 0; border-radius: 3px;
  }
  .nav .nav-sub a:hover {
    background: var(--brand-tint); color: var(--brand-ink); border-bottom-color: transparent;
  }
  .nav .nav-sub a[aria-current="page"] {
    background: var(--brand-tint); color: var(--brand-accent-ink); border-bottom-color: transparent;
  }
}

@media (max-width: 900px) {
  /* The wrappers used to dissolve with display:contents, on the reasoning that
     the phone nav was a horizontal scroll row that would clip a positioned
     panel. It is not a scroll row any more — it is a stacked panel — and the
     consequence of the stale reasoning was that Cybersecurity and Websites
     stood at the same level as Products, so the menu claimed four top-level
     destinations where the site has two. Children are children here too. */
  .nav-item { display: flex; flex-direction: column; align-items: stretch; position: static; }
  .nav-item > a { justify-content: space-between; }
  .nav-item > a .chev { transform: rotate(180deg); opacity: .55; }
  .nav-sub {
    display: flex; flex-direction: column;
    margin: 0 0 0 10px; padding-left: 14px;
    border-left: 2px solid var(--brand-tint-2);
  }
  /* (0,2,1), so these beat .site-head .nav a's own border and weight. */
  .site-head .nav .nav-sub a {
    font-size: var(--fs-base); font-weight: 500; color: var(--ink-2);
    padding: 11px 2px; border-bottom: 1px solid var(--hair);
  }
  .site-head .nav .nav-sub a:last-child { border-bottom: 0; }
  .site-head .nav .nav-sub a[aria-current="page"] {
    background: var(--brand-tint); color: var(--brand-accent-ink); font-weight: 700;
  }
  .chev { display: none; }
}

/* ---------- Concept 5: the layer stack ----------
   Strata, not a list. Each band is taller and more present than the one below
   it, so the section argues its case by its own shape before a word is read. */
.strata { padding: var(--sec-y) 0; border-top: 1px solid var(--rule); }
.strata-head { display: flex; flex-direction: column; gap: 13px; align-items: flex-start; margin-bottom: 30px; }
.strata-head h2 {
  font-size: clamp(28px, 4.2vw, 50px); letter-spacing: -.02em; line-height: 1.08; max-width: 16ch;
}
.strata-lede { color: var(--ink-2); max-width: 62ch; font-size: clamp(15px, 1.7vw, 17.5px); }

.layers {
  list-style: none; margin: 26px 0 0; padding: 0;
  display: flex; flex-direction: column;
  perspective: 1400px;
}
/* Slabs, not rows. Each one overlaps the edge of the one before it and casts a
   shadow onto it, so the section reads as strata laid down in order rather than
   as a three-column table — which is what it was, and why it argued nothing. */
.layer {
  position: relative;
  display: grid; grid-template-columns: 82px minmax(0, 232px) minmax(0, 1fr);
  gap: 4px 24px; align-items: baseline;
  margin-top: -6px;
  padding: 22px 28px 24px;
  border-radius: 6px;
  background: var(--brand-sand-2);
  border: 1px solid var(--hair);
  box-shadow: 0 -10px 24px rgba(29,28,33,.05);
  transition: transform .5s cubic-bezier(.22,.7,.28,1);
}
/* Each layer sits a little prouder than the one above it, and later ones carry
   more weight — the stack gets heavier as it approaches now.

   :not(.layer-now) on every one of these is load-bearing, not tidiness.
   Release QA (F-1, High) found the "Now" slab rendering on a near-white ground
   with var(--on-ink) text at 1.72:1 — text chosen for a dark ground, on a light one.
   No CSS had changed: a copy edit removed the 1950s row, the list went from six
   items to five, and .layer-now became :nth-child(5). `.layer:nth-child(5)` is
   (0,2,0) and `.layer-now` is (0,1,0), so the positional rule won the
   background, and `.layer:nth-child(5) .layer-k` (0,3,0) won the heading colour
   from `.layer-now .layer-k` (0,2,0) — which is why that heading took its
   font-size from one rule and its colour from another.

   Raising .layer-now's specificity would fix today's symptom and leave the trap
   armed for the next edit. Excluding the slab from the positional rules instead
   makes the stack's colours independent of how many rows the list happens to
   have. QA enumerated all 30 positional selectors in this file: these are the
   only ones that set colour rather than an animation delay. */
.layer:nth-child(1):not(.layer-now) { z-index: 1; width: 88%;  background: #F7F3EC; }
.layer:nth-child(2):not(.layer-now) { z-index: 2; width: 90%;  background: #F8F4ED; }
.layer:nth-child(3):not(.layer-now) { z-index: 3; width: 92%;  background: #F9F5EF; }
.layer:nth-child(4):not(.layer-now) { z-index: 4; width: 95%;  background: var(--brand-sand-2); }
.layer:nth-child(5):not(.layer-now) { z-index: 5; width: 97%;  background: #FFFDF9; }

.layer-y {
  font-size: var(--fs-sm); font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  font-variant-numeric: tabular-nums; color: var(--ink-3);
}
.layer-k {
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-weight: var(--wt-display-sm); letter-spacing: -.015em; color: var(--ink-2);
  font-size: clamp(18px, 2.3vw, 25px); line-height: 1.1;
}
.layer:nth-child(4):not(.layer-now) .layer-k,
.layer:nth-child(5):not(.layer-now) .layer-k { color: var(--brand-ink); }
.layer-d { color: var(--ink-2); font-size: var(--fs-base); line-height: 1.5; }

/* The one we are on. Full width, so it is visibly the thing the rest supports. */
.layer-now {
  z-index: 6; width: 100%;
  margin-top: -6px; padding: 34px 30px 36px;
  background: var(--brand-ink); border-color: var(--brand-ink);
  color: var(--brand-sand);
  box-shadow: 0 -14px 40px rgba(29,28,33,.26), 0 22px 46px rgba(29,28,33,.20);
}
.layer-now .layer-y { color: var(--accent-label-on-ink); }
.layer-now .layer-k { color: var(--brand-sand); font-size: clamp(23px, 3vw, 34px); }
.layer-now .layer-d { color: var(--on-ink); font-size: var(--fs-md); }

.strata-foot {
  margin: 26px 0 0; color: var(--ink-3); font-size: var(--fs-base);
  padding-top: 16px; border-top: 1px solid var(--rule); max-width: 52ch;
}

/* Each slab slides up onto the stack as it arrives. Legibility never depends
   on it — only position does. */
.fx .layer { transform: translateY(calc((1 - var(--in, 1)) * 70px)); }

@media (max-width: 860px) {
  .layer, .layer-now { width: 100% !important; padding: 18px 18px 20px; }
  .layer { grid-template-columns: minmax(0, 1fr); gap: 3px; }
  .layer-y { order: -1; }
}

/* ---------- the ticker's own ground ----------
   This is the one sand band sitting between two ink ones, so it needs a
   texture of its own to read as a separate block. Deliberately NOT the network
   a third time: used once it is a signature, used three times it is wallpaper.

   Pure CSS on two pseudo-elements. A canvas here would be a third paint loop
   competing for frames, and the cycler already taught this file what happens
   when animation state and a throttled tab disagree. */
.ticker { position: relative; overflow: hidden; isolation: isolate; }
.ticker::before, .ticker::after {
  content: ""; position: absolute; inset: -25%; z-index: 0; pointer-events: none;
}
/* Faint on purpose, and the alphas are measured rather than chosen by eye.
   At the first values the heaviest blend put the 16px eyebrow at 4.43:1 —
   under. At these it is 4.59:1. The big cycler type needs only 3.0 since it is
   display size, and clears it at 3.31. */
/* The two drifting fields are warm neutral now, not accent. They were magenta
   at 7.8% / 6% / 5.5% plus a tint at 36% — faint individually, and part of the
   same full-screen rosiness as the hero blooms once you saw the page whole
   (Naveen, 2026-09-18, "do it across all pages"). The drift is what the ticker
   wanted; the hue was never carrying it, and dropping the accent only raises
   the contrast the comment above was protecting. */
.ticker::before {
  background:
    radial-gradient(38% 46% at 22% 36%, rgba(214,204,186,.42), transparent 70%),
    radial-gradient(34% 42% at 78% 28%, rgba(224,215,199,.36), transparent 72%);
  animation: tickA 28s ease-in-out infinite alternate;
}
.ticker::after {
  background:
    radial-gradient(44% 52% at 62% 80%, rgba(232,225,211,.46), transparent 70%),
    radial-gradient(30% 38% at 10% 78%, rgba(214,204,186,.34), transparent 70%);
  animation: tickB 37s ease-in-out infinite alternate;
}
/* Different durations and opposite directions, so the two fields drift apart
   and back rather than moving as one sheet. */
@keyframes tickA {
  from { transform: translate3d(-2.5%, -1.5%, 0) scale(1);    }
  to   { transform: translate3d( 2.5%,  1.5%, 0) scale(1.10); }
}
@keyframes tickB {
  from { transform: translate3d( 3%,  1%, 0) scale(1.08); }
  to   { transform: translate3d(-3%, -1%, 0) scale(1);    }
}
.ticker .wrap { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  /* The field stays — it is what makes the band a block — it simply holds
     still. */
  .ticker::before, .ticker::after { animation: none; }
}

/* An empty shelf is a real state and reads better than a false badge. */
.shelf-empty {
  margin: 16px 0 0; padding: 18px 20px;
  border: 1px dashed var(--hair); border-radius: 4px;
  color: var(--ink-2); font-size: var(--fs-base); max-width: 70ch;
}

/* .cycler keeps its stacking context: the ticker's fields sit behind it. */
.cycler { position: relative; z-index: 1; }

/* ---------- the objects behind the cycling line ----------
   Outlines only, faint, out at the edges. They change with the profession the
   line names, so the background is reading the sentence. Each icon is its own
   square svg: one stretched layer turned the clock face into an ellipse. */
.tick-icons {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
  /* Without a perspective on the parent, a rotate3d on the children is
     orthographic: the object turns but never comes towards you, which is the
     difference between a 3D transform and a 3D effect. */
  perspective: 900px;
}
.ti-set { position: absolute; inset: 0; opacity: .30; transition: opacity .55s ease; }
/* Fainter where the objects are closest to the words. On a wide band they sit
   in the margins and can afford to be seen; on a phone they are only ever
   above and below the sentence, a few pixels from it. */
@media (max-width: 720px) {
  .ti-set { opacity: .18; }
}
.ti-set.in, .ti-set.out { opacity: 0; }
.ti-i {
  position: absolute;
  left: var(--x); top: var(--y);
  width: var(--s); height: var(--s);
  translate: -50% -50%;
  rotate: var(--r, 0deg);
  /* Each travels its own distance, direction and duration, so the set never
     moves as one sheet. */
  animation: tiFloat var(--dur, 19s) ease-in-out infinite alternate;
  animation-delay: var(--d, 0ms);
}
.ti-r {
  display: block; width: 100%; height: 100%;
  transform-style: preserve-3d;
  animation: tiTumble var(--rdur, 40s) linear infinite;
  /* Thickness. The hard offset in the deep accent is the extruded edge of the
     stroke; the soft one underneath is what it casts onto the band. Two
     drop-shadows, because a single blurred one reads as a glow. */
  filter: drop-shadow(1px 2px 0 rgba(142,16,84,.22))
          drop-shadow(0 7px 9px rgba(29,28,33,.13));
}
.ti-i path {
  fill: none; stroke: var(--brand-accent-ink);
  stroke-width: 3.1; stroke-linecap: round; stroke-linejoin: round;
}
@keyframes tiTumble {
  from { transform: rotate3d(var(--ax, .3), var(--ay, .45), 1, 0deg); }
  to   { transform: rotate3d(var(--ax, .3), var(--ay, .45), 1, var(--spin, 360deg)); }
}
@keyframes tiFloat {
  from { translate: -50% -50%; }
  to   { translate: calc(-50% + var(--dx, 0px)) calc(-50% + var(--dy, 0px)); }
}
@media (prefers-reduced-motion: reduce) {
  .ti-i, .ti-r { animation: none; }
  .ti-set { transition: none; }
}


/* Naveen, 2026-09-29: a moving glow, not the whole heading pulsing at once -
   corrected from a first attempt (a uniform text-shadow pulse) after Naveen
   saw it and said "the glow should move" / "it is not glow all". A bright
   band travels across the letters on a loop, same shimmer technique as a
   loading skeleton, built from the type itself (background-clip: text) so
   it needs no image or extra markup. Its own class, not a change to .h-sec -
   every other page using that shared heading size (responsible-ai,
   products, how-we-build, cybersecurity) is untouched. */
.h-glow {
  background-image: linear-gradient(100deg,
    var(--brand-ink) 0%, var(--brand-ink) 42%,
    var(--brand-accent) 50%,
    var(--brand-ink) 58%, var(--brand-ink) 100%);
  background-size: 260% 100%;
  background-position: 100% 0;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: hGlow 3.2s linear infinite;
}
@keyframes hGlow { to { background-position: -160% 0; } }
@media (prefers-reduced-motion: reduce) {
  .h-glow { animation: none; background-position: 0 0; }
}

/* Section headings, named rather than pasted inline into four files. */
.h-sec  { font-size: var(--fs-h2); }
.h-sub  { font-size: var(--fs-h2-sm); }
.h-feat {
  font-size: var(--fs-feature);
  letter-spacing: -.02em; line-height: 1.08;
}
/* The one lede that needed a colour and a measure set on it. */
.lede-quiet { color: var(--ink-2); max-width: 56ch; }

/* ---------- measure and flow utilities ----------
   Thirteen different max-widths were being written inline (17ch, 26, 28, 30,
   44, 45, 46, 47, 48, 52, 62, 64, 66ch), which is thirteen decisions where
   there should be four. Reading measure is a typographic constant, not a
   per-element choice. */
.m-tight  { max-width: 30ch; }   /* a statement, a few words per line */
.m-short  { max-width: 46ch; }   /* a lede or a standfirst            */
.m-read   { max-width: 56ch; }   /* comfortable prose                 */
.m-wide   { max-width: 66ch; }   /* the longest that still reads well */

/* A section that follows one already carrying its own top space. It was
   written inline on 17 sections. */
.sec-tight { padding-top: 0; }

/* ---------- sections as surfaces ----------
   Naveen, 2026-09-17: "we do not know where the section is ending and where
   the next block is starting… it should not look like a word document."

   He was describing something real and specific. Two consecutive .sec-tight
   sections had `padding-top: 0` AND the same ground, so between them there was
   no space, no rule and no colour change — literally no edge. The page was
   heading, paragraph, heading, paragraph down one column, which is what a text
   document is.

   Two fixes, and both are needed. Space alone still reads as one long column;
   a ground change alone with no air reads as a stripe. A section gets its own
   air when it follows another, and alternate ones get their own ground, so the
   boundary is visible before a word is read. */
.sec-tight + .sec-tight,
.sec-tight + .sec-alt,
.sec-alt + .sec-tight { padding-top: var(--sec-y); }

/* A section whose CONTENT IS SHORT — prose, or a strip a few hundred pixels
   tall. (The name says "doc" because prose was the first case; it means short.)
   --sec-y is the rhythm for full-width bands and for sections carrying grids
   and cards: on /how-we-build a section is 500-900px tall and 104px of air
   around it is right. Applying the same value to a section that is two
   paragraphs gave /security's "Forms" 167px of content inside 207px of air —
   more emptiness than text (Naveen, 2026-09-18). Equal absolute padding is not
   equal spacing when the content it surrounds differs by a factor of five.

   The ground change is what separates these sections; the padding only has to
   avoid crowding, so it sits near the block rhythm instead of the band one. */
/* padding-BOTTOM only, and the top comes from the sibling rule below — the same
   shape `--sec-y` already uses a few lines up, and for the same reason.

   Written as `padding-top` here too, it was (0,1,0) and sat after
   `.sec-tight { padding-top: 0 }`, so it won by source order and gave the FIRST
   prose section 34-60px of top padding that no other page has. These four pages
   ended up sitting lower under their hero than the other thirteen — the
   opposite of what this change was for (release QA P-1). It also made the
   sibling rules below dead, since they set what this line already set.
   The `.sec-tight { padding-top: 0 }` rule now applies to the first section
   again, exactly as it does everywhere else. */
.sec-doc { padding-bottom: var(--sec-y-doc); }
.sec-doc + .sec-tight.sec-doc { padding-top: var(--sec-y-doc); }

/* --ground is what the section is actually painted in. Anything that has to
   punch a hole in its own ground — the ladder's dots sit on its rule — reads
   this instead of naming a colour, or it stays sand-coloured on an alternate
   section and shows as a pale blob. */
.sec-alt {
  background: var(--brand-sand-3); --ground: var(--brand-sand-3);
  position: relative; isolation: isolate;
}

/* The ground itself, rather than a flat fill ("can we have some sort of
   professional background for alternative sections"). Deliberately the SAME
   language as the hero: a low bloom over a sand gradient, plus the identical
   grain tile — because a second, unrelated texture invented for these sections
   is how a site starts looking like several sites. Everything here is a third
   of the hero's strength: the hero opens the page and may be felt, an
   alternate section only has to be distinguishable from the one above it.

   Hairline edges as well, so the boundary is still readable when the ground is
   not — printed, in forced-colours mode, or for anyone who cannot see the
   ~4% difference in tone. */
.sec-alt { border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.sec-alt::before, .sec-alt::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
}
.sec-alt::before {
  /* The same blooms at a third strength, removed for the same reason — a tint
     that is faint on one section is cumulative across a page that alternates.
     The alternate ground is carried by tone and the grain below, which is what
     distinguishes the section; the colour was never doing that work. */
  background: linear-gradient(168deg, #F0E9DC 0%, var(--brand-sand-3) 58%, #E7DFD0 100%);
}
.sec-alt::after {
  opacity: .32;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
}

/* ...except after a band that paints its own ground. A band's bottom padding
   sits INSIDE the band, so it is dark space rather than air the next section
   can share. Zeroing the top padding there puts the following heading flush
   against a hard colour edge — which is exactly how "Or pick your trade" ended
   up welded to the bottom of the dark panel on /build. */
.aih + .sec-tight,
.pin-band + .sec-tight,
.netband + .sec-tight,
.cta-band + .sec-tight,
.sweep-band + .sec-tight { padding-top: var(--sec-y); }

/* ...and the HERO is one of those grounds, which this list did not say.
   The hero paints itself with `.hero::before` — three blooms over a sand
   gradient — so its bottom edge is a hard colour change exactly like a band's.
   With `.sec-tight { padding-top: 0 }` the next section's heading landed 8px
   below that edge, welded to it, which is what Naveen kept reporting as "the
   start of the new section immediately after the first section". Fixing the
   gap BELOW the heading did not touch it, because the problem was above.
   The reasoning was already written three comments up; the hero was simply
   never added to the selector list it belongs in. */
.hero + .sec-tight,
.hero + .sec-alt,
.hero + .lab       { padding-top: var(--sec-y); }
.hero + .sec-doc   { padding-top: var(--sec-y-doc); }
/* .lab carried `style="padding-top:0"` inline on /products, which outranks every
   rule here — that page alone measured 0px to the hero. Spacing lives in the
   stylesheet, so the inline attribute is gone and this covers it. */

/* ---------- site additions to the assistant panel ----------
   Kept apart from the assistant's own block. On the release branch that block
   moves into the vendored component, and a rule left inside it would be
   dropped when the branches merge (F11-4). The subtitle says what the panel is,
   in the header, which never scrolls away (F10-5). */
.assist-titles { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.assist-sub { margin: 0; font-size: var(--fs-xs); line-height: 1.35; color: var(--ink-2); }
/* "Ask us" in magenta, not ink (G4-2). The component draws it in --sa-ink, and
   over the homepage's dark band or the black footer an ink pill on ink is 1:1:
   the icon and label float with no button around them, and the back-to-top
   circle beside it looked more important. Magenta holds on both grounds (3.54:1
   on ink, 4.14:1 on sand) and matches "Talk to us", the other way to reach a
   person. assist.css loads AFTER this file, so every rule carries .assist in
   front; at the component's own specificity they would silently lose.
   The 2px sand ring is for the site's own magenta cards and buttons: on a phone
   the pill floated over them at 1.00–1.25:1 and lost its shape again (F19-2).
   The ring is 4.14:1 against magenta and 14.65:1 against ink; on sand it simply
   merges, where the pill itself already stands out. */
.assist .assist-fab {
  background: var(--brand-accent-deep); color: #fff;   /* 4.79:1 */
  box-shadow: 0 0 0 2px var(--brand-sand), 0 6px 22px -8px rgba(29,28,33,.55);
}
.assist .assist-fab:hover { background: var(--brand-accent-hover); }   /* 5.98:1 */
.assist .assist-fab svg { color: #fff; }
/* Keyboard focus: an ink ring between two sand ones, so one of them always
   contrasts — ink on sand, sand on ink, both on magenta. The site's own magenta
   focus ring measured 1.40:1 over a magenta card. The outline paints over the
   box-shadow, so the order out from the pill is sand, ink, sand. */
.assist .assist-fab:focus-visible {
  outline: 2px solid var(--brand-ink); outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--brand-sand), 0 6px 22px -8px rgba(29,28,33,.55);
}

/* ---------- jumping to a form field ----------
   "Ask for early access" links to /contact#message. At desktop widths the sticky
   header (76px) covered the field's first line and its label (F13-7). */
.form .field :is(input, textarea) { scroll-margin-top: 112px; }
/* "Jump to the calendar" landed #book flush with the top of the window, under
   the 76px sticky header, cutting off the top of its heading (F14-5). Only
   where the header is sticky; below 901px it scrolls away with the page. */
@media (min-width: 901px) { #book { scroll-margin-top: 96px; } }
/* /build?for= scrolls the chosen trade's panel into view: clear of the sticky
   header on desktop; the phone header scrolls away, so only a small gap there. */
#panel { scroll-margin-top: 16px; }
@media (min-width: 901px) { #panel { scroll-margin-top: 96px; } }
/* site-assistant 5b829dd (A15): every h2/h3 in <main> can now carry an id of
   its own (addAnchors(), scripts/build.js) so a citation can open the exact
   heading it quotes - a capability this site never had before this vendor
   bump, so no heading needed this clearance until now. Same values as #panel
   just above, for the same reason: the browser's native fragment scroll (not
   a JS-driven one, so CSS is what has to account for the sticky header)
   otherwise lands the heading flush under it. QA on the vendor bump found
   this exact gap (headings scrolled ~44px too far, hidden behind the
   header). */
main h2, main h3 { scroll-margin-top: 16px; }
@media (min-width: 901px) { main h2, main h3 { scroll-margin-top: 96px; } }
/* The claim two comments up used to end here: "the FAQ <details> case is
   unaffected, since revealTarget()'s own scrollIntoView() already clears the
   header correctly." That was never actually true and nobody had measured
   it - revealTarget() (vendor/site-assistant/browser/assist.js) calls plain
   target.scrollIntoView({ block: 'start' }), which respects scroll-margin
   when it is set and otherwise lands the element flush at the viewport top,
   same as any other scrollIntoView call. .qa-item had no scroll-margin-top,
   so every FAQ citation landed with the <details>'s own question hidden
   behind the 76px sticky header - only the answer text below it was ever
   visible. Found live on the Madurai_14g preview, Naveen: "when I click on
   Read in FAQ, it does not land correctly." Same values as #panel/#book/the
   headings above, same reason. */
.qa-item { scroll-margin-top: 16px; }
@media (min-width: 901px) { .qa-item { scroll-margin-top: 96px; } }

/* ---------- /cybersecurity: a heading with one very long word ----------
   The shared h1 size fits every page but this one. "Cybersecurity" cannot wrap,
   and where the hero column is six of twelve (621–900px) it ran up to 11.2px past
   the column (F-3, BACKLOG 30). Scoped to this heading and this band, so no other
   page's h1 changes. 6.7vw, not the 6.9 that only just fits: swept every 8px
   across the band in Chromium and WebKit, 6.9 cleared the column by 0.2px and
   6.7 by 7px, which is room for a real tablet's font rendering to differ. */
@media (min-width: 621px) and (max-width: 900px) {
  h1.h1-longword { font-size: clamp(42px, 6.7vw, 88px); }
}

/* ---------- /build on a touch screen (BACKLOG 27) ----------
   The tiles' icons stand up on hover, which a phone never has, so on a phone
   the grid was static. Now: the icons pop once, in a quick stagger, as the grid
   first scrolls into view; and a tap plays the same pop, long enough to be seen
   (a tap is over in a tenth of a second, which is too short for :active alone).
   build-picker.js adds the classes. fill-mode is backwards, never both: a
   finished animation that kept its last frame would pin the icon and override
   :active and the selected state from then on. */
@keyframes chipPop {
  0%, 100% { transform: none; }
  45% { transform: translateY(-5px) rotate(-6deg); }
}
/* The same motion under a second name. A tap during the arrival would otherwise
   share its animation name and never restart, and the arrival rule is the more
   specific of the two, so the tap is listed under it as well to win. */
@keyframes chipTap {
  0%, 100% { transform: none; }
  45% { transform: translateY(-5px) rotate(-6deg); }
}
@media (hover: none) and (prefers-reduced-motion: no-preference) {
  .chips-arrive .chips > .chip .chip-i {
    animation: chipPop .56s cubic-bezier(.22,.9,.3,1.12) backwards;
    animation-delay: calc(var(--k, 0) * 45ms);
  }
  .chip.chip-tap .chip-i,
  .chips-arrive .chips > .chip.chip-tap .chip-i {
    animation: chipTap .42s cubic-bezier(.22,.9,.3,1.12) backwards;
    animation-delay: 0s;
  }
}

/* ---------- /studio sample invoice (opened from the Create demo) ----------
   Uses the screenshot dialog's frame; the page inside is drawn here. */
/* On a phone the row wraps: the numbers never split ("INV-" / "0043", F17-3), and
   "View the invoice" drops to its own line instead of squeezing them. */
.gp-seq { flex-wrap: wrap; row-gap: 8px; }
.gp-seq-prev, .gp-seq-next, .gp-inv-link { white-space: nowrap; }
button.gp-seq-next { font: inherit; font-weight: 800; border: 0; cursor: pointer; }
button.gp-seq-next:hover { filter: brightness(1.08); }
.gp-inv-link {
  font: inherit; font-size: var(--fs-sm); font-weight: 700; color: var(--brand-ink);
  background: none; border: 0; padding: 4px 2px; cursor: pointer; text-decoration: underline var(--brand-accent-deep);
  text-underline-offset: 3px;
}
.gp-inv-open:focus-visible, .gp-inv-link:focus-visible { outline: 2px solid var(--brand-accent-deep); outline-offset: 3px; }
.shot-dialog.inv-dialog { width: min(760px, calc(100vw - 32px)); }
.inv-doc {
  position: relative; overflow: hidden; background: #fff; color: var(--brand-ink);
  margin: 16px; padding: 32px 32px 28px; border-radius: 3px;
  box-shadow: inset 0 0 0 1px rgba(29,28,33,.08); font-size: var(--fs-sm); line-height: 1.5;
  font-variant-numeric: tabular-nums;
}
.inv-mark {
  position: absolute; left: 50%; top: 52%; transform: translate(-50%, -50%) rotate(-24deg);
  font-size: 96px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: rgba(29,28,33,.06); pointer-events: none; user-select: none; white-space: nowrap;
}
.inv-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
.inv-from { margin: 0; font-weight: 800; font-size: var(--fs-md); }
.inv-addr { margin: 4px 0 0; color: var(--ink-2); }
.inv-title { margin: 0; font-size: 28px; letter-spacing: -.02em; }
.inv-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 24px; margin: 24px 0 16px; }
.inv-meta span, .inv-to span { display: block; font-size: var(--fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); }
.inv-to p { margin: 2px 0 0; }
.inv-lines { width: 100%; border-collapse: collapse; margin: 20px 0 8px; }
.inv-lines th { text-align: left; font-size: var(--fs-xs); letter-spacing: .08em; text-transform: uppercase;
  color: var(--brand-sand); background: var(--brand-ink); padding: 8px 10px; }
.inv-lines td { padding: 10px; border-bottom: 1px solid var(--hair); }
.inv-lines th:not(:first-child), .inv-lines td:not(:first-child) { text-align: right; white-space: nowrap; }
.inv-sums { margin-left: auto; width: min(280px, 100%); }
.inv-sums div { display: flex; justify-content: space-between; gap: 16px; padding: 6px 10px; }
.inv-total { border-top: 2px solid var(--brand-ink); font-weight: 800; font-size: var(--fs-md); margin-top: 4px; padding-top: 10px !important; }
.inv-foot { margin: 20px 0 0; color: var(--ink-2); }
/* On a phone the invoice keeps its layout and scrolls sideways inside the dialog. */
@media (max-width: 620px) { .inv-doc { min-width: 520px; padding: 24px 20px; } .inv-mark { font-size: 72px; } }

/* ============================================================
   PAGE KIT (Madurai_2)
   Naveen, 2026-09-17: "except home page, rest of the pages have same colour
   tone and looks bland… make this site an example of a high professional site",
   with a steer to look at award-winning work. Two things were taken from the
   references rather than their looks: long-form body text set far larger than
   ours (Shape Up runs 24px/1.5 in a narrow column against our 17px), and a
   muted, narrow standfirst under a large heading (Linear's Method). The rest is
   ours: Bricolage display, sand and ink, one magenta.

   These blocks are deliberately generic — /how-we-build is the first page built
   from them, and the others follow.
   ============================================================ */

/* Long-form prose: bigger, looser, and narrower than UI copy. */
.doc { gap: 20px; }
.doc p, .doc li { font-size: clamp(17px, 1.25vw, 20px); line-height: 1.66; max-width: 64ch; }
/* Bigger body type needs more room around a heading, or the rule under it sits
   on the words and the section above runs into the next ("this heading does not
   have right gap"). Space above a heading belongs to the heading, not to the
   paragraph that ended. */
.doc h2 {
  font-size: clamp(24px, 3vw, 34px); letter-spacing: -.018em;
  margin-top: clamp(34px, 4.2vw, 60px); padding-bottom: 16px; margin-bottom: 4px;
}
.doc h2:first-child { margin-top: 0; }

/* The figures a page turns on, said once, large. */
.figs {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(20px, 3vw, 52px);
  border-top: 1px solid var(--hair); border-bottom: 1px solid var(--hair);
  padding: clamp(22px, 3vw, 38px) 0;
}
.figs li { display: flex; flex-direction: column; gap: 6px; }
.figs b {
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-variation-settings: "wdth" 94;
  font-size: clamp(44px, 6vw, 76px); font-weight: var(--wt-display); line-height: .9;
  letter-spacing: -.02em; color: var(--brand-ink);
}
.figs span { font-size: var(--fs-base); color: var(--ink-2); max-width: 26ch; }
@media (max-width: 720px) { .figs { grid-template-columns: minmax(0, 1fr); gap: 22px; } }

/* A fixed list, shown as one: eight items that are a set, not a paragraph. */
/* No margins on any kit block: the wrap sets the gaps. A margin here is how the
   grid ended up with 26px above it and nothing below. */
.dims {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px;
  background: var(--hair); border: 1px solid var(--hair); border-radius: 4px; overflow: hidden;
}
.dims li {
  display: flex; align-items: baseline; gap: 11px;
  padding: clamp(17px, 1.6vw, 22px) clamp(18px, 1.7vw, 24px);
  background: var(--brand-sand-2); font-size: var(--fs-base); font-weight: 600; color: var(--brand-ink);
}
.dims-n { font-size: var(--fs-xs); font-weight: 700; letter-spacing: .1em; color: var(--brand-accent-ink-2); }
@media (max-width: 900px) { .dims { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .dims { grid-template-columns: minmax(0, 1fr); } }

/* Roles, ladders: numbered things that happen in order. */
.roles, .ladder { list-style: none; margin: 0; padding: 0; }
.roles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(16px, 2vw, 26px); }
.role {
  background: var(--brand-sand-2); border: 1px solid var(--hair); border-top: 3px solid var(--brand-accent-deep);
  border-radius: 4px; padding: clamp(20px, 2.2vw, 28px);
  display: flex; flex-direction: column; gap: 10px;
}
.role-n, .ladder-n {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .12em;
  color: var(--brand-accent-ink-2);
}
.role h3 { font-size: clamp(18px, 1.6vw, 21px); letter-spacing: -.02em; margin: 0; }
.role p { margin: 0; font-size: var(--fs-base); line-height: 1.55; color: var(--ink-2); }
@media (max-width: 860px) { .roles { grid-template-columns: minmax(0, 1fr); } }

.ladder { border-left: 2px solid var(--hair); }
.ladder li { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 14px; padding: 16px 0 16px 22px; position: relative; }
.ladder li + li { border-top: 1px solid var(--hair); }
.ladder li::before {
  content: ""; position: absolute; left: -6px; top: 22px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--ground); border: 2px solid var(--brand-accent-deep);
}
.ladder p { margin: 0; font-size: clamp(16px, 1.15vw, 18px); line-height: 1.55; max-width: 58ch; }

/* One dark band per page, carrying the page's strongest sentence. Same ground
   as the homepage's bands, so the site reads as one thing. */
.quote-band { background: var(--brand-ink); color: var(--brand-sand); padding: var(--sec-y) 0; }
.quote-band .tag { color: var(--label-on-ink); }
.quote-big {
  margin: 14px 0 0; max-width: 24ch;
  font-family: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  font-variation-settings: "wdth" 94, "opsz" 64;
  font-size: clamp(28px, 4.4vw, 54px); font-weight: var(--wt-display-dark); line-height: 1.1;
  letter-spacing: -.02em; color: var(--brand-sand);
}
.quote-foot { margin: 22px 0 0; max-width: 52ch; font-size: clamp(16px, 1.3vw, 19px); color: var(--on-ink); }

/* The phone still is on the <video> element's own `poster` attribute, in the
   MARKUP — see index.html. It used to be set only by band-video.js, so that a
   phone would not download the 1920-wide desktop still; the cost was a <video>
   with no poster and no source until the script ran, which renders as a BLACK
   BOX with native controls over it. That is what Naveen photographed.

   A CSS background on .pin-video was tried first and does not work: the video
   element paints its own black box on top of it. The poster attribute is the
   only thing the element itself will show. Desktop pays 27KB for the phone
   still it does not use, and band-video.js still upgrades it to the 61KB
   desktop still on wide screens. */

/* ---- the held bands need far more scroll on a touch screen ----
   The five stages are spread across the band's scroll length, which gave 203px
   per stage on a phone against 231px on desktop — near enough identical, and
   that is the bug.

   (Those figures are travel / (5 x 1.08), not travel / 5. `network.js:223`
   picks the stage with `floor(t * n * 1.08)`, so a stage spans about 8% less
   than a naive fifth. I first wrote 219 and 249, which release QA corrected —
   L-1. The ratio was right and the numbers were not, which is exactly the kind
   of figure that gets quoted back later as if it had been measured.) 249px is two or three notches of a mouse wheel and reads as
   controllable. A single momentum flick on a phone covers 800-1500px, which is
   four to seven stages: the text rolls past before it can be read (Naveen, on
   the live site: "in mobile a single scroll rolls this text ... to 3
   sentences").

   The band is tuned for a wheel notch and a flick is an order of magnitude
   larger, so the fix is distance, not maths — the progress calculation is
   shared with the other held band and is correct. 230vh becomes 460vh, which
   measures 563px per stage on a 390x844 phone — a 2.8x increase, and a flick
   now crosses about one stage rather than five.

   `.pin-band` gets the same treatment: it uses the same mechanism and has the
   same problem, less visibly because it drives one headline rather than five
   stages. */
@media (max-width: 900px) and (pointer: coarse) {
  .net-motion .netband  { height: 460vh; }
  .net-motion .pin-band { height: 300vh; }
}

/* ---- footer policy links: tap target ----
   Privacy, Security and Accessibility measured 45x21, 52x21 and 79x21 CSS px on
   a 390px phone, against the 24x24 WCAG 2.5.8 (AA) asks for, with only 18px to
   the nearest other target — so the spacing exception did not rescue them
   either. Found on the LIVE site after Madurai_2 merged.

   Padding rather than a larger font: the 11-13px type is deliberate and passes
   contrast at 8.15:1, and the negative margin keeps the row's visual alignment
   unchanged while the hit area grows. Block-level padding on an inline link does
   not move the line box, so the footer's layout is untouched.

   Recorded because of how it was missed: on 2026-09-17 I measured footer tap
   targets and reported them fine. Those were the footer NAV links, which are
   46px tall. The policy links in the bottom bar are a different set and were
   never measured — a clean result on the elements a check happened to select,
   reported as a clean result for the footer. */
.foot-bottom a {
  padding: 8px 6px;
  margin: -8px -6px;
  display: inline-block;
}
