/* ============================================================================
   samantha.design — AIR — styles

   Same DOM budget as the antechamber: the canvas is the page. A wordmark, one
   invitation line, a dev readout. Everything rides the --sun-* custom
   properties js/sun.js writes from the local clock, so the paper the ink sits
   in and the page around it are lit by the same sun.

   NOTE ON MOTION: the only CSS transitions here are the day/night collapse
   pair and the invitation's wipe pair — both are responses to a real event
   (the clock moving, the mic being granted). There is no ambient/looping
   animation anywhere in this file, on purpose: the page's claim is that
   nothing moves without sound, and a keyframe loop in the CSS would break
   that claim just as surely as a clock in the shader would.
   ============================================================================ */

*{ margin:0; padding:0; box-sizing:border-box; }

/* 100dvh, not 100vh. On iPhone `vh` is locked to the LARGE viewport — the one
   with Safari's toolbar collapsed — so at rest the page is taller than the
   visible area and the bottom of the canvas sits behind the toolbar. `dvh`
   tracks the viewport as the toolbar collapses and expands, so the piece is
   always exactly the size of what you can see. `vh` first as the fallback for
   anything that doesn't know `dvh`. */
html,body{ height:100vh; height:100dvh; }
body{ overscroll-behavior:none; }

body{
  font-family:var(--f-primary);
  background:var(--sun-bg);
  color:var(--sun-ink);
  overflow:hidden;
  transition:background-color var(--collapse-dur) var(--collapse-ease),
             color var(--collapse-dur) var(--collapse-ease);
  -webkit-font-smoothing:antialiased;
}

#air{
  position:fixed; inset:0;
  width:100%; height:100%;
  display:block;
  cursor:default;              /* nothing here is clickable — the room is the input */
  /* the canvas is not a scroll surface; without this a drag on iOS rubber-bands
     the whole page instead of doing nothing */
  touch-action:none;
}

/* ---- centre title — sharp over soft plate ---- */

/* ---- wordmark — same placement grammar as V3 and the antechamber ---- */
.nav{
  position:fixed;
  /* 12/20, not the old 16/16: MEASURED to land the wordmark on exactly the
     same pixel as the work page's (V3 rest: x:20, y:12) — the name must not
     jump when the dive hands one page to the other (2026-08-10). */
  /* VERTICALLY CENTRED (2026-09-02, per direct request) — kept at the left edge,
     lifted from the top corner to the screen's midline so the name, the prompt and
     Enter read as one horizontal band across the middle. */
  top:50%; transform:translateY(-50%);
  left:calc(20px + env(safe-area-inset-left));
  z-index:3; pointer-events:none;
}
.nav__word{
	margin:0;
  /* EXACT match to the work page's wordmark (2026-08-10, "I like how it is
     being set at Work"): same family (Junicode, the artist voice), same 500
     weight — it was inheriting the body sans here, so the same name rendered
     in two different faces across the dive. */
  font-family:var(--f-secondary); font-weight:500;
  font-size:var(--t-nav-word);
  line-height:var(--lh-nav-word);
  letter-spacing:var(--ls-nav-word);
  color:var(--sun-ink);
  transition:color var(--collapse-dur) var(--collapse-ease);
}

/* ---- the one line — Junicode, artist voice, bottom centre ---- */
.cues{
  /* Full-width and centred by text-align, NOT left:50% + translateX(-50%).
     The transform version sizes to its content, so "listening to the quiet of
     your room" runs off both edges of a phone before it can wrap. */
  /* CENTRED on screen now (2026-09-02, per direct request), not pinned to the
     bottom. top:50% + a translateY is safe where a translateX was not — the
     warning at the width note is about horizontal sizing; left:0/right:0 still
     gives full width to wrap in, and only the vertical axis is transformed. */
  position:fixed; left:0; right:0; top:50%;
  transform:translateY(-50%);
  z-index:3;
  pointer-events:none;
  padding:0 var(--sp-24);
  text-align:center;
}
.line{
  font-family:var(--f-secondary);
  font-size:var(--t-hint);
  line-height:var(--lh-hint);
  color:var(--sun-ink-muted);
  text-align:center;
  opacity:1;
  transition:opacity var(--wipe-dur) var(--wipe-ease),
             color var(--collapse-dur) var(--collapse-ease);
  user-select:none;
}
.line.is-visible{ opacity:1; }
/* Once the first breath is heard, keep "blow." legible for four full seconds,
   then let it leave as slowly as the instruction asks. The class is added only
   after the hold, so the ordinary prompt/calibration wipes keep their existing
   pace. Reduced-motion below still collapses this transition to 1ms. */
.line.is-blow-fading{
  opacity:0;
  transition-duration:4000ms,var(--collapse-dur);
}

/* ---- the M key hint appears only after M is pressed. It names what the key
   does NEXT ("M — mouse" while listening, "M — sound" while the pointer has
   the field); air.js rewrites the word on each swap. Hidden where there is no
   keyboard, so a phone never shows a key it cannot press. ---- */
.keyhint{
  margin:var(--sp-8) 0 0;
  font-family:var(--f-mono);
  font-size:calc(var(--t-hint) * .72);
  letter-spacing:.08em;
  color:var(--sun-ink-muted);
  opacity:.55;
  user-select:none;
}
.keyhint[hidden]{ display:none; }
@media (hover:none), (pointer:coarse){
  .keyhint{ display:none; }
}

/* The invitation is a real <button> so it is keyboard reachable — the only
   interactive element on the page. It removes itself once the mic is on;
   there is nothing else to ask for. */
.invite{
  position:relative;
  font:inherit; color:inherit;
  background:none; border:none;
  cursor:pointer;
  pointer-events:auto;
  padding:0 1px 1px;
  margin-left:.35em;
}
.invite::after{
  position:absolute;
  right:1px;
  bottom:1px;
  left:1px;
  height:1px;
  content:"";
  background:currentColor;
}
.invite:hover,
.invite:focus-visible,
.invite.is-link-scrambling{ color:var(--black); }
.invite:focus-visible{ outline:1px solid var(--sun-ink-muted); outline-offset:3px; }
@keyframes invite-underline-inks{
  0%{ background:var(--landing-ink-1); }
  16.666%{ background:var(--landing-ink-2); }
  33.333%{ background:var(--landing-ink-3); }
  50%{ background:var(--landing-ink-4); }
  66.666%{ background:var(--landing-ink-5); }
  83.333%{ background:var(--landing-ink-6); }
}
@media (hover:hover) and (pointer:fine){
  .invite:hover::after{
    animation:invite-underline-inks 2400ms steps(1,end) infinite;
  }
}
@media (prefers-reduced-motion:reduce){
  .invite:hover::after{
    animation:none;
    background:var(--landing-ink-1);
  }
}
/* the button's visibility is driven from JS now (it comes BACK for the deaf
   state), so the old blanket hide would fight it */

/* ---- WebGL2 / float-render-target fallback. This page cannot degrade to a
   cheaper visual the way the antechamber could — a fluid sim without float
   render targets is not a fluid sim — so it says so plainly rather than
   showing an empty room. ---- */
.nogl{
  position:fixed; inset:0; z-index:10;
  display:none; align-items:center; justify-content:center;
  padding:var(--sp-32);
  background:var(--sun-bg); color:var(--sun-ink-muted);
  font-family:var(--f-secondary); font-size:var(--t-hint); line-height:var(--lh-hint);
  text-align:center;
}
.nogl.is-on{ display:flex; }

@media (prefers-reduced-motion: reduce){
  body, .nav__word, .line{ transition-duration:1ms; }
}

/* ---- tap to resume — only ever visible after the GL context has been lost.
   Full-bleed and deliberately plain: at that moment the canvas is dead, so this
   is the entire page. ---- */
.resume{
  position:fixed; inset:0; z-index:8;
  display:none; align-items:center; justify-content:center;
  background:var(--sun-bg); color:var(--sun-ink-muted);
  font-family:var(--f-secondary); font-size:var(--t-hint); line-height:var(--lh-hint);
  cursor:pointer; user-select:none;
  padding:calc(var(--sp-32) + env(safe-area-inset-top)) var(--sp-24);
}
.resume.is-on{ display:flex; }

/* ---- preview controls (this build only) -------------------------------------
   Opposite the wordmark, same type voice as the nav, --sun-ink colours so
   they survive their own toggles. Whole-pixel offsets. */
.enter{
  text-decoration:none;
  /* THE MIRROR of the wordmark. Same face, weight and size as .nav__word, on the
     opposite top corner, on the SAME baseline (top:12/right:20 = nav's 12/20), so
     the name and the way out frame the field between them. */
  position:fixed; z-index:5;
  top:50%; transform:translateY(-50%);   /* the mirror stays a mirror — same midline as the name */
  right:calc(20px + env(safe-area-inset-right));
  appearance:none; background:none; border:0; padding:0;
  font-family:var(--f-secondary); font-weight:500;
  font-size:var(--t-nav-word); line-height:var(--lh-nav-word);
  letter-spacing:var(--ls-nav-word);
  color:var(--sun-ink);
  cursor:pointer; user-select:none;
  transition:opacity var(--collapse-dur) var(--collapse-ease);
}
/* THE HIT ZONE IS TWICE THE AREA (2026-09-30, per direct request: "increase the
   tap, hover and click zone of 'Enter' by 100%"). An invisible box around the
   label: -20.71% on every side is x1.414 in each dimension, so x2.00 in area
   (51.8x34 -> 73.3x48.1 on desktop), and it follows the label at any size.
   Part of the link, so tap, hover and click (and the scramble) all count; the
   glyphs do not move. */
.enter::before{
  content:"";
  position:absolute;
  inset:-20.71%;
}
.enter:hover{ opacity:1; }
.enter:focus-visible{ outline:1px solid var(--sun-ink-muted); outline-offset:4px; }

/* The landing is static and does not mount React's route feedback. Show the
   same small, immediate signal while the wind hands off to /works. */
.page-loader{position:fixed;inset:0 0 auto;z-index:10;height:5.3125px;overflow:hidden;opacity:0;pointer-events:none;transition:opacity 80ms linear;background:rgb(0 0 0 / 5%)}
.page-loader span{display:block;width:0;height:100%;background:#000}
.page-loader-vertical{position:fixed;top:0;bottom:0;left:0;z-index:10;width:5.3125px;overflow:hidden;opacity:0;pointer-events:none;transition:opacity 80ms linear;background:rgb(0 0 0 / 5%)}
.page-loader-vertical span{display:block;width:100%;height:0;background:#000}
html.is-navigating .page-loader{opacity:1}
html.is-navigating .page-loader span{animation:page-loading 800ms cubic-bezier(.22,.61,.36,1) forwards}
html.is-navigating .page-loader-vertical{opacity:1}
html.is-navigating .page-loader-vertical span{animation:page-loading-vertical 800ms cubic-bezier(.22,.61,.36,1) forwards}
@keyframes page-loading{to{width:100%}}
@keyframes page-loading-vertical{to{height:100%}}

/* Desktop landing type adjustment; mobile keeps its authored sizes. */
@media (min-width:601px){
  .nav__word,
  .enter{
    font-size:calc(var(--t-nav-word) + 2px);
  }
  .line{
    font-size:calc(var(--t-hint) + 2px);
  }
}

/* FIGMA 3928:12762 — iPhone 13 / Air landing.
   The source frame is 390×844. Vertical positions are expressed against dvh
   so its 199 / 442 / 668px rhythm survives shorter and taller phone screens;
   at the authored viewport they resolve to those exact coordinates. Desktop
   remains the approved single centred band above this breakpoint. */
@media (max-width:600px){
  .nav{
    top:23.5782dvh; /* 199 / 844 */
    left:calc(20px + env(safe-area-inset-left));
    transform:none;
  }
  .cues{
    top:52.3697dvh; /* 442 / 844 */
    transform:none;
    pointer-events:auto;
    line-height:0; /* removes the inline-block baseline offset: .line starts at y442 */
  }
  .line{
    position:relative;
    display:inline-block;
    font-family:var(--f-primary);
    /* 13px -> 15px (2026-09-30, per direct request: the phone's line 2px
       bigger, as the desktop one was). */
    font-size:calc(13px + 2px);
    line-height:19px;
    color:var(--sun-ink-muted);
  }
  .line:not(.is-visible){ pointer-events:none; }
  /* Keep the permission action visible on touch devices. iOS requires a direct
     tap before it can offer microphone access, so hiding "begin" leaves the
     essential interaction undiscoverable. */
  .invite{
    position:relative;
    inset:auto;
    display:inline-block !important;
    width:auto;
    height:auto;
    margin-left:.35em;
    padding:0 1px 1px;
    border:none;
    opacity:1;
  }
  .enter{
    top:79.1469dvh; /* 668 / 844 */
    right:calc(20px + env(safe-area-inset-right));
    transform:none;
  }
}

/* ---- INVERT (2026-09-22) -------------------------------------------------
   "invert only affect the site, no need affect panel" + "hue-preserving".
   air.js flips <html data-invert>; this filters the PAGE's own elements —
   the canvas, the wordmark, the enter link, the copy line, the loaders — one
   by one, never <html> or <body>, because a filter on an ancestor would take
   the slot toasts with it. `invert` alone turns neon green
   magenta; the half-turn of hue puts every colour back where it was, so only
   the lightness flips. Each element is inverted on its own, so dark type sits
   on dark paper as light type — nothing is left unreadable. */
html[data-invert] body > :not([data-air-ui]):not(script):not(style) {
  filter: invert(1) hue-rotate(180deg);
}
