/* ─────────────────────────────────────────────────────────
   Joe Parker-Rees — digital business card

   A card object sitting on a dark green field, tilting as one plane. Design
   tokens hand-ported from the newportfolio design system (src/app/globals.css)
   rather than pulled in as a dependency, so this stays a static single page
   that loads fast on conference wifi.

   Octave throughout — no Romie.
   ───────────────────────────────────────────────────────── */

@font-face {
  font-family: 'Octave';
  src: url('/card/fonts/Octave-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --field: #00220A;         /* the ground the card sits on */
  --card: #F3F8F0;          /* white-green card stock */
  --ink: #00220A;
  --ink-60: rgba(0, 34, 10, 0.6);
  --ink-40: rgba(0, 34, 10, 0.4);
  --ink-12: rgba(0, 34, 10, 0.12);
  --window-ink: rgba(0, 34, 10, 0.38);
  --badge-base: #001505;
  --radius-card: 18px;
  --radius-window: 10px;
  --radius-sm: 4px;
  --ease: cubic-bezier(0.215, 0.61, 0.355, 1);

  /* Where the light is, as a unit vector across the face of the card, and how
     deep the recess reads. Driven every frame from the tilt in card.js —
     declared here so the card still looks lit before the first frame lands,
     and on the desktop fallback where nothing ever drives them. */
  --light-x: -0.7;
  --light-y: -0.7;
  --glint-depth: 15px;
  --glint-opacity: 1;
  --shadow-x: 0px;
  --shadow-y: 18px;

  /* The field left around the card. This is clearance, not styling: a card
     rotating under a perspective projects *larger* than its layout box,
     because the edge swinging toward the eye is now nearer to it. The stage
     clips, so anything the card grows past this gap gets its corner sliced
     off mid-tilt. 14px was not enough: at full tilt the card overran an
     iPhone 15 by 1px and a 15 Pro Max by 3px. 26px is the smallest gap that
     clears an SE, a 15 and a Pro Max, measured rather than guessed. */
  --field-gap: 26px;
  --perspective: 1200px;
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--field);
  color: var(--ink);
  font-family: 'Octave', sans-serif;
  font-size: 14px;
  line-height: 1.04;
  font-weight: 400;
  letter-spacing: -0.05em;
  -webkit-font-smoothing: antialiased;
}

/* Nothing scrolls. Fixing the body rather than relying on overflow alone also
   stops iOS rubber-banding, which reads as broken while tilting the phone. */
body {
  position: fixed;
  inset: 0;
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: none;
}

/* The display tracking (-0.05em) bites at small sizes; loosen for UI text. */
.tracking-ui { letter-spacing: -0.02em; }

/* ── Field ───────────────────────────────────────────── */

.stage {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding:
    calc(env(safe-area-inset-top) + var(--field-gap))
    calc(env(safe-area-inset-right) + var(--field-gap))
    calc(env(safe-area-inset-bottom) + var(--field-gap))
    calc(env(safe-area-inset-left) + var(--field-gap));
  /* Perspective lives on the field so the card tilts inside it, and the
     card's own children can sit at different depths. */
  perspective: var(--perspective);
  perspective-origin: 50% 45%;
}

/* ── Card ────────────────────────────────────────────── */

.card {
  position: relative;
  width: 100%;
  max-width: 420px;
  height: 100%;
  max-height: 780px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 14px;
  background: var(--card);
  border-radius: var(--radius-card);
  overflow: hidden;
  /* Lifted off the field. The shadow is what makes it read as an object
     resting on something rather than a panel painted onto the page, and it
     swings opposite the tilt so the card keeps its distance from the ground
     while it turns. */
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.25),
    var(--shadow-x) var(--shadow-y) 50px rgba(0, 0, 0, 0.45);
  transform-style: preserve-3d;
  will-change: transform;
}

/* ── Holographic reflection layer ───────────────────────── */

.card__shine {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-card);
  pointer-events: none;
  z-index: 50;
  background:
    /* Specular light hotspot */
    radial-gradient(
      550px circle at var(--shine-x, 50%) var(--shine-y, 30%),
      rgba(255, 255, 255, 0.9) 0%,
      rgba(255, 255, 255, 0.35) 22%,
      rgba(255, 255, 255, 0) 60%
    ),
    /* Holographic rainbow sweep */
    linear-gradient(
      var(--shine-angle, 135deg),
      rgba(255, 255, 255, 0) 0%,
      rgba(240, 254, 9, 0.32) 20%,
      rgba(0, 255, 133, 0.38) 38%,
      rgba(0, 229, 255, 0.42) 50%,
      rgba(251, 11, 198, 0.35) 62%,
      rgba(240, 254, 9, 0.28) 78%,
      rgba(255, 255, 255, 0) 100%
    ),
    /* Fine diffraction grating lines */
    repeating-linear-gradient(
      calc(var(--shine-angle, 135deg) + 45deg),
      rgba(255, 255, 255, 0.15) 0px,
      rgba(255, 255, 255, 0.15) 1px,
      transparent 1px,
      transparent 4px
    );
  mix-blend-mode: color-dodge;
  opacity: var(--shine-opacity, 0.95);
  transition: opacity 0.3s var(--ease);
}

/* ── Window ──────────────────────────────────────────── */

/* The frame is the layout box; the window is the clipping box inside it, so
   the seal can overhang the corner without being cut off. */
.frame {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}

/* The hollow.

   A well pressed into the stock, deep enough to have walls. Nothing here is a
   picture of a recess — it's built the way a real one behaves under one light:
   the floor sits a shade darker than the surface because less of the room
   reaches it, the wall facing the light picks up a hard highlight, the wall
   opposite drops into shadow, and the two trade places as the card swings
   under the light. That trade is the glint. */
.window {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: var(--radius-window);
  /* Floor: the ambient shade it sits in, with the light's own reflection
     pooled on it and sliding as the card turns. */
  background:
    radial-gradient(
      130% 110% at calc(50% - var(--light-x) * 42%) calc(50% - var(--light-y) * 42%),
      rgba(255, 255, 255, 0.62) 0%,
      rgba(255, 255, 255, 0.14) 38%,
      rgba(0, 34, 10, 0.05) 78%
    ),
    rgba(0, 34, 10, 0.055);
}

/* The walls. Two insets pointing opposite ways off the same light vector, so
   they can never disagree about where the light is. */
.window::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 4;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--glint-opacity);
  box-shadow:
    /* Wall in shadow, on the side the light comes from: the lip above it
       is what's casting. */
    inset calc(var(--light-x) * -1 * var(--glint-depth))
          calc(var(--light-y) * -1 * var(--glint-depth))
          calc(var(--glint-depth) * 1.5)
          rgba(0, 34, 10, 0.34),
    /* Wall square to the light, opposite it. */
    inset calc(var(--light-x) * var(--glint-depth))
          calc(var(--light-y) * var(--glint-depth))
          calc(var(--glint-depth) * 1.1)
          rgba(255, 255, 255, 0.9),
    /* The lip itself: a hairline that stays put, so the edge of the well
       reads as milled rather than as a soft gradient. */
    inset 0 1px 0 rgba(0, 34, 10, 0.16),
    inset 0 -1px 0 rgba(255, 255, 255, 0.75);
}

#head-canvas {
  display: block;
  position: relative;
  z-index: 2;
  width: 100%;
  height: 100%;
  touch-action: none;
}

.hint,
.cta {
  position: absolute;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
}

.hint {
  margin: 0;
  z-index: 5; /* above the recess walls */
  font-size: 11px;
  color: var(--window-ink);
  transition: opacity 0.4s var(--ease);
  pointer-events: none;
}

.hint[data-state='active'] { opacity: 0; }
.hint[hidden] { display: none; }

/* ── Body ────────────────────────────────────────────── */

.card__body {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
  transform-style: preserve-3d;
}

.identity { transform: translateZ(14px); }

/* Always one line. The name is the single biggest thing on the card, so it is
   sized to fill the width rather than to a fixed scale — and nowrap means a
   long surname breaks the layout loudly rather than silently wrapping. */
.name {
  margin: 0;
  font-size: clamp(30px, 10.5vw, 46px);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.05em;
  white-space: nowrap;
}

.bio {
  margin: 0;
  font-size: clamp(19px, 5.4vw, 26px);
  line-height: 1.14;
  letter-spacing: -0.04em;
  font-weight: 400;
  color: var(--ink);
  transform: translateZ(6px);
}

.email {
  display: block;
  font-size: clamp(17px, 4.8vw, 21px);
  letter-spacing: -0.05em;
  color: var(--ink);
  text-decoration: none;
  transition: opacity 0.2s var(--ease);
  transform: translateZ(12px);
}

.email:active { opacity: 0.55; }

/* ── Handoff: QR + controls + tagline lockup ───────────── */

.handoff {
  display: flex;
  align-items: center;
  gap: 14px;
  transform: translateZ(18px);
}

/* No white panel: the QR is generated on the card's own stock colour, so it
   sits flush. Scanners only need dark modules on a light field, and cream
   clears that comfortably. */
.qr {
  flex: none;
  display: block;
  width: clamp(104px, 30vw, 128px);
  height: auto;
  border-radius: var(--radius-sm);
  /* Keep module edges hard at non-integer scales — a blurred QR is a QR that
     takes three tries to scan off a screen. */
  image-rendering: pixelated;
}

.handoff__side {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

/* A pill with a sliding indicator, matching the mobile nav's segmented
   control: translucent track, one solid thumb that moves rather than two
   separately-filled buttons. */
.switch {
  position: relative;
  display: flex;
  width: 100%;
  padding: 3px;
  background: rgba(0, 34, 10, 0.09);
  border-radius: 100px;
  isolation: isolate;
}

.switch__thumb {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: calc(50% - 3px);
  border-radius: 100px;
  background: var(--ink);
  transition: transform 0.32s var(--ease);
  z-index: -1;
}

.switch[data-active='1'] .switch__thumb { transform: translateX(100%); }

.switch button {
  flex: 1 1 0;
  min-width: 0;
  padding: 7px 4px;
  font: inherit;
  font-size: 10.5px;
  color: var(--ink-60);
  background: transparent;
  border: 0;
  border-radius: 100px;
  cursor: pointer;
  transition: color 0.28s var(--ease);
}

.switch button[aria-selected='true'] { color: var(--card); }

.handoff__hint {
  margin: 0;
  font-size: 10.5px;
  line-height: 1.3;
  color: var(--ink-40);
}

/* ── Footline ────────────────────────────────────────── */

.footline {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--ink-12);
  font-size: 10px;
  color: var(--ink-40);
  transform: translateZ(4px);
}

/* ── Haptics ─────────────────────────────────────────── */

/* Driven programmatically; never seen. Kept in the layout (rather than
   display:none) because iOS ignores the haptic on a fully hidden control. */
.haptic-switch {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
  z-index: -1;
}

/* ── Entrance ────────────────────────────────────────── */

@keyframes card-entrance {
  from { opacity: 0; transform: translateY(22px) scale(0.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* On .stage rather than .card: the card's own transform is driven by the
   tilt every frame, so animating it here would be overwritten — and a filter
   or transform left on .card would flatten the preserve-3d depths. */
.stage {
  animation: card-entrance 0.75s var(--ease) 0.05s both;
}

@supports (animation-timing-function: linear(0, 1)) {
  .stage {
    animation-timing-function: linear(
      0, 0.044, 0.143, 0.263, 0.383, 0.494, 0.592, 0.675, 0.743, 0.799,
      0.844, 0.88, 0.907, 0.929, 0.946, 0.959, 0.969, 0.976, 0.982, 0.987, 1
    );
  }
}

@media (prefers-reduced-motion: reduce) {
  .stage { animation: none; }
  .cta__border { animation: none; --cta-angle: 180deg; }
}
