/*
 * JB Capital — the shared sheet.
 *
 * One stylesheet for all three pages. It was inline in index.html while there
 * was one page; three inline copies would drift, and drift here means the
 * instrument plate rendering differently depending on which page you measured
 * it on, which would quietly break the one promise the site makes.
 *
 * That promise, and every rule below defends it: NO NUMBER RENDERS UNLESS THE
 * BROWSER MEASURED IT, Intl COMPUTED IT, OR THE NETWORK REPORTED IT.
 *
 * Serif sets sentences a human wrote. Mono sets every label and readout.
 */

:root{
  --ink:#0B1219;
  --ink-2:#111C25;
  --paper:#F4F2ED;
  --steel:#7FA8BD;
  --rule:rgba(244,242,237,.14);
  --rule-dark:rgba(11,18,25,.12);

  --display:ui-serif,"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
  --sans:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Helvetica,Arial,sans-serif;
  /* Mono is the structural voice: every label, coordinate and readout. The
     serif sets only the sentences a human wrote. No webfont, no request. */
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;

  --gutter:clamp(1.5rem,6vw,7rem);
  --measure:38rem;
  --nav-h:clamp(58px,4.5rem,72px);

  /* Nav text colour. Flipped by a 1px sentinel at the top of each paper-ground
     section. Defaults to paper because every page opens on ink, so with JS off
     the default is correct by construction rather than by luck. */
  --nav-ink:var(--paper);
}
[data-ground="paper"]{--nav-ink:var(--ink)}

*,*::before,*::after{box-sizing:border-box}
/* Canvas matches the FOOTER, not the hero: any gap below short content, and
   mobile overscroll at the bottom of the page, land on paper rather than a
   stripe of ink under a light footer. */
html{-webkit-text-size-adjust:100%;background:var(--paper)}
body{
  margin:0;
  background:var(--ink);
  color:var(--paper);
  font-family:var(--sans);
  font-size:clamp(1rem,.94rem + .3vw,1.0625rem);
  line-height:1.65;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
  /* Short pages (/people is ~50 words) must still push the footer to the
     bottom of the viewport rather than leaving it floating mid-screen. */
  min-height:100svh;
  display:flex;
  flex-direction:column;
}
main{flex:1}
img{max-width:100%;display:block}

/* Film grain. The cheapest thing that separates a flat page from one that
   looks photographed. Kept very low and pointer-transparent. */
body::after{
  content:"";
  position:fixed;
  inset:-50%;
  z-index:9;
  pointer-events:none;
  opacity:.035;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* -- type primitives ------------------------------------------------------ */

/* Tabular figures are load-bearing, not a nicety: without them the clocks
   re-flow the nav on every update, and that single detail is the difference
   between instrument-grade and hobbyist. */
.mono{
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-feature-settings:'tnum' 1;
  letter-spacing:.14em;
  text-transform:uppercase;
}

.label{
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-size:.6875rem;
  letter-spacing:.16em;
  text-transform:uppercase;
  font-weight:500;
  color:var(--steel);
  margin:0 0 1.75rem;
}

/* Folio: the rule breaks around the number the way a book folio does. The nav
   is the same device at page scale, which is why the header is not a new idea. */
.folio{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:1rem}
.folio .rule{display:block;height:1px;background:var(--rule);min-width:2rem}
.contact .folio .rule,.paper .folio .rule{background:var(--rule-dark)}

.lede{
  font-family:var(--display);
  font-weight:400;
  font-size:clamp(1.5rem,1.1rem + 1.9vw,2.75rem);
  line-height:1.28;
  letter-spacing:-.012em;
  max-width:var(--measure);
  margin:0;
}
.lede + p{
  margin:2rem 0 0;
  max-width:var(--measure);
  color:rgba(244,242,237,.62);
}

/* -- chrome --------------------------------------------------------------- */

/* The entire bezel: one rectangle. mix-blend-mode:difference against white
   resolves to near-paper over the ink sections and near-ink over the paper
   one, so it inverts across the contact boundary with no JS and no second
   element. Against a mid-tone difference collapses to ~1:1, which is why only
   hairlines use it and never text.

   The eight backgrounds are corner registration marks: an L at each corner,
   inset 14px. Same element, same blend, no new mechanism. */
.plate-frame{
  position:fixed;
  inset:clamp(14px,2vw,26px);
  border:1px solid currentColor;
  color:#fff;
  mix-blend-mode:difference;
  opacity:.14;
  z-index:11;
  pointer-events:none;
  background-image:
    linear-gradient(currentColor 0 0),linear-gradient(currentColor 0 0),
    linear-gradient(currentColor 0 0),linear-gradient(currentColor 0 0),
    linear-gradient(currentColor 0 0),linear-gradient(currentColor 0 0),
    linear-gradient(currentColor 0 0),linear-gradient(currentColor 0 0);
  background-repeat:no-repeat;
  background-size:10px 1px,1px 10px,10px 1px,1px 10px,10px 1px,1px 10px,10px 1px,1px 10px;
  background-position:
    left 14px top 14px,left 14px top 14px,
    right 14px top 14px,right 14px top 14px,
    left 14px bottom 14px,left 14px bottom 14px,
    right 14px bottom 14px,right 14px bottom 14px;
}

/* Focus. The site previously had essentially none. --dot is the same custom
   property .spec__row:hover drives, so focus and hover speak one language
   (ink spreading on the stock) instead of being two unrelated inventions. */
:focus-visible{
  outline:2px solid currentColor;
  outline-offset:3px;
  --dot:1.1px;
}
.skip{
  position:fixed;
  top:0;left:0;
  z-index:20;
  transform:translateY(-110%);
  background:var(--ink);
  color:var(--paper);
  padding:.75rem 1.25rem;
  font-family:var(--mono);
  font-size:.6875rem;
  letter-spacing:.16em;
  text-transform:uppercase;
  text-decoration:none;
}
.skip:focus{transform:none}

/* Ground sentinel. One at the top of every section that changes ground, read
   by the observer in site.js to flip --nav-ink. The negative margin cancels its
   own height so it never affects layout. */
.ground{display:block;height:1px;margin-bottom:-1px;pointer-events:none}

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

.nav{
  position:fixed;
  top:0;left:0;right:0;
  z-index:12;
  display:grid;
  grid-template-columns:auto 1fr auto auto;
  align-items:center;
  gap:clamp(1rem,3vw,2.5rem);
  height:var(--nav-h);
  padding:0 var(--gutter);
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-size:.625rem;
  letter-spacing:.16em;
  font-weight:500;
  text-transform:uppercase;
  color:var(--nav-ink);
  /* Structural element: it settles, it does not fade. */
  transition:color .28s steps(4);
}
.nav a{color:inherit;text-decoration:none}
.nav__mark{font-weight:500;white-space:nowrap}
.nav__list{display:flex;gap:clamp(.75rem,2.2vw,1.75rem);list-style:none;margin:0;padding:0}
/* 44px tap target (WCAG 2.2) without inflating the visual row height. */
.nav__list a,.nav__mark{display:inline-block;padding-block:1.15rem}

/* The only blended element in the header. A hairline that momentarily loses
   contrast over a mid-tone is a hairline, not information. */
.nav__rule{
  height:1px;
  background:currentColor;
  color:#fff;
  mix-blend-mode:difference;
  opacity:.14;
  transform:scaleX(0);
  transform-origin:left;
  /* 420ms matches .rule-draw's easing but is less than half its 900ms: that is
     an entrance, this is a response. Under ~300ms it reads as a pop. */
  transition:transform .42s cubic-bezier(.16,1,.3,1);
}
[data-scrolled] .nav__rule{transform:none}

.nav__clock{white-space:nowrap;opacity:.72}
@media(max-width:51.999rem){.nav__clock{display:none}}

/* Current page gets a printer's registration mark. Registration is how you
   know which plate you are looking at. */
.nav [aria-current="page"]{opacity:1}
.nav [aria-current="page"]::before{
  content:"";
  display:inline-block;
  width:7px;height:7px;
  margin-right:.5rem;
  vertical-align:middle;
  background:
    linear-gradient(currentColor 0 0) center/100% 1px no-repeat,
    linear-gradient(currentColor 0 0) center/1px 100% no-repeat;
}
/* Hover: colour is locked by the token, so the response is a rule growing from
   the left rather than a colour change. */
.nav__list a{position:relative}
.nav__list a::after{
  content:"";
  position:absolute;
  left:0;right:0;bottom:.9rem;
  height:1px;
  background:currentColor;
  transform:scaleX(0);
  transform-origin:left;
  transition:transform .18s cubic-bezier(.16,1,.3,1);
}
.nav__list a:hover::after,.nav__list a:focus-visible::after{transform:none}

/* Separation without backdrop-filter. A blur is the default answer, costs a
   full-width GPU blur every scroll frame, and fogs the one texture this brand
   owns. This is a SCREEN: a 6px dot field (DEFAULTS.cell) difference-blended,
   so it lightens dark ground and darkens light ground in a dot pattern with no
   branch and no JS. 1.6px dots on a 6px cell is 22.3% coverage. */
.nav__screen{
  position:fixed;
  inset:0 0 auto;
  height:112px;
  z-index:10;
  pointer-events:none;
  background-image:radial-gradient(circle,#fff 1.3px,transparent 1.4px);
  background-size:6px 6px;
  mix-blend-mode:difference;
  /* Absent at the top of the page and faded in once content starts passing
     under the header. At rest every page opens on a dark, quiet field where
     the nav is already legible, so a permanent band there is a stripe doing no
     work. Measured at .55 it read as a glitch across the masthead. */
  opacity:0;
  transition:opacity .42s ease;
  -webkit-mask-image:linear-gradient(#000 0%,#000 46%,transparent 100%);
          mask-image:linear-gradient(#000 0%,#000 46%,transparent 100%);
}
[data-scrolled] .nav__screen{opacity:.34}

/* Mobile: a native <details> disclosure, not a hamburger. Keyboard-operable
   for free, works with JS off, and there is no focus trap to get wrong. */
.nav__menu{display:none}
.nav__menu > summary{
  list-style:none;
  cursor:pointer;
  padding-block:1.15rem;
}
.nav__menu > summary::-webkit-details-marker{display:none}
.nav__menu[open] > summary::after{content:" ×"}
.nav__menu:not([open]) > summary::after{content:" +"}
/* The open panel is the ONE place on this site where a solid background is
   correct: a plate dropping over the sheet. This is why the blend lives on
   .nav__rule and not on .nav — a child cannot escape its parent's blend group. */
.nav__menu ul{
  position:absolute;
  left:0;right:0;
  top:var(--nav-h);
  margin:0;
  padding:.5rem var(--gutter) 1rem;
  list-style:none;
  background:var(--ink);
  color:var(--paper);
  border-top:1px solid var(--rule);
}
.nav__menu ul a{display:block;padding:.9rem 0}

@media(max-width:33.999rem){
  .nav__list{display:none}
  .nav__menu{display:block}
}

/* -- hero (home only) ----------------------------------------------------- */
.hero{
  position:relative;
  min-height:100svh;
  display:flex;
  align-items:flex-end;
  padding:var(--gutter);
  /* Clears the fixed nav in the landscape-phone case where 100svh is ~380px
     and the type reaches up into the header. */
  padding-top:clamp(5.5rem,10vh,7rem);
  padding-bottom:clamp(3rem,9vh,6rem);
  isolation:isolate;
}
.hero__img{
  position:absolute;
  inset:0;
  z-index:-2;
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:center 45%;
  transition:opacity .6s ease;
}
/* The canvas takes over only once the engine has decoded frames and painted.
   Until then, and forever if JS is off, the photograph is what shows. */
.dither-canvas{
  position:absolute;
  inset:0;
  z-index:-2;
  display:block;
  width:100%;
  height:100%;
}
[data-dither-active] .hero__img{opacity:0}
.hero__scrim{
  position:absolute;
  inset:0;
  z-index:-1;
  background:
    linear-gradient(180deg,rgba(11,18,25,.55) 0%,rgba(11,18,25,0) 32%),
    linear-gradient(0deg,rgba(11,18,25,.95) 0%,rgba(11,18,25,.62) 22%,rgba(11,18,25,.18) 46%,rgba(11,18,25,0) 68%);
}
/* Only enough to seat the type. Any more and the halftone stops reading as a
   halftone: the dots are what the treatment IS, so they have to survive. */
/* Tighter and stronger than it was. The snow source puts its brightest band
   exactly where the eyebrow sits, and 11px uppercase mono cannot survive on it
   with a text-shadow alone. Pulling the wash in (90% x 78% rather than
   120% x 90%) and holding .88 out to 34% spends the darkening on the corner
   the type actually occupies, so the halftone is still full strength across
   the other three quarters of the frame. */
[data-dither-active] .hero__scrim{
  background:
    radial-gradient(90% 82% at 0% 100%,rgba(11,18,25,.97) 0%,rgba(11,18,25,.9) 46%,rgba(11,18,25,.44) 68%,rgba(11,18,25,0) 88%),
    linear-gradient(0deg,rgba(11,18,25,.66) 0%,rgba(11,18,25,.14) 24%,rgba(11,18,25,0) 42%);
}
/* A shadow on the type itself rather than a stronger scrim, which would
   flatten the halftone everywhere to fix one band. Unconditional, because the
   register keyframe has to END on this exact shadow. */
.eyebrow,h1,.hero__sub{
  text-shadow:0 1px 12px rgba(11,18,25,.9),0 0 3px rgba(11,18,25,.7);
}
/* The eyebrow needs a denser, tighter halo than the serif does: it is 11px on
   a field of 6px dots, so a soft 12px blur spreads past the glyph and does
   nothing. Two tight stops carry the counters. */
.hero .eyebrow{
  text-shadow:0 0 4px rgba(11,18,25,1),0 0 9px rgba(11,18,25,.98),0 1px 2px rgba(11,18,25,1);
}

/* Spot ink landing out of register and snapping in. Once per page. */
@keyframes reg{
  from{text-shadow:5px -4px 0 var(--steel)}
  to{text-shadow:0 1px 12px rgba(11,18,25,.9),0 0 3px rgba(11,18,25,.7)}
}

/* The title materialises dot by dot on the engine's own 6px cell: .hero.closing
   run in reverse, on type. 8 steps is the Bayer matrix's own step count. The
   text stays real, selectable and screen-readable throughout, because a mask
   does not touch the accessibility tree.

   The end value is 110%, not 75%. A colour-stop percentage inside a radial
   gradient is measured along the gradient's RAY, and the default ray for
   `circle` is farthest-corner, so 100% already reaches the tile corner. 75%
   stops short of it and leaves a permanent hole at every cell corner, which
   renders as type that never finishes arriving. */
@property --r{syntax:'<percentage>';inherits:false;initial-value:110%}
@keyframes ink{from{--r:0%}to{--r:110%}}
.js .register{
  -webkit-mask-image:radial-gradient(circle,#000 var(--r),transparent calc(var(--r) + 1%));
          mask-image:radial-gradient(circle,#000 var(--r),transparent calc(var(--r) + 1%));
  -webkit-mask-size:6px 6px;mask-size:6px 6px;
  -webkit-mask-repeat:repeat;mask-repeat:repeat;
  animation:ink .64s steps(8,end) both;
}
.js .hero h1.register{
  animation:ink .64s steps(8,end) both,
            rise .95s cubic-bezier(.16,1,.3,1) .10s both,
            reg .9s cubic-bezier(.16,1,.3,1) .22s both;
}

/* The hero screens away dot by dot on its own grid. The .closing gate means no
   mask and no compositing layer exists until the visitor actually scrolls. */
.hero.closing{
  -webkit-mask-image:radial-gradient(circle,#000 var(--r),transparent calc(var(--r) + 1%));
          mask-image:radial-gradient(circle,#000 var(--r),transparent calc(var(--r) + 1%));
  -webkit-mask-size:6px 6px;mask-size:6px 6px;
  -webkit-mask-repeat:repeat;mask-repeat:repeat;
}
.hero__inner{width:100%;max-width:72rem}

/* Full paper, not .62. The snow source puts its brightest band at exactly this
   line's height, and 11px uppercase mono at 62% opacity measured p95 132 there:
   dimmer than the halftone it was sitting on. On the interior mastheads, where
   the ground is a quiet dot field, .62 is restored below. */
.eyebrow{
  font-family:var(--mono);
  font-size:.6875rem;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--paper);
  margin:0 0 clamp(1.25rem,3vh,2rem);
}
h1{
  font-family:var(--display);
  font-weight:400;
  font-size:clamp(3rem,11.5vw,9.5rem);
  line-height:.92;
  letter-spacing:-.025em;
  margin:0;
}
.hero__sub{
  margin:clamp(1.5rem,4vh,2.5rem) 0 0;
  max-width:30rem;
  font-size:clamp(1.0625rem,1rem + .4vw,1.3125rem);
  line-height:1.5;
  color:rgba(244,242,237,.82);
}

.scrollcue{
  position:absolute;
  left:var(--gutter);
  bottom:1.5rem;
  width:1px;
  height:clamp(2rem,6vh,3.5rem);
  background:linear-gradient(180deg,rgba(244,242,237,.5),rgba(244,242,237,0));
  animation:drift 2.8s ease-in-out infinite;
}
@keyframes drift{0%,100%{transform:scaleY(.55);opacity:.45}50%{transform:scaleY(1);opacity:.95}}

/* -- masthead (interior pages) -------------------------------------------- */
/* 52svh, not 100. The hierarchy home > interior reads without a word of
   explanation, and the dither hero stays used exactly once. */
.masthead{
  position:relative;
  min-height:52svh;
  display:flex;
  align-items:flex-end;
  padding:clamp(5.5rem,10vh,7rem) var(--gutter) clamp(3rem,7vh,4.5rem);
  background:var(--ink);
  overflow:hidden;
  isolation:isolate;
  --screen:15deg;
  /* Read back by site.js to compute the printed beat period. Changing it here
     changes the number the masthead prints, which is the point. */
  --moire:3deg;
}
.masthead h1{font-size:clamp(2.5rem,8vw,6rem)}
/* No photograph. A dot field at the engine's own 6px cell, built from a single
   gradient: zero image bytes, zero JS, zero CPU after first paint.
   1.4px on a 6px cell is 17.1% coverage: dense enough to read as a screen,
   light enough that it never competes with type. inset:-40% so the rotation
   never exposes a corner. */
.masthead__field{
  position:absolute;
  inset:-40%;
  z-index:-1;
  transform:rotate(var(--screen));
  background-image:radial-gradient(circle,var(--steel) 1.4px,transparent 1.5px);
  background-size:6px 6px;
  opacity:.28;
  -webkit-mask-image:linear-gradient(160deg,#000 0%,rgba(0,0,0,.35) 42%,transparent 78%);
          mask-image:linear-gradient(160deg,#000 0%,rgba(0,0,0,.35) 42%,transparent 78%);
}
/* A SECOND identical field rotated 3 degrees produces real moire: physical
   interference computed by the browser's rasteriser, no image and no
   animation. Beat period is p / (2 sin(theta/2)) = 114.6px at 6px and 3deg,
   about three beats across the masthead. 1deg gives 344px (invisible here),
   5deg gives 69px (busy). The masthead prints the computed figure. */
.masthead__field::after{
  content:"";
  position:absolute;
  inset:0;
  transform:rotate(var(--moire));
  background-image:inherit;
  background-size:inherit;
  opacity:.7;
}
/* Interior ground is a quiet dot field, not a photograph, so the eyebrow goes
   back to the restrained weight it has everywhere else on the site. */
.masthead .eyebrow{color:rgba(244,242,237,.62);text-shadow:none}

/* -- sections ------------------------------------------------------------- */
section{padding:clamp(5rem,16vh,11rem) var(--gutter)}
.wrap{max-width:72rem;margin:0 auto}
.approach{background:var(--ink-2)}

/* Datasheet, not cards. Three floating cards is the most generic layout on the
   web; ruled rows let the titles and bodies share a true baseline across
   columns, which is the whole reason this reads as a spec. */
@property --dot{syntax:'<length>';inherits:false;initial-value:0px}

.spec{margin-top:clamp(3rem,7vh,4.5rem)}
.spec__row{
  border-top:1px solid var(--rule);
  padding:1.75rem 0;
  display:grid;
  gap:.5rem 2rem;
  /* Dot gain: ink spreads on absorbent stock, so the print comes out denser
     than the plate. A real press phenomenon rather than a hover effect. */
  background-image:radial-gradient(circle,rgba(244,242,237,.10) var(--dot),transparent calc(var(--dot) + .6px));
  background-size:6px 6px;
  transition:border-top-color .25s ease,--dot .42s ease;
}
@media(min-width:52rem){
  .spec__row{grid-template-columns:4rem 14rem 1fr;align-items:baseline}
  .spec__row.has-proof{grid-template-columns:4rem 14rem 1fr auto}
}
.spec__row:hover{border-top-color:var(--steel);--dot:1.1px}
.spec__no{
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-size:.5938rem;
  letter-spacing:.14em;
  color:var(--steel);
}
.spec__row h3{
  font-family:var(--display);
  font-weight:400;
  font-size:1.375rem;
  letter-spacing:-.01em;
  margin:0;
}
.spec__row p{margin:0;color:rgba(244,242,237,.62);font-size:.9375rem;line-height:1.7}
.spec__row a{color:var(--steel);text-decoration:none;border-bottom:1px solid transparent}
.spec__row a:hover,.spec__row a:focus-visible{border-bottom-color:currentColor}

/* Proof sheet. Each plate is a deterministic function of the entry's name, run
   through the same Bayer matrix and circle loop as the hero. Not a photograph
   and not a chart: the readout under it says exactly what it is. */
.proof{display:block;width:220px;height:140px;opacity:.85}
.proof-cap{
  display:block;
  margin-top:.5rem;
  font-family:var(--mono);
  font-size:.5313rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:rgba(244,242,237,.62);
}
@media(max-width:51.999rem){.proof{width:100%;height:120px}}

/* -- paper ground --------------------------------------------------------- */
.contact,.paper{background:var(--paper);color:var(--ink)}
.contact .label,.paper .label{color:#4A6B7C}
.contact .lede{max-width:34rem}
.paper .lede + p,.paper .spec__row p{color:rgba(11,18,25,.72)}
.mail{
  display:inline-block;
  margin-top:2.5rem;
  font-family:var(--display);
  font-size:clamp(1.25rem,1rem + 1.1vw,1.875rem);
  color:var(--ink);
  text-decoration:none;
  border-bottom:1px solid var(--rule-dark);
  padding-bottom:.25rem;
  transition:border-color .35s ease;
  /* A paper highlight under the ink reads as debossed. The padding
     compensation stops the 1px to 2px border shifting the baseline. */
  text-shadow:0 1px 0 rgba(244,242,237,.9);
}
.mail:hover,.mail:focus-visible{
  border-bottom-color:var(--ink);
  border-bottom-width:2px;
  padding-bottom:calc(.25rem - 1px);
}
.place{
  margin-top:1.25rem;
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-size:.625rem;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:rgba(11,18,25,.62);
  line-height:2;
}

/* -- footer --------------------------------------------------------------- */
footer{
  background:var(--paper);
  color:rgba(11,18,25,.62);
  padding:0 var(--gutter) clamp(2.5rem,6vh,4rem);
  font-size:.8125rem;
}
/* The plate. The instrument measuring itself: unfakeable because the numbers
   differ on every device, and it lets the page state its weight without
   writing a sentence claiming performance. */
.plate{
  margin-top:clamp(3rem,8vh,5rem);
  border-top:1px solid var(--rule-dark);
  padding-top:1.75rem;
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-size:.6875rem;
  letter-spacing:.12em;
  text-transform:uppercase;
  display:grid;
  gap:.6rem 1.5rem;
}
@media(min-width:52rem){.plate{grid-template-columns:6rem 1fr}}
/* Alphas are floored at .59, where ink composited over paper crosses 4.5:1.
   The plate is 11px uppercase mono, so it is normal text by WCAG and gets no
   large-text discount. .42 read as a nice quiet grey and measured 2.73:1. */
.plate dt{color:rgba(11,18,25,.62)}
.plate dd{margin:0;color:rgba(11,18,25,.82)}
@media(max-width:51.999rem){
  .plate dt{margin-top:.5rem}
  .plate .m-hide{display:none}
}

/* Step wedge. The engine's own tone steps, drawn as Bayer patches from
   Dither.DEFAULTS, so the footer carries a densitometer rather than a claim. */
.wedge{display:block;width:120px;height:40px;margin-top:1rem}
.wedge-cap{
  font-family:var(--mono);
  font-size:.5313rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:rgba(11,18,25,.62);
}

/* Verified accounts. Set at the plate's exact type spec so it reads as a
   second instrument panel rather than a social bar. No icons, no brand
   colours. The closing line is why this is a registry and not vanity. */
.accounts{
  margin-top:clamp(2.5rem,6vh,3.5rem);
  border-top:1px solid var(--rule-dark);
  padding-top:1.75rem;
  font-family:var(--mono);
  font-variant-numeric:tabular-nums;
  font-size:.6875rem;
  letter-spacing:.12em;
  text-transform:uppercase;
}
.accounts__grid{display:grid;gap:1.75rem 3rem}
@media(min-width:52rem){.accounts__grid{grid-template-columns:1fr 1fr}}
.accounts h2{
  font:inherit;
  color:rgba(11,18,25,.62);
  margin:0 0 1.25rem;
  font-weight:500;
}
.accounts h3{
  font:inherit;
  color:#4A6B7C;
  margin:0 0 .75rem;
  font-weight:500;
}
.accounts dl{display:grid;grid-template-columns:5.5rem 1fr;gap:.5rem 1rem;margin:0}
.accounts dt{color:rgba(11,18,25,.62)}
.accounts dd{margin:0;color:rgba(11,18,25,.82)}
.accounts a{color:rgba(11,18,25,.82);text-decoration:none;border-bottom:1px solid var(--rule-dark)}
.accounts a:hover,.accounts a:focus-visible{border-bottom-color:var(--ink)}
/* The npub is selectable text, never a link and never truncated. A truncated
   key in an anti-impersonation registry cannot be verified against, and a
   nostr: URI is dead in most desktop browsers. */
.npub{
  text-transform:none;
  letter-spacing:.02em;
  -webkit-user-select:all;user-select:all;
  overflow-wrap:anywhere;
  color:rgba(11,18,25,.82);
}
.accounts__note{
  margin:1.75rem 0 0;
  color:rgba(11,18,25,.62);
  text-transform:none;
  letter-spacing:.04em;
  font-size:.6875rem;
}

.foot-row{
  border-top:1px solid var(--rule-dark);
  margin-top:1.75rem;
  padding-top:1.75rem;
  display:flex;
  flex-wrap:wrap;
  gap:.5rem 2rem;
  justify-content:space-between;
}
.foot-nav{display:flex;flex-wrap:wrap;gap:.5rem 1.5rem;list-style:none;margin:0;padding:0}
.foot-nav a{color:rgba(11,18,25,.82);text-decoration:none;border-bottom:1px solid transparent}
.foot-nav a:hover,.foot-nav a:focus-visible{border-bottom-color:var(--ink)}
.foot-nav [aria-current="page"]{color:rgba(11,18,25,.62)}

/* -- entrance motion ------------------------------------------------------
   Progressive enhancement, deliberately. The hidden state is scoped to `.js`,
   set by an inline script in <head>. If that script never runs (JS disabled,
   blocked, still parsing) every element renders at full opacity rather than
   the page appearing blank. A hero that needs JavaScript in order to be
   VISIBLE is a hero that is sometimes missing. */

/* Above the fold: pure CSS, fires on load. Nothing to wait for. */
.js .rise{animation:rise .95s cubic-bezier(.16,1,.3,1) both}
.js .rise[data-d="1"]{animation-delay:.10s}
.js .rise[data-d="2"]{animation-delay:.20s}
@keyframes rise{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}

/* Below the fold: observed, so it animates when it comes into view. */
.js .reveal{
  opacity:0;
  transform:translateY(18px);
  transition:opacity .95s cubic-bezier(.16,1,.3,1),transform .95s cubic-bezier(.16,1,.3,1);
}
.js .reveal.in{opacity:1;transform:none}
.js .reveal[data-d="1"]{transition-delay:.09s}
.js .reveal[data-d="2"]{transition-delay:.18s}
.js .reveal[data-d="3"]{transition-delay:.27s}

/* Motion split. Serif keeps the eased fade; structural elements settle
   mechanically. A stepped opacity on a coordinate label and an eased fade on a
   serif sentence, on the same page, states the whole idea without words. */
.js .reveal-m{opacity:0;transition:opacity .5s steps(4)}
.js .reveal-m.in{opacity:1}
.js .rule-draw{transform:scaleX(0);transform-origin:left;transition:transform .9s cubic-bezier(.16,1,.3,1)}
.js .rule-draw.in{transform:none}

/* -- page transitions -----------------------------------------------------
   Cross-document, so it works on a static multi-page site with no router.
   <main> is named rather than root: a named descendant is lifted out of its
   ancestor's snapshot, so the SHEET screens away and back while the nav, the
   plate frame and the footer hold still. That is a plate change on a press.
   It also keeps the difference-blended chrome out of the transition, where it
   would blend against a snapshot instead of the live page.
   Firefox lacks cross-document support and falls back to an instant
   navigation, which is the whole reason this beats a router. */
@media(prefers-reduced-motion:no-preference){
  @view-transition{navigation:auto}
}
main{view-transition-name:sheet}
::view-transition-old(root),
::view-transition-new(root){animation:none;mix-blend-mode:normal}
@keyframes plate-off{from{--r:110%}to{--r:0%}}
@keyframes plate-on{from{--r:0%}to{--r:110%}}
::view-transition-old(sheet),
::view-transition-new(sheet){
  animation-duration:.42s;              /* 42ms per step, ~24fps film cadence */
  animation-timing-function:steps(8,end);
  /* The UA default is plus-lighter, which blows out the overlap under a mask. */
  mix-blend-mode:normal;
  -webkit-mask-image:radial-gradient(circle,#000 var(--r),transparent calc(var(--r) + 1%));
          mask-image:radial-gradient(circle,#000 var(--r),transparent calc(var(--r) + 1%));
  -webkit-mask-size:6px 6px;mask-size:6px 6px;
  -webkit-mask-repeat:repeat;mask-repeat:repeat;
}
/* Both names must be set explicitly or the UA cross-fade stacks under the mask. */
::view-transition-old(sheet){animation-name:plate-off}
::view-transition-new(sheet){animation-name:plate-on;animation-delay:.06s}

@media(prefers-reduced-motion:reduce){
  .js .rise{animation:none}
  .js .register,.js .hero h1.register{animation:none;-webkit-mask-image:none;mask-image:none}
  .js .reveal{opacity:1;transform:none;transition:none}
  /* Readouts keep updating. Position and measurement are information and must
     not be suppressed, only their easing. */
  .js .reveal-m{opacity:1;transition:none}
  .js .rule-draw{transform:none;transition:none}
  .scrollcue{animation:none}
  .nav__rule{transform:none;transition:none}
  .nav__list a::after{transition:none}
}
