@charset "UTF-8";

/* Moser Reinigungen, Buchholz im Rheintal. Fictional showcase site for pronta.ch.
   Design idea: everything sits on a line. A strict twelve column grid, hairline
   rules instead of boxes, one single circle in the whole site (the guarantee
   seal on the seam between hero image and text zone). */

@layer base, layout, components, utilities;

/* Hebel 7: the one moment every kit site fumbles. No framework, no router,
   no JavaScript; without support the browser simply navigates as usual. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

/* ------------------------------------------------------------------ fonts */

/* One family for the whole site. The serif for running text went on
   24.08.2026 (five of five test readers called the page dated and the serif
   carried most of that impression), and Karla followed on 25.08.2026: it
   reads humanistic and slightly wilful next to the neutral, high x-height
   grotesque the reference asks for. Hanken Grotesk is app-modern without
   being cold, carries no AI marker, and is on neither the burnt nor the
   stopgap list.

   Built from google/fonts ofl/hankengrotesk/HankenGrotesk[wght].ttf: the
   wght axis instanced down to 400-700, then subset to the same 219 Latin
   codepoints Karla carried, delivered as woff2. It lands at 21'160 bytes,
   220 bytes under the Karla file it replaces, so the LCP budget is untouched.
   Its figures are tabular by construction (every digit 560 units), which is
   why the missing tnum feature costs nothing. */
@font-face {
  font-family: "Hanken Grotesk";
  src: url("../fonts/hanken-grotesk-latin-var.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* ------------------------------------------------------------------- base */

@layer base {
  :root {
    color-scheme: light;
    accent-color: var(--accent);

    /* Three anchors carry the palette; everything that is not fixed by the
       charter table is mixed out of them instead of being listed by hand.

       salbei: sage ground (anchor hue 168), clear blue accent (258). The
       24.08. calibration moved the ground off the yellow-green side towards
       cyan and gave the accent a little more chroma at a slightly lower
       lightness: cooler and cleaner, still sage.
         ink/paper 14.29:1   ink-soft/paper 6.37:1   ink-soft/surface 5.83:1
         paper on accent (button) 5.35:1   accent/surface 4.90:1
         paper on accent-deep (hover) 6.34:1   accent-deep/accent-faint 5.41:1
         ink-quiet/paper 3.58:1   field-line/paper 3.80:1, /surface 3.47:1 */
    --paper: oklch(0.978 0.007 168);
    --ink: oklch(0.265 0.019 168);
    --accent: oklch(0.515 0.132 258);

    --surface: oklch(0.947 0.014 168);
    --ink-soft: oklch(0.470 0.015 168);

    --rule: color-mix(in oklch, var(--ink) 13%, var(--paper));
    --ink-quiet: color-mix(in oklch, var(--ink) 52%, var(--paper));
    --accent-deep: color-mix(in oklch, var(--accent) 82%, var(--ink));
    --accent-faint: color-mix(in oklch, var(--accent) 12%, var(--paper));
    /* WCAG 1.4.11 wants 3:1 for control boundaries; the hairline used for
       decorative rules only reaches 1.32:1. This one measures 3.44:1 at
       worst (kreide against the tinted band).

       The title panel on the band (25.08.) puts ink on a ground that is 82
       percent paper over a photograph, so its contrast depends on what the
       picture happens to be doing behind it. Computed for the worst case
       there is, a pure black detail, with the compositing done in the sRGB
       gamma space rather than in luminance: 9.39:1 salbei, 9.29:1 kreide,
       9.40:1 porzellan. Without backdrop-filter support the ground goes to
       94 percent and the same worst case reads 12.52 / 12.38 / 12.53:1. On
       a mid grey it is around 11.7:1, on plain paper 14.3:1. AA asks 4.5:1
       and AAA asks 7:1; the panel clears both in every concept whatever the
       photograph does. */
    --field-line: color-mix(in oklch, var(--ink) 54%, var(--paper));

    --sans: "Hanken Grotesk", "Helvetica Neue", Arial, sans-serif;

    /* Five type steps, fluid within each step, no size ever set in a media
       query. The eight-step scale went on 25.08.2026: eight sizes is how a
       page ends up showing six or seven text styles at once, and the target
       is four. What used to be a size difference is now a weight or a grey
       difference, which is what the reference does. Every step carries its
       tracking, and wherever a step is used its track goes with it.

         step-5   display: the H1 in the panel, the pagehead H1, the ordinals
         step-3   titles: every h2
         step-1   row titles: the service lines, the detail rows, the steps,
                  the name and direct number of the person
         step-0   running text, buttons, form fields
         step--1  meta: second lines, labels, fine print
         step--2  ONLY the two uppercase voices (navigation, wordmark
                  underline). Never running text.

       Exactly three weights carry the hierarchy: 400 running text, 550
       labels and emphasised meta, 650 every title, row title, button and
       figure. Two deliberate exceptions: the place line sits at 440 so it
       reads light rather than small, and the wordmark keeps 700 as a brand
       mark. The only letterspacing outside the step coupling
       is the uppercase treatment of the wordmark and the navigation: all
       caps needs its own spacing law. */
    --step--2: clamp(0.76rem, 0.735rem + 0.08vw, 0.82rem);
    --step--1: clamp(0.88rem, 0.85rem + 0.12vw, 0.95rem);
    --step-0: clamp(1rem, 0.965rem + 0.17vw, 1.06rem);
    /* Back on 25.08. after the tester and the critic both said the same
       thing: collapsed onto the body step, the service lines lost the
       presence they had been praised for. At 22.4px and weight 650 they read
       as they did in round one (24px at 600), and the gap to their second
       line is a size, a weight and a grey again instead of only the last
       two. It is a row-title voice, not a fourth heading: dt, summary and
       the phone number stay on the body step, which is what keeps the
       guarantee view at two sizes. */
    --step-1: clamp(1.2rem, 1.1rem + 0.5vw, 1.4rem);
    --step-3: clamp(1.35rem, 1.13rem + 0.9vw, 1.7rem);
    --step-5: clamp(1.55rem, 1.02rem + 2.2vw, 2.4rem);
    --track--2: 0.015em;
    --track--1: 0.006em;
    --track-0: 0em;
    --track-1: -0.01em;
    --track-3: -0.014em;
    --track-5: -0.02em;

    /* Exactly two underline offsets on the whole site: one for links inside
       running text, one for the tighter UI links. Both dropped when the
       serif went, whose deeper descenders had needed the extra room. */
    --underline-run: 0.16em;
    --underline-ui: 0.12em;

    /* Two shadow tints, both mixed from the ink so they stay in the family
       of the chosen concept. Used at low opacity on the buttons and under
       the stuck masthead, nowhere else: this site works with lines, and a
       shadow is only ever the sign that something floats above the page. */
    --shade-near: color-mix(in oklch, var(--ink) 11%, transparent);
    --shade-far: color-mix(in oklch, var(--ink) 8%, transparent);

    --ease-out: cubic-bezier(0.2, 0, 0, 1);
    --dur: 150ms;
    --dur-reveal: 340ms;

    /* Block size of the sticky masthead, spent again as scroll padding so
       no anchor ever comes to rest underneath it. One value: the head only
       sticks from 960px up, where it is a single row (masthead block). */
    --stick: 5rem;

    --pad: clamp(1.15rem, 0.45rem + 2.9vw, 3.25rem);
    --gutter: clamp(1rem, 0.55rem + 1.4vw, 1.75rem);
    --maxw: 76rem;
    --sect: clamp(3.1rem, 1.9rem + 4.2vw, 5.5rem);
    /* The charter caps the line at 64 characters, and the box has to be
       re-measured every time the family changes, because ems say nothing
       about how many letters fit in them. Against Hanken Grotesk (25.08.)
       the longest line runs 58 characters at 768px, with the column itself
       the limit everywhere else (39 at 360px, 38 at 900px, 56 at 1440px).
       Inside the charter and well clear of the 50 below which the box would
       have to open up, so 30em stands. */
    --measure: 30em;
  }

  @supports not (color: oklch(0 0 0)) {
    :root {
      --paper: #f4f9f7;
      --surface: #e5f1ec;
      --ink: #1c2823;
      --ink-soft: #535e59;
      --rule: #d4dbd8;
      --ink-quiet: #7b8581;
      --accent: #3366b2;
      --accent-deep: #006093;
      --accent-faint: #d7eae5;
      --field-line: #77817c;
      --shade-near: rgba(28, 40, 35, 0.11);
      --shade-far: rgba(28, 40, 35, 0.08);
    }
  }

  /* ------------------------------------------------- palette concepts 2, 3

     Concept 1 is the sage default above and needs no attribute. The tool
     layer switches concepts by setting data-palette on <html>; each block
     overrides only the five base tokens, everything else re-derives
     through the color-mix chain. A browser without oklch simply keeps the
     concept 1 hex fallback, which is acceptable for a preview control.

     Both concepts keep the discipline of the default: one single accent
     hue, greys tinted towards their own anchor, no pure black or white.
     WCAG 2.1 ratios below are computed (OKLCH -> sRGB -> relative
     luminance), not estimated, including the color-mix derivatives. */

  /* kreide: warm chalk ground (anchor hue 95), bottle green accent (158).
     The green reads as soap and fresh air without drifting into wellness
     pastel; the warm grey keeps the pages bright and calm. The 24.08.
     calibration took chroma out of the ground, so the chalk stays warm but
     loses the creamy cast, and moved the green a little towards the blue
     side of its range.
       ink/paper 14.13:1   ink-soft/paper 6.40:1   ink-soft/surface 5.84:1
       paper on accent (button) 6.08:1   accent/surface 5.55:1
       paper on accent-deep (hover) 7.19:1   accent-deep/accent-faint 6.00:1
       ink-quiet/paper 3.56:1   field-line/paper 3.77:1, /surface 3.44:1 */
  :root[data-palette="kreide"] {
    --paper: oklch(0.978 0.005 95);
    --ink: oklch(0.270 0.015 95);
    --accent: oklch(0.470 0.105 158);
    --surface: oklch(0.947 0.011 95);
    --ink-soft: oklch(0.470 0.012 95);
  }

  /* porzellan: cool porcelain ground (anchor hue 248), plum accent (322).
     Colder and more formal than the default, aimed at the property
     managers; the plum stays far from every neighbouring site accent.
       ink/paper 14.31:1   ink-soft/paper 6.38:1   ink-soft/surface 5.82:1
       paper on accent (button) 7.28:1   accent/surface 6.65:1
       paper on accent-deep (hover) 8.30:1   accent-deep/accent-faint 6.88:1
       ink-quiet/paper 3.59:1   field-line/paper 3.80:1, /surface 3.47:1 */
  :root[data-palette="porzellan"] {
    --paper: oklch(0.977 0.006 248);
    --ink: oklch(0.265 0.017 248);
    --accent: oklch(0.455 0.125 322);
    --surface: oklch(0.946 0.011 248);
    --ink-soft: oklch(0.470 0.013 248);
  }

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

  html {
    -webkit-text-size-adjust: 100%;
    /* One declaration instead of scroll-margin on every anchor: it applies
       to fragment navigation and to scrollIntoView alike, so nothing ever
       comes to rest underneath the sticky masthead. The extra 1.5rem is
       not just air: a block that has not been revealed yet still carries
       its 12px offset, and the browser scrolls to the shifted box, so the
       block would settle that much higher once the transform resolves.
       Where the head does not stick (see the masthead block) this padding
       is taken back, because there is then nothing to clear. */
    scroll-padding-block-start: calc(var(--stick) + 1.5rem);
  }

  body {
    margin: 0;
    padding-block-end: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--sans);
    font-size: var(--step-0);
    line-height: 1.62;
  }

  figure {
    margin: 0;
  }

  picture {
    display: contents;
  }

  img {
    display: block;
    max-inline-size: 100%;
    height: auto;
  }

  svg {
    display: block;
  }

  ul,
  ol,
  dl,
  dd {
    margin: 0;
    padding: 0;
  }

  ul,
  ol {
    list-style: none;
  }

  h1,
  h2,
  h3,
  h4 {
    margin: 0;
    font-family: var(--sans);
    font-weight: 650;
    line-height: 1.14;
    text-wrap: balance;
    /* No automatic hyphenation: a German compound broken mid-word reads as a
       flaw, not as typesetting (27.08., operator). Words move to the next line
       instead. overflow-wrap stays as the backstop for the one case the old
       hyphenation actually earned its place in: a compound wider than a 320px
       column, which would otherwise scroll the page sideways. It only ever
       fires when a word does not fit a line on its own. */
    -webkit-hyphens: manual;
    hyphens: manual;
    overflow-wrap: break-word;
  }

  h1 {
    font-size: var(--step-5);
    line-height: 1.12;
    letter-spacing: var(--track-5);
  }

  /* The place line no longer steps down a size; it steps down in weight and
     keeps the display size, so the two halves of the H1 read as one block
     with a light second voice instead of as a heading with a subheading. */
  .h1place {
    display: block;
    margin-block-start: 0.1em;
    font-weight: 440;
  }

  h2 {
    font-size: var(--step-3);
    line-height: 1.1;
    font-weight: 650;
    letter-spacing: var(--track-3);
  }

  h3 {
    font-size: var(--step-1);
    font-weight: 650;
    letter-spacing: var(--track-1);
  }

  p,
  li,
  dd,
  dt {
    text-wrap: pretty;
    -webkit-hyphens: manual;
    hyphens: manual;
    overflow-wrap: break-word;
  }

  p {
    margin-block: 0 1.05em;
  }

  p:last-child {
    margin-block-end: 0;
  }

  a {
    color: inherit;
    text-decoration-thickness: 1px;
    text-underline-offset: var(--underline-run);
  }

  /* Buttons and the skip link carry their own colour on an accent surface;
     without the exclusion this rule would paint the focused skip link
     accent-deep on accent, which measures 1.3:1. Paper on accent holds in
     every concept: 5.13:1 salbei, 5.82:1 kreide, 6.74:1 porzellan. */
  a:not(.btn):not(.skip):hover {
    color: var(--accent-deep);
  }

  strong {
    font-weight: 650;
  }

  /* Phone links are the main call to action, so they never fall below the
     24px WCAG target height, not even inside running text. */
  a[href^="tel:"] {
    display: inline-block;
    padding-block: 0.35rem;
  }

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

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

  /* --- scroll reveals -----------------------------------------------------

     The 20.08. verdict still stands for scroll DRIVEN animation: two view()
     reveals cost about 175 ms of median blocking time on this page and took
     Lighthouse from 98 down to 93, because they run work on every frame. An
     IntersectionObserver that fires once per element and then stops watching
     it costs none of that. Three rules keep it honest: only opacity and
     transform move, so the layout never shifts; the hidden state exists only
     while JavaScript is actually running (html.js is set by the boot snippet
     in <head>, before first paint, so nothing flashes and a page without
     scripts simply shows everything); and none of it exists at all for a
     visitor who asked for less motion, which is why the hidden state lives
     inside the media query rather than relying on the blanket duration
     override above. */
  @media (prefers-reduced-motion: no-preference) {
    .js .reveal {
      opacity: 0;
      transform: translateY(12px);
    }

    .js .reveal.is-in {
      opacity: 1;
      transform: none;
      transition:
        opacity var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms),
        transform var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms);
    }
  }
}

/* ----------------------------------------------------------------- layout */

@layer layout {
  .section {
    display: grid;
    grid-template-columns:
      [full-start] minmax(var(--pad), 1fr)
      [content-start] minmax(0, var(--maxw))
      [content-end] minmax(var(--pad), 1fr)
      [full-end];
    padding-block: var(--sect);
  }

  .section > * {
    grid-column: content-start / content-end;
  }

  .section > .bleed {
    grid-column: full-start / full-end;
  }

  /* A hairline that starts the section and stays on the text grid. */
  .section--rule {
    padding-block-start: 0;
  }

  .section--rule > :first-child {
    border-block-start: 1px solid var(--rule);
    padding-block-start: var(--sect);
  }

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

  /* Baseline, not top: the right-hand column of a section almost always opens
     with a hairline and its own padding, so top-aligned boxes put the section
     heading a good two lines above the first line it belongs to (27.08.,
     operator: "komisch vertikal verschoben"). Baseline alignment lets the
     browser work the offset out per section instead of it being carried by a
     hand-set number that goes stale the moment a component changes its lead.
     Below 56.25em every column spans the full width and sits in a row of its
     own, where the rule has nothing to do. */
  .g12 {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--gutter);
    row-gap: clamp(1.6rem, 1rem + 2vw, 2.6rem);
    align-items: baseline;
  }

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

  /* Default: everything full width. Column spans switch on at 56.25em,
     which is 900px at the browser default size (Haertung 6). */
  .col-1-4,
  .col-1-5,
  .col-1-6,
  .col-1-7,
  .col-1-8,
  .col-4-12,
  .col-6-12,
  .col-7-12,
  .col-9-12 {
    grid-column: 1 / -1;
  }

  @media (min-width: 56.25em) {
    .col-1-4 {
      grid-column: 1 / span 4;
    }

    .col-1-5 {
      grid-column: 1 / span 5;
    }

    .col-1-6 {
      grid-column: 1 / span 6;
    }

    .col-1-7 {
      grid-column: 1 / span 7;
    }

    .col-1-8 {
      grid-column: 1 / span 8;
    }

    .col-4-12 {
      grid-column: 4 / -1;
    }

    .col-6-12 {
      grid-column: 6 / -1;
    }

    .col-7-12 {
      grid-column: 7 / -1;
    }

    .col-9-12 {
      grid-column: 9 / -1;
    }

    /* A picture column that stands LEFT of a text column it FOLLOWS in the
       markup (start page, 11.09.2026): a phone reads title, text and then
       the picture with its onward button, a desktop shows the picture beside
       the text. Auto-placement would put a later item in a lower row, so the
       row is named; --row2 for the section whose intro spans the row above. */
    .col-fig {
      grid-row: 1;
    }

    .col-fig--row2 {
      grid-row: 2;
    }

    /* The picture column of the services block runs the full height of the
       list beside it, so the button's lower edge meets the list's last rule
       (Martin 12.09.2026): the figure takes what the button leaves, and the
       landscape motif shows an upright crop of itself. */
    .col-fig.col-fig--row2 {
      align-self: stretch;
      display: flex;
      flex-direction: column;
    }

    .col-fig--row2 .plate {
      flex: 1 1 auto;
      min-block-size: 0;
      display: grid;
    }

    /* Martin, issues 59 and 60 (23.09.2026): on "Ueber uns" the text block
       beside a picture has to start on the picture's top edge. It did not,
       and the reason was measurable: the heading and the picture shared one
       column, so the row began at the heading and the baseline alignment of
       .g12 put the text's first line on the heading's first line, 36px above
       the picture at 1870 and 31px in the section below it. Fixed where both
       sections meet rather than per section: the heading keeps row one on its
       own, picture and text share row two, and both start at its top edge.
       Below 900px every column is full width in source order, so the class
       has nothing to do there and stays inside this query. */
    .row2 {
      grid-row: 2;
      align-self: start;
    }
  }

  /* Start, not baseline: a picture has no first line to share with the
     heading beside it, and a synthesised baseline at its foot would push the
     text column down by the picture's height. */
  .col-fig {
    align-self: start;
  }
}

/* ------------------------------------------------------------- components */

@layer components {
  /* --- masthead: wordmark, navigation, and the two ways to reach the firm

     Sticky from the top, but the pronta bar above it scrolls away first,
     because sticky only engages once the element reaches its own offset.
     The block size never changes: a head that shrinks when it sticks would
     pull every following line up by the difference, which is a layout shift
     on a page that measures zero. So it is compact from the start and only
     the ground under it changes when the page starts to run underneath.

     This is not the four-links-and-a-button bar. The wordmark keeps its own
     two line lockup, the navigation speaks in the site's spaced capitals
     rather than sentence case, the phone number is set as a number and not
     as an icon, and there is no rule along the foot: what marks the stuck
     state is the milk glass and a single soft shadow. */

  .mast {
    position: sticky;
    inset-block-start: 0;
    z-index: 10;
    display: grid;
    grid-template-columns:
      [full-start] minmax(var(--pad), 1fr)
      [content-start] minmax(0, var(--maxw))
      [content-end] minmax(var(--pad), 1fr)
      [full-end];
    padding-block: 0.55rem;
    background: var(--paper);
    transition: box-shadow var(--dur) var(--ease-out);
  }

  /* Milk glass, dosed: 82 percent paper, so the ground stays a paper tone
     and never turns into a pane of its own. At the top of the page it sits
     on the same paper and is invisible; it only becomes visible once there
     is something to blur behind it. The solid background above is the
     fallback wherever backdrop-filter is missing. */
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .mast {
      background: color-mix(in oklch, var(--paper) 82%, transparent);
      -webkit-backdrop-filter: blur(9px) saturate(1.3);
      backdrop-filter: blur(9px) saturate(1.3);
    }
  }

  /* js/site.js sets this once the pronta bar has left the screen. */
  .mast.is-stuck {
    box-shadow: 0 6px 20px var(--shade-far);
  }

  /* A head that cannot be scrolled past is the wrong trade on a short
     screen, which is also where 200 percent zoom lands, and since 11.09.2026
     equally wrong below 960px: there the dock at the foot of the screen
     carries number and request, and a two-row head pinned on top of that
     would hold a fifth of a phone screen for good. Both cases give the
     space back and let the head scroll away like any other block. */
  @media (max-height: 30rem), (max-width: 59.99em) {
    /* relative, not static: the head stops travelling with the page, which
       is the point of this query, and at the same time becomes the box the
       open menu hangs off (see the menu button block below). Relative keeps
       it in the flow exactly where static did and costs nothing; what it
       adds is that the z-index above starts to count, so the panel and the
       head lie over the page instead of under it. */
    .mast {
      position: relative;
    }

    /* Both declarations answer the same condition, so they sit in the same
       query: a head that travels with the page needs no scroll padding, and
       expressing the complement as its own media query would only invite the
       two bounds to drift apart. */
    html {
      scroll-padding-block-start: 0;
    }
  }

  .mast > * {
    grid-column: content-start / content-end;
  }

  .mast__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.15rem 0.75rem;
  }

  /* Below the width where all three parts fit one line, the navigation drops
     to a line of its own and the wordmark keeps the offer path beside it.
     The markup runs wordmark, offer path, navigation, which is exactly that
     reading order, so nothing is reordered here and the focus ring walks the
     head in the order the eye does (WCAG 2.4.3). */
  .mast__nav {
    flex-basis: 100%;
  }

  .mast__mark {
    margin-inline-end: auto;
  }

  .mast__acts {
    display: flex;
    align-items: center;
    gap: 0.4rem 0.7rem;
  }

  /* Number and request are the two button standards of the site, same type,
     same size (Martin 11.09.2026, after the elektromeier head): the number
     outlined, the request filled. Below 960px the head shows only the
     number; the request lives in the dock at the foot of the screen there,
     together with the number, so the head never needs a third row. Measured
     at 320px: wordmark plus number button leave 9px over. */
  .mast__acts .btn {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  /* Two classes, because .btn sets display further down in this layer and
     would otherwise win the tie. */
  @media (max-width: 59.99em) {
    .mast__acts .mast__cta {
      display: none;
    }
  }

  /* --- the menu button ---------------------------------------------------

     Martin, issue #54 (23.09.2026): "Startseite" joins the navigation, and
     the row has to stay one line on a phone, otherwise a hamburger. It does
     not stay one line, and that is measured, not assumed. The four words are
     the site's spaced capitals; laid out with no wrap they need 385px at the
     standing size and gap, while the content column holds 283px at 320 and
     323px at 360. Everything the type allows was tried: the gap down from
     1.75rem to 0.75rem and the tracking from 0.1em to 0.04em still need
     312px at 320. Only 10.9px type with 0.02em fits, which is below the
     smallest step of the scale and no longer reads as spaced capitals. So
     the fallback Martin named applies, and below 960px, where the head is
     static and the dock carries number and request anyway, the navigation
     goes behind one button.

     No framework and no layer of its own: the button is a button, the
     navigation is the same <nav> as on a desktop, and the boot snippet in
     <head> sets data-nav before the first paint, so a page without
     JavaScript simply keeps the navigation open and never shows the button.
     That is also what keeps the switch free of a layout jump. */
  .mast__burger {
    display: none;
    align-items: center;
    justify-content: center;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    padding: 0;
    border: 1.5px solid var(--accent);
    border-radius: 10px;
    background: var(--accent-faint);
    color: var(--ink);
    cursor: pointer;
    transition:
      background-color var(--dur) var(--ease-out),
      color var(--dur) var(--ease-out);
  }

  /* Three lines out of one box: the bar itself plus two shadows of it. */
  .mast__burger span {
    display: block;
    inline-size: 1.15rem;
    block-size: 1.5px;
    background: currentColor;
    box-shadow:
      0 -0.4rem 0 currentColor,
      0 0.4rem 0 currentColor;
  }

  /* Open reads in the same language as the secondary button under the
     pointer: the ground fills with the accent. Paper on accent holds in all
     three concepts (5.35 / 6.08 / 7.28:1). */
  .mast__burger[aria-expanded="true"] {
    background: var(--accent);
    color: var(--paper);
  }

  @media (max-width: 59.99em) {
    html[data-nav] .mast__burger {
      display: inline-flex;
    }

    html[data-nav] .mast__nav {
      display: none;
    }

    /* The open menu lies OVER the page, it does not push it. In the flow it
       moved the place band 197px down the moment the button was tapped,
       which is a layout jump and not what a hamburger is for. Absolute
       against the head (relative in the same query above), full bleed,
       links back on the content edge through the page padding. Nothing in
       the flow changes, so the measured shift is zero.

       Solid paper, not the milk glass of the head: this panel lies over
       running text and pictures, and at the 94 percent cover the glass
       would need, the small spaced capitals fall to 4.04:1 over a dark
       photograph, under the 4.5:1 they owe. The hairline and the shadow
       say "floats above" on their own. */
    html[data-nav] .mast__nav.is-open {
      position: absolute;
      inset-inline: 0;
      inset-block-start: 100%;
      z-index: 1;
      display: flex;
      flex-direction: column;
      gap: 0;
      padding: 0.15rem var(--pad) 0.9rem;
      border-block-start: 1px solid var(--rule);
      background: var(--paper);
      box-shadow: 0 12px 24px var(--shade-far);
    }
  }

  @media (min-width: 30em) {
    .mast__row {
      column-gap: 1.5rem;
    }

    .mast__acts {
      column-gap: 1.1rem;
    }
  }

  /* From 960px all three parts share one line with room to spare, and there
     the navigation belongs between the wordmark and the offer path, so this
     is the one width where the painted order differs from the markup. */
  @media (min-width: 60em) {
    .mast {
      padding-block: 0.9rem;
    }

    .mast__row {
      gap: 0.5rem 2.5rem;
    }

    .mast__nav {
      order: 1;
      flex-basis: auto;
    }

    .mast__acts {
      order: 2;
    }
  }

  /* The main navigation speaks the wordmark's language: spaced capitals in
     the site's one label style, no underlines, generous gaps. The current
     page carries ink and a small accent square, the site's marker shape
     (seal box, numbered squares); every link reserves the square's slot so
     nothing shifts between pages. Hover answers through the global
     accent-deep rule. */
  .mast__nav {
    display: flex;
    flex-wrap: wrap;
    /* 1.75rem, not the 1.35 of before: the current page's square now hangs
       into the gap in front of its word instead of standing in the line, so
       the gap has to carry the square as well. Measured at 320px, where the
       three words plus two gaps still leave 14px over. */
    gap: 0 1.75rem;
    font-family: var(--sans);
    font-size: var(--step--2);
    font-weight: 650;
    letter-spacing: 0.1em;
    text-transform: uppercase;
  }

  /* The navigation carries the project's 44px target like every other
     control on the page. It is a line of spaced capitals, so the height has
     to be asked for: the type alone would leave the row at 37px. */
  .mast__nav a {
    position: relative;
    display: inline-flex;
    align-items: center;
    min-block-size: 2.75rem;
    padding-block: 0.85rem;
    color: var(--ink-soft);
    text-decoration: none;
    transition: color var(--dur) var(--ease-out);
  }

  /* The marker hangs beside the word instead of standing in front of it.
     Every link used to reserve the square's slot so that nothing shifted
     between pages, and on a phone, where the navigation drops to a row of
     its own, that reserved slot indented all three words against the
     wordmark above them (27.08., operator). Taken out of the flow the square
     sits exactly where it always did, half an em to the left of the word,
     and the words themselves start on the content edge. */
  .mast__nav a::before {
    content: "";
    position: absolute;
    inset-inline-end: 100%;
    inset-block-start: 50%;
    margin-inline-end: 0.5em;
    translate: 0 -50%;
    inline-size: 0.5em;
    block-size: 0.5em;
    background: transparent;
    transition: background-color var(--dur) var(--ease-out);
  }

  /* Hover fills the square that the current page fills solid, one shade
     lighter: the answer uses the site's own marker, not an underline. */
  .mast__nav a:not([aria-current]):hover::before {
    background: color-mix(in oklch, var(--accent) 45%, var(--paper));
  }

  .mast__nav a[aria-current="page"] {
    color: var(--ink);
  }

  .mast__nav a[aria-current="page"]::before {
    background: var(--accent);
  }

  @media (min-width: 60em) {
    .mast__nav {
      gap: 0 2.2rem;
      letter-spacing: 0.14em;
    }
  }

  /* The masthead is the only element that truly persists across pages, so it
     is the only one that gets a name. The lead image differs on every page;
     morphing it would stretch one crop into another. */
  .mast {
    view-transition-name: mast;
  }

  .mast__mark {
    display: inline-block;
    inline-size: fit-content;
    font-family: var(--sans);
    line-height: 1.16;
    text-decoration: none;
  }

  .mast__mark b {
    display: block;
    font-size: var(--step-0);
    font-weight: 700;
    letter-spacing: 0.17em;
    text-transform: uppercase;
  }

  .mast__mark span {
    display: block;
    font-size: var(--step--2);
    font-weight: 550;
    letter-spacing: 0.26em;
    text-transform: uppercase;
    color: var(--ink-soft);
  }

  /* --- stage: the place band with the title lying on it ---------------

     Until 24.08. the title sat under the picture and the page opened like a
     municipal portal: landscape, then words. Two of Martin's colleagues named
     it independently. The title now lies ON the band, which is the one thing
     the reference does, and the band keeps its motif, its crop and its
     picture setup untouched.

     One grid, three tracks like every section, everything in row 1: the
     figure across the full bleed, the panel bottom left inside the content
     column, the seal bottom right. The picture sizes the row through its own
     aspect ratio, so as long as the panel is shorter than the picture the
     stage is exactly as tall as the band always was. When an edited title
     grows past that (the field takes 90 plus 90 characters), the row grows
     with it and object-fit keeps covering; nothing overflows and nothing
     below moves. Measured at 390px with the maximum text: 347px instead of
     292px. */

  .stage {
    display: grid;
    grid-template-columns:
      [full-start] minmax(var(--pad), 1fr)
      [content-start] minmax(0, var(--maxw))
      [content-end] minmax(var(--pad), 1fr)
      [full-end];
  }

  .stage > * {
    grid-row: 1;
  }

  /* The figure is a one cell grid of its own so the picture and the mark that
     edits it can share that cell. Until now the mark sat above the picture in
     the flow, which was right when the picture was just a band; on a stage
     that carries a panel and a seal, a strip of paper above the image would
     be the only thing in the composition that is not the picture. The mark
     goes to the top edge, the panel is at the bottom, and the two can never
     meet. */
  .stage > .herostrip {
    grid-column: full-start / full-end;
    display: grid;
  }

  .stage > .herostrip > picture,
  .stage > .herostrip > .pr-band {
    grid-area: 1 / 1;
  }

  /* <picture> is display: contents site-wide, which is right everywhere the
     image sizes itself; here the picture has to carry the cell height down
     to the image, so it becomes a box again. */
  .stage > .herostrip > picture {
    display: block;
    block-size: 100%;
  }

  /* .pr-band belongs to the tool layer and is placed there (css/tool.css):
     an unlayered rule outranks anything in a layer, so its geometry can only
     be corrected in its own file. */
  .stage > .herostrip > .pr-band {
    align-self: start;
    z-index: 2;
  }

  /* Haertung 1: the strip height is a ratio of its width, not of the
     viewport height. The old 28vh/38vh left a slit on a landscape phone,
     where 1vh is tiny while the width is huge. The phone gets its own
     4:3 crop via <picture>; the cut between the two crops sits at 45em
     and must match the media attributes in index.html. */
  .herostrip img {
    inline-size: 100%;
    aspect-ratio: 4 / 3;
    max-block-size: 26rem;
    object-fit: cover;
    object-position: 50% 58%;
  }

  /* min, not block-size: a minimum beats a maximum in CSS, so the picture
     fills the row even when an edited title has pushed the row past the crop
     cap above, while during row sizing the percentage still resolves to auto
     and lets the aspect ratio state the natural height. That is what keeps
     the stage exactly as tall as the band has always been in the normal
     case, with no gap in the grown one. */
  .stage > .herostrip img {
    min-block-size: 100%;
    /* the one liberty the truth boundary allows on this picture: a touch
       more bite under the panel, far short of blowing out the sky */
    filter: contrast(1.07) saturate(1.05);
  }

  @media (min-width: 45em) {
    .herostrip img {
      aspect-ratio: 12 / 5;
      max-block-size: 30rem;
    }
  }

  /* desktop: a fixed 3:1 whatever the window height, no cap (Martin #105,
     decision 178) */
  @media (min-width: 62.5em) {
    .herostrip img {
      aspect-ratio: 3 / 1;
      max-block-size: none;
    }
  }

  /* --- the title panel: milk glass, the site's one word for "floats" ---

     The site knows exactly one meaning for milk glass plus a shadow: this
     thing lies above the page. So far only the stuck masthead spoke it; the
     panel says the same thing, in the same voice, and mixes its ground out
     of --paper, which makes it follow all three concepts by itself. A dark
     box was the alternative and was dropped: on this bright alpine motif it
     reads heavy. */

  .stage__panel {
    grid-column: content-start / content-end;
    align-self: end;
    justify-self: start;
    z-index: 1;
    margin-block: 3rem 0.75rem;
    /* The lane on the right belongs to the seal. 5.875rem is not a round
       number but a measured one: it is the widest lane at which "im St.
       Galler Rheintal" still holds one line at 390px, which is the break the
       spec asks for, and it still leaves 18px of air beside the seal. */
    max-inline-size: calc(100% - 5.875rem);
    /* One value on every side the panel owns (the seal lane on the right is
       outside the box). Martin, 27.08.2026: left, bottom and top of the box
       must read as the same distance. --zone-pad hands the top value to the
       tool layer so switching the editing view off keeps it (tool.css). */
    --zone-pad: 1rem;
    padding: var(--zone-pad) 1rem 1rem;
    /* Two hairlines, not one (Martin, issue #53, 23.09.2026: "ein duenner
       Rand zur Abgrenzung und Erzeugung von Abhebung"). The inner one is the
       lit edge of the pane, mixed out of paper; the outer one is a barely
       visible ink ring that keeps the panel readable as an edge even where
       the photograph behind it happens to be as bright as the glass. The
       ink-14% line this replaces did the second job and not the first, and
       at the lower cover below it read as a drawn box. */
    border: 1px solid color-mix(in oklch, var(--paper) 62%, transparent);
    border-radius: 12px;
    background: color-mix(in oklch, var(--paper) 94%, transparent);
    box-shadow:
      0 0 0 1px color-mix(in oklch, var(--ink) 9%, transparent),
      0 8px 18px var(--shade-far);
  }

  /* Martin, issue #41 (20.09.2026): half the cover, so the picture shows
     through the glass. 82% became 41%, the blur grew from 10 to 12px so the
     landscape behind the title stays a wash rather than detail. Issue #53
     (23.09.2026) asks for another step, and 41% became 30%.

     Where 30 comes from: the picture under the panel can be swapped by the
     visitor, so the number is set by the worst photograph there is, a black
     one. Through 30 percent of paper that composites to a relative luminance
     of 0.28, which is 3.49:1 against the ink of all three concepts, above the
     3:1 the WCAG asks of large text (both lines are 24.8px and up). 24 percent
     would be 2.9:1 and is therefore out. Against the photograph actually
     delivered, measured at the rendered pixel (2nd percentile of the ground
     under each line, method of decision 85): 5.41 to 6.14:1 in salbei, kreide
     and porzellan at 320, 390, 1440 and 1870. The 94% fallback above stays:
     without blur, 30% would put the picture right under the letters. */
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .stage__panel {
      background: color-mix(in oklch, var(--paper) 30%, transparent);
      -webkit-backdrop-filter: blur(12px) saturate(1.3);
      backdrop-filter: blur(12px) saturate(1.3);
    }
  }

  /* Under 360px the lane does not work any more: "Umzugsreinigung" alone
     wants 182px and the panel beside the seal has 159, so the compound ran
     out of the box. Below that width the two stop sharing a line and share
     the picture instead, seal at the top right, panel across the full width
     at the foot. That turns five lines with an orphaned "und" into the same
     three the spec asks for at 390. The panel takes the upper layer for the
     editing state, where its own mark can grow it up into the seal.

     The switch sat at 386px until 27.08.2026 and now sits at 456px, because
     the lane got narrower: the pencil of the editing layer holds the bottom
     right corner of the screen, so the stage gives up 5.5rem of its right
     gutter to it (css/tool.css) and the seal comes left by that much. Measured
     against the same yardstick as before, the panel needs 453px of viewport
     to keep the width it had at 390 before the pencil existed, so 456 it is.
     Stacked below that, side by side above it. */
  @media (max-width: 28.5em) {
    .stage__panel {
      /* 6.5rem is measured, not chosen. Stacked, the seal column reaches
         12px of padding plus 9.6px of gap plus the seal itself, and the seal
         is 76px from 352px upward (only under that does it drop to 68), so
         its foot sits at 97.6px. A panel that starts at 104 clears it
         whatever the title does, and a title long enough to grow the panel
         simply grows the stage with it. The 4.5rem this replaces cleared
         only the seal's BUTTON, and a long saved title still ran the panel
         into the seal itself (critic: 357px of panel at 78 plus 80
         characters); 6rem then still fell 1.6px short at 360. The 0.75rem on
         top of the measured 6.5rem is breathing room between seal foot and
         panel edge, asked for by Martin on 27.08.2026. */
      margin-block-start: 7.25rem;
      max-inline-size: 100%;
      z-index: 2;
    }
  }

  @media (min-width: 45em) {
    .stage__panel {
      margin-block: 4rem 2rem;
      max-inline-size: min(44rem, 66%);
      /* Wider panel, wider padding: the top value rides along via --zone-pad,
         otherwise the tool layer pins it to the narrow-view 1rem. */
      --zone-pad: 1.3rem;
      padding: var(--zone-pad) 1.6rem 1.3rem;
    }
  }

  /* --- the seal: a stamp on the picture, bottom right ------------------

     It used to sit on the seam between picture and text, a circle halfway
     over a panorama, and that composition was itself a carrier of the
     municipal look. Now it lies fully on the picture, over meadow, opposite
     the panel on a shared baseline. No shadow: a stamp is flat, only
     floating things cast one.

     The column is stretched over the whole cell so the edit button can sit
     at the top edge while the seal stays at the bottom. That vertical
     distance is what keeps the button clear of the panel at every width,
     whatever the two of them happen to be wide. */

  .stage__seal {
    grid-column: content-start / content-end;
    justify-self: end;
    align-self: stretch;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: space-between;
    /* The seal lies at the top right of the picture (its mark is taken out
       of the flow by the tool layer, so the stamp is the column's only
       in-flow child and sits at the start). The room above it equals the
       room under the panel to the foot of the picture, 0.75rem here and 2rem
       from 45em, Martin 11.09.2026. Declared as --zone-pad so the tool layer
       keeps it with the marks off and adds the button row above it with the
       marks on (tool.css). */
    --zone-pad: 0.75rem;
    padding-block: var(--zone-pad) 0.75rem;
  }

  @media (min-width: 45em) {
    .stage__seal {
      --zone-pad: 2rem;
      padding-block: var(--zone-pad) 2rem;
    }
  }

  /* Hiding the seal must not move anything, so it goes invisible rather than
     away: the site measures zero CLS and the visitor may well switch it back
     on in the next breath. */
  html[data-seal-off] .stage__sealart {
    visibility: hidden;
  }

  .seal {
    inline-size: clamp(4.75rem, 3.4rem + 3.4vw, 7rem);
    block-size: auto;
  }

  /* The other half of the stacked arrangement (see the panel above): the
     seal leaves the foot and goes to the top right corner, where the panel
     can grow underneath it without ever reaching it. Written here rather
     than beside the panel rule, because .stage__seal is defined below that
     point and would otherwise simply win. */
  @media (max-width: 28.5em) {
    .stage__seal {
      align-self: start;
      justify-content: flex-start;
      gap: 0.6rem;
      /* Above the panel, not under it. The clearance above handles the
         clean state on its own; in the editing state the seal's own button
         makes the column half again as tall, and rather than push the panel
         down by that much every time the layer is switched on (which would
         stretch the picture by ninety-odd pixels in one frame), the seal
         simply stays on top. A stamp lying over the corner of a card is
         what a stamp does. */
      z-index: 3;
    }
  }

  @media (max-width: 22.49em) {
    .seal {
      inline-size: 4.25rem;
    }
  }

  /* The action row that used to follow the picture (button and number on a
     tinted band) went on 11.09.2026: the head carries both at every width,
     on phones together with the dock, so the row only said it twice
     (Martin). */

  /* The one and only circle on this site. */
  .seal__disc {
    fill: var(--accent);
  }

  .seal__ring {
    fill: none;
    stroke: var(--paper);
    stroke-width: 1;
    opacity: 0.45;
  }

  /* The arc is 147.7 units long (radius 47, half a turn). The delivered value
     went from "ABNAHMEGARANTIE" to "QUALITAETSVERSPRECHEN" on 27.08.2026, six
     letters more, and at the old 11 units with 1.3 of tracking that ran past
     the end of the arc. At 9.5 with 0.4 it measures 127 and sits inside the
     arc with room at both ends. The field's 22 character ceiling (js/tool.js)
     is a backstop, not a guarantee: 22 of the widest capital there is still
     measure 206 and wrap a little past the ends, the same as they did at the
     old size, only less. */
  .seal__arc {
    fill: var(--paper);
    font-family: var(--sans);
    font-size: 9.5px;
    font-weight: 650;
    letter-spacing: 0.4px;
  }

  /* A ticked box, the actual mark of a handover protocol, and a square, so
     the disc stays the only circle on the site. */
  .seal__box {
    fill: none;
    stroke: var(--paper);
    stroke-width: 2.4;
  }

  .seal__tick {
    fill: none;
    stroke: var(--paper);
    stroke-width: 3.6;
    stroke-linecap: butt;
    stroke-linejoin: miter;
  }

  /* --- buttons and telephone lines ------------------------------------ */

  /* Softened corners and a shadow at low opacity, so the button reads as a
     surface that lies on the paper rather than a stamped rectangle. No
     gradient, no pill, no bevel: the radius stays small enough that the
     button can never be mistaken for one of the magenta tool pills, which
     are fully rounded and belong to pronta, not to the firm. */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-block-size: 3rem;
    padding: 0.85rem 1.6rem;
    /* Same colour as the surface, so it is invisible here and gives the
       outlined variant below the exact same box: both standards of the
       site (filled, outlined) share one height (Martin, 11.09.2026). */
    border: 1.5px solid var(--accent);
    border-radius: 10px;
    background: var(--accent);
    color: var(--paper);
    font-family: var(--sans);
    font-size: var(--step-0);
    font-weight: 650;
    letter-spacing: var(--track-0);
    text-decoration: none;
    box-shadow: 0 1px 2px var(--shade-near);
    transition:
      background-color var(--dur) var(--ease-out),
      box-shadow var(--dur) var(--ease-out),
      translate var(--dur) var(--ease-out);
  }

  /* Hover darkens the same surface, it never inverts (Haertung 8), and
     lifts the button by a single pixel. */
  .btn:hover {
    background: var(--accent-deep);
    border-color: var(--accent-deep);
    color: var(--paper);
    translate: 0 -1px;
    box-shadow:
      0 2px 3px var(--shade-near),
      0 8px 18px var(--shade-far);
  }

  .btn:active {
    translate: 0 1px;
    box-shadow: 0 1px 2px var(--shade-near);
  }

  /* The second standard: outlined in the accent, ink text, flat. Used for
     the number in the head and the dock, and for the onward links under
     the pictures on the start page (Martin, 11.09.2026, after the
     elektromeier head).

     Since Martin's issue #55 (23.09.2026) the empty outline is filled: "in
     einer dezenten Milchglasoptik ausgefuellt ... und beim Mouseover in die
     gewaehlte Layoutfarbe wechseln". Both states move one step up. At rest
     the button carries the panel's milk glass, mixed out of accent-faint
     rather than out of paper, because over an opaque paper ground paper on
     paper would be invisible and there would be nothing to see; over the
     head and the dock, where there is a page running underneath, the blur
     makes it read as glass for real. Hover fills with the accent.

     This is the one place on the site where hover inverts instead of
     darkening the same surface. It is Martin's express wish, and paper on
     accent measures 5.35:1 salbei, 6.08:1 kreide and 7.28:1 porzellan, so
     the filled state is legible in every concept. The filled standard keeps
     accent-deep on hover, so the two standards never look alike, not even
     under the pointer. */
  .btn--line {
    background: var(--accent-faint);
    color: var(--ink);
    box-shadow: none;
  }

  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .btn--line {
      background: color-mix(in oklch, var(--accent-faint) 72%, transparent);
      -webkit-backdrop-filter: blur(10px) saturate(1.3);
      backdrop-filter: blur(10px) saturate(1.3);
    }
  }

  .btn--line:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--paper);
    box-shadow: none;
  }

  /* The masthead size: still the 44px target the project works to, but it
     no longer outweighs the wordmark next to it. */
  .btn--sm {
    min-block-size: 2.75rem;
    padding: 0.5rem 1.05rem;
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
  }


  /* The one label style on the site (Haertung 3). The uppercase treatment
     went on 25.08.2026: spaced capitals over a block of content is the eyebrow
     pattern the rules forbid, and it was also the fifth simultaneous text
     style in every view that carried one. Sentence case in the meta voice,
     told apart by weight and grey instead. Capitals now live only in the
     navigation and the wordmark. */
  .form label {
    font-family: var(--sans);
    font-size: var(--step--1);
    font-weight: 550;
    letter-spacing: 0.01em;
    color: var(--ink-soft);
  }

  .lede {
    font-size: var(--step-0);
    letter-spacing: var(--track-0);
    line-height: 1.55;
  }

  .svcs li {
    border-block-start: 1px solid var(--rule);
  }

  .svcs li:last-child {
    border-block-end: 1px solid var(--rule);
  }

  /* The whole line is the link, not the five words of its heading: the row
     carries the padding, and the negative inline margin lets the hover
     ground reach a little past the text on both sides. The second line
     keeps its quiet colour on hover, so only the name answers. */
  .svcs li > a {
    position: relative;
    display: block;
    padding-block: 0.95rem;
    padding-inline: 0.7rem 2.4rem;
    margin-inline: -0.7rem 0;
    border-radius: 8px;
    color: inherit;
    text-decoration: none;
    transition:
      background-color var(--dur) var(--ease-out),
      color var(--dur) var(--ease-out);
  }

  /* The row has to say that it leads somewhere while nobody is pointing at
     it: on a phone there is no hover to reveal it, and a tinted ground that
     only appears under the cursor is no affordance at all. Two hairlines
     meeting at a right angle, which is the site's own vocabulary, quiet
     enough at ink-quiet to stay out of the reading. Worst case across the
     three concepts: 3.56:1 on paper and 3.25:1 on the tinted band the list
     actually sits on, so it clears the 3:1 that WCAG 1.4.11 asks of a
     meaningful graphic. It steps aside on hover instead of growing. */
  .svcs li > a::after {
    content: "";
    position: absolute;
    inset-inline-end: 1rem;
    inset-block-start: 50%;
    inline-size: 0.5rem;
    block-size: 0.5rem;
    border-block-start: 1.5px solid var(--ink-quiet);
    border-inline-end: 1.5px solid var(--ink-quiet);
    translate: 0 -50%;
    rotate: 45deg;
    transition:
      border-color var(--dur) var(--ease-out),
      translate var(--dur) var(--ease-out);
  }

  /* The colour is set here rather than left to the global link hover: this
     rule sets color on the link itself, and a components layer declaration
     outranks the base layer answer no matter how specific that one is. */
  .svcs li > a:hover {
    background: var(--accent-faint);
    color: var(--accent-deep);
  }

  .svcs li > a:hover::after {
    border-color: var(--accent-deep);
    translate: 3px -50%;
  }

  .svcs h3 {
    font-size: var(--step-1);
    font-weight: 650;
    letter-spacing: var(--track-1);
    line-height: 1.2;
  }

  .svcs p {
    margin-block: 0.25rem 0;
    font-family: var(--sans);
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
    line-height: 1.5;
    color: var(--ink-soft);
  }

  /* --- detail rows on the inner pages ---------------------------------- */

  .rows {
    margin-block-start: clamp(1.5rem, 1rem + 1.8vw, 2.2rem);
  }

  .rows > li {
    display: grid;
    row-gap: 0.55rem;
    border-block-start: 1px solid var(--rule);
    padding-block: clamp(1.3rem, 1rem + 1.2vw, 1.9rem);
  }

  .rows > li:last-child {
    border-block-end: 1px solid var(--rule);
  }

  .rows {
    container: cq / inline-size;
  }

  @container cq (inline-size > 34rem) {
    .rows > li {
      grid-template-columns: 4fr 8fr;
      column-gap: var(--gutter);
    }
  }

  .rows p {
    max-inline-size: 62ch;
  }

  /* Martin, issue #42 (20.09.2026): the rows arrive one after another, not
     as a block. The observer in js/site.js hands a batch stagger of 70ms
     steps to --reveal-delay as an inline style; these rows override it with
     a fixed rhythm of their own, hence the !important, the one way a
     stylesheet beats an inline value without touching the script. Nothing
     here hides anything: the delay only exists inside the transition that
     the reduced-motion query already switches off. */
  .rows > li:nth-child(1) { --reveal-delay: 0ms !important; }
  .rows > li:nth-child(2) { --reveal-delay: 140ms !important; }
  .rows > li:nth-child(3) { --reveal-delay: 280ms !important; }
  .rows > li:nth-child(n + 4) { --reveal-delay: 420ms !important; }

  .rows h2,
  .rows h3 {
    font-size: var(--step-1);
    font-weight: 650;
    letter-spacing: var(--track-1);
    line-height: 1.14;
  }

  /* The fact lists (register year, legal form, staff count on ueber-uns;
     address, hours, phone, mail on kontakt) went on 13.09.2026 together
     with the staff switch: every line repeated the head, the lede or the
     footer (Martin, issues 27 and 30). */

  /* A one-person business has no teams, so the whole section can go. It leaves
     the flow and the accessible tree outright, unlike the seal: a section-sized
     hole is not a layout to keep. Only in the delivered state though, because
     the switch that brings it back lives inside it; while the editing layer is
     showing, the section stays and merely reads as switched off. */
  html[data-teams-off][data-marks="off"] #teams {
    display: none;
  }

  html[data-teams-off]:not([data-marks="off"]) #teams {
    opacity: 0.45;
  }

  /* --- contact map: a static OpenStreetMap excerpt, no live tile requests

     Two crops stitched once at build time (img/LICENSES.md), delivered as
     plain WebP, not a bordered box: this site's own idiom is a hairline,
     not a frame, so the picture sits flush like .plate and .strip. Pin,
     address flag and licence credit are HTML laid over the picture, so
     they stay sharp text at every width while the picture itself keeps
     its own width/height attributes for zero CLS. */

  .map__frame {
    position: relative;
  }

  .map__link {
    display: block;
    line-height: 0;
  }

  .map__link img {
    inline-size: 100%;
    block-size: auto;
  }

  /* Both crops are centred on the office, so one position serves both. */
  .map__pin {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    translate: -50% -100%;
  }

  .map__addr {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    translate: -50% 0.6rem;
    max-inline-size: min(13rem, 80%);
    margin: 0;
    padding: 0.3rem 0.6rem;
    border: 1px solid var(--ink);
    background: var(--paper);
    color: var(--ink);
    font-family: var(--sans);
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
    line-height: 1.4;
    text-align: center;
    text-wrap: balance;
  }

  /* ODbL 1.0 requires attribution wherever the map is shown. It carries no
     pr-* class, so it stays visible under html[data-marks="off"] too, the
     same treatment as .pr-proof on the team photo. */
  .map__credit {
    position: absolute;
    inset-block-end: 0.6rem;
    inset-inline-end: 0.6rem;
    margin: 0;
    padding: 0.05rem 0.4rem;
    background: var(--paper);
    color: var(--ink-soft);
    font-family: var(--sans);
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
    line-height: 1.5;
  }

  .map__credit a {
    display: inline-block;
    padding-block: 0.28rem;
    text-underline-offset: var(--underline-ui);
  }

  /* --- catchment area: the place list, plain text (map stays removed) --- */

  .places {
    columns: 2;
    column-gap: var(--gutter);
    margin-block-start: 1.4rem;
    font-family: var(--sans);
    font-size: var(--step-0);
    letter-spacing: var(--track-0);
  }

  .places li {
    border-block-start: 1px solid var(--rule);
    padding-block: 0.55rem;
    break-inside: avoid;
  }

  /* --- closing row of the offer sections --------------------------------

     Two filled buttons close the section on their own hairline row: the
     request and the call, side by side, both saying what they do (Martin
     11.09.2026: "klare Anweisungen", the grey number-plus-hours line was a
     duplicate of the head and is gone). */

  .closing {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem 1rem;
    border-block-start: 1px solid var(--rule);
    padding-block-start: clamp(1.3rem, 1rem + 1.2vw, 1.9rem);
  }

  /* Under 480px the two stack at full width; the narrower padding keeps
     the longer label on one line at 320px (measured: 245px of 251). */
  @media (max-width: 29.99em) {
    .closing .btn {
      flex: 1 1 100%;
      padding-inline: 0.9rem;
    }
  }

  /* --- process steps --------------------------------------------------- */

  /* Same lead as .rows: the section title stands directly above the steps
     since 11.09.2026 (Martin), not beside them. */
  .steps {
    margin-block-start: clamp(1.5rem, 1rem + 1.8vw, 2.2rem);
  }

  .steps li {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: clamp(1rem, 0.5rem + 2vw, 2.25rem);
    align-items: start;
    border-block-start: 1px solid var(--rule);
    padding-block: clamp(1.3rem, 1rem + 1.2vw, 2rem);
  }

  .steps li:last-child {
    border-block-end: 1px solid var(--rule);
  }

  .steps .num {
    font-family: var(--sans);
    font-size: var(--step-5);
    font-weight: 650;
    line-height: 0.95;
    letter-spacing: var(--track-5);
    /* Quiet, but still 3.36:1 against the paper: the ordinals are real text. */
    color: var(--ink-quiet);
    font-variant-numeric: tabular-nums;
  }

  .steps h3 {
    margin-block-end: 0.4rem;
  }

  .steps p {
    max-inline-size: 62ch;
  }

  /* --- leadership block ------------------------------------------------

     Name and role open the text column, above the two paragraphs, while the
     heading and the upright symbol portrait stand on the left (Martin
     12.09.2026, issues 24 to 26); the hairline they used to sit under and the
     closing button are gone with that move. */

  .person {
    margin-block-end: 1.2rem;
    font-family: var(--sans);
  }

  .person b {
    display: block;
    font-size: var(--step-1);
    letter-spacing: var(--track-1);
    font-weight: 650;
  }

  .person span {
    display: block;
    color: var(--ink-soft);
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
  }

  /* --- questions -------------------------------------------------------- */

  .faq details {
    border-block-start: 1px solid var(--rule);
  }

  .faq details:last-of-type {
    border-block-end: 1px solid var(--rule);
  }

  .faq summary {
    position: relative;
    padding-block: 0.95rem;
    padding-inline-end: 2.6rem;
    font-family: var(--sans);
    font-size: var(--step-0);
    letter-spacing: var(--track-0);
    font-weight: 650;
    cursor: pointer;
    list-style: none;
  }

  .faq summary::-webkit-details-marker {
    display: none;
  }

  .faq summary::before,
  .faq summary::after {
    content: "";
    position: absolute;
    background: var(--accent);
  }

  .faq summary::after {
    inset-inline-end: 0.4rem;
    inset-block-start: 50%;
    inline-size: 0.9rem;
    block-size: 2px;
    translate: 0 -50%;
  }

  .faq summary::before {
    inset-inline-end: 0.84rem;
    inset-block-start: 50%;
    inline-size: 2px;
    block-size: 0.9rem;
    translate: 0 -50%;
    transition: opacity 0.16s ease-out;
  }

  .faq details[open] summary::before {
    opacity: 0;
  }

  .faq summary {
    transition: color var(--dur) var(--ease-out);
  }

  .faq summary:hover {
    color: var(--accent-deep);
  }

  .faq summary:active {
    translate: 0 1px;
  }

  .faq .answer {
    padding-block: 0 1.25rem;
    max-inline-size: var(--measure);
  }

  /* --- form -------------------------------------------------------------- */

  .form {
    display: grid;
    gap: 1.15rem;
    margin-block-start: 1.6rem;
  }

  /* One exception to the baseline alignment of .g12, and the reason that rule
     says baseline rather than "first line": a form is not prose. Its opening
     line is a small label over a field, and a grid container hands the field's
     baseline up to its parent rather than the label's, so the heading beside
     it came to rest on the field instead of on the label. That section is
     top-aligned instead, and the form gives up its own lead, which lands the
     first label exactly on the heading's line. Written here, in the same layer
     as the margin it takes back, because a layer beats specificity. */
  @media (min-width: 56.25em) {
    .g12:has(.form) {
      align-items: start;
    }

    .form {
      margin-block-start: 0;
    }
  }

  @media (min-width: 42em) {
    .form {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      column-gap: var(--gutter);
    }

    .form .form__wide,
    .form .form__foot {
      grid-column: 1 / -1;
    }
  }

  .form label {
    display: block;
    margin-block-end: 0.4rem;
  }

  .form input,
  .form select,
  .form textarea {
    inline-size: 100%;
    min-block-size: 3rem;
    padding: 0.7rem 0.8rem;
    border: 1px solid var(--field-line);
    border-radius: 7px;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--sans);
    font-size: var(--step-0);
    line-height: 1.4;
  }

  .form textarea {
    min-block-size: 7rem;
    resize: vertical;
  }

  /* The pencil floats over the bottom right corner and the form runs all the
     way to the content edge, so on a phone it covered the right end of a
     select together with its chevron: a tap there hit the button, not the
     field (endcheck 27.08.). The form keeps the pencil's lane clear, and only
     as much of it as the page gutter does not already cover, which is nothing
     at all once the window is wide enough for the gutter to hold the button
     on its own. No breakpoint: the gutter is the wider of the page padding
     and half of what the window has over the measure, the same arithmetic the
     grid itself uses. */
  .form {
    padding-inline-end: max(
      0rem,
      4.75rem - max(var(--pad), (100vw - var(--maxw)) / 2)
    );
  }

  .form input[type="file"] {
    padding-block: 0.6rem;
    cursor: pointer;
  }

  .form input[type="file"]::file-selector-button {
    margin-inline-end: 0.8rem;
    padding: 0.4rem 0.9rem;
    border: 1px solid var(--field-line);
    border-radius: 7px;
    background: var(--surface);
    color: var(--ink);
    font-family: var(--sans);
    font-size: var(--step--1);
    cursor: pointer;
  }

  /* Progressive disclosure: the three object fields and the photo field only
     concern a request for a quote, so they wait until the visitor says that is
     what this is (js/site.js). Nothing above the switch moves, and without
     JavaScript no field is ever hidden, which is why the closed state lives in
     the script rather than in this file. The fade keeps the appearance from
     reading as a glitch; prefers-reduced-motion kills it through the blanket
     duration override in the base layer. */
  .form [data-case].is-shown {
    animation: form-case 200ms var(--ease-out) both;
  }

  @keyframes form-case {
    from {
      opacity: 0;
      translate: 0 -0.35rem;
    }
  }

  .form input,
  .form select,
  .form textarea {
    transition:
      border-color var(--dur) var(--ease-out),
      background-color var(--dur) var(--ease-out);
  }

  .form input:hover,
  .form select:hover,
  .form textarea:hover {
    border-color: var(--ink);
  }

  .form input:active,
  .form select:active,
  .form textarea:active {
    border-color: var(--accent-deep);
  }

  .form input:disabled,
  .form select:disabled,
  .form textarea:disabled,
  .btn:disabled {
    border-color: var(--rule);
    background: var(--surface);
    color: var(--ink-soft);
    cursor: not-allowed;
  }

  .form input:focus,
  .form select:focus,
  .form textarea:focus {
    border-color: var(--accent);
    background: var(--accent-faint);
  }

  /* Only complains once the field has actually been left, never while typing. */
  .form input:user-invalid,
  .form textarea:user-invalid {
    border-color: var(--accent-deep);
    border-inline-start-width: 3px;
  }

  .form textarea {
    field-sizing: content;
  }

  .form__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.9rem 1.6rem;
  }

  .form__note {
    max-inline-size: 66ch;
    margin: 0;
    font-family: var(--sans);
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
    line-height: 1.45;
    color: var(--ink-soft);
  }

  /* --- page heads on the inner pages ------------------------------------- */

  .pagehead {
    display: grid;
    grid-template-columns:
      [full-start] minmax(var(--pad), 1fr)
      [content-start] minmax(0, var(--maxw))
      [content-end] minmax(var(--pad), 1fr)
      [full-end];
    background: var(--surface);
    /* Equal above and below the head content: the head used to carry less on
       top than below, and once the tool's zone padding erasure was fixed the
       remaining imbalance was still visible. Martin, 27.08.2026: "Soett obe
       und unde glich si." --zone-pad doubles as the tool's design-padding
       handshake (tool.css). */
    --zone-pad: clamp(3.9rem, 2.7rem + 3.6vw, 5.1rem);
    padding-block: var(--zone-pad);
  }

  .pagehead > * {
    grid-column: content-start / content-end;
  }

  .pagehead .lede {
    margin-block-start: 1.1rem;
  }

  /* --- image figures on the inner pages ----------------------------------- */

  /* Two aspect ratios in the whole site: 3:2 for inset plates, 3:1 for the
     full bleed panorama bands. Enforced here, never by the image file. */
  .plate img {
    inline-size: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
  }

  /* A figure that fills the grid cell it stands in instead of sizing itself:
     the services picture beside its list (start page, height set by the
     list) and the two pictures of "So finden Sie uns" (contact page), which
     Martin wants at one top edge and one height (12.09.2026, issue 32). The
     map keeps its pixel scale, only the few lines of difference to the
     photo's 3:2 are cropped off it. */
  .fill {
    align-self: stretch;
  }

  /* Two rows: the first is the tool's "Foto aendern" mark (0 px while the
     editing layer is off), the second is the picture and takes the rest. The
     fr track matters: with plain auto rows the browser hands the free space
     to the empty mark row as well and the picture starts 130 px down. */
  .fill,
  .col-fig--row2 .plate {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
  }

  .fill img,
  .fill .map__frame,
  .col-fig--row2 .plate img {
    grid-row: 2;
    block-size: 100%;
    min-block-size: 0;
    aspect-ratio: auto;
    object-fit: cover;
  }

  .fill .map__link {
    block-size: 100%;
  }

  /* The paper grain that used to lie over every picture (a feTurbulence
     data URI at 3.5 percent, multiplied) went on 24.08.2026. It was the
     one device on the site that imitated print instead of using it, and on
     a throttled phone the noise had to be rasterised and blended across
     three full width figures for an effect nobody could name. */

  figcaption {
    margin-block-start: 0.7rem;
    font-family: var(--sans);
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
    line-height: 1.5;
    max-inline-size: 56ch;
    color: var(--ink-soft);
  }

  .strip img {
    inline-size: 100%;
    aspect-ratio: 3 / 1;
    object-fit: cover;
  }

  /* --- team figure: the container only ---------------------------------

     The team photo is a marked symbol image, but its duotone and the
     "Symbolbild" bar are drawn by the tool layer (package C) in the
     layer's own two tones, not in the site palette. This stays a plain
     relative container so that layer has something to attach to. */

  .teamfig {
    position: relative;
  }

  .teamfig img {
    inline-size: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
  }

  /* The two people pictures on the start page stand upright beside a block
     of text (Martin 11.09.2026: "in einem Hochformat"). 4:5, the classic
     portrait plate, so the picture and the three rows next to it end
     within a few lines of each other. */
  .teamfig--tall img {
    aspect-ratio: 4 / 5;
  }

  /* --- legal pages --------------------------------------------------------- */

  .legal h2 {
    margin-block: 2.4rem 0.8rem;
  }

  .legal h2:first-of-type {
    margin-block-start: 0;
  }

  .legal p,
  .legal li {
    max-inline-size: var(--measure);
  }

  .legal ul li {
    border-block-start: 1px solid var(--rule);
    padding-block: 0.55rem;
  }

  /* --- footer: two links and the copyright line -----------------------------

     The framed object sheet went on 27.08.2026. Every line it carried already
     stood either further up the page or on the contact page, and the two it
     did not repeat (insurance cover, room for an association mark) are exactly
     the two the operator said raise questions rather than answer them. What is
     legally required is the pair of links; the rest of the page is the firm. */

  .foot {
    display: grid;
    grid-template-columns:
      [full-start] minmax(var(--pad), 1fr)
      [content-start] minmax(0, var(--maxw))
      [content-end] minmax(var(--pad), 1fr)
      [full-end];
    /* Haertung 5: the last line of the page clears the home indicator on
       notch phones (viewport-fit=cover is set in the pages). A fixed
       bottom action bar, when one exists, must add the same env(). */
    padding-block: var(--sect) calc(clamp(2rem, 1.4rem + 2vw, 3rem) + env(safe-area-inset-bottom, 0px));
  }

  .foot > * {
    grid-column: content-start / content-end;
  }

  /* Below 960px the pencil sits above the dock, 8.7rem up from the foot of
     the screen; the status line runs the full width there, so the page end
     gets enough room for its last line to stop above the pencil. */
  @media (max-width: 59.99em) {
    .foot {
      padding-block-end: calc(clamp(2rem, 1.4rem + 2vw, 3rem) + 3rem + env(safe-area-inset-bottom, 0px));
    }
  }

  .foot__base {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.35rem 2.25rem;
    border-block-start: 1px solid var(--rule);
    padding-block-start: clamp(1.3rem, 1rem + 1.2vw, 1.9rem);
    font-family: var(--sans);
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
    line-height: 1.55;
    color: var(--ink-soft);
  }

  .foot__base p {
    margin: 0;
  }

  .foot__links {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1.6rem;
  }

  .foot__links a {
    display: inline-block;
    padding-block: 0.35rem;
    color: var(--ink);
    text-underline-offset: var(--underline-ui);
  }

  /* The status of the preview, decision 107 wording, under the link row on
     every page: same measure and line spacing as the status cell of the
     elektromeier footer (56ch, 1.6), in the quiet colour, the link in ink. */
  .foot__status {
    margin: 1.1rem 0 0;
    max-inline-size: 56ch;
    font-family: var(--sans);
    font-size: var(--step--1);
    letter-spacing: var(--track--1);
    line-height: 1.6;
    color: var(--ink-soft);
  }

  /* --- contact dock: the contact path on phones and tablets ---------------

     Not a pinned slip in the gutter (measured 2026: below roughly 1590px
     there is no gutter wide enough, so a slip always covered running words)
     but one bar along the foot of the screen, two equal actions, thumb zone,
     the market standard for local trades (decision 87, elektromeier). Below
     960px the head shows only the number, so the bar carries number and
     request together; from 960px up the head carries both and the bar goes.
     Milk glass like the head, a hairline on top, the site's own two button
     standards inside. The body makes room below the footer so the last line
     of the page never hides behind it. */

  .dock {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 10;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.6rem;
    padding: 0.6rem var(--pad) calc(0.6rem + env(safe-area-inset-bottom, 0px));
    border-block-start: 1px solid var(--rule);
    background: var(--paper);
  }

  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .dock {
      background: color-mix(in oklch, var(--paper) 82%, transparent);
      -webkit-backdrop-filter: blur(9px) saturate(1.3);
      backdrop-filter: blur(9px) saturate(1.3);
    }
  }

  /* Narrow padding so both labels hold one line at 320px (measured: 123px
     of 136 per button). */
  .dock .btn {
    min-block-size: 3rem;
    padding-inline: 0.5rem;
    white-space: nowrap;
  }

  @media (max-width: 59.99em) {
    body {
      padding-block-end: calc(4.2rem + 1px + env(safe-area-inset-bottom, 0px));
    }
  }

  @media (min-width: 60em) {
    .dock {
      display: none;
    }
  }
}

/* -------------------------------------------------------------- utilities */

@layer utilities {
  .skip {
    position: absolute;
    inset-block-start: 0.4rem;
    inset-inline-start: 0.4rem;
    z-index: 20;
    padding: 0.7rem 1rem;
    border-radius: 10px;
    background: var(--accent);
    color: var(--paper);
    font-family: var(--sans);
    font-weight: 650;
    text-decoration: none;
    translate: 0 -160%;
  }

  .skip:focus-visible {
    translate: 0 0;
    color: var(--paper);
  }

  .span-all {
    grid-column: 1 / -1;
  }

  .mt {
    margin-block-start: 1.1rem;
  }

  /* Martin, issue 57 (23.09.2026): the two plates beside the service list on
     "Leistungen" read vertically cramped on a desktop. Measured at 1870:
     17.6px from the intro to the first picture and the same from the first
     caption to the second picture, while the column had 380px of unused
     height beside the list. Both plates take the grid's own row distance
     instead of the 1.1rem step, which is 41.6px at 1870 and 25.6px at 900,
     so the air around them is the distance the page already keeps between
     rows. It belongs in this layer and not in layout, because the layer
     order gives utilities the last word over .mt whatever the selector. */
  @media (min-width: 56.25em) {
    .col-1-5 > .plate.mt {
      margin-block-start: clamp(1.6rem, 1rem + 2vw, 2.6rem);
    }
  }
}
