/* ==========================================================================
   ui.css — page chrome shared by both pages
   Left copy columns · bottom bar · top-right preview panel · film grain
   Ported from the Figma frames (1920x1080 reference).
   ========================================================================== */

:root{
  --ui-ink:#393939;
  --ui-dim:rgba(57,57,57,.5);
  --ui-red:#e8271c;
  --ui-grey:#c8c8c8;
  --ui-bar:#393939;
  --ui-10:10px;
  --ui-14:14px;
  --ui-track:-0.04em;
}

/* ---------------- left copy columns ---------------------------------------- */
/* Figma: text-[10px] leading-[0.5] tracking-[-0.4px], columns at left:200
   w:91, gap:6 between the mark and the text group and between lines.
   line-height 0.5 is load-bearing — at 1 each block grows ~40px taller and
   the 64px gap between blocks collapses to ~13px, which is what made them
   read as crowded. */
.cols{ position:fixed; inset:0; z-index:40; pointer-events:none;
       font-size:var(--ui-10); letter-spacing:-0.4px; text-transform:uppercase;
       color:var(--ui-ink); line-height:.5; }
.col{ position:absolute; left:200px; width:91px;
      display:flex; flex-direction:column; gap:6px; }
.col__mark{ width:6px; height:6.667px; opacity:.5; }   /* Figma: 50% */
.col p{ margin:0; }
.col__label{ position:absolute; left:8px; color:var(--ui-dim); }

/* ---------------- bottom bar ----------------------------------------------- */
.bar{
  position:fixed; left:0; right:0; bottom:0; height:28px; z-index:60;
  background:var(--ui-bar);
  display:flex; align-items:center; justify-content:space-between;
}
.bar__left{ display:flex; align-items:center; gap:16px; padding-left:10px; }
.bar__btn{
  background:none; border:0; padding:0; cursor:pointer;
  font-family:inherit; font-size:var(--ui-14); letter-spacing:var(--ui-track);
  text-transform:uppercase; color:var(--ui-grey); line-height:1;
  display:flex; align-items:center; gap:4px; text-decoration:none;
  transition:color .18s;
}
.bar__btn:hover{ color:#fff; }
.bar__btn.is-current{ color:#fff; }
.bar__mark{ width:6px; height:6.667px; }

/* the email button — white block, copies on click */
.bar__mail{
  background:#fff; color:var(--ui-ink);
  height:20px; margin-right:4px; padding:0 6px;
  display:flex; align-items:center; gap:10px;
  border:0; cursor:pointer; font-family:inherit; font-size:var(--ui-14);
  letter-spacing:var(--ui-track); text-transform:uppercase; line-height:1;
  transition:background .18s, color .18s;
}
.bar__mail:hover{ background:var(--ui-grey); }
.bar__mail.is-copied{ background:var(--ui-red); color:#fff; }

/* ---------------- hover preview (wheel page only) --------------------------- */
.peek{ position:fixed; inset:0; z-index:50; pointer-events:none;
       font-size:var(--ui-10); letter-spacing:-0.4px; text-transform:uppercase;
       color:var(--ui-ink); line-height:1; }

.peek__frame{
  position:absolute; right:10px; top:10px;
  /* Always on. It used to be gated behind .peek.is-showing and only appeared
     while a card was hovered; the brief is now that there is ALWAYS an image
     up there, changing with hover and with the wheel's own rotation. */
  opacity:1;
  /* The frame is now exactly the image — no white mat behind it. Sizing it
     to the image (rather than centring a smaller image inside a larger white
     box) also keeps it anchored to the top-right corner instead of drifting
     inward as the image shrinks. */
  /* Figma node 14349:12076 — w:343 h:215, right:10 top:10, no fill. */
  width:var(--peek-w, 210px);
  height:var(--peek-h, 132px);
  background:transparent; overflow:hidden;
}
/* Each incoming image is its own stacked layer, and the newest resolves out
   of a blur over the one beneath — the reference (nicolaromei.com/the-archive)
   swaps with a fast directional blur, not a wipe or a cut. The layer beneath
   is never faded out; it is simply covered and then removed, which is what
   keeps the swap readable at speed instead of flashing the empty panel. */
.peek__layer{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
}
.peek__layer img{
  width:100%; height:100%; object-fit:cover; display:block;
}
.peek__layer.is-entering{
  animation:peekIn var(--peek-ms, 200ms) cubic-bezier(.22,.61,.36,1) both;
}
@keyframes peekIn{
  from{ opacity:0; filter:blur(16px) saturate(1.4); transform:scale(1.08); }
  60% { opacity:1; }
  to  { opacity:1; filter:blur(0) saturate(1);   transform:scale(1); }
}

@media (prefers-reduced-motion: reduce){
  .peek__layer.is-entering{ animation-duration:1ms; }
}

/* awards block, to the left of / under the frame */
/* Figma positions, expressed against the edges they actually belong to.
   The awards block is explicitly centred in the frame (top:50%), so the
   label and the platform block are placed RELATIVE TO THAT CENTRE rather
   than as absolute tops — otherwise they drift apart on any viewport that
   isn't exactly 1080 tall. */
/* "awards" starts exactly where the preview image starts: the frame is
   right:10 + 210 wide, so its left edge is 220px in from the right. */
.peek__awardsLabel{ position:absolute;
                    left:calc(100% - 220px); right:auto;
                    /* Aligned with the "awwwwards" row, not with the block
                       that contains it: .peek__awards starts with the 6.667px
                       plus-mark and a 6px gap, so its first ROW sits 9px above
                       where the label was. Measured, not derived. */
                    top:calc(50% - 21px);
                    color:var(--ui-ink); }
.peek__awards{ position:absolute; right:10px;              /* 1753+157=1910 */
               top:50%; transform:translateY(-50%);
               width:125px; display:flex; flex-direction:column; gap:6px; }
.peek__row{ display:flex; align-items:center; justify-content:space-between; }
.peek__row span:last-child{ color:var(--ui-dim); text-align:right; width:35px; }
/* left edge aligns with the awards block (both left:1753 in Figma) */
.peek__meta{ position:absolute; right:10px; width:125px;
             top:calc(50% + 88px);                         /* 628 vs 540 */
             display:flex; flex-direction:column; gap:6px; }
.peek__num{ position:absolute; right:10px; width:125px;
            top:calc(100% - 105px); }                      /* 975 of 1080 */
.peek .col__mark{ opacity:.5; }

/* ---------------- body text reveal -----------------------------------------
   Every line resolves from blurred + transparent to sharp, staggered down
   the page. Titles are deliberately excluded — they carry their own gradient
   treatment and reading them blurred looks like a rendering fault. */
.anim-line{
  opacity:0;
  filter:blur(7px);
  animation:lineIn .52s cubic-bezier(.22,.61,.36,1) both;
  animation-delay:calc(var(--i,0) * 42ms + var(--group-delay, 0ms));
}
@keyframes lineIn{
  from{ opacity:0; filter:blur(7px); }
  60% { opacity:var(--in-opacity, 1); }
  to  { opacity:var(--in-opacity, 1); filter:blur(0); }
}

/* WHY THE HEADLINE CARRIES THIS CLASS ITSELF, and not its two line spans.
   .stage__title / .hero__title paint their glyphs with `background-clip: text`
   — the gradient is the h1's own background, clipped to the shape of its text,
   and the spans' `color` is transparent. Animating opacity or filter on a SPAN
   therefore animates nothing anybody can see: the visible glyphs come from the
   parent's background and are never touched. Measured with the class on the
   spans, the headline sat at a darkest value of 226 on every frame of the
   0.52s animation — the animation ran, and was completely inert.

   Same family as the two other traps this file records: a child of a
   background-clip:text element cannot be given its own opacity, filter or
   position and still be painted by the parent's clip. */
@media (prefers-reduced-motion: reduce){
  .anim-line{ animation-duration:1ms; animation-delay:0ms; }
}


/* ---------------- film grain -----------------------------------------------
   Technique supplied by the client. One fixed element, a tiled noise
   texture, and a stepped background-position animation — no canvas and no
   per-frame JavaScript at all, which is why it costs essentially nothing.

   The texture is served LOCALLY. The original reference pointed at
   https://osmo.b-cdn.net/resource-media/noise.avif — swapping that URL back
   in is a one-line change, but a third-party CDN is a dependency an awards
   submission does not want, and the tile is generic monochrome noise, so a
   local copy is equivalent. Drop any noise image in at the same path to
   replace it.

   Everything is expressed as a custom property so the tuner can move it live.
   background-size is in `em`: 5em = 80px at a 16px root. SMALLER makes the
   grain finer, larger makes it chunkier. */
.grain{
  position: fixed;
  inset: 0;
  z-index: 999;
  pointer-events: none;
  /* Declared, not just defaulted. These four used to live only as var()
     fallbacks, which meant the custom properties were never actually SET —
     getPropertyValue('--grain-size') returned "" and the tuner's readback fell
     through to its own hardcoded numbers. The panel then reported, and "Copy
     settings" exported, 7.8125 / 125% / 60% / 0.6s while the page was really
     rendering 5em / 300% / 80% / 0.85s. Declaring them here makes the readback
     tell the truth. Do not collapse these back into the var() fallbacks. */
  --grain-size: 5em;
  --grain-opacity: .13;
  --grain-blend: overlay;
  --grain-contrast: 300%;
  --grain-brightness: 80%;
  --grain-speed: .85s;

  background: url('../assets/grain/noise.png') 50% 50% /
              var(--grain-size, 5em) repeat;
  opacity: var(--grain-opacity, .13);
  filter: contrast(var(--grain-contrast, 300%))
          brightness(var(--grain-brightness, 80%));

  /* The blend and the four values above are the deviation from the snippet,
     and they are the client's own tuning — do not "restore" the original.

     The snippet was written for a dark site: `normal` blending at opacity
     0.5 and brightness 60% is a dark grey wash, and on this near-white page
     it flattened the whole design to flat grey (measured: everything
     obscured). It shipped on `multiply` instead, then was tuned by eye to
     `overlay` at contrast 300%.

     Overlay is worth understanding before touching it: it is a no-op against
     mid-grey, which is most of a raw noise tile. Two different settings get
     round that, and this file has shipped both. Pushing CONTRAST hard (300%)
     drives the tile toward black and white extremes, where overlay bites at
     both ends. Pushing BRIGHTNESS down (60%, as here) instead makes the whole
     tile dark, so overlay darkens more or less uniformly and contrast can sit
     low. Either works; what does not work is low contrast AND neutral
     brightness together, which is the combination that makes the grain
     vanish. Move one, check the other. */
  mix-blend-mode: var(--grain-blend, overlay);
}

/* Stepped, not eased — the texture must JUMP between positions. A smooth
   interpolation would slide the noise across the screen, which reads as a
   moving pattern rather than as grain resampling. */
.grain[data-grain-animate="true"]{
  animation: grain var(--grain-speed, .85s) steps(1, end) infinite;
}
@keyframes grain{
  0%   { background-position: 0em 0em; }
  20%  { background-position: -0.5em 0.3em; }
  40%  { background-position: 0.4em -0.2em; }
  60%  { background-position: -0.3em -0.4em; }
  80%  { background-position: 0.2em 0.5em; }
}
@media (prefers-reduced-motion: reduce){
  .grain[data-grain-animate="true"]{ animation: none; }
}

/* ---------------- the clock ------------------------------------------------
   Icon, offset, red mark, time — the order the design gives, reading left to
   right as: is it light there, how far ahead, and what o'clock exactly.

   `margin-left:auto` rather than a fixed position: the bar is a flex row with
   space-between, and pinning this by hand would put it somewhere different on
   every viewport. */
.bar__clock{
  margin-left:auto; margin-right:16px;
  display:flex; align-items:center; gap:6px;
  font-size:var(--ui-14); letter-spacing:var(--ui-track);
  text-transform:uppercase; line-height:1;
  color:var(--ui-grey);
  /* IT IS NOT A CONTROL. Nothing here is clickable, so it must not swallow a
     click meant for the bar or show a text caret. */
  pointer-events:none; user-select:none;
}
/* WHICH ICON, from one class on the parent. Not from `hidden` on each SVG:
   that property belongs to HTMLElement, an <svg> is an SVGElement, and setting
   it there writes a JavaScript expando that never becomes an attribute — so
   script reads back the state it wrote while CSS, which only sees attributes,
   matches nothing at all. See the note in js/clock.js. */
.bar__sun, .bar__moon{ width:9px; height:9px; display:block; }
.bar__clock .bar__moon,
.bar__clock.is-night .bar__sun{ display:none; }
.bar__clock.is-night .bar__moon{ display:block; }

/* THE GAP BEFORE THE MARK IS WIDER than the others. In the design the offset
   and the time are two separate readings with a pause between them, and the
   red square sits in that pause rather than being punctuation inside one
   phrase. */
.bar__clockMark{
  width:5px; height:5px; flex:none;
  margin-left:6px;
  background:var(--ui-red);
}
/* TABULAR FIGURES, or the line shuffles sideways every time a 1 becomes a 2 —
   and with seconds running that is a twitch every second rather than every
   minute. */
.bar__clockTime{ font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; }
