/* Saudi Box - base layer.
   Structure and typography only at this stage; sections get built out in
   later steps. Written with logical properties throughout so that rtl.css
   stays empty. If you find yourself reaching for `left`, `right`,
   `margin-left` or `padding-right` here, that is the bug. */

/* ══════════════════════════════════════════════════════════════════════════
   Thmanyah Serif Display, self-hosted. One family for the whole site, the
   way the Urban City build does it. Bilingual, so no per-language switching.
   Licence: public/fonts/thmanyah/LICENSE.pdf

   Two rules, both learned on the Urban City build - do not "tidy" either one:

   1. ONE weight value per @font-face, never a range. Thmanyah is a static
      family, not variable. `font-weight: 600 700` works on Android and
      desktop Chrome, but WebKit - every browser on iPhone - rejects the whole
      declaration and silently falls back to the system font. This site
      targets iOS Safari 16.

   2. Every weight from 100 to 900 gets a declaration, repeating files for the
      ones the family does not ship. Leave 100/200/600/800 undeclared and the
      browser synthesises them by smearing the glyphs, which breaks the joins
      between Arabic letters.
   ═══════════════════════════════════════════════════════════════════════ */

@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Light.woff2')   format('woff2'); font-weight: 100; font-style: normal; font-display: swap; }
@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Light.woff2')   format('woff2'); font-weight: 200; font-style: normal; font-display: swap; }
@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Light.woff2')   format('woff2'); font-weight: 300; font-style: normal; font-display: swap; }
@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Medium.woff2')  format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Bold.woff2')    format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Bold.woff2')    format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Bold.woff2')    format('woff2'); font-weight: 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Thmanyah Serif Display'; src: url('../public/fonts/thmanyah/thmanyahserifdisplay-Black.woff2')   format('woff2'); font-weight: 900; font-style: normal; font-display: swap; }


/* Never let the browser fake a weight it was not given a file for. */
body { font-synthesis-weight: none; -webkit-font-synthesis: none; }

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: auto; /* Lenis owns scrolling; never both */
  /* The gutter is reserved at all times, so locking the page behind the
     dialog does not take the scrollbar away and shift the layout under it. */
  scrollbar-gutter: stable;
}

/* Set while the Instagram dialog is open - see lock() in js/instagram.js. */
html[data-modal-open] { overflow: hidden; }

/* Every in-page link is animated by Lenis in js/lenis-init.js, which measures
   the floating header and offsets by it. This is the same offset for the path
   that does not go through Lenis: `prefers-reduced-motion`, a :target landing
   from another page, or a browser that never ran the module. The two have to
   agree or a section arrives underneath the bar in one of them. */
main[id],
main > section[id],
footer[id] {
  scroll-margin-block-start: calc(var(--header-inset) + var(--header-height) + 0.75rem);
}

body,
h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol {
  margin: 0;
}

ul[role='list'],
ol[role='list'] {
  padding-inline-start: 0;
  list-style: none;
}

img,
picture,
svg,
canvas {
  display: block;
  max-inline-size: 100%;
  block-size: auto;
}

button,
input,
select,
textarea { font: inherit; color: inherit; }

/* ------------------------------------------------------------- base type */
body {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: var(--fw-body);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-body);
  color: var(--on-surface-light);
  background-color: var(--surface-light);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: var(--fw-heading);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}

h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); font-family: var(--font-body); }

/* Fine text - annotation cards, captions, eyebrows - runs in Thmanyah Light.
   Still sentence case and untracked; Light is the weight, not a licence to
   letterspace. */
.caption,
.eyebrow {
  font-weight: var(--fw-light);
}

p { max-inline-size: var(--measure); }

/* Sizes are at parity now that both scripts come from one family; Arabic
   still takes a little more leading. */
:lang(ar), :lang(ar) * { letter-spacing: 0; }
:lang(ar) h1,
:lang(ar) h2 { line-height: calc(var(--lh-heading) * var(--ar-lh-scale)); }
:lang(ar) .hero__headline { line-height: calc(var(--lh-hero) * var(--ar-lh-scale)); }

/* Prices and phone numbers stay in Western digits in both languages. */
.numeral { font-variant-numeric: lining-nums tabular-nums; }

a {
  color: inherit;
  text-decoration-color: var(--accent);
  text-underline-offset: 0.22em;
  text-decoration-thickness: from-font;
}
a:hover { color: var(--accent); }

/* Two complete spellings of the same word - never one word split across an
   element boundary. The first attempt put the tatweel in a <span> inside the
   word; Chrome shapes straight through that (measured: 165.70px whole vs
   165.72px split) but WebKit does not shape Arabic across inline box edges,
   so on every iPhone the letters came apart around the stretch while looking
   perfect on the desktop it was tested on. Each variant below is a whole
   word, so shaping never meets an edge.

   display:none also keeps the accessibility tree honest: exactly one of the
   two is ever present, so the heading always has one clean name. */
/* The stretched spelling is the default, not a phone-only flourish: the
   headline sets on two lines at every width now and the kashida is what
   squares the short line against the long one. Which one shows is decided by
   LANGUAGE, not by a breakpoint - kashida is a property of the Arabic script,
   so English takes the plain spelling at every size.

   The whole stretch is one run on the ي-ا join - the other available join,
   ب-س, is left alone (client, 2026-09-02).

   Two complete spellings in two spans, never a tatweel span inside the word.
   WebKit does not shape Arabic across an inline box boundary: splitting a
   word around a span measures identically in Chrome and comes apart on every
   iPhone. Chrome cannot prove this one. */
.headline__word { display: none; }
.headline__word--kashida { display: inline; }

.headline__word:lang(en) { display: inline; }
.headline__word--kashida:lang(en) { display: none; }

/* The headline is three written lines, not one string left to wrap: the copy
   is a sentence that breaks in a particular place and reads wrong anywhere
   else. Block spans rather than <br> so each line can be keyed for i18n and
   measured on its own. */
.headline__line { display: block; }

/* Never remove this without replacing it. */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* ----------------------------------------------------------- scaffolding */
.wrap {
  inline-size: 100%;
  max-inline-size: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.visually-hidden:not(:focus):not(:active) {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  inset-block-start: var(--space-2xs);
  inset-inline-start: var(--space-2xs);
  z-index: var(--z-skip-link);
  padding: var(--space-2xs) var(--space-s);
  background-color: var(--surface-dark);
  color: var(--on-surface-dark);
  text-decoration: none;
}
.skip-link:not(:focus):not(:active) {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* -------------------------------------------------------------- the logo
   Inlined as a sprite in the document so it inherits currentColor: cream on
   burgundy in the footer, burgundy on cream in the header. */
.logo {
  display: block;
  block-size: auto;
  fill: currentColor;
}
.logo--lockup { block-size: var(--logo-height); inline-size: auto; }
.logo--mark { block-size: 2.25rem; inline-size: auto; }

/* --------------------------------------------------------- section shells
   One colour transition for the whole page: cream above, burgundy and then
   espresso below. Do not introduce a second one. */
.section { padding-block: var(--section-padding-block); }

/* The reset zeroes every margin, so spacing has to be given back on purpose.
   Doing it per component is how the story heading ended up sitting directly
   on its first paragraph - 0px - while the branches heading had 64. These
   two rules own it for every section; components only override. */
.section h2 { margin-block-end: var(--space-xl); }
.section p + p { margin-block-start: var(--space-m); }

.section--light {
  background-color: var(--surface-light);
  color: var(--on-surface-light);
}
.section--dark {
  background-color: var(--surface-dark);
  color: var(--on-surface-dark);
}
.section--deep {
  background-color: var(--surface-deep);
  color: var(--on-surface-deep);
}

/* The header sits over the hero rather than above it. In flow it was 213px
   tall and pushed a 100vh hero a full header below the fold, which is what
   was cutting the composition off at the bottom. */
.site-header {
  position: fixed;
  inset-block-start: var(--header-inset);
  inset-inline: var(--gutter);
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  block-size: var(--header-height);
  padding-inline: clamp(var(--space-s), 2vw, var(--space-l));
  max-inline-size: none;
  color: var(--on-surface-light);
  transition: color var(--dur-base) var(--ease-out);
}

/* The glass, as a layer *behind* the bar's own contents: the logo, the nav
   and the switcher stay crisp on top of it while the page refracts underneath.
   This is one of the three places glass is allowed, and the only one that
   earns it by movement - the entire page passes under it.

   It is a floating box rather than a full-width strip, inset from all three
   edges and carrying the same corner radius as the language switcher, so the
   two glass surfaces in the bar read as one family. Because it floats, it
   takes the drop shadow from the shared .glass rule as well - a strip flush
   to the edge has nothing to cast onto, a floating box does.

   Off at the top of the page, where the hero is a flat cream field and there
   is nothing behind it to refract; glass over a flat field is all of the cost
   and none of the effect. It comes on the moment content starts sliding
   underneath. Opacity is what moves; the blur radius never does. */
.site-header::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  background-color: var(--glass-bg-light);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border: var(--hairline) solid var(--glass-line);
  border-radius: var(--glass-radius);
  box-shadow: 0 1px 0 var(--glass-sheen) inset,
              0 8px 24px var(--glass-shadow);
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out),
              background-color var(--dur-slow) var(--ease-out),
              inset-block-end var(--dur-base) var(--ease-out);
}

.site-header[data-scrolled]::before,
.site-header[data-nav-open]::before { opacity: 1; }

/* Past the hero the strip is over burgundy, so the tint and the type flip
   together. Set from glass.js at the seam, not guessed from scroll distance. */
.site-header[data-over='dark'] {
  color: var(--on-surface-dark);
  --glass-saturate: 1;
}
.site-header[data-over='dark']::before { background-color: var(--glass-bg-dark); }

/* Over the two espresso sections the bar tints with espresso rather than
   burgundy, so it takes the colour of what it is crossing instead of laying a
   red band over a brown one. Same 42%, same blur - only the hue moves. */
.site-header[data-over='deep'] {
  color: var(--on-surface-deep);
  --glass-saturate: 1;
}
.site-header[data-over='deep']::before { background-color: var(--glass-bg-deep); }

/* Everything in the bar paints above the glass. */
.site-header > * { position: relative; z-index: 1; }

.site-header__home { display: flex; text-decoration: none; }

.site-nav__list {
  display: flex;
  align-items: center;
  gap: clamp(var(--space-s), 2vw, var(--space-l));
  margin: 0;
}
.site-nav a {
  font-size: var(--fs-small);
  text-decoration: none;
  color: inherit;
}
.site-nav a:hover { text-decoration: underline; }

/* The switcher is the third and last glass surface. It is small and it sits
   over the canvas, so it gets the full treatment including the radius. */
.lang-switch {
  border: var(--hairline) solid var(--glass-line);
  padding: var(--space-3xs) var(--space-xs);
  font-family: inherit;
  font-size: var(--fs-small);
  color: inherit;
  cursor: pointer;
}
.lang-switch:hover { color: var(--accent); border-color: var(--accent); }

/* ------------------------------------------------------------------ glass
   The shared surface. Only ever on the hero cards, the header strip and the
   language switcher - never on menu items, branch cards, section backgrounds
   or the footer, all of which sit on flat burgundy with nothing behind them
   to refract. No chromatic aberration, no shimmer, no blur that changes on
   hover, no gradients inside the glass. */
.glass {
  background: var(--glass-bg-light);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border: var(--hairline) solid var(--glass-line);
  border-radius: var(--glass-radius);
  box-shadow: 0 1px 0 var(--glass-sheen) inset,
              0 8px 24px var(--glass-shadow);
}

.section--dark .glass,
.site-header[data-over='dark'] .glass {
  background: var(--glass-bg-dark);
  --glass-saturate: 1;
}

/* Everything glass over the deep sections takes the deep tint - the bar's own
   children, the follow button, both dialogs. One rule per surface, one token
   behind all of them. */
.site-header[data-over='deep'] .glass,
.section--deep .glass,
.picker,
.picker .glass,
.ig-modal,
.ig-modal .glass {
  background: var(--glass-bg-deep);
  --glass-saturate: 1;
}

/* Both spellings, or this fires on every iPhone before iOS 18: Safari has
   had -webkit-backdrop-filter since 9 but only shipped the unprefixed
   property in 18, so `not (backdrop-filter: blur(1px))` was true there and
   flattened the glass on the devices that support it best. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass { background: var(--glass-solid-light); }
  .site-header::before { background-color: var(--glass-solid-light); }
  .site-header[data-over='dark']::before,
  .section--dark .glass { background-color: var(--glass-solid-dark); }
  .site-header[data-over='deep']::before,
  .section--deep .glass,
  .picker,
  .ig-modal { background-color: var(--glass-solid-deep); }
}

/* The kill switch. backdrop-filter over a canvas that repaints on every
   scroll frame recomputes the blur on every paint; glass.js measures frame
   rate under scroll and sets this when the device cannot keep up. The
   surfaces stay - they just stop being expensive. */
:root[data-glass='off'] .glass,
:root[data-glass='off'] .site-header::before {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
:root[data-glass='off'] .glass,
:root[data-glass='off'] .site-header::before { background-color: var(--glass-solid-light); }
:root[data-glass='off'] .section--dark .glass,
:root[data-glass='off'] .site-header[data-over='dark']::before { background-color: var(--glass-solid-dark); }
:root[data-glass='off'] .section--deep .glass,
:root[data-glass='off'] .site-header[data-over='deep']::before { background-color: var(--glass-solid-deep); }

@media (prefers-reduced-transparency: reduce) {
  .glass,
  .site-header::before {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background-color: var(--cream);
  }
  .site-header[data-over='dark']::before { background-color: var(--burgundy); }
  .site-header[data-over='deep']::before { background-color: var(--espresso); }
}

/* ---------------------------------------------------------------- the hero
   The frames are transparent cut-outs, so three things are built here rather
   than baked into the image: the flat cream field, the headline sitting
   *behind* the canvas so the cup crops it, and the cards in front. */
.hero {
  position: relative;
  background-color: var(--surface-light);
}

.hero__stage {
  position: relative;
  block-size: 100vh;
  block-size: 100svh;   /* iOS: avoid the toolbar resize jump mid-scrub */
  overflow: clip;
}

/* Real HTML text, behind the canvas. It sits in the upper band and the cup and
   splash rise into it from below, so the crop happens along the headline's
   lower edge - it is never parked under the middle of the cup. */
.hero__text {
  position: absolute;
  z-index: var(--z-hero-text);
  --hero-text-drop: clamp(var(--space-m), 5vh, var(--space-2xl));
  inset-block-start: calc(var(--header-inset) + var(--header-height) + var(--hero-text-drop));
  inset-inline-start: var(--gutter);
  color: var(--hero-heading);

  /* No measure on the wrapper. Each child owns its own, and a clamp here
     silently overrides them both: at 40vw the headline could not fit
     "قهوة تستحق" on one line and broke into three. */
  max-inline-size: none;
}

/* English drops further below the bar than the Arabic does, and the reason is
   composition rather than measurement: in pixels it already clears the header
   by MORE than the Arabic (41.3px of ink against 30.4px at 1440, measured
   from the real glyph extents, because Arabic's hamza reaches 14.6px above
   its own line box while English's line-height of 0.86 pulls Latin caps down
   into theirs). What changes is what it sits under. RTL puts the text block
   on the right, beneath a small button; LTR puts it on the left, directly
   beneath the logo lockup - the one tall mass in the bar - and the same gap
   reads as crowding. Portrait is unaffected: there the block is centred and
   anchored by its bottom, so this variable is not in play. */
.hero__text:lang(en) {
  --hero-text-drop: clamp(var(--space-xl), 8vh, var(--space-3xl));
}

.hero__headline {
  margin: 0;
  /* No measure. The headline is written as explicit .headline__line blocks,
     so a ch count here cannot help - it can only break a written line
     somewhere the copy did not ask for. It did: at 1440 the 13ch it used to
     carry measured 682.7px and "specialty coffee" needs 683.0px, so English
     split into three rows on a third of a pixel. The gutters are the only
     limit, and every line clears them at every width - checked from 768 to
     2560. */
  max-inline-size: none;
  font-size: var(--fs-hero);
  /* Stated, never inherited from the UA: the base copy is an <h1> and the
     recolouring copy a <p>, and the UA gives those different weights - which
     sets them to different widths and slides the cream copy off the ink one. */
  font-weight: var(--fw-heading);
  line-height: var(--lh-hero);
  letter-spacing: var(--tracking-hero);
  text-wrap: balance;
}

/* One sentence, Light, at body size. It is there to say where and what -
   the headline only says why - so it earns its place by carrying meaning,
   not by filling a gap. Sentence case, no tracking: it is a small label. */
.hero__lede {
  margin: 0;
  margin-block-start: clamp(var(--space-s), 2vh, var(--space-m));
  max-inline-size: 26ch;
  font-size: var(--fs-body);
  font-weight: var(--fw-light);
  line-height: var(--lh-body);
  color: var(--hero-muted);
  text-wrap: pretty;
}


/* The blend that sits the cut-out into the page.
   It is a cream field drawn over the canvas, not alpha baked into the frames:
   the frames stay pure cut-outs, and the join belongs to the hero design.
   Fading to --cream-0 rather than `transparent` keeps the ramp clean. */
.hero__stage::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: var(--hero-fade-height);
  z-index: var(--z-hero-fade);
  background-image: linear-gradient(to top, var(--cream) var(--hero-fade-stop), var(--cream-0) 100%);
  pointer-events: none;
}

.hero__canvas,
.hero__poster {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  z-index: var(--z-hero-canvas);
}
/* The poster stands in for the canvas until the first frame decodes, so it
   has to be the SAME composition - a different one reads as the cup changing
   size a beat after load, which is exactly what it did. These numbers mirror
   FRAMES in js/hero-scrub.js and have to move with it.

   Landscape: the canvas draws fit x zoom(0.82). Contain inside a box scaled to
   82% reproduces that exactly, because contain takes the min of both ratios
   and scaling both by 0.82 scales the min by 0.82. */
.hero__poster {
  inset-block-start: auto;
  inset-block-end: 0;
  inset-inline-start: 4%;      /* (100 - 92) / 2, so the box stays centred */
  inset-inline-end: auto;
  inline-size: 92%;            /* FRAMES.desktop.zoom */
  block-size: 92%;
  object-fit: contain;
  object-position: center bottom;
}


/* The branch lines. On a wide screen the subject is portrait-shaped - 620
   of the frame's 1600px at rest, measured - so a landscape viewport always
   leaves a column open. That column is filled with something the site
   actually needs to say rather than with decoration. */
.hero__places {
  position: absolute;
  z-index: var(--z-hero-cards);
  inset-block-end: var(--space-2xl);
  inset-inline-end: var(--gutter);
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  font-size: var(--fs-small);
  font-weight: var(--fw-light);
  line-height: var(--lh-tight);
  /* The branch lines keep the neutral muted ink, not the burgundy the
     headline and lede took - they are utility information, not brand voice
     (client, 2026-09-02). On the flat cream band they now sit on, that is
     4.66:1. It only failed earlier because they were over the cup. */
  color: var(--muted-on-light);
}

/* -------------------------------------------------------- the scroll cue
   Nothing else on the page says it goes anywhere. The hero fills the screen,
   the artwork sits still until it is scrolled, and a reader who does not know
   that has been handed a poster (client, 2026-09-08).

   Centred on the inline axis rather than parked in a corner, because it is the
   one thing here addressed to the reader rather than describing the brand. A
   short vertical hairline under the words does the pointing - no chevron, no
   bouncing arrow, both of which are the tells CLAUDE.md rules out.

   It is a static mark. The MOTION that teaches the scrub lives in the canvas
   itself: js/hero-scrub.js pours the cup a little and brings it back, once. */
.hero__cue {
  position: absolute;
  z-index: var(--z-hero-text);
  /* As low as it will go. The hero's cream blend only reaches a few dozen
     pixels up from the bottom edge - the forearm runs off the bottom of every
     frame, so there is no empty ground here, only ground the blend has
     covered. Measured: at 32px up with a 64px line the words landed on the cup
     itself at 1.54:1. Down here they sit on cream at 5.8:1.

     Prominence comes from the SIZE of the type and from the line moving, never
     from moving it up the page. There is nowhere up the page to move it to. */
  inset-block-end: var(--space-3xs);
  inset-inline: 0;
  margin: 0;
  max-inline-size: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  /* Body size, not caption size. At --fs-small this was 13px at the foot of a
     screen-high hero and it simply was not seen (client, 2026-09-08, twice).
     It is the one line on the page addressed to the reader; it can afford to
     be the size of the copy it is competing with. */
  font-size: var(--fs-body);
  font-weight: var(--fw-light);
  line-height: var(--lh-tight);
  /* FULL burgundy, not --hero-muted. The cue is the one line on the page
     addressed to the reader rather than describing the brand, and it is also
     the one sitting over the artwork rather than over flat cream. Measured
     against what actually paints behind it - the cup through the hero's cream
     blend - the muted 80% ink came out at 4.98:1 on a wide screen but 4.17:1
     on a phone, which fails AA for text this size. Full burgundy is 6.2:1 on
     the phone and better again on desktop. */
  color: var(--hero-heading);
  pointer-events: none;
  opacity: 1;
  transition: opacity var(--dur-slow) var(--ease-out);
}

/* A hairline with a short bright run travelling DOWN it, over and over,
   until the reader scrolls. This is the part that does the pulling: a word
   sitting still says the page goes down, and this makes you feel it.

   It is a hairline because that is what this brand is made of - the branch
   list, the ingredients label, the footer rule are all one of these. It is
   deliberately NOT a chevron and NOT a bouncing arrow; CLAUDE.md names both as
   tells, and either would be the only piece of stock iconography on the site.

   The motion is one background-position on a 1px box. Nothing is laid out
   again, nothing repaints outside those 32 pixels, and it is running next to a
   canvas that is redrawing on every scroll frame - which is exactly why it had
   to be this cheap. */
.hero__cue::after {
  content: '';
  inline-size: var(--hairline);
  block-size: var(--space-2xl);
  background-image: linear-gradient(
    to bottom,
    rgba(122, 23, 23, 0) 0%,
    rgba(122, 23, 23, 0) 26%,
    var(--hero-heading) 42%,
    var(--hero-heading) 58%,
    rgba(122, 23, 23, 0) 74%,
    rgba(122, 23, 23, 0) 100%);
  background-size: 100% 200%;
  background-repeat: no-repeat;
  animation: hero-cue-fall 2.4s var(--ease-in-out) infinite;
}

@keyframes hero-cue-fall {
  /* Starts above the track and leaves below it, so the run enters and exits
     rather than appearing and vanishing in place. */
  from { background-position: 0 -100%; }
  to   { background-position: 0 200%; }
}

/* Once the cue is gone the animation goes with it. An infinite keyframe on an
   invisible element is a compositor layer kept alive for nothing. */
.hero__cue[data-gone]::after { animation: none; }


/* The words get a box. Loose type at the foot of a screen-high hero reads as a
   caption on the artwork; an outlined pill reads as a thing put there for you,
   and that is the difference between describing the page and instructing the
   reader (client, 2026-09-08, after three quieter versions were missed).

   The whole cue falls and returns. Not a bouncing chevron - there is no
   glyph here at all, only the brand's own hairline and its own type - but the
   eye needs something at the bottom edge to catch, and 20% of the screen
   changing in the middle of it does not do that: motion in the artwork says
   "this is a film", never "there is more below". */
.hero__cue-label {
  padding: var(--space-2xs) var(--space-m);
  border: var(--hairline) solid var(--hero-heading);
  border-radius: 999px;
  background-color: rgba(243, 232, 211, 0.72);
  animation: hero-cue-fall-block 2.4s var(--ease-in-out) infinite;
}

@keyframes hero-cue-fall-block {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(10px); }
}

.hero__cue[data-gone] .hero__cue-label { animation: none; }

/* Set by js/hero-scrub.js on the first real scroll, and never unset. */
.hero__cue[data-gone] { opacity: 0; }

/* The hero is at its tightest on a short phone in landscape, where the cue
   would land on top of the cup rather than under it. The reader there is
   already scrolling to see anything at all. */
@media (max-height: 520px) {
  .hero__cue { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .hero__cue { transition: none; }
  /* The line stays and stops moving - it is still a mark pointing down, and
     the canvas nudge is skipped in this mode too, so it is all there is. */
  .hero__cue-label { animation: none; }
  .hero__cue::after {
    animation: none;
    background-image: linear-gradient(to bottom, var(--hero-heading), rgba(122, 23, 23, 0));
  }
}

/* ---------------------------------------------------------- the burger
   Phone only. On a wide screen the three links are already in the bar and
   there is nothing to reveal, so the button is not rendered at all - not
   merely hidden, which would leave it in the tab order. */
.nav-toggle {
  display: none;
  place-items: center;
  inline-size: 2.75rem;          /* 44px: the minimum comfortable tap target */
  block-size: 2.75rem;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}

.nav-toggle__bars {
  display: grid;
  gap: 5px;
  inline-size: 22px;
}

.nav-toggle__bars > span {
  display: block;
  block-size: 2px;
  background-color: currentColor;
  transition: transform var(--dur-base) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}

/* Open: the outer bars travel to the middle and cross; the middle one goes.
   Transform and opacity only - never the blur, and no shimmer. */
.site-header[data-nav-open] .nav-toggle__bars > span:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}
.site-header[data-nav-open] .nav-toggle__bars > span:nth-child(2) {
  opacity: 0;
  transform: scaleX(0.2);
}
.site-header[data-nav-open] .nav-toggle__bars > span:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}


/* Portrait: the headline takes the top, the cup sits underneath it rather than
   behind it - there is no room for them to overlap without one killing the
   other, and legibility wins. */
@media (max-width: 767px) {
  /* Centred, and sat down into the upper third rather than tucked under the
     header. Bottom-anchored art cannot move up without either growing or
     lifting off the bottom edge, so the gap closes from both sides: the
     headline comes down to meet the cup and the cup lifts a little into the
     cream blend. */
  /* Anchored by its BOTTOM, not its top. English is longer than the Arabic
     - "قهوة تستحق الانتظار" sets on two lines, its translation may take
     three - and a top-anchored block grows downward, straight into the cup.
     Anchored here it grows upward into the open space under the header,
     which is the only direction that has room. Do not flip this back when
     the language switch lands. */
  /* Two anchors, not one. The bottom edge is what positions it - the block
     grows upward as the copy runs longer - but growing upward with nothing
     above it walked straight under the floating header, which is what it was
     doing at 76svh: measured on a 440x880 screen the block came to 152px
     tall, its top landing at 59px with the header ending at 68px. The top
     inset is a ceiling, not an anchor: justify-content keeps the content
     sitting on the bottom edge, and the ceiling only ever catches it. */
  .hero__text {
    inset-block-start: calc(var(--header-inset) + var(--header-height) + var(--space-s));
    /* 70svh, not 66: the block is anchored by its bottom, so this number is
       what sets the gap to the header at the top - and at 66 that gap came
       out at 74px on a 440px screen, a dead band under the bar. Solved for a
       ~36px gap instead, which lands on 70svh at 440x880 AND at 360x800,
       because the copy and the viewport shrink together. */
    /* 68svh. The bar grew to 64px and the Arabic headline reaches about
       9px above its own line box - the hamza on the alef - so a 39px gap
       between the boxes was under 30px of daylight and read as touching
       (client, 2026-09-02). This puts ~52px between the boxes, ~43px of
       real gap, at every phone width. */
    inset-block-end: 68svh;
    inset-inline: var(--gutter);
    margin-inline: auto;
    max-inline-size: none;
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
  }
  /* --fs-hero is flat at 48px on every phone, because its 6.4vw term does not
     reach 3rem until 750px - so the headline was the same size on a 320px
     screen as on a 430px one and overran the narrow ones. The portrait size
     is its own clamp, tuned so the longest written line ("إلى أول قهوة مختصة",
     353px at 48px) clears the gutters: 396px of room at 440, 324px at 360.
     No max-inline-size - the gutters are the measure. Line 1 still breaks
     between "قهوة تستحق" and the stretched "الانتظــــــــــار", which is the
     break the kashida squares. */
  .hero__headline {
    --fs-hero: clamp(2rem, 13vw, 3.5rem);
    margin-inline: auto;
    max-inline-size: none;
  }

  /* English sets longer than the Arabic at the same size, and on a phone it
     showed: at the Arabic 56px, "specialty coffee" broke onto a second row,
     the lede took two lines, and the whole block grew upward until the
     headline sat 18px UNDER the floating header. Measured, the longest
     English line is 7.408em, so the size it can take is the gutter width
     divided by that - 53.5px at 440, 43.7px at 360. This clamp stays just
     under both, which is ~5% off the Arabic: close enough that the two read
     as the same headline, small enough that neither line ever wraps.
     Re-measure if the English copy changes. */
  .hero__headline:lang(en) {
    --fs-hero: clamp(1.75rem, 11vw, 3.25rem);
    /* And the leading English needs to occupy the same block as the Arabic.
       The block is anchored by its BOTTOM here, so a shorter one starts
       lower and opens a gap under the header - 63px against the Arabic 39px
       before this. Arabic already takes --ar-lh-scale (1.18) for its
       ascenders and descenders; English at 0.86 and 4px smaller came out
       24px shorter over two rows. 1.1 closes it, and 0.86 was too tight for
       Latin at this size anyway - the y of "specialty" was reaching the row
       below. Portrait only: on a wide screen the block is anchored by its
       TOP and both languages set at the same size, so loosening it there
       would push the English headline DOWN into the artwork instead. */
    line-height: 1.1;
  }
  .hero__lede {
    margin-inline: auto;
    max-inline-size: 30ch;
  }

  /* The forearm runs off the bottom edge of every frame - measured, the
     bottom row is opaque in all 61 - so lifting the art exposes a hard cut.
     The blend has to cover that cut without eating the hand, which is a
     question of where the cream is *solid*, not how tall the band is: a
     deeper solid block chops the palm flat.

     The lift is now 4% of the viewport (~35px on a 440x880 screen) and the
     solid block is 28% of a 15% band, ~37px, so it clears the cut by ~2px.
     Deeper than it used to be, but it does NOT eat more hand than before:
     the hand rose by the same 22px the block grew, so the cream still meets
     it in the same place. The two numbers move together - change
     FRAMES.mobile.lift and this has to follow. */
  .hero__stage {
    --hero-fade-height: 15%;
    --hero-fade-stop: 28%;
  }

  /* The phone bar carries three things: the logo, the language button and
     the burger. The section links move inside it (client, 2026-09-02).
     Desktop is untouched. */
  /* A little taller than it was, not much (client, 2026-09-02: 68px was too
     big). 64px of bar carrying a 40px logo and two 44px buttons. 44px is the
     WCAG 2.2 minimum target and the point of the change - the language
     button used to be about 30px tall, which is under it. */
  .site-header {
    --header-height: 4rem;
    --header-inset: var(--space-2xs);
    --logo-height: 2.5rem;
    gap: var(--space-2xs);
  }
  .lang-switch {
    min-block-size: 2.75rem;
    padding-inline: var(--space-xs);
  }
  .site-header__home { margin-inline-end: auto; }
  .lang-switch { order: 2; }
  .nav-toggle { display: grid; order: 3; }

  /* The panel is not a second box under the header - it IS the header. The
     bar's own glass layer grows downward to hold it, so there is one rounded
     surface with one shadow and one radius rather than two stacked ones.
     --nav-panel-height is measured and written by nav.js: the panel's real
     content height, so the box and the reveal finish together instead of the
     box overshooting to a guessed maximum. */
  .site-nav {
    position: absolute;
    inset-block-start: 100%;
    inset-inline: 0;
    overflow: hidden;
    max-block-size: 0;
    transition: max-block-size var(--dur-base) var(--ease-out);
  }
  .site-header[data-nav-open] .site-nav {
    max-block-size: var(--nav-panel-height, 18rem);
  }
  .site-header[data-nav-open]::before {
    inset-block-end: calc(-1 * var(--nav-panel-height, 18rem));
  }

  .site-nav__list {
    flex-direction: column;
    /* stretch, not the inherited center. On a column the cross axis is the
       horizontal one, so center was shrinking every row to the width of its
       own word - a 45px target in a 300px panel, and a tap two centimetres
       to the side of the text did nothing at all. */
    align-items: stretch;
    gap: 0;
    padding-block: var(--space-2xs) var(--space-s);
  }
  .site-nav__list li {
    border-block-start: var(--hairline) solid var(--glass-line);
    opacity: 0;
    transform: translateY(-8px);
    transition: opacity var(--dur-base) var(--ease-out),
                transform var(--dur-base) var(--ease-out);
  }
  .site-nav__list a {
    display: block;
    /* The row is the target, not the word - the whole width is tappable.
       The label is centred in it (client, 2026-09-02); these are two
       separate things and it is worth saying so, because centring text by
       shrinking its box to fit is exactly what made the target 45px wide
       in the first place. */
    inline-size: 100%;
    padding-block: var(--space-xs);
    padding-inline: var(--space-2xs);
    text-align: center;
    font-size: var(--fs-lead);
  }
  /* The rows arrive after the box has started opening, one behind the next,
     so the panel reads as unfolding rather than as a block appearing. On
     close they all go at once - a staggered exit reads as hesitation. */
  .site-header[data-nav-open] .site-nav__list li {
    opacity: 1;
    transform: none;
  }
  .site-header[data-nav-open] .site-nav__list li:nth-child(1) { transition-delay: 70ms; }
  .site-header[data-nav-open] .site-nav__list li:nth-child(2) { transition-delay: 115ms; }
  .site-header[data-nav-open] .site-nav__list li:nth-child(3) { transition-delay: 160ms; }
  .site-header[data-nav-open] .site-nav__list li:nth-child(4) { transition-delay: 205ms; }

  /* Off entirely on a phone (client, 2026-09-02). They exist on a wide screen
     to fill the column the portrait subject leaves open; a phone has no such
     column, so they only compete with the artwork for the one band of clear
     cream there is. The Branches section carries the same three lines. */
  .hero__places { display: none; }

  /* Portrait: cover inside a box of the canvas's own height reproduces the
     same scale, and object-position is the fraction the pan starts from -
     so the poster and the first drawn frame are the same picture. */
  .hero__poster {
    inset-inline-start: 0;
    inline-size: 100%;
    block-size: 55%;             /* FRAMES.mobile.fillHeight */
    inset-block-end: 4%;         /* FRAMES.mobile.lift */
    object-fit: cover;
    object-position: 29% bottom; /* FRAMES.mobile.pan[0] */
  }

}

/* Shown until every frame has decoded. The scroll experience does not start
   before that, so this is not decoration - it is the gate. */
.hero__loader {
  position: absolute;
  inset-block-end: var(--space-2xl);
  inset-inline: var(--gutter);
  z-index: var(--z-loader);
  display: flex;
  justify-content: center;
}
.hero__loader-track {
  inline-size: min(18rem, 40vw);
  block-size: var(--hairline);
  background-color: var(--hairline-on-light);
}
.hero__loader-bar {
  display: block;
  inline-size: 0%;
  block-size: 100%;
  background-color: var(--accent);
  transition: inline-size var(--dur-fast) linear;
}

/* Poster and canvas share a slot; once frames are ready the poster is hidden
   with [hidden] rather than opacity, so it stops costing a composite layer. */
.hero__poster[hidden],
.hero__loader[hidden] { display: none; }

/* Step-2 scaffolding, only present with ?hud in the URL. */
.hero-hud {
  position: fixed;
  inset-block-end: var(--space-2xs);
  inset-inline-start: var(--space-2xs);
  z-index: var(--z-loader);
  padding: var(--space-3xs) var(--space-2xs);
  background-color: var(--espresso);
  color: var(--cream);
  font-size: var(--step--2);
  font-variant-numeric: tabular-nums;
}

/* Set by the inline bootstrap in <head>, and ONLY when the stored language
   is not the one in the markup. Without it a returning English reader gets a
   frame of Arabic before the JSON lands. visibility, not display: the boxes
   keep their space, so nothing reflows when the copy arrives. The bootstrap
   also drops the attribute on a timer, so a failed fetch shows Arabic rather
   than nothing. */
[data-i18n-pending] [data-i18n] { visibility: hidden; }


/* ================================================================== menu
   A wheel of product cards (client, 2026-09-02, replacing the priced lists).

   Two layouts in one, and the order matters. What is written first is the
   NO-JAVASCRIPT layout: a plain swipeable row that works on its own. js/menu.js
   adds `data-ready` and the rules under it take over and build the arc. If the
   module never loads, the menu is still a menu. */
/* The arc is WIDER than the content column on purpose - the outer pair of
   cards runs past it and fades out - so the clip that holds it has to be the
   full-width section rather than .wrap. It is `clip` and not `hidden`: hidden
   on one axis forces the other to auto, which would make the section its own
   scrollport.

   This is not a nicety. The ring keeps every card positioned and only fades
   the far ones out, and opacity is not layout: eight cards made the document
   1303px wide inside a 390px phone, and the entire page scrolled sideways.
   js/menu.js parks the far ones close in as well; both are needed. */
.section--clip { overflow: clip; }

.wheel {
  /* Bigger than it was. At 17rem a card was 272px in a 1400px viewport and the
     section read as a small object in a large empty room. */
  --card-w: clamp(15rem, 27vw, 22rem);
  position: relative;
  margin-block-start: var(--space-xl);
}

@media (max-width: 767px) {
  .wheel { --card-w: clamp(13rem, 66vw, 17.5rem); }
}

.wheel__track {
  display: flex;
  gap: var(--space-l);
  overflow-x: auto;
  padding-block-end: var(--space-s);
  padding-inline: var(--gutter);
  margin-inline: calc(var(--gutter) * -1);
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
}

.wheel__slide {
  flex: 0 0 auto;
  inline-size: var(--card-w);
  scroll-snap-align: center;
}

/* ---------------------------------------------------------------- the arc */
.wheel[data-ready] { perspective: 1400px; }

.wheel[data-ready] .wheel__track {
  display: block;
  position: relative;
  block-size: var(--wheel-height, 26rem);
  overflow: visible;
  padding: 0;
  margin-inline: 0;
  transform-style: preserve-3d;
  /* The cards are dragged, so the browser must not also try to pan. */
  touch-action: pan-y;
  cursor: grab;
}
.wheel[data-ready] .wheel__track:active { cursor: grabbing; }

.wheel[data-ready] .wheel__slide {
  position: absolute;
  inset-block-start: 0;
  /* `left`, not `inset-inline-start`: the wheel is centred either way, and JS
     writes a single transform string that has to compose with one known axis
     rather than one that flips under it. The direction sign lives in that
     string instead, where it can follow the language. */
  left: 50%;
  transform-origin: 50% 50%;
  /* The transform settles with a hair of overshoot and the fade does not -
     a card that brightens past full and comes back reads as a flicker. */
  transition: transform var(--dur-settle) var(--ease-settle),
              opacity var(--dur-slow) var(--ease-out);
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .wheel[data-ready] .wheel__slide { transition: none; }
}

/* ------------------------------------------------------------- the card */
.dish {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

/* 3:4 portrait (client, 2026-09-03), and square-cornered. The 14px radius it
   had was --glass-radius, borrowed by a surface that is not glass, and it
   turned every drink into the same rounded tile CLAUDE.md rules out. The mark
   is a thin square outline; the branches are a hairline and plain text. This
   is the same object.

   The ratio lives in THREE places and they have to agree or the photograph
   gets cropped by object-fit: here, the width/height attributes on every <img>
   in index.html, and W/H in tools/shoot-cards.js. */
/* Rounded to --glass-radius, the SAME corner as the floating header and the
   language switcher (client, 2026-09-03), so the cards read as the same family
   of object as the chrome around them rather than as a second, harder one.
   Square was the earlier reading of the mark - a thin square outline - and the
   client's is better: the mark is a printed logo, the header is the thing a
   card sits next to on screen.

   A fixed 3:4 box holding two things stacked: the photograph, which takes
   whatever is left, and the ingredients label, which takes nothing until it is
   asked for. That is the whole mechanism - the card's height NEVER changes, so
   opening a drink moves nothing else on the page, and the picture is never
   covered, only made smaller. Rejected before this: a band across the middle
   of the photograph, which cut the cup in half. */
.dish__media {
  position: relative;
  display: flex;
  flex-direction: column;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: var(--glass-radius);
  background-color: var(--espresso);
}

/* `min-block-size: 0` is the load-bearing line: without it a flex item refuses
   to shrink below its content, the image holds its full height, and the label
   pushes the bottom of the picture out of the box instead of taking room from
   it. */
.dish__media img {
  flex: 1 1 auto;
  min-block-size: 0;
  inline-size: 100%;
  object-fit: cover;
  /* The window onto the photograph slides UP as the label takes its room, so
     what `cover` crops away is the empty backdrop above the drink rather than
     the drink itself (client, 2026-09-03: the label was covering the product).
     78% and not 100%: cropping the whole difference off the top would start
     taking the lid off a tall cup. It is an animated length like everything
     else here, so the picture rises as the label opens rather than jumping. */
  object-position: center 50%;
  transition: object-position var(--dur-base) var(--ease-out);
}

.dish[data-open] .dish__media img { object-position: center 78%; }

/* The inset hairline rectangle is the logo's frame, drawn only on the card in
   front - so the frame is what says "this one", not a shadow and not a
   colour. */
.dish__media::after {
  content: '';
  position: absolute;
  inset: var(--space-xs);
  /* One step tighter than the card's own corner. An inset frame drawn at the
     SAME radius reads as a stroke that has slipped off the edge; the eye
     expects the inner curve of a border to be smaller than the outer one. */
  border-radius: calc(var(--glass-radius) - var(--space-3xs));
  border: var(--hairline) solid var(--hairline-on-dark);
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

.wheel[data-ready] .wheel__slide[data-active] .dish__media::after { opacity: 1; }

/* Depth is carried by the turn, the scale and the fade. The one thing added
   here is a shadow under the card in FRONT only - not under all of them,
   which is the "soft grey shadow under everything" CLAUDE.md forbids. On the
   receding cards it would only muddy the burgundy behind them. */
.wheel[data-ready] .wheel__slide[data-active] .dish__media {
  box-shadow: 0 26px 60px rgba(26, 10, 8, 0.34);
}

/* The receding cards get a much quieter one. Without any shadow at all a
   rounded card on flat burgundy has nothing to separate it from the card in
   front of it where the two overlap, which is most of the deck on a phone. */
.wheel[data-ready] .wheel__slide:not([data-active]) .dish__media {
  box-shadow: 0 10px 26px rgba(26, 10, 8, 0.22);
}

@media (prefers-reduced-motion: reduce) {
  .dish__media::after,
  .dish__media img { transition: none; }
}

/* BEIGE, and solid. Beige is the only accent the brand has and this is the
   one place below the hero that earns it: a label in the accent colour on a
   burgundy photograph, espresso type on top of it, which is how the printed
   identity works and is the highest contrast pair the palette owns.

   Deliberately NOT a card flip: a flip is a nested preserve-3d inside an
   already rotated card, which is exactly where WebKit falls over, and this
   site targets iOS Safari 16. This is one animated length and cannot break.

   Height is written by js/menu.js from a real measurement. A stock
   `max-height: 999px` animated to lands the motion early and reads as
   generic - the same rule nav.js follows for the phone menu.

   Only the arc closes it. With no JavaScript every label sits open under its
   picture, which is a menu; closed by default in the stylesheet it would be a
   list nobody could reach next to a button that does nothing. */
.dish__panel {
  flex: 0 0 auto;
  overflow: hidden;
  background-color: var(--accent);
  color: var(--espresso);
  text-align: center;
}

.wheel[data-ready] .dish__panel {
  block-size: 0;
  transition: block-size var(--dur-base) var(--ease-out);
}

/* Padding lives on the inside so that animating the OUTER height to zero
   actually reaches zero - padding on .dish__panel itself would leave a beige
   stripe across every closed card. */
/* The hairline and the padding both live on the INNER box. The client's
   photography is shot on a cream table, and cream against beige is close
   enough in tone that the label's top edge disappeared into the bottom of the
   picture; one espresso hairline fixes that. It cannot go on .dish__panel
   itself, because a border-box of height 0 still paints its border - measured,
   the closed panel came out 1px tall and every shut card wore a stripe across
   the bottom of its photograph. */
.dish__panel-body {
  padding: var(--space-s) var(--space-s) var(--space-m);
  border-block-start: var(--hairline) solid rgba(58, 33, 24, 0.22);
}

/* A class that sets display beats the UA sheet's [hidden], so the button
   would show through the attribute that is meant to be keeping it out of the
   no-JavaScript page. js/menu.js drops the attribute, and it does it BEFORE
   the cards are measured - hiding it with a :not([data-ready]) rule instead
   measured every card one button short and the hint line landed on top of it. */
.dish__more[hidden] { display: none; }

/* Beige, light, sentence case. The rule above it belongs to the panel, so the
   label needs no second one of its own. */
/* Espresso on beige from here down - the label is the accent surface, so
   everything on it inverts. Light weight and sentence case: CLAUDE.md does not
   allow a small label to be uppercased or letterspaced, and it does not need
   to be. */
.dish__reveal-title {
  margin-block-end: var(--space-3xs);
  max-inline-size: none;
  font-size: var(--fs-small);
  font-weight: var(--fw-light);
  /* Espresso at 72% on beige is 5.2:1. The full ink would out-shout the
     ingredients, which are the thing somebody opened the label to read. */
  color: rgba(58, 33, 24, 0.72);
}

/* ONE LINE, not a stacked list with rules between the rows. Four ingredients
   set as four rows is a table, and a table is what forced the old panel to eat
   half the photograph; set as one wrapped line with a beige-on-espresso dot
   between them it needs a third of the space and reads as a tasting note.
   The dots are generated, so the markup stays a real <ul> of real <li>s. */
.dish__ingredients {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0 var(--space-2xs);
  font-size: var(--fs-body);
  line-height: var(--lh-tight);
  color: var(--espresso);
}

.dish__ingredients li + li::before {
  content: '';
  display: inline-block;
  vertical-align: middle;
  inline-size: 3px;
  block-size: 3px;
  margin-inline-end: var(--space-2xs);
  border-radius: 50%;
  background-color: rgba(58, 33, 24, 0.4);
}

/* Dealt rather than shown: each ingredient arrives just after the one before
   it. The hidden state is only ever set while the wheel is running, so with
   the module gone the line is plain visible text. */
.wheel[data-ready] .dish__ingredients li {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}

.wheel[data-ready] .dish[data-open] .dish__ingredients li { opacity: 1; transform: none; }
.dish[data-open] .dish__ingredients li:nth-child(1) { transition-delay: 120ms; }
.dish[data-open] .dish__ingredients li:nth-child(2) { transition-delay: 170ms; }
.dish[data-open] .dish__ingredients li:nth-child(3) { transition-delay: 220ms; }
.dish[data-open] .dish__ingredients li:nth-child(4) { transition-delay: 270ms; }
.dish[data-open] .dish__ingredients li:nth-child(5) { transition-delay: 320ms; }

@media (prefers-reduced-motion: reduce) {
  .wheel[data-ready] .dish__panel,
  .wheel[data-ready] .dish__ingredients li { transition: none; }
  .wheel[data-ready] .dish__ingredients li { transform: none; }
}

.dish__foot {
  text-align: center;
  transition: opacity var(--dur-slow) var(--ease-out);
}

/* On a phone the neighbours are cropped by the edge of the screen, and a
   cropped name is not context, it is «كابتش» and «000» - two pieces of litter
   either side of the card you are meant to be looking at. There, the cards
   behind are pictures and nothing else. On a wide screen they are whole words
   and they stay: that is what tells you the wheel has more on it. */
@media (max-width: 767px) {
  .wheel[data-ready] .wheel__slide:not([data-active]) .dish__foot { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .dish__foot { transition: none; }
}

.dish__name {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: var(--fw-heading);
  line-height: var(--lh-tight);
}

/* Beige is the accent and the price is the one number on the card, so it gets
   it. At --fs-small it read as a caption of the name rather than as the other
   half of a menu line. */
.dish__price {
  margin-block-start: var(--space-3xs);
  max-inline-size: none;
  color: var(--accent);
  font-size: var(--fs-body);
  font-weight: var(--fw-light);
}

/* The unit is not the price. It sets a shade smaller and a little quieter than
   the number so that scanning the wheel reads 400, 450, 250 rather than
   sixteen repetitions of the same two letters. Both are beige: a second colour
   for the unit would be a second accent, which this palette does not have. */
.dish__unit {
  font-size: 0.78em;
  opacity: 0.72;
}

/* Only the card in front carries a working button. On the others it keeps its
   box - so nothing jumps as the wheel turns - and loses its visibility, which
   also takes it out of the tab order for nothing. */
.dish__more {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 44px;
  margin-block-start: var(--space-s);
  padding-inline: var(--space-m);
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: 999px;
  background: none;
  color: var(--on-surface-dark);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--fw-medium);
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}

.dish__more:hover,
.dish__more:focus-visible {
  background-color: var(--beige);
  border-color: var(--beige);
  color: var(--espresso);
}

.dish__more:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.wheel[data-ready] .wheel__slide:not([data-active]) .dish__more {
  visibility: hidden;
  opacity: 0;
}

/* ------------------------------------------------------------- turning it
   There is no bar of arrows and dots any more (client, 2026-09-03). Two things
   replace it, and neither of them is chrome sitting under the wheel:

   - the wheel TURNS ITSELF once as the section arrives (js/menu.js), which is
     the only reveal CLAUDE.md allows the menu and is now also how the reader
     finds out the cards move;
   - one quiet line of copy, which is what the dots were actually saying.

   The track itself is the control: it takes the tab stop and the arrow keys,
   so removing eight dot buttons did not remove keyboard access to eight cards.
   Its ring is drawn OUTSIDE the section's clip, so the offset is 0 rather than
   the usual 3px - at 3px the ring's outer edge fell in the clipped strip and
   the top and bottom of it simply were not painted. */
.wheel[data-ready] .wheel__track:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: 0;
}


/* ================================================================= story
   Three parts, three jobs. The section is set start-aligned like every other
   one, so nothing here is centred - a centred line inside a start-aligned
   block reads as a stray rather than as emphasis.

   Client copy, verbatim, 2026-09-04. The claim to be the first specialty
   coffee in Syria is the client's own about themselves; it is one of the
   things to confirm in writing before launch. */
.story__lede {
  max-inline-size: var(--measure-tight);
  font-size: var(--fs-lead);
  font-weight: var(--fw-light);
  line-height: var(--lh-heading);
}

:lang(ar) .story__lede { line-height: calc(var(--lh-heading) * var(--ar-lh-scale)); }

.story__body { max-inline-size: var(--measure); }

/* The last line signs the section off rather than continuing it, so it is set
   as a signature: the display face, the accent, and a short rule above it -
   the same rule the ingredients label uses, not a new device. The rule is a
   border on the paragraph and not a separate element, so there is nothing to
   keep in sync when the copy changes length. */
.story__sign {
  margin-block-start: var(--space-xl);
  padding-block-start: var(--space-m);
  max-inline-size: max-content;
  border-block-start: var(--frame-line) solid var(--accent);
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: var(--fw-heading);
  line-height: var(--lh-heading);
  color: var(--accent);
}

/* On a phone the signature runs to the full column instead of hugging its own
   text: at 390px «خمسة إخوة، شغف واحد.» is nearly the whole line anyway, and a
   rule that stops two characters short of the edge reads as a mistake rather
   than as a measure. */
@media (max-width: 767px) {
  .story__sign { max-inline-size: none; }
}

/* ============================================================== branches
   Not cards. A hairline above each one and then plain text, which is all a
   branch is: where it is, when it is open, and how to find it. */
.branches__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-xl) var(--space-2xl);
}

.branch {
  padding-block-start: var(--space-m);
  border-block-start: var(--hairline) solid var(--hairline-on-dark);
}

.branch__name {
  margin-block-end: var(--space-3xs);
  font-size: var(--fs-h3);
}

.branch__address { max-inline-size: none; }

.branch__hours {
  margin-block: var(--space-2xs) var(--space-s);
  max-inline-size: none;
  font-size: var(--fs-small);
  font-weight: var(--fw-light);
  font-variant-numeric: tabular-nums;
  color: var(--muted-on-dark);
}

/* The third branch has no address and no hours yet, so it says so and steps
   back rather than pretending to be one of the other two. */
.branch--soon { color: var(--muted-on-dark); }



/* ------------------------------------------------------------- the map
   A picture with our pins on it. The picture is a real street map of
   Damascus - OpenStreetMap through OpenFreeMap - rendered ONCE by MapLibre in
   a style written entirely from --cream and --beige, and saved as a WebP. The
   rendering is a build step on this machine; the page loads no library and
   opens no tile session. The tool and the whole history are at the top of
   js/map.js.

   There is no text in the image, deliberately. Every name on the map is HTML
   on top of it, which is what buys them Thmanyah, correctly joined Arabic and
   a language switch for free.

   The aspect ratio here and the image bounds in map.js are the same frame
   seen twice, and they have to be: the pins are positioned as a percentage of
   this box while the picture fills it, so the moment they disagree every pin
   slides off its street. Verified to four decimal places - the Mercator
   aspect of each bounding box equals its image aspect exactly. */
.branches__figure {
  margin: 0 0 var(--space-2xl);
}

.branches__map {
  position: relative;
  /* 3:2 on a phone, 2:1 on a wide screen. These two numbers and the image
     bounds in js/map.js are the same frame seen twice and they have to agree,
     or every pin sits off its street. */
  aspect-ratio: 3 / 2;
  border-radius: var(--glass-radius);
  overflow: hidden;
  /* The colour of the map's own land, so the box is already the right colour
     for the moment before the picture decodes. Measured off the render:
     237,223,199, which is mix(cream, beige, 0.14). */
  background-color: var(--map-land, var(--cream));
  isolation: isolate;
}

@media (min-width: 768px) {
  .branches__map {
    aspect-ratio: 2 / 1;
    max-inline-size: 64rem;
    margin-inline: auto;
  }
}

/* Only one of the two images is ever fetched: js/map.js picks from the same
   breakpoint this stylesheet uses and sets a single src. */
.map-image {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* The names, on top of the picture rather than inside it. Positioned with
   `left` and `top` - physical properties, deliberately, and one of only two
   places in the project where that is right rather than a bug. The map is a
   picture of the ground and the ground does not flip when the page does. The
   other is the hero canvas. */
.map-area,
.map-pin {
  position: absolute;
  font-family: var(--font-body);
}

.map-area {
  transform: translate(-50%, -50%);
  color: var(--ink);
  opacity: 0.66;
  font-size: var(--step--1);
  font-weight: var(--fw-medium);
  line-height: 1.2;
  white-space: nowrap;
  pointer-events: none;
  /* A halo in the land colour, not a pill. Nine pills on a map this size is a
     row of buttons; a halo lets a name sit on the street it belongs to and
     still be read over it. */
  text-shadow:
    0 0 3px var(--map-land), 0 0 3px var(--map-land),
    0 0 6px var(--map-land), 0 0 6px var(--map-land);
}

.map-area--water { color: var(--burgundy); opacity: 0.4; font-weight: var(--fw-light); }
.map-area--relief { opacity: 0.45; }

/* The pin, and the card it opens.

   The marker itself is a ZERO-SIZED box sitting exactly on the coordinate.
   Everything visible hangs off it: the pin above, the card above or below
   that. Anchoring this way is what lets the card flip sides without the pin
   moving a pixel - both are positioned against the same point rather than
   against each other. */
.map-marker {
  position: absolute;
  inline-size: 0;
  block-size: 0;
  animation: pin-drop var(--dur-slow) var(--ease-out) var(--pin-delay, 0ms) backwards;
}

/* The pin points at its coordinate with its tip, so it hangs above the marker
   rather than being centred on it. 9px is how far the tip of the rotated
   square falls below the body's own box: half of 44px at 45 degrees is 31.1px
   from the centre, and the box ends 22px from it. */
.map-pin {
  position: absolute;
  inset-block-end: 9px;
  left: 0;
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font-family: var(--font-body);
}

.map-pin:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-hairline);
}

/* A burgundy square with the mark cut out of it in cream - the same
   relationship the logo has on a burgundy surface anywhere else on the page -
   with one corner squared off to point at the ground. */
.map-pin__body {
  display: grid;
  place-items: center;
  inline-size: var(--pin-size, 44px);
  block-size: var(--pin-size, 44px);
  background-color: var(--burgundy);
  color: var(--cream);
  border: 2px solid var(--cream);
  border-radius: 50% 50% 50% 2px;
  transform: rotate(-45deg);
  box-shadow: 0 6px 14px rgba(58, 33, 24, 0.35);
  transition: box-shadow var(--dur-fast) var(--ease-out);
}


/* A pin is sized for the ground it stands on. The phone map covers 15km since
   it was widened to reach Qudsaya, which puts Abu Rummaneh and Kafr Sousa 25px
   apart - and a 44px pin (62px with its border and its tilt) turned the two of
   them into one blob (client, 2026-09-08). At 30px they read as two.

   The tip offset moves with it: the pin points at its coordinate with its
   corner, and that corner sits half the diagonal below the body's centre. */
@media (max-width: 767px) {
  /* 38px, not 44 and not 30. At 44 the two open branches were one blob;
     at 30 they were legible and too small to read as the brand's mark, which
     is the entire reason the pin is the logo and not a dot (client,
     2026-09-08, both ways). 38 clears the overlap and still carries the
     mark. */
  .map-pin { --pin-size: 38px; inset-block-end: 8px; }
  .map-pin__body svg { inline-size: 18px; }
  .map-pin__area { font-size: var(--step--2); padding: 1px var(--space-3xs); }
}

.map-marker[data-open] .map-pin__body {
  box-shadow: 0 6px 14px rgba(58, 33, 24, 0.35), 0 0 0 3px var(--cream);
}

/* The branch that has not opened yet. It is the SAME mark - it is the same
   brand arriving in Qudsaya, which is the reason for pinning it at all - but
   hollow instead of solid: the burgundy field is what says "we are here", so
   a pin that is only its outline says "not yet" without a second colour, a
   dashed border or a badge. Beside the two solid pins the difference reads
   immediately; on its own it is still unmistakably the Saudi Box mark. */
.map-marker--soon .map-pin__body {
  background-color: transparent;
  color: var(--burgundy);
  border-color: var(--burgundy);
  box-shadow: none;
}

/* The cream pill under the two open branches carries their name on a solid
   ground. This one takes the outline as well, for the same reason. */
.map-marker--soon .map-pin__area {
  background-color: transparent;
  color: var(--burgundy);
  box-shadow: inset 0 0 0 var(--hairline) var(--burgundy);
}

/* And it is held back a step further (client, 2026-09-08). Two branches are
   open and one is not, and the pin that is not should not be competing with
   them for the eye. Faded on the PIN rather than on .map-marker, so the focus
   ring and the card it opens both stay at full strength - a control nobody can
   see the focus on is a different problem from a mark that is deliberately
   quiet. Pointing at it, or opening it, brings it back to full. */
.map-marker--soon .map-pin__body,
.map-marker--soon .map-pin__area {
  opacity: 0.66;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.map-marker--soon .map-pin:hover .map-pin__body,
.map-marker--soon .map-pin:hover .map-pin__area,
.map-marker--soon .map-pin:focus-visible .map-pin__body,
.map-marker--soon .map-pin:focus-visible .map-pin__area,
.map-marker--soon[data-open] .map-pin__body,
.map-marker--soon[data-open] .map-pin__area {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .map-marker--soon .map-pin__body,
  .map-marker--soon .map-pin__area { transition: none; }
}

.map-pin__body svg {
  inline-size: 20px;
  block-size: auto;
  fill: currentColor;
  transform: rotate(45deg);   /* stand the mark back up inside the tilted pin */
}


/* The pill under the pin instead of over it. The pin points DOWN at its
   coordinate, so the space beneath its tip is empty ground and the label can
   take it without covering anything. */
.map-marker[data-pill='below'] .map-pin {
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
}

.map-marker[data-pill='below'] .map-pin__area {
  margin-block-end: 0;
  margin-block-start: calc(var(--space-2xs) + var(--space-3xs));
}

/* The branch name gets the pill the area names do not: these two are the
   subject of the picture and everything else on it is context. */
.map-pin__area {
  /* Shifted sideways by tidy() in js/map.js when the frame would cut it off,
     and dropped under the pin when it would land on a neighbour's. Both are
     measurements taken against the real picture at the real width. */
  transform: translateX(var(--area-shift, 0px));
  margin-block-end: var(--space-3xs);
  padding: 2px var(--space-2xs);
  background-color: var(--cream);
  color: var(--ink);
  border-radius: 999px;
  font-size: var(--step--1);
  font-weight: var(--fw-medium);
  line-height: 1.7;
  white-space: nowrap;
  box-shadow: 0 2px 6px rgba(58, 33, 24, 0.28);
}

/* The card. Solid cream, not glass: CLAUDE.md names the three surfaces glass
   is allowed on and this is not one of them, and a card that has to be read
   at 13px over a busy street map wants a solid ground anyway.

   --shift is written by fit() in map.js when the card would otherwise run out
   of the picture - the box clips, so an unshifted card near the edge loses
   its address. 84px is the pin's own height plus its tip. */
.map-popup {
  position: absolute;
  left: 0;
  z-index: 2;
  inline-size: max-content;
  max-inline-size: min(16rem, 62vw);
  padding: var(--space-s) var(--space-m);
  background-color: var(--cream);
  color: var(--ink);
  border-radius: var(--glass-radius);
  box-shadow: 0 10px 28px rgba(58, 33, 24, 0.3);
  font-size: var(--fs-small);
  line-height: var(--lh-tight);
  transform: translate(calc(-50% + var(--shift, 0px)), var(--shift-y, 0px));
  animation: popup-in var(--dur-base) var(--ease-out) backwards;
}

.map-marker[data-place='above'] .map-popup { inset-block-end: 84px; }
.map-marker[data-place='below'] .map-popup { inset-block-start: 16px; }

.map-popup p { max-inline-size: none; margin: 0; }

/* The phone map box is 234px tall. A card of 144px leaves almost nothing to
   place it in, so on a phone it gives up the padding it can spare - the
   picture is small and the card should not be most of it. */
@media (max-width: 767px) {
  .map-popup {
    padding: var(--space-2xs) var(--space-s);
    max-inline-size: min(15rem, 66vw);
  }
}


.map-popup__name {
  font-size: var(--fs-body);
  font-weight: var(--fw-strong);
}

.map-popup__address { margin-block-start: var(--space-3xs) !important; }

.map-popup__hours {
  margin-block-start: var(--space-3xs) !important;
  color: var(--muted-on-light);
  font-variant-numeric: lining-nums tabular-nums;
}

/* The way out to Google. Beige is the only accent, so it is the one thing in
   the card that is not text. */
.map-popup__go {
  display: block;
  margin-block-start: var(--space-s);
  /* padding-inline is not optional. Without it the label sits flush against
     the two rounded ends and reads as text that has escaped the pill - which
     is exactly what "Open in Google Maps" did, being the longest of the four
     strings this button ever carries. */
  padding-block: var(--space-2xs);
  padding-inline: var(--space-m);
  white-space: nowrap;
  background-color: var(--beige);
  color: var(--espresso);
  border-radius: 999px;
  font-weight: var(--fw-medium);
  text-align: center;
  text-decoration: none;
}

.map-popup__go:hover { color: var(--espresso); filter: brightness(1.06); }

.map-popup__go:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* Both shifts, not just the horizontal one. A keyframe that names transform
   REPLACES the element's own for as long as it runs, so a vertical clamp left
   out here was simply discarded for the length of the animation - the card
   arrived clipped and then jumped into place when the animation ended. */
@keyframes popup-in {
  from {
    opacity: 0;
    transform: translate(calc(-50% + var(--shift, 0px)), var(--shift-y, 0px)) scale(0.96);
  }
  to {
    opacity: 1;
    transform: translate(calc(-50% + var(--shift, 0px)), var(--shift-y, 0px)) scale(1);
  }
}

/* No hover growth and no bounce: the pins are a label on a map, not a toy.
   The one thing that moves is the drop as they arrive. */
@keyframes pin-drop {
  from { opacity: 0; transform: translateY(-14px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .map-marker,
  .map-popup { animation: none; }
}

/* Required, and this is the smallest it is allowed to be. The picture is a
   rendering of OpenStreetMap data, which is ODbL: a produced work has to say
   where it came from. It used to be a caption under the map and that read as
   a line of copy nobody asked for, so it is where every other map on the
   internet puts it - inside the corner of the picture, at 9px, in the map's
   own ink. Do not delete it. */
.map-credit {
  position: absolute;
  inset-block-end: 5px;
  inset-inline-end: 8px;
  z-index: 1;
  color: var(--ink);
  opacity: 0.38;
  font-size: var(--step--2);
  line-height: 1.4;
  text-decoration: none;
  text-shadow: 0 0 3px var(--map-land), 0 0 3px var(--map-land);
}

.map-credit:hover { color: var(--ink); opacity: 0.7; }

.map-credit:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: 2px;
  opacity: 0.8;
}


/* ============================================================= instagram
   Static tiles. No API: it needs a business token and will break. */
.instagram__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-s);
  /* Capped. Left to fill the 76rem content width the tiles come out at 368px
     each, which is more presence than a secondary section should take from
     the menu above it. */
  max-inline-size: 60rem;
}

.instagram__tile {
  position: relative;
  overflow: hidden;
  /* 4:5, not square. Measured across all six posts: five render at 0.800 and
     one at 0.750, so a square tile was cropping a fifth off every one. At 4:5
     the media lands at exactly 1:1 with no scaling and nothing is lost. */
  aspect-ratio: 4 / 5;
  background-color: var(--espresso);
}

.instagram__tile img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* Instagram's own embed, cropped.
 *
 * The endpoint renders a card: an account header, then the media at exactly
 * the iframe's width, then an action bar and caption. Only the media is
 * wanted, so the iframe is made taller than the tile and pulled up by the
 * header's height - the media then lands square in a square tile at 1:1, no
 * scaling, no resampling.
 *
 * --ig-embed-header is the one number to touch if Instagram changes that
 * header. Measured against a live post; recalibrate the same way rather than
 * nudging it by eye. */
.instagram__tile {
  --ig-embed-header: 54px;
  --ig-embed-footer: 220px;
}

.instagram__tile iframe,
.ig-modal__frame iframe {
  position: absolute;
  inset-block-start: calc(-1 * var(--ig-embed-header));
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: calc(100% + var(--ig-embed-header) + var(--ig-embed-footer));
  border: 0;
  /* The overlay link owns every click, so the iframe must not eat any. This
   * also stops Instagram's own inconsistent tap handling - sometimes the
   * post, sometimes the profile, sometimes nothing. */
  pointer-events: none;
}

/* One link over the whole tile, going to that post. It is the only
 * interactive thing in the tile, so it carries the label. */
/* One button over the whole tile. A button, not a link: it opens the post
   here rather than navigating away. The way out to Instagram lives inside the
   dialog, where it is a deliberate choice instead of the only option. */
.instagram__tile button {
  position: absolute;
  inset: 0;
  z-index: 1;
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}
.instagram__tile button:focus-visible {
  outline-offset: calc(-1 * var(--focus-ring-width) - 2px);
}

/* The button is a full-tile hit area, so its label has to be there for a
   screen reader without being drawn over the photo.

   It has to be taken out of flow to do that. text-indent and overflow, which
   this used, apply to block containers - on the inline span they sit on they
   do nothing at all, and the label was printed across the top of all six
   tiles. Same technique as .visually-hidden, and for the same reason. */
.visually-hidden-label {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}


/* --------------------------------------------------- the post, opened here
   Glass, because a modal is the one surface that always has something behind
   it to refract. Dark tint with saturate(1), for the reason the header uses
   them: saturating burgundy invents a second red. */
.ig-modal {
  /* fixed, and never relative. A modal <dialog> is laid out against the
     viewport; giving it position:relative to anchor the close button hands
     its containing block back to its DOM parent - #instagram, most of the
     way down the page - and the browser then scrolls to reach it. Opening a
     post threw the reader back to the hero. fixed anchors the close button
     just as well and keeps the dialog where a dialog belongs. */
  position: fixed;
  inset: 0;
  margin: auto;
  padding: 0;
  inline-size: min(92vw, 26rem);
  max-block-size: 90svh;
  overflow: hidden;
  color: var(--on-surface-deep);
  /* Espresso, not burgundy: this opens over the Instagram section, and glass
     takes the colour of what it covers. */
  background: var(--glass-bg-deep);
  backdrop-filter: blur(var(--glass-blur)) saturate(1);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1);
  border: var(--hairline) solid var(--glass-line);
  border-radius: var(--glass-radius);
  box-shadow: 0 1px 0 var(--glass-sheen) inset,
              0 8px 24px var(--glass-shadow);
}

.ig-modal::backdrop { background-color: rgba(58, 33, 24, 0.72); }

/* The dialog lives in the top layer, so it is outside .section--dark and its
   glass children were falling back to the light recipe: cream text on a cream
   pane, about 2:1. They take the dark one, and saturate(1) with it - over
   burgundy, 1.4 renders #7A1717 as #8B1111 and invents a second red. */
.ig-modal .glass {
  background: var(--glass-bg-deep);
  --glass-saturate: 1;
}

/* The way out, floating over the top of the post rather than sitting under it
   (client, 2026-09-02). Under the post it was a row of its own, and a row of
   its own is height - which a 9:16 reel does not have to spare, so it was the
   first thing the dialog clipped when it ran out of room. Floating, it costs
   nothing and cannot be cut, on a reel or anything else. */
/* Bottom of the post, floating over it - not a row under it. A row is
   height, and a 9:16 reel has none to spare: it was the first thing the
   dialog clipped. Floating it costs nothing, so it survives at every
   ratio and every screen size. The top corner is where the subject of a
   portrait shot usually is; the bottom strip is dead space in both. */
.ig-modal__out {
  position: absolute;
  z-index: 2;
  inset-block-end: var(--space-2xs);
  inset-inline: var(--space-2xs);
  display: block;
  padding-block: var(--space-xs);
  text-align: center;
  color: var(--on-surface-dark);
  text-decoration: none;
  font-size: var(--fs-small);
}
.ig-modal__out:hover,
.ig-modal__out:focus-visible { color: var(--accent); }

/* Floats over the top corner of the media rather than taking a row of its
   own - the post is what the dialog is for. */
.ig-modal__close {
  position: absolute;
  z-index: 2;
  inset-block-start: var(--space-2xs);
  inset-inline-end: var(--space-2xs);
  display: grid;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  padding: 0;
  color: var(--on-surface-dark);
  cursor: pointer;
}
.ig-modal__close:hover { color: var(--accent); }

/* Cropped to the media exactly as the tiles are - but interactive here, so a
   reel plays instead of only being looked at. */
.ig-modal__frame {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  --ig-embed-header: 54px;
  --ig-embed-footer: 220px;
}

/* Reels.
 *
 * The embed always draws its media box at 4:5, so a 9:16 reel arrives inside
 * it pillarboxed - black down both sides. Those bars are Instagram's, painted
 * inside a cross-origin iframe, so they cannot be recoloured. They can be
 * cropped off: make the iframe wider than its box by exactly the ratio of the
 * two shapes and slide it back to centre.
 *
 *   zoom = (4/5) / (9/16) = 1.4222,  offset = (zoom - 1) / 2 = 21.11%
 *
 * In the dialog the frame itself becomes 9:16, so the whole reel shows at
 * full height with the bars gone and nothing cropped. In the grid the tile
 * stays 4:5 - six tiles of two different shapes is not a grid - so the same
 * zoom crops the reel top and bottom instead, which is what a thumbnail is
 * for. */
.ig-modal__frame[data-ig-type='reel'] { aspect-ratio: 9 / 16; }

/* A 9:16 frame is 1.78x its own width tall, so on any ordinary screen the
   dialog runs out of height long before it runs out of width - and with
   overflow:hidden that is not an overflow, it is a silent crop. Size the
   dialog from the height instead when it is holding a reel: whichever of the
   three limits is smallest wins. */
.ig-modal:has(.ig-modal__frame[data-ig-type='reel']) {
  inline-size: min(92vw, 26rem, 88svh * 9 / 16);
}

[data-ig-type='reel'] iframe {
  inline-size: 142.22%;
  inset-inline-start: -21.11%;
}
/* Interactive in the dialog, so a reel plays. Inert in the grid, where a
   button over it owns the click. */
.ig-modal__frame iframe { pointer-events: auto; }

/* A button, not an underlined line of text: it is the section's one action
   and it was reading as a footnote. Glass because it sits over the espresso
   with the grid behind it - and because the client asked for the two ways
   out of this section to look like the same thing (2026-09-02). */
.instagram__cta {
  display: inline-block;
  margin-block-start: var(--space-xl);
  padding: var(--space-xs) var(--space-l);
  color: var(--on-surface-dark);
  text-decoration: none;
  /* Espresso, not burgundy: this one sits on the deep surface, and the dark
     glass recipe is mixed for burgundy. It mixed its own value before
     --glass-bg-deep existed; it is the same number. */
  background: var(--glass-bg-deep);
  --glass-saturate: 1;
}
.instagram__cta:hover,
.instagram__cta:focus-visible { color: var(--accent); }

@media (max-width: 767px) {
  .instagram__grid { grid-template-columns: repeat(2, 1fr); }
}


/* ================================================================= links
   Beige, underlined with a border rather than text-decoration so the line
   clears the descenders Arabic sits below the baseline. No arrow appended -
   the link text says where it goes. */
.link {
  color: var(--accent);
  text-decoration: none;
  border-block-end: var(--hairline) solid currentColor;
  padding-block-end: 0.15em;
  transition: color var(--dur-fast) var(--ease-out);
}
.link:hover,
.link:focus-visible { color: var(--on-surface-dark); }


/* ================================================================ footer
   The mark and the three ways out on one side, the branches on the other,
   then one line. Espresso, the deepest surface on the page, and the end of
   the walk that started on the cream hero.

   Two rows, two columns, and the branches column spans both rows anchored to
   the top - so the logo and the buttons read as one block rather than as two
   things that happen to be stacked. The written-out contact column that used
   to sit here is gone (client, 2026-09-02): with three buttons doing the same
   three jobs, it was the same information twice. */
.footer__cols {
  display: grid;
  gap: var(--space-xl) var(--space-3xl);
  grid-template-areas:
    'brand'
    'branches'
    'actions';
}

.footer__brand { grid-area: brand; }
.footer__col--branches { grid-area: branches; }
.footer__actions { grid-area: actions; }

@media (min-width: 768px) {
  /* One row: the mark, then who and where, then the three ways out pushed to
     the far edge.

     The version before this stacked the mark over the buttons in a 1fr track
     and sent the branches to the opposite edge, which at 1216px wide left
     about 700px of nothing across the middle - the two ends were anchored and
     the middle was a hole. Grouping the mark and the branches together makes
     the space between them and the buttons a gutter instead: both sides are
     content, and the gap is the distance between two things rather than the
     absence of a third. */
  .footer__cols {
    grid-template-columns: auto auto minmax(0, 1fr);
    grid-template-areas: 'brand branches actions';
    column-gap: var(--space-2xl);
    align-items: start;
  }
  /* Centred against the block beside it, not top-aligned to it. The mark
     and the branches together stand about 200px tall and the button row is
     48; sitting it on their top line left the whole trailing half of the
     footer weighted to one corner. */
  .footer__actions { justify-self: end; align-self: center; }

  /* The mark is optically a little taller than the text beside it, so it
     drops by its own overshoot rather than sitting on the cap line. */
  .footer__brand { margin-block-start: -0.35rem; }
}

/* The full lockup, not the bare mark, and about twice the header's size
   (client, 2026-09-02). The footer is where the name gets to be said once
   properly; the header is where it has to get out of the way. */
.logo--footer {
  /* Small on a phone, where the footer is a stack and the lockup has to sit
     in it rather than lead it, and about twice the header size on a desktop,
     where there is a column for it (client, 2026-09-02). Not one clamp: a
     clamp wide enough to do both bottoms out at its minimum on every phone,
     so the phone value is the one that never gets chosen deliberately. */
  block-size: 3.5rem;
  inline-size: auto;
  color: var(--on-surface-deep);
}

@media (min-width: 768px) {
  .logo--footer { block-size: clamp(5rem, 7vw, 7rem); }
}

/* Small, quiet, sentence case, untracked - CLAUDE.md does not allow these to
   be uppercase or letterspaced, and they do not need to be. What separates a
   heading from its list here is colour and weight, not size. */
.footer__heading {
  margin-block-end: var(--space-s);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--fw-medium);
  line-height: var(--lh-tight);
  color: var(--muted-on-dark);
}

.footer__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  font-style: normal;
  line-height: var(--lh-tight);
}

.footer__sub {
  font-size: var(--fs-small);
  font-weight: var(--fw-light);
  color: var(--muted-on-dark);
}

/* The third branch has no address yet, so it says so and steps back rather
   than pretending to be one of the other two. */
.footer__soon { color: var(--muted-on-dark); font-size: var(--fs-small); }

/* An email address and a handle are Latin strings, and neither is copy that
   flips. Left to the paragraph direction, "@saudibox1" comes out of an RTL
   run as "saudibox1@" - the @ is bidi-neutral, so it is reordered to the end.
   isolate rather than plain direction:ltr so the run cannot reach out and
   reorder the Arabic beside it either. */
.ltr {
  direction: ltr;
  unicode-bidi: isolate;
}

/* ------------------------------------------------------- the three ways out
   Find us, follow us, write to us. A pill with a hairline, filling to beige
   on hover - beige is the only accent, so the emphasis is the fill and not a
   second colour.

   48px minimum, like every other tap target. The icons are inline SVG rather
   than a font or an image: three shapes at 18px in currentColor, so they
   invert with the button instead of needing a second file. */
.footer__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
  align-self: start;
}

.footer__action {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-block-size: 48px;
  padding-inline: var(--space-m);
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: 999px;
  background: none;
  color: var(--on-surface-deep);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--fw-medium);
  line-height: 1;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}

.footer__action svg {
  flex: none;
  color: var(--beige);
  transition: color var(--dur-fast) var(--ease-out);
}

.footer__action:hover,
.footer__action:focus-visible {
  background-color: var(--beige);
  border-color: var(--beige);
  color: var(--espresso);
}

.footer__action:hover svg,
.footer__action:focus-visible svg { color: var(--espresso); }

.footer__action:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* On a phone the three fill the width and stack, so none of them is the odd
   one out on a short second row. */
@media (max-width: 479px) {
  .footer__actions { flex-direction: column; align-items: stretch; }
  .footer__action { justify-content: center; }
}

.footer__rights {
  margin-block-start: var(--space-2xl);
  padding-block-start: var(--space-m);
  border-block-start: var(--hairline) solid var(--hairline-on-dark);
  max-inline-size: none;
  font-size: var(--fs-small);
  font-weight: var(--fw-light);
  font-variant-numeric: lining-nums;   /* Western digits in both languages */
  color: var(--muted-on-dark);
}

/* ------------------------------------------------------------- the picker
   Which branch, which address. A real <dialog> in the top layer, which is the
   only reason it is allowed to be glass down here: a panel anchored to a
   footer button would sit on flat espresso and be all of the cost of a blur
   with none of the effect. In the top layer the whole page is behind it.

   `fixed`, and never `relative`. A modal <dialog> is laid out against the
   viewport; giving it position:relative to anchor the close button hands its
   containing block back to its DOM parent and the browser scrolls to reach
   it - which is exactly how opening an Instagram post used to throw the
   reader back to the hero. */
.picker {
  position: fixed;
  inset: 0;
  margin: auto;
  padding: var(--space-l);
  inline-size: min(92vw, 23rem);
  max-block-size: 88svh;
  overflow: auto;
  color: var(--on-surface-dark);
  /* Espresso: this opens over the footer, and glass takes the colour of what
     it covers rather than carrying the brand red everywhere. */
  background: var(--glass-bg-deep);
  backdrop-filter: blur(var(--glass-blur)) saturate(1);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1);
  border: var(--hairline) solid var(--glass-line);
  border-radius: var(--glass-radius);
  box-shadow: 0 1px 0 var(--glass-sheen) inset,
              0 8px 24px var(--glass-shadow);
}

.picker::backdrop { background-color: rgba(58, 33, 24, 0.72); }

/* The dialog lives in the top layer, so it is outside .section--deep and its
   glass children fall back to the light recipe - cream on a cream pane, about
   2:1. They restate the dark one. */
.picker .glass {
  background: var(--glass-bg-deep);
  --glass-saturate: 1;
}

.picker__title {
  margin-block-end: var(--space-m);
  padding-inline-end: var(--space-2xl);   /* clear of the close button */
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  line-height: var(--lh-tight);
}

.picker__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.picker__item {
  display: block;
  min-block-size: 58px;
  padding: var(--space-s) var(--space-m);
  border: var(--hairline) solid var(--hairline-on-dark);
  border-radius: var(--glass-radius);
  color: var(--on-surface-dark);
  line-height: var(--lh-tight);
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}

.picker__item:hover,
.picker__item:focus-visible {
  background-color: var(--beige);
  border-color: var(--beige);
  color: var(--espresso);
}

.picker__item:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.picker__name { font-weight: var(--fw-medium); }

.picker__sub {
  display: block;
  margin-block-start: 2px;
  font-size: var(--fs-small);
  font-weight: var(--fw-light);
  color: var(--muted-on-dark);
}

.picker__item:hover .picker__sub,
.picker__item:focus-visible .picker__sub { color: var(--espresso); opacity: 0.72; }

.picker__close {
  position: absolute;
  inset-block-start: var(--space-s);
  inset-inline-end: var(--space-s);
  display: grid;
  place-items: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  padding: 0;
  color: var(--on-surface-dark);
  border-radius: 999px;
  cursor: pointer;
}

.picker__close:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}


/* ------------------------------------------------------- reduced motion
   Under `reduce` the hero does not pin and does not scrub: last frame as a
   static image, page scrolls normally. Enforced in JS as well. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
