/* ============================================================================
   Proceptio.AI - design system
   proceptio.ai only. Nothing here is shared with, or deployed to, any other site.

   DIRECTION, 2026-08-10: measured from retool.com (see design/retool-direction/),
   chosen deliberately over the proceptio.com "Rectilinear" system because this is
   a product on its own domain and the brief was "not so red oriented". What that
   buys and what it costs:

     + The page ground is a warm near-black and the primary action is an off-white
       pill. Colour appears almost nowhere, which is exactly what "less red" means.
     + Status colour therefore reads LOUDLY, because nothing competes with it. On a
       page whose whole argument is "this was allowed / this was refused", that is
       the single most useful property the palette can have.
     - It does NOT look like proceptio.com. Same company, different product, and a
       visitor moving between the two will notice. Accepted on the white-label
       argument: a partner-operated instance was never going to wear Proceptio red.

   MEASURED FROM THE REFERENCE (live, 1440x900, 2026-08-10):
     body            #151515 ground, #E9EBDF text
     h1              71.7px / 75.3px / w300 / -1.58px tracking
     primary button  filled #E9EBDF, dark label, border-radius 9999px, 13.9px label
     ghost button    transparent, 1px #8B867F border, same pill
   Ours differs in two places on purpose, both noted at the rule.

   THE COLOUR LAW, carried over unchanged from the single-page version and still
   the most important rule in this file: --signal-no and --signal-yes MEAN refused
   and permitted. They are not decoration and nothing else on the page may use
   them. The page has no third accent competing for attention, so a red word is
   always a refusal and a green word is always a permission.
   ========================================================================== */

/* ---- Faces. Self-hosted files, not data URIs.

   The single-page version base64'd these into the HTML because it was ONE file
   and a blocked font fails silently. With several pages that is the wrong trade:
   it would re-send ~90 KB of font on every navigation and defeat the cache. They
   are files now, with `font-src 'self'` in web.config, and the silent-failure risk
   is handled by the build asserting the files exist. ---- */
@font-face {
  font-family: 'Sora';
  src: url(/assets/fonts/sora-latin.woff2) format('woff2');
  font-weight: 200 700; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Sora';
  src: url(/assets/fonts/sora-latin-ext.woff2) format('woff2');
  font-weight: 200 700; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url(/assets/fonts/jetbrains-mono-latin.woff2) format('woff2');
  font-weight: 400 600; font-style: normal; font-display: swap;
}

:root {
  /* Ground and surface. Warm near-black, measured from the reference. */
  --ground:     #151515;
  --surface:    #1C1C1B;
  --surface-2:  #232322;

  /* Ink. Warm off-white, NOT #FFF - the reference's #E9EBDF reads softer and is
     the one colour idea the July teardown already said was worth taking. */
  --ink:        #E9EBDF;
  /* TWO muted steps, and both MEASURED on --ground rather than chosen by eye:
       --ink-2  0.66 -> 7.11:1
       --ink-3  0.55 -> 5.32:1
     --ink-3 was first written at 0.44, which measures 3.80:1 and put 29 elements
     below the 4.5 floor - every eyebrow, every table header, every caption. That is
     the second time this exact mistake has been made on this product's pages with a
     fresh palette, so it is written here as a number rather than as a warning:
     ANYTHING quieter than 0.55 on this ground fails. If a third, quieter step feels
     necessary, the answer is size or weight, not more transparency. */
  --ink-2:      rgba(233, 235, 223, 0.66);
  --ink-3:      rgba(233, 235, 223, 0.55);

  /* Lines. Depth is hairlines only - no shadows anywhere on this site. */
  --line:       rgba(233, 235, 223, 0.12);
  --line-2:     rgba(233, 235, 223, 0.22);
  --line-btn:   #8B867F;

  /* STATUS ONLY. Never decoration. See the colour law above. */
  --signal-no:  #E5806A;
  --signal-yes: #86C8A3;
  /* Data highlight: tool names, reason codes, figures. The one warm accent, and
     it is deliberately not red. */
  --accent:     #E8A765;

  --font-ui:   'Sora', 'Inter', ui-sans-serif, system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;

  /* Type. The reference runs its display at WEIGHT 300 with tight negative
     tracking - the opposite of proceptio.com's w600 - and that light-and-large
     register is most of what makes it feel like this kind of product. */
  --t-display: clamp(2.75rem, 1.6rem + 4.6vw, 4.5rem);
  --t-h2:      clamp(2rem, 1.4rem + 2.4vw, 3rem);
  --t-h3:      1.5rem;
  --t-lead:    1.1875rem;
  --t-body:    1rem;
  --t-fine:    0.875rem;
  --t-mono:    0.8125rem;
  --t-micro:   0.6875rem;

  --shell:     1280px;
  --gutter:    48px;
  --band-y:    clamp(5rem, 9vw, 8.5rem);
  --measure:   68ch;
  --pill:      9999px;

  /* Easing, lifted verbatim from retool.com's own token scale (2026-08-10). The
     founder named `.ease-soft-ease-out` specifically; it is the slow-settling curve
     that makes a hover feel like something arriving rather than something snapping. */
  --ease-soft-out:    cubic-bezier(0.24, 0.01, 0.36, 1);
  --ease-soft-in-out: cubic-bezier(0.72, 0, 0.36, 1);
  --ease-out:         cubic-bezier(0.12, 1, 0.72, 1);

  /* ---- ARTWORK COLOUR ----
     Colour lives in the ARTWORK and never in the chrome. Retool's page ground is a
     near-black neutral like ours; all of its colour is in the media - peach and blue
     ambient fields, chart bars, avatars, syntax highlighting. Adopting that is what
     lets this site stop being monochrome WITHOUT breaking the rule that a coloured
     word means a permission decision.
     These may be used inside `.win`, `.asst` and the ambient fields. Nowhere else.

     TWO OF THESE WERE THE SIGNAL COLOURS UNDER A SECOND NAME, and it was latent
     rather than harmless. `--art-rose` was #E5806A and `--art-green` was #86C8A3 -
     byte-identical to --signal-no and --signal-yes. Both were already in the panel
     washes, invisible at 0.06 alpha, so nothing looked wrong; the moment those washes
     were strengthened to the reference's weight (2026-08-11) panel 2 lit red and panel
     5 lit green, and on a page whose argument is allowed-versus-refused that reads as
     the panel's verdict. Rose moved to a true magenta-rose and green to a cool cyan,
     both far enough from the signals to never be mistaken for one.

     THE STANDING RULE: an artwork hue may never equal a signal hue, and no artwork
     colour may land on a WORD. Geometry, fields, badges and bars may be coloured;
     text stays --ink/--ink-2/--ink-3 unless it is stating a permission decision. That
     is the whole of what keeps a coloured word meaningful while the pictures gain
     colour.

     THERE IS NO ARTWORK GREEN, and that is the deliberate hole in the palette. The
     reference's chart bars are lime (#A8D848) and lime was drafted here; it sits 75
     degrees off --signal-yes in hue, which is genuinely a different colour and still
     reads as "greenish" at chart-bar size on this ground. A reader who has learned
     that green means permitted should never have to decide whether a green shape is
     making a claim. Six hues is enough for geometry without borrowing the one that
     is spoken for. */
  --art-peach:  #E8A765;
  --art-rose:   #D97BA8;
  --art-blue:   #7BA7D9;
  --art-violet: #9B8BD9;
  --art-cyan:   #6FB6D9;
  --art-teal:   #6FBFB4;

  /* The client rail. Band depth and gap are the reference's (proceptio.com --rail-h:57px,
     gap 4.5rem); --rail-secs is DERIVED from the roster and must be re-derived when the
     roster changes - see the rail block for the formula. */
  --rail-h:    57px;
  --rail-gap:  4.5rem;
  --rail-secs: 103s;
}

@media (max-width: 900px) { :root { --gutter: 24px; } }
@media (max-width: 768px) { :root { --rail-h: 52px; --rail-gap: 3rem; } }

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--t-body);
  font-weight: 300;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

::selection { background: rgba(232, 167, 101, 0.28); color: var(--ink); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

img { max-width: 100%; height: auto; display: block; }

/* ---- Shell ---- */
.shell { max-width: var(--shell); margin-inline: auto; padding-inline: var(--gutter); }
.band > .shell { padding-block: var(--band-y); }
.band-tight > .shell { padding-block: clamp(3rem, 5vw, 4.5rem); }

/* The rail band is the one band whose content is NOT inside the shell: a marquee stopped
   at the content column would fade its logos out 128px short of each edge and read as a
   clipped box rather than as a strip crossing the page. So the label keeps the shell and
   the track bleeds, which means this band carries its own bottom padding - the shell's
   padding-block would otherwise sit BETWEEN the label and the rail and push them apart. */
.band-rail > .shell { padding-block: clamp(3rem, 5vw, 4.5rem) 2.5rem; }
.band-rail { padding-bottom: clamp(3rem, 5vw, 4.5rem); }
/* ...and it is the one band with NO grid verticals, which is the reference's own rule
   (Proceptio.Web design-tokens.css 3642-3651, founder 2026-07-22: "no grid verticals
   cutting through the moving logos, on either side"). Ours paint at z-index -1 so a mark
   occludes the hairline rather than being sliced by it, and I kept them at first on the
   argument that a break in the frame mid-page reads as a mistake. Looking at it settled
   it: the vertical bisects the strip and meets the rail's hairline in a T-junction that
   means nothing, and the eye goes straight to the junction instead of to the logos. The
   full-width hairline is the terminus that makes the gap read as deliberate, so the two
   changes are one decision and reversing either alone brings the cross back. */
.band-rail::before { display: none; }

/* ---- THE GRID.

   Taken from proceptio.com's `.page-lines` (Proceptio.Web/wwwroot/css/design-tokens.css),
   technique and rulings included. Two full-height hairlines at the outer edges of the
   content column, running top to bottom through every band, so the page reads as one
   architectural frame rather than as a stack of unrelated sections.

   FOUR THINGS COPIED DELIBERATELY, each of which is a decision rather than a detail:

   1. TWO lines, not five. The interior 25/50/75% lines existed and were removed on a
      founder note (2026-07-21). The five-line version still described in
      design/grid-audit.md predates that removal, so building it here would be
      re-introducing something already rejected on the site being copied.

   2. ONE pseudo-element painting BOTH lines, via two background gradients rather than
      ::before plus ::after. That leaves ::after free, and - the real reason - it means
      there is exactly ONE place to switch the grid off. Their notes record a second
      implementation of the same verticals that the mobile rule did not know about, so
      six product pages drew stray hairlines at phone width "for as long as the rule has
      existed", and a grep for the class name found none of it.

   3. z-index: -1 inside a stacking context. The lines paint ABOVE the band's background
      and BEHIND its content, so text is never sliced by a rule passing through it.

   4. NO HORIZONTALS on full-width bands. Adjacent bands divide by their own edge; laying
      a hairline over that boundary reads as a doubled border. Our bands do not even
      change background, so the vertical frame is the whole grid.

   NO GRID BELOW 768px (founder, 2026-07-22). The column stacks, the gutter drops to 24px,
   and the verticals end up hard against the screen edges where they read as two stray
   hairlines rather than as a frame. ---- */
.band { position: relative; z-index: 0; }
.band::before {
  content: "";
  position: absolute; z-index: -1;
  top: 0; bottom: 0;
  left:  calc(max((100% - var(--shell)) / 2, 0px) + var(--gutter));
  right: calc(max((100% - var(--shell)) / 2, 0px) + var(--gutter));
  pointer-events: none;
  background-image:
    linear-gradient(var(--line), var(--line)),
    linear-gradient(var(--line), var(--line));
  background-repeat: no-repeat;
  background-size: 1px 100%;
  background-position: 0 0, 100% 0;
}
@media (max-width: 768px) { .band::before { display: none; } }

/* ---- Type ---- */
h1, h2, h3 { margin: 0; font-weight: 300; color: var(--ink); text-wrap: balance; }
h1 { font-size: var(--t-display); line-height: 1.05; letter-spacing: -0.022em; max-width: 17ch; }
h2 { font-size: var(--t-h2);      line-height: 1.08; letter-spacing: -0.018em; max-width: 22ch; }
h3 { font-size: var(--t-h3);      line-height: 1.25; letter-spacing: -0.01em; }
p  { margin: 0; max-width: var(--measure); }
.lead  { font-size: var(--t-lead); color: var(--ink-2); line-height: 1.5; max-width: 52ch; }
.fine  { font-size: var(--t-fine); color: var(--ink-3); line-height: 1.6; }
strong { font-weight: 500; color: var(--ink); }
.stack   { display: flex; flex-direction: column; gap: 1.25rem; }
.stack-l { display: flex; flex-direction: column; gap: clamp(2.5rem, 4vw, 3.5rem); }
.stack-s { display: flex; flex-direction: column; gap: 0.6rem; }

/* An eyebrow, which the single-page version banned as a NUMBERED section rail.
   This is a category label, not a chapter number, and on a site with eight
   sections per page the reader needs to know which one they are in. */
.eyebrow {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-3);
}

/* ---- Buttons. Pills, per the reference. Ours are larger: theirs run a 13.9px
   label, which is a self-serve "Start for free" button. This page's action is
   "talk to us about your security review", and that does not belong on a 14px
   control. ---- */
.btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font: inherit; font-size: var(--t-fine); font-weight: 500;
  text-decoration: none; white-space: nowrap; cursor: pointer;
  padding: 0.8rem 1.4rem; border-radius: var(--pill);
  background: var(--ink); color: var(--ground); border: 1px solid var(--ink);
  transition: background .18s ease, border-color .18s ease, color .18s ease;
}
.btn:hover { background: #FFFFFF; border-color: #FFFFFF; }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-btn); }
.btn-ghost:hover { background: rgba(233, 235, 223, 0.06); border-color: var(--ink); }
.row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }

/* ---- Nav ---- */
.nav {
  position: sticky; top: 0; z-index: 50;
  background: rgba(21, 21, 21, 0.86);
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--line);
}
.nav-in { display: flex; align-items: center; gap: 1.5rem; min-height: 64px; }
/* ---- The brand lockup.

   The real asset, `logo-text.webp`, is the procept.io lockup in BRAND RED. On this
   ground that is both low-contrast and off-palette - red is reserved here for
   refusal, and the whole point of this direction was to stop leading with it. So it
   is rendered monochrome: brightness(0) crushes it to black, invert(1) lifts it to
   white, and the opacity step tints it toward --ink rather than leaving it a harder
   #FFF than any text on the page.

   THE MARK STANDS ALONE, 2026-08-10. It briefly carried a hairline and an "AI" chip
   after it, to say which product this is. It read as a badge stuck onto somebody
   else's logo rather than as a lockup, which is what a divider plus two floating
   letters usually does. The domain, the page titles and the copy all carry the .AI;
   the bar does not have to. ---- */
/* `color` here is not redundant. The anchor had text-decoration removed but no
   colour, so it kept the user agent's default link blue (rgb(0,0,238)) - invisible
   today only because every child sets its own colour, and one word added directly
   to the anchor would have shipped a blue link on a near-black bar. */
.nav-brand { display: inline-flex; align-items: center; gap: 0.6rem; text-decoration: none; color: var(--ink); }
/* THE -6px IS NOT A FUDGE, 2026-08-11. `align-items: center` centres the image BOX,
   and the box is not what a reader sees. The asset is 156x40 with its ink from y4 to
   y37: the ring stops at the baseline (y33) and the bottom four rows are descender
   room for the mark's stem and the `p`. So box-centring hangs the lockup on its
   descenders and drops the visible baseline. At the old 21px it put the wordmark at
   y39.5 against the menu's y36.5 - a 3px drop under 14px text, which is exactly the
   size that reads as "the logo is sitting wrong" without being obvious why.

   30px, so the wordmark's x-height (9.7px) finally matches the menu's cap height
   (11px) instead of sitting under it at 6.5px, and the 40px source downscales 75%
   rather than 52.5%.

   -6px puts the baseline on y36.1 against the menu's y36.25: aligned to within a
   sixth of a pixel. THE COST, because the bigger the lockup the sharper this trade
   gets: the ring now tops out at y14 in a 64px bar, so the lockup carries 14px of
   air above and 24.5px below and sits high of the bar's centre by design. Baseline
   won over box-centring on a founder call. Whole pixels only - a half-pixel offset
   resamples the raster and softens the ring at 1x.

   Scoped to `.nav-brand` on purpose. The footer runs the same asset with nothing
   beside it on its line, so there is no baseline to match and nothing to correct.

   If the asset is ever replaced or the height changes, RE-MEASURE. -6px describes
   this file's padding at this size, not a general truth about logos. */
.nav-brand img {
  height: 30px; width: auto;
  position: relative; top: -6px;
  filter: brightness(0) invert(1);
  opacity: 0.92;
}
.nav-links { display: flex; gap: 1.5rem; margin-left: 1.25rem; }
.nav-links a {
  color: var(--ink-2); text-decoration: none; font-size: var(--t-fine);
  transition: color .16s ease;
}
/* `[aria-current]` unqualified, NOT `[aria-current="page"]`. The build emits "page"
   on the exact page and "true" on the other pages that item owns, and both are the
   answer to "where am I". Pinned to the exact token, the six pages that are owned
   but not linked directly - /platform/limits, /security/whose-permissions, the two
   under Learn, the two under Start - would carry the attribute for a screen reader
   and show nothing at all to everybody else. */
.nav-links a:hover, .nav-links a[aria-current] { color: var(--ink); }
.nav-cta { margin-left: auto; display: flex; gap: 0.6rem; align-items: center; }
/* MEASURED when "Use cases" made it six items. `.nav-links` is a flex row that does
   not wrap, so it does not overflow when it runs out of room - the ITEMS shrink and
   their labels break internally. At 900px "Use cases" split across two lines and the
   bar grew to two rows, which reads as a broken menu rather than a tight one, and
   nothing overlapped so no overflow check would have caught it. `nowrap` makes the
   failure a measurable overlap instead of a silent restyle, and the tighter gap
   between 1024 and the 860 cutoff is what buys the six labels their single line. */
.nav-links a { white-space: nowrap; }
@media (max-width: 1024px) { .nav-links { gap: 1rem; margin-left: 0.75rem; } }
@media (max-width: 860px) { .nav-links { display: none; } }
/* Below 640 the bar carries the wordmark and ONE action. Two pills plus the
   wordmark measured 429px against a 375px viewport, so "Talk to us" was cut off at
   the right edge - the bar looked deliberate and the primary action was the part
   that had gone. The secondary is dropped rather than shrunk, because both
   shrinking to fit would leave two 11px pills and no primary. */
@media (max-width: 640px) {
  .nav-cta .btn-ghost { display: none; }
  .nav-cta .btn { padding: 0.6rem 1rem; }
  .nav-in { gap: 0.75rem; }
}

/* ---- Hero. Centred, per the reference. ---- */
.hero > .shell { padding-block: clamp(4.5rem, 9vw, 8rem) clamp(3rem, 5vw, 4.5rem); }
.hero-in { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 1.75rem; }
.hero-in h1 { max-width: 18ch; }
.hero-in .lead { max-width: 56ch; }

/* The reference sits its hero on a large rounded media panel. Ours is a real
   artefact rather than a product still: an audit record, which is the thing this
   product actually produces. Same framing, honest content. */
.panel {
  width: 100%; margin-top: 1.5rem;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 14px; overflow: hidden;
  /* The hero centres its children. A record is data in columns and must not inherit
     that - centred table cells read as a broken layout, and every value in the panel
     drifted to the middle of its column before this line existed. */
  text-align: left;
}
.panel-hd {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.7rem 1.1rem; border-bottom: 1px solid var(--line);
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3);
}
.panel-body { overflow-x: auto; }

/* ============================================================================
   SCENES.

   Ported from proceptio.com's `.ms-scene` (Components/Sections/AISection.razor):
   a depicted product surface, a connector rail with a node, and the assistant on the
   other side. Theirs is DRAWN IN HTML AND CSS rather than being a screenshot, and
   that is the property worth copying - it reflows to one column on a phone, the text
   inside it is real text a screen reader can read, and it never goes stale the way a
   screenshot of a UI does.

   Everything here is invented data, and the scene carries `illustrative` where it is
   visible. The tool names, reason codes and status words are the real ones.
   ========================================================================== */
.scene {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 0.82fr);
  align-items: center;
  gap: 0;
  width: 100%;
  text-align: left;
}
@media (max-width: 900px) {
  .scene { grid-template-columns: minmax(0, 1fr); }
}

/* The depicted surface. A title bar with its three dots, a body, and a status foot -
   the vocabulary everybody reads as "an application window" without being told. */
/* A COLUMN, so the foot sits on the floor of the window rather than directly under
   the last row. Inside `.pair` the two windows are stretched to a common height and
   one of them always has fewer rows than the other; with the foot following the
   content, that window ended short and the pair read as two unrelated cards. A
   status bar pinned to the bottom edge is what makes both read as windows. */
.win {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; overflow: hidden; min-width: 0;
  display: flex; flex-direction: column;
}
.win > .win-foot { margin-top: auto; }
.win-bar {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.6rem 0.85rem; border-bottom: 1px solid var(--line);
  font-size: var(--t-micro); color: var(--ink-3);
}
.win-bar i { width: 8px; height: 8px; border-radius: 50%; background: var(--line-2); flex: none; }
.win-bar span { margin-left: 0.35rem; color: var(--ink-2); font-weight: 400; }
.win-bar em { margin-left: auto; font-style: normal; font-family: var(--font-mono); }
.win-tbl { width: 100%; border-collapse: collapse; font-size: var(--t-fine); }
.win-tbl th {
  text-align: left; font-weight: 400; color: var(--ink-3);
  font-size: var(--t-micro); letter-spacing: 0.08em; text-transform: uppercase;
  padding: 0.6rem 0.85rem 0.3rem;
}
.win-tbl td { padding: 0.4rem 0.85rem; color: var(--ink-2); border-top: 1px solid var(--line); }
.win-tbl .num { text-align: right; font-family: var(--font-mono); font-size: var(--t-mono); color: var(--ink); }
/* WRAPS, because the same foot now appears in a full-width window and in a ~275px
   column of a `.pair`. Unwrapped, the narrow one broke mid-phrase into a two-line
   stack per side ("your / tenant" beside "your joiner and leaver / process") and the
   status bar read as a paragraph. Wrapping lets the second half drop to its own
   line, which is the only way two strings and one row can both be true. */
.win-foot {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 0.2rem 1rem;
  padding: 0.6rem 0.85rem; border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: var(--t-micro); color: var(--ink-3);
}
.win-foot .conn { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--ink-2); }
.win-foot .conn i { width: 6px; height: 6px; border-radius: 50%; background: var(--signal-yes); }

/* Status pills, and the ONE documented exception to the colour law.

   "Overdue" and "Paid" are the depicted system's own statuses, not access decisions,
   so strictly they are not what --signal-no and --signal-yes mean. They are allowed
   here because they sit INSIDE `.win`, which depicts somebody else's application -
   and proceptio.com carries exactly this boundary as a law: an accent may paint the
   product's own screen, never the site's chrome.

   The line to hold: colour inside a depicted window belongs to that window. The
   moment one of these pills appears outside `.win`, it is decoration and the law is
   broken - because then a coloured word on this site no longer reliably means a
   permission was granted or refused. */
.pill {
  display: inline-block; padding: 0.05rem 0.5rem; border-radius: var(--pill);
  font-size: var(--t-micro); font-family: var(--font-mono);
  border: 1px solid currentColor;
}
.pill-no  { color: var(--signal-no); }
.pill-ok  { color: var(--signal-yes); }
.pill-due { color: var(--accent); }

/* The rail: one hairline with a node on it, horizontal on desktop and rotated to
   vertical when the scene stacks. The node is the thing the whole product is. */
.rail-x { position: relative; display: flex; align-items: center; justify-content: center; width: 84px; }
.rail-x::before { content: ""; position: absolute; left: 0; right: 0; height: 1px; background: var(--line-2); }
.rail-node {
  position: relative; width: 30px; height: 30px; display: grid; place-items: center;
  background: var(--ground); border: 1px solid var(--line-2);
  transform: rotate(45deg); color: var(--accent);
}
.rail-node svg { width: 14px; height: 14px; transform: rotate(-45deg); fill: currentColor; }
@media (max-width: 900px) {
  .rail-x { width: 100%; height: 64px; }
  .rail-x::before { left: 50%; right: auto; top: 0; bottom: 0; width: 1px; height: auto; }
}

/* The assistant side. Deliberately NOT branded as any one product: the page says
   "the assistant your people already use", so drawing one vendor's window would
   contradict the sentence above it. */
.asst {
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 10px; padding: 0.85rem; min-width: 0;
  display: flex; flex-direction: column; gap: 0.7rem;
}
/* The transcript takes the slack and the composer sits on the floor, for the same
   reason `.win-foot` does. Stretched inside a `.pair` with only two lines of
   conversation, the assistant was a tall empty box with a caption at the top - and
   an empty assistant beside a full database window reads as the thing having failed
   to answer, which is the exact opposite of this panel's sentence. */
.asst > .convo { flex: 1; }
.asst-status {
  display: flex; align-items: center; gap: 0.45rem;
  font-size: var(--t-fine); color: var(--ink-2);
}
.asst-status i { width: 6px; height: 6px; border-radius: 50%; background: var(--signal-yes); flex: none; }
.asst-composer {
  border: 1px solid var(--line-2); border-radius: 8px; padding: 0.6rem 0.7rem;
  display: flex; flex-direction: column; gap: 0.9rem;
}
.asst-composer .ph { font-size: var(--t-fine); color: var(--ink-3); }
.asst-composer .r { display: flex; align-items: center; justify-content: space-between; color: var(--ink-3); }
.asst-composer .send {
  width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center;
  background: var(--ink); color: var(--ground); font-size: 0.75rem;
}

/* Tool chips: the unit a grant hands out or withholds, so the page can SHOW a set
   being narrowed instead of describing it. */
.chips { display: flex; flex-wrap: wrap; gap: 0.35rem; padding: 0.8rem 0.85rem; }
.chip {
  font-family: var(--font-mono); font-size: var(--t-micro);
  border: 1px solid var(--line-2); border-radius: 5px;
  padding: 0.18rem 0.5rem; color: var(--ink-2); white-space: nowrap;
}
.chip-on {
  color: var(--accent); border-color: rgba(232, 167, 101, 0.42);
  background: rgba(232, 167, 101, 0.08);
}
/* Withheld, not broken. Struck through rather than hidden, because the point of the
   picture is that you can see what was taken away. */
.chip-off { color: var(--ink-3); border-style: dashed; text-decoration: line-through; }

/* The narrowing funnel. Four gates, each with what survives it - the one idea on the
   site that is genuinely easier to see than to read. */
.gates { display: flex; flex-direction: column; }
/* THE BAR TAKES THE FLEXIBLE COLUMN, not a fixed 5.5rem, and the label is capped
   instead. At 88px the five category segments were 12 to 33 pixels each - present in
   the markup, invisible on the screen, which is the worst of both. At ~370px the
   narrowing is legible as a picture, which is the only reason to draw it rather than
   print the five numbers. */
.gate {
  display: grid; grid-template-columns: 1.5rem 11rem minmax(0, 1fr) 2rem;
  gap: 0 0.9rem; align-items: center;
  padding: 0.6rem 0.85rem; border-top: 1px solid var(--line);
  font-size: var(--t-fine); color: var(--ink-2);
}
.gate:first-child { border-top: 0; }
.gate .g-n { font-family: var(--font-mono); font-size: var(--t-micro); color: var(--ink-3); }
.gate .g-c { font-family: var(--font-mono); font-size: var(--t-mono); color: var(--ink); text-align: right; }
/* THE BAR IS SEGMENTED BY CATEGORY, in the five hues panel 2 establishes, and the
   segments shrink and disappear as the gates remove them. This is the one place the
   funnel stops being five numbers: you watch CRM, tasks and the handbook vanish at
   her grant, planning and documents shorten at what the administrator switched on,
   and documents go entirely when nothing is connected.

   IT IS NOT THE THING THE NOTE BELOW FORBIDS. That defect was colouring gates by
   OUTCOME - the last two bars coral and green - which said nothing, because both of
   those gates remove. Here hue means WHICH CATEGORY, the same meaning it carries two
   panels earlier, and no segment is ever a signal colour: there is no artwork green
   and the rose is a magenta. A reader who has learned green-means-permitted is never
   shown a green bar to misread. */
.gate-bar { height: 7px; background: var(--line); border-radius: 4px; overflow: hidden; display: flex; }
.gate-bar i { display: block; height: 100%; transition: width .4s var(--ease-soft-out); }
.gate-bar .s-blue   { background: var(--art-blue); }
.gate-bar .s-violet { background: var(--art-violet); }
.gate-bar .s-teal   { background: var(--art-teal); }
.gate-bar .s-peach  { background: var(--art-peach); }
.gate-bar .s-cyan   { background: var(--art-cyan); }
/* Widths live here rather than on the elements: an inline style= fails the build,
   and the sequence is fixed, so nth-child is the honest place for it.

   EVERY WIDTH IS A COUNT OVER 21, so the segments are readable as quantities rather
   than as a shape somebody liked. The roster is planning 8, documents 3, CRM 3,
   tasks 3, handbook 4:

     row      planning  docs  crm  tasks  handbook   total
     server      8       3     3     3       4         21
     01 grant    6       3     -     -       -          9    her skills are planning_read, docs_read
     02 own      6       3     -     -       -          9    nothing here is another client's
     03 on       4       2     -     -       -          6    three not switched on for this project
     04 conn     4       -     -     -       -          4    no document system connected

   The last row is why panel 5 shows `docs_search` refused with `no_connection`: the
   two pictures are the same five numbers, and if one is edited the other has to
   move with it. */
.gates .gate:nth-child(1) .gate-bar .s-blue   { width: 38.1%; }
.gates .gate:nth-child(1) .gate-bar .s-violet { width: 14.3%; }
.gates .gate:nth-child(1) .gate-bar .s-teal   { width: 14.3%; }
.gates .gate:nth-child(1) .gate-bar .s-peach  { width: 14.3%; }
.gates .gate:nth-child(1) .gate-bar .s-cyan   { width: 19.0%; }

.gates .gate:nth-child(2) .gate-bar .s-blue   { width: 28.6%; }
.gates .gate:nth-child(2) .gate-bar .s-violet { width: 14.3%; }
.gates .gate:nth-child(2) .gate-bar .s-teal,
.gates .gate:nth-child(2) .gate-bar .s-peach,
.gates .gate:nth-child(2) .gate-bar .s-cyan   { width: 0; }

.gates .gate:nth-child(3) .gate-bar .s-blue   { width: 28.6%; }
.gates .gate:nth-child(3) .gate-bar .s-violet { width: 14.3%; }
.gates .gate:nth-child(3) .gate-bar .s-teal,
.gates .gate:nth-child(3) .gate-bar .s-peach,
.gates .gate:nth-child(3) .gate-bar .s-cyan   { width: 0; }

.gates .gate:nth-child(4) .gate-bar .s-blue   { width: 19.0%; }
.gates .gate:nth-child(4) .gate-bar .s-violet { width: 9.5%; }
.gates .gate:nth-child(4) .gate-bar .s-teal,
.gates .gate:nth-child(4) .gate-bar .s-peach,
.gates .gate:nth-child(4) .gate-bar .s-cyan   { width: 0; }

.gates .gate:nth-child(5) .gate-bar .s-blue   { width: 19.0%; }
.gates .gate:nth-child(5) .gate-bar .s-violet,
.gates .gate:nth-child(5) .gate-bar .s-teal,
.gates .gate:nth-child(5) .gate-bar .s-peach,
.gates .gate:nth-child(5) .gate-bar .s-cyan   { width: 0; }
.gate-out { font-family: var(--font-mono); font-size: var(--t-micro); color: var(--ink-3); }

/* A key/value face for small record cards. */
.kv { width: 100%; border-collapse: collapse; font-size: var(--t-fine); }
.kv td { padding: 0.42rem 0.85rem; border-top: 1px solid var(--line); color: var(--ink-2); }
.kv tr:first-child td { border-top: 0; }
/* The label column takes only what its longest label needs, rather than a fixed
   11rem. Fixed, it consumed most of a half-width panel inside `.pair` and shredded
   every value into a four-line stack ("the / moment / she / asks"). A width that is
   fine in one container and destroys the content in another is not a width, it is a
   guess about the container. */
.kv td:first-child { color: var(--ink-3); white-space: nowrap; width: 1%; padding-right: 1rem; }
.kv td:last-child { width: auto; }
.kv td.m { font-family: var(--font-mono); font-size: var(--t-mono); color: var(--ink); }

/* Two faces side by side with the rail between them, STRETCHED to a common height.
   Centred, they were 396px and 318px with their tops and bottoms both ragged, which
   reads as two cards that happen to be near each other rather than as two systems
   with a boundary between them - and the boundary is the entire point of the
   picture. The rail stays centred inside its own stretched track. */
.pair { display: grid; grid-template-columns: minmax(0,1fr) auto minmax(0,1fr); align-items: stretch; }
@media (max-width: 900px) { .pair { grid-template-columns: minmax(0,1fr); } }

/* A conversation exchange, for the "ask in plain language" beat. */
.convo { display: flex; flex-direction: column; gap: 0.6rem; }
.convo-q {
  align-self: flex-end; max-width: 82%;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 12px 12px 4px 12px; padding: 0.55rem 0.8rem;
  font-size: var(--t-fine); color: var(--ink);
}
.convo-tool {
  display: inline-flex; align-items: center; gap: 0.5rem; align-self: flex-start;
  font-family: var(--font-mono); font-size: var(--t-micro); color: var(--ink-3);
  border: 1px solid var(--line); border-radius: 6px; padding: 0.25rem 0.6rem;
}
/* A project slug is one token. Left to wrap it broke as "planning- / test",
   which reads as two different environments on a page whose argument is that an
   answer must name exactly which environment it came from. */
.convo-tool b { color: var(--accent); font-weight: 500; white-space: nowrap; }
.convo-a {
  align-self: flex-start; max-width: 92%;
  font-size: var(--t-fine); color: var(--ink-2); line-height: 1.5;
}
.convo-a strong { color: var(--ink); font-weight: 500; }

/* ---- Records. Mono lives here, because here it is data. ---- */
.rec { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: var(--t-mono); }
.rec th {
  text-align: left; font-weight: 400; color: var(--ink-3); white-space: nowrap;
  font-size: var(--t-micro); letter-spacing: 0.09em; text-transform: uppercase;
  padding: 0.75rem 1.1rem 0.35rem;
}
.rec td { padding: 0.32rem 1.1rem; color: var(--ink-2); white-space: nowrap; text-align: left; }
.rec tr:last-child td { padding-bottom: 0.9rem; }
.rec td.v { color: var(--ink); }
.code {
  font-family: var(--font-mono); color: var(--accent);
  background: rgba(232, 167, 101, 0.10);
  border: 1px solid rgba(232, 167, 101, 0.26);
  border-radius: 4px; padding: 0.05rem 0.4rem;
}

/* ============================================================================
   ARTWORK DETAIL - the parts that make a depicted window read as a real screen.

   ALL OF THIS EXISTS FOR ONE MEASURED REASON. The story panels' visuals occupied
   184-285px inside a 620px panel - 30% to 46% - so the panel was mostly void and
   the reference's is 100% filled by its product shot. Density was the missing
   half of "make the panels taller"; without it, raising the height only buys
   more emptiness.

   AND IT IS WHERE THE COLOUR GOES. Sampling the reference's five frames, the
   chromatic UI hits are each under 0.5% of frame - the lime bars, the orange
   bars, the syntax tokens. Small, saturated, and always on GEOMETRY: a bar, an
   avatar, a status dot, a legend swatch. Never on a word. That is what lets this
   page gain colour while a coloured WORD still means only one thing.
   ========================================================================== */

/* ---- Who, with a face. An avatar is the cheapest signal that a screen is about
   a person, and it is pure geometry so it may carry an artwork hue. Initials
   only: the no-invented-people rule means no surname ever appears. ---- */
.idcard {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: 0 0.7rem; align-items: center;
  padding: 0.8rem 0.85rem; border-bottom: 1px solid var(--line);
}
.avatar {
  grid-row: span 2; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  font-family: var(--font-mono); font-size: var(--t-micro); letter-spacing: 0.04em;
  color: var(--ink);
}
/* The tint is the hue at low alpha with a solid ring of the same hue, rather than
   a filled circle: a saturated 34px disc on this ground is the loudest thing on
   the page, which is not what a supporting detail should be. */
.av-blue { background: rgba(123, 167, 217, 0.20); border: 1px solid rgba(123, 167, 217, 0.55); }
.idcard-t { font-size: var(--t-fine); color: var(--ink); align-self: end; }
.idcard-s { font-size: var(--t-micro); color: var(--ink-3); align-self: start; }

/* A system badge in a window foot. Same shape as `.conn`, but its dot is an
   artwork hue rather than --signal-yes, because "which system" is not a verdict.
   `.conn`'s green dot stays exactly what it was: a permitted connection. */
.win-foot .sys { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--ink-2); }
.win-foot .sys i { width: 6px; height: 6px; border-radius: 50%; flex: none; }
.dot-blue   { background: var(--art-blue); }
.dot-violet { background: var(--art-violet); }
.dot-teal   { background: var(--art-teal); }
.dot-peach  { background: var(--art-peach); }
.dot-cyan   { background: var(--art-cyan); }
.dot-rose   { background: var(--art-rose); }

/* ---- Tools by category. Five groups, five hues, and the hue is on the MARKER
   and the METER - never on the tool name.

   The tempting version coloured the chips themselves, so a grant read as a
   coloured set. It was rejected: a chip is a word, the panel two screens below
   colours words green and red to mean permitted and refused, and six hues of
   word on one page is exactly how "a coloured word is a decision" stops being
   true. Offered-versus-withheld is carried where it always was - solid border and
   amber for offered, dashed and struck through for withheld - which survives
   greyscale, and the category is carried by a dot. ---- */
/* ONE LINE PER CATEGORY, and the tool names live in a single strip below rather
   than inside each row. Nested, the five categories each carried their own wrapped
   chip block and the panel measured 975px - 108vh, taller than the window it was
   supposed to sit inside, which is the opposite of the brief. Split, the shape of
   the grant (five meters) and its contents (one strip) are both legible and the
   panel fits. */
.cats { display: flex; flex-direction: column; }
.cat {
  display: grid; grid-template-columns: 0.6rem 6.5rem minmax(0, 1fr) 2.4rem;
  gap: 0.1rem 0.7rem; align-items: center;
  padding: 0.5rem 0.85rem; border-top: 1px solid var(--line);
}
.cat:first-child { border-top: 0; }
.cat-m { width: 7px; height: 7px; border-radius: 50%; }
.m-blue   { background: var(--art-blue); }
.m-violet { background: var(--art-violet); }
.m-teal   { background: var(--art-teal); }
.m-peach  { background: var(--art-peach); }
.m-cyan   { background: var(--art-cyan); }
.cat-l { font-size: var(--t-fine); color: var(--ink-2); }
.cat-meter { height: 5px; border-radius: 3px; background: var(--line); overflow: hidden; }
.cat-meter span { display: block; height: 100%; }
.b-blue   span { background: var(--art-blue); }
.b-violet span { background: var(--art-violet); }
.b-teal   span { background: var(--art-teal); }
.b-peach  span { background: var(--art-peach); }
.b-cyan   span { background: var(--art-cyan); }
/* Fill widths, in the same nth-child form the gate bars use and for the same
   reason: an inline style= fails the build and the sequence is fixed. Two of
   these are ZERO, and a zero-width fill is the honest picture - the category is
   listed, and none of it was granted. */
.cats .cat:nth-child(1) .cat-meter span { width: 75%; }
.cats .cat:nth-child(2) .cat-meter span { width: 100%; }
.cats .cat:nth-child(3) .cat-meter span { width: 0; }
.cats .cat:nth-child(4) .cat-meter span { width: 0; }
.cats .cat:nth-child(5) .cat-meter span { width: 0; }
.cat-c { font-family: var(--font-mono); font-size: var(--t-micro); color: var(--ink-3); text-align: right; }
.cat .chips { grid-column: 2 / -1; padding: 0.35rem 0 0; }
/* The overflow chip. "+1" rather than a name, because naming tools this server
   does not have would be a claim, and every claim on this site has to trace to
   something in the code. */
.chip-more { color: var(--ink-3); border-style: dashed; }

/* ---- Gates, with what each one removed. The subtitle is the density: four rows
   of "21, 9, 9, 6, 4" is a funnel nobody can act on, and the same four rows with
   "-12, CRM, tasks and the handbook are not skills she holds" is the argument.
   The bars stay NEUTRAL - see the note at .gate-bar, where colouring the last two
   was the status-colour-as-decoration defect. ---- */
/* The window's body takes the height instead of stacking at the top and leaving the
   lower third empty. Applies to the gate list and to the tables alike: a stretched
   window with its rows bunched under the title bar reads as a screen that failed to
   load, which is a worse impression than the short window it replaced.

   Scoped to `.story-panel`, because the same faces appear in the hero and on the
   inner pages at their natural height, where there is no slack to take. */
.win > .gates { flex: 1; }
.gates .gate { flex: 1; }
.story-panel .win > .kv,
.story-panel .win > .win-tbl { flex: 1; }
.gate { grid-template-rows: auto auto; align-content: center; }
.gate .g-t { font-size: var(--t-fine); }
.gate .g-d {
  grid-column: 2 / -1; font-size: var(--t-micro); color: var(--ink-3);
  font-family: var(--font-mono); padding-top: 0.15rem;
}

/* ---- A plot. The one place a real chart appears, and the densest colour on the
   page: two series of eight bars. Bars are geometry, so they carry artwork hues
   outright - this is the reference's own move (their lime and orange bar charts
   are the most saturated thing in any of their five frames).

   The legend's SWATCH is coloured and its LABEL is not, which is the whole rule
   in one element. ---- */
.plot { padding: 0.85rem 0.85rem 0.7rem; border-top: 1px solid var(--line); }
.plot-h {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  font-size: var(--t-micro); color: var(--ink-2); margin-bottom: 0.7rem;
}
.plot-u { color: var(--ink-3); font-family: var(--font-mono); }
.chart { display: flex; align-items: flex-end; gap: 0.5rem; height: 96px; }
/* Each column holds the two series as siblings, so a column is one period and the
   pair reads as this-week against last-week rather than as sixteen loose bars. */
.cb { flex: 1; display: flex; align-items: flex-end; gap: 2px; height: 100%; }
.cb i, .cb b { flex: 1; border-radius: 2px 2px 0 0; display: block; }
.cb i { background: var(--art-blue); }
.cb b { background: rgba(155, 139, 217, 0.55); }
.chart .cb:nth-child(1) i { height: 54%; } .chart .cb:nth-child(1) b { height: 52%; }
.chart .cb:nth-child(2) i { height: 61%; } .chart .cb:nth-child(2) b { height: 57%; }
.chart .cb:nth-child(3) i { height: 49%; } .chart .cb:nth-child(3) b { height: 50%; }
.chart .cb:nth-child(4) i { height: 68%; } .chart .cb:nth-child(4) b { height: 63%; }
.chart .cb:nth-child(5) i { height: 74%; } .chart .cb:nth-child(5) b { height: 66%; }
.chart .cb:nth-child(6) i { height: 66%; } .chart .cb:nth-child(6) b { height: 65%; }
.chart .cb:nth-child(7) i { height: 88%; } .chart .cb:nth-child(7) b { height: 80%; }
.chart .cb:nth-child(8) i { height: 96%; } .chart .cb:nth-child(8) b { height: 88%; }
.plot-k { display: flex; gap: 1rem; margin-top: 0.6rem; font-size: var(--t-micro); color: var(--ink-3); }
.plot-k span { display: inline-flex; align-items: center; gap: 0.4rem; }
.plot-k span::before { content: ""; width: 8px; height: 8px; border-radius: 2px; }
.k-blue::before   { background: var(--art-blue); }
.k-violet::before { background: rgba(155, 139, 217, 0.55); }

/* ---- Filter row. A console has controls, and drawing them is most of why a
   table reads as a screen somebody uses rather than as a specimen. Inert on
   purpose: there is no JavaScript on this site, so these are depicted, and the
   panel is labelled illustrative. ---- */
.filters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem;
  padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--line);
}
.filter {
  font-size: var(--t-micro); color: var(--ink-3);
  border: 1px solid var(--line); border-radius: var(--pill);
  padding: 0.12rem 0.55rem;
}
.filter-on { color: var(--ink-2); border-color: var(--line-2); background: var(--surface-2); }
.filter-sp { margin-left: auto; font-family: var(--font-mono); font-size: var(--t-micro); color: var(--ink-3); }

/* The hour's outcomes as one bar. This is the ONE chart on the site drawn in the
   signal colours, and it is the case they exist for: the segments are literally the
   allowed calls and the refused ones, so the green and the coral are carrying their
   own meaning rather than borrowing it. Everywhere else a chart takes an artwork hue
   precisely so this bar keeps saying something.

   It also earns its place as information: nine rows in a table do not add up in the
   reader's head, and "5 allowed, 4 refused" is the sentence the panel is making. */
.outcome {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.7rem 0.85rem; border-bottom: 1px solid var(--line);
}
.outcome-bar { flex: 1; display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--line); }
.outcome-bar i { display: block; height: 100%; }
.outcome-bar .o-yes { width: 55.6%; background: var(--signal-yes); }
.outcome-bar .o-no  { width: 44.4%; background: var(--signal-no); }
.outcome-k { font-family: var(--font-mono); font-size: var(--t-micro); color: var(--ink-3); white-space: nowrap; }
.outcome-k b { font-weight: 500; }

/* ---- The client rail.

   PORTED FROM proceptio.com's `.hero-rail` (Proceptio.Web/wwwroot/css/design-tokens.css
   6426-6508, HeroSection.razor 63-91), on a founder note 2026-08-10: "have it the same".
   Technique, geometry and timing are copied to the pixel. Two things are deliberately not:

     1. THE GROUND. Theirs is a maroon hero under a photograph, with a bottom-up
        `rgba(6,2,2,.55)` wash to hold the marks off the image. This page has no
        photograph and its ground was moved OFF proceptio.com red this morning (see the
        header of this file). The wash on a flat #151515 would only mud a strip darker
        than the band around it, so it is dropped and the band sits on our own ground.
     2. THE PLACEMENT. Theirs is the last line of a full-viewport hero, so its band is a
        natural terminus and the page's grid verticals STOP at it. Ours is mid-page,
        where a break in the frame would read as a mistake, so the verticals run on -
        they are at z-index -1, BEHIND the marks, so a logo occludes the hairline rather
        than being sliced by it. That is the same intent through a different stack.

   WHAT THE MOVE FIXES, and it is not decoration: the old strip was a wrapped flex grid
   forcing EVERY mark to 26px. Brand assets carry wildly different amounts of ink per unit
   of height, so one CSS height makes Baillie Gifford (a long thin wordmark) read at twice
   the optical weight of Heineken (a stacked mark). The reference solves it with a per-mark
   optical cap, 16-30px, and that table is the load-bearing part of this port. ---- */
.rail-label { text-align: center; color: var(--ink-3); font-size: var(--t-fine); }

.rail {
  position: relative; overflow: hidden;
  /* Fixed band height, not content height, so the strip is the same depth whatever
     the tallest mark in the roster happens to be. */
  height: var(--rail-h);
  border-top: 1px solid var(--line);
  /* Marks dissolve at the viewport edges instead of being cut by them. A mask, not
     two overlay divs: the band is transparent to the page ground, and a solid fade
     would have to know the ground colour to fake it. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 4%, #000 96%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 4%, #000 96%, transparent 100%);
}

/* The track carries the roster TWICE and slides exactly -50%, so the loop has no seam.
   --rail-secs is derived, not chosen: it is the reference's own formula, the summed drawn
   width of the set (each mark's cap x its aspect ratio, plus the 72px gap) held at
   ~32px/second. For this 22-mark roster that is 3291px -> 103s. ADDING A LOGO MEANS
   RE-DERIVING IT: leave the number alone and the rail silently speeds up instead. */
.rail-track {
  display: flex; height: 100%; width: max-content;
  animation: rail-slide var(--rail-secs) linear infinite;
  will-change: transform;
}

/* Each set carries its own trailing gap, so both halves are exactly the same width and
   -50% lands the clone precisely where the original started. */
.rail-set {
  display: flex; align-items: center; height: 100%;
  gap: var(--rail-gap); padding-right: var(--rail-gap);
}

.rail img {
  flex: none; width: auto;
  /* The files are already white ink on transparent, trimmed to the ink. This is belt and
     braces: any mark later dropped in from a brand kit is forced to the same polarity
     rather than arriving as a black rectangle nobody notices on a dark ground. */
  filter: brightness(0) invert(1);
  opacity: 0.55;
  transition: opacity .25s ease;
}
.rail img:hover { opacity: 1; }
.rail:hover .rail-track { animation-play-state: paused; }

@keyframes rail-slide {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* THE OPTICAL CAP TABLE, carried over value for value from `LogoSpec` in
   HeroSection.razor. The reference sets this inline per image; this build FAILS on an
   inline style= (see build.py), so it is keyed off the filename instead - which is the
   better home for it anyway, since it is a property of the artwork rather than of the
   page. A single-line wordmark caps at 16-22, a stacked or square mark at 26-30. */
.rail img[src$="/bg.webp"]                { height: 16px; }
.rail img[src$="/ingka.webp"]             { height: 17px; }
.rail img[src$="/lpkf.webp"]              { height: 17px; }
.rail img[src$="/epson.webp"]             { height: 18px; }
.rail img[src$="/techspace.webp"]         { height: 20px; }
.rail img[src$="/acterys.webp"]           { height: 22px; }
.rail img[src$="/ascendx.webp"]           { height: 22px; }
.rail img[src$="/dlm.webp"]               { height: 22px; }
.rail img[src$="/fcg.webp"]               { height: 24px; }
.rail img[src$="/gumiimpex.webp"]         { height: 24px; }
.rail img[src$="/first-samuel.webp"]      { height: 26px; }
.rail img[src$="/pacificag.webp"]         { height: 26px; }
.rail img[src$="/swo-netz.webp"]          { height: 26px; }
.rail img[src$="/whiskeyhouse.webp"]      { height: 26px; }
.rail img[src$="/mti.webp"]               { height: 28px; }
.rail img[src$="/ws.webp"]                { height: 28px; }
.rail img[src$="/bomark.webp"]            { height: 30px; }
.rail img[src$="/cofra.webp"]             { height: 30px; }
.rail img[src$="/cygnum.webp"]            { height: 30px; }
.rail img[src$="/evolvingsolutions.webp"] { height: 30px; }
.rail img[src$="/heineken.webp"]          { height: 30px; }
.rail img[src$="/roc.webp"]               { height: 30px; }

@media (max-width: 768px) {
  /* A 4% fade is only ~15px on a phone, which still reads as a hard cut. */
  .rail {
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 9%, #000 91%, transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0, #000 9%, #000 91%, transparent 100%);
  }
  /* The set is narrower at the tighter gap, so the same duration would slow the rail
     down. 0.7 holds the perceived speed, as on the reference. */
  .rail-track { animation-duration: calc(var(--rail-secs) * 0.7); }
}

/* A strip of logos crossing the viewport is exactly the motion this is for. Static, and
   it still reads: the mask does the edge, the roster just stops moving. */
@media (prefers-reduced-motion: reduce) {
  .rail-track { animation: none; }
}

/* ---- Cards ---- */
/* Explicit column counts, not auto-fit. `auto-fit` with a min width made .grid-2
   resolve to THREE columns at 1440, so a four-card group wrapped 3+1 and left a
   dead cell the width of a card. The class name is the contract: .grid-2 is two
   columns, .grid-3 is three, and they collapse at named widths. */
.grid { display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 1040px) { .grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 760px)  { .grid-3, .grid-2 { grid-template-columns: 1fr; } }
.card { background: var(--ground); padding: 1.75rem; display: flex; flex-direction: column; gap: 0.7rem; }
.card h3 { font-size: 1.125rem; font-weight: 400; }
.card p { font-size: var(--t-fine); color: var(--ink-2); }

/* ---- Numbered beats ---- */
.beats { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2rem; max-width: 54rem; }
.beat { display: grid; grid-template-columns: 2.25rem minmax(0, 1fr); gap: 0 1.1rem; }
.beat .i { font-family: var(--font-mono); font-size: var(--t-mono); color: var(--accent); padding-top: 0.35rem; }
.beat h3 { font-size: 1.125rem; font-weight: 400; margin-bottom: 0.4rem; }
.beat p { color: var(--ink-2); font-size: var(--t-fine); }
.beat .by { display: block; margin-top: 0.5rem; font-size: var(--t-fine); color: var(--ink-3); }

/* ---- Ledger table ---- */
.tbl-wrap { overflow-x: auto; max-width: 62rem; }
.tbl { width: 100%; border-collapse: collapse; text-align: left; font-size: var(--t-fine); min-width: 38rem; }
.tbl th {
  font-family: var(--font-mono); font-size: var(--t-micro); font-weight: 400;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3);
  padding: 0 1rem 0.7rem 0; border-bottom: 1px solid var(--line-2); vertical-align: bottom;
}
.tbl td { padding: 0.95rem 1rem 0.95rem 0; border-bottom: 1px solid var(--line); color: var(--ink-2); vertical-align: top; }
.tbl tr:last-child td { border-bottom: 0; }
.tbl td.k { color: var(--ink); white-space: nowrap; }
.tbl td.k span { display: block; font-size: var(--t-micro); color: var(--ink-3); margin-top: 0.15rem; }
.tbl td.m { font-family: var(--font-mono); font-size: var(--t-mono); white-space: nowrap; }

/* ---- Two-column ledger (held / not held) ---- */
.ledger { display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); gap: 2rem 3rem; max-width: 58rem; }
.ledger > div { display: flex; flex-direction: column; gap: 0.9rem; }
.ledger h3 { font-size: 1.0625rem; font-weight: 400; padding-bottom: 0.6rem; border-bottom: 1px solid var(--line-2); }
.ledger ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.ledger li { font-size: var(--t-fine); color: var(--ink-2); }
.ledger li b { font-weight: 500; color: var(--ink); }

/* ============================================================================
   THE STORY SCROLL.

   Ported from proceptio.com's `#ai` section (Components/Sections/AISection.razor):
   a STICKY left narrative listing the steps, against a right column of panels that
   scroll past it, so the reader keeps the whole shape in view while moving through
   the detail.

   THE REASON IT PORTS AT ALL: theirs is a sticky column plus a plain vertical stack
   of anchored panels, and its JavaScript only adds the active-step highlight - the
   markup's no-JS state is "every step undimmed", which is the baseline they
   deliberately designed for. So the structure needed no JavaScript to begin with,
   and this site has none.

   THE ACTIVE-STEP SYNC IS DONE IN CSS, with scroll-driven animations: each panel
   publishes a view-timeline, `timeline-scope` lifts those names to the common
   ancestor, and each step animates against its own panel's timeline. Where that is
   unsupported the @supports block never applies and every step sits at full opacity
   - which is exactly the reference's own fallback, not a degraded guess.

   It is also off below the two-column breakpoint. Sticky plus scroll-sync on a
   single narrow column means a step highlighting itself while its panel is nowhere
   near the screen.
   ========================================================================== */
/* 0.62/1.38, measured from the reference: their narrative column is 440px against a
   760px media column at 1440. A narrative column as wide as its media makes the
   media read as a diagram beside some text; making it clearly narrower is most of
   why theirs reads as a product shot with a caption. */
.story { display: grid; grid-template-columns: minmax(0, 0.73fr) minmax(0, 1.27fr); gap: 3.5rem; align-items: start; }
/* STICKY, AND CAPPED TO WHAT THE WINDOW CAN SHOW. A sticky element taller than the
   viewport pins at the top and its own bottom is then permanently out of reach - it
   never scrolls up, because sticky only offsets the top. Measured at the current copy:
   the column needs 767px at 1440 and 883px at 1001, because the descriptions run four
   lines wide and six lines narrow, so a 1024x768 window lost the last step and the
   link under it entirely. The cap costs nothing in the common case (no scrollbar
   appears while it fits) and turns the failure into an ordinary scroll when it does not.

   The padding/margin pair is not spacing: `overflow-y` computes overflow-x to `auto`
   as well, which would clip the hover underscore living at left: -0.85rem. The padding
   brings the marker inside the box and the negative margin puts the text back exactly
   where it was. */
.story-aside {
  position: sticky; top: calc(64px + 2.5rem);
  /* Exactly the space between where it pins and the bottom of the window, so the cap
     never bites earlier than it has to. At `- 4rem` it was 24px stingier than the room
     actually available and put a 16px scrollbar on a 1366x768 laptop, which fits. */
  max-height: calc(100vh - 64px - 2.5rem); overflow-y: auto;
  padding-left: 0.85rem; margin-left: -0.85rem;
}
.story-aside h2 { max-width: 15ch; }
.story-aside .lead { max-width: 38ch; margin-top: 1rem; }

/* The top margin, the lead's, and the step padding below are all one budget: the
   sticky column is capped at the window height, and at 761px it had 27px more content
   than room even after the ramp fix. Trimmed 36px out of the rhythm rather than
   shortening the copy or letting the cap bite, which buys ~9px of headroom at the
   shortest laptop height worth supporting. Re-measure this if the copy grows again. */
.story-steps { list-style: none; margin: 1.5rem 0 0; padding: 0; display: flex; flex-direction: column; }
.story-step { border-top: 1px solid var(--line); }
.story-step:last-child { border-bottom: 1px solid var(--line); }
/* Both lines sit on FULL --ink, and the title/description hierarchy is carried by
   size, not by a second colour. That is not a style preference, it is arithmetic:
   the dimming below is an `opacity`, and opacity MULTIPLIES a token's own alpha.
   With the title on --ink-2 (0.66) and the description on --ink-3 (0.55), a dimmed
   step measured 2.26:1 and 1.94:1 - both far under the floor, in a state the reader
   sees for most of the scroll. On full --ink the same dim measures 6.4:1.
   This is the "vary size or weight, never more transparency" rule applied to the one
   place where it is enforced by a multiplication rather than by taste. */
.story-step {
  position: relative;
  transition: opacity .25s ease;
}
/* HOVER LIGHTENS A DIMMED STEP, and `!important` is genuinely required rather than
   lazy: the dim is a running scroll-driven ANIMATION, and a CSS animation beats a
   normal author declaration in the cascade. Without it the hover rule computes and
   loses, silently, which looks exactly like a hover that was never written.
   Measured from retool.com, where the accordion items sit at full #E9EBDF and the
   dimming lives on the wrapper. */
.story-step:hover { opacity: 1 !important; }

/* The leading marker: Retool's terminal-cursor motif, an underscore that appears on
   the item you are pointing at. It occupies its own column at all times so nothing
   shifts sideways when it appears - a marker that reflows the row it marks reads as
   a bug. */
.story-step a::before {
  content: "_";
  position: absolute; left: -0.85rem; top: 0.85rem;
  font-family: var(--font-mono); color: var(--accent);
  opacity: 0; transition: opacity .25s ease;
}
.story-step:hover a::before { opacity: 1; }

.story-step a {
  display: grid; grid-template-columns: 2.5rem minmax(0, 1fr);
  gap: 0 0.75rem; padding: 0.75rem 0;
  text-decoration: none; color: var(--ink);
}
.story-step .n { font-family: var(--font-mono); font-size: var(--t-mono); color: var(--accent); }
.story-step .t { font-size: 1.0625rem; font-weight: 400; }
.story-step .d { grid-column: 2; font-size: 0.8125rem; margin-top: 0.2rem; overflow: hidden; }
/* Hovering a collapsed step opens it too, for the same reason the hover lightens
   it: a reader pointing at a step is asking what it is. Same !important reason as
   above - it is competing with a running animation. */
.story-step:hover .d { max-height: 9rem !important; opacity: 1 !important; margin-top: 0.2rem !important; }

.story-panels { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: clamp(4rem, 8vw, 7.5rem); }

/* Each panel sits on a soft ambient field, which is the thing that makes the
   reference's media read as PRESENT rather than as a table on a page. Retool does it
   with coloured gradients (peach, blue-grey) behind rounded cards; ours is a single
   warm neutral wash, because a colour there would be decoration and this palette has
   exactly two colours that mean something. Set behind the content with z-index and
   wider than the panel, so it reads as light falling on the surface rather than as a
   second box. */
/* ALMOST FULL WINDOW HEIGHT, which is measured rather than chosen: the reference's
   panels are 760x760 inside a 900px viewport - 84vh, and square. Ours are 716 wide,
   so 86vh capped at 820px lands at 774px on a 900px viewport and holds a 1.08 ratio
   against that width. The cap matters on a tall monitor, where 86vh of a 1200px
   viewport would be a 1032px panel and the sticky narrative beside it would run out
   of steps to highlight.

   HEIGHT ALONE MADE IT WORSE, and that is the part worth keeping. At min-height 620
   the depicted surface inside measured 184-285px - between 30% and 46% of the panel -
   so the rest was already void and raising the floor only bought more of it. The
   reference's frame is 100% filled by its product shot. So the surfaces below grew
   with this change (more rows, a chart, a real result set) and the panel now sizes
   from `justify-content: space-between` with the visual stretched, not from a
   centred block floating in the middle of a tall box. */
.story-panel {
  scroll-margin-top: 6rem; position: relative; z-index: 0;
  min-height: min(86vh, 820px);
  display: flex; flex-direction: column;
  padding: 2.75rem 2.5rem;
  border-radius: 18px;
  border: 1px solid transparent;
  transition: transform .55s var(--ease-soft-out),
              border-color .55s var(--ease-soft-out),
              box-shadow .55s var(--ease-soft-out);
}
/* The visual takes the slack. `min-height: 0` because a flex child's automatic
   minimum is its content size, so without it a tall table refuses to shrink and
   pushes the panel past its own floor instead of fitting inside it. */
.story-panel > .panel-art { flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: center; }
/* The depicted surface takes the whole art area rather than sitting centred in it.
   Centred it filled 70-75% of the panel, which is the same void the height change
   was supposed to remove, only smaller; the reference's frame is filled to the edge.
   The windows absorb it gracefully because each one already pins its foot to its own
   floor, so the extra height lands as room inside the window instead of as a gap
   under it. */
.panel-art > * { flex: 1; min-height: 0; }
/* The hover: it lifts and its edge appears. Slow, on the soft curve, so it settles
   rather than snapping - the whole point of that easing. */
.story-panel:hover {
  transform: translateY(-6px);
  border-color: var(--line);
  box-shadow: 0 28px 70px rgba(0, 0, 0, 0.42);
}
@media (prefers-reduced-motion: reduce) { .story-panel { transition: none; } .story-panel:hover { transform: none; } }
/* The vh floor is a DESKTOP idea and is dropped here. On a phone the panel is a
   single stacked column and 86vh of an 844px viewport is 726px of mostly nothing;
   the reference drops its square frame on narrow screens for the same reason. */
@media (max-width: 900px) { .story-panel { min-height: 0; padding: 1.5rem 0; border-radius: 0; } }

/* The panels' tables at phone width. Narrowing the track alone would have handed the
   overflow to `.win`, whose `overflow: hidden` would then CLIP the reason codes -
   quieter than a scrollbar and worse, because the column the panel exists to show
   would simply be missing. So the cells give up their padding and the codes are
   allowed to break: everything stays on screen and readable. */
@media (max-width: 900px) {
  .win-tbl { font-size: var(--t-mono); }
  .win-tbl th, .win-tbl td { padding-left: 0.5rem; padding-right: 0.5rem; }
  .filter-sp { margin-left: 0; }
  .cat { grid-template-columns: 0.6rem 5.5rem minmax(0, 1fr) 2.2rem; }
  .plot { padding-left: 0.6rem; padding-right: 0.6rem; }

  /* THE FUNNEL BAR GETS ITS OWN ROW ON A PHONE. Sharing the row, the 11rem label
     column left it 21 pixels wide - five category segments inside 21px, which is a
     bar that exists only in the markup. Widening the bar for desktop had quietly
     taken the whole of the narrow layout's width; a fixed column is always a
     guess about the container it has not been tested in. */
  .gate { grid-template-columns: 1.5rem minmax(0, 1fr) 2rem; row-gap: 0.35rem; }
  .gate .g-n { grid-column: 1; grid-row: 1; }
  .gate .g-t { grid-column: 2; grid-row: 1; }
  .gate .g-c { grid-column: 3; grid-row: 1; }
  .gate .gate-bar { grid-column: 2 / -1; grid-row: 2; }
  .gate .g-d { grid-column: 2 / -1; grid-row: 3; }

  /* THE AUDIT TABLE STILL DID NOT FIT, and fixing the page overflow had quietly
     moved the problem rather than solved it: at 426px inside a 327px window,
     `.win`'s own `overflow: hidden` was clipping the REASON column clean off. A
     refusal with its reason cut away is the one thing this panel must never show,
     and it is invisible in a screenshot taken at desktop width.

     Two changes, in this order. The timestamp and the person go: they are the least
     load-bearing of the five, and the panel's sentence is tool-outcome-reason. Then
     the table becomes its own scroll container, so whatever still does not fit can
     be reached instead of cropped. `display: block` is what makes a table
     scrollable; the rows keep their alignment because they still lay out as an
     anonymous table inside it.

     THE CODES MUST NOT BREAK, and letting them was the wrong first fix. With
     `overflow-wrap: anywhere` the reason column stopped overflowing and started
     rendering `skill_not_enabled` as "ski / not_ / able" down three lines - a
     hyphen-free shredding of the one value that has to be read exactly, since it is
     the name the audit trail froze. Nowrap plus a scroll container keeps every code
     whole and reachable, which is the trade the reference makes with its own wide
     tables. */
     THE COLUMN-DROP IS SCOPED TO `.tbl-audit`, not to `.win-tbl`, because the fourth
     panel's result set is also a `.win-tbl` and its first two columns are Period and
     Plan - hiding those would have left "last week" and "move" with nothing to be
     a move FROM. A positional rule needs a selector that names the table it was
     reasoned about. */
  .story-panel .win-tbl { display: block; overflow-x: auto; }
  .story-panel .win-tbl th, .story-panel .win-tbl td { white-space: nowrap; }
  .story-panel .tbl-audit th:nth-child(-n+2),
  .story-panel .tbl-audit td:nth-child(-n+2) { display: none; }
}
.story-panel::before {
  content: ""; position: absolute; z-index: -1; inset: 0;
  border-radius: inherit;
  opacity: 0.9;
  transition: opacity .55s var(--ease-soft-out);
  pointer-events: none;
}
.story-panel:hover::before { opacity: 1; }
/* Alternating warm and cool grounds, the way the reference alternates peach and
   blue-grey behind consecutive states.

   THE WEIGHT IS MEASURED, not judged. Sampling the reference's own five frames, 3%
   to 14% of every frame is saturated pixels and the bulk of it is exactly this - the
   ambient field behind the product shot (peach #F0C0A8 at 2.8% of frame, blue-grey
   #A8C0D8 at 2.8%, lilac #D8C0D8 at 3.7%). The genuinely chromatic UI hits - the lime
   bars, the orange bars, the syntax tokens - are each under 0.5%. So the thing that
   makes that page read as colourful is the FIELD, not the widgets, and ours was
   drawn at 0.10 peak alpha on a #151515 ground, where it was very nearly invisible.
   Now 0.26 peak with a second and third stop, which measures in the same 4-6% band.

   TWO PANELS CHANGED HUE, and not for looks. Panel 2's second stop was rgba(229,128,
   106) and panel 5's first was rgba(134,200,163) - the literal values of --signal-no
   and --signal-yes. At 0.06 alpha nobody could see it. At this weight panel 2 would
   glow red and panel 5 green, which on this page reads as a verdict on the step. The
   fields now draw only from hues that mean nothing. */
.story-panel::before {
  background-repeat: no-repeat;
}
/* THE ALPHA IS NOT THE REFERENCE'S ALPHA, and copying theirs was the mistake worth
   recording. Their fields sit on a LIGHT ground, where a 0.10 wash is plainly
   coloured; the same wash on #151515 renders (45,56,66) against (21,21,21) - a real
   lift, and still so dark and so desaturated that the saturated-pixel metric used on
   their frames scores it ZERO. A wash on near-black has to be roughly three times
   the alpha to carry the same colour, so these run 0.34-0.40 at the peak with a wide
   soft falloff. Verified by sampling rendered pixels, not by eye: the peak now
   measures around (60,74,88), which is unmistakably blue while still 6:1 quieter
   than any text on top of it. */
/* EVERY STRONG STOP SITS BELOW 45%, and that is a contrast fix rather than a
   composition preference. With the peaks in the top corners the panel's own
   paragraph sat on the brightest part of them: measured, the background under that
   text reached (56,73,90) and --ink-2 body copy on it computed 4.38:1 - under the
   4.5 floor, on text that had been safely at 7.11:1 against the nominal ground. The
   token had not changed; the ground under it had. That is the third time on this
   product's pages that a contrast number has been trusted against a ground the
   element no longer sits on, so it is now a rule with a geometry: the head of a
   story panel is the top 30% and NOTHING above 0.10 alpha may be painted there.

   The broad third stop stays full-height because 0.09 alpha is safe under text -
   measured at 6.3:1 - and it is what keeps the whole panel faintly tinted rather
   than only its lower half.

   It also looks more like the reference, which was the point of the exercise: their
   field surrounds the product shot, not the caption above it. */
.story-panel:nth-child(1)::before { background-image: radial-gradient(78% 60% at 18% 62%, rgba(123, 167, 217, 0.40), transparent 72%), radial-gradient(66% 52% at 92% 90%, rgba(155, 139, 217, 0.26), transparent 72%), radial-gradient(100% 80% at 50% 55%, rgba(123, 167, 217, 0.09), transparent 74%); }
.story-panel:nth-child(2)::before { background-image: radial-gradient(78% 60% at 84% 60%, rgba(232, 167, 101, 0.36), transparent 72%), radial-gradient(66% 52% at 8% 92%, rgba(217, 123, 168, 0.25), transparent 72%), radial-gradient(100% 80% at 50% 55%, rgba(232, 167, 101, 0.08), transparent 74%); }
.story-panel:nth-child(3)::before { background-image: radial-gradient(80% 62% at 14% 58%, rgba(155, 139, 217, 0.38), transparent 72%), radial-gradient(66% 54% at 90% 92%, rgba(111, 182, 217, 0.26), transparent 72%), radial-gradient(100% 80% at 50% 55%, rgba(155, 139, 217, 0.09), transparent 74%); }
.story-panel:nth-child(4)::before { background-image: radial-gradient(78% 60% at 88% 58%, rgba(111, 191, 180, 0.38), transparent 72%), radial-gradient(66% 52% at 10% 90%, rgba(123, 167, 217, 0.27), transparent 72%), radial-gradient(100% 80% at 50% 55%, rgba(111, 191, 180, 0.09), transparent 74%); }
.story-panel:nth-child(5)::before { background-image: radial-gradient(78% 60% at 18% 60%, rgba(111, 182, 217, 0.36), transparent 72%), radial-gradient(66% 52% at 88% 92%, rgba(232, 167, 101, 0.26), transparent 72%), radial-gradient(100% 80% at 50% 55%, rgba(111, 182, 217, 0.09), transparent 74%); }
/* The depicted surfaces lift slightly off that field. This is the ONE shadow on the
   site and it is scoped to a depicted product surface, which is the same exception
   proceptio.com's own token file carves out - never site chrome, never a card. */
.story-panel .win, .story-panel .asst {
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.32);
}
.story-panel > h3 {
  display: flex; align-items: baseline; gap: 0.75rem;
  font-size: 1.25rem; font-weight: 400; margin-bottom: 1rem;
}
.story-panel > h3 .n { font-family: var(--font-mono); font-size: var(--t-mono); color: var(--accent); }
.story-panel p { font-size: var(--t-fine); color: var(--ink-2); margin-bottom: 1rem; max-width: 46ch; }

@media (max-width: 1000px) {
  /* minmax(0, 1fr), NOT 1fr, and the difference is a 272px horizontal scroll on a
     phone. A bare `1fr` is `minmax(auto, 1fr)`, and that auto minimum is the track's
     MIN-CONTENT width - so the five-column audit table, which cannot get narrower
     than about 620px, pushed the single column to 623px inside a 327px parent and
     took the whole page with it. The desktop rule above always had minmax(0, ...);
     this override was written as plain `1fr` and inherited none of it.
     The tell is worth remembering: `.story` measured 327px while its own computed
     grid-template-columns read 623.461px. When a track is wider than its container,
     it is the auto minimum, never the fr. */
  .story { grid-template-columns: minmax(0, 1fr); gap: 2.5rem; }
  /* The cap and its overflow belong to the sticky behaviour and have to be undone with
     it: left on a static column they would clip the heading against a short phone
     viewport and hand the reader a scrollbox inside the page. */
  .story-aside {
    position: static;
    max-height: none; overflow: visible;
    padding-left: 0; margin-left: 0;
  }
  .story-steps { display: none; }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) and (min-width: 1001px) {
    .story { timeline-scope: --s1, --s2, --s3, --s4, --s5; }
    .story-panel:nth-child(1) { view-timeline-name: --s1; }
    .story-panel:nth-child(2) { view-timeline-name: --s2; }
    .story-panel:nth-child(3) { view-timeline-name: --s3; }
    .story-panel:nth-child(4) { view-timeline-name: --s4; }
    .story-panel:nth-child(5) { view-timeline-name: --s5; }
    .story-step { animation: stepActive linear both; animation-range: cover 0% cover 100%; }
    .story-step:nth-child(1) { animation-timeline: --s1; }
    .story-step:nth-child(2) { animation-timeline: --s2; }
    .story-step:nth-child(3) { animation-timeline: --s3; }
    .story-step:nth-child(4) { animation-timeline: --s4; }
    .story-step:nth-child(5) { animation-timeline: --s5; }

    /* THE ACCORDION. An inactive step collapses to its title alone; only the step
       you are reading expands to show its line of detail. This is the single biggest
       difference between the reference's narrative column and a list of five items -
       theirs never shows more than one description at a time, so the column reads as
       one thing with a focus rather than as five equal things.

       The description animates on the SAME timeline as its step, so the two cannot
       disagree about which step is active.

       Base state is EXPANDED, and the collapse only exists inside this block: where
       scroll-driven animations are unsupported every description is simply visible,
       which is a readable list rather than five titles whose detail never appears. */
    .story-step .d { animation: stepReveal linear both; animation-range: cover 0% cover 100%; }
    .story-step:nth-child(1) .d { animation-timeline: --s1; }
    .story-step:nth-child(2) .d { animation-timeline: --s2; }
    .story-step:nth-child(3) .d { animation-timeline: --s3; }
    .story-step:nth-child(4) .d { animation-timeline: --s4; }
    .story-step:nth-child(5) .d { animation-timeline: --s5; }

    /* THE PANEL YOU ARE NOT READING SITS IN SHADOW; the one you are reading is lit.
       Founder note against retool.com 2026-08-11, where the inactive frame is visibly
       darkened and the active one is at full brightness. It is the strongest thing
       that page does with the scroll, and it costs nothing here because every panel
       already publishes a view timeline for the step highlight.

       IT IS SCOPED TO `.panel-art`, NOT THE PANEL, and that is the whole care in it.
       The reference's panels ARE images, so dimming the frame dims a picture. Ours
       carry an `h3` and a paragraph of real prose above the picture, and dimming
       those would be the file's oldest trap: the dim is an opacity, opacity
       MULTIPLIES a token's own alpha, and the same move on the story steps once
       measured 2.26:1. So the artwork dims and the words do not - which also keeps
       the reading order legible while you scroll past.

       The depicted surfaces are decorative by construction: each `.pair`/`.win` is
       `role="img"` with an aria-label, and everything inside is `aria-hidden`. The
       accessible name is unaffected by any of this. */
    .panel-art {
      animation: artFocus linear both; animation-range: cover 0% cover 100%;
      will-change: opacity, filter;
    }
    .story-panel:nth-child(1) .panel-art { animation-timeline: --s1; }
    .story-panel:nth-child(2) .panel-art { animation-timeline: --s2; }
    .story-panel:nth-child(3) .panel-art { animation-timeline: --s3; }
    .story-panel:nth-child(4) .panel-art { animation-timeline: --s4; }
    .story-panel:nth-child(5) .panel-art { animation-timeline: --s5; }
  }
}
/* THE RAMPS ARE SHARP BECAUSE THE SOFT ONES DID NOT READ. Drawn first at 35/65 like
   the step dimming, the effect measured 0.79 on the outgoing panel and 0.91 on the
   incoming one at a normal scroll position - both nearly lit, so the screen showed two
   similar panels rather than one in shadow and one in light. A panel is ~774px inside
   a ~900px window, so consecutive panels share the screen for most of their cover
   range and a long ramp means they are almost always BOTH mid-fade. The contrast the
   reference gets comes from being close to binary: dark until the panel is the
   subject, lit while it is, dark again after.

   Brightness does most of the work and the opacity is a small assist. A heavy opacity
   alone greys the artwork toward the ground; brightness keeps its colour relationships
   and simply turns the light down, which is what being in shadow actually looks like.

   Still an animation, not a step: 4% ramps are two or three frames of scroll, enough
   that it settles rather than snapping.

   THE LIT WINDOW IS 60% WIDE AND THAT NUMBER IS DERIVED, not chosen. Consecutive
   panels are offset within their own cover ranges by pitch / (viewport + panel), and
   at 1440x900 that is 889 / (900 + 774) = 53%. So a lit window NARROWER than 53%
   leaves a stretch where neither panel qualifies and the whole screen goes dark
   between steps - which is what 34-66% did: measured 0.50 on both panels at the
   hand-off, a worse result than the soft ramp it replaced. At 20-80% the incoming
   panel lights 7% before the outgoing one dims, so there is always exactly one lit
   and briefly two, never none.

   The corollary is a maintenance note: change the panel height or the gap between
   panels and this number moves. If the hand-off ever goes dark, re-derive it. */
@keyframes artFocus {
  0%, 16%   { opacity: 0.5; filter: brightness(0.45) saturate(0.8); }
  20%, 80%  { opacity: 1;   filter: brightness(1) saturate(1); }
  84%, 100% { opacity: 0.5; filter: brightness(0.45) saturate(0.8); }
}
/* max-height rather than height: each description is two sentences now, which is three
   lines wide and six lines narrow, so a fixed height would clip it at some viewport
   nobody tested. 9rem clears six lines at this size, measured at 1001px where the
   sticky column is at its narrowest before it disappears.

   The number has been raised twice as the copy grew (3.4rem -> 5.2rem -> 9rem), and
   the failure mode both times was the same: the cap is generous at 1440 and exactly
   one line short at 1001, so the clipping appears only on the narrow half of the
   range where nobody looks. MEASURE the last line at 1001 before changing the copy. */
/* THE RAMPS ARE SHORT ON PURPOSE, and that is what stops the sticky column growing
   a scrollbar (found 2026-08-11, reported from a 1242x761 window).

   The reveal is a MAX-HEIGHT animation, so an opening description takes real layout
   height in the column. Consecutive panels overlap in their cover ranges - panel N+1
   enters the viewport long before panel N leaves it - so with a 35% ramp on each side
   there is a long stretch where TWO descriptions are part-open and their heights add.
   Measured: the column settles at 656px with one open, and reached 766px mid-scroll
   with two part-open, against a cap of 657px at that window height. Hence 109px of
   overflow and a scrollbar, on scroll only, at short viewport heights only - which is
   exactly the kind of fault that never shows up on the machine it was built on.

   Ramps of 4% instead of 35% mean a description is essentially either open or closed,
   and the hand-off is brief enough that the two part-open heights never sum past one
   open one. The OPEN WINDOW is unchanged (30-70% against the old 35-65%), so a step
   stays lit for as long as its panel is being read; only the transition is sharp.

   The dimming in `stepActive` keeps its soft 35/65 ramps: that one animates opacity,
   which costs no layout height, and its slowness is the part that feels considered. */
@keyframes stepReveal {
  0%, 26%   { max-height: 0;    opacity: 0; margin-top: 0; }
  30%, 70%  { max-height: 9rem; opacity: 1; margin-top: 0.2rem; }
  74%, 100% { max-height: 0;    opacity: 0; margin-top: 0; }
}
/* The dim floor is 0.70, and every step of getting there was a measurement:
     0.42  ->  --ink text at 2.26:1   (fails badly)
     0.62  ->  --ink text at 6.4:1, but the AMBER step number at 4.1:1 (still fails)
     0.70  ->  --ink at 7.5:1, amber at 4.90:1   (passes)
   The second one is the instructive one: fixing the text left the smallest, most
   saturated element in the row still under the floor, because opacity multiplies
   EVERY colour in the subtree and the accent had the least headroom to start with.
   An inactive step is the section's table of contents; a reader arriving mid-scroll
   has to be able to read the steps they have not reached yet. */
@keyframes stepActive {
  0%, 100% { opacity: 0.70; }
  35%, 65% { opacity: 1; }
}

/* ---- CTA band ---- */
.cta { text-align: center; }
.cta .shell { display: flex; flex-direction: column; align-items: center; gap: 1.5rem; }
.cta h2 { max-width: 20ch; }

/* ---- Footer ---- */
.ft { background: var(--surface); border-top: 1px solid var(--line); }
/* One track per section of the site, with the brand lockup on its own row above them.
   It used to sit BESIDE them as a 1.6fr first track, which held at four link columns
   and could not hold at five: `1.6fr repeat(5, 1fr)` drops each link column to about
   160px, and the measured wrap threshold is 151px for "AI access governance" and
   133px for "Security review pack" - so the fifth column would have arrived by
   breaking the other four. Giving the lockup its own row is the arrangement the
   sub-1200 breakpoint already used, so this generalises a proven layout rather than
   inventing one, and it leaves each link column at roughly 208px at full width.
   MEASURE the column widths again before adding a sixth. */
.ft-in { display: grid; grid-template-columns: repeat(5, 1fr); gap: 2.5rem; padding-block: 3.5rem 2rem; }
.ft-brand { grid-column: 1 / -1; }
.ft-col h4 { margin: 0 0 0.9rem; font-size: var(--t-micro); font-family: var(--font-mono);
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3); font-weight: 400; }
.ft-col a { display: block; color: var(--ink-2); text-decoration: none; font-size: var(--t-fine); padding: 0.22rem 0; }
.ft-col a:hover { color: var(--ink); }
/* Capped for the same reason `.ft-bottom p` is: below 1200 the lockup takes a full
   row of its own, and an uncapped paragraph there runs to about 130 characters. */
.ft-brand p { max-width: 46ch; }
.ft-bottom { border-top: 1px solid var(--line); padding-block: 1.5rem 2.5rem; display: flex;
  flex-wrap: wrap; gap: 1rem; justify-content: space-between; }
.ft-bottom p { font-size: var(--t-fine); color: var(--ink-3); max-width: 46ch; }
/* MEASURED, not guessed. Five tracks hold to 1280 and then start failing from the
   longest link inward: "AI access governance" wraps at a 151px column, "Security
   review pack" and "Whose permissions" at 133px, and by 860px every column but
   Platform's is on two lines - a footer that reads as five entries per column when
   it has three. The brand lockup takes its own row instead, which buys the four link
   columns 221px each at 1100 and keeps the wrap threshold below the phone
   breakpoint entirely. */
@media (max-width: 1200px) { .ft-in { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 820px) { .ft-in { grid-template-columns: 1fr 1fr; } }
@media (max-width: 520px) { .ft-in { grid-template-columns: 1fr; } }

/* ---- Motion. One beat, from an already-visible default, and gone entirely
   under reduced-motion. Nothing is hidden until JS shows it - there is no JS. ---- */
@media (prefers-reduced-motion: no-preference) {
  .rise { animation: rise .6s cubic-bezier(.16,1,.3,1) both; }
  .d1 { animation-delay: .04s; } .d2 { animation-delay: .12s; } .d3 { animation-delay: .22s; }
}
@keyframes rise { from { opacity: .3; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---- Prose links. There was NO rule for an inline <a> in body copy until the use-case
   pages needed several, so every prose link on the site was rendering in the user
   agent's default blue with its default underline. On a #151515 ground that measures
   about 2.6:1 - under the floor - and it had already shipped that way on the glossary,
   where four links sat in the definitions.

   This is the one link style that cannot be left to the browser, and the reason is the
   block directly below: the palette is colourless BY DESIGN so that a coloured word
   always means a status. An unstyled link drops a meaningless colour into the middle of
   a sentence and quietly breaks that rule everywhere it appears.

   Ink text with a hairline underline that brightens on hover, so the affordance is the
   underline rather than a hue. Selectors are wrapped in :where() to contribute ZERO
   specificity, which is what lets `.story-step a`, `.nav-links a` and `.ft-col a` keep
   winning without any of them being restated here. ---- */
:where(.band p, .ledger li, .tbl td, .win-tbl td) a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--line-2);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
:where(.band p, .ledger li, .tbl td, .win-tbl td) a:hover { text-decoration-color: var(--ink); }
/* A card heading that is a link is a title, not a reference, so it carries no
   underline at rest - the card is the click target and the underline on hover is the
   confirmation. Without this the three use-case cards read as a bulleted list of
   underlined blue phrases. */
.card h3 a { color: inherit; text-decoration: none; }
.card h3 a:hover { text-decoration: underline; text-decoration-color: var(--line-2); text-underline-offset: 0.18em; }

/* ============================================================================
   STATUS COLOUR. Last block in the file, and deliberately.

   These two classes carry the only meaning-bearing colour on the site: --signal-no
   is REFUSED and --signal-yes is PERMITTED. Nothing else may use them.

   They live at the end, at table-cell specificity, because they were written once
   as bare `.no` / `.yes` (0,1,0) and lost to `.rec td` and `.tbl td` (0,1,1). The
   result was `allow` and `deny` rendering in the SAME muted grey, measured at
   rgba(233,235,223,.66) for both - on a page whose entire argument is that one of
   those words means the system said no. The text was correct, the markup was
   correct, and the distinction the product exists to make was invisible.

   So: if a new table wrapper is added, its cells must not restate `color`, or add
   the wrapper here. A status colour that loses a specificity fight fails silently.

   IT FAILED SILENTLY AGAIN, and the instruction above is exactly what was not
   followed. `.win-tbl` and `.kv` both restate `color` on their cells, and neither
   was ever listed here - so the AUDIT PANEL, the one picture on this site whose
   whole job is to show an allow beside a deny, rendered both words at
   rgba(233,235,223,.66), and so did every yes/never/no in the identity panel. Found
   2026-08-11 by measuring the computed colour of those cells, not by reading the
   markup, which was correct throughout.

   WHY THE WARNING DID NOT PREVENT IT: this rule set is at the BOTTOM of the file and
   `.win-tbl` is defined 900 lines above it, so the person adding a table never reads
   this note. That is a property of the layout, not of their care, and it is the
   reason the list is now enumerated defensively rather than kept minimal. `.kv td.m`
   is (0,2,1), so beating it needs `.kv td.m.yes` at (0,3,1) - a bare `td.yes` would
   have lost that fight too, and lost it invisibly.
   ========================================================================== */
.yes, .no { font-weight: 500; }
.yes, .rec td.yes, .tbl td.yes, .win-tbl td.yes, .kv td.yes, .kv td.m.yes { color: var(--signal-yes); }
.no,  .rec td.no,  .tbl td.no,  .win-tbl td.no,  .kv td.no,  .kv td.m.no  { color: var(--signal-no); }

/* ---- Skip link ---- */
.skip {
  position: absolute; left: -9999px; top: 0; background: var(--ink); color: var(--ground);
  padding: 0.75rem 1.25rem; z-index: 100; text-decoration: none;
}
.skip:focus { left: 0; }
