/* ==========================================================================
   Hamrah Tour — design system
   White-first, photo-led catalogue UI. Content starts immediately; chrome
   stays out of the way. The brand blue is the single accent, and it now
   carries the conversion action too - see --cta below.
   ========================================================================== */

:root {
  --white: #ffffff;
  --ink: #222222;
  --ink-muted: #717171;
  --ink-faint: #9c9c9c;

  --line: #dddddd;
  --line-soft: #ebebeb;
  --surface-alt: #f7f7f7;

  --accent: #0d72c4;
  --accent-dark: #0a5c9e;
  --accent-soft: #eaf4fc;

  /* There were three --cat-* accents here, one per category, read by the map
     pins and the small card dot. Eight categories cannot be told apart by
     colour without inventing five more accents, so the pin carries the
     category's icon instead and everything category-coloured now uses
     --accent. Blue stays the only accent, as it is everywhere else. */

  /* The conversion action. This was WhatsApp's own green, which is a strong
     recognition cue, but the client asked for one blue palette across the
     site; the WhatsApp glyph stays on every one of these buttons and carries
     that recognition instead. Two lines to put the green back. */
  --cta: var(--accent);
  --cta-dark: var(--accent-dark);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08);
  --shadow-md: 0 6px 16px rgba(0, 0, 0, 0.12);

  --dur: 180ms;
  /* Long enough for a fill to read as a sweep, short enough to finish
     before the pointer moves on. The original template used 1000ms. */
  --dur-sweep: 300ms;
  --ease: cubic-bezier(0.2, 0, 0.2, 1);

  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-pill: 999px;

  --container: 1280px;
  --gutter: clamp(1rem, 4vw, 2.5rem);

  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

/* Both Arabic-script languages share Vazirmatn. Cairo was tried for Arabic on
   the argument that Vazirmatn is drawn for Persian, but it read thin and narrow
   at body sizes; Vazirmatn covers the script and sets heavier. */
html[lang="fa"],
html[lang="ar"] {
  --font: "Vazirmatn", -apple-system, "Segoe UI", sans-serif;
}

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

body {
  margin: 0;
  background: var(--white);
  color: var(--ink);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img {
  display: block;
  max-width: 100%;
}

a {
  color: inherit;
  text-decoration: none;
}

h1,
h2,
h3 {
  margin: 0;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
}

html[lang="fa"] h1,
html[lang="fa"] h2,
html[lang="fa"] h3 {
  letter-spacing: 0;
  line-height: 1.45;
}

p {
  margin: 0;
}

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: 4px;
}

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.skip-link {
  position: absolute;
  inset-inline-start: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.7rem 1.1rem;
  background: var(--white);
  box-shadow: var(--shadow-md);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}

.skip-link:focus {
  inset-inline-start: 1rem;
}

/* ---------- Header ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--white);
  border-bottom: 1px solid var(--line-soft);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.9rem;
}

/* Scoped to the header: the footer reuses .brand, and an auto margin there
   would shove its tagline around. */
.header-inner .brand {
  margin-inline-end: auto;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--accent);
}

/* The mark is sized in em, so it tracks the wordmark at every breakpoint, and
   it takes its blue from .brand through currentColor - the same single-token
   arrangement the map pins and the category icons use. It is deliberately not
   a span: .brand span is the second half of the wordmark, and an svg keeps
   clear of that rule and of the hover colour that goes with it. */
.brand svg {
  flex: none;
  width: 1.4em;
  height: 1.4em;
}

.brand span {
  font-weight: 500;
  color: var(--ink);
  transition: color var(--dur) var(--ease);
}

.brand:hover span {
  color: var(--accent);
}

.nav {
  display: flex;
  align-items: center;
  gap: 0.2rem;
}

.nav a {
  position: relative;
  padding: 0.5rem 0.9rem;
  border-radius: var(--radius-pill);
  font-size: 0.94rem;
  font-weight: 500;
  color: var(--ink);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

/* Underline animates on transform, so it runs on the compositor rather
   than triggering layout on every hover. */
.nav a::after {
  content: "";
  position: absolute;
  inset-inline: 0.9rem;
  bottom: 0.25rem;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transition: transform var(--dur) var(--ease);
}

.nav a:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

.nav a:hover::after {
  transform: scaleX(1);
}

/* The current page is a filled pill, so it stays distinct from hover
   rather than sharing one grey the way it used to. */
.nav a[aria-current="page"] {
  background: var(--accent);
  color: var(--white);
}

.nav a[aria-current="page"]::after {
  display: none;
}

/* A segmented pill holding every language, sitting outside the collapsible
   nav so it is reachable at any width. The active item borrows the nav's
   current-page treatment exactly, so the header speaks with one voice. */
/* The shell is only a positioning context now: the segmented pill's border
   lives on the list, so that below the breakpoint the list can lift away into
   a dropdown panel and the border goes with it. */
.lang-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.lang-switch__list {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  padding: 0.2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
}

/* Only exists below the breakpoint; four languages will not fit a segmented
   pill beside the brand and the menu button on a phone. */
.lang-switch__toggle {
  display: none;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.7rem;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease),
    background var(--dur) var(--ease);
}

.lang-switch__toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.lang-switch__toggle[aria-expanded="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--white);
}

.lang-switch__caret {
  width: 0;
  height: 0;
  border-inline: 3.5px solid transparent;
  border-top: 4px solid currentColor;
  transition: transform var(--dur) var(--ease);
}

.lang-switch__toggle[aria-expanded="true"] .lang-switch__caret {
  transform: rotate(180deg);
}

.lang-switch__item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-pill);
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--ink-muted);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

/* :not([aria-current]) matters. Both items are links now, and a bare
   a.lang-switch__item:hover outranks the active rule on specificity, so
   hovering the language you are already on would wash the blue pill out. */
.lang-switch__item:not([aria-current]):hover {
  background: var(--accent-soft);
  color: var(--accent);
}

.lang-switch__item[aria-current="true"] {
  background: var(--accent);
  color: var(--white);
}

/* Both flags carry large white fields that would otherwise dissolve into the
   white header, so each gets a hairline ring - light on the blue active pill,
   dark elsewhere.

   No RTL mirroring: a flag is a picture, not text. The canton belongs at its
   own top-left in Persian too, so the scaleX(-1) used on .venture__arrow must
   never be copied here. */
.lang-switch__item svg,
.lang-switch__toggle svg {
  flex: none;
  width: 18px;
  height: 12px;
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12);
}

/* An inline SVG with a viewBox and no width/height collapses to nothing inside
   a flex container, so the trigger must be listed here too. */
.lang-switch__item[aria-current="true"] svg,
.lang-switch__toggle[aria-expanded="true"] svg {
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35);
}

.nav-toggle {
  display: none;
  align-items: center;
  padding: 0.55rem 0.7rem;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: 0.88rem;
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease),
    background var(--dur) var(--ease);
}

.nav-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* Open menu: the toggle reads as active, matching the nav pills */
.nav-toggle[aria-expanded="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--white);
}

/* Three bars rather than the word "Menu" — the accessible name moved to
   aria-label on the button, so the control is still announced. currentColor is
   what lets the bars turn white against the blue when the menu is open. */
.nav-toggle__bars {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  width: 18px;
  height: 14px;
}

.nav-toggle__bars span {
  display: block;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

/* 6px is the gap between bar centres: 2px bars evenly spread over 14px. */
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:first-child {
  transform: translateY(6px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) {
  opacity: 0;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:last-child {
  transform: translateY(-6px) rotate(-45deg);
}

@media (max-width: 760px) {
  .nav-toggle {
    display: inline-flex;
  }

  .nav {
    position: absolute;
    inset-inline: var(--gutter);
    top: calc(100% + 0.5rem);
    flex-direction: column;
    align-items: stretch;
    gap: 0.1rem;
    padding: 0.5rem;
    background: var(--white);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
  }

  .nav[hidden] {
    display: none;
  }

  .nav a {
    padding: 0.75rem 0.9rem;
  }
}

/* Same breakpoint as the navigation, so the header has one compact mode
   rather than two. Two languages fitted a pill on a phone; four do not, so the
   list becomes a dropdown and the current language moves onto the trigger.
   Above this it stays the one-tap segmented pill. */
@media (max-width: 760px) {
  .lang-switch__toggle {
    display: inline-flex;
  }

  .lang-switch__list {
    position: absolute;
    top: calc(100% + 0.5rem);
    inset-inline-end: 0;
    z-index: 60;
    flex-direction: column;
    align-items: stretch;
    gap: 0.1rem;
    min-width: 11rem;
    padding: 0.4rem;
    background: var(--white);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
  }

  /* Beats the .lang-switch__list display above on specificity, so the panel
     really does close. */
  .lang-switch__list[hidden] {
    display: none;
  }

  .lang-switch__item {
    padding: 0.6rem 0.7rem;
    border-radius: var(--radius-sm);
  }
}

/* ---------- Page intro ---------- */

.page-intro {
  padding-block: clamp(1.8rem, 4vw, 2.8rem) 0.4rem;
}

.page-intro h1 {
  font-size: clamp(1.55rem, 3.2vw, 2.1rem);
}

.page-intro p {
  max-width: 62ch;
  margin-top: 0.5rem;
  color: var(--ink-muted);
}

/* ---------- Category tiles ---------- */

/* The type filter. Deliberately NOT inside .filters: it scrolls away while the
   city chips stay pinned, because nine tiles plus the chip row stuck under the
   header would take most of a phone screen.

   auto-fit with a minimum does the whole responsive job with no breakpoints,
   the same way .card-grid does: three across on a 390px phone, and at desktop
   widths the empty tracks collapse and the nine tiles stretch to one row. */
.category-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
  gap: 0.6rem;
  padding-block: 0.25rem 1.1rem;
}

/* Everything below this point up to the variant blocks is SHARED by both looks:
   the box, the icon and the label. The two variants differ only in how a tile
   is framed and how it marks hover and selection, so the shared half stays one
   copy and the flag in site.json -> design.categoryTiles picks the other half.
   See the two "variant" blocks at the foot of this section. */
.category-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.9rem 0.5rem;
  border-radius: var(--radius-md);
  text-align: center;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.category-tile__icon {
  display: flex;
  width: 24px;
  height: 24px;
  color: var(--accent);
  transition: color var(--dur) var(--ease);
}

/* Fill rather than stroke: the Font Awesome solid set is filled artwork, and
   currentColor is what lets one rule invert the glyph on the active tile. */
.category-tile__icon svg {
  width: 100%;
  height: 100%;
  fill: currentColor;
}

.category-tile__label {
  font-size: 0.82rem;
  font-weight: 500;
  line-height: 1.25;
}

/* Hover takes the label to 700, which in principle can rewrap it and grow the
   grid under the cursor. A hidden bold copy reserving the width was built for
   that and then removed: measured across all four languages at 390px and
   1280px, and against five invented labels sitting at the wrap boundary, the
   bold weight never changed a tile's height. The row is sized by its tallest
   tile, and with nine tiles something already carries two lines. If a future
   label does start jumping, reserve the width with a zero-height
   ::after { content: attr(data-label); font-weight: 700 } - but do not carry
   that machinery until something actually moves. */

/* Nothing is framed at rest, which is what makes the selected tile legible: it
   ends up the only bordered thing in the row. A deliberate exception to the
   site's "solid blue pill" active treatment - the client rejected the fill on
   these after seeing it live. Blue survives as the icon, the label and the
   selected border, never as a field of colour.

   The other Type filter, "chips", is not styled here at all: it reuses .chip
   and .filter-row, the same rules the City row and the map legend use. */
.category-tile[aria-current="true"] {
  box-shadow: inset 0 0 0 2px var(--accent);
}

.category-tile[aria-current="true"] .category-tile__label {
  color: var(--accent);
  font-weight: 700;
}

/* With no border at rest the hit area is invisible until it is touched, and a
   phone never sees the hover tint. This is the only feedback a tap gets. */
.category-tile:active {
  background: var(--surface-alt);
}

/* At the foot of the section, after every rule it overrides. A media block
   placed above the rules it overrides loses silently on source order - the
   mobile card block lost ten of thirteen declarations that way.

   No :not([aria-current]) guard on the whole rule, unlike .chip and
   .lang-switch__item. There, hover and active both set a background fill, so an
   unguarded :hover outranks the active rule and washes the blue out. These
   tiles have escaped that: hover sets a background, selected sets a box-shadow,
   and the two do not collide. The one thing worth withholding is the grey tint
   on an already-selected tile - it still scales, so hovering it is not dead. */
@media (hover: hover) {
  .category-tile:hover {
    transform: scale(1.06);
    z-index: 1;
  }

  .category-tile:not([aria-current]):hover {
    background: var(--surface-alt);
  }

  .category-tile:hover .category-tile__label {
    color: var(--accent);
    font-weight: 700;
  }
}

/* ---------- Filters ---------- */

.filters {
  position: sticky;
  /* Set from the header's measured height in scripts/main.js. The literal is
     only the no-JS fallback - do not tune it by hand. */
  top: var(--header-h, 61px);
  z-index: 40;
  padding-block: 1rem;
  background: var(--white);
}

.filter-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 0.15rem;
}

.filter-row::-webkit-scrollbar {
  display: none;
}

.filter-row + .filter-row {
  margin-top: 0.55rem;
}

.filter-row > span {
  flex: none;
  min-width: 3rem;
  font-size: 0.8rem;
  color: var(--ink-faint);
}

.chip {
  flex: none;
  padding: 0.45rem 1rem;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: 0.87rem;
  color: var(--ink);
  white-space: nowrap;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

.chip:hover {
  border-color: var(--ink);
}

/* The catalogue's filter chips are links to generated pages and mark
   themselves with aria-current; the map legend's chips are still buttons
   toggling a layer, so they keep aria-pressed. Same pill either way. */
.chip[aria-current="true"],
.chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--white);
}

.results-count {
  padding-block: 0.4rem 1.2rem;
  font-size: 0.86rem;
  color: var(--ink-muted);
}

.empty-state {
  padding: 4rem 1rem;
  text-align: center;
  color: var(--ink-muted);
}

/* ---------- Pagination ---------- */

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding-block: 2.75rem 0.5rem;
}

.pagination__pages {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.pagination__step,
.pagination__page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-width: 2.3rem;
  height: 2.3rem;
  padding-inline: 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  font-size: 0.88rem;
  color: var(--ink);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

/* :not([aria-current]) is load-bearing, the same way it is on the language
   switcher: every item here is a link, so an unguarded :hover outranks the
   current-page rule and washes the filled pill out. */
.pagination__step:hover,
.pagination__page:not([aria-current]):hover {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}

.pagination__page[aria-current="page"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--white);
}

.pagination__gap {
  padding-inline: 0.15rem;
  color: var(--ink-faint);
}

/* Literal left/right arrows, so RTL has to mirror them to point along the
   reading direction. inline-block because transforms do not apply to inline
   boxes. */
.pagination__arrow {
  display: inline-block;
}

html[dir="rtl"] .pagination__arrow {
  transform: scaleX(-1);
}

@media (max-width: 480px) {
  .pagination__step,
  .pagination__page {
    min-width: 2.1rem;
    height: 2.1rem;
    padding-inline: 0.6rem;
    font-size: 0.82rem;
  }
}

/* ---------- Card grid ---------- */

/* Three across on desktop rather than four or five: this is a curated
   shortlist, not a page of search results, so the photos get room. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 2.75rem 2rem;
  padding-bottom: 4rem;
}

.card {
  transition: transform var(--dur) var(--ease);
}

/* Flex column with the price pushed to the bottom, so the price sits on
   the same line across a row even when titles wrap to different depths. */
.card > a {
  display: flex;
  flex-direction: column;
  height: 100%;
  color: inherit;
}

.card__media {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-alt);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--dur) var(--ease);
}

/* A described photo arrives wrapped in <picture> for its WebP source. Without a
   box of its own, the img keeps sizing against .card__media exactly as a bare
   img does. */
.card__media picture,
.gallery__slide picture {
  display: contents;
}

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 400ms var(--ease);
}

/* Slides up over the foot of the photo on hover. It carries no price:
   there is no hover on touch, so anything hidden here would be invisible
   to most visitors. It is an affordance cue, nothing more. */
.card__reveal {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  padding: 0.85rem;
  background: var(--accent);
  color: var(--white);
  font-size: 0.9rem;
  font-weight: 600;
  transform: translateY(100%);
  transition: transform var(--dur) var(--ease);
}

html[dir="rtl"] .card__arrow {
  transform: scaleX(-1);
}

.card h2 {
  transition: color var(--dur) var(--ease);
}

@media (hover: hover) {
  .card:hover {
    transform: translateY(-4px);
  }

  .card:hover .card__media {
    box-shadow: var(--shadow-md);
  }

  .card:hover .card__media img {
    transform: scale(1.04);
  }

  .card:hover .card__reveal {
    transform: translateY(0);
  }

  .card:hover h2 {
    color: var(--accent);
  }
}

/* Keyboard users get the cue too, without needing a pointer */
.card:focus-within .card__reveal {
  transform: translateY(0);
}

.card__badge {
  position: absolute;
  top: 0.7rem;
  inset-inline-start: 0.7rem;
  padding: 0.28rem 0.7rem;
  background: var(--white);
  border-radius: var(--radius-pill);
  font-size: 0.75rem;
  font-weight: 600;
  box-shadow: var(--shadow-sm);
}

.card__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  padding-top: 0.7rem;
}

.card__meta {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--ink-muted);
}

/* The colour used to encode the category; it does not any more, because the
   card already names the type in .card__badge and eight blue dots would say
   nothing. It stays as a separator between the city and the rest of the meta
   line. */
.card__dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
}

.card h2 {
  margin-top: 0.15rem;
  font-size: 0.98rem;
  font-weight: 600;
}

.card__short {
  margin-top: 0.1rem;
  font-size: 0.9rem;
  color: var(--ink-muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card__foot {
  margin-top: auto;
  padding-top: 0.45rem;
}

.price {
  font-size: 0.92rem;
  color: var(--ink);
}

.price b {
  font-weight: 600;
}

.price span {
  color: var(--ink-muted);
}

/* Phones keep two across. A single full-width column turns the catalogue
   into a slow vertical scroll where nothing can be compared side by side.

   This block has to stay at the FOOT of the card section, after every base
   rule it overrides. It used to sit directly under `.card-grid`, which reads
   more logically but put it ahead of `.card__media`, `.card__badge`,
   `.card__meta`, `.card h2`, `.card__short` and `.price` - equal specificity,
   so the later base rule won and ten of the thirteen declarations here never
   applied at any width. Only `.card-grid`'s two and the `line-height` did
   anything. Nothing reports a rule that loses on source order; it simply has
   no effect, which is why this went unnoticed. */
@media (max-width: 620px) {
  .card-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.6rem 0.8rem;
  }

  .card__media {
    border-radius: var(--radius-md);
  }

  .card__badge {
    top: 0.5rem;
    inset-inline-start: 0.5rem;
    padding: 0.2rem 0.5rem;
    font-size: 0.68rem;
  }

  .card__meta {
    font-size: 0.78rem;
  }

  .card h2 {
    font-size: 0.92rem;
    line-height: 1.3;
  }

  .card__short {
    font-size: 0.82rem;
    -webkit-line-clamp: 2;
  }

  .price {
    font-size: 0.85rem;
  }
}

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

.btn {
  position: relative;
  /* Own stacking context, so the ::before fill reliably paints above the
     button's background but below its label — never behind an ancestor. */
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.8rem 1.4rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

/* The fill sweeps in from the inline-start edge behind a skewed edge.
   Each variant sets its own --btn-sweep, so the mechanic is shared while
   every button keeps its own identity. */
.btn::before {
  content: "";
  position: absolute;
  inset-block: -1px;
  inset-inline-start: -25%;
  width: 0;
  background: var(--btn-sweep, transparent);
  transform: skewX(-20deg);
  transition: width var(--dur-sweep) var(--ease);
  z-index: -1;
}

.btn:hover::before,
.btn:focus-visible::before {
  width: 150%;
}

html[dir="rtl"] .btn::before {
  transform: skewX(20deg);
}

.btn--primary {
  --btn-sweep: var(--accent-dark);
  background: var(--accent);
  color: var(--white);
}

.btn--outline {
  --btn-sweep: var(--accent);
  border-color: var(--line);
  background: var(--white);
  color: var(--ink);
}

.btn--outline:hover {
  border-color: var(--accent);
  color: var(--white);
}

/* Still the WhatsApp button by name and by icon - only the colour moved. */
.btn--whatsapp {
  --btn-sweep: var(--cta-dark);
  background: var(--cta);
  color: var(--white);
}

.btn--block {
  width: 100%;
}

.btn svg {
  flex: none;
  width: 18px;
  height: 18px;
}

/* ---------- Map ---------- */

.map-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding-block: 1rem;
}

.map-legend > span {
  font-size: 0.8rem;
  color: var(--ink-faint);
}

.legend-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}

/* Matches the glyph inside the pin it toggles. currentColor means the icon
   inverts with the chip when it is pressed, with no second rule. */
.legend-chip__icon {
  display: inline-flex;
  flex: none;
  width: 14px;
  height: 14px;
  color: var(--accent);
}

.legend-chip[aria-pressed="true"] .legend-chip__icon {
  color: currentColor;
}

.legend-chip__icon svg {
  width: 100%;
  height: 100%;
  fill: currentColor;
}

.map-shell {
  height: min(70vh, 640px);
  margin-bottom: 3rem;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface-alt);
}

#map {
  width: 100%;
  height: 100%;
}

/* Leaflet map pins. These are real DOM, so they take their colour from the
   stylesheet — one source of truth, and nothing about the palette is duplicated
   into the JSON payload.

   Every pin is the same brand blue and identifies its category by the glyph
   inside it rather than by hue. Eight distinguishable pin colours would have
   meant five new accents on a site where blue is the only one. */
.pin {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: 3px solid var(--white);
  border-radius: 50%;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  background: var(--accent);
  cursor: pointer;
}

.pin svg {
  width: 11px;
  height: 11px;
  fill: var(--white);
}

.leaflet-marker-icon:focus-visible .pin {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* Leaflet's popup chrome, brought in line with the site's surfaces */
.leaflet-popup-content-wrapper {
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: 0;
}

.leaflet-popup-content {
  margin: 0;
  padding: 0.7rem;
  font-family: var(--font);
}

.leaflet-container {
  font-family: var(--font);
  background: var(--surface-alt);
}

.leaflet-container a {
  color: var(--accent);
}

.leaflet-control-attribution {
  font-size: 0.72rem;
}

.info-card {
  min-width: 190px;
  max-width: 230px;
  font-family: var(--font);
}

.info-card img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: var(--radius-sm);
}

.info-card h4 {
  margin: 0.55rem 0 0.15rem;
  font-size: 0.95rem;
  font-weight: 600;
}

.info-card p {
  font-size: 0.82rem;
  color: var(--ink-muted);
}

.info-card__actions {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.65rem;
}

.info-card__actions a {
  flex: 1;
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-sm);
  font-size: 0.8rem;
  font-weight: 600;
  text-align: center;
}

/* These two sit side by side in a map pin's popup. With the CTA no longer
   green they would read as one undifferentiated pair, so the hierarchy is
   carried by weight instead of hue: WhatsApp stays solid as the action, and
   Details steps back to the pale tint. */
.info-card__actions .is-primary {
  background: var(--accent-soft);
  color: var(--accent);
}

.info-card__actions .is-wa {
  background: var(--cta);
  color: var(--white);
}

/* ---------- Listing detail ---------- */

.breadcrumb {
  display: inline-block;
  padding-block: 1.4rem 0.4rem;
  font-size: 0.88rem;
  color: var(--ink-muted);
}

.breadcrumb:hover {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.detail-head h1 {
  font-size: clamp(1.5rem, 3.2vw, 2rem);
}

.meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin-top: 0.5rem;
  font-size: 0.9rem;
  color: var(--ink-muted);
}

/* ---------- Test site banner ---------- */

/* Rendered only on the test deployment (DECISIONS §46), never on production.
   Amber on purpose: blue is the product's one accent, and this strip is not
   part of the product - it has to read as "not the real site" at a glance.
   It is not sticky, so the sticky header and filter bar are measured and placed
   exactly as on production. */
.test-banner {
  padding: 0.35rem var(--gutter);
  background: #fff3cd;
  border-bottom: 1px solid #f1d27a;
  color: #6b4a00;
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.4;
  text-align: center;
}

/* ---------- 404 ---------- */

/* One file answers for all four languages: each is a block here and an inline
   script unhides the one matching the URL. With scripting off the site default
   stays visible, which is why the blocks are plain hidden attributes rather
   than anything cleverer. */
.notfound__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 70vh;
  text-align: center;
}

.notfound__block {
  max-width: 46ch;
}

.notfound__code {
  margin: 0 0 0.5rem;
  font-size: clamp(3rem, 12vw, 5rem);
  font-weight: 700;
  line-height: 1;
  color: var(--accent-soft);
}

.notfound__block h1 {
  font-size: clamp(1.4rem, 4vw, 2rem);
}

.notfound__body {
  margin: 0.75rem 0 1.75rem;
  color: var(--ink-muted);
}

/* ---------- Listing photo carousel ---------- */

/* A filmstrip on a scroll-snap row rather than a JavaScript slider: swiping and
   trackpad scrolling work on their own, and the arrows below are enhancement.

   Every photo is shown whole, at its own shape, and all at one height. There is
   no frame around them, so nothing has to fill the space beside a portrait
   photo - the two earlier answers were a fixed 2:1 frame that cropped the
   client's portrait photos to a third of their height, and then a blurred copy
   of the photo behind it, which the client disliked.

   --gallery-h is the one height. It is never taller than the window minus the
   header and the title above (12rem leaves room for a name that wraps), so a
   desktop visitor sees whole photos without scrolling; the old frame was 600px
   starting 207px down, and ran past the foot of an 800px window. It is a fixed
   height rather than a maximum so that photos arriving never move the page. */
.gallery {
  --gallery-h: max(20rem, min(calc(100vh - var(--header-h, 61px) - 12rem), 42rem));
  --gallery-h: max(20rem, min(calc(100svh - var(--header-h, 61px) - 12rem), 42rem));
  /* No photo is wider than the column, so even a panorama is seen whole. */
  --gallery-photo-max: min(calc(100vw - 2 * var(--gutter)), calc(var(--container) - 2 * var(--gutter)));
  position: relative;
  margin-top: 1.2rem;
}

.gallery__track {
  display: flex;
  gap: 0.5rem;
  height: var(--gallery-h);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.gallery__track::-webkit-scrollbar {
  display: none;
}

/* Auto margins on the two ends centre a strip that does not fill the column -
   one photo, or two portraits on a desktop - and collapse to nothing once it
   overflows, so unlike justify-content: center they never push the first photo
   out of reach. */
.gallery__slide {
  flex: none;
  margin: 0;
  scroll-snap-align: start;
}

.gallery__slide:first-child {
  margin-inline-start: auto;
}

.gallery__slide:last-child {
  margin-inline-end: auto;
}

/* The height is fixed and the width follows the photo's shape (until it
   arrives, the 3:4 in the markup). contain only matters when the width cap
   binds - a panorama, or a landscape photo on a phone - and then the photo is
   letterboxed on the white page rather than stretched. */
.gallery__photo {
  display: block;
  width: auto;
  height: var(--gallery-h);
  max-width: var(--gallery-photo-max);
  object-fit: contain;
  border-radius: var(--radius-md);
}

/* Hidden until html.js proves the script can run. Without it the track still
   scrolls, so the reader loses nothing but a control that would not work. */
.gallery__nav,
.gallery__dots {
  display: none;
}

/* Centred on the strip, not on .gallery, which also holds the dots below. */
.js .gallery__nav {
  position: absolute;
  top: calc(var(--gallery-h) / 2);
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
    border-color var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

.js .gallery__nav--prev {
  inset-inline-start: 0.75rem;
}

.js .gallery__nav--next {
  inset-inline-end: 0.75rem;
}

.gallery__nav:hover:not(:disabled) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--white);
}

/* At the ends rather than wrapping, the way pagination drops Previous on
   page 1. Kept in the layout so the frame does not shift. */
.gallery__nav:disabled {
  opacity: 0;
  pointer-events: none;
}

/* The glyphs are literal left/right arrows, so RTL has to mirror them. */
.gallery__arrow {
  display: inline-block;
  font-size: 1.05rem;
  line-height: 1;
}

html[dir="rtl"] .gallery__arrow {
  transform: scaleX(-1);
}

/* Below the strip rather than over a photo: with several photos in view there
   is no one photo for them to sit on. Every dot whose photo is fully on screen
   is dark, so on a desktop they show the stretch of the set you are looking
   at. */
.js .gallery__dots {
  display: flex;
  justify-content: center;
  gap: 0.4rem;
  margin-top: 0.75rem;
}

.gallery__dot {
  width: 0.5rem;
  height: 0.5rem;
  padding: 0;
  background: var(--line);
  border: 0;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.gallery__dot[data-visible] {
  background: var(--ink-muted);
}

.gallery__dot[aria-current="true"] {
  transform: scale(1.3);
}

/* Everything already in view: nothing to page through, and dots that are all
   dark say nothing. gallery.js sets this, and re-checks on resize. */
.gallery.is-static .gallery__nav,
.gallery.is-static .gallery__dots {
  display: none;
}

@media (max-width: 560px) {
  /* One photo at a time on a phone, at most 88% of the column so the next one
     shows at the edge - the cue that there is more. The height fits a 3:4
     portrait at that width, the shape the client's photos arrive in. */
  .gallery {
    --gallery-photo-max: calc((100vw - 2 * var(--gutter)) * 0.88);
    --gallery-h: calc((100vw - 2 * var(--gutter)) * 0.88 * 4 / 3);
  }

  .js .gallery__nav {
    width: 2.1rem;
    height: 2.1rem;
  }

  .js .gallery__nav--prev {
    inset-inline-start: 0.5rem;
  }

  .js .gallery__nav--next {
    inset-inline-end: 0.5rem;
  }
}

.detail {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(280px, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
  padding-block: 2rem 3.5rem;
}

@media (max-width: 880px) {
  .detail {
    grid-template-columns: 1fr;
  }
}

.detail h2 {
  font-size: 1.15rem;
}

.detail__body p {
  margin-top: 0.8rem;
  color: var(--ink-muted);
}

.detail__body h2 + p {
  margin-top: 0.6rem;
}

.detail__body h2:not(:first-child) {
  margin-top: 2rem;
}

.highlights {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0.6rem 1.5rem;
  margin-top: 0.9rem;
  padding: 0;
  list-style: none;
}

.highlights li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--ink-muted);
}

.highlights li::before {
  content: "";
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
}

.booking-panel {
  position: sticky;
  top: 5.5rem;
  padding: 1.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

.booking-panel .price {
  font-size: 1.1rem;
}

.booking-panel .price b {
  font-size: 1.35rem;
}

.booking-panel .btn {
  margin-top: 1.1rem;
}

.booking-panel .btn + .btn {
  margin-top: 0.55rem;
}

.booking-panel .note {
  margin-top: 0.9rem;
  font-size: 0.8rem;
  color: var(--ink-faint);
}

/* Category facts: compact label/value pairs above the detail lists. */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1.1rem 1.5rem;
  margin: 1.5rem 0 0;
  padding: 1.2rem 0;
  border-block: 1px solid var(--line-soft);
}

.fact dt {
  font-size: 0.82rem;
  color: var(--ink-muted);
}

.fact dd {
  margin: 0.15rem 0 0;
  font-weight: 600;
}

/* ---------- Prose ---------- */

.prose {
  max-width: 66ch;
  padding-block: clamp(1.8rem, 4vw, 2.8rem) 3rem;
}

.prose h1 {
  font-size: clamp(1.55rem, 3.2vw, 2.1rem);
}

.prose .lead {
  margin-top: 0.9rem;
  font-size: 1.1rem;
}

.prose p + p {
  margin-top: 1rem;
}

.prose p:not(.lead) {
  color: var(--ink-muted);
}

.prose h2 {
  margin-top: 2.4rem;
  font-size: 1.15rem;
}

.prose .actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.65rem;
  margin-top: 1.4rem;
}

/* ---------- Closing CTA ---------- */

.cta-band {
  padding-block: clamp(2.5rem, 6vw, 3.5rem);
  border-top: 1px solid var(--line-soft);
  text-align: center;
}

.cta-band h2 {
  font-size: clamp(1.3rem, 2.8vw, 1.7rem);
}

.cta-band p {
  margin-top: 0.4rem;
  color: var(--ink-muted);
}

.cta-band .btn {
  margin-top: 1.3rem;
}

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

.site-footer {
  padding-block: 2.5rem 2rem;
  background: var(--surface-alt);
  border-top: 1px solid var(--line-soft);
}

.footer-top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 2rem;
  padding-bottom: 1.8rem;
  border-bottom: 1px solid var(--line);
}

.footer-brand {
  max-width: 34ch;
}

.footer-brand p {
  margin-top: 0.5rem;
  font-size: 0.9rem;
  color: var(--ink-muted);
}

.footer-col h3 {
  margin-bottom: 0.6rem;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.footer-col a {
  display: block;
  padding-block: 0.2rem;
  font-size: 0.92rem;
  color: var(--ink-muted);
  transition: color var(--dur) var(--ease);
}

/* The accent rather than plain ink, so the footer answers the navigation. */
.footer-col a:hover {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.footer-bottom {
  padding-top: 1.4rem;
  font-size: 0.82rem;
  color: var(--ink-faint);
}

.footer-bottom p + p {
  margin-top: 0.5rem;
}

/* The developer's signature, deliberately reading as punctuation at the end of
   the copyright line rather than as a byline: the site speaks as Hamrah Tour
   everywhere outside About, so a name here would be the wrong voice. It is
   still a real link with an accessible name - an unlabelled one would be a
   dead end for a screen reader. Full attribution is in the HTML comment at the
   top of every page, which is where a developer would look. */
.footer-sign {
  display: inline-block;
  /* The glyph is ~8px, which is nowhere near a usable tap target. The padding
     buys a real hit area and the negative margin gives the space back, so the
     mark stays punctuation-sized without being unreachable on a phone. */
  padding: 0.5rem;
  margin: -0.5rem;
  margin-inline-start: -0.35rem;
  font-size: 0.62em;
  line-height: 1;
  color: var(--ink-faint);
  vertical-align: 0.15em;
  transition: color var(--dur) var(--ease);
}

.footer-sign:hover {
  color: var(--accent);
}

/* ---------- WhatsApp floating action ---------- */

.wa-fab {
  position: fixed;
  inset-inline-end: clamp(1rem, 3vw, 1.8rem);
  bottom: clamp(1rem, 3vw, 1.8rem);
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.8rem 1.2rem;
  background: var(--cta);
  color: var(--white);
  font-weight: 600;
  font-size: 0.92rem;
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-md);
  transition: background var(--dur) var(--ease);
}

.wa-fab:hover {
  background: var(--cta-dark);
}

.wa-fab svg {
  flex: none;
  width: 21px;
  height: 21px;
}

@media (max-width: 560px) {
  .wa-fab span {
    display: none;
  }

  .wa-fab {
    padding: 0.9rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.001ms !important;
  }

  .card:hover .card__media img,
  .card:hover,
  .category-tile:hover {
    transform: none;
  }
}

/* ==========================================================================
   About page
   The one page that steps outside the white catalogue style, at the client's
   request, to read as a portfolio. Two constraints shaped it:

   1. The source photographs top out at 591px. Nothing here is displayed above
      its native size, and the dark hero is doing real work — softness and
      compression noise read far less on dark than on white.
   2. Reveal styles sit behind html.js, set by an inline script in the head.
      Without JS the content is simply visible; it is never left at opacity 0.

   These class names are About-scoped on purpose. Nothing else on the site uses
   a tinted band or a section head, and claiming .section globally would invite
   a second, conflicting definition later.
   ========================================================================== */

.about-section {
  padding-block: clamp(2.75rem, 7vw, 4.5rem);
}

.about-section--tint {
  background: var(--surface-alt);
}

/* ---------- hero ---------- */

.about-hero {
  /* The brand blue, at the one size the rest of the site never gives it. It
     stays dark because the portrait is low resolution and softness reads far
     less on a dark ground - the navy base is the brand hue taken most of the
     way to black, so the bloom on top reads as brand rather than as a stock
     blue gradient. */
  --hero-ink: #eef4fb;
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(105% 80% at 82% 10%, rgba(13, 114, 196, 0.62), transparent 62%),
    radial-gradient(90% 85% at 4% 98%, rgba(13, 114, 196, 0.32), transparent 60%),
    linear-gradient(155deg, #0c2b49 0%, #08203a 50%, #0a1c30 100%);
  color: var(--hero-ink);
  padding-block: clamp(3rem, 8vw, 5.5rem);
}

.about-hero__inner {
  display: grid;
  grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
  align-items: center;
  gap: clamp(1.75rem, 5vw, 3.5rem);
}

/* Capped at 300px — half the 591px source, so it stays sharp. */
.about-hero__portrait {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.8);
}

.about-hero__portrait img {
  display: block;
  width: 100%;
  height: auto;
}

/* A soft wash at the foot of the frame settles the photo onto the ground
   instead of leaving it a bright rectangle floating on dark. */
.about-hero__portrait::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 55%, rgba(8, 26, 45, 0.6));
  pointer-events: none;
}

.about-hero__eyebrow {
  margin: 0 0 0.9rem;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* The accent is too dark to sit on this ground, so the eyebrow takes a
     lightened tint of the same hue rather than a neutral grey. */
  color: #8ec5f0;
}

/* Persian has no uppercase, so the tracking would only read as loose. */
html[lang="fa"] .about-hero__eyebrow {
  text-transform: none;
  letter-spacing: 0.04em;
}

.about-hero h1 {
  margin: 0;
  font-size: clamp(2rem, 6vw, 3.4rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--hero-ink);
}

html[lang="fa"] .about-hero h1 {
  letter-spacing: 0;
}

.about-hero__role {
  margin: 0.7rem 0 0;
  font-size: clamp(1rem, 2.2vw, 1.15rem);
  color: rgba(238, 244, 251, 0.82);
}

@media (max-width: 720px) {
  .about-hero__inner {
    grid-template-columns: 1fr;
    justify-items: start;
  }

  .about-hero__portrait {
    max-width: 200px;
  }
}

/* ---------- story ---------- */

.about-story {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2.4fr);
  gap: clamp(1rem, 5vw, 4rem);
  align-items: start;
}

.about-story h2 {
  margin: 0.35rem 0 0;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink-muted);
}

.about-story__body p {
  margin: 0;
  font-size: clamp(1.02rem, 2.2vw, 1.15rem);
  line-height: 1.75;
  color: var(--ink);
}

.about-story__body p + p {
  margin-top: 1.1rem;
}

@media (max-width: 760px) {
  .about-story {
    grid-template-columns: 1fr;
  }
}

/* ---------- section heads ---------- */

.about-head {
  max-width: 62ch;
  margin-bottom: 1.9rem;
}

.about-head--spaced {
  margin-top: 3.25rem;
}

.about-head h2 {
  margin: 0;
  font-size: clamp(1.35rem, 3vw, 1.8rem);
}

.about-head p {
  margin: 0.5rem 0 0;
  color: var(--ink-muted);
}

/* ---------- ventures ---------- */

/* 320px min, not 270px: with five ventures a 4-across grid leaves a lone card
   on the second row and squeezes the descriptions to five lines. Three across
   reads as 3+2 and gives the copy a comfortable measure. */
.venture-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 1.1rem;
}

.venture {
  display: flex;
  flex-direction: column;
  padding: 1.5rem;
  background: var(--white);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

@media (hover: hover) {
  .venture:hover {
    transform: translateY(-4px);
    border-color: var(--accent);
    box-shadow: var(--shadow-md);
  }
}

.venture__field {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
}

html[lang="fa"] .venture__field {
  text-transform: none;
  letter-spacing: 0.02em;
}

.venture h3 {
  margin: 0.6rem 0 0;
  font-size: 1.1rem;
}

.venture p {
  margin: 0.5rem 0 0;
  font-size: 0.93rem;
  color: var(--ink-muted);
}

.venture__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: 1.2rem;
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.venture__arrow {
  transition: transform var(--dur) var(--ease);
}

/* The glyph is a literal right arrow, so RTL needs it mirrored before it can
   be nudged along the reading direction. */
html[dir="rtl"] .venture__arrow {
  transform: scaleX(-1);
}

@media (hover: hover) {
  .venture:hover .venture__arrow {
    transform: translateX(3px);
  }

  html[dir="rtl"] .venture:hover .venture__arrow {
    transform: scaleX(-1) translateX(3px);
  }
}

/* ---------- instagram accounts ---------- */

.account-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(255px, 1fr));
  gap: 1rem;
}

.account {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 1.1rem 1.3rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.account:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.account svg {
  flex: none;
  width: 26px;
  height: 26px;
  color: var(--accent);
}

.account__body {
  display: flex;
  flex-direction: column;
}

.account__handle {
  font-weight: 600;
  font-size: 0.98rem;
}

/* A handle is Latin text carrying a leading @, and @ is bidi-neutral: inside
   an RTL paragraph it gets reordered to the far end and the name reads as
   "masoud.hamrah@". Isolating the run keeps it intact and left-to-right while
   the surrounding layout stays RTL. */
.account__handle,
.venture__handle {
  direction: ltr;
  unicode-bidi: isolate;
}

.account__label {
  font-size: 0.86rem;
  color: var(--ink-muted);
}

/* ---------- gallery ---------- */

/* Sized to the source material rather than the viewport. The originals are
   335px wide, and the casual shot is only 225x225. The cap is arithmetic, not
   taste: four columns inside 720px with a 16px gap give frames of 168x224, and
   224 is the most the 225px-tall square can cover without being enlarged. Lift
   the cap only once that photo is replaced with a larger one.
   auto-fill is deliberately avoided here — across a 1200px container it would
   lay down seven tracks and strand four photos against the inline start. */
/* The gallery is editable through the CMS, so it has to hold at any count.
   auto-fit handles that, but the cap belongs on the TRACK, not on the row:
   with `minmax(150px, 1fr)` and a 720px row, one photo stretched to 720x960 -
   a 4x upscale of a 335px source - and five left one stranded on its own line.
   Capping each track at 168px instead means a frame is never larger than the
   four the page was composed for, whatever the count, and the row simply grows
   to fit more of them.

   168px is arithmetic, not taste: a 3:4 frame at that width is 224 tall, and
   the smallest source photo is 225px. Nothing here is ever displayed above its
   native size - checkAboutPhotos() in build.js measures the real files and
   says so when that stops being true, and names the photo holding the cap.

   Four photos still land exactly four across at 720px total, unchanged.
   Narrower viewports drop to three then two on their own, so the old 620px
   breakpoint is gone. */
.shot-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 168px));
  justify-content: start;
  gap: 1rem;
}

.shot {
  margin: 0;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-alt);
}

.shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 500ms var(--ease);
}

@media (hover: hover) {
  .shot:hover img {
    transform: scale(1.05);
  }
}

/* ---------- contact ---------- */

/* The measure belongs on the text, not the block: this element is also the
   .container, so a max-width here would override the container width and
   centre the whole section out of line with everything above it. */
.about-contact h2,
.about-contact p {
  max-width: 60ch;
}

.about-contact h2 {
  margin: 0;
  font-size: clamp(1.35rem, 3vw, 1.8rem);
}

.about-contact p {
  margin: 0.6rem 0 0;
  color: var(--ink-muted);
}

.about-contact .btn {
  margin-top: 1.4rem;
}

/* ---------- reveals (About only) ---------- */

/* Gated behind html.js, set by an inline script in the head: with scripting
   off the rule never applies and every section renders normally.
   scripts/reveal.js adds .is-in as each element comes into view. */
.js .reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 600ms var(--ease), transform 600ms var(--ease);
  /* Staggers a whole grid off one inline --i, with no per-item JS. */
  transition-delay: calc(var(--i, 0) * 70ms);
}

.js .reveal.is-in {
  opacity: 1;
  transform: none;
}

/* The global reduced-motion rule collapses the transition but would leave the
   element at opacity 0 until observed; reveal.js shows everything at once in
   that case, and this makes the resting state correct regardless. */
@media (prefers-reduced-motion: reduce) {
  .js .reveal {
    opacity: 1;
    transform: none;
  }
}
