/* Shared components used site-wide. BEM-ish naming: .block__element--modifier. */

/* ---------- Site nav ---------- */

.site-header {
  /* Sized to land under a hard 80px desktop cap: 62px mark + 8px padding top and
     bottom + the 1px bottom border = 79px. It used to be 130px + 12px, which put the
     bar at 155px and ate 17% of a short laptop viewport on every page, pushing every
     hero a third of the way down the screen. The mark stays deliberately larger than
     the wordmark (same 1.58 ratio as before), it just no longer sets the page's whole
     vertical budget.
     See .agents/skills/design-taste-frontend §4.7 (nav height cap 80px). */
  /* Locked up the way the business card sets it: the wordmark tucked in under
     the mark's right arm, its left edge 36% of the way across the mark and its
     top 58% of the way down, and about 0.78x the mark's height. Measured off
     the printed card. The lockup is therefore taller than the mark alone, and
     --header-h follows the lockup. */
  --mark-h: 50px;
  --word-h: 39px;
  --lockup-h: calc(var(--mark-h) * 0.58 + var(--word-h));
  --header-h: calc(var(--lockup-h) + var(--space-2) * 2);
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  background-color: var(--paper);
  background-image: var(--texture-scan);
  background-size: var(--texture-scan-size);
  background-blend-mode: multiply;
  border-bottom: 1px solid var(--color-border);
  /* Deliberately no backdrop-filter/transform here - either would turn this sticky
     header into a containing block for the mobile nav panel's position:fixed,
     breaking it into a near-zero-height box instead of a full-viewport overlay.
     Same reason the auto-hide below (assets/js/components/nav-autohide.js) animates
     `top` rather than `transform` - a transform here would reintroduce that bug. */
  transition: top var(--duration-medium) var(--ease-default);
}

.site-header--hidden {
  top: calc(-1 * var(--header-h));
}

.site-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Must stay in lockstep with --header-h above. */
  padding-block: var(--space-2);
}

.site-nav__logo {
  display: flex;
  align-items: flex-start;
  height: var(--lockup-h);
}

/* Mark and wordmark are separate vector files (both extracted from the same source
   print file, see ARCHITECTURE.md §2 / assets/brand/NOTES.md) so they can scale
   independently - the mark reads as a bolder, bigger icon than the wordmark text. */
.site-nav__logo-mark-wrap {
  position: relative;
  display: inline-block;
  height: var(--mark-h);
  /* Matches the shared canvas the 6 texture PNGs were cropped onto - cropped by the
     exact vector bounding box of the source mark (not pixel-detected), so all 6
     frames plus the base mark overlay exactly with zero shift during the hover
     cycle. See ARCHITECTURE.md §9 / assets/brand/NOTES.md. */
  aspect-ratio: 333 / 420;
}

/* The mark and wordmark ship in seal red. In the site chrome the lockup is
   pressed in graphite, so the page carries one accent colour. A filter rather
   than recoloured files, so assets/brand/ stays exactly as drawn. */
/* The nav lockup is cobalt, from cobalt copies of the brand files
   (logo-mark-cobalt.svg, wordmark-full-cobalt.svg, mark-variants/cobalt-N.png), so no
   filter there. The gate mark stays graphite until hovered. */
.site-gate__mark img {
  filter: grayscale(1) brightness(0.26);
}

.site-nav__logo-mark-wrap img {
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
  object-fit: contain;
  pointer-events: none;
}

/* Hover/focus texture cycle - see assets/js/components/logo-hover.js and
   ARCHITECTURE.md §9. Textures are real archival material: cropped from Frankie's own
   mood-board of print texture treatments, not invented here. Hidden by default;
   JS reveals them only once GSAP is confirmed loaded (progressive enhancement - the
   plain mark is always the fallback, never only-visible-via-animation content). */
.site-nav__logo-variant {
  opacity: 0;
}

.site-nav__logo-word {
  height: var(--word-h);
  width: auto;
  /* Tucked under the right arm: pulled left by 64% of the mark's width (the
     mark is 333:420) and dropped 58% of its height. */
  margin-left: calc(var(--mark-h) * 333 / 420 * -0.64);
  margin-top: calc(var(--mark-h) * 0.58);
}

@media (max-width: 480px) {
  /* Full size is too wide next to the toggle at very small viewports - step both
     down together, same ratio, rather than let them collide with the hamburger. */
  .site-header {
    --mark-h: 42px;
    --word-h: 33px; /* same proportions as desktop */
  }
}

.site-nav__links {
  display: flex;
  align-items: center;
  gap: var(--space-7);
}

/* Nav is a row of tiny, widely tracked labels rather than ordinary links. This is
   the signature move in both references: the navigation recedes and the page
   breathes. */
.site-nav__links a {
  position: relative;
  font-size: var(--font-size-xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-label);
  color: var(--color-text);
  padding-block: var(--space-2);
}

/* Hairline, not a bar. Red is reserved for "you are here", so the seal color
   carries meaning instead of decorating every hover. */
.site-nav__links a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 100%;
  bottom: 0;
  height: 1px;
  background: var(--color-text);
  transition: right var(--duration-medium) var(--ease-default);
}

.site-nav__links a[aria-current="page"]::after {
  background: var(--color-accent);
}

.site-nav__links a:hover::after,
.site-nav__links a[aria-current="page"]::after {
  right: 0;
}

.site-nav__toggle {
  display: none;
  width: 32px;
  height: 32px;
  position: relative;
  z-index: var(--z-nav);
  flex-shrink: 0;
}

.site-nav__toggle span {
  position: absolute;
  left: 4px;
  right: 4px;
  height: 1px;
  background: var(--color-text);
  transition: transform var(--duration-short) var(--ease-default),
              opacity var(--duration-short) var(--ease-default);
}

.site-nav__toggle span:nth-child(1) { top: 12px; }
.site-nav__toggle span:nth-child(2) { top: 19px; }

.site-nav__toggle[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(3px) rotate(45deg);
}

.site-nav__toggle[aria-expanded="true"] span:nth-child(2) {
  transform: translateY(-4px) rotate(-45deg);
}

@media (max-width: 768px) {
  .site-nav__toggle {
    display: block;
  }

  .site-nav__links {
    position: fixed;
    inset: 0;
    top: var(--header-h);
    z-index: var(--z-nav);
    background-color: var(--paper);
    background-image: var(--texture-scan);
    background-size: var(--texture-scan-size);
    background-blend-mode: multiply;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    gap: var(--space-6);
    padding: var(--space-8) var(--container-pad);
    transform: translateX(100%);
    transition: transform var(--duration-medium) var(--ease-default);
  }

  .site-nav__links[data-open="true"] {
    transform: translateX(0);
  }

  .site-nav__links a {
    font-size: var(--font-size-sm);
  }
}

/* ---------- Entry gate (homepage only) ---------- */
/* `display: none` here is load-bearing, exactly as the old splash's was: gate.js is
   the only thing that ever reveals this, and it bails out rather than showing a door
   it can't open. If JS fails, the visitor just gets the site. */

.site-gate {
  display: none;
  /* The whole screen is the door (gate.js), so the whole screen says so. */
  cursor: pointer;
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  align-items: center;
  justify-content: center;
  background-color: var(--paper);
  background-image: var(--texture-scan);
  background-size: var(--texture-scan-size);
  background-blend-mode: multiply;
}

.site-gate__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-5);
}

/* Small on purpose. The reference gate's glyph is tiny against an enormous ground,
   and that ratio is most of why it feels calm rather than like a loading screen. */
.site-gate__mark {
  position: relative;
  display: block;
  width: min(22vw, 124px);
  aspect-ratio: 333 / 420;
}

.site-gate__mark img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
}

.site-gate__variant {
  opacity: 0;
}

.site-gate__enter {
  padding: var(--space-3) var(--space-4);
  border: 0;
  background: none;
  cursor: pointer;
  font-family: var(--font-family-mono);
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  transition: color var(--duration-short) var(--ease-default);
  /* Centred on the page, in line with the centre of the mark's box (Frankie moved
     it here from under the stem). text-indent balances the trailing letter-spacing
     so the letters themselves are centred, not the letters plus one invisible gap.
     Keep the two values equal. */
  text-indent: 0.08em;
}

.site-gate__enter:hover {
  color: var(--color-text);
}

/* Focus on the door darkens the word, in place of a ring or underline (Frankie's
   call: both read as stray marks on this screen). */
.site-gate__enter:focus-visible {
  outline: none;
  color: var(--color-text);
}

/* Hovering the mark inks the print that is on screen in cobalt, keeping its
   texture. gate.js adds .is-inked to the mark and .is-on to the matching cobalt
   twin (or to the plain tint, when there is no cycle). The grey prints hide
   underneath so they do not show through the twin's see-through paper areas.
   visibility rather than opacity: GSAP owns the prints' inline opacity. */
.site-gate__cobalt,
.site-gate__tint {
  opacity: 0;
  transition: opacity var(--duration-short) var(--ease-default);
}

.site-gate__tint {
  position: absolute;
  inset: 0;
  background-color: var(--color-accent);
  -webkit-mask: url(/assets/brand/logo-mark.svg) center / contain no-repeat;
  mask: url(/assets/brand/logo-mark.svg) center / contain no-repeat;
  pointer-events: none;
}

.site-gate__mark img.site-gate__cobalt {
  filter: none;
}

.site-gate__mark.is-inked .site-gate__mark-base,
.site-gate__mark.is-inked .site-gate__variant {
  visibility: hidden;
}

.site-gate__cobalt.is-on,
.site-gate__tint.is-on {
  opacity: 1;
}

/* ---------- Dot-divider motif ---------- */
/* A row of small circles, echoing the letterhead divider. Pure CSS, no markup needed
   beyond an empty element carrying the class. Smaller and further apart than before
   so it reads as a stitched rule rather than a dotted band. */

/* A plain hairline. This was a repeating red dot pattern, which spent the brand's
   one saturated colour on decoration on all 28 pages: the seal red is supposed to be
   rare and small, and a rule between two footer blocks is structure, not a graphic.
   See .agents/skills/design-taste-frontend §4.2 and §9.F. */
.dot-divider {
  height: 1px;
  background: var(--color-ink-border);
}

/* ---------- Buttons ---------- */

/* Squared off and set in the studio's mono, in sentence case, the same as the
   "Start a project" control on the home intro. No pill, no lift on hover: in this
   direction a button is a stamped label, not a candy shape. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0.95em 1.6em;
  border-radius: var(--radius-sm);
  font-family: var(--font-family-mono);
  font-weight: 400;
  font-size: 13px;
  letter-spacing: 0.02em;
  transition: background var(--duration-short) var(--ease-default),
              color var(--duration-short) var(--ease-default),
              border-color var(--duration-short) var(--ease-default);
}

.btn--primary {
  background: var(--color-ink);
  color: var(--color-ink-text);
  border: 1px solid var(--color-ink);
}

.btn--primary:hover {
  background: var(--color-ink-raised);
  border-color: var(--color-ink-raised);
}

/* For a button standing on the ink ground (the footer). .btn--primary is an ink fill,
   which on ink is an invisible button: correct text contrast, zero affordance. This
   outlines instead and fills on hover, so it reads as a control without punching a
   bright hole in the dark band.
   See .agents/skills/design-taste-frontend §4.5 (button contrast check). */
.btn--on-ink {
  background: none;
  border: 1px solid var(--color-ink-text);
  color: var(--color-ink-text);
}

.btn--on-ink:hover {
  background: var(--color-ink-text);
  border-color: var(--color-ink-text);
  color: var(--color-ink);
}

.btn--ghost {
  border: 1px solid var(--color-text);
  color: var(--color-text);
}

.btn--ghost:hover {
  background: var(--color-text);
  color: var(--color-bg);
}

/* ---------- Tags / labels ---------- */

.tag {
  display: inline-block;
  font-size: var(--font-size-xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-label);
  color: var(--color-text-muted);
}

/* ---------- Footer ---------- */

.site-footer {
  overflow: hidden;
  background: var(--color-ink);
  color: var(--color-ink-text);
}

.site-footer a:hover {
  color: var(--color-accent-soft);
}

.site-footer__top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--space-7);
  padding-block: var(--space-9);
}

.site-footer__cta {
  font-family: var(--font-family-display);
  font-size: var(--font-size-3xl);
  line-height: var(--line-height-tight);
  letter-spacing: -0.04em;
  max-width: 14ch;
}

.site-footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5) var(--space-8);
  padding-block: var(--space-6);
  border-top: 1px solid var(--color-ink-border);
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-label);
}

/* The sign-off: a row of the eight textured Frankly marks (the same prints as the
   home masthead), inked in cobalt and spread edge to edge across the footer.
   Generated here rather than written into every footer, because this site has no
   build step and the footer markup is copied into every page. One element, eight
   mask layers: each layer is one mark at full height, and percentage positions
   spread them evenly with the first and last flush to the edges. */
.site-footer > .container::after {
  content: '';
  display: block;
  height: clamp(56px, 10vw, 160px);
  margin-block: var(--space-6) var(--space-8);
  background-color: var(--color-accent);
  -webkit-mask-image:
    url(/assets/brand/marks/blur.webp),
    url(/assets/brand/marks/blotch.webp),
    url(/assets/brand/marks/halftone.webp),
    url(/assets/brand/marks/badge.webp),
    url(/assets/brand/marks/waves.webp),
    url(/assets/brand/marks/grain.webp),
    url(/assets/brand/marks/outline.webp),
    url(/assets/brand/marks/torn.webp);
  mask-image:
    url(/assets/brand/marks/blur.webp),
    url(/assets/brand/marks/blotch.webp),
    url(/assets/brand/marks/halftone.webp),
    url(/assets/brand/marks/badge.webp),
    url(/assets/brand/marks/waves.webp),
    url(/assets/brand/marks/grain.webp),
    url(/assets/brand/marks/outline.webp),
    url(/assets/brand/marks/torn.webp);
  -webkit-mask-size: auto 100%;
  mask-size: auto 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: 0.0% 0, 14.286% 0, 28.571% 0, 42.857% 0, 57.143% 0, 71.429% 0, 85.714% 0, 100.0% 0;
  mask-position: 0.0% 0, 14.286% 0, 28.571% 0, 42.857% 0, 57.143% 0, 71.429% 0, 85.714% 0, 100.0% 0;
}

.site-footer__meta {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding-block: var(--space-6);
  font-size: var(--font-size-xs);
  color: var(--color-ink-muted);
}

/* ---------- Objects ---------- *
 *
 * The shared vocabulary for treating a piece of work as a physical thing sitting on
 * the page rather than as a thumbnail inside a container. Used by the work index, the
 * archive, the case studies and the About page, so the whole site handles an image the
 * same way. See ARCHITECTURE.md §16.
 *
 * Deliberately NOT a card system. No rounded container, no panel, no lifted box. An
 * object here is the artwork itself plus, at most, a contact shadow that says it is
 * lying on paper.
 */

/* The catalogue hand: a real museum object label, not a decorative caption. Only ever
   carries information the reader cannot get from looking (year, medium, client, role).
   Never an index number: the reader can already count. */
.spec {
  font-family: var(--font-family-mono);
  font-size: 11px;
  line-height: 1.5;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/*
  The field label in a label/value pair.

  It used to be --color-warm-gray, which measures 2.42:1 on oat. That is a decorative
  value, and the token file says so; using it for real label text is a straight WCAG
  failure. There is no headroom to fix it with a lighter ink either: anything paler
  than --color-text-muted drops under 4.5 on oat and further still on parchment.

  So the hierarchy stops being carried by making the LABEL quieter and starts being
  carried by making the VALUE louder. The key sits at the muted weight the rest of
  the catalogue hand uses, and the value it introduces goes to full text colour.

  If :has() is unavailable the value simply stays muted and the pair reads flat.
  Both halves still pass AA, which is the right way round for a fallback.
*/
.spec--key {
  color: var(--color-text-muted);
}

.spec:has(.spec--key) {
  color: var(--color-text);
}

/* A row of label/value pairs under an object. Columns rather than a separator
   character, so the metadata reads as a table and not as "foo . bar . baz". */
.spec-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
}

.spec-row > div {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/*
  An object on paper.

  The shadow is tinted to the oat ground rather than black, and it is offset down and
  slightly right so it reads as a sheet resting on the page under a single soft light.
  Two layers: a tight one for the contact edge, a wide faint one for the ambient.
*/
.object {
  display: block;
  position: relative;
  width: 100%;
  height: auto;
  box-shadow:
    0 1px 2px rgb(62 54 40 / 0.10),
    0 14px 34px -18px rgb(62 54 40 / 0.34);
}

/* Cut-out artwork with its own silhouette. A contact shadow on a shape with no edge
   just fogs the paper around it, so these get none and float instead. */
.object--cutout {
  box-shadow: none;
}

/*
  Off-axis. Real printed matter is never perfectly square to the page, and the
  reference board is full of sheets lying a degree or two off. Small enough that it
  reads as a hand placing something rather than as a CSS trick.

  Applied on a WRAPPER, never on a sticky/fixed ancestor: a transform creates a
  containing block, which is what broke the mobile nav panel once already (see the
  note on .site-header). The rotation is also dropped entirely under reduced motion
  further down, because a permanently tilted layout is disorienting for some readers
  even when nothing is moving.
*/
/*
  Sets the resting angle as a custom property and nothing else.

  It does NOT write `transform`. Everything that tilts also does something else to
  its transform (a hover lift, a drag offset, a scroll drift), so the element's own
  rule assembles the whole thing and this just supplies one number. Any new user of
  .off-axis has to include rotate(var(--rot, 0deg)) in its own transform.

  There used to be a fallback here that wrote the transform for elements "that only
  tilt", scoped with :not([class*='__obj']) to keep it away from the composed ones.
  Matching class names by substring is exactly as fragile as it sounds:
  .opening__object contains "__obj", so it was excluded from the fallback, and since
  it had no transform of its own it simply never tilted.
*/
.off-axis {
  --rot: -0.55deg;
}

.off-axis--r {
  --rot: 0.7deg;
}

/* A bone sheet laid under an object, showing as a thin margin on two sides. Gives a
   flat scan an edge without drawing a box around it. */
.mount {
  /* Used on a <span> so it can sit inside a <figure> next to the caption without
     adding a div. A span is inline by default, and an inline box paints its padding
     beside the content rather than around it, which showed up as a white sliver down
     the left edge of every mounted sheet. */
  display: block;
  position: relative;
  /* Coarser stock than the page under it, so a mounted sheet reads as a different
     paper rather than as a lighter rectangle. */
  /* No paper sheet any more: the work sits straight on the page (Sept 2026). */
}

.mount > .object {
  box-shadow: none;
}

@media (prefers-reduced-motion: reduce) {
  .off-axis,
  .off-axis--r {
    --rot: 0deg;
  }
}
