/* ============ Tokens: swap these for your brand colours ============ */
:root {
  --bg: #ffffff;      /* page background */
  --surface: #f2f2f2;       /* raised surfaces, panels. Neutral, matching the
                               app's --color-surface-hover. The app inverts this
                               site (grey page, white cards), so its white
                               --color-surface is not the equivalent token. */
  --border: #ebebeb;       /* borders, dividers. The app's --color-border. */
  --text: #161616;       /* primary text */
  --text-dim: #5f6368;        /* secondary text */
  --accent: #161616;       /* accent: buttons */
  --accent-hover: #3a3a3a; /* accent hover */
  --accent-ink: #ffffff;   /* text on accent */

  /* The app's --color-error. Error messages only: invalid fields are marked by
     the red text under them, not by recolouring their border. */
  --error: #fb2c36;

  /* For a line drawn ON a --surface panel rather than between one and the page.
     --border is #ebebeb, seven levels off #f2f2f2, so it disappears against
     grey. Use this only for lines with grey on both sides: a panel's own outer
     border still wants --border, because that edge reads against the white
     page. */
  --border-on-surface: #dfdfdf;

  --font: "Google Sans Flex", "Google Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  --max: 1440px;
  --pad: clamp(20px, 5vw, 48px);
  --radius-lg: 20px;
  --radius-sm: 10px;
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 68px; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
img, video { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

/* Line breaking. balance evens out the lines of short headings, pretty stops a
   single word being left alone on the last line of a paragraph. Browsers that
   do not support these ignore them, so nothing breaks. */
h1, h2, h3, h4, blockquote, .lead, .standfirst { text-wrap: balance; }
p, li, dd, .card p, .article-body p { text-wrap: pretty; }

/* Utility classes for one-off overrides */
.pretty  { text-wrap: pretty; }
.balance { text-wrap: balance; }
.nowrap-last { text-wrap: nowrap; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }

.wrap { max-width: var(--max); margin: 0 auto; padding: 0 var(--pad); }

/* ============ Buttons ============ */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: 44px; padding: 0 22px; border-radius: 999px;
  font: 500 15px/1 var(--font); cursor: pointer; border: 1px solid transparent;
  transition: background .2s, border-color .2s, color .2s;
}
.btn-primary { background: var(--accent); color: var(--accent-ink); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-ghost { border-color: var(--border); color: var(--text); }
.btn-ghost:hover { border-color: var(--text-dim); }
/* Filled black at rest like .btn-primary, inverting to a black outline on
   hover. Used by "Send another message", which was a ghost button and became
   near-invisible once --border lightened to #ebebeb. */
.btn-invert { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.btn-invert:hover { background: transparent; color: var(--text); border-color: var(--text); }
/* The same idea on a grey panel. A ghost button there is invisible, because its
   border is --border, seven levels off --surface. This fills it white instead
   and inverts to a black outline on hover, mirroring .btn-invert above. Use it
   for a ghost button sitting on --surface, not on the white page. */
.btn-white { background: var(--bg); color: var(--text); border-color: var(--bg); }
.btn-white:hover { background: transparent; border-color: var(--text); }

/* ============ Nav ============ */
.nav {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}
.nav .wrap { display: flex; align-items: center; height: 68px; gap: 32px; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 20px; letter-spacing: -0.01em; }
.logo img { height: 35px; width: auto; display: block; }
.caveat-semibold {
  font-family: "Caveat", cursive;
  font-optical-sizing: auto;
  font-weight: 600;
  font-style: normal;
}
.footer .logo .caveat-semibold { font-size: 28px; line-height: 1; color: var(--text); }
/* The 2px nudge is optical, not arbitrary. body sets line-height 1.6, so these
   15px links get a 24px line box, and flex centres that box. The box reserves
   room under the baseline for descenders, but none of the labels has one
   (Product, Features, Who it's for, Business), so the ink sits high inside it
   while the logo beside it is artwork centred on its geometry. transform
   rather than padding or margin, so nothing reflows and the links keep their
   hit area. Revisit if a label with a descender is ever added. */
.nav-links {
  display: flex; gap: 28px; font-size: 15px; color: var(--text-dim);
  transform: translateY(2px);
}
.nav-links a:hover { color: var(--text); }
.nav-actions { margin-left: auto; display: flex; align-items: center; gap: 20px; }
.nav-actions .signin { font-size: 15px; color: var(--text-dim); }
.nav-actions .signin:hover { color: var(--text); }
.nav-actions .btn { height: 38px; padding: 0 18px; font-size: 14px; }

/* ============ Media placeholders ============ */
/* Each .media holds a <video>. Leave src empty to show the placeholder label. */
/* NO BORDER. Every slot is filled edge to edge by a screenshot or by a drawn
   mockup that paints its own screen, so a hairline just draws a frame around
   something that already reads as one. The rounded corners carry the shape,
   and --surface keeps an empty slot visible while its image is still missing.
   If a supplied image turns out to be near-white at its edges it will float on
   this page, and the fix is to put the border back here rather than on one
   slot: the two desktop slots sit side by side at the same size and must match. */
.media {
  position: relative; overflow: hidden;
  background: var(--surface);
}
/* img as well as video: the slots take stills now, and the rule has to cover
   both so a slot can hold either without a second set of styles. object-fit
   crops to the slot's own aspect ratio rather than squashing, so an image that
   is not exactly 4:3 or 16:10 still fills it properly. */
.media video, .media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.media .ph {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--text-dim); font-size: 14px; text-align: center; padding: 16px;
  background: var(--surface);
}

.frame-browser { border-radius: var(--radius-lg); aspect-ratio: 16 / 10; }

.frame-phone {
  width: min(300px, 70vw); aspect-ratio: 9 / 19.5; margin: 0 auto;
  border-radius: var(--radius-lg);
}

/* ============ Hero ============ */
.hero { padding: clamp(56px, 9vw, 112px) 0 clamp(48px, 7vw, 88px); }
.hero .wrap { display: grid; grid-template-columns: 1.15fr 1fr; gap: clamp(32px, 5vw, 72px); align-items: center; }
.hero h1 {
  font-size: clamp(44px, 6.4vw, 84px); line-height: 1.02; letter-spacing: -0.035em;
  font-weight: 500; margin: 0 0 24px; max-width: 16ch;
}
.hero p.lead { font-size: clamp(18px, 1.7vw, 21px); color: var(--text-dim); max-width: 44ch; margin: 0 0 36px; }
.hero .cta-row { display: flex; gap: 12px; flex-wrap: wrap; }
.hero .note { margin-top: 20px; font-size: 14px; color: var(--text-dim); }

/* Hero media */
.hero-media { width: 100%; }
.hero-media .media { border-radius: var(--radius-lg); aspect-ratio: 4 / 3; }

/* ============ Labels strip ============ */
.labels { border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); padding: 22px 0; overflow: hidden; }
.labels ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: 12px 36px;
  white-space: nowrap; color: var(--text-dim); font-size: 15px;
}
/* Declared on the item, not just inherited from the list above. A two word
   topic ("Emerging markets", "Personal finance", "Real estate") was breaking
   onto a second line on a phone. flex: 0 0 auto stops the item shrinking, which
   is the same thing that squashed the blog filter pills instead of letting them
   overflow, and the repeated nowrap means the rule does not depend on
   inheriting through a flex item. */
.labels li { flex: 0 0 auto; white-space: nowrap; }

/* Smaller screens: one left-aligned line you can swipe or drag sideways */
@media (max-width: 900px) {
  .logo img { height: 26px; }
  .labels .wrap { padding: 0; }
  .labels ul {
    flex-wrap: nowrap; justify-content: flex-start;
    overflow-x: auto; overscroll-behavior-x: contain;
    padding: 0 var(--pad);
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    cursor: grab; user-select: none;
  }
  .labels ul::-webkit-scrollbar { display: none; }
  .labels ul::after { content: ""; flex: 0 0 1px; } /* keeps end padding when scrolled */
  .labels ul.dragging { cursor: grabbing; }
}

/* ============ Hero morph ============ */
/* The animated grid that sits over the hero image.
   Three things here are load-bearing:

   1. THE SELECTOR CARRIES .media. A few rules up, `.media video, .media img`
      sets position/inset/width/height/object-fit on every image in a media
      box. A second <img> in there inherits all of it and goes full bleed. This
      needs the higher specificity to escape, and it fails silently otherwise:
      the animation still runs, just edge to edge instead of centred.

   2. `inset` IS RESET, not just overridden. That shorthand set right and bottom
      to 0 as well as top and left. Leaving them at 0 while width is auto makes
      the browser compute the width from the offsets, and the size collapses.

   3. THE WHITE IS A FILTER, not an edit. grid-morph.svg hard-codes stroke:#000
      and fill:#000 in its own <style>, which external CSS cannot reach through
      an <img>. The artwork is pure black on transparent, so invert(1) turns it
      pure white and leaves the transparency alone. The file stays byte
      identical, C2PA manifest included. The CSS animation inside it still runs:
      an <img>-embedded SVG blocks scripts, not declarative animation, and the
      file's own prefers-reduced-motion rule keeps working.

   Height rather than width, because the artwork is square and the frame is 4:3,
   so the height is the dimension that can run out. 55% of the frame. */
.hero-media .media .morph {
  position: absolute;
  inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  width: auto;
  height: 55%;
  object-fit: contain;
  filter: invert(1);
  pointer-events: none;
}

/* ============ Eyebrow ============ */
/* The small uppercase label above a section heading, carried over from the
   previous landing (index-old.php, .block-eyebrow).
   Two deliberate departures from that original:

   1. The site's own sans, not the monospace the old .eyebrow rule used. There
      is no other monospace anywhere on this site, and four labels is not a
      reason to introduce one.
   2. --text-dim rather than white at 55%. The old page was dark; this one is
      not, so the opacity trick has nothing to sit on.

   Weight 500 is inside the self-hosted Google Sans Flex range (400 to 800,
   includes/data/fonts.php). Anything outside it renders faked and blurred.

   This is the ONLY uppercase treatment on the site, which is why it is a named
   component rather than a property bolted onto each heading. */
.eyebrow {
  display: block;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 14px;
}

/* ============ Feature rows ============ */
.features { padding: clamp(72px, 10vw, 136px) 0; }
.feature {
  display: grid; grid-template-columns: 1fr 1.25fr; gap: clamp(32px, 6vw, 96px);
  align-items: center;
}
.feature + .feature { margin-top: clamp(80px, 11vw, 150px); }
.feature.flip .copy { order: 2; }
.feature h2 {
  font-size: clamp(30px, 3.6vw, 46px); line-height: 1.1; letter-spacing: -0.025em;
  font-weight: 500; margin: 0 0 18px; max-width: 20ch;
}
.feature p { color: var(--text-dim); margin: 0 0 22px; max-width: 42ch; }
.feature ul { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); max-width: 42ch; }
.feature li { padding: 12px 0; border-bottom: 1px solid var(--border); font-size: 15px; font-weight: 500; }

/* ============ Audience ============ */
.audience { padding: 0 0 clamp(72px, 10vw, 136px); }
.audience h2 {
  font-size: clamp(30px, 3.6vw, 46px); line-height: 1.1; letter-spacing: -0.025em;
  font-weight: 500; margin: 0 0 48px; max-width: 26ch;
}
.audience .cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(24px, 4vw, 48px); }
.audience .col { border-top: 1px solid var(--text); padding-top: 20px; }
.audience h3 { font-size: 20px; font-weight: 500; margin: 0 0 10px; }
.audience p { color: var(--text-dim); margin: 0; font-size: 16px; }

/* ============ Quote ============ */
.quote { background: var(--surface); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); padding: clamp(72px, 10vw, 128px) 0; }
.quote blockquote {
  margin: 0; font-size: clamp(26px, 3.2vw, 40px); line-height: 1.25; letter-spacing: -0.02em;
  max-width: 36ch;
}
.quote .who { display: flex; align-items: center; gap: 14px; margin-top: 36px; }
.quote .avatar { width: 48px; height: 48px; border-radius: 50%; }
.quote .avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.quote .who strong { display: block; font-weight: 500; }
.quote .who span { color: var(--text-dim); font-size: 15px; }

/* ============ Final image ============ */
/* Full-width background image, same height as the old closing section */
.final {
  position: relative; overflow: hidden;
  height: clamp(380px, 38vw, 560px);
  background: var(--surface); border-top: 1px solid var(--border);
}
.final img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }
.final .ph {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--text-dim); font-size: 14px; text-align: center; padding: 16px;
  background: var(--surface);
}

/* ============ Footer ============ */
.footer { border-top: 1px solid var(--border); padding: 56px 0 40px; font-size: 14px; color: var(--text-dim); }
.footer .top { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: 32px; }
.footer h4 { color: var(--text); font-weight: 500; font-size: 14px; margin: 0 0 14px; }
.footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.footer a:hover { color: var(--text); }
.footer .site { font-weight: 800; color: var(--text); }
.footer .disclaimer {
  margin: 28px 0 0; padding-top: 24px; border-top: 1px solid var(--border);
  font-size: 12.5px; line-height: 1.65; color: var(--text-dim); max-width: none;
}
.footer .disclaimer a { text-decoration: underline; text-underline-offset: 2px; }
.footer .bottom { margin-top: 56px; display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }

/* ============ About: header ============ */
.about-head { padding: clamp(56px, 9vw, 120px) 0 clamp(40px, 6vw, 72px); }
.about-head .kicker { color: var(--text-dim); font-size: 15px; margin: 0 0 20px; }
.about-head h1 {
  font-size: clamp(44px, 6.4vw, 88px); line-height: 1.02; letter-spacing: -0.035em;
  font-weight: 500; margin: 0 0 28px; max-width: 18ch;
}
.about-head h1.verify-title {
  /* Never more than two lines. Measured in Google Sans Flex 500 at this
     letter-spacing, the wider half of "Every conviction / on Lumin is sealed."
     is 8.03 times the font size, so two lines fit when the size is at most the
     column width (100vw less both --pad gutters) / 8.03. 8.2 leaves 2% for
     rendering differences. min() only ever shrinks it below the usual clamp, so
     wider screens are unchanged; browsers without min() keep the clamp above.
     Re-measure if the title's wording changes. */
  font-size: min(clamp(44px, 6.4vw, 88px), calc((100vw - 2 * var(--pad)) / 8.2));
}
.about-head .lead { font-size: clamp(18px, 1.7vw, 21px); color: var(--text-dim); max-width: 52ch; margin: 0; }
/* A second lead paragraph (the verify page's intro) gets a paragraph's gap. */
.about-head .lead + .lead { margin-top: 1em; }

/* ============ About: wide image ============ */
.about-image .media { border-radius: var(--radius-lg); aspect-ratio: 21 / 9; }
.about-image .media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }

/* ============ About: story ============ */
.story { padding: clamp(72px, 10vw, 136px) 0; }
.story .wrap { display: grid; grid-template-columns: 1fr 1.3fr; gap: clamp(32px, 6vw, 96px); align-items: start; }
.section-title {
  font-size: clamp(30px, 3.6vw, 46px); line-height: 1.1; letter-spacing: -0.025em;
  font-weight: 500; margin: 0; max-width: 18ch;
}
.story .body p { color: var(--text-dim); font-size: 18px; margin: 0 0 20px; max-width: 60ch; }
.story .body p:first-child { color: var(--text); font-size: 21px; line-height: 1.5; }

/* ============ About: facts ============ */
.facts { border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.facts dl { display: grid; grid-template-columns: repeat(4, 1fr); margin: 0; }
.facts .fact { padding: clamp(28px, 4vw, 44px) 24px; border-left: 1px solid var(--border); }
.facts .fact:first-child { border-left: 0; padding-left: 0; }
.facts dt { color: var(--text-dim); font-size: 14px; margin-bottom: 8px; }
.facts dd { margin: 0; font-size: clamp(22px, 2.2vw, 28px); letter-spacing: -0.02em; font-weight: 500; }

/* ============ About: principles ============ */
.principles { padding: clamp(72px, 10vw, 136px) 0; }
.principles .section-title { margin-bottom: 48px; }
.principles .cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(24px, 4vw, 48px); }
.principles .col { border-top: 1px solid var(--text); padding-top: 20px; }
.principles h3 { font-size: 20px; font-weight: 500; margin: 0 0 10px; }
.principles p { color: var(--text-dim); margin: 0; font-size: 16px; }

/* ============ About: closing ============ */
.about-cta { padding: clamp(80px, 11vw, 150px) 0; background: var(--surface); border-top: 1px solid var(--border); }
.about-cta .wrap { display: flex; align-items: end; justify-content: space-between; gap: 32px; flex-wrap: wrap; }
.about-cta h2 {
  font-size: clamp(36px, 5vw, 64px); line-height: 1.04; letter-spacing: -0.035em;
  font-weight: 500; margin: 0; max-width: 16ch;
}
.about-cta .actions { display: flex; gap: 12px; flex-wrap: wrap; }

/* ============ Careers: open application ============ */
.open-app { padding: 0 0 clamp(72px, 10vw, 136px); }
.open-app .box {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: clamp(32px, 5vw, 64px);
  display: grid; grid-template-columns: 1.2fr 1fr; gap: clamp(24px, 5vw, 72px); align-items: center;
}
.open-app h2 {
  font-size: clamp(30px, 3.6vw, 46px); line-height: 1.1; letter-spacing: -0.025em; font-weight: 500; margin: 0 0 16px;
}
.open-app p { color: var(--text-dim); margin: 0; max-width: 48ch; }
.open-app .side p { margin-bottom: 24px; font-size: 15px; }
.open-app .side ul { list-style: none; margin: 0 0 28px; padding: 0; border-top: 1px solid var(--border-on-surface); }
.open-app .side li { padding: 10px 0; border-bottom: 1px solid var(--border-on-surface); font-size: 15px; }

/* ============ Careers: members first ============ */
.members-first { border-top: 1px solid var(--border); padding: clamp(72px, 10vw, 136px) 0; }
.members-first .wrap { display: grid; grid-template-columns: 1fr 1.3fr; gap: clamp(32px, 6vw, 96px); align-items: start; }
.members-first p { color: var(--text-dim); font-size: 18px; margin: 0 0 28px; max-width: 52ch; }

/* ============ Contact ============ */
.contact { padding: 0 0 clamp(72px, 10vw, 136px); }
.contact .wrap { display: grid; grid-template-columns: 1.25fr 1fr; gap: clamp(32px, 6vw, 96px); align-items: start; }

.field { margin-bottom: 24px; }
.field label { display: block; font-size: 15px; margin-bottom: 8px; }
.field .hint { color: var(--text-dim); font-size: 14px; margin: 8px 0 0; }
.field input[type="text"], .field input[type="email"], .field select, .field textarea {
  width: 100%; font-family: var(--font); font-size: 16px; color: var(--text);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 12px 14px; transition: border-color .15s;
}
.field textarea { min-height: 170px; resize: vertical; line-height: 1.6; }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--text); }
.field select {
  appearance: none; -webkit-appearance: none; cursor: pointer; padding-right: 40px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none'%3E%3Cpath stroke='%235f6368' stroke-linecap='round' stroke-linejoin='round' d='M1 4c1.34 2.69 2.69 4.17 5.12 5.53a1.8 1.8 0 0 0 1.76 0C10.31 8.17 11.66 6.69 13 4'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center; background-size: 14px;
}
/* Subject dropdown on the contact page.

   A button and a menu, not a <select>. The closed state of a native select is
   styleable (see .field select above, which this deliberately matches down to
   the chevron), but the open list is drawn by the operating system and cannot
   be, so it never looked like the rest of the page. Same component the app uses
   as .contact-dropdown, rebuilt on this site's own tokens rather than importing
   the app's, which do not exist here. */
.dropdown { position: relative; }

/* Matches .field input / .field select exactly: same border, radius, padding
   and type, so the row reads as one set of fields. */
.dropdown-trigger {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: 12px; font-family: var(--font); font-size: 16px; color: var(--text);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 12px 14px; cursor: pointer; text-align: left; transition: border-color .15s;
}
.dropdown-trigger:focus-visible { outline: none; border-color: var(--text); }
.dropdown-trigger.active { border-color: var(--text); }

.dropdown-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Until something is chosen, the trigger reads as the placeholder a select
   would have shown. */
.dropdown-text.placeholder { color: var(--text-dim); }

.dropdown-arrow {
  width: 14px; height: 14px; flex: 0 0 auto; color: var(--text-dim);
  transition: transform .2s ease;
}
.dropdown-trigger.active .dropdown-arrow { transform: rotate(180deg); }

.dropdown-menu {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 20;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 4px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, .08);
  /* Long enough to need it on a short screen; the list has seven entries. */
  max-height: 320px; overflow-y: auto;
}
.dropdown-menu.hidden { display: none; }

.dropdown-option {
  width: 100%; display: block; padding: 10px 14px; text-align: left;
  font-family: var(--font); font-size: 15px; color: var(--text);
  background: transparent; border: 0; border-radius: 8px; cursor: pointer;
  transition: background-color .15s;
}
.dropdown-option:hover,
.dropdown-option:focus-visible { background: var(--surface); outline: none; }
.dropdown-option.selected { font-weight: 600; }

.field .count { display: block; text-align: right; color: var(--text-dim); font-size: 13px; margin-top: 6px; }
/* An invalid field shakes; it does not turn red and it does not print a message.
   The message is still rendered, just taken off the screen, so a screen reader
   still says which field is wrong and why. Hidden only while invalid, so none
   of it is announced on a form nobody has submitted yet. */
.error { display: none; color: var(--error); font-size: 14px; margin: 8px 0 0; }
.field.invalid .error {
  display: block;
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

@keyframes fieldShake {
  0%, 100% { transform: translateX(0); }
  15% { transform: translateX(-6px); }
  30% { transform: translateX(5px); }
  45% { transform: translateX(-4px); }
  60% { transform: translateX(3px); }
  75% { transform: translateX(-2px); }
}
/* The control shakes, not the whole group: moving the label with it reads as
   the layout breaking rather than as an answer to the click. */
.field.shake input,
.field.shake textarea,
.field.shake .dropdown-trigger,
.consent.shake { animation: fieldShake 420ms ease; }

/* Someone who has asked for less motion cannot be shown a shake, and the shake
   is the entire signal, so they would otherwise be left with no way to tell
   which field is wrong. They get a static marker instead of the movement: the
   control's own border, darkened. No red and no text, so the page still says
   what it says to everyone else. */
@media (prefers-reduced-motion: reduce) {
  .field.shake input,
  .field.shake textarea,
  .field.shake .dropdown-trigger,
  .consent.shake { animation: none; }

  .field.invalid input,
  .field.invalid textarea,
  .field.invalid .dropdown-trigger { border-color: var(--text); }
}

.consent { display: flex; gap: 12px; align-items: flex-start; margin: 4px 0 28px; font-size: 15px; color: var(--text-dim); }
.consent input { margin-top: 3px; width: 18px; height: 18px; accent-color: var(--accent); flex: 0 0 auto; }
.consent a { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }


.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form-note { color: var(--text-dim); font-size: 14px; margin: 16px 0 0; }
.sending { opacity: .6; pointer-events: none; }

.sent {
  display: none; border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); padding: clamp(28px, 4vw, 48px);
}
.sent h2 { font-size: clamp(24px, 2.6vw, 32px); font-weight: 500; letter-spacing: -0.02em; margin: 0 0 12px; }
.sent p { color: var(--text-dim); margin: 0 0 24px; max-width: 46ch; }

/* Side panel */
.contact-side { border-top: 1px solid var(--text); padding-top: 20px; }
.contact-side h2 { font-size: 20px; font-weight: 500; margin: 0 0 16px; }
.contact-side dl { margin: 0 0 32px; }
.contact-side dt { color: var(--text-dim); font-size: 14px; margin-bottom: 4px; }
.contact-side dd { margin: 0 0 18px; font-size: 16px; }
.contact-side a { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.contact-side address { font-style: normal; color: var(--text-dim); font-size: 15px; line-height: 1.7; }

/* ============ Legal page header ============ */
.legal-head { padding: clamp(48px, 7vw, 96px) 0 clamp(32px, 4vw, 48px); border-bottom: 1px solid var(--border); }
.legal-tabs { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 32px; padding: 0; list-style: none; }
.legal-tabs a {
  display: inline-flex; align-items: center; height: 36px; padding: 0 16px;
  border: 1px solid var(--border); border-radius: 999px; font-size: 14px; color: var(--text-dim);
}
.legal-tabs a:hover { color: var(--text); border-color: var(--text-dim); }
.legal-tabs a[aria-current="page"] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.legal-head h1 {
  font-size: clamp(40px, 5.6vw, 72px); line-height: 1.04; letter-spacing: -0.035em;
  font-weight: 500; margin: 0 0 20px;
}
.legal-meta { display: flex; gap: 8px 28px; flex-wrap: wrap; color: var(--text-dim); font-size: 15px; margin: 0; }

/* ============ Legal body ============ */
.legal-body { padding: clamp(40px, 6vw, 80px) 0 clamp(72px, 10vw, 120px); }
.legal-body .wrap { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: clamp(40px, 7vw, 120px); align-items: start; }

.toc { position: sticky; top: 100px; font-size: 14px; max-height: calc(100vh - 124px); overflow-y: auto; scrollbar-width: thin; }
.toc summary {
  list-style: none; cursor: default; font-weight: 500; color: var(--text); margin-bottom: 14px; pointer-events: none;
}
.toc summary::-webkit-details-marker { display: none; }
.toc ol { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--border); }
.toc a {
  display: block; padding: 7px 0 7px 16px; margin-left: -1px; border-left: 2px solid transparent;
  color: var(--text-dim); line-height: 1.4;
}
.toc a:hover { color: var(--text); }
.toc a.active { color: var(--text); border-left-color: var(--text); }

.legal-article { max-width: 72ch; }
.legal-article section + section { margin-top: 56px; padding-top: 56px; border-top: 1px solid var(--border); }
.legal-article h2 {
  font-size: clamp(24px, 2.4vw, 30px); line-height: 1.2; letter-spacing: -0.02em; font-weight: 500; margin: 0 0 18px;
}
.legal-article h2 .num { color: var(--text-dim); margin-right: 10px; }
.legal-article h3 { font-size: 18px; font-weight: 500; margin: 32px 0 10px; }
.legal-article p, .legal-article li { color: var(--text-dim); }
.legal-article p { margin: 0 0 16px; }
.legal-article ul { margin: 0 0 16px; padding-left: 20px; }
.legal-article li + li { margin-top: 6px; }
.legal-article strong { color: var(--text); font-weight: 600; }

/* Bold inside an article body, lifted out of the dim body colour the same way
   .legal-article strong is. Colour and weight together, rather than weight
   alone: paragraphs here are --text-dim, so without the lift the only thing
   marking a bold word is its weight, which is why it read as needing to be
   heavier than it should. 600 rather than the browser's default 700, which sat
   too heavy against 500 headings. Whatever this is, it must be a weight that
   is inside the self-hosted Google Sans Flex range (400 to 800), or
   the browser fakes it by smearing the 400 and it looks blurred. Legal pages
   use exactly the same treatment, see above, so bold means one thing across
   the whole site. Note they carry roughly 25 bolds a page against two or three
   here, mostly as label lead-ins rather than emphasis, so if those pages ever
   read as too heavy that rule is the one to soften, not this one. */
.article-body strong,
.article-body b { color: var(--text); font-weight: 600; }
.legal-article a { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.legal-article a.btn { text-decoration: none; }

.table-scroll { overflow-x: auto; margin: 8px 0 20px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.legal-article table { width: 100%; border-collapse: collapse; font-size: 15px; min-width: 560px; }
.legal-article th, .legal-article td { text-align: left; padding: 14px 16px; vertical-align: top; border-bottom: 1px solid var(--border); }
.legal-article th { font-weight: 500; color: var(--text); background: var(--surface); }
.legal-article td { color: var(--text-dim); }
.legal-article tr:last-child td { border-bottom: 0; }
.legal-article td:first-child { color: var(--text); white-space: nowrap; }
.legal-article td.nowrap { white-space: nowrap; }

.contact-box {
  margin-top: 56px; padding: clamp(22px, 3vw, 32px); border-radius: var(--radius-lg);
  border: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
}
.contact-box h2 { font-size: 20px; font-weight: 500; margin: 0 0 4px; }
.contact-box p { margin: 0; color: var(--text-dim); }

/* ============ Editorial: index ============ */
.ed-head { padding: clamp(48px, 7vw, 96px) 0 clamp(28px, 4vw, 44px); }
.ed-head h1 {
  font-size: clamp(44px, 6vw, 80px); line-height: 1.02; letter-spacing: -0.035em;
  font-weight: 500; margin: 0 0 20px;
}
.ed-head p { color: var(--text-dim); font-size: clamp(17px, 1.6vw, 20px); max-width: 52ch; margin: 0; }

.filters { padding-bottom: clamp(32px, 5vw, 56px); }
/* One line that scrolls, rather than wrapping onto a second row on a narrow
   phone. The negative side margins cancel .wrap's padding so the track reaches
   both screen edges, and the matching padding puts the first pill back in line
   with the heading above it. That is what makes a part-visible pill at the edge
   read as "there is more" instead of as a clipped layout.

   Longhand margin-left/right rather than margin-inline: the test iPhone's
   WebKit is old enough to drop the logical properties silently.

   The scrollbar is hidden because the pills themselves are the affordance; on
   a desktop the row fits, so nothing scrolls and none of this shows. */
.filters ul {
  display: flex; gap: 8px; list-style: none; margin: 0; padding: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  margin-left: calc(var(--pad) * -1);
  margin-right: calc(var(--pad) * -1);
  padding-left: var(--pad);
  padding-right: var(--pad);
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.filters ul::-webkit-scrollbar { display: none; }
/* Without this the pills shrink to fit instead of overflowing, and nothing scrolls. */
.filters li { flex: 0 0 auto; }
.filters button {
  font-family: var(--font); font-size: 14px; color: var(--text-dim); cursor: pointer;
  height: 36px; padding: 0 16px; border: 1px solid var(--border); border-radius: 999px;
  background: none; white-space: nowrap;
  transition: background .15s, border-color .15s, color .15s;
}
.filters button:hover { color: var(--text); border-color: var(--text-dim); }
.filters button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

/* Cards */
.card { display: block; color: inherit; }
.card .thumb {
  position: relative; overflow: hidden; aspect-ratio: 16 / 9; margin-bottom: 18px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
}
.card .thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform .4s ease; }
.card .thumb .ph { position: absolute; inset: 0; display: grid; place-items: center; background: var(--surface); color: var(--text-dim); font-size: 14px; }
.card:hover .thumb img { transform: scale(1.03); }
.meta { display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--text-dim); margin-bottom: 10px; }
.label {
  display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border-radius: 999px;
  border: 1px solid var(--border); color: var(--text); font-size: 12px; letter-spacing: .01em;
}
.card h3 { font-size: 21px; line-height: 1.25; letter-spacing: -0.015em; font-weight: 500; margin: 0 0 8px; }
.card p { color: var(--text-dim); font-size: 16px; margin: 0; }
.card:hover h3 { text-decoration: underline; text-underline-offset: 3px; }

/* Headline over the artwork (ElevenLabs-style tiles) */
.card--overlay .thumb { margin-bottom: 14px; }
.card--overlay .tile-title {
  position: absolute; inset: auto 0 0 0; z-index: 1;
  padding: 20% 22px 20px;
  background: linear-gradient(to top, rgba(0,0,0,.62) 0%, rgba(0,0,0,.35) 45%, rgba(0,0,0,0) 100%);
}
.card--overlay .tile-title h3 {
  color: #fff; margin: 0; font-size: clamp(18px, 1.5vw, 22px); line-height: 1.22;
  letter-spacing: -0.015em; text-shadow: 0 1px 12px rgba(0,0,0,.28);
}
.card--overlay .meta { margin-bottom: 8px; }
.card--overlay:hover .tile-title h3 { text-decoration: none; }

/* Featured */
.featured { padding-bottom: clamp(48px, 7vw, 80px); }
.featured .card { display: grid; grid-template-columns: 1.2fr 1fr; gap: clamp(24px, 4vw, 56px); align-items: center; }
.featured .thumb { margin-bottom: 0; }
.featured h3 { font-size: clamp(26px, 3vw, 38px); line-height: 1.15; letter-spacing: -0.025em; margin-bottom: 12px; }
.featured p { font-size: 18px; max-width: 46ch; }

/* Grid */
.ed-grid { padding-bottom: clamp(72px, 10vw, 136px); }
.ed-grid .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(28px, 3vw, 44px) clamp(24px, 3vw, 40px); }
.ed-grid .empty { display: none; color: var(--text-dim); padding: 24px 0; }
.ed-grid.is-empty .empty { display: block; }

/* ============ Editorial: article ============ */
.article-head { padding: clamp(40px, 6vw, 72px) 0 clamp(24px, 3vw, 36px); }
.article-head .back { color: var(--text-dim); font-weight: 400; font-size: 15px; }
.article-head .back:hover { color: var(--text); }
.article-head h1 {
  font-size: clamp(36px, 4.6vw, 62px); line-height: 1.06; letter-spacing: -0.03em;
  font-weight: 500; margin: 20px 0 20px; max-width: 22ch;
}
.article-head .head-row { display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.article-head .meta { margin-bottom: 0; }
.share { display: flex; align-items: center; gap: 8px; }
.share .share-label { display: inline-flex; align-items: center; gap: 6px; font-weight: 400; font-size: 13px; color: var(--text-dim); margin-right: 4px; }
.share .ico { width: 15px; height: 15px; flex: 0 0 auto; }
.share button { display: inline-flex; align-items: center; gap: 7px; }
.share button {
  /* A button does not inherit font properties from the page, so without an
     explicit weight this falls back to the browser's own, not the site's. */
  font-family: var(--font); font-weight: 400; font-size: 13px; color: var(--text-dim); cursor: pointer;
  height: 32px; padding: 0 14px; border: 1px solid var(--border); border-radius: 999px;
  background: none; transition: color .15s, border-color .15s;
}
.share button:hover { color: var(--text); border-color: var(--text-dim); }
.share button.done { color: var(--text); border-color: var(--text); }
.share .native { display: none; }

.article-lead { padding-bottom: clamp(32px, 5vw, 56px); }
.article-lead .media { position: relative; overflow: hidden; aspect-ratio: 21 / 9; border-radius: var(--radius-lg); background: var(--surface); border: 1px solid var(--border); }
.article-lead .media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.article-lead .media .ph { position: absolute; inset: 0; display: grid; place-items: center; background: var(--surface); color: var(--text-dim); font-weight: 400; font-size: 14px; }

.article-body { padding-bottom: clamp(56px, 8vw, 96px); }
.article-body .wrap > div { max-width: 68ch; }
.article-body p, .article-body li { color: var(--text-dim); font-weight: 400; font-size: 18px; line-height: 1.7; }
.article-body p { margin: 0 0 22px; }
/* Fluid, so it keeps its distance from h2 at every width. It used to be a flat
   21px while h1 and h2 both scaled, which read correctly on a phone and then
   collapsed toward the body copy on a desktop as h2 grew to 30px and this did
   not. The 21px floor is the old value exactly, so phones are unchanged; 2.1vw
   against h2's 2.4vw holds the same 0.875 ratio the phone had, all the way up. */
.article-body .standfirst {
  color: var(--text); font-weight: 400;
  font-size: clamp(21px, 2.1vw, 26px);
  line-height: 1.55; margin-bottom: 28px;
}
.article-body h2 { font-size: clamp(24px, 2.4vw, 30px); font-weight: 500; letter-spacing: -0.02em; margin: 40px 0 14px; }
/* No article uses h3 or h4 today. Without this they would take the browser
   default of bold 700, which since 700 became a real loaded weight would read
   as much heavier than the h2 above them. Weight only: give them sizes when
   an article actually needs one. */
.article-body h3, .article-body h4 { font-weight: 500; letter-spacing: -0.015em; }
.article-body ul { margin: 0 0 22px; padding-left: 20px; }
.article-body li + li { margin-top: 8px; }
.article-body blockquote {
  margin: 32px 0; padding-left: 24px; border-left: 2px solid var(--text);
  font-weight: 400; font-size: 22px; line-height: 1.4; letter-spacing: -0.015em; color: var(--text);
}
.article-body a { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }

.related { border-top: 1px solid var(--border); padding: clamp(48px, 7vw, 80px) 0 clamp(72px, 10vw, 120px); }
.related h2 { font-size: 20px; font-weight: 500; margin: 0 0 32px; }
.related .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(24px, 3vw, 40px); }

/* ============ Verify (/verify, 2026-10-02) ============ */
/* Built from the About header, the About principles block (How it works) and
   the contact page's two-column layout and field. Only what follows is new. */
.verify-steps { padding-top: 0; }
.verify-check .btn:disabled { opacity: .55; cursor: default; }
/* Verify record and Check on Base, side by side; they wrap on a narrow phone. */
.verify-actions { display: flex; flex-wrap: wrap; gap: 12px; }
/* Check on Base before there is a published record to show: greyed out, not
   clickable. It is a link, so this is aria-disabled rather than :disabled. */
.verify-check .btn[aria-disabled="true"] { opacity: .45; pointer-events: none; cursor: default; }

/* Unlike the contact form's errors, which are spoken but not shown, this one
   is shown too: there is a single field, and "it shook" alone doesn't say
   what is wrong with a 66-character string. Dim, not red: the field still
   shakes, which is the signal. */
.verify-field-error { color: var(--text-dim); font-size: 14px; margin: 8px 0 0; }
.verify-field-error[hidden] { display: none; }

.verify-result {
  display: flex; gap: 14px; align-items: flex-start; margin-top: 28px;
  padding: 18px 20px; border-radius: var(--radius-lg); background: var(--surface);
}
.verify-result[hidden] { display: none; }
.verify-result p { margin: 0; font-size: 16px; line-height: 1.55; color: var(--text); }
.verify-result strong { font-weight: 600; }
.verify-result a { color: var(--text); text-decoration: underline; text-underline-offset: 3px; font-weight: 500; white-space: nowrap; }
/* The one success state gets the check and a green tint (the design's colours;
   the site has no green token because nothing else on it needs one). */
.verify-result--ok { background: #eaf6ee; }
.verify-result--ok p, .verify-result--ok a { color: #0d542b; }
.verify-result-icon {
  width: 28px; height: 28px; flex: 0 0 auto; border-radius: 50%;
  background: #008236; color: #fff; display: inline-flex; align-items: center; justify-content: center;
}

.verify-scope { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.verify-scope ul { list-style: none; margin: 0; padding: 0; }
.verify-scope li { color: var(--text-dim); font-size: 16px; margin: 0 0 10px; }
/* ============ Responsive ============ */
@media (max-width: 900px) {
  .nav-links { display: none; }
  .hero .wrap, .feature { grid-template-columns: 1fr; }
  .feature.flip .copy { order: 0; }
  .audience .cols { grid-template-columns: 1fr; }
  .footer .top { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px 16px; }
  .footer .top > div:first-child { grid-column: 1 / -1; }
  .logo img { height: 26px; }
  .story .wrap { grid-template-columns: 1fr; gap: 24px; }
  .facts dl { grid-template-columns: 1fr 1fr; }
  .facts .fact { padding: 24px 0; border-left: 0; border-top: 1px solid var(--border); }
  .facts .fact:nth-child(-n+2) { border-top: 0; }
  .facts .fact:nth-child(even) { padding-left: 20px; border-left: 1px solid var(--border); }
  .principles .cols { grid-template-columns: 1fr; }
  .about-image .media { aspect-ratio: 4 / 3; }
  .open-app .box, .members-first .wrap { grid-template-columns: 1fr; }
  .contact .wrap { grid-template-columns: 1fr; gap: 48px; }
  .legal-body .wrap { grid-template-columns: minmax(0, 1fr); gap: 32px; }
  .toc { position: static; max-height: none; overflow: visible; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0 16px; }
  .toc summary { pointer-events: auto; cursor: pointer; margin: 0; padding: 14px 0; display: flex; justify-content: space-between; }
  .toc summary::after { content: "+"; color: var(--text-dim); }
  .toc[open] summary::after { content: "\2212"; }
  .toc ol { margin-bottom: 14px; }
  .featured .card { grid-template-columns: 1fr; gap: 20px; }
  /* ROW-GAP HAS TO GROW WHEN THE COLUMNS COLLAPSE. In three columns the
     28px row-gap only ever separated one card from the one below it, with
     clear space either side doing the grouping. In one column it is the only
     thing separating every card from the next, and it was competing with the
     18px gap inside a card between its image and its headline. 28 against 18
     is not a big enough difference for the eye to tell "next article" from
     "same article", so the cards ran together.

     56px is deliberately about 3x the internal gap rather than a round
     number: what matters is the ratio between the space inside a card and the
     space between cards, not the absolute value. */
  .ed-grid .grid, .related .grid { grid-template-columns: 1fr; row-gap: 56px; }
  .article-lead .media { aspect-ratio: 16 / 9; }
  .article-head .head-row { justify-content: flex-start; gap: 16px; }
}
@media (max-width: 480px) {
  .nav .wrap { gap: 16px; }
  .nav-actions { gap: 14px; }
  .nav-actions .btn { padding: 0 14px; }
}

/* ============ Feature animations: shared frame ============
   Every animated feature image sits in one of these: a 16:10 panel in the
   site's grey with a 10 x 10 dot grid centred in it. Each feature then adds
   its own block below, under its own class prefix (sb- for Feature 1, and so
   on), so one animation can be changed without touching another.

   Animations start paused. assets/site.js adds .is-visible to a frame while it
   is on screen, and each feature's block runs its animations only under
   .lg-frame.is-visible, so nothing animates off screen. Each block also needs
   its own prefers-reduced-motion rule, showing a meaningful still frame.

   Same grey and radius as the .media slots (--surface, --radius-lg), so an
   animated slot matches a still one exactly. The marks are pure black. */
.lg-frame {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: #000;   /* the dots and crosses (they draw in currentColor): pure black, not --text's #161616 */
  overflow: hidden;
}
.lg-frame svg {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 45%;   /* 72% of the frame height, as a square */
  height: 72%;
  transform: translate(-50%, -50%);
  overflow: visible;
}
/* 8 x 8 variant: same 108 module and mark size as the 10 x 10 master,
   so the grid is 80% of the size inside the frame.
   Single column only (900px and narrower, where every frame is the same
   width). On desktop the 8 x 8 grids sit in the flipped rows, whose frame is
   already 80% as wide, so the base sizing above draws each cell at exactly the
   size of a 10 x 10 grid's in a full-width frame. Applying this there as well
   would shrink the marks to 80% of the other features'. */
@media (max-width: 900px) {
  .lg-frame--8 svg { width: 36%; height: 57.6%; }
}
/* Photo variant: an image behind the grid, with white marks so they read on
   it. The image is set inline on the frame through lumin_v() in
   site/index.php (style="background-image: url(...)"), so a replaced file
   shows at once. Supply img/feature-N-<name>.webp at 1620 x 1013 (16:10);
   another shape is trimmed by cover rather than stretched. The grey shows
   while the image loads. Used by features 2, 4 and 6. */
.lg-frame--image {
  color: #fff;
  background-color: var(--surface);
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
}
/* White marks on a photo get a little more weight so they hold their own:
   4 viewBox units, about 1px on desktop (a 108-unit cell draws at roughly
   28-31px there), shrinking with the frame on smaller screens. Lines grow
   their stroke by 4; the dots, which have no line to thicken, get a 4-unit
   outline in their own colour, 4 units wider across. Each feature's own
   block keeps its designed weight; these override it on photos only. */
.lg-frame--image .qm-marks path { stroke-width: 8; }                       /* feature 2: 4 + 4 */
.lg-frame--image .qb-dots circle { stroke: currentColor; stroke-width: 4; } /* feature 4: r 7.5 + 2 each side */
.lg-frame--image .lk-marks path { stroke-width: 9; }                       /* feature 6: 5 + 4 */
/* Phones: every frame scales its marks down so far that they look too thin,
   so all seven gain about 1px over their designed weight (the photo frames'
   desktop +4 is replaced by the same +7 here, so all seven match). On a 360-480px phone a
   108-unit cell draws at roughly 13-19px, so 1px is about 7 viewBox units
   (0.9-1.3px across that range). Lines grow by 7; dots get a 7-unit outline in
   their own colour. Tablets and desktop keep the designed weight.
   .lg-frame is in each selector on purpose: the feature blocks come later in
   this file and set their own stroke-width, so without the extra class they
   would win. */
@media (max-width: 480px) {
  .lg-frame .sb-dots circle { stroke: currentColor; stroke-width: 7; }  /* feature 1: r 7.5 + 3.5 each side */
  .lg-frame .bk-marks path { stroke-width: 12; }                        /* feature 3: 5 + 7 */
  .lg-frame .fl-marks path { stroke-width: 11; }                        /* feature 5: 4 + 7 */
  .lg-frame .tr-dots circle { stroke: currentColor; stroke-width: 7; }  /* feature 7: r 7.5 + 3.5 each side */
  .lg-frame--image .qm-marks path { stroke-width: 11; }                 /* feature 2: 4 + 7 */
  .lg-frame--image .qb-dots circle { stroke-width: 7; }                 /* feature 4: r 7.5 + 3.5 each side */
  .lg-frame--image .lk-marks path { stroke-width: 12; }                 /* feature 6: 5 + 7 */
}

/* ============ Feature 1: Recognised for substance (sb-) ============
   Source: for_claude/lumin-feature-1-substance.html. Keep the animation
   exactly as designed there: timings, easing, keyframe percentages and the
   9s loop. Each dot's positions (--x1..--y3), visibility (--o1..--o3) and
   stagger (--s) are inline on its <circle> in site/index.php, by design. */
/* Feature 1: substance, not reach. The busy grid clears in three steps,
   then returns in one move.
   Step 1: the outer ring steps in onto a frame and the inside gathers
   onto a centre square. Step 2: the frame merges into the square.
   Step 3: the square closes down to the core four dots.
   Return: every dot travels straight from the core back to its place.
   The same 100 dots, at rest always on the grid; dots fade as they merge.
   Each dot carries its position after each step (--x1..--y3) and whether
   it is visible there (--o1..--o3). */
.sb-dots circle {
  fill: currentColor;
  animation: sb-distil 9s infinite;
  animation-delay: calc(var(--s) * .4);
  animation-play-state: paused;
}
.lg-frame.is-visible .sb-dots circle { animation-play-state: running; }

/* Timeline (9s loop):
   0.4 to 1.2s  step 1, grid to frame and square
   2.0 to 2.8s  step 2, frame into square
   3.6 to 4.4s  step 3, square into the core four
   4.4 to 6.0s  hold
   6.0 to 7.2s  everything returns to the full grid in one move */
@keyframes sb-distil {
  0%, 4.44%      { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  11.3%          { opacity: 1; }
  13.33%, 22.22% { transform: translate(var(--x1), var(--y1)); opacity: var(--o1); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  29.1%          { opacity: var(--o1); }
  31.11%, 40%    { transform: translate(var(--x2), var(--y2)); opacity: var(--o2); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  46.9%          { opacity: var(--o2); }
  48.89%, 66.67% { transform: translate(var(--x3), var(--y3)); opacity: var(--o3); animation-timing-function: cubic-bezier(.5, 0, .2, 1); }
  70%            { opacity: 1; }
  80%, 100%      { transform: translate(0, 0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .sb-dots circle { animation: none; transform: translate(var(--x3), var(--y3)); opacity: var(--o3); }
}

/* ============ Feature 2: Quality marker (qm-) ============
   Source: for_claude/lumin-feature-2-quality.html. Keep the animation
   exactly as designed there: timings, easing, keyframes and the 7s loop.
   An 8 x 8 grid (viewBox 0 0 864 864) in an .lg-frame.lg-frame--8.
   Each cross's positions (--x1..--y2), turns (--r1, --r2), visibility
   (--o1, --o2) and sweep delay (--s) are inline on its <path> in
   site/index.php, by design. */
/* Feature 2: a quality marker that surfaces the sharpest content.
   The 8 x 8 cross grid is cut into a diamond, like a stone being cut.
   A diagonal sweep runs from the top left corner to the bottom right,
   and every cross that moves turns a quarter turn as it travels, so the
   marks flick through an x and back to a plus.
   Step 1: the corners fold in along the 45 degree diagonals.
   Step 2: the outer edge cuts in one cell, straight across or down.
   Return: the grid unfolds in the same diagonal sweep.
   The same 100 crosses, at rest always on the grid; crosses fade as they
   merge. Each carries its position (--x1..--y2), turn (--r1, --r2) and
   visibility (--o1, --o2) after each step. */
.qm-marks path {
  fill: none;
  stroke: currentColor;
  stroke-width: 4;
  transform-box: fill-box;
  transform-origin: center;
  animation: qm-cut 7s infinite;
  animation-delay: var(--s);
  animation-play-state: paused;
}
.lg-frame.is-visible .qm-marks path { animation-play-state: running; }

/* Timeline (7s loop, plus the sweep offset of up to 0.8s per cross):
   0.4 to 1.2s  step 1, corners fold in along the diagonals
   2.0 to 2.8s  step 2, the edge cuts in
   2.8 to 4.8s  hold
   4.8 to 5.8s  the grid unfolds back to full */
@keyframes qm-cut {
  0%, 5.71%      { transform: translate(0, 0) rotate(0deg); opacity: 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  14.6%          { opacity: 1; }
  17.14%, 28.57% { transform: translate(var(--x1), var(--y1)) rotate(var(--r1)); opacity: var(--o1); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  37.4%          { opacity: var(--o1); }
  40%, 68.57%    { transform: translate(var(--x2), var(--y2)) rotate(var(--r2)); opacity: var(--o2); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  72.5%          { opacity: 1; }
  82.86%, 100%   { transform: translate(0, 0) rotate(0deg); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .qm-marks path { animation: none; transform: translate(var(--x2), var(--y2)); opacity: var(--o2); }
}

/* ============ Feature 3: Backing with Lumin (bk-) ============
   Source: for_claude/lumin-feature-3-backing.html. Keep the animation
   exactly as designed there: timings, easing, keyframes and the 7s loop.
   A 10 x 10 grid (viewBox 0 0 1080 1080) in a plain .lg-frame.
   Each slash's positions (--x1..--y2), visibility (--o1, --o2) and row
   delay (--s) are inline on its <path> in site/index.php, by design. */
/* Feature 3: anyone can like a post, here you back it.
   Every slash keeps its angle. The 10 x 10 grid closes into a band of
   four rows, then the band shears into a block that leans at the same
   45 degrees as the marks, each row starting one cell further right:
   many marks leaning into the same push.
   Step 1: rows above and below step in to the band.
   Step 2: each row slides into its place in the leaning block, bottom
   row first. Return: everything goes back in one move.
   The same 100 slashes, at rest always on the grid; slashes fade as
   they merge. */
.bk-marks path {
  fill: none;
  stroke: currentColor;
  stroke-width: 5;
  animation: bk-lean 7s infinite;
  animation-delay: var(--s);
  animation-play-state: paused;
}
.lg-frame.is-visible .bk-marks path { animation-play-state: running; }

/* Timeline (7s loop, plus a row offset of up to 0.4s):
   0.4 to 1.2s  step 1, rows close in to a band
   2.0 to 2.8s  step 2, the band shears into the leaning block
   2.8 to 4.8s  hold
   4.8 to 5.8s  everything returns to the full grid */
@keyframes bk-lean {
  0%, 5.71%      { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  14.6%          { opacity: 1; }
  17.14%, 28.57% { transform: translate(var(--x1), var(--y1)); opacity: var(--o1); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  37.4%          { opacity: var(--o1); }
  40%, 68.57%    { transform: translate(var(--x2), var(--y2)); opacity: var(--o2); animation-timing-function: cubic-bezier(.5, 0, .2, 1); }
  72.5%          { opacity: 1; }
  82.86%, 100%   { transform: translate(0, 0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .bk-marks path { animation: none; transform: translate(var(--x2), var(--y2)); opacity: var(--o2); }
}

/* ============ Feature 4: Set your own quality bar (qb-) ============
   Source: for_claude/lumin-feature-4-quality-bar.html. Keep the animation
   exactly as designed there: timings, easing, keyframes and the 8s loop.
   An 8 x 8 grid (viewBox 0 0 864 864) in an .lg-frame.lg-frame--8.
   Each dot's offsets (--y1, --y2, --y3), visibility (--o) and start
   offset (--s) are inline on its <circle> in site/index.php, by design. */
/* Feature 4: set your own quality bar.
   The 8 x 8 dot grid drops and merges into a single bar along the
   bottom. The bar slides all the way to the top, then settles back in
   the middle, like a slider being set. Then the grid opens back out
   from the bar.
   Dots only ever move up or down, at rest always on the grid, and fade
   as they merge. Hidden dots travel inside the bar, so the grid can
   reform from wherever the bar settles. Each dot carries its offset at
   the bottom, top and middle (--y1, --y2, --y3), whether it shows as
   part of the bar (--o) and its start offset (--s). */
.qb-dots circle {
  fill: currentColor;
  animation: qb-slider 8s infinite;
  animation-delay: var(--s);
  animation-play-state: paused;
}
.lg-frame.is-visible .qb-dots circle { animation-play-state: running; }

/* Timeline (8s loop):
   0.4 to 1.2s  the grid merges into a bar at the bottom
   1.6 to 2.6s  the bar slides to the top
   3.0 to 3.8s  the bar settles in the middle
   3.8 to 5.2s  hold
   5.2 to 6.4s  the grid opens back out from the bar */
@keyframes qb-slider {
  0%, 5%      { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(.55, 0, .3, 1); }
  12.8%       { opacity: 1; }
  15%, 20%    { transform: translate(0, var(--y1)); opacity: var(--o); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  32.5%, 37.5% { transform: translate(0, var(--y2)); opacity: var(--o); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  47.5%, 65%  { transform: translate(0, var(--y3)); opacity: var(--o); animation-timing-function: cubic-bezier(.5, 0, .2, 1); }
  69%         { opacity: 1; }
  80%, 100%   { transform: translate(0, 0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .qb-dots circle { animation: none; transform: translate(0, var(--y3)); opacity: var(--o); }
}

/* ============ Feature 5: Business Premium, the room (fl-) ============
   Source: for_claude/lumin-feature-5-room.html. Keep the animation
   exactly as designed there: the top left to bottom right wave, timings,
   easing, keyframes and the 8s loop.
   A 10 x 10 grid (viewBox 0 0 1080 1080) in a plain .lg-frame. The SVG
   carries its own clipPath (id="fl-clip") that hides crosses as they
   travel off the grid; the id is unique on the page, keep it so.
   Each moving cross's travel (--t), visibility (--o) and wave delay (--s)
   are inline on its <path> in site/index.php, by design. */
/* Feature 5: your name, in the room where finance actually reads.
   The 10 x 10 cross grid moves from the top left to the bottom right,
   in a wave from the first cross to the last, leaving the 4 x 4 square of crosses at the
   centre behind. Crosses heading for the square merge into it; the rest
   travel off the edge of the grid. Then the grid returns in one move.
   The square never moves. */
.fl-marks path {
  fill: none;
  stroke: currentColor;
  stroke-width: 4;
}
.fl-marks .fl-move {
  animation: fl-flow 8s infinite;
  animation-delay: var(--s);   /* a wave: 60ms later per diagonal, top left to bottom right */
  animation-play-state: paused;
}
.lg-frame.is-visible .fl-marks .fl-move { animation-play-state: running; }

/* Same moves, easing and timing as features 1 to 4, set off as a wave:
   the top left cross moves first and each diagonal follows 60ms later,
   so the bottom right crosses move last, both out and back.
   Timeline (8s loop):
   0.4 to 1.2s  every outer cross travels down and to the right along its
                diagonal; crosses that land on the square disappear as
                they land, the rest travel just past the grid edge, where
                the grid clips them
   1.2 to 6.0s  only the square is left
   6.0 to 7.2s  everything returns to the grid in one move */
@keyframes fl-flow {
  0%, 5%      { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  12.8%       { opacity: 1; }
  15%, 75%    { transform: translate(var(--t), var(--t)); opacity: var(--o); animation-timing-function: cubic-bezier(.5, 0, .2, 1); }
  79%         { opacity: 1; }
  90%, 100%   { transform: translate(0, 0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .fl-marks .fl-move { animation: none; opacity: 0; }
}

/* ============ Feature 6: Privacy, the padlock (lk-) ============
   Source: for_claude/lumin-feature-6-privacy.html. Keep the animation
   exactly as designed there: timings, easing, keyframes and the 7s loop.
   An 8 x 8 grid (viewBox 0 0 864 864) in an .lg-frame.lg-frame--8.
   Each slash's position (--x1, --y1), visibility (--o1, on most) and
   stagger (--s) are inline on its <path> in site/index.php, by design.
   The six shackle slashes carry .lk-rise, which swaps in their own
   keyframes. */
/* Feature 6: privacy, built in, not bolted on.
   Same motion as feature 3: two steps, the same easing and stagger,
   slashes fade as they merge and reappear as they leave, a hold, then
   one move back. Every slash keeps its angle.
   Step 1: the 8 x 8 slash grid gathers into a solid block, six wide and
   five deep, across the lower half: the body of a padlock, its hatching
   like a security pattern.
   Step 2: the shackle rises out of the body: two columns from its top
   edge, joined across the top with the corners left open. The body is
   five rows and the shackle three, so the lock fills the grid's full
   height and sits centred.
   Return: everything goes back in one move. */
.lk-marks path {
  fill: none;
  stroke: currentColor;
  stroke-width: 5;
  animation: lk-lock 7s infinite;
  animation-delay: var(--s);
  animation-play-state: paused;
}
.lk-marks .lk-rise { animation-name: lk-rise; }
.lg-frame.is-visible .lk-marks path { animation-play-state: running; }

/* Timeline (7s loop, plus a stagger of up to 0.45s, lower rows first):
   0.4 to 1.2s  step 1, the grid gathers into the lock body
   2.0 to 2.8s  step 2, the shackle rises out of the body
   2.8 to 4.8s  hold
   4.8 to 5.8s  everything returns to the full grid */
@keyframes lk-lock {
  0%, 5.71%      { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  14.6%          { opacity: 1; }
  17.14%, 68.57% { transform: translate(var(--x1), var(--y1)); opacity: var(--o1); animation-timing-function: cubic-bezier(.5, 0, .2, 1); }
  72.5%          { opacity: 1; }
  82.86%, 100%   { transform: translate(0, 0); opacity: 1; }
}
@keyframes lk-rise {
  0%, 5.71%      { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  14.6%          { opacity: 1; }
  17.14%, 28.57% { transform: translate(var(--x1), var(--y1)); opacity: 0; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  31.2%          { opacity: 1; }
  40%, 100%      { transform: translate(0, 0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .lk-marks path { animation: none; transform: translate(var(--x1), var(--y1)); opacity: var(--o1); }
  .lk-marks .lk-rise { transform: none; opacity: 1; }
}

/* ============ Feature 7: Analytics, the trace (tr-) ============
   Source: for_claude/lumin-feature-7-trace.html. Keep the animation
   exactly as designed there: timings, easing, keyframes and the 7s loop.
   A 10 x 10 grid (viewBox 0 0 1080 1080) in a plain .lg-frame.
   Each dot's offset (--y1), visibility (--o1, on most) and stagger (--s)
   are inline on its <circle> in site/index.php, by design. The dots under
   the line carry .tr-fill, which swaps in their own keyframes. */
/* Feature 7: a clear view of your content and your activity.
   Same motion as the other features: two steps, the same easing and
   stagger, dots fade as they merge and reappear as they leave, a hold,
   then one move back.
   Step 1: every column of the 10 x 10 dot grid gathers onto a single
   dot at its own height, so the grid becomes a line of ten dots that
   rises and dips across the frame, drawing in from left to right.
   Step 2: the dots under the line drop back in beneath it, filling the
   area below the line like an area chart.
   Return: everything goes back in one move. Dots only move up or down. */
.tr-dots circle {
  fill: currentColor;
  animation: tr-line 7s infinite;
  animation-delay: var(--s);
  animation-play-state: paused;
}
.tr-dots .tr-fill { animation-name: tr-fill; }
.lg-frame.is-visible .tr-dots circle { animation-play-state: running; }

/* Timeline (7s loop, plus a left to right stagger of up to 0.5s):
   0.4 to 1.2s  step 1, the columns gather into the line
   2.0 to 2.8s  step 2, the area under the line fills in
   2.8 to 4.8s  hold
   4.8 to 5.8s  everything returns to the full grid */
@keyframes tr-line {
  0%, 5.71%      { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  14.6%          { opacity: 1; }
  17.14%, 68.57% { transform: translate(0, var(--y1)); opacity: var(--o1); animation-timing-function: cubic-bezier(.5, 0, .2, 1); }
  72.5%          { opacity: 1; }
  82.86%, 100%   { transform: translate(0, 0); opacity: 1; }
}
@keyframes tr-fill {
  0%, 5.71%      { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  14.6%          { opacity: 1; }
  17.14%, 28.57% { transform: translate(0, var(--y1)); opacity: 0; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  31.2%          { opacity: 1; }
  40%, 100%      { transform: translate(0, 0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .tr-dots circle { animation: none; transform: translate(0, var(--y1)); opacity: var(--o1); }
  .tr-dots .tr-fill { transform: none; opacity: 1; }
}
