/* css/site.css
 *
 * The public pages of evoai.bio: the landing page, references, terms, privacy and the 404. The shell that
 * this file styles (header, nav, token pills, footer) is rendered by js/shell.js and is shared with the app,
 * so everything under "the shell" below is on every route.
 *
 * Requires css/tokens.css first.
 *
 * Two references, both measured, both in docs/research:
 *   REF-APTOS  the instrument panel: numbered sections, 1px hairlines instead of shadows, bracketed
 *              uppercase mono micro labels, a framed canvas panel, a version stamp, sharp corners.
 *   REF-LIGHT  the restraint: a calm type scale, prose at a 680px measure, few paints, hover as colour and
 *              opacity only, nothing that springs or bounces, and the load choreography in section 5.3.
 *
 * There are no box shadows in this file. Depth is a hairline, a gradient border and a backdrop blur.
 */

/* =============================================================================== the shell: header */

.skip-link:focus,
.skip-link:focus-visible {
  position: fixed;
  inset: auto auto auto 0;
  top: 0;
  z-index: 100;
  width: auto;
  height: auto;
  margin: 0;
  padding: 12px 18px;
  clip: auto;
  clip-path: none;
  background: var(--surface-2);
  border: var(--hairline-bright);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: var(--t-small);
}

/* Two rows under 720 (brand and pills, then the nav), one row from 720 up. One set of pills either way:
   hiding a token control at any width is a hard failure in the gate, and rightly so. */
.site-header {
  --pad: max(var(--gutter), calc((100% - var(--w-wide)) / 2 + var(--gutter)));
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-header);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: 'brand tokens' 'nav nav';
  align-items: center;
  column-gap: var(--sp-4);
  padding-inline: var(--pad);
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border-bottom: var(--hairline);
}

.brand {
  grid-area: brand;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding-right: 4px;
  text-decoration: none;
  color: var(--text);
  transition: color var(--dur-fast) var(--ease);
}

.brand:hover { color: var(--green-300); }

.brand .mark { display: block; flex: 0 0 auto; }

.brand__word {
  font-family: var(--font-mono);
  font-size: 17px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1;
}

.site-header__tokens {
  grid-area: tokens;
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}

/* -- the two token pills. While TOKEN.ca and TOKEN.buyUrl are blank these are visibly disabled and read
      "Soon". They are never hidden, never an anchor and never href="#". */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 0 12px;
  background: transparent;
  border: var(--hairline);
  border-radius: var(--r-xs);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

.pill:disabled {
  cursor: not-allowed;
  opacity: 1;                 /* visibly disabled, not faded out of existence */
  color: var(--text-muted);
  border-color: var(--line);
}

.pill__label { color: var(--text); }
.pill:disabled .pill__label { color: var(--text-muted); }

.pill__state {
  display: inline-flex;
  align-items: center;
  padding: 3px 7px;
  border-radius: var(--r-xs);
  background: color-mix(in srgb, var(--moss) 30%, transparent);
  color: var(--green-300);
  font-size: var(--t-nano);
  letter-spacing: var(--track-label);
  line-height: 1.2;
}

.pill--live:hover { color: var(--text); border-color: var(--green-600); }

/* Below 1024 the nav and both pills share one row, and REFERENCES is the first thing to be cut if the
   pills keep their long labels. The aria-label on each pill carries the full name either way. */
@media (max-width: 1023px) { .pill__ext { display: none; } }

/* At 320 the brand, both pills and the gutters have to share 288px. Everything stays visible and stays a
   40px target: only the paddings and the tracking give. */
@media (max-width: 400px) {
  .site-header { column-gap: 8px; }
  .site-header__tokens { gap: 6px; }
  .brand { gap: 8px; }
  .brand__word { font-size: 15px; letter-spacing: 0.08em; }
  .pill { padding: 0 8px; gap: 6px; font-size: var(--t-nano); letter-spacing: 0.04em; }
  .pill__state { padding: 2px 5px; font-size: 10px; letter-spacing: 0.04em; }
}

/* -- the nav row
 *
 * The nav scrolls sideways under 768 and its scrollbar is hidden, which at 320 left REFERENCES ending exactly
 * on the viewport edge with nothing to suggest it could be swiped. Two things fix it and both are needed: a
 * real trailing gutter, so the last item is never flush against the edge, and a fade at the right while the
 * box can still be scrolled, so the swipe is discoverable. The layout gate cannot see this, because the
 * overflow is inside a nested overflow-x:auto and the document itself never scrolls sideways. */
.site-nav {
  grid-area: nav;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-inline: calc(var(--pad) * -1);
  padding-inline: var(--pad);
  /* A gutter the same size as the page gutter at the end of the scroller, so REFERENCES can never sit on the
     edge of the glass. scroll-padding keeps a keyboard scroll inside the same gutter. */
  padding-inline-end: calc(var(--pad) + var(--sp-2));
  scroll-padding-inline: var(--pad);
  border-top: var(--hairline);
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scrollbar-width: none;
  /* The fade only exists where the box really overflows: a mask with a hard stop at 100 percent of the
     SCROLL width is the same as no mask when nothing is cut off. */
  mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 16px), transparent 100%);
  mask-repeat: no-repeat;
}

/* Nothing is cut off from 768 up, so the fade would only dim the last link for no reason. */
@media (min-width: 720px) { .site-nav { mask-image: none; padding-inline-end: 0; } }

.site-nav::-webkit-scrollbar { display: none; }

.site-nav__row {
  display: flex;
  align-items: center;
  gap: 4px;
}

.site-nav__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  min-width: 44px;
  padding: 0 10px;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text-muted);
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease);
}

.site-nav__link:hover { color: var(--green-300); }
.site-nav__link.is-active { color: var(--text); }

/* All four labels have to fit inside 288px with a real gutter left at the end, or REFERENCES ends exactly on
   the edge of the glass with its scrollbar hidden and the nav reads as broken rather than as swipeable.
   Measured at 320 before this: the row was 304px wide inside a 288px box. These rules have to sit AFTER the
   base rule above, not inside the narrow header block further up: the two selectors have the same
   specificity, so at equal specificity the later one wins whatever the media query says, which is why the
   earlier 320 override never applied. Every link is still a 40px tall target. */
@media (max-width: 420px) {
  .site-nav__link {
    padding: 0 5px;
    min-width: 40px;
    font-size: var(--t-nano);
    letter-spacing: 0.04em;
  }
  .site-nav__row { gap: 2px; }
}

/* The version stamp used to sit at the right hand end of the nav, where at 1440 it ran into the BUY $EVOAI
   pill and was cut mid string. A version stamp belongs beside the project line, so that is where it is now,
   at every width, where it has room and where a reader looking for it will look. */
.site-footer__stamp {
  margin: var(--sp-2) 0 0;
  /* --text-dim, not --text-faint: --text-faint is 4.28:1 on --bg and 4.40:1 on the footer's --bg-deep, both
     under the 4.5 the layout gate enforces for text this size. The token file says so beside the token. */
  color: var(--text-dim);
  white-space: nowrap;
}

/* The single row header already fits at 768, and at 200 percent page zoom a 1440 by 900 window is a 720 by
   450 viewport: the two row header would take 88 of those 450 pixels, fixed, on every scroll position. The
   single row form fits at 720 too, so the breakpoint starts there. */
@media (min-width: 720px) {
  .site-header {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: 'brand nav tokens';
    column-gap: var(--sp-6);
    min-height: 64px;
  }
  .site-nav {
    margin-inline: 0;
    padding-inline: 0;
    border-top: 0;
    justify-content: flex-start;
    gap: var(--sp-6);
    min-width: 0;
  }
  .site-nav__row { gap: 2px; }
}

/* =============================================================================== the shell: footer */

.site-footer {
  border-top: var(--hairline);
  background: var(--bg-deep);
  margin-top: var(--sp-12);
}

.site-footer__inner {
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: var(--sp-10) var(--gutter) var(--sp-9);
}

.site-footer__top {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-9);
}

@media (min-width: 900px) {
  .site-footer__top { grid-template-columns: minmax(280px, 1fr) minmax(360px, 1.1fr); gap: var(--sp-10); }
}

.brand--footer { margin-bottom: var(--sp-4); }

.site-footer__line {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  letter-spacing: 0.04em;
  color: var(--text);
  margin-bottom: var(--sp-4);
}

.site-footer__required {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}

.site-footer__nav {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--sp-7);
}

.site-footer__head {
  color: var(--text-dim);
  margin-bottom: var(--sp-3);
  font-weight: 400;
}

.site-footer__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }

.site-footer__link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--text-muted);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}

.site-footer__link:hover { color: var(--green-300); }
.site-footer__link .icon { opacity: 0.7; flex: 0 0 auto; }

.site-footer__bottom {
  margin-top: var(--sp-9);
  padding-top: var(--sp-6);
  border-top: var(--hairline);
  display: grid;
  gap: var(--sp-3);
  max-width: 880px;
}

.site-footer__attr,
.site-footer__disclaimer {
  font-size: var(--t-micro);
  line-height: 1.7;
  color: var(--text-muted);
}

/* ============================================================================================ toasts */

.toasts {
  position: fixed;
  z-index: 95;
  inset: auto var(--gutter) var(--gutter) auto;
  display: grid;
  gap: 8px;
  justify-items: end;
  pointer-events: none;
}

.toasts__item {
  padding: 10px 14px;
  background: var(--surface-2);
  border: var(--hairline-bright);
  border-radius: var(--r-xs);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--text);
}

/* ============================================================================ layout and primitives */

main#main { display: block; }

.wrap {
  max-width: var(--w-wide);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

.prose { max-width: var(--measure); }

.prose p,
.prose li { font-size: var(--t-body); line-height: var(--lh-prose); color: var(--text-muted); }
.prose p + p { margin-top: var(--sp-6); }
.prose strong { color: var(--text); font-weight: 600; }

.lede {
  font-size: var(--t-lede);
  line-height: 1.55;
  color: var(--text);
  max-width: 34ch;
}

.link {
  color: var(--green-300);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--green-500) 45%, transparent);
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

.link:hover { color: var(--text); border-bottom-color: var(--green-300); }

.rule { border: 0; border-top: var(--hairline); margin: 0; }

/* -- the numbered section, the instrument panel spine */

.section {
  position: relative;
  padding-block: var(--section-y);
  border-top: var(--hairline);
}

.section:first-of-type { border-top: 0; }

.section__head {
  display: grid;
  gap: var(--sp-4);
  margin-bottom: var(--sp-8);
}

@media (min-width: 900px) {
  .section__head {
    grid-template-columns: 120px minmax(0, 1fr);
    gap: var(--sp-8);
    align-items: start;
  }
}

.section__index {
  display: flex;
  align-items: baseline;
  gap: 8px;
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

.section__num {
  font-size: var(--t-small);
  color: var(--green-500);
}

.section__title {
  font-size: var(--t-h2);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
  max-width: 22ch;
}

.section__lede {
  margin-top: var(--sp-5);
  max-width: var(--measure);
  font-size: var(--t-lede);
  line-height: 1.55;
  color: var(--text-muted);
}

.section__body { display: grid; gap: var(--sp-7); }

@media (min-width: 900px) {
  .section__body { grid-template-columns: 120px minmax(0, 1fr); gap: var(--sp-8); }
  .section__body > .section__aside { grid-column: 1; }
  .section__body > * { grid-column: 2; }
  .section__body > .section__full { grid-column: 1 / -1; }
}

.section__aside {
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  line-height: 1.7;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* -- buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 48px;
  padding: 0 20px;
  border: var(--hairline-bright);
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease), opacity var(--dur-reveal) var(--ease-in-out);
}

.btn:hover { color: var(--bg-deep); background: var(--green-400); border-color: var(--green-400); }
.btn .icon { opacity: 0.9; }

.btn--primary {
  background: var(--green-500);
  border-color: var(--green-500);
  color: var(--bg-deep);
  font-weight: 500;
}

.btn--primary:hover { background: var(--green-300); border-color: var(--green-300); color: var(--bg-deep); }

.btn--quiet { border-color: var(--line); color: var(--text-muted); }
.btn--quiet:hover { color: var(--bg-deep); background: var(--green-400); border-color: var(--green-400); }

/* -- panels */

.panel {
  position: relative;
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  border: 1px solid transparent;
  border-image: var(--panel-border);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
}

.panel__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: 10px 14px;
  border-bottom: var(--hairline);
  color: var(--text-dim);
}

.panel__body { padding: var(--sp-6); }

.bracket::before { content: '[ '; color: var(--green-600); }
.bracket::after { content: ' ]'; color: var(--green-600); }

blockquote { margin: 0; }

.quote {
  border-left: 2px solid var(--green-600);
  padding-left: var(--sp-5);
}

.quote p {
  font-size: var(--t-lede);
  line-height: 1.55;
  color: var(--text);
}

/* -- the honesty note */

.note {
  border-left: 2px solid var(--line-bright);
  padding: 2px 0 2px var(--sp-4);
  font-size: var(--t-small);
  line-height: 1.7;
  color: var(--text-muted);
  max-width: var(--measure);
}

.note--warn { border-left-color: var(--warn); }
.note strong { color: var(--text); font-weight: 600; }

/* -- cards */

.grid {
  display: grid;
  border-top: var(--hairline);
  border-left: var(--hairline);
}

.grid--2 { grid-template-columns: 1fr; }
.grid--3 { grid-template-columns: 1fr; }

@media (min-width: 700px) { .grid--2 { grid-template-columns: repeat(2, 1fr); } .grid--3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .grid--3 { grid-template-columns: repeat(3, 1fr); } }

/* Seven simulation cards in a three column grid leave the seventh alone in row three, a third of the page
   wide, so the block finishes on a ragged step. The last card spans the remaining columns instead, which
   also gives the longest blurb on the page the room it wants. */
@media (min-width: 700px) {
  [data-sims] > .cell:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (min-width: 1000px) {
  [data-sims] > .cell:last-child:nth-child(3n + 1) { grid-column: 1 / -1; }
  [data-sims] > .cell:last-child:nth-child(3n + 2) { grid-column: span 2; }
}

.cell {
  padding: var(--sp-6);
  display: grid;
  gap: 10px;
  align-content: start;
  border-right: var(--hairline);
  border-bottom: var(--hairline);
}

.cell__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  color: var(--text-dim);
}

.cell__title { font-size: var(--t-h3); font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
.cell__text { font-size: var(--t-small); line-height: 1.65; color: var(--text-muted); }

.cell .icon { color: var(--green-500); }

.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  border: var(--hairline);
  border-radius: var(--r-xs);
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.tag--on { color: var(--green-300); border-color: var(--line-bright); }

/* -- tables */

/* A sideways scroller with no affordance reads as broken text, not as something you can swipe. macOS hides
 * its overlay scrollbar until you scroll, so at 320 the credits table simply said "CREDITS ARE KEYED TO A
 * QUESTIO" and stopped. Three things, all of them cheap:
 *   the fade at the right edge, which disappears on its own once the box is scrolled to the end
 *   a visible slim scrollbar, rather than the hidden one
 *   scroll-snap off, because these are tables and a snap would fight a careful read
 * `mask-image` with a stop expressed against the CONTENT box means no fade at all when nothing overflows. */
.table-scroll {
  --scroll-ground: var(--bg);
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  /* An always visible slim scrollbar rather than the platform's overlay one, which on macOS is invisible
     until the visitor already knows to swipe. */
  scrollbar-width: thin;
  scrollbar-color: var(--line-bright) transparent;
  /* The classic two layer affordance, and the only one that is right in every browser: the FADE is attached
     to the scroll box and stays where it is, the COVER is attached to the content and slides with it. At the
     far right the cover lands on top of the fade and the fade disappears, so the edge is only ever soft while
     there is really something past it. */
  background-image:
    linear-gradient(to left, var(--scroll-ground) 30%, transparent 100%),
    linear-gradient(to left, color-mix(in srgb, var(--bg-deep) 92%, transparent) 0%, transparent 100%);
  background-position: right center, right center;
  background-size: 28px 100%, 34px 100%;
  background-repeat: no-repeat, no-repeat;
  background-attachment: local, scroll;
}

/* The hint under a scroller, printed by the page, so the swipe is discoverable without relying on a fade. */
.table-scroll + .table-hint,
.scroll-hint {
  margin-top: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-dim);
}

@media (min-width: 760px) { .scroll-hint { display: none; } }

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-small);
}

.table caption {
  text-align: left;
  padding-bottom: var(--sp-4);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.table th,
.table td {
  text-align: left;
  padding: 14px 16px 14px 0;
  border-bottom: var(--hairline);
  vertical-align: top;
}

.table thead th {
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  font-weight: 400;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  border-bottom: var(--hairline-bright);
  padding-bottom: 10px;
}

.table tbody th {
  font-weight: 400;
  color: var(--text-muted);
  padding-right: var(--sp-6);
}

.table td {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
}

.table td.is-wide { white-space: normal; }

/* At 320 the page gutter leaves 288px and these tables were eleven pixels over it. Tightening the cell
   padding on a phone is the difference between a table that fits and one that has to be swiped. */
@media (max-width: 420px) {
  .table th,
  .table td { padding: 12px 10px 12px 0; }
  .table tbody th { padding-right: var(--sp-4); }
}

/* ==================================================================================== the reveal */

/* Nothing is hidden until the script says it can hide things, so a visitor with no JavaScript, and a
   browser whose IntersectionObserver never fires, both read the whole page. */
.js [data-reveal] {
  opacity: 0;
  transition: opacity var(--dur-reveal) var(--ease-out);
}

.js [data-reveal].is-in { opacity: 1; }

/* Set by js/site.js on anything that was already in view at first paint, for the two frames it takes to
   put it on screen without an animation. */
.js [data-reveal].no-anim { transition: none; }

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { opacity: 1; }
}

/* ========================================================================================= the hero */

.hero {
  --pad: max(var(--gutter), calc((100% - var(--w-wide)) / 2 + var(--gutter)));
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  align-items: stretch;
  border-top: 0;
  padding-block: 0;
  min-height: 560px;
}

.hero__viz {
  position: relative;
  min-height: 46vh;
  height: calc(56vh + var(--header-h));
  border-bottom: var(--hairline);
  background: var(--bg-deep);
}

.hero__viz .viz { position: absolute; inset: 0; }

/* The canvas fades up over a full second, the copy over half of one. REF-LIGHT section 5.3. */
.js .hero__viz { opacity: 0; transition: opacity var(--dur-canvas) var(--ease-in-out); }
.js .hero__viz.is-in { opacity: 1; }

.hero__copy {
  display: grid;
  align-content: center;
  gap: var(--sp-5);
  padding: var(--sp-9) var(--pad) var(--sp-10);
}

/* The copy does NOT fade. Text whose opacity is animated is, for the length of the animation, painted
   below the contrast ratio it needs, and a visitor on a slow frame really does see it that way. The
   headline reveals instead by uncovering itself, so every character is either painted at its full colour
   or not painted at all. Same 100 ms steps and same half second as the reference, different property. */
.js .hero__copy { opacity: 1; }

.hero__mark {
  font-family: var(--font-mono);
  font-size: var(--t-display);
  font-weight: 500;
  line-height: 0.95;
  letter-spacing: 0.02em;
  color: var(--text);
  text-transform: uppercase;
}

/* The headline lands one word at a time, 100 ms apart, each over half a second. */
.hero__word { display: inline-block; }

.js .hero__word {
  clip-path: inset(0 100% -0.3em 0);
  transition: clip-path var(--dur-reveal) var(--ease-in-out);
}

.js .hero__word.is-in { clip-path: inset(0 0 -0.3em 0); }

.hero__title { display: grid; gap: var(--sp-5); font-weight: 600; }

.hero__line {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--t-lede);
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: 0;
  color: var(--text-muted);
  max-width: 30ch;
}

.hero__project {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--green-400);
}

.hero__actions { display: flex; flex-wrap: wrap; gap: var(--sp-4); }

/* The two controls are painted from the first frame. A control that fades in is not hit testable while it
   fades, and a 48px target that answers nothing for a second is worse than one that simply arrives with the
   page. This is the one place the reference's choreography is not copied, and this is why. */

.hero__caption {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* The separator is a character a screen reader and a contrast checker both see, so it gets a text colour
   (5.94:1 on the page ground), not the hairline colour. */
.hero__caption .dot { color: var(--green-600); }

.hero__legend { margin-top: var(--sp-3); max-width: 46ch; }

.hero__source {
  margin-top: var(--sp-3);
  display: block;
  line-height: 2.1;
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.hero__source > * { margin-right: 18px; }
.hero__source > *:last-child { margin-right: 0; }

@media (min-width: 900px) {
  .hero {
    grid-template-columns: minmax(340px, 45%) minmax(0, 1fr);
    min-height: min(92vh, 940px);
  }
  .hero__copy {
    grid-column: 1;
    grid-row: 1;
    padding: calc(var(--header-h) + var(--sp-9)) var(--sp-9) var(--sp-10) var(--pad);
  }
  .hero__viz {
    grid-column: 2;
    grid-row: 1;
    height: auto;
    min-height: 0;
    border-bottom: 0;
    border-left: var(--hairline);
  }
}

/* ================================================================================= section 04, live */

.finding { display: grid; gap: var(--sp-6); }

.finding__state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.finding__state .on { color: var(--green-300); }
.finding__state .warn { color: var(--warn); }

.gate-state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  margin-bottom: var(--sp-5);
}

.gate-state span:not(.tag) {
  font-size: var(--t-small);
  color: var(--text-muted);
}

.skel {
  display: inline-block;
  width: 84px;
  height: 11px;
  vertical-align: middle;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--line-bright) 70%, transparent),
    color-mix(in srgb, var(--line) 60%, transparent));
  border-radius: 1px;
}

.js .skel { animation: skel 1.4s var(--ease-in-out) infinite; }

@keyframes skel { 0%, 100% { opacity: 0.45; } 50% { opacity: 0.9; } }

.finding__timing {
  display: grid;
  gap: 2px;
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.finding__timing b { color: var(--text-muted); font-weight: 400; }

/* A 66 character hash wrapped onto a flush left continuation makes three labelled rows read as six ragged
   ones. The label gets its own line and the digest hangs under it, so the block scans as three things. */
.finding__timing > div { padding-left: 0; }
[data-finding-hashes] > div {
  display: grid;
  gap: 2px;
  padding-block: 4px;
}
[data-finding-hashes] b { display: block; }
[data-finding-hashes] .hashval { padding-left: var(--sp-3); }

.hashval {
  overflow-wrap: anywhere;
  word-break: break-all;
  color: var(--green-300);
  /* A keccak digest is lowercase hex with a lowercase x. The uppercase run of the label around it must not
     reach into it, or the page prints 0X where every tool prints 0x. */
  text-transform: none;
}

/* Under 700 the finding table stacks: the metric on its own line, then each value with its own column
   label. No sideways scrolling on the one section that has to be readable on a phone. */
@media (max-width: 699px) {
  .finding-table,
  .finding-table tbody,
  .finding-table tr,
  .finding-table th,
  .finding-table td { display: block; width: auto; }

  .finding-table thead { display: none; }
  .finding-table caption { display: block; width: auto; }

  .finding-table tr {
    padding: 14px 0;
    border-bottom: var(--hairline);
  }

  .finding-table tbody th {
    padding: 0 0 8px;
    border: 0;
    color: var(--text);
  }

  .finding-table td {
    display: grid;
    grid-template-columns: 62px minmax(0, 1fr);
    gap: 10px;
    padding: 2px 0;
    border: 0;
    white-space: normal;
  }

  .finding-table td::before {
    content: attr(data-col);
    color: var(--text-dim);
    font-size: var(--t-nano);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
  }

  .finding-table td.is-span { grid-template-columns: minmax(0, 1fr); }
  .finding-table td.is-span::before { content: none; }
}

/* ============================================================================ the run log, section 05 */

.log { display: grid; border-top: var(--hairline); border-left: var(--hairline); border-right: var(--hairline); }

.log__row {
  border-bottom: var(--hairline);
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px;
  padding: 14px var(--sp-5);
}

@media (min-width: 700px) {
  .log__row { grid-template-columns: 64px 1fr 120px 120px; align-items: center; gap: var(--sp-4); }
}

.log__cell {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.log__cell--id { color: var(--green-500); }
.log__empty { padding: var(--sp-7) var(--sp-5); border-bottom: var(--hairline); }

/* ========================================================================= specimens strip, section 08 */

.spec-grid {
  display: grid;
  border-top: var(--hairline);
  border-left: var(--hairline);
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

.spec {
  padding: var(--sp-5);
  display: grid;
  gap: 6px;
  align-content: start;
  text-decoration: none;
  border-right: var(--hairline);
  border-bottom: var(--hairline);
  transition: background-color var(--dur-fast) var(--ease);
}

a.spec:hover { background: var(--surface); }

.spec__id {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  letter-spacing: 0.1em;
  color: var(--green-300);
}

.spec__label { font-size: var(--t-small); color: var(--text); line-height: 1.4; }

.spec__meta {
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* ================================================================================== references page */

.ref-list { display: grid; border: var(--hairline); border-bottom: 0; }

.ref {
  padding: var(--sp-6);
  display: grid;
  gap: 8px;
  border-bottom: var(--hairline);
}

.ref__title { font-size: var(--t-h3); font-weight: 600; color: var(--text); line-height: 1.35; }
.ref__authors { font-size: var(--t-small); color: var(--text-muted); line-height: 1.6; }

.ref__meta {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.ref__links { margin-top: 2px; line-height: 2; }
.ref__links > * { margin-right: 16px; }
.ref__links > *:last-child { margin-right: 0; }

.ref-entry {
  padding: var(--sp-5) var(--sp-6);
  display: grid;
  gap: 6px;
  border-bottom: var(--hairline);
}

@media (min-width: 900px) {
  .ref-entry { grid-template-columns: 96px minmax(0, 1fr) 190px; gap: var(--sp-5); align-items: start; }
}

.ref-entry__id {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  letter-spacing: 0.12em;
  color: var(--green-300);
}

.ref-entry__title { font-size: var(--t-small); color: var(--text); line-height: 1.5; }
.ref-entry__cite { font-size: var(--t-micro); color: var(--text-muted); line-height: 1.6; margin-top: 4px; }

.ref-entry__meta {
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
  line-height: 1.7;
  font-variant-numeric: tabular-nums;
}

/* ======================================================================== plain document pages, 404 */

.page-head {
  padding-block: calc(var(--header-h) + var(--sp-10)) var(--sp-8);
  border-bottom: var(--hairline);
}

.page-head__title {
  font-size: var(--t-h2);
  font-weight: 600;
  letter-spacing: -0.02em;
  margin-top: var(--sp-4);
  max-width: 24ch;
}

.page-head__lede { margin-top: var(--sp-5); max-width: var(--measure); color: var(--text-muted); font-size: var(--t-lede); line-height: 1.55; }
.page-head .stamp { margin-top: var(--sp-5); }

.doc { padding-block: var(--sp-9) var(--sp-11); }
.doc h2 { font-size: var(--t-h3); margin-top: var(--sp-9); margin-bottom: var(--sp-4); color: var(--text); }
.doc h2:first-of-type { margin-top: 0; }
.doc h3 { font-size: var(--t-body); margin-top: var(--sp-6); margin-bottom: var(--sp-3); color: var(--text); }
.doc p, .doc li { font-size: var(--t-small); line-height: 1.8; color: var(--text-muted); }
.doc p + p { margin-top: var(--sp-4); }
.doc ul, .doc ol { margin: var(--sp-4) 0 0; padding-left: 20px; display: grid; gap: 8px; }
.doc ul + p, .doc ol + p, .doc ul + h2, .doc ol + h2 { margin-top: var(--sp-5); }
.doc .updated { font-family: var(--font-mono); font-size: var(--t-micro); color: var(--text-dim); letter-spacing: 0.04em; }

.notfound {
  min-height: 64vh;
  display: grid;
  align-content: center;
  gap: var(--sp-6);
  padding-block: calc(var(--header-h) + var(--sp-11)) var(--sp-11);
}

.notfound__code {
  font-family: var(--font-mono);
  font-size: clamp(56px, 12vw, 120px);
  line-height: 1;
  color: var(--green-500);
  letter-spacing: 0.04em;
}

/* ===================================================================================== small print */

/* A row of small mono links and facts. Deliberately NOT a flex container: a flex item computes to a block,
   and the layout gate's 40px tap target rule exempts an inline link inside prose and nothing else. These
   are words in a line, so they stay inline and the line height carries the spacing. */
.stamp {
  display: block;
  line-height: 2.1;
  font-family: var(--font-mono);
  font-size: var(--t-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.stamp > * { margin-right: 18px; }
.stamp > *:last-child { margin-right: 0; }

/* A DOI name is case insensitive and it still resolves in capitals, but a DOI printed in capitals is not the
 * published form of the identifier, and on a page whose whole job is attribution the identifier is the
 * point. The uppercase run of the mono voice around it must not reach into it. Same rule as .hashval. */
.doi,
.stamp .doi,
.ref__links .doi { text-transform: none; }

.stack { display: grid; gap: var(--sp-5); }
.stack--tight { gap: var(--sp-3); }
.stack--wide { gap: var(--sp-7); }
