/* =============================================================
   Tapur — portfolio
   Two families, one palette, nothing decorative that isn't earning
   its place.
     Instrument Serif  → the greeting and page titles
     Inter             → everything else
   ============================================================= */

:root{
  /* ---- the only colours in the project ---- */
  --bone:#F4F1EC;          /* paper */
  --bone-2:#E9E4DB;        /* paper, one step down */
  --ink:#16130F;           /* text, and the dark surround */
  --ink-2:#3A342C;         /* secondary text */
  --ink-3:#6E665B;         /* tertiary text */
  --clay:#B4643C;          /* the single accent. used sparingly */
  /* The second accent, and the only green in the project: sampled off the
     foliage in Tapur's own Garden Aid cover and lifted until white sits on
     it at 7:1. It is for states -- an avatar, a hover -- never a surface. */
  --moss:#3E5D44;
  /* The recolour the notebook's painted links use: a duotone that maps the
     artwork's dark ink onto --clay and leaves white as white, defined as
     #ink-to-clay in index.html. A hue rotation could not do this without
     also tinting the page it is printed on. */
  --hit-tint:url(#ink-to-clay);
  --line:rgba(22,19,15,.14);

  --art-w:1672;
  --art-h:941;
  --radius:10px;         /* one radius for every button on the site */

  /* one easing curve everywhere */
  --ease:cubic-bezier(.22,1,.36,1);
  /* ---- the only three faces in the project ----
     display  titles: page titles, project titles, section headings
     ui       navigation, buttons, back links, small caps labels, controls
     body     body copy, descriptions, captions, metadata
     The second name in each stack is a fallback for a failed webfont
     load only -- nothing else may set a font-family. */
  --font-display:'Instrument Serif', Georgia, serif;
  --font-ui:'Archivo', system-ui, sans-serif;
  --font-body:'Inter', system-ui, -apple-system, sans-serif;

  /* ---- the one horizontal container every page shares ----
     Card-sized pages get the smaller inset; a page that has taken the
     screen gets the full one (about 154px at 1920). */
  /* ---- THE ONE SIZE EVERY CLOSE-UP IS FITTED INTO ----
     The monitor and the pinboard are different shapes, so they cannot share
     a width AND a height -- they share this box, each fitted inside it
     without cropping and without stretching. It is measured in JS off
     .focus-box-probe, so this is the only place the number lives. */
  --focused-subject-width:min(1100px, calc(100vw - 2 * var(--page-gutter)));
  --focused-subject-height:calc(100dvh - 2 * var(--safe-bottom)
                                - var(--control-size) - var(--content-gap));

  /* ============================================================
     THE SPACING SYSTEM
     One set of tokens for every screen. Nothing positions itself with
     a number of its own any more: controls take the safe area, sections
     take the gaps, and the whole interface rescales together.
     ============================================================ */
  --page-gutter:clamp(20px, 6.94vw, 100px);   /* 100px at 1440 and above */
  --section-gap:clamp(24px, 3vw, 48px);
  --content-gap:clamp(16px, 2vw, 32px);
  --small-gap:clamp(8px, 1vw, 12px);
  --control-size:clamp(44px, 3.5vw, 52px);    /* never below a 44px target */

  /* the gutter, but never inside a notch or a rounded corner */
  --safe-left:max(var(--page-gutter), env(safe-area-inset-left, 0px));
  --safe-right:max(var(--page-gutter), env(safe-area-inset-right, 0px));
  --safe-top:max(var(--page-gutter), env(safe-area-inset-top, 0px));
  --safe-bottom:max(var(--page-gutter), env(safe-area-inset-bottom, 0px));

  /* ONE position for the sound control, shared by the landing screen and
     the room, so it does not move when the camera goes through the window.
     And one bottom line for the primary action on both screens. */
  --room-control-right:clamp(16px, 2.2vw, 32px);
  --room-control-bottom:clamp(28px, 4.2vh, 56px);
  --cta-bottom:clamp(24px, 3.6vh, 48px);

  /* the one colour pair every text action uses */
  --text-action:#4A4542;
  --text-action-hi:#211E1C;
  --navy:#16214F;            /* artwork navy -- NOT a UI button colour */

  --page-padding:var(--page-gutter);
  --page-bg:#FCFAF7;        /* the page surface: a very subtle warm white */
  --measure:800px;          /* readable width for long paragraphs */
  --measure-wide:none;      /* case studies use the full container width */
  --s:8px;                  /* the spacing step every gap is a multiple of */
}

/* The gutter's fluid band is retuned per breakpoint so it actually lands on
   the safe-area targets rather than just sliding past them: 100px on a large
   desktop, 64-80 on a standard one, 40-48 on a tablet, 20-24 on a phone.
   Within each band it is still fluid -- there is no jump at the edges. */
@media (max-width:1439px){ :root{ --page-gutter:clamp(64px, 5.6vw, 80px); } }
@media (max-width:1023px){ :root{ --page-gutter:clamp(40px, 4.7vw, 48px); } }
@media (max-width:767px){  :root{ --page-gutter:clamp(20px, 5.5vw, 24px); } }

/* nothing may scroll sideways, at any width */
html, body{ max-width:100%; overflow-x:hidden; }

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

html{ background:var(--ink); overscroll-behavior-x:none; }

body{
  margin:0; min-height:100vh;
  background:var(--ink);
  color:var(--bone);
  font-family:var(--font-body);
  font-size:16px;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
  overscroll-behavior-x:none;
}

body.route-open{ overflow:hidden; }

button{ font:inherit; color:inherit; background:none; border:0; }

/* anything you can act on says so */
button, a, .hot, [role="button"]{ cursor:pointer; }

::selection{ background:var(--clay); color:var(--bone); }

:focus-visible{ outline:2px solid var(--clay); outline-offset:3px; }

/* =============================================================
   0 · GATE -- Tapur's wall video, window punched out
   The window opening is a flat keyed rectangle in the footage. Rather
   than key it at runtime (an alpha video would rule out Safari), the
   stage is sized to the video's exact aspect and a two-layer CSS mask
   removes that rectangle -- so the live room behind #gate shows
   through the opening. Keep .gate__stage's box matched to the video's
   aspect or the mask stops lining up with the art.
   ============================================================= */
#gate{
  position:fixed; inset:0; z-index:1000;
  display:grid; place-items:center;
  overflow:hidden; background:transparent;
  /* how far the caption and button drift with the wall (set by main.js) */
  --pan-x:0px; --pan-y:0px;
  /* The wall footage's box: exactly the video's aspect, grown to cover the
     viewport. --gate-zoom is overscan: a few percent past the viewport in
     BOTH axes, so the cursor pan (and the layout's small shift, below) has
     somewhere to travel without ever exposing an edge. */
  --gate-zoom:1.07;
  --stage-w:calc(max(100vw, 100vh * 1620 / 972) * var(--gate-zoom));
  --stage-h:calc(max(100vh, 100vw * 972 / 1620) * var(--gate-zoom));
  /* where main.js has shifted the footage so its window meets the layout */
  --gate-dx:0px; --gate-dy:0px;
  /* the one gap: heading to window, and window to button */
  --entry-section-gap:clamp(16px, 4.2vh, 32px);
}
.gate__stage{
  /* exactly the video's aspect (--stage-w/-h on #gate), so the hole
     percentages below always land on the painted window. Grown uniformly,
     so the mask still lines up with the art.
     Centred with position + translate (the translate half lives on the
     element's GSAP transform as xPercent/yPercent, so the pan and the
     camera can add to it). #gate's place-items:center does NOT centre
     this: the grid track auto-sizes to the stage, so once the stage is
     bigger than the viewport there is nothing to centre against. The
     --gate-dx/-dy term is the shift that puts the painted window on the
     layout's window box (see layoutGate in main.js). */
  position:absolute; top:calc(50% + var(--gate-dy)); left:calc(50% + var(--gate-dx));
  z-index:0;
  width:var(--stage-w);
  height:var(--stage-h);
  will-change:transform;

  /* full sheet MINUS the window rectangle. Values are the keyed rect
     measured off the footage (x 41.05-59.26%, y 29.22-68.93%), grown a
     hair so the antialiased edge falls inside the hole. Position
     percentages are offset/(100 - size), the usual background-position
     maths, not the raw edge. */
  -webkit-mask-image:linear-gradient(#000 0 0), linear-gradient(#000 0 0);
          mask-image:linear-gradient(#000 0 0), linear-gradient(#000 0 0);
  -webkit-mask-repeat:no-repeat, no-repeat;
          mask-repeat:no-repeat, no-repeat;
  -webkit-mask-size:100% 100%, 18.51% 40.01%;
          mask-size:100% 100%, 18.51% 40.01%;
  -webkit-mask-position:0 0, 50.19% 48.46%;
          mask-position:0 0, 50.19% 48.46%;
  -webkit-mask-composite:xor;
          mask-composite:exclude;
}
.gate__video{ display:block; width:100%; height:100%; object-fit:cover; }

/* legibility for the caption and button without tinting the window --
   the opening sits between 29% and 69%, so this stays clear of it.
   --gate-veil clears it as the camera leaves (see applyCamera), so it
   never rides over the room on the way in. */
#gate::after{
  content:''; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(to bottom,
    rgba(40,26,14,.34) 0%, rgba(40,26,14,0) 22%,
    rgba(40,26,14,0) 78%, rgba(40,26,14,.42) 100%);
  opacity:var(--gate-veil, 1);
}

/* ONE LAYOUT: the heading, the window and the button in a single flex
   column -- one centre line, one gap variable, the whole column centred in
   the viewport. None of the three is positioned on its own. The column
   carries the wall's cursor drift (--pan-*) so all three move with it. */
.gate__layout{
  position:absolute; inset:0; z-index:2;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--entry-section-gap);
  transform:translate(var(--pan-x), var(--pan-y));
  pointer-events:none;
}
/* An empty box exactly the size of the painted window -- frame, sill and
   shutters: x 31.60-68.64%, y 23.70-75.00% of the footage. main.js moves
   the footage so the painted window sits on it. KEEP IN SYNC with
   WINDOW_STRUCTURE in main.js. */
.gate__window{
  flex:0 0 auto;
  width:calc(var(--stage-w) * .3704);
  height:calc(var(--stage-h) * .5130);
}
/* set above the window, eyebrow over title, per the reference */
.gate__title{
  /* sized to its own text, so the box's centre IS the text's centre */
  width:max-content; max-width:calc(100vw - 48px);
  text-align:center;
  opacity:0;
}
.gate__eyebrow{
  margin:0 0 clamp(6px,1vh,12px);
  font-family:var(--font-ui); font-weight:500;
  font-size:clamp(10px,.85vw,13px);
  letter-spacing:.42em; text-indent:.42em;   /* indent cancels the trailing letter-space so it stays centred */
  text-transform:uppercase; color:var(--bone);
  text-shadow:0 2px 16px rgba(40,26,14,.55);
}
.gate__name{
  margin:0;
  font-family:var(--font-ui); font-weight:700;
  /* dialled back from 3.5vw/52px -- at full width it crowded the frame */
  font-size:clamp(22px,2.5vw,38px); line-height:1;
  letter-spacing:.055em; text-indent:.055em;
  text-transform:uppercase; color:var(--bone);
  text-shadow:0 3px 26px rgba(40,26,14,.5);
  text-wrap:balance;               /* if it ever wraps, never leave one word alone */
}
/* "Product & UX/UI Designer" is wider than a phone allows at the 22px floor,
   and wrapped with DESIGNER on a line of its own. Below 480px the headline
   scales with the screen instead, meeting the 22px floor at 480. */
@media (max-width:480px){
  .gate__name{ font-size:clamp(17px, 4.6vw, 22px); }
}

.gate__enter{
  pointer-events:auto;
  opacity:0;
  /* the hover lift is on `translate`, so it never touches the pan the
     column carries */
  transition:background .3s, translate .3s var(--ease), opacity .3s;
}
.gate__enter svg{ flex:0 0 auto; }

.gate__enter:disabled{ cursor:default; }
.gate__enter:not(:disabled):hover{ translate:0 -2px; }

/* =============================================================
   THE ROOM
   ============================================================= */
#theatre{
  position:fixed; inset:0;
  overflow:hidden;
  /* An opaque, intentional ground -- the room's own warm wall tone rather
     than near-black. A composited tile that has not finished rasterising
     shows THIS for a frame instead of a black hole. */
  background:#C98B6B;
}

.house{
  /* Centred with position + translate rather than grid's place-items:
     place-items only centres an item that FITS its track: once .house
     is sized bigger than the viewport (see width below) a grid track
     sizes itself to the item's own content and centring has nothing to
     act against, so the overflow all lands on one edge instead of
     splitting evenly. Position+translate centres unconditionally. */
  position:absolute; top:50%; left:50%; transform:translate(-50%, -50%);
  /* COVER the viewport, not contain it: whichever dimension would leave
     a gap grows past the edge instead, and #theatre's overflow:hidden
     crops it. (This used to be min(), which fits the art fully on
     screen and opens up the ink-coloured background as bars above/below
     or left/right on any viewport whose ratio isn't exactly the art's
     own -- that was the black space around the room on the way in.) */
  width:max(100vw, calc(100vh * var(--art-w) / var(--art-h)));
  aspect-ratio:var(--art-w) / var(--art-h);
  overflow:hidden;
}

.layer{
  position:absolute; inset:0;
  background-size:100% 100%; background-repeat:no-repeat;
}
.stage-bg{ z-index:1; }
.stage-bg--centre{ background-image:url("../assets/stage-full.webp"); }

/* THE CAMERA RIG. The whole room lives inside this one box; opening an
   object translates and scales THIS, never the object, so the room travels
   around whatever the visitor picked. #theatre is the viewport it moves
   inside (it already clips). */
.room-camera{
  position:absolute; inset:0;
  transform-origin:0 0;
  backface-visibility:hidden;
}
/* only while it is actually moving -- see cameraPushIn/cameraPullOut */
.room-camera.is-moving{ will-change:transform; }

/* The room and everything standing on it. Scaled up a hair so there is
   something just off-frame to pan into, then nudged by the cursor as a
   single unit -- see main.js. Sits under the greeting (24), so that
   never moves with it. */
.room-pan{
  position:absolute; inset:0; z-index:1;
  will-change:transform;
}

/* =============================================================
   THE TWO LIGHT STATES
   Light on is the room photograph; light off is the same photograph with
   the blinds closed, laid exactly over it (same box, same size, same
   crop) and cross-faded. Nothing is drawn over the room, so the plant,
   the wall and every hotspot stay exactly where they are.
   ============================================================= */
.stage-bg--closed{
  z-index:1;                       /* directly over the open room */
  opacity:0; transition:opacity .6s var(--ease);
}
.stage-bg--closed.is-visible{ opacity:1; }

/* This room is lit by the window, not by lamps — so the light control is
   the blinds. One switch instead of four. */
.daylight{
  position:absolute; inset:0; z-index:4; pointer-events:none;
  background:linear-gradient(105deg,
    rgba(255,238,205,.30) 0%, rgba(255,238,205,.10) 26%, transparent 55%);
  transition:opacity .75s var(--ease);
}

.dark{
  position:absolute; inset:0; z-index:6; pointer-events:none; opacity:0;
  /* the room goes dim because the window closed -- it is never a black
     sheet over the page, and it keeps the room readable */
  background:linear-gradient(160deg, rgba(18,16,26,.42), rgba(14,12,10,.52));
  transition:opacity .75s var(--ease);
}

/* The room used to sit in a dark inset shadow that darkened all four edges.
   It read as a black oval over the artwork every time the room came back,
   and nothing else in the interface is framed that way. Removed -- the
   element stays only so the room's stacking order is unchanged. */
.prosc{
  position:absolute; inset:0; z-index:30; pointer-events:none;
  box-shadow:none;
}

/* =============================================================
   MOVING PARTS
   Nothing here reacts to the cursor position — the room holds
   still. These drift on their own, slowly.
   ============================================================= */

/* the hanging slate tiles swing a hair, like they're on string */
.sway{
  position:absolute; z-index:2; pointer-events:none;
  left:66.45%; top:27.20%; width:12.26%; height:34.43%;
  transform-origin:50% 0;
  animation:sway 8s var(--ease) infinite;
}
@keyframes sway{
  0%,100%{ transform:rotate(-.3deg); }
  50%    { transform:rotate(.3deg); }
}

/* the plant by the window drifts */
.leaf{
  position:absolute; z-index:2; pointer-events:none;
  left:7.8%; top:29.8%; width:17.9%; height:39.3%;
  transform-origin:50% 100%;
  animation:leaf 12s ease-in-out infinite;
}
@keyframes leaf{
  0%,100%{ transform:rotate(-.55deg); }
  50%    { transform:rotate(.55deg); }
}

/* =============================================================
   THE PINBOARD — artwork laid over the blank board on the wall
   Same box as .hot--board, which is the board painted into the room
   art. Sits under the hotspots (z-index 9) so the dot still reads, and
   inside .room-pan so it travels with the wall.
   ============================================================= */
/* Sized to the pinboard artwork's OWN aspect (1533x1026), not the painted
   board's (which is wider, 1.83 vs 1.49) -- matching the painted box would
   squash the artwork by about a quarter. Same width and centre as the
   painted board, so it hangs where the board is; the extra height means it
   covers the painting completely rather than letting it peek out. */
/* The collage sits ON the navy board that's painted into the room art --
   it's a transparent PNG, so the board shows through behind it, which is
   the whole point. (An earlier pass papered the painted board over with a
   patch of reconstructed wall so the collage could be any size; Tapur
   wanted the board back, so the patch is gone and the collage is fitted
   inside the painting instead.)

   Sized off the board's own box, at the artwork's aspect so it can never
   stretch: --board-fit is the share of the board's HEIGHT it takes, and
   the width follows. The board is wider than the collage, so height is
   the binding dimension and the fit always lands inside. */
/* ---- the lift layers ----
   The pinboard and the monitor are painted into the room art, so neither
   can scale on its own. Each .lift re-draws exactly its own patch of that
   same art -- same file, same scale, same position, measured off
   stage-full.png -- so at rest it is pixel-identical to what's beneath and
   invisible. On hover it scales as one object together with the overlay
   it carries (the collage, the screen), and the painting stays put under
   it. Sits under the lighting layers (daylight 4, dark 6) like the art. */
/* =============================================================
   THE FOCUS LAYERS
   Three siblings inside the room, in this order:
     the room itself          z 1-9   (art, light, photo, tiles, hotspots)
     .room-veil               z 20    the blur, and only over the room
     .lift.is-subject         z 25    whatever is being looked at, SHARP
   and the back control, which is fixed to the viewport at z 620.
   The veil is a sibling of the subject, never an ancestor: a filter on an
   ancestor would take the subject down with it (and start a stacking
   context the subject could not climb out of). Nothing above .room-veil
   carries filter, backdrop-filter or opacity.
   ============================================================= */
.room-veil{
  position:absolute; inset:-50%; z-index:20;
  background:rgba(20,16,22,.12);
  /* --veil-blur is set from the camera's destination scale: the veil rides
     inside the rig, so a radius written here is magnified along with
     everything else, and 10px at rest would be 54px at the monitor. */
  -webkit-backdrop-filter:blur(var(--veil-blur, 10px));
          backdrop-filter:blur(var(--veil-blur, 10px));
  opacity:0; pointer-events:none; cursor:pointer;
  transition:opacity .3s var(--ease);
}
.room-veil.is-in{ opacity:1; pointer-events:auto; }
/* THE SUBJECT. Above the veil, unblurred, and taking its own clicks so a
   click on it can never read as a click on the space around it. */
.lift.is-subject{
  z-index:25; pointer-events:auto;
  filter:none; -webkit-backdrop-filter:none; backdrop-filter:none;
  opacity:1; visibility:visible;
}
/* the room's vignette sits above the rig, so it is taken off while
   something is being read -- it is the room's frame, not the subject's */
body.is-focused .prosc{ opacity:0; transition:opacity .3s var(--ease); }

.lift{
  position:absolute; z-index:3; pointer-events:none;
  background-image:url("../assets/stage-full.webp");
  background-repeat:no-repeat;
  transition:transform .4s var(--ease), box-shadow .4s var(--ease);
}
.lift.is-lifted{ box-shadow:0 1.1vw 2.6vw rgba(40,24,12,.30), 0 .25vw .6vw rgba(40,24,12,.18); }

/* THE SHARP CLOSE-UPS
   The room is one 1672 x 941 photograph, so the camera magnifies it 2.6x at
   the board and 5.4x at the monitor: past its own pixels. These two skins
   are the same two objects drawn in CSS at exactly the boxes the artwork
   paints them in, in the artwork's own colours (sampled from the file), so
   they have no resolution to run out of. They cross-fade in over the last
   quarter of the camera move, once the object is nearly at its final size,
   and the sharp content on top of them -- the collage at 3693px, the live
   My Work interface, the to-do, the tool stickers -- was always sharp. */
.focus-skin{
  position:absolute; inset:0; z-index:0;
  opacity:0; pointer-events:none;
  transition:opacity .26s var(--ease);
}
.lift.is-focused .focus-skin{ opacity:1; }
/* Once the skin has taken over, the magnified patch of the photograph
   underneath it is dropped. The room art still paints the same object in
   the same box beneath the lift, so nothing disappears -- but the patch's
   own soft, 5.4x-magnified edge no longer shows as a second outline just
   outside the crisp one the skin draws. */
.lift.is-focused{ background-image:none; box-shadow:none; }

/* The monitor's bezel: near black, a touch lighter along the top. The
   outer ring is the same colour as the bezel's foot and covers the last
   magnified pixel of the painted monitor beneath, so the close-up shows
   ONE frame with one edge -- no halo, no double border. */
.focus-skin--monitor{
  border-radius:1.70% / 3.23%;
  background:linear-gradient(178deg, #33343B 0%, #2A2A2F 38%, #232326 72%, #1B1C1F 100%);
  box-shadow:inset 0 0 0 .5% rgba(255,255,255,.06),
             inset 0 1.5% 3% rgba(255,255,255,.05),
             0 0 0 .55% #1F2023;
}
/* the pinboard: navy felt, lighter at the left where the window falls */
.focus-skin--board{
  border-radius:3.51% / 6.41%;
  background:
    radial-gradient(ellipse 90% 80% at 22% 34%, #3F4A62 0%, #39425A 42%, #313A50 72%, #2C3346 100%);
  box-shadow:inset 0 0 0 .45% #46546F,
             inset 0 0 0 .75% #232838,
             inset 0 2% 5% rgba(10,12,20,.30),
             0 0 0 .4% #232838;
}

/* the pinboard: art px x 623-1049, y 179-412 of 1672x941, 15px corners */
.lift--board{
  left:37.261%; top:19.022%; width:25.538%; height:24.867%;
  background-size:391.569% 402.137%;          /* 1672/427, 941/234 */
  background-position:50.040% 25.318%;        /* 623/(1672-427), 179/(941-234) */
  border-radius:3.51% / 6.41%;
}
.lift--board.is-lifted{ transform:scale(1.016); }

/* the monitor's bezel: art px x 699-933, y 436-559, 4px corners. Lifts
   from its foot, since the stand below it stays on the desk. */
.lift--monitor{
  left:41.806%; top:46.334%; width:14.055%; height:13.177%;
  background-size:711.489% 758.871%;          /* 1672/235, 941/124 */
  background-position:48.643% 53.366%;        /* 699/(1672-235), 436/(941-124) */
  border-radius:1.70% / 3.23%;
  transform-origin:50% 100%;
}
.lift--monitor.is-lifted{ transform:translateY(-1.6%) scale(1.02); }

/* The collage sits ON the navy board -- it's a transparent PNG, so the
   board shows through behind it. --board-fit is the share of the board's
   HEIGHT it takes, and the width follows from the artwork's own aspect
   (3693x2328 = 1.586, against the board's 1.825) so it can never stretch.
   The board is wider than the collage, so height binds and the collage
   always lands inside. */
.board-art{
  --board-fit:0.96;
  position:absolute; container-type:inline-size;   /* sizes the to-do (cqw) */
  height:calc(var(--board-fit) * 100%);
  width:calc(var(--board-fit) * 86.93%);      /* 1.586 / 1.825 */
  left:calc(50% - var(--board-fit) * 86.93% / 2);
  top:calc(50% - var(--board-fit) * 100% / 2);
  background-position:center; background-repeat:no-repeat;
  background-size:100% 100%;
}

/* The three tasks, as ONE block written on the sticky note in the collage.
   Everything is a share of the collage (.board-art in the room,
   the collage's own container), taken off the art:
   the front note runs x 2692-3252, y 1390-1965 of 3693 x 2328
   (72.9-88.1%, 59.7-84.4%), and the red pin's bottom edge is at y 1455
   (62.5%). The block starts --pin-gap below the pin and inset from the
   note's left edge, so it never touches the pin and stays on the paper
   at any size. Tilted to the note's own top edge (~1.8deg). */
.board-todo{
  --pin-bottom:62.5%;
  --pin-gap:4.1%;                        /* of the collage's height */
  position:absolute;
  left:74.4%; top:calc(var(--pin-bottom) + var(--pin-gap)); width:12.8%;
  margin:0; padding:0; list-style:none;
  display:flex; flex-direction:column; gap:.22em;
  transform:rotate(1.8deg); transform-origin:0 0;
  font-family:var(--font-display);
  font-size:1.9cqw; line-height:1.15;    /* of the collage's width */
  color:#3B3327;
}
.board-todo li{
  display:flex; align-items:baseline; gap:.4em;
  white-space:nowrap;
}
.board-todo li::before{
  content:'☐'; font-size:.9em; opacity:.75;
}

/* The tools, as paper stickers along the foot of the board: small white
   backing, a scrap of tape, a soft shadow, and a hair of tilt so they sit
   like something stuck on rather than printed. Sized off the collage, so
   the row works both in the room and in the enlarged board. */
.board-tools{ position:absolute; inset:0; pointer-events:none; }
.tool{
  position:absolute; width:5.5%; aspect-ratio:1;
  left:var(--x); top:var(--y);
  display:grid; place-items:center;
  background:#FDFCF9; border-radius:8%;
  rotate:var(--tilt, 0deg);
  box-shadow:0 1.2% 2.6% rgba(8,10,18,.40), 0 0 0 1px rgba(255,255,255,.35) inset;
}
/* the scrap of tape across the top edge */
.tool::before{
  content:''; position:absolute; top:-9%; left:28%; width:44%; height:20%;
  background:rgba(255,255,255,.42);
  box-shadow:0 1px 2px rgba(8,10,18,.28);
}
.tool img{ width:62%; height:62%; object-fit:contain; display:block; }

/* =============================================================
   THE MONITOR — the work carousel shown on the screen
   The box is the screen INSIDE the bezel, measured off the room art
   (x 704-927, y 444-552 of 1672x941). `contain` rather than cover: the
   artwork's arrows sit hard against its left and right edges, and
   cropping would clip them. Its cream ground is near enough to the
   painted screen's that the margins don't read as letterboxing.
   ============================================================= */
.screen-art{
  /* the screen inside the bezel: art px x 704-927, y 444-552 */
  position:absolute;
  left:2.128%; top:6.452%; width:94.894%; height:87.097%;
  background:#FFF6E9;                 /* the screen's own ground: ONE cream,
                                         shared by .screen-ui and every card's
                                         picture box, so nothing reads as a
                                         panel laid on the screen */
  overflow:hidden;
}
/* The live My Work interface, laid out in the artwork's own proportions and
   sized in container units -- so it is the same interface whether the camera
   is across the room or right up against the screen. It is only clickable
   once the camera has arrived (see #theatre.zoom-work below). */
.screen-ui{
  position:absolute; inset:0; margin:auto;
  aspect-ratio:1672 / 941; max-width:100%; max-height:100%;
  background:#FFF6E9;
  container-type:size;
  pointer-events:none;
  display:grid; place-items:center;
  /* Defaults; both are re-set in px-accurate terms by the camera when it
     knows how wide the screen will actually land (see setScreenMetrics),
     so the heading-to-cards gap stays 36px and the side padding stays
     within 56-80px at every viewport rather than growing with the screen. */
  --stack-gap:3.56cqw;
  --screen-pad:7cqw;
}
/* ONE composition, not two things positioned apart: the heading and the row
   of cards, a single gap between them, the whole group centred in the cream.
   No absolute tops, no spacer, no margins of their own to fight over.
   The units are cqw, so this must be a CHILD of the container -- a
   container does not query itself. */
.screen-ui__stack{
  display:flex; flex-direction:column; align-items:stretch;
  width:100%;
  gap:var(--stack-gap);             /* 28px on the arrived screen */
}

#theatre.zoom-work .screen-ui{ pointer-events:auto; }
/* THE BLUE FURNITURE, WHILE MY WORK IS OPEN.
   The veil blurs the room but only tints it 12%, so the navy pinboard and
   the navy skill tiles still read as blue bands above the monitor. They are
   faded out for the duration of this one view and come straight back when
   it closes -- nothing is removed, and the pinboard's own close-up is
   untouched. */
#theatre.zoom-work .lift--board,
#theatre.zoom-work .tile-labels{
  opacity:0;
  transition:opacity .32s var(--ease);
}
/* The navy board is PAINTED INTO the room photograph as well, so fading the
   lift patch above cannot remove it -- the artwork underneath still shows a
   blue strip over the monitor. The veil is therefore opaque enough to cover
   it for this one view rather than merely blurring it, which also gives My
   Work the isolation it wants. The other close-ups keep the light veil. */
#theatre.zoom-work .room-veil{ background:rgba(26,19,15,.72); }
.lift--board, .tile-labels{ transition:opacity .32s var(--ease); }

/* while the camera is in close, nothing else in the room takes a click */
#theatre.is-zoomed .hot{ pointer-events:none; }


/* =============================================================
   THE PHOTO — pupils follow the cursor
   ============================================================= */
/* Sits inside the blank frame already hanging on the wall, so your photo
   arrives in a real frame rather than floating on the plaster. */
.portrait{
  position:absolute; z-index:8;
  left:25.00%; top:20.62%; width:9.03%; aspect-ratio:151/187;
  background:transparent;
  padding:2.6%;
}
.portrait__img{
  /* The white mat painted into the frame opens at roughly 6% / 4% in from
     this box, so anything past that shows as white around the photo. At
     9%/7% only a slim, even mat is left -- Tapur found the earlier 14.3%
     /12.7% too white. Kept proportional, not px: the tracking eyes are
     placed in percentages, so a fixed inset would re-crop the photo at
     every window size and slide them off the face. */
  position:absolute; inset:7% 9%;
  background:#e6e0d4 center/cover no-repeat;   /* your photo goes here */
  display:grid; place-items:center;
}
/* a plain silhouette so the eyes have a face to sit on before your
   photo goes in. Add .has-photo on .portrait and it disappears. */
.portrait__ph{
  position:absolute; inset:0;
  background:
    radial-gradient(ellipse 30% 23% at 50% 36%, #d6cec0 0 99%, transparent 100%),
    radial-gradient(ellipse 52% 30% at 50% 106%, #d6cec0 0 99%, transparent 100%);
  font-size:0;
}
.portrait.has-photo .portrait__ph{ display:none; }

/* The tracking eyes. They stay ON once a photo goes in -- the photo is a
   face, and the eyes land on top of its own, so it looks around the room
   with you. Positions come from CONTENT.art.eyePos (see content.js), which
   writes these four custom properties; the defaults below are what the
   placeholder silhouette wants. */
.portrait{ --eye-l:39%; --eye-r:52%; --eye-top:34%; --eye-size:9%; }
.eye{
  position:absolute; width:var(--eye-size); aspect-ratio:1;
  background:var(--bone); border-radius:50%;
  top:var(--eye-top); overflow:hidden;
  box-shadow:inset 0 .2vmin .4vmin rgba(22,19,15,.25);
}
.eye--l{ left:var(--eye-l); }
.eye--r{ left:var(--eye-r); }
.pupil{
  position:absolute; left:50%; top:50%; width:46%; aspect-ratio:1;
  margin:-23% 0 0 -23%; border-radius:50%; background:var(--ink);
  will-change:transform;
}

/* =============================================================
   INTRO CARD — who you are, and simple nav, under the photo frame
   ============================================================= */
.intro-card{
  position:absolute; z-index:8;
  /* hung straight under the photo frame, whose painted edge is at 25.06%
     from the left and 40.6% from the top of the room art */
  left:25.06%; top:calc(40.6% + 20px);
  display:flex; flex-direction:column;
  opacity:0;
}
/* A grid sized to max-content: the column is exactly as wide as the
   longest label ("About Me"), and every button stretches to fill it --
   equal width, equal height, one left edge, even spacing. */
.intro-card__nav{
  display:grid; grid-auto-rows:1fr; justify-items:stretch;
  width:max-content; gap:5px;
}
.intro-nav-btn{
  /* lighter pill so Work/About/Contact reads as part of the same quiet
     type block above it, not a separate boxed widget */
  display:inline-flex; align-items:center; justify-content:flex-start; gap:6px;
  width:100%; box-sizing:border-box; text-align:left;
  padding:4px 9px; border-radius:var(--radius);
  background:transparent; color:var(--ink);
  font-size:.6vw; letter-spacing:.03em;
  border:1px solid rgba(22,19,15,.16);
  transition:background .3s, color .3s, border-color .3s;
}
.intro-nav-btn svg{ width:11px; height:11px; flex:0 0 auto; }
/* they are buttons now (two open dialogs); the label keeps the link look */
.intro-nav-btn span{ text-decoration:underline; text-underline-offset:.18em; line-height:1.2; }
.intro-nav-btn:hover{ background:var(--ink); color:var(--bone); border-color:var(--ink); }

/* =============================================================
   SKILL LABELS — real text sitting on the nine blank tiles
   ============================================================= */
.tile-labels{ position:absolute; inset:0; z-index:9; pointer-events:none; }
/* Small light type on a dark slate is the hardest case for legibility, so
   this leans on weight and contrast rather than size: semibold, full-
   strength bone, and tighter tracking (wide letter-spacing thins small
   caps out). It was 400 at 82% opacity, which greyed out at this size. */
.tile-label{
  position:absolute; display:flex; align-items:center; justify-content:center;
  text-align:center; padding:0 4px;
  font-size:.58vw; font-weight:600; letter-spacing:.03em;
  text-transform:uppercase;
  color:var(--bone); line-height:1.25;
  text-shadow:0 1px 2px rgba(10,12,20,.45);
}
.tile-label:nth-child(1){ left:66.51%; top:27.52%; width:5.56%; height:5.63%; }
.tile-label:nth-child(2){ left:72.66%; top:27.52%; width:5.62%; height:5.63%; }
.tile-label:nth-child(3){ left:66.51%; top:34.65%; width:5.56%; height:5.63%; }
.tile-label:nth-child(4){ left:72.66%; top:34.65%; width:5.62%; height:5.63%; }
.tile-label:nth-child(5){ left:66.51%; top:41.76%; width:5.56%; height:5.42%; }
.tile-label:nth-child(6){ left:72.66%; top:41.76%; width:5.62%; height:5.42%; }
.tile-label:nth-child(7){ left:66.51%; top:48.57%; width:5.56%; height:5.63%; }
.tile-label:nth-child(8){ left:72.66%; top:48.57%; width:5.62%; height:5.63%; }

/* =============================================================
   HOTSPOTS + DOTS
   ============================================================= */
.hot{
  position:absolute; z-index:9;
  padding:0; border-radius:8px; opacity:0;
  /* opacity is in the transition so the markers FADE as the camera starts
     travelling, rather than blinking out */
  transition:transform .35s var(--ease), opacity .32s var(--ease);
}
/* The clay-coloured glow that used to bloom behind a hotspot on hover is
   gone -- over the pinboard it read as an orange smear across the art.
   The dot growing and the tooltip appearing are signal enough. */

.hot--board   { left:37.20%; top:19.02%; width:25.54%; height:24.76%; }
.hot--monitor { left:41.75%; top:46.33%; width:14.05%; height:14.56%; }
.hot--resume  { left:57.30%; top:59.72%; width:7.18%;  height:3.83%; }

/* The marker itself. It has to read on a peach wall, on a navy board and on
   a lit monitor, so it is near-white with both a dark ring and a soft dark
   halo: light enough to find on the dark furniture, ringed enough to find on
   the wall, and small enough that neither is loud. */
.dot{
  position:absolute; left:50%; top:50%;
  width:8px; height:8px; margin:-4px 0 0 -4px;
  border-radius:50%; pointer-events:none;
  background:#FFFDF8;
  box-shadow:0 0 0 2px rgba(22,19,15,.45), 0 2px 7px rgba(22,19,15,.4);
  transition:transform .35s var(--ease);
}
.dot::after{
  content:''; position:absolute; inset:0; border-radius:50%;
  border:1.5px solid #FFFDF8;
  animation:ping 3.4s var(--ease) infinite;
}
@keyframes ping{
  0%  { transform:scale(1);   opacity:.7; }
  70% { transform:scale(3.2); opacity:0; }
  100%{ transform:scale(3.2); opacity:0; }
}
.hot--monitor .dot::after{ animation-delay:.5s; }
.hot--resume  .dot::after{ animation-delay:1.5s; }

.hot:hover .dot, .hot:focus-visible .dot{ transform:scale(1.7); }
.hot:hover .dot::after{ animation-play-state:paused; opacity:0; }


/* a small text hint on hover, instead of relying on colour alone */
.hot__tip{
  /* anchored to the dot's own centre (top:50%, same as .dot) rather than the
     hotspot box's edges -- some hotspots are tall/wide, so their box edge
     sat well away from the dot itself. The -Npx term IS the gap from the
     dot's centre to the tooltip's near edge (translateY(-50%) already
     centres the tooltip's own box on that point) -- keep it at least
     20px + the dot's hover radius so the tip never crowds the dot. */
  position:absolute; left:50%; top:50%;
  transform:translate(-50%, calc(-50% - 24px));
  white-space:nowrap; pointer-events:none;
  background:var(--ink); color:var(--bone);
  font-size:11px; letter-spacing:.04em;
  padding:6px 12px; border-radius:var(--radius);
  opacity:0; transition:opacity .25s var(--ease), transform .25s var(--ease);
}
.hot:hover .hot__tip, .hot:focus-visible .hot__tip{ opacity:1; transform:translate(-50%, calc(-50% - 30px)); }
/* no hint ever shows through a dialog */
/* Nothing that belongs to the whole room survives a close-up: the way out,
   the sound control and every hotspot marker go, so the object is all
   there is. They are gone, not merely invisible -- nothing under them can
   be clicked or tabbed to. */
#theatre.is-zoomed .hot__tip{ opacity:0; }
/* the dots fade out while the camera is still travelling */
.hot .dot{ transition:opacity .32s var(--ease), transform .35s var(--ease); }
body.is-focused .hot{ opacity:0 !important; pointer-events:none; }
body.is-focused .hot .dot::after{ animation:none; }
body.is-focused .room-exit,
body.is-focused #sound-toggle{ display:none; }
/* THE ROOM IS LEAVING. From the instant Step Back Outside is pressed the
   room's own controls stop taking clicks: the camera is about to sweep the
   whole room -- hotspots, nav and all -- back through the window, and a
   second click during that move would otherwise land on whichever control
   the camera had just dragged under the cursor. That is how Step Back
   Outside used to arrive at About Me. */
body[data-view="outside"] .hot,
body[data-view="outside"] .intro-card,
body[data-view="outside"] .room-exit{ pointer-events:none; }
/* a page (the notebook included) is a focused view too */
body.route-open .room-exit,
body.route-open #sound-toggle{ display:none; }

/* =============================================================
   ROUTED PAGES
   ============================================================= */
/* A page opens as a card in the middle of the room. Scroll inside it and it
   takes over the screen; scroll back to the top and it shrinks again. */
.page{
  position:fixed; inset:0; z-index:500;
  display:grid; place-items:center;
  /* NO backdrop-filter here. Both pages that use this layer cover the
     screen with an opaque frame, so the blur was only ever visible during
     the half-second fade -- and that is exactly when it broke: a
     backdrop-filtered layer being composited while the room underneath is
     mid-camera-move (and while this element's own width/height animate to
     full screen) is what paints the black and chequered tiles. */
  background:rgba(20,17,13,.55);
  opacity:0; visibility:hidden;
  transition:opacity .45s var(--ease), visibility .45s;
}
.page.is-open{ opacity:1; visibility:visible; }

.page__frame{
  position:relative;
  width:min(900px, 88vw);
  height:min(660px, 78vh);
  background:var(--page-bg); color:var(--ink);
  border-radius:16px;
  overflow-y:auto; overscroll-behavior:contain;
  box-shadow:0 40px 110px rgba(0,0,0,.5);
  transform:translateY(20px) scale(.985);
  transition:width .65s var(--ease), height .65s var(--ease),
             max-width .65s var(--ease), border-radius .65s var(--ease),
             transform .5s var(--ease);
  scrollbar-width:thin;
}
.page.is-open .page__frame{ transform:none; }

/* expanded */
.page.is-full .page__frame{
  width:100vw; height:100vh; max-width:none; border-radius:0;
}
/* full-screen pages get the wide inset -- ~154px a side at 1920 */
.page.is-full{ --page-padding:clamp(24px, 8vw, 180px); }

/* THE shared page container. Every page -- About, Resume, every project,
   their heroes, their content and the More projects grid -- is laid out
   inside this one box, so they share a left edge, a right boundary and a
   heading alignment. Long paragraphs get a readable max-width (--measure)
   but keep the same left edge; they are never centred. */
.page__inner{
  width:100%; margin:0;
  padding-block:clamp(26px,4vw,54px) clamp(70px,10vh,120px);
  padding-inline:var(--page-padding);
  box-sizing:border-box;
}

/* a project's own links: the real Figma file, the live site, and so on */
.proj__links{
  display:flex; flex-wrap:wrap; gap:calc(var(--s) * 3);
  margin:calc(var(--s) * 5) 0 0;
}
.proj__link{
  font-family:var(--font-ui); font-weight:600; font-size:15px;
  letter-spacing:.02em; color:var(--ink);
  text-decoration:underline; text-underline-offset:.24em;
  transition:color .25s;
}
.proj__link:hover{ color:var(--clay); }

/* =============================================================
   A REAL PAGE, NOT A CARD
   About Me is a page in its own right: no rounded card, no blur over the
   room, no scroll-to-expand. Warm white, full bleed, and the same
   container every case study uses.
   ============================================================= */
.page--doc{
  background:var(--page-bg);
  backdrop-filter:none; -webkit-backdrop-filter:none;
  --page-padding:clamp(24px, 8vw, 180px);
}
.page--doc .page__frame{
  width:100vw; height:100vh; max-width:none;
  border-radius:0; box-shadow:none; transform:none;
  background:var(--page-bg);
}

/* =============================================================
   ABOUT ME
   Three sections on warm paper, and nothing between them that is only
   there to fill space: the hero she is introduced in, one block that
   carries work and life together, and a closing block of other people's
   words. Everything is built by frameOf(PAGES.about) in js/main.js.
   ============================================================= */

/* ---- the paper itself ----
   Built entirely in CSS so it rescales with the window, and built from
   NOISE rather than from lines. A repeating gradient weave is a regular
   grid, and a regular grid moires the moment the page is zoomed or scaled;
   these three feTurbulence tiles are irregular, so there is nothing to beat
   against. Each one carries stitchTiles, which makes its edges meet
   exactly -- no seam at any size. Two of them are stretched along one axis,
   which is what reads as woven fibre rather than as grain.
   Warm ivory, never peach: #FBF7EF at the top, #F3ECDE in the far corner. */
.about{
  --paper:#FBF7EF;
  position:relative; isolation:isolate;
  background-color:var(--paper);
  background-image:
    /* the grain in the sheet */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)' opacity='.055'/%3E%3C/svg%3E"),
    /* the threads running across it */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='w'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.014 .7' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23w)' opacity='.05'/%3E%3C/svg%3E"),
    /* and the ones running down it, a shade lighter */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='v'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7 .014' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23v)' opacity='.04'/%3E%3C/svg%3E"),
    /* the light falling on it */
    radial-gradient(130% 95% at 50% -10%, #FFFDF8 0%, #FBF7EF 44%, #F3ECDE 100%);
  background-size:240px 240px, 300px 300px, 300px 300px, auto;
  color:var(--ink);
}
/* the page surface behind it, so a short window never shows the old white */
#page-about, #page-about .page__frame{ background:#FBF7EF; }

/* ---- the About container ----
   ONE content container for the whole page, shared by the nav: the same
   inset the case studies use (--page-padding), held to a readable maximum
   on very wide screens. --ab-pad is resolved against .about (a size
   container), so the nav, every section and the belt's bleed all agree. */
.about{
  container-type:inline-size;
  --ab-max:1240px;
  --ab-pad:max(var(--page-padding), calc((100cqw - var(--ab-max)) / 2));
  /* spacing, desktop */
  --ab-section-gap:clamp(64px, 7vw, 104px);
  --ab-head-gap:clamp(24px, 2.8vw, 40px);      /* section heading -> cards/icons */
  min-height:100%;
}

/* THE NAV. The same component every case study uses (pageNav in main.js):
   same sticky position, height, padding, Back control and scroll behaviour.
   On this page it sits on the paper rather than the case studies' white, and
   carries no rule under it. */
.about > .page-nav{
  margin:0;
  padding-inline:var(--ab-pad);
  /* the case studies' bar: a solid backing and the same 1px rule under it,
     in this page's paper colour */
  background:color-mix(in srgb, var(--paper) 94%, transparent);
  border-bottom:1px solid var(--line);
}

.about__inner{
  position:relative; z-index:1;
  padding:clamp(20px, 3.4vw, 48px) var(--ab-pad) clamp(72px, 9vw, 120px);
}

/* ---- the type ---- */
.ab-eyebrow{
  margin:0 0 14px;
  font-family:var(--font-ui); font-weight:600;
  font-size:12px; letter-spacing:.2em; line-height:1.2;
  text-transform:uppercase; color:var(--clay);
}
.ab-h1{
  margin:0 0 22px; max-width:18ch;
  font-family:var(--font-display); font-weight:400;
  font-size:clamp(36px, 4vw, 56px); line-height:1.04; letter-spacing:-.015em;
  color:var(--ink); text-wrap:balance;
}
.ab-h2{
  margin:0 0 var(--ab-head-gap);
  font-family:var(--font-display); font-weight:400;
  font-size:clamp(28px, 2.7vw, 40px); line-height:1.08; letter-spacing:-.01em;
  color:var(--ink);
}
.ab-h3{
  margin:0 0 10px;
  font-family:var(--font-ui); font-weight:600;
  font-size:clamp(16px, 1.15vw, 17px); line-height:1.3; letter-spacing:.005em;
  color:var(--ink);
}
.ab-p{
  margin:0 0 16px; max-width:60ch;
  font-family:var(--font-body); font-size:clamp(15.5px, 1.1vw, 17px);
  line-height:1.62; color:var(--ink-2); text-wrap:pretty;
}
.ab-p:last-of-type{ margin-bottom:0; }

.ab-section{ margin-top:var(--ab-section-gap); }

/* ---- 1. MY STORY ---- */
.ab-story{
  display:grid; grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);
  gap:clamp(32px, 5vw, 80px); align-items:center;
}
.ab-story__text{ max-width:60ch; }

/* two numbers under the story -- spacing only, no rule above them */
.ab-facts{
  list-style:none; margin:clamp(28px, 3vw, 40px) 0 0; padding:0;
  display:flex; flex-wrap:wrap; gap:clamp(24px, 3.4vw, 56px);
}
.ab-facts li{ display:grid; gap:4px; }
.ab-facts b{
  font-family:var(--font-display); font-weight:400;
  font-size:clamp(26px, 2.3vw, 32px); line-height:1; color:var(--ink);
}
.ab-facts span{
  font-family:var(--font-body); font-size:13px; line-height:1.4; color:var(--ink-3);
}

/* the drag area: the portrait's own column, with room round the frame for
   the pins to be moved about in */
.ab-visual{
  position:relative;
  padding:clamp(28px, 3.4vw, 52px) clamp(20px, 3vw, 48px);
}
.ab-portrait{ position:relative; width:min(360px, 100%); margin-inline:auto; }
.ab-portrait__frame{
  margin:0; position:relative;
  background:#FFFDFA;
  border:solid #FFFDFA; border-width:12px 12px 34px;
  box-shadow:0 16px 34px rgba(62,45,24,.14), 0 3px 6px rgba(62,45,24,.1);
  rotate:-1.6deg;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.ab-portrait__frame:focus-visible{ outline:2px solid var(--clay); outline-offset:4px; }
/* both photographs in ONE fixed box, with identical size, crop and position */
.ab-portrait__photo{
  position:relative; display:block; overflow:hidden;
  aspect-ratio:4/5;
  background:#E9E4DB;
}
/* The two files are transparent cut-outs framed slightly differently:
   measured against each other, the subject in the black-and-white file sits
   9px lower and 8px further right, at 100.5% of the colour file's scale
   (source pixels, 1086 x 1405). The mono copy is corrected by exactly that,
   below. The shared stack is enlarged 3% -- the same for both images -- so
   the correction can never uncover a transparent edge inside the crop. */
.ab-portrait__stack{
  position:absolute; inset:0; display:block;
  transform:scale(1.03);
}
.ab-portrait__img{
  position:absolute; inset:0;
  display:block; width:100%; height:100%;
  object-fit:cover; object-position:50% 50%;
  transform-origin:50% 50%;
  backface-visibility:hidden;
  will-change:opacity;
  user-select:none; -webkit-user-select:none; -webkit-user-drag:none;
}
/* STATIC, in every state: -8px of 1086 across, -9px of the 1357.5px-tall
   4:5 box (cover fits the width), then 99.5% */
.ab-portrait__img--mono{
  transform:translate(-0.737%, -0.663%) scale(.995);
}
/* only the opacity of the colour copy ever changes */
.ab-portrait__img--colour{
  opacity:0;
  transition:opacity .3s ease;
}
@media (hover:hover){
  .ab-portrait__frame:hover .ab-portrait__img--colour{ opacity:1; }
}
.ab-portrait__frame:focus-visible .ab-portrait__img--colour,
.ab-portrait__frame.is-colour .ab-portrait__img--colour{ opacity:1; }
@media (prefers-reduced-motion:reduce){
  .ab-portrait__img--colour{ transition:none; }
}

/* the camera and the headphones: pick them up and move them */
.ab-pin{
  --dx:0px; --dy:0px; --z:2;
  position:absolute; z-index:var(--z);
  width:var(--w); height:auto;
  translate:var(--dx) var(--dy);
  rotate:var(--rot, 0deg);
  filter:drop-shadow(0 8px 14px rgba(62,45,24,.2));
  cursor:grab;
  touch-action:none;                 /* only the pins: the page still scrolls */
  user-select:none; -webkit-user-select:none; -webkit-user-drag:none;
  -webkit-tap-highlight-color:transparent;
}
.ab-pin.is-dragging{
  cursor:grabbing;
  filter:drop-shadow(0 18px 26px rgba(62,45,24,.26));
}

/* ---- 2. BEYOND THE PIXELS ----
   One mark and one word each, on an even grid: the same tile size for every
   icon, and every label on the same line. */
.ab-hobbies{
  list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:repeat(7, minmax(0, 128px));
  justify-content:start;
  gap:28px 24px;
}
.ab-hobby{
  display:grid; justify-items:center; align-content:start; gap:14px;
  text-align:center;
}
.ab-hobby__icon{
  display:grid; place-items:center;
  width:76px; height:76px; border-radius:20px;
  background:rgba(180,100,60,.09);
  color:var(--clay);
}
.ab-hobby__icon svg{ width:34px; height:34px; stroke-width:1.5; }
.ab-hobby__label{
  font-family:var(--font-ui); font-weight:600;
  font-size:14.5px; line-height:1.25; letter-spacing:.01em;
  color:var(--ink);
}

/* ---- 3. HOW I WORK: three equal cards ---- */
.ab-values{
  list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr));
  grid-auto-rows:1fr;
  gap:24px; align-items:stretch;
}
.ab-card{
  padding:clamp(24px, 2.2vw, 32px);
  border-radius:14px;
  background:rgba(180,100,60,.07);
  box-shadow:0 1px 2px rgba(62,45,24,.05), 0 10px 28px rgba(62,45,24,.06);
}
.ab-card p{
  margin:0; font-family:var(--font-body); font-size:15.5px;
  line-height:1.6; color:var(--ink-2); text-wrap:pretty;
}

/* ---- 4. KIND WORDS ----
   One track carrying TWO identical runs of the same cards. js/main.js moves
   it and wraps it at exactly one run's width, so the loop has no seam. It
   bleeds to the window's edges and fades there; its cards start on the
   content edge. */
.ab-belt{
  margin:0 calc(-1 * var(--ab-pad));
  padding-block:4px clamp(20px,3vh,30px);
  overflow-x:auto; overscroll-behavior-x:contain;
  scrollbar-width:none; -ms-overflow-style:none;
  -webkit-mask-image:linear-gradient(90deg, transparent 0,
                     #000 var(--page-padding), #000 calc(100% - var(--page-padding)), transparent 100%);
          mask-image:linear-gradient(90deg, transparent 0,
                     #000 var(--page-padding), #000 calc(100% - var(--page-padding)), transparent 100%);
}
.ab-belt::-webkit-scrollbar{ display:none; }
.ab-belt__track{
  display:flex; width:max-content;
  padding-inline:var(--ab-pad);
  will-change:transform;
}
.ab-belt__run{
  flex:0 0 auto; display:flex; align-items:stretch; gap:24px;
  list-style:none; margin:0; padding:0 24px 0 0;
}
@media (prefers-reduced-motion:reduce){
  .ab-belt__track{ transform:none !important; }
  .ab-belt__run[aria-hidden="true"]{ display:none; }
}

/* one row, one height, one baseline */
.tcard{
  flex:0 0 clamp(240px, 22vw, 290px);
  display:grid; grid-template-rows:1fr auto; gap:14px;
  align-self:stretch;
}
.tcard__quote{
  margin:0; padding:clamp(20px, 1.8vw, 24px);
  border-radius:14px;
  background:var(--tint, rgba(180,100,60,.10));
  box-shadow:0 1px 2px rgba(62,45,24,.04), 0 8px 20px rgba(62,45,24,.05);
  font-family:var(--font-body); font-size:14.5px; line-height:1.6;
  color:var(--ink-2); text-wrap:pretty;
}
.tcard__hi{ color:var(--clay); }
/* The person under each card: icon beside a two-line block, the name above
   the designation, both left-aligned. The row has ONE minimum height on every
   card -- room for a name and a two-line designation -- and the icon and the
   text block are both centred in it, so the icon is always centred on the
   text beside it and a longer designation never moves anything else. */
.tcard__who{
  display:flex; align-items:center; gap:12px;
  min-height:52px; margin:0; padding-left:2px;
}
.tcard__face{
  flex:0 0 36px; width:36px; height:36px; border-radius:50%;
  object-fit:cover; background:var(--bone-2);
}
.tcard__who .tcard__face--mono{
  display:grid; place-items:center;
  background:var(--clay); color:#FFFFFF;
  font-family:var(--font-ui); font-weight:600; font-size:11.5px; letter-spacing:.04em;
}
.tcard__who figcaption{
  display:flex; flex-direction:column; justify-content:center; gap:2px;
  min-width:0; text-align:left;
}
.tcard__who b{
  display:block;
  font-family:var(--font-ui); font-weight:600; font-size:13px; line-height:1.3;
  color:var(--ink);
}
.tcard__who span{
  display:block;
  font-family:var(--font-body); font-size:11.5px; line-height:1.35; color:var(--ink-3);
  overflow-wrap:break-word;
}
.ab-belt--words .ab-belt__run > :nth-child(4n+1){ --tint:rgba(180,100,60,.10); }
.ab-belt--words .ab-belt__run > :nth-child(4n+2){ --tint:rgba(62,45,24,.055); }
.ab-belt--words .ab-belt__run > :nth-child(4n+3){ --tint:rgba(201,139,107,.15); }
.ab-belt--words .ab-belt__run > :nth-child(4n+4){ --tint:rgba(233,228,219,.7); }

/* ---- 5. ABOUT THIS SITE ---- */
.ab-site .ab-h2{ margin-bottom:20px; }
.ab-site .ab-p{ max-width:none; }
.ab-sign{
  margin:24px 0 0;
  font-family:var(--font-display); font-style:italic; font-weight:400;
  font-size:clamp(19px,1.6vw,24px); line-height:1.35; color:var(--clay);
}

/* ---- the widths ---- */
@media (max-width:1100px){
  .ab-hobbies{ grid-template-columns:repeat(4, minmax(0, 1fr)); max-width:640px; }
}
@media (max-width:900px){
  .about{
    --ab-section-gap:clamp(56px, 9vw, 72px);
    --ab-head-gap:clamp(20px, 3.4vw, 28px);
  }
  /* the portrait goes under the writing */
  .ab-story{ grid-template-columns:minmax(0,1fr); gap:8px; }
  .ab-story__text{ max-width:none; }
  .ab-h1{ max-width:22ch; }
  .ab-values{ grid-template-columns:minmax(0,1fr); grid-auto-rows:auto; gap:16px; }
}
@media (max-width:640px){
  .ab-h1{ font-size:clamp(32px, 9vw, 40px); margin-bottom:18px; }
  .ab-h2{ font-size:clamp(26px, 7.4vw, 32px); }
  .ab-portrait{ width:min(300px, 76%); }
  .ab-pin{ width:calc(var(--w) * .72); }
  /* seven interests: three to a row, the last one centred */
  .ab-hobbies{
    display:flex; flex-wrap:wrap; justify-content:center;
    gap:24px 0; max-width:none;
  }
  .ab-hobby{ flex:0 0 33.333%; padding-inline:4px; }
  .ab-hobby__icon{ width:64px; height:64px; border-radius:18px; }
  .ab-hobby__icon svg{ width:30px; height:30px; }
  .ab-hobby__label{ font-size:13px; overflow-wrap:anywhere; }
  .tcard{ flex-basis:min(78vw, 300px); }
}

.section-head{
  margin:0 0 calc(var(--s) * 2);
  font-family:var(--font-display); font-weight:400;
  font-size:clamp(24px,2.6vw,34px); line-height:1.1; letter-spacing:-.01em;
}

/* the nudge that tells you there's more, and that it will grow */
.page__more{
  position:absolute; left:50%; bottom:calc(11vh - 26px); transform:translateX(-50%);
  z-index:5; margin:0; pointer-events:none;
  font-size:10.5px; letter-spacing:.18em; text-transform:uppercase;
  color:var(--ink-3);
  transition:opacity .4s;
}
.page.is-full .page__more{ opacity:0; }

/* THE back link. One component, used by About, Resume, Contact, every
   project page and the My Work dialog: transparent, left arrow,
   underlined label, Archivo. Only the label and the action differ --
   never the look, and nothing else may style a back control. */
/* ONE back control. The same pill, the same dark navy, the same icon and
   the same 44px minimum target on every page and in every focused view --
   About, the case studies, the notebook, the pinboard, My Work and the
   envelope. It is navy rather than white because white was disappearing
   against the light paper pages and the blurred room alike. */
/* ═══════════════════════════════════════════════════════════════
   THE TEXT ACTION
   One component for every secondary navigation control on the site --
   Back, Read the full story, All work, the case-study links. It is TEXT:
   no fill, no pill, no border, nothing drawn behind it. The label and its
   arrow are one target, and that target is at least 44px tall even though
   only the words are visible.
   Variants: .t-act--back (arrow first) and .t-act--fwd (arrow last).
   ═══════════════════════════════════════════════════════════════ */
.back, .t-act{
  display:inline-flex; align-items:center; gap:8px;
  margin:0 0 var(--content-gap);
  min-height:44px; padding:0;
  background:none; border:0; border-radius:0; box-shadow:none;
  font-family:var(--font-ui); font-weight:600;
  font-size:clamp(13px, .95vw, 15px); letter-spacing:.02em; line-height:1;
  color:var(--text-action); text-decoration:none;
  cursor:pointer;
  transition:color .2s var(--ease);
}
.back:hover, .t-act:hover,
.back:focus-visible, .t-act:focus-visible{ color:var(--text-action-hi); }
.back:focus-visible, .t-act:focus-visible{
  outline:2px solid var(--text-action-hi); outline-offset:4px; border-radius:3px;
}
/* the arrow is drawn at the label's weight: same colour, same optical
   thickness, same height as the cap line */
.back svg, .t-act svg{
  flex:0 0 auto; width:16px; height:16px;
  stroke-width:2.2; stroke:currentColor;
}
/* the arrow leans toward where it is going, gently */
.back svg{ transition:transform .2s var(--ease); }
.back:hover svg{ transform:translateX(-3px); }
.t-act--fwd svg{ transition:transform .2s var(--ease); }
.t-act--fwd:hover svg{ transform:translateX(3px); }

/* ONE sticky nav for the long pages. Solid warm-neutral rather than a
   blurred pane: the only backdrop-filter in this stack was what painted
   black tiles during a route change, and a page that scrolls does not need
   one. It hides going down and returns going up, in 200ms. */
.page-nav{
  position:sticky; top:0; z-index:40;
  display:flex; align-items:center;
  margin:calc(-1 * clamp(26px,4vw,54px)) calc(-1 * var(--page-padding)) var(--content-gap);
  padding:var(--small-gap) var(--page-padding);
  background:color-mix(in srgb, var(--page-bg) 94%, transparent);
  border-bottom:1px solid var(--line);
  transition:transform .2s var(--ease), opacity .2s var(--ease);
}
.page-nav .back{ margin:0; }
/* Resume and Contact Me, on the right of every case study's nav. The same
   height, the same Archivo, 16px apart; Resume is the quiet one and
   Contact Me carries the accent. */
.page-nav__actions{
  margin-left:auto; flex:0 0 auto;
  display:flex; align-items:center; gap:16px;
}
.nav-btn{
  display:inline-flex; align-items:center; justify-content:center;
  height:40px; padding:0 18px; margin:0;
  border:0; border-radius:0; box-shadow:none;
  -webkit-appearance:none; appearance:none;
  font-family:var(--font-ui); font-weight:600;
  font-size:14px; letter-spacing:.02em; line-height:1;
  white-space:nowrap; cursor:pointer;
  transition:background-color .2s var(--ease), color .2s var(--ease);
}
/* Resume: text only -- no fill, no shadow. Hover underlines the word. */
.nav-btn--secondary{
  background:transparent; color:var(--ink);
  text-decoration:underline; text-decoration-color:transparent;
  text-decoration-thickness:1.5px; text-underline-offset:.3em;
  transition:text-decoration-color .2s var(--ease), color .2s var(--ease);
}
.nav-btn--secondary:hover{ text-decoration-color:currentColor; }
/* Contact Me: the orange, square-cornered in every state */
.nav-btn--primary{ background:var(--clay); color:#FFFFFF; }
.nav-btn--primary:hover{ background:#9C5330; }
.nav-btn, .nav-btn:hover, .nav-btn:focus, .nav-btn:focus-visible, .nav-btn:active{ border-radius:0; }
.nav-btn:focus-visible{ outline:2px solid var(--clay); outline-offset:3px; }
.page-nav{ gap:16px; }
@media (max-width:480px){
  .page-nav__actions{ gap:10px; }
  .nav-btn{ padding:0 13px; font-size:13.5px; }
}
@media (max-width:360px){
  .page-nav .back{ gap:5px; }
  .page-nav__actions{ gap:8px; }
  .nav-btn{ padding:0 10px; font-size:13px; }
}
.page-nav.is-hidden{ transform:translateY(-105%); opacity:0; pointer-events:none; }
@media (prefers-reduced-motion:reduce){
  .page-nav{ transition:none; }
  .page-nav.is-hidden{ transform:none; opacity:1; pointer-events:auto; }
}

.page__kicker{
  margin:0 0 calc(var(--s) * 2);
  font-family:var(--font-ui); font-weight:600;
  font-size:clamp(14px,1vw,16px); letter-spacing:.14em; line-height:1.2;
  text-transform:uppercase; color:var(--clay);
}
/* smaller while it's a card, full size once it takes the screen */
.page__title{
  margin:0 0 clamp(18px,3vh,34px); font-family:var(--font-display); font-weight:400;
  /* Sized to hold the longest title in the project ("EdTech Learning
     Platform", 24 characters) on ONE line at every width, and still be the
     largest thing on the page. The measure is what forced the wrap. */
  font-size:clamp(26px,2.6vw,38px); line-height:1.08; letter-spacing:-.01em;
  max-width:100%; text-wrap:balance;
  overflow-wrap:break-word;            /* never past the viewport */
  transition:font-size .65s var(--ease);
}
.page.is-full .page__title{ font-size:clamp(44px,5.2vw,76px); line-height:1.02; }
/* a project title is the page's one poster-sized element (matching the
   specificity of the .page.is-full rule above, which a project page has).
   A step down from 7vw/104: the longest title in the project, "EdTech
   Learning Platform", now holds one line with room to spare at every
   desktop width, and it is still far and away the largest thing here. */
.page.page--project .page__title{ font-size:clamp(50px,6.2vw,92px); line-height:.98; }
/* A case study's copy runs the full width between the page's own left and
   right padding: same starting point, same margin each side, text still
   left aligned -- it simply stops wrapping early. */
.page--project{ --measure:var(--measure-wide); }
.page__title em{ font-style:italic; }
.page__lead{
  margin:0 0 clamp(22px,3.5vh,40px); max-width:var(--measure);
  font-family:var(--font-body);
  font-size:clamp(15px,1.1vw,17px); line-height:1.72; color:var(--ink-2);
  text-wrap:pretty; transition:font-size .65s var(--ease);
}
.page.is-full .page__lead{ font-size:clamp(18px,1.45vw,22px); line-height:1.6; }

/* --- notes --- */
.notes{ list-style:none; margin:0; padding:0; display:grid; gap:14px; }
.notes li{ position:relative; padding-left:22px; font-size:15.5px;
  line-height:1.65; color:var(--ink-2); }
.notes li::before{
  content:''; position:absolute; left:0; top:.62em;
  width:6px; height:6px; border-radius:50%; background:var(--clay);
}

/* --- a single project --- */
/* the hero runs the full width of the shared container. `contain`, not
   cover: these are interface screens and spec sheets, and cropping one
   cuts off the very thing it is there to show. A project with no real
   cover image in the repo renders no hero at all rather than showing
   placeholder art (see renderProject in main.js). */
/* ONE maximum for every image inside a case study, hero and figure alike,
   centred with room to breathe. They used to run the full measure of the
   page, which on a wide screen is most of the display. */
:root{ --case-media:min(100%, 880px); }
.proj__hero{
  display:block; width:var(--case-media); height:auto;
  margin:0 auto calc(var(--s) * 7);
  /* the image's own ratio: no forced box to letterbox it, and no crop --
     these are interface screens, and cropping one cuts off the very thing
     it is there to show. It is preloaded, so it lands without a shift. */
  object-fit:contain;
  background:var(--bone-2); border:1px solid var(--line);
}
.proj__meta{ display:flex; flex-wrap:wrap; gap:clamp(22px,4vw,54px);
  padding:calc(var(--s) * 3) 0; border-top:1px solid var(--line);
  border-bottom:1px solid var(--line); margin:0 0 calc(var(--s) * 6); }
.proj__meta div{ display:flex; flex-direction:column; gap:5px; }
.proj__meta dt{ font-family:var(--font-ui); font-weight:600; font-size:12px;
  letter-spacing:.14em; text-transform:uppercase; color:var(--ink-3); }
.proj__meta dd{ margin:0; font-family:var(--font-body); font-size:16px; }
.proj__section{ margin-bottom:calc(var(--s) * 6); max-width:var(--measure); }
.proj__section h3{ margin:0 0 12px; font-family:var(--font-display); font-weight:400;
  font-size:clamp(22px,2.6vw,32px); }
.proj__section p{ margin:0 0 12px; font-size:16px; line-height:1.75; color:var(--ink-2); }
.proj__section p:last-child{ margin-bottom:0; }
.proj__section .notes{ margin-top:14px; }

/* a benchmark study or a principle group: sits under its parent heading,
   indented off a rule so the nesting is visible without another type size */
.proj__section--sub{
  margin-left:clamp(0px,1.4vw,18px);
  padding-left:clamp(14px,1.8vw,22px);
  border-left:2px solid var(--line);
  margin-bottom:clamp(22px,3.4vh,36px);
}
.proj__section--sub h4{
  margin:0 0 10px; font-family:var(--font-ui); font-weight:600;
  font-size:14px; letter-spacing:.1em; text-transform:uppercase;
  color:var(--clay);
}
/* the takeaway under a study — the "so what", not more body copy */
.proj__note{
  margin-top:16px !important; padding-top:14px;
  border-top:1px solid var(--line);
  font-style:italic; color:var(--ink-3) !important;
}

/* screens run full width of the reading column, and past it once the page
   is full -- these are dense UI specs, so they need the room */
.shot{
  width:var(--case-media);
  margin:clamp(22px,3vh,32px) auto 0;
  background:var(--bone-2); border:1px solid var(--line); border-radius:4px;
  overflow:hidden;
}
.shot img{ display:block; width:100%; height:auto; }
.shot figcaption{
  padding:9px 14px; border-top:1px solid var(--line);
  font-size:12px; letter-spacing:.04em; color:var(--ink-3);
}
.page.is-full .proj__section{ max-width:var(--measure); }
/* full-screen or not, a case study's images keep the one maximum set
   above -- the page going full width is not a reason for the pictures to
   grow with it. They stay centred in the reading column. */
.page.is-full .shot{ max-width:none; width:var(--case-media); }
.page.is-full .proj__hero{ width:var(--case-media); }

/* More projects: the other three, never the one being read, as whole
   clickable cards. Three across, two on a tablet, one on a phone -- no
   arrows, no carousel, no previous/next loop. */
.more{ margin-top:calc(var(--s) * 9); padding-top:calc(var(--s) * 5);
  border-top:1px solid var(--line); }
.more__grid{
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr));
  gap:clamp(20px,2.4vw,34px); margin-top:calc(var(--s) * 4);
}
.more__card{
  display:flex; flex-direction:column; gap:calc(var(--s) * 1.5);
  color:inherit; text-decoration:none;
  transition:transform .35s var(--ease);
}
.more__all{ display:inline-flex; margin-top:calc(var(--s) * 4);
  font-family:var(--font-ui); font-size:14px; color:var(--ink-2); }

.links{ display:flex; gap:clamp(20px,3.5vw,44px); flex-wrap:wrap; }
.links a{ position:relative; padding-bottom:3px; font-size:15.5px;
  color:var(--ink-2); text-decoration:none; }
.links a::after{ content:''; position:absolute; left:0; bottom:0;
  width:0; height:1px; background:var(--clay); transition:width .35s var(--ease); }
.links a:hover{ color:var(--ink); }
.links a:hover::after{ width:100%; }

/* =============================================================
   RESUME — Tapur's notebook artwork, with real links laid over it
   The buttons are painted into the image, so each gets an invisible
   anchor on top (boxes in CONTENT.resume.hits). Those boxes are
   percentages, which only stay on target while the container has the
   SAME shape as the file -- hence aspect-ratio on .book__sheet, and a
   frame that lets it size itself rather than stretching it.
   ============================================================= */
/* The notebook has no dimming of its own: the ONE shared .focus-backdrop
   is behind it, exactly as it is behind My Work, the pinboard and the
   envelope. Without this there would be two blurs stacked on the room. */
.page--book{
  transition:opacity .62s var(--ease) .18s, visibility .62s .18s;
  background:transparent; backdrop-filter:none; -webkit-backdrop-filter:none;
  z-index:500;
}

/* Nothing behind the notebook: no card, no white panel, no rounded
   rectangle -- just the notebook's own transparent PNG over the softly
   blurred room (the blur belongs to .page). */
.page--book .page__frame{
  width:100%; height:100dvh;
  background:transparent; box-shadow:none; border-radius:0;
  overflow:visible;
  /* THE BACKDROP IS THE WAY OUT. This frame fills most of the screen, so
     while it took clicks the blurred area around the notebook did nothing.
     It lets them through now; the notebook itself takes them back. */
  pointer-events:none;
}
.page--book .book__sheet,
.page--book .back{ pointer-events:auto; }


.book{
  /* flex, not grid: the sheet must shrink-wrap the notebook exactly (a grid
     area would clamp its height and slide the hotspots off the buttons).
     The top pad only clears the back link, which is positioned. */
  padding-block:clamp(44px,6vh,64px) clamp(16px,2vh,28px) !important;
  height:100%; display:flex; align-items:center; justify-content:center;
}
.book__sheet{
  /* the box IS the picture's box, whatever the artwork's proportions --
     which is what keeps the invisible link hotspots (in % below) on the
     painted buttons. Nothing behind it: no card, no panel. */
  position:relative; display:inline-block; line-height:0;
}
.book__img{
  display:block; background:none;
  width:auto; height:auto;
  /* Larger, and still whole: the image's own maximums decide the size now
     rather than a frame around it. 86vw x 86vh keeps the complete PNG on
     screen at its own aspect with room left for the floating back control. */
  max-width:min(1500px, 86vw);
  max-height:86vh;
  object-fit:contain;
  /* NO shadow of our own. Her export keeps the notebook's natural internal
     shadows, and a drop-shadow here would follow the alpha silhouette and
     draw a halo round the clips and tabs -- which is exactly what was being
     seen. Nothing is added behind the image either: no wrapper, no panel. */
  filter:none;
}

/* The live targets over the painted buttons. INVISIBLE, always: the
   notebook already draws the buttons, and a second outlined rectangle
   appearing over the orange one reads as a duplicate button. The only
   thing they ever show is a keyboard focus ring -- never a hover state. */
.book__hit{
  position:absolute;
  background:none; border:0; box-shadow:none; border-radius:7px;
  cursor:pointer;
}
.book__hit:hover{ background:none; box-shadow:none; }
/* THE HOVER, AS ONE CHANGE.
   There is no second line any more. The clay bar that used to be drawn
   under the painted word has gone; in its place the hit fades in a clipped
   copy of the artwork's own slice, hue-rotated, so the painted word, the
   painted rule beneath it and the painted arrow all move to green at the
   same moment. See hitTint() in js/main.js for the geometry.
   The ink in the file is #3449A8; this rotation lands it on the portfolio's
   own clay, #B4643C. The paper it sits on is unsaturated, so the same
   rotation leaves the paper alone -- which is why no rectangle shows up
   around the word. */
.book__hit__tint{
  position:absolute; inset:0; overflow:hidden; pointer-events:none;
  opacity:0; transition:opacity .2s var(--ease);
}
.book__hit__tint i{
  position:absolute; display:block;
  background-repeat:no-repeat; background-size:100% 100%;
  filter:var(--hit-tint);
}
.book__hit:hover .book__hit__tint,
.book__hit:focus-visible .book__hit__tint{ opacity:1; }
@media (prefers-reduced-motion:reduce){ .book__hit__tint{ transition:none; } }
/* VIEW RESUME LIFTS, and it is the drawn button itself that moves.
   The button is pixels in Tapur's artwork and it is tilted in the picture,
   so nothing can be styled over it. What lifts is a clipped copy of exactly
   that slice of the artwork, laid on top of the original and registered to
   the pixel: raise it 4px and the button appears to rise. The slice carries
   about 11px of paper on every side, which is what covers the original
   where the copy has moved off it -- so there is no double image and no
   sliver of the old button showing underneath. */
.book__lift{
  position:absolute; overflow:hidden; pointer-events:none;
  transition:transform .2s var(--ease);
  will-change:transform;
}
.book__lift i{
  position:absolute; display:block;
  background-repeat:no-repeat; background-size:100% 100%;
}
/* the shadow it casts. Its own box is the BUTTON's box at the button's own
   angle and nothing is painted inside it: a box-shadow draws outside its
   element, so this darkens the paper round the button and never the button.
   It is laid after the slice, so the shadow is not covered by it. */
.book__lift-sh{
  position:absolute; pointer-events:none;
  border-radius:10px; background:none;
  transform:rotate(var(--rot, 0deg));
  box-shadow:0 3px 7px rgba(40,26,14,.13);
  opacity:0;
  transition:opacity .2s var(--ease), transform .2s var(--ease),
             box-shadow .2s var(--ease);
}
.book__overlay:has(.book__hit--lift:hover) .book__lift,
.book__overlay:has(.book__hit--lift:focus-visible) .book__lift{
  transform:translateY(-4px);
}
.book__overlay:has(.book__hit--lift:hover) .book__lift-sh,
.book__overlay:has(.book__hit--lift:focus-visible) .book__lift-sh{
  opacity:1;
  transform:rotate(var(--rot, 0deg)) translateY(-4px);
  box-shadow:0 11px 18px rgba(40,26,14,.26);
}
.book__hit{ transition:transform .2s var(--ease); }
@media (prefers-reduced-motion:reduce){
  .book__hit, .book__lift, .book__lift-sh{ transition:none; }
  .book__overlay:has(.book__hit--lift:hover) .book__lift,
  .book__overlay:has(.book__hit--lift:focus-visible) .book__lift{ transform:none; }
  .book__overlay:has(.book__hit--lift:hover) .book__lift-sh,
  .book__overlay:has(.book__hit--lift:focus-visible) .book__lift-sh{
    transform:rotate(var(--rot, 0deg)); }
}
/* the filter host never takes space */
.ink-filter{ position:absolute; width:0; height:0; overflow:hidden; }
.book__hit:focus-visible{ outline:2px solid var(--clay); outline-offset:2px; border-radius:7px; }

/* The overlay the hits live in. Absolutely positioned inside the sheet, so
   it takes the picture's box without changing how the sheet shrink-wraps
   the image -- and being sized by its parent rather than its content, it
   can be a size container, which is what lets the one link the artwork
   does NOT paint be set in the artwork's own units. */
.book__overlay{
  position:absolute; inset:0;
  container-type:size;
  line-height:normal;
}

.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

.btn-secondary{
  display:inline-flex; align-items:center; gap:8px;
  padding:12px 26px; border-radius:var(--radius);
  border:1px solid var(--ink); background:transparent; color:var(--ink);
  font-size:13.5px; letter-spacing:.02em; text-decoration:none;
  transition:background .3s, color .3s, border-color .3s;
}
.btn-secondary:hover{ background:var(--ink); color:var(--bone); }

/* THE HEADING. It was lettering cut out of work.png; that file is gone, so
   the sprite had nothing to draw and the heading disappeared off the screen.
   It is real type now, in the portfolio's display face, centred on the same
   axis as the row of cards and sized in the screen's own units so it holds
   its proportion at every camera distance. */
/* Archivo, not Instrument Serif. Instrument Serif ships one weight and is
   a narrow display face, so at this size it read as squeezed and there is
   no semi-bold to reach for -- synthesising one would have smeared it. This
   is Archivo 600: upright, properly spaced, the same family the cards below
   set their own titles in. Nothing here is condensed or tracked in. */
.screen-ui__title{
  position:static; flex:0 0 auto; margin:0 auto;
  width:100%; text-align:center;
  font-family:var(--font-ui); font-weight:600; font-style:normal;
  font-size:3.6cqw; line-height:1.15; letter-spacing:.004em;
  font-stretch:normal; transform:none;
  color:#16214F;
  white-space:nowrap;
}

/* ---- the carousel ----
   Three cards at a time, the fourth one arrow away. A three-column grid:
   the row of cards is the centre column and therefore centred on the
   screen's own centre line whatever the arrows do, and each arrow is
   parked the same distance in from its own edge of the cream screen.
   --arrow-inset is that distance, measured from the cream screen's own
   edge to the centre of the arrow: the screen is 1065px across when the
   camera has arrived on a 1440 x 900 display, so 9.4cqw is the 100px
   asked for. The card row is sized to leave the arrows clear of it. */
.carousel{
  /* --screen-pad is the ONE horizontal inset for the content: the row of
     cards runs from it to the mirror of it, so the left and right sides are
     equal by construction, and the arrows live INSIDE that inset rather
     than outside it -- which is what lets the cards take the width back. */
  position:relative; flex:0 0 auto;
  width:100%;
  display:flex; justify-content:center;
}
.carousel__viewport{
  --gap:2.21cqw;
  /* --visible equal cards, filling everything between the two insets */
  --n:var(--visible, 3);
  /* a card never grows past 46cqw: at one-per-row a full-width card is
     taller than the cream screen, and the row has to stay inside it */
  --card-w:min(
    calc((100cqw - 2 * var(--screen-pad) - (var(--n) - 1) * var(--gap)) / var(--n)),
    42cqw);
  position:relative;
  width:calc(var(--n) * var(--card-w) + (var(--n) - 1) * var(--gap));
  overflow-x:clip; overflow-y:visible;
  touch-action:pan-y;
}
.carousel__track{
  display:flex; gap:var(--gap);
  margin:0; padding:0; list-style:none;
  transition:transform .42s cubic-bezier(.22,1,.36,1);
  will-change:transform;
}
/* every card the same width, every card the same height */
.carousel__track > li{ flex:0 0 var(--card-w); display:flex; align-items:stretch; }

/* ---- THE PROJECT FOLDER CARD ----
   One component, four identical instances. A rounded square in the
   portfolio's own cream, the real project screen filling the upper media
   section, and the information panel stepping up out of the picture on the
   left the way a card sits in a folder. Every size is a share of the cream
   screen (cqw), so the card is the same card whether the camera is across
   the room or right up against it -- and every card is therefore the same
   width, the same picture height, the same panel height, the same padding,
   the same corner radius and the same shadow as every other card. ---- */
.pcard{
  /* ONE component, four instances, built to the supplied reference: a white
     rounded card, the project's own screen inset inside it with equal
     padding on all four sides, then title, description and the action. */
  position:relative; display:flex; flex-direction:column;
  width:100%; height:100%;
  background:#FFFFFF;
  border-radius:2.4cqw; overflow:hidden;
  padding:1.5cqw;
  color:inherit; text-decoration:none; text-align:left;
  -webkit-tap-highlight-color:transparent;
  box-shadow:0 .5cqw 1.6cqw rgba(24,22,20,.13),
             0 .12cqw .35cqw rgba(24,22,20,.07);
  transition:transform .3s var(--ease), box-shadow .3s var(--ease);
}
/* the picture, inset and rounded, filling its box */
.pcard__media{
  position:relative; display:block; width:100%;
  aspect-ratio:16 / 10;
  background:#DCE5F2;                 /* the reference's soft blue ground */
  border-radius:1.2cqw; overflow:hidden;
  flex:0 0 auto;
}
.pcard__media img{
  position:absolute; inset:0;
  display:block; width:100%; height:100%;
  object-fit:cover; object-position:center;
}
.pcard__panel{
  display:flex; flex-direction:column; flex:1 1 auto;
  padding:1.5cqw .3cqw .3cqw;
}
.pcard__title{
  /* Top aligned, and no reserved second line: a two-line title still fills
     the same space it did, so the other cards do not move -- but a one-line
     title (Omniful AI: PIM) now starts on the same line as the rest instead
     of being pushed to the floor of an empty two-line box, and its
     subheading follows directly under it. The action row is pinned to the
     bottom by margin-top:auto, so it does not move either. */
  display:block;
  font-family:var(--font-ui); font-weight:700;
  font-size:2.35cqw; line-height:1.1; letter-spacing:-.01em;
  color:#100F0E;
  transition:color .3s var(--ease);
}
.pcard__sub{
  margin-top:1cqw;                    /* 10px on the arrived screen */
  min-height:2.8em;
  font-family:var(--font-body); font-weight:400;
  font-size:1.3cqw; line-height:1.45; color:#5B6670;
}
/* the action: the reference's italic, in the portfolio's own clay */
.pcard__action{
  display:flex; align-items:center; gap:.6cqw;
  margin-top:auto; padding-top:1.1cqw;
  font-family:var(--font-body); font-weight:400; font-style:italic;
  font-size:1.35cqw; letter-spacing:.01em; color:var(--clay);
}
.pcard__action svg{
  width:1.4cqw; height:1.4cqw; flex:0 0 auto;
  transition:transform .3s var(--ease);
}
.pcard:hover, .pcard:focus-visible{
  transform:translateY(-.45cqw);
  box-shadow:0 1.15cqw 2.6cqw rgba(24,22,20,.18),
             0 .12cqw .35cqw rgba(24,22,20,.08);
}
.pcard:hover .pcard__title{ color:var(--clay); }
.pcard:hover .pcard__action svg{ transform:translateX(.35cqw); }
.pcard:focus-visible{ outline:.18cqw solid var(--clay); outline-offset:.4cqw; }

/* ---- the plainer card the written pages use under "More projects" ---- */
.more__card{
  display:grid; grid-template-rows:auto auto auto;
  align-content:start; gap:calc(var(--s) * 1.5);
  width:100%; text-align:left;
  color:inherit; text-decoration:none;
  -webkit-tap-highlight-color:transparent;
  transition:transform .3s var(--ease);
}
.more__card__media{
  display:block; position:relative;
  width:100%; aspect-ratio:16 / 9;
  background:#FFF3E4; border-radius:6px; overflow:hidden;
}
.more__card__media img{
  display:block; width:100%; height:100%;
  object-fit:cover; object-position:center;
}
.more__card__title{
  font-family:var(--font-display); font-weight:400;
  font-size:clamp(19px,1.6vw,24px); line-height:1.15;
  margin-top:4px;
}
.more__card__subtitle{
  font-family:var(--font-body); font-weight:400; font-size:15px; color:var(--ink-3);
}
.more__card:hover, .more__card:focus-visible{ transform:translateY(-4px); }
.more__card:focus-visible{ outline:2px solid #22437F; outline-offset:6px; }
.more__card:hover .more__card__title{ color:var(--clay); }

/* The arrows: same size, one horizontal line, each the same distance in
   from its own side of the cream screen, and centred against the WHOLE
   card (picture, title and subtitle) rather than the picture alone. The
   box is deliberately larger than the glyph: it is the hit area. */
/* The arrows sit in the side inset with real air between them and the first
   and last card. They used to FILL that inset -- 33px of button in a 44px
   gap -- which left about five pixels between the chevron and the card and
   read as the two touching. Each is 1.55cqw wide now, parked .3cqw in from
   the cream screen's own edge, which leaves better than 20px to the cards at
   every size the screen lands at. The cards did not move: the inset they
   are measured from, --screen-pad, is untouched.
   top:50% is the centre of the whole card row: picture, title and
   subtitle together. */
.carousel__arrow{
  position:absolute; top:50%;
  /* container units ONLY. A px length in here is a LAYOUT px, and the
     camera magnifies this whole screen about five times -- a 15px floor
     came out as a 73px button on the arrived screen. */
  width:1.55cqw; height:10cqw;
  display:grid; place-items:center; padding:0;
  color:#22437F; border-radius:2px;
  transition:opacity .25s, transform .25s var(--ease);
}
.carousel__arrow--prev{ left:.3cqw;  translate:0 -50%; }
.carousel__arrow--next{ right:.3cqw; translate:0 -50%; }
/* scale, not transform: `translate` above does the centring, and the two
   properties compose instead of one replacing the other */
.carousel__arrow:not([aria-disabled="true"]):hover{ scale:1.12; }
.carousel__arrow svg{ width:1.35cqw; height:2.5cqw; overflow:visible; }
.carousel__arrow[aria-disabled="true"]{ opacity:.22; cursor:default; }
.carousel__arrow:focus-visible{ outline:.22cqw solid #22437F; outline-offset:0; }

/* =============================================================
   THE PAPER BUTTON
   A real white card with a soft shadow. On hover, and on keyboard focus,
   its top right corner folds in like a sheet of paper. The corner is cut
   OUT of the button itself and ONE pseudo element draws the fold, so no
   white ever remains above or behind it and nothing overflows the button.
   Only the corner moves: the label and icon never shift or distort.
   ============================================================= */
/* inherits:true so ::after (which draws the fold) sees the same value the
   button animates; with inherits:false the triangle always measured 0 */
@property --fold{ syntax:'<length>'; inherits:true; initial-value:0px; }

.paper-btn{
  /* --fold drives the cut in the button (registered, so it animates);
     --fold-size is a plain property, which inherits into ::after and gives
     the flap exactly the same corner. Both change together. */
  --fold:0px;
  --fold-size:0px;
  position:relative; isolation:isolate;
  background:#fff;
  color:#10162B;
  border-radius:2px;
  /* the cut. clip-path would crop a box-shadow, so the shadow is a filter,
     which follows the cut shape exactly. */
  clip-path:polygon(0 0, calc(100% - var(--fold)) 0, 100% var(--fold), 100% 100%, 0 100%);
  filter:drop-shadow(0 8px 18px rgba(20,16,12,.18)) drop-shadow(0 2px 4px rgba(20,16,12,.10));
  transition:--fold .26s var(--ease), filter .26s var(--ease),
             translate .3s var(--ease), opacity .3s;
}
/* the fold: exactly the triangle the cut leaves behind, turned down onto
   the face, showing its light grey underside */
.paper-btn::after{
  content:''; position:absolute; top:0; right:0; z-index:2;
  width:var(--fold-size); height:var(--fold-size);
  background:linear-gradient(225deg, #EFECE6 0%, #D5D1C8 100%);
  clip-path:polygon(0 0, 100% 100%, 0 100%);
  transition:width .26s var(--ease), height .26s var(--ease);
}
.paper-btn:hover, .paper-btn:focus-visible{ --fold:18px; --fold-size:18px; }
/* the clip would crop an outline, so the focus ring is drawn INSIDE the
   button's own shape, where it stays fully visible */
.paper-btn:focus-visible{
  outline:none;
  box-shadow:inset 0 0 0 2px var(--clay);
}
.paper-btn:disabled{ --fold:0px; --fold-size:0px; }

/* the ambient sound control: the same card, icon only, never below the
   44x44 a visitor needs to hit it */
.paper-btn--icon{
  width:44px; height:44px; min-width:44px; min-height:44px;
  display:grid; place-items:center; padding:0;
  color:#10162B;
}
.paper-btn--icon:hover, .paper-btn--icon:focus-visible{ --fold:16px; --fold-size:16px; }

/* =============================================================
   THE CONTACT ENVELOPE
   Three states of one envelope, drawn on one canvas and aligned to the
   same width, centre and bottom edge, so the body never moves: only the
   flap opens and the paper rises. The writing is real HTML on top.
   ============================================================= */
/* The envelope has no dimming of its own: the ONE shared .focus-backdrop
   is behind it. Both this layer and the stage inside it are dismissal
   surfaces -- a click that lands on either, and not on the envelope,
   returns to the room. */
.contact{ position:fixed; inset:0; z-index:600; cursor:pointer; }
body.contact-open{ overflow:hidden; }
.contact[hidden]{ display:none; }
.contact__stage{
  position:absolute; inset:0;
  display:grid; place-items:center;
  padding:clamp(16px,3vh,40px) clamp(16px,3vw,40px);
}
/* the composition keeps the artwork's own proportions and never outgrows
   the viewport */
/* ONE soft shadow, under the paper, and nothing else. It used to be a
   drop-shadow on each of the three stacked states -- three envelope-shaped
   shadows painting at once through every cross-fade, which is the dark
   frame that appeared around the whole composition. */
.envelope::after{
  content:''; position:absolute; z-index:-1;
  left:9%; right:9%; bottom:1%; height:5.5%;
  background:radial-gradient(ellipse at 50% 50%,
             rgba(20,10,6,.34) 0%, rgba(20,10,6,.16) 45%, rgba(20,10,6,0) 72%);
  filter:blur(5px);
}
.envelope{
  position:relative; cursor:auto;
  /* Bigger than it was: the readable paper is only 40.9% of the canvas
     height, and the writing needs room to sit clear of both the top edge
     and the envelope's fold. */
  /* 80vh, not 90: the floating back control needs the same headroom above
     the envelope that it has above the board, the monitor and the notebook,
     and at 90vh there were only 45px to work with. The letter inside is
     sized in cqw, so it rescales with the paper and stays balanced. */
  width:min(820px, 92vw, calc(80vh * 1068 / 1172));
  aspect-ratio:1068 / 1172;
  container-type:size;
  opacity:0;
}
.envelope__state{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:contain;
  image-rendering:auto;
  filter:none;                      /* see .envelope::after */
}
.envelope__state--closed{ opacity:1; }
.envelope__state--rise{ opacity:0; }
/* The sealed envelope is the same registered canvas as the other two, so
   nothing moves between them; the open state is revealed upward from the
   sealed envelope's own top edge (39.85%) as it dissolves. */
.envelope__state--open{ clip-path:inset(39.85% 0 0 0); }
/* THE LETTER, AND ONLY THE LETTER.
   envelope-letter-out.png is ONE continuous sheet: the paper runs from
   4.3% of the canvas down to where the envelope's own front pocket crosses
   it, with that pocket already drawn in front. The two artworks are
   registered -- the sheet occupies x 11.1%-88.9% in both -- and in the
   open state its top edge is at 19.45%. So the whole state is laid over
   the open one and clipped at that line, and the clip opens upward to 0:
   one sheet growing out of one envelope. There is no mask: a mask here
   left everything below the join invisible, which is what put a detached
   strip of paper above a letter that was not there. */
.envelope__state--rise{
  clip-path:inset(19.45% 0 0 0);
}

/* THE READABLE PAPER, and the writing centred in it.
   Measured off envelope-letter-out.png: the sheet is full width from y 4.3%
   to y 45.2% of the canvas and spans x 11.1%-88.9%. This box IS that
   rectangle -- so "centred" here means centred in the part of the letter a
   visitor can actually read, not in the image canvas, and the space above
   the first line and below the last is equal by construction. The side
   inset is equal left and right and sits inside the paper's own edges. */
.letter{
  position:absolute;
  left:13.6%; right:13.6%;         /* 2.5% of paper margin either side */
  top:4.3%; bottom:54.8%;          /* the full-width part of the sheet */
  /* ONE LEFT EDGE for the whole card. The plane, the label, the heading,
     the note and the three ways to answer all start on the same line, the
     way a letter is actually written -- nothing has to be eyeballed into
     alignment and the block reads as one piece of writing on the paper. */
  display:flex; flex-direction:column; align-items:flex-start; text-align:left;
  /* The mark, label, heading and note sit in the upper half; the links
     group in the lower-middle, with the breathing space between them rather
     than all of it below. Equal padding top and bottom, set by the box. */
  justify-content:center;
  gap:clamp(16px, 3.6cqw, 34px);   /* the breathing space, and a bounded one */
  padding:1cqw 0;
  cursor:auto;
  opacity:0;
}
.letter__head{ display:block; }

/* THE LITTLE PLANE above the writing.
   Drawn, not imported: three strokes on the same 1.5 weight as the rest of
   the card's line work, in the portfolio's clay. It is the quietest thing
   on the paper -- no fill, no shadow -- and it steps down with the sheet. */
/* a third bigger than it was, and still the quietest thing on the paper */
.letter__mark{
  display:block; width:clamp(29px, 4.7cqw, 39px);
  margin:0 0 clamp(8px, 1.7cqw, 14px);
  color:#C4735F;
}
.letter__mark svg{ display:block; width:100%; height:auto; }

/* Say hello: Archivo, medium, restrained tracking, the portfolio's salmon.
   2.03cqw is 15px on the envelope at 1440 and 16.6px at 2048. */
/* GET IN TOUCH -- the small label above the heading */
.letter__kicker{
  margin:0 0 1.9cqw;                  /* 14px to the heading */
  font-family:var(--font-ui); font-weight:500;
  font-size:clamp(11px, 2.03cqw, 16px); letter-spacing:.14em; text-transform:uppercase;
  /* the tracking is on the right of the last letter too; the indent takes
     it back so the label is centred on the same axis as the heading */
  color:#9A4038;
}
/* Let's connect -- the heading */
.letter__title{
  margin:0 0 2cqw;                    /* 16px to the note */
  font-family:var(--font-display); font-weight:400;
  font-size:clamp(22px, 5.4cqw, 38px); line-height:1.08; color:#2A1A16;
}
/* the note: Inter, regular, 1.5 leading, a readable measure, warm charcoal */
.letter__lead{
  margin:0;
  font-family:var(--font-body); font-weight:400;
  font-size:clamp(13px, 2.1cqw, 17px); line-height:1.5; color:#3E2C25;
  max-width:52ch;
  text-wrap:balance;                  /* no one-word last line on the paper */
}

/* THE THREE WAYS TO ANSWER, ON ONE LINE.
   Mark and word travel together as one item, the gap between items is the
   same everywhere, and the row starts on the card's own left edge. It wraps
   only when the paper genuinely cannot hold three across. */
.letter__row{
  list-style:none; margin:0; padding:0;
  display:flex; flex-flow:row wrap; align-items:center;
  gap:clamp(8px, 1.4cqw, 14px) clamp(16px, 3.4cqw, 34px);
  width:100%;
  text-align:left;
}
.letter__item{ display:block; }
.letter__link{
  display:flex; align-items:center;
  gap:clamp(8px, 1.5cqw, 13px);       /* one mark-to-word distance for all three */
  min-height:clamp(24px, 3.4cqw, 30px);
  padding:0 2px;
  background:none; border:0; border-radius:2px; box-shadow:none;
  font-family:var(--font-ui); font-weight:500;
  font-size:clamp(12px, 2.17cqw, 17px); letter-spacing:.01em; color:#2A1A16;
  text-decoration:none;
  transition:color .25s;
}
/* the marks: one box, one optical weight. The LinkedIn glyph fills its
   square where the envelope and the handset do not, so it is set a step
   smaller -- the three then read as the same size on the paper. */
.letter__ico{
  flex:0 0 auto; display:grid; place-items:center;
  width:clamp(15px, 2.2cqw, 18px); height:clamp(15px, 2.2cqw, 18px);
  color:#9A4038;
  transition:color .25s;
}
.letter__ico svg{ display:block; width:100%; height:100%; fill:currentColor; }
.letter__ico[data-brand="linkedin"] svg{ width:88%; height:88%; }
/* the underline stays on the word, so the mark is never struck through */
.letter__label{
  text-decoration:underline; text-underline-offset:.3em;
  text-decoration-thickness:from-font;
  text-decoration-color:rgba(42,26,22,.4);
  transition:color .25s, text-decoration-color .25s;
}
.letter__link:hover{ color:#9A4038; }
.letter__link:hover .letter__label{ text-decoration-color:#9A4038; }
.letter__link:hover .letter__ico{ color:#C4735F; }
/* a ring for the keyboard only -- never a permanent box round one of them */
.letter__link:focus{ outline:none; }
.letter__link:focus-visible{
  outline:2px solid #9A4038; outline-offset:4px; border-radius:3px;
  color:#9A4038; }
.letter__link:focus-visible .letter__label{ text-decoration-color:#9A4038; }
/* the letter takes focus when it opens, so no link starts out ringed */
#contact-letter:focus{ outline:none; }

@media (max-width:640px){
  .envelope{ width:min(92vw, calc(80vh * 1068 / 1172)); }
  /* The paper is a good deal smaller here than the type can follow it
     down, so the writing is set at its own small-screen sizes and the box
     is held INSIDE the clear part of the sheet (which ends at 45.2% of the
     canvas): nothing is ever drawn over the envelope's front pocket, and
     if a line still will not fit, the writing scrolls on the paper rather
     than the paper being cropped. */
  /* flex-start, not centre: a centred flex column whose content is taller
     than the box pushes its first line out of the TOP, where no scroll can
     reach it. Starting at the top of the paper keeps every line reachable. */
  .letter{ top:4.3%; bottom:53%; overflow-y:auto; overscroll-behavior:contain;
           justify-content:flex-start; gap:8px; padding:0; }
  /* the plane steps down with everything else, and gives up its place
     entirely once the paper is too short to carry it */
  .letter__mark{ width:24px; margin-bottom:6px; }
  .letter__kicker{ margin-bottom:4px; font-size:11px; letter-spacing:.14em; }
  .letter__title{ margin-bottom:6px; font-size:19px; }
  .letter__lead{ margin-bottom:0; font-size:12px; line-height:1.45; }
  .letter__row{ gap:6px 14px; }
  /* a comfortable target for a thumb, without changing the type */
  .letter__link{ min-height:26px; padding:1px 2px; gap:9px; font-size:12.5px; }
  .letter__ico{ width:15px; height:15px; }
}

/* The last resort on a short window: the paper is the same shape whatever
   the screen, so when there is genuinely no room for the plane it goes
   rather than the writing being clipped. */
/* The plane goes only when the paper genuinely cannot hold it. A phone can
   now: the three ways to answer sit on one line rather than three, which
   gave the sheet back the room the mark needs. */
@media (max-height:560px){
  .letter__mark{ display:none; }
}

/* The controls that belong to a close-up. They are fixed to the viewport,
   not to the room, so the camera's scale never stretches them. */
/* ONE back control for every focused view -- My Work, the pinboard, the
   resume notebook, the envelope -- and it is the SAME component the pages
   use (.back above): a small left arrow, a short label, Archivo, and
   nothing drawn behind it. No white square, no card, no shadow. */
/* the same control, fixed to the shared safe area for a focused view */
/* The same text action, fixed to the shared safe area for a focused view.
   Over a blurred, darkened room the neutral greys would vanish, so the
   pair is inverted -- still text, still no shape behind it. */
/* A back control sits on a blurred, darkened room, so it takes the light
   end of the same neutral pair -- still text, still nothing behind it. The
   notebook's own is white outright: it sits over a bright cream page. */
.focus-back{
  --text-action:#FFFFFF;
  --text-action-hi:#FFFFFF;
  /* a soft dark halo so the label and its arrow hold up over the pale top
     of the pinboard as readily as over the dark room */
  text-shadow:0 1px 3px rgba(14,11,8,.55), 0 0 10px rgba(14,11,8,.35);
}
.focus-back svg{ filter:drop-shadow(0 1px 3px rgba(14,11,8,.6)); }
/* the chevron carries the same weight as the word beside it: 1.6 read
   lighter than 600-weight Archivo at this size */
.focus-back svg path, .page--book .back svg path{ stroke-width:1.9; }
.page--book .back{
  --text-action:#FFFFFF;
  --text-action-hi:#FFFFFF;
  text-shadow:0 1px 3px rgba(18,14,10,.55);
}
.page--book .back svg{ filter:drop-shadow(0 1px 3px rgba(18,14,10,.55)); }
/* ═══════════════════════════════════════════════════════════════
   THE BACK CONTROL'S PLACE -- one rule, all four focused views.
   Top right of the viewport, and ABOVE the thing being looked at rather
   than level with it: --subject-top is written by whichever view opened
   (the camera for the board and the monitor, the sheet for the notebook,
   the envelope for contact), so the control lifts just clear of it and
   never sits on the artwork. It floats over everything at z 700.
   ═══════════════════════════════════════════════════════════════ */
.focus-back, .page--book .back{
  position:fixed; z-index:700;
  left:var(--safe-left); right:auto;
  /* --overlay-back-top is where the MONITOR lands, published once by
     publishBackTop(). Every overlay's control uses it, so the Desktop's
     button is unmoved and the other three sit at the same coordinates. */
  top:clamp(16px,
            calc(var(--overlay-back-top, 160px) - 44px - var(--content-gap)),
            var(--safe-top));
  margin:0;
}
/* The close-up's own copies fade in with the camera -- and while they are
   not shown they take no clicks at all. An invisible control that is still
   clickable is how a click aimed at the room could land on "Read the full
   story", which is a link to About Me. */
.zoom-back, .contact-back{ opacity:0; pointer-events:none; }
.zoom-back:not([hidden]).is-in,
.contact-back:not([hidden]).is-in,
.zoom-link:not([hidden]).is-in{ opacity:1; pointer-events:auto; }

/* The pinboard's way out to the written story. Centred on THE BOARD, not
   on the window: --subject-cx and --subject-bottom are written by the
   camera when it works out where the board is going to land, so the button
   sits under the middle of the board at every viewport size. It is placed
   24px BELOW the board's own bottom edge rather than inside it: the collage
   fills 96% of the board, so a button 24px up from the inside edge would
   sit on the stickers. Step Back Outside is not on screen while a close-up
   is open, so the bottom centre line is free. */
.zoom-link{
  position:fixed; z-index:620;
  left:var(--subject-cx, 50%);
  /* directly under the board, one shared gap away, and never near the
     bottom edge: the safe area below it is the same as everywhere else */
  /* never closer than 24px to the object, whatever the gap token is on a
     small screen, and never nearer the floor than the shared safe area */
  /* placed from the board above it, and held off the bottom edge by its own
     breathing room rather than by the page gutter: the gutter is 100px on a
     wide window, and reserving all of it under this control ate into the
     one distance that has to stay fixed -- the gap between the board and
     the button, which is what makes the two read as one group. */
  top:min(calc(var(--subject-bottom, 50dvh) + max(24px, var(--content-gap))),
          calc(100dvh - max(24px, min(var(--safe-bottom), 48px)) - var(--control-size)));
  transform:translateX(-50%);
  /* THE PAPER LABEL. .paper-btn gives it the card, the cut corner and the
     drop-shadow the rest of the site's controls use; this adds only the
     size, the little crooked pin angle, and the lift on hover. The whole
     label is the target, not just the words. */
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  min-height:44px; padding:11px clamp(16px,1.4vw,22px);
  color:#10162B; text-decoration:none;
  font-family:var(--font-ui); font-weight:500;
  font-size:clamp(13px,.95vw,15px); letter-spacing:.02em; white-space:nowrap;
  cursor:pointer;
  opacity:0; pointer-events:none;
  transition:--fold .26s var(--ease), filter .26s var(--ease),
             opacity .3s var(--ease), rotate .3s var(--ease),
             translate .3s var(--ease);
}
/* ═══════════════════════════════════════════════════════════════
   THE TILTED PAPER CONTROL
   The look "Read the full story" already has, lifted into one class so
   Step Back Outside and the way back in share it exactly: the same pin
   angle, the same straighten-and-lift on hover, the same press. Only the
   words and the action differ.
   ═══════════════════════════════════════════════════════════════ */
.paper-tilt{
  rotate:-.7deg;                    /* pinned on a shade crooked, like the notes */
  transition:--fold .26s var(--ease), filter .26s var(--ease),
             opacity .3s var(--ease), rotate .3s var(--ease),
             translate .3s var(--ease);
}
.paper-tilt:hover, .paper-tilt:focus-visible{ rotate:0deg; translate:0 -3px; }
.paper-tilt:active{ rotate:-.3deg; translate:0 -1px; }
.zoom-link svg{ flex:0 0 auto; transition:transform .26s var(--ease); }
.zoom-link:hover svg{ transform:translateX(2px); }
/* the clip would crop an outline, so the paper label's focus ring is drawn
   inside its own shape (as .paper-btn does); the text back control keeps
   its own ring */
.zoom-back:focus-visible{
  color:var(--text-action-hi);
  outline:2px solid var(--text-action-hi); outline-offset:4px; border-radius:3px; }

/* =============================================================
   THE FOCUS BACKDROP
   One layer behind all four focused views. It never moves with the room
   and never takes the camera's transform: it is fixed to the viewport, so
   the room behind it is held still, blurred and dimmed while something is
   being read. A click on the backdrop ITSELF returns to the room.
   ============================================================= */
/* LEAVING, NOW. While the room is being handed back to the landing screen
   every focused layer is taken down in one frame; these transitions would
   otherwise keep it half-visible for the length of the camera move, which
   is the ghost that stayed on screen for a second or two. */
body.is-snapping .page,
body.is-snapping .page__frame,
body.is-snapping .contact,
body.is-snapping .envelope,
body.is-snapping .focus-backdrop,
body.is-snapping .room-veil,
body.is-snapping .focus-back,
body.is-snapping .zoom-link,
body.is-snapping .focus-skin,
body.is-snapping .lift,
body.is-snapping .prosc{
  transition:none !important;
  animation:none !important;
}

.focus-box-probe{
  position:fixed; left:0; top:0; z-index:-1;
  width:var(--focused-subject-width);
  height:var(--focused-subject-height);
  visibility:hidden; pointer-events:none;
}
/* the strip the button below the object lives in, measured the same way */
.focus-box-probe > i{
  display:block; height:calc(var(--control-size) + var(--content-gap));
}

.focus-backdrop{
  position:fixed; inset:0; z-index:450;
  background:rgba(18,13,9,.46);
  -webkit-backdrop-filter:blur(16px) saturate(.92);
          backdrop-filter:blur(16px) saturate(.92);
  opacity:0; cursor:pointer;
  /* stage one of the envelope's sequence: the room dims and blurs first,
     and the envelope arrives into the quiet it leaves behind */
  transition:opacity .26s var(--ease);
}
.focus-backdrop[hidden]{ display:none; }
.focus-backdrop.is-in{ opacity:1; }

/* =============================================================
   CHROME
   ============================================================= */
/* bottom RIGHT, on the shared safe area -- 100px in from both edges at
   1440, and clear of Step Back Outside, which is centred */
#sound-toggle{
  position:fixed; right:var(--room-control-right); bottom:var(--room-control-bottom);
  left:auto; z-index:400; opacity:0;
  min-width:var(--control-size); min-height:var(--control-size);
}
/* the same control on the landing screen: inside #gate, so it goes with the
   gate when the camera moves through the window. Bottom left, well clear of
   the heading, the window and the way in. */
/* THE SAME CONTROL, THE SAME PLACE. The landing screen and the room use
   one pair of values, so the button does not jump as the camera moves
   between them. */
#gate-sound{
  position:fixed; z-index:3;
  right:var(--room-control-right); bottom:var(--room-control-bottom);
  left:auto; pointer-events:auto;
  min-width:var(--control-size); min-height:var(--control-size);
}
/* both ambient controls share one icon rule */
.sound-toggle .ico-off{ display:none; }
.sound-toggle.is-muted .ico-on{ display:none; }
.sound-toggle.is-muted .ico-off{ display:block; }

/* the reverse trip out of the room -- same pill treatment as
   .gate__enter, fixed to the bottom of the viewport so it holds its
   place regardless of the room's own pan/zoom. Hidden until the room
   is live, faded in with the rest of the room's chrome. */
/* ONE LINE FOR BOTH WAYS THROUGH THE WINDOW.
   Step Back Outside (in the room) and the way back in (on the landing
   screen) are the same control in the same place: same anchor, same size,
   same padding, same type. Nothing moves when the camera crosses between
   the two screens. */
.cta-line{
  position:fixed; z-index:400; left:50%; bottom:var(--cta-bottom);
  transform:translateX(-50%);
  display:inline-flex; align-items:center; justify-content:center; gap:10px;
  min-height:var(--control-size); padding:12px 22px;
  font-family:var(--font-ui); font-weight:500;
  font-size:clamp(12px,.9vw,14px); letter-spacing:.02em;
}
.room-exit{ opacity:0; }
/* The landing screen's control drifts with the heading above it: both read
   the same --pan-x/--pan-y that captionPan() writes on #gate, so they move
   as one group. Its resting place is still the shared .cta-line. */
#gate-enter.cta-line{
  transform:translate(calc(-50% + var(--pan-x, 0px)), var(--pan-y, 0px));
}
.room-exit svg{ flex:0 0 auto; }

.room-exit:hover{ translate:0 -2px; }

#rotate-hint{
  position:fixed; left:50%; top:20%; transform:translateX(-50%);
  z-index:410; margin:0; display:none;
  font-size:12px; letter-spacing:.06em; color:rgba(244,241,236,.5);
  text-align:center; padding:0 24px; pointer-events:none;
}

/* =============================================================
   SMALL SCREENS
   ============================================================= */
/* tablet: the More projects grid drops to two across */
@media (max-width:1100px){
  .more__grid{ grid-template-columns:repeat(2, minmax(0,1fr)); }
}
@media (max-width:900px){
  .portrait, .intro-card, .tile-labels{ display:none; }
  .page.page--project .page__title{ font-size:clamp(34px,8.2vw,50px); }
}
/* phone: one card per row */
@media (max-width:640px){
  .more__grid{ grid-template-columns:minmax(0,1fr); }
  .page__title{ max-width:none; }
}
@media (max-width:900px) and (orientation:portrait){
  #rotate-hint{ display:block; }
}

@media (prefers-reduced-motion:reduce){
  *{ animation-duration:.01ms !important; animation-iteration-count:1 !important; }
}
