/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 15px 26px;
  border-radius: var(--r-pill);
  font-size: 15px;
  font-weight: 700;
  white-space: nowrap;
}
.btn .arrow { font-family: var(--font-mono); }
.btn--accent { background: var(--accent); color: var(--ink-on-accent); }
.btn--accent:hover { background: var(--accent-hover); color: var(--ink-on-accent); }
.btn--light { background: var(--fg); color: var(--bg); }
.btn--light:hover { background: var(--fg-body); color: var(--bg); }
.btn--ghost { border: 1px solid var(--line-strong); color: var(--fg); }
.btn--ghost:hover { border-color: var(--accent); color: var(--accent); }
.btn--dark { background: var(--bg); color: var(--accent); font-size: 16px; padding: 17px 30px; }
.btn--dark:hover { background: var(--bg-deep); color: var(--accent-hover); }
.btn--sm { padding: 13px 22px; font-size: 14.5px; }
.btn--nav { padding: 11px 18px; font-size: 14px; }
/* Nav CTA shown as a plain <span> on the page it links to (own-page,
   non-interactive) -- cancel .btn--accent's hover colour swap so it doesn't
   look clickable. */
.btn--static { cursor: default; }
.btn--static.btn--accent:hover { background: var(--accent); color: var(--ink-on-accent); }

/* Default styling assumes a dark ground; the yellow band overrides it below.
   Without the split, the hover colour lands black-on-black off the band. */
.email-link {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding-block: 3px;
  font-family: var(--font-mono);
  font-size: 14.5px;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 4px;
}
.email-link:hover { color: var(--accent-hover); }
.section--accent .email-link { color: var(--ink-on-accent); }
.section--accent .email-link:hover { color: var(--bg-deep); }

/* Compact nav */
.menu-toggle {
  display: none;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 10px 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--fg);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
}
.menu-toggle__bars { display: flex; flex-direction: column; gap: 3px; }
.menu-toggle__bars span { display: block; width: 16px; height: 1.5px; background: var(--accent); }

.menu {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  flex-direction: column;
  padding: 8px var(--gutter) 22px;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.menu[data-open="true"] { display: flex; }
/* `:not(.btn)` and `>` both matter — see the note on .nav > a in layout.css.
   The child combinator keeps this rule off the language switcher's nested
   link, which would otherwise be styled as a full menu row. */
.menu > a:not(.btn) {
  display: flex;
  align-items: center;
  min-height: 52px;
  border-bottom: 1px solid var(--line-soft);
  color: var(--fg);
  font-size: 17px;
  font-weight: 600;
}
.menu .lang { min-height: 52px; font-size: 13px; letter-spacing: 0.08em; }
/* 44px target here: this one is a touch surface. */
.menu .lang a { border-bottom: 0; min-height: 44px; padding-inline: 4px; font-size: 13px; }
.menu .btn { justify-content: center; min-height: 52px; margin-top: 8px; font-size: 16px; }

@media (max-width: 880px) {
  .nav { display: none; }
  .menu-toggle { display: flex; }
}

/* Above the compact breakpoint the panel must stay closed even if a stale
   data-open attribute survives a resize. */
@media (min-width: 881px) {
  .menu, .menu[data-open="true"] { display: none; }
}

/* Chip marquee */
.marquee {
  margin-top: clamp(40px, 5.5vw, 72px);
  padding-block: 20px;
  border-block: 1px solid var(--line-soft);
  background: var(--bg-deep);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* No gap on the row: the -50% keyframe must equal exactly one .marquee__set,
   and a row gap would make half the row wider than one set (visible jump on
   every loop). The set's own padding-right supplies the seam spacing. */
.marquee__row { display: flex; width: max-content; }
.marquee__row--a { animation: marquee-a 52s linear infinite; }
.marquee__row--b { animation: marquee-b 64s linear infinite; }
.marquee__set { display: flex; gap: 12px; padding-right: 12px; }

.chip {
  padding: 9px 17px;
  border: 1px solid rgba(231, 231, 231, 0.14);
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--fg-body);
  white-space: nowrap;
}
.chip--accent { border-color: rgba(247, 206, 20, 0.45); color: var(--accent); }
.chip--tint { border-color: rgba(191, 209, 255, 0.35); color: var(--on-navy); }
.chip--quiet { color: var(--fg-faint); }

/* Cards */
.card {
  background: var(--bg-deep);
  padding: clamp(24px, 3vw, 38px);
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.card__kicker { font-family: var(--font-mono); font-size: 12.5px; color: var(--fg-faint); letter-spacing: 0.03em; }
.card h3 { font-size: var(--step-h3); line-height: 1.12; font-weight: 700; letter-spacing: -0.02em; }

.cap {
  padding: clamp(22px, 2.4vw, 30px);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  display: flex;
  flex-direction: column;
  gap: 12px;
  transition: border-color var(--dur-fast) ease;
}
.cap:hover { border-color: rgba(247, 206, 20, 0.5); }
.cap .label { color: var(--accent); font-size: 11px; }
.cap h3 { font-size: 21px; font-weight: 700; letter-spacing: -0.015em; }
.cap .muted { text-wrap: wrap; } /* override body's text-wrap: pretty -- in this narrow column it breaks a line early to dodge a short last line, leaving visible slack */

/* Project cards */
/* The 1px seam is the grid gap showing the card's own background through.
   A border on the visual would sit on the wrong edge once auto-fit collapses
   the two columns into a stack. */
.project {
  margin-top: clamp(26px, 4vw, 44px);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--line);
  overflow: hidden;
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
}
.project__body { padding: clamp(26px, 3.4vw, 52px); display: flex; flex-direction: column; gap: 20px; background: var(--bg-deep); }
.project__body h2,
.project__body h3 { font-size: clamp(30px, 3.8vw, 52px); line-height: 1.02; font-weight: 800; letter-spacing: -0.03em; }
.project__body p { font-size: clamp(16px, 1.4vw, 19px); line-height: 1.55; color: var(--fg-muted); max-width: 44ch; }
.project__visual {
  min-height: 340px;
  padding: clamp(26px, 3vw, 44px);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--navy);
}
/* Used where a real screenshot sits in a phone frame -- a neutral panel
   reads better behind a photographed screen than the navy default above.
   Note: as of the projects/homepage sync this modifier is on every
   .project__visual in the markup, so the --navy default above is currently
   unreachable. Kept as the fallback for a future tile with no screenshot. */
.project__visual--muted { background: var(--bg-panel-muted); }

.project-sm { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--bg-deep); overflow: hidden; display: flex; flex-direction: column; }
.project-sm__body { padding: clamp(22px, 2.4vw, 32px); display: flex; flex-direction: column; gap: 14px; flex: 1; }
.project-sm__body h3 { font-size: clamp(22px, 2.2vw, 28px); line-height: 1.1; font-weight: 700; letter-spacing: -0.02em; }
.project-sm__body .last { margin-top: auto; }

/* Standalone links (a whole line to themselves, not inside a sentence) get a
   real target box. WCAG 2.2 2.5.8 exempts inline-in-a-sentence links but not
   these, so both tile variants need it — the small tiles and the large one. */
.project-sm__body a.last,
.project__body a.last { display: inline-flex; align-items: center; gap: 8px; min-height: 26px; }

/* Links that ARE inline in a sentence stay inline so they do not disturb the
   line box. Vertical padding on a genuinely inline element grows the hit area
   without changing layout, which buys a comfortable target for free.
   Only add selectors here that are display:inline — .email-link is
   inline-flex with its own min-height, so padding WOULD move layout there. */
.project__body p a,
.project-sm__body p a,
p.muted a,
p.label a { padding-block: 5px; }

.tags { display: flex; flex-wrap: wrap; gap: 8px; }
.tag {
  padding: 6px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  color: var(--fg-muted);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.tag--accent { color: var(--accent); }
.tag--tint { border-color: rgba(191, 209, 255, 0.35); color: var(--on-navy); }
.tag--green { border-color: rgba(74, 222, 128, 0.35); color: var(--green); }
.tag--sm { padding: 5px 11px; font-size: 10.5px; }

.facts { display: flex; flex-direction: column; gap: 11px; border-top: 1px solid var(--line); padding-top: 20px; }
.facts li { display: flex; gap: 12px; font-size: 14.5px; color: var(--fg-body); }
.facts .arrow { color: var(--accent); font-family: var(--font-mono); }

.portrait {
  width: min(360px, 100%);
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: 20px;
  border: 1px solid rgba(191, 209, 255, 0.45);
  box-shadow: 0 28px 80px rgba(0, 0, 0, 0.25);
}
/* Square sibling of .portrait: no border (unlike .portrait) and sized by
   its column's width like .portrait, but without the 360px cap -- this one
   is meant to be read, not just glanced at, so it gets the full column. */
.card-photo {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 20px;
  box-shadow: 0 28px 80px rgba(0, 0, 0, 0.25);
}

/* Phone frame: a CSS-only chassis (no device-mockup image asset) around a
   real screenshot, so it reads as "on a phone" rather than a floating
   rectangle. The notch and home indicator are pseudo-elements layered over
   the screen; the notch inherits the chassis colour to look cut from it. */
.phone-frame {
  position: relative;
  width: min(225px, 100%); /* 70% of the original 300px, then +7% */
  padding: 16px 10px;
  border-radius: 24px;
  background: #131417;
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 28px 80px rgba(0, 0, 0, 0.35);
}
/* Only the SortedSam tile's single phone gets the tilt + float -- a static
   pair (see .phone-duo) reads calmer for a two-screenshot flow. */
.phone-frame--tilt {
  transform: rotate(2.5deg);
  animation: phone-float 4s ease-in-out infinite;
}
.phone-frame::before {
  content: "";
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  width: 34%;
  height: 16px;
  background: inherit;
  border-radius: 0 0 8px 8px;
  z-index: 1;
}
.phone-frame::after {
  content: "";
  position: absolute;
  bottom: 6px;
  left: 50%;
  transform: translateX(-50%);
  width: 34%;
  height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.35);
}
.app-shot {
  display: block;
  width: 100%;
  aspect-ratio: 600 / 1333;
  object-fit: cover;
  border-radius: 14px;
}

/* Two static phones side by side (O2) -- no tilt/float, calmer than the
   single SortedSam phone since this is a flow of two screens, not one hero
   shot. height: 352px is hardcoded to match .shot-banner's fixed height
   exactly (not "approximately" via aspect-ratio/stretch math that silently
   failed -- see the phone-duo commit history). 142px is precalculated
   from that same 352px: subtract this panel's own 24px+24px padding
   (304px left), then the phone frame's 16px+16px bezel padding (272px
   left) -- at the screenshot's own 600:1333 ratio that's a ~122px-wide
   image, so a 142px frame (image + 10px bezel each side) fills it via
   plain box-model math, no forced stretch. */
.phone-duo {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 352px;
  gap: 75px;
  padding: 24px 16px;
  background: var(--bg-panel-muted);
  border-bottom: 1px solid var(--line);
}
.phone-duo .phone-frame {
  width: 142px;
  /* Same proportion as SortedSam's phone (24px on a 225px-wide frame) --
     percentage so it stays correct if 142px is ever retuned. */
  border-radius: 10.667%;
}
.phone-duo .app-shot { border-radius: 6.222%; } /* 14px on 225px, same proportion as SortedSam's screen */

/* A real explanatory diagram, not decorative chrome -- sized by its own
   aspect ratio and "contain" rather than cropped to a fixed banner height,
   so none of the pattern example is ever cut off. The PNG has real
   transparency (the label band in the middle is see-through by design), so
   the panel colour shows through it -- same muted ground as the SortedSam
   phone-frame panel. */
.shot-banner {
  height: 352px; /* fixed, not aspect-ratio -- matched pixel-for-pixel against
                    .phone-duo below rather than left to drift at whatever
                    width the two-column grid happens to give each card */
  padding-inline: 15%; /* shrinks the (now width-responsive) image roughly
                          30%; contain keeps it centred either way */
  border-bottom: 1px solid var(--line);
  background: var(--bg-panel-muted);
}
.shot-banner img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.page-hero {
  padding: clamp(58px, 10vw, 128px) var(--gutter) clamp(64px, 10vw, 118px);
  border-bottom: 1px solid var(--line-soft);
}
/* Contact only: the merged hero+content section is already tall (photo plus
   two stacked blocks of copy), so the shared top padding above just reads as
   extra distance from the header -- about/projects keep the full amount. */
.page-hero--compact { padding-top: clamp(24px, 5vw, 56px); }
.page-hero h1 {
  max-width: 13ch;
  margin-top: 18px;
  font-size: var(--step-h2-lg);
  line-height: 0.94;
  font-weight: 800;
  letter-spacing: -0.035em;
  text-transform: uppercase;
}
.page-hero .lead {
  max-width: 58ch;
  margin-top: 24px;
}
/* Composition, applied at every width. */
.page-hero--compact h1 { max-width: none; }
/* Forced break at the comma instead of leaving it to reflow -- at this
   column width, wrapping wherever the text happens to run out looks
   accidental. Smaller first line doubles as emphasis: "say hi" is the
   point of this page. */
.page-hero--compact h1 > span { display: block; }
.page-hero--compact h1 > span:first-child { font-size: 0.7em; }
/* Vertically centre the text column against the much taller photo next to
   it, instead of the shared .page-grid default of pinning both to the top. */
.page-hero--compact .page-grid { align-items: center; }

/* The dialed-down type scale exists only because the photo squeezes the text
   into roughly a 280-430px column, so it is scoped to the widths where that
   two-column layout is actually active. Below 761px the grid collapses and
   the text column is full width — there the page uses the site's normal
   scale. Unscoped, the clamp() minimums also applied on mobile, which left
   the body copy at 13px and the eyebrow at 9px: smaller than the footer
   links directly beneath them, and smaller than every other page. */
@media (min-width: 761px) {
  .page-hero--compact h1 { font-size: clamp(26px, 5.25vw, 72px); }
  .page-hero--compact .lead,
  .page-hero--compact .btn,
  .page-hero--compact .email-link { font-size: clamp(13px, 1.1vw, 16px); }
  .page-hero--compact .copy h2 { font-size: clamp(17px, 2.4vw, 30px); }
  .page-hero--compact .copy p { font-size: clamp(13px, 1.1vw, 16px); }
}
.page-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(280px, 0.45fr);
  gap: clamp(28px, 5vw, 76px);
  align-items: start;
}
/* Reversed weighting: use when the narrow visual comes first in the source and
   the prose deserves the wide column. */
.page-grid--visual-first { grid-template-columns: minmax(280px, 0.45fr) minmax(0, 0.85fr); }
/* Visual leads instead: use when the photo is the point and the text next to
   it is short -- gives the image column more room than .page-grid's default. */
.page-grid--photo-lead { grid-template-columns: minmax(0, 0.65fr) minmax(340px, 1.1fr); }
.copy {
  display: flex;
  flex-direction: column;
  gap: 22px;
}
.copy h2 {
  margin-top: 18px;
  font-size: var(--step-h2);
  line-height: 1.06;
  letter-spacing: -0.03em;
}
.copy h3 {
  font-size: 22px;
  line-height: 1.16;
}
.copy p,
.copy li {
  color: var(--fg-muted);
  font-size: clamp(16px, 1.2vw, 18px);
  line-height: 1.65;
}
.side-note {
  padding: 24px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--bg-deep);
  position: sticky;
  top: 100px; /* clears the ~75px sticky header */
}
.side-note p { margin-top: 12px; }

@media (max-width: 760px) {
  .page-grid,
  .page-grid--visual-first,
  .page-grid--photo-lead { grid-template-columns: 1fr; }
  .side-note { position: static; }
}

/* Rule-of-thumb band */
.quote { display: flex; flex-direction: column; gap: 20px; }
.quote .label { color: var(--accent); }
.quote p { font-size: clamp(24px, 3.6vw, 46px); line-height: 1.14; font-weight: 700; letter-spacing: -0.028em; }
.quote p span { display: block; text-align: right; }

/* Contact page easter egg: a waving-hand cursor that sways for the first
   5s after load (toggled by js/main.js, which cycles the -a../-e class
   through five frames rotated +-12deg around the wrist at the glyph's
   bottom-right, rather than the box centre, so it reads as a wave
   instead of a spin). Set on body so it inherits everywhere;
   .btn--static and .menu-toggle need their own override since they set
   an explicit cursor of their own that would otherwise win. */
body.cursor-wave-a,
body.cursor-wave-a .btn--static,
body.cursor-wave-a .menu-toggle {
  cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoLTEyIDI0IDI0KSI+PHRleHQgeT0iMjYiIGZvbnQtc2l6ZT0iMjgiPvCfkYs8L3RleHQ+PC9nPjwvc3ZnPg==") 16 16, auto;
}
body.cursor-wave-b,
body.cursor-wave-b .btn--static,
body.cursor-wave-b .menu-toggle {
  cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoLTYgMjQgMjQpIj48dGV4dCB5PSIyNiIgZm9udC1zaXplPSIyOCI+8J+RizwvdGV4dD48L2c+PC9zdmc+") 16 16, auto;
}
body.cursor-wave-c,
body.cursor-wave-c .btn--static,
body.cursor-wave-c .menu-toggle {
  cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoMCAyNCAyNCkiPjx0ZXh0IHk9IjI2IiBmb250LXNpemU9IjI4Ij7wn5GLPC90ZXh0PjwvZz48L3N2Zz4=") 16 16, auto;
}
body.cursor-wave-d,
body.cursor-wave-d .btn--static,
body.cursor-wave-d .menu-toggle {
  cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoNiAyNCAyNCkiPjx0ZXh0IHk9IjI2IiBmb250LXNpemU9IjI4Ij7wn5GLPC90ZXh0PjwvZz48L3N2Zz4=") 16 16, auto;
}
body.cursor-wave-e,
body.cursor-wave-e .btn--static,
body.cursor-wave-e .menu-toggle {
  cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoMTIgMjQgMjQpIj48dGV4dCB5PSIyNiIgZm9udC1zaXplPSIyOCI+8J+RizwvdGV4dD48L2c+PC9zdmc+") 16 16, auto;
}
