/* PlausiDen motion layer.
 *
 * WHY THIS IS BUILT THE WAY IT IS
 *
 * An earlier version of the site hid every section with `opacity: 0` and used
 * an IntersectionObserver to reveal it. When the observer did not fire — which
 * happened on mobile — the page rendered a hero above a blank void. Content
 * that is invisible until JavaScript agrees is not a design flourish, it is an
 * outage on the page a buyer is reading.
 *
 * So the rule here: NOTHING IS EVER HIDDEN UNLESS THE BROWSER HAS ALREADY
 * PROVEN IT CAN BRING IT BACK. Reveal-on-scroll uses native CSS scroll-driven
 * animation, and the hidden start state lives INSIDE `@supports
 * (animation-timeline: view())`. A browser without that feature never applies
 * `opacity: 0` at all, so it simply shows the content. No JavaScript is
 * involved in making text visible.
 *
 * Motion is also opt-out: `prefers-reduced-motion: reduce` disables every
 * animation and transition defined here, leaving the final visual state.
 *
 * Loaded AFTER the frozen Tailwind bundle, so these rules win ties on equal
 * specificity. See src/views/layout.rs for the order.
 */

/* ---------------------------------------------------------------- tokens -- */

:root {
  /* One easing curve for the whole site: a gentle decelerate. Using a single
     curve is most of what makes motion feel like one system rather than
     several people's defaults stacked together. */
  --pd-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --pd-fast: 160ms;
  --pd-base: 320ms;
  --pd-slow: 640ms;
  /* Distance an element travels while arriving. Small on purpose — long
     travel reads as a slideshow, not a professional site. */
  --pd-rise: 14px;

  /* The frozen bundle ships `.font-mono { font-family: var(--font-mono) }`
     but never defines --font-mono anywhere, so the declaration is invalid at
     computed-value time and the element silently inherits the body sans. The
     evidence transcript on /sample-report measured as Plus Jakarta Sans: an
     HTTP request/response that does not align in columns undermines the one
     page whose entire job is looking like a real deliverable.

     A system stack rather than a webfont — nothing to download, and every
     platform already has a good monospace face. */
  --font-mono:
    ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono",
    "Courier New", monospace;

  /* ---- design tokens the frozen bundle consumes but never defines ----
     The bundle carries the component CSS of the React app it came from,
     including its token *references*. The token *definitions* lived in that
     app's theme layer, which did not survive. A `var()` with no definition
     and no fallback makes the whole declaration invalid at computed-value
     time, so the property falls back to its inherited or initial value and
     the browser reports no error at all.

     Measured before defining these:
       .border-primary-border  -> white, on 21 elements. Every primary CTA
                                  carried a 1px white ring.
       [border-color:var(--button-outline)]
                               -> slate-900, i.e. currentColor leaking
                                  through. A near-black outline on a
                                  secondary button, which is the opposite of
                                  the hairline treatment used everywhere else.

     Only the three tokens reachable from rendered markup are defined here.
     The other 30 undefined properties belong to Radix and sidebar components
     this site never renders; giving them values would be inventing a design
     for dead code. */

  /* Consumed directly as a colour. Slightly darker than the primary fill
     (hsl(220 90% 28%)) so a filled button keeps a crisp edge on a light
     background rather than dissolving into it. */
  --primary-border: hsl(220 90% 22%);

  /* Secondary buttons. One step stronger than the slate-200 hairline used
     for dividers — interactive edges should read a little firmer than
     decorative ones — but nowhere near the slate-900 it was falling back to. */
  --button-outline: #cbd5e1;

  /* Consumed inside hsl(var(--card-border) / <alpha>), so this must be bare
     channels rather than a colour function. slate-200. */
  --card-border: 214 32% 91%;
}

/* ------------------------------------------------------- reveal on scroll -- */

/* Baseline: fully visible. This is the state every browser gets unless the
   @supports block below opts it into the animation. */
.pd-reveal {
  opacity: 1;
  transform: none;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .pd-reveal {
      animation: pd-rise-in linear both;
      /* Run the animation as the element crosses the viewport, finishing
         while it is still comfortably on screen. */
      animation-timeline: view();
      animation-range: entry 0% cover 22%;
    }
  }
}

@keyframes pd-rise-in {
  from {
    opacity: 0;
    transform: translateY(var(--pd-rise));
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ------------------------------------------------------------ on arrival -- */

/* Above-the-fold entrance lives in animations.css as `.animate-fade-in-up`
   (46 elements use it) with `delay-1..3` for stagger. There was a second,
   unused implementation here — `.pd-enter` plus `.pd-enter-1..4` — offering
   the same effect with the same easing and nobody calling it. Two ways to do
   one thing, and the one this file defined was the one nothing used, so it is
   gone. If entrance timing needs changing, animations.css is the place. */

/* ------------------------------------------------------------ interaction -- */

/* Cards lift slightly and deepen their shadow on hover, and — importantly —
   do the same on keyboard focus, so the affordance is not mouse-only. */
@media (prefers-reduced-motion: no-preference) {
  .pd-lift {
    transition:
      transform var(--pd-base) var(--pd-ease),
      box-shadow var(--pd-base) var(--pd-ease),
      border-color var(--pd-base) var(--pd-ease);
  }
}

/* Values match Loom's FeatureCard, which rolls its own lift because it is
   shared with another tenant and cannot depend on this stylesheet. Two
   implementations is a fact of the repo boundary; two different lifts on the
   same page is not. -2px is Tailwind's -translate-y-0.5, and the shadow is the
   same ink-tinted hover shadow `shadow-lg` resolves to here, so a card lifts
   identically whichever system drew it.

   The `:focus-within` half is the part the ad-hoc versions never had: four
   hand-written lift treatments across the site all fired on :hover only, so a
   keyboard user tabbing through cards got no feedback at all. */
.pd-lift:hover,
.pd-lift:focus-within {
  transform: translateY(-2px);
  box-shadow:
    0 14px 32px -12px rgb(15 23 42 / 0.14),
    0 3px 8px -3px rgb(15 23 42 / 0.07);
}

/* `.pd-press` used to live here — a 1px lift on hover, released on :active.
   Nothing ever used it, and it should stay gone. Every button on the site
   answers a hover by shifting its background, verified as one behaviour
   across all of them, and movement is now the signal that a *card* is a
   link. A button that also moved would spend that signal where it carries
   no information: a button is already obviously a button. */

/* An underline that grows from the left on hover/focus. Used for nav links. */
.pd-underline {
  position: relative;
}

.pd-underline::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -2px;
  height: 2px;
  width: 0;
  background: currentColor;
  border-radius: 2px;
}

@media (prefers-reduced-motion: no-preference) {
  .pd-underline::after {
    transition: width var(--pd-base) var(--pd-ease);
  }
}

.pd-underline:hover::after,
.pd-underline:focus-visible::after {
  width: 100%;
}

/* ------------------------------------------------------------------ focus -- */

/* A visible, consistent focus ring everywhere. Keyboard users are not an edge
   case, and a security firm's site failing keyboard navigation is a bad look
   in front of exactly the audience that will notice.
 *
 * TWO THINGS WERE WRONG HERE, both invisible unless you tab with a keyboard.
 *
 * 1. The colour. This was #0f4c81, which is 8.86:1 on white and gorgeous —
 *    and 2.02:1 on the slate-900 bands, against a 3:1 requirement (SC 1.4.11,
 *    non-text contrast). The footer is slate-900 and renders on every page, so
 *    that single value failed on 83 of the 154 focusable elements measured
 *    across four routes: every footer link on the site, plus the dark CTA
 *    bands. #3b82f6 is the only candidate tried that clears 3:1 on all four
 *    surfaces the site actually paints — measured 3.68 on white, 3.52 on
 *    slate-50, 3.52 on gray-50, 4.85 on slate-900. It gives up some contrast
 *    on light surfaces to stop failing outright on dark ones.
 *
 * 2. The specificity. `:where()` scores zero, so the frozen bundle's
 *    `.focus-visible\:outline-none:focus-visible` (0-2-0) beat it and removed
 *    the ring entirely on eight elements. `:is(...)` scores by its most
 *    specific argument — `[tabindex]`, 0-1-0 — and the doubled
 *    `:focus-visible:focus-visible` takes the whole selector to 0-3-0, which
 *    wins. Repeating a pseudo-class purely for specificity looks like a typo,
 *    hence this paragraph. A focus ring is a floor, not a default: no
 *    component gets to opt out of being reachable by keyboard. */
:is(a, button, input, select, textarea, summary, [tabindex]):focus-visible:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
  border-radius: 3px;
}

/* Skip link: present for keyboard users, out of the way for everyone else. */
.pd-skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.75rem 1rem;
  background: #ffffff;
  color: #0f172a;
  font-weight: 600;
  box-shadow: 0 4px 16px rgb(15 23 42 / 0.18);
  border-radius: 0 0 6px 0;
}

.pd-skip-link:focus {
  left: 0;
}

/* ------------------------------------------------------------- proof row -- */

/* The homepage proof columns: a hairline accent rule with the client outcome
   beside it. Written here as one class rather than assembled from utilities,
   because the obvious utilities do not exist in the frozen bundle — `pl-6`
   resolves to nothing (only pl-4 and pl-8 were compiled) and
   `border-primary/30` has no opacity variant, so the first attempt rendered
   as 2px of padding against a default-grey line. The browser reports that
   honestly; the markup does not. */
.pd-proof {
  border-left: 2px solid rgb(30 64 175 / 0.35);
  padding-left: 1.25rem;
}

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

/* Two measured contrast failures, on every one of the 25 routes because the
   footer is shared:
     - the "LLC" brand accent renders text-primary (#073288) on the slate-900
       footer: ratio 1.55 against a 3.0 requirement for 20px bold,
     - the legal row renders slate-500 (#64748b) at 12px: ratio 3.75 against
       a 4.5 requirement.
   A dark brand colour on a dark band is a normal way to lose a logo. The fix
   is not to abandon the brand colour but to use its light-on-dark variant,
   which is what a brand palette is for: blue-400 measures 7.02 here and still
   reads as the same blue. The legal row moves to slate-400 at 6.96.

   Scoped to this site rather than fixed in the Loom footer component, which
   several tenants share and theme differently — their primary may be light
   already, and silently repainting another tenant's brand mark is not this
   change's business. If the same fault shows up elsewhere, it belongs in Loom
   as a tone-aware accent rather than as this override.

   It did show up elsewhere. A contrast sweep of every text node on every route
   found the same fault in the dark posture bands: the inline links reading
   "how we work" and "pricing posture" render #073288 on slate-900 at 1.55
   against a 4.5 requirement. Same cause, same fix, so the selector now covers
   both places rather than growing a second rule that says the same thing. */
footer .text-primary,
.bg-slate-900 .text-primary {
  color: #60a5fa;
}

footer .text-slate-500 {
  color: #94a3b8;
}

/* ------------------------------------------------------ headings on dark -- */

/* The frozen bundle contains a base rule:
     h1,h2,h3,h4,h5,h6 { ... color: rgb(15 23 42) }
   An element-selector rule beats an INHERITED value, so a heading inside a
   dark band marked `text-white` does not turn white — it stays slate-900 and
   disappears. That is not hypothetical: the homepage's "What working with us
   actually looks like" heading rendered slate-900 on a slate-900 section,
   contrast ratio 1.0, leaving a 90px hole between the eyebrow and the
   paragraph. It shipped that way, and its predecessor had the same fault, so
   the section heading on the front page had been invisible for some time.
   Nothing failed; the markup is perfectly reasonable.

   Fixing the one heading would leave the trap armed for the next person who
   writes a plain <h2> on a dark band. Instead: inside a container that has
   declared itself light-on-dark, a heading that specifies no colour of its own
   inherits, which is what the author expected in the first place. Headings
   that DO carry an explicit colour class are untouched, so a white card inside
   a dark section still gets its dark heading.

   The exclusion list names colour utilities specifically. A first attempt used
   :not([class*="text-"]), which also excluded every SIZE utility — the very
   heading being fixed carries `text-4xl`, so the rule matched nothing and the
   heading stayed invisible. Surveyed the rendered markup: the only colour
   classes that appear on headings anywhere on this site are text-slate-900 and
   text-white, so those are what must be allowed to win. */
.text-white
  :is(h1, h2, h3, h4, h5, h6):not([class*="text-slate"]):not([class*="text-white"]):not(
    [class*="text-primary"]
  ) {
  color: inherit;
}

/* ---------------------------------------------------------------- depth -- */

/* A censussed problem, not a taste one. Across eight pages the site used six
   shadow levels, and the two heaviest carried most of the weight: shadow-xl 34
   times and shadow-2xl twice. The frozen bundle defines them as pure black —
   shadow-2xl is `0 25px 50px -12px rgb(0 0 0 / .25)` — and a 25%-black 50px
   blur is the single most reliable way to make a page look like a 2015
   template. Big soft black clouds read as cheap; tight, low-opacity, slightly
   ink-tinted shadows read as considered.

   Two changes, both here rather than in markup so no page has to be touched:
   1. Every level gets softer and is tinted with slate-900 (the body ink)
      instead of pure black, so shadows sit in the same colour family as the
      type rather than muddying it.
   2. shadow-2xl is collapsed onto shadow-xl. Six levels of elevation is more
      than this site distinguishes between; five is already generous.

   Tailwind's utilities set box-shadow from the --tw-shadow custom property, so
   redefining just that property here is enough — this file loads after the
   bundle, and the bundle's own box-shadow declaration picks up the new value. */
.shadow-sm {
  --tw-shadow: 0 1px 2px 0 rgb(15 23 42 / 0.04);
}

.shadow-md {
  --tw-shadow:
    0 2px 6px -1px rgb(15 23 42 / 0.06), 0 1px 2px -1px rgb(15 23 42 / 0.04);
}

.shadow-lg {
  --tw-shadow:
    0 8px 20px -6px rgb(15 23 42 / 0.1), 0 2px 6px -2px rgb(15 23 42 / 0.06);
}

.shadow-xl,
.shadow-2xl {
  --tw-shadow:
    0 14px 32px -12px rgb(15 23 42 / 0.12), 0 3px 8px -3px rgb(15 23 42 / 0.06);
}

/* ------------------------------------------------------------ tap targets -- */

/* Measured on a 390px viewport: the menu button rendered 40x40 and every link
   in the mobile drawer was under 44px tall. WCAG 2.1 AA (2.5.5) asks for 44x44,
   and below that a thumb starts hitting the wrong row — on the nav, which is
   the one control every mobile visitor uses.
   Set as min-* so nothing shrinks and the existing padding still applies. */
/* Scoped below the lg breakpoint on purpose. An earlier version of this rule
   set `display: inline-flex` unconditionally; because this file loads after the
   Tailwind bundle, it beat `lg:hidden` and the menu button appeared on desktop
   next to the full nav — a regression introduced by an accessibility fix and
   caught only by looking at a screenshot, which is the argument for looking at
   screenshots. Setting display anywhere a breakpoint also sets it is a fight
   this file should not pick, so the whole block now lives where the button is
   meant to be visible. */
@media (max-width: 1023.98px) {
  #mobile-menu-toggle {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  #mobile-menu a {
    min-height: 44px;
    display: flex;
    align-items: center;
  }
}

/* --------------------------------------------------------------- headings -- */

/* `.pd-rule` and `.pd-rule-center` used to draw a 44x3 gradient dash under
   section headings. Never applied to anything, and worth not reviving: a
   gradient bar under every heading is the decoration the rest of this design
   deliberately avoids — the brief asks for hairline rules and restraint, and
   section separation here already comes from alternating band backgrounds. */

/* ------------------------------------------------- global reduced motion -- */

@media (prefers-reduced-motion: reduce) {
  /* `.animate-fade-in-up`, the live entrance animation, is switched off in
     animations.css rather than here. It sets `animation: none`, which drops
     the `both` fill along with it, so the element renders at its natural
     opacity — the property that matters after an earlier version of this site
     shipped content stuck at opacity 0. */
  .pd-reveal,
  .pd-lift,
  .pd-underline::after {
    animation: none !important;
    transition: none !important;
  }

  .pd-reveal {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ==========================================================================
   FROZEN-BUNDLE GAP FILLS
   --------------------------------------------------------------------------
   static/index-*.css is a frozen Tailwind build with no build tool attached,
   so any utility the original React app did not happen to compile emits no
   CSS at all and fails silently. A census of the rendered HTML found 71 such
   classes in live use (measured, not guessed: `.mb-2` computed to 0px on a
   real paragraph). The rules below restore the authored intent using the
   bundle's own idioms -- hsl(var(--primary) / a) for tints, the standard
   Tailwind spacing scale (n * 0.25rem) for spacing.

   Ordering is load-bearing: base rules first, then min-width blocks, then
   state variants, so responsive and interactive overrides still win. Do not
   reach for :where() or @layer here -- Tailwind preflight resets
   `p { margin: 0 }` unlayered, and it would beat a zero-specificity rule.

   `lib.rs::utility_class_coverage` fails the build if this drifts again.
   ========================================================================== */

/* -- spacing ------------------------------------------------------------- */
.mb-2 { margin-bottom: 0.5rem; }
.mb-9 { margin-bottom: 2.25rem; }
.mb-10 { margin-bottom: 2.5rem; }
.mb-12 { margin-bottom: 3rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-8 { margin-top: 2rem; }
.mt-12 { margin-top: 3rem; }
.p-5 { padding: 1.25rem; }
.p-7 { padding: 1.75rem; }
.pt-7 { padding-top: 1.75rem; }
.pb-9 { padding-bottom: 2.25rem; }
.pb-16 { padding-bottom: 4rem; }
.pl-5 { padding-left: 1.25rem; }
.px-7 { padding-left: 1.75rem; padding-right: 1.75rem; }
.py-10 { padding-top: 2.5rem; padding-bottom: 2.5rem; }
.gap-5 { gap: 1.25rem; }
.space-y-10 > :not([hidden]) ~ :not([hidden]) { margin-top: 2.5rem; }
.space-y-12 > :not([hidden]) ~ :not([hidden]) { margin-top: 3rem; }

/* -- sizing -------------------------------------------------------------- */
.w-12 { width: 3rem; }
.max-w-6xl { max-width: 72rem; }

/* -- lists and scroll containers ---------------------------------------- */
.list-decimal { list-style-type: decimal; }
.list-inside { list-style-position: inside; }

/* Wide content (evidence blocks, request/response dumps) must scroll inside
   its own container so the page body never scrolls horizontally. */
.overflow-x-auto { overflow-x: auto; }

/* -- heading line balance ------------------------------------------------ */

/* Headings distribute their text evenly across lines instead of filling each
   line greedily and dropping whatever is left onto the last one. Without this,
   a two-line hero regularly strands a single word — "properly." alone under a
   full line — which reads as a mistake at the exact moment a visitor is
   forming an impression.

   Progressive enhancement: a browser without text-wrap keeps today's greedy
   breaking, so nothing is lost. Applied to headings only; `balance` is capped
   at a handful of lines by implementations and is the wrong tool for body
   copy, which wants `pretty` at most. */
h1,
h2,
h3 {
  text-wrap: balance;
}

/* -- typography ---------------------------------------------------------- */
.text-\[10px\] { font-size: 10px; }
.text-\[11px\] { font-size: 11px; }
.text-\[15px\] { font-size: 15px; }
.leading-\[1\.15\] { line-height: 1.15; }
.tracking-\[0\.18em\] { letter-spacing: 0.18em; }
.tracking-\[0\.2em\] { letter-spacing: 0.2em; }
.font-extralight { font-weight: 200; }
.font-light { font-weight: 300; }
.italic { font-style: italic; }
.underline { text-decoration-line: underline; }

/* -- surfaces and borders ------------------------------------------------ */
.bg-primary\/8 { background-color: hsl(var(--primary) / 0.08); }
.bg-slate-50\/80 { background-color: rgb(248 250 252 / 0.8); }
.border-primary\/15 { border-color: hsl(var(--primary) / 0.15); }
.border-primary\/40 { border-color: hsl(var(--primary) / 0.4); }
.border-slate-200\/80 { border-color: rgb(226 232 240 / 0.8); }
.shadow-xs { box-shadow: 0 1px 2px 0 rgb(15 23 42 / 0.06); }

/* Ring: the only ring-1 users are the service-card icon tiles, which carry
   no shadow utility, so box-shadow is free here. */
.ring-1 { box-shadow: 0 0 0 1px var(--pd-ring-color, rgb(15 23 42 / 0.08)); }
.ring-primary\/15 { --pd-ring-color: hsl(var(--primary) / 0.15); }

/* -- gradients ----------------------------------------------------------- */
/* Positions are hardcoded rather than read from --tw-gradient-*-position,
   which the frozen bundle never initialises. */
.bg-gradient-to-br { background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); }
.from-primary\/10 {
  --tw-gradient-from: hsl(var(--primary) / 0.1) 0%;
  --tw-gradient-to: hsl(var(--primary) / 0) 100%;
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-primary\/30 {
  --tw-gradient-from: hsl(var(--primary) / 0.3) 0%;
  --tw-gradient-to: hsl(var(--primary) / 0) 100%;
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-slate-900\/30 {
  --tw-gradient-from: rgb(15 23 42 / 0.3) 0%;
  --tw-gradient-to: rgb(15 23 42 / 0) 100%;
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.via-slate-900\/10 {
  --tw-gradient-to: rgb(15 23 42 / 0) 100%;
  --tw-gradient-stops: var(--tw-gradient-from), rgb(15 23 42 / 0.1) 50%, var(--tw-gradient-to);
}
.to-primary\/5 { --tw-gradient-to: hsl(var(--primary) / 0.05) 100%; }

/* -- button elevate (shadcn pattern the bundle never compiled) ------------ */
.hover-elevate,
.active-elevate-2 { position: relative; }

.hover-elevate::after,
.active-elevate-2::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background-color: transparent;
  transition: background-color 150ms ease;
}

.hover-elevate:hover::after { background-color: rgb(15 23 42 / 0.06); }
.active-elevate-2:active::after { background-color: rgb(15 23 42 / 0.12); }

@media (prefers-reduced-motion: reduce) {
  .hover-elevate::after,
  .active-elevate-2::after { transition: none; }
}

/* -- responsive: md ------------------------------------------------------ */
@media (min-width: 768px) {
  .md\:inline-block { display: inline-block; }
  .md\:col-span-2 { grid-column: span 2 / span 2; }
  .md\:gap-5 { gap: 1.25rem; }
  .md\:gap-7 { gap: 1.75rem; }
  .md\:mt-4 { margin-top: 1rem; }
  .md\:p-6 { padding: 1.5rem; }
  .md\:p-8 { padding: 2rem; }
  .md\:p-9 { padding: 2.25rem; }
  .md\:px-9 { padding-left: 2.25rem; padding-right: 2.25rem; }
  .md\:py-14 { padding-top: 3.5rem; padding-bottom: 3.5rem; }
  .md\:pt-44 { padding-top: 11rem; }
  .md\:pb-24 { padding-bottom: 6rem; }
  .md\:h-16 { height: 4rem; }
  .md\:w-16 { width: 4rem; }
  .md\:text-base { font-size: 1rem; line-height: 1.5rem; }
  .md\:text-lg { font-size: 1.125rem; line-height: 1.75rem; }
  .md\:text-\[1\.875rem\] { font-size: 1.875rem; }
}

/* -- responsive: lg ------------------------------------------------------ */
@media (min-width: 1024px) {
  .lg\:text-6xl { font-size: 3.75rem; line-height: 1; }
  .lg\:text-\[2\.125rem\] { font-size: 2.125rem; }
}

/* -- state variants ------------------------------------------------------ */
.hover\:bg-primary\/90:hover { background-color: hsl(var(--primary) / 0.9); }
.hover\:border-primary\/40:hover { border-color: hsl(var(--primary) / 0.4); }
.hover\:shadow-lg:hover { box-shadow: 0 14px 32px -12px rgb(15 23 42 / 0.14), 0 3px 8px -3px rgb(15 23 42 / 0.07); }
.open\:border-primary\/30[open] { border-color: hsl(var(--primary) / 0.3); }
.open\:shadow-xl[open] { box-shadow: 0 14px 32px -12px rgb(15 23 42 / 0.12), 0 3px 8px -3px rgb(15 23 42 / 0.06); }
.group[open] .group-open\:rotate-90 { transform: rotate(90deg); }
.group\/cta:hover .group-hover\/cta\:translate-x-1 { transform: translateX(0.25rem); }

/* ==========================================================================
   BLOG SURFACE GAP FILLS
   --------------------------------------------------------------------------
   The coverage guard only walked the marketing routes, so the blog went
   unaudited and 25 classes were doing nothing. Two were visible on every
   post: `list-disc` meant bullet lists rendered without bullets, and
   `bg-slate-100` + `px-1.5` meant inline code rendered as plain text with no
   background and no side padding. The rest is page spacing (`md:pt-40`,
   `pb-12`) and the blog index's hover treatment.

   `prose` and `prose-slate` are the Tailwind Typography plugin, which the
   frozen bundle never included — that is why the posts carry per-element
   classes at all. They are left inert rather than reimplemented: the
   per-element classes already do the job now that their utilities exist, and
   a second typography system layered over them would double every margin.
   ========================================================================== */

/* -- lists --------------------------------------------------------------- */
.list-disc { list-style-type: disc; }

/* -- inline code --------------------------------------------------------- */
.bg-slate-100 { background-color: rgb(241 245 249); }
.px-1\.5 { padding-left: 0.375rem; padding-right: 0.375rem; }

/* -- post + index spacing ------------------------------------------------ */
.pb-12 { padding-bottom: 3rem; }
.mt-5 { margin-top: 1.25rem; }
.top-6 { top: 1.5rem; }
.bottom-6 { bottom: 1.5rem; }

/* -- type ---------------------------------------------------------------- */
.leading-\[1\.2\] { line-height: 1.2; }
.tracking-normal { letter-spacing: 0; }
.tracking-wider { letter-spacing: 0.05em; }
.normal-case { text-transform: none; }

/* -- the index card's underline sweep ------------------------------------ */
/* A 2px rule that scales up from the baseline on hover. `origin-top` is in
   the markup but the transform grows downward from the top edge, so the
   origin has to be the bottom for the rule to appear to rise. Kept as
   authored: the class names describe the intent, and correcting the origin
   here would silently contradict the markup. */
.origin-top { transform-origin: top; }
.scale-y-0 { transform: scaleY(0); }
.ease-out { transition-timing-function: cubic-bezier(0, 0, 0.2, 1); }

.group:hover .group-hover\:scale-y-100 { transform: scaleY(1); }
.group:hover .group-hover\:text-primary { color: hsl(var(--primary)); }
.hover\:-translate-y-1:hover { transform: translateY(-0.25rem); }

@media (min-width: 768px) {
  .md\:py-16 { padding-top: 4rem; padding-bottom: 4rem; }
  .md\:py-20 { padding-top: 5rem; padding-bottom: 5rem; }
  .md\:pt-40 { padding-top: 10rem; }
  .md\:pb-16 { padding-bottom: 4rem; }
  .md\:pb-20 { padding-bottom: 5rem; }
}

@media (min-width: 1024px) {
  .lg\:text-\[2rem\] { font-size: 2rem; }
}

/* ==========================================================================
   LEGAL / STATUS / SUBSCRIBE GAP FILLS
   --------------------------------------------------------------------------
   Found once the guard was pointed at every public route rather than a
   hand-kept subset. Each of these was doing nothing:

     - the callout box on both legal pages had no background, border or text
       colour, so a deliberately-highlighted notice read as plain paragraph;
     - the status page's health dot had no fill, so the one element whose
       entire job is being green was invisible;
     - `select-all` on /subscribe, which is what makes a single click select
       the whole feed URL — the page tells the reader to copy it.
   ========================================================================== */

/* -- amber notice, used on /privacy-directive and /terms-of-service ------- */
.bg-amber-50 { background-color: rgb(255 251 235); }
.border-amber-200 { border-color: rgb(253 230 138); }
.text-amber-800 { color: rgb(146 64 14); }
.text-amber-900 { color: rgb(120 53 15); }
.hover\:text-amber-700:hover { color: rgb(180 83 9); }

/* -- status page --------------------------------------------------------- */
.bg-emerald-500 { background-color: rgb(16 185 129); }
.text-emerald-900 { color: rgb(6 78 59); }
.gap-x-8 { column-gap: 2rem; }
.gap-y-4 { row-gap: 1rem; }

/* -- subscribe ----------------------------------------------------------- */
.items-baseline { align-items: baseline; }
.pl-6 { padding-left: 1.5rem; }

/* One click selects the whole feed URL. The page asks the reader to copy it
   into a feed reader, so this is the difference between a copy and a careful
   drag across a long string. */
.select-all { user-select: all; }

/* ==========================================================================
   ARTICLE PROSE
   --------------------------------------------------------------------------
   Blog posts styled every element by hand: `class="mb-6"` on 90 paragraphs,
   plus the list and inline-code chrome, 103 class attributes across five
   files. The posts were originally written for the Tailwind Typography
   plugin (`prose prose-slate`), which the frozen bundle never contained, and
   the per-element classes are the patch someone applied when it did nothing.

   The reason to replace them is not tidiness. An author writing a new post
   who forgets `mb-6` gets a paragraph with no space under it, and nothing
   anywhere reports that — the same silent-failure shape as the rest of this
   stylesheet's history. Content should not have to remember its own spacing.

   Every value below reproduces exactly what the utility classes computed to,
   measured before the change: paragraphs 24px bottom margin, lists 24px with
   8px between items, code at 14px on slate-100 with 2px/6px padding and a 4px
   radius. The article heights at 1440 and 390 are pinned in the commit
   message; if a future edit here changes them, it changed the design.
   ========================================================================== */

.pd-prose > p {
  margin-bottom: 1.5rem;
}

/* The lede. This used to be four utility classes typed into every post
 * (text-lg text-slate-600 leading-relaxed mb-8) — the container now infers
 * it from position so post bodies can be entirely class-free, which is what
 * lets the post_bodies_are_class_free guard exist at all. The earlier
 * comment here argued the opposite trade (explicit classes over positional
 * inference); the guard is why it flipped.
 *
 * Values are the MEASURED pre-change computed styles, not the classes'
 * nominal ones: the old mb-8 was dead weight — .pd-prose > p (0-1-1) beat
 * it (0-1-0), so the real margin was 24px and stays inherited from above.
 * Measured 2026-08-20 on all five posts: 18px / 29.25px / rgb(71,85,105).
 * Kept ABOVE the :has(+ ul) rule so that tie (both 0-2-1) keeps resolving
 * to the list lead-in margin, exactly as it would have before. */
.pd-prose > p:first-child {
  font-size: 1.125rem;
  line-height: 1.625;
  color: rgb(71 85 105);
}

/* A paragraph that introduces a list belongs to that list, so it closes
 * the gap rather than sitting in open space. Every such paragraph in the
 * blog ends in a colon ("The questions are:"), which is the tell.
 *
 * This was previously an mb-4 typed onto one of the three lead-ins by
 * hand; the other two were missed, which is what a per-element class
 * gets you. Deriving it from structure means a new post is spaced
 * correctly without the author knowing the rule exists.
 *
 * Note the specificity: .pd-prose > p is 0-1-1 and outranks a 0-1-0
 * utility like .mb-4, so an author cannot override this from the
 * template. That is deliberate — the container owns prose rhythm.
 * Browsers without :has() fall back to the uniform 1.5rem above. */
.pd-prose > p:has(+ ul) {
  margin-bottom: 1rem;
}

/* Markers hang in the gutter (`outside`) rather than sitting in the first
 * line box (`inside`). With `inside`, the second and subsequent lines of an
 * item wrap back under the bullet, so a multi-line item has no single left
 * edge and the marker stops anchoring anything — worst on narrow screens,
 * where most items wrap. `outside` gives every line one edge to align to.
 *
 * The 1.5rem padding is not decoration: `outside` markers are painted in
 * that gutter, so without it they would sit outside the content box.
 *
 * 1.5rem matches the `pl-6` already used by the numbered list on /subscribe,
 * which was the only list on the site rendering hanging indents correctly.
 * This makes all four marker lists on the site share one treatment. */
.pd-prose > ul {
  list-style-type: disc;
  list-style-position: outside;
  padding-left: 1.5rem;
  margin-bottom: 1.5rem;
  color: rgb(51 65 85);
}

.pd-prose > ul > li + li {
  margin-top: 0.5rem;
}

/* Inline code, not blocks. Descendant rather than child because it appears
   inside paragraphs and list items. */
.pd-prose code {
  font-size: 0.875rem;
  background-color: rgb(241 245 249);
  padding: 0.125rem 0.375rem;
  border-radius: 0.25rem;
}

/* ---------------------------------------------------------------------------
   PAGE HERO — desktop geometry

   `.pd-hero` is on every hero on the site: the eight rendered by Loom's Hero
   component, the six still hand-rolled in views, and the home hero. One class,
   one number. When only the home hero carried it, the front door ended up with
   144px of top padding while all thirteen pages behind it had 176 — the front
   door tighter than the rooms, which is backwards, and drift I introduced
   myself in the commit that widened the headline.

   The md: utilities are left in place on every one of those sections. They are
   the sub-768px cadence, and they are the fallback: motion.css loads after the
   frozen bundle, so at md+ these rules win the tie, and if this stylesheet ever
   fails to load the pages keep a sane cadence instead of collapsing.

   Paul, looking at 1440px: "the desktop site looks poor". He was right, and
   the measurement said why. The hero copy sat in a max-w-3xl box: 768px of
   content ending at x=872 inside a 1440px viewport, so the right 40% of the
   screen was empty and the 72px headline was folded into three ragged lines
   with room to spare. That is a phone layout stretched to a desktop.

   The band was also 912px tall — nearly a full viewport — mostly padding.

   Only the headline is released. The lede and the trust strip keep their own
   max-w-2xl, because 672px is a 68-character measure and widening body copy
   to match a headline is how you get an unreadable wall. Wide headline over
   narrow body is ordinary editorial practice, not a compromise.

   These are md/lg-only. Below 768px nothing changes: the phone layout was
   never the problem, and max-width only ever removes space it cannot use.
   --------------------------------------------------------------------------- */
@media (min-width: 768px) {
  .pd-hero {
    padding-top: 9rem;
    padding-bottom: 6rem;
  }
}

@media (min-width: 1024px) {
  .pd-hero-copy {
    max-width: 70rem;
  }
}

/* ---------------------------------------------------------------------------
   CMS PULL-QUOTE GAP FILLS

   The pull-quote block in cms_pages.rs asks for `border-l-4`, `my-6` and
   `not-italic`, and the frozen bundle compiled none of them — the React app it
   came from never used a pull-quote. So /docs/why-pps rendered its quotes with
   no accent bar, no vertical breathing room, and a `cite` still italicised by
   the user-agent default. Three declarations that were silently doing nothing.

   Found only because the /docs pages were finally admitted to the guard that
   checks every rendered class has CSS behind it; they had been exempted as
   "CMS-backed with no compile-time content to enumerate", which was untrue.
   --------------------------------------------------------------------------- */
.border-l-4 {
  border-left-width: 4px;
}

.my-6 {
  margin-top: 1.5rem;
  margin-bottom: 1.5rem;
}

.not-italic {
  font-style: normal;
}

/* Gap-fill: /services industries band container (2026-08-22). The frozen
   bundle never compiled max-w-5xl; 64rem sits between the 4xl posture band
   and the 6xl service cards, which is the width five link cards want. */
.max-w-5xl {
  max-width: 64rem;
}

/* In-body prose links (2026-08-22). Tailwind preflight strips every anchor
   affordance (color: inherit, no underline), and post bodies are class-free
   by guard, so the container provides it: the TextLink Underlined treatment
   (primary #073288, semibold, underline) expressed as CSS. Added for the
   first in-body link, plausible-deniability -> /docs/why-pps. */
.pd-prose a {
  color: rgb(7 50 136);
  font-weight: 600;
  text-decoration: underline;
}

/* List markers step back one shade (2026-08-22). Markers inherited body
   colour everywhere, weighing bullets and numbers the same as the words.
   One site-wide rule keeps the single list treatment converged in ce83166;
   slate-500 measures 4.76:1 on white — AA even as text, and a marker is
   not text. Deliberately main-scoped: footer/nav have no marker lists. */
main li::marker {
  color: rgb(100 116 139);
}

/* 404 escape links (2026-08-23). The page previously offered one exit; the
   list styles match the prose treatment: primary anchors, hanging markers. */
.pd-404-links {
  list-style-type: disc;
  list-style-position: outside;
  padding-left: 1.5rem;
  margin: 0.75rem 0 1.5rem;
  text-align: left;
  display: inline-block;
}
.pd-404-links li + li {
  margin-top: 0.5rem;
}
.pd-404-links a {
  color: rgb(7 50 136);
  font-weight: 600;
  text-decoration: underline;
}

/* Gap-fill: drawer CTA row on /services (2026-08-23). The frozen bundle
   compiled gap-x-8 but never gap-y-3; without it the two links touch when
   the row wraps on narrow screens. */
.gap-y-3 {
  row-gap: 0.75rem;
}

/* Gap-fill: 404 band rhythm (2026-08-23). The frozen bundle compiled py-24
   but never md:py-32; without it the 404 keeps mobile spacing on desktop. */
@media (min-width: 768px) {
  .md\:py-32 {
    padding-top: 8rem;
    padding-bottom: 8rem;
  }
}
