/* ============================================================
   LEU & PARTNER — Web Refresh V2.0 mockup
   Based on brandbook LEU_2602.01 V03 (Küng Art Direction)
   ============================================================ */

@font-face {
  font-family: "Acherus Feral";
  src: url("../fonts/AcherusFeral-Light.otf") format("opentype");
  font-weight: 300;
  font-style: normal;
}
@font-face {
  font-family: "Acherus Feral";
  src: url("../fonts/AcherusFeral-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
}
@font-face {
  font-family: "Town 80";
  src: url("../fonts/Town80Text-Book.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
}

:root {
  /* Main level */
  --sand: #cebfa6;
  --green-dark: #47514c;
  --taupe: #79776b;
  /* Support level */
  --sup-green: #3d6331;
  --sup-brick: #922e1c;
  --sup-petrol: #004560;
  --sup-yellow: #f8efb6;
  --sup-blue: #9dc6d6;
  --sup-salmon: #eba09a;
  --sup-palegreen: #d9e5d1;
  /* Techsupport level */
  --paper: #f0ece4;
  --bg: #f9f7f3;
  /* Highlight */
  --orange: #ef7d00;

  /* body ink: #79776b measured 3.82:1 on --paper, under AA.
     #6a685d keeps the warm grey and reaches 4.75:1 */
  --text: #6a685d;
  --heading: #767d75;
  /* only for text that must carry paragraphs on a light support field
     (salmon detail block): --green-dark reaches 3.94:1 there, this 4.94:1 */
  --ink-deep: #3a423e;
  --maxw: 1920px;
  --pad: clamp(24px, 6.25vw, 120px); /* 120px at 1920 */

  /* One height for the fixed bar AND the menu drawer's top row, so the
     wordmark does not jump when the menu opens. 93 including the hairline
     leaves an even 92px content box, in which the burger's 44px square
     centres on a whole pixel. */
  --header-h: 93px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 20px); /* anchor targets clear the fixed header */
  overflow-y: scroll; /* scrollbar always visible, no layout jump between pages */
  scrollbar-color: var(--sand) var(--green-dark); /* Firefox */
}

/* WebKit/Chromium scrollbar in brand colors */
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--green-dark); }
::-webkit-scrollbar-thumb {
  background: var(--sand);
  border: 2px solid var(--green-dark); /* inset look, slimmer thumb */
  border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover { background: #fff; }
::-webkit-scrollbar-corner { background: var(--green-dark); }
::-webkit-scrollbar-button { display: none; }

body {
  font-family: "Acherus Feral", "Segoe UI", sans-serif;
  font-weight: 300;
  background: var(--bg);
  color: var(--text);
  font-size: 19px;
  line-height: 1.42;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

/* the surfaces the browser draws still belong to the palette: the scrollbar
   above, and the two a visitor meets the moment they start typing */
::selection { background: var(--sand); color: var(--green-dark); }
input, select, textarea { caret-color: var(--green-dark); }

/* ---------- location (Luzern / Zürich) ----------
   data-ort is stamped on <html> by an inline head script, before first paint,
   so the other city never flashes. This rule is what makes the switch work
   without JavaScript at all; main.js additionally removes the hidden blocks
   from the DOM so sliders and endless lists count only what is on screen. */
[data-ort="lu"] [data-loc="zh"],
[data-ort="zh"] [data-loc="lu"] { display: none; }
.f-loc { margin-top: 26px; }

/* ---------- focus ----------
   The stylesheet previously stripped outlines on form fields and supplied
   nothing in their place. One ring, drawn from the palette, everywhere. */
:focus-visible {
  outline: 2px solid var(--green-dark);
  outline-offset: 3px;
}
/* over photography and on dark grounds the green ring disappears */
.panel :focus-visible,
.detail-hero :focus-visible,
.menu-overlay :focus-visible,
.site-footer :focus-visible,
.gallery :focus-visible {
  outline-color: var(--sand);
}
/* the bar is light glass now; Alpine Sand on it measures 1.76:1 */
.site-header :focus-visible { outline-color: var(--green-dark); }
.panel .slide:focus-visible { outline-offset: -6px; }

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------- wordmark ----------
   The authored logo, not type set to look like it. Küng's layout draws the mark
   at a 10.13:1 ink ratio; the letter-spaced live text this replaced measured
   14.02:1 and stood 20.5px tall against the layout's 30px at 1920.

   Carried as a mask rather than an image element so it takes currentColor:
   white over photography, Body Ink on Paper pages, white again in the footer —
   one file, three grounds. The base rule below is the text fallback, so a
   browser without mask support shows a wordmark, not a solid rectangle. */
.wordmark {
  font-family: "Town 80", serif;
  font-weight: 400;
  letter-spacing: 0.30em;
  font-size: clamp(20px, 1.45vw, 28px);
  white-space: nowrap;
  color: inherit;
}
.wordmark .amp { font-style: italic; letter-spacing: 0; }

@supports ((-webkit-mask-image: url("../img/wordmark.svg")) or (mask-image: url("../img/wordmark.svg"))) {
  .wordmark {
    /* 31px box at 1920 puts the ink at 29.9px against the layout's 30px */
    --wm-h: clamp(14px, 1.62vw, 31px);
    display: block;
    height: var(--wm-h);
    aspect-ratio: 264 / 27;
    background-color: currentColor;
    -webkit-mask: url("../img/wordmark.svg") no-repeat center / 100% 100%;
    mask: url("../img/wordmark.svg") no-repeat center / 100% 100%;
  }
  /* the name stays in the document for assistive tech and for a failed mask */
  .wordmark .wm-text {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ---------- header ---------- */
/* Fixed, not absolute: the bar used to scroll away with the hero, leaving a
   7-panel page with no navigation. Glass rather than a solid fill so the
   photograph still reads through it. */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: var(--header-h);
  padding: 0 var(--pad);
  /* Neutral glass, not a tinted slab. The bar used to be Deep Pine Slate at
     0.76, which reads as a dark band laid over the photograph. Plain white
     glass carries no colour of its own and lets the image through.
     Set to 0.55 by the client. Measured against the darkest ground that
     passes under the bar — the Verkaufen panel's door — Deep Pine Slate reads
     3.63:1 there, under the 4.5:1 body-text threshold; 0.70 reached 4.92:1.
     Lightness was chosen over that margin.
     Dropping the tint altogether was measured too — white type then sat on
     grounds as weak as 1.35:1, and a halo cannot help because it changes the
     glyph, not the ground. */
  color: var(--green-dark);
  background: rgba(255,255,255,0.40);
  -webkit-backdrop-filter: blur(22px) saturate(118%);
  backdrop-filter: blur(22px) saturate(118%);
  border-bottom: 1px solid rgba(71,81,76,0.16);
}
/* blur unavailable (older Firefox): fall back to a denser tint so the
   header text keeps its contrast rather than floating on the photo */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .site-header { background: rgba(255,255,255,0.92); }
}
/* pages whose top area is Paper rather than photography */
.site-header.on-light {
  color: var(--text);
  background: rgba(249,247,243,0.72);
  border-bottom-color: rgba(106,104,93,0.20);
}
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .site-header.on-light { background: rgba(249,247,243,0.94); }
}
.site-header .h-right {
  display: flex;
  align-items: center;
  gap: clamp(24px, 4vw, 72px);
}
.h-location {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 300;
  font-size: clamp(18px, 1.35vw, 26px);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
/* In the header the location is a control, not a caption: both cities are
   named and the current one is marked, so the way to change it is visible
   rather than something to go looking for in the menu. Marked by weight and
   strength, the same pair the menu switch uses — and by currentColor, because
   this bar sits on Glass Dark over photography and on Glass Light over Paper.
   The pin belongs to the group, not to either city. */
/* The header names only the city you are in. As a link it inherits everything
   — colour, weight, no underline — so it looks exactly like the indicator
   Küng draws and carries no second state to read. */
a.h-location { color: inherit; text-decoration: none; }
.h-location svg {
  width: clamp(19px, 1.35vw, 26px);
  height: clamp(19px, 1.35vw, 26px);
  /* flex centres the icon on the line box, but LUZERN is all caps: its optical
     centre sits above the line box centre, which left the pin reading low.
     -0.06em puts the pin's ink centre on the cap-height centre (measured to
     within 0.12px, stable across three ink thresholds). */
  transform: translateY(-0.06em);
}

.burger {
  background: none;
  border: 0;
  cursor: pointer;
  color: inherit;
  padding: 0;
  /* whole-pixel geometry: 3 bars of 2px on a 9px pitch = a 20px stack,
     centred in a 44px box inside a 92px bar. At 2.5px on a 9.5px pitch every
     bar landed on a different subpixel phase and the middle one rendered light. */
  display: flex;
  flex-direction: column;
  align-items: flex-end; /* bars flush to the page inset, like the wordmark */
  justify-content: center;
  gap: 7px;
  width: 44px;
  height: 44px;
}
/* Two nested elements per bar: the outer one carries the travel, the inner one
   the rotation. One element cannot stage them — a single transform property
   interpolates both at once and the bar swings in on an arc. Split, they read
   as two moves: three rules converge to one line, then the line opens into a
   cross. Returning runs the same two moves in reverse order and 40ms quicker. */
.burger > span {
  display: block;
  width: clamp(26px, 1.8vw, 34px);
  height: 2px;
  /* to the burger: un-rotate first, then travel back apart */
  transition: transform 130ms cubic-bezier(0.33, 0, 0.2, 1) 110ms,
              opacity 90ms linear 190ms;
}
.burger > span > span {
  display: block;
  width: 100%;
  height: 100%;
  background: currentColor;
  transition: transform 110ms cubic-bezier(0.33, 0, 0.2, 1);
}
/* to the cross: converge first, then rotate */
.burger.is-x > span {
  transition: transform 130ms cubic-bezier(0.33, 0, 0.2, 1),
              opacity 80ms linear;
}
.burger.is-x > span > span {
  transition: transform 150ms cubic-bezier(0.16, 1, 0.3, 1) 130ms;
}
.burger.is-x > span:nth-child(1) { transform: translateY(9px); }
.burger.is-x > span:nth-child(2) { opacity: 0; }
.burger.is-x > span:nth-child(3) { transform: translateY(-9px); }
.burger.is-x > span:nth-child(1) > span { transform: rotate(45deg); }
.burger.is-x > span:nth-child(3) > span { transform: rotate(-45deg); }

/* ---------- full-screen menu overlay ---------- */
.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--taupe);
  color: #fff;
  padding: 0 var(--pad) clamp(40px, 8vh, 80px);
  display: none;
  overflow-y: auto;
  /* the ground crossfades so the icon's return is watchable — without it the
     overlay vanishes on the first frame and the cross never opens back out */
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms ease;
}
.menu-overlay.open { display: block; }
.menu-overlay.shown {
  opacity: 1;
  pointer-events: auto;
  transition-duration: 220ms;
}
/* the drawer's top row is the header: same height, same insets, same 44px
   control box on the right, so nothing shifts when the menu opens */
.menu-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* the bar spends 1px of --header-h on its hairline; the drawer has none, so
     it matches the bar's content box rather than its outer height */
  min-height: calc(var(--header-h) - 1px);
  margin-bottom: 64px;
}
.menu-top .h-right { display: flex; align-items: center; gap: clamp(24px, 4vw, 72px); }
/* the drawer's control is the same drawn burger, not a ✕ character: a text
   glyph would sit at a different weight from the bars it is meant to be */
.menu-close { color: #fff; }
.menu-cols {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(32px, 6vw, 140px);
}
/* group labels, not document headings — they used to be <h3> elements sitting
   ahead of every page's <h1> in the DOM, which broke the outline on all 7 pages */
.menu-cols .menu-group {
  font-weight: 700;
  font-size: clamp(19px, 1.45vw, 28px);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 10px;
}
.menu-cols ul { list-style: none; }
.menu-cols a {
  display: block;
  font-size: clamp(19px, 1.45vw, 28px);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  line-height: 1.6;
}
.menu-cols a:hover { color: var(--sand); }
.menu-switch { margin-top: clamp(40px, 5vh, 72px); }
.menu-switch a {
  display: block;
  font-size: clamp(19px, 1.45vw, 28px);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--sand);
  line-height: 1.6;
}
.menu-switch a.active { color: #fff; font-weight: 700; }
@media (max-width: 1000px) {
  .menu-cols { grid-template-columns: repeat(2, 1fr); row-gap: 48px; }
}
@media (max-width: 640px) {
  .menu-cols { grid-template-columns: 1fr; row-gap: 40px; }
}

/* ---------- landing: scroll snap (html gets .snap-scroll on the homepage) ---------- */
/* scroll-padding is for anchor jumps, but it also offsets snap positions:
   every panel was landing 92px down with a strip of the previous one above it. */
html.snap-scroll { scroll-snap-type: y mandatory; scroll-padding-top: 0; }
html.snap-scroll .panel { scroll-snap-align: start; scroll-snap-stop: always; }
html.snap-scroll .site-footer { scroll-snap-align: end; }
/* while the JS section animation runs, native snapping and smooth scrolling
   must not fight the frame-by-frame positioning */
html.snap-scroll.no-snap { scroll-snap-type: none; scroll-behavior: auto; }

/* ---------- landing: full-viewport image panels ---------- */
.panel {
  position: relative;
  min-height: 100vh;
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
.panel--short { min-height: 88vh; }

.panel-title {
  position: relative;
  z-index: 1;
  color: #fff;
  font-weight: 300;
  font-size: clamp(30px, 3.35vw, 64px);
  letter-spacing: 0.045em;
  text-transform: uppercase;
  text-align: center;
  padding: 0 70px;
  /* German compounds set in caps do not fit a phone: IMMOBILIENANGEBOTE alone
     wants 358px. The page is lang="de", so let the browser hyphenate it
     properly, and keep break-word as the backstop that cannot fail. */
  hyphens: auto;
  -webkit-hyphens: auto;
  overflow-wrap: break-word;
  /* a flex item's automatic minimum size is its min-content width, which
     break-word does not shrink — without this the unbroken word still forces
     the panel wider than the screen */
  min-width: 0;
  /* No scrim over the photography, so legibility has to come from the type
     itself: a tight halo close to the glyphs plus a wider falloff. This
     darkens only the pixels around each letter and leaves the frame alone.
     It is weaker than a scrim — the real fix is frames shot with a quiet
     zone across the centre band. */
  text-shadow:
    0 1px 2px rgba(28,32,30,0.55),
    0 0 12px rgba(28,32,30,0.60),
    0 0 42px rgba(28,32,30,0.45);
}
.panel-title strong { font-weight: 700; }

/* a quote panel carries its attribution: an unattributed testimonial is a
   claim, an attributed one is evidence */
.panel-copy {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(16px, 2.2vh, 34px);
  padding: 0 70px;
  min-width: 0;
  max-width: 100%;
}
.panel-copy .panel-title { padding: 0; }
.panel-attrib {
  color: #fff;
  font-weight: 700;
  font-size: 21px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-shadow:
    0 1px 2px rgba(28,32,30,0.60),
    0 0 12px rgba(28,32,30,0.65),
    0 0 34px rgba(28,32,30,0.45);
}

/* slides inside a panel or quote section */
.panel .slide {
  position: absolute;
  inset: 0;
  display: none;
  background-size: cover;
  background-position: center;
  align-items: center;
  justify-content: center;
  animation: fadein .6s ease;
}
.panel .slide.active { display: flex; }

/* The one panel whose photography is not curated.
   Every other panel on the landing page carries a frame someone chose for it,
   with a quiet band across the middle for the title to sit in. This one now
   carries all 69 clients, and their portraits were shot to be portraits: the
   subject is often centred, and measured across the 45 Luzern frames, nine sit
   above 170 luminance exactly where the quote goes and thirty are busy enough
   to eat a text shadow. The halo on .panel-title was already the weaker option
   — the comment on it says so — and it was chosen against four known photos.
   So this panel, and only this panel, gets a scrim: a soft ellipse under the
   words and a low band under the counter, both fading to nothing well before
   the edges so the photograph is still a photograph. */
.panel--quotes .slide::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 62% 34% at 50% 46%,
      rgba(28,32,30,0.50) 0%, rgba(28,32,30,0.32) 48%, rgba(28,32,30,0) 78%),
    linear-gradient(to top,
      rgba(28,32,30,0.48) 0%, rgba(28,32,30,0.30) 5%,
      rgba(28,32,30,0.12) 11%, rgba(28,32,30,0) 20%);
}
.quote-section .slide { display: none; animation: fadein .6s ease; }
.quote-section .slide.active { display: block; }
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }

.slider-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 5;
  background: none;
  border: 0;
  cursor: pointer;
  color: #fff;
  padding: 10px;
}
.slider-arrow svg { width: 26px; height: 48px; display: block; }
.slider-arrow.prev { left: clamp(12px, 5.5vw, 106px); }
.slider-arrow.next { right: clamp(12px, 5.5vw, 106px); }
.slider-arrow.dark { color: var(--green-dark); }

/* slide position. Without it a two-slide carousel whose slides share a title
   reads as a broken control — the visitor cannot tell anything changed. */
.slider-dots {
  position: absolute;
  z-index: 2;
  left: 0; right: 0; bottom: clamp(26px, 4vh, 54px);
  display: flex;
  justify-content: center;
  gap: 12px;
}
.slider-dots button {
  width: 34px;
  height: 3px;
  padding: 0;
  border: 0;
  cursor: pointer;
  background: rgba(255,255,255,0.42);
  transition: background .3s ease;
}
.slider-dots button[aria-current="true"] { background: #fff; }
.slider-dots button:hover { background: rgba(255,255,255,0.75); }
.quote-section .slider-dots { bottom: 18px; }
.quote-section .slider-dots button { background: rgba(71,81,76,0.28); }
.quote-section .slider-dots button[aria-current="true"] { background: var(--green-dark); }

/* Past nine slides a dot each is no longer an indicator — it is a second
   navigation bar, and 45 of them are wider than the screen. The count says the
   same thing in the same place: where you are, and how much is left. It sits
   on the photograph like the panel titles do, so it carries their halo rather
   than a scrim. */
.slider-count {
  position: absolute;
  z-index: 2;
  left: 0; right: 0; bottom: clamp(26px, 4vh, 54px);
  margin: 0;
  text-align: center;
  color: #fff;
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;   /* so the line does not jog on 9 -> 10 */
  /* the same three-layer halo .panel-attrib carries, not a lighter version of
     it: this is smaller type than the attribution and it lands wherever the
     bottom of an uncurated photograph happens to be — pale sandstone, snow,
     sky — so it needs at least as much help, not less */
  text-shadow:
    0 1px 2px rgba(28,32,30,0.60),
    0 0 12px rgba(28,32,30,0.65),
    0 0 34px rgba(28,32,30,0.45);
}
.quote-section .slider-count {
  bottom: 18px;
  color: var(--green-dark);
  text-shadow: none;
}

/* ---------- content pages ---------- */
.page-head {
  background: var(--paper);
  padding: 140px var(--pad) 8px;
}
.page-body { padding: 0 var(--pad) 90px; }
.page-head + .page-body { padding-top: 34px; }

.content-col { max-width: 620px; }

/* KUNDENSTIMMEN, IMMOBILIENANGEBOTE: single German words wider than a narrow
   phone. Hyphenate them (the pages are lang="de") instead of letting them push
   the whole document sideways. */
h1, h2, h3 {
  hyphens: auto;
  -webkit-hyphens: auto;
  overflow-wrap: break-word;
}

h1.page-title {
  color: var(--heading);
  font-weight: 300;
  font-size: clamp(34px, 2.9vw, 56px);
  line-height: 1.16;
  letter-spacing: 0.015em;
  text-transform: uppercase;
  margin-bottom: 26px;
}
.lead { max-width: 620px; }

.copy p { margin-bottom: 1.4em; }
.copy p:last-child { margin-bottom: 0; }
.copy strong { font-weight: 700; }

/* ---------- legal prose: Impressum, Datenschutz ----------
   Eleven thousand words of it, so this is the one place on the site that is
   asking to be read straight through rather than looked at. The measure stays
   at the 620px the rest of the content pages use — a legal text is the last
   place to widen the line and make it harder work — and the numbered sections
   take the same uppercase mark the property pages give their headings, so the
   page still belongs to the site while looking like nothing else on it. */
.legal .content-col { padding-bottom: 20px; }
.legal h2 {
  font-weight: 700;
  font-size: 21px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--heading);
  margin: 2.4em 0 1em;
}
.legal .content-col > :first-child { margin-top: 0; }
.legal h3 {
  font-weight: 700;
  letter-spacing: 0.02em;
  margin: 1.9em 0 .7em;
}
.legal ul {
  list-style: disc;
  padding-left: 1.3em;
  margin-bottom: 1.4em;
}
.legal li { margin-bottom: .5em; }
.legal strong { font-weight: 700; }
.legal a {
  text-decoration: underline;
  text-underline-offset: 4px;
}
.legal a:hover { color: var(--heading); }

/* the Impressum is an address, not an essay */
.legal .office-addr { margin-bottom: 1.6em; font-style: normal; }
.legal .office-lines { margin-bottom: 1.6em; }
.legal .legal-uid { color: var(--heading); }

.text-link {
  font-weight: 300;
  text-decoration: underline;
  text-underline-offset: 5px;
}
.text-link:hover { color: var(--heading); }
.arrow-link::before { content: "\2192\00a0"; text-decoration: none; }

/* quote sections (carousel of statements) */
.quote-section {
  background: var(--paper);
  padding: 96px var(--pad);
  position: relative;
}
.quote-section .quote {
  max-width: 640px;
  margin: 0 auto;
}
/* the portrait bleeds to the viewport edge; only the type keeps the inset */
.quote-section.bleed { padding: 0 0 96px; }
.quote-section.bleed .quote { padding: 96px var(--pad) 40px; }
.quote-section.bleed .slider-arrow { top: auto; bottom: 220px; }

/* ---------- Ansprechpersonen ----------
   Küng stacks these as two full-bleed bands: a ~1074px portrait, then the
   quote beneath it. Faithful, and on any real screen it means you see the face
   or you see what she said, never both — on the one page whose entire claim is
   that these are people you will deal with personally.
   So the portrait and the words sit side by side, and the pair is sized against
   the viewport rather than against nothing. That is also closer to this
   system's own north star than the stacked version: the portrait is the work,
   and the field beside it is the label. */
/* Alpine Sand, not Paper: the intro band above is Paper too, so on Paper this
   block had no edge and the two ran together. Of the three main-level colours
   it is the only one that separates from BOTH neighbours — Weathered Stone
   sits at 1.83 against the Deep Pine Slate footer directly below and would
   merge into it, and Deep Pine Slate is that footer. Sand reads 1.53 against
   Paper above and 4.56 against the footer below, and it is the colour the
   brand is recognised by. Deep Pine Slate type on it measures 4.56:1, which
   carries the small attribution lines as well as the quote. */
.quote-section.persons {
  padding: 0;
  background: var(--sand);
  color: var(--green-dark);
}
.quote-section.persons blockquote,
.quote-section.persons .attribution .name,
.quote-section.persons .attribution p { color: var(--green-dark); }

/* Every slide sits in the same grid cell, so the band is always as tall as the
   TALLEST of them and nothing moves when you advance. Quotes differ in length —
   Armando's is one line, Ariane's is six — and with display:none the block
   resized under the reader on every step, taking the arrows and dots with it.
   Hidden slides use visibility rather than display so they still contribute
   their height; visibility:hidden also keeps them out of the tab order and out
   of the accessibility tree, which display:none was doing for us before. */
.quote-section.persons { display: grid; grid-template-columns: minmax(0, 1fr); }
.quote-section.persons .slide,
.quote-section.persons .slide.active {
  grid-area: 1 / 1;
  display: grid;
  /* a grid item floors at min-content unless told otherwise, and the widest
     slide was dragging the whole band 2px past a 320px screen */
  min-width: 0;
  animation: none;               /* the base fade is tied to the display switch */
  transition: opacity 380ms ease;
}
.quote-section.persons .slide { visibility: hidden; opacity: 0; }
.quote-section.persons .slide.active { visibility: visible; opacity: 1; }

@media (min-width: 901px) {
  /* every slide, not just the active one: they share a grid cell now, so a
     hidden slide left in its single-column default laid the portrait out at
     full width and drove the row to twice the height it should be */
  .quote-section.persons .slide {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: stretch;
    /* one screen minus the bar that sits over it, floored so a laptop in a
       short window still gets a portrait rather than a letterbox */
    min-height: clamp(560px, calc(100vh - var(--header-h)), 920px);
  }
  @supports (height: 100svh) {
    .quote-section.persons .slide {
      min-height: clamp(560px, calc(100svh - var(--header-h)), 920px);
    }
  }
  .quote-section.persons .img-band,
  .quote-section.persons .img-band.crop { max-height: none; height: auto; overflow: hidden; }
  /* Out of flow, like the gallery stage. `height: 100%` against an auto-height
     grid row falls back to auto, so a tall portrait sized itself from its own
     aspect and drove the band past the viewport — the Zürich set is 1379x1920,
     which at an 800px column wants 1114px of height while the words need 612.
     Absolute positioning lets the row be decided by the type and the clamp,
     and the photograph fills whatever that turns out to be. */
  .quote-section.persons .img-band img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  /* a centred crop is only right for a centred subject: Elisabeth Gassmann
     stands at 73% of her frame and a centre crop pinned her to the right edge.
     --focus is per-portrait, set in the markup where the fact lives. */
  .quote-section.persons .img-band img { object-position: var(--focus, 50% 50%); }
  /* the words get the larger half. The Measure Rule's 620px is for running
     copy at 19px; this is display type at 38px, where 620px is four words to
     the line and a short statement ran to six of them. */
  .quote-section.persons .slide { grid-template-columns: minmax(0, 0.82fr) minmax(0, 1fr); }
  .quote-section.persons .quote {
    align-self: center;
    margin: 0;
    /* px, not ch: `ch` resolves against this container's 19px, while the
       quote inside is set at 38-46px, so 40ch came out narrower than the
       620px it replaced and added four lines instead of removing them. */
    max-width: 900px;
    padding: 56px clamp(28px, 3.2vw, 68px) 56px clamp(34px, 4.4vw, 84px);
  }
  /* the arrows belong to the words, not to the face: keeping them off the
     portrait leaves it uninterrupted and puts them where the reader is.
     :not(.testimony) — the detail page has no slider, and puts its prev/next
     at the section's own edges instead. */
  .quote-section.persons:not(.testimony) .slider-arrow.prev { left: auto; right: calc(var(--pad) + 46px); }
  .quote-section.persons:not(.testimony) .slider-arrow.next { right: var(--pad); }
  .quote-section.persons:not(.testimony) .slider-arrow { top: auto; bottom: clamp(30px, 5vh, 60px); transform: none; }
  .quote-section.persons .slider-dots { justify-content: flex-end; right: var(--pad); left: auto; }
}

@media (max-width: 900px) {
  /* Stacked, but the portrait takes only what the words do not need — 92vh of
     face pushed every word below the fold. The floor keeps it a portrait rather
     than a letterbox on a short screen; the middle term is the share it would
     like; the last term is what is actually left once the quote is served. */
  /* named once so the chevrons can centre on the portrait without repeating it */
  .quote-section.persons { --portrait-h: max(240px, min(52vh, calc(100vh - 400px))); }
  @supports (height: 100svh) {
    .quote-section.persons { --portrait-h: max(240px, min(52svh, calc(100svh - 400px))); }
  }
  .quote-section.persons .img-band.crop { max-height: none; }
  .quote-section.persons .img-band.crop img {
    height: auto;
    aspect-ratio: 4 / 5;
    max-height: var(--portrait-h);
    object-fit: cover;
    object-position: var(--focus, 50% 50%);
  }
  .quote-section.persons .quote { padding: clamp(26px, 4.5vw, 44px) var(--pad) 72px; }
  .quote-section.persons .attribution { margin-top: 28px; }
  .quote-section.persons:not(.testimony) .slider-arrow { top: auto; bottom: 18px; transform: none; }
}
/* A Kundenstimme detail page reuses the band, but its words are a paragraph of
   the client's own account rather than one line — body copy, not display type.
   The short line they are known by is the page's h1 instead. */
.quote-section.testimony .quote .copy p { margin-bottom: 1.1em; }
.quote-section.testimony .quote .copy p:last-child { margin-bottom: 0; }
.quote-section.testimony .attribution { margin-top: 34px; }
/* The same chevrons the panel sliders use, at the section's own edges — the
   biggest, most obvious targets the band can offer. The two edges are different
   grounds, so each chevron takes the colour its own ground needs: the left one
   sits on the photograph and is white with the cast shadow the gallery chevrons
   use, the right one sits on Alpine Sand and is Deep Pine Slate. They are never
   seen side by side, so the pair reads as one control. */
.stimme-nav .slider-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  padding: 22px 16px;              /* a larger hit area than the panel sliders */
}
.stimme-nav .slider-arrow.prev { left: clamp(4px, 1.4vw, 26px); color: #fff; }
.stimme-nav .slider-arrow.next { right: clamp(4px, 1.4vw, 26px); color: var(--green-dark); }
.stimme-nav .slider-arrow.prev svg {
  /* three layers, not the gallery's two: those photographs were interiors, and
     these are uncontrolled client portraits. Measured across the set, the
     ground behind this chevron runs from 18:1 down to 1.01:1 — a pure white
     wall — and only the halo separates it there. */
  filter: drop-shadow(0 1px 2px rgba(28, 32, 30, 0.9))
          drop-shadow(0 0 6px rgba(28, 32, 30, 0.7))
          drop-shadow(0 0 18px rgba(28, 32, 30, 0.5));
}
.stimme-nav .slider-arrow.next:hover { color: var(--taupe); }

@media (max-width: 900px) {
  /* stacked: both chevrons centre on the portrait, so neither lands on the
     text and both keep a photographic ground */
  .stimme-nav .slider-arrow { top: calc(var(--portrait-h) / 2); }
  .stimme-nav .slider-arrow.next { color: #fff; }
  .stimme-nav .slider-arrow.next svg {
    filter: drop-shadow(0 1px 2px rgba(28, 32, 30, 0.9))
            drop-shadow(0 0 6px rgba(28, 32, 30, 0.7))
            drop-shadow(0 0 18px rgba(28, 32, 30, 0.5));
  }
}

@media (max-width: 640px) {
  /* down one documented step, to the Quote Card Heading floor: 28px uppercase
     in a 272px column runs three words to the line */
  .quote-section.persons blockquote {
    font-size: 24px;
    /* `anywhere` rather than break-word: German compounds get long, and
       LÖSUNGSORIENTIERT, measures 274px against a 272px column at 320. Only
       `anywhere` also lowers the min-content width, which is what stopped the
       slide dragging the whole page 2px sideways. Hyphenation still gets first
       refusal, so the break lands on a syllable where the dictionary allows. */
    overflow-wrap: anywhere;
  }
}
.quote-section blockquote {
  color: var(--heading);
  font-weight: 300;
  font-size: clamp(28px, 2.4vw, 46px);
  line-height: 1.22;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  /* the same treatment h1-h3 get: LÖSUNGSORIENTIERT set at 28px uppercase is
     wider than a 320px screen's text column, and pushed the page sideways */
  hyphens: auto;
  -webkit-hyphens: auto;
  overflow-wrap: break-word;
}
.quote-section .attribution { margin-top: 40px; }
.quote-section .attribution .name {
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--heading);
}
.quote-section .attribution p { line-height: 1.5; }

/* centered intro text section (paper background) */
.text-section {
  background: var(--paper);
  padding: 96px var(--pad);
}
.text-section .inner { max-width: 640px; margin: 0 auto; }
.text-section h1,
.text-section h2 {
  color: var(--heading);
  font-weight: 300;
  font-size: clamp(28px, 2.4vw, 46px);
  line-height: 1.2;
  letter-spacing: 0.015em;
  text-transform: uppercase;
  margin-bottom: 30px;
}

/* full-width image band */
.img-band { position: relative; }
.img-band img { width: 100%; height: auto; }
.img-band.crop { max-height: 92vh; overflow: hidden; }
.img-band.crop img { object-fit: cover; width: 100%; height: 92vh; }

/* ---------- listing cards ---------- */
.card-grid {
  display: grid;
  /* min() rather than a bare 300px: below ~350px the hard floor was wider than
     the content well and pushed the cards off the right edge */
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 460px));
  gap: clamp(28px, 3vw, 58px);
  align-items: start;
}
.card-grid .col { display: grid; gap: clamp(28px, 3vw, 58px); min-width: 0; }
.card-grid .col.offset { margin-top: 250px; }
@media (max-width: 900px) {
  .card-grid .col.offset { margin-top: 0; }
}

/* grid items default to min-width:auto, so the card's padding plus its longest
   word became a hard floor the column could not shrink under */
.listing-card, .quote-card { min-width: 0; }
.listing-card { display: block; }
.listing-card .photo { position: relative; aspect-ratio: 460/278; overflow: hidden; }
.listing-card .photo img { width: 100%; height: 100%; object-fit: cover; transition: transform .5s ease; }
/* only the cards that lead somewhere breathe on hover — an archived reference
   is not a link, and the scale would promise a page that isn't there */
a.listing-card:hover .photo img { transform: scale(1.04); }

/* Availability marker. The live site drops the link on anything not on the
   market and prints the state instead; this says the same thing in the card's
   own language — a support field bled into the photo's corner, never a badge
   floated on top of it. The photograph stays in colour: desaturating a sold
   property would break the one rule the brand is built on. */
.listing-card .status {
  position: absolute;
  top: 0; left: 0;
  z-index: 1;
  background: var(--green-dark);
  color: #fff;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 8px 15px;
}
/* reserved is not sold: Alpine Sand carries the warmer, still-in-play reading */
.listing-card .status.reserved { background: var(--sand); color: var(--green-dark); }
.listing-card .info {
  color: #fff;
  padding: 30px 34px 34px;
}
.listing-card .info h3 {
  font-weight: 700;
  font-size: 25px;
  line-height: 1.25;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  margin-bottom: 24px;
}
/* --heading (#767d75) gives only 2.31–3.63:1 on the light support fields and
   the 15px meta row needs 4.5:1; --green-dark clears it on all four */
.listing-card .info.text-dark { color: var(--green-dark); }

/* Salmon is the darkest of the light fields: --green-dark reaches only 3.93:1
   there, which carries a 25px heading but not the 15px meta row. These need
   to outrank the .listing-card / .quote-card rules above, hence the longer
   selectors rather than a bare .bg-salmon.text-dark. */
.listing-card .info.bg-salmon.text-dark,
.quote-card .bubble.bg-salmon.text-dark h3,
.bg-salmon.text-dark { color: var(--ink-deep); }
.meta {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: 10px 18px;
  font-size: 15px;
  letter-spacing: 0.02em;
}
.meta span { display: inline-flex; align-items: center; gap: 9px; }
.meta svg { width: 17px; height: 17px; flex: none; }
/* the price is the longest value in the block and broke across two lines in a
   half-width cell on a phone; it takes the full row, as it does on the live site */
.meta .price { grid-column: 1 / -1; }

/* CTA tile inside grid */
.cta-tile {
  color: #fff;
  padding: 36px 34px 42px;
  display: flex;
  flex-direction: column;
  gap: 130px;
}
.cta-tile h3 {
  font-weight: 700;
  font-size: clamp(26px, 2vw, 38px);
  line-height: 1.25;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.cta-tile .cta-sub {
  font-weight: 700;
  font-size: 21px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  margin-bottom: 14px;
}
.cta-tile a { text-decoration: underline; text-underline-offset: 5px; }

/* ---------- testimonial masonry (Kundenstimmen) ---------- */
.quote-card { display: block; }
.quote-card .bubble {
  color: #fff;
  padding: 30px 34px;
  min-height: 170px;
}
.quote-card .bubble.tall { min-height: 230px; }
.quote-card .bubble h3 {
  font-weight: 700;
  font-size: clamp(24px, 1.75vw, 33px);
  line-height: 1.28;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
/* Küng's layout draws this photo at 3:2. The client's own portraits are square
   masters, and cropping them to 3:2 takes the top off several heads — so the
   card runs 1:1 until the new shoot delivers landscape frames. One value. */
.quote-card .photo {
  position: relative;
  aspect-ratio: var(--quote-photo-ratio, 1 / 1);
  overflow: hidden;
}
.quote-card .photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.quote-card .photo .credit {
  position: absolute;
  z-index: 1;
  left: 20px; bottom: 18px;
  color: #fff;
  text-shadow: 0 1px 2px rgba(28,32,30,0.7), 0 0 14px rgba(28,32,30,0.6);
  font-size: 19px;
  text-decoration: underline;
  text-underline-offset: 5px;
}
/* light support fields take the darkest value, not --heading:
   #767d75 on sand is only 2.35:1, --green-dark is 4.57:1 */
.quote-card .bubble.text-dark h3 { color: var(--green-dark); }

/* ---------- progressive reveal (endless lists) ----------
   Every card is in the document — 45 testimonials, 189 properties. Without JS
   the page is simply long and complete; the batching below is added at runtime,
   so nothing that matters is hidden behind a script. */
.quote-card.is-pending,
.listing-card.is-pending { display: none; }
.list-sentinel { height: 1px; }
/* announced to assistive tech, never drawn — the scrollbar is the sighted
   reader's progress indicator and the page wants no counter chrome */
.list-status,
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
@media (prefers-reduced-motion: no-preference) {
  .quote-card.is-entering,
  .listing-card.is-entering { animation: list-rise 320ms cubic-bezier(0.16, 1, 0.3, 1) backwards; }
  @keyframes list-rise {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: none; }
  }
}

/* ---------- forms ----------
   Küng bands the Suchprofil across changes of ground rather than dividers —
   Paper under the head, a second field under "Ihr Wunschobjekt", white under
   the contact block. That banding is not decoration: it is the only thing
   that makes a white boxed field visible. Küng's own tint (--bg, #f9f7f3)
   leaves a 1.07:1 edge against the box, which is no edge at all; Paper
   leaves 1.18:1 — wider than the Paper/Background pair the system already
   trusts to separate two stacked surfaces without a border. So the band
   change lands where the form changes subject: what you are looking for,
   then who you are. */
.form-band { padding: 60px var(--pad) 84px; }
.form-band.tint { background: var(--paper); }
.form-band.plain { background: #fff; }
/* the page head is already Paper — the two run on as one field */
.page-head + .form-band.tint,
.page-head + form > .form-band.tint { padding-top: 46px; }

/* the form column, and beside it the space Küng leaves empty. The phone
   escalation lives there: it is about Bauland and Mehrfamilienhäuser, which
   are options in the very first select, so it belongs next to that choice
   rather than after it. Below 1080px it falls back under the fields. */
.form-aside-grid {
  display: grid;
  grid-template-columns: minmax(0, 640px) minmax(0, 1fr);
  gap: clamp(30px, 4vw, 90px);
  align-items: start;
}
/* minmax(0,...), never a bare 1fr: a bare fr floors at the item's min-content,
   and a nowrap 19px label is wide enough to push the track past the page */
@media (max-width: 1080px) { .form-aside-grid { grid-template-columns: minmax(0, 1fr); } }

.form-section { max-width: 640px; }
.form-block { border: 0; padding: 0; margin: 0; min-width: 0; }
.form-block + .form-block { margin-top: 56px; }
/* the legend matches its labels in size — Küng sets both at body weight and
   scale — so space and ink, not a type step, mark it as the block's head */
.form-block > legend {
  display: block;
  padding: 0;
  font-weight: 700;
  color: var(--green-dark);
  margin-bottom: 26px;
}

/* Underline rows. The rule carries the field's whole state, because in a form
   drawn as a printed document the rule IS the field: 1px at 62% for an empty
   line, solid Form Graphite once there is ink on it, 2px Deep Pine Slate under
   the cursor, Support Brick when it is wrong. The form visibly fills in.
   Thickening is done with a zero-blur shadow rather than border-width so the
   row never reflows as you tab through it. */
.form-row {
  display: flex;
  align-items: baseline;
  gap: 14px;
  border-bottom: 1px solid rgba(74, 74, 74, 0.62);
  padding: 18px 0 8px;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.form-row.tall { align-items: flex-start; }
/* .row-label is unscoped on purpose — it labels a group that has no .form-row
   around it, and scoping it there once cost it its weight and its colour */
.form-row label,
.row-label {
  font-weight: 700;
  color: #4a4a4a;
  font-size: 19px;
  white-space: nowrap;
}
/* the two address splits Küng draws unevenly: street over number, PLZ over town */
.split-street > .field:first-child { flex: 2.4; }
.split-town > .field:first-child { flex: 0.6; }
.split-town > .field:last-child { flex: 2; }
/* :not([type="radio"]) is load-bearing — min-width/min-height outrank the
   width/height a 17px choice mark sets, so an unscoped rule here stretches
   every radio in the form to a 40×34 slab */
.form-row input:not([type="radio"]),
.form-row textarea {
  flex: 1;
  border: 0;
  background: transparent;
  font: inherit;
  color: #4a4a4a;
  min-width: 40px;
  min-height: 34px;
  padding: 0;
}
.form-row textarea { resize: none; overflow: hidden; line-height: 1.42; }
.form-row.is-filled { border-bottom-color: #4a4a4a; }
/* the row owns the focus treatment, so the ring follows the underline */
.form-row:focus-within { border-bottom-color: var(--green-dark); box-shadow: 0 1px 0 0 var(--green-dark); }
.form-row input:focus-visible,
.form-row textarea:focus-visible,
.form-row select:focus-visible { outline: none; }
.field.is-error .form-row { border-bottom-color: var(--sup-brick); box-shadow: 0 1px 0 0 var(--sup-brick); }

.form-split { display: flex; gap: 40px; }
.form-split > .field { flex: 1; }
/* The rhythm margin below is for fields stacked one under the next. Inside a
   split they sit side by side, where a top margin is not spacing — it is 8px
   of drift, and Nr. and Ort sat that far below Strasse and PLZ. The phone
   block turns the split into a column and puts the margin back. */
.form-split > .field + .field { margin-top: 0; }
.field + .field,
.field + .form-split,
.field + .opt-set,
.form-split + .field,
.form-split + .opt-set,
.form-split + .form-split { margin-top: 8px; }

/* Never colour alone: the rule turns brick and the reason is written out.
   Meta size, sentence case — an error is running copy, not a label. */
.field-err {
  display: none;
  margin-top: 7px;
  font-size: 15px;
  letter-spacing: 0.02em;
  color: var(--sup-brick);
}
.field.is-error > .field-err { display: block; }

/* Selects keep the system's own icon vocabulary — 24px box, 1.6 stroke —
   rather than the platform triangle or a ▾ glyph standing in for a drawing */
.sel { display: inline-flex; align-items: center; gap: 8px; flex: 1; }
.sel select {
  border: 0; background: none; font: inherit; color: #4a4a4a;
  appearance: none; min-height: 34px; padding: 0; flex: 1; min-width: 40px;
  cursor: pointer;
}
.chev { width: 17px; height: 17px; flex: none; color: #4a4a4a; }

/* single answer, drawn as Küng's square */
.opt-set { display: flex; flex-wrap: wrap; }
.opt {
  display: inline-flex; align-items: center; gap: 11px;
  font-weight: 300; color: #4a4a4a; cursor: pointer;
  min-height: 34px;
  transition: color 160ms ease;
}
.opt input {
  appearance: none; flex: none;
  width: 17px; height: 17px; padding: 3px;
  border: 1px solid rgba(74, 74, 74, 0.62); background: transparent;
  cursor: pointer;
  transition: border-color 160ms ease, background-color 160ms ease;
  /* centred against the 27px line box the mark sits 2px low to the eye: the
     box is measured to the descender, but the word's optical centre is the
     middle of its x-height. Seat it on that instead. */
  margin: -2px 0 0;
}
.opt:hover input { border-color: #4a4a4a; }
.opt input:checked {
  background: var(--green-dark);
  background-clip: content-box;
  border-color: var(--green-dark);
}
/* the answer that was given darkens — the mark alone is 17px of evidence */
.opt:has(input:checked) { color: var(--green-dark); }

/* Desktop: one question, one row, ONE rule.
   Three rule segments of different lengths, at different heights, separated by
   gaps, read as three truncated input fields — and this row takes no writing
   at all. It gets the single full-width rule every other row on the page has,
   which is also what the stacked mobile version already does per row. */
/* label and options are bare children of the group — every one of them a
   34px-tall box, so centring them lands their type on one line with nothing
   to correct for */
.opt-set > * { flex: none; }
.opt-set .row-label { display: flex; align-items: center; min-height: 34px; }

/* 641, not 701: the stacked treatment lives in the 640px phone block, and the
   two have to meet exactly or the widths between them get neither */
@media (min-width: 641px) {
  .opt-set {
    align-items: center;
    flex-wrap: nowrap;
    gap: clamp(26px, 3vw, 54px);
    border-bottom: 1px solid rgba(74, 74, 74, 0.62);
    padding: 18px 0 8px;
  }
}

/* Boxed fields (Suchprofil "Ihr Wunschobjekt"). Küng draws the label column
   and the field column at the same width; 240/240 inside a 640px form was
   neither. */
.field-box {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 40px;
  align-items: center;
}
.field-box + .field-box { margin-top: 20px; }
.field-box > label { grid-row: 1; font-weight: 700; color: #4a4a4a; }
.field-box > .box { grid-column: 2; grid-row: 1; }
.field-box > .field-err,
.field-box > .box-more,
.field-box > .box-add { grid-column: 2; margin-top: 10px; }
button.text-link {
  border: 0; background: none; padding: 0; cursor: pointer;
  font: inherit; font-weight: 300; color: inherit;
  text-decoration: underline; text-underline-offset: 5px;
}
/* White on Paper is a 1.18:1 edge — visible as a shape, but under the 3:1 a
   field boundary owes the reader. The box takes the same hairline, in the same
   colour, at the same three weights as the underline rows, so the form is drawn
   throughout with one line rather than two unrelated species of field. */
.box {
  background: #fff;
  padding: 12px 16px;
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px solid rgba(74, 74, 74, 0.62);
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.box.is-filled { border-color: #4a4a4a; }
.box input, .box select {
  border: 0; background: none; font: inherit; color: #4a4a4a;
  flex: 1; min-width: 0; appearance: none; min-height: 24px; padding: 0;
}
.box:focus-within { border-color: var(--green-dark); box-shadow: inset 0 0 0 1px var(--green-dark); }
.box input:focus-visible, .box select:focus-visible { outline: none; }
.box .unit { color: #4a4a4a; white-space: nowrap; }
.field.is-error .box { border-color: var(--sup-brick); box-shadow: inset 0 0 0 1px var(--sup-brick); }
.box-drop { position: relative; }
/* the repeated Wunschwohnort rows carry their own remove control */
.box-more { display: flex; align-items: center; gap: 12px; margin-top: 10px; }
.box-more .box { flex: 1; }
.box-drop-btn {
  border: 0; background: none; cursor: pointer; color: #4a4a4a;
  padding: 8px; display: inline-flex; flex: none;
}
.box-drop-btn svg { width: 17px; height: 17px; }
.box-drop-btn:hover { color: var(--sup-brick); }
@media (max-width: 700px) {
  .field-box { grid-template-columns: minmax(0, 1fr); row-gap: 10px; }
  .field-box > label { white-space: normal; }
  /* the desktop placement is explicit (label row 1, box column 2 row 1); one
     column has to release BOTH axes or the two keep sharing a row and the
     grid invents a second column to hold them */
  .field-box > label,
  .field-box > .box,
  .field-box > .field-err,
  .field-box > .box-more,
  .field-box > .box-add { grid-column: 1; grid-row: auto; }
  .field-box > .box-add { margin-top: 12px; }
}

.form-note { max-width: 340px; color: var(--text); }
.form-note p + p { margin-top: 18px; }
.form-tel {
  font-weight: 700;
  font-size: 23px;
  letter-spacing: 0.03em;
  color: var(--green-dark);
  white-space: nowrap;
}
.form-tel:hover { color: var(--taupe); }
@media (max-width: 1080px) { .form-note { margin-top: 44px; max-width: 640px; } }

.form-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 22px; flex-wrap: wrap; margin-top: 44px;
}
.form-req {
  font-size: 12.5px; letter-spacing: 0.08em; line-height: 1.55;
  text-transform: uppercase; color: var(--text);
}
.form-alert {
  display: none;
  margin-bottom: 30px; padding: 16px 20px;
  background: var(--sup-brick); color: #fff; font-weight: 700;
}
.form-alert.on { display: block; }

/* The one authored moment on these pages: the form is replaced by its answer.
   A full band, not a panel floating on white — this system separates surfaces
   by changing ground, and it has no vocabulary for a card sitting on one. */
.form-band.done {
  background: var(--sup-palegreen);
  color: var(--green-dark);
  padding-top: 100px;
  padding-bottom: 112px;
}
.form-done { max-width: 640px; }
.form-done:focus { outline: none; }
.form-done h2 {
  font-weight: 700; font-size: clamp(24px, 1.75vw, 33px);
  line-height: 1.28; letter-spacing: 0.02em; text-transform: uppercase;
  margin-bottom: 18px;
}
@media (prefers-reduced-motion: no-preference) {
  .form-band.done { animation: form-settle 520ms cubic-bezier(0.16, 1, 0.3, 1) backwards; }
  @keyframes form-settle {
    from { opacity: 0; transform: translateY(14px); }
    to { opacity: 1; transform: none; }
  }
}

.btn {
  display: inline-block;
  border: 0;
  cursor: pointer;
  font: inherit;
  font-weight: 700;
  color: #fff;
  background: var(--green-dark);
  padding: 12px 26px;
  letter-spacing: 0.02em;
  transition: background-color 160ms ease, color 160ms ease;
}
/* the primary button had no hover state at all — it inverts, like the themed one */
.btn:hover { background: var(--sand); color: var(--green-dark); }
/* the order button carries the property's own colour, so the control ties back
   to the information block above it whatever colour that block is */
.btn.themed { background: var(--obj-bg); color: var(--obj-ink); }
.btn.themed:hover { background: var(--obj-ink); color: var(--obj-bg); }
.btn[aria-busy="true"],
.btn[aria-busy="true"]:hover { background: var(--taupe); color: #fff; cursor: default; }

/* ---------- property detail ---------- */
.detail-hero {
  position: relative;
  min-height: 96vh;
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: flex-end;
}
.detail-hero h1 {
  position: relative;
  z-index: 1;
  color: #fff;
  text-shadow:
    0 1px 2px rgba(28,32,30,0.55),
    0 0 12px rgba(28,32,30,0.60),
    0 0 42px rgba(28,32,30,0.45);
  font-weight: 300;
  font-size: clamp(30px, 2.9vw, 56px);
  line-height: 1.18;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  padding: 0 var(--pad) 60px;
  max-width: 1100px;
}
/* A property page is themed by its own teaser: the colour field a visitor
   clicked in the grid is the field that greets them on the detail page, so the
   card and the page are visibly the same object. Küng draws this block in
   Salmon; that is now one of nine cases rather than the only one.
   This is the only place a support colour carries running paragraphs, which
   needs 4.5:1 rather than the 3:1 a card heading needs. Every ink below is the
   one that clears it — the four deep fields take white, the five light fields
   take --ink-deep. Taupe on white is the tightest in the set at exactly 4.50:1;
   Salmon is why --ink-deep exists at all (4.94:1, where Deep Pine Slate reaches
   only 3.93:1). */
:root { --obj-bg: var(--sup-salmon); --obj-ink: var(--ink-deep); }
[data-theme="taupe"]     { --obj-bg: var(--taupe);         --obj-ink: #fff; }
[data-theme="green"]     { --obj-bg: var(--sup-green);     --obj-ink: #fff; }
[data-theme="petrol"]    { --obj-bg: var(--sup-petrol);    --obj-ink: #fff; }
[data-theme="brick"]     { --obj-bg: var(--sup-brick);     --obj-ink: #fff; }
[data-theme="blue"]      { --obj-bg: var(--sup-blue);      --obj-ink: var(--ink-deep); }
[data-theme="sand"]      { --obj-bg: var(--sand);          --obj-ink: var(--ink-deep); }
[data-theme="salmon"]    { --obj-bg: var(--sup-salmon);    --obj-ink: var(--ink-deep); }
[data-theme="yellow"]    { --obj-bg: var(--sup-yellow);    --obj-ink: var(--ink-deep); }
[data-theme="palegreen"] { --obj-bg: var(--sup-palegreen); --obj-ink: var(--ink-deep); }

.detail-block {
  background: var(--obj-bg);
  color: var(--obj-ink);
  padding: 80px var(--pad) 70px;
  display: grid;
  grid-template-columns: minmax(0, 620px) minmax(240px, 320px);
  justify-content: space-between;
  gap: 60px;
}
@media (max-width: 900px) { .detail-block { grid-template-columns: 1fr; } }
.detail-block h2 {
  font-weight: 700;
  font-size: 21px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 28px;
}
.detail-block .copy p { margin-bottom: 1.3em; }
.detail-block .address {
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-top: 36px;
}
.detail-meta { display: grid; gap: 22px; align-content: start; font-size: 23px; }
.detail-meta span { display: inline-flex; align-items: center; gap: 14px; }
.detail-meta svg { width: 26px; height: 26px; flex: none; }

/* ---------- property gallery ----------
   Küng draws one full-bleed frame, two chevrons, and a counter sitting on a
   rule that runs to the far edge. All of that is kept; the rule is simply made
   to mean something (it fills), and the previews the comp had no room for are
   given their own band beneath. */
.gallery {
  position: relative;
  background: var(--bg);
  /* The stage is measured against what sits under it, so the frame, the counter
     and all six previews land on one screen together. At 92vh the stage alone
     filled the viewport and the previews were always below the fold — a set of
     previews you have to scroll to find is not a set of previews.
     The strip is six across, so a thumbnail is a ninth of the well: its width is
     (well / 6) and its height is that over 3:2. */
  --g-gap: clamp(8px, 0.9vw, 16px);
  --g-strip-h: calc((100vw - 2 * var(--pad) - 5 * var(--g-gap)) / 9);
  --g-bar-top: 26px;
  --g-bar-h: 48px;
  --g-strip-top: clamp(20px, 2vw, 30px);
  --g-strip-bottom: clamp(44px, 5vh, 76px);
  --g-furniture: calc(var(--g-bar-top) + var(--g-bar-h) + var(--g-strip-top)
                    + var(--g-strip-h) + var(--g-strip-bottom));
}
.g-stage { position: relative; }

/* the frame is the control: clicking the photograph is how a visitor expects
   to enlarge it, so the whole thing is one button rather than a photo with a
   separate small affordance floated on top */
.g-zoom {
  display: block;
  position: relative;
  width: 100%;
  height: clamp(340px, calc(100vh - var(--g-furniture)), 88vh);
  padding: 0; border: 0; background: none;
  cursor: zoom-in;
  overflow: hidden;
}
/* svh, so a phone's collapsing address bar cannot make the frame taller than
   the screen it was measured against */
@supports (height: 100svh) {
  .g-zoom { height: clamp(340px, calc(100svh - var(--g-furniture)), 88vh); }
}
.g-shot {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: none;
}
/* Only the frame on show is displayed, which is what keeps the other five
   `loading="lazy"` tags from being fetched at load — a stack of transparent
   images would all count as in-viewport and pull 1.5MB before a first click.
   The incoming frame rides above the outgoing one and fades in over it. */
.g-shot.is-on { display: block; }
.g-shot.is-next { z-index: 1; opacity: 0; transition: opacity 320ms ease; }
.g-shot.is-next.is-lit { opacity: 1; }

/* A chevron is a 2.4px stroke with a cast shadow, not a 13px letterform: it
   survives a pale wall where type does not. Measured, the counter used to sit
   on grounds as weak as 1.45:1 over these interiors, which is why the reading
   furniture came off the photograph entirely — see .g-bar. */
.g-stage .slider-arrow svg {
  filter: drop-shadow(0 1px 3px rgba(28,32,30,0.75)) drop-shadow(0 0 12px rgba(28,32,30,0.5));
}

/* Küng floats the counter and its rule on the photograph. That works over the
   comp's shaded terrace and fails over the client's actual set, which is six
   bright white rooms — white type measured 1.45:1 against the palest of them,
   and the system has no overlay to fall back on. So the counter, the rule and
   the enlarge control take a field of their own directly beneath the frame.
   The rule survives, and now it runs from the count to the control it belongs
   to rather than off the edge of the picture. */
.g-bar {
  display: grid;
  grid-template-columns: auto minmax(40px, 1fr) auto;
  align-items: center;
  gap: clamp(16px, 2vw, 34px);
  padding: 26px var(--pad) 0;
  color: var(--green-dark);
}
.g-count {
  font-weight: 700;
  font-size: 19px;
  letter-spacing: 0.06em;
  white-space: nowrap;   /* "Bild 4 / 6" is one thing, never two lines */
}
.g-enlarge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  border: 0; background: none; cursor: pointer;
  color: var(--green-dark);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.g-enlarge svg { width: 19px; height: 19px; flex: none; }
.g-enlarge:hover { color: var(--taupe); }
/* the count and the control keep the line; the rule drops beneath and runs the
   full width, which is closer to what it does on a wide screen than stacking
   the control under it would be */
@media (max-width: 620px) {
  .g-bar {
    grid-template-columns: auto auto;
    justify-content: space-between;
    row-gap: 13px;
  }
  .g-track { grid-row: 2; grid-column: 1 / -1; }
  /* the Label step (12.5px/0.08em), not an invented one — see the type ramp */
  .g-enlarge { font-size: 12.5px; gap: 8px; }
  .g-enlarge svg { width: 17px; height: 17px; }
}
/* below this the count and the control stop sharing a line honestly — they fit
   with 15px between them, which reads as a collision rather than a pair */
@media (max-width: 370px) {
  .g-bar { grid-template-columns: 1fr; justify-content: start; row-gap: 12px; }
  .g-track { grid-row: auto; grid-column: auto; }
  .g-enlarge { justify-self: start; }
}
.g-track { height: 2px; background: rgba(71,81,76,0.22); overflow: hidden; }
/* scaled rather than widened: the same drawing, but it runs on the compositor
   instead of laying the bar out again on every frame */
.g-track i {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--green-dark);
  transform-origin: left center;
  transition: transform 400ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* previews */
.g-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: clamp(8px, 0.9vw, 16px);
  padding: clamp(20px, 2vw, 30px) var(--pad) clamp(44px, 5vh, 76px);
}
.g-strip button {
  display: block;
  position: relative;
  padding: 0; border: 0; background: none;
  cursor: pointer;
}
.g-strip img {
  display: block;
  width: 100%;
  /* the height attribute on the tag is a used value and outranks aspect-ratio;
     without this the previews rendered 280px tall whatever their width */
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  /* held back, not bleached: any lower and six photographs on a Paper ground
     read as washed out rather than as the one that is currently up */
  opacity: 0.62;
  transition: opacity 260ms ease;
}
.g-strip button:hover img,
.g-strip button:focus-visible img,
.g-strip button[aria-current="true"] img { opacity: 1; }
/* the current frame is marked with a sand rule, not a ring or a glow */
.g-strip button::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 3px;
  background: var(--sand);
  opacity: 0;
  transition: opacity 260ms ease;
}
.g-strip button[aria-current="true"]::before { opacity: 1; }
/* Six across stops being a preview and starts being a stamp once a thumbnail
   drops under ~90px tall. Three across on two rows keeps them readable and,
   more to the point, keeps all six on screen — a horizontal scroller hides
   half the set behind a gesture nothing announces. */
@media (max-width: 900px) {
  .gallery {
    --g-strip-h: calc(2 * ((100vw - 2 * var(--pad) - 2 * var(--g-gap)) / 4.5) + var(--g-gap));
  }
  .g-strip {
    grid-auto-flow: row;
    grid-template-columns: repeat(3, 1fr);
    grid-auto-columns: auto;
  }
}
/* On a phone the frame stops cropping. A 3:2 photograph forced into a portrait
   viewport shows about a quarter of itself, which is a poor way to look at a
   flat; at the picture's own ratio the whole frame is there and the gallery
   still costs less than half a screen. */
@media (max-width: 700px) {
  .g-zoom { height: auto; aspect-ratio: 3 / 2; }
}

/* full view. Deep Pine Slate rather than black, per the palette's darkest
   value, and the photograph is contained rather than cropped — seeing the whole
   frame is the entire reason a visitor opens this. */
.g-lightbox {
  position: fixed;
  inset: 0;
  z-index: 120;
  background: var(--green-dark);
  display: none;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 200ms ease;
}
.g-lightbox.open { display: flex; }
.g-lightbox.shown { opacity: 1; }
.g-full { margin: 0; }
.g-full img {
  display: block;
  max-width: min(92vw, 1800px);
  max-height: 82vh;
  width: auto; height: auto;
  object-fit: contain;
}
.g-lightbox .g-close {
  position: absolute;
  top: 26px; right: var(--pad);
  color: #fff;
}
.g-lightbox .slider-arrow { color: #fff; }
/* the full view supplies its own ground, so the counter needs no field here */
.g-full-count {
  position: absolute;
  left: var(--pad); bottom: 38px;
  color: #fff;
}

/* ---------- Kontakt: the office bands ----------
   One band per office, split down the middle: the neighbourhood plan on one
   side, the address on the other, and the second band mirrored so the two
   read as a spread rather than a repeat. The plans are SVG drawn from OSM
   vector data in the brand palette (tools/make-maps.py) — no third-party
   frame, no foreign grey, and crisp at any size. */
.offices { background: var(--bg); }

.office {
  display: grid;
  /* Exactly half each, and not a hair more. At 1.06fr the plan took 51.5%
     of every band, so the first band's plan ran to 772px and the second's
     began at 728 — a 44px strip where paper map sat directly above paper
     map and the two read as one continuous plate across the fold. At 50/50
     they meet at a single corner and the fold is a clean diagonal.
     minmax(0,...) on both tracks: a bare 1fr floors at min-content, and the
     address block's longest line is a nowrap company name. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: stretch;
  min-height: clamp(440px, 58vh, 660px);
}
.office + .office { border-top: 1px solid rgba(71,81,76,0.22); }
.offices { border-bottom: 1px solid rgba(71,81,76,0.14); }
.office.mirror .office-plan { order: 2; }

.office-plan {
  position: relative;
  overflow: hidden;
  min-width: 0;
  background: var(--paper);
}
/* height:100% against an auto grid row resolves to auto and lets the image
   set the band's height. Take it out of flow instead, as the gallery stage
   and the Ansprechpersonen band both do. */
.office-plan img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.plan-credit {
  position: absolute;
  right: 10px;
  bottom: 8px;
  /* the Label step — 12.5px at 0.08em uppercase, the same voice the footer's
     legal links use, which is what this line is. 11px was a size the ramp
     does not carry. */
  font-size: 12.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* the plan's ground is Paper everywhere, so the credit needs no field of
     its own. Taupe measured 3.82:1 there, under AA; the body ink reaches
     4.75:1 and is still quiet enough not to compete with the pin. */
  color: var(--text);
}
.plan-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

.office-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 20px;
  min-width: 0;
  padding: clamp(40px, 4.2vw, 86px);
  color: var(--green-dark);
}
.office-city {
  color: var(--heading);
  font-weight: 300;
  font-size: clamp(30px, 2.5vw, 46px);
  line-height: 1.1;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.office-addr {
  font-style: normal;
  line-height: 1.75;
  color: var(--green-dark);
}
.office-lines { list-style: none; line-height: 1.75; }
/* the T/E gutter is set in ch of this list's own font-size — ch resolves
   against the element it is written on, not against a child */
.office-lines li { display: grid; grid-template-columns: 2.4ch minmax(0, 1fr); }
/* --taupe is 4.21:1 on this ground, under AA even for a one-letter label */
.office-lines span { color: var(--text); }
.office-lines a { color: var(--green-dark); text-decoration: none; }
.office-lines a:hover { text-decoration: underline; text-underline-offset: 5px; }
.office-go a { display: inline-flex; align-items: center; gap: 9px; color: var(--green-dark); }
.office-go svg { width: 15px; height: 15px; flex: none; }

@media (max-width: 900px) {
  /* .office.mirror must be named here too: it outranks a bare .office, so
     the single-column reset would not reach the second band and it kept a
     two-track grid 336px wider than the phone */
  .office,
  .office.mirror { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  /* order is reset because the plan must stay above its address on a phone —
     mirroring is a spread device and a single column has no spread */
  .office.mirror .office-plan { order: 0; }
  .office-plan { aspect-ratio: 4 / 3; }
  .office-card { padding: 40px var(--pad) 52px; }
}

/* ---------- footer ---------- */
.site-footer {
  background: var(--green-dark);
  color: var(--sand);
  padding: 88px var(--pad) 60px;
}
.footer-grid {
  display: grid;
  grid-template-columns: minmax(200px, 1fr) minmax(280px, 460px);
  justify-content: space-between;
  gap: 60px;
}
@media (max-width: 760px) { .footer-grid { grid-template-columns: 1fr; } }
.f-nav { font-size: 12.5px; letter-spacing: 0.08em; text-transform: uppercase; line-height: 1.55; }
.f-nav .grp { font-weight: 700; color: #fff; margin-top: 14px; }
.f-nav .grp:first-child { margin-top: 0; }
.f-nav a { display: block; }
.f-nav a:hover { color: #fff; }
.f-brand .wordmark { --wm-h: clamp(20px, 2.2vw, 30px); color: #fff; font-size: 25px; margin-bottom: 34px; }
.f-addr { font-size: 12.5px; letter-spacing: 0.08em; text-transform: uppercase; line-height: 1.55; }
.f-addr p { margin-bottom: 18px; }
.f-legal { font-size: 12.5px; letter-spacing: 0.08em; text-transform: uppercase; margin-top: 8px; }
.f-legal a:hover { color: #fff; }
.f-bottom {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 40px;
  margin-top: 90px;
}
.f-claim {
  color: #fff;
  font-weight: 700;
  font-size: 23px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.f-logos { display: flex; align-items: center; gap: 44px; }
.f-logos img { height: 42px; width: auto; }

/* ---------- phones ----------
   At 390px the header ran 461px wide and pushed the burger 18px off-screen.
   The location indicator duplicates the menu's Luzern/Zürich switch, so it
   is the right thing to drop when space runs out. */
@media (max-width: 640px) {
  /* shorter bar on phones — the drawer follows it through --header-h */
  :root { --header-h: 67px; }
  /* the drawer carries the Luzern/Zürich switch of its own, so the indicator
     goes from both rows at once — leaving it in the drawer overflowed its top
     row at 360px and pushed the close button 16px past the wordmark's inset */
  .h-location { display: none; }
  .site-header .h-right { gap: 16px; }
  /* font-size/tracking only reach the text fallback; --wm-h drives the mask */
  .wordmark { font-size: clamp(14px, 4vw, 20px); letter-spacing: 0.22em; }
  .panel-title { padding: 0 clamp(24px, 12vw, 70px); }
  /* clear the slider arrows so quote marks stop colliding with them */
  .panel[data-slider] .panel-title { padding: 0 56px; }
  .panel-copy { padding: 0 56px; }
  /* the arrow inset belongs to the wrapper, not to both — at (0,2,0) this rule
     lost to .panel[data-slider] .panel-title above it, so the quote slide paid
     56px twice on each side and pushed 40px off a 390px screen */
  .panel[data-slider] .panel-copy .panel-title { padding: 0; }
  .panel-attrib { font-size: 15px; }
  .slider-arrow { padding: 14px 6px; }
  .slider-arrow svg { width: 18px; height: 34px; }
  .f-nav a, .f-legal a { padding: 6px 0; }
  .f-bottom { flex-direction: column; align-items: flex-start; gap: 28px; }
  .form-row { padding: 16px 0 8px; }
  .form-split { flex-direction: column; gap: 0; }
  .form-split > .field + .field { margin-top: 8px; }
  /* stacked, each item keeping its own rule — the same shape as every row
     above it, which is what makes it read correctly on a phone */
  .opt-set { flex-direction: column; align-items: stretch; row-gap: 8px; }
  /* content-box: min-height is otherwise a border-box measure, so once the row
     carries its own 24px of padding a 44px touch target silently becomes 20px */
  .opt-set > * {
    box-sizing: content-box;
    border-bottom: 1px solid rgba(74, 74, 74, 0.62);
    padding: 16px 0 8px;
  }
  .opt { min-height: 44px; }
  .form-row label, .row-label { white-space: normal; }
  /* a 20-field form ends in a button that must be easy to hit, not tidy */
  .form-foot { flex-direction: column-reverse; align-items: stretch; gap: 20px; }
  .form-foot .btn { width: 100%; text-align: center; }
  .form-band { padding: 44px var(--pad) 62px; }
}

/* coarse pointers get real touch targets regardless of viewport width */
@media (pointer: coarse) {
  .menu-cols a, .menu-switch a { padding: 6px 0; }
  .form-row input:not([type="radio"]), .form-row textarea, .sel select { min-height: 40px; }
  .box input, .box select { min-height: 28px; }
  .opt { min-height: 44px; }
  .btn { padding: 15px 26px; }
}

/* utility background colors */
.bg-sand { background: var(--sand); }
.bg-taupe { background: var(--taupe); }
.bg-green { background: var(--sup-green); }
.bg-brick { background: var(--sup-brick); }
.bg-petrol { background: var(--sup-petrol); }
.bg-yellow { background: var(--sup-yellow); }
.bg-blue { background: var(--sup-blue); }
.bg-salmon { background: var(--sup-salmon); }
.bg-palegreen { background: var(--sup-palegreen); }
.bg-paper { background: var(--paper); }
