/* IZUNAWORKS v2 — home page.
 *
 * Built from Figma "IZUNAWORKS" file YaETz3cPISDScUsya8AJEz, section
 * "01 · Main — Desktop" (node 33:54 / frame 7:2). Every length here is the
 * design's own value at its native 1920 width; the fractional type sizes
 * (21.306, 35.934, 76.894, 41.537, 22.062, 14.247) are Figma's, kept rather
 * than rounded so the page measures against the artboard.
 *
 * The design is desktop-only. Below 1920 the shell paddings and the giant
 * headline scale down, and the works grid drops to two columns then one —
 * the type otherwise keeps its designed size, matching how the rest of this
 * project treats an artboard (shrink the canvas, never the type).
 *
 * Fonts are self-hosted on purpose: v2 is owner-eyes-only behind the edge
 * gate, so nothing may be fetched from a third party at render time. Satoshi
 * and Zen Kaku Gothic New are licensed for THIS site only. That licence was taken
 * out for the development site; the site has since moved to izunaworks.com, so
 * confirm it covers that domain, and never ship either face anywhere else.
 */

/* ---------- fonts ---------- */

/* font-display is `block`, never `swap`, and that is the whole fix for the
 * face-change-on-tab-return the owner reported (2026-09-21): leave the tab,
 * come back, and every English run painted in a system gothic for a moment
 * before snapping back to Satoshi.
 *
 * Nothing on the page causes it — there is no visibilitychange handler and no
 * re-render. It is the browser: a backgrounded tab gets its decoded font data
 * purged, so returning to it re-resolves every face while the page repaints.
 * Under `swap` the only thing a re-resolution can possibly do is paint the
 * fallback first, so the flash was guaranteed rather than unlucky. Under
 * `block` the text holds blank for those few milliseconds instead — the face
 * is already in the HTTP cache, so it is a decode, not a download — and the
 * wrong typeface is never drawn. The gate's error screens in
 * functions/_middleware.js made the same choice for the same reason.
 *
 * Two things hold the block period down to that, and both must stay:
 *   * every face a page paints is preloaded in its <head>, so the fetch starts
 *     at parse time rather than when js/site.js inserts the text — which is
 *     after /api/content answers. The Zen Kaku faces are ~1 MB each and carry
 *     no unicode-range, so late is very late for them.
 *   * /assets/fonts/* is served immutable for a year. functions/_middleware.js
 *     has to FORCE that header, because Pages sets its own on assets first.
 * Drop either and `block` turns a flash of the wrong font into a flash of no
 * font, which is worse. Change them together or not at all. */

@font-face {
  font-family: 'Satoshi';
  src: url('/assets/fonts/Satoshi-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: 'Satoshi';
  src: url('/assets/fonts/Satoshi-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: 'Satoshi';
  src: url('/assets/fonts/Satoshi-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: 'Zen Kaku Gothic New';
  src: url('/assets/fonts/ZenKakuGothicNew-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: 'Zen Kaku Gothic New';
  src: url('/assets/fonts/ZenKakuGothicNew-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: block;
}

/* ---------- tokens ---------- */
/* Names and values are the Figma variables verbatim. */

:root {
  /* This design is light-only — there is no prefers-color-scheme rule anywhere
     here and every token below is a fixed Figma value — so say so, rather than
     leaving it at `normal` and letting the browser guess. Left unsaid, a
     visitor whose OS is in dark mode gets dark browser UI against a #fafafa
     page: the scrollbar renders #2c2c2c, and the full-bleed banner running
     flush into that black slab reads as the video continuing underneath it
     (owner, 2026-09-21: 動画はみ出してる). Declaring light makes the scrollbar
     light to match the page, and keeps native form controls on /contact from
     coming up dark too. It changes no colour the site itself draws. */
  color-scheme: light;

  --ink: #000000;
  --background: #fafafa;
  --surface: #ffffff;
  --red: #ff3838;
  --status-few: #ff8800;
  --status-open: #009dff;

  --placeholder-box: #dddddd;
  --placeholder-ink: #999999;

  /* The header's divider. Solid --ink at 0.5px still resolved to a hard black
     hairline on most displays and read as a bold bar rather than a rule
     (owner, 2026-09-22). Half-transparent black over --background lands on a
     soft grey that divides without drawing the eye, and stays correct if the
     header ever sits on --surface. */
  --hairline: rgba(0, 0, 0, 0.12);

  /* Satoshi carries no CJK, so Zen Kaku Gothic New sits behind it in the Latin
     stack too: a mixed run then sets its Latin in Satoshi and its Japanese in
     Zen Kaku, instead of dropping to whatever gothic the OS happens to have.
     A rule that overrides font-family must keep both, in this order. */
  --en: 'Satoshi', 'Zen Kaku Gothic New', system-ui, sans-serif;
  --jp: 'Zen Kaku Gothic New', 'Satoshi', system-ui, sans-serif;

  /* Shell gutters. 200 for header/footer, 100 for sections, per the design. */
  --shell-pad: 200px;
  --sec-pad: 100px;
}

@media (max-width: 1600px) {
  :root { --shell-pad: 120px; --sec-pad: 72px; }
}
@media (max-width: 1100px) {
  :root { --shell-pad: 56px; --sec-pad: 48px; }
}
@media (max-width: 680px) {
  :root { --shell-pad: 24px; --sec-pad: 24px; }
}

/* ---------- reset ---------- */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--background);
  color: var(--ink);
  font-family: var(--jp);
  /* Japanese line breaking. `line-break: strict` is 禁則処理 and nothing more:
     it keeps 。」 off the start of a line. It does NOT know where words are —
     CSS ships no Japanese dictionary, so by default every character is a legal
     break and the browser splits 納期 as 納 / 期 and 入ります as 入りま / す.
     That is the "random" bad break: it only shows when a word lands on the line
     edge, so it moves as the text or the window changes.

     `auto-phrase` hands the browser its built-in phrase model and breaks at
     文節 boundaries instead. It is Chrome/Edge 119+; Safari and Firefox drop
     the declaration and fall back to the `word-break: normal` above, which is
     exactly what this rule did before — so it cannot regress anywhere.

     The design still never wraps a Japanese run mid-bracket. */
  word-break: normal;
  word-break: auto-phrase;
  overflow-wrap: anywhere;
  line-break: strict;
}

img { display: block; max-width: 100%; }

a { color: inherit; text-decoration: none; }

/* Headings keep the browser's margins otherwise, and the artboard has none:
   they added 60px to every section heading and 128px to the CTA. */
h1, h2, h3, p { margin: 0; }

/* Focus is visible everywhere — the design has no focus state of its own, so
   this is the browser default made consistent rather than a new one. */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--red);
  outline-offset: 3px;
}

/* ---------- selection and dragging ---------- */

/* Chrome is not copy. Everything a visitor reads for its CONTENT stays
   selectable — the pitch, the bio, the legal sections, a work's meta values,
   the contact addresses — and the furniture around it does not. Highlighting a
   nav item, the word inside a button or a section heading happens by accident,
   on a double click or a drag that runs past the end of a paragraph, and it
   leaves the page looking broken (owner, 2026-09-21).

   The list below is the chrome and only the chrome. Adding a selector to it is
   asserting that nobody will ever want that text on their clipboard; if that is
   not obviously true, leave it out. Route-page chrome is listed in the same way
   at the foot of css/routes.css — the two lists are one rule split by file.

   Images also get -webkit-user-drag: none, or a slow press on the logo, a
   social glyph or a works thumbnail starts a ghost-image drag. Firefox
   implements neither that property nor -moz-user-drag, so js/site.js cancels
   dragstart on images as well; the two together cover every browser. Text drags
   need no equivalent — text that cannot be selected cannot be dragged. */

.hdr,
.ftr,
.sec__heading,
.more,
.btn,
.card,
.news__item,
.news__empty,
.cta__headline,
.pitch__headline,
.status__title,
.slot__label,
.slot__value,
.notice__label,
.empty-note {
  -webkit-user-select: none;
  user-select: none;
}

img {
  -webkit-user-drag: none;
  user-select: none;
}

/* The highlight is the site's red, paired with --surface exactly as .btn and
   the footer pair them — the browser's blue is the one colour on the page that
   belongs to no one. ::selection takes only a handful of properties (colour,
   background, text-shadow), so the literal token is spelled out rather than
   inherited. Both spellings are needed: Firefox still answers only to
   ::-moz-selection, and a selector list containing an unknown pseudo-element
   is dropped whole, so the two rules cannot be combined. */
::selection {
  background: var(--red);
  color: var(--surface);
}
::-moz-selection {
  background: var(--red);
  color: var(--surface);
}

/* ---------- header ---------- */

.hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  height: 80px;
  padding: 0 var(--shell-pad);
  background: var(--background);
  /* Bottom edge only. This was `border: 0.5px solid var(--ink)`, a full box —
     which also painted an 80px hairline hard against the scrollbar at the right
     edge, another at the left, and one along the very top of the viewport. The
     right-hand one was reported as a black vertical line beside the scrollbar
     (owner, 2026-09-21); the other two were the same artefact, just somewhere
     less noticeable. Only the rule under the header was ever the design.

     1px of --hairline, not 0.5px of --ink: a sub-pixel border is rounded up to
     a full black line on a 1x display anyway, so the thinness bought nothing
     and the colour was doing all the damage. */
  border-bottom: 1px solid var(--hairline);
}

.hdr__logo { width: 118px; height: 35px; object-fit: cover; flex: none; }

.hdr__nav {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 30px;
}

.hdr__link {
  font-family: var(--en);
  font-weight: 700;
  font-size: 21.306px;
  line-height: normal;
  white-space: nowrap;
  color: var(--ink);
}

/* The colour change is the fallback. js/site.js rebuilds each link as a
   roll-over (.hdr__link--roll, in the motion section at the foot of this
   file), where the red arrives as a second copy of the word instead. */
.hdr__link:hover { color: var(--red); }

/* The slash between nav items — a 19.065 x 22.451 stroked line in the design. */
.hdr__sep {
  width: 18px;
  height: 22px;
  flex: none;
}

@media (max-width: 900px) {
  .hdr__link { font-size: 16px; }
  .hdr__sep { width: 13px; height: 16px; }
}
@media (max-width: 620px) {
  .hdr { height: auto; padding-block: 14px; flex-direction: column; align-items: flex-start; gap: 12px; }
  .hdr__nav { height: auto; flex-wrap: wrap; }
}

/* ---------- hero ---------- */

/* The header's rule divides it from the page beneath it on every other page,
   but on the home page that page IS the banner, and a black line ruled across
   the video is not a divider (owner, 2026-09-21) — so it is dropped here and
   only here. box-sizing is border-box, so the header keeps its 80px and
   nothing below it moves. Scoped to the page rather than the hero because the
   border belongs to .hdr: the hero cannot erase its neighbour's edge from
   inside. With this off, the home page's header carries no border at all. */
body[data-page="home"] .hdr { border-bottom: 0; }

/* The band's own grey stays as the backdrop: it is what shows for the instant
   before the poster decodes, and behind a video whose aspect no longer matches
   the band. container-type is gone with the placeholder — it existed only to
   give that plate's cqw font size something to resolve against. */
.hero {
  height: 500px;
  overflow: hidden;
  background: var(--placeholder-box);
}

/* The banner video and an /admin hero image are interchangeable here: both fill
   the 1920×500 band and both crop rather than letterbox, so the band's height is
   the only thing that decides what is visible. The artboard's grey "1920 × 500"
   plate is gone with them — index.html always ships the video, so there is no
   longer an empty state for it to stand in for. */
.hero__img,
.hero__video { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Above the artboard's 1920 the band keeps its fixed 500px while the banner is
   magnified to fill it, which cuts ~19% off the top and bottom — at 2384 the
   video renders at 123% and loses 117px (owner, 2026-09-21: it looked "out").
   Letting the height follow the banner's own 1920:500 instead keeps the whole
   frame visible AND still full-bleed, which neither cropping nor letterboxing
   can do at once. At exactly 1920 the two agree — 1920 / 3.84 = 500 — so this
   is continuous, not a jump.

   min-height keeps the artboard's 500 as the floor: a media query measures the
   viewport but aspect-ratio resolves against the CONTENT width, so at a 1920
   viewport the scrollbar leaves 1905 and the ratio alone would give 496. The
   floor holds the band at its designed height there and hands those 15px back
   to object-fit: cover, exactly as before. */
@media (min-width: 1920px) {
  .hero {
    height: auto;
    aspect-ratio: 1920 / 500;
    min-height: 500px;
  }
}

@media (max-width: 1100px) {
  .hero { height: 340px; }
}
@media (max-width: 680px) {
  .hero { height: 220px; }
}

/* ---------- section scaffolding ---------- */

.sec {
  display: flex;
  flex-direction: column;
  gap: 47px;
  padding-inline: var(--sec-pad);
}

.sec--works { padding-top: 45px; }
.sec--news { padding-top: 62px; }

.sec__heading { display: flex; flex-direction: column; align-items: flex-start; }

.sec__title {
  font-family: var(--en);
  font-weight: 700;
  font-size: 35.934px;
  line-height: normal;
  color: var(--red);
  white-space: nowrap;
}

/* The 413.245px rule under every section title.

   Red, not ink. The artboard strokes this line #ff3838 — the same value as the
   title above it — on both home sections that carry one: "RECENT WORKS"
   (node 9:2, Line 4) and "ANNOUNCEMENT" (node 10:7, Line 5). It was left on
   var(--ink) here, which put a black rule under a red heading on the home page
   while routes.css's .page__rule drew the identical component red on every
   content page (owner, 2026-09-21: the two did not match). Thickness, width and
   margins are untouched. */
.sec__rule {
  width: 413.245px;
  max-width: 100%;
  height: 0;
  border: 0;
  border-top: 1.38px solid var(--red);
  margin: 0;
}

.sec__subtitle {
  padding-top: 14px;
  font-family: var(--jp);
  font-weight: 400;
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink);
}

@media (max-width: 680px) {
  .sec__title { font-size: 26px; }
}

/* ---------- "more" links ---------- */

.more {
  display: inline-flex;
  align-items: center;
  align-self: flex-end;
  font-family: var(--jp);
  font-weight: 700;
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink);
}

.more__icon { width: 20px; height: 20px; flex: none; }

.more:hover { color: var(--red); }
.more:hover .more__icon { transform: translateX(3px); }
.more__icon { transition: transform 160ms ease; }

/* ---------- recent works ---------- */

.works {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}

.works__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 86px;
  width: 100%;
}

@media (max-width: 1280px) {
  .works__grid { grid-template-columns: repeat(2, 1fr); gap: 48px; }
}
@media (max-width: 780px) {
  .works__grid { grid-template-columns: 1fr; gap: 40px; }
}

.card { display: flex; flex-direction: column; gap: 14px; }

/* THE GRID HOLDS ITS ROW BEFORE THE CONTENT ARRIVES (owner, 2026-09-22).

   The cards are built by js/site.js after /api/content answers, so for that
   first stretch the grid was empty and everything under it sat a row higher,
   then dropped when the cards went in. The pages now ship one row of blank
   .card--pending cards in their markup, the same boxes with nothing in them,
   and the renderer replaces them. They are never revealed by the motion
   section -- its selector skips them -- so under html.motion they are simply
   invisible space; without it they are the grey frames the site already uses
   for a post with no picture, which reads as loading. The announcement row
   does the same with a minimum height: one post under its number. */
.card--pending .card__title,
.card--pending .card__meta { visibility: hidden; }
.news:empty { min-height: 210px; }

.card__thumb {
  aspect-ratio: 516 / 290;
  width: 100%;
  overflow: hidden;
  background: var(--placeholder-box);
  container-type: inline-size;
}

.card__thumb img { width: 100%; height: 100%; object-fit: cover; }

/* Same proportion rule as the hero plate: the artboard's label spans 309 of
   the 516 thumb, ~60%. */
.card__placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-family: var(--en);
  font-weight: 700;
  font-size: 12.73cqw;
  color: var(--placeholder-ink);
}

.card__caption { display: flex; flex-direction: column; gap: 6px; }

.card__title {
  font-family: var(--jp);
  font-weight: 700;
  font-size: 18px;
  line-height: 1.6;
  color: var(--ink);
}

.card__meta {
  font-family: var(--jp);
  font-weight: 400;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ink);
}

/* Hover: the caption shifts right and a red rule grows down its left edge.
 *
 * The thumbnail does not move. It used to — `transform: scale(1.03)` on the
 * img, with the thumb clipping it — and the owner rejected it on 2026-09-22:
 * scaling a still inside a fixed frame resamples it, so the work itself went
 * soft on the one interaction that is supposed to invite you into it. Nine
 * alternatives were drawn up in a preview page and this was the pick. **Do not
 * reintroduce a zoom, or any other transform on the image.**
 *
 * So the whole response lives in the type instead, which is the one part of
 * the card this site owns rather than the artist. Two details hold it up:
 *
 *   * the rule sits at left: -14px, OUTSIDE the card's box, in the grid gap
 *     (86px, 48px, 40px) and the shell padding (>= 24px) — both leave room at
 *     every breakpoint. `.card` must therefore never get `overflow: hidden`.
 *   * it grows from the top with transform-origin, not by animating height,
 *     so it stays on the compositor.
 *
 * Scoped to `a.card` throughout: workCard() in js/site.js builds a plain div
 * when a post has no page to link to, and a card that cannot be opened should
 * not answer the pointer. */
a.card:hover .card__title { color: var(--red); }

a.card .card__caption {
  position: relative;
  transition: transform 220ms cubic-bezier(.22, .61, .36, 1);
}

a.card .card__caption::before {
  content: '';
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: -14px;
  width: 2px;
  background: var(--red);
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform 240ms cubic-bezier(.22, .61, .36, 1);
}

a.card:hover .card__caption { transform: translateX(14px); }
a.card:hover .card__caption::before { transform: scaleY(1); }

/* Same reasoning as the cursor dot below: the red rule is the signal, the
   travel is decoration. A visitor who asked for less motion keeps the first
   and loses the second. */
@media (prefers-reduced-motion: reduce) {
  a.card .card__caption,
  a.card .card__caption::before { transition: none; }
  a.card:hover .card__caption { transform: none; }
}

/* ---------- announcement ---------- */

/* FOUR NUMBERED POSTS SIDE BY SIDE (owner-picked "H3", 2026-09-25).

   This was a bordered box of 61px date-and-title rows in a 1080 column, the
   artboard's; the owner asked for it to change along with /news, and picked
   this from four alternatives drawn in the same family as /news's "04": each
   post stands under its number, the newest one's in red, and whitespace
   separates them -- no box, no rules. The link to the rest moved up beside the
   heading, where the eye leaves the section. The number is the post's place
   from the top, newest 01 (newsNumber() in js/site.js), as on /news.
   Hover lives in the type, as on the WORKS cards: the title turns red and the
   arrow steps right; nothing else moves. */

/* Also used by the CTA below. */
.centered { display: flex; justify-content: center; width: 100%; }

.sec__head-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
}

.news {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 56px 64px;
}

.news__item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  min-width: 0;
  color: var(--ink);
}

.news__no {
  font-family: var(--en);
  font-weight: 700;
  font-size: 104px;
  line-height: 0.85;
  letter-spacing: -0.04em;
  color: rgba(0, 0, 0, 0.12);
}

.news__item--latest .news__no { color: var(--red); }

.news__date {
  margin-top: 10px;
  font-family: var(--en);
  font-weight: 500;
  font-size: 15px;
  line-height: 22px;
  letter-spacing: 0.06em;
  color: rgba(0, 0, 0, 0.45);
  white-space: nowrap;
}

.news__title {
  font-family: var(--jp);
  font-weight: 700;
  font-size: 20px;
  line-height: 1.6;
  overflow-wrap: anywhere;
  transition: color 160ms ease;
}

.news__arrow {
  width: 22px;
  height: 22px;
  flex: none;
  transition: transform 160ms ease;
}

a.news__item:hover .news__title { color: var(--red); }
a.news__item:hover .news__arrow { transform: translateX(4px); }

.news__empty {
  grid-column: 1 / -1;
  font-family: var(--jp);
  font-size: 15px;
  color: var(--placeholder-ink);
}

@media (max-width: 1100px) {
  .news { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* On a phone four stacked giant numerals would be most of the section, so
   each post becomes a row: the number to the left of its date and title. */
@media (max-width: 680px) {
  .news { grid-template-columns: minmax(0, 1fr); gap: 28px; }
  .news__item {
    display: grid;
    grid-template-columns: 72px minmax(0, 1fr);
    column-gap: 16px;
    row-gap: 2px;
    align-items: start;
  }
  .news__no { grid-row: 1 / span 2; font-size: 56px; }
  .news__date { margin-top: 0; font-size: 13px; line-height: 20px; }
  .news__title { font-size: 17px; }
  .news__arrow { display: none; }
}

/* ---------- let's work together ---------- */

.cta {
  display: flex;
  flex-direction: column;
  gap: 60px;
  padding: 57px var(--sec-pad) 94px;
}

.cta__headline {
  font-family: var(--en);
  font-weight: 700;
  font-size: 76.894px;
  line-height: normal;
  color: var(--red);
  text-align: center;
  white-space: nowrap;
}

@media (max-width: 1280px) {
  .cta__headline { font-size: 54px; }
}
@media (max-width: 820px) {
  .cta__headline { font-size: 34px; }
}
@media (max-width: 520px) {
  .cta__headline { font-size: 24px; }
}

.cta__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 60px;
  width: 1080px;
  max-width: 100%;
}

/* Stacked, the two columns must stretch rather than size to content: the
   status card is a fixed 460 wide, so a flex-start column leaves it sticking
   out of a narrow viewport instead of capping at max-width. */
@media (max-width: 1080px) {
  .cta__row { flex-direction: column; align-items: stretch; gap: 40px; }
  .pitch, .status { min-width: 0; }
}

.pitch {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-self: stretch;
  gap: 40px;
}

.pitch__copy { display: flex; flex-direction: column; gap: 12px; }

.pitch__headline {
  font-family: var(--jp);
  font-weight: 700;
  font-size: 24px;
  line-height: 1.4;
  color: var(--ink);
}

.pitch__body {
  font-family: var(--jp);
  font-weight: 400;
  font-size: 14px;
  line-height: 24px;
  color: var(--ink);
  width: 560px;
  max-width: 100%;
  /* The owner types line breaks in the admin; keep them. */
  white-space: pre-line;
}

.btn {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: 8px;
  height: 60px;
  padding: 16px 28px 16px 32px;
  background: var(--red);
  font-family: var(--en);
  font-weight: 700;
  font-size: 21.306px;
  line-height: normal;
  color: var(--surface);
  white-space: nowrap;
}

/* The hover -- a darker red sweeping in from the left, the arrow leaving and
   returning -- lives in the motion section at the foot of this file, shared
   with /contact's and /404's buttons. */
.btn__icon { width: 20px; height: 20px; flex: none; }

/* ---------- status card ---------- */

.status { display: flex; flex-direction: column; gap: 4px; }

.status__title {
  font-family: var(--en);
  font-weight: 700;
  font-size: 41.537px;
  line-height: normal;
  color: var(--ink);
}

.status__card {
  display: flex;
  flex-direction: column;
  width: 460px;
  max-width: 100%;
  padding: 24px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 4px;
}

.status__slots { display: flex; flex-direction: column; gap: 18px; }

.slot { display: flex; align-items: center; gap: 7px; }

.slot__icon { width: 16px; height: 16px; flex: none; }

.slot__label,
.slot__value {
  font-family: var(--jp);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.6;
}

.slot__label { color: var(--ink); }

/* Each state carries its own colour AND its own icon — the pair is the
   design's whole signal, so never set one without the other. */
.slot__value[data-state='closed'] { color: var(--red); }
.slot__value[data-state='few']    { color: var(--status-few); }
.slot__value[data-state='open']   { color: var(--status-open); }

.notice { display: flex; flex-direction: column; gap: 4px; padding-top: 20px; }

.notice__label {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 20px;
  font-family: var(--en);
  font-weight: 700;
  font-size: 14.247px;
  line-height: normal;
  color: var(--ink);
}

.notice__icon { width: 16px; height: 16px; flex: none; }

.notice__body {
  font-family: var(--jp);
  font-weight: 700;
  font-size: 12px;
  line-height: 18px;
  color: var(--ink);
  white-space: pre-line;
}

/* ---------- footer ---------- */

.ftr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  height: 150px;
  padding: 0 var(--shell-pad);
  background: var(--red);
}

.ftr__legal { display: flex; flex-direction: column; gap: 12px; }

.ftr__link {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--en);
  font-weight: 500;
  font-size: 12px;
  line-height: normal;
  color: var(--surface);
  white-space: nowrap;
}

.ftr__link:hover { text-decoration: underline; }
.ftr__link img { width: 14px; height: 14px; flex: none; }

.ftr__socials { display: flex; align-items: center; gap: 10px; }

.ftr__social {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  opacity: 1;
  transition: opacity 160ms ease;
}

.ftr__social:hover { opacity: 0.7; }
.ftr__social img { width: 20px; height: 20px; }

/* The YouTube glyph is 20 x 13.341 in the artboard, centred in its 20 box. */
.ftr__social img[data-glyph='youtube'] { height: 13.341px; }

@media (max-width: 620px) {
  .ftr { height: auto; padding-block: 28px; flex-direction: column; align-items: flex-start; gap: 20px; }
}

/* ---------- cursor dot ---------- */

/* A 10px accent dot trailing the pointer. Decoration and nothing else: it never
   takes a click, a hover or a text selection, which is what pointer-events: none
   buys — without it this would sit between the visitor and every link on the
   site.

   Gated on a fine, hover-capable pointer. On touch there is no cursor to follow,
   and the dot would simply strand itself wherever the last tap landed; the
   guard in js/site.js means it is never even built there.

   Nothing else in this stylesheet sets z-index, so 1 already clears the page.
   It is spelled out rather than left to DOM order so that a positioned element
   added later cannot quietly paint over it.

   The dot starts off-screen and transparent: js/site.js reveals it on the first
   real pointer move, so it never flashes at 0,0 before the pointer is known.
   Size lives here and only here — the script centres the dot with a percentage
   translate, so changing the 10px below needs no change to the JS. */
@media (hover: hover) and (pointer: fine) {
  .cursor-dot {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--red);
    pointer-events: none;
    opacity: 0;
    transform: translate3d(-50px, -50px, 0) translate(-50%, -50%);
    transition: opacity 180ms ease-out;
    will-change: transform;
  }

  .cursor-dot.is-on { opacity: 1; }
}

/* Belt and braces with the guard in js/site.js: a trailing dot is pure motion,
   and a visitor who asked for less of it should not be given a moving object
   that tracks their pointer everywhere. */
@media (prefers-reduced-motion: reduce) {
  .cursor-dot { display: none; }
}

/* ---------- motion (2026-09-22) ---------- */

/* Seven animations, chosen by the owner from a preview of eighteen on
   2026-09-22 (A2, B1, C3, C4, D2, D3 and E2 in that preview's numbering):
     * section and page titles are typeset letter by letter, the rule draws
       and the subtitle fades up after them;
     * LET'S WORK TOGETHER! drops in word by word with a small overshoot;
     * a red plate sweeps across each WORKS card and leaves the still behind
       it -- the still itself never moves, which is the rule set on
       2026-09-22 when the hover zoom was rejected;
     * an announcement's parts slide in one after another, number first, on
       the home page and on /news alike (C4 also drew a hairline under each
       row; the rules went with the 2026-09-25 redesign, and the line with
       them);
     * a nav word rolls up out of view and a red copy rolls in beneath it;
     * the red buttons' darker red sweeps in from the left and the arrow
       leaves right and re-enters left;
     * a red curtain rises over the page on every internal link and lifts
       off the next one.

   THREE THINGS HOLD THE WHOLE SECTION UP.

   1. Nothing here runs unless <html> carries `motion`. The inline script in
      every page's <head> sets it after checking prefers-reduced-motion, so a
      visitor who asked for less gets the page exactly as it was before this
      section existed, and so does a page whose scripts failed: every hidden
      pre-state below is scoped to html.motion. The class has to come from the
      <head> rather than from js/site.js because the pre-states must be in
      place at first paint -- a title that paints, vanishes and then rises is
      worse than one that never animated.

   2. Every entrance uses animation-fill-mode: backwards, never both or
      forwards. A filled animation keeps its last keyframe applied over the
      normal rules for ever, which would pin the card caption's transform and
      break the hover shift that lives on it. Backwards covers only the delay
      before the animation starts; once it ends the ordinary rules take over,
      and those already describe the resting state. The pre-state for an
      element that has not yet entered the viewport is the :not(.is-in) rule,
      not a fill.

   3. The stagger index is --i, set by js/site.js when the element is revealed,
      counted within the batch that entered the viewport together -- so the
      twelfth card on /works waits on the two beside it, not on the eleven
      before it. Letters get theirs when the title is split. */

@keyframes m-rise      { from { transform: translateY(105%); } to { transform: none; } }
@keyframes m-draw      { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes m-fade-up   { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes m-slide-in  { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } }
@keyframes m-drop      { from { opacity: 0; transform: translateY(-0.55em); } to { opacity: 1; transform: none; } }
@keyframes m-plate {
  0%      { transform: scaleX(0); transform-origin: left center; }
  50%     { transform: scaleX(1); transform-origin: left center; }
  50.001% { transform: scaleX(1); transform-origin: right center; }
  100%    { transform: scaleX(0); transform-origin: right center; }
}
@keyframes m-show      { 0%, 50% { opacity: 0; } 50.001%, 100% { opacity: 1; } }
@keyframes m-arrow-swap {
  0%   { transform: none; opacity: 1; }
  45%  { transform: translateX(18px); opacity: 0; }
  50%  { transform: translateX(-18px); opacity: 0; }
  100% { transform: none; opacity: 1; }
}
@keyframes m-curtain-rise { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes m-curtain-lift { from { transform: scaleY(1); } to { transform: scaleY(0); } }

/* --- titles: letter stagger, rule, subtitle --- */

/* The heading block is the unit: .sec__heading on the home page, .page__heading
   on the routes. js/site.js splits the title into .ch spans and puts .is-in on
   the block when it enters the viewport. The title is its own mask, so a letter
   below its baseline is simply not painted.

   The pre-states hang off the components' own classes, not off a class the
   script adds, so that they hold from the first paint of a static title --
   the script only ever adds .is-in. That is also why the <head> line has a
   4-second fallback that drops html.motion if js/site.js never reports in: a
   title hidden by CSS and never revealed by a script that failed to load
   would otherwise stay hidden. */
html.motion .sec__title,
html.motion .page__title { overflow: hidden; }

.ch { display: inline-block; }

html.motion .sec__heading:not(.is-in) .sec__title,
html.motion .page__heading:not(.is-in) .page__title { opacity: 0; }
html.motion .sec__heading:not(.is-in) .sec__rule,
html.motion .page__heading:not(.is-in) .page__rule { transform: scaleX(0); }
html.motion .sec__heading:not(.is-in) .sec__subtitle,
html.motion .page__heading:not(.is-in) .page__subtitle { opacity: 0; }

html.motion .sec__heading.is-in .ch,
html.motion .page__heading.is-in .ch {
  animation: m-rise 680ms cubic-bezier(.16, 1, .3, 1) backwards;
  animation-delay: calc(var(--i, 0) * 35ms);
}
html.motion .sec__heading.is-in .sec__rule,
html.motion .page__heading.is-in .page__rule {
  transform-origin: left center;
  animation: m-draw 650ms cubic-bezier(.16, 1, .3, 1) 420ms backwards;
}
html.motion .sec__heading.is-in .sec__subtitle,
html.motion .page__heading.is-in .page__subtitle {
  animation: m-fade-up 520ms cubic-bezier(.16, 1, .3, 1) 680ms backwards;
}

/* --- LET'S WORK TOGETHER!: word drop --- */

/* The overshoot easing is the owner's pick (2026-09-22): the words land with a
   small bounce, the "!" last. It is deliberately not the site's deceleration
   curve. */
.w { display: inline-block; }
html.motion .cta__headline:not(.is-in) { opacity: 0; }
html.motion .cta__headline.is-in .w {
  animation: m-drop 620ms cubic-bezier(.34, 1.3, .64, 1) backwards;
  animation-delay: calc(var(--i, 0) * 120ms);
}

/* --- WORKS cards: red plate sweep --- */

/* .card__plate is built into every thumbnail by workCard() in js/site.js. It
   grows from the left edge, and at the moment it fills the frame the still
   switches on beneath it; then it shrinks away to the right. The frame's grey
   and an imported post's blur-up stay visible before the sweep, so a card
   waiting below the fold is a grey plate, never a hole. */
.card__thumb { position: relative; }
.card__plate {
  position: absolute;
  inset: 0;
  background: var(--red);
  transform: scaleX(0);
  transform-origin: left center;
  pointer-events: none;
}
/* A waiting card is blank, not a grey frame: the still, the placeholder and
   the frame's own grey all arrive with the sweep (owner-directed, 2026-09-22:
   nothing of the card before the wipe, the finished picture after it).
   js/site.js only starts the sweep once the still has decoded. */
html.motion .card:not(.is-in) .card__thumb { background-color: transparent; }
html.motion .card:not(.is-in) .card__thumb > :not(.card__plate) { opacity: 0; }
html.motion .card:not(.is-in) .card__caption { opacity: 0; }
html.motion .card.is-in .card__plate {
  animation: m-plate 900ms cubic-bezier(.22, .61, .36, 1) backwards;
  animation-delay: calc(var(--i, 0) * 160ms);
}
html.motion .card.is-in .card__thumb > :not(.card__plate) {
  animation: m-show 900ms linear backwards;
  animation-delay: calc(var(--i, 0) * 160ms);
}
html.motion .card.is-in .card__caption {
  animation: m-fade-up 650ms cubic-bezier(.16, 1, .3, 1) backwards;
  animation-delay: calc(var(--i, 0) * 160ms + 560ms);
}

/* --- announcement posts: the parts slide in, one after another --- */

/* This was C4, the hairline draw: each row's rule drew itself from the left
   and the text followed. The redesign of 2026-09-25 (home "H3", /news "04")
   has no rules left to draw, so only the second half remains -- the owner's
   stagger of the row's own parts, number first. --i counts posts within one
   batch, so a row of four still steps left to right. */
html.motion .news__item:not(.is-in) > *,
html.motion .entry:not(.is-in) > * { opacity: 0; }
html.motion .news__item.is-in > *,
html.motion .entry.is-in > * {
  animation: m-slide-in 600ms cubic-bezier(.16, 1, .3, 1) backwards;
}
html.motion .news__item.is-in > :nth-child(1),
html.motion .entry.is-in > :nth-child(1) { animation-delay: calc(var(--i, 0) * 140ms + 120ms); }
html.motion .news__item.is-in > :nth-child(2),
html.motion .entry.is-in > :nth-child(2) { animation-delay: calc(var(--i, 0) * 140ms + 200ms); }
html.motion .news__item.is-in > :nth-child(3),
html.motion .entry.is-in > :nth-child(3) { animation-delay: calc(var(--i, 0) * 140ms + 280ms); }
html.motion .news__item.is-in > :nth-child(4) { animation-delay: calc(var(--i, 0) * 140ms + 360ms); }

/* --- nav: roll-over --- */

/* js/site.js rebuilds each .hdr__link as two stacked copies of its word and
   adds this class; the second copy is red and sits just below the mask. The
   plain :hover colour above still covers a link the script did not reach. It
   is a transition, not an entrance, so it needs no html.motion: the reduced-
   motion rule at the foot of this section makes the swap instant instead. */
.hdr__link--roll {
  position: relative;
  display: inline-block;
  overflow: hidden;
  line-height: 1.2;
}
.hdr__link--roll:hover { color: var(--ink); }
.hdr__link--roll span {
  display: block;
  transition: transform 320ms cubic-bezier(.16, 1, .3, 1);
}
.hdr__link--roll span + span {
  position: absolute;
  top: 100%;
  left: 0;
  color: var(--red);
}
.hdr__link--roll:hover span { transform: translateY(-100%); }

/* --- buttons: sweep and arrow --- */

/* The three red buttons -- .btn on the home page, .ask__btn on /contact and
   .nf__btn on /404 -- share one response. The darker red is a layer under the
   label that grows from the left, in place of the cross-fade the buttons had;
   isolation: isolate keeps that layer's negative z-index inside the button, so
   it paints above the button's own red and below its text. The arrow plays its
   exit-and-return once per hover. routes.css no longer styles those two
   buttons' hover at all -- this is where it lives. */
.btn,
.ask__btn,
.nf__btn {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  transition: none;
}
.btn::before,
.ask__btn::before,
.nf__btn::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: #e62727;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 360ms cubic-bezier(.16, 1, .3, 1);
}
.btn:hover,
.ask__btn:hover,
.nf__btn:hover { background: var(--red); }
.btn:hover::before,
.ask__btn:hover::before,
.nf__btn:hover::before { transform: scaleX(1); }
.btn:hover .btn__icon,
.ask__btn:hover img,
.nf__btn:hover img {
  transform: none;
  animation: m-arrow-swap 460ms cubic-bezier(.22, .61, .36, 1);
}

/* --- the curtain between pages --- */

/* body::after is the plate, so it exists before any script has built anything:
   the <head> script puts curtain-in on <html> when the page was reached through
   a curtain, and the plate is already up at first paint. js/site.js holds it
   there until the page beneath is ready -- content rendered, fonts and the
   first viewport's images in -- then adds curtain-lift and removes both when
   the lift ends. A click on an internal link adds curtain-rise; the navigation
   follows the rise. z-index 5 clears the cursor dot's 1.

   .is-set is what js/site.js puts on everything in the first viewport just
   before the lift: shown at rest, with its entrance switched off, so the wipe
   itself is the reveal and nothing is caught mid-animation as the plate clears
   it. !important because the entrances are declared with more specific
   selectors above and this must beat every one of them. */
html.motion .is-set,
html.motion .is-set *,
html.motion .is-set::after { animation: none !important; }

html.curtain-in body::after,
html.curtain-rise body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 5;
  background: var(--red);
  pointer-events: none;
}
html.curtain-in body::after { transform-origin: top center; }
html.curtain-lift body::after {
  animation: m-curtain-lift 520ms cubic-bezier(.22, .61, .36, 1) 80ms forwards;
}
html.curtain-rise body::after {
  transform-origin: bottom center;
  animation: m-curtain-rise 480ms cubic-bezier(.22, .61, .36, 1) forwards;
}

/* Reduced motion. The entrances and the curtain never run at all -- html.motion
   is not set -- so only the two pointer responses need saying: the nav swaps
   its copies in place, and the button changes shade without the sweep. */
@media (prefers-reduced-motion: reduce) {
  .hdr__link--roll span { transition: none; }
  .btn::before, .ask__btn::before, .nf__btn::before { transition: none; }
  .btn:hover .btn__icon, .ask__btn:hover img, .nf__btn:hover img { animation: none; }
}
