/* ===================================================================
   Ruyi NQC — studio page
   Mobile first, dark by default. Every animation is transform/opacity
   only, so the compositor handles it without touching layout.
   =================================================================== */

:root {
  color-scheme: dark;

  --ink:       #14100F;   /* the page */
  --ink-2:     #1C1715;   /* raised surfaces */
  --ink-3:     #272120;   /* hairlines */
  --cream:     #FDF8F5;
  --cream-dim: #C9BFBA;
  --muted:     #8B807B;

  --rose:      #E8B4B8;   /* the invitations side of the studio */
  --rose-deep: #C98B87;
  --sky:       #9FC4DE;   /* the web & app side */
  --gold:      #D9B48F;

  /* Colours that are not a plain swap of one of the above — translucent
     layers, a shadow, a placeholder — pulled out of the rules that used to
     hard-code them so the light theme has something to override. */
  --nav-bg:      rgba(20, 16, 15, .7);   /* the fixed bar, over whatever scrolls under it */
  --placeholder: #8A7F7A;   /* form placeholders — real guidance is written in these, so they clear 4.5:1 on --ink */
  --art-shadow:  rgba(0, 0, 0, .55);
  --grid-line:   var(--ink-3);
  --glow-rose:   var(--rose-deep);
  --glow-sky:    var(--sky);
  --glow-alpha:  .5;

  /* The ground behind a work card's artwork. Two of the three plates are
     filled edge to edge by their own image, so this only ever shows on the
     invitations card, whose two previews are fanned in the middle of it.
     It gets a light twin below rather than staying dark in both themes:
     a black plate inside a white card reads as a hole in the page. */
  --art-plate: #14100F;
  /* Text and chrome sitting ON that plate. It cannot read through --cream:
     that token is near-black in the light theme, which would put dark text
     on a surface that stays dark in both. */
  --art-plate-ink: #FDF8F5;

  /* One family for the whole page — Instrument Sans carries the entire type
     hierarchy on weight alone (400 body up to 700 for h1), so there is no
     second, serif face to keep in step with it any more. Both variables
     still exist and both still point at it: html[lang="ar"] below
     overrides them together to swap the page onto Amiri, and every rule
     that reads var(--serif) or var(--sans) needs that swap to keep working
     without being touched itself. */
  --serif: 'Instrument Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --sans:  'Instrument Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;

  --gut:  clamp(1.25rem, 5vw, 3rem);
  --max:  72rem;
  --ease: cubic-bezier(.22, .61, .36, 1);
  --nav-h: 4.5rem;
}

/* ==================================================================
   LIGHT THEME
   ==================================================================
   Set as data-theme="light" on <html> by the inline snippet in each
   page's <head> — from the visitor's saved choice, or from the OS
   preference when they have not made one. Deliberately an attribute
   rather than a bare `prefers-color-scheme` media query: the toggle has
   to be able to beat the OS in both directions, and start.html — which
   loads this stylesheet but carries no toggle — stays dark rather than
   flipping underneath a page that was never checked in light.

   Only the tokens are redefined. Every rule below reads through them, so
   nothing else in this file (or in quote.css) needs a light-mode twin.

   The names stay as they are and stop being literal: --ink is "the page"
   and --cream is "the text on it", which is what every rule means by
   them. Reading them as colour names in here will mislead — --cream is
   near-black in this block. Their ORDER is what carries: --ink → --ink-2
   is still page → raised, so a card lifts off the page and an input set
   back to --ink still reads as recessed inside one.

   The four accents are redrawn rather than reused: #E8B4B8 on white is
   about 1.6:1, so the dark theme's pale rose/sky/gold would fail as text
   the moment the ground went light. These are the same hues taken deep
   enough to clear 4.5:1 on both --ink and --ink-2. */
html[data-theme="light"] {
  color-scheme: light;

  --ink:       #F7F2ED;   /* the page, and anything recessed into a card */
  --ink-2:     #FFFFFF;   /* raised surfaces */
  --ink-3:     #E4DAD2;   /* hairlines */
  --cream:     #1A1412;   /* primary text */
  --cream-dim: #554944;   /* secondary text */
  --muted:     #7A6C66;   /* labels, hints, tertiary text */

  --rose:      #A0504C;
  --rose-deep: #82393A;
  --sky:       #2E6E96;
  --gold:      #8A6224;

  --nav-bg:      rgba(255, 255, 255, .72);
  --placeholder: #766862;
  --art-shadow:  rgba(60, 42, 36, .16);
  /* A shade under --ink, so the plate still reads as recessed inside the
     white card the way the dark theme's near-black one does. */
  --art-plate:   #EFE6DF;
  --grid-line:   #DDD0C6;   /* a touch stronger than --ink-3: the grid is painted at half opacity */
  /* The blooms keep the ORIGINAL pale tints here rather than the deepened
     accents above. They are 70px-blurred washes, never text — the deep
     versions read as two grey bruises on cream. */
  --glow-rose:   #E8B4B8;
  --glow-sky:    #9FC4DE;
  --glow-alpha:  .55;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: var(--nav-h);
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--cream);
  font: 400 clamp(1rem, 2.4vw, 1.0625rem)/1.7 var(--sans);
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; display: block; }
a { color: inherit; }

h1, h2, h3 { margin: 0; font-family: var(--serif); line-height: 1.08; }
h1 { font-weight: 700; }
h2 { font-weight: 600; }
h3 { font-weight: 600; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* Focus is never removed, only restyled — this page is one long form funnel
   and keyboard users have to be able to see where they are. */
:focus-visible { outline: 2px solid var(--rose); outline-offset: 3px; border-radius: 2px; }

.wrap { width: min(100% - 2 * var(--gut), var(--max)); margin-inline: auto; }

/* The eyebrow above every section heading. */
.eyebrow {
  margin: 0 0 1rem;
  font-size: .7rem;
  font-weight: 500;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--rose-deep);
}

.section { padding: clamp(4.5rem, 12vw, 8rem) 0; }
.section__title {
  font-size: clamp(2.1rem, 6.5vw, 3.6rem);
  letter-spacing: -.01em;
}
.section__lede {
  margin: 1.1rem 0 0;
  max-width: 34rem;
  color: var(--cream-dim);
}

/* ==================================================================
   The mark
   ==================================================================
   Drawn rather than loaded: it is four arcs and a dot, so an inline SVG
   is smaller than any image of it, stays crisp at every size, and can
   take its colour from currentColor.
   Sizing lives on the .navbar__mark / .foot__mark side (a width plus
   aspect-ratio); this class must not set width or height of its own. An
   explicit `height: 100%` here beats aspect-ratio outright, which stretched
   the mark to the full height of its column and shoved the whole hero off
   the bottom of the screen.

   The stroke/fill are presentation attributes on the shapes rather than
   rules here, so the one filled shape — the dot — can opt out. A blanket
   `.mark circle { fill: none }` would beat its own fill="currentColor",
   because presentation attributes lose to any CSS rule at all. */
.mark { overflow: visible; }

/* ==================================================================
   0. NAVBAR
   ================================================================== */
.navbar {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 50;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  background: var(--nav-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-block-end: 1px solid var(--ink-3);
}
.navbar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: 100%;
}

.navbar__brand {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  font-size: .74rem;
  font-weight: 500;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--cream);
  text-decoration: none;
  white-space: nowrap;
}
.navbar__mark { width: 1.5rem; aspect-ratio: 1; color: var(--rose); }

/* Bare button, drawn as two bars rather than an icon font or SVG import —
   it is two divs, so there is nothing to load and nothing to keep in sync
   with the mark's stroke colour. */
.navbar__toggle {
  display: grid;
  gap: .3rem;
  width: 2.5rem;
  height: 2.5rem;
  place-items: center;
  border: 1px solid var(--ink-3);
  border-radius: .6rem;
  background: transparent;
  cursor: pointer;
}
.navbar__bar { width: 1.1rem; height: 1px; background: var(--cream); }

/* Collapsed to a dropdown under the bar on narrow screens; the media query
   below turns it back into a plain inline row once there is room for one. */
.navbar__menu {
  position: absolute;
  inset-block-start: 100%;
  inset-inline: 0;
  display: grid;
  gap: .25rem;
  padding: .75rem var(--gut) 1.25rem;
  background: var(--ink);
  border-block-end: 1px solid var(--ink-3);
  clip-path: inset(0 0 100% 0);
  opacity: 0;
  pointer-events: none;
  transition: clip-path .3s var(--ease), opacity .25s var(--ease);
}
.navbar.is-open .navbar__menu {
  clip-path: inset(0 0 0 0);
  opacity: 1;
  pointer-events: auto;
}
.navbar__link {
  padding: .7rem .25rem;
  color: var(--cream-dim);
  text-decoration: none;
  font-size: .95rem;
  transition: color .2s var(--ease);
}
.navbar__link:hover { color: var(--cream); }
.navbar__cta { justify-self: start; margin-top: .5rem; }

/* Language and theme sit together as one row of preferences, so they stay
   side by side in the mobile dropdown instead of stacking as two more
   entries in the navbar's grid. */
.navbar__prefs { display: flex; align-items: center; gap: .5rem; margin: .5rem 0; }

.lang-switch { display: inline-flex; gap: .35rem; }
.lang-switch__btn {
  padding: .35rem .6rem;
  border: 1px solid var(--ink-3);
  border-radius: .4rem;
  background: transparent;
  color: var(--cream-dim);
  font: 500 .68rem/1 var(--sans);
  letter-spacing: .06em;
  cursor: pointer;
  transition: border-color .2s var(--ease), color .2s var(--ease);
}
.lang-switch__btn:hover { border-color: var(--rose-deep); }
.lang-switch__btn[aria-current="true"] { border-color: var(--rose-deep); color: var(--cream); }

@media (min-width: 48rem) {
  .navbar__toggle { display: none; }
  .navbar__menu {
    position: static;
    display: flex;
    align-items: center;
    gap: clamp(1.5rem, 3vw, 2.5rem);
    padding: 0;
    background: none;
    border: 0;
    clip-path: none;
    opacity: 1;
    pointer-events: auto;
    transition: none;
  }
  .navbar__link { padding: 0; font-size: .82rem; letter-spacing: .03em; }
  .navbar__cta { margin: 0; padding: .8rem 1.4rem; }
  .navbar__prefs { margin: 0; }
}

/* ==================================================================
   THEME TOGGLE
   ==================================================================
   Shared by the studio page's navbar and the quote page's top bar.

   A single button rather than a light/dark/system triple: the OS
   preference is already the starting point, so the only thing left to
   express is "not that one", and one tap says it. Marked up as a toggle
   button — the label names what is being switched and aria-pressed
   carries whether it is on, so the state survives the icon being the
   only visual cue. theme.js keeps that attribute in step.

   The icon shows the destination, not the current state: a sun while
   dark (tap for light), a moon while light. */
.theme-toggle {
  display: inline-grid;
  place-items: center;
  width: 2rem; height: 2rem;
  padding: 0;
  border: 1px solid var(--ink-3);
  border-radius: .4rem;
  background: transparent;
  color: var(--cream-dim);
  cursor: pointer;
  transition: border-color .2s var(--ease), color .2s var(--ease);
}
.theme-toggle:hover { border-color: var(--rose-deep); color: var(--cream); }
.theme-toggle__icon { width: 1.05rem; height: 1.05rem; }
.theme-toggle__icon--moon { display: none; }
html[data-theme="light"] .theme-toggle__icon--sun { display: none; }
html[data-theme="light"] .theme-toggle__icon--moon { display: block; }

/* ==================================================================
   1. HERO
   ================================================================== */
.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding: clamp(6rem, 16vh, 9rem) 0 clamp(4rem, 10vh, 6rem);
  overflow: hidden;
  isolation: isolate;
}

/* Two slow, huge, blurred blooms drifting behind the type. `filter: blur`
   on a static element is painted once and then only transformed, so this
   costs nothing per frame — an animated gradient would repaint every one. */
.hero__glow {
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  filter: blur(70px);
  opacity: var(--glow-alpha);
  pointer-events: none;
  will-change: transform;
}
.hero__glow--rose {
  width: min(70vw, 34rem); aspect-ratio: 1;
  top: -12%; left: -18%;
  background: radial-gradient(circle, var(--glow-rose), transparent 68%);
  animation: drift1 22s var(--ease) infinite alternate;
}
.hero__glow--sky {
  width: min(62vw, 30rem); aspect-ratio: 1;
  bottom: -16%; right: -14%;
  background: radial-gradient(circle, var(--glow-sky), transparent 68%);
  animation: drift2 26s var(--ease) infinite alternate;
}
@keyframes drift1 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(14%, 10%, 0) scale(1.16); }
}
@keyframes drift2 {
  from { transform: translate3d(0, 0, 0) scale(1.1); }
  to   { transform: translate3d(-12%, -12%, 0) scale(1); }
}

/* A fine grid, faded out towards the bottom, to keep the dark from going
   flat. Pure CSS — no image request. */
.hero__grid {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 4.5rem 4.5rem;
  opacity: .5;
  -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 35%, #000, transparent 100%);
          mask-image: radial-gradient(ellipse 70% 60% at 50% 35%, #000, transparent 100%);
  pointer-events: none;
}

.hero__name {
  margin: 0 0 1.4rem;
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .42em;
  text-transform: uppercase;
  color: var(--cream-dim);
}

.hero__title {
  font-size: clamp(2.9rem, 11vw, 7rem);
  letter-spacing: -.015em;
}
.hero__title em {
  font-style: italic;
  /* Instrument Sans' axis bottoms out at 400 — its lightest weight — where
     Cormorant went down to 300; this is the lightest this face gets. */
  font-weight: 400;
  /* The gradient rides on the text itself; background-clip needs the colour
     knocked out or the fill paints over it. */
  background: linear-gradient(100deg, var(--rose), var(--gold) 55%, var(--sky));
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}

.hero__lede {
  margin: clamp(1.5rem, 4vw, 2rem) auto 0;
  max-width: 32rem;
  font-size: clamp(1.02rem, 2.6vw, 1.15rem);
  color: var(--cream-dim);
}

.hero__cta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: .85rem;
  margin-top: clamp(2rem, 5vw, 2.75rem);
}

/* With no artwork beside it, the copy is the whole hero: centre-aligned
   within the page's usual .wrap measure so every line reads as one
   composed unit instead of sitting in a leftover left column. */
.hero .wrap {
  text-align: center;
}

/* ==================================================================
   Buttons
   ================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  padding: .95rem 1.6rem;
  border: 1px solid var(--ink-3);
  border-radius: 999px;
  background: transparent;
  color: var(--cream);
  font: 500 .78rem/1 var(--sans);
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: transform .25s var(--ease), background-color .25s var(--ease),
              border-color .25s var(--ease), color .25s var(--ease);
}
.btn:hover { transform: translateY(-2px); border-color: var(--rose-deep); }

.btn--fill {
  border-color: transparent;
  background: var(--cream);
  color: var(--ink);
}
.btn--fill:hover { background: var(--rose); }

.btn--wide { width: 100%; justify-content: center; padding-block: 1.1rem; }
.btn[disabled] { opacity: .55; cursor: not-allowed; transform: none; }

/* ==================================================================
   2. MARQUEE — the catchy strip between hero and work
   ================================================================== */
.strip {
  padding: 1.15rem 0;
  border-block: 1px solid var(--ink-3);
  background: var(--ink-2);
  overflow: hidden;
}
.strip__track {
  display: flex;
  width: max-content;
  animation: slide 32s linear infinite;
}
.strip:hover .strip__track { animation-play-state: paused; }
/* The trailing space is padding on the item, not a gap on the track. With a
   gap, the track measures item + gap + item, so -50% advances by only
   item + gap/2 and the loop jumps half a gap every lap. As padding it is part
   of the item, both copies are identical, and -50% is exactly one lap. */
.strip__item {
  display: flex;
  align-items: center;
  gap: 3rem;
  padding-right: 3rem;
  font-family: var(--serif);
  font-size: clamp(1.15rem, 3.5vw, 1.6rem);
  font-style: italic;
  white-space: nowrap;
  color: var(--cream-dim);
}
.strip__dot { color: var(--rose-deep); font-style: normal; }
@keyframes slide { to { transform: translate3d(-50%, 0, 0); } }

/* ==================================================================
   3. WORK
   ================================================================== */
.grid2 {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.5rem);
  margin-top: clamp(2.5rem, 6vw, 3.5rem);
}
@media (min-width: 46rem) { .grid2 { grid-template-columns: 1fr 1fr; } }

/* The work section runs to three cards, so it gets its own tracks rather
   than .grid2's pair — services still wants exactly two.

   Three into two columns leaves an orphan in the second row, which reads as
   a card that failed to load. Between the one-up and three-up breakpoints
   the last card takes the full width instead. It can carry it: its plate
   holds two centred previews, not a photograph, so a wider plate just gives
   them more room. */
.grid3 {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.5rem);
  margin-top: clamp(2.5rem, 6vw, 3.5rem);
}
@media (min-width: 46rem) {
  .grid3 { grid-template-columns: 1fr 1fr; }
  .grid3 > :last-child { grid-column: 1 / -1; }
}
@media (min-width: 68rem) {
  .grid3 { grid-template-columns: repeat(3, 1fr); }
  .grid3 > :last-child { grid-column: auto; }
}

/* The invitation card's plate does not take .work__shot's 16:10: that ratio
   fits a photo or a video, and this card holds neither — two PORTRAIT
   previews, fanned. Locked into a landscape frame at any width, they end up
   as a small island in a mostly-empty rectangle, which is what was showing
   through as "just a black box". Sizing the plate off its own content
   instead — the same clamp() .opt-card__stack uses on the quote page these
   previews come from — means it is never taller than the previews actually
   need, at every breakpoint, independent of the column width the grid
   above hands it. */
.grid3 > :last-child .work__shot { aspect-ratio: auto; height: clamp(11rem, 17vw, 14.5rem); }

/* A column rather than a plain block: grid already stretches all three cards
   to the tallest of the row, and the one with the longest note used to leave
   the other two with dead space under their link. As a flex column the link
   takes `margin-top: auto` and sits on the floor of every card, so the slack
   goes above it and the three read as one row instead of three ragged ones. */
.work {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: clamp(1.75rem, 4vw, 2.5rem);
  border: 1px solid var(--ink-3);
  border-radius: 1.25rem;
  background: var(--ink-2);
  text-decoration: none;
  overflow: hidden;
  transition: transform .3s var(--ease), border-color .3s var(--ease);
}
.work:hover { transform: translateY(-4px); border-color: var(--rose-deep); }

/* A wash that only shows on hover, tinted per card by --tint. */
.work::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 70% 90% at 80% 0%, var(--tint), transparent 70%);
  opacity: 0;
  transition: opacity .35s var(--ease);
  pointer-events: none;
}
.work:hover::before { opacity: .16; }

.work__shot {
  position: relative;
  flex: none;
  aspect-ratio: 16 / 10;
  margin-bottom: 1.6rem;
  border: 1px solid var(--ink-3);
  border-radius: .7rem;
  background: var(--art-plate);
  display: grid;
  place-items: center;
  overflow: hidden;
}
/* Every plate carries a piece of the project's own artwork — Mirayti's title
   sequence, Match & Attack's cover, two of the invitation designs — rather
   than a screenshot of a whole page, which dates the moment the layout around
   it changes. The first two cover their plate edge to edge, so --art-plate is
   only ever visible on the invitations card, and it follows the theme with
   the rest of the page.

   object-fit is what lets a source that was not cut to 16:10 fill the plate
   without distorting. Mirayti's clip is already exactly 16:10 by the time it
   ships (tools/prepare-studio.mjs widens it), so for the <video> cover is a
   no-op — it is there for the day the clip is re-cut. */
.work__art { display: block; width: 100%; height: 100%; object-fit: cover; }

/* The invitation card's plate: the same two previews, fanned, that open the
   quote page (.opt-card__stack in quote.css). Everything is sized off the
   plate's own height rather than the viewport, because this card is one of
   three narrow columns on a wide screen and a full-width banner just below
   that breakpoint — a vw-based size would be wrong at one end or the other.
   The offsets are percentages of the preview's own width for the same
   reason, so the fan keeps its shape at every size. */
.work__stack { position: absolute; inset: 0; display: grid; place-items: center; }

/* A pool of the card's own tint under the previews. Two things need it: the
   plate this card gets between the two-up and three-up breakpoints is wide
   enough to leave a lot of bare ground either side of them, and on a plate
   this dark a navy invitation has almost nothing to sit against. */
.work__stack::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 45% 78% at 50% 52%, var(--tint), transparent 72%);
  opacity: .16;
}
.work__stack-img {
  position: absolute;
  height: 90%;
  aspect-ratio: 2 / 3;
  border-radius: .5rem;
  overflow: hidden;
  border: 1px solid var(--ink-3);
  box-shadow: 0 .6rem 1.6rem var(--art-shadow);
  transition: transform .3s var(--ease);
}
.work__stack-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.work__stack-img--back { z-index: 1; transform: translateX(-30%) rotate(-6deg); filter: brightness(.82); }
.work__stack-img--front { z-index: 2; transform: translateX(30%) rotate(5deg); }
.work:hover .work__stack-img--back { transform: translateX(-38%) rotate(-9deg); }
.work:hover .work__stack-img--front { transform: translateX(38%) rotate(8deg); }

.work__kind {
  margin: 0 0 .5rem;
  font-size: .66rem;
  font-weight: 500;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--muted);
}
.work__name {
  position: relative;
  font-size: clamp(1.5rem, 4vw, 1.95rem);
}
.work__note { position: relative; margin: .6rem 0 0; color: var(--cream-dim); font-size: .95rem; }
.work__go {
  position: relative;
  display: inline-flex;
  align-self: flex-start;   /* stay as wide as its own text, not the card */
  align-items: center;
  gap: .45rem;
  margin-top: auto;
  padding-top: 1.1rem;
  font-size: .74rem;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--rose);
}
.work:hover .work__go .arrow { transform: translateX(4px); }
.arrow { transition: transform .25s var(--ease); }

/* ==================================================================
   POSTS
   ==================================================================
   Reels are shot portrait, so these cards are the one place on the page
   that isn't landscape. auto-fill rather than a fixed track count: the
   number of posts changes every time one is added, and a grid that counts
   its own columns never has to be edited to match. */
.posts {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.5rem);
  margin-top: clamp(2.5rem, 6vw, 3.5rem);
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}

.post {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: clamp(1rem, 2.5vw, 1.25rem);
  border: 1px solid var(--ink-3);
  border-radius: 1.25rem;
  background: var(--ink-2);
  text-decoration: none;
  transition: transform .3s var(--ease), border-color .3s var(--ease);
}
.post:hover { transform: translateY(-4px); border-color: var(--rose-deep); }

/* 4:5 rather than a reel's true 9:16 — a full-height portrait plate would
   push the caption of even a three-up row below the fold. The clip is
   centred and cropped to it, which is where the interesting half of a reel
   sits anyway. */
.post__media {
  position: relative;
  aspect-ratio: 4 / 5;
  margin-bottom: .9rem;
  border-radius: .7rem;
  overflow: hidden;
  background: var(--art-plate);
  display: grid;
  place-items: center;
}
.post__art { display: block; width: 100%; height: 100%; object-fit: cover; }

/* The stand-in a card wears until its clip has been baked: a quiet wash in
   the studio's own colours, so an un-shot card still reads as deliberate
   rather than as an image that failed. */
.post--soon .post__media {
  background:
    radial-gradient(ellipse 70% 55% at 30% 25%, var(--rose-deep), transparent 70%),
    radial-gradient(ellipse 70% 55% at 75% 80%, var(--gold), transparent 70%),
    var(--art-plate);
}

.post__badge {
  position: absolute;
  top: .6rem;
  inset-inline-start: .6rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .25rem .55rem;
  border-radius: 999px;
  background: rgba(20, 16, 15, .62);
  backdrop-filter: blur(6px);
  color: var(--art-plate-ink);
  font-size: .62rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.post__badge svg { width: .7rem; height: .7rem; }

/* Three lines, then an ellipsis. A caption is written for Instagram, where
   it runs as long as it likes; the card only has to say enough that the
   click is an informed one. */
.post__caption {
  margin: 0 0 .9rem;
  color: var(--cream-dim);
  font-size: .92rem;
  line-height: 1.5;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

.post__go {
  margin-top: auto;          /* pinned to the bottom, so a short caption and
                                a clamped one still line their links up */
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  font-size: .68rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--rose);
}
.post:hover .post__go .arrow { transform: translateX(4px); }

.posts__all { margin-top: clamp(1.5rem, 4vw, 2rem); }

/* ==================================================================
   4. SERVICES
   ================================================================== */
.serve {
  display: flex;
  flex-direction: column;
  padding: clamp(1.75rem, 4vw, 2.5rem);
  border: 1px solid var(--ink-3);
  border-radius: 1.25rem;
  background: var(--ink-2);
}
.serve__icon {
  width: 2.75rem; aspect-ratio: 1;
  margin-bottom: 1.4rem;
  color: var(--accent, var(--rose));
}
.serve__name { font-size: clamp(1.5rem, 4vw, 1.95rem); }
.serve__note { margin: .7rem 0 1.3rem; color: var(--cream-dim); font-size: .95rem; }
.serve__list { margin: 0 0 1.75rem; padding: 0; list-style: none; display: grid; gap: .55rem; }
.serve__list li {
  position: relative;
  padding-left: 1.4rem;
  font-size: .92rem;
  color: var(--cream-dim);
}
.serve__list li::before {
  content: '';
  position: absolute;
  left: 0; top: .62em;
  width: .42rem; height: .42rem;
  border-radius: 50%;
  background: var(--accent, var(--rose));
}
/* auto pins it to the card's bottom edge via the flex column above, so both
   cards' buttons line up even when their bullet lists run to different
   lengths; the list's own bottom margin sets the minimum space above it. */
.serve__cta { margin-top: auto; align-self: flex-start; }

/* ==================================================================
   5. THE BRIEF FORM
   ================================================================== */
.brief { position: relative; }
.brief__card {
  margin-top: clamp(2.5rem, 6vw, 3.5rem);
  padding: clamp(1.5rem, 5vw, 3rem);
  border: 1px solid var(--ink-3);
  border-radius: 1.5rem;
  background: var(--ink-2);
}

.form { display: grid; gap: 1.1rem; }
@media (min-width: 42rem) {
  .form { grid-template-columns: 1fr 1fr; }
  .form .field--full, .form .form__foot { grid-column: 1 / -1; }
}

/* align-content:start is load-bearing. A .field is a grid inside a grid row
   it shares with the other column, and a row is only as tall as its tallest
   field — the discount code, which carries a hint line the others do not.
   Left to stretch, the shorter field's own rows grow to fill that extra
   height: its label drifts down and its control grows taller than the one
   beside it, which is exactly how the select and the discount box came to
   sit at two different heights. Packing the rows to the top keeps every
   label and every control on the same line across both columns. */
.field { display: grid; gap: .5rem; align-content: start; }
.field__lbl {
  font-size: .7rem;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
}
.field__in {
  width: 100%;
  padding: .85rem 1rem;
  border: 1px solid var(--ink-3);
  border-radius: .6rem;
  background: var(--ink);
  color: var(--cream);
  font: 400 1rem/1.5 var(--sans);
  transition: border-color .2s var(--ease);
}
.field__in::placeholder { color: var(--placeholder); }
.field__in:focus { border-color: var(--rose-deep); outline: none; }
.field__in:focus-visible { outline: 2px solid var(--rose); outline-offset: 2px; }
.field__in--area { resize: vertical; min-height: 7rem; }

/* The native arrow is drawn in the OS's own colours, which on the dark field
   reads as a light-mode artefact. Hidden, and replaced with a painted one —
   in --cream-dim, so it follows the theme along with the text beside it. */
select.field__in {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--cream-dim) 50%),
                    linear-gradient(135deg, var(--cream-dim) 50%, transparent 50%);
  background-position: right 1.15rem top 55%, right .85rem top 55%;
  background-size: .35rem .35rem, .35rem .35rem;
  background-repeat: no-repeat;
}

/* The discount code sits apart from the rest — it is the one field that is
   about price, not about the work, and it reads as a reward rather than
   another thing to fill in. */
.field--code .field__in {
  border-style: dashed;
  border-color: var(--gold);
  letter-spacing: .18em;
  text-transform: uppercase;
}
.field--code .field__in::placeholder { letter-spacing: .12em; text-transform: none; }
.field__hint { font-size: .78rem; color: var(--muted); }

/* The honeypot. Not display:none — some bots skip anything hidden that way,
   and it is the ones that fill everything we are trying to catch. Taken out
   of flow instead, off-screen, and out of the grid's column count. */
.hp {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.form__foot { display: grid; gap: .9rem; margin-top: .4rem; }
.form__status { margin: 0; min-height: 1.4rem; font-size: .88rem; color: var(--cream-dim); }
/* A status worth saying is worth seeing: the rule and the weight are what
   separate it from the note sitting under it, which used to be the same
   size and nearly the same colour. */
.form__status.is-bad,
.form__status.is-good {
  padding-inline-start: .7rem;
  border-inline-start: 3px solid currentColor;
  font-weight: 500;
}
.form__status.is-bad { color: var(--rose); }
.form__status.is-good { color: var(--sky); }

/* ==================================================================
   6. FOOTER
   ================================================================== */
.foot {
  padding: clamp(3.5rem, 9vw, 5.5rem) 0 clamp(2rem, 5vw, 3rem);
  border-top: 1px solid var(--ink-3);
  background: var(--ink-2);
}
.foot__top { display: grid; gap: 2.5rem; }
@media (min-width: 46rem) {
  .foot__top { grid-template-columns: 1fr auto; align-items: start; }
}
.foot__mark { width: 2.6rem; aspect-ratio: 1; color: var(--rose); margin-bottom: 1rem; }
.foot__say { margin: 0; max-width: 24rem; color: var(--cream-dim); font-size: .95rem; }

/* The credit line, quieter than the blurb above it so it reads as a
   signature rather than a second sentence. The name keeps the brighter
   colour: it is the part worth reading. */
.foot__by { margin: .9rem 0 0; font-size: .82rem; color: var(--muted); }
.foot__by-name { color: var(--cream-dim); }

.foot__links { display: grid; gap: .7rem; }
.foot__link {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  color: var(--cream);
  text-decoration: none;
  font-size: .95rem;
}
.foot__link:hover { color: var(--rose); }
.foot__link svg { width: 1.05rem; height: 1.05rem; flex: none; }

.foot__base {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem 1.25rem;
  align-items: center;
  justify-content: space-between;
  margin-top: clamp(2.5rem, 6vw, 3.5rem);
  padding-top: 1.5rem;
  border-top: 1px solid var(--ink-3);
  font-size: .78rem;
  color: var(--muted);
}
.foot__base p { margin: 0; }

/* ==================================================================
   Scroll reveals
   ==================================================================
   Gated on BOTH `.js` and no-preference, so a failed script or a reduced
   motion setting leaves every section at its visible resting state rather
   than permanently blank. */
@media (prefers-reduced-motion: no-preference) {
  .js .reveal > * {
    opacity: 0;
    transform: translate3d(0, 22px, 0);
  }
  /* Must out-specify `.js .reveal > *`: `.reveal.is-in > *` alone ties on
     specificity and loses on order, which would blank the page for good. */
  .js .reveal.is-in > * {
    opacity: 1;
    transform: none;
    transition: opacity .8s var(--ease), transform .8s var(--ease);
    transition-delay: calc(var(--i, 0) * .09s);
  }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* ==================================================================
   LANGUAGE — ARABIC
   ==================================================================
   Set by i18n.js on <html> when Arabic is picked. Instrument Sans carries
   no Arabic glyphs, so the swap happens at the --serif/--sans tokens
   themselves — every rule in this file already reads through those two
   variables, so retargeting them here is enough to move the whole page
   onto Amiri without touching each rule again.

   The rest is undoing two Latin-specific habits that read as broken on
   Arabic script rather than as a design choice: wide uppercase tracking
   fights the script's joined letterforms, and Amiri only shipped here in
   its upright cut, so an italic style would otherwise be faked with a
   mechanical slant instead of a real drawn one. */
html[lang="ar"] {
  --serif: 'Amiri', 'Times New Roman', serif;
  --sans: 'Amiri', Tahoma, sans-serif;
}
html[lang="ar"] .eyebrow,
html[lang="ar"] .navbar__brand,
html[lang="ar"] .navbar__link,
html[lang="ar"] .hero__name,
html[lang="ar"] .btn,
html[lang="ar"] .work__kind,
html[lang="ar"] .work__go,
html[lang="ar"] .field__lbl,
html[lang="ar"] .field--code .field__in,
html[lang="ar"] .field--code .field__in::placeholder {
  letter-spacing: normal;
}
html[lang="ar"] .hero__title em,
html[lang="ar"] .strip__item {
  font-style: normal;
}
/* Amiri only shipped here at one weight, so h1–h3's 700/600 hierarchy — real
   weights in Instrument Sans — would otherwise fall back to the browser's
   faux-bold on Arabic, which distorts joined letterforms far more than it
   does Latin ones. */
html[lang="ar"] h1,
html[lang="ar"] h2,
html[lang="ar"] h3 {
  font-weight: 400;
}

/* This page's own layout — not just the RTL default every browser already
   applies to text — mirrors just its two asymmetric tells: the bullet
   marker pinned to one side of each service line, and the hover arrow's
   direction of travel. Everything else (nav row, card grids, form columns)
   reads fine unmirrored because gap-based flex/grid layouts have no
   inherent left/right handedness. */
html[dir="rtl"] .serve__list li { padding-left: 0; padding-right: 1.4rem; }
html[dir="rtl"] .serve__list li::before { left: auto; right: 0; }
html[dir="rtl"] .work:hover .work__go .arrow { transform: translateX(-4px); }
