/* ---------------------------------------------------------------------------
   Kustlyne Store — the design system.

   One accent, one neutral ramp, one spacing scale. The palette is the platform's
   own (navy and cyan), so the store and the machine look like the same product.

   No font *host*. The two typefaces are served from /static as part of this
   application, which keeps the page fast, works on a segregated network, and
   avoids the third-party request a font host would add to a site that publishes a
   security.txt. Before they were self-hosted the stylesheet asked for "Space
   Grotesk" and nothing ever loaded it, so the intended headings had silently been
   falling back for the whole build -- which is why every face here is declared
   with a real fallback stack behind it.
   --------------------------------------------------------------------------- */
/* The typefaces are the brand's own, self-hosted at /static. `swap` so text is readable
   while they load rather than invisible. */
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;
src:url(/static/fonts/inter-latin-400-normal.woff2) format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:500;font-display:swap;
src:url(/static/fonts/inter-latin-500-normal.woff2) format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:600;font-display:swap;
src:url(/static/fonts/inter-latin-600-normal.woff2) format('woff2')}
@font-face{font-family:'Inter';font-style:normal;font-weight:700;font-display:swap;
src:url(/static/fonts/inter-latin-700-normal.woff2) format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400;font-display:swap;
src:url(/static/fonts/jetbrains-mono-latin-400-normal.woff2) format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:500;font-display:swap;
src:url(/static/fonts/jetbrains-mono-latin-500-normal.woff2) format('woff2')}

:root{
  /* Kustlyne's own tokens, taken verbatim from the marketing site's brand system so the
     two halves of the product cannot drift apart again. Names are this file's; values
     are the brand's. */
  --navy:#0B2545; --navy-2:#123255; --navy-3:#1B3F66;
  --deep-ink:#071A2F;
  --edge:#22D3EE; --edge-deep:#0B8295; --edge-dark:#071A2F;
  --steel:#5B799D; --n300:#AFC0D2;
  /* Body and accent text, darkened a step from the brand hues so they clear AA on the
     slightly tinted surfaces below. This is the brand system's own correction and it is
     kept rather than re-derived. */
  --ink:#0B2545; --ink-2:#516E92; --muted:#516E92; --accent-text:#AE3A09;
  --line:#E1E7EF; --line-2:#EEF2F7;
  --bg:#ffffff; --bg2:#FBFCFE; --bg3:#F1F4F8;
  --good:#12883E; --good-bg:#e6f6ee; --good-line:#bfe6cf;
  --warn:#B36205; --warn-bg:#fdf1dd; --warn-line:#f0dcae;
  --bad:#DC2626;  --bad-bg:#fbe6e4;  --bad-line:#f0c4bf;
  /* The same correction the brand system makes for --muted and --accent-text, applied
     to the state hues: a badge is 0.7rem bold on a tinted chip and needs 4.5:1, which
     the display versions miss by a hair (4.03-4.07). These clear it -- and notices use
     them too, for the same reason. The originals are for large text and for the
     borders and marks that carry no words. */
  --good-text:#0f7434; --warn-text:#9d5605; --bad-text:#c42222;
  --code-bg:#071A2F;
  --sans:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
  /* Body grey rather than black, and a near-black that is not quite black. Measured
     off a site that has been selling for years: #1C1C1C text, #49494B leads,
     #7A7A7F secondary. Black-on-white at full contrast reads as a template. */
  --calm-ink:#1B1D21; --calm-lead:#4B4E54;
  /* #7C8087 is what the reference site uses and it lands at 3.97 on white and 3.63 on
     the tinted band -- below AA for text this size. Copying a soft palette is worth
     doing; copying its contrast defect is not, least of all on a site whose readers
     are looking at it in a plant office. #6B6F77 keeps the softness and clears 4.5 on
     both grounds. */
  --calm-mute:#5B5F67;
  --band-1:#FBFBFD; --band-2:#F3F5F9;
  --calm-line:#E9ECF1;
  /* The chart's one series. The site accent sits at chroma .089, which
     reads grey as a data mark -- the palette validator fails it for that.
     This is the same family, saturated enough to be a mark and still over
     3:1 on the light surface. */
  --series:#0893AE;
  /* The warm counterpart. Not a second brand colour competing with cyan for the same job:
     cyan is the machine's — signatures, data, the platform — and copper is the half of
     this business that involves people, which is training, partners and the company
     itself. A reader should be able to tell which half of the site they are in without
     reading the heading. Both clear 4.5 on their own grounds; `--copper-ink` is the text
     weight, `--copper` the mark weight. */
  /* One accent, since 28 September: Dutch orange, with navy (Rick: "use more the
     Dutch orange, as we are Dutch", after "we are using many different colours").
     Navy carries the words, the bar and the footer; orange marks what to press and
     what to notice; cyan is left to the logo and the product's own screens. The
     copper names stay so nothing that uses them breaks.
     Contrast: navy words on `--orange` are 5.5:1, so a button is orange with navy
     text -- white on this orange would be 3.3:1, too faint for a button's words.
     `--copper-ink` is the orange for text on white, 5.2:1. */
  --orange:#F97316; --orange-hover:#FB8A3C;
  --copper:#D9500A; --copper-ink:#AE3A09; --copper-soft:#EEF1F5;
  --copper-line:#DCE3EA; --copper-lit:#FB923C;
  /* The grounds are silver, not tinted (Rick, 28 September: "don't like the orange tint,
     make it silver, more tech"). Orange marks what to press; it never colours a band. */
  /* Inter for the headings as well as the text, since 29 September. Space Grotesk was the
     display face until then; Rick chose Inter from a side-by-side, after asking for a site
     where "you should not see it was made with vibe coding" -- and Space Grotesk is one
     of the faces generated sites reach for first. One family, set tighter and heavier
     for headings, reads as a company rather than a template.
     JetBrains Mono stays for real data: hashes, keys, versions, and the small technical
     labels inside the drawing. */
  --display:'Inter',-apple-system,"Segoe UI",system-ui,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,"SF Mono",Consolas,monospace;
  --s1:.25rem; --s2:.5rem; --s3:.75rem; --s4:1rem; --s5:1.5rem;
  --s6:2rem; --s7:3rem; --s8:4.5rem;
  /* The brand's radii: 16 for surfaces, 24 for large ones, and buttons are pills. */
  /* Radii collapse to a hairline chamfer. The 16/24 the brand sheet specifies is a
     consumer-software radius; on a page whose organising idea is a drawing sheet it is
     the single most generic thing on screen. */
  --r1:6px; --r2:10px; --r3:14px;

  /* Depth, as a scale rather than a default. Every one of these was `none` after the
     calm pass. They are used on the few things that should lift off the page -- a plan
     card, the drawing, a panel the eye should find first -- and nowhere else. */
  --lift-1:0 1px 2px rgba(11,37,69,.05), 0 2px 6px -1px rgba(11,37,69,.06);
  --lift-2:0 2px 4px rgba(11,37,69,.05), 0 8px 20px -6px rgba(11,37,69,.12);
  --lift-3:0 4px 8px rgba(11,37,69,.05), 0 20px 44px -12px rgba(11,37,69,.20);
  --lift-glow:0 10px 26px -10px rgba(34,211,238,.75),
              inset 0 1px 0 rgba(255,255,255,.45);

  /* The deep ground. Not one flat fill: three light sources over a vertical ramp, which
     is the difference between a dark section and a dark rectangle. */
  --deep-1:#0B2A50; --deep-2:#081A33; --deep-3:#06152A;
  --on-deep:#AFC6DE; --on-deep-soft:#8EA6C2; --edge-soft:#7FE6F7;
  /* No shadows. A drawing sheet has none, and the shadow is what made every panel
     read as a floating tile rather than a plate on a page. */
  --shadow-1:none;
  --shadow-2:none;
  --shadow-3:none;
}
*{box-sizing:border-box}
/* The full-bleed bands escape the container with `calc(50% - 50vw)`, and `vw` counts the
   scrollbar while the content box does not -- so every banded page scrolled sideways by
   the width of the scrollbar. Both elements, and `clip` rather than `hidden`, are load
   bearing; this was measured in a browser rather than reasoned about, because the rules
   for how overflow propagates from the body to the viewport are not intuitive:

     body:hidden   stops the scroll, but the body becomes the scroll container and the
                   sticky top bar scrolls away with the page
     html:clip     leaves the 8px scroll -- `clip` does not propagate the way `hidden`
                   does, so the viewport never hears about it
     both:clip     no sideways scroll, and the bar still sticks

   `overflow-x` alone, so a long table can still scroll inside its own container. */
html{-webkit-text-size-adjust:100%;overflow-x:clip;
/* The top bar is sticky and 60px tall, so a jump to `#what-is-included` put the heading
   underneath it and the reader arrived at what looked like the middle of a paragraph.
   A little more than the bar's own height, so the heading lands with air above it
   rather than welded to the bar. */
scroll-padding-top:5.5rem}
body{margin:0;font-family:var(--sans);color:var(--ink);background:var(--bg2);
font-size:1rem;line-height:1.55;-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;overflow-x:clip}
a{color:var(--accent-text);text-decoration:none}
a:hover{text-decoration:underline}
img{max-width:100%;height:auto}
:focus-visible{outline:2px solid var(--edge-deep);outline-offset:2px;border-radius:4px}
/* The same ring on a dark ground is a teal line on navy: 3.4:1, which passes and is not
   generous. On the bar, the two dark bands and the footer it gets the bright edge
   instead, which is 8.5:1 there and would be 1.8:1 on the page — hence two rules and not
   one colour. */
.topbar :focus-visible,.dark-calm :focus-visible,.plant-band :focus-visible,
.foot :focus-visible,.hero :focus-visible,.stage :focus-visible,
.skip:focus-visible{outline-color:var(--edge)}
/* `main` is focused by the skip link, and a focus ring round the whole page is noise:
   the point of moving focus there is that the next Tab starts from the content. */
main:focus-visible{outline:none}

/* The first thing in the document and the first thing a keyboard reaches. Off-screen
   until it is focused, because a visitor using a mouse has no use for it and a visitor
   on a keyboard has seven navigation links to get past on every page otherwise. */
.skip{position:absolute;left:-9999px;top:0;z-index:100;
background:var(--navy);color:#fff;padding:.7rem 1.1rem;border-radius:0 0 var(--r1) 0;
font-weight:600;text-decoration:none}
.skip:focus{left:0}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* --- type ---------------------------------------------------------------- */
h1,h2,h3,h4{font-family:var(--display);color:var(--navy);text-wrap:balance;
letter-spacing:-.01em;font-weight:600;margin:0}
h1{font-size:clamp(1.75rem,1.2rem + 1.7vw,2.35rem);line-height:1.14;font-weight:600;
margin:0 0 var(--s4)}
h2{font-size:clamp(1.3rem,1.1rem + .7vw,1.6rem);line-height:1.2;font-weight:600;
margin:var(--s7) 0 var(--s4)}
h3{font-size:1.1rem;line-height:1.3;font-weight:600;margin:var(--s5) 0 var(--s2)}
p{margin:0 0 var(--s4)}
p:last-child{margin-bottom:0}
.lead{font-size:1.115rem;line-height:1.6;color:var(--ink-2);max-width:68ch}
.muted{color:var(--muted)}
.small{font-size:.86rem}
.inline{display:inline-block;margin:0 var(--s1) 0 0}
/* `overflow-wrap:anywhere` on every mono run, because what this class holds is hashes,
   platform ids, app keys and licence keys -- tokens with nothing in them a browser may
   break at. One sha256 is sixty-four characters and 565 pixels wide whatever the screen
   is, so on a phone it took its table, its card or its paragraph off the side with it,
   and the value a reader came to copy was the part that went. `anywhere` rather than
   `break-word` so the element's minimum width shrinks too, which is what lets the table
   around it fit; it only breaks a token that would otherwise overflow, so a short app key
   reads exactly as it did. Code blocks are not this: `.code pre` scrolls instead, because
   a broken command line is a command line somebody pastes wrong. */
.mono{font-family:var(--mono);font-size:.92em;overflow-wrap:anywhere}

/* --- the bar ------------------------------------------------------------- */
/* Quiet chrome. The cyan register button was the loudest thing on a deliberately
   calm page. It becomes a solid navy, and the active tab a plain underline. */
.topright .btn-primary,.menu-foot .btn-primary{background:#fff;color:var(--navy);border-color:#fff}
.topright .btn-primary:hover,.menu-foot .btn-primary:hover{background:#EAF0F7;border-color:#EAF0F7}
.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:1.1rem;
height:60px;padding:0 1.3rem;background:var(--navy);color:#fff;
border-bottom:1px solid rgba(34,211,238,.35);box-shadow:0 1px 0 rgba(0,0,0,.2)}
.brand{display:flex;align-items:center;gap:10px;color:#fff;font-family:var(--display);
font-weight:600;font-size:1.12rem;letter-spacing:-.01em;line-height:1.15}
.brand .mark{width:32px;height:30px;flex:none}
.brand small{display:block;font-family:var(--sans);font-weight:400;font-size:.62rem;
letter-spacing:.08em;text-transform:uppercase;color:var(--n300);margin-top:1px}
.brand:hover{text-decoration:none}
.foot-brand .mark{width:28px;height:26px;flex:none}
/* min-width:0 is what stops a long navigation pushing the whole page wider than the
   window. A signed-in customer has nine links and staff have twelve; without it the bar
   wins the fight against the viewport and every page scrolls sideways. */
.topnav{display:flex;align-items:center;gap:.15rem;flex:1;min-width:0}
/* The drop-downs. A button in the bar, and a panel of links that each say what they are
   for. Open on hover and on keyboard focus with no script at all; the script at the
   bottom of the page adds tap-to-open for touch screens and Safari, which does not
   focus a button it was clicked on. */
.navgroup{position:relative;flex:none}
.navtop{appearance:none;background:none;border:0;color:#b9c8da;padding:.42rem .68rem;
border-radius:var(--r1);font:inherit;font-size:.9rem;font-weight:500;cursor:pointer;
display:flex;align-items:center;gap:.4rem;white-space:nowrap}
.navtop:hover,.navgroup:focus-within .navtop,.navgroup.open .navtop{
background:rgba(255,255,255,.09);color:#fff}
.navgroup.active .navtop{color:#fff;font-weight:600;box-shadow:inset 0 -2px 0 var(--orange)}
.caret{width:.4rem;height:.4rem;border-right:1.5px solid currentColor;
border-bottom:1.5px solid currentColor;transform:translateY(-2px) rotate(45deg)}
.navmenu{display:none;position:absolute;top:calc(100% + 10px);left:0;z-index:30;
min-width:18rem;padding:.4rem;background:#fff;border:1px solid #d7e1ec;
border-radius:var(--r2);box-shadow:0 14px 34px rgba(11,37,69,.22)}
/* The gap between the button and the panel would drop the hover on the way down. */
.navmenu::before{content:"";position:absolute;left:0;right:0;top:-12px;height:12px}
.navgroup:hover .navmenu,.navgroup:focus-within .navmenu,.navgroup.open .navmenu{
display:grid;gap:.1rem}
.topnav .navmenu a{display:block;padding:.55rem .75rem;border-radius:var(--r1);
color:var(--navy);white-space:normal;font-size:.92rem}
.topnav .navmenu a:hover,.topnav .navmenu a:focus-visible{background:#EEF4FA;
color:var(--navy);text-decoration:none}
.navmenu b{display:block;font-weight:600}
.navmenu small{display:block;color:#5b6b80;font-size:.8rem;font-weight:400;
margin-top:.1rem}
.topnav a{color:#b9c8da;padding:.42rem .68rem;border-radius:var(--r1);font-size:.9rem;
font-weight:500;white-space:nowrap;flex:none}
.topnav a:hover{background:rgba(255,255,255,.09);color:#fff;text-decoration:none}
/* One box-shadow, not two. Adding the underline without removing the ring left both
   declarations in the rule, and the later one simply won -- so the tab kept the cyan
   outline it was supposed to be losing. */
.topnav a.active{background:transparent;color:#fff;font-weight:600;
box-shadow:inset 0 -2px 0 var(--orange)}
.topright{display:flex;align-items:center;gap:.55rem}
.who{color:#b9c8da;font-size:.84rem;padding:.3rem .5rem;border-radius:var(--r1)}
.who:hover{background:rgba(255,255,255,.09);color:#fff;text-decoration:none}
.role{color:#8fa6bd;text-transform:uppercase;font-size:.66rem;letter-spacing:.11em;
margin-left:.3rem}
/* The menu control is a checkbox, so the navigation works with no JavaScript. Kept
   focusable rather than display:none, so it is reachable from a keyboard. */
.menu-check{position:absolute;opacity:0;width:1px;height:1px}
.menu-button{display:none;cursor:pointer;flex-direction:column;gap:4px;padding:.5rem;
border-radius:var(--r1);border:1px solid rgba(255,255,255,.25)}
.menu-button span{display:block;width:18px;height:2px;background:#c7d3e2;border-radius:2px}
.menu-check:focus-visible+.menu-button{outline:2px solid var(--edge);outline-offset:2px}
/* Only in the phone menu: the cross, the dimmed page behind it, and the sign-in buttons
   repeated at its foot so nobody has to close the menu to use them. */
.menu-close,.menu-backdrop,.menu-foot{display:none}

/* --- layout -------------------------------------------------------------- */
.wrap{max-width:1240px;margin:0 auto;padding:var(--s6) 1.4rem var(--s8)}
.pagehead{display:flex;align-items:center;justify-content:space-between;gap:var(--s4);
flex-wrap:wrap;margin-bottom:var(--s4)}
.pagehead h1{margin-bottom:0}
.back{color:var(--muted);font-size:.9rem;display:inline-block;margin-bottom:var(--s3)}
/* A band breaks out of the content column to run the full width, which is what gives
   a long page a rhythm instead of one undifferentiated scroll. */
.band{margin:var(--s8) calc(50% - 50vw);padding:var(--s8) calc(50vw - 50% + 1.4rem);
background:var(--bg3);border-block:1px solid var(--line)}
.band>:first-child{margin-top:0}

/* --- buttons ------------------------------------------------------------- */
/* One button, at two sizes. `.btn` is the working control on forms, tables and the
   portal; `.btn-calm` is the same button with more room around the words, for a page
   where pressing it is the whole point. They have to agree: a customer who reads the
   pricing page and then signs in should not find the buttons have changed shape and
   colour on the way. Before this they had -- navy and rounded out front, cyan and
   square inside, which is two products wearing one logo.

   Cyan stays the accent it always was: links, the nav underline, the one box in the
   diagram that matters. It is a colour for marking a thing, not for filling a control
   -- at 4.6:1 on white it cannot carry white text, which is why the cyan button needed
   near-black text and read as a highlighter pen. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;
cursor:pointer;font:inherit;font-size:.92rem;font-weight:600;padding:.62rem 1.25rem;
border-radius:8px;border:1px solid transparent;text-align:center;white-space:nowrap;
transition:background .15s,color .15s,border-color .15s}
.btn:hover{text-decoration:none}
/* Orange with navy words, the same as the marketing pages' buttons (29 September): the
   one colour on the site that means "press this", signed in or not. */
.btn-primary{background:var(--orange);color:var(--navy);border-color:var(--orange)}
.btn-primary:hover{background:var(--orange-hover);border-color:var(--orange-hover);
color:var(--navy)}
.btn-ghost{background:#fff;color:var(--navy);border-color:var(--calm-line)}
.btn-ghost:hover{background:#fff;color:var(--navy);border-color:#C6CFDB}
/* The ghost button still appears on two grounds -- light pages and the top bar -- and
   each needs its own contrast. The failure mode is silent: the control is present and
   simply cannot be read. */
/* `background` as well as `colour`: the light-ground ghost now has a white fill, and
   inheriting that here would put #dbe6f1 text on white in the bar -- present, and
   unreadable. This is the failure the note above is about. */
.topbar .btn-ghost{background:transparent;color:#dbe6f1;
border-color:rgba(255,255,255,.32)}
.topbar .btn-ghost:hover{background:rgba(255,255,255,.1);color:#fff;
border-color:rgba(255,255,255,.5)}
.btn-danger{background:#fff;color:var(--bad);border-color:var(--bad)}
.btn-danger:hover{background:var(--bad-bg)}
.btn-lg{font-size:1rem;padding:.82rem 1.6rem}
.btn-row{display:flex;flex-wrap:wrap;gap:var(--s3);align-items:center;margin-top:var(--s5)}

/* --- cards --------------------------------------------------------------- */
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(280px,100%),1fr));
gap:var(--s4)}
.card{display:flex;flex-direction:column;background:var(--bg);border:1px solid var(--line);
border-radius:var(--r2);padding:var(--s5);box-shadow:var(--shadow-1);
transition:box-shadow .16s,transform .16s,border-color .16s}
.card:hover{box-shadow:var(--shadow-3);transform:translateY(-3px);
border-color:#c5dfe8;text-decoration:none}
.card-top{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;margin-bottom:var(--s2)}
.card-type{font-family:var(--mono);font-size:.66rem;text-transform:uppercase;
letter-spacing:.13em;color:var(--muted);font-weight:500;margin-right:auto}
.card h3{margin:0 0 var(--s2);font-size:1.06rem}
.card-desc{color:var(--muted);font-size:.92rem;margin:0 0 var(--s4);flex:1}
.card-foot{display:flex;justify-content:space-between;align-items:center;
color:var(--muted);font-size:.82rem;border-top:1px solid var(--line-2);
padding-top:var(--s3);margin-top:auto}

/* --- one app ------------------------------------------------------------- */
.app-head{margin-bottom:var(--s4)}
.app-head h1{margin-bottom:var(--s2)}
.app-by{color:var(--muted);font-size:.94rem;margin:0 0 var(--s3)}
.app-tags{display:flex;flex-wrap:wrap;gap:.35rem}
code.path{font-family:var(--mono);font-size:.82rem;background:var(--bg3);
border:1px solid var(--line);border-radius:6px;padding:.1rem .4rem;
display:inline-block;margin:.1rem .15rem .1rem 0;word-break:break-all}
.install-steps{margin:0 0 var(--s4);padding-left:1.15rem;color:var(--ink-2);font-size:.94rem}
.install-steps li{margin-bottom:var(--s2)}

/* --- getting started ----------------------------------------------------- */
.journey{background:var(--bg);border:1px solid var(--line);border-radius:var(--r2);
padding:var(--s5);margin-bottom:var(--s6);box-shadow:var(--shadow-1)}
.journey h2{margin:0 0 var(--s2);font-size:1.2rem}
.journey>p{color:var(--muted);font-size:.94rem}
.journey ol{list-style:none;padding:0;margin:var(--s4) 0 0;display:flex;
flex-direction:column;gap:var(--s2)}
.journey li{display:flex;gap:var(--s3);align-items:flex-start;padding:var(--s3);
border-radius:var(--r1);border:1px solid var(--line-2)}
.journey li.next{border-color:#F6C9A6;background:#FFF8F2}
.journey li.done{color:var(--muted)}
.journey .tick{flex:none;width:22px;height:22px;border-radius:50%;display:flex;
align-items:center;justify-content:center;font-size:.72rem;font-weight:700;
border:1px solid var(--line);color:var(--muted);background:var(--bg2);margin-top:.15rem}
.journey li.done .tick{background:var(--good-bg);border-color:var(--good-line);
color:var(--good)}
.journey li.next .tick{background:var(--orange);border-color:var(--orange);
color:var(--navy)}
.journey .step-body{min-width:0;flex:1}
.journey .step-body b{display:block;color:var(--ink);font-weight:650}
.journey li.done .step-body b{color:var(--muted);font-weight:600}
.journey .step-body span{font-size:.9rem;color:var(--muted)}
.journey .step-body a{font-size:.9rem;font-weight:600}

/* --- storefront filters -------------------------------------------------- */
/* The filter box. On a wide screen it is the bare form it always was: the wrapper has no
   shape of its own and the summary is not rendered at all. The `details` element earns
   its place only on a phone, where the four controls stacked one per row put the first
   app 1,123 pixels down a page whose job is showing apps. */
.filter-box{margin:0}
.filter-summary{display:none}

.filters{display:grid;grid-template-columns:2fr 1fr 1fr auto auto;gap:var(--s3);
align-items:end;margin:var(--s5) 0 var(--s2)}
.filters label{display:flex;flex-direction:column;gap:.28rem;min-width:0}
.filter-label{font-family:var(--mono);font-size:.68rem;text-transform:uppercase;
letter-spacing:.11em;color:var(--muted);font-weight:500}
.filters input,.filters select{font:inherit;font-size:.94rem;padding:.55rem .65rem;
border:1px solid var(--line);border-radius:var(--r1);background:#fff;color:var(--ink);
min-width:0}
.filters input:focus,.filters select:focus{outline:2px solid var(--edge-deep);
border-color:var(--edge-deep)}
.result-count{margin:0 0 var(--s5);font-family:var(--mono);font-size:.78rem;
letter-spacing:.04em;color:var(--muted)}

/* --- tables, tiles, key/value ------------------------------------------- */
.tiles{display:flex;flex-wrap:wrap;gap:var(--s3);margin:var(--s4) 0}
.tile{background:var(--bg);border:1px solid var(--line);border-radius:var(--r2);
padding:var(--s3) var(--s4);min-width:7.5rem;box-shadow:var(--shadow-1)}
.tile-n{font-family:var(--display);font-size:1.7rem;font-weight:700;line-height:1.1;
letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.tile-l{font-family:var(--mono);font-size:.68rem;text-transform:uppercase;
letter-spacing:.11em;color:var(--muted)}
table.kv{border-collapse:collapse;width:100%}
/* `width:1%` with `nowrap` sizes the key column to its own longest label, so two of
   these stacked -- "What it can reach" above "What it needs" on an app page -- put their
   values at two different left edges. A floor makes them agree without stopping a longer
   key from taking the room it needs. Dropped on a narrow screen, where 8.5rem of label
   beside a value is worse than a ragged edge. */
table.kv th{text-align:left;color:var(--muted);font-weight:600;font-size:.92rem;
padding:.55rem .9rem .55rem 0;vertical-align:top;white-space:nowrap;width:1%;
min-width:8.5rem}
@media (max-width:34rem){table.kv th{min-width:0}}

/* The install dialog's labels are sentences rather than keys -- "Tiles: datapoint names
   or paths, comma-separated (blank for the first six numeric values)" is a real one. The
   `kv` rule sizes its key column to the longest label and refuses to wrap it, which is
   right for "Runs as" and absurd here: it pushed the key column past half the page and
   left the answer wrapping three words to a line. Proportions instead, and on a narrow
   screen the two stack, because 55% of 340px is not a column. */
table.kv-ask th{white-space:normal;width:55%;min-width:0;padding-right:1.6rem}
table.kv-ask td{width:45%;color:var(--calm-lead)}
@media (max-width:38rem){
table.kv-ask th,table.kv-ask td{display:block;width:auto;padding-right:0}
table.kv-ask th{border-bottom:0;padding-bottom:.1rem}
table.kv-ask td{padding-top:0}
}
table.kv td{padding:.55rem 0;border-bottom:1px solid var(--line-2)}
.table-wrap{overflow-x:auto}
table.list{width:100%;border-collapse:collapse;background:var(--bg);
border:1px solid var(--line);border-radius:var(--r2);overflow:hidden;
box-shadow:var(--shadow-1)}
table.list th{background:var(--bg3);color:var(--ink-2);text-align:left;
padding:.62rem .95rem;font-size:.78rem;font-weight:600;text-transform:uppercase;
letter-spacing:.06em;border-bottom:1px solid var(--line)}
table.list td{padding:.7rem .95rem;border-top:1px solid var(--line-2)}
table.list tbody tr:hover{background:var(--bg2)}
.lic{display:flex;justify-content:space-between;align-items:center;gap:var(--s2);
padding:.35rem 0;border-top:1px solid var(--line-2);font-size:.9rem}

/* --- panels, notices, badges -------------------------------------------- */
.install-note{background:var(--bg);border:1px solid var(--line);border-radius:var(--r2);
padding:var(--s5);box-shadow:var(--shadow-1)}
.install-note h3{margin-top:0}
.badge{display:inline-block;font-size:.7rem;font-weight:700;padding:.16rem .55rem;
border-radius:2px;letter-spacing:.02em;line-height:1.5}
.badge-neutral{background:var(--bg3);color:#44566c;border:1px solid var(--line)}
.badge-accent{background:rgba(34,211,238,.16);color:var(--edge-dark)}
.badge-good{background:var(--good-bg);color:var(--good-text)}
.badge-warn{background:var(--warn-bg);color:var(--warn-text)}
.badge-bad{background:var(--bad-bg);color:var(--bad-text)}
.notice{padding:.8rem 1.05rem;border-radius:var(--r1);margin:var(--s4) 0;
font-size:.94rem;border:1px solid transparent}
.notice-info{background:var(--bg3);color:var(--ink-2);border-color:var(--line)}
/* The corrected hues, not the display ones. A notice is .94rem regular weight, which
   is body text and needs 4.5:1 -- the display versions land at 4.03-4.07 on their own
   tinted grounds. The stylesheet used to say these kept "their documented roles for
   notices and larger text"; a notice is not larger text, and the dev-key warning on the
   storefront had been failing since the day the tokens were corrected. */
.notice-good{background:var(--good-bg);color:var(--good-text);border-color:var(--good-line)}
.notice-warn{background:var(--warn-bg);color:var(--warn-text);border-color:var(--warn-line)}
.notice-bad{background:var(--bad-bg);color:var(--bad-text);border-color:var(--bad-line)}
.checks{list-style:none;padding:0;margin:var(--s3) 0}
.chk{display:flex;gap:var(--s3);align-items:baseline;padding:.6rem .8rem;
border:1px solid var(--line);border-radius:var(--r1);margin-bottom:var(--s2);
background:var(--bg);font-size:.92rem}
.chk-attention{border-color:var(--warn-line);background:var(--warn-bg)}
.decision{background:var(--bg);border:1px solid var(--line);border-radius:var(--r2);
padding:var(--s5);position:sticky;top:76px;box-shadow:var(--shadow-1)}
.decision .btn{width:100%;margin-top:var(--s2)}
.detail-grid{display:grid;grid-template-columns:2fr 1fr;gap:var(--s6);align-items:start}
.code{margin:var(--s4) 0}
.code pre{margin:0;background:var(--code-bg);color:#dbe7f3;padding:1.05rem 1.2rem;
border-radius:var(--r2);overflow-x:auto;font-family:var(--mono);font-size:.82rem;
line-height:1.6;max-height:460px}

/* --- support thread ------------------------------------------------------ */
.thread{display:flex;flex-direction:column;gap:var(--s3);margin:var(--s4) 0}
.msg{background:#fff;border:1px solid var(--line);border-radius:var(--r2);
padding:var(--s4);box-shadow:var(--shadow-1)}
.msg-staff{border-left:3px solid var(--edge-deep);background:#f7fbfc}
.msg-head{display:flex;gap:var(--s2);align-items:center;margin-bottom:var(--s2)}
.msg p{margin:0;white-space:pre-wrap}

/* --- forms --------------------------------------------------------------- */
.form{display:flex;flex-direction:column;gap:var(--s4);max-width:640px}
.form label{display:flex;flex-direction:column;gap:.35rem;font-weight:600;
font-size:.9rem;color:var(--ink-2)}
.form input,.form textarea,.form select{font:inherit;padding:.62rem .75rem;
border:1px solid var(--line);border-radius:var(--r1);background:#fff;color:var(--ink)}
.form textarea{font-family:var(--mono);font-size:.85rem;line-height:1.55;resize:vertical}
.form input:focus,.form textarea:focus,.form select:focus{outline:2px solid var(--edge-deep);
border-color:var(--edge-deep)}
.form .btn{align-self:flex-start}
.authbox{max-width:420px;margin:var(--s7) auto;background:var(--bg);
border:1px solid var(--line);border-radius:var(--r3);padding:var(--s6);
box-shadow:var(--shadow-2)}
.authbox h1{margin-top:0}

/* --- footer -------------------------------------------------------------- */
.foot{background:var(--navy);color:#8fa6bd;border-top:1px solid #1a3450;
padding:var(--s7) 1.4rem var(--s5);font-size:.9rem}
.foot-inner{max-width:1240px;margin:0 auto;display:grid;
grid-template-columns:1.4fr repeat(3,1fr);gap:var(--s6)}
/* The footer's column labels are `h2` for structure and this size for the eye. A
   heading level is not a size, which is the whole reason the two are separate. */
.foot h2{color:#fff;font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;
font-family:var(--sans);font-weight:600;margin:0 0 var(--s3)}
.foot ul{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;
gap:.45rem}
.foot a{color:#a9bccf}
.foot a:hover{color:#fff}
.foot-brand{display:flex;align-items:center;gap:.55rem;color:#fff;
font-family:var(--display);font-weight:700;margin-bottom:var(--s3)}
.foot-note{max-width:34ch;color:#7f97ae;font-size:.86rem}
.foot-base{max-width:1240px;margin:var(--s6) auto 0;padding-top:var(--s4);
border-top:1px solid #1a3450;display:flex;flex-wrap:wrap;gap:var(--s4);
justify-content:space-between;color:#7f97ae;font-size:.82rem}

/* An empty state that is a next step rather than an explanation. Same surface as a
   card, more room, because it is the only thing on the page. */
.empty-cta{background:var(--bg);border:1px solid var(--line);border-radius:var(--r3);
padding:var(--s6);box-shadow:var(--shadow-1);margin:var(--s5) 0}
.empty-cta form{margin:var(--s5) 0 var(--s3)}
.empty-cta .lead{margin-bottom:0}

/* The buy panel on an app page. Same surface as a pricing plan, because it is the same
   question in a different place. */
.buy-panel{background:var(--bg);border:1px solid var(--edge-deep);border-radius:var(--r3);
padding:var(--s5);box-shadow:var(--shadow-2);margin:var(--s5) 0}
.buy-panel .plan-price{margin:0}
.buy-panel .lead{margin:var(--s3) 0}
.buy-form{display:flex;gap:var(--s4);flex-wrap:wrap;align-items:flex-end;
margin:var(--s4) 0 0}
.buy-form label{display:flex;flex-direction:column;gap:var(--s1);font-size:.85rem;
color:var(--muted);min-width:0}
.buy-form input{padding:.5rem .7rem;border:1px solid var(--line);border-radius:var(--r1);
font:inherit;color:var(--ink);background:var(--bg);min-width:0}
.buy-form input[type=number]{width:6rem}
.card-price{font-weight:600;color:var(--navy);font-size:.85rem}


/* --- the stage ---------------------------------------------------------------
   A lit ground, not a dark rectangle. Three light sources over a vertical ramp, a
   fine engineering grid masked so it fades before it becomes wallpaper, and the
   drawing sitting on top of all of it. */
.stage{position:relative;overflow:hidden;background:var(--deep-2);color:#fff;
margin:0 calc(-50vw + 50%);padding:clamp(3.5rem,7vw,6rem) calc(50vw - 50%) clamp(3rem,6vw,5rem)}
.stage::before{content:"";position:absolute;inset:0;pointer-events:none;
background:
 radial-gradient(1100px 520px at 78% 18%, rgba(34,211,238,.16), transparent 62%),
 radial-gradient(900px 600px at 12% 92%, rgba(46,107,176,.30), transparent 66%),
 linear-gradient(180deg,var(--deep-1) 0%, var(--deep-2) 52%, var(--deep-3) 100%)}
.stage::after{content:"";position:absolute;inset:0;pointer-events:none;opacity:.5;
background-image:linear-gradient(rgba(255,255,255,.045) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,255,255,.045) 1px,transparent 1px);
background-size:72px 72px;
-webkit-mask-image:radial-gradient(120% 100% at 50% 0%,#000 35%,transparent 78%);
mask-image:radial-gradient(120% 100% at 50% 0%,#000 35%,transparent 78%)}
.stage > *{position:relative;z-index:1}
/* The front door's picture (`_hero_dashboard`): the real product, framed like a screen
   and lifted off the navy, a little larger than its column on a wide screen. */
.hero-shot{align-self:center;margin:0;position:relative}
.hero-screen{display:block;width:100%;height:auto;border-radius:12px;
border:1px solid rgba(255,255,255,.16);background:#0b1b33;
box-shadow:0 34px 80px rgba(0,0,0,.5),0 0 0 1px rgba(34,211,238,.10),
0 0 70px rgba(34,211,238,.12)}
.hero-shot-note{position:relative}

.hero-shot-note{margin:.8rem 0 0;color:var(--on-deep);opacity:.75;font-size:.82rem}
/* On a wide screen the dashboard gets the larger column and runs a little past the page's
   edge, like a screen seen from the side: the product is the picture, so it is not shrunk
   to fit beside a paragraph. */
@media (min-width:62rem){
  .stage-split.stage-split-shot{grid-template-columns:minmax(0,1fr) minmax(0,1.05fr)}
  .hero-shot{margin-right:calc(-1 * clamp(0rem,9vw,10rem))}
  /* Three lines, not four, so "Plan a pilot" is on a laptop's first screen. */
  .stage .stage-split-shot h1{font-size:clamp(2.4rem,1.2rem + 2.9vw,3.65rem)}
  /* The three marks on one row: on two lines they read as scattered. */
  .stage-split-shot .assure{flex-wrap:nowrap;gap:1.2rem;font-size:.84rem;white-space:nowrap}
}
/* A photo behind a header (`photos.py`). Inside the same layer as the gradient, under a
   dark wash that is heaviest behind the words: the text keeps the contrast it has without
   a photo, and the drawing on the right sits over the brighter part. A phone gets the
   small file and a heavier wash, because there the words cover the whole width. */
.stage.has-photo::before{background:
 radial-gradient(1100px 520px at 78% 18%, rgba(34,211,238,.12), transparent 62%),
 linear-gradient(100deg,rgba(7,22,42,.95) 0%,rgba(7,22,42,.86) 45%,rgba(7,22,42,.62) 100%),
 var(--photo) center/cover no-repeat}
@media (max-width:700px){.stage.has-photo::before{background:
 linear-gradient(180deg,rgba(7,22,42,.86),rgba(7,22,42,.76)),
 var(--photo-sm) center/cover no-repeat}}
@media (min-width:1700px){.stage.has-photo::before{background:
 radial-gradient(1100px 520px at 78% 18%, rgba(34,211,238,.12), transparent 62%),
 linear-gradient(100deg,rgba(7,22,42,.95) 0%,rgba(7,22,42,.86) 45%,rgba(7,22,42,.62) 100%),
 var(--photo-lg) center/cover no-repeat}}

.stage-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.92fr);
gap:clamp(2rem,4vw,4rem);align-items:center}
@media (max-width:62rem){.stage-split{grid-template-columns:1fr;gap:clamp(2.5rem,6vw,3.5rem)}}

.stage h1{color:#fff;font-family:var(--display);font-weight:700;
font-size:clamp(2.4rem,1.4rem + 4.2vw,4.5rem);line-height:1.02;letter-spacing:-.035em;
margin:0 0 var(--s5)}
/* One phrase in the headline carries the accent, as a gradient rather than a flat fill:
   the flat cyan is not readable at this weight and the gradient keeps its darker end
   doing the work. */
.stage h1 em{font-style:normal;
background:linear-gradient(100deg,#7FE6F7 0%,#22D3EE 45%,#5BB4F0 100%);
-webkit-background-clip:text;background-clip:text;color:transparent}
.stage .lede{color:var(--on-deep);max-width:32rem}
.stage h2{color:#fff;font-family:var(--display)}

.pill{display:inline-flex;align-items:center;gap:.55rem;font-family:var(--mono);
font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;color:var(--edge-soft);
background:rgba(34,211,238,.10);border:1px solid rgba(34,211,238,.28);
padding:.4rem .75rem;border-radius:999px;margin:0 0 var(--s5)}
.pill i{width:6px;height:6px;border-radius:50%;background:var(--edge);
box-shadow:0 0 0 3px rgba(34,211,238,.22);flex:none}

.btn-glow{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
padding:.85rem 1.5rem;border-radius:10px;font-weight:600;font-size:.98rem;
text-decoration:none;border:1px solid transparent;
background:var(--orange);color:var(--navy);
box-shadow:0 8px 20px -10px rgba(234,88,12,.7);transition:transform .16s,box-shadow .16s}
.btn-glow:hover{transform:translateY(-1px);text-decoration:none;background:var(--orange-hover);
box-shadow:0 12px 26px -10px rgba(234,88,12,.8)}
.btn-veil{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
padding:.85rem 1.5rem;border-radius:10px;font-weight:600;font-size:.98rem;
text-decoration:none;background:rgba(255,255,255,.06);color:#fff;
border:1px solid rgba(255,255,255,.22);transition:background .16s,border-color .16s}
.btn-veil:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.4);
text-decoration:none}

/* Three things a buyer checks before reading a word of the page. */
.assure{display:flex;gap:var(--s5);flex-wrap:wrap;margin-top:var(--s6);
font-size:.87rem;color:var(--on-deep-soft)}
.assure span{display:inline-flex;align-items:center;gap:.45rem}
.assure svg{width:15px;height:15px;flex:none;color:var(--edge)}

/* --- the drawing -------------------------------------------------------------
   Isometric, because that is the projection engineering drawings use and a box keeps
   its scale wherever it stands on the plane. The viewBox is kept close to the size it
   actually renders at: the first version was 840 units wide inside a 500px column,
   which delivered every 11px caption to the screen at under 7px. */
.scene{width:100%;height:auto;display:block;max-width:34rem;margin:0 auto;
filter:drop-shadow(0 30px 60px rgba(0,0,0,.45))}
.deck{fill:url(#k-deck)}
.deck-hi{fill:url(#k-deck-hi)}
.deck-edge{fill:none;stroke:rgba(140,190,240,.34);stroke-width:1.2}
.grid{stroke:rgba(140,190,240,.13);stroke-width:1}
.beam{fill:url(#k-beam)}
.link{stroke:rgba(127,230,247,.5);stroke-width:1.6;fill:none;stroke-dasharray:4 5}
.rail{fill:rgba(180,215,245,.10);stroke:rgba(140,190,240,.3);stroke-width:1}
.gate{fill:rgba(34,211,238,.09);stroke:rgba(34,211,238,.55);stroke-width:1.4}
.gate-in{fill:none;stroke:rgba(34,211,238,.3);stroke-width:1;stroke-dasharray:3 4}
.seal{fill:#04121F;stroke:#2FE0F8;stroke-width:1.6}
.led{fill:#3BE3A0}
.srv-t{fill:#2B5A94}.srv-l{fill:#16355E}.srv-r{fill:#1E4478}
.ipc-t{fill:#123A57}.ipc-l{fill:#08202F}.ipc-r{fill:#0C2C41}
.mch-t{fill:#2A4A75}.mch-l{fill:#15294A}.mch-r{fill:#1E3A60}
.pkg-t{fill:#7FE6F7}.pkg-l{fill:#19A6C4}.pkg-r{fill:#26C3E0}
.lb{fill:#fff;font-family:var(--display);font-size:17px;font-weight:600;
text-anchor:middle;letter-spacing:-.01em}
.lb-s{fill:#93AEC9;font-family:var(--sans);font-size:13px;text-anchor:middle}
.lb-a{fill:var(--edge-soft);font-family:var(--mono);font-size:12px;text-anchor:middle;
letter-spacing:.06em}
.lb-n{fill:#94B2D2;font-family:var(--mono);font-size:11.5px;letter-spacing:.14em}
/* Labels the newer scenes need. `lb-p` takes its colour from the element, because a
   protocol's label and the block it names have to be the same pigment for the drawing to
   say "these are five different things". */
.lb-p{font-family:var(--mono);font-size:12px;font-weight:500;text-anchor:middle;
letter-spacing:.05em}
.lb-t{fill:#9FBBD8;font-family:var(--mono);font-size:11px;text-anchor:middle}
/* Text *on* a face rather than beside one. `lb-s` is tuned for a label floating on the
   dark ground and is about 2.5:1 over a mid-navy slab, which is a decoration that happens
   to be shaped like words. */
.lb-w{fill:#EAF2FB;font-family:var(--sans);font-size:13px;font-weight:500;
text-anchor:middle}
.lb-seam{fill:#7E93AC;font-size:20px;text-anchor:middle;letter-spacing:.3em}
.tangle{fill:none;stroke-width:1.7;opacity:.62;stroke-linecap:round}
.branch{fill:none;stroke:rgba(127,230,247,.45);stroke-width:1.5}
/* The comparison is an elevation rather than a projection: see `scenes.py`. In
   perspective the four slabs overlapped, which hid the gaps between them -- and the gaps
   are the entire argument.

   Flat is not the same as unlit. Everything in it has a light source above: a gradient
   down the face, a highlight along the top edge, a shadow underneath. That is most of the
   difference between a wireframe and an illustration, and the first version of this was
   four grey rectangles. */
.slab{stroke:rgba(170,195,225,.20);stroke-width:1}
.lit-edge{stroke:rgba(255,255,255,.22);stroke-width:1.5;stroke-linecap:round;fill:none}
.lit-edge-bright{stroke:rgba(127,230,247,.55);stroke-width:1.6}
/* A joint, drawn as one: two flanges, four bolts and a run between them. "Every seam is
   something you maintain" is the argument, and a dotted hairline does not carry it.
   Amber because that is what this palette already uses for a thing wanting attention. */
.seam{stroke:rgba(232,147,90,.55);stroke-width:1.6;fill:none;stroke-dasharray:4 4}
.flange{fill:#33404F;stroke:rgba(232,147,90,.75);stroke-width:1.3}
.bolt{fill:rgba(243,178,124,.9)}
.seam-dot{fill:#0B2545;stroke:rgba(190,210,235,.5);stroke-width:1.4}
.one-box-fill{fill:url(#s-box)}
.band-div{stroke:rgba(127,230,247,.20);stroke-width:1;fill:none}
/* The fence diagram. A zone needs a real edge -- the whole question the drawing answers
   is which side of it a thing is on. */
.zone{fill:rgba(140,190,240,.045);stroke:rgba(140,190,240,.22);stroke-width:1;
stroke-dasharray:6 5}
.net-link{stroke:rgba(127,230,247,.45);stroke-width:1.6;fill:none}
.net-out{stroke:rgba(127,230,247,.8);stroke-width:1.8;fill:none;stroke-dasharray:5 4}
.net-blocked{stroke:rgba(190,210,235,.28);stroke-width:1.6;fill:none;stroke-dasharray:4 5}
.net-strike{stroke:#E3697C;stroke-width:2.2;stroke-linecap:round}
.scene-net{max-width:50rem;filter:none}
/* The contract sits between the two halves and belongs to neither, so it is drawn
   on top of both rather than inside either. */
.contract{stroke:rgba(127,230,247,.4);stroke-width:1.4;stroke-dasharray:6 5;fill:none}
.contract-plate{fill:#06152A;stroke:rgba(34,211,238,.5);stroke-width:1.4}
.half{stroke:rgba(140,190,240,.26);stroke-width:1}
.device{fill:none;stroke:rgba(34,211,238,.6);stroke-width:1.8}
/* The package's state at each hand-off. Red for unsigned is not an error colour
   here -- it is the accurate one: an unsigned package is exactly what the machine
   refuses. */
.state-bad{fill:rgba(227,105,124,.14);stroke:rgba(227,105,124,.55);stroke-width:1.2}
.state-good{fill:rgba(59,227,160,.14);stroke:rgba(59,227,160,.55);stroke-width:1.2}
.state-bad-text,.state-good-text{font-family:var(--mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-anchor:middle}
.state-bad-text{fill:#F093A2}
.state-good-text{fill:#6FE7B8}
.scene-pipe{max-width:52rem;filter:none}
.half-rule{stroke:rgba(140,190,240,.22);stroke-width:1;fill:none}
.scene-halves{max-width:48rem;filter:none}
.one-box{fill:none;stroke:rgba(90,224,247,.85);stroke-width:1.8}
/* Solid, not dashed. This is one object, and a dashed edge reads as a selection
   marquee or a thing not yet decided -- which is the opposite of the claim. */
.scene-uns{max-width:33rem}
.scene-stack{max-width:56rem;filter:none;overflow:visible}
.scene-pair{max-width:58rem;filter:none;overflow:visible}
.lb-chip{font-family:var(--mono);font-size:11px;font-weight:500;letter-spacing:.07em;
text-anchor:middle}
.lb-tag{fill:#A8C4DF;font-family:var(--mono);font-size:12.5px}
/* `fill:none` is not a nicety here. A bezier without it is filled black, so the five
   connections render as solid wedges -- which is exactly what they did for ten minutes
   when the patch that should have added this rule asserted out before writing. */
.pair-line{fill:none;stroke:rgba(127,230,247,.5);stroke-width:1.7;stroke-linecap:round}
.slab-in,.joint-in,.whole-in{transform-box:fill-box;transform-origin:center}  /* Wider than the other scenes and with no shadow: it is two things being
     compared rather than one object sitting on a ground, and a drop shadow on a
     flat elevation reads as a mistake. */
.scene-keep{max-width:38rem;filter:none}

/* --- the plant ---------------------------------------------------------------
   An illustration of the environment, not a photograph of a product. The machines
   and the cabinet are generic on purpose: where the software lives is true of
   every installation, what the hardware looks like is Michiel's to photograph. */
.plant-band{position:relative;overflow:hidden;background:var(--deep-2);color:#fff;
margin:0 calc(-50vw + 50%);
padding:clamp(3rem,6vw,5rem) calc(50vw - 50%) clamp(2.5rem,5vw,4rem)}
.plant-band::before{content:"";position:absolute;inset:0;pointer-events:none;
background:
 radial-gradient(900px 460px at 70% 8%, rgba(34,211,238,.14), transparent 60%),
 radial-gradient(820px 520px at 10% 98%, rgba(46,107,176,.28), transparent 64%),
 linear-gradient(180deg,var(--deep-3) 0%, var(--deep-2) 50%, var(--deep-3) 100%)}
.plant-band > *{position:relative;z-index:1}

/* The 3D. One perspective container, and the layers sit at different depths inside
   it, so a small tilt separates them the way a diorama separates. The tilt comes
   from the pointer; with no pointer, or reduced motion, it simply never moves. */
.plant-stage{perspective:1400px;perspective-origin:50% 40%;margin-top:var(--s6);
max-width:min(100%,50rem);margin-left:auto;margin-right:auto}
.plant-tilt{transform-style:preserve-3d;transition:transform .35s cubic-bezier(.22,.61,.36,1);
will-change:transform}
.plant{width:100%;height:auto;display:block;
filter:drop-shadow(0 40px 70px rgba(0,0,0,.5))}
.pl-layer{transform-style:preserve-3d}

.p-floor{fill:url(#p-floor)}
.p-edge{fill:none;stroke:rgba(140,190,240,.30);stroke-width:1.2}
.p-grid{stroke:rgba(140,190,240,.10);stroke-width:1}

/* The room. Darker than anything standing in it, so the furniture reads in front of
   the walls rather than merging with them. */
.p-wall-b{fill:#13294A}
.p-wall-l{fill:#0D1F3A}
.p-rib{stroke:rgba(140,190,240,.07);stroke-width:1}
.p-win{fill:rgba(127,230,247,.09);stroke:rgba(140,200,240,.16);stroke-width:1}
.p-aisle{fill:rgba(255,214,122,.06)}
.p-aisle-edge{fill:rgba(255,214,122,.30)}
.p-col-t{fill:#294F79}.p-col-r{fill:#1A3557}.p-col-l{fill:#12263F}
.p-rack-t{fill:#375C86}.p-rack-r{fill:#24406A}.p-rack-l{fill:#1A3152}
.p-pallet-t{fill:#4A7BA4}.p-pallet-r{fill:#305478}.p-pallet-l{fill:#20395A}
.p-fence-t{fill:#31597F}.p-fence-r{fill:#1F3C5E}.p-fence-l{fill:#162E49}
.p-fence-rail{fill:rgba(140,200,240,.28)}

.p-mch-t{fill:#2F5688}.p-mch-r{fill:#20406B}.p-mch-l{fill:#16304F}
.p-belt-t{fill:#33608E}.p-belt-r{fill:#1E3E61}.p-belt-l{fill:#16304F}
.p-crate-t{fill:#8FD6E8}.p-crate-r{fill:#3FA8C4}.p-crate-l{fill:#2B7F97}
.p-cab-t{fill:#25507F}.p-cab-r{fill:#17365C}.p-cab-l{fill:#102844}
.p-door{fill:rgba(140,200,240,.10);stroke:rgba(160,210,245,.35);stroke-width:1}
.p-rail{fill:rgba(200,225,250,.14)}
.p-ipc{fill:#0B1F31;stroke:rgba(160,210,245,.45);stroke-width:1}
.p-post-t{fill:#2A4E78}.p-post-r{fill:#1B3A5C}.p-post-l{fill:#132C47}
.p-screen{fill:rgba(34,211,238,.20);stroke:rgba(127,230,247,.55);stroke-width:1.2}
.p-led{fill:#3BE3A0}
.p-wire{stroke:rgba(127,230,247,.34);stroke-width:1.4;fill:none;stroke-dasharray:3 5}
.p-pulse{fill:#7FE6F7}
.p-up{fill:url(#p-up)}
.p-gate{fill:rgba(34,211,238,.10);stroke:rgba(34,211,238,.55);stroke-width:1.4}
.p-gate-glow{fill:url(#p-glow)}
.p-pkg-b{fill:#7FE6F7;stroke:#2FE0F8;stroke-width:1.2}
.p-store{fill:rgba(43,90,148,.55);stroke:rgba(160,210,245,.40);stroke-width:1.2}
.p-lb{fill:#fff;font-family:var(--display);font-size:16px;font-weight:600;
letter-spacing:-.01em}
.p-lb-s{fill:#9FBAD6;font-family:var(--sans);font-size:12.5px}
.p-lead{stroke:rgba(160,200,235,.45);stroke-width:1;fill:none}
.p-acc{fill:var(--edge-soft);font-family:var(--mono);font-size:11.5px;
text-anchor:middle;letter-spacing:.08em}

/* --- the plant in motion ------------------------------------------------------
   Every one of these is a loop, so all of them stop under reduced motion. The
   crates and the pulses are the only things that move far; the rest is a blink. */
@media (prefers-reduced-motion:no-preference){
  .p-crate-run{animation:crate 8s linear infinite;
  transform:translate(var(--dx),var(--dy))}
  @keyframes crate{
    0%{transform:translate(0,0);opacity:0}
    6%{opacity:1}
    92%{opacity:1}
    100%{transform:translate(var(--dx),var(--dy));opacity:0}
  }
  .p-pulse{animation:pulse 3.3s cubic-bezier(.4,0,.6,1) infinite;opacity:0}
  @keyframes pulse{
    0%{transform:translate(0,0);opacity:0}
    10%{opacity:1}
    88%{opacity:1}
    100%{transform:translate(var(--tx),var(--ty));opacity:0}
  }
  .p-led{animation:led 2.6s ease-in-out infinite}
  @keyframes led{0%,70%{opacity:1}80%{opacity:.35}100%{opacity:1}}
  .p-pkg{animation:pkgdrop 6s cubic-bezier(.5,0,.5,1) infinite}
  @keyframes pkgdrop{
    0%{transform:translateY(-30px);opacity:0}
    12%{opacity:1}
    40%{transform:translateY(64px);opacity:1}
    56%{transform:translateY(64px);opacity:1}
    80%{transform:translateY(150px);opacity:1}
    90%,100%{transform:translateY(150px);opacity:0}
  }
  .p-gate{animation:gatelit 6s ease-in-out infinite}
  @keyframes gatelit{0%,42%{opacity:.6}50%{opacity:1}60%{opacity:.6}100%{opacity:.6}}
}

/* --- the shelf ----------------------------------------------------------------
   A card per app, because the useful thing about a listing here is not its name --
   it is what the app is allowed to touch, which is read from the manifest that was
   signed and which the machine will hold it to. No other app store can print that,
   and it is the question somebody buying industrial software actually has. */
.app-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(20rem,100%),1fr));
gap:clamp(1.25rem,2.5vw,1.75rem);margin-top:clamp(2.5rem,5vw,4rem)}
.card-app{display:flex;flex-direction:column;background:#fff;
border:1px solid var(--calm-line);border-radius:var(--r3);
padding:clamp(1.4rem,2.5vw,1.75rem);text-decoration:none;color:inherit;
box-shadow:var(--lift-1);transition:transform .18s,box-shadow .18s,border-color .18s}
.card-app:hover{transform:translateY(-3px);box-shadow:var(--lift-3);
border-color:#C6CFDB;text-decoration:none}
.ca-head{display:flex;gap:var(--s4);align-items:center}
.ca-id{min-width:0;display:flex;flex-direction:column}
.ca-name{font-family:var(--display);font-weight:700;font-size:1.14rem;
letter-spacing:-.015em;color:var(--calm-ink);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.card-app:hover .ca-name{color:var(--accent-text)}
/* A card with no page behind it yet (kustlyne.com/apps before the store opens): the same
   card, without the lift and colour change that promise a click. */
.card-app-still:hover,.app-grid-still .card-app:hover{transform:none;box-shadow:var(--lift-1)}
.card-app-still:hover .ca-name{color:inherit}
/* The sentence under a card's reach rows: what those three lines understate. Smaller
   than the description and in the muted colour, because it is a footnote to the rows
   above it rather than a second description. */
.ca-note{margin:var(--s2) 0 0;font-size:.86rem;line-height:1.5;color:var(--calm-mute)}
.ca-by{font-size:.85rem;color:var(--calm-mute);margin-top:2px}
.ca-desc{margin:var(--s4) 0 0;color:var(--calm-lead);font-size:.96rem;line-height:1.55}

/* The part worth the card. A definition list, because that is what it is.
   `margin-top:auto` pins it just above the footer, so on a shelf where one app has a
   screenshot and its neighbours do not, all three still line up along the bottom. The
   slack lands after the description, where it reads as space rather than as a card that
   ran out of things to say. */
.ca-reach{margin:var(--s5) 0 0;padding:var(--s4) 0 0;margin-top:auto;
border-top:1px solid var(--calm-line);display:grid;gap:.4rem}
.reach-row{display:grid;grid-template-columns:5.2rem minmax(0,1fr);gap:var(--s3);
align-items:baseline}
.reach-row dt{font-size:.82rem;color:var(--calm-mute)}
.reach-row dd{margin:0;font-family:var(--mono);font-size:.8rem;color:var(--calm-lead);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* No `margin-top:auto` here any more: two of them in one flex column split the slack
   between them, which put a gap above the reach block *and* below it. The one on
   `.ca-reach` takes it all, so the reach block and the footer both sit at the bottom of
   every card and the space lands after the description. */
.ca-foot{display:flex;justify-content:space-between;align-items:center;gap:var(--s3);
padding-top:var(--s5)}
.ca-kind{font-size:.8rem;color:var(--calm-mute);background:var(--band-2);
border-radius:999px;padding:.22rem .6rem}
.ca-price{font-size:.92rem;font-weight:600;color:var(--calm-ink);white-space:nowrap}
.ca-price.is-free{color:var(--good-text)}

/* --- the chart --------------------------------------------------------------
   One series, so no legend: the caption names it. Thin marks, a recessive grid,
   values on the three points worth naming and the rest on hover -- a number on
   every point is noise. The table underneath is not a fallback, it is the
   accessible reading of the same figures, and on a specification page somebody
   wants to copy them anyway. */
.figure{margin:clamp(2rem,4vw,3rem) 0 0}
.chart{width:100%;height:auto;display:block;overflow:visible}
.c-grid{stroke:var(--calm-line);stroke-width:1}
.c-ax{font-family:var(--sans);font-size:13px;fill:var(--calm-mute)}
.c-ax-t{font-family:var(--mono);font-size:11px;fill:var(--calm-mute);
letter-spacing:.1em;text-transform:uppercase}
.c-line{fill:none;stroke:var(--series);stroke-width:2;stroke-linejoin:round;
stroke-linecap:round}
.c-area{stroke:none}
/* A 2px surface ring so a mark sitting on the line still reads as a mark. */
.c-ring{fill:var(--bg)}
.c-dot{fill:var(--series)}
.c-val{font-family:var(--sans);font-size:14px;font-weight:600;fill:var(--calm-ink)}
.c-hov{font-family:var(--sans);font-size:13px;font-weight:600;fill:var(--calm-ink);
opacity:0;transition:opacity .15s}
.c-pt{cursor:default}
.c-pt:hover .c-hov,.c-pt:focus-within .c-hov{opacity:1}
.c-pt:hover .c-dot{r:6}

/* A screenshot of the product. The ground under it is the platform's own dark blue
   rather than white, so the space the image will occupy does not flash bright before it
   arrives -- and `aspect-ratio` holds that space open at every width, which the width and
   height attributes alone do not once the image is fluid. */
/* A listing's pictures. One column, because a screenshot shrunk to half a page is a
   screenshot nobody can read — and there are at most four. */
/* --- the offer ---------------------------------------------------------------
   Four cards, each one a link. Same surface as an app card so the site reads as one
   place, and a four-up grid that becomes two-up before it becomes one — three columns
   would leave a widow, and this is a set of four. */
/* Five, not four. The count is fixed rather than `auto-fit` because auto-fit chose four
   columns for five cards and left Partners alone on a second row, which reads as a card
   that arrived late rather than as the fifth of five. Content is 1200px, so five tracks
   are about 13rem each — narrow, and narrow in a row of five is a deliberate rhythm,
   where one orphan is a mistake. */
.offers{display:grid;gap:var(--s4);margin:var(--s6) 0 var(--s7);
grid-template-columns:repeat(5,minmax(0,1fr))}
@media (max-width:86rem){.offers{grid-template-columns:repeat(3,minmax(min(15rem,100%),1fr))}}
@media (max-width:64rem){.offers{grid-template-columns:repeat(2,minmax(min(15rem,100%),1fr))}}
@media (max-width:42rem){.offers{grid-template-columns:1fr}}
.offer{display:flex;flex-direction:column;gap:var(--s3);text-decoration:none;color:inherit;
background:#fff;border:1px solid var(--calm-line);border-radius:var(--r3);
padding:clamp(1.4rem,2.4vw,1.7rem);box-shadow:var(--lift-1);
transition:transform .18s,box-shadow .18s,border-color .18s}
.offer:hover{transform:translateY(-3px);box-shadow:var(--lift-3);border-color:#C6CFDB;
text-decoration:none}
.offer-mark{display:inline-flex;align-items:center;justify-content:center;
width:2.5rem;height:2.5rem;border-radius:12px;background:var(--band-2);color:var(--series)}
.offer-mark svg{width:1.35rem;height:1.35rem}
.offer h3{margin:0;font-size:1.18rem;letter-spacing:-.015em}
.offer p{margin:0;color:var(--calm-lead);font-size:.96rem;line-height:1.55}
/* Pushed to the bottom so four cards of different lengths still end on one line. */
.offer-go{margin-top:auto;color:var(--series);font-weight:600;font-size:.92rem}

/* The pill again, at the size a card can carry: on a hero it is the loudest thing on
   the page, and inside a card it would fight the heading two lines below it. */
.pill-quiet{font-size:.72rem;padding:.2rem .6rem;margin:0 0 var(--s3)}

.shots{display:grid;gap:clamp(1.6rem,3vw,2.4rem);margin:var(--s6) 0 var(--s5)}
.shots .figure{margin-top:0}
/* On a card the picture is the first thing, so it sits flush to the top and keeps the
   card's own corners. */
.ca-shot{display:block;width:calc(100% + 2 * var(--s5));max-width:none;
margin:var(--s4) calc(-1 * var(--s5)) var(--s4);border-top:1px solid var(--line);
border-bottom:1px solid var(--line);background:var(--deep-2)}

.shot{margin-top:clamp(2.4rem,5vw,3.6rem)}
.shot-img{display:block;width:100%;height:auto;
border-radius:14px;border:1px solid var(--calm-line);box-shadow:var(--lift-2);
background:var(--deep-2)}

/* An install-bound permission is a sentence, not a path, so it is not dressed as code.
   Italic rather than mono is the whole point: the reader should not go looking for
   "a path you choose" in their namespace. */
.bound{font-style:italic;color:var(--calm-lead)}
.egress-row{margin:0 0 .35rem}
.egress-row:last-child{margin-bottom:0}

/* The area a review check belongs to, beside its severity. Small and uppercase: it is
   a label for scanning, not a word to read. */
.chk-area{display:inline-block;min-width:8.5rem;margin:0 .6rem;font-size:.72rem;
letter-spacing:.09em;text-transform:uppercase;color:var(--muted);font-weight:600;
vertical-align:baseline}
@media (max-width:44rem){.chk-area{min-width:0;margin-left:0}}

.fig-note{margin:var(--s4) 0 0;color:var(--calm-mute);font-size:.9rem;max-width:44rem}
.fig-table{margin-top:var(--s5);border-collapse:collapse;font-size:.92rem;
font-variant-numeric:tabular-nums}
.fig-table caption{text-align:left;color:var(--calm-mute);font-size:.85rem;
padding-bottom:var(--s2)}
.fig-table th{text-align:left;font-weight:600;color:var(--calm-ink);
padding:.4rem 1.4rem .4rem 0;border-bottom:1px solid var(--calm-line)}
.fig-table td{padding:.4rem 1.4rem .4rem 0;border-bottom:1px solid var(--line-2);
color:var(--calm-lead)}

/* --- motion -----------------------------------------------------------------
   All of it opt-in and all of it off under `prefers-reduced-motion`. The reveal
   is applied by script to elements that are already visible, so a browser with
   no IntersectionObserver -- or no JavaScript -- simply sees the page. Hiding
   content first and showing it with script is one broken script from a blank
   page. */
@media (prefers-reduced-motion:no-preference){
  /* A whole block is never invisible at rest (28 September). It used to wait at
     opacity 0 until it scrolled into view, and anyone scrolling quickly -- or a link
     preview, or a screenshot -- met empty white panels where the illustrations were.
     Now the resting state is the finished page, and a block arriving gets a short rise
     from partly visible. The script starts it just before the block reaches the screen. */
  .js-reveal.is-in{animation:rise .5s cubic-bezier(.22,.61,.36,1) backwards}
  @keyframes rise{from{opacity:.35;transform:translateY(12px)} to{opacity:1;transform:none}}
  .steps-lift .js-reveal.is-in:nth-child(2){animation-delay:.08s}
  .steps-lift .js-reveal.is-in:nth-child(3){animation-delay:.16s}

  /* The package makes the journey the page is describing. Four seconds, and it
     pauses at the gate, because the pause is the point. */
  .pkg-move{animation:pkg 5.5s cubic-bezier(.5,0,.5,1) infinite}
  @keyframes pkg{
     0%   {transform:translateY(-86px);opacity:0}
     12%  {opacity:1}
     42%  {transform:translateY(0);opacity:1}
     58%  {transform:translateY(0);opacity:1}
     78%  {transform:translateY(86px);opacity:1}
     88%  {transform:translateY(86px);opacity:0}
    100%  {transform:translateY(86px);opacity:0}
  }
  .gate-pulse{animation:gatep 5.5s ease-in-out infinite}
  @keyframes gatep{
    0%,44%{opacity:.5} 52%{opacity:1} 62%{opacity:.5} 100%{opacity:.5}
  }
  /* Data moving down the tangle. The five paths carry the argument of the drawing --
     five sources arriving at one place -- and a still drawing makes a reader work that
     out from the geometry. Slow and low-contrast on purpose: this sits behind a headline
     somebody is reading, and anything faster competes with it. Each path is offset so
     they do not pulse in unison, which would read as a loading state. */
  .tangle{stroke-dasharray:7 11;animation:flow 3.4s linear infinite}
  @keyframes flow{to{stroke-dashoffset:-36}}
  .branch{stroke-dasharray:5 9;animation:flow 4.6s linear infinite}
  /* --- the assembly ------------------------------------------------------------------
     The motion is the argument, not decoration. Four parts that were never designed to
     sit together arrive separately and settle; the couplings appear after them, because
     the joints are what you are left holding. The one box rises once, whole, with no
     stagger — a thing that arrives rather than a thing you assemble.

     Everything starts from its finished position and is *moved away* by the animation's
     first keyframe, so a browser that never runs it shows the completed drawing. Hiding
     the parts in the stylesheet and revealing them with script is one broken script from
     an empty figure. */
  /* Hidden only under `.js-reveal`, which the script adds to the figure -- so a browser
     with no IntersectionObserver, no JavaScript, or a script that threw shows the
     finished drawing instead of an empty panel. The rules below were written without
     that scope for about ten minutes, which is the mistake the comment at the top of this
     block already warns about and which I made anyway. */
  .js-reveal .slab-in{opacity:0}
  .js-reveal.is-in .slab-in{animation:slab-drop .62s cubic-bezier(.22,1.2,.4,1) forwards}
  .js-reveal.is-in .slab-in-0{animation-delay:.05s}
  .js-reveal.is-in .slab-in-1{animation-delay:.17s}
  .js-reveal.is-in .slab-in-2{animation-delay:.29s}
  .js-reveal.is-in .slab-in-3{animation-delay:.41s}
  @keyframes slab-drop{
    from{opacity:0;transform:translate3d(0,-26px,0)}
    to  {opacity:1;transform:none}
  }
  .js-reveal .joint-in{opacity:0}
  .js-reveal.is-in .joint-in{animation:joint-set .5s ease-out .72s forwards}
  @keyframes joint-set{from{opacity:0;transform:scale(.82)} to{opacity:1;transform:none}}
  /* The one place a joint keeps asking for attention afterwards. Slow enough to read as
     a state rather than as a blinking light. */
  .js-reveal.is-in .joint-in .bolt{animation:bolt-pulse 3.6s ease-in-out 1.3s infinite}
  @keyframes bolt-pulse{0%,100%{opacity:.75} 50%{opacity:.32}}

  /* --- pair and play -----------------------------------------------------------------
     Five machines join one at a time: the line draws, the machine comes up in its own
     colour, its value appears in the name space below. 0.42s apart -- slow enough to read
     as five separate events, quick enough to be over in three seconds.

     Scoped under `.js-reveal` like everything else here: that class is added by the same
     script that later removes it, so a browser which cannot run it shows the finished
     drawing rather than an empty frame. */
  .js-reveal .pair-dev{opacity:.18}
  .js-reveal .pair-tag{opacity:0}
  .js-reveal .pair-line{stroke-dasharray:340;stroke-dashoffset:340}
  .js-reveal.is-in .pair-dev{animation:pair-wake .5s ease-out forwards}
  .js-reveal.is-in .pair-tag{animation:pair-name .45s cubic-bezier(.22,1,.4,1) forwards}
  .js-reveal.is-in .pair-line{animation:pair-draw .55s ease-in-out forwards,
                              flow 3.4s linear 3.6s infinite}
  @keyframes pair-wake{from{opacity:.18} to{opacity:1}}
  @keyframes pair-name{
    from{opacity:0;transform:translate3d(-10px,0,0)}
    to  {opacity:1;transform:none}
  }
  @keyframes pair-draw{to{stroke-dashoffset:0}}
  /* One machine per beat, and in the order it happens: the line reaches the platform,
     the machine wakes, then the name appears. */
  .js-reveal.is-in .pair-line.pair-0{animation-delay:.15s,3.6s}
  .js-reveal.is-in .pair-line.pair-1{animation-delay:.57s,3.6s}
  .js-reveal.is-in .pair-line.pair-2{animation-delay:.99s,3.6s}
  .js-reveal.is-in .pair-line.pair-3{animation-delay:1.41s,3.6s}
  .js-reveal.is-in .pair-line.pair-4{animation-delay:1.83s,3.6s}
  .js-reveal.is-in .pair-dev.pair-0{animation-delay:.42s}
  .js-reveal.is-in .pair-dev.pair-1{animation-delay:.84s}
  .js-reveal.is-in .pair-dev.pair-2{animation-delay:1.26s}
  .js-reveal.is-in .pair-dev.pair-3{animation-delay:1.68s}
  .js-reveal.is-in .pair-dev.pair-4{animation-delay:2.10s}
  .js-reveal.is-in .pair-tag.pair-0{animation-delay:.62s}
  .js-reveal.is-in .pair-tag.pair-1{animation-delay:1.04s}
  .js-reveal.is-in .pair-tag.pair-2{animation-delay:1.46s}
  .js-reveal.is-in .pair-tag.pair-3{animation-delay:1.88s}
  .js-reveal.is-in .pair-tag.pair-4{animation-delay:2.30s}

  .js-reveal .whole-in{opacity:0}
  .js-reveal.is-in .whole-in{animation:whole-rise .7s cubic-bezier(.22,.9,.3,1) .12s
  forwards}
  @keyframes whole-rise{
    from{opacity:0;transform:translate3d(0,18px,0) scale(.985)}
    to  {opacity:1;transform:none}
  }

  /* The curve draws itself the first time it is seen, once. */
  .c-line{stroke-dasharray:2600;stroke-dashoffset:2600}
  .figure.is-in .c-line{animation:draw 1.5s cubic-bezier(.33,.9,.5,1) forwards}
  @keyframes draw{to{stroke-dashoffset:0}}
}
/* --- calm --------------------------------------------------------------------
   Big quiet headings, a lot of air beneath them, body text in grey, a narrow
   measure, and white almost everywhere. The restraint is the professionalism.

   The `+` inside these clamps must have spaces around it. CSS math functions require
   whitespace-separated operators, so `1.3rem+3.4vw` is a parse error -- and a parse
   error inside a declaration drops that declaration silently, leaving the heading at
   whatever the base rule said. These three shipped broken and the headings were the
   old size for a week without anything appearing wrong. */

.wrap{max-width:1200px}

/* Scoped to `.mkt`, and that scoping is the point. A 56px headline is right above a
   sentence somebody is deciding whether to believe, and wrong above a licence table or
   a sign-in form -- those are screens people work in, where a heading is a label. Left
   unscoped these rules put a marketing headline on top of every admin page on the site,
   which is what they were doing. The base scale in the type section still serves those. */
.mkt h1{font-family:var(--display);font-size:clamp(2.4rem,1.4rem + 4.4vw,4.5rem);
line-height:1.02;letter-spacing:-.035em;font-weight:700;margin:0 0 var(--s5)}
.mkt h2{font-family:var(--display);font-size:clamp(1.8rem,1.2rem + 2.2vw,3rem);
line-height:1.08;letter-spacing:-.028em;font-weight:700;margin:0 0 var(--s5)}
.mkt h3{font-size:1.38rem;line-height:1.26;letter-spacing:-.015em;font-weight:700}
/* The rhythm is not more air everywhere. On the reference site a heading that stands
   alone gets up to 144px beneath it, but a heading with a lead under it gets 16px and
   the long pause comes after the lead instead. So: heading, its lead close underneath,
   then real silence before the content. */
/* Deliberately not `.mkt h2 + .lede`: inside a paired header the two are siblings, so a
   negative margin meant for the stacked case pulls the lead 11px up out of the block it
   belongs to. The stacked case has its own rule in the media query below. */

/* Heading and its lead, side by side, on one baseline. Below 64rem they stack, which is
   what they did everywhere before this. */
/* The heading gets the larger share. At 64px a three-word line is already wide, and an
   even split left "How an app reaches your machine" breaking across three ragged lines
   while the lead beside it used half of its column. */
.sec-head{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(0,1fr);
gap:clamp(2rem,4vw,4rem);align-items:end}
/* Both tracks start at the top of the row and the lead is pushed down to meet the
   heading's last line, rather than both being bottom-aligned -- with `end` alone a lead
   taller than its heading grows upward and climbs out of the section. */
.sec-head > *{align-self:start}
.sec-head .lede{margin-top:auto}
.sec-head h1,.sec-head h2{margin:0;max-width:none}
/* A second shelf under the first is a subheading of the page, not a second front door.
   The marketing scale is right for "Apps" and absurd over one card, and the fix is the
   size rather than the level — the outline still has to make sense to a reader who
   cannot see either. */
.sec-head-small{margin-top:clamp(3rem,6vw,4.5rem)}
.sec-head-small h2{font-size:clamp(1.5rem,1.1rem + 1.4vw,2.1rem);letter-spacing:-.02em}
.sec-head .lede{margin:0;max-width:26rem;padding-bottom:.35em}
@media (max-width:64rem){
  .sec-head{grid-template-columns:1fr;gap:var(--s4)}
  .sec-head .lede{margin-top:calc(var(--s5) * -.45)}
}
.mkt .points,.mkt .facts,.mkt .plans,.mkt .steps-lift,.mkt .list-calm,
.mkt .qa-calm,.mkt .open-items-calm,.mkt .spec-calm{margin-top:clamp(3rem,6vw,5.5rem)}
.sec-head + *{margin-top:clamp(3rem,6vw,5.5rem)}
/* A heading with nothing between it and the content carries that pause itself. */
.mkt h2 + .points,.mkt h2 + .facts,.mkt h2 + .steps-lift{margin-top:clamp(3rem,7vw,6rem)}

/* A band, not a border. Sections separate by the faintest change of ground -- which is
   what a page does when it is confident there is nothing to shout about. */
.band-calm{background:var(--band-1);margin:0 calc(-50vw + 50%);
padding:clamp(4rem,9vw,7.5rem) calc(50vw - 50%)}
.band-calm-2{background:var(--band-2);margin:0 calc(-50vw + 50%);
padding:clamp(4rem,9vw,7.5rem) calc(50vw - 50%)}
.air{padding:clamp(3.5rem,7vw,6rem) 0}
/* A band already carries its own air; the section before it must not add a second
   helping, or the gap reads as something failing to load. */
.air + .band-calm,.air + .band-calm-2,.air + .dark-calm{margin-top:calc(-1 * clamp(3.5rem,7vw,6rem))}
.air-top{padding-top:clamp(4rem,9vw,7.5rem)}

.measure{max-width:34rem}
.measure-wide{max-width:44rem}
.lede{font-size:clamp(1.08rem,1rem + .62vw,1.375rem);line-height:1.5;
color:var(--calm-lead);max-width:30rem;margin:0}
.sub{color:var(--calm-mute);font-size:.96rem;line-height:1.6}

/* A quiet label. Sentence case, no letterspaced monospace shouting. */
.kicker{font-size:.9rem;font-weight:600;color:var(--accent-text);margin:0 0 var(--s3)}

.calm-actions{display:flex;gap:var(--s3);flex-wrap:wrap;margin-top:var(--s6)}
.btn-calm{display:inline-flex;align-items:center;justify-content:center;
padding:.85rem 1.6rem;border-radius:8px;font-size:.98rem;font-weight:600;
background:var(--orange);color:var(--navy);border:1px solid var(--orange);text-decoration:none}
.btn-calm:hover{background:var(--orange-hover);border-color:var(--orange-hover);text-decoration:none}
.btn-calm-2{display:inline-flex;align-items:center;justify-content:center;
padding:.85rem 1.6rem;border-radius:8px;font-size:.98rem;font-weight:600;
background:#fff;color:var(--navy);border:1px solid var(--calm-line);text-decoration:none}
.btn-calm-2:hover{border-color:#C6CFDB;text-decoration:none}
.btn-warm{display:inline-flex;align-items:center;justify-content:center;
padding:.85rem 1.6rem;border-radius:8px;font-size:.98rem;font-weight:600;
background:var(--orange);color:var(--navy);border:1px solid var(--orange);text-decoration:none}
.btn-warm:hover{background:var(--orange-hover);border-color:var(--orange-hover);
text-decoration:none}

/* --- an eyebrow --------------------------------------------------------------------
   A section used to open on its heading, which gives a reader one line to decide from.
   Two lines -- what this section is, then what it says -- is the difference between a
   page of paragraphs and a page making an argument. Set in the mono face because that is
   the site's voice for labels, and it keeps the eyebrow from competing with the heading
   it is introducing. */
.sec-lead{min-width:0}
.eyebrow{margin:0 0 var(--s3);font-family:var(--sans);font-size:.8rem;font-weight:600;
letter-spacing:.08em;text-transform:uppercase;color:var(--copper-ink)}
.eyebrow-warm{color:var(--copper-ink)}
.sec-lead h2{margin:0}

/* --- the three forces ---------------------------------------------------------------
   The "why". Numbered, because they are not three features to browse -- they are a case,
   and a case has an order. The rule above each is the drawing sheet's, not a card's: a
   card would make them look like things we sell. */
.forces{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(19rem,100%),1fr));
gap:clamp(2rem,4vw,3.5rem)}
.force{border-top:2px solid var(--navy);padding-top:var(--s5);min-width:0}
.force-n{display:block;font-family:var(--mono);font-size:.82rem;font-weight:500;
letter-spacing:.14em;color:var(--calm-mute);margin-bottom:var(--s4)}
.force h3{margin:0 0 var(--s3);font-size:1.18rem;line-height:1.3;letter-spacing:-.01em;
text-wrap:balance}
.force p{margin:0;color:var(--calm-lead);font-size:.98rem;line-height:1.65}

/* --- the belief ---------------------------------------------------------------------
   One sentence on a dark ground with nothing else on it. Every other section is making a
   case; this one is stating the position the case is for, and it earns the interruption
   by being the only place on the page that says what we think rather than what we built. */
.belief{background:var(--deep-2);color:#fff;margin:0 calc(-50vw + 50%);
padding:clamp(4.5rem,10vw,8rem) calc(50vw - 50%);text-align:center;
background-image:radial-gradient(60% 90% at 50% 0%,rgba(34,211,238,.10),transparent 70%)}
.belief-eyebrow{margin:0 0 var(--s5);font-family:var(--sans);font-size:.8rem;
font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--edge-soft)}
.belief blockquote{margin:0 auto;max-width:40rem;font-family:var(--display);
font-size:clamp(1.45rem,1.1rem + 1.6vw,2.35rem);font-weight:500;line-height:1.32;
letter-spacing:-.02em;text-wrap:balance;color:#fff}
.belief-by{margin:var(--s6) 0 0;color:var(--on-deep-soft);font-size:.95rem}

/* --- every header light (29 September) -------------------------------------------
   Rick: "the navy is nice, but too much, everywhere" (28 September), and the next day,
   of the inner pages: "make it better like the home page". The front door went light
   first; now every header does, with the same parts -- the grid, the buttons -- on an
   off-white ground, navy for the words and orange for the one phrase that matters.
   A header carrying a photo stays dark (`has-photo`), because words over a photograph
   need the dark wash. A drawing made for the dark ground brings it along (`.plate`). */
.stage:not(.has-photo){background:#F4F7FB;color:var(--navy);
border-bottom:1px solid #E1E7EE}
.stage:not(.has-photo)::before{background:
 radial-gradient(900px 480px at 88% 8%, rgba(148,163,184,.22), transparent 62%),
 radial-gradient(700px 500px at 5% 100%, rgba(11,37,69,.05), transparent 66%),
 linear-gradient(180deg,#F8FAFD 0%,#EEF3F8 100%)}
.stage:not(.has-photo)::after{background-image:linear-gradient(rgba(11,37,69,.055) 1px,transparent 1px),
 linear-gradient(90deg,rgba(11,37,69,.055) 1px,transparent 1px);opacity:.7}
.stage:not(.has-photo) h1,.stage:not(.has-photo) h2{color:var(--navy)}
.stage:not(.has-photo) h1 em{background:none;color:var(--copper);-webkit-text-fill-color:var(--copper)}
/* The label above a headline: the same quiet label as every section's, not a badge with
   a glowing dot. */
.stage .pill{font-family:var(--sans);font-size:.8rem;font-weight:600;
letter-spacing:.08em;color:var(--copper-ink);background:none;border:0;padding:0;
border-radius:0;margin-bottom:var(--s4)}
.stage .pill i{display:none}
.stage.has-photo .pill{color:var(--copper-lit)}
.stage.has-photo h1 em{background:none;color:var(--copper-lit);
-webkit-text-fill-color:var(--copper-lit)}
/* A header with no picture beside it: the sentence under the headline may use the width
   the headline has, instead of wrapping into a narrow column beside nothing. */
.stage .measure-wide ~ .lede{max-width:44rem}
.stage:not(.has-photo) .lede{color:#3F4F66}
.stage:not(.has-photo) .stage-sub{color:#56677E}

.stage:not(.has-photo) .btn-veil{background:#fff;color:var(--navy);border-color:rgba(11,37,69,.18);
box-shadow:0 1px 2px rgba(11,37,69,.06)}
.stage:not(.has-photo) .btn-veil:hover{background:#F0F5FA;border-color:rgba(11,37,69,.3)}
.stage:not(.has-photo) .assure{color:#56677E}
.stage:not(.has-photo) .assure svg{color:var(--copper)}
.stage:not(.has-photo) .hero-shot-note{color:#56677E;opacity:1}
.stage:not(.has-photo) .hero-screen{border-color:rgba(11,37,69,.18);
box-shadow:0 30px 70px rgba(11,37,69,.30),0 0 0 1px rgba(11,37,69,.06)}
.belief.belief-light{background:#EEF3F8;color:var(--navy);
background-image:radial-gradient(60% 90% at 50% 0%,rgba(34,211,238,.10),transparent 70%)}
.belief.belief-light .belief-eyebrow{color:var(--copper-ink)}
.belief.belief-light blockquote{color:var(--navy)}

/* --- the five layers -----------------------------------------------------------------
   Two columns on a wide screen, because five items in one column is a list somebody
   scrolls past and five in a grid of three is a widow. The hairline is the drawing sheet
   again: these are parts of one object, not five separate offers. */
.layers{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(24rem,100%),1fr));
gap:0 clamp(2rem,4vw,4rem)}
/* The rule sits above each row rather than below it. With five items in two columns the
   bottom border leaves a hairline under the short column that belongs to nothing; above,
   every rule has an item under it and the last row simply ends. */
.layer{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--s4);
padding:var(--s5) 0;border-top:1px solid var(--calm-line)}
.layer-n{font-family:var(--mono);font-size:.82rem;font-weight:500;color:var(--accent-text);
padding-top:.18rem}
.layer h3{margin:0 0 var(--s2);font-size:1.02rem;letter-spacing:-.01em}
.layer p{margin:0;color:var(--calm-lead);font-size:.94rem;line-height:1.62}

/* --- the warm half -------------------------------------------------------------------
   Training and partners are the parts of this business that are people rather than
   machines, and they get the warm ground so a reader can feel the change of subject
   before reading the heading. Copper rather than a second blue: the site already has one
   cool accent doing a specific job, and a second would dilute it rather than balance it. */
.warm-band{background:linear-gradient(180deg,#F3F5F8 0%,#E8ECF1 100%);margin:0 calc(-50vw + 50%);
padding:clamp(4rem,9vw,7rem) calc(50vw - 50%)}
.warm-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(17rem,100%),1fr));
gap:clamp(1.25rem,2.5vw,2rem);margin-top:clamp(2.5rem,5vw,4rem)}
.warm-card{background:#fff;border:1px solid var(--copper-line);border-radius:var(--r3);
padding:clamp(1.5rem,3vw,2rem);border-top:3px solid var(--copper);min-width:0}
.warm-card-plain{background:transparent;border:0;border-top:2px solid var(--copper);
border-radius:0;padding:var(--s5) 0 0}
/* The same shape in the cool half of the palette. Copper on this site means people --
   training, partners, the company -- and using it for the SDK would spend the one rule
   that makes a second colour worth having. */
.tile{background:transparent;border-top:2px solid var(--navy);padding:var(--s5) 0 0;
min-width:0}
.tile h3{margin:0 0 var(--s3);font-size:1.02rem;letter-spacing:-.01em}
.tile p{margin:0;color:var(--calm-lead);font-size:.95rem;line-height:1.65}
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(17rem,100%),1fr));
gap:clamp(1.25rem,2.5vw,2rem);margin-top:clamp(2.5rem,5vw,4rem)}
.warm-card h3{margin:0 0 var(--s3);font-size:1.06rem;letter-spacing:-.01em}
.partner-form{display:grid;gap:var(--s5);max-width:44rem}
.pf-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(15rem,100%),1fr));
gap:var(--s5)}
.partner-form label{display:grid;gap:var(--s2);font-size:.92rem;font-weight:600;
color:var(--navy)}
.partner-form input,.partner-form select,.partner-form textarea{font:inherit;
font-weight:400;padding:.7rem .85rem;border:1px solid var(--calm-line);
border-radius:var(--r1);background:#fff;color:var(--ink);min-width:0}
.partner-form textarea{resize:vertical;line-height:1.6}
.partner-form input:focus-visible,.partner-form select:focus-visible,
.partner-form textarea:focus-visible{outline:2px solid var(--copper);outline-offset:1px;
border-color:var(--copper)}
.pf-note{margin:0;color:var(--calm-mute);font-size:.88rem;max-width:34rem}
/* The field only a robot fills in. Off-screen rather than display:none, which some
   robots know to skip. */
.pf-hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.partner-form button{justify-self:start}
/* A shorter stage for a page that is not the front door: the hero there carries the whole
   proposition and earns full height, and every inner page copying it makes the site feel
   like five front doors. */
.stage-short{padding-top:clamp(2.5rem,5vw,4rem);padding-bottom:clamp(2.5rem,5vw,4rem)}
.warm-card p{margin:0;color:var(--calm-lead);font-size:.96rem;line-height:1.65}
.warm-kind{margin:0 0 var(--s3) !important;font-family:var(--sans);font-size:.76rem;
font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--copper-ink)}
.warm-who{margin:var(--s4) 0 0 !important;padding-top:var(--s3);
border-top:1px solid var(--copper-line);color:var(--calm-mute);font-size:.88rem}
.scene-name{max-width:42rem}

/* --- a figure's caption ---------------------------------------------------------------
   Under the drawing rather than inside it: an isometric scene can carry five words of
   label and not a sentence, and the sentence is usually where the argument is. */
.figcap{margin:var(--s5) auto 0;color:var(--calm-mute);font-size:.93rem;line-height:1.65}
.figcap a{color:var(--accent-text)}

/* The split, with the drawing beside the steps rather than under them. Centred, because
   three cards and one tall scene do not share a baseline. */
.stage-split-flip{align-items:center}
.stage-split-flip .steps-col{grid-template-columns:1fr;margin-top:0}

/* --- a figure on its own ground --------------------------------------------------------
   The scenes are drawn for the dark hero -- pale labels, navy decks, a cyan halo -- so on
   a white section they lose most of their contrast. Rather than keep a second, lighter
   copy of every drawing in step with the first, the drawing brings its ground with it.
   A deep plate between two pale sections is also the tonal variety this page was short
   of: it reads as a figure, which is what it is. */
.plate{background:linear-gradient(168deg,var(--deep-1),var(--deep-3));
border-radius:var(--r3);padding:clamp(1.5rem,4vw,3rem) clamp(1rem,3vw,2.5rem);
box-shadow:var(--lift-3);overflow:hidden;position:relative}
.plate::after{content:"";position:absolute;inset:0;border-radius:inherit;
border:1px solid rgba(140,190,240,.14);pointer-events:none}
.plate .scene{filter:none}
.plate .figcap{color:var(--on-deep-soft);max-width:44rem;text-align:center}
.plate .figcap a{color:var(--edge-soft)}

/* `minmax(min(240px,100%),1fr)` rather than `minmax(240px,1fr)`, here and in every
   other auto-fit grid on the site. A fixed minimum is a floor the track cannot go below
   even when the column it sits in is narrower, so on a 320px phone the Q&A grid -- whose
   minimum was 20rem, wider than the whole content box -- pushed its text past the right
   edge. `min(..., 100%)` lets the track collapse to the column when it has to.

   That was worth finding rather than living with: the page now clips its horizontal
   overflow to stop the full-bleed bands scrolling sideways, so anything past the right
   edge is not merely awkward to reach, it cannot be reached at all.

   Three points, with room. No borders, no tiles -- the space does the separating. */
/* A step, as a card that lifts. The number is drawn rather than set: a line of small
   grey text saying "Step one" is a label, and a numbered mark is a sequence. */
.steps-lift{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(16rem,100%),1fr));
gap:clamp(1.25rem,2.5vw,2rem);margin-top:clamp(3rem,6vw,4.5rem);counter-reset:stp}
.lift{counter-increment:stp;position:relative;background:#fff;
border:1px solid var(--calm-line);border-radius:var(--r3);
padding:clamp(1.6rem,3vw,2.2rem);box-shadow:var(--lift-2);
transition:transform .18s,box-shadow .18s}
.lift:hover{transform:translateY(-3px);box-shadow:var(--lift-3)}
.lift::before{content:counter(stp,decimal-leading-zero);display:flex;
align-items:center;justify-content:center;width:44px;height:44px;border-radius:12px;
font-family:var(--mono);font-size:.94rem;font-weight:600;color:var(--navy);
background:var(--orange);
box-shadow:0 6px 16px -6px rgba(234,88,12,.6);margin-bottom:var(--s5)}
.lift h3{margin:0 0 var(--s3)}
.lift p{margin:0;color:var(--calm-lead);font-size:1rem;line-height:1.6}

.points{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(240px,100%),1fr));
gap:clamp(2rem,4vw,3.5rem);margin-top:clamp(2.5rem,5vw,4rem)}
.point h3{margin:0 0 var(--s3)}
.point p{margin:0;color:var(--calm-lead);font-size:1rem;line-height:1.6}
.point .idx{display:block;font-size:.85rem;font-weight:600;color:var(--calm-mute);
margin-bottom:var(--s3)}

/* Catalogue rows: generous, quiet, separated by one faint rule. */
/* The mark. Publishers will send real icons; until they do this is derived from the app
   key, so an app always looks like itself, and it is geometric rather than an initial in
   a circle -- which is what every storefront does when it has no icons. */
.mark{width:46px;height:46px;border-radius:13px;flex:none;display:block;
box-shadow:var(--lift-1)}
@media (max-width:760px){.mark{width:38px;height:38px;border-radius:11px}}

/* Facts, set as a quiet definition list rather than a bordered rail. */
.facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(160px,100%),1fr));
gap:clamp(1.75rem,3.5vw,3rem);margin-top:clamp(2.5rem,5vw,3.5rem)}
.fact dt{font-size:.9rem;color:var(--calm-mute);margin-bottom:6px}
.fact dd{margin:0;font-size:1.28rem;font-weight:700;letter-spacing:-.02em;
color:var(--calm-ink)}
.fact dd small{display:block;font-size:.85rem;font-weight:400;color:var(--calm-mute);
letter-spacing:0;margin-top:4px}

/* The dark band, lit the same way the stage is. It was a flat #12161D rectangle sitting
   under a hero built from three light sources, which read as two different pages. */
.dark-calm{position:relative;overflow:hidden;background:var(--deep-2);color:#fff;
margin:0 calc(-50vw + 50%);
padding:clamp(3.5rem,7vw,6rem) calc(50vw - 50%) clamp(3.5rem,6vw,5rem)}
.dark-calm::before{content:"";position:absolute;inset:0;pointer-events:none;
background:
 radial-gradient(900px 420px at 82% 12%, rgba(34,211,238,.13), transparent 60%),
 radial-gradient(800px 520px at 8% 96%, rgba(46,107,176,.26), transparent 64%),
 linear-gradient(180deg,var(--deep-3) 0%, var(--deep-2) 55%, var(--deep-3) 100%)}
.dark-calm > *{position:relative;z-index:1}

/* --- text on a dark band ------------------------------------------------------
   There are two dark bands with different grounds and different padding, and they
   were each given their own text colours. `.dark-calm` got all five; `.plant-band`
   got two. So inside the factory band the three headings were near-black navy on
   near-black navy at a contrast of 1.13, and their paragraphs at 2.09 — which is to
   say the front door's argument about signing, review and independence was written
   in ink the colour of the paper.

   Nothing failed. A heading takes the site's ink because that is the default, and
   the default is right everywhere except on the two sections that changed the
   ground under it. Which is exactly why these are one rule now: the next dark band
   inherits the whole set or none of it, rather than whichever lines somebody
   remembered. */
.dark-calm h2,.plant-band h2{color:#fff}
.dark-calm .lede,.plant-band .lede{color:var(--on-deep)}
.dark-calm .point p,.plant-band .point p{color:var(--on-deep)}
.dark-calm .point h3,.plant-band .point h3{color:#fff}
.dark-calm .point .idx,.plant-band .point .idx{color:var(--edge-soft)}

/* A hero with something beside the words. Below 62rem the column stacks and the
   diagram sits under the text, which is where it was before and works fine on a phone --
   the two-column arrangement is only there to stop a wide screen looking half empty. */
.hero-split{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr);
gap:clamp(2rem,5vw,4.5rem);align-items:center}
.hero-art{display:flex;justify-content:center}
@media (max-width:62rem){
  .hero-split{grid-template-columns:1fr;gap:clamp(2.5rem,6vw,4rem)}
  .hero-art{justify-content:flex-start}
}

/* The diagram, on a light ground. Thin dark strokes read as something drawn for a
   brochure; the same lines in cyan on near-black read as a terminal window. */
.dia{width:100%;height:auto;display:block;max-width:27rem}
.dia-fence{fill:#FBFCFE;stroke:#DAE1EA;stroke-width:1}
.dia-box{fill:#fff;stroke:#CBD5E1;stroke-width:1}
.dia-solid{fill:var(--navy);stroke:var(--navy)}
.dia-accent{fill:#fff;stroke:var(--edge-deep);stroke-width:1.4}
.dia-wire{stroke:#B8C4D4;stroke-width:1.2;fill:none}
.dia-accent-line{stroke:var(--edge-deep);stroke-width:1.4;fill:none}
.dia-lbl{font-family:var(--sans);font-size:10.5px;fill:#6B7280}
.dia-solid + .dia-strong,.dia-solid ~ .dia-strong{fill:#fff}
.dia-strong{font-family:var(--sans);font-size:12px;font-weight:700;fill:#111827;
letter-spacing:-.01em}
.dia-note{font-family:var(--sans);font-size:11px;fill:#6B7280;font-weight:500}
.dia-note-sm{font-family:var(--sans);font-size:10.5px;fill:#6B7280}
.dia-acc-lbl{font-family:var(--sans);font-size:10.5px;fill:var(--edge-deep);
font-weight:600}

/* --- marketing pages ------------------------------------------------------
   The pricing plans, the platform page's spec and comparison blocks, and the
   question-and-answer grids. All built from the same tokens as everything else --
   a marketing page that invents its own colours is how the site and the product
   start looking like two companies again. */

/* An inner marketing page sets its own section rhythm. Inside `.air` and `.band-calm`
   the padding does it; a bare heading in a column has nothing, and without this the
   sections of /pricing run together into one undifferentiated page. */
/* Only a heading that is a direct child of the page: on the storefront every heading
   sits inside a section that already carries its own padding. */
.mkt > h2,.mkt > .sec-head{margin-top:clamp(3.5rem,7vw,5.5rem)}
.mkt > h2 + .lede{margin-bottom:var(--s5)}
.mkt > .sec-head{margin-bottom:0}
.mkt > :first-child{margin-top:0}
.mkt > section + h2,.mkt > section + .sec-head{margin-top:0}
.mkt > .air:first-child{padding-top:clamp(2.5rem,5vw,4rem)}
/* The stage bleeds to the window edge and carries its own padding, so the page wrapper
   must not add a gap above it or it floats. */
.mkt > .stage:first-child{margin-top:calc(var(--s6) * -1)}
.stage-art{display:flex;justify-content:center;min-width:0}

/* Text that is not ours, quoted back: the installer's own release notes. */
.quote-calm{background:var(--band-1);border:1px solid var(--calm-line);border-radius:10px;
padding:var(--s5);color:var(--calm-lead);font-size:.98rem;line-height:1.6;
margin:var(--s4) 0;max-width:44rem}

/* The note under a hero sentence. On the dark ground this had a cyan dot in front of
   it to put something bright on the page; on white the sentence carries itself. */
.hero-sub{margin-top:var(--s5);color:var(--calm-mute);font-size:.92rem;max-width:34rem}
/* The same note, on the lit ground. */
.stage-sub{margin-top:var(--s5);color:var(--on-deep-soft);font-size:.92rem;
max-width:34rem}
/* An inner hero has no drawing beside it, so the words get the whole width to
   themselves rather than half of it. */
.stage .measure-wide{max-width:46rem}

/* --- a feature row (29 September) ----------------------------------------------------
   Words on one side and the picture that proves them on the other, alternating down the
   page. It replaces heading, paragraph, table, repeated eight times -- which read as a
   template filled in rather than a page somebody laid out. */
.feature{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.12fr);
gap:clamp(2rem,5vw,5rem);align-items:center;margin-top:clamp(4.5rem,9vw,7.5rem)}
.feature-top{align-items:start}
.feature-flip .feature-text{order:2}
.feature h2{margin:0 0 var(--s4)}
.feature .lede{margin:0 0 var(--s5);max-width:34rem}
.feature-text > p:not(.eyebrow):not(.lede){color:var(--calm-lead);font-size:1rem;
line-height:1.65;margin:0 0 var(--s4);max-width:34rem}
.feature-text code{font-size:.84em}
.feature-media{min-width:0}
.feature-media .figure,.feature-media .shot{margin-top:0}
.feature-media .shot-img{margin:0 auto}
.feature-list{margin:0;border-top:1px solid var(--calm-line)}
.feature-list > div{padding:var(--s4) 0;border-bottom:1px solid var(--calm-line)}
.feature-list dt{font-weight:600;color:var(--calm-ink);margin:0 0 .3rem}
.feature-list dd{margin:0;color:var(--calm-lead);font-size:.98rem;line-height:1.6}
@media (max-width:62rem){
  .feature{grid-template-columns:1fr}
  .feature-flip .feature-text{order:0}
}
/* Three facts under the drawing they describe: closer than a new section would be. */
.mkt .points-under{margin-top:clamp(2rem,4vw,3rem)}
.plans-note{margin-top:var(--s5)}

/* Questions that open one at a time. Seven answers laid out at once was a wall; the
   questions alone are the overview, and a reader opens the one that is theirs. */
.faq{margin-top:clamp(2rem,4vw,3rem);border-top:1px solid var(--calm-line);max-width:54rem}
.faq details{border-bottom:1px solid var(--calm-line)}
.faq summary{cursor:pointer;list-style:none;position:relative;
padding:1.15rem 2.75rem 1.15rem 0;font-weight:600;font-size:1.06rem;color:var(--calm-ink)}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after,.fold summary::after{content:"";position:absolute;right:.6rem;top:50%;width:.55rem;
height:.55rem;border-right:2px solid var(--calm-mute);border-bottom:2px solid var(--calm-mute);
transform:translateY(-70%) rotate(45deg);transition:transform .2s}
.faq details[open] summary::after,.fold[open] summary::after{transform:translateY(-25%) rotate(225deg)}
/* A long text somebody may want, folded (the release notes): the page is a download page
   first, and a changelog laid open is most of its length. */
.fold{margin-top:var(--s5);border:1px solid var(--calm-line);border-radius:12px;
background:#fff;max-width:54rem}
.fold summary{cursor:pointer;list-style:none;position:relative;
padding:1.05rem 3rem 1.05rem 1.25rem;font-weight:600;color:var(--calm-ink)}
.fold summary::-webkit-details-marker{display:none}
.fold summary::after{right:1.3rem}
.fold summary:hover{color:var(--copper-ink)}
.fold summary:focus-visible{outline:2px solid var(--copper);outline-offset:3px;border-radius:10px}
.fold .quote-calm{margin:0 1.25rem 1.25rem;max-width:none}
.faq summary:hover{color:var(--copper-ink)}
.faq summary:focus-visible{outline:2px solid var(--copper);outline-offset:3px;border-radius:4px}
.faq details p{margin:0 0 1.3rem;color:var(--calm-lead);line-height:1.65;max-width:46rem}
@media (prefers-reduced-motion:reduce){.faq summary::after{transition:none}}

/* The end of a marketing page (`_closing_band`). */
.close-band,.cta-calm{background:linear-gradient(180deg,#F3F5F8 0%,#E8ECF1 100%);
margin:clamp(4.5rem,9vw,7rem) calc(-50vw + 50%) 0;
padding:clamp(3.5rem,7vw,5.5rem) calc(50vw - 50%);text-align:center;
border-top:1px solid #E1E7EE}
.close-band h2,.cta-calm h2{margin:0 auto var(--s4);max-width:36rem;
text-wrap:balance}
.close-band p,.cta-calm p{margin:0 auto;max-width:36rem;color:var(--calm-lead);
font-size:1.06rem;line-height:1.6}
.close-band .calm-actions,.cta-calm .calm-actions{justify-content:center;
margin-top:var(--s6)}
.mkt > .band-calm + .close-band,.mkt > .band-calm + .cta-calm{margin-top:0}
/* The last thing before the footer: it takes back the page's bottom padding so the two
   meet, rather than leaving a white strip between them. */
.mkt > .close-band:last-child,.mkt > .cta-calm:last-child{margin-bottom:calc(var(--s8) * -1)}
.mkt > .sec-head-small + *{margin-top:clamp(1.75rem,3vw,2.5rem)}
/* A section following the stage must not add its own top padding: the stage already
   carries generous padding of its own, and two helpings read as a page that failed to
   finish loading. */
.mkt > .stage + h2,.mkt > .stage + .sec-head{margin-top:clamp(3.5rem,7vw,5.5rem)}

/* Three plans. Quiet cards with a real border and a lot of padding -- no shadow, no
   coloured fill, and the one we expect people to buy marked by a darker border rather
   than by being tinted. A pricing page that shouts about its middle column is arguing
   with the reader; the number should be able to do that on its own. */
/* `stretch`, not `start`: three cards of three different heights, with the prices at
   three different offsets and the feature lists starting wherever the prose above them
   happened to end, is the thing that makes a pricing table look homemade. Equal heights
   plus `margin-top:auto` on the list puts the prices on one line and the lists on
   another. */
.plans{display:grid;gap:var(--s5);grid-template-columns:repeat(auto-fit,minmax(min(17rem,100%),1fr));
align-items:stretch;margin:clamp(2.5rem,5vw,3.5rem) 0 var(--s5)}
.plan{background:#fff;border:1px solid var(--calm-line);border-radius:14px;
padding:clamp(1.75rem,3vw,2.4rem);display:flex;flex-direction:column;min-width:0}
.plan-lead{border-color:#C2CBD8}
.plan-badge{display:block;margin:0 0 var(--s4);font-size:.78rem;line-height:1.4;
font-weight:600;color:var(--accent-text);letter-spacing:0;text-transform:none;padding:0}
/* Only one plan carries a badge, and without this the other two start a line higher and
   every number below them is off by that line. The empty row costs nothing and keeps
   the three cards reading as one table. */
.plan:not(.plan-lead)::before{content:"";display:block;font-size:.78rem;line-height:1.4;
height:1.4em;margin-bottom:var(--s4)}
.plan-name{font-weight:600;color:var(--calm-mute);font-size:.95rem;margin:0 0 var(--s3)}
.plan-price{margin:0;display:flex;align-items:baseline;gap:var(--s2);flex-wrap:wrap}
.plan-figure{font-size:2.6rem;font-weight:700;color:var(--calm-ink);line-height:1;
letter-spacing:-.035em}
.plan-unit{color:var(--calm-mute);font-size:.9rem}
.plan-term{margin:var(--s3) 0 var(--s5);color:var(--calm-mute);font-size:.9rem}
.plan-note{margin:0 0 var(--s5);color:var(--calm-lead);font-size:.96rem;line-height:1.6}
/* Hairline rows, not a column of dots. A dotted list is the single most generic thing
   on a pricing page and it says nothing the rule does not. */
.plan-list{list-style:none;margin:auto 0 0;padding:var(--s4) 0 0;display:grid;gap:0;
font-size:.94rem;border-top:1px solid var(--calm-line)}
.plan-list li{padding:var(--s3) 0;color:var(--calm-lead);line-height:1.5;
border-top:1px solid var(--calm-line)}
.plan-list li:first-child{border-top:0}
.plan-list li::before{content:none}

/* The staff pages still use the amber note box, and should: on an operations screen a
   tinted panel is a signal somebody needs, not decoration. It is only on a page a
   customer reads that it shouts. */
.open-items{display:grid;gap:var(--s4);grid-template-columns:repeat(auto-fit,minmax(min(17rem,100%),1fr));
margin:var(--s5) 0}
.open-item{background:var(--warn-bg);border:1px solid var(--warn-line);
border-radius:var(--r2);padding:var(--s5)}
.open-item h3{font-size:1rem;margin:0 0 var(--s2);color:var(--warn-text)}
.open-item p{margin:0;color:var(--ink);font-size:.92rem}

/* Things we have deliberately not answered. The amber box shouted; the label says the
   same thing and lets the page stay quiet. Marked as open, not dressed as a feature. */
.open-items-calm{display:grid;
grid-template-columns:repeat(auto-fit,minmax(min(18rem,100%),1fr));
gap:clamp(2rem,4vw,3.5rem);margin:clamp(2.5rem,5vw,3.5rem) 0}
.open-item-calm h3{margin:0 0 var(--s3)}
.open-item-calm p{margin:0;color:var(--calm-lead);font-size:1rem;line-height:1.6}
.open-item-calm::before{content:"Not settled";display:block;font-size:.85rem;
font-weight:600;color:var(--warn-text);margin-bottom:var(--s3)}

/* Question and answer, on white. Two columns of real prose with room between them. */
.qa-calm{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr));
gap:clamp(2rem,4vw,3.5rem);margin-top:clamp(2rem,4vw,3rem)}
.qa-calm h3{font-size:1.18rem;margin:0 0 var(--s3)}
.qa-calm p{margin:0;color:var(--calm-lead);font-size:1rem;line-height:1.6}

/* The closing block is the same band as `_closing_band` (29 September): `cta-calm` is the
   older name, kept by the pages that write their own. It was a hairline and a small
   heading, which read as the page running out rather than ending. It stays an `h2` for
   the outline: at `h3` it jumped a level on every page whose own title is an `h1`. */

/* The platform page's specification table: label and value on hairlines, no box.
   Readable on a phone without becoming a horizontal scroll. */
.spec{display:grid;gap:0;margin:clamp(2rem,4vw,3rem) 0;border:1px solid var(--line);
border-radius:var(--r2);overflow:hidden;background:var(--bg)}
.spec-row{display:grid;grid-template-columns:minmax(9rem,15rem) 1fr;gap:var(--s4);
padding:var(--s4) var(--s5);border-top:1px solid var(--line-2)}
.spec-row:first-child{border-top:0}
.spec-key{font-weight:600;color:var(--navy);font-size:.92rem;min-width:0}
.spec-val{color:var(--ink-2);font-size:.92rem;min-width:0}
.spec-val .mono{font-size:.85rem}
/* The machine's own sentence, quoted. It has to look like something copied off a screen
   rather than something we wrote, or a customer reads it as our paraphrase and stops
   matching it against what is in front of them. */
.says{margin-top:var(--s4);border-left:3px solid var(--warn-line);
padding-left:var(--s4)}
.says-lead{margin:0 0 .25rem;font-weight:600;color:var(--navy);font-size:.86rem}
.says-quote{margin:0;font-family:var(--mono);font-size:.82rem;line-height:1.55;
color:var(--ink);overflow-wrap:anywhere}
.says-answer{margin:.5rem 0 0;font-size:.88rem;color:var(--ink-2)}
.spec-calm{border:0;border-top:1px solid var(--calm-line);border-radius:0;
background:transparent}
.spec-calm .spec-row{padding:var(--s4) 0;border-top:0;
border-bottom:1px solid var(--calm-line);align-items:baseline}
.spec-calm .spec-key{color:var(--calm-ink);font-size:.98rem}
.spec-calm .spec-val{color:var(--calm-lead);font-size:.98rem;line-height:1.6}
@media (max-width:34rem){
  .spec-row{grid-template-columns:1fr;gap:var(--s1)}
}

/* A claim paired with what backs it up. Used where the page says something a reader
   has every reason to disbelieve. */

/* --- responsive ---------------------------------------------------------- */
@media(max-width:900px){
  .detail-grid{grid-template-columns:1fr}
  .decision{position:static}
  .foot-inner{grid-template-columns:1fr 1fr;gap:var(--s5)}
}
@media(max-width:1024px){
  .topbar{flex-wrap:wrap;height:auto;padding:.6rem .9rem;gap:.5rem}
  /* One row: name, menu, then whatever the visitor needs to press. The menu sits next
     to the brand rather than after the buttons, which is where a thumb goes looking. */
  .brand{order:1;flex:0 0 auto;font-size:1rem}
  .menu-button{display:flex;order:2}
  .topright{order:3;margin-left:auto}
  /* The menu is a panel that slides over the page from the right, with the page still
     there behind it, dimmed (28 September, Rick: "like other sites"). It used to open
     inside the header and push the page off the screen, so opening the menu meant
     losing where you were. A tap on the dimmed page, the cross or Escape closes it. */
  .topnav{position:fixed;top:0;right:0;bottom:0;z-index:41;width:min(22rem,86vw);
    display:flex;flex-direction:column;gap:.15rem;overflow-y:auto;overscroll-behavior:contain;
    padding:3.4rem 1rem 1.6rem;background:var(--navy);box-shadow:-18px 0 40px rgba(0,0,0,.35);
    transform:translateX(105%);visibility:hidden;transition:transform .25s ease,visibility .25s}
  .menu-check:checked~.topnav{transform:none;visibility:visible}
  .topnav a{padding:.55rem .7rem}
  .menu-backdrop{display:block;position:fixed;inset:0;z-index:40;background:rgba(4,14,28,.55);
    opacity:0;pointer-events:none;transition:opacity .25s ease}
  .menu-check:checked~.menu-backdrop{opacity:1;pointer-events:auto;cursor:pointer}
  .menu-close{display:flex;position:absolute;top:.7rem;right:.8rem;width:2.2rem;height:2.2rem;
    align-items:center;justify-content:center;border-radius:var(--r1);font-size:1.6rem;
    line-height:1;color:#dbe6f1;cursor:pointer;border:1px solid rgba(255,255,255,.25)}
  .menu-close:hover{background:rgba(255,255,255,.1);color:#fff}
  .menu-foot{display:flex;flex-wrap:wrap;gap:.55rem;margin-top:auto;padding-top:1.2rem;
    border-top:1px solid rgba(255,255,255,.14)}
  .menu-foot .who{display:block}
  body:has(.menu-check:checked){overflow:hidden}
  /* On a phone the menu is the subjects, one row each, and a subject opens with a tap
     (29 September, Rick: the subjects with their drop-downs give the overview). It used to
     show every group opened out, which made it a long list with Company off the bottom of
     the screen. The tap-to-open script is the one the bar already uses. */
  .topnav{align-items:stretch;gap:0}
  .topnav > a,.topnav > .navgroup{border-bottom:1px solid rgba(255,255,255,.08)}
  .navgroup{position:static}
  .topnav > a,.navtop{width:100%;padding:.85rem .7rem;color:#fff;font-size:1.02rem;
    font-weight:600;border-radius:var(--r1)}
  .navtop{justify-content:space-between}
  .topnav > a:hover,.navtop:hover,.navgroup.open .navtop{background:rgba(255,255,255,.07)}
  .topnav a.active,.navgroup.active .navtop{box-shadow:inset 3px 0 0 var(--orange)}
  .caret{transition:transform .2s}
  .navgroup.open .caret{transform:translateY(2px) rotate(225deg)}
  /* Shut unless tapped open: hover and focus would otherwise hold a group open on a
     touch screen, where the button keeps focus after the tap that closed it. */
  .topnav .navgroup .navmenu{display:none}
  .topnav .navgroup.open .navmenu{display:grid}
  .navmenu{position:static;min-width:0;padding:0 0 .5rem .6rem;background:none;
    border:0;box-shadow:none}
  .navmenu::before{display:none}
  .topnav .navmenu a{color:#dbe6f1;padding:.5rem .7rem}
  .topnav .navmenu a:hover{background:rgba(255,255,255,.09);color:#fff}
  .navmenu small{color:#8fa3ba}
}
@media(max-width:1024px) and (prefers-reduced-motion:reduce){.caret{transition:none}}
@media(max-width:760px){
  /* The company name is the first thing to go: the sign-out button is the part that has
     to stay reachable, and the name is on every page below anyway. */
  .who{display:none}
  .brand-sub{display:none}
  /* Shut, it is one line; open, the three narrow selects sit two to a row rather than
     four rows of one. The search field and the buttons keep the full width, because a
     half-width search box is a search box nobody types in. */
  .filter-summary{display:block;cursor:pointer;list-style:none;
    font:600 .95rem/1 var(--font-ui);color:var(--ink);
    padding:.7rem .9rem;border:1px solid var(--line);border-radius:var(--r2);
    background:var(--paper)}
  .filter-summary::-webkit-details-marker{display:none}
  .filter-summary::after{content:"";float:right;width:.5rem;height:.5rem;margin-top:.22rem;
    border-right:2px solid var(--calm-mute);border-bottom:2px solid var(--calm-mute);
    transform:rotate(45deg);transition:transform .15s ease}
  .filter-box[open] .filter-summary{margin-bottom:var(--s3)}
  .filter-box[open] .filter-summary::after{transform:rotate(-135deg);margin-top:.45rem}
  .filter-summary:focus-visible{outline:2px solid var(--edge-deep);outline-offset:2px}

  .filters{grid-template-columns:1fr 1fr}
  .filters .filter-search,.filters button,.filters .btn-clear{grid-column:1 / -1}
  .wrap{padding:var(--s5) 1.1rem var(--s7)}
  .band{margin:var(--s7) calc(50% - 50vw);padding:var(--s7) calc(50vw - 50% + 1.1rem)}
  .foot-inner{grid-template-columns:1fr}
  /* The page's bottom padding is smaller here, so the closing band takes back less. */
  .mkt > .close-band:last-child,.mkt > .cta-calm:last-child{margin-bottom:calc(var(--s7) * -1)}
}
@media(max-width:34rem){
  /* One row on a phone (29 September): the name, the menu, and the one button a visitor
     came for. It wrapped to two rows, a quarter of the first screen. Sign in and Sign out
     are in the menu's foot, so the bar does not need to carry them as well. */
  .topbar{flex-wrap:nowrap}
  .topright .btn-ghost{display:none}
  .topright .btn{padding:.45rem .8rem;font-size:.88rem}
  /* "1.77 TB" is one value, not two lines. */
  .fig-table td{white-space:nowrap}
}
