/* site.css — the public marketing pages and the signed-in pages.
   Loaded after style.css by index / pixpicker / services / contact, and by
   login / account / billing, which used to wear the admin panel's chrome.

   This file holds the site's identity — the light-table palette and the
   typeface, at the top — and its structure: bands, the hero grid, the service
   cards, the auth split. style.css's own tokens are pointed at the palette on
   `.site` only, so the admin panel, which never loads this file, keeps its own.

   The accent, brand and status tokens are still style.css's, shared with the
   panel and ported from the app's key-art palette; what differs here is the
   ground, the ink and the type. No test catches a colour regression, so a
   change to a shared token still means looking at /admin as well as /.

   Nothing here shares a name with anything in style.css. That is deliberate:
   .chip is already defined twice with opposite intent (style.css has a neutral
   pill, index.html's inline block silently re-skinned it as an accent pill) and
   cascade order decides which one you get. Every class here is prefixed so that
   cannot happen again. */

/* Archivo, self-hosted: no page may fetch from a third party
   (test_privacy_policy.py). One variable file carries both axes the site uses —
   weight, and width, which does the expressive work: display type is set wide,
   captions condensed, like the edge print on film. */
@font-face {
  font-family: "Archivo PP";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  font-stretch: 62% 125%;
  src: url("/static/fonts/archivo-12020a6fdc.woff2") format("woff2-variations"),
       url("/static/fonts/archivo-12020a6fdc.woff2") format("woff2");
}

:root {
  /* The light table. A photographer culls by laying the contact sheet out on a
   * lit table, marking the keeper and letting the rest drop away. Every site
   * page is built from the same three surfaces:
   *   --table  the page ground: cool, faintly green, never cream;
   *   --sheet  what is laid on it — panels, cards, the emphasised band;
   *   --mount  the one dark surface a page may have — the keeper's mount on
   *            /, the sign-in pitch, the app icon's plate.
   * The brand's deep teal is the ink, which ties the body to the chrome.
   *
   * LIGHT values; the dark ones are in the two guarded blocks below. Measured:
   * ink 13.7:1 on the table and 15.5:1 on the sheet; dim 5.8:1 and 6.6:1.
   * Cyan and gold never land on the table or the sheet — 1.9:1 and 1.5:1 on a
   * light ground — only on the mount (8.2, 9.2) or the chrome. */
  --table:   #eef2ef;
  --sheet:   #ffffff;
  --ink:     #072832;
  --ink-dim: #4a6168;
  --line:    rgba(7, 40, 50, .14);
  --edge:    rgba(7, 40, 50, .32);
  --mount:   #072832;
  --on-ink:  #ffffff;   /* the label on an ink fill */

  /* The emphasised band — what `.band-dark` paints, despite the name: it is
   * the sheet band now, in both themes, and the alternation is table / sheet.
   * --canvas-2 is what sits raised ON that band, so it is the table colour;
   * on a plain band the raised ground is the sheet (see `.band`). Derived here
   * rather than repeated in the dark blocks: a var() in :root resolves against
   * :root's own --sheet, which the dark blocks change. */
  --canvas:      var(--sheet);
  --canvas-2:    var(--table);
  --canvas-3:    color-mix(in srgb, var(--ink) 6%, var(--table));
  --canvas-line: var(--line);
  --canvas-edge: var(--edge);
  --canvas-text: var(--ink);
  --canvas-dim:  var(--ink-dim);

  /* The chrome — the sticky nav and the footer — stays the key art's deep teal
     in BOTH themes, and that is not laziness. The two-tone wordmark is gold
     "Pix" and cyan "Picker", and gold is 1.68:1 on white: on a light bar the
     word "Pix" simply disappears. A branded dark bar over a light page is a
     deliberate constant. .site-nav and .site-foot alias --canvas-* to these,
     so every rule inside them keeps working untouched. */
  --chrome:      #072832;
  --chrome-2:    #0d3642;
  --chrome-3:    #154654;
  --chrome-line: rgba(255, 255, 255, .10);
  --chrome-edge: rgba(255, 255, 255, .18);
  --chrome-text: #eaf4f6;
  --chrome-dim:  #9fb4ba;

  /* Two hues that have to read on the sheet in both themes. The deep one on
     white, the bright one on the dark sheet — a single value cannot do both
     (--svc-4 is 2.2:1 on white). */
  --canvas-green: #15601a;   /* 7.4:1 on the sheet; #1c7a20 is only 5.3 */
  --canvas-amber: #8a4b06;
  /* The gold card is a fill with no edge of its own. On a dark ground it needs
     none; on the light table it is 1.5:1 and fails 1.4.11 without one. */
  --gold-edge:    #7a5600;

  /* The funnel's tunnel, painted on a canvas. demo.js reads these with
     getComputedStyle and caches them, so theme.js fires a `themechange` event
     to make it re-read — a switch would otherwise leave the tunnel drawn in
     the colours of the theme you just left. Tinted with the ink, so it is the
     same teal on the light table and pale on the dark one. */
  --fn-pipe:   color-mix(in srgb, var(--ink) 6%, transparent);
  --fn-pipe-2: color-mix(in srgb, var(--ink) 16%, transparent);
  --fn-edge:   color-mix(in srgb, var(--ink) 40%, transparent);

  /* One colour per step, so five destinations do not read as one. Theme-aware
     for the same reason --canvas-green is: the bright hues are 4.5:1 on the
     dark sheet and about 2:1 on white, so the light theme uses their readable
     depths. Assigned by index in demo.js, never by step name — the step names
     live in the manifest and test_demo_set.py keeps them out of the script. */
  --fn-1: #04616f;   /* clutter    */
  --fn-2: #8a4b06;   /* quality    */
  --fn-3: #5733c0;   /* duplicates */
  --fn-4: #952838;   /* people     */
  --fn-5: #15601a;   /* scenes     */

  /* What is legible ON one of those five, for the filled chip in the step rail.
     It cannot be a single value: the light theme's hues are deep, so white sits
     on them at 6.8:1 or better, and the dark theme's are bright, where white
     would be about 1.5:1. The same split --canvas-green already needs. */
  --fn-ink: #ffffff;

  /* The key art's three colours, unmodified. Fills only — on white they are
     1.68, 1.88 and 2.21 to one, so none of them is ever type on a light page,
     and they appear only on the mount or the chrome. */
  --brand-gold:  #f8c000;
  --brand-cyan:  #00d0e8;
  --brand-green: #40c830;

  /* A focus ring has to read on every ground this site has. The vivid cyan is
     1.88:1 on white — a 1.4.11 failure — so the ring gets its own value: it
     clears 3:1 on the sheet (4.13) and the table (3.7). */
  --focus: #00899f;

  /* No shadows. Prints and panels lie flat on the table and a 1px line is
     what separates them; there are no --shadow-* tokens any more. */

  --wrap: 1120px;

  /* The type scale, from the classic steps: 14, 17, 19, 21, and the display
     clamps (.h-display / .h-section). The phone mock keeps its own 10 and
     11px, because that is a drawing of a phone's UI and is to scale. */
  --t-meta:  14px;   /* captions, list items, badges, footnotes */
  --t-body:  17px;   /* body copy, buttons, feature bullets */
  --t-lede:  19px;   /* section intros, sub-heads, the sign-in pitch */
  --t-card:  21px;   /* card headings */

  /* One hue per service, used consistently on the homepage band, the services
     page and the tile icons, so the two pages read as one system.
       --svc-N     reads on --canvas
       --svc-N-bg  tint behind an icon on a light card
       --svc-N-fg  the same hue as text/icon on that tint
     Three of the six are now literally the brand: svc-1 is the key art's cyan
     and svc-4 its check green, and their tint pairs equal style.css's accent
     and success pairs. svc-3 is amber and **not** the brand gold on purpose —
     gold means the app across the whole site, and svc-3 is Data filtering, a
     service. Every -fg clears 4.5:1 on its -bg; every hue clears 4.5:1 on both
     canvas steps. */
  --svc-1: #00d0e8;  --svc-1-bg: #d8f3f8;  --svc-1-fg: #04616f;  /* selection  */
  --svc-2: #a78bfa;  --svc-2-bg: #ece6fe;  --svc-2-fg: #5733c0;  /* capture    */
  --svc-3: #ff9e2c;  --svc-3-bg: #ffead3;  --svc-3-fg: #8a4b06;  /* filtering  */
  --svc-4: #40c830;  --svc-4-bg: #e0f5dd;  --svc-4-fg: #1c7a20;  /* annotation */
  --svc-5: #fb7185;  --svc-5-bg: #fde4e8;  --svc-5-fg: #952838;  /* training   */
  --svc-6: #5b9bf0;  --svc-6-bg: #e2edfc;  --svc-6-fg: #17538f;  /* mixed      */
  --svc-7: #818cf8;  --svc-7-bg: #e0e7ff;  --svc-7-fg: #3730a3;  /* VLM        */
  --svc-8: #e879f9;  --svc-8-bg: #fae8ff;  --svc-8-fg: #86198f;  /* OCR        */
}

/* The dark theme's values, written once in a custom property block and applied
   from the two selectors below. Same shape as style.css: the media query is
   the default and carries :not([data-theme="light"]) so an explicit light
   choice wins; the attribute rule covers explicitly choosing dark on a machine
   set to light. The two lists must stay identical. Measured: ink 16.0:1 on the
   table and 13.3:1 on the sheet, dim 8.6 and 7.1, cyan on the mount 6.4, gold
   7.1. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --table:   #031419;
    --sheet:   #0a2731;
    --ink:     #e4efee;
    --ink-dim: #9db3b8;
    --line:    rgba(228, 239, 238, .12);
    --edge:    rgba(228, 239, 238, .30);
    --mount:   #0e3c48;
    --on-ink:  #031419;

    /* The pale tints would glare on a dark page; the bright hue becomes the
       foreground and the tint becomes a wash of itself. */
    --svc-1-bg: #06303a;  --svc-1-fg: #5ee2f5;
    --svc-2-bg: #1f1a3d;  --svc-2-fg: #bda6ff;
    --svc-3-bg: #3a2408;  --svc-3-fg: #ffb260;
    --svc-4-bg: #0e2c10;  --svc-4-fg: #63d255;
    --svc-5-bg: #351a21;  --svc-5-fg: #fd93a2;
    --svc-6-bg: #10243d;  --svc-6-fg: #8fbef7;
    --svc-7-bg: #1e1b4b;  --svc-7-fg: #a5b4fc;
    --svc-8-bg: #3b0d3f;  --svc-8-fg: #f0abfc;

    --canvas-green: #40c830;
    --canvas-amber: #ff9e2c;
    --gold-edge:   transparent;
    --fn-1: #00d0e8;  --fn-2: #ff9e2c;  --fn-3: #a78bfa;
    --fn-4: #fb7185;  --fn-5: #40c830;
    --fn-ink: #05202a;
  }
}
:root[data-theme="dark"] {
  --table:   #031419;
  --sheet:   #0a2731;
  --ink:     #e4efee;
  --ink-dim: #9db3b8;
  --line:    rgba(228, 239, 238, .12);
  --edge:    rgba(228, 239, 238, .30);
  --mount:   #0e3c48;
  --on-ink:  #031419;

  /* The pale tints would glare on a dark page; the bright hue becomes the
     foreground and the tint becomes a wash of itself. */
  --svc-1-bg: #06303a;  --svc-1-fg: #5ee2f5;
  --svc-2-bg: #1f1a3d;  --svc-2-fg: #bda6ff;
  --svc-3-bg: #3a2408;  --svc-3-fg: #ffb260;
  --svc-4-bg: #0e2c10;  --svc-4-fg: #63d255;
  --svc-5-bg: #351a21;  --svc-5-fg: #fd93a2;
  --svc-6-bg: #10243d;  --svc-6-fg: #8fbef7;
  --svc-7-bg: #1e1b4b;  --svc-7-fg: #a5b4fc;
  --svc-8-bg: #3b0d3f;  --svc-8-fg: #f0abfc;

  --canvas-green: #40c830;
  --canvas-amber: #ff9e2c;
  --gold-edge:   transparent;
  --fn-1: #00d0e8;  --fn-2: #ff9e2c;  --fn-3: #a78bfa;
  --fn-4: #fb7185;  --fn-5: #40c830;
  --fn-ink: #05202a;
}

/* The chrome opts out of the theme by aliasing the canvas tokens back to the
   fixed ones. Every var(--canvas-*) inside the nav and the footer — the
   wordmark's ground, the link colours, the sign-in button's border — resolves
   to the deep teal in both themes, with no rule in either block edited. */
.site-nav, .site-foot {
  --canvas:      var(--chrome);
  --canvas-2:    var(--chrome-2);
  --canvas-3:    var(--chrome-3);
  --canvas-line: var(--chrome-line);
  --canvas-edge: var(--chrome-edge);
  --canvas-text: var(--chrome-text);
  --canvas-dim:  var(--chrome-dim);
}

/* ---------------------------------------------------------------- shell -- */

/* style.css's surface and text tokens, pointed at the light table for these
   pages only. On the body rather than :root because style.css's own dark
   blocks set them on :root with a stronger selector; a declaration on the
   body wins over anything it would otherwise inherit, in either theme, and
   the admin panel — which never loads this file — keeps its own. */
.site {
  --surface-0: var(--table);
  --surface-1: var(--table);
  --surface-2: var(--sheet);
  --text-primary:   var(--ink);
  --text-secondary: var(--ink-dim);
  --text-muted:     var(--ink-dim);
  --border:         var(--line);
  --border-strong:  var(--edge);
  font-family: "Archivo PP", var(--font-sans);
  font-stretch: 100%;
  font-size: var(--t-body); line-height: 1.55;
  background: var(--table); color: var(--ink);
}
.site main :focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }


/* These pages lay themselves out in full-bleed bands, so they do not use the
   900px `main` from style.css. Marked with class="site" on <body>. */
.site main { max-width: none; margin: 0; padding: 0; }

.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 clamp(18px, 5vw, 32px);
}
.wrap-narrow { max-width: 760px; }
/* /services: the hero and the six strips use (nearly) the whole screen. */
.wrap-wide { max-width: 1440px; }

.band { padding: clamp(52px, 8vw, 88px) 0; --canvas-2: var(--sheet); }
.band-tight { padding: clamp(34px, 5vw, 52px) 0; }

/* The emphasised band: the sheet laid across the table, in both themes. The
   name is older than the design; JS and tests know it, so it stays. Anything
   raised on it takes the table colour, the way a print sits on its mat. */
.band-dark {
  background: var(--canvas);
  color: var(--canvas-text);
  border-block: 1px solid var(--canvas-line);
  --canvas-2: var(--table);
}
.band-dark h1, .band-dark h2, .band-dark h3 { color: var(--canvas-text); }
.band-dark p { color: var(--canvas-dim); }

.band-tint { background: var(--table); }
/* Two table bands in a row would run together; a line marks the seam. */
.band:not(.band-dark) + .band:not(.band-dark) { border-top: 1px solid var(--line); }

/* ------------------------------------------------------------ typography -- */

/* One family, and its width axis does the work: display type wide, captions
   condensed. Left-aligned throughout — nothing on these pages is centred. */
.h-display {
  font-size: clamp(40px, 6.4vw, 72px);
  font-stretch: 116%; font-weight: 760;
  line-height: .96; letter-spacing: -.02em;
  margin: 0 0 24px;
  text-wrap: balance;
}
.h-section {
  font-size: clamp(28px, 4vw, 36px);
  font-stretch: 108%; font-weight: 700;
  line-height: 1.1; letter-spacing: -.01em;
  margin: 0 0 10px;
}
.lede {
  font-size: 21px; line-height: 1.4; font-weight: 450;
  margin: 0 0 28px;
  max-width: 42ch;
}
.band-dark .lede { color: var(--canvas-text); }

.sec-intro { margin: 0 0 clamp(28px, 4vw, 44px); max-width: 40rem; }
.sec-intro p { font-size: var(--t-body); margin: 0; color: var(--text-secondary); }
.band-dark .sec-intro p { color: var(--canvas-dim); }

/* ------------------------------------------------------------------ nav -- */

/* Its own element rather than .topbar: that class is the admin panel's and the
   signed-in pages', and those must not change. */
.site-nav {
  position: sticky; top: 0; z-index: 40;
  /* --canvas at .88, written out: a var() inside rgba() is not a thing, and
     like --canvas this must not follow prefers-color-scheme. */
  background: rgba(7, 40, 50, .88);
  backdrop-filter: saturate(150%) blur(12px);
  -webkit-backdrop-filter: saturate(150%) blur(12px);
  border-bottom: .5px solid var(--canvas-line);
}
.site-nav .wrap {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding-top: 12px; padding-bottom: 12px;
}
.site-brand {
  display: inline-flex; align-items: center; gap: 10px;
  text-decoration: none; color: var(--canvas-text);
}
.site-brand .logo { width: 30px; height: 30px; display: block; }
/* The wordmark, two-tone like the key art: gold "Pix", cyan "Picker".
   **Only ever on --canvas or darker.** Against this nav's composited ground
   gold is 8.99:1 and cyan 8.03:1; on a light card gold is 1.68:1 and the word
   "Pix" vanishes. The scope is what enforces that — style.css's white .topbar
   uses `.brand`, a different class, so it can never pick this up. */
.site-brand .name { font-size: 18px; font-stretch: 108%; font-weight: 650; letter-spacing: -.01em;
                    color: var(--brand-cyan); }
.site-brand .name b { color: var(--brand-gold); font-weight: inherit; }
.site-links { display: flex; align-items: center; gap: 4px; }
/* Buttons as well as links: the signed-in pages put a real <button> in here
   for Sign out, and a bare `button` keeps the UA's grey face, Arial and its own
   font-size — which on this dark bar rendered as near-white text on near-white.
   `font: inherit` and the background reset are the whole fix. */
.site-links a,
.site-links button {
  color: var(--canvas-dim); text-decoration: none;
  font-family: inherit; font-size: 15px; font-weight: 500; line-height: 1.6;
  background: transparent; border: 0; cursor: pointer; white-space: nowrap;
  padding: 7px 12px; border-radius: var(--radius);
  display: inline-flex; align-items: center; gap: 7px;
}
.site-links a:hover,
.site-links button:hover { color: var(--canvas-text); background: rgba(255, 255, 255, .07); }
.site-links a[aria-current="page"] { color: var(--canvas-text); }
.site-links .sbtn {
  color: var(--canvas-text);
  border: .5px solid var(--canvas-edge);
  margin-left: 6px;
}
.site-links .sbtn:hover { background: rgba(255, 255, 255, .10); }

/* The theme switch, injected by theme.js. Icon-only, so it needs a squarer box
   than the text links beside it. Deliberately not .opt — that class is hidden
   under 620px and a reader on a phone has as much right to the other theme. */
.theme-toggle { padding: 7px 10px; }
.theme-toggle i { font-size: 17px; line-height: 1; }

@media (max-width: 620px) {
  .site-links .opt { display: none; }   /* keep the CTA and sign-in, drop the rest */
  .site-links a { padding: 7px 9px; }
}
/* At phone width the bar holds the wordmark, Contact, the theme switch and
   Sign in, and no more. The text links drop their icons — the word says what
   the link does — rather than let "Sign in" break over two lines or push the
   page sideways. The switch keeps its icon: it has nothing else. */
@media (max-width: 420px) {
  .site-links > :not(.theme-toggle) > i { display: none; }
  .site-links .sbtn { margin-left: 2px; }
}

/* ---------------------------------------------------------------- button -- */

/* Marketing buttons, larger than .btn. Two kinds and no more: the filled one
   is the page's ask, in ink; the outlined one is a quiet way on. Controls take
   a 10px radius, which is how they differ from prints (2px) and panels (4px). */
.cta {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 12px 20px; border-radius: 10px;
  font-size: var(--t-body); font-weight: 650; font-family: inherit;
  text-decoration: none; cursor: pointer; border: 1px solid transparent;
  white-space: nowrap;
}
.cta-fill { background: var(--ink); color: var(--on-ink); border-color: var(--ink); }
.cta-fill:hover { filter: brightness(1.15); }
.cta-plain {
  background: transparent; color: var(--ink);
  border-color: var(--edge);
}
.cta-plain:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); }
.cta:active { transform: scale(.985); }
/* Off while its request runs, or while a sent email cools down (login.html). */
.cta:disabled { opacity: .6; cursor: not-allowed; transform: none; }
.cta:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.cta-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }

/* ------------------------------------------------------------ hero grid -- */

.hero-grid {
  display: grid; grid-template-columns: 1.08fr .92fr;
  gap: clamp(28px, 5vw, 56px); align-items: center;
}
@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; gap: 34px; }
}

.hero-note {
  display: inline-flex; align-items: center; gap: 8px;
  margin: 22px 0 0; font-size: var(--t-meta); color: var(--canvas-dim);
}
.hero-note i { color: var(--canvas-text); font-size: var(--t-body); }

/* ------------------------------------------------- the app's own mark -- */

/* The mark is 160x133 — a gold P and a cyan P, 3-D, on transparency, cut from
   the key art by mobile/store/assets/make_brand.py. **It is not square.** Set
   the height and let the width follow; a fixed square box squashes it. Do not
   draw it above ~64px: 133 source pixels is exactly 2x there.

   Gold and cyan are 1.68:1 and 1.88:1 on a light ground, so the mark always
   sits on the mount — which is what the icon looks like installed, on a
   launcher or a Start menu. In both themes: the dark mount is still a step up
   from the dark table, so the tile reads there too. */
.app-icon { display: block; width: auto; height: 34px; flex: none; }
.app-icon-lg { height: clamp(46px, 6vw, 64px); }
.app-plate {
  display: inline-grid; place-items: center; flex: none;
  background: var(--mount);
  border-radius: 2px;
  padding: 8px 10px;
}
.af-mark { margin-bottom: 22px; }

/* The same six, on a light page: full cards, each with a drawing of the work,
   a deliverables list and its own Quick enquiry button. Equal widths now —
   the drawings give every card a place for the eye to start, which is what
   the old 4-of-6 lead card was standing in for. */
.svc-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(14px, 2vw, 20px); }
@media (max-width: 940px) { .svc-cards { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) { .svc-cards { grid-template-columns: 1fr; } }

.svc-card {
  background: var(--sheet);
  border: 1px solid var(--line);
  border-radius: 4px; padding: 24px;
}
.svc-card .c-icon {
  width: 40px; height: 40px; border-radius: 10px;
  display: grid; place-items: center; font-size: 21px;
  background: var(--hue-bg, var(--bg-accent)); color: var(--hue-fg, var(--text-accent));
  margin-bottom: 14px;
}
.svc-card h2 {
  font-size: var(--t-card); font-stretch: 104%; font-weight: 700; letter-spacing: -.01em;
  color: var(--text-primary); margin: 0 0 8px;
}
.svc-card .c-body > p { color: var(--text-secondary); font-size: var(--t-body); line-height: 1.6; margin: 0 0 14px; }
.svc-card ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.svc-card li {
  display: flex; gap: 9px; align-items: flex-start;
  font-size: var(--t-meta); color: var(--text-muted); line-height: 1.5;
}
.svc-card li::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%; flex: none;
  background: var(--hue-fg, var(--accent)); margin-top: 7px;
}

/* --------------------------------------------------- capability strip --- */

.caps { display: flex; flex-wrap: wrap; gap: 10px; }
.cap {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 15px; border-radius: 2px;
  background: var(--sheet); border: 1px solid var(--line);
  font-size: var(--t-body); color: var(--text-primary);
}
.cap i { font-size: var(--t-lede); color: var(--hue-fg, var(--text-accent)); }

/* -------------------------------------------------------- store badges --- */

/* The badges ship as "Coming soon" with aria-disabled, and then they are
   outlines — a filled button would promise a download that is not there.
   apps.js lifts aria-disabled when a store is live, and the badge fills with
   ink. The same on every ground: the tokens carry the theme. */
.badge-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.store-badge {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 11px 18px; border-radius: 10px;
  background: var(--ink); color: var(--on-ink);
  border: 1px solid var(--ink); text-decoration: none;
  font-size: var(--t-meta); line-height: 1.15;
}
.store-badge:hover { filter: brightness(1.15); }
.store-badge i { font-size: 27px; color: inherit; }
.store-badge b { display: block; font-size: var(--t-body); font-weight: 700; }
.store-badge small { color: inherit; }
.store-badge[aria-disabled="true"] {
  cursor: default; filter: none;
  background: transparent; color: var(--ink); border-color: var(--edge);
}
.store-badge[aria-disabled="true"] small { color: var(--ink-dim); }

/* ------------------------------------------------------------- steps ----- */

/* Four sheets, numbered because they are a sequence: the order a run goes in. */
.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(12px, 1.8vw, 18px); }
@media (max-width: 940px) { .steps { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .steps { grid-template-columns: 1fr; } }

.step-card {
  background: var(--sheet); border: 1px solid var(--line);
  border-radius: 4px; padding: 20px;
}
.step-card .n {
  font-size: 28px; font-stretch: 116%; font-weight: 760; line-height: 1;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.step-card h3 {
  font-size: var(--t-lede); font-stretch: 104%; font-weight: 700; color: var(--text-primary);
  letter-spacing: -.01em; margin: 14px 0 6px;
}
.step-card p { font-size: var(--t-meta); color: var(--text-muted); margin: 0 0 14px; line-height: 1.55; }
.step-viz {
  height: 84px; border-radius: 2px; overflow: hidden;
  background: var(--table); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
}
/* Prefixed: style.css already owns .chip/.chips, and index.html's old inline
   block quietly redefined them. Two names, two meanings, no collision. */
.viz-chips { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px; justify-content: center; }
.viz-chip {
  font-size: 10px; padding: 3px 8px; border-radius: 11px;
  background: var(--svc-1-bg); color: var(--svc-1-fg); white-space: nowrap;
}
.viz-thumbs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; padding: 8px; width: 100%; }
.viz-thumbs span { padding-bottom: 100%; border-radius: 4px; }
.vt1 { background: linear-gradient(135deg, var(--svc-4-bg), var(--svc-1-bg)); }
.vt2 { background: linear-gradient(135deg, var(--svc-6-bg), var(--svc-2-bg)); }
.vt3 { background: linear-gradient(135deg, var(--svc-3-bg), var(--svc-5-bg)); }
.viz-ba { display: flex; align-items: center; gap: 12px; font-size: 12px; text-align: center; }
.viz-ba .big { font-size: 23px; font-weight: 700; line-height: 1; letter-spacing: -.02em; }
.viz-ba .big.to { color: var(--svc-4-fg); }
.viz-ba small { color: var(--text-muted); }

/* ------------------------------------------------------- contact sheet --- */

/* The 500 -> 100 demo set. Every tile is one frame of the sprite atlas
   generated by cloud/tools/make_demo_set.py, addressed by background-position
   — 500 <img> elements would be 500 requests, and the atlas is one.

   The tiles are RENDERED ILLUSTRATIONS, not photographs. Nothing on this page
   may call them photos.

   Two details that are load-bearing:
   - `--cols`/`--rows` must match the atlas (25x20). They come from the
     manifest via demo.js rather than being written here twice.
   - the step transition animates opacity only, never layout. 500 elements
     reflowing on every step is visibly janky; the one reflow that does happen
     is the last step, where the rejected tiles leave and the keepers pack. */
.sheet-grid {
  --sc: 50;
  display: grid; grid-template-columns: repeat(var(--sc), minmax(0, 1fr));
  gap: 1px; border-radius: 2px; overflow: hidden;
}
.sheet-grid i {
  display: block; aspect-ratio: 4 / 3; background-color: var(--canvas-2);
  background-image: var(--sheet-src);
  background-size: calc(var(--cols, 25) * 100%) calc(var(--rows, 20) * 100%);
  background-position: var(--x) var(--y);
  transition: opacity .45s ease, filter .45s ease;
}
.sheet-grid i.is-cut { opacity: .13; filter: grayscale(1); }
.sheet-grid i.is-gone { display: none; }

/* --------------------------------------------------------- the funnel ---- */

/* The moving version of the same 500 frames. The tiles are painted on a
   <canvas> — 500 sprites move every frame, and 500 positioned elements would
   thrash layout — but the gate labels below it are ordinary DOM, so they
   inherit these tokens, can be selected, and are readable to a screen reader.

   --fn-pipe / --fn-edge are read out of here by demo.js with
   getComputedStyle, so the drawing is themed from the same place as
   everything else rather than carrying colours of its own. */
.funnel {
  position: relative;                 /* the labels are placed over the canvas */
  margin-bottom: clamp(20px, 3vw, 30px);
}
/* Tall enough for a band above and below the tunnel: the steps sit around it
   now, alternating, because that is where the rejected frames fly to. */
.funnel canvas {
  display: block; width: 100%;
  height: clamp(260px, 34vw, 400px);
}

/* The drawing is the pause control.
   There was a Play/Pause button above the canvas; it is gone, because the
   funnel now runs while it is on screen and stops when it is not, so the button
   was asking the reader to manage something that manages itself. What the
   button also did still has to be done: this animation runs 15.5 seconds and
   loops, which needs a way to stop it (WCAG 2.2.2), and scrolling away is not
   that — it only stops what you can no longer see. So demo.js makes the canvas
   itself focusable and clickable, and these two rules are what say so on
   screen. Set by script, so a page whose JS never arrives shows no affordance
   it cannot honour, and never under reduced motion, where nothing is moving to
   pause. */
.funnel canvas { cursor: pointer; }

/* The keyboard's way to the same thing, off-screen until focused.
   Not `display:none` and not `visibility:hidden`: both remove it from the tab
   order, which would leave 2.2.2 satisfied for pointers only. Clipped to a
   single pixel instead, then given its real size back on focus so a keyboard
   reader can see what they have landed on — the skip-link pattern.

   `:focus`, not `:focus-visible`. The latter is a browser heuristic: Chrome
   does not match it on programmatic focus, so whether the control ever becomes
   visible would depend on how focus arrived. For a control that is invisible
   until focused, that is the wrong thing to leave to a heuristic — and since
   the pointer route is the drawing itself, this appearing on a click-focus
   costs nothing. */
.fn-pause {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}
.fn-pause:focus {
  position: static; width: auto; height: auto; margin: 0 0 8px;
  padding: 6px 15px; overflow: visible; clip-path: none;
  font: inherit; font-size: var(--t-meta); font-weight: 600; cursor: pointer;
  border-radius: 999px; color: var(--canvas-text);
  background: var(--canvas-2); border: 1px solid var(--canvas-line);
  outline: 2px solid var(--focus); outline-offset: 2px;
}

/* The labels lie over the canvas, because each one is now a destination: a
   rejected frame flies to its step and is absorbed there. demo.js measures
   these elements to find those targets, so their real position has to be the
   one on screen — which rules out a separate strip underneath.

   pointer-events: none so the layer never swallows a click meant for the
   Play button or a link beneath it. */
.fn-gates { position: absolute; inset: 0; pointer-events: none; }
.fn-gate {
  position: absolute; transform: translate(-50%, -50%);
  text-align: center; white-space: nowrap; line-height: 1.25;
}
.fn-lab {
  display: block; font-size: var(--t-meta); font-weight: 600;
  letter-spacing: .01em; color: var(--canvas-text);
}
.fn-n {
  display: block; font-weight: 700; font-variant-numeric: tabular-nums;
  font-size: 22px; line-height: 1.15; color: var(--hue, var(--canvas-amber));
}

/* Every step is a chip now, on the same raised ground as the two end boxes, so
   the whole row reads as one system rather than five captions and two boxes.
   --hue is set per step by demo.js (by index, from --fn-1..5) and carries both
   the left edge and the number. */
.fn-box, .fn-gates .fn-gate {
  padding: 7px 13px; border-radius: 4px;
  background: var(--canvas-2); border: 1px solid var(--canvas-line);
}
.fn-gates .fn-gate:not(.fn-box) { border-left: 3px solid var(--hue); }
/* Hugging the edge rather than centred on it — a box centred on x=0 loses its
   left half off the canvas at any width. */
.fn-start { left: 0; transform: translateY(-50%); }
.fn-end { right: 0; transform: translateY(-50%); }
.fn-start .fn-n { color: var(--canvas-text); }
.fn-kept .fn-n { color: var(--svc-4-fg); }

/* Five chips do not fit across 375px — as bare labels they clear each other
   with room to spare, so under the breakpoint they give up the ground, the
   border and the padding and keep only the colour. The two end boxes keep
   theirs: there are two of them and they hug opposite edges. */
@media (max-width: 620px) {
  .fn-lab { font-size: 11px; }
  .fn-n { font-size: 15px; }
  /* .fn-gates .fn-box, not .fn-box: the end boxes are also .fn-gate inside
     .fn-gates, so the chip rule above outranks a bare .fn-box here and this
     was silently doing nothing. */
  .fn-gates .fn-box { padding: 4px 9px; }
  .fn-gates .fn-gate:not(.fn-box) {
    padding: 0; background: none; border: 0; box-shadow: none;
  }
}

/* Frames borrowed into the phone mock and the step cards. These elements
   already carry a gradient from .cell/.vt1-3, so the shorthand has to replace
   `background` wholesale rather than set background-image under it. */
.shots .cell.has-tile,
.viz-thumbs > .has-tile {
  background: var(--canvas-2) var(--sheet-src) var(--x) var(--y) /
              calc(var(--cols, 25) * 100%) calc(var(--rows, 20) * 100%) no-repeat;
}

.sheet-read {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px;
  margin: 14px 0 0; font-variant-numeric: tabular-nums;
}
.sheet-read .n {
  font-size: clamp(22px, 2.8vw, 30px); font-weight: 700; letter-spacing: -.02em;
  line-height: 1; color: var(--canvas-text);
}
.sheet-read .lab { font-size: var(--t-meta); color: var(--canvas-dim); }
.sheet-read .cut { font-size: var(--t-meta); color: var(--canvas-green); font-weight: 600; }
.sheet-note { margin: 10px 0 0; font-size: var(--t-meta); color: var(--canvas-dim); }

@media (max-width: 900px) { .sheet-grid { --sc: 34; } }
@media (max-width: 560px) { .sheet-grid { --sc: 20; } }

/* A handful of frames at a size you can actually read. Used on /pixpicker,
   where there is room for the evidence rather than a summary of it. */
.shots-row {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(10px, 1.6vw, 16px);
}
.shots-row figure { margin: 0; }
.shots-row img {
  display: block; width: 100%; height: auto; aspect-ratio: 4 / 3;
  object-fit: cover; border-radius: 2px; background: var(--canvas-2);
}
.shots-row figcaption {
  margin-top: 8px; font-size: var(--t-meta); color: var(--text-muted);
}
.shots-row .verdict { display: block; font-weight: 600; color: var(--text-primary); }
.shots-row .verdict.is-cut { color: var(--svc-3-fg); }
@media (max-width: 720px) { .shots-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* Both of the above live on dark bands, and every colour named so far is a
   light-ground token. Same escape as .band-dark .proof, for the same reason. */
.band-dark .shots-row figcaption { color: var(--canvas-dim); }
.band-dark .shots-row .verdict { color: var(--canvas-text); }
.band-dark .shots-row .verdict.is-cut { color: var(--canvas-amber); }

/* Where there is room to look at the frames rather than the shape of the pile.
   After the media queries above, so it wins on source order. */
.sheet-grid.sheet-big { --sc: 25; gap: 2px; }
@media (max-width: 900px) { .sheet-grid.sheet-big { --sc: 20; } }
@media (max-width: 560px) { .sheet-grid.sheet-big { --sc: 14; } }

/* ---------------------------------------------------- the app window ----- */

/* The step walk, framed as the application it is describing.

   The rail across the top is the shape the real one has
   (mobile/lib/ui/wizard/step_bar.dart): chips centred above the content, each
   carrying its label and what it removed, the current one filled with its own
   hue, lifted and underlined, the ones behind it showing a check. Three states,
   and the middle one is the point - a step ahead of you is outlined, not
   greyed, because it is reachable.

   The window aliases the canvas tokens onto the page's surface tokens, exactly
   as .site-nav / .site-foot alias them onto the chrome. The band around it is
   --canvas (the sheet) and every .sheet-* rule inside names --canvas-*, so
   without this the grid and the readout would draw band colours on a window
   ground. Aliasing means not one of those rules is touched, and the window
   still follows the theme both ways. */
.app-window {
  --canvas:      var(--surface-0);
  --canvas-2:    var(--surface-1);
  --canvas-text: var(--text-primary);
  --canvas-dim:  var(--text-muted);
  --canvas-line: var(--border);
  margin-top: clamp(22px, 3.4vw, 34px);
  border-radius: 4px; overflow: hidden;
  background: var(--surface-0);
  /* --canvas-edge, not --border: this is an edge drawn ON the band, and the
     window's ground is only 1.2:1 against it (dark) and 1.13:1 (light), so the
     ring is what separates the two, not the fill. The phone mock uses the same
     token for the same reason. Deliberately NOT in the alias list above -- if
     it were aliased it would resolve to the window's own surface and vanish. */
  border: 1px solid var(--canvas-edge);
}
.aw-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 14px; background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  font-size: var(--t-meta); color: var(--text-secondary);
}
.aw-dots { display: flex; gap: 5px; }
.aw-dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--border); }
.aw-title { font-weight: 600; color: var(--text-primary); }

/* The rail and the frames share this box, because a thread starts at a chip and
   ends on a frame. */
.aw-stage { position: relative; }

/* The weave: one thread from each step to every frame still surviving after it.
   Over the frames, not behind them - behind, a thread would only ever show in
   the 2px gutters between tiles, which is not the effect. At the alpha demo.js
   draws with (.07 settled, .16 active) it veils nothing.

   pointer-events: none, or the layer swallows clicks meant for the rail. */
.aw-weave {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 1;
}

.aw-rail {
  position: relative; z-index: 2;
  display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-end;
  gap: 8px; padding: 14px 14px 0;
}

/* The strip's rule, in the current step's hue - the app draws the same 2px line
   under its bar. --rail-hue is set on the window by demo.js. */
.aw-body {
  padding: 14px; margin-top: 13px;
  border-top: 2px solid var(--rail-hue, var(--border));
}

.aw-step {
  position: relative; font: inherit; font-size: var(--t-meta); cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; border-radius: 10px;
  background: transparent; color: var(--text-primary);
  border: 1px solid var(--hue, var(--border));
  transition: background .2s ease, border-color .2s ease, color .2s ease;
}
.aw-step i { font-size: 15px; line-height: 1; color: var(--hue, var(--text-muted)); }
.aw-step .s-cut { font-size: 12px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.aw-step:hover { background: var(--surface-1); }
.aw-step:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* done - a card ground, and the hue kept on the icon alone. Every label is the
   same colour, which is what stops the rail reading as five competing things. */
.aw-step.is-done { background: var(--surface-2); border-color: var(--border); }

/* active - filled and underlined. Where you are is the first thing the rail
   should say. --fn-ink rather than white: see the token. The first chip (all
   500) and the last (the keepers) carry no step hue, and an unset --hue left
   them with no fill under white type; they fill with ink instead, which
   --fn-ink reads on in both themes. */
.aw-step[aria-current="true"] {
  background: var(--hue, var(--ink)); border-color: var(--hue, var(--ink)); color: var(--fn-ink);
  font-weight: 700;
}
.aw-step[aria-current="true"] i { color: var(--fn-ink); }
.aw-step[aria-current="true"] .s-cut { color: var(--fn-ink); opacity: .75; }
.aw-step[aria-current="true"]::after {
  content: ""; position: absolute; left: 50%; transform: translateX(-50%);
  bottom: -9px; width: 26px; height: 3px; border-radius: 2px; background: var(--hue, var(--ink));
}

.app-window + .shots-row { margin-top: clamp(20px, 3vw, 30px); }

/* The weave is 1320 threads at the last step. Across a phone that is a wall
   rather than a mesh, so it thins and then goes: demo.js reads these two
   breakpoints itself (it has to - a canvas cannot be styled into drawing fewer
   lines), and this is only the hiding half. */
@media (max-width: 480px) {
  .aw-weave { display: none; }
  .aw-rail { gap: 6px; padding: 11px 11px 0; }
  .aw-step { padding: 6px 9px; }
  .aw-step .s-cut { display: none; }
  .aw-body { padding: 10px; }
}

/* ------------------------------------------------------------- phone ----- */

/* The app mock. Every colour that was a literal in index.html is a token here,
   so it stops being a light-mode-only graphic. The handset is the mount: dark
   in both themes, as the hardware is. */
.phone-wrap { display: flex; justify-content: center; }
.phone {
  width: 268px; height: 552px; border-radius: 42px;
  background: var(--mount); padding: 11px;
  position: relative;
}
.phone::before {
  content: ""; position: absolute; top: 19px; left: 50%; transform: translateX(-50%);
  width: 88px; height: 22px; background: var(--mount); border-radius: 0 0 14px 14px; z-index: 2;
}
.screen {
  width: 100%; height: 100%; border-radius: 32px; overflow: hidden;
  background: var(--surface-0); display: flex; flex-direction: column;
}
.app-bar { padding: 26px 16px 12px; background: var(--surface-2); border-bottom: 1px solid var(--border); }
.app-bar .t { font-weight: 700; font-size: 15px; color: var(--text-primary); }
.app-bar .s { font-size: 11px; color: var(--text-muted); }
.shots {
  flex: 1; padding: 12px; display: grid; grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 1fr; gap: 6px; overflow: hidden;
}
.cell { position: relative; border-radius: 8px; }
.cell.g1 { background: linear-gradient(135deg, var(--svc-4-bg), var(--svc-1-bg)); }
.cell.g2 { background: linear-gradient(135deg, var(--svc-6-bg), var(--svc-2-bg)); }
.cell.g3 { background: linear-gradient(135deg, var(--svc-3-bg), var(--svc-5-bg)); }
.cell.drop { filter: grayscale(1) contrast(.9); opacity: .62; }
.cell .mk {
  position: absolute; top: 3px; right: 3px; width: 15px; height: 15px; border-radius: 50%;
  display: grid; place-items: center; font-size: 9px; color: #fff;
}
.cell .mk.keep { background: var(--svc-4-fg); }
.cell .mk.no { background: rgba(0, 0, 0, .5); }
.app-fab {
  margin: 0 12px 14px; padding: 11px; border-radius: 12px;
  background: var(--ink); color: var(--on-ink);
  text-align: center; font-weight: 600; font-size: var(--t-meta);
  display: flex; gap: 8px; justify-content: center;
}

/* ----------------------------------------------------------- contact ----- */

.contact-grid {
  display: grid; grid-template-columns: 1.25fr .85fr;
  gap: clamp(20px, 3vw, 34px); align-items: start;
}
@media (max-width: 860px) { .contact-grid { grid-template-columns: 1fr; } }

.panel {
  background: var(--sheet); border: 1px solid var(--line);
  border-radius: 4px; padding: clamp(20px, 3vw, 28px);
}
.panel h2 { font-size: var(--t-card); font-stretch: 104%; font-weight: 700; letter-spacing: -.01em; margin: 0 0 6px; }

/* What happens next: a numbered list because it is an order of events. It
   sits on the table beside the form's sheet, with a line rather than a fill. */
.aside-note {
  border-left: 1px solid var(--line);
  padding: 4px 0 4px clamp(18px, 2.4vw, 26px);
}
.aside-note h3 { font-size: var(--t-body); font-weight: 700; color: var(--text-primary); margin: 0 0 14px; }
.aside-note ol { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 16px; }
.aside-note li { display: flex; gap: 12px; font-size: var(--t-meta); color: var(--text-secondary); line-height: 1.55; }
.aside-note .s-n {
  flex: none; min-width: 1.2em;
  font-size: var(--t-body); font-stretch: 116%; font-weight: 760; line-height: 1.35;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.aside-note b { color: var(--text-primary); font-weight: 600; }

.form-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 15px; }
.form-field > span { font-size: var(--t-meta); font-weight: 500; color: var(--text-secondary); }
.form-field input, .form-field select, .form-field textarea { max-width: none; width: 100%; }
.form-field textarea {
  min-height: 130px; resize: vertical; padding: 10px 12px;
  border-radius: var(--radius); border: 1px solid var(--border-strong);
  background: var(--surface-2); color: var(--text-primary); font: inherit;
}
.form-field textarea:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--bg-accent);
}
.form-two { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
@media (max-width: 560px) { .form-two { grid-template-columns: 1fr; } }

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

.site-foot {
  /* style.css's `footer` rule reaches this element too, and it is a centred
     flex row — which centred the brand, the blurb and all three columns. Undo
     it explicitly rather than relying on which file loads last. */
  display: block; text-align: left; font-size: inherit;
  background: var(--canvas); color: var(--canvas-dim);
  border-top: .5px solid var(--canvas-line);
  padding: clamp(36px, 5vw, 52px) 0 30px;
}
.site-foot .f-top {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 28px; flex-wrap: wrap; margin-bottom: 28px;
}
.site-foot .f-blurb { max-width: 34ch; font-size: var(--t-meta); line-height: 1.6; margin: 12px 0 0; }
.site-foot .f-cols { display: flex; gap: clamp(28px, 6vw, 64px); flex-wrap: wrap; }
.site-foot .f-col { display: flex; flex-direction: column; gap: 9px; }
.site-foot .f-col strong {
  font-size: var(--t-meta); font-weight: 650;
  color: var(--canvas-text); margin-bottom: 2px;
}
.site-foot .f-col a { color: var(--canvas-dim); text-decoration: none; font-size: var(--t-meta); }
.site-foot .f-col a:hover { color: var(--canvas-text); text-decoration: underline; }
.site-foot .f-base {
  border-top: .5px solid var(--canvas-line); padding-top: 20px;
  display: flex; justify-content: space-between; gap: 14px; flex-wrap: wrap;
  font-size: var(--t-meta);
}
/* The marketing footer puts its links in .f-col; the signed-in pages use a
   one-line footer whose links live here, and without this they render in the
   UA's link blue and visited purple on a teal band. */
.site-foot .f-base a { color: var(--canvas-dim); text-decoration: none; }
.site-foot .f-base a:hover { color: var(--canvas-text); text-decoration: underline; }

/* -------------------------------------------------- signed-in pages ------- */

/* /login, /account and /billing wear the site's shell now, not the admin
   panel's. They used to open with style.css's white `.topbar`, which made
   signing in to PixPicker look like arriving at a different product.

   The card is capped here rather than on `main`, because `.site main
   { max-width: none }` above is what makes every band full-bleed and must not
   be undone to centre one form. */
.auth-band { padding: clamp(40px, 7vw, 84px) 0; }
.auth-grid {
  display: grid; grid-template-columns: minmax(0, 420px) 1fr;
  gap: clamp(28px, 5vw, 64px); align-items: center;
}
@media (max-width: 860px) {
  /* The pitch is reassurance, not information — it goes rather than stacking
     below the form and pushing the fields off a phone screen. */
  .auth-grid { grid-template-columns: 1fr; }
  .auth-pitch { display: none; }
}
.auth-card {
  background: var(--sheet); border: 1px solid var(--line);
  border-radius: 4px; padding: clamp(22px, 4vw, 32px);
}
.auth-card .auth-title { font-size: 28px; font-stretch: 112%; font-weight: 760; letter-spacing: -.015em; }
.auth-card .cta { width: 100%; justify-content: center; margin-top: 4px; }
.auth-sep { border: 0; border-top: 1px solid var(--border); margin: 20px 0 16px; }

/* The pitch beside the form, on the page's one mount: the two-tone wordmark
   is gold and cyan, which only read on a dark ground (9.2 and 8.2 to one on
   the light mount, 7.1 and 6.4 on the dark one). Display-size wordmark, one
   line, and the store badges — somebody signed out on /login is the likeliest
   person on the site to want the app. */
.auth-pitch {
  align-self: stretch;
  display: flex; flex-direction: column; justify-content: center;
  background: var(--mount); border-radius: 4px;
  padding: clamp(28px, 4vw, 48px);
  /* The badges and the line take the chrome's light ink on the mount. */
  --ink: var(--chrome-text); --ink-dim: var(--chrome-dim);
  --edge: var(--chrome-edge); --on-ink: var(--mount);
}
.auth-pitch .auth-mark {
  font-size: clamp(38px, 5vw, 56px); font-stretch: 116%; font-weight: 760;
  letter-spacing: -.02em; line-height: 1; margin: 0 0 16px;
}
.auth-pitch .auth-mark b { color: var(--brand-gold); font-weight: inherit; }
.auth-pitch .auth-mark i { color: var(--brand-cyan); font-style: normal; }
.auth-pitch .auth-say { font-size: var(--t-lede); max-width: 30ch; margin: 0 0 24px; color: var(--chrome-text); }
/* Repeated after the rule above, which would otherwise win on source order. */
@media (max-width: 860px) { .auth-pitch { display: none; } }

/* /account and /billing keep a readable measure on the table. They are tools,
   so nothing here is the memorable thing: style.css's cards become sheets and
   its buttons take the site's ink, and that is all. */
.auth-page { padding: clamp(28px, 4vw, 44px) 0 64px; }
.auth-page .card:last-child { margin-bottom: 0; }
.site .card { border: 1px solid var(--line); border-radius: 4px; }
.site .card .step h2 { font-size: var(--t-card); font-stretch: 104%; font-weight: 700; letter-spacing: -.01em; margin: 0; }
.site .btn { border-width: 1px; border-radius: 10px; }
.site .btn.primary { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.site .btn.primary:hover { background: var(--ink); filter: brightness(1.15); }

/* Anchored sections (/pixpicker#worked-example, /services#services) land
   below the sticky nav rather than under it. */
.site section[id] { scroll-margin-top: 64px; }

/* /services hero: text only, the full width of the page. */
.svc-hero .h-display { max-width: 22ch; margin-bottom: 14px; }

/* Every service as a bullet under the headline, A to Z; each jumps to its card. */
.svc-bullets { list-style: none; padding: 0; margin: 0; display: grid; gap: 9px; }
.svc-bullets-az {
  grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px 28px;
  max-width: 80rem; margin: 0 0 28px;
}
@media (max-width: 1100px) { .svc-bullets-az { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 900px) { .svc-bullets-az { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .svc-bullets-az { grid-template-columns: 1fr; } }

/* The search box: one rounded row on the hero, magnifier inside, a live
   count on the right. service-search.js reveals it and does the filtering. */
.svc-search {
  position: relative; display: flex; align-items: center; gap: 10px;
  width: min(100%, 36rem); margin: 4px 0 22px; padding: 0 16px 0 44px;
  background: var(--sheet); border: 1px solid var(--edge); border-radius: 10px;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.svc-search[hidden] { display: none; }
.svc-search:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
}
.svc-search-ic {
  position: absolute; left: 16px; top: 50%; width: 18px; height: 18px; transform: translateY(-50%);
  fill: none; stroke: var(--canvas-dim); stroke-width: 2.2; stroke-linecap: round;
}
.svc-search input {
  flex: 1; min-width: 0; height: 48px; padding: 0; margin: 0;
  border: 0; outline: 0; background: transparent; box-shadow: none;
  font: inherit; font-size: var(--t-body); color: var(--canvas-text);
}
.svc-search input:focus-visible { box-shadow: none; }
.svc-search input::placeholder { color: var(--canvas-dim); opacity: .85; }
.svc-count { flex: none; font-size: var(--t-meta); color: var(--canvas-dim); white-space: nowrap; }
.svc-none { margin: -8px 0 22px; color: var(--canvas-dim); }
.svc-none b { color: var(--canvas-text); }
.svc-none a { color: var(--canvas-text); font-weight: 600; }
@media (max-width: 520px) { .svc-count { display: none; } }

.svc-bullets a {
  display: inline-flex; align-items: center; gap: 10px;
  color: var(--canvas-text); text-decoration: none;
  font-size: var(--t-body); font-weight: 600;
}
.svc-bullets a::before {
  content: ""; flex: none; width: 9px; height: 9px; border-radius: 50%;
  background: var(--dot, var(--brand-cyan));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--dot, var(--brand-cyan)) 25%, transparent);
}
.svc-bullets a:hover { text-decoration: underline; text-underline-offset: 4px; }
.svc-bullets a:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 4px; }

/* ======================================== service cards: drawing + ask == */

/* One full-width strip per service, stacked: the drawing on the left on the
   service's own tint, everything to read and the Quick enquiry on the right.
   The hue rail moves to the left edge to match. Under 760px it stacks, with
   the drawing on top. */
.svc-cards { grid-template-columns: 1fr; }
.svc-card[hidden], .svc-bullets li[hidden] { display: none; }
.svc-card {
  display: grid; grid-template-columns: minmax(240px, 34%) minmax(0, 1fr);
  padding: 0; overflow: hidden; scroll-margin-top: 90px;
  border-left: 3px solid var(--hue-rail, var(--accent));
}
.svc-card:target { outline: 2px solid var(--hue-fg); outline-offset: 2px; }
.svc-card .c-body {
  display: flex; flex-direction: column; min-width: 0;
  padding: clamp(18px, 2.4vw, 28px) clamp(18px, 2.6vw, 32px);
}
.svc-card .c-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.svc-card .c-head .c-icon { margin: 0; flex: none; }
.svc-card .c-head h2 { margin: 0; }
.svc-card .c-body > p { max-width: 72ch; }
.svc-card ul {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px 22px;
}
.svc-card .c-foot { margin-top: auto; padding-top: 16px; display: flex; justify-content: flex-end; }

/* The drawing: the strip's left column, on the table like a print on its mat,
   every stroke and fill taken from --hue-fg so it re-themes with the card.
   The hue is in the drawing and the rail, not washed over the ground. 280x140
   viewBox, centred in whatever height the text gives the strip. */
.svc-viz {
  background: var(--table);
  border-right: 1px solid var(--line);
  padding: clamp(14px, 2vw, 22px);
  display: grid; place-items: center;
}
@media (max-width: 760px) {
  .svc-card { grid-template-columns: 1fr; border-left-width: 1px; border-top: 3px solid var(--hue-rail, var(--accent)); }
  .svc-viz { border-right: 0; border-bottom: 1px solid var(--line); }
}

.vz { display: block; width: 100%; height: auto; aspect-ratio: 2 / 1; overflow: visible; }
.vz .vz-n, .vz .vz-cull > rect, .vz .vz-keep > rect:first-child {
  fill: color-mix(in srgb, var(--hue-fg) 22%, transparent);
}
.vz .vz-n2 { fill: color-mix(in srgb, var(--hue-fg) 45%, transparent); }
.vz .vz-t { font-size: 10.5px; fill: var(--text-muted); font-family: inherit; }
.vz .vz-strong { fill: var(--hue-fg); font-weight: 700; }
.vz .vz-line { fill: none; stroke: var(--hue-fg); stroke-width: 2; stroke-linecap: round; }
.vz .vz-dash { stroke-dasharray: 3 4; opacity: .6; }
.vz .vz-axis { fill: none; stroke: color-mix(in srgb, var(--hue-fg) 55%, transparent); stroke-width: 1.5; }
.vz .vz-grid { fill: none; stroke: color-mix(in srgb, var(--hue-fg) 16%, transparent); stroke-width: 1; }
.vz .vz-tag rect { fill: var(--hue-fg); }
.vz .vz-tag text { fill: var(--surface-2); font-size: 9.5px; font-weight: 700; font-family: inherit; }
.vz .vz-card { fill: var(--surface-2); stroke: color-mix(in srgb, var(--hue-fg) 40%, transparent); stroke-width: 1; }

/* Every animated element's RESTING style is the finished frame. The
   animation only replays how it got there, so with reduced motion (or no
   animation support) the drawing still shows the result.

   And it replays only when asked. Seventeen drawings looping at once on one
   page is noise, so a drawing plays while its card is pointed at, holds
   focus, or is the card a hero link just landed on — and otherwise rests on
   that finished frame. One rule does it, stronger than every .vz rule below,
   so none of them had to change. */
.svc-card:not(:hover):not(:focus-within):not(:target) .vz * { animation: none; }
@media (prefers-reduced-motion: reduce) { .vz * { animation: none !important; } }

/* 1. bulk selection */
.vz .vz-cull { opacity: .22; animation: vzCull 6s ease-in-out infinite; animation-delay: var(--d, 0s); }
@keyframes vzCull { 0%, 18% { opacity: 1; } 38%, 86% { opacity: .22; } 100% { opacity: 1; } }
.vz .vz-ring { fill: none; stroke: var(--hue-fg); stroke-width: 2; animation: vzLate 6s ease-in-out infinite; }
.vz .vz-tick circle { fill: var(--hue-fg); }
.vz .vz-tick path { fill: none; stroke: var(--surface-2); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.vz .vz-tick { animation: vzLate 6s ease-in-out infinite; }
@keyframes vzLate { 0%, 40% { opacity: 0; } 52%, 88% { opacity: 1; } 100% { opacity: 0; } }

/* 2. data capture */
.vz .vz-rec { fill: var(--svc-5); animation: vzBlink 1.2s steps(2, jump-none) infinite; }
@keyframes vzBlink { 50% { opacity: .2; } }
.vz .vz-flash { fill: var(--surface-2); opacity: 0; animation: vzFlash 2.7s ease-out infinite; }
@keyframes vzFlash { 0% { opacity: .85; } 14%, 100% { opacity: 0; } }
.vz .vz-pop { animation: vzPop 2.7s ease-out infinite; animation-delay: var(--d, 0s); }
@keyframes vzPop { 0% { opacity: 0; transform: translateX(-14px); } 22%, 100% { opacity: 1; transform: none; } }

/* 3. data filtering */
.vz .vz-x { fill: none; stroke: var(--svc-5-fg); stroke-width: 2.2; stroke-linecap: round;
  animation: vzX 5s ease-out infinite; animation-delay: var(--d, 0s); }
@keyframes vzX { 0% { opacity: 0; } 10%, 100% { opacity: 1; } }
.vz .vz-funnel { fill: color-mix(in srgb, var(--hue-fg) 12%, transparent);
  stroke: var(--hue-fg); stroke-width: 1.5; stroke-linejoin: round; }
.vz .vz-drop { fill: var(--hue-fg); opacity: 0; animation: vzDrop 2.4s ease-in infinite; animation-delay: var(--d, 0s); }
@keyframes vzDrop {
  0% { opacity: 0; transform: translateY(0); } 15% { opacity: 1; }
  80% { opacity: 1; transform: translateY(70px); } 100% { opacity: 0; transform: translateY(74px); }
}
.vz .vz-out { fill: var(--hue-fg); animation: vzPop 5s ease-out infinite; animation-delay: var(--d, 0s); }

/* 4. annotation and 5. training: strokes that draw themselves (pathLength=1) */
.vz .vz-draw { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round;
  stroke-dasharray: 1; stroke-dashoffset: 0; animation: vzDraw 6s ease-in-out infinite; }
@keyframes vzDraw { 0% { stroke-dashoffset: 1; } 30%, 90% { stroke-dashoffset: 0; opacity: 1; } 100% { stroke-dashoffset: 0; opacity: 0; } }
.vz .vz-box { stroke: var(--svc-1-fg); }
.vz .vz-poly { stroke: var(--hue-fg); fill: color-mix(in srgb, var(--hue-fg) 18%, transparent);
  animation-name: vzDrawFill; }
@keyframes vzDrawFill {
  0%, 25% { stroke-dashoffset: 1; fill-opacity: 0; opacity: 1; }
  55% { stroke-dashoffset: 0; fill-opacity: 0; }
  65%, 90% { stroke-dashoffset: 0; fill-opacity: 1; opacity: 1; }
  100% { opacity: 0; }
}
.vz .vz-tag { animation: vzLate 6s ease-in-out infinite; }
.vz .vz-tag-early { animation-name: vzEarly; }
.vz .vz-tag-early rect { fill: var(--svc-1-fg); }
@keyframes vzEarly { 0%, 22% { opacity: 0; } 32%, 90% { opacity: 1; } 100% { opacity: 0; } }
.vz .vz-handles rect { fill: var(--surface-2); stroke: var(--hue-fg); stroke-width: 1.5; }
.vz .vz-handles { animation: vzLate 6s ease-in-out infinite; }
.vz .vz-curve { stroke: var(--hue-fg); }
.vz .vz-curve2 { stroke: var(--svc-4-fg); }

/* 6. something in between */
.vz .vz-node circle { fill: var(--surface-2); stroke: var(--hue-fg); stroke-width: 2;
  animation: vzNodeFill 4.8s ease-in-out infinite; animation-delay: var(--d, 0s); }
.vz .vz-node text { fill: var(--hue-fg); font-size: 12px; font-weight: 700; font-family: inherit; }
@keyframes vzNodeFill {
  0%, 30%, 100% { fill: var(--surface-2); }
  6%, 22% { fill: color-mix(in srgb, var(--hue-fg) 30%, var(--surface-2)); }
}
.vz .vz-run { fill: var(--hue-fg); animation: vzRun 4.8s linear infinite; }
@keyframes vzRun {
  0% { transform: translateX(0); opacity: 1; }
  75% { transform: translateX(204px); opacity: 1; }
  76%, 100% { transform: translateX(204px); opacity: 0; }
}


/* 7. VLM fine-tuning: a question, then the answer types in */
.vz .vz-q rect { fill: var(--surface-2); stroke: color-mix(in srgb, var(--hue-fg) 40%, transparent); stroke-width: 1; }
.vz .vz-q text { fill: var(--text-primary); font-size: 11px; font-family: inherit; }
.vz .vz-a rect { fill: var(--hue-fg); }
.vz .vz-a text { fill: var(--surface-2); font-size: 11px; font-weight: 700; font-family: inherit; }
.vz .vz-q { animation: vzEarly 6s ease-in-out infinite; }
.vz .vz-a { animation: vzLate 6s ease-in-out infinite; }
.vz .vz-type { clip-path: inset(0 0 0 0); animation: vzType 6s steps(18, end) infinite; }
@keyframes vzType { 0%, 48% { clip-path: inset(0 100% 0 0); } 72%, 100% { clip-path: inset(0 0 0 0); } }

/* 8. custom OCR: a scan bar, word boxes, extracted fields */
.vz .vz-scanbar { fill: var(--hue-fg); opacity: 0; animation: vzScan 6s ease-in-out infinite; }
@keyframes vzScan { 0% { opacity: .9; transform: translateY(0); } 40% { opacity: .9; transform: translateY(121px); } 45%, 100% { opacity: 0; transform: translateY(121px); } }
.vz .vz-words rect { fill: color-mix(in srgb, var(--hue-fg) 14%, transparent); stroke: var(--hue-fg); stroke-width: 1.4;
  animation: vzWord 6s ease-out infinite; animation-delay: var(--d, 0s); }
@keyframes vzWord { 0%, 15% { opacity: 0; } 25%, 92% { opacity: 1; } 100% { opacity: 0; } }
.vz .vz-fields rect { fill: color-mix(in srgb, var(--hue-fg) 22%, transparent); }
.vz .vz-fields g { animation: vzField 6s ease-out infinite; animation-delay: var(--d, 0s); }
@keyframes vzField { 0%, 42% { opacity: 0; transform: translateX(-8px); } 55%, 92% { opacity: 1; transform: none; } 100% { opacity: 0; } }

/* 9. edge optimisation: the big model shrinks, a chip drops into the phone */
.vz .vz-big { transform-box: fill-box; transform-origin: center; opacity: .3; transform: scale(.55);
  animation: vzBig 6s ease-in-out infinite; }
@keyframes vzBig { 0%, 15% { opacity: 1; transform: none; } 45%, 100% { opacity: .3; transform: scale(.55); } }
.vz .vz-phone { fill: none; stroke: var(--hue-fg); stroke-width: 2; }
.vz .vz-chip rect { fill: var(--hue-fg); }
.vz .vz-chip path { fill: none; stroke: var(--hue-fg); stroke-width: 2; stroke-linecap: round; }
.vz .vz-chip { animation: vzChip 6s cubic-bezier(.3, .7, .2, 1) infinite; }
@keyframes vzChip { 0%, 30% { opacity: 0; transform: translateX(-120px) scale(.6); } 45% { opacity: 1; transform: translateX(-90px); } 70%, 100% { opacity: 1; transform: none; } }

/* 10. visual search: similarity bars fill, the best match is ringed */
.vz .vz-simbg { fill: color-mix(in srgb, var(--hue-fg) 14%, transparent); }
.vz .vz-sim { fill: var(--hue-fg); transform-box: fill-box; transform-origin: left center;
  animation: vzGrowX 6s ease-out infinite; animation-delay: var(--d, 0s); }
@keyframes vzGrowX { 0%, 10% { transform: scaleX(0); } 40%, 100% { transform: none; } }
.vz .vz-top { stroke-width: 2.5; }

/* 11. defect inspection: the belt moves on, the cracked part is flagged */
.vz .vz-belt { animation: vzBelt 6s cubic-bezier(.3, .7, .2, 1) infinite; }
@keyframes vzBelt { 0% { transform: translateX(-66px); } 30%, 100% { transform: none; } }
.vz .vz-crack { fill: none; stroke: var(--svc-5-fg); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.vz .vz-flag { stroke: var(--svc-5-fg); stroke-width: 2.4; }
.vz .vz-tag-bad rect { fill: var(--svc-5-fg); }
.vz .vz-ok circle { fill: var(--svc-4-fg); }
.vz .vz-ok path { fill: none; stroke: var(--surface-2); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.vz .vz-ok { animation: vzLate 6s ease-in-out infinite; }

/* 12. video analytics: two tracks cross the count line */
.vz .vz-cline { fill: none; stroke: var(--hue-fg); stroke-width: 1.5; stroke-dasharray: 4 4;
  animation: vzCline 6s ease-in-out infinite; }
@keyframes vzCline { 0%, 30% { opacity: .35; } 38%, 46% { opacity: 1; stroke-width: 3; } 56%, 100% { opacity: .35; } }
.vz .vz-trail { stroke: color-mix(in srgb, var(--hue-fg) 60%, transparent); stroke-dasharray: 1; }
.vz .vz-trail2 { stroke: color-mix(in srgb, var(--svc-1-fg) 60%, transparent); }
.vz .vz-idtag rect { fill: var(--hue-fg); }
.vz .vz-idtag text { fill: var(--surface-2); font-size: 9px; font-weight: 700; font-family: inherit; }
.vz .vz-walk1 { animation: vzWalk1 6s ease-in-out infinite; }
.vz .vz-walk2 { animation: vzWalk2 6s ease-in-out infinite; }
.vz .vz-walk2 .vz-idtag rect { fill: var(--svc-1-fg); }
@keyframes vzWalk1 { 0% { transform: translate(-188px, 62px); } 30%, 100% { transform: none; } }
@keyframes vzWalk2 { 0% { transform: translate(-174px, -68px); } 30%, 100% { transform: none; } }

/* 13. synthetic data: the generator glows, varied tiles pop out */
.vz .vz-gen { fill: color-mix(in srgb, var(--hue-fg) 16%, var(--surface-2)); stroke: var(--hue-fg); stroke-width: 2;
  animation: vzGen 2.7s ease-in-out infinite; }
@keyframes vzGen { 50% { fill: color-mix(in srgb, var(--hue-fg) 32%, var(--surface-2)); } }
.vz .vz-fill { fill: var(--hue-fg); }

/* 14. dataset audit: rows scan in, two are flagged; balance bars grow */
.vz .vz-row { fill: color-mix(in srgb, var(--hue-fg) 6%, var(--surface-2));
  stroke: color-mix(in srgb, var(--hue-fg) 25%, transparent); stroke-width: 1; }
.vz .vz-row-bad { fill: color-mix(in srgb, var(--svc-3) 22%, var(--surface-2)); stroke: var(--svc-3-fg);
  animation: vzRowBad 6s ease-in-out infinite; animation-delay: var(--d, 0s); }
@keyframes vzRowBad { 0%, 25% { fill: color-mix(in srgb, var(--hue-fg) 6%, var(--surface-2)); stroke: color-mix(in srgb, var(--hue-fg) 25%, transparent); } 40%, 100% { fill: color-mix(in srgb, var(--svc-3) 22%, var(--surface-2)); stroke: var(--svc-3-fg); } }
.vz .vz-warn path:first-child { fill: var(--svc-3-fg); }
.vz .vz-warn-i { fill: none; stroke: var(--surface-2); stroke-width: 1.8; stroke-linecap: round; }
.vz .vz-warn { animation: vzLate 6s ease-in-out infinite; }
.vz .vz-bar { fill: var(--hue-fg); transform-box: fill-box; transform-origin: center bottom;
  animation: vzGrowY 6s ease-out infinite; animation-delay: var(--d, 0s); }
.vz .vz-bar-low { fill: var(--svc-3-fg); }
@keyframes vzGrowY { 0%, 10% { transform: scaleY(0); } 40%, 100% { transform: none; } }

/* 15. privacy redaction: pixel blocks cover the face and the plate */
.vz .vz-plate { fill: var(--surface-2); stroke: color-mix(in srgb, var(--hue-fg) 50%, transparent); }
.vz .vz-pix rect { fill: var(--hue-fg); opacity: var(--o, .8); }
.vz .vz-pix { animation: vzPix 6s steps(4, end) infinite; }
.vz .vz-pix2 { animation-delay: .3s; }
@keyframes vzPix { 0%, 30% { opacity: 0; } 50%, 92% { opacity: 1; } 100% { opacity: 0; } }

/* 16. pose: keypoints pop, limbs draw, an arm raises */
.vz .vz-kp { fill: var(--hue-fg); transform-box: fill-box; transform-origin: center;
  animation: vzKp 6s ease-out infinite; animation-delay: var(--d, 0s); }
@keyframes vzKp { 0% { transform: scale(0); } 10%, 100% { transform: none; } }
.vz .vz-limb { fill: none; stroke: color-mix(in srgb, var(--hue-fg) 70%, transparent); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.vz .vz-arm-down { opacity: 0; animation: vzArmDown 6s ease-in-out infinite; }
.vz .vz-arm-up, .vz .vz-kp-hand { animation: vzArmUp 6s ease-in-out infinite; }
@keyframes vzArmDown { 0%, 45% { opacity: 1; } 55%, 100% { opacity: 0; } }
@keyframes vzArmUp { 0%, 45% { opacity: 0; } 55%, 100% { opacity: 1; } }

/* 17. edge inference: pipeline stages shrink, the work moves CPU -> NPU */
.vz .vz-shrink { fill: var(--hue-fg); transform-box: fill-box; transform-origin: left center;
  animation: vzShrink 6s cubic-bezier(.3, .7, .2, 1) infinite; animation-delay: var(--d, 0s); }
@keyframes vzShrink { 0%, 15% { transform: scaleX(3.7); } 55%, 100% { transform: none; } }
.vz .vz-chipbox { fill: color-mix(in srgb, var(--hue-fg) 6%, var(--surface-2)); stroke: var(--hue-fg); stroke-width: 2; }
.vz .vz-lane { fill: var(--surface-2); stroke: color-mix(in srgb, var(--hue-fg) 35%, transparent); stroke-width: 1; }
.vz .vz-lane-on { fill: color-mix(in srgb, var(--hue-fg) 28%, transparent); stroke: var(--hue-fg); stroke-width: 2;
  transform: translateY(52px); animation: vzLane 6s cubic-bezier(.3, .7, .2, 1) infinite; }
@keyframes vzLane { 0%, 15% { transform: none; } 32%, 40% { transform: translateY(26px); } 55%, 100% { transform: translateY(52px); } }

/* The Quick enquiry button: a quiet pill in the card's own hue, so six of
   them on one page do not shout over the content they sit under. */
.qe-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 14px; border-radius: 10px; cursor: pointer;
  font: inherit; font-size: var(--t-meta); font-weight: 650;
  color: var(--ink); background: transparent;
  border: 1px solid var(--edge);
}
.qe-btn:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); }
.qe-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.qe-btn i { font-size: 16px; }

/* ================================================= quick enquiry dialog == */

.qe {
  width: min(480px, calc(100vw - 32px)); max-height: calc(100vh - 32px);
  padding: 0; border: 1px solid var(--border-strong); border-radius: 4px;
  background: var(--sheet); color: var(--text-primary);
}
.qe::backdrop { background: rgba(6, 20, 26, .55); backdrop-filter: blur(2px); }
.qe form { padding: clamp(20px, 4vw, 28px); margin: 0; }
.qe .qe-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.qe h2 { font-size: var(--t-card); font-stretch: 104%; font-weight: 700; letter-spacing: -.01em; margin: 0; }
.qe .qe-sub { font-size: var(--t-meta); color: var(--text-secondary); margin: 6px 0 18px; }
.qe .qe-x {
  flex: none; width: 34px; height: 34px; border-radius: 50%; cursor: pointer;
  display: grid; place-items: center; font-size: 18px;
  background: var(--surface-1); color: var(--text-secondary); border: 1px solid var(--border);
}
.qe .qe-x:hover { color: var(--text-primary); }
.qe .qe-x:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.qe .form-field textarea { min-height: 96px; }
.qe .qe-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-top: 4px; }
.qe .qe-done { text-align: center; padding: 10px 0 4px; }
.qe .qe-done i { font-size: 44px; color: var(--text-success); }
.qe .qe-done p { color: var(--text-secondary); font-size: var(--t-meta); }
.qe [hidden] { display: none !important; }
@media (max-width: 480px) { .qe .form-two { grid-template-columns: 1fr; } }
.vz .vz-score { fill: var(--svc-4-fg); font-weight: 700; }

/* The landing page's own rules — the hero and its contact sheet, the AI
   Services card, the check-list and the "What it looks for" drawings — live
   in home.css. */
