/* ═══════════════════════════════════════════════════════════════════════════
   drowningdot · shared foundation
   Tokens, reset, chrome and the row primitives. Every page links this.

   Two contrast rules are load-bearing here and were measured, not guessed:
   1. Never put a muted colour inside an element that already carries a resting
      opacity. graphite at .62 row opacity composites to 2.45:1. Use
      `color:inherit` and carry hierarchy by size instead.
   2. --redline is 3.76:1 on proof and can never hold text under 24px.
      --redline-text exists for that and measures 4.95:1.
   ═══════════════════════════════════════════════════════════════════════════ */

:root{
  --proof:#0D0D0C;         /* unexposed border of a contact sheet */
  --sheet:#EAE8E1;         /* proofing paper */
  --graphite:#8A877E;      /* annotation pencil. 5.4:1 on proof */
  --graphite-ink:#656258;  /* the same role on the light band. 5.0:1 on sheet */
  --redline:#C33F27;       /* graphics only: rules, strikes, the mark */
  --redline-text:#D9563C;  /* text under 24px */

  --rule:rgba(234,232,225,.16);
  --rule-ink:rgba(13,13,12,.15);

  /* The dot screen over the imagery. Fixed: it was driven from scroll speed and
     from each cell's distance to the viewport centre, and repainting a gradient
     tile per cell per frame is what made scrolling feel heavy. Only the MEDIA is
     ever screened, never the type, which is how halftone printing actually works
     and makes the effect safe by construction rather than by tuning an opacity. */
  --dot:3px;
  /* still driven from site.js by scroll speed — one fixed element, cheap */
  --grain-scale:128px;

  --chrome:13px;           /* all interface text. display is 8x this or more */
  --gap:26px;
  --band:clamp(72px,9vw,132px);
  --label-col:150px;
  --max:1480px;
  --ease:cubic-bezier(.2,.7,.2,1);
}

*{margin:0;padding:0;box-sizing:border-box}
/* Form controls do not inherit type, so every <button> here — the nine face
   rows, the twelve contact-sheet cells, the six panel triggers — was rendering
   in the UA default, which is Arial. 24 buttons, 39 visible elements. The worst
   of it sat in the specimen: the labels naming the nine voices were set in none
   of them. `font` and not `font-family`, because size and weight were defaulting
   too — .cell was coming out at the UA's 13.33px rather than the body's 15px. */
button,input,select,textarea{font:inherit;color:inherit}
html{background:var(--proof)}
/* Instrument Sans, not Archivo. Archivo is a free Google grotesque and reads as
   one next to Khteka on Monolog or PP Neue Montreal on the specimen site — a
   thesis problem on a page arguing that the typeface is the voice. Instrument
   Sans is also free, but it is the companion to Instrument Serif, which was
   already carrying the display, so the pairing is now one decision rather than
   two. Archivo stays loaded at 400: it is still the ninth specimen row, the
   neutral workhorse, which is a fair thing to show and no longer what we speak in. */
body{background:var(--proof);color:var(--sheet);
  font-family:'Instrument Sans',sans-serif;
  font-size:15px;line-height:1.45;-webkit-font-smoothing:antialiased;
  overflow-x:hidden}
img{max-width:100%;display:block}
::selection{background:var(--redline);color:var(--sheet)}
code{font-family:'DM Mono',ui-monospace,monospace;font-size:.94em}

.skip{position:absolute;left:-9999px;top:0;padding:13px 14px;background:var(--sheet);
  color:var(--proof);font-size:var(--chrome);z-index:100}
.skip:focus{left:8px;top:8px}
/* the landing's h1 is the real heading; the name itself is drawn on canvas, so
   the text stays here for screen readers and search rather than being lost */
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

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

/* ─── grain ───────────────────────────────────────────────────────────────
   Her Monolog autopsy names texture as one of that site's only two sparks:
   "animated grain overlay across entire page". Without it the page reads as
   flat colour fields. Oversized and inset so the jitter can never expose an
   edge, and stepped rather than smoothed so it flickers like film.
   The URL resolves against this stylesheet, so /work pages get it too.      */
.grain{position:fixed;inset:-100%;width:300%;height:300%;
  pointer-events:none;z-index:400;
  background-image:url('img/grain.png');background-repeat:repeat;
  /* size, not opacity: scaling symmetric noise coarsens it without shifting
     average luminance, so speed cannot cost contrast */
  background-size:var(--grain-scale) var(--grain-scale);
  opacity:.42;mix-blend-mode:overlay;
  animation:grain-shift 1.1s steps(1) infinite}
@keyframes grain-shift{
  0%  {transform:translate3d(0,0,0)}
  12% {transform:translate3d(-1.5%,-2%,0)}
  24% {transform:translate3d(-3%,1.5%,0)}
  36% {transform:translate3d(1.5%,-3%,0)}
  48% {transform:translate3d(-2%,2.5%,0)}
  60% {transform:translate3d(2.5%,1%,0)}
  72% {transform:translate3d(-1%,-1.5%,0)}
  84% {transform:translate3d(2%,-2.5%,0)}
  100%{transform:translate3d(0,0,0)}
}

/* ─── halftone ────────────────────────────────────────────────────────────
   A dot screen over the photography, the other half of Monolog's texture spark.
   Lives on a pseudo so each image's inline background-image stays untouched.

   LESSONS.md #3 is the spec: Monolog's dots are "fine, dense, and clearly
   visible on every dark section". Dark dots on multiply were invisible over dark
   photography, which is the exact washout that lesson warns about. So light
   dots, no blend mode, and verified by measuring pixel variance, not by eye.

   It CANNOT live inside .plate: a filter blurs its own pseudo-elements, so
   .plate's blur(1.5px) smeared a 3px pitch into a flat grey wash. Hence a
   separate .dots layer on the index, outside the blur. */
/* Static pitch, deliberately. This screen was driven from scroll twice over —
   globally by velocity, and per cell by distance from the viewport centre — and
   both are gone. Animating background-size on a gradient repaints the whole
   tile for every cell on every frame, and with nine cells carrying blurred
   plates underneath, that was the cost that made scrolling feel heavy. A fixed
   3px screen reads as the printed thing it is imitating; a moving one mostly
   read as jitter. */
.cell .dots,.take::before{content:"";position:absolute;inset:0;
  pointer-events:none;z-index:1;
  background-image:radial-gradient(rgba(234,232,225,.44) 30%,transparent 32%);
  background-size:var(--dot) var(--dot);opacity:.6}

/* ─── loader ──────────────────────────────────────────────────────────────
   Hidden by default and only shown once html.js is set, so a visitor with
   JavaScript off never gets a black overlay they cannot dismiss.          */
#loader{display:none}
html.js #loader{display:block;position:fixed;inset:0;z-index:200;
  background:var(--proof)}
#loader canvas{display:block;width:100%;height:100%}
#loader.done{opacity:0;pointer-events:none;transition:opacity .6s ease}
html.loading,html.loading body{overflow:hidden}

/* ─── invert reveal ───────────────────────────────────────────────────────
   A circle grown from the cursor, inside which the element flips to white
   ground with redline letters. Two identical faces stacked, the top one
   clipped. Redline on white measures 5.19:1, so even 13px labels pass.
   Driven from site.js for every [data-invert], so the nav link and the big
   contact row share one implementation.                                    */
[data-invert]{position:relative;overflow:hidden;isolation:isolate}
/* the solid core carries the text, so legibility never depends on the dots */
.inv-top{position:absolute;inset:0;background:#FFFFFF;color:var(--redline);
  clip-path:circle(var(--ri,0px) at var(--cx,50%) var(--cy,50%))}
/* the halftone fringe leads the core: a wider circle of redline dots that
   resolves into the solid panel behind it. No text lives here, so a dot screen
   costs nothing in contrast. This is the site's halftone language applied to
   every reveal edge rather than a hard circle. */
.inv-dots{position:absolute;inset:0;background:var(--redline);
  clip-path:circle(var(--ro,0px) at var(--cx,50%) var(--cy,50%));
  -webkit-mask-image:radial-gradient(#000 31%,transparent 33%);
  mask-image:radial-gradient(#000 31%,transparent 33%);
  -webkit-mask-size:4px 4px;mask-size:4px 4px}
/* with no pointer the reveal can never fire, so it is removed rather than left
   sitting there as a dead layer */
@media (hover:none){ .inv-top,.inv-dots{display:none} }

/* ─── halftone on hover ───────────────────────────────────────────────────
   The same dot language on the row links. z-index:-1 with isolation on the
   parent, so the screen sits behind the text and cannot touch its contrast. */
.big-row,.work-row{isolation:isolate}
.big-row::before,.work-row::before{content:"";position:absolute;inset:0;
  z-index:-1;pointer-events:none;opacity:0;
  transition:opacity .3s var(--ease);
  background-image:radial-gradient(var(--redline) 30%,transparent 32%);
  background-size:4px 4px}
.big-row,.work-row{position:relative}
.big-row:hover::before,.big-row:focus-visible::before,
.work-row:hover::before,.work-row:focus-visible::before{opacity:.16}

/* ─── chrome ─────────────────────────────────────────────────────────────── */
.nav{position:fixed;inset:0 0 auto;height:60px;z-index:50;
  display:flex;align-items:center;justify-content:space-between;
  padding:0 var(--gap);font-size:var(--chrome);transition:color .4s ease}
.nav a{color:currentColor;text-decoration:none}
/* Padding, not size: the hit area grows to 44px while the mark keeps its 19px
   optical height. The nav is 60px, so a 44px target sits inside it with room.
   Negative margin pulls the box back so the extra padding cannot shift the
   mark off the --gap alignment it shares with every section below. */
.nav .mark{font-weight:500;letter-spacing:-.01em;display:flex;align-items:center;
  gap:8px;padding:13px 10px;margin-left:-10px}
.nav .mark svg{width:15px;height:15px;display:block}
.nav ul{display:flex;gap:20px;list-style:none}
.nav ul a,.nav .back{opacity:.62;transition:opacity .22s ease}
.nav ul a:hover,.nav ul a:focus-visible,
.nav .back:hover,.nav .back:focus-visible{opacity:1}
/* the only nav item, and it carries the same invert as the big contact row */
.nav-contact{display:inline-block;text-decoration:none;color:currentColor;
  font-size:var(--chrome)}
/* 7px vertical gave a 33px target on the page's primary conversion action.
   13px takes it to 45px without moving the label, and the invert circle scales
   with the box so the reveal still fills it. */
.nc-face{display:block;padding:13px 12px;letter-spacing:normal;white-space:nowrap}
/* the nav inverts over the one light band rather than sitting on a scrim */
body.nav-ink .nav{color:var(--proof)}

/* ─── nav scrim ───────────────────────────────────────────────────────────
   The bar is fixed, 60px tall and fully transparent, so it floated over live
   type: "Contact" landed ten pixels off the specimen's first kind label, and
   the mark landed on the descenders of "Creating the message". A solid bar
   would cut a hard edge across sections that are deliberately full bleed, so
   this is a gradient that has faded out before it reaches content — a
   vignette, not a chrome bar.

   It stays off over the pleat. There is nothing to collide with up there, and
   a scrim would dull the top of the curtain, which is the one thing on this
   page that has to arrive at full strength. site.js turns it on past 60vh. */
.nav::before{content:"";position:absolute;inset:0 0 auto;height:128px;
  pointer-events:none;z-index:-1;opacity:0;transition:opacity .4s ease;
  background:linear-gradient(to bottom,
    rgba(13,13,12,.9) 0%,rgba(13,13,12,.6) 44%,rgba(13,13,12,0) 100%)}
body.nav-scrim .nav::before{opacity:1}
body.nav-ink .nav::before{background:linear-gradient(to bottom,
  rgba(234,232,225,.9) 0%,rgba(234,232,225,.6) 44%,rgba(234,232,225,0) 100%)}

/* ─── section header: margin label plus content ─────────────────────────── */
.head{display:grid;grid-template-columns:var(--label-col) minmax(0,1fr);gap:30px;
  margin-bottom:calc(var(--band) * .5)}
.head .label{font-size:var(--chrome);color:var(--graphite)}
.head h2{font-family:'Instrument Serif',serif;font-weight:400;
  font-size:clamp(28px,3.9vw,56px);line-height:1.02;letter-spacing:-.02em;max-width:24ch}
.head p{font-size:var(--chrome);color:var(--graphite);max-width:54ch;margin-top:14px}

/* ─── giant conversion rows ──────────────────────────────────────────────── */
.big-rows{border-top:1px solid var(--rule)}
.big-row{display:flex;align-items:baseline;justify-content:space-between;gap:20px;
  padding:26px 0;border-bottom:1px solid var(--rule);text-decoration:none;
  color:var(--sheet);font-family:'Instrument Serif',serif;
  font-size:clamp(28px,4.4vw,68px);line-height:1.04;letter-spacing:-.022em;
  opacity:.72;transition:opacity .26s var(--ease)}
.big-row:hover,.big-row:focus-visible{opacity:1;outline:none}
/* inherit, not graphite: graphite at the row's .72 opacity measured 3.33:1.
   letter-spacing MUST be reset. An em value computes against the declaring
   element's font-size and then inherits as an absolute length, so the row's
   -.022em at 63px becomes -1.39px on this 13px label and eats the spaces:
   "Seven sites" rendered as "Sevensites". */
.big-row .arrow{font-family:'Instrument Sans',sans-serif;font-size:var(--chrome);
  letter-spacing:normal;color:inherit;white-space:nowrap}

.foot{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;
  font-size:var(--chrome);color:var(--graphite);padding:22px 0 30px}

/* ─── site footer ─────────────────────────────────────────────────────────
   Monolog treats the footer as a full section: giant links, studio details, a
   live local clock. One line of text was leaving a whole beat on the table.  */
.site-foot{border-top:1px solid var(--rule);padding-top:var(--band)}
.foot-ledger{margin-top:calc(var(--band) * .6);
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  border-top:1px solid var(--rule)}
.foot-ledger div{padding:17px 0;border-bottom:1px solid var(--rule);
  display:flex;flex-direction:column;gap:5px;font-size:var(--chrome)}
.foot-ledger dt{color:var(--graphite)}
.foot-ledger dd{color:var(--sheet)}
.foot-ledger dd b{font-weight:500;font-variant-numeric:tabular-nums}
/* flex-end, not space-between: this row carries a single child now, and
   space-between would park it against the left margin. */
.foot-tail{display:flex;justify-content:flex-end;gap:20px;flex-wrap:wrap;
  font-size:var(--chrome);color:var(--graphite);padding:20px 0 36px}

@media (max-width:820px){
  .foot-ledger{grid-template-columns:repeat(2,minmax(0,1fr));column-gap:24px}
}
@media (max-width:460px){
  .foot-ledger{grid-template-columns:1fr}
}

/* ─── reveal, gated so the page is never blank without javascript ──────────
   Never put data-reveal on an element that owns a resting opacity: this rule
   outranks .work-row{opacity:.62} and would pin it open, silently killing the
   hover hierarchy. Reveal the container instead.                           */
html.js [data-reveal]{opacity:0;transform:translateY(14px)}
html.js [data-reveal].shown{opacity:1;transform:none;
  transition:opacity .65s ease,transform .65s var(--ease)}

/* ─── stagger ─────────────────────────────────────────────────────────────
   A block of nine specimen rows arriving as one rectangle is a fade, not
   choreography. These arrive in sequence instead, --i set per child in site.js.

   Two modes, and the split is the same warning as above applied one level
   down. .work-row rests at .62 and .face-row at .58, and that resting opacity
   IS their hover hierarchy — animating opacity on them would land the row at 1
   and silently flatten it. So the default mode moves and never touches opacity.
   Only groups whose children have no resting opacity get "fade".

   .steps is deliberately not staggered: .step is position:sticky, and any
   transform on it would make it the containing block for its own sticky
   positioning, which kills the stack. */
html.js [data-stagger] > *{transform:translateY(20px);
  transition:transform .72s var(--ease);
  transition-delay:calc(var(--i,0) * 55ms)}
html.js [data-stagger="fade"] > *{opacity:0;
  transition:opacity .72s ease,transform .72s var(--ease);
  transition-delay:calc(var(--i,0) * 55ms)}
html.js [data-stagger].shown > *{transform:none}
html.js [data-stagger="fade"].shown > *{opacity:1}

@media (max-width:900px){
  .head{grid-template-columns:1fr;gap:14px}
  .head .label{order:-1}
}
@media (prefers-reduced-motion:reduce){
  /* delay as well as duration. Zeroing duration alone leaves the stagger's
     transition-delay intact, so a reader who asked for no motion would still
     watch nine rows pop in one at a time over half a second — the sequence
     preserved and only the movement removed, which is the opposite of the ask. */
  *,*::before,*::after{transition-duration:.01ms !important;
    animation-duration:.01ms !important;
    transition-delay:0ms !important;animation-delay:0ms !important}
  /* the grain stays as static texture rather than becoming a 0.01ms strobe */
  .grain{animation:none !important;transform:none !important}
}
