/* maioka.co — the editorial page surface
   ---------------------------------------------------------------------------
   Shared by the 22 project pages, /about and /contact. Reference 2 is the
   governing image: off-white paper on the left, a full-bleed photograph on the
   right, a hard vertical split with no blending and no frame, and rotated
   vertical type used as texture. (Its crossing hairline was removed by Ken on
   2026-09-19 — DESIGN_DIRECTION.md, the amendment. Do not reintroduce it.)

   Everything is built from tokens.css. The only invented values are component
   metrics scoped to .plate and .shot (the paper-column width, the figure size
   caps) — layout, not design system, declared at the top of their component.

   Contents:
     1. plate — the hard split
     2. the vertical rail
     3. title, page copy
     3b. hero modes — Phase 3; a portrait film's hero fills its panel
     4b. tabs — Phase 3: Levi's campaigns
     4. the media stack — one thing per screen
     5. project nav — prev / next / back to the ring
     6. contact + about specifics
     7. enhancement states — pre-animation, reduced motion, failed motion
     8. narrow widths
   ========================================================================= */

@import url("chrome.css");


/* =========================================================================
   1. THE PLATE — reference 2's hard vertical split.
   Paper column left, media running to the top, right and bottom edges with
   no margin and no frame. The two never blend; the join is a butt edge.
   ========================================================================= */

.plate {
  /* ---- component metrics ---- */
  --paper-col: clamp(320px, 42vw, 620px);

  position: relative;
  display: grid;
  grid-template-columns: var(--paper-col) 1fr;
  min-height: 100vh;
  min-height: 100dvh;
  /* Nothing in the plate is meant to exceed it — the media panel clips
     itself — so this only stops a stray overflow from widening the page. The
     plate grows with its content, so nothing can be cut off. */
  overflow: hidden;
}

.plate-paper {
  padding: var(--margin);
  display: grid;
  /* head / rail / the calm empty middle / the title block */
  grid-template-rows: auto auto 1fr auto;
  gap: var(--s-3);
  align-content: start;
  min-width: 0;
}
.plate-paper > .site-head { grid-row: 1; }

.plate-media {
  position: relative;
  overflow: hidden;
  background: var(--void);
  min-width: 0;
}
/* The entrance scales this wrapper, never the panel itself — the slate
   caption is a sibling and must not ride the scale. */
.plate-media-inner { position: absolute; inset: 0; }
.plate-media img,
.plate-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  max-width: none;
}
/* Phase B1: stills are a <picture> wrapping the WebP <source>/<img> — see
   tools/build-work.js pictureSrc(). <picture> is inline by default, which
   would (a) leave transform inert on it in .shot-frame's reveal animation
   below and (b) is needless ambiguity here, so it gets a plain box exactly
   like the <img> it replaced. */
.plate-media picture,
.shot-frame picture { display: block; width: 100%; height: 100%; }
/* ---- video -------------------------------------------------------------
   A paused <video controls> draws a black transport bar across the bottom of
   whatever it is showing. Over a full-bleed hero that bar was permanent under
   reduced motion and with JS off, which is exactly where the page is meant to
   be at its calmest.

   The controls stay in the markup — they are what makes the video playable
   with JS off, from the keyboard and with a screen reader, and an unusable
   video is a worse trade than an ugly bar. They are simply held at zero
   opacity until the pointer or the keyboard asks for them. They still take
   clicks while invisible, so a tap on the picture still starts it, and the
   poster carries the frame in the meantime. Chrome's own centre button is
   dropped in favour of one affordance in the site's own language, below. */
.plate-media video { background: var(--void); }

.film { position: relative; display: block; width: 100%; height: 100%; }

/* Phase 3: a stack film's poster is a lazy <img> UNDER the video (see
   mediaTag() in tools/build-work.js), so a film far down the page, or in a
   hidden tab, fetches nothing until it is near. The <video> has no poster of
   its own and paints nothing until its first frame, so the image shows
   through; from the first frame on, the film covers it. The frame is the
   media's own aspect, so cover here crops nothing. */
.film-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.film > video { position: relative; }

.film video::-webkit-media-controls-panel {
  opacity: 0;
  transition: opacity var(--d-fast) var(--e-out);
}
.film:hover video::-webkit-media-controls-panel,
.film:focus-within video::-webkit-media-controls-panel { opacity: 1; }
.film video::-webkit-media-controls-overlay-play-button { display: none; }

/* The affordance: a mono label and a small solid triangle, set like the slate
   on the opposite corner. Shown while the film is not running — which is
   every state that has no JS to start it, as well as reduced motion, where
   nothing autoplays on purpose. It never takes a click of its own: the video
   underneath is the control, and adding a second one would add a tab stop
   for something the transport already does. */
.film-cue {
  position: absolute;
  left: var(--s-2);
  bottom: var(--s-2);
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--s-1);
  color: var(--ink-invert);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--d-base) var(--e-out);
}
.plate-media .film-cue { left: var(--margin); bottom: var(--margin); }
.film-cue-mark {
  width: 0;
  height: 0;
  border-left: 0.62em solid currentColor;
  border-top: 0.36em solid transparent;
  border-bottom: 0.36em solid transparent;
}
.film.is-idle .film-cue,
html:not(.has-js) .film-cue,
html.motion-failed .film-cue { opacity: 1; }

/* A frame counter on the media, bottom-right, like a slate. */
.plate-slate {
  position: absolute;
  right: var(--margin);
  bottom: var(--margin);
  z-index: 2;
  color: var(--ink-invert-soft);
  font-variant-numeric: tabular-nums;
}


/* =========================================================================
   2. THE VERTICAL RAIL
   Client, year and roles, rotated bottom-to-top. Texture and signature, never
   wayfinding — nothing here is something the visitor must read quickly, and
   every one of these facts is also stated in plain horizontal type elsewhere
   on the page (the slate, the caption, the summary).
   ========================================================================= */

.rail {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  /* Underscore joiners are the file-naming device from reference 2. */
}
.rail-item { margin: 0; color: var(--ink-soft); }
.rail-item--lead { color: var(--ink); }


/* =========================================================================
   3. TITLE, SUMMARY, COPY
   ========================================================================= */

.plate-titleblock {
  grid-row: 4;
  display: grid;
  gap: var(--s-2);
  min-width: 0;
}

.plate-title {
  font-size: var(--t-title);
  font-weight: 500;
  line-height: var(--lh-mega);
  letter-spacing: var(--tr-mega);
  /* mono titles are wide; a long client name must break rather than overflow */
  overflow-wrap: break-word;
  text-wrap: balance;
}

/* The one-paragraph copy of contact and 404. Project pages have none since
   Phase 3 (no descriptions: title, client, year and roles only). */
.plate-summary {
  font-size: var(--t-body);
  line-height: var(--lh-body);
  max-width: 44ch;
}
/* About and contact carry more than one paragraph. */
.plate-copy { display: grid; gap: var(--s-2); max-width: 46ch; }
.plate-copy p { font-size: var(--t-body); line-height: var(--lh-body); }

.plate-index {
  display: flex;
  gap: var(--s-2);
  flex-wrap: wrap;
  align-items: baseline;
}


/* =========================================================================
   3b. HERO MODES (Phase 3, PHASE3_PLAN.md §3) — by the hero FILM's aspect,
   written by tools/build-work.js as .plate[data-hero] with --hero-ar
   ("w / h") and --hero-arn (w/h as a number). A photograph hero has no
   data-hero and keeps the split exactly as before.
     tall  a portrait film in the split, FILLING the right panel (Ken,
           2026-10-02: "it to fill the space just as a design element"): no
           bars. object-fit: cover, positioned on --hero-pos, the hero's
           heroFocus (a face-aware point from detect-focus; "x% y%" of the
           film, so the subject stays in the panel at every panel shape). The
           whole, uncropped film is the first item of the stack below
           (build-work.js hasTwin(), page.js pauses whichever copy is not the
           one being watched). Under 720px the box is the film's own aspect,
           so cover is the whole frame (cropped on the focus only if the
           screen is shorter than the film).
     wide  a landscape film takes the whole width at its own aspect, at most
           one screen tall and contained, under a compact header row.
     cover S4, Ken's extra option for beats ("heroMode": "split-cover" in
           projects.json): a landscape film in Genesis's split plate, CROPPED
           to fill the right panel, centred on its subject (--hero-pos, from
           the project's heroFocus). Under 720px it is like every film hero:
           its own aspect, full width, never cropped.
   ========================================================================= */

.plate[data-hero="wide"] .plate-media video { object-fit: contain; }
.plate[data-hero="tall"] .plate-media video,
.plate[data-hero="cover"] .plate-media video {
  object-fit: cover;
  object-position: var(--hero-pos, 50% 50%);
}

.plate[data-hero="wide"] {
  grid-template-columns: 1fr;
  grid-template-rows: auto auto;
  min-height: 0;
  container-type: inline-size;
}
/* The header row: the chrome across the top, then the title block on the
   left and the rail on the right, set flat — the rail's facts (client,
   year, roles) on one line of micro type, the file-name texture without the
   height a rotated column would add. */
.plate[data-hero="wide"] .plate-paper {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: end;
  column-gap: var(--s-3);
  padding-bottom: var(--s-3);
}
.plate[data-hero="wide"] .plate-paper > .site-head { grid-column: 1 / -1; grid-row: 1; }
.plate[data-hero="wide"] .plate-paper > .plate-titleblock { grid-column: 1; grid-row: 2; }
.plate[data-hero="wide"] .plate-paper > .rail {
  grid-column: 2;
  grid-row: 2;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s-1);
  text-align: right;
}
.plate[data-hero="wide"] .rail .t-vertical { writing-mode: horizontal-tb; transform: none; }
.plate[data-hero="wide"] .plate-media {
  width: 100%;
  height: min(calc(100vw / var(--hero-arn)), 100vh);
  height: min(calc(100cqw / var(--hero-arn)), 100vh);
  height: min(calc(100cqw / var(--hero-arn)), 100dvh);
}


/* =========================================================================
   4. THE MEDIA STACK
   One thing per screen. A block of six that staggers in together is a web-app
   pattern; one image, alone, arriving, is editorial. Each shot owns most of a
   viewport and leans toward one side, alternating.
   AMENDED 2026-09-23 (Ken): the shots were a touch too big and sat hard on
   the window edge. They are ~15% smaller and held off the edge by
   --shot-inset, so they lean rather than touch. The plate above keeps its
   hard full-bleed edge; only the stack moved in.
   ========================================================================= */

.stack { display: block; }

.shot {
  /* ---- component metrics ---- */
  --shot-cap: min(52vw, 760px);   /* never wider than this */
  --shot-inset: clamp(var(--margin), 9vw, 160px);  /* gap to the edge it leans on */
  --shot-h: 76vh;                 /* nor taller — the height cap does the work
                                     for portrait media, the width cap for
                                     landscape, and --arn picks the binding one */

  min-height: 88vh;
  display: grid;
  align-content: center;
  padding-block: var(--s-4);
}
.shot[data-side="right"] { justify-items: end;   padding-left: var(--margin);  padding-right: var(--shot-inset); }
.shot[data-side="left"]  { justify-items: start; padding-right: var(--margin); padding-left: var(--shot-inset); }

.shot-figure {
  position: relative;
  display: grid;
  gap: var(--s-1);
  /* --arn is the media's width/height as a bare number, written per item by
     tools/build-work.js from the real pixel dimensions in projects.json. One
     rule therefore sizes portrait and landscape correctly without branching. */
  width: min(var(--shot-cap), calc(var(--shot-h) * var(--arn, 1)), 100%);
  max-width: 100%;
}
.shot-frame {
  position: relative;
  overflow: hidden;
  background: var(--paper-deep);
  aspect-ratio: var(--ar, 3 / 2);
}
.shot-frame img,
.shot-frame video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The caption stands in the empty half, rotated, hard against the frame — the
   same device as the plate's rail, and it is what gives the quiet side of each
   spread something to be. It carries a frame number and "still" / "film", both
   of which are texture: nothing on the page depends on reading it. */
.shot-caption {
  position: absolute;
  top: 0;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  display: flex;
  gap: var(--s-2);
  white-space: nowrap;
}
.shot[data-side="right"] .shot-caption { right: 100%; margin-right: var(--s-2); }
.shot[data-side="left"]  .shot-caption { left: 100%;  margin-left: var(--s-2); }

/* One hairline opens the stack. Reference 2 has exactly one rule; repeating it
   under every shot would turn a device into a pattern. */
.stack-open { margin-inline: var(--margin); }


/* =========================================================================
   4b. TABS (Phase 3: Levi's, one campaign per tab — PHASE3_PLAN.md §3)
   S4, Ken: "the tab is what has the wrong font". The tabs were .t-label
   (uppercase, weight 500, --tr-label tracking), which read as another face
   beside the nav. They are now set EXACTLY like the top navigation's links
   (chrome.css .site-nav-list): lowercase as written (Ken's voice), --t-label
   size, 0.04em tracking, weight 400, ink; the selected tab carries the nav's
   current-page underline (a hairline in --ink, 0.2em below). The no-JS
   campaign headings are set the same way. Nothing moves: a tab switch has no
   transition. With JS off the row is a set of jump links and every campaign
   is stacked under its own heading; page.js adds the tab roles, .is-tabbed and
   `hidden` on the inactive panels.
   ========================================================================= */

.tabs-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-2);
  padding: var(--s-3) var(--margin) 0;
}
.tab,
.tab-heading {
  font-size: var(--t-label);
  font-weight: 400;
  letter-spacing: 0.04em;
  line-height: 1.1;
  text-transform: none;
  color: var(--ink);
}
.tab {
  border-bottom-color: transparent;
  padding-bottom: 0.2em;
}
.tab:hover,
.tab:focus-visible,
.tab[aria-selected="true"] { border-bottom-color: var(--ink); }

.tab-panel[hidden] { display: none !important; }
/* A deep link (#super-bowl) lands with the tab row still in view. */
.tab-panel { scroll-margin-top: 5rem; }
.tab-heading {
  margin: 0;
  padding: var(--s-4) var(--margin) 0;
}
.tabs.is-tabbed .tab-heading { display: none; }


/* =========================================================================
   5. PROJECT NAV
   Prev / next wrap around the 15, and there is always a way back to the ring.
   ========================================================================= */

.project-nav {
  padding-inline: var(--margin);
  padding-block: var(--s-4) var(--s-3);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: var(--s-2) var(--s-3);
  align-items: start;
}
.project-nav-link { display: grid; gap: var(--s-1); border-bottom: 0; max-width: 22ch; }
.project-nav-link .t-label { color: var(--ink-soft); transition: color var(--d-fast) var(--e-out); }
.project-nav-link:hover .t-label,
.project-nav-link:focus-visible .t-label { color: var(--ink); }
.project-nav-title {
  font-size: var(--t-lead);
  line-height: 1.2;
  letter-spacing: -0.02em;
  overflow-wrap: break-word;
}
.project-nav-next { justify-self: end; text-align: right; }
.project-nav-all { justify-self: center; align-self: center; }


/* =========================================================================
   6. CONTACT + ABOUT
   ========================================================================= */


/* The full-bleed frame is cropped to fill a tall column, and object-fit
   centres by default — which on a narrow media column cuts the handwriting
   out of the picture. Held on the annotation instead, so the thing the page
   is about survives every width. */
.about-photo { object-position: 44% 38%; }

/* The address block is the page's only call to action, so it is given the lead
   step rather than body size. No button, no form — see contact.html.
   It is NOT --t-statement: at that size a mono email address does not fit the
   paper column and wraps to "reachmaioka@gmail." / "com", and an address broken
   mid-token reads as a bug however big it is. The <wbr> in the markup gives it
   one honest break point, before the @, for the narrowest widths. */
.contact-lines { display: grid; gap: var(--s-2); }
.contact-line {
  font-size: var(--t-lead);
  line-height: 1.25;
  letter-spacing: -0.02em;
  overflow-wrap: break-word;
}
.contact-line a { border-bottom-color: var(--rule); }
.contact-note { color: var(--ink-soft); }


/* =========================================================================
   7. ENHANCEMENT STATES
   Nothing here hides content unless html.has-js is present, and every hiding
   rule is cancelled by html.motion-failed and by reduced motion.
   MOTION_RESEARCH.md §4 — same contract as the home page.
   ========================================================================= */

/* Every pre-animation state lives inside no-preference, so under reduced
   motion it does not exist at all rather than being cancelled by a second
   rule that then has to win a specificity argument. .reduced (set by the
   inline head script) and .motion-failed are the second and third belts. */
@media (prefers-reduced-motion: no-preference) {

  html.has-js:not(.reduced):not(.motion-failed) [data-enter-media] {
    opacity: 0;
    transform: scale(1.05);
  }
  html.has-js:not(.reduced):not(.motion-failed) [data-enter] {
    opacity: 0;
    transform: translateY(14px);
  }

  /* The reveal pre-state is keyed on [data-reveal] and the OBSERVED element is
     .shot, which is never hidden and never clipped — INCIDENTS.md 2026-09-18:
     a clipped or zero-area element reports intersectionRatio 0 for ever and
     the reveal silently never fires. Settling a reveal is therefore just
     removeAttribute('data-reveal'), which is what the sweep guard does. */
  html.has-js:not(.reduced):not(.motion-failed) .shot[data-reveal] .shot-figure {
    opacity: 0;
    transform: translateY(26px);
  }
  html.has-js:not(.reduced):not(.motion-failed) .shot[data-reveal] .shot-frame > * {
    transform: scale(1.04);
  }
}


/* =========================================================================
   8. NARROW WIDTHS
   The split is the whole idea, so it is kept as long as there is room for a
   readable paper column beside a photograph — below that the two halves
   stack, media first, and the photograph still runs edge to edge.
   ========================================================================= */

@media (max-width: 900px) {
  .plate { --paper-col: clamp(300px, 40vw, 420px); }
  .shot { --shot-cap: min(64vw, 620px); }
}

@media (max-width: 720px) {
  .plate {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
    min-height: 0;
  }
  /* media first: the photograph is the reason to stay on the page */
  .plate-media { grid-row: 1; min-height: 58vh; }
  /* A film hero, any mode, takes its own aspect at full width, at most one
     screen tall. A portrait film fills that box (cover), which is the whole
     frame unless the screen is shorter than the film (a phone on its side),
     where it is cropped on --hero-pos rather than barred. */
  .plate[data-hero] .plate-media {
    min-height: 0;
    width: 100%;
    height: auto;
    aspect-ratio: var(--hero-ar);
    max-height: 100vh;
    max-height: 100dvh;
  }
  .plate[data-hero="cover"] .plate-media video { object-fit: contain; }
  /* The wide mode's header row returns to the phone layout below. */
  .plate[data-hero="wide"] .plate-paper {
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto 1fr;
    padding-bottom: var(--s-4);
    column-gap: var(--s-2);
    align-items: stretch;
  }
  .plate[data-hero="wide"] .plate-paper > .rail {
    grid-column: 1;
    grid-row: 2;
    flex-direction: row;
    align-items: flex-start;
    text-align: left;
    gap: var(--s-1);
  }
  .plate[data-hero="wide"] .plate-paper > .plate-titleblock { grid-column: 2; grid-row: 2; }
  .plate[data-hero="wide"] .rail .t-vertical { writing-mode: vertical-rl; transform: rotate(180deg); }
  /* The rail moves alongside the title rather than above it. Stacked, a
     rotated roles list is 200px of empty column between the photograph and
     the title; beside it, it is exactly reference 2's margin note. */
  .plate-paper {
    grid-row: 3;
    padding-block: var(--s-3) var(--s-4);
    grid-template-columns: auto 1fr;
    grid-template-rows: auto 1fr;
    column-gap: var(--s-2);
    row-gap: var(--s-3);
  }
  .plate-paper { grid-template-rows: auto auto 1fr; }
  .plate-paper > .site-head { grid-column: 1 / -1; grid-row: 1; }
  .plate-paper > .rail { grid-column: 1; grid-row: 2; }
  .plate-paper > .plate-titleblock { grid-column: 2; grid-row: 2; min-width: 0; }

  .plate-slate { right: var(--gutter); bottom: var(--gutter); }

  .shot {
    --shot-cap: 100%;
    --shot-h: 62vh;
    min-height: 0;
    padding-block: var(--s-3);
  }
  /* Full width now, so lean on neither side: an even margin both ways. The
     [data-side] keeps this above the desktop side rules in specificity. */
  .shot[data-side] { padding-inline: var(--margin); justify-items: center; }
  /* The figure now fills the width, so there is no empty half to stand the
     caption in — and an absolutely positioned one would sit off the page.
     It returns to the flow, horizontal, under the frame. */
  .shot-caption {
    position: static;
    writing-mode: horizontal-tb;
    transform: none;
    margin: 0;
  }
  .shot[data-side="right"] .shot-caption { justify-content: flex-end; }

  .project-nav {
    grid-template-columns: 1fr;
    padding-block: var(--s-3);
  }
  .project-nav-next { justify-self: start; text-align: left; }
  .project-nav-all { justify-self: start; }
  .project-nav-link { max-width: none; }
}

@media (max-width: 640px) {
  /* The rail stays vertical at this width — it is the page's signature, it is
     three short strings, and beside the title it costs no vertical space that
     the title was not already using. It is never clipped: the roles are stated
     nowhere else on the page, so cutting them would lose information rather
     than hide decoration. */
  .rail { gap: var(--s-1); }
}
