@charset "utf-8";
/* ==========================================================================
   ו.ז.נ יזמות ובנייה בע"מ — design system
   Palette is drawn from the company wordmark (bronze) and the projects'
   dusk renders (slate). Structure borrows the vocabulary of the sales
   drawings the company itself publishes: hairline rules, sheet labels,
   plate letters A–E.
   ========================================================================== */

/* --- 1. tokens ----------------------------------------------------------- */
:root {
  /* surfaces */
  /* PALETTE A · LIMESTONE (owner, 2026-09-27: "all the colors need to be
     bright"). The --ink family used to be the site's dark slate — lifted twice,
     L* 7.5 -> 13.3 -> 20.3, and still 37-81% of every page's height. It is now
     the warm light BAND that stands where those dark bands stood: the gallery,
     the availability band, the feature band, the project-card band, the header
     and the call bar. The names are kept because every rule, check.py and the
     accessibility palette address them; read "--ink" as "the band", and
     "--on-ink" as "text on the band", which is now dark.
       --ink / --ink-2   #f2ede4  the band (one colour, as the owner chose it;
                                  two names because two families of rules use it)
       --ink-3           #e8e1d5  the deeper band: the footer, picture wells
     Photographs are the one thing that stays dark, and their overlays do not
     read these tokens at all — see --on-photo below and §22. */
  --ink:          #f2ede4;
  --ink-2:        #f2ede4;
  --ink-3:        #e8e1d5;

  /* Text and icons that sit ON a bronze fill — buttons, the selected apartment,
     the active filter and plate, the call bar's second half, the accessibility
     trigger. This used to be --ink, and that coupling is what capped how far the
     dark surfaces could be brightened: every step lighter took the primary
     button's own contrast down with it, because one token was doing two
     unrelated jobs. Split apart, the surface went from L* 7.5 to 20.3 while the
     button went the other way, 5.50:1 back up to 6.24:1. Do not re-merge them.
     It is the wordmark's own deep slate, which is where --ink started. */
  --on-bronze:    #11171e;
  /* the page itself, and the second step of it */
  --limestone:    #fbf9f5;
  --limestone-2:  #e8e1d5;
  --white:        #ffffff;

  /* brand */
  --bronze:       #c08e5f;
  /* Two jobs, both of them on something DARK or on bronze, never as text on a
     light surface: the hover fill of a bronze button (with --on-bronze on it),
     and the accent over a photograph's scrim. Text accents on the light
     surfaces are --bronze-dk. */
  --bronze-lt:    #dcbb99;
  /* bronze TEXT on every light surface — eyebrows, figures, text links */
  --bronze-dk:    #7a5333;
  /* reserved for the "בשיווק" status, which only ever sits over a photograph */
  --sea:          #6fa8d2;

  /* Apartment availability (September 2026, the owner's call): solid, plainly
     coded fills instead of tints of the page — green for free, yellow for in
     process, grey for sold, and a lighter hatched grey for שוק חופשי (flats
     whose sale status the site does not disclose). Each fill carries its own
     text colour, so the plan reads the same on a dark band or a light one.
     Measured in check.py: free 5.28, hold 10.39, sold 6.13, market ≥6.30. */
  --st-free:      #2b7a47;
  --st-free-ink:  #ffffff;
  --st-hold:      #f2cf4a;
  --st-hold-ink:  #2a2203;
  --st-sold:      #9aa0a6;
  --st-sold-ink:  #1d2126;
  --st-market:    #c3c7cb;
  --st-market-2:  #aeb3b8;
  --st-market-ink:#2b3036;

  /* text — one body colour and one muted colour for every light surface. On
     the band these are the old paper values' job; there is no third, fainter
     step any more because #56616c is already the lightest grey that clears AA
     on the deeper band (4.87:1), and --on-ink-faint is kept as a name only. */
  --on-ink:       #1f2933;
  --on-ink-mute:  #56616c;
  --on-ink-faint: #56616c;
  --on-paper:     #1f2933;
  --on-paper-mute:#56616c;

  /* TEXT OVER A PHOTOGRAPH. The hero, the project hero, the title block, the
     project cards' status chip and the lightbox all set light type on a dark
     scrim, and those scrims deliberately did not move (STATUS.md: lifting them
     costs 9-12% of the contrast on every run over a render). These are the
     values --on-ink / --on-ink-mute / --line-ink / --edge-ink had before the
     palette went light; everything drawn over a picture reads them, so the
     band could flip without touching a single photograph. */
  --on-photo:     #e9e6e0;
  --on-photo-mute:#b4bcc2;

  /* hairlines */
  --line-ink:     rgba(31, 41, 51, .16);
  --line-ink-2:   rgba(31, 41, 51, .09);
  --line-paper:   rgba(21, 28, 36, .16);
  --line-paper-2: rgba(21, 28, 36, .08);
  --line-bronze:  rgba(192, 142, 95, .42);
  --line-photo:   rgba(233, 230, 224, .16);
  --line-photo-2: rgba(233, 230, 224, .09);

  /* Boundaries you can CLICK, as distinct from hairlines that merely divide.
     WCAG 1.4.11 asks 3:1 of a control's own edge; the .16/.09 hairlines above
     measure 1.00-1.56:1, because they were drawn to separate things, not to
     afford them. Every ghost button, filter, chip, plate and form field on the
     site was outlined with one — a shape you could not see. These two alphas
     were solved for 3:1 against every surface the site actually puts a control
     on, at the LIFTED ink values: --edge-ink is 4.03 on --ink, 3.77 on --ink-2,
     3.44 on --ink-3; --edge-paper is 3.57 on --limestone, 3.46 on --limestone-2,
     3.74 on --white. (They were first solved against the old darker ink, where
     .40 sufficed; at the new brightness .40 falls to 2.85 on --ink-3. If the
     surfaces ever move again, re-solve these with them — and they already have,
     twice: .40 was right for the original ink, .48 for the first lift, and .56
     for this one, which lands at 4.42 / 4.03 / 3.53 on ink / ink-2 / ink-3.)
     Bronze cannot do this job on paper at any alpha — --bronze itself is
     2.45:1 on --limestone — which is why the paper edge is ink, not the brand.
     PALETTE A (2026-09-27): the band went light, so --edge-ink is now a DARK
     overlay, re-solved a fourth time: .56 of the body colour is 3.46 on the
     band, 3.33 on the deeper band and 3.57 on the detail panel. --edge-photo
     is the old light edge, for the controls that sit over a picture. */
  --edge-ink:     rgba(31, 41, 51, .56);
  --edge-paper:   rgba(21, 28, 36, .54);
  --edge-photo:   rgba(233, 230, 224, .56);

  /* Form-state colours. They belong here and not inline in §12 for two reasons:
     the high-contrast mode can only move what is a token, and check.py can only
     measure what it can parse — it used to carry hand-copies of both values. */
  --placeholder:  #70767e;
  --error:        #b4402f;

  /* 44px, the floor both platform guidelines and WCAG 2.5.5 use. `.backlinks
     .tlink` had already hard-coded 2.75rem for exactly this reason; this
     publishes the number once so the rest can consume it. */
  --tap:          2.75rem;

  /* A control never reads smaller than the prose beside it. Buttons were
     --fs-sm (15px) against 17px body copy, which put the most important word on
     a page two steps below the paragraph explaining it. */
  --fs-ui:        var(--fs-base);

  /* type */
  --display: "Frank Ruhl Libre", "Times New Roman", serif;
  --body:    "Assistant", "Segoe UI", system-ui, sans-serif;
  --tech:    "Barlow Condensed", "Assistant", sans-serif;

  --fs-micro: .75rem;
  --fs-sm:    .9375rem;
  --fs-base:  1.0625rem;
  --fs-lead:  clamp(1.05rem, .95rem + .5vw, 1.3rem);
  --fs-h3:    clamp(1.35rem, 1.15rem + .9vw, 1.9rem);
  --fs-h2:    clamp(1.75rem, 1.3rem + 2vw, 3rem);
  --fs-h1:    clamp(2.4rem, 1.5rem + 4.2vw, 4.9rem);

  /* rhythm */
  --gut:      clamp(1.25rem, 4vw, 3.5rem);
  /* The mobile call bar's real height, published once. It was measured at 61.6px
     and three separate places hard-coded 3.5rem (56px) to clear it, so the last
     6px of the page and of the apartment sheet sat behind the bar. Anything that
     has to stay clear of the bar consumes this, and §14 pins the bar to it, so
     the two cannot drift apart again. */
  --callbar-h: 3.875rem;
  --wrap:     78rem;
  --wrap-narrow: 46rem;
  --sect:     clamp(4.5rem, 9vw, 8.5rem);

  --ease:     cubic-bezier(.22, .61, .36, 1);
  /* softened with palette A: a .55-.7 black shadow was drawn for a dark page
     and reads as a smudge on a light one */
  --shadow:   0 1.5rem 4rem -1.5rem rgba(9, 13, 18, .22);
  --shadow-lift: 0 2.25rem 5rem -1.75rem rgba(9, 13, 18, .28);
}

/* --- 1a. the three typefaces, self-hosted ---------------------------------
   These used to come from fonts.googleapis.com, and that cost a whole extra
   origin on the critical path: the <link> was render-blocking, so nothing could
   paint until a second server had been resolved, negotiated over TLS and asked
   for a 12 KB stylesheet — and only then did the browser learn the names of the
   woff2 files and open a THIRD connection, to fonts.gstatic.com, to fetch them.
   On the Fast 3G the audit measured (1.6 Mbps, 750 ms RTT) that stylesheet
   landed at 1657 ms and first contentful paint at 2084 ms.

   The files are the same bytes Google was serving — `prep_assets.py --fonts`
   mirrors them into assets/fonts/ and prints these rules, and the ten faces
   were measured glyph-for-glyph against the served ones before the switch.
   Nothing about the typography changes.

   Two details worth keeping straight if this is ever regenerated:

   * Frank Ruhl Libre and Assistant are VARIABLE. Google ships one file per
     subset and pins the wght axis per @font-face; a single `font-weight` RANGE
     says the same thing in one rule, and it measured identical at every weight
     the site uses. Barlow Condensed is not variable — it needs one file and one
     rule per weight, which is why it is three of the nine files.
   * `unicode-range` is what makes this cheap: a face is only downloaded when a
     character inside its range is actually painted. The latin-ext faces are
     declared and, on today's copy, never fetched — they cost nothing and they
     stop one stray accented letter dropping a heading into Times New Roman.
     Barlow Condensed has no latin-ext for the same reason in reverse: `--tech`
     already falls back to Assistant, which has it.

   `font-display: swap` is unchanged from the Google URL: text paints in the
   fallback immediately and re-paints in the real face. It was measured at
   0.0079 CLS, which is the price of never showing a blank paragraph. */
@font-face {
  font-family: 'Frank Ruhl Libre'; font-style: normal; font-weight: 300 700;
  font-display: swap; src: url(../fonts/frank-ruhl-libre-hebrew.woff2) format('woff2');
  unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
@font-face {
  font-family: 'Frank Ruhl Libre'; font-style: normal; font-weight: 300 700;
  font-display: swap; src: url(../fonts/frank-ruhl-libre-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Frank Ruhl Libre'; font-style: normal; font-weight: 300 700;
  font-display: swap; src: url(../fonts/frank-ruhl-libre-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Assistant'; font-style: normal; font-weight: 300 600;
  font-display: swap; src: url(../fonts/assistant-hebrew.woff2) format('woff2');
  unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
@font-face {
  font-family: 'Assistant'; font-style: normal; font-weight: 300 600;
  font-display: swap; src: url(../fonts/assistant-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Assistant'; font-style: normal; font-weight: 300 600;
  font-display: swap; src: url(../fonts/assistant-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Barlow Condensed'; font-style: normal; font-weight: 300;
  font-display: swap; src: url(../fonts/barlow-condensed-latin-300.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Barlow Condensed'; font-style: normal; font-weight: 400;
  font-display: swap; src: url(../fonts/barlow-condensed-latin-400.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Barlow Condensed'; font-style: normal; font-weight: 500;
  font-display: swap; src: url(../fonts/barlow-condensed-latin-500.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 6rem;
}

body {
  margin: 0;
  background: var(--limestone);
  color: var(--on-ink);
  font-family: var(--body);
  font-size: var(--fs-base);
  font-weight: 400;
  line-height: 1.8;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

h1, h2, h3, h4, h5, h6 { margin: 0; font-family: var(--display); font-weight: 500; line-height: 1.18; }
p, figure, blockquote, dl, dd { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, picture, video, svg { display: block; max-width: 100%; }
img { height: auto; }
/* `img()` in build.py wraps an image in <picture> whenever the phone cap has
   something to hide from a phone, and that box would otherwise become the
   layout parent every existing selector was written against: `.hero__layer img`
   is `height: 100%`, and a percentage height against an auto-height <picture>
   resolves to auto — the hero render collapsed from 784px to 233px and the rest
   of the screen went flat ink. `display: contents` takes the wrapper out of the
   box tree entirely, so the <img> stays the direct layout child it always was.
   Everything below §2 may therefore keep assuming there is no wrapper. */
picture { display: contents; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
/* A link is underlined unless it is a BOX. `text-decoration: none` here meant
   every in-flow link inherited its paragraph's colour with no other mark: a
   breadcrumb link measured 1.00:1 against the "/" beside it, a .prose link
   2.84:1 against the body text around it. Colour alone owes 3:1 (SC 1.4.1) and
   none of them had it. Anything that carries its own border, fill or drawn rule
   says so below — including the two footer NAVs, where a list of links in a
   column is self-evidently a list of links; the <address> in the same class
   keeps its underlines, because a phone number inside running text is not. */
a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .28em;
}
.btn, .hdr__cta, .hdr__logo, .nav__link, .tlink, .skip, .callbar a,
.titleblock__link, .pcard__link, .gthumb, .gtile, .doc__link,
.lbox__nav, .lbox__close, nav.ftr__links a,
.ftr__bottom .ftr__links a { text-decoration: none; }
address { font-style: normal; }

/* Two concentric rings, because no single colour clears 3:1 on both halves of
   this site: --bronze-lt measured 9.97:1 on --ink but 1.53:1 on --limestone,
   1.37 on --limestone-2 and 1.60 on the bronze button fill — so on every paper
   section, which is the whole contact form and both legal pages, the focus
   indicator was effectively invisible. A dark ring hugs the control and bronze
   sits outside it; whichever surface you land on, one of the two clears 3:1.
   Reads as a drafting registration mark rather than a glow. Since palette A
   the dark ring is --on-ink (the body colour), not --ink: --ink is the light
   band now, and on a light page it is the dark ring that carries the mark —
   bronze still carries it over a photograph. */
:focus-visible {
  outline: 3px solid var(--bronze);
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--on-ink);
  border-radius: 2px;
}

/* Every control clears 44px. Bought with padding — or with padding plus a
   matching negative margin where the layout must not move — so that with two
   exceptions noted at their rules, nothing here changes a rendered height. */
.btn, .hdr__cta, .tlink, .filter, .plate, .ichip, .bstack,
.doc__action, .lbox__nav, .lbox__close, .detail__close, .burger,
.callbar a, .titleblock__link, .check { min-block-size: var(--tap); }

::selection { background: var(--bronze); color: var(--on-bronze); }

/* Latin fragments inside an RTL document */
.ltr { direction: ltr; unicode-bidi: isolate; }

.skip {
  position: absolute;
  inset-block-start: -100%;
  inset-inline-start: 1rem;
  z-index: 200;
  padding: .75rem 1.25rem;
  background: var(--bronze);
  color: var(--on-bronze);
  font-weight: 600;
}
.skip:focus { inset-block-start: 1rem; }

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

/* --- 3. layout primitives ------------------------------------------------ */
/* The viewport meta opts into the full display (`viewport-fit=cover`), so on a
   notched phone the page is laid out edge to edge — under the notch in landscape
   and under the home indicator in portrait. Nothing used to put it back, so the
   header's burger and logo ran beneath the notch. These insets are PHYSICAL
   properties on purpose: `safe-area-inset-left` is the left of the handset
   whichever way the text runs, and pairing it with a logical property would be a
   coin toss on rotation. Every fallback is 0px, so a device with no cutout —
   every desktop, every Android without a notch — lays out exactly as before. */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-left: max(var(--gut), env(safe-area-inset-left, 0px));
  padding-right: max(var(--gut), env(safe-area-inset-right, 0px));
}
.wrap--wide { max-width: 90rem; }
.wrap--narrow { max-width: var(--wrap-narrow); }

.section { padding-block: var(--sect); position: relative; }
.section--tight { padding-block: clamp(3rem, 6vw, 5rem); }
.section--paper { background: var(--limestone); color: var(--on-paper); }
.section--paper-2 { background: var(--limestone-2); color: var(--on-paper); }
.section--ink { background: var(--ink); color: var(--on-ink); }
.section--ink-2 { background: var(--ink-2); color: var(--on-ink); }

/* sheet label — the recurring structural device: a solid drafting tick
   followed by a Latin sheet name. Never set `direction` here; on a flex
   container it would flip flex-start/flex-end in this RTL document. */
.eyebrow {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: .75rem;
  margin-block-end: 1.25rem;
  font-family: var(--tech);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--bronze-dk);
}
/* over a photograph the label keeps the light accent */
.hero .eyebrow, .phero .eyebrow { color: var(--bronze-lt); }
.eyebrow::before {
  content: "";
  flex: 0 0 .4rem;
  height: .4rem;
  background: currentColor;
  transform: rotate(45deg);
}

.rule {
  height: 1px;
  background: var(--line-ink);
  border: 0;
  margin: 0;
}
.section--paper .rule, .section--paper-2 .rule { background: var(--line-paper); }

.head { max-width: 44rem; }
.head h2 { font-size: var(--fs-h2); font-weight: 400; letter-spacing: -.01em; }
.head h2 em { font-style: normal; color: var(--bronze); }
.head p {
  margin-block-start: 1.25rem;
  font-size: var(--fs-lead);
  font-weight: 300;
  color: var(--on-ink-mute);
  max-width: 38rem;
}
.section--paper .head p, .section--paper-2 .head p { color: var(--on-paper-mute); }

.head--center { margin-inline: auto; text-align: center; }
.head--center .eyebrow { justify-content: center; }
.head--center p { margin-inline: auto; }

/* Latin label lines keep their own reading order but stay start-aligned */
.eyebrow > span { unicode-bidi: isolate; }

/* --- 4. buttons ---------------------------------------------------------- */
.btn {
  --btn-fg: var(--on-bronze);
  --btn-bg: var(--bronze);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  /* 17px, not 15px: the label was set two steps below the paragraph next to it.
     The block padding drops by about the amount the line box grows, so the
     measured height barely moves and no hero re-wraps on that account. */
  padding: .75rem 1.85rem;
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--body);
  font-size: var(--fs-ui);
  font-weight: 600;
  letter-spacing: .01em;
  border: 2px solid var(--btn-bg);
  transition: background .35s var(--ease), color .35s var(--ease),
              border-color .35s var(--ease), transform .35s var(--ease);
}
.btn:hover { --btn-bg: var(--bronze-lt); transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
/* The filled button is the primary and has to look it. On the project hero the
   ghost beside it measured 9,442 px2 against the primary's 9,493 — 99.5%, i.e.
   the same button as far as anyone's eye is concerned. This is the whole of the
   difference in geometry; the rest is a 5.50:1 fill against a 3.44:1 edge. */
.btn:not(.btn--ghost) { padding-inline: 2.3rem; }
/* The bronze fill is 2.2-2.9:1 against every light surface, so the fill alone
   does not mark the control's edge. A dark-bronze keyline does (5.2:1 on the
   deeper band, 6.4 on the page), and it reads as the drafting rule the rest of
   the page is drawn with. This used to be paper-only, in --ink; since palette A
   every surface a button sits on is light, so it is everywhere. It stays put on
   hover, where the fill lightens to --bronze-lt. */
.btn:not(.btn--ghost) { border-color: var(--bronze-dk); }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--on-ink);
  border-color: var(--edge-ink);
}
.btn--ghost:hover {
  --btn-bg: transparent;
  --btn-fg: var(--limestone);
  background: var(--on-ink);
  border-color: var(--on-ink);
}
.section--paper .btn--ghost, .section--paper-2 .btn--ghost {
  --btn-fg: var(--on-paper);
  border-color: var(--edge-paper);
}
.section--paper .btn--ghost:hover, .section--paper-2 .btn--ghost:hover {
  --btn-fg: var(--limestone);
  background: var(--on-paper);
  border-color: var(--on-paper);
}
/* over a photograph: light words, a light edge, and a light fill on hover */
.hero .btn--ghost, .phero .btn--ghost {
  --btn-fg: var(--on-photo);
  border-color: var(--edge-photo);
}
.hero .btn--ghost:hover, .phero .btn--ghost:hover {
  --btn-fg: var(--on-bronze);
  background: var(--on-photo);
  border-color: var(--on-photo);
}

.btn__arrow { width: .95rem; height: .95rem; flex: none; transition: transform .35s var(--ease); }
.btn:hover .btn__arrow { transform: translateX(-.25rem); }

/* Text link with a DRAWN rule. It used to be a 1px --line-bronze hairline under
   15.2px type — 1.36:1 on limestone-2, 1.42 on limestone, 2.10 on ink, all well
   under the 3:1 a control's boundary owes — and its weight was never set, so it
   inherited 300 inside a .prose paragraph and 400 everywhere else. Two of its
   three identity marks were fictions in Hebrew anyway: `text-transform:
   uppercase` does nothing to Hebrew glyphs, and var(--tech) carries no Hebrew,
   so it already fell through to Assistant — which is exactly why §19 has to
   re-tighten its tracking. What is left of the idea, a rule under a label that
   answers on hover, is the part that actually reads, so that is what is kept and
   made solid. currentColor puts the rule at the label's own contrast: 8.78:1 on
   ink, 5.12 on limestone, 4.59 on limestone-2. */
.tlink {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  white-space: nowrap;
  font-family: var(--body);
  font-size: var(--fs-ui);
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--bronze-dk);
  padding-block: .55rem .45rem;
  border-block-end: 2px solid currentColor;
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.tlink:hover { color: var(--on-ink); border-color: var(--on-ink); }

/* status chip — every one on the site sits over a photograph (the project
   hero's strap, the card's corner), so its colours are the photo family */
.chip {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .3rem .8rem .35rem;
  font-family: var(--tech);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .14em;
  border: 1px solid currentColor;
  color: var(--bronze-lt);
  line-height: 1.5;
}
.chip::before {
  content: "";
  width: .35rem; height: .35rem;
  background: currentColor;
  border-radius: 50%;
}
.chip--selling { color: var(--sea); }
.chip--done    { color: var(--on-photo-mute); }

/* --- 5. header ----------------------------------------------------------- */
/* The header has two faces. At the top of a page that opens on a photograph it
   is transparent, and its words sit on the hero's scrim — so they are the photo
   family. Stuck (and on the five --solid pages, which are stuck from the first
   paint) it is the light band, and its words are the band's. Three properties
   carry the switch so no rule below has to know which face it is on; the mobile
   drawer (§16) is always a light panel and resets them to the band's. */
.hdr {
  --hdr-fg: var(--on-photo);
  --hdr-accent: var(--bronze-lt);
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 100;
  color: var(--hdr-fg);
  transition: background .45s var(--ease), border-color .45s var(--ease),
              backdrop-filter .45s var(--ease);
  border-block-end: 1px solid transparent;
}
.hdr.is-stuck, .hdr.hdr--solid {
  --hdr-fg: var(--on-ink);
  --hdr-accent: var(--bronze-dk);
}
.hdr__in {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2.5rem);
  min-height: 5.25rem;
  transition: min-height .45s var(--ease);
}
/* the band, #f2ede4, at .94 — THEME_COLOR in build.py is the same colour */
.hdr.is-stuck {
  background: rgba(242, 237, 228, .94);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  border-block-end-color: var(--line-ink);
}
.hdr.is-stuck .hdr__in { min-height: 4.25rem; }
/* pages that open on a light surface need the solid bar from the start */
.hdr.hdr--solid { background: rgba(242, 237, 228, .97); border-block-end-color: var(--line-ink); }

.hdr__logo { flex: none; }
/* The wordmark is the only brand mark in the bar, so it is sized to fill it —
   and the box IS the mark now: prep_assets.py trims the client's transparent
   margin (24.6% of the canvas height) instead of the design paying for it here.
   The vw term is written `rem + vw` because a bare `3.4vw` sat under the floor
   until 1129px, so every phone, tablet and small laptop got the minimum and the
   clamp did nothing at all. This one engages from 1137px to 1432px. */
.hdr__logo img {
  width: auto;
  height: clamp(2.9rem, 1.55rem + 1.9vw, 3.25rem);
  transition: height .45s var(--ease);
}
/* The five hdr--solid pages are is-stuck from first paint (site.js: `solid ||
   scrollY > 40`), so this is the size half the site always shows — it was the
   highest-leverage number in the file and it was the smallest. It now gives up
   15% rather than the bar's 19%, so scrolling no longer demotes the mark. */
.hdr.is-stuck .hdr__logo img { height: 2.75rem; }

.nav { margin-inline-start: auto; }
.nav__list { display: flex; align-items: center; gap: clamp(1rem, 2.2vw, 2.25rem); }
.nav__link {
  position: relative;
  display: block;
  padding-block: .55rem;
  /* not `min-inline-size`: that widened only the shortest link, and since the
     resting rule spans the box it left 7.5px of rule with no letters over it */
  padding-inline: .4rem;
  text-align: center;
  font-size: var(--fs-ui);
  font-weight: 400;
  color: var(--hdr-fg);
  transition: color .3s var(--ease);
}
.nav__link::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  /* The rule used to exist only on hover, so at rest four nav items were four
     plain words with nothing marking them as navigation. It is drawn at rest
     now, at 4.03:1, and answers in bronze — the same idea, given a resting
     state a touch screen can see. */
  width: 100%;
  height: 1px;
  background: var(--edge-photo);
  transition: background .35s var(--ease), height .35s var(--ease);
}
.hdr.is-stuck .nav__link::after, .hdr.hdr--solid .nav__link::after { background: var(--edge-ink); }
.nav__link:hover { color: var(--hdr-accent); }
.nav__link:hover::after, .nav__link[aria-current="page"]::after { background: var(--bronze); height: 2px; }
.nav__link[aria-current="page"] { color: var(--hdr-accent); }

.hdr__cta {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  flex: none;
  padding: .6rem 1.15rem;
  border: 2px solid var(--edge-photo);
  font-family: var(--tech);
  font-size: 1.0625rem;
  letter-spacing: .06em;
  transition: background .3s var(--ease), border-color .3s var(--ease), color .3s var(--ease);
}
.hdr__cta, .hero__actions .btn--ghost, .titleblock__mark {
  box-shadow: 0 0 0 1px rgba(9, 13, 18, .55);
}
/* stuck, the bar is light: the band's edge, and no dark halo round it */
.hdr.is-stuck .hdr__cta, .hdr.hdr--solid .hdr__cta { border-color: var(--edge-ink); box-shadow: none; }
.hdr__cta:hover { background: var(--bronze); border-color: var(--bronze); color: var(--on-bronze); }
.hdr__cta svg { width: .95rem; height: .95rem; flex: none; }

.burger {
  display: none;
  flex: none;
  width: 2.75rem; height: 2.75rem;
  margin-inline-start: auto;
  place-items: center;
}
.burger span {
  display: block;
  width: 1.4rem; height: 1px;
  background: var(--hdr-fg);
  transition: transform .35s var(--ease), opacity .25s var(--ease);
}
.burger span + span { margin-block-start: .35rem; }
.burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(.36rem) rotate(45deg); }
.burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-.36rem) rotate(-45deg); }

/* --- 6. hero ------------------------------------------------------------- */
/* The hero doubles as the project index: the title block at the foot of the
   screen lists every project, and pointing at a row changes the view. */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  isolation: isolate;
  background: var(--ink);
  color: var(--on-photo);
}
.hero__media { position: absolute; inset: 0; z-index: -2; overflow: hidden; }
.hero__layer {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1.1s var(--ease);
}
.hero__layer.is-on { opacity: 1; }
.hero__layer img {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1.06);
  animation: drift 22s ease-in-out infinite alternate;
}
@keyframes drift { to { transform: scale(1.13) translate3d(1.2%, -1%, 0); } }

/* FROZEN AT rgba(17,23,30). When the ink tokens were lifted in August 2026 five
   photo scrims deliberately did NOT move with them: this one, .phero__media::after,
   both of their §22 mobile counterparts, and .pcard__chip. (.feature__bg::after
   and .titleblock DID move — they veil a photograph that is itself already at
   30% over an --ink-2 band, so they read as surfaces rather than as scrims, and
   the text on them was measured at 7.75:1 or better afterwards.) These are darkening layers
   over photography, not surfaces: §22's stops were tuned to the lightest value
   that still clears AA against the brightest pixel each one covers, and that
   calibration is against rgb(17,23,30). Measured, lifting them costs 9-12% of
   the contrast on every light run over a render and puts .hero__lede on the
   mitzpor crop at 4.32:1 — under AA. Keeping them costs nothing: every hero is
   followed by a paper section, so a frozen scrim never abuts a lifted surface.
   PALETTE A (2026-09-27) turned every surface light and left these exactly as
   they are. What changed is the words on them: they used to read the --on-ink
   family, which is dark now, so they read --on-photo instead. The title block's
   veil stays the old slate for the same reason; the feature band's veil did go
   light, because on that band the render is a pale backdrop and the copy is the
   band's own dark type. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(to top, rgba(17,23,30,.97) 0%, rgba(17,23,30,.78) 30%, rgba(17,23,30,.34) 64%, rgba(17,23,30,.82) 100%),
    linear-gradient(to left, rgba(17,23,30,.62), rgba(17,23,30,0) 68%);
}

/* thin rule across the top of the hero */
.hero__meta {
  margin-block-start: 6.5rem;
  padding-block: 1rem;
  border-block-end: 1px solid var(--line-photo-2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-family: var(--tech);
  font-size: var(--fs-micro);
  letter-spacing: .26em;
  text-transform: uppercase;
  /* --on-photo, not --on-photo-mute: this is a 12px label sitting on the
     thinnest part of the hero scrim, and the muted tone measured 3.86:1 there
     against the brightest pixels of the render. §22 had already made exactly
     this swap below 56rem for exactly this reason; the desktop crop needs it too. */
  color: var(--on-photo);
}
.hero__meta span { direction: ltr; }
.hero__meta .he { direction: rtl; letter-spacing: .12em; }

.hero__body {
  flex: 1;
  display: flex;
  align-items: flex-end;
  padding-block: clamp(3rem, 8vh, 6rem);
}
.hero h1 {
  font-size: var(--fs-h1);
  font-weight: 300;
  line-height: 1.06;
  letter-spacing: -.015em;
  max-width: 18ch;
  text-wrap: balance;
}
.hero h1 strong { display: block; font-weight: 500; color: var(--bronze-lt); }
.hero__lede {
  margin-block-start: 1.75rem;
  max-width: 34rem;
  font-size: var(--fs-lead);
  font-weight: 300;
  color: var(--on-photo-mute);
}
.hero__actions { margin-block-start: 2.5rem; display: flex; flex-wrap: wrap; gap: .85rem; }

/* title block: the drawing-sheet index at the foot of the hero. It is laid over
   the photograph, so it is a scrim and not a surface: its veil is the old slate
   (the value --ink had before palette A, frozen here as a literal) and its
   words are the photo family. */
.titleblock {
  border-block-start: 1px solid var(--line-photo);
  background: rgba(41, 50, 59, .55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.titleblock__list { display: grid; grid-template-columns: repeat(3, 1fr); }
/* in RTL the divider between two columns sits on the later one's inline-start */
.titleblock__item + .titleblock__item { border-inline-start: 1px solid var(--line-photo); }
.titleblock__link {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 1rem;
  height: 100%;
  padding: 1.5rem clamp(1rem, 2vw, 1.75rem);
  transition: background .4s var(--ease);
}
/* same corner, same reason: on the home page the trigger covered a whole
   project row in the title block, above the fold */
.titleblock__item:last-child .titleblock__link { padding-inline-end: calc(var(--tap) + 1.25rem); }
.titleblock__link:hover, .titleblock__link:focus-visible { background: rgba(192, 142, 95, .14); }
.titleblock__name { font-family: var(--display); font-size: 1.2rem; font-weight: 500; }
.titleblock__where {
  margin-block-start: .15rem;
  font-family: var(--tech);
  font-size: .8125rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--on-photo-mute);
}
.titleblock__mark {
  flex: none;
  width: 2.5rem; height: 2.5rem;
  display: grid; place-items: center;
  border: 1px solid var(--line-photo);
  color: var(--bronze-lt);
  transition: background .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease);
}
.titleblock__link:hover .titleblock__mark { background: var(--bronze); border-color: var(--bronze); color: var(--on-bronze); }
.titleblock__mark svg { width: .9rem; height: .9rem; }

/* --- 7. statement + principles ------------------------------------------- */
.statement { display: grid; gap: clamp(2.5rem, 6vw, 5rem); align-items: center; }
@media (min-width: 62rem) { .statement { grid-template-columns: 1fr 1fr; } }

.statement__text p + p { margin-block-start: 1.2rem; }
.statement__text .lead {
  font-family: var(--display);
  font-size: var(--fs-h3);
  font-weight: 400;
  line-height: 1.45;
  color: var(--on-paper);
}
.statement__text .body { color: var(--on-paper-mute); font-weight: 300; }

.statement__figure { position: relative; }
.statement__figure img { width: 100%; }
.statement__figure figcaption {
  margin-block-start: .9rem;
  font-family: var(--tech);
  font-size: var(--fs-micro);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--on-paper-mute);
}
.statement__stack { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.statement__stack img:first-child { grid-column: 1 / -1; aspect-ratio: 16/9; object-fit: cover; }
.statement__stack img { aspect-ratio: 1; object-fit: cover; }

.principles {
  display: grid;
  gap: 0;
  border-block-start: 1px solid var(--line-paper);
}
@media (min-width: 48rem) { .principles { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 68rem) { .principles { grid-template-columns: repeat(4, 1fr); } }
.principle {
  padding: 2.25rem clamp(1rem, 2vw, 1.75rem) 2rem;
  border-block-end: 1px solid var(--line-paper);
}
@media (min-width: 48rem) and (max-width: 67.999rem) {
  .principle:nth-child(even) { border-inline-start: 1px solid var(--line-paper); }
  .principle:nth-child(n+3) { border-block-end: 0; }
}
@media (min-width: 68rem) {
  .principle { border-block-end: 0; }
  .principle + .principle { border-inline-start: 1px solid var(--line-paper); }
}
.principle__no {
  font-family: var(--tech);
  font-size: var(--fs-micro);
  letter-spacing: .24em;
  color: var(--bronze-dk);
}
.principle h3 { margin-block: .75rem .6rem; font-size: 1.35rem; font-weight: 500; }
.principle p { font-size: var(--fs-sm); color: var(--on-paper-mute); font-weight: 300; line-height: 1.75; }

/* --- 8. project register (cards) ----------------------------------------- */
.register { display: grid; gap: clamp(1.5rem, 3vw, 2.25rem); }
@media (min-width: 56rem) { .register { grid-template-columns: repeat(2, 1fr); } }
.register--three { }
@media (min-width: 56rem) { .register--three { grid-template-columns: repeat(3, 1fr); } }

.pcard {
  position: relative;
  display: flex;
  flex-direction: column;
  /* the card brings its own surface, so it must bring its own foreground too.
     It is white: the one surface that stands clear of both the page it sits on
     at home (#fbf9f5) and the band it sits on everywhere else (#f2ede4) — the
     hairline, not the fill, draws its edge on the page. */
  background: var(--white);
  color: var(--on-ink);
  border: 1px solid var(--line-ink);
  overflow: hidden;
  transition: border-color .4s var(--ease), transform .4s var(--ease), box-shadow .4s var(--ease);
}
.pcard:hover { border-color: var(--line-bronze); transform: translateY(-4px); box-shadow: var(--shadow-lift); }
.pcard--wide { grid-column: 1 / -1; }
@media (min-width: 56rem) {
  .pcard--wide { flex-direction: row-reverse; }
  .pcard--wide .pcard__media { flex: 1 1 58%; }
  .pcard--wide .pcard__body { flex: 1 1 42%; justify-content: center; }
}

.pcard__media { position: relative; overflow: hidden; background: var(--ink-3); }
.pcard__media img {
  width: 100%; height: 100%;
  aspect-ratio: 4/3;
  object-fit: cover;
  transition: transform 1.2s var(--ease);
}
.pcard--wide .pcard__media img { aspect-ratio: 16/10; height: 100%; }
.pcard:hover .pcard__media img { transform: scale(1.045); }
.pcard__chip { position: absolute; inset-block-start: 1rem; inset-inline-start: 1rem; background: rgba(17,23,30,.88); backdrop-filter: blur(6px); }

/* the body grows so `margin-block-start:auto` can bottom-align the footer
   across cards of unequal content in the same grid row */
.pcard__body { display: flex; flex: 1 1 auto; flex-direction: column; gap: 1rem; padding: clamp(1.5rem, 3vw, 2.5rem); }
.pcard__where {
  font-family: var(--tech);
  font-size: var(--fs-micro);
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--bronze-dk);
}
.pcard h3 { font-size: var(--fs-h3); font-weight: 400; }
.pcard__desc { font-size: var(--fs-sm); font-weight: 300; color: var(--on-ink-mute); }
.pcard__facts {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  padding-block-start: 1rem;
  border-block-start: 1px solid var(--line-ink-2);
  font-family: var(--tech);
  font-size: .9375rem;
  letter-spacing: .1em;
  color: var(--on-ink-mute);
}
.pcard__facts b { color: var(--on-ink); font-weight: 500; }
.pcard__foot { margin-block-start: auto; padding-block-start: .5rem; }
.pcard__link::after { content: ""; position: absolute; inset: 0; }
/* The ::after is a 1326x481 overlay over the whole card, so the POINTER target
   is the card while the focus ring was still drawn around the 169x39 title.
   Move it onto the overlay and it frames what you actually click. */
.pcard__link:focus-visible { outline: 0; box-shadow: none; }
.pcard__link:focus-visible::after {
  outline: 3px solid var(--bronze);
  outline-offset: -3px;
  box-shadow: inset 0 0 0 6px var(--on-ink);
}

/* A .pcard used to be an ink plate laid on paper, and the paper overrides in §3
   and §4 — which select by SECTION, not by surface — reached inside it and put
   bronze-dk on ink-2 at 2.69:1. Since palette A the card and every section are
   light and those overrides are gone; the lesson stands: a component that brings
   its own background must not be repainted by the section it sits in. */

/* filter bar */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-block-end: clamp(2rem, 4vw, 3rem);
  padding-block-end: 1.25rem;
  border-block-end: 1px solid var(--line-ink-2);
}
.section--paper .filters { border-block-end-color: var(--line-paper); }
.filter {
  padding: .6rem 1.25rem;
  font-family: var(--tech);
  font-size: var(--fs-ui);
  letter-spacing: .1em;
  color: var(--on-ink-mute);
  /* was `1px solid transparent` — a 1.00:1 boundary. There is no hover on a
     phone, so until one was pressed the filter bar showed no controls at all. */
  border: 2px solid var(--edge-ink);
  transition: color .3s var(--ease), border-color .3s var(--ease), background .3s var(--ease);
}
.filter:hover { color: var(--on-ink); border-color: var(--on-ink); }
.filter[aria-pressed="true"] { color: var(--on-bronze); background: var(--bronze); border-color: var(--bronze); }
.section--paper .filter { color: var(--on-paper-mute); }
.section--paper .filter { border-color: var(--edge-paper); }
.section--paper .filter:hover { color: var(--on-paper); border-color: var(--on-paper); }
.section--paper .filter[aria-pressed="true"] { color: var(--on-bronze); }

.is-filtered-out { display: none !important; }

/* --- 9. feature band ----------------------------------------------------- */
.feature { position: relative; overflow: hidden; background: var(--ink-2); }
.feature__bg { position: absolute; inset: 0; }
.feature__bg img { width: 100%; height: 100%; object-fit: cover; opacity: .3; }
/* The band is light since palette A, so the veil is the band's own colour: the
   render shows through as a pale drawing rather than a dusk photograph, and the
   copy is the band's dark type. The veil is heavy (.8 at its thinnest) because
   it is dark text now that owes the contrast, and a dark pixel of the render is
   what could take it away — at .8 over pure black the muted label still clears
   AA. */
.feature__bg::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to left, rgba(242,237,228,.97) 12%, rgba(242,237,228,.86) 58%, rgba(242,237,228,.8));
}
.feature__in { position: relative; }
/* The neotYam wordmark is drawn near-white for a dark ground. On the light band
   it is inverted and turned back through 180deg of hue: the letters go near-
   black and the blue Y stays blue — a CSS move, so the asset and the project
   hero, where it still sits on a photograph, are untouched. */
.feature__wordmark { width: min(22rem, 62vw); margin-block-end: 2rem; filter: invert(1) hue-rotate(180deg); }

.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  margin-block-start: clamp(2.5rem, 5vw, 3.5rem);
  border-block: 1px solid var(--line-ink);
}
.stat { padding: 1.75rem 1rem; text-align: center; }
.stat + .stat { border-inline-start: 1px solid var(--line-ink); }
.stat__num {
  display: block;
  font-family: var(--display);
  font-size: clamp(2.4rem, 5vw, 3.6rem);
  font-weight: 300;
  line-height: 1;
  color: var(--bronze-dk);
  direction: ltr;
  font-variant-numeric: tabular-nums;
}
.stat__label {
  display: block;
  margin-block-start: .6rem;
  font-size: var(--fs-sm);
  font-weight: 300;
  color: var(--on-ink-mute);
}

/* --- 10. project page ---------------------------------------------------- */
.phero {
  position: relative;
  min-height: min(82svh, 46rem);
  display: flex;
  align-items: flex-end;
  isolation: isolate;
  background: var(--ink);
  color: var(--on-photo);
}
.phero__media { position: absolute; inset: 0; z-index: -2; }
.phero__media img { width: 100%; height: 100%; object-fit: cover; }
.phero__media::after {
  content: "";
  position: absolute; inset: 0;
  /* the top band keeps the fixed header legible over a bright sky */
  background: linear-gradient(to top,
    rgba(17,23,30,.96) 2%, rgba(17,23,30,.74) 38%,
    rgba(17,23,30,.42) 68%, rgba(17,23,30,.72) 100%);
}
.phero__in { width: 100%; padding-block: clamp(2.5rem, 6vh, 4.5rem) clamp(2.5rem, 5vh, 3.5rem); }
.phero__crumbs {
  /* wraps, because at 320px with the accessibility menu's text at 150% the row
     needs 303px inside a 260px content box and pushed 13px of horizontal scroll
     onto the page — SC 1.4.10, on the narrowest phone, in the feature that
     exists to help the people most likely to be on it */
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  margin-block-end: 1.5rem;
  font-family: var(--tech);
  font-size: .8125rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--on-photo-mute);
}
/* 21.6px tall, and because of the old reset the same colour as the "/" beside
   it — 1.00:1, with no other mark on it. The padding buys the 44px and the
   negative margin hands it straight back, so the hero's vertical composition
   is untouched. */
.phero__crumbs a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* the two shortest crumbs measured 43.2px wide — a floor is cheaper here than
     inline padding, which would have eaten the .6rem gap to the "/" beside it */
  min-inline-size: var(--tap);
  padding-block: .65rem;
  margin-block: -.65rem;
  text-underline-offset: .4em;
}
.phero__crumbs a:hover { color: var(--bronze-lt); }
.phero__wordmark { width: min(28rem, 74vw); margin-block-end: 1.25rem; }
.phero h1 { font-size: clamp(2.2rem, 5vw, 3.8rem); font-weight: 300; }
.phero__sub { margin-block-start: .9rem; display: flex; flex-wrap: wrap; align-items: center; gap: .85rem; }
.phero__where { font-family: var(--tech); font-size: 1.05rem; letter-spacing: .2em; text-transform: uppercase; color: var(--on-photo-mute); }

.prose { max-width: 44rem; }
.prose p { font-weight: 300; line-height: 1.95; }
.prose p + p { margin-block-start: 1.15rem; }
.prose .lead { font-family: var(--display); font-size: var(--fs-h3); font-weight: 400; line-height: 1.5; }

/* gallery — one stage, one thumbnail strip
   ─────────────────────────────────────────────────────────────────────────
   This was a vertical mosaic of seven tiles: 2.4 screens on a desktop, 2.7 on
   a phone, and the picture a visitor was actually looking at was half a column
   wide. It is now a horizontal scroll-snap filmstrip (`.gfilm`) whose slides
   are the full width of the stage, with a strip of thumbnails under it — about
   one screen in total, and the viewed image roughly a third larger than the
   mosaic's biggest tile.

   Two rules hold the whole thing up:

   1. NO `direction` ANYWHERE IN HERE. Both boxes are flex containers, and
      setting direction on one flips the meaning of flex-start/flex-end (trap 1
      in CLAUDE.md). The document is already RTL, so a plain flex row lays the
      first picture out on the RIGHT and the scroller starts there, which is
      exactly what is wanted — and `scrollBy({left})` in site.js works off a
      measured delta, so it never has to care which sign the browser gives
      scrollLeft in an RTL box.
   2. THE BOX IS RESERVED BEFORE THE PICTURE DECODES. The film states its
      `aspect-ratio` outright and every thumbnail's box is a fixed width and
      height, so no image here can shift the page by arriving. The `<img>`
      elements carry their intrinsic width/height too, which is what makes the
      lightbox and the `object-fit` crop agree about the shape.

   The stage's size is capped by VIEWPORT HEIGHT, not by a fixed pixel figure:
   `min(62vh, 34rem)` is what keeps a 900px-tall laptop inside one screen while
   still growing on a large monitor, and the 16/9 max-width is what stops a wide
   screen from cropping a render into a letterbox. */
.gallery { position: relative; }

.gfilm {
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  background: var(--ink-3);
  aspect-ratio: 4 / 3;
  /* A phone turned sideways is 390px tall and 844 wide, and 4/3 of the column
     would be a 517px stage in a 390px screen — the strip and the heading both
     off the bottom. The cap is in vh so it only ever binds on a viewport too
     short for the shape; in portrait, at every width, it is inert. Above 56rem
     the 16/9 max-width below is doing the same job in the same units. */
  max-height: 70vh;
  /* When that cap binds, the aspect-ratio takes the WIDTH down with the height
     rather than letting the box go wider than its shape — so the film can end up
     narrower than the column it sits in, and in an RTL document an un-centred
     box parks against the right edge with a hole beside it. */
  margin-inline: auto;
  /* the strip is the navigation; a native scrollbar under the stage is noise */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.gfilm::-webkit-scrollbar { width: 0; height: 0; }
/* a focusable scroll container has to say so — the arrow keys move it even with
   no script, and with no ring nothing tells a keyboard user they are on it */
.gfilm:focus-visible { outline: 2px solid var(--bronze); outline-offset: 3px; }

/* On a phone the stage is pulled out to the screen edges: the column is only
   350px wide there and the extra 40 is the difference between a picture and a
   postage stamp. The negative margin mirrors .wrap's own padding exactly,
   including the safe-area inset, so it can never overflow the page — in RTL,
   margin-inline-start is the RIGHT edge, which is why the two envs are
   swapped. */
@media (max-width: 40rem) {
  .gfilm {
    margin-inline-start: calc(-1 * max(var(--gut), env(safe-area-inset-right, 0px)));
    margin-inline-end: calc(-1 * max(var(--gut), env(safe-area-inset-left, 0px)));
  }
}
@media (min-width: 40.0625rem) { .gfilm { aspect-ratio: 3 / 2; } }
@media (min-width: 56rem) {
  .gfilm {
    aspect-ratio: 16 / 9;
    max-width: calc(min(62vh, 34rem) * 16 / 9);
    margin-inline: auto;
  }
}

.gtile {
  flex: 0 0 100%;
  position: relative;
  overflow: hidden;
  background: var(--ink-3);
  cursor: zoom-in;
  /* NOT `scroll-snap-stop: always`. It reads like the right thing for a
     filmstrip — it stops a flick from skipping past a picture — but it also
     stops a PROGRAMMATIC scroll at the very next snap point, so a thumbnail
     seven along moved the stage exactly one picture and no further. */
  scroll-snap-align: center;
}
.gtile img { width: 100%; height: 100%; object-fit: cover; transition: transform 1s var(--ease); }
.gtile:hover img { transform: scale(1.03); }
.gtile::after {
  content: "";
  position: absolute; inset: 0;
  background: rgba(17,23,30,.28);
  opacity: 0;
  transition: opacity .4s var(--ease);
}
.gtile:hover::after { opacity: 1; }
/* inset ink ring inside the bronze one, because the outer box-shadow the rest
   of the site relies on is clipped by .gfilm's overflow-x scroller — and a bare
   bronze line over a bright render measured 1.00:1 */
.gtile:focus-visible {
  outline: 3px solid var(--bronze);
  outline-offset: -6px;
  box-shadow: inset 0 0 0 6px var(--on-ink);
}

.gstrip {
  display: flex;
  gap: .5rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  margin-block-start: .75rem;
  /* the ring on a thumbnail is drawn outside its box */
  padding-block: 2px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.gstrip::-webkit-scrollbar { width: 0; height: 0; }
@media (min-width: 56rem) {
  .gstrip {
    gap: .75rem;
    margin-block-start: 1rem;
    max-width: calc(min(62vh, 34rem) * 16 / 9);
    margin-inline: auto;
  }
}

.gthumb {
  flex: 0 0 5.25rem;
  height: 3.5rem;
  position: relative;
  overflow: hidden;
  background: var(--ink-3);
  scroll-snap-align: center;
  /* was .45 — six of the seven thumbnails sat at 45% and read as wallpaper
     rather than as a picker. .62 still recedes behind the current one. */
  opacity: .62;
  transition: opacity .35s var(--ease);
}
.gthumb img { width: 100%; height: 100%; object-fit: cover; }
.gthumb:hover { opacity: .8; }
.gthumb:focus-visible { opacity: 1; outline: 2px solid var(--bronze); outline-offset: 2px; }
/* the current picture, marked on the link itself so it is true for a screen
   reader and not only in colour */
.gthumb[aria-current="true"] { opacity: 1; }
.gthumb[aria-current="true"]::after {
  content: "";
  position: absolute; inset: 0;
  border: 2px solid var(--bronze);
}
@media (min-width: 56rem) { .gthumb { flex-basis: 7rem; height: 4.5rem; } }

/* Nothing here animates for a visitor who asked for that. The scroll itself is
   handled in site.js, which passes behavior:'auto' under the same query. */
@media (prefers-reduced-motion: reduce) {
  .gtile img, .gthumb { transition: none; }
  .gtile:hover img { transform: none; }
  .gfilm, .gstrip { scroll-behavior: auto; }
}

/* lightbox */
.lbox {
  --lbox-pad: clamp(1rem, 4vw, 3rem);
  --lbox-bar-inset: max(clamp(1rem, 3vw, 2rem), env(safe-area-inset-bottom, 0px));
  --lbox-bar-h: 3rem;
  /* the band the counter and the two arrows occupy. The bar is absolutely
     positioned, so it takes no room of its own: reserve it here or the image is
     free to be drawn straight over the controls. */
  --lbox-foot: calc(var(--lbox-bar-inset) + var(--lbox-bar-h) + 1rem);
  position: fixed;
  inset: 0;
  z-index: 300;
  display: none;
  place-items: center;
  background: rgba(9, 13, 18, .96);
  padding: var(--lbox-pad);
  /* the box locks body scroll while it is open; iOS has historically leaked a
     flick through that anyway */
  overscroll-behavior: contain;
  /* the close button sits in the top inline-end corner, which in landscape is
     exactly where the notch is */
  padding-left: max(var(--lbox-pad), env(safe-area-inset-left, 0px));
  padding-right: max(var(--lbox-pad), env(safe-area-inset-right, 0px));
  padding-bottom: var(--lbox-foot);
}
.lbox[open], .lbox.is-open { display: grid; }
/* At 844x390 an 82svh image is 320px tall and the box is 390: centred in the
   padded area it ran 38px over the counter and both arrows, drawn onto a sunlit
   render with no scrim behind them. The padding above now excludes the bar's
   band and this caps the image to what is left, so the two cannot meet at any
   aspect ratio. min() means nothing changes wherever there was already room. */
.lbox__img {
  max-width: min(100%, 82rem);
  max-height: min(82svh, calc(100svh - var(--lbox-pad) - var(--lbox-foot)));
  object-fit: contain;
  margin-inline: auto;
}
.lbox__bar {
  position: absolute;
  inset-block-end: var(--lbox-bar-inset);
  inset-inline: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  font-family: var(--tech);
  font-size: .9375rem;
  letter-spacing: .18em;
  color: var(--on-photo-mute);
}
/* The lightbox stays dark on purpose: it is a picture viewer laid over the page,
   not a surface of it, and a photograph is read best against black. Its words
   and edges are the photo family. */
.lbox__nav, .lbox__close {
  width: var(--lbox-bar-h); height: var(--lbox-bar-h);
  display: grid; place-items: center;
  border: 2px solid var(--edge-photo);
  color: var(--on-photo);
  transition: background .3s var(--ease), border-color .3s var(--ease);
}
.lbox__nav:hover, .lbox__close:hover { background: var(--bronze); border-color: var(--bronze); color: var(--on-bronze); }
.lbox__close { position: absolute; inset-block-start: clamp(1rem, 3vw, 2rem); inset-inline-end: clamp(1rem, 3vw, 2rem); }
.lbox__nav svg, .lbox__close svg { width: 1.1rem; height: 1.1rem; }
.lbox__count { direction: ltr; font-variant-numeric: tabular-nums; }

/* --- 10a. the compact project page ----------------------------------------
   The owner's note: the project page's two jobs are the files to download and
   the apartments on offer, and a visitor had to scroll 3,650px on a phone to
   reach the first of them. So on project-neot-yam.html (and only there — the
   class is on its <main>) the page runs in a tighter rhythm, and the secondary
   material — the about prose past its lead, and the site plan — folds into
   native <details>, which open with JavaScript off and need no script at all.

   The second note (2026-09-29) set the order — the project in words and
   pictures, then the availability, then the documents — and the size: a
   section that fills the whole screen hides from an older visitor that there is
   anything under it. So each of the three is held to about four fifths of the
   screen left between the sticky header and the call bar, which leaves the
   NEXT section's heading showing when this one's top is at the header. That is
   measured at 390x844 and 1440x900, not assumed.

   The rhythm is the --sect token, re-declared for this <main>. It is a length,
   not a colour, so it is not a palette token and §1's :root rule does not bind
   it; every .section inside, the contact block included, picks it up. */
.main--compact { --sect: clamp(2.75rem, 5.5vw, 5rem); }
.main--compact .section--tight { padding-block: clamp(2rem, 4.5vw, 3.25rem); }

/* The hero keeps its picture and loses a fifth of its height, the about page's
   62svh. `align-items: flex-end` still pins the content to the foot of the box;
   the top padding is floored at 4.75rem because on a short phone (320x568) the
   content is taller than 62svh, the box sizes to it, and 6vh of padding would
   put the breadcrumb under the 4.25rem fixed header. 4.75rem is the value the
   landscape rule in §16 already settled on for the same reason. */
.main--compact .phero { min-height: min(62svh, 34rem); }
.main--compact .phero__in { padding-block-start: max(4.75rem, clamp(2.5rem, 6vh, 4.5rem)); }
/* was an inline 2rem. In vh so a short phone (360x640) gives back the 12px
   that keeps the first section's heading above the call bar on first load. */
.main--compact .phero__actions { margin-block-start: clamp(1.25rem, 4vh, 2rem); }

/* a band that carries one row of content, not a section's worth */
.section--strip { padding-block: clamp(1.25rem, 3vw, 2rem); }

/* a section head that gives the content under it the room instead */
.head--tight { margin-block-end: clamp(1.25rem, 3vw, 2rem); }
.head--tight .eyebrow { margin-block-end: .75rem; }
.head--tight p { margin-block-start: .6rem; }

/* folds — a native <details>. The summary is the control, so it clears 44px and
   its toggle carries the same 2px --edge-paper boundary every other control on
   paper does (a boundary marks a control; see the register's בהכנה note). The
   chevron is two borders of an empty box, not a glyph, so a screen reader reads
   the summary's words and its expanded state and nothing else. Its borders are
   PHYSICAL (right + bottom) on purpose: it is a drawing of an arrow pointing
   down, and a logical property would mirror it in this RTL document. */
.fold > summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 1rem;
  min-block-size: var(--tap);
  cursor: pointer;
  list-style: none;
}
.fold > summary::-webkit-details-marker { display: none; }
.fold__icon {
  grid-column: 2;
  grid-row: 1 / span 2;
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  border: 2px solid var(--edge-paper);
  transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease);
}
.fold__icon::before {
  content: "";
  width: .6rem; height: .6rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-.15rem) rotate(45deg);
  transition: transform .3s var(--ease);
}
.fold[open] > summary .fold__icon::before { transform: translateY(.15rem) rotate(-135deg); }
.fold > summary:hover .fold__icon { background: var(--on-paper); border-color: var(--on-paper); color: var(--limestone); }
.fold__label { font-size: var(--fs-ui); font-weight: 600; }
.fold__body { padding-block-start: 1.15rem; }

/* the about prose: the lead stands, the rest reads on */
.fold--more { margin-block-start: 1.15rem; }
.fold--more > summary { border-block: 1px solid var(--line-paper); padding-block: .35rem; }

/* the site plan: one row, styled as the section head it replaces */
.fold--section > summary .eyebrow { margin-block-end: .35rem; }
.fold--section > summary h2 { font-size: var(--fs-h2); font-weight: 400; letter-spacing: -.01em; line-height: 1.2; }
.fold--section > .fold__body { padding-block-start: clamp(1.5rem, 3vw, 2.25rem); }

/* 1. The project, in words and pictures — one section, one composition.
   It replaced a prose section under a one-row facts table and a separate
   "הדמיות" band. A wide screen sets the words in a column beside the stage, so
   the two read as one thing and the section costs one picture's height; a
   phone reads down it — the lead, the stage, the rest of the prose folded.
   Grid areas, so the fold sits under the words on a wide screen and under the
   pictures on a phone without the DOM changing order. No `direction` anywhere
   (trap 1): in this RTL document the first column is the right-hand one, which
   is where the words belong. */
.about__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "text" "more" "media";
  row-gap: .8rem;
}
.about__text { grid-area: text; min-inline-size: 0; }
.about__media { grid-area: media; min-inline-size: 0; }
.about__more { grid-area: more; }
.about__text .eyebrow { margin-block-end: .5rem; }
.about__text h2 { font-size: var(--fs-h2); font-weight: 400; letter-spacing: -.01em; line-height: 1.2; }
.about__lead { margin-block-start: .75rem; }
/* The lead sheds the display size it had when it stood alone under the facts:
   at --fs-h3 it was seven lines on a phone. --fs-lead is still a step above the
   body it opens onto. */
.about__lead .lead { font-size: var(--fs-lead); line-height: 1.75; }
/* On a phone the lead stops after three lines, with "המשך קריאה" right under
   it and the pictures after that — the "read more" every phone reader knows —
   and opening the fold lets the lead run on into the rest. The words are all
   in the DOM either way (a clamp only stops drawing them), so a screen reader
   and the copy overlay read the whole paragraph. Behind @supports because the
   un-clamp needs :has(); a browser without it never clamps, rather than
   clamping for good. */
@supports selector(:has(*)) {
  @media (max-width: 39.9375rem) {
    .about__lead .lead {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 3;
      line-clamp: 3;
      overflow: hidden;
    }
    .about:has(.about__more[open]) .about__lead .lead {
      display: block;
      -webkit-line-clamp: unset;
      line-clamp: none;
      overflow: visible;
    }
  }
}
.about .fold--more { margin-block-start: 0; }
.about__more > summary { padding-block: 0; }
.about__more > .fold__body { padding-block-start: .9rem; }
.about .fold__body { max-width: 44rem; }
/* The renders' own disclaimer, under the strip it qualifies. Not --fs-micro:
   it is a sentence an older reader is meant to be able to read. */
.about__note {
  margin-block-start: .6rem;
  font-size: .8125rem;
  font-weight: 300;
  line-height: 1.5;
  color: var(--on-paper-mute);
}
/* The renders are 16:9, so the stage is too, at every width. The 4:3 phone
   stage cropped each one's sides and cost 91px of a 390px phone. It also stays
   inside the column here rather than bleeding to the screen edges (§10's
   phone rule): it lines up with the words and the strip above and below it,
   and the 18px it gives back is what lets the next heading show. */
.about .gfilm { aspect-ratio: 16 / 9; margin-inline: auto; }
@media (max-width: 39.9375rem) {
  .about__grid { row-gap: .65rem; }
  .about__lead .lead { line-height: 1.65; }
  .main--compact .head--tight { margin-block-end: 1rem; }
  .main--compact .head--tight .eyebrow { margin-block-end: .5rem; }
}
/* 44px tall, the floor and no more, and a phone still sees five of the seven */
.about .gthumb { flex-basis: 4.5rem; height: var(--tap); }
.about .gstrip { margin-block-start: .5rem; }

@media (min-width: 62rem) {
  .about__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
    grid-template-rows: auto 1fr;
    grid-template-areas: "text media" "more media";
    column-gap: clamp(2rem, 4vw, 4rem);
    row-gap: 1.25rem;
    align-items: start;
  }
  /* beside a picture the lead has a column's height to fill, and the display
     face at a size between the body and --fs-h3 fills it without shouting */
  .about__lead .lead { font-size: clamp(1.15rem, .55rem + 1vw, 1.4rem); line-height: 1.7; }
  /* the stage, the strip and the disclaimer share one width, capped by the
     screen's HEIGHT so a short laptop still sees the whole section */
  .about .gfilm, .about .gstrip, .about__note { max-width: calc(min(58vh, 34rem) * 16 / 9); margin-inline: auto; }
  .about .gstrip { gap: .5rem; margin-block-start: .6rem; }
  /* seven thumbnails share the stage's width instead of scrolling past it; a
     longer gallery (the owner can add renders) falls back to the scroll */
  .about .gthumb { flex: 1 1 0; min-inline-size: 4.5rem; height: auto; aspect-ratio: 3 / 2; }
}

/* 2. The invitation, tightened for this page. Its markup is the parity-bound
   fragment (build.py invite_block / render.js renderInvite), so every change
   is here and none is in it. */
.main--compact .invite { gap: clamp(1rem, 2.5vw, 1.75rem); }
.main--compact .invite__cell { padding-block: .85rem; }
.main--compact .invite__num { font-size: clamp(1.7rem, 3vw, 2.3rem); }
.main--compact .invite__label { margin-block-start: .3rem; }
.main--compact .invite__go { gap: .9rem; }
.main--compact .invite__mixhead { margin-block-end: .5rem; }
/* one type per line on a phone — plate, type, all, free — instead of a plate
   beside three stacked lines in two columns: the same rows at under half the
   height. The Hebrew track is minmax(0, 1fr), so at 150% text the label wraps
   rather than the page widening. */
@media (max-width: 39.9375rem) {
  .main--compact .invite__mixlist { grid-template-columns: minmax(0, 1fr); }
  .main--compact .imix {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas: "plate label all free";
    column-gap: .75rem;
    padding-block: .25rem;
  }
  .main--compact .imix__plate { width: 2rem; height: 2rem; }
  /* The paragraph describing the plan is four lines here, 150px, and the
     button under it already says where it goes. A phone gets the numbers, the
     button and the note; from 40rem, where it costs two lines, it is back. */
  .main--compact .invite__lede { display: none; }
}

/* 3. The register, tightened for this page, and folded where a sheet is long.
   On a wide screen the rows run in two columns, so the ten general documents
   are five rows deep; narrower, a long sheet shows its first rows and site.js
   adds a "הצגת כל N המסמכים" button under them (§11, .doclist__more). With no
   script nothing is folded and every row is simply there. */
.main--compact .sheet__head { margin-block-end: .85rem; }
.main--compact .sheet__head p { margin-block-start: .3rem; line-height: 1.6; }
.main--compact .doc { padding-block: .5rem; }
.main--compact .doc__icon { width: 2.2rem; height: 2.6rem; }
@media (max-width: 61.9375rem) { .main--compact .docs { gap: 1rem; } }
@media (min-width: 80rem) {
  .main--compact .doclist { columns: 2; column-gap: clamp(1.5rem, 3vw, 2.5rem); }
  .main--compact .doc { break-inside: avoid; }
}

/* --- 11. document register ----------------------------------------------- */
/* The signature block: the sales drawings, presented the way an architect's
   document register presents them — grouped, labelled, with plate tabs. */
.docs { display: grid; gap: 2rem; }
@media (min-width: 62rem) { .docs { grid-template-columns: 15rem 1fr; gap: clamp(2rem, 4vw, 3.5rem); } }

.plates { display: flex; flex-wrap: wrap; gap: .5rem; }
@media (min-width: 62rem) {
  .plates { flex-direction: column; gap: 0; border-inline-start: 1px solid var(--line-paper); align-self: start; position: sticky; top: 7rem; }
}
.plate {
  display: flex;
  align-items: center;
  gap: .85rem;
  padding: .85rem 1rem;
  border: 2px solid var(--edge-paper);
  font-size: var(--fs-ui);
  color: var(--on-paper-mute);
  text-align: start;
  transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease);
}
@media (min-width: 62rem) {
  .plate { width: 100%; border: 0; border-block-end: 2px solid var(--edge-paper); }
  .plate:first-child { border-block-start: 2px solid var(--edge-paper); }
}
.plate__letter {
  flex: none;
  width: 2.1rem; height: 2.1rem;
  display: grid; place-items: center;
  border: 1px solid currentColor;
  font-family: var(--tech);
  font-size: .95rem;
  font-weight: 500;
  letter-spacing: .04em;
  direction: ltr;
}
.plate:hover { color: var(--on-paper); background: rgba(21,28,36,.04); }
.plate[aria-selected="true"] { color: var(--on-paper); background: var(--white); border-inline-start: 2px solid var(--bronze); }
.plate[aria-selected="true"] .plate__letter { background: var(--bronze); border-color: var(--on-bronze); color: var(--on-bronze); }

/* label that opens the run of building tabs */
.plates__group {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-basis: 100%;
  margin-block-start: .5rem;
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--on-paper);
}
.plates__group::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line-paper);
}
@media (min-width: 62rem) {
  .plates__group { margin-block-start: 0; padding: 1.6rem 1rem .65rem; }
  /* On the compact project page (§10a) the column of plates was the tallest
     thing in the register once the rows went two-up — 440px against 280 of
     rows. Still 50px a plate, over the 44px floor. After the base rules on
     purpose: check.py reads the first `.plate {…}` for the boundary gate. */
  .main--compact .plate { padding-block: .45rem; }
  .main--compact .plates__group { padding-block: 1rem .4rem; }
}

.sheet { display: none; }
.sheet.is-on { display: block; animation: fadeUp .5s var(--ease) both; }
.sheet__head { margin-block-end: 1.5rem; }
.sheet__head h3 { font-size: var(--fs-h3); font-weight: 400; }
.sheet__head p { margin-block-start: .5rem; font-size: var(--fs-sm); color: var(--on-paper-mute); font-weight: 300; }

.doclist { border-block-start: 1px solid var(--line-paper); }
.doc {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 1rem;
  /* was 1.15rem: the 48px file plate sets the row's height anyway, and ten rows
     of the first sheet at 92px each were 920px of a 900px screen */
  padding: .8rem .5rem;
  border-block-end: 1px solid var(--line-paper);
  transition: background .3s var(--ease), padding-inline .3s var(--ease);
}
.doc:hover { background: var(--white); padding-inline: 1rem .5rem; }
.doc__icon {
  flex: none;
  width: 2.4rem; height: 3rem;
  display: grid; place-items: center;
  border: 1px solid var(--line-paper);
  background: var(--white);
  font-family: var(--tech);
  font-size: .6875rem;
  font-weight: 500;
  letter-spacing: .08em;
  color: var(--bronze-dk);
  direction: ltr;
}
.doc__name { font-size: var(--fs-ui); font-weight: 600; }
.doc__meta {
  display: block;
  margin-block-start: .1rem;
  font-family: var(--tech);
  font-size: var(--fs-micro);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--on-paper-mute);
}
.doc__name { display: block; }
.doc__action {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .6rem 1.1rem;
  border: 2px solid var(--edge-paper);
  font-family: var(--tech);
  font-size: var(--fs-ui);
  font-weight: 600;
  letter-spacing: .12em;
  white-space: nowrap;
  transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease);
}
/* On the compact project page (§10a) the pill is slimmer. It is not the
   control — the row is (.doc__link::after covers it) — so it owes no 44px of
   its own, and the row still clears it. Kept AFTER the rule above: check.py
   reads the first `.doc__action {…}` in the file for the boundary gate. */
.main--compact .doc__action { padding-block: .4rem; }
/* "בהכנה" is a STATUS, not a control — those rows carry no link at all. Giving
   it the same 2px box as "הורדה" put seven non-controls on the project page
   looking exactly like the three real download buttons, which is the opposite
   of what a heavier boundary was for. Selected on the inline style both
   renderers already emit, so this stays CSS-only and out of the parity law. */
.doc__action[style*="cursor:default"] {
  border-color: transparent;
  font-weight: 400;
}

/* .doc__action is a <span>, and the real control is .doc__link, whose ::after
   covers the whole row — so it sits ON TOP of this pill and :hover never
   reached it. The element that looks most like a button on the page was the one
   thing on it that never lit up. Drive it from the row instead. Both renderers
   emit the span, so making it a real control is a parity-law change and is not
   done here. */
.doc:hover .doc__action { background: var(--on-paper); border-color: var(--on-paper); color: var(--limestone); }
.doc__action svg { width: .85rem; height: .85rem; }
.doc__link::after { content: ""; position: absolute; inset: 0; }
/* Keyboard focus landed on the 21.5x22 anchor around the file name while the
   pointer target was the 1032x90 row. The overlay already knows the right
   shape, so draw the ring on it. */
.doc__link:focus-visible { outline: 0; box-shadow: none; }
.doc__link:focus-visible::after {
  outline: 3px solid var(--bronze);
  outline-offset: -3px;
  box-shadow: inset 0 0 0 6px var(--on-ink);
}
.doc { position: relative; }

/* A long sheet's first rows, and the button that shows the rest. Built by
   site.js (register), never baked: a script that never runs leaves every row
   showing and no dead button. A folded row is display:none, so nothing in it
   is a tab stop until it is shown. The button is a real control, so it carries
   the 2px --edge-paper boundary. */
.doc.is-folded { display: none; }
.doclist__more {
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  min-block-size: var(--tap);
  margin-block-start: .85rem;
  padding: .5rem 1.1rem;
  border: 2px solid var(--edge-paper);
  font-size: var(--fs-ui);
  font-weight: 600;
  color: var(--on-paper);
  transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease);
}
.doclist__more[hidden] { display: none; }
.doclist__more:hover { background: var(--on-paper); border-color: var(--on-paper); color: var(--limestone); }
/* the fold's chevron, drawn the same way for the same reason: physical borders,
   because it is a picture of an arrow pointing down */
.doclist__more::after {
  content: "";
  width: .5rem; height: .5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-.12rem) rotate(45deg);
  transition: transform .3s var(--ease);
}
.doclist__more[aria-expanded="true"]::after { transform: translateY(.12rem) rotate(-135deg); }

/* Below the two-column register the plates used to wrap into a block of their
   own — 323px of a 390px phone before the first document. They are one row now,
   scrolled sideways like the gallery's thumbnail strip, and the row bleeds to
   the screen edges so a plate cut off at the edge says "there is more".
   Trap 1 applies: no `direction` here; the document is RTL, so the row starts
   on the right and scrolls left, and site.js's arrow keys (ArrowLeft advances)
   already match it — moving focus to a plate scrolls it into view natively.
   The block padding is room for the two-ring focus indicator (3px outline at a
   3px offset), which an overflow box would otherwise clip. */
@media (max-width: 61.9375rem) {
  .docs { gap: 1.25rem; }
  .plates {
    flex-wrap: nowrap;
    gap: .4rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: thin;
    margin-inline: calc(-1 * var(--gut));
    padding: .45rem var(--gut);
    scroll-padding-inline: var(--gut);
  }
  .plate { flex: none; gap: .6rem; padding: .45rem .85rem; scroll-snap-align: start; }
  .plate .plate__letter { width: 1.9rem; height: 1.9rem; }
  .plates__group { flex: none; flex-basis: auto; margin-block-start: 0; padding-inline: .6rem .15rem; }
  .plates__group::after { display: none; }
  .sheet__head { margin-block-end: 1rem; }
}

/* site plan */
.siteplan { display: grid; gap: clamp(2rem, 4vw, 3rem); align-items: center; }
@media (min-width: 62rem) { .siteplan { grid-template-columns: 1fr 1.15fr; } }
.siteplan__img { border: 1px solid var(--line-paper); background: var(--white); padding: .75rem; cursor: zoom-in; }
.siteplan__legend { display: grid; gap: .75rem; margin-block-start: 1.75rem; }
.siteplan__legend li { display: flex; align-items: center; gap: .85rem; font-size: var(--fs-sm); color: var(--on-paper-mute); }
.siteplan__legend .plate__letter { color: var(--bronze-dk); }

/* --- 12. form ------------------------------------------------------------ */
.contact { display: grid; gap: clamp(2.5rem, 5vw, 4rem); }
@media (min-width: 62rem) { .contact { grid-template-columns: 1fr 1.35fr; } }
/* a grid item's default `min-width: auto` cannot shrink below its min-content,
   so one unbreakable token pushes the whole page sideways */
.contact > * { min-width: 0; }

.contact__details { display: grid; gap: 1.75rem; align-content: start; }
.cline { display: grid; gap: .3rem; padding-block-end: 1.25rem; border-block-end: 1px solid var(--line-paper); }
.section--ink .cline, .section--ink-2 .cline { border-block-end-color: var(--line-ink-2); }
.cline__label {
  font-family: var(--tech);
  font-size: var(--fs-micro);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--bronze-dk);
}
/* `wzn.ltd.2018@gmail.com` is one unbreakable token. At Chrome's "Very Large"
   font setting (root 24px) it measures 350px of min-content inside a 300px
   column, and the whole document went 380px wide against a 360px screen — the
   fixed header and call bar with it. Nothing changes at the default size, where
   the address is 233px in a 320px column. */
.cline__value { font-family: var(--display); font-size: 1.35rem; font-weight: 400; overflow-wrap: anywhere; }
.cline__value a { transition: color .3s var(--ease); }
.cline__value a:hover { color: var(--bronze-dk); }
.cline__value .ltr { font-variant-numeric: tabular-nums; }

.form { display: grid; gap: 1.25rem; }
.form__row { display: grid; gap: 1.25rem; }
@media (min-width: 40rem) { .form__row--2 { grid-template-columns: 1fr 1fr; } }

.field { display: grid; gap: .5rem; }
.field > label {
  font-family: var(--tech);
  font-size: .875rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--on-paper-mute);
}
.section--ink .field > label, .section--ink-2 .field > label { color: var(--on-ink-mute); }
.field input, .field select, .field textarea {
  width: 100%;
  padding: .95rem 1.1rem;
  font-family: var(--body);
  font-size: var(--fs-base);
  color: var(--on-paper);
  background: var(--white);
  border: 2px solid var(--edge-paper);
  border-radius: 0;
  transition: border-color .3s var(--ease), box-shadow .3s var(--ease);
  appearance: none;
}
.field textarea { min-height: 7rem; resize: vertical; }
.field select {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%23586470' stroke-width='1.4'><path d='M1 1.5 6 6.5 11 1.5'/></svg>");
  background-repeat: no-repeat;
  background-position: left 1.1rem center;
  background-size: .75rem;
  padding-inline-start: 2.75rem;
}
/* `outline: 0` left these three the only controls on the site with no focus
   ring at all — the indicator was a border-colour change to --bronze, which is
   2.89:1 on white and 2.45:1 on limestone, and a halo that flattened to
   #e7dbcd, 1.15:1. Both under the 3:1 SC 1.4.11 asks. */
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  outline: 3px solid var(--bronze);
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--on-ink);
  border-color: var(--bronze);
}
.field input::placeholder, .field textarea::placeholder { color: var(--placeholder); }
/* The terms error lives in a `.check`, not a `.field`, so it used to miss this
   rule entirely and inherit `.check`'s own muted grey — byte-identical to the
   consent sentence right above it. Forgetting the terms box is the most common
   way this form fails, and the message read as a fourth line of small print
   rather than the reason nothing was sent. */
.field .err, .check .err {
  font-size: .875rem;
  color: var(--error);
  min-height: 0;
}
.field[data-invalid] input, .field[data-invalid] select { border-color: var(--error); }

/* The <label> is the target, and it measured 27px on one line. The box inside
   it was 18.4px — the smallest control anywhere on the site. */
.check {
  display: flex; align-items: flex-start; gap: .75rem;
  padding-block: .55rem;
  font-size: var(--fs-sm); color: var(--on-paper-mute); font-weight: 400;
}
.check input {
  flex: none;
  width: 1.5rem; height: 1.5rem;
  margin-block-start: .25rem;
  accent-color: var(--bronze);
  outline: 2px solid var(--edge-paper);   /* the native box has no visible edge */
  outline-offset: -1px;
}
.check a { color: var(--bronze-dk); text-decoration: underline; text-underline-offset: .2em; }
/* Inline links inside running text are exempt from SC 2.5.5, but these four are
   the site's actual conversion paths — the phone number under the submit
   button, the contact block's number and address, the phone in the unverified
   -availability notice — and they measured 18-28px tall. The negative margin
   returns exactly what the padding takes, so no paragraph re-flows. */
.form__note a, .cline__value a, .prose a, .invguard__notice a {
  display: inline-block;
  padding-block: .65rem;
  margin-block: -.65rem;
}

.form__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.5rem; margin-block-start: .5rem; }
.form__note { font-size: .875rem; color: var(--on-paper-mute); font-weight: 300; }

.form__status {
  padding: 1rem 1.25rem;
  border: 1px solid var(--line-paper);
  background: var(--white);
  font-size: var(--fs-sm);
  display: none;
}
.form__status.is-on { display: block; }
.form__status.is-ok { border-color: var(--bronze); }

/* --- 13. footer ---------------------------------------------------------- */
.ftr { background: var(--ink-3); color: var(--on-ink); padding-block: clamp(3.5rem, 7vw, 5.5rem) 0; }
.ftr__grid { display: grid; gap: 2.5rem; }
@media (min-width: 48rem) { .ftr__grid { grid-template-columns: 1.4fr 1fr 1fr; } }
/* 8.6rem/1.8rem against the trimmed asset reproduce the 137x64px mark and the
   28.8px gap the padded 9.5rem/1.25rem produced — the canvas was contributing
   8.8px of that gap and 12.1px above it. What changes: the mark now starts
   flush with the sibling columns' headings instead of hanging 12px below. */
.ftr__brand img { width: 8.6rem; margin-block-end: 1.8rem; }
.ftr__brand p { font-size: var(--fs-sm); font-weight: 300; color: var(--on-ink-mute); max-width: 26rem; }
.ftr h2 {
  margin-block-end: 1.25rem;
  font-family: var(--tech);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--bronze-dk);
}
/* The rows were 27px tall on a 37.4px pitch: 10.4px of dead space between every
   pair of targets. Close the gap and spend it on the links themselves and each
   row clears 44px — the column grows about 7px per link, and that is the whole
   visible change. */
.ftr__links { display: grid; gap: 0; }
.ftr__links a {
  display: block;
  padding-block: .55rem;
  font-size: var(--fs-ui);
  color: var(--on-ink-mute);
  transition: color .3s var(--ease);
}
.ftr__links a:hover { color: var(--on-ink); }
.ftr__bottom {
  margin-block-start: clamp(2.5rem, 5vw, 4rem);
  padding-block: 1.5rem;
  border-block-start: 1px solid var(--line-ink-2);
  display: flex;
  flex-wrap: wrap;
  gap: .75rem 1.5rem;
  align-items: center;
  justify-content: space-between;
  /* the accessibility trigger is fixed in this corner, and it was measured
     sitting on top of the word "הצהרת נגישות" — the very link it duplicates */
  padding-inline-end: calc(var(--tap) + 1.5rem);
  font-size: .8125rem;
  color: var(--on-ink-faint);
}
.ftr__bottom a:hover { color: var(--on-ink); }

/* --- 14. mobile call bar ------------------------------------------------- */
.callbar {
  position: fixed;
  inset-block-end: 0;
  inset-inline: 0;
  z-index: 90;
  display: none;
  grid-template-columns: 1fr 1fr;
  /* pinned to the token everything else clears it by, so the reserve and the bar
     cannot drift apart */
  min-height: var(--callbar-h);
  border-block-start: 1px solid var(--line-ink);
  background: rgba(242, 237, 228, .97);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  /* On an iPhone the bottom 34px is the home-indicator band and the system
     swallows a swipe there: without this the lower half of both targets — the
     phone number and "השאירו פרטים", the site's two conversion paths — is inside
     it. Physical properties, physical insets; see the note on .wrap. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}
.callbar a {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .55rem;
  padding: .95rem;
  font-family: var(--tech);
  font-size: 1.05rem;
  letter-spacing: .1em;
  color: var(--on-ink);
}
.callbar a + a { background: var(--bronze); color: var(--on-bronze); }
.callbar svg { width: 1rem; height: 1rem; }

/* --- 15. motion ---------------------------------------------------------- */
@keyframes fadeUp { from { opacity: 0; transform: translateY(1.25rem); } to { opacity: 1; transform: none; } }

/* A block is hidden only while a script that can reveal it is known to be alive.
   `js-reveal` is set by the inline script in <head> — before the first paint, so
   a visitor never sees the page flash in and out — and taken off again on
   DOMContentLoaded (or after 5s) unless site.js has announced itself with
   `js-live`. Unconditional `opacity: 0` meant that a dropped site.js — an
   ordinary event on cellular, and the whole point of a data-saver proxy —
   rendered every page completely blank: 16 of 16 blocks on the home page, 12 of
   12 on the project page, all of them present in the DOM and all invisible.

   The gate is wrapped in `:where()` so it adds NO specificity: the rule still
   weighs exactly what `[data-reveal] { … }` weighed, and `.is-in` below and the
   reduced-motion override further down still beat it (CLAUDE.md RTL trap #6 is
   about state classes losing a specificity race — this is the same hazard). A
   browser too old to parse `:where()` drops the rule and shows the content,
   which is the right way round to fail. */
:where(html.js-reveal) [data-reveal] { opacity: 0; transform: translateY(1.75rem); }
/* A transformed element is the containing block of any `position: fixed`
   descendant, so a block that HOLDS one fades but must not move: see the
   comment on the inventory wrapper in build.py. */
[data-reveal="fade"] { transform: none; }
[data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity .85s var(--ease), transform .85s var(--ease);
  transition-delay: var(--d, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  [data-reveal] { opacity: 1; transform: none; }
  .hero__layer img { animation: none; transform: none; }
}

/* --- 16. responsive ------------------------------------------------------ */
@media (max-width: 60rem) {
  .nav {
    position: fixed;
    inset-block-start: 0;
    inset-inline-end: 0;
    width: min(22rem, 88vw);
    height: 100svh;
    padding: 6.5rem var(--gut) 2rem;
    background: var(--ink-2);
    border-inline-start: 1px solid var(--line-ink);
    /* the drawer is a light panel whatever face the header is showing */
    --hdr-fg: var(--on-ink);
    --hdr-accent: var(--bronze-dk);
    transform: translateX(-100%);
    transition: transform .45s var(--ease);
    overflow-y: auto;
  }
  .nav.is-open { transform: none; }
  .nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .nav__list li { border-block-end: 1px solid var(--line-ink-2); }
  .nav__link { padding-block: 1.15rem; font-size: 1.2rem; min-inline-size: 0; text-align: start; }
  .nav__link::after { display: none; }
  .burger { display: grid; }
  .hdr__cta { display: none; }
  .nav__phone {
    display: flex !important;
    margin-block-start: 2rem;
    justify-content: center;
  }
  body.nav-open { overflow: hidden; }
  .nav-veil {
    position: fixed; inset: 0; z-index: 99;
    background: rgba(9,13,18,.6);
    opacity: 0; pointer-events: none;
    transition: opacity .4s var(--ease);
  }
  body.nav-open .nav-veil { opacity: 1; pointer-events: auto; }
  .callbar { display: grid; }
  /* the bar is 62px, not the 56px this used to reserve, and on a notched phone
     the home-indicator band sits under it as well */
  body { padding-block-end: calc(var(--callbar-h) + env(safe-area-inset-bottom, 0px)); }
  .nav { padding-block-end: calc(2rem + env(safe-area-inset-bottom, 0px)); }
  .hdr { z-index: 100; }
}
@media (min-width: 60.0625rem) { .nav__phone { display: none; } }

@media (max-width: 56rem) {
  .titleblock__list { grid-template-columns: 1fr; }
  .titleblock__item + .titleblock__item { border-inline-end: 0; border-block-start: 1px solid var(--line-photo-2); }
  .titleblock__link { padding-block: 1.05rem; }
  .titleblock__name { font-size: 1.05rem; }
  .hero { min-height: auto; }
  .hero__body { padding-block: 4rem 3rem; }
  .hero__meta { margin-block-start: 5.5rem; font-size: .6875rem; letter-spacing: .18em; }
  .hero__meta .hide-sm { display: none; }
  .stats { grid-template-columns: 1fr; }
  .stat + .stat { border-inline-end: 0; border-block-start: 1px solid var(--line-ink); }
  .stat { display: flex; align-items: baseline; justify-content: space-between; text-align: start; padding-inline: 0; }
  .stat__label { margin-block-start: 0; }
}

@media (max-width: 40rem) {
  /* The download action stays on the name's line. It used to take a full-width
     line of its own below the name, which made every row 159px tall on a 390px
     phone and the register 2,400px long. The whole row is the link (the
     .doc__link overlay in §11), so the action is a label for it, not a second
     target — it only needs to be legible, and it keeps its 44px height. The
     name column is minmax(0, 1fr) so a long Hebrew name wraps inside it rather
     than pushing the row wider than a 320px screen. */
  .doc { grid-template-columns: auto minmax(0, 1fr) auto; gap: .75rem; padding: .7rem .25rem; }
  .doc:hover { padding-inline: .5rem .25rem; }
  .doc__icon { width: 2.1rem; height: 2.6rem; }
  .doc__action { gap: .35rem; padding: .45rem .75rem; }
  .form__foot { flex-direction: column; align-items: stretch; }
  /* At 390 the three hero CTAs wrapped 2 + 1, which handed the LAST one a row
     of its own and made the least important button the most prominent. Give the
     first line to the primary instead. */
  .hero__actions { gap: .7rem; }
  .hero__actions > .btn:not(.btn--ghost):nth-last-child(n+3) { flex: 1 0 100%; }
  /* and the two ghosts then share the line rather than making a staircase —
     at 360 they need 332px side by side in a 320px box, so they have to be
     allowed to shrink */
  .hero__actions > .btn:not(.btn--ghost):nth-last-child(n+3) ~ .btn--ghost {
    flex: 1 1 0;
    min-inline-size: 0;
    padding-inline: 1rem;
  }
  .ftr__links a { padding-block: .7rem; }
  .form__foot .btn { justify-content: center; }
}

/* A phone turned sideways is 390px tall, and 147px of that is fixed chrome — the
   header and the call bar together take 38% of the screen before any content.
   Every compaction rule above keys on WIDTH, so at 844x390 the hero still spent
   88px on the eyebrow's top margin and 112px on the body padding and measured
   800px tall: the two CTAs and the project index — the hero's whole second job,
   and one of the design's two signature ideas — sat a screen and a half down.
   These rules are height-aware, so portrait is untouched at every width. */
@media (max-height: 33rem) and (orientation: landscape) {
  .hero { min-height: auto; }
  /* must stay clear of the stuck header (4.25rem) or the rule hides under it */
  .hero__meta { margin-block-start: 4.4rem; padding-block: .35rem; }
  .hero__body { padding-block: 1rem .75rem; }
  .hero h1 { font-size: clamp(1.6rem, 3.2vw, 2.3rem); }
  .hero__lede { margin-block-start: .7rem; font-size: 1rem; }
  /* the two CTAs are the point of the first screen: this is trimmed until they
     clear the call bar, which is as far as 390px of height will stretch */
  .hero__actions { margin-block-start: .9rem; }
  .titleblock__link { padding-block: .7rem; }
  /* The project hero relies on `min-height: min(82svh, 46rem)` + `align-items:
     flex-end` to push its content clear of the fixed header. Sideways the
     content is TALLER than 82svh, so the box is content-sized, flex-end does
     nothing and the breadcrumb lands at the top — underneath the header and its
     logo. The clearance has to be explicit here. */
  .phero__in { padding-block: 4.75rem 1.25rem; }
  .phero__crumbs { margin-block-end: .9rem; }
  .phero__wordmark { margin-block-end: .75rem; }
}

@media print {
  .hdr, .callbar, .lbox, .form, .nav-veil { display: none !important; }
  body { background: #fff; color: #000; }
}

/* --- 17. bidi utilities --------------------------------------------------
   `.ltr` flips the block direction and must only wrap inline runs. Numeric
   labels that head an RTL block are isolated instead, so they stay
   start-aligned with the heading beneath them. */
.principle__no, .pcard__where { unicode-bidi: isolate; }

/* A hyphenated Latin token is a line-break opportunity, and the parentheses
   around it are bidi-neutral: when the break falls inside the token the two
   lines resolve their neutrals independently and the brackets come out
   mirrored and split. Pair with `.ltr` on the same span to keep the run whole
   and the brackets attached to it. */
.nowrap { white-space: nowrap; }

/* --- 18. corrections ------------------------------------------------------ */
.tlink svg { width: .85rem; height: .85rem; flex: none; transition: transform .3s var(--ease); }
.tlink:hover svg { transform: translateX(-.25rem); }
.phero__crumbs svg { width: .8rem; height: .8rem; }

/* a centred wide card should not also push its footer to the bottom */
.pcard--wide .pcard__foot { margin-block-start: .5rem; }

/* pull quote — used once, on the about page, for the company's own promise */
.pull {
  margin: 2rem 0;
  padding-inline-start: 1.5rem;
  border-inline-start: 2px solid var(--bronze);
  font-family: var(--display);
  font-size: var(--fs-h3);
  font-weight: 400;
  line-height: 1.5;
  color: var(--on-paper);
}

/* --- 19. Hebrew tracking --------------------------------------------------
   Barlow Condensed carries no Hebrew, so Hebrew labels fall back to Assistant.
   Hebrew has no case and no serifs to separate letters — wide tracking makes
   it harder to read, not more refined. Latin labels keep the wide tracking;
   Hebrew ones are tightened. */
.field > label,
.filter,
.chip,
.titleblock__where,
.phero__where,
.pcard__where,
.doc__meta,
.doc__action,
.fact__label,
.ftr h2,
.hero__meta .he,
.pcard__facts,
.plate,
.imix__plate,
.phero__crumbs,
.statement__figure figcaption,
.callbar a,
.form__note { letter-spacing: .02em; }

/* the plate letter itself is Latin and keeps its drafting tracking — and so
   does a room-count mark, which is digits; a kind mark (גן, פנט׳) is Hebrew and
   keeps the tightened tracking above */
.plate__letter, .imix__plate.ltr { letter-spacing: .06em; }

/* a page-leading heading is an <h1> even when it is styled as a section head */
.head h1 { font-size: var(--fs-h2); font-weight: 400; letter-spacing: -.01em; }

/* any icon inside a button-styled element keeps a fixed optical size */
.btn svg { width: .95rem; height: .95rem; flex: none; }
.nav__phone { white-space: nowrap; }

/* checkbox errors get their own line, and the control itself is marked */
/* the resting outline below is more specific than :focus-visible, so the focus
   ring has to be restated at the same specificity or it never lands */
.check input:focus-visible {
  outline: 3px solid var(--bronze);
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--on-ink);
}
.check .err { display: block; margin-block-start: .35rem; }
.check[data-invalid] input { outline: 2px solid var(--error); outline-offset: 2px; }

/* text over photography carries a soft shadow so contrast survives whichever
   render is behind it — the scrim alone cannot cover a bright sunset */
.hero h1, .hero__lede, .hero__meta,
.phero h1, .phero__where, .phero__crumbs,
.phero .eyebrow { text-shadow: 0 1px 22px rgba(9, 13, 18, .7); }

/* --- 20. apartment inventory ---------------------------------------------
   A stacking plan: one row per floor, read top-down, one cell per apartment.
   Rows share a column count that is the LCM of the flat-counts on that
   building's floors, so a floor with two penthouses lines up with a floor of
   five flats and the penthouses correctly read as wider. */
.inv { display: grid; gap: clamp(1.5rem, 3vw, 2.25rem); }

/* summary */
.invsum {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  border: 1px solid var(--line-ink);
}
@media (min-width: 48rem) { .invsum { grid-template-columns: repeat(4, 1fr); } }
.invsum__cell { padding: 1.25rem 1rem; text-align: center; }
.invsum__cell + .invsum__cell { border-inline-start: 1px solid var(--line-ink); }
@media (max-width: 47.999rem) {
  .invsum__cell:nth-child(odd) { border-inline-start: 0; }
  .invsum__cell:nth-child(-n+2) { border-block-end: 1px solid var(--line-ink); }
}
/* Five cells once שוק חופשי is in the summary (build.py / render.js): one row
   of five from 48rem; below it the total spans the top and the four statuses
   sit two by two underneath. */
@media (min-width: 48rem) { .invsum:has(> :nth-child(5)) { grid-template-columns: repeat(5, 1fr); } }
@media (max-width: 47.999rem) {
  .invsum:has(> :nth-child(5)) > .invsum__cell:first-child { grid-column: 1 / -1; }
  .invsum:has(> :nth-child(5)) > .invsum__cell:nth-child(n) { border-inline-start: 1px solid var(--line-ink); border-block-end: 0; }
  .invsum:has(> :nth-child(5)) > .invsum__cell:nth-child(even) { border-inline-start: 0; }
  .invsum:has(> :nth-child(5)) > .invsum__cell:first-child { border-inline-start: 0; }
  .invsum:has(> :nth-child(5)) > .invsum__cell:nth-child(-n+3) { border-block-end: 1px solid var(--line-ink); }
}
.invsum__num {
  display: block;
  font-family: var(--display);
  font-size: clamp(1.9rem, 3.5vw, 2.7rem);
  font-weight: 300;
  line-height: 1;
  color: var(--bronze-dk);
  font-variant-numeric: tabular-nums;
}
.invsum__label {
  display: block;
  margin-block-start: .5rem;
  font-size: var(--fs-sm);
  font-weight: 300;
  color: var(--on-ink-mute);
}

/* building selector */
.inv__buildings { display: grid; grid-template-columns: repeat(2, 1fr); gap: .5rem; }
@media (min-width: 40rem) { .inv__buildings { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 62rem) { .inv__buildings { grid-template-columns: repeat(5, 1fr); } }

.bstack {
  display: flex;
  align-items: center;
  gap: .8rem;
  padding: .85rem 1rem;
  text-align: start;
  border: 2px solid var(--edge-ink);
  color: var(--on-ink-mute);
  transition: background .3s var(--ease), border-color .3s var(--ease), color .3s var(--ease);
}
.bstack:hover { border-color: var(--line-bronze); color: var(--on-ink); }
.bstack[aria-selected="true"] {
  background: rgba(192, 142, 95, .16);
  border-color: var(--bronze);
  color: var(--on-ink);
}
.bstack__letter {
  flex: none;
  width: 2.2rem; height: 2.2rem;
  display: grid; place-items: center;
  border: 1px solid currentColor;
  font-family: var(--tech);
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: .06em;
}
.bstack[aria-selected="true"] .bstack__letter {
  background: var(--bronze); border-color: var(--bronze); color: var(--on-bronze);
}
.bstack__meta { display: grid; gap: .1rem; min-width: 0; }
/* At Chrome's "Very Large" setting the root is 24px, every rem in the tile grows
   with it, and two tiles no longer fit 360px: the block's min-content was 341 in
   a 300px column and the whole document went 372px wide against a 360px screen —
   the fixed header and call bar with it. `min-width: 0` above only lets the
   TRACK shrink; the longest Hebrew word in the tally is what actually held the
   floor up. Nothing changes at any normal font size, where these lines never
   reach their own min-content. */
.bstack__name, .bstack__count { overflow-wrap: anywhere; }
.bstack__name { font-size: .9875rem; }
.bstack__count { font-size: .8125rem; color: var(--on-ink-faint); }
.bstack[aria-selected="true"] .bstack__count { color: var(--bronze-dk); }

/* filters — a progressive enhancement, hidden until the script runs */
.inv__controls { display: none; }
.inv.is-js .inv__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1rem;
  border-block: 1px solid var(--line-ink-2);
}
.inv__chips { display: flex; flex-wrap: wrap; gap: .4rem; }
.ichip {
  padding: .5rem 1rem;
  font-size: var(--fs-ui);
  color: var(--on-ink-mute);
  border: 2px solid var(--edge-ink);
  transition: background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.ichip:hover { color: var(--on-ink); border-color: var(--on-ink); }
.ichip[aria-pressed="true"] { background: var(--bronze); border-color: var(--bronze); color: var(--on-bronze); font-weight: 600; }
.inv__toggle { display: flex; align-items: center; gap: .6rem; font-size: .9375rem; color: var(--on-ink-mute); cursor: pointer; }
.inv__toggle input { width: 1.1rem; height: 1.1rem; accent-color: var(--bronze); }

/* plan */
.inv__body { display: grid; gap: clamp(1.5rem, 3vw, 2rem); align-items: start; }
@media (min-width: 68rem) { .inv__body { grid-template-columns: minmax(0, 1fr) 19rem; } }

.inv.is-js .stack:not(.is-on) { display: none; }
.stack + .stack { margin-block-start: 2.5rem; }
.inv.is-js .stack + .stack { margin-block-start: 0; }

.stack__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .3rem .75rem;
  margin-block-end: 1.1rem;
  font-size: 1.25rem;
  font-weight: 500;
}
.stack__head span { font-family: var(--body); font-size: .875rem; font-weight: 300; color: var(--on-ink-mute); }

.stack__plan { display: grid; gap: .4rem; }
.floor {
  display: grid;
  grid-template-columns: 5.25rem minmax(0, 1fr) 2.75rem;
  grid-template-areas: "label units tally";
  align-items: stretch;
  gap: .5rem;
}
.floor__label {
  grid-area: label;
  display: flex;
  align-items: center;
  font-size: .8125rem;
  color: var(--on-ink-mute);
}
.floor__tally {
  grid-area: tally;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: .75rem;
  font-variant-numeric: tabular-nums;
  color: var(--on-ink-faint);
}
.floor__units {
  grid-area: units;
  display: grid;
  grid-template-columns: repeat(var(--cols, 4), minmax(0, 1fr));
  gap: .4rem;
}

/* one apartment */
.unit {
  display: grid;
  align-content: center;
  gap: .1rem;
  min-height: 4.25rem;
  padding: .5rem .35rem;
  text-align: center;
  /* The neutral cell (לבירור, and every flat until a live config confirms it)
     is a white card on the band with a drawn edge: plainly "a flat, status not
     stated", and unlike any of the four status fills — no fill is white. On
     the dark plan it was a 3% tint; on a light one that would vanish. */
  border: 1px solid var(--line-ink);
  background: rgba(255, 255, 255, .72);
  color: var(--on-ink);
  transition: background .25s var(--ease), border-color .25s var(--ease), transform .25s var(--ease);
}
.unit__no { font-size: .6875rem; color: var(--on-ink-mute); }
.unit__rooms { font-family: var(--display); font-size: 1.05rem; font-weight: 500; line-height: 1.2; }
.unit__status { font-size: .6875rem; }

.unit[data-status="available"],
.unit[data-status="available"] .unit__no,
.unit[data-status="available"] .unit__status {
  color: var(--st-free-ink);
}
.unit[data-status="available"] { background: var(--st-free); border-color: var(--st-free); }
.unit[data-status="available"] .unit__status { font-weight: 600; }

.unit[data-status="reserved"],
.unit[data-status="reserved"] .unit__no,
.unit[data-status="reserved"] .unit__status {
  color: var(--st-hold-ink);
}
.unit[data-status="reserved"] { background: var(--st-hold); border-color: var(--st-hold); }

.unit[data-status="sold"],
.unit[data-status="sold"] .unit__no,
.unit[data-status="sold"] .unit__status {
  color: var(--st-sold-ink);
}
.unit[data-status="sold"] { background: var(--st-sold); border-color: var(--st-sold); }
.unit[data-status="sold"] .unit__rooms { font-weight: 400; }

/* שוק חופשי: as recessive as sold, but not the same thing — lighter, hatched,
   with a dashed edge, so the two never read as one status. */
.unit[data-status="market"],
.unit[data-status="market"] .unit__no,
.unit[data-status="market"] .unit__status {
  color: var(--st-market-ink);
}
.unit[data-status="market"] {
  background: repeating-linear-gradient(135deg, var(--st-market) 0 6px, var(--st-market-2) 6px 12px);
  border: 1px dashed var(--st-market-ink);
}
.unit[data-status="market"] .unit__rooms { font-weight: 400; }

.unit:hover { transform: translateY(-2px); border-color: var(--bronze); }
.unit[aria-pressed="true"] {
  background: var(--bronze);
  border-color: var(--bronze);
  color: var(--on-bronze);
}
/* Solid, not 72% — the translucent version measured 3.95:1 on the bronze fill
   at 11px and nothing gated it, because check.py's PAIRS are all text-on-surface
   and this is text-on-fill. --on-bronze is 6.24:1. */
.unit[aria-pressed="true"] .unit__no,
.unit[aria-pressed="true"] .unit__status { color: var(--on-bronze); }
.unit.is-dimmed, .unit:disabled { opacity: .22; pointer-events: none; cursor: default; }

.stack__empty {
  margin-block-end: 1rem;
  padding: .85rem 1rem;
  border: 1px solid var(--line-ink-2);
  color: var(--on-ink-mute);
  font-size: var(--fs-sm);
}

.stack__breakdown { margin-block-start: 1.75rem; padding-block-start: 1.25rem; border-block-start: 1px solid var(--line-ink-2); }
.stack__breakdown h4 { font-family: var(--body); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .02em; color: var(--bronze-dk); margin-block-end: .75rem; }
.stack__breakdown ul { display: flex; flex-wrap: wrap; gap: .5rem 1.75rem; }
.stack__breakdown li { display: flex; gap: .6rem; font-size: var(--fs-sm); color: var(--on-ink-mute); }
.stack__breakdown li span:last-child { color: var(--on-ink); font-variant-numeric: tabular-nums; }

/* detail panel */
.detail {
  padding: 1.5rem;
  border: 1px solid var(--line-ink);
  background: rgba(255, 255, 255, .72);
}
@media (min-width: 68rem) {
  /* the aside must stretch to the row height, or the sticky card has no travel */
  .inv__panel { align-self: stretch; }
  .inv__panel .detail { position: sticky; top: 7rem; }
}

/* Below the two-column breakpoint the panel sits under the whole stacking plan,
   so a card rendered in place would land several screens away. The selected
   apartment becomes a bottom sheet instead — always in view, whatever the scroll
   position, and no scrolling to chase. The legend stays in flow underneath so
   the page does not shift when a card opens. */
@media (max-width: 67.999rem) {
  .inv__panel .detail:not([data-detail-rest]) {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 80;
    max-height: 72svh;
    overflow-y: auto;
    /* the sheet is the one scrollable region on a phone: a flick that reaches
       its end must not hand the momentum to the plan behind it */
    overscroll-behavior: contain;
    border-inline: 0;
    border-block-end: 0;
    /* opaque, and the page's lightest surface, so the sheet reads as a card
       lifted off the band it covers */
    background: var(--limestone);
    box-shadow: 0 -1.25rem 3rem -1rem rgba(9, 13, 18, .35);
    animation: sheetUp .35s var(--ease);   /* no fill: the resting state is the correct one */
    /* it spans the screen, so in landscape its inline edges run under the notch */
    padding-left: max(1.5rem, env(safe-area-inset-left, 0px));
    padding-right: max(1.5rem, env(safe-area-inset-right, 0px));
  }
  /* The sheet scrolls, so a dismiss control in the flow can sit below its own
     visible edge — in landscape "חזרה למקרא" was 57px past the bottom of a box
     that covered the entire plan. The header row is sticky, so the ✕ is on
     screen whatever the content height and wherever the sheet is scrolled. */
  .inv__panel .detail:not([data-detail-rest]) { padding-block-start: 0; }
  .inv__panel .detail:not([data-detail-rest]) .detail__bar {
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
    /* the card's own block padding, moved onto the bar: no negative margins, so
       nothing can end up painted underneath it */
    padding-block: 1.25rem 1rem;
    margin-block-end: .5rem;
    background: var(--limestone);
    border-block-end: 1px solid var(--line-ink-2);
  }
}
@media (max-width: 60rem) {
  /* clear the fixed call bar — and the home indicator underneath it */
  .inv__panel .detail:not([data-detail-rest]) {
    inset-block-end: calc(var(--callbar-h) + env(safe-area-inset-bottom, 0px));
  }
}
@media (max-width: 67.999rem) and (min-width: 60.0625rem) {
  .inv__panel .detail:not([data-detail-rest]) { inset-block-end: env(safe-area-inset-bottom, 0px); }
}
/* Sideways there are ~320px between the header and the call bar, and a 72svh
   sheet took 281px of them: the plan the buyer is choosing from vanished
   completely, so there was no second apartment left to tap and no way back to
   one. Half the screen still shows a floor row or two behind it. */
@media (max-width: 67.999rem) and (max-height: 33rem) {
  .inv__panel .detail:not([data-detail-rest]) {
    max-height: 52svh;
    padding-block-end: 0;      /* so the pinned button below sits flush */
    /* the pinned bronze CTA otherwise runs straight into the call bar's bronze
       half and the two read as one clipped block */
    border-block-end: 2px solid var(--limestone);
  }
  /* 200px of sheet cannot hold 370px of card, so it scrolls — and the enquiry
     button is the reason the sheet exists. Pin it to the foot the way the ✕ is
     pinned to the head, and let the spec rows scroll between them. The whole
     action row is pinned, not the button alone: תיק דירה sits in that row, and
     half a pinned pair would scroll out from under the other half. It needs the
     sheet's own background — the gap between the two controls is see-through. */
  .inv__panel .detail:not([data-detail-rest]) .detail__acts {
    position: sticky;
    inset-block-end: 0;
    margin-block-start: .75rem;
    background: var(--limestone);
    box-shadow: 0 -1rem 1.75rem -.5rem var(--limestone);
  }
  /* every millimetre of the 200px counts here */
  .inv__panel .detail:not([data-detail-rest]) .detail__title { font-size: 1.25rem; }
  .inv__panel .detail:not([data-detail-rest]) .detail__rows { margin-block: .75rem; }
  .inv__panel .detail:not([data-detail-rest]) .detail__rows div { padding-block: .45rem; }
  /* the ✕ closes the sheet and is always on screen; a second, smaller closer
     below the fold is just another thing to scroll past */
  .inv__panel .detail:not([data-detail-rest]) .tlink[data-clear] { display: none; }
}
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: none; } }

/* The backdrop that makes "tap anywhere else" work — the same idiom, and the
   same shape, as the nav drawer's veil. It is deliberately lighter than the
   drawer's .6: the plan behind it is still live (site.js forwards a tap on a
   flat straight through), so it must read as "on top of", not "instead of". */
.detail-veil {
  position: fixed;
  inset: 0;
  z-index: 79;
  background: rgba(9, 13, 18, .45);
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s var(--ease);
}
.detail-veil.is-on { opacity: 1; pointer-events: auto; }
@media (min-width: 68rem) { .detail-veil { display: none; } }

.detail__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.detail__bar .detail__eyebrow { margin-block-end: 0; }
.detail__close {
  flex: none;
  width: 2.75rem; height: 2.75rem;
  display: grid; place-items: center;
  /* pulled out to the card's padding edge so the row keeps the eyebrow's height */
  margin-block: -.75rem;
  margin-inline-end: -.6rem;
  border: 2px solid var(--edge-ink);
  color: var(--on-ink-mute);
  transition: background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.detail__close:hover { background: var(--bronze); border-color: var(--bronze); color: var(--on-bronze); }
.detail__close svg { width: 1rem; height: 1rem; }

.detail__eyebrow {
  font-family: var(--tech);
  font-size: var(--fs-micro);
  letter-spacing: .02em;
  color: var(--bronze-dk);
  margin-block-end: .6rem;
}
.detail__title { font-size: 1.5rem; font-weight: 400; }
.detail__where { margin-block-start: .3rem; font-size: var(--fs-sm); color: var(--on-ink-mute); }
.detail__hint, .detail__note { font-size: var(--fs-sm); font-weight: 300; color: var(--on-ink-mute); }
.detail__note { margin-block-start: 1.25rem; padding-block-start: 1.25rem; border-block-start: 1px solid var(--line-ink-2); font-size: .8125rem; }
.detail__rows { display: grid; gap: 0; margin-block: 1.25rem; }
.detail__rows div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-block: .65rem;
  border-block-end: 1px solid var(--line-ink-2);
  font-size: var(--fs-sm);
}
.detail__rows dt { color: var(--on-ink-mute); font-weight: 300; }
.detail__rows dd { margin: 0; color: var(--on-ink); }
.detail .btn { width: 100%; justify-content: center; margin-block-start: .25rem; }

/* תיק דירה — the file one apartment carries, beside the enquiry button.
   It is an <a>, not a button: it navigates to a file, which is also what makes
   it keyboard reachable and openable in a new tab for free. It borrows the
   document register's language rather than inventing a third — a type plate and
   a size — so a buyer on a phone plan knows what a tap costs before it costs it.
   Absent when the flat has none; there is no disabled state to lay out. */
.detail__acts { display: flex; flex-wrap: wrap; gap: .6rem; margin-block-start: 1.25rem; }
/* The enquiry button is the reason the card exists, so it keeps the wider basis
   and takes the whole row back the moment the two no longer fit side by side —
   which is what the flex-basis pair decides: side by side from ~26rem of card,
   stacked below it. In a phone's short landscape, where the sheet is 200px tall,
   they are side by side and the row costs one line instead of two. */
.detail__acts .btn { flex: 1 1 14rem; width: auto; margin-block-start: 0; }
.detail__doss {
  flex: 1 1 11rem;
  display: flex;
  align-items: center;
  /* centred, because the enquiry button beside it is centred and two controls in
     one row that align differently read as two unrelated things */
  justify-content: center;
  gap: .65rem;
  min-height: 2.9rem;              /* 46px — a finger target inside the sheet */
  padding: .5rem 1.1rem;
  border: 1px solid var(--line-ink);
  color: var(--on-ink);
  transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease);
}
/* ink on bronze, never white — the fill is 5.5:1 and white would be 2.9:1 */
.detail__doss:hover { background: var(--bronze); border-color: var(--bronze); color: var(--on-bronze); }
.detail__doss svg { width: 1.05rem; height: 1.05rem; flex: none; }
/* a grid, so the two lines are blocks and `start` still means the right-hand
   edge; the LTR run is the inline <span> inside the meta, never this container */
.detail__doss-text { display: grid; gap: .1rem; min-width: 0; }
.detail__doss-name { font-size: var(--fs-sm); font-weight: 600; }
/* Latin and digits only by construction (site.js::fileKind falls back to FILE),
   so this one keeps the drafting tracking and stays out of the §19 list */
.detail__doss-meta {
  font-family: var(--tech);
  font-size: var(--fs-micro);
  letter-spacing: .14em;
  color: var(--on-ink-mute);
}
.detail__doss:hover .detail__doss-meta { color: var(--on-bronze); }

/* Several files (owner, 2026-09-27): a labelled list under the action row, one
   full-width control per file, each in the single control's own dress. They
   start-align rather than centre — a column of names reads down its right-hand
   edge in Hebrew, and centred names of different lengths would zig-zag. The
   name wraps anywhere: an operator-written file name has no spaces to break on
   and must never push the sheet sideways. */
.detail__files { margin-block-start: 1rem; }
.detail__files-head { margin: 0 0 .5rem; font-size: var(--fs-sm); font-weight: 600; color: var(--on-ink); }
.detail__files-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.detail__files-list .detail__doss { justify-content: flex-start; }
.detail__files-list .detail__doss-name { overflow-wrap: anywhere; }

.legend { display: grid; gap: .6rem; margin-block-start: 1.25rem; }
.legend__item { display: flex; align-items: center; gap: .7rem; font-size: var(--fs-sm); color: var(--on-ink-mute); }
.legend__swatch { width: 1.15rem; height: 1.15rem; flex: none; border: 1px solid var(--line-ink); background: rgba(255, 255, 255, .72); }
.legend__swatch.is-available { background: var(--st-free); border-color: var(--st-free); }
.legend__swatch.is-reserved { background: var(--st-hold); border-color: var(--st-hold); }
.legend__swatch.is-sold { background: var(--st-sold); border-color: var(--st-sold); }
.legend__swatch.is-market {
  background: repeating-linear-gradient(135deg, var(--st-market) 0 3px, var(--st-market-2) 3px 6px);
  border: 1px dashed var(--st-market-ink);
}

.inv__updated { font-size: .8125rem; color: var(--on-ink-faint); }

@media (max-width: 44rem) {
  .floor {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "label tally" "units units";
    gap: .3rem .5rem;
    padding-block-end: .6rem;
  }
  .floor + .floor { border-block-start: 1px solid var(--line-ink-2); padding-block-start: .6rem; }
  .unit { min-height: 3.9rem; padding-inline: .2rem; }
  .unit__rooms { font-size: .95rem; }
}

/* --- 20a. the invitation to the stacking plan ------------------------------
   The plan has a page of its own now, and this is what stands in its place on
   the project page. A bare "click here" would have been a downgrade, so it says
   what is behind the link: how many flats, how many are free, and what the mix
   of room counts is — in the plan's own drafting language, so the two read as
   one thing in two places.

   Three of the numbers are STRUCTURE (flats, buildings, per-type totals) and
   are true until the next deploy. "Available" is the same availability CLAIM
   the plan makes, so it lives and dies with it: §21 below withdraws it exactly
   as it withdraws the statuses on the plan, and the neutral twin beside it
   takes its place. That is why this block is wrapped in the same
   availability_guard(). */
.invite { display: grid; gap: clamp(1.5rem, 3vw, 2.25rem); }

/* Three across at EVERY width, including 360px. They used to stack below 40rem
   and that cost 210px of a phone screen for nothing: the numbers are three
   digits at most and tabular, and the labels are two short Hebrew words that
   wrap happily in a 105px cell. The owner has complained twice about scrolling
   on this site, and stacking these was the single largest avoidable scroll on
   the page a buyer lands on. */
.invite__counts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border: 1px solid var(--line-ink); }
.invite__cell { padding: 1.25rem .55rem; text-align: center; }
/* trap 4: the rule between two siblings belongs to the LATER one, or the
   container is left with a stray line on its outer edge */
.invite__cell + .invite__cell { border-inline-start: 1px solid var(--line-ink); }
@media (min-width: 40rem) { .invite__cell { padding-inline: 1rem; } }

.invite__num {
  display: block;
  font-family: var(--display);
  font-size: clamp(1.9rem, 3.5vw, 2.7rem);
  font-weight: 300;
  line-height: 1;
  color: var(--bronze-dk);
  font-variant-numeric: tabular-nums;
}
/* the neutral twin of a count, shown only when nothing has confirmed it */
.invite__check {
  display: none;
  font-family: var(--display);
  font-size: clamp(1.35rem, 2.4vw, 1.8rem);
  font-weight: 300;
  line-height: 1.2;
  color: var(--on-ink-mute);
}
.invite__label {
  display: block;
  margin-block-start: .5rem;
  font-size: var(--fs-sm);
  font-weight: 300;
  color: var(--on-ink-mute);
}

.invite__mixhead {
  font-family: var(--body);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--bronze-dk);
  margin-block-end: .9rem;
}
/* Two columns of types at every width, so six types take three rows and not
   six. Each type is a small grid rather than a flex line, because a flex line
   is what forced the choice this used to document: at 360-390px the label and
   the two counts do not fit on one line, and flex-wrap was the only honest
   answer. Named areas give three honest answers instead, one per width, and
   every track that holds Hebrew is minmax(0, 1fr) — it wraps a word, it never
   clips one and it never pushes the page wider than the screen (the .bstack bug
   at large font sizes):
     phone        the plate beside three short lines — type, all, free
     40rem up     type on one line, the two counts on the next
     80rem up     one line, as it always was on a desktop
   The neutral twin (.imix__check) shares the free count's area, so wherever the
   count would be, that is where the word stands in for it — in flow when the
   config failed (§21), and drawn over the hidden count while pending (below). */
.invite__mixlist {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(1rem, 3vw, 2.25rem);
  border-block-start: 1px solid var(--line-ink-2);
}
.imix {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "plate label" "plate all" "plate free";
  align-items: center;
  gap: .05rem .75rem;
  padding-block: .6rem;
  border-block-end: 1px solid var(--line-ink-2);
}
@media (min-width: 40rem) {
  .imix {
    grid-template-columns: auto auto minmax(0, 1fr);
    grid-template-areas: "plate label label" "plate all free";
    column-gap: .9rem;
  }
}
@media (min-width: 80rem) {
  .imix {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas: "plate label all free";
    padding-block: .7rem;
  }
}
.imix__plate {
  grid-area: plate;
  width: 2.2rem; height: 2.2rem;
  display: grid; place-items: center;
  border: 1px solid var(--line-ink);
  font-family: var(--tech);
  font-size: .9375rem;
  font-weight: 500;
  color: var(--bronze-dk);
}
.imix__label { grid-area: label; min-inline-size: 0; font-size: var(--fs-sm); color: var(--on-ink); }
.imix__all, .imix__free, .imix__check { font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.imix__all { grid-area: all; color: var(--on-ink-mute); }
.imix__free { grid-area: free; color: var(--bronze-dk); }
.imix__check { grid-area: free; display: none; color: var(--on-ink-mute); }
/* §21 draws the pending word over the hidden count, pinned to the row's
   inline-END — which, in a flex line, is where the count was. In the grid the
   free count's own area is the containing block (an absolutely positioned grid
   child with a grid-area is placed in that area), and the count sits at that
   area's START, so the word is pinned there too and nothing moves on reveal.
   One class more specific than the §21 rule, which comes later in the file. */
html.js .invguard.is-pending .invite__mixlist .imix__check { inset-inline-start: 0; inset-inline-end: auto; }

/* In one column the CTA comes BEFORE the mix. This section is an invitation to
   the plan: a reader who came for the plan should not have to scroll past six
   rows of detail to find the way to it, and a reader who came for the detail
   loses nothing by meeting the link first. It saved 320px of a 390px phone.
   `order` and not a DOM change, because the DOM order is the one a screen
   reader takes and "the numbers, then the mix, then the way through" is the
   right narrative — this is a visual concession to a short screen. */
.invite__mix { order: 2; }
.invite__go { display: grid; gap: 1.25rem; justify-items: start; order: 1; }
.invite__lede { font-size: var(--fs-sm); font-weight: 300; line-height: 1.8; color: var(--on-ink-mute); }
.invite__note { font-size: var(--fs-micro); font-weight: 300; color: var(--on-ink-faint); }

/* Two columns, and the mix goes back to the wide one. This block sits BELOW
   every rule it overrides on purpose: `order` above and `order` here have the
   same specificity, so a media query written earlier in the file would lose to
   the flat rule that follows it and the two columns would silently swap. */
@media (min-width: 68rem) {
  .invite { grid-template-columns: minmax(0, 1fr) 22rem; align-items: start; }
  .invite__counts { grid-column: 1 / -1; }
  .invite__mix, .invite__go { order: 0; }
}

/* The way back, and the way sideways, at the foot of the apartments page.

   These two are the only in-page way off this page — there is no plan to scroll
   back into, and the burger is a screen away — so unlike a .tlink sitting beside
   a paragraph they have to be real touch targets. The house .tlink is 34px tall;
   2.75rem is the 44px WCAG 2.5.5 and both platform guidelines ask for. The
   padding is split so the underline stays where the eye expects it, right under
   the words, rather than floating at the bottom of a taller box. */
.backlinks {
  display: flex;
  flex-wrap: wrap;
  gap: .9rem 2rem;
  margin-block-start: clamp(2rem, 4vw, 3rem);
}
.backlinks .tlink {
  /* the 2.75rem floor this used to set by hand is now --tap, in §2 */
  align-items: flex-end;
}

/* the breadcrumb, reused off the project hero where there is no photograph
   under it and therefore nothing for the shadow to rescue — and, since the band
   went light, set in the band's dark type rather than the photo family */
.crumbs { text-shadow: none; }
.phero__crumbs.crumbs { color: var(--on-ink); }
.phero__crumbs.crumbs a:hover { color: var(--bronze-dk); }

/* --- 21. unverified availability -----------------------------------------
   The stacking plan above is baked by the build, which means every status,
   price, tally and date in it is true as of the last deploy and not one minute
   later. A flat sold last week still reads פנוי there. The owner's rule is
   blunt: on failure, show nothing rather than something wrong.

   So availability is treated as a CLAIM THE RUNTIME HAS TO CONFIRM — and, since
   the owner settled it explicitly, the claim is WITHHELD BY DEFAULT and revealed
   on confirmation, not stated by default and withdrawn on failure. The
   difference is the fetch window: 73 ms on wifi, 5.8 s on a slow 3G, ten full
   seconds if /api/config never answers. Stating a baked status for those seconds
   means a flat sold since the last deploy reads פנוי to whoever is looking, and
   "briefly wrong" is the thing this whole mechanism exists to prevent. The build
   therefore bakes `is-pending` onto the guard and site.js takes it off.

   Three gates now:

     html:not(.js)          JavaScript never ran. The inline script in <head>
                            adds `js` before the body is parsed, so a visitor
                            who has it never sees this state flash. Its rules are
                            untouched by the neutral-first change — a page with
                            no script has no reveal to wait for, so it collapses
                            the withdrawn boxes outright rather than holding
                            room for something that is never coming.
     .invguard.is-pending   JavaScript ran and has not yet been given a config
                            it believes. THE BAKED STATE. Same claims withdrawn,
                            but the geometry is the geometry of the confirmed
                            page: everything that the two states above `display:
                            none` is `visibility: hidden` here instead, so the
                            reveal changes colour and text and moves nothing.
                            Scoped to `html.js` so it can never apply to the
                            no-script page, which carries the class too.
     .invguard.is-stale     JavaScript ran and could not get a config it
                            believes: fetch failed, timed out, was not JSON, had
                            no inventory, had no buildings, failed the shared
                            validator, or the renderer threw. site.js swaps
                            `is-pending` for this and unhides the notice; that
                            is a real event with a real explanation attached, so
                            it is allowed to collapse the boxes it withdraws.

   What goes: the status word, the status colour coding, the per-flat spec rows
   (site.js builds those), the פנויות/בהליך/נמכרו counters, the per-building and
   per-floor tallies, the "פנויות בלבד" filter and the "עודכן ב־" date — every
   one of them is the same availability claim in another form. What stays: the
   structure. Which buildings, how many floors, how many flats, what size — none
   of that goes stale in a way that costs a buyer anything.

   The status TEXT stays in the markup for crawlers and is only hidden; the
   neutral `.unit__check` twin next to it is what shows instead. The accessible
   name needs neither, because both renderers already bake the neutral word into
   `aria-label` and site.js writes the real status in only on confirmation — CSS
   cannot reach an attribute, and a screen-reader user must not be the one person
   still told last month's truth. */

.invguard { display: grid; gap: clamp(1.5rem, 3vw, 2.25rem); }

.invguard__notice {
  padding: 1rem 1.15rem;
  border: 1px solid var(--bronze);
  border-inline-start-width: 3px;
  background: rgba(192, 142, 95, .1);
  color: var(--on-ink);
  font-size: var(--fs-sm);
  font-weight: 300;
  line-height: 1.6;
}
.invguard__notice a { color: var(--bronze-dk); text-decoration: underline; text-underline-offset: .2em; }

/* the neutral twin of the status, one per flat, shown only when unverified.
   Same font-size and same line box as .unit__status, and .unit's min-height is
   4.25rem against ~3.2rem of content, so swapping one for the other cannot
   change the height of a cell even if לבירור wraps where פנוי did not. */
.unit__check { display: none; font-size: .6875rem; color: var(--on-ink-mute); }

html:not(.js) .unit__status,
.invguard.is-stale .unit__status { display: none; }
html:not(.js) .unit__check,
.invguard.is-stale .unit__check { display: block; }

/* no colour-coded status either: the swatches ARE the claim */
html:not(.js) .unit[data-status]:not([aria-pressed="true"]),
.invguard.is-stale .unit[data-status]:not([aria-pressed="true"]) {
  background: rgba(255, 255, 255, .72);
  border: 1px solid var(--line-ink);
  color: var(--on-ink);
}
html:not(.js) .unit[data-status] .unit__no,
.invguard.is-stale .unit[data-status] .unit__no { color: var(--on-ink-mute); }
html:not(.js) .unit[data-status] .unit__rooms,
.invguard.is-stale .unit[data-status] .unit__rooms { font-weight: 500; }

/* every aggregate of the same claim */
html:not(.js) .invsum,
.invguard.is-stale .invsum { grid-template-columns: 1fr; }
html:not(.js) .invsum__cell + .invsum__cell,
.invguard.is-stale .invsum__cell + .invsum__cell { display: none; }   /* keeps only "דירות בפרויקט" */
html:not(.js) .bstack__count,
html:not(.js) .floor__tally,
html:not(.js) .stack__breakdown [data-bd],
html:not(.js) .inv__toggle,
html:not(.js) .inv__updated,
html:not(.js) .detail--rest .legend,
.invguard.is-stale .bstack__count,
.invguard.is-stale .floor__tally,
.invguard.is-stale .stack__breakdown [data-bd],
.invguard.is-stale .inv__toggle,
.invguard.is-stale .inv__updated,
.invguard.is-stale .detail--rest .legend { display: none; }

/* The invitation on the project page states the same claim in another form, so
   it is withdrawn by the same two gates. What stays is the structure it also
   states — how many flats, how many buildings, how many of each room type —
   none of which depends on a single sale. */
html:not(.js) .invite__num[data-total="available"],
html:not(.js) .imix__free,
.invguard.is-stale .invite__num[data-total="available"],
.invguard.is-stale .imix__free { display: none; }
html:not(.js) .invite__check,
.invguard.is-stale .invite__check { display: block; }
html:not(.js) .imix__check,
.invguard.is-stale .imix__check { display: inline; }

/* the tally column would otherwise hold an empty 2.75rem gutter open */
html:not(.js) .floor,
.invguard.is-stale .floor {
  grid-template-columns: 5.25rem minmax(0, 1fr);
  grid-template-areas: "label units";
}
@media (max-width: 44rem) {
  html:not(.js) .floor,
  .invguard.is-stale .floor {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "label" "units";
  }
}

/* --- the pending state ----------------------------------------------------
   Everything above withdraws a claim by taking its box away, which is right
   when nothing is coming back. Here something is: this state lasts from first
   paint until site.js has a live config, and the visitor must not have the page
   move under a reading thumb the moment it arrives. So the same claims are
   withdrawn — the status word, the colour coding, the counters, the tallies,
   the filter, the date, the free counts — but every box stays exactly where the
   confirmed page will put it. `visibility: hidden` also takes the element out of
   the accessibility tree, so a screen reader is told no more than a sighted
   visitor is; the aria-labels themselves already end in the neutral word, and
   site.js only writes a real status into them at the same moment it lifts this
   class.

   `html.js` is load-bearing. The guard carries `is-pending` in the BAKED html,
   which is also the html a visitor with no JavaScript gets, and for them the
   reveal never comes — so their rules are the ones above, unchanged. */
html.js .invguard.is-pending .unit__status { display: none; }
html.js .invguard.is-pending .unit__check { display: block; }

html.js .invguard.is-pending .unit[data-status]:not([aria-pressed="true"]) {
  background: rgba(255, 255, 255, .72);
  border: 1px solid var(--line-ink);
  color: var(--on-ink);
}
html.js .invguard.is-pending .unit[data-status] .unit__no { color: var(--on-ink-mute); }
html.js .invguard.is-pending .unit[data-status] .unit__rooms { font-weight: 500; }

/* The aggregates keep their boxes and lose their contents. The summary strip
   loses only its NUMBERS and keeps "פנויות / בהליך / נמכרו" underneath them:
   blanking the four cells outright left a 500 px empty rectangle at the top of
   the plan for the length of the fetch, which reads as a page that has broken
   rather than one that is checking. The labels are structure — the project has
   those three categories whatever today's numbers are — and only the counts are
   the claim. */
html.js .invguard.is-pending .invsum__cell + .invsum__cell .invsum__num,
html.js .invguard.is-pending .bstack__count,
html.js .invguard.is-pending .floor__tally,
html.js .invguard.is-pending .stack__breakdown [data-bd],
html.js .invguard.is-pending .inv__toggle,
html.js .invguard.is-pending .inv__updated,
html.js .invguard.is-pending .detail--rest .legend { visibility: hidden; }

/* the invitation's free count and its mix tallies, same swap as above */
html.js .invguard.is-pending .invite__num[data-total="available"] { display: none; }
/* Not `display: block` as the other two gates use, but a grid box the exact
   height of the .invite__num it stands in for — that number's font-size at
   line-height 1. Otherwise the smaller twin lets the label under it ride 4-5 px
   up, and the reveal moves it back down. A line-height in em cannot do this: the
   two clamps have different floors, slopes and ceilings, so no single ratio
   holds at every viewport width. The other two gates keep `block` on purpose —
   for them there is no reveal to line anything up with, and the no-script page
   must render exactly as it did before any of this. */
html.js .invguard.is-pending .invite__check {
  display: grid;
  place-content: center;
  min-height: clamp(1.9rem, 3.5vw, 2.7rem);
}
/* The mix row is `flex-wrap`, which makes a display swap here a WRAPPING
   decision and not just a width: at 320 px the real count ("62 פנויות") needs a
   second line where the neutral word does not, so hiding one for the other made
   every row 59 px in the pending state and 91 px after the reveal — 131 px of
   the page jumping at the exact moment the visitor was told something. So the
   count keeps its box, invisible, and the neutral word is drawn over it out of
   flow. The row therefore wraps in the pending state exactly as it will wrap in
   the confirmed one. `.imix` is only made a containing block while this class is
   on, so nothing about the settled row changes. */
html.js .invguard.is-pending .imix { position: relative; }
html.js .invguard.is-pending .imix__free { visibility: hidden; }
html.js .invguard.is-pending .imix__check {
  display: flex;
  align-items: center;
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
}

/* --- 22. contrast over the mobile image crops -----------------------------
   The hero renders are 1920x1080. `object-fit: cover` fits one into a 390-wide,
   ~800-tall box by scaling it to 1422px and keeping the middle 27% — and on all
   of these renders the middle is the sky, the brightest band in the frame. The
   desktop crop keeps the dark tower and the ground, which is why the SAME tokens
   measure 7.13:1 on the hero lede at 1440 and 2.60:1 at 390, and why the project
   breadcrumb measures 1.9:1 on a phone. check.py cannot see any of this: it
   reads token pairs and translucent surfaces out of this stylesheet, and there
   is no stylesheet colour behind a photograph.

   Two moves, and the order is deliberate. First lift the copy off the muted
   tokens — --on-photo is 1.62x brighter than --on-photo-mute, which is most of the
   gap on its own and costs the image nothing. Only then deepen the scrim, and
   only in the band where the copy actually sits (measured: the lede at 60-67%
   of the hero's height, the headline at 72-79%, the breadcrumb near the foot of
   the project hero). The render still reads as a dusk photograph — this is the
   same gradient, retuned for a crop it was never measured against, not a panel
   dropped on top of it.

   Verified by sampling the composited pixels behind each run with the glyphs
   made transparent, at 360 and 390, the way the audit measured it. */
@media (max-width: 56rem) {
  /* Measured against the copy, not eyeballed: on the mobile crop the lede sits
     between 47% and 59% of the hero's height, the headline between 63% and 74%
     and the label rule between 82% and 89%. The stops are placed at those bands
     and set to the lightest value that still clears AA on the brightest pixel
     each one covers, so the render keeps as much of itself as the contract
     allows. */
  .hero__scrim {
    background:
      linear-gradient(to top,
        rgba(17,23,30,.97) 0%,
        rgba(17,23,30,.82) 32%,
        rgba(17,23,30,.66) 58%,
        rgba(17,23,30,.56) 76%,
        rgba(17,23,30,.74) 100%),
      linear-gradient(to left, rgba(17,23,30,.62), rgba(17,23,30,0) 68%);
  }
  /* 17px normal-weight text owes 4.5:1, and it is the one paragraph that says
     who the company is and where it builds */
  .hero__lede { color: var(--on-photo); }
  /* 11px was the smallest type on the site and it sat over photography, where
     11px owes 4.5:1 and measured 3.0:1 at worst. Back to the 12px the rest of
     the site uses for a label, and off the muted token. (The same rule still
     hides the Hebrew half of the strip at this width; that is a composition
     question for the client, not a contrast one.) */
  .hero__meta { font-size: var(--fs-micro); color: var(--on-photo); }

  /* same method: the breadcrumb sits at 57–60% of the project hero's height and
     the title block at 30–35%, so those two stops move and the top of the frame
     is left almost alone (.72 → .66 above 70% is a LIGHTENING). rimon and
     mitzpor already measured 7:1 on the breadcrumb and must not be flattened for
     a problem they do not have. */
  .phero__media::after {
    background: linear-gradient(to top,
      rgba(17,23,30,.96) 2%, rgba(17,23,30,.86) 38%,
      rgba(17,23,30,.66) 70%, rgba(17,23,30,.84) 100%);
  }
}

/* The breadcrumb is 12px — normal-size text, so it owes 4.5:1, not 3:1 — and it
   is the wayfinding on the page a buyer lands on from an advert. It measured
   1.9:1 on a phone and 2.9:1 on the desktop crop, so this one is not a mobile
   correction: the muted token was never right behind a photograph at that size. */
.phero__crumbs { color: var(--on-photo); }
.phero__crumbs a:hover { color: var(--bronze-lt); }

/* --- 23. תפריט נגישות — the accessibility menu ----------------------------
   Israeli law (תקנות שוויון זכויות לאנשים עם מוגבלות (התאמות נגישות לשירות),
   תשע"ג-2013, reg. 35א) obliges this site to meet תקן ישראלי 5568 at level AA.
   It does NOT oblige a floating widget — but every Israeli site carries one,
   its absence reads as neglect, and the controls below are genuinely useful.

   What this is not: an accessibility overlay. Nothing here injects ARIA,
   guesses at alt text, reads the page aloud, or offers a "blind mode". Those
   assert meaning nobody authored, which is worse than silence, because
   assistive technology cannot tell a fabricated semantic from a real one. Every
   control below changes this stylesheet's own custom properties and says
   exactly what it does.

   The whole widget is BUILT BY site.js, not by build.py. Three reasons: the
   built HTML stays byte-identical so none of check.py's structure, copy or
   data-copy checks move; render.js must stay free of `document`/`window`
   (check.py greps it) and this needs both; and if site.js never arrives there
   is no dead button in the corner — the footer's link to the accessibility
   statement is the no-script path, which is the house rule about degrading to
   working HTML. The one thing build.py does own is applying the SAVED
   preference before first paint, in the inline <head> script, so a visitor who
   chose large text does not watch the page paint small and then jump. */

/* --- 23a. the state, applied to <html> --- */

/* Text size. Everything on this site is sized in rem — 526 rem declarations
   against 178 px ones — so scaling the root scales the type and the rhythm
   together. Capped at 1.5, and the cap is measured — worst-page horizontal
   overflow, four pages x four widths:

       width   1.0  1.125  1.25  1.5   1.75  2.0
       320      0     0     0     0     50   101
       360      0     0     0     0     10    61
       390      0     0     0     0      0    31
       414      0     0     0     0      0     7

   Zero everywhere through 1.5, and the first breakage is 1.75 on a 320px
   phone. Raise the cap and you ship a horizontal scrollbar to the narrowest
   handset, which is not a trade worth making for one more step.

   Two things had to be fixed before 1.5 was actually safe, and both were found
   only by measuring at 320 and 360 rather than at 390:
   * the call bar's label wraps to two lines and the bar grows 62 -> 137px. It
     is `--callbar-h` that the page's bottom reserve and the accessibility
     trigger are anchored to, and that token could not know — so site.js
     measures the bar and republishes the token. Without it the last 44px of
     every page sat behind the bar and the trigger itself was 20px underneath
     it, which meant a visitor who turned the text up could not turn it back.
   * `.invite__counts` and `.imix__label` had min-content floors (`1fr` and a
     `7rem` flex-basis), which forced 12px of horizontal overflow at 320px. */
html { font-size: calc(100% * var(--a11y-scale, 1)); }

[data-a11y-space="on"] body { line-height: 2.05; word-spacing: .16em; }
[data-a11y-space="on"] p,
[data-a11y-space="on"] li { margin-block-end: .35em; }
/* NOT letter-spacing. The usual 1.4.12 recipe adds it; Hebrew has no case and
   no serifs to separate letters, so tracking makes it harder to read, which is
   why §19 exists to strip it. */

/* Readable font: the display serif becomes the body sans everywhere. Not called
   a "dyslexia font" — that claim is not evidenced. */
[data-a11y-font="readable"] { --display: var(--body); }
[data-a11y-font="readable"] h1,
[data-a11y-font="readable"] h2,
[data-a11y-font="readable"] h3 { font-weight: 600; letter-spacing: 0; }

/* Underline and box every link, including the ones that opted out in §2 because
   they carry their own shape. */
[data-a11y-links="on"] a {
  text-decoration: underline !important;
  text-decoration-thickness: 2px !important;
  text-underline-offset: .22em;
}
[data-a11y-links="on"] .btn,
[data-a11y-links="on"] .callbar a { text-decoration: none !important; outline: 2px solid currentColor; outline-offset: 2px; }

/* Always-on focus rings, for anyone who navigates by keyboard and does not want
   to depend on the browser's heuristic for when to show them. */
[data-a11y-focus="on"] a:focus,
[data-a11y-focus="on"] button:focus,
[data-a11y-focus="on"] input:focus,
[data-a11y-focus="on"] select:focus,
[data-a11y-focus="on"] textarea:focus,
[data-a11y-focus="on"] [tabindex]:focus {
  outline: 3px solid var(--bronze) !important;
  outline-offset: 3px !important;
  box-shadow: 0 0 0 3px var(--on-ink) !important;
}

/* Stop animations. §15's block is `@media (prefers-reduced-motion: reduce)`,
   which a class cannot trigger, so this restates it as a sledgehammer. site.js
   reads the same attribute for the three places it drives motion from script. */
[data-a11y-motion="off"] *,
[data-a11y-motion="off"] *::before,
[data-a11y-motion="off"] *::after {
  animation-duration: .001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
}
[data-a11y-motion="off"] { scroll-behavior: auto !important; }

/* High contrast. A token swap, not a filter: `filter` on <body> makes it the
   containing block for fixed descendants and strands the header 1200px up the
   page (measured). Everything here goes through :root's own properties, so the
   whole design system follows without a single component knowing about it.
   Since palette A (2026-09-27) it is a LIGHT theme, like the site it sharpens:
   every surface white, every word black, bronze text a deep brown, every edge
   solid. Over a photograph it goes the other way — near-black scrim, white
   words — because that is the one place the default is dark too. */
[data-a11y-contrast="high"] {
  --ink:          #ffffff;
  --ink-2:        #ffffff;
  --ink-3:        #ffffff;
  --on-ink:       #000000;
  --on-ink-mute:  #000000;
  --on-ink-faint: #000000;
  --limestone:    #ffffff;
  --limestone-2:  #ffffff;
  --on-paper:     #000000;
  --on-paper-mute:#000000;
  --bronze-dk:    #6b3f16;
  --on-photo:     #ffffff;
  --on-photo-mute:#ffffff;
  --line-ink:     rgba(0, 0, 0, .85);
  --line-ink-2:   rgba(0, 0, 0, .65);
  --line-paper:   rgba(0, 0, 0, .85);
  --line-paper-2: rgba(0, 0, 0, .65);
  --line-bronze:  rgba(107, 63, 22, .9);
  --line-photo:   rgba(255, 255, 255, .85);
  --line-photo-2: rgba(255, 255, 255, .65);
  --edge-ink:     #000000;
  --edge-paper:   #000000;
  --edge-photo:   #ffffff;
  --placeholder:  #4a4f55;
  --error:        #8f2d1e;
}
/* The photo scrims are frozen near-black by design (§1, §22). In high contrast
   the photograph has to give way to the words on top of it. */
[data-a11y-contrast="high"] .hero__scrim,
[data-a11y-contrast="high"] .phero__media::after {
  background: rgba(0, 0, 0, .9);
}
/* the feature band is a light surface with a pale render in it: the render gives
   way to the band instead */
[data-a11y-contrast="high"] .feature__bg::after { background: rgba(255, 255, 255, .94); }
[data-a11y-contrast="high"] .titleblock,
[data-a11y-contrast="high"] .pcard__chip { background: rgba(0, 0, 0, .92); }
/* the header's and call bar's translucent band, made the theme's solid white */
[data-a11y-contrast="high"] .hdr.is-stuck,
[data-a11y-contrast="high"] .hdr.hdr--solid,
[data-a11y-contrast="high"] .callbar { background: #ffffff; }
[data-a11y-contrast="high"] .a11y__btn { border-color: #000000; box-shadow: 0 0 0 2px #ffffff; }
[data-a11y-contrast="high"] .hero h1, [data-a11y-contrast="high"] .hero__lede,
[data-a11y-contrast="high"] .hero__meta, [data-a11y-contrast="high"] .phero h1,
[data-a11y-contrast="high"] .phero__where,
[data-a11y-contrast="high"] .phero__crumbs { text-shadow: none; }

/* --- 23b. the trigger --- */

.a11y {
  /* Hidden until site.js announces itself, exactly like [data-reveal] in §15 —
     the panel is script-driven, so a script that never arrives leaves no
     control behind rather than a button that does nothing. */
  display: none;
  position: fixed;
  z-index: 95;   /* over the call bar (90); under the nav veil (99), the header
                    (100), the skip link (200) and the lightbox (300), all of
                    which SHOULD cover it */
  inset-inline-end: 1rem;
  inset-block-end: 1rem;
}
.js-live .a11y { display: block; }
body:has(.detail-veil.is-on) .a11y { display: none; }

.a11y__btn {
  width: var(--tap); height: var(--tap);
  display: grid; place-items: center;
  background: var(--bronze);
  color: var(--on-bronze);
  border: 2px solid var(--on-bronze);
  box-shadow: 0 0 0 2px rgba(233, 230, 224, .92), var(--shadow);
  transition: background .3s var(--ease), transform .3s var(--ease);
}
.a11y__btn:hover { background: var(--bronze-lt); border-color: var(--bronze-lt); transform: translateY(-2px); }
.a11y__btn svg { width: 1.5rem; height: 1.5rem; }

/* --- 23c. the panel --- */

.a11y__panel {
  position: fixed;
  z-index: 400;   /* a settings panel a visitor opened stays on top of whatever
                     they opened it over, including the drawer and the lightbox */
  inset-inline-end: 1rem;
  /* How far the panel sits above the bottom of the screen, published once
     because the max-height has to subtract exactly the same distance. Both are
     rem-based, so they scale together when the menu's own text-size control
     raises the root font-size — an earlier `min(34rem, 100dvh - 7rem)` did not,
     and at 150% the panel's top edge measured -33px, i.e. off the screen. */
  --a11y-lift: calc(var(--tap) + 1.75rem);
  inset-block-end: var(--a11y-lift);
  width: min(21rem, calc(100vw - 2rem));
  max-height: min(34rem, calc(100dvh - var(--a11y-lift) - 2rem));
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--bronze) transparent;   /* the cut read as clipped, not scrollable */
  padding: 1.25rem;
  background: var(--limestone);
  color: var(--on-paper);
  border: 2px solid var(--edge-paper);
  box-shadow: var(--shadow-lift);
}
.a11y__panel[hidden] { display: none; }

.a11y__head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  margin-block-end: 1rem;
}
.a11y__title { font-family: var(--display); font-size: 1.25rem; font-weight: 500; }
.a11y__x {
  width: var(--tap); height: var(--tap);
  flex: none;
  display: grid; place-items: center;
  margin: -.5rem -.5rem -.5rem 0;
  border: 2px solid var(--edge-paper);
}
.a11y__x:hover { background: var(--on-paper); color: var(--limestone); border-color: var(--on-paper); }
.a11y__x svg { width: 1rem; height: 1rem; }

.a11y__group { margin-block-end: 1rem; }
.a11y__label {
  display: block;
  margin-block-end: .5rem;
  font-family: var(--body);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: .02em;
}
.a11y__size { display: flex; align-items: center; gap: .5rem; }
.a11y__step {
  width: var(--tap); height: var(--tap);
  flex: none;
  display: grid; place-items: center;
  font-size: 1.35rem; font-weight: 600; line-height: 1;
  border: 2px solid var(--edge-paper);
}
.a11y__step:hover:not(:disabled) { background: var(--on-paper); color: var(--limestone); border-color: var(--on-paper); }
.a11y__step:disabled { opacity: .4; cursor: not-allowed; }
.a11y__pct {
  flex: 1;
  min-block-size: var(--tap);
  display: grid; place-items: center;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  border: 2px solid var(--edge-paper);
}

.a11y__list { display: grid; gap: .4rem; }
.a11y__opt {
  display: flex; align-items: center; gap: .7rem;
  min-block-size: var(--tap);
  padding: .5rem .75rem;
  text-align: start;
  font-size: var(--fs-sm);
  border: 2px solid var(--edge-paper);
  transition: background .25s var(--ease), color .25s var(--ease);
}
.a11y__opt:hover { background: rgba(21, 28, 36, .06); }
/* The state is carried by aria-pressed, and the tick is generated from it, so
   the visual and the accessibility tree can never disagree. Never colour alone:
   the box fills AND a check mark appears. */
.a11y__box {
  flex: none;
  width: 1.4rem; height: 1.4rem;
  display: grid; place-items: center;
  border: 2px solid var(--edge-paper);
  font-size: .9rem; font-weight: 700; line-height: 1;
}
.a11y__opt[aria-pressed="true"] { background: var(--bronze); border-color: var(--on-bronze); color: var(--on-bronze); }
.a11y__opt[aria-pressed="true"] .a11y__box { background: var(--on-bronze); color: var(--bronze); border-color: var(--on-bronze); }
.a11y__opt[aria-pressed="true"] .a11y__box::after { content: "\2713"; }

.a11y__foot {
  margin-block-start: 1rem;
  padding-block-start: 1rem;
  border-block-start: 1px solid var(--line-paper);
  display: grid; gap: .6rem;
}
.a11y__reset {
  min-block-size: var(--tap);
  padding: .5rem 1rem;
  font-size: var(--fs-sm); font-weight: 600;
  border: 2px solid var(--edge-paper);
}
.a11y__reset:hover { background: var(--on-paper); color: var(--limestone); border-color: var(--on-paper); }
/* inline-block plus padding, so the link to the statement is a 44px target like
   everything else in the panel rather than a 27px line of text. */
.a11y__link {
  display: inline-block;
  padding-block: .55rem;
  font-size: var(--fs-sm);
  color: var(--bronze-dk);
}
.a11y__note { font-size: var(--fs-micro); color: var(--on-paper-mute); line-height: 1.6; }

/* On a phone the call bar owns the bottom of the screen, and on a notched
   handset the home-indicator band owns 34px under it. Consumes --callbar-h, the
   same token the bar's own min-height and body's bottom padding consume, so the
   three cannot drift apart. */
@media (max-width: 60rem) {
  .a11y { inset-block-end: calc(var(--callbar-h) + env(safe-area-inset-bottom, 0px) + 1rem); }
  .a11y__panel { --a11y-lift: calc(var(--callbar-h) + env(safe-area-inset-bottom, 0px) + var(--tap) + 1.75rem); }
}
/* On a very short viewport — 320x256 is 1280x1024 at 400% zoom, the case an
   IS 5568 audit actually runs — the lift is taller than the screen, so
   `100dvh - lift` went negative and the panel's own padding pushed its heading
   and close button off the top. Anchoring to the top instead means it can
   always be read and scrolled, which is the whole requirement. */
@media (max-height: 30rem) {
  .a11y__panel {
    inset-block-start: 1rem;
    inset-block-end: auto;
    max-height: calc(100dvh - 2rem);
  }
}
