/* ===========================================================================
   Noya Hazan - styles
   Transcribed from the Claude Design canvas "Noya Hazan Site.dc.html" and its
   design system (noya-hazan-photography-design-system-028cb06e). DESIGN.md is
   binding; the canvas is binding over DESIGN.md.

   Font sizes are rem, not the canvas's px, so the accessibility toolbar's
   text-size control actually works. 1rem = 16px, so every value below is the
   canvas value divided by 16 and renders identically at default zoom.
   =========================================================================== */

/* ---- tokens: colors (verbatim from tokens/colors.css, except the two below) ---- */
:root {
  /* Lightened on the client's instruction, 2026-08-22: the canvas's #DEDBD3 read
     as gray rather than cream. Same hue (~43deg) and gap between the two, just
     lifted - see DESIGN.md > Palette for the exact before/after and the
     contrast recheck. */
  --color-cream: #F3F1EC;
  --color-cream-deep: #E8E3D9;
  --color-white: #FFFFFF;
  --color-charcoal: #2A2926;
  --color-charcoal-soft: #5B5850;
  --color-terracotta: #A8583A;
  --color-terracotta-light: #C98863;
  --color-terracotta-dark: #7C3F27;
  --color-border: #C4BFB4;
  --color-border-on-white: #DEDAD1;
  --color-placeholder: #B4AC9E;
  --color-placeholder-dark: #8B8375;

  --surface-page: var(--color-cream);
  --surface-card: var(--color-white);
  --surface-placeholder: var(--color-placeholder);
  --text-primary: var(--color-charcoal);
  /* #5B5850 is 4.48:1 on the cream-deep wells and hatches, two hundredths under
     AA. Darkened just enough to clear it on both surfaces (5.81 / 5.07). */
  --text-secondary: #545046;
  --text-on-accent: var(--color-white);
  --accent-primary: var(--color-terracotta);
  --accent-primary-hover: var(--color-terracotta-light);
  --accent-primary-active: var(--color-terracotta-dark);
  --border-default: var(--color-border);
  --border-on-white: var(--color-border-on-white);

  /* Accessibility override, not from the design system. #A8583A is 3.68:1 on the
     cream page: it clears the 3:1 bar for borders and other non-text UI, where it
     is still used, but fails WCAG AA for text. Accent-coloured TEXT therefore uses
     the design system's own dark terracotta (5.84:1), and hover goes DARKER, not
     lighter: --accent-primary-hover is 2.11:1 on cream and is unusable for text. */
  --accent-text: var(--color-terracotta-dark);
  --accent-text-hover: #5F2F1D;

  /* Added here, not in the canvas: the canvas hardcoded these as rgba(), and a
     hardcoded rgba() cannot be re-themed by the accessibility colour themes.
     Same limitation applies to the cream lightening below: these three carry
     --color-cream's RGB as literal numbers, so they do not follow it the way
     --on-scrim does. Keep them in sync by hand if the cream ever moves again. */
  --surface-page-blur: rgba(243, 241, 236, 0.92);
  --scrim-strong: rgba(42, 41, 38, 0.97);
  --on-scrim: var(--color-cream);
  --on-scrim-soft: rgba(243, 241, 236, 0.75);
  --on-scrim-border: rgba(243, 241, 236, 0.4);
}

/* ---- tokens: spacing (verbatim from tokens/spacing.css) ---- */
:root {
  --space-1: 4px;  --space-2: 8px;   --space-3: 12px; --space-4: 16px;
  --space-5: 24px; --space-6: 32px;  --space-7: 48px; --space-8: 64px;
  --space-9: 96px; --space-10: 128px;
}

/* ---- tokens: shape and motion (verbatim from tokens/effects.css) ---- */
:root {
  --radius-none: 0px;
  --transition-fast: 150ms ease;
  --transition-base: 220ms ease;
  --ease-photo: cubic-bezier(.2, .7, .2, 1);
  --a11y-sticky-h: 71px;
}

/* ---- base ---- */
* { box-sizing: border-box; }

html {
  color-scheme: light;   /* light only by design; dark is offered via the a11y toolbar */
  -webkit-text-size-adjust: 100%;
}

html, body { margin: 0; padding: 0; background: var(--surface-page); }

body {
  font-family: Jost, Rubik, Helvetica, Arial, sans-serif;
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
  font-size: 1rem;
}

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

a { color: var(--accent-text); text-decoration: none; transition: color var(--transition-fast); }
a:hover { color: var(--accent-text-hover); }
a:active { color: var(--accent-text-hover); }

/* The browser's default selection blue was the one colour on this page that
   the palette did not choose. Charcoal ground, cream ink - both already
   tokens, both re-themed by the a11y colour themes along with everything
   else, so a highlight never lands on an untested pairing. Photographs are
   excluded: a wash over an image is not a reading affordance, and dragging
   across the mosaic used to paint every tile. */
::selection { background: var(--text-primary); color: var(--surface-page); }
img::selection { background: transparent; }

:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 3px;
}

#main:focus { outline: none; }

h1, h2 { font-weight: 900; letter-spacing: 0; }

.is-placeholder { color: var(--border-default); }

/* ---- header ---- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  padding: 18px var(--space-6);
  background: var(--surface-page-blur);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border-default);
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 1.1875rem;
  font-weight: 900;
  letter-spacing: 0;
  color: var(--text-primary);
}
.brand:hover { color: var(--accent-text); }

/* The logo: a simple three-blade aperture, picked from ten concepts shown
   to the client against a live header mock (2026-08-26) - option 5. Built
   from three rotated divs rather than an SVG path; --accent-primary is
   correct here even though it's banned for text (see the accessibility
   override above) because this is non-text UI, exactly the case that
   override carves out. */
.brand-mark {
  position: relative;
  width: 24px;
  height: 24px;
  flex: none;
}
.brand-mark i {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 2px;
  height: 10px;
  background: var(--accent-primary);
  border-radius: 1px;
  transform-origin: 50% 12px;
  transition: background-color var(--transition-fast);
}
.brand-mark i:nth-child(1) { transform: rotate(0deg); }
.brand-mark i:nth-child(2) { transform: rotate(60deg); }
.brand-mark i:nth-child(3) { transform: rotate(120deg); }
.brand:hover .brand-mark i { background: var(--accent-text); }

.site-nav {
  display: flex;
  align-items: baseline;
  gap: 26px;
  font-size: 0.8125rem;
  font-weight: 600;
}
.site-nav a { color: var(--text-primary); }
.site-nav a:hover { color: var(--accent-text); }
.site-nav a.nav-accent { color: var(--accent-text); }
.site-nav a.nav-accent:hover { color: var(--text-primary); }
/* aria-current is set by the router for screen readers only. The canvas has no
   visible active-nav state, so this deliberately paints nothing. */

/* Instagram's own glyph, not its brand gradient: stroke="currentColor" so it
   is exactly the site's own accent-text/text-primary swap on hover, the same
   as every other nav link. align-self recentres it against the row's own
   align-items: baseline, which would otherwise sit an icon (no text baseline
   of its own) too low next to the Hebrew labels beside it. */
.nav-instagram { align-self: center; }
.nav-instagram svg {
  display: block;
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---- views ---- */
.view[hidden] { display: none; }

/* ---- home: hero ---- */
.hero {
  display: grid;
  /* min() so a 320px viewport cannot be forced into a 360px track and overflow. */
  grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
  gap: 0;
  align-items: stretch;
  min-height: 620px;
  background: var(--surface-page);
  border-bottom: 1px solid var(--border-default);
}

.hero-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-6);
  padding: 56px var(--space-7);
}

.hero-title {
  margin: 0 0 14px;
  font-size: clamp(2.5rem, 7vw, 6rem);
  line-height: 0.98;
}

.hero-lede {
  margin: 0;
  max-width: 340px;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--text-secondary);
}

.stats {
  display: flex;
  gap: 40px;
  flex-wrap: wrap;
  margin: 0;
  padding-top: 28px;
  border-top: 1px solid var(--border-on-white);
}
.stat dt {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--text-secondary);
  letter-spacing: 0.04em;
}
.stat-value {
  margin: 6px 0 0;
  font-size: 1.125rem;
  font-weight: 800;
}

.hero-media {
  position: relative;
  overflow: hidden;
  background: var(--color-cream-deep);
  min-height: 420px;
}

.photo-slot {
  width: 100%;
  height: 100%;
  min-height: inherit;
  display: flex;
  align-items: center;
  justify-content: center;
  background: repeating-linear-gradient(135deg, var(--color-cream-deep) 0 8px, var(--surface-page) 8px 16px);
}
.photo-slot span {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-secondary);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-page);
  border: 1px dashed var(--border-default);
}

/* ---- buttons ---- */
.btn {
  display: inline-block;
  width: fit-content;
  font-size: 0.8125rem;
  font-weight: 700;
  background: none;
  cursor: pointer;
  transition: background-color var(--transition-fast), color var(--transition-fast);
}
.btn:active { transform: translateY(1px); }

.btn-accent {
  padding: 14px 30px;
  border: 1px solid var(--accent-primary);
  color: var(--accent-text);
}
.btn-accent:hover { background: var(--accent-primary); color: var(--text-on-accent); }

.btn-ink {
  padding: 15px 26px;
  border: 1px solid var(--text-primary);
  color: var(--text-primary);
}
.btn-ink:hover { background: var(--text-primary); color: var(--surface-page); }

.btn-row .btn-accent { padding: 15px 26px; }

/* ---- home: section heading ---- */
.section-head { padding: 72px var(--space-6) var(--space-5); }
.section-head-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  border-bottom: 1px solid var(--border-default);
  padding-bottom: 14px;
}
.section-head-row h2 { margin: 0; font-size: 0.875rem; font-weight: 600; }
.section-head-row a { font-size: 0.875rem; font-weight: 600; }

/* ---- gallery covers -------------------------------------------------------
   Justified rows, same maths as the photo mosaic inside a gallery: row height
   = row width / sum(the row's own cover ratios), each cover grows in
   proportion to its own ratio. Noya's chosen covers are not all one shape -
   some landscape, some portrait - and this is what lets every one of them
   show its true frame instead of being cropped into a fixed box. See
   COVER_ROWS / coverAr() in app.js. */
.covers { display: flex; flex-direction: column; gap: var(--space-1); }
.covers-home { padding: 0 var(--space-6); }

.cover-row {
  display: flex;
  gap: var(--space-1);
  aspect-ratio: var(--sum, 2) / 1;
}

.cover {
  position: relative;
  display: block;
  flex: var(--ar, 0.8) 1 0;
  min-width: 0;
  padding: 0;
  border: 0;
  background: var(--text-primary);
  cursor: pointer;
  overflow: hidden;
  text-align: left;
  font: inherit;
  color: inherit;
}
.cover img {
  display: block;
  width: 100%;
  height: 100%;
  /* The gutters between covers sit outside the aspect-ratio sum, so a row
     runs a couple of pixels tall. cover absorbs that; nothing visible is
     cropped - the box already matches the photograph's own true ratio. */
  object-fit: cover;
  opacity: 0.92;
  transition: transform 700ms var(--ease-photo), opacity 400ms ease;
}
.cover:hover img, .cover:focus-visible img { transform: scale(1.04); opacity: 1; }

.cover-label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: var(--space-5);
  background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.6) 100%);
}
.cover-title { font-size: 1.25rem; font-weight: 800; color: var(--surface-page); }
.covers-work .cover-title { font-size: 1.375rem; }
.cover-meta { margin-top: var(--space-1); font-size: 0.75rem; font-weight: 600; color: var(--on-scrim-soft); }

.cover-empty {
  position: relative;
  display: flex;
  flex: var(--ar, 0.8) 1 0;
  min-width: 0;
  flex-direction: column;
  justify-content: flex-end;
  padding: var(--space-5);
  border: 1px dashed var(--border-default);
  background: repeating-linear-gradient(135deg, var(--color-cream-deep) 0 8px, var(--surface-page) 8px 16px);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
}
.cover-empty .cover-title { font-size: 1.25rem; color: var(--text-primary); }
.covers-work .cover-empty .cover-title { font-size: 1.375rem; }
.cover-empty .cover-meta { color: var(--text-secondary); }

/* ---- home: statement ---- */
.statement {
  display: grid;
  grid-template-columns: 1fr;
  gap: 40px;
  padding: 88px var(--space-6) var(--space-9);
  border-top: 1px solid var(--border-default);
  margin-top: 88px;
}
.statement p {
  margin: 0;
  max-width: 900px;
  font-size: clamp(1.375rem, 3.4vw, 2.5rem);
  line-height: 1.3;
  font-weight: 500;
  letter-spacing: 0;
  text-wrap: pretty;
}
.btn-row { display: flex; gap: 14px; flex-wrap: wrap; }

/* ---- work + gallery views ---- */
[data-view="work"], [data-view="gallery"] { padding: 56px var(--space-6) var(--space-9); }

/* text-wrap: balance on the display type, pretty on the prose. Neither
   changes a size, a measure or a line-height - they only decide where an
   already-fixed line breaks, which is the one typographic lever this
   transcription never had. It matters most on Hebrew display weight 900,
   where a one-word last line reads as a mistake rather than a rag. A
   browser without support ignores both and gets exactly today's page. */
.page-title {
  margin: 0 0 var(--space-2);
  font-size: clamp(2.375rem, 7.5vw, 6.25rem);
  line-height: 1;
  text-wrap: balance;
}
.page-lede { margin: 0 0 44px; font-size: 0.9375rem; color: var(--text-secondary); text-wrap: pretty; }

.back-link { display: inline-block; margin-bottom: 26px; font-size: 0.8125rem; font-weight: 600; }

.gallery-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border-default);
  padding-bottom: 18px;
  margin-bottom: var(--space-6);
}
[data-view="gallery"] .page-title { margin: 0; font-size: clamp(2.25rem, 7vw, 5.625rem); }
.gallery-meta { font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); }

/* ---- gallery: justified mosaic ----
   Not a square grid. Photographs are laid in rows of two or three, and a row is
   sized by the sum of its photographs' aspect ratios: height = width / sum. Each
   figure then grows in proportion to its own ratio over a zero basis, so the row
   fills the width exactly, every photograph in it lands on one height, and each
   is shown at its true shape. A row of three is shorter than a row of two, which
   is where the rhythm down the page comes from. No captions: the description
   lives in alt. */
.photo-grid {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.photo-row {
  display: flex;
  gap: var(--space-1);
  aspect-ratio: var(--sum, 3) / 1;
}
.photo-row figure {
  margin: 0;
  flex: var(--ar, 1) 1 0;
  min-width: 0;
}
.photo-btn {
  position: relative;   /* the open-affordance overlay below hangs off this */
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: var(--color-cream-deep);
  cursor: zoom-in;
  overflow: hidden;
}
.photo-btn img {
  display: block;
  width: 100%;
  height: 100%;
  /* The 4px gutters are outside the aspect-ratio sum, so a row runs a couple of
     pixels tall. cover absorbs it; nothing visible is lost. */
  object-fit: cover;
  transition: transform 700ms var(--ease-photo);
}
.photo-btn:hover img, .photo-btn:focus-visible img { transform: scale(1.04); }

/* ---- the open affordance ("F", client's pick 2026-09-02) ------------------
   The push-in above says "look at me"; it never said "I open". This adds the
   second half: a soft corner shading with the expand mark on it. Chosen from
   six options against three photographs - a high-key black and white, a busy
   daylight frame and a dark evening one - because it is the only one that
   reads as clearly as a centred mark without covering the faces, which is
   the thing people are actually looking at.

   The mark is deliberately the ordinary two-corner-arrow expand glyph. Nobody
   has to learn it; it is the same one every photo viewer already uses.

   Bottom-RIGHT is physical, and correct here: this document is RTL-only, so
   physical bottom-right IS the inline-start corner, and it is the corner
   furthest from where the lightbox will put its own controls. If an LTR
   overlay ever arrives (DESIGN.md says it will not), this is the line to
   revisit - the same standing exception the lightbox arrows carry.

   Mosaic photographs only. A gallery COVER on /work is deliberately left
   alone: a cover navigates into a gallery, it does not open a photograph,
   and an expand mark would promise the wrong thing. */
.photo-fx {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
}
.photo-fx-wedge {
  position: absolute;
  inset: 0;
  /* Literal first, themeable second: a browser without color-mix keeps the
     literal, everything current re-themes with --text-primary. Charcoal is
     never transitioned - only this element's own opacity is. */
  background: linear-gradient(to top left, rgba(42, 41, 38, .72), rgba(42, 41, 38, 0) 46%);
  background: linear-gradient(to top left, color-mix(in srgb, var(--text-primary) 72%, transparent), transparent 46%);
  opacity: 0;
  /* 220ms literal, not var(--transition-base): that token is "220ms ease" -
     duration AND easing in one - so pairing it with --ease-photo makes a
     shorthand with two timing functions, which is invalid and silently drops
     the whole transition. Shipped that way for one build; caught by reading
     transitionDuration back off the element, which said 0s. */
  transition: opacity 220ms var(--ease-photo);
}
.photo-fx-mark {
  position: absolute;
  bottom: 14px;
  right: 14px;
  display: block;
  width: 22px;
  height: 22px;
  color: var(--on-scrim);
  opacity: 0;
  transform: translate(-5px, 5px);
  transition: opacity 220ms var(--ease-photo),
              transform 220ms var(--ease-photo);
}
.photo-fx-mark svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Hover is gated on a device that has one. Focus is not, and is not optional:
   without this line a keyboard user is the only visitor who never learns the
   photograph opens. */
@media (hover: hover) {
  .photo-btn:hover .photo-fx-wedge { opacity: 1; }
  .photo-btn:hover .photo-fx-mark { opacity: 1; transform: translate(0, 0); }
}
.photo-btn:focus-visible .photo-fx-wedge { opacity: 1; }
.photo-btn:focus-visible .photo-fx-mark { opacity: 1; transform: translate(0, 0); }

/* A phone has no hover at all, and this site's traffic is Instagram
   link-in-bio - so touch is the case that matters most, not the edge case.
   There the mark is simply present, quietly, and never moves. */
@media (hover: none) {
  .photo-fx-wedge { opacity: .85; }
  .photo-fx-mark { opacity: .92; transform: none; }
}

/* Reduced motion, and the toolbar's stop-animations control. Same gate the
   About marquee uses, and pure CSS for the same reason: it holds even with
   JS dead. The mark still APPEARS - it is information, and hiding it would
   withhold the affordance from exactly the people who asked for less motion.
   Only the travel stops. */
@media (prefers-reduced-motion: reduce) {
  .photo-fx-wedge, .photo-fx-mark { transition: none; transform: none; }
}
:root.a11y-motion-off .photo-fx-wedge,
:root.a11y-motion-off .photo-fx-mark { transition: none; transform: none; }

/* Rows arrive as you reach them, photographs inside a row staggered. The class
   is put on by app.js and only when motion is allowed, so with JS or motion off
   the mosaic is simply there. A row already on screen when the gallery mounts
   gets .is-instant: the view transition was its entrance, it does not need a
   second one. */
.photo-grid.is-staggered .photo-row:not(.is-in) figure {
  opacity: 0;
  transform: translateY(18px);
}
.photo-grid.is-staggered figure {
  transition: opacity 520ms ease, transform 520ms var(--ease-photo);
  transition-delay: calc(var(--i, 0) * 70ms);
}
.photo-grid.is-staggered .is-instant figure { transition: none; }

.empty-gallery {
  padding: var(--space-8) var(--space-5);
  border: 1px dashed var(--border-default);
  background: repeating-linear-gradient(135deg, var(--color-cream-deep) 0 8px, var(--surface-page) 8px 16px);
  text-align: center;
}
.empty-gallery p { margin: 0; font-size: 1rem; color: var(--text-secondary); text-wrap: pretty; }

/* ---- about ---- */
/* align-items: center, not start. The portrait runs to its own 88vh and the
   copy beside it is four short lines, so `start` left 449px of empty cream
   under the last sentence at 1280x860 - measured, not eyeballed. Centring
   the copy against the photograph is the only value changed here: the
   column widths, the 4px gutter and the photograph's own height are all
   untouched, and below 700px the grid is one column where align-items does
   nothing at all. Deviation from the canvas, recorded in DESIGN.md; it
   could not be diffed against the canvas in the session that made it
   (DesignSync had no authorization), so check it if the canvas is ever
   opened again. */
.about-hero {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: var(--space-1);
  align-items: center;
}
.about-photo { width: 100%; height: 100%; max-height: 88vh; object-fit: cover; }
.about-copy { padding: var(--space-8) var(--space-6) var(--space-9); }
.view-about .page-title { margin: 0 0 28px; font-size: clamp(2.25rem, 5.5vw, 4.75rem); }
.about-lead {
  margin: 0 0 var(--space-5);
  max-width: 46ch;
  font-size: 1.1875rem;
  line-height: 1.6;
  text-wrap: pretty;
}
.about-lead-soft { margin: 0; color: var(--text-secondary); }

/* ---- about: testimonials, a moving marquee ------------------------------
   Client request, 2026-08-29: "a moving recommendation like batchen's
   website" - so this follows that site's own pattern rather than
   inventing a new one: two rows drifting in opposite directions, each
   track holding its quotes TWICE (the animation travels exactly one
   set-width, so the loop is seamless), the second copy aria-hidden so a
   screen reader doesn't read every quote twice. Radii stay zero and the
   card uses the site's own cream-deep well, same token Contact's info
   grid uses - not batchen's rounded, differently-themed card. Direction
   is forced ltr on the row only, same reason the lightbox arrows stay
   physical: translateX needs a fixed axis, and unicode-bidi: plaintext
   on the quote lets the Hebrew inside still read correctly regardless. */
.about-testimonials { padding: var(--space-8) var(--space-6) var(--space-9); border-top: 1px solid var(--border-default); }
.testimonials-title { margin: 0 0 var(--space-6); font-size: clamp(1.5rem, 3vw, 2rem); font-weight: 800; text-wrap: balance; }

.quotes { display: grid; gap: var(--space-4); margin-inline: calc(-1 * var(--space-6)); }
.quotes-row {
  overflow: hidden;
  direction: ltr;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.quotes-track {
  display: flex;
  width: max-content;
  animation: quote-drift 42s linear infinite;
}
.quotes-row-back .quotes-track { animation-direction: reverse; animation-duration: 52s; }
.quotes-row:hover .quotes-track, .quotes-row:focus-within .quotes-track { animation-play-state: paused; }
@keyframes quote-drift { to { transform: translateX(-50%); } }

/* Rounded on the client's instruction, 2026-08-29 - "too squary". The one
   exception to this site's zero-radius rule (see CLAUDE.md "Do not"):
   scoped to this card only, not applied anywhere else on the site.
   Filled solid with --accent-text (same terracotta as the header's
   Instagram icon, client's instruction, same day) rather than the
   cream-deep well every other card/well on the site uses - text goes
   white on top of it for contrast, not the usual --text-primary. */
.quote {
  flex: 0 0 min(78vw, 26rem);
  margin: 0 var(--space-4) 0 0;
  padding: var(--space-5);
  background: var(--accent-text);
  border: 1px solid var(--accent-text);
  border-radius: 16px;
  unicode-bidi: plaintext;
}
.quote blockquote { margin: 0; font-size: 1rem; line-height: 1.6; color: #FFFFFF; }

@media (max-width: 700px) {
  .about-testimonials { padding: 40px var(--space-4) var(--space-8); }
  .quotes { margin-inline: calc(-1 * var(--space-4)); }
  .quotes-track { animation-duration: 28s; }
  .quotes-row-back .quotes-track { animation-duration: 34s; }
}

/* Same drop-to-static rule the lightbox stagger and the gallery reveal
   already follow: prefers-reduced-motion AND the toolbar's own
   motion-off both stop it dead, not just slow it down. */
@media (prefers-reduced-motion: reduce) {
  .quotes { display: block; margin-inline: 0; }
  .quotes-row { overflow: visible; mask-image: none; -webkit-mask-image: none; direction: inherit; }
  .quotes-track { display: grid; grid-template-columns: repeat(2, 1fr); width: auto; gap: var(--space-4); animation: none; }
  .quotes-row + .quotes-row { margin-top: var(--space-4); }
  .quote-dupe { display: none; }
  .quote { flex: none; margin: 0; }
}
@media (prefers-reduced-motion: reduce) and (max-width: 700px) {
  .quotes-track { grid-template-columns: 1fr; }
}
:root.a11y-motion-off .quotes { display: block; margin-inline: 0; }
:root.a11y-motion-off .quotes-row { overflow: visible; mask-image: none; -webkit-mask-image: none; direction: inherit; }
:root.a11y-motion-off .quotes-track { display: grid; grid-template-columns: repeat(2, 1fr); width: auto; gap: var(--space-4); animation: none; }
:root.a11y-motion-off .quotes-row + .quotes-row { margin-top: var(--space-4); }
:root.a11y-motion-off .quote-dupe { display: none; }
:root.a11y-motion-off .quote { flex: none; margin: 0; }
@media (max-width: 700px) {
  :root.a11y-motion-off .quotes-track { grid-template-columns: 1fr; }
}

/* ---- contact ---- */
.view-contact { padding: var(--space-8) var(--space-6) var(--space-9); }
/* Smaller than the canvas's own clamp (client request, 2026-08-29: the
   heading read too big against the new, longer line). */
.contact-title {
  margin: 0 0 var(--space-5);
  font-size: clamp(2rem, 6vw, 5.5rem);
  line-height: 1.02;
  text-wrap: balance;
}
/* Concept "1 - חצי-חצי" (split two-tone) from the six-direction exploration
   (2026-08-29, client's pick from `noya-contact-concepts.html` in scratch,
   not part of this repo) - replaces the single cream-deep well tried
   earlier the same day. The page splits into two true tones instead of one
   flat field: intro on the page's own cream, contact details on a charcoal
   panel. Both colours are already in the palette - --color-charcoal is the
   same dark the a11y-contrast/invert themes are built from - so this adds
   no new colour to the system. Contained inside the page's own padded
   column, not full-bleed to the viewport edge: full-bleed section
   backgrounds are a Home-only pattern (Home is the one view the canvas does
   not govern), and Contact is canvas-bound like About/Work/Gallery, none of
   which break out of the column. See DESIGN.md.
   --color-charcoal / --color-cream / --color-terracotta-light are used here
   as their fixed values, not the a11y-themeable aliases (--surface-page
   etc) - same reason photographs aren't recoloured by the toolbar's colour
   themes. This panel's own contrast (terracotta-light on charcoal: 4.98:1;
   60%-cream labels on charcoal: 5.63:1 - both AA, checked by hand) holds
   regardless of which toolbar theme is active, rather than drifting to an
   untested combination. */
.contact-split {
  display: flex;
  flex-wrap: wrap;
  max-width: 1000px;
}
.contact-intro {
  flex: 1.1 1 300px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Physical padding, not logical: this page is RTL-only (no overlay), and
     the gap needs to sit on the panel side specifically - .contact-panel is
     the second flex child, which a row in dir="rtl" places to the LEFT of
     this one, so the gap is padding-left, not padding-inline-end. */
  padding: var(--space-6) 0 var(--space-6) var(--space-7);
}
.contact-note {
  margin: var(--space-5) 0 0;
  max-width: 44ch;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--text-secondary);
  text-wrap: pretty;
}
.contact-panel {
  flex: 1 1 300px;
  background: var(--color-charcoal);
  padding: var(--space-7) var(--space-6);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.contact-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: var(--space-6);
}
.contact-label { margin-bottom: var(--space-2); font-size: 0.75rem; font-weight: 600; color: rgba(243, 241, 236, 0.6); }
.contact-value { display: inline-block; font-size: 1.3125rem; font-weight: 600; color: var(--color-terracotta-light); }
/* The first item (email) reads in plain cream, same emphasis break the
   concept mockup made - it's the primary way to reach her, so it gets the
   brighter of the panel's two text colours. */
.contact-grid > div:first-child .contact-value { color: var(--color-cream); }

/* WhatsApp's own brand green, not a site token: this button borrows the
   colour visitors already recognise from the app itself, the same reason
   the sticky button below does. #25D366 is WhatsApp's own icon green. */
.btn-whatsapp {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  width: fit-content;
  margin-top: var(--space-6);
  padding: 15px 28px;
  border: 1px solid #25D366;
  background: #25D366;
  color: #FFFFFF;
  font-size: 0.875rem;
  font-weight: 700;
  cursor: pointer;
  transition: background-color var(--transition-fast), border-color var(--transition-fast);
}
.btn-whatsapp svg { width: 20px; height: 20px; flex: none; fill: #FFFFFF; }
.btn-whatsapp:hover { background: #1DA851; border-color: #1DA851; color: #FFFFFF; }
.btn-whatsapp:active { background: #128C7E; border-color: #128C7E; }

/* ---- footer ---- */
/* padding-inline-start is 76px, not --space-6, and that is not a spacing
   choice: .whatsapp-sticky and the a11y trigger stacked above it are fixed
   to inset-inline-start + bottom, which in RTL is this corner, and the
   footer is the one block that is always under them when the page is
   scrolled to its end. Measured at 1280x860: .footer-name ran x1190-1233,
   the WhatsApp bubble x1201-1249 - the brand sat behind the button. 76px =
   the bubble's 48px + its own 16px inset + a 12px gap. Same technique as
   .menu-foot in hero.css and .lb-thumbs below, and chosen over z-index for
   the same reason they were: it holds at every scroll position. */
.site-footer {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  padding: 26px var(--space-6) 34px;
  padding-inline-start: 76px;
  border-top: 1px solid var(--border-default);
}
.footer-name { font-size: 0.8125rem; font-weight: 700; }
.footer-legal { display: flex; gap: var(--space-4); flex-wrap: wrap; font-size: 0.75rem; font-weight: 600; }
.footer-legal a { color: var(--text-secondary); }
.footer-legal a:hover { color: var(--accent-text); }
.footer-rights { font-size: 0.75rem; color: var(--text-secondary); }

/* ---- legal pages (terms, privacy, accessibility) --------------------------
   Not in the canvas - added 2026-08-29 for launch. Plain running text, same
   page-title treatment as work/about, capped to a readable measure rather
   than the full column width. */
.view-legal { max-width: 720px; padding: 56px var(--space-6) var(--space-9); }
.view-legal .page-title { font-size: clamp(2rem, 5vw, 3.5rem); margin-bottom: var(--space-2); }
.legal-updated { margin: 0 0 var(--space-6); font-size: 0.8125rem; color: var(--text-secondary); }
.view-legal p { margin: 0 0 var(--space-5); font-size: 1rem; line-height: 1.7; text-wrap: pretty; }
.view-legal ul { margin: 0 0 var(--space-5); padding-inline-start: 1.4em; }
.view-legal li { margin-bottom: var(--space-2); font-size: 1rem; line-height: 1.7; text-wrap: pretty; }

@media (max-width: 700px) {
  .view-legal { padding: 40px var(--space-4) var(--space-8); }
}

/* ---- lightbox ---- */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 72px 108px;
  background: var(--scrim-strong);
  cursor: zoom-out;
}
.lightbox[hidden] { display: none; }

/* The photograph is dragged, so the stage swallows the browser's own touch
   panning and the photograph itself is not a close target any more - the
   backdrop around it still is. */
.lb-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  max-width: 100%;
  max-height: 100%;
  touch-action: none;
  cursor: grab;
}
.lb-stage:active { cursor: grabbing; }
.lb-stage img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  transition: transform 260ms var(--ease-photo), opacity 260ms ease;
  will-change: transform;
}
.lb-stage.is-dragging img { transition: none; }

.lb-btn {
  position: absolute;
  border: 1px solid var(--on-scrim-border);
  background: none;
  color: var(--on-scrim);
  cursor: pointer;
  font: inherit;
  transition: background-color var(--transition-fast), color var(--transition-fast);
}
.lb-btn:hover { background: var(--surface-page); color: var(--text-primary); }

/* Physical left/right on purpose, paired with a physical translate. Never swap
   one of the pair to a logical property: that only centres in one direction. */
.lb-prev, .lb-next {
  top: 50%;
  transform: translateY(-50%);
  width: 46px;
  height: 46px;
  font-size: 1.125rem;
}
.lb-prev { left: 14px; }
.lb-next { right: 14px; }
.lb-close { right: 20px; top: 18px; padding: var(--space-2) 14px; font-size: 0.75rem; font-weight: 600; }
.lb-caption {
  position: absolute;
  left: 20px;
  bottom: 84px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  color: var(--on-scrim-soft);
}

/* The rest of the gallery, along the bottom. A phone visitor swipes; this is
   for everyone else, and it says how much more there is. */
.lb-thumbs {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--space-5);
  display: flex;
  gap: var(--space-1);
  justify-content: center;
  /* The accessibility toolbar's button sits in the bottom right corner. A long
     strip must not run underneath it. */
  padding: 0 88px 0 var(--space-5);
  overflow-x: auto;
  scrollbar-width: none;
  cursor: default;
}
.lb-thumbs::-webkit-scrollbar { display: none; }
.lb-thumb {
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  line-height: 0;
  opacity: 0.4;
  transition: opacity var(--transition-base);
}
.lb-thumb img { width: 68px; height: 46px; object-fit: cover; display: block; }
.lb-thumb:hover, .lb-thumb:focus-visible { opacity: 0.8; }
.lb-thumb[aria-selected="true"] {
  opacity: 1;
  outline: 2px solid var(--accent-primary);
  outline-offset: -2px;
}

/* ---- motion ----
   One authored moment: the photo push-in above. Everything else is a colour
   transition or this 420ms entrance on a view other than Home. */
@keyframes riseIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
.rise-in:not([hidden]) { animation: riseIn 420ms ease both; }

/* The cover photograph carries its own name across the view change, so the
   browser morphs it from the tile into its place in the mosaic instead of
   cross-fading the two screens over each other. Unsupported browsers get the
   plain view swap and lose nothing. */
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 320ms; }
::view-transition-group(hero-photo) { animation-duration: 560ms; animation-timing-function: var(--ease-photo); }

@media (prefers-reduced-motion: reduce) {
  .rise-in:not([hidden]) { animation: none; }
  .lb-stage img { transition: none; }
  .cover img, .photo-btn img { transition: opacity 400ms ease; }
  .btn:active { transform: none; }
}
:root.a11y-motion-off .rise-in:not([hidden]) { animation: none; }
:root.a11y-motion-off .cover img,
:root.a11y-motion-off .photo-btn img,
:root.a11y-motion-off .lb-stage img { transition: none; }

/* ---- responsive ----
   The canvas is a desktop artboard and specifies no small-screen behaviour.
   These are the only additions to it: horizontal padding steps down, the nav
   is allowed to wrap, and the photo grid drops to two columns. Verified at
   320px, 390px and 740x360. */
@media (max-width: 700px) {
  .site-header { padding: 14px var(--space-4); gap: var(--space-3); flex-wrap: wrap; }
  .site-nav { gap: var(--space-4); flex-wrap: wrap; }

  .hero { min-height: 0; }
  .hero-copy { padding: 40px var(--space-4); gap: var(--space-5); }
  .hero-media { min-height: 320px; }
  .stats { gap: var(--space-5); }

  .section-head { padding: var(--space-7) var(--space-4) var(--space-4); }
  .covers-home { padding: 0 var(--space-4); }
  .statement { padding: 56px var(--space-4) var(--space-8); margin-top: 56px; gap: var(--space-6); }

  [data-view="work"], [data-view="gallery"], .view-contact { padding: 40px var(--space-4) var(--space-8); }
  /* Stacked, not side-by-side, below 700px: the intro's left gap-padding
     (meant for the panel sitting beside it) would just be dead space once
     the panel wraps underneath, so both sides reset to a plain block rhythm. */
  .contact-intro { padding: 0 0 var(--space-6); }
  .contact-panel { padding: var(--space-6) var(--space-5); }
  .about-copy { padding: 40px var(--space-4) var(--space-8); }
  .about-photo { max-height: 60vh; }
  .page-lede { margin-bottom: var(--space-6); }

  /* A row of three across a phone is three postage stamps. One per line, each at
     its own true height, is what the mosaic degrades to. */
  .photo-row { display: block; aspect-ratio: auto; }
  .photo-row figure + figure { margin-top: var(--space-1); }
  .photo-btn { height: auto; }
  .photo-btn img { height: auto; }

  /* Same degrade, same reason, for the gallery covers: one per line at its
     own true aspect ratio rather than a landscape and a portrait squeezed
     into a shared row width. */
  .cover-row { display: block; aspect-ratio: auto; }
  .cover-row .cover + .cover,
  .cover-row .cover-empty + .cover-empty { margin-top: var(--space-1); }
  .cover, .cover-empty { aspect-ratio: var(--ar, 0.8); }
  .cover img { height: auto; }

  /* Same 76px reservation as above, re-declared because the shorthand on
     this line resets it. Not reduced for mobile even though .whatsapp-
     sticky shrinks to 44px here: the a11y trigger stacked above it stays
     48px, and it is the wider of the two that has to clear. */
  .site-footer { padding: var(--space-5) var(--space-4) 28px; padding-inline-start: 76px; }

  /* On a phone the arrows would sit on top of the photograph, so the whole
     control cluster moves below it: ribbon at the bottom, arrows above it in
     the corners where a thumb reaches them, counter centred between. */
  .lightbox { padding: 56px var(--space-4) 148px; }
  .lb-prev, .lb-next { top: auto; bottom: 72px; transform: none; }
  .lb-thumbs {
    justify-content: flex-start;
    bottom: 20px;
    padding: 0 60px 0 var(--space-4);
  }
  .lb-thumb img { width: 56px; height: 40px; }
  .lb-prev { left: var(--space-2); }
  .lb-next { right: var(--space-2); }
  .lb-caption { left: 0; right: 0; bottom: 88px; text-align: center; }
}

@media (max-width: 380px) {
  .site-nav { gap: var(--space-3); font-size: 0.75rem; }
}

/* Landscape phone: the hero must not eat the whole screen. */
@media (max-height: 420px) and (orientation: landscape) {
  /* A 360px-tall screen cannot afford a 620px hero: the CTA would sit below
     the fold on every landscape phone. */
  .hero { min-height: 0; }
  .hero-copy { padding: var(--space-6) var(--space-5); }
  .hero-media { min-height: 240px; }
  .about-photo { max-height: 100vh; }
  /* A landscape phone has no room for the ribbon; the swipe replaces it, and
     the arrows go back to the sides. */
  .lightbox { padding: var(--space-5) 56px; }
  .lb-thumbs { display: none; }
  .lb-prev, .lb-next { top: 50%; bottom: auto; transform: translateY(-50%); }
  .lb-caption { left: 20px; right: auto; bottom: var(--space-3); text-align: start; }
}

/* ---- whatsapp: sticky button, every page --------------------------------
   Same corner as the accessibility toolbar's own trigger (a11y-trigger sits
   inset-inline-start / bottom, a11y.css) - stacked under it, not the
   opposite corner: on Home the opposite corner sits directly on top of the
   wordmark (hero.css .wordmark is bottom-left). WhatsApp takes the true
   bottom-most spot and the two rules below push the toolbar's own trigger
   and panel up out of its way, by exactly its own height + gap at each
   breakpoint. WhatsApp's own green, not the site's accent, so it reads as
   the recognisable WhatsApp affordance it is - not a themed site button.
   (Briefly recoloured to --accent-text on 2026-08-29 to match the header's
   Instagram icon, reverted the same day - client wanted that colour on the
   testimonial quotes instead, not here. See .quote in the about section.) */
.whatsapp-sticky {
  position: fixed;
  inset-inline-start: max(1rem, env(safe-area-inset-left, 0px));
  inset-block-end: max(1rem, env(safe-area-inset-bottom, 0px));
  z-index: 145;
  width: 48px;
  height: 48px;
  display: grid;
  place-items: center;
  background: #25D366;
  border: 2px solid #FFFFFF;
  border-radius: 50%;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
  transition: background-color var(--transition-fast);
}
.whatsapp-sticky svg { width: 26px; height: 26px; fill: #FFFFFF; }
.whatsapp-sticky:hover { background: #1DA851; }
.whatsapp-sticky:active { background: #128C7E; }
.whatsapp-sticky:focus-visible { outline: 3px solid #128C7E; outline-offset: 3px; }

/* Not a11y-themes.css: that file is documented as colour themes only. This
   is a layout override of the third-party a11y module, so it lives with the
   button that made it necessary. `body` in front is deliberate, not
   decorative - styles.css loads BEFORE a11y.css, so a plain `.a11y-trigger`
   here would lose the cascade to a11y.css's own same-specificity rule; the
   extra type selector wins regardless of load order. 60px = whatsapp-
   sticky's 48px + a 12px gap; the mobile figure matches its 44px mobile
   size the same way. The a11y-panel opens above its own trigger (a11y.css's
   own +58px), so it needs the same push plus that 58px on top. */
body .a11y-trigger { inset-block-end: calc(max(1rem, env(safe-area-inset-bottom, 0px)) + 60px); }
body .a11y-panel { inset-block-end: calc(max(1rem, env(safe-area-inset-bottom, 0px)) + 60px + 58px); }

@media (max-width: 700px) {
  .whatsapp-sticky { width: 44px; height: 44px; }
  .whatsapp-sticky svg { width: 24px; height: 24px; }
  body .a11y-trigger { inset-block-end: calc(max(1rem, env(safe-area-inset-bottom, 0px)) + 56px); }
  body .a11y-panel { inset-block-end: calc(max(1rem, env(safe-area-inset-bottom, 0px)) + 56px + 58px); }
}

/* ---- no JS ----
   The router hides every view but Home. Without JS nothing would ever unhide,
   so show them all, stacked, and say why the galleries are empty. */
.noscript-note {
  margin: 0;
  padding: var(--space-5) var(--space-6);
  border-bottom: 1px solid var(--border-default);
  font-size: 0.875rem;
  color: var(--text-secondary);
}
