/* ---------- reset ---------- */
* { box-sizing: border-box; margin: 0; padding: 0; font: inherit; }

@font-face {
  font-family: 'Droulers Clarendon';
  src: url('../assets/fonts/DroulersClarendonRegular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'PP Radio Grotesk';
  src: url('../assets/fonts/PPRadioGroteskRegular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* --accent is the one value js/accent.js and each page's inline
     anti-flash script actually change (cycling through the palette on
     every load/click) — --bg and --text-color just point at it and the
     fixed brown, and swap which is which on the work index below, so
     that page always reads as "inverted" relative to whichever accent
     is currently selected. */
  --brown: #553A32;
  --accent: #F9A5C2;
  --bg: var(--brown);
  --text-color: var(--accent);
  --font-text: 'Droulers Clarendon', Georgia, serif;
  --font-tag: 'PP Radio Grotesk', Arial, sans-serif;
}

/* Site-wide desktop grid: 12 columns, 35px outer margin, 15px gutter.
   Undefined below this breakpoint, so every var(--grid-margin, Npx)
   below keeps its own mobile-only fallback until this kicks in. */
@media (min-width: 768px) {
  :root {
    --grid-margin: 35px;
    --grid-gutter: 15px;
  }
}

html {
  /* The page is intentionally dark (brown bg, pink text). Declaring
     that explicitly stops some mobile browsers' "forced dark mode"
     from repainting text white on top of it. */
  color-scheme: dark;
  scroll-behavior: smooth;
  /* Desktop browsers otherwise draw a solid track behind the scrollbar
     thumb — a hard vertical line against the dark background. Mobile
     already shows just the thin overlay thumb by default; this makes
     desktop match it instead of that solid track. */
  scrollbar-width: thin;
  scrollbar-color: var(--text-color) transparent;
}

::-webkit-scrollbar {
  width: 8px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background-color: var(--text-color);
  border-radius: 4px;
}

body {
  background: var(--bg);
  font-family: var(--font-text);
  font-weight: 400;
  font-size: 12pt;
  line-height: 12pt;
  letter-spacing: 0;
  color: var(--text-color);
  min-height: 100svh;
  overflow-x: hidden;
  overflow-y: auto;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* ---------- stage ---------- */
.stage {
  position: relative;
  width: 100%;
  height: 100svh;
  cursor: pointer;
  /* Without this, a mobile browser's double-tap-to-zoom gesture
     recognizer can delay or coalesce two quick taps while it waits to
     see if a second one is coming, which reads as the click handler
     skipping a state (e.g. the empty gap between the outline state and
     the first photo of the next cycle). */
  touch-action: manipulation;
}

/* ---------- intro typography ---------- */
/* Landing state: name/role and the caption sit grouped together at
   screen center (one line of gap between them). On the first mouse
   move (or first touch, since mobile has no hover) .stage gets
   `is-active` and both blocks slide to their resting positions —
   name/role to a fixed 20px from the top, caption to a fixed 20px
   from the bottom. Each element anchors via `top` + `transform` in
   both states (transform flips between top-edge and bottom-edge
   anchoring) so the margin is exact regardless of text height. */
.intro {
  position: absolute;
  left: 50%;
  top: calc(50svh - 8px);
  width: min(90vw, 520px);
  text-align: center;
  transform: translate(-50%, -100%);
  transition: top 0.7s cubic-bezier(.2,.8,.2,1), transform 0.7s cubic-bezier(.2,.8,.2,1);
  z-index: 5;
}

.stage.is-active .intro {
  top: 20px;
  transform: translate(-50%, 0);
}

/* Applied briefly while restoring a saved reveal state (returning from
   work.html), so the header/caption snap straight to their split
   position instead of animating there again. */
.stage.no-anim .intro,
.stage.no-anim .caption {
  transition: none;
}

.intro__name {
  text-transform: uppercase;
}

.intro__role {
  margin-top: 0.35em;
  text-transform: uppercase;
}

/* Part of the header unit (name/role + this dot): fixed to the
   viewport at the same top/right margin on every page, so it stays
   pixel-identical whether the page is index.html or work.html. Right
   margin follows the site grid (35px on desktop, 20px on mobile). */
.work-link {
  position: fixed;
  top: 20px;
  right: var(--grid-margin, 20px);
  width: 16px;
  height: 16px;
  border: 1px solid var(--text-color);
  border-radius: 50%;
  background: transparent;
  z-index: 30;
  transition: background-color 0.2s ease;
}

.work-link:hover,
.work-link:active,
.work-link:focus-visible {
  background-color: var(--text-color);
}

/* Homepage only: takes over .work-link's old job of linking to the
   work index (that dot now scrolls to #about instead). Stacked below
   the dot on mobile, moved beside it (to its left) on desktop — same
   16px box either way so it lines up with the dot exactly. */
.index-link {
  position: fixed;
  top: 44px;
  right: var(--grid-margin, 20px);
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-text);
  font-size: 16px;
  line-height: 1;
  color: var(--text-color);
  text-decoration: none;
  z-index: 30;
}

@media (min-width: 768px) {
  .index-link {
    top: 20px;
    right: calc(var(--grid-margin, 20px) + 24px);
  }
}

/* Accent-color picker — same fixed top margin and 16px box as
   .work-link's dot, mirrored to the opposite (left) side. Each click
   (and each fresh page load) moves to the next color in the fixed
   palette js/accent.js cycles through; shown as a sparkle glyph rather
   than the current color itself, so it reads as "pick a color" rather
   than showing one particular state. The glyph's own ink only fills a
   fraction of its em-square (unlike the dot or the U+259E quadrant,
   which both fill their box edge to edge), so it needs a noticeably
   larger font-size — with the box itself staying 16px — to read as the
   same visual size/weight as those two. */
.accent-toggle {
  position: fixed;
  top: 20px;
  left: var(--grid-margin, 20px);
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  padding: 0;
  cursor: pointer;
  z-index: 30;
  background: none;
  font-family: var(--font-text);
  font-size: 28px;
  line-height: 1;
  color: var(--text-color);
}

/* ---------- photo stack ---------- */
.stack {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(80vw, 360px);
}

/* Each photo already has its own paper border and drop shadow baked in
   (transparent PNG). .card is a wrapper that positions/rotates on
   reveal (absolutely positioned with no explicit size, so it shrinks
   to fit its photo exactly); .card__photo carries the actual sizing
   constraints — split this way so the "outline" state (after the
   last photo, one more click) can hide the photo while keeping a
   border traced around the exact same box. No transition on
   transform: the reveal is an instant frame swap (like a gif), not
   an eased pop-in. */
.card {
  position: absolute;
  left: 50%;
  top: 50%;
  opacity: 0;
  transform: translate(-50%, -50%) rotate(var(--rot, 0deg));
}

.card.is-visible {
  opacity: 1;
  transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px))) rotate(var(--rot, 0deg));
}

.card__photo {
  display: block;
  max-width: min(calc(100vw - 140px), 210px);
  max-height: calc(100svh - 260px);
  width: auto;
  height: auto;
}

/* Hidden (but still occupying the box, so .card keeps sizing to it)
   whenever .card__outline-fill is covering it — avoids any edge
   mismatch between the two showing through as a stray sliver of the
   outline PNG's own baked-in color. */
.card__photo.is-masked {
  opacity: 0;
}

/* Fills the outline's silhouette with the current accent color —
   .card is the only sized child (the outline-fill is absolutely
   positioned and doesn't contribute to it), so this exactly overlays
   whichever box .card__photo established. */
.card__outline-fill {
  position: absolute;
  inset: 0;
  background-color: var(--text-color);
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  -webkit-mask-size: contain;
  opacity: 0;
  pointer-events: none;
}

.card__outline-fill.is-visible {
  opacity: 1;
}

/* ---------- number tags ---------- */
.tags {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 20;
}

.tag {
  position: absolute;
  color: #ffffff;
  text-decoration: none;
  opacity: 0;
  pointer-events: auto;
  transition: opacity 0.4s ease 0.1s, color 0.2s ease;
}

/* During the outline state, hovering/tapping a number brings its photo
   back — the number itself turns pink to match. */
.tag.is-peeking {
  color: var(--text-color);
}

.tag.is-visible {
  opacity: 1;
}

/* ---------- caption ---------- */
.caption {
  position: absolute;
  left: 50%;
  top: calc(50svh + 8px);
  width: min(90vw, 480px);
  text-align: center;
  transform: translate(-50%, 0);
  transition: top 0.7s cubic-bezier(.2,.8,.2,1), transform 0.7s cubic-bezier(.2,.8,.2,1);
  z-index: 5;
}

.stage.is-active .caption {
  top: calc(100svh - 20px);
  transform: translate(-50%, -100%);
}

/* ---------- desktop / tablet ---------- */
@media (min-width: 768px) {
  .stack {
    width: 420px;
  }

  .card__photo {
    max-width: min(36vw, 300px);
  }

  .caption {
    width: 420px;
  }
}

@media (min-width: 1100px) {
  .stack {
    width: 460px;
  }

  .card__photo {
    max-width: min(24vw, 340px);
  }
}

/* ---------- work index page ---------- */
body.page-archive {
  cursor: auto;
}

/* White by default — only the header (name/role) uses the pink
   --text-color, matching the rest of the site. */
.archive {
  padding: 0 var(--grid-margin, 24px) 100px;
  color: #ffffff;
}

/* Sticky: stays pinned to the top while everything else — filters,
   the photo list, the lightbox's dark backdrop — scrolls underneath
   it. No background of its own, so that content is visible passing
   behind the name/role text rather than masked by an opaque bar. Top
   padding matches the homepage header's 20px margin exactly. */
.archive__header {
  position: sticky;
  top: 0;
  z-index: 10;
  padding: 20px 0 24px;
  text-align: center;
  color: var(--text-color);
}

.archive__header-link {
  display: inline-block;
  color: inherit;
  text-decoration: none;
}

.archive__header .intro__name,
.archive__header .intro__role {
  text-transform: uppercase;
}

.archive__header .intro__role {
  margin-top: 0.35em;
}

/* ---------- category filters ---------- */
.archive__filters {
  display: flex;
  flex-wrap: wrap;
  column-gap: 32px;
  row-gap: 12px;
  padding: 24px 0 40px;
}

.archive__filter {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-family: var(--font-tag);
  font-size: 16px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #ffffff;
  cursor: pointer;
}

/* Mobile-only accordion trigger for .archive__filters below — hidden
   entirely on desktop, where the filter row is always visible. */
.archive__filters-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: none;
  padding: 24px 0 0;
  font: inherit;
  font-family: var(--font-tag);
  font-size: 16px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #ffffff;
  cursor: pointer;
}

@media (max-width: 767px) {
  .archive__filters {
    display: none;
    flex-direction: column;
    row-gap: 12px;
    padding: 16px 0 40px;
  }

  .archive__filters.is-open {
    display: flex;
  }
}

@media (min-width: 768px) {
  .archive__filters-toggle {
    display: none;
  }
}

.archive__filter-dot {
  width: 10px;
  height: 10px;
  border: 1px solid currentColor;
  border-radius: 50%;
  background: transparent;
  transition: background-color 0.2s ease;
}

.archive__filter.is-active {
  color: var(--text-color);
}

.archive__filter.is-active .archive__filter-dot {
  background-color: currentColor;
}

.archive__list {
  display: grid;
  grid-template-columns: 1fr;
}

.archive__row {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding: 20px 0;
  scroll-margin-top: 110px;
}

.archive__number {
  flex: 0 0 auto;
  width: 24px;
  padding-top: 2px;
  text-align: left;
  /* .archive__row's flex `gap` is shared by both number->image and
     image->caption; pull the first one in by 15px on its own, leaving
     the second untouched. */
  margin-right: -15px;
}

.archive__image {
  flex: 0 0 auto;
  width: clamp(90px, 26vw, 150px);
  height: auto;
  display: block;
  cursor: pointer;
}

.archive__body {
  flex: 1 1 auto;
  min-width: 0;
  padding-top: 2px;
}

.archive__tag {
  margin-top: 6px;
  font-family: var(--font-tag);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* The arrow glyph (U+E443) lives in Droulers Clarendon itself — just
   the glyph, no circle around it. Hidden by default, shown only on
   the row matched by the page's URL hash. */
.archive__arrow {
  display: none;
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  align-items: center;
  justify-content: center;
  color: var(--text-color);
  text-decoration: none;
  font-size: 28pt;
  line-height: 1;
}

.archive__row.is-target .archive__arrow {
  display: flex;
}

@media (min-width: 768px) {
  .archive {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    column-gap: var(--grid-gutter);
  }

  /* Full-bleed so the sticky header's backdrop still masks the whole
     row width as it scrolls underneath. The filter bar and the photo
     grid both use the full 12 columns. */
  .archive__header {
    grid-column: 1 / -1;
  }

  .archive__filters {
    grid-column: 1 / -1;
  }

  .archive__list {
    grid-column: 1 / -1;
    grid-template-columns: repeat(3, 1fr);
    column-gap: calc(((var(--grid-gutter) * 2 + 10px) * 2) * 2 / 3);
    /* Fixed row height (matches the original first row: a 200px-tall
       image plus .archive__row's 20px top/bottom padding) so every row
       lines up regardless of content — an image taller than that is
       capped below and shrinks in proportion instead of stretching the
       grid row and throwing off every row after it. */
    grid-auto-rows: 240px;
  }

  .archive__image {
    width: auto;
    max-width: 160px;
    max-height: 200px;
  }
}

/* Desktop only: tablet (768px+) keeps the 3-column list above; wider
   screens step up to 4. */
@media (min-width: 1100px) {
  .archive__list {
    grid-template-columns: repeat(4, 1fr);
  }

  /* Fixed width instead of the 3-column layout's max-width — smaller
     and consistent across every photo (135px, matching #18's own
     rendered width there) instead of each photo filling up to 160px
     at its own aspect ratio, leaving more room for the caption text.
     object-fit keeps a narrow/tall outlier (taller than the 200px row
     cap at that width) shrinking in proportion instead of stretching,
     same as the max-width/max-height pair did before. */
  .archive__image {
    width: 135px;
    max-width: none;
    height: auto;
    max-height: 200px;
    object-fit: contain;
  }
}

/* ---------- about (below the homepage's first-screen hero) ---------- */
.about {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 48px;
  padding: 60px var(--grid-margin, 24px) 100px;
  text-align: left;
  /* body disables selection site-wide (it would otherwise get in the
     way of the stage's click-to-reveal interaction) — this is the
     actual readable copy, so selection/copy needs to work here. */
  user-select: text;
  /* .work-link (now an in-page "#about" anchor on the homepage) lands
     here with a little breathing room above, rather than flush against
     the very top edge. */
  scroll-margin-top: 20px;
  /* Inverted relative to the rest of the page: background becomes
     whichever accent color is currently selected, and everything that
     was either white or accent-colored text (titles, contacts, the
     "[n]" markers) becomes the fixed brown instead — --text-color is
     overridden here so every var(--text-color) use downstream (and
     .about__text/.note-ref, re-pointed at it below) picks it up too. */
  background: var(--accent);
  --text-color: var(--brown);
  color: var(--text-color);
}

.about__blocks {
  min-width: 0;
}

.about__block + .about__block {
  margin-top: 48px;
}

.about__title {
  font-size: 14px;
  line-height: 30px;
  text-transform: uppercase;
}

.about__text {
  margin-top: 4px;
  font-family: var(--font-tag);
  font-size: 19px;
  line-height: 23px;
  color: var(--text-color);
}

/* A "[n]" footnote marker in an about text, shown as the circled-digit
   glyph from Droulers Clarendon's icon set (the only one of the two
   fonts that has it) — white by default, pink on hover/focus. 2px
   smaller than the surrounding text, but pinned to the exact same
   line-height rather than inheriting it, so a mismatched value here
   (font metrics differ enough between the two families that "inherit"
   alone isn't reliably exact) can't stretch just the lines that happen
   to contain one. */
.note-ref {
  font-family: var(--font-text);
  font-size: 17px;
  line-height: 23px;
  color: inherit;
  transition: color 0.2s ease;
}

.note-ref:hover,
.note-ref:focus-visible {
  /* Literal white, not var(--text-color) — inside .about that variable
     is now the same brown the marker already is by default, which
     would make hover a no-op there. */
  color: #ffffff;
  outline: none;
}

/* Single shared preview image (not nested per-marker, since its
   horizontal position is pinned to the grid rather than following
   whichever line the hovered marker happens to wrap to) — shown/hidden
   and positioned vertically by JS to sit level with the hovered
   marker. Left-aligned to the about padding on mobile; to column 8 on
   desktop. No border, no rotation — shown flat and unstyled. */
.note-preview {
  position: absolute;
  top: 0;
  left: var(--grid-margin, 24px);
  width: 220px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
  z-index: 40;
}

.note-preview.is-visible {
  opacity: 1;
}

.note-preview img {
  display: block;
  width: 100%;
  height: auto;
}

@media (min-width: 768px) {
  .about {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    column-gap: var(--grid-gutter);
  }

  .about__blocks {
    grid-column: 1 / span 6;
  }

  .note-preview {
    left: auto;
    grid-column-start: 8;
  }
}

/* ---------- about typography — mobile only ---------- */
@media (max-width: 767px) {
  .about__title {
    font-size: 12px;
    line-height: 18px;
  }

  .about__text {
    font-size: 16px;
    line-height: 18px;
  }

  .note-ref {
    font-size: 14px;
    line-height: 18px;
  }
}

/* ---------- site footer (shared by both pages) ---------- */
.site-footer {
  display: flex;
  flex-direction: column;
  gap: 32px;
  padding: 60px var(--grid-margin, 24px) 60px;
  background: var(--bg);
  color: var(--text-color);
  font-family: var(--font-tag);
  font-size: 16px;
  line-height: 18px;
  /* See .about's own comment — same reason, same fix. */
  user-select: text;
}

.site-footer__address {
  white-space: pre-line;
}

.site-footer__contact > * {
  margin-top: 0;
}

.site-footer__email-row {
  /* Same gap as the about section's own title→text relationship
     ("Website" → "This website is..."), since Contact relates to
     Email the same way. Email/Instagram/LinkedIn themselves sit flush
     with no gap between them. */
  margin-top: 4px;
}

.site-footer__label {
  font-family: var(--font-text);
  font-weight: 400;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.site-footer__contact a {
  display: block;
  color: inherit;
  text-decoration: none;
}

.site-footer__contact a:hover,
.site-footer__contact a:focus-visible {
  text-decoration: underline;
}

.site-footer__email-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
}

.site-footer__email {
  display: block;
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: none;
}

.site-footer__email:hover,
.site-footer__email:focus-visible {
  text-decoration: underline;
}

.site-footer__copied {
  font-family: var(--font-text);
  font-size: 12px;
  color: var(--text-color);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.site-footer__copied.is-visible {
  opacity: 1;
}

@media (min-width: 768px) {
  .site-footer {
    flex-direction: row;
    gap: 120px;
    font-size: 19px;
    line-height: 21px;
  }

  .site-footer__label {
    font-size: 14px;
  }
}

/* ---------- work index: photo lightbox ---------- */
/* Sits below the sticky header's z-index (10) so the header stays
   visible/readable on top of the dark backdrop, matching the
   reference. Hidden while .work-link's own dot fills that same
   top-right spot; .lightbox__close takes over there once open. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 6;
  display: none;
  align-items: center;
  justify-content: center;
  /* Extra side clearance (vs. the 24px everywhere else) so the image
     never renders underneath the prev/next buttons, which sit close to
     the edges at this width; also leaves room at the bottom for the
     caption, which is positioned independently below. */
  padding: 100px 60px 110px;
  background: var(--bg);
  cursor: pointer;
}

.lightbox.is-open {
  display: flex;
}

body.lightbox-open .work-link {
  display: none;
}

.lightbox__image {
  display: block;
  /* Relative to .lightbox's own (padded) content box, not the raw
     viewport — a vw-based max-width ignored that padding and let the
     image overflow underneath the prev/next buttons regardless of how
     much side clearance was reserved for them. */
  max-width: 100%;
  max-height: 62svh;
  width: auto;
  height: auto;
  cursor: default;
}

/* Fixed to the same spot regardless of the image's own height, so the
   caption sits on the same baseline for every photo instead of
   trailing whichever height that photo happens to render at. */
.lightbox__caption {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 36px;
  color: #ffffff;
  text-align: center;
  cursor: default;
}

.lightbox__close,
.lightbox__prev,
.lightbox__next {
  display: none;
  position: fixed;
  /* Above the sticky header (10) and the work-link dot (30) it's
     standing in for — otherwise the header would swallow the click. */
  z-index: 31;
  background: none;
  border: none;
  color: var(--text-color);
  cursor: pointer;
  padding: 8px;
  line-height: 1;
}

body.lightbox-open .lightbox__close,
body.lightbox-open .lightbox__prev,
body.lightbox-open .lightbox__next {
  display: block;
}

.lightbox__close {
  /* Centered on the same point as .work-link's dot (top:20px + half its
     16px size, both directions) rather than matched top/right offsets —
     those would align a corner, not the center, once the glyph is
     twice the dot's rendered size. */
  top: 28px;
  right: calc(var(--grid-margin, 20px) + 8px);
  transform: translate(50%, -50%);
  font-size: 44px;
}

.lightbox__prev,
.lightbox__next {
  top: 50%;
  transform: translateY(-50%);
  font-size: 32px;
}

.lightbox__prev {
  left: 4px;
}

.lightbox__next {
  right: 4px;
}

@media (min-width: 768px) {
  .lightbox {
    padding: 100px 24px 110px;
  }

  .lightbox__prev {
    left: 20px;
  }

  .lightbox__next {
    right: 20px;
  }
}
