/* ==========================================================================
   Bierfux — bierfux.at
   --------------------------------------------------------------------------
   Hand-written and served as-is: there is no build step. See App\Support\Asset
   for why. Every colour below is taken from the app's own theme
   (bierfux-flutter/lib/core/theme/brand.dart) and its logo SVG, so the page and
   the screenshots sitting on it belong to one world.

   The app draws everything with RoundedRectangleBorder() at zero radius — every
   button, menu, input and snackbar. Nothing here has a radius either. That is
   the house style, not an oversight.
   ========================================================================== */

/* --- Fonts ---------------------------------------------------------------
   Committed under public/fonts/ and referenced relatively. No CDN, no Google
   Fonts, no Bunny: the privacy policy claims this page makes no third-party
   request, and self-hosting is what makes that sentence true rather than
   aspirational. Latin only for the display face — German and English need
   nothing outside it. */

/* Archivo — latin. Variable on both weight and width; the width axis is the
   whole reason this face is here. */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url('../fonts/archivo-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* IBM Plex Sans — latin-ext, kept for place names like Plzeň. */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('../fonts/plex-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('../fonts/plex-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* IBM Plex Mono — latin. Prices, currency codes, ABV, dates. */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/plex-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- Tokens -------------------------------------------------------------- */

:root {
  /* Brand, identical in both themes — exactly as the app pins them. */
  --amber: #ffac00;         /* brandSeed */
  --on-amber: #000000;
  --chrome: #5c5c5c;        /* headerGrey: the app bar in light AND dark */
  --on-chrome: #ffffff;
  --win: #70954b;           /* podium green, rank 1 by volume */
  --alc: #896c87;           /* podium purple, rank 1 by alcohol */
  --on-podium: #000000;

  /* Light surfaces. */
  --paper: #f5f5f5;
  --surface: #ffffff;
  --sunken: #ebebeb;
  --display: #432a2a;       /* the logo's linework brown */
  --text: #1a1a1a;
  --muted: #5c5c5c;
  --rule: #a7a7a7;
  --hairline: #d8d8d8;

  --measure: 34rem;
  --gutter: clamp(1.25rem, 5vw, 4rem);
  --band: clamp(3.25rem, 6.5vw, 6rem);

  --sans: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif;
  --display-face: 'Archivo', 'Arial Black', ui-sans-serif, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
}

/* The app's dark mode drops the seed tint entirely for a chroma-free grey ramp.
   Mirrored here. `:not([data-theme="light"])` lets the toggle override the system
   preference in both directions. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --paper: #131313;
    --surface: #1b1b1b;
    --sunken: #0b0b0b;
    --display: #e9dede;
    --text: #e6e6e6;
    --muted: #c6c6c6;
    --rule: #909090;
    --hairline: #414141;
  }
}

:root[data-theme='dark'] {
  --paper: #131313;
  --surface: #1b1b1b;
  --sunken: #0b0b0b;
  --display: #e9dede;
  --text: #e6e6e6;
  --muted: #c6c6c6;
  --rule: #909090;
  --hairline: #414141;
}

/* --- Reset --------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--text);
  font-family: var(--sans);
  font-size: 1.0625rem;
  line-height: 1.62;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

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

a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

a:hover {
  text-decoration-color: var(--amber);
  text-decoration-thickness: 3px;
}

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

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
}

/* --- Type --------------------------------------------------------------- */

.display {
  font-family: var(--display-face);
  font-weight: 800;
  font-stretch: 125%;
  text-transform: uppercase;
  line-height: 0.94;
  letter-spacing: -0.015em;
  color: var(--display);
  margin: 0;
  text-wrap: balance;
  /* A safety net for a word that genuinely cannot fit, but deliberately NOT
     `hyphens: auto`: on a hero set in expanded caps that hyphenates every line,
     and turned "Welches Bier ist wirklich billiger?" into six of them. Only the
     document titles opt into hyphenation — see `.doc h1`. */
  overflow-wrap: break-word;
}

h1.display {
  /* Capped at 4rem, not 5.25: the hero is two columns on a wide screen, so the
     headline gets about 560px, and "BILLIGER?" in expanded 900 needs roughly
     7em. Above 4rem the longest German word no longer fits on one line. */
  font-size: clamp(2.2rem, 5.2vw, 4rem);
  font-weight: 900;
}

h2.display {
  font-size: clamp(1.7rem, 3.6vw, 2.9rem);
}

h3.display {
  font-size: clamp(1.15rem, 1.9vw, 1.45rem);
  font-stretch: 112%;
  letter-spacing: 0;
}

/* An eyebrow says where you are, in the vocabulary of the thing it labels. */
.eyebrow {
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 0.75rem;
}

.lede {
  font-size: clamp(1.08rem, 1.5vw, 1.3rem);
  line-height: 1.5;
  color: var(--text);
  max-width: var(--measure);
}

.prose {
  max-width: var(--measure);
}

.prose p {
  margin: 0 0 1.1em;
}

.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum';
}

/* --- Shell -------------------------------------------------------------- */

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

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip {
  position: absolute;
  left: -9999px;
}

.skip:focus {
  left: var(--gutter);
  top: 0.5rem;
  z-index: 20;
  background: var(--amber);
  color: var(--on-amber);
  padding: 0.6rem 1rem;
  font-weight: 600;
}

/* The app bar is flat grey with white text in both themes, and casts no shadow.
   So does this one. */
.masthead {
  background: var(--chrome);
  color: var(--on-chrome);
  position: relative;
  z-index: 10;
}

.masthead__in {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 3.5rem;
  padding-block: 0.5rem;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  text-decoration: none;
  margin-inline-end: auto;
}

/*
 * The app bar puts the mark on a light plate rather than letting it sit on the
 * grey: home_screen.dart wraps a 24pt BierfuxLogo in a Container with
 * badgeBackground (#e6e6e6), 4pt of padding and a 6pt radius. Same proportions
 * here, radius included — that plate is the one rounded thing in the app, and
 * matching the app matters more than this page's own square rule. The mark's
 * linework stays black; inheriting the masthead's white was the bug.
 */
.brand svg {
  width: 1.5rem;
  height: 1.5rem;
  padding: 0.25rem;
  background: #e6e6e6;
  /* Black on the plate, as in the app. The footer overrides both. */
  color: #000000;
  border-radius: 6px;
  box-sizing: content-box;
  flex: none;
}

.brand__name {
  font-family: var(--display-face);
  font-weight: 800;
  font-stretch: 112%;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 1.1rem;
  color: var(--on-chrome);
}

.masthead a {
  color: var(--on-chrome);
}

.masthead__links {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  font-size: 0.85rem;
}

.masthead__links a {
  text-decoration: none;
  border-bottom: 2px solid transparent;
  padding-block: 0.15rem;
}

.masthead__links a:hover,
.masthead__links a[aria-current='page'] {
  border-bottom-color: var(--amber);
}

/* Language switch: two codes, the active one filled. Not a dropdown — with
   exactly two options a dropdown hides half the answer. */
.lang {
  display: flex;
  border: 1px solid rgba(255, 255, 255, 0.45);
}

.lang a {
  font-family: var(--mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.28rem 0.5rem;
  text-decoration: none;
}

.lang a[aria-current='true'] {
  background: var(--amber);
  color: var(--on-amber);
}

/* Theme toggle mirrors the app's own three-way setting: system, light, dark. */
.theme {
  display: flex;
  border: 1px solid rgba(255, 255, 255, 0.45);
}

.theme button {
  background: none;
  border: 0;
  padding: 0.3rem 0.4rem;
  line-height: 1;
  display: grid;
  place-items: center;
  color: var(--on-chrome);
}

.theme button svg {
  width: 1rem;
  height: 1rem;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
}

.theme button[aria-pressed='true'] {
  background: var(--amber);
  color: var(--on-amber);
}

/* --- Sections ----------------------------------------------------------- */

.section {
  padding-block: var(--band);
  border-top: 1px solid var(--hairline);
}

.section--flush {
  border-top: 0;
}

.section__head {
  margin-bottom: clamp(2rem, 4vw, 3.5rem);
}

/* Feature rows: text and phone, alternating side. */
.feature {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
  /* Top-aligned, not centred: a phone is about three times the height of the
     paragraph beside it, so centring leaves a band of dead space above and below
     the text and the section reads as unfinished rather than airy. */
  align-items: start;
  grid-template-columns: 1fr;
}

@media (min-width: 56rem) {
  /*
   * Placed by column, not by `order`. The two tracks are different widths — wide
   * for prose, narrow for a phone — so reordering the children alone swapped the
   * text into the narrow track on every mirrored row and gave the phone the wide
   * one. Naming the column each child belongs in keeps the prose measure
   * identical whichever side the screenshot is on.
   */
  .feature {
    grid-template-columns: 1fr minmax(13rem, 18rem);
  }

  /*
   * `grid-row: 1` is not decoration. Auto-placement walks the children in DOM
   * order and never moves its cursor backwards, so on a mirrored row the body
   * claims column 2 first and the phone — asking for column 1, which the cursor
   * has already passed — gets pushed onto a second row. The result was a
   * screenshot sitting underneath its text instead of beside it. Pinning both to
   * row 1 is what keeps them on one line.
   */
  .feature__body {
    grid-column: 1;
    grid-row: 1;
  }

  .feature .phone {
    grid-column: 2;
    grid-row: 1;
  }

  .feature--mirror {
    grid-template-columns: minmax(13rem, 18rem) 1fr;
  }

  .feature--mirror .feature__body {
    grid-column: 2;
    grid-row: 1;
  }

  .feature--mirror .phone {
    grid-column: 1;
    grid-row: 1;
  }
}

.feature + .feature {
  margin-top: var(--band);
}

/* A feature without a screenshot stands on its own rather than leaving a hole
   where the phone would be. Six identical rows would read as a template. */
.feature--solo {
  grid-template-columns: 1fr;
}

@media (min-width: 56rem) {
  .feature--solo,
  .feature--solo.feature--mirror {
    grid-template-columns: 1fr;
  }

  /* One track only, so column 2 does not exist — reset what .feature--mirror set. */
  .feature--solo .feature__body,
  .feature--solo.feature--mirror .feature__body {
    grid-column: 1;
    grid-row: auto;
    max-width: 46rem;
  }
}

.feature__body > * + * {
  margin-top: 1rem;
}

/* --- The tally ----------------------------------------------------------
   Feature sections are counted, not ordered. A tally is the app's own
   vocabulary — the Bierdeckel counts rounds this way — and unlike 01 / 02 / 03
   it claims nothing about sequence, which a feature list does not have. */

/* Sized by height, so a tally of one and a tally of six have the same stroke
   weight and only differ in width. Sizing by width instead scales the whole
   drawing, and a single upright would end up four times as fat as six of them. */
.tally {
  height: clamp(2.1rem, 2.8vw, 2.5rem);
  width: auto;
  margin-bottom: 1.1rem;
  overflow: visible;
}

.tally path {
  fill: none;
  stroke: var(--display);
  stroke-width: 3.5;
  stroke-linecap: round;
  /* Deliberately NOT vector-effect: non-scaling-stroke. That puts the dash
     pattern into screen pixels while the stroke width stays in user units, so
     the dasharray below stops matching pathLength and each stroke draws as a
     fat blob with a gap in it. */
}

/* Undrawn until the section arrives. The `.js` guard means a browser with
   JavaScript off — a Play reviewer's fetch, for instance — sees a finished
   tally rather than an empty box. */
.js .tally path {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset 0.42s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.js .is-in .tally path {
  stroke-dashoffset: 0;
}

/* --- Reveal ------------------------------------------------------------
   One gesture, used sparingly: a short rise and a fade. Applied only on the
   landing page, and only when JavaScript is running. */

.js .reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
}

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

/* --- Hero --------------------------------------------------------------- */

.hero {
  padding-block: clamp(3rem, 7vw, 6rem) var(--band);
}

.hero__grid {
  display: grid;
  gap: clamp(2.5rem, 5vw, 4rem);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 62rem) {
  .hero__grid {
    grid-template-columns: 1.25fr 0.95fr;
    align-items: center;
  }
}

.hero__claim {
  margin-top: 1.5rem;
}

.hero__note {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin: 1.5rem 0 0;
}

.cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  margin-top: 2rem;
}

/* An amber block with a black glyph, which is the app's floating action button
   at poster scale. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  background: var(--amber);
  color: var(--on-amber);
  border: 0;
  padding: 0.95rem 1.4rem;
  font-family: var(--display-face);
  font-weight: 800;
  font-stretch: 112%;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: 0.92rem;
  text-decoration: none;
}

.btn:hover {
  background: var(--display);
  color: var(--paper);
}

.btn svg {
  width: 1.2rem;
  height: 1.2rem;
  flex: none;
}

.btn--soon {
  background: none;
  color: var(--muted);
  border: 2px solid var(--rule);
  cursor: default;
}

.btn--soon:hover {
  background: none;
  color: var(--muted);
}

/* --- The comparison (the hero's whole argument) -------------------------
   Real arithmetic on real numbers: a €12.90 litre is cheaper per half-litre
   than a €4.80 third-litre, and the €4.80 one is cheaper per unit of alcohol.
   The answer flips with the mode, which is precisely why the app has two. */

.compare {
  border: 3px solid var(--display);
  background: var(--surface);
}

.compare__modes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-bottom: 3px solid var(--display);
}

.compare__mode {
  background: var(--surface);
  border: 0;
  border-inline-end: 1px solid var(--hairline);
  padding: 0.7rem 0.5rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.compare__mode:last-child {
  border-inline-end: 0;
}

.compare__mode[aria-pressed='true'] {
  color: var(--text);
  background: var(--sunken);
  box-shadow: inset 0 -4px 0 0 var(--amber);
}

.offer {
  display: grid;
  grid-template-columns: 2.75rem 1fr auto;
  align-items: center;
  gap: 0 1rem;
  padding: 1rem 1rem 1rem 0;
  border-bottom: 1px solid var(--hairline);
}

.offer:last-of-type {
  border-bottom: 0;
}

/* The rank badge, in the row's podium colour, as the app draws it. */
.offer__rank {
  align-self: stretch;
  display: grid;
  place-items: center;
  font-family: var(--display-face);
  font-weight: 900;
  font-stretch: 112%;
  font-size: 1.15rem;
  background: var(--rule);
  color: var(--on-podium);
  min-height: 3.5rem;
}

.offer[data-rank='1'] .offer__rank {
  background: var(--win);
}

.compare[data-mode='alcohol'] .offer[data-rank='1'] .offer__rank {
  background: var(--alc);
}

.offer__name {
  font-weight: 600;
  line-height: 1.25;
}

.offer__where {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  color: var(--muted);
  text-transform: uppercase;
}

.offer__raw {
  font-family: var(--mono);
  font-size: 0.82rem;
  color: var(--muted);
  margin-top: 0.15rem;
}

.offer__figure {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  line-height: 1.15;
}

.offer__price {
  font-size: clamp(1.15rem, 2.2vw, 1.5rem);
  color: var(--text);
  display: block;
}

.offer__unit {
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  color: var(--muted);
  text-transform: uppercase;
}

/* The figure arrives a beat after the page, so the comparison is seen being
   worked out. Without JavaScript it is simply already there. */
.js .compare--pending .offer__figure,
.js .compare--pending .offer__rank {
  opacity: 0;
}

.js .offer__figure,
.js .offer__rank {
  transition: opacity 0.4s ease 0.1s;
}

.compare__verdict {
  border-top: 3px solid var(--display);
  background: var(--sunken);
  padding: 0.85rem 1rem;
  font-size: 0.9rem;
}

.compare__verdict strong {
  font-weight: 600;
}

/* --- Phone frame -------------------------------------------------------
   Square corners and a hard border. The app has no rounded corners and this is
   an Android app; a rounded iPhone shell with a notch would misdescribe both. */

/* `width: 100%` with `justify-self` rather than `margin-inline: auto`: an auto
   margin switches a grid item out of stretch and into shrink-to-fit, and since
   the screenshot is a background on an empty div there is nothing to shrink to —
   the whole frame collapses to its own padding. */
/*
 * A device frame, and the corners are rounded on purpose. That does not
 * contradict the square rule the rest of the page follows: the phone is a
 * physical object and really is rounded, while the app's own UI inside it stays
 * hard-cornered — which is the part that would have been a lie in a rounded
 * iPhone mockup with a notch.
 *
 * `width: 100%` with `justify-self` rather than `margin-inline: auto`: an auto
 * margin switches a grid item out of stretch and into shrink-to-fit, and since
 * the screenshot is a background on an empty div there is nothing to shrink to —
 * the whole frame collapses to its own padding.
 */
.phone {
  position: relative;
  width: 100%;
  max-width: 15.5rem;
  justify-self: center;
  padding: 0.45rem;
  background: #2b2b2b;
  border-radius: 1.9rem;
  /* An inset hairline makes the body read as a rim rather than a flat block. The
     drop shadow is for a depicted object, not for UI chrome — the app has no
     elevation shadows and neither does anything else here. */
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.16),
    0 14px 30px -14px rgba(0, 0, 0, 0.55);
}

/* Power button and volume rocker on the right edge, as on the phone these were
   taken with. Decoration, so they are hidden from assistive tech by being
   pseudo-elements with no content. */
.phone::before,
.phone::after {
  content: '';
  position: absolute;
  right: -2px;
  width: 3px;
  background: #3a3a3a;
  border-radius: 0 2px 2px 0;
}

.phone::before {
  top: 21%;
  height: 7%;
}

.phone::after {
  top: 31%;
  height: 13%;
}

/*
 * The screenshot is a background image, not an <img>, so that one file is
 * fetched and it still swaps when the theme toggle is used. Each phone carries
 * both URLs as --shot-light / --shot-dark; the theme blocks below point --shot at
 * one of them, and because custom properties resolve where they are used, the
 * indirection picks up the per-element values. Two <img> tags would either
 * download both or stop responding to the toggle.
 *
 * The pairing is deliberately crossed: a light screen on a dark page, and a dark
 * screen on a light one. A phone screen emits light, so a bright screenshot on a
 * dark page reads as a screen that is actually switched on — which is the
 * situation the app gets used in, a dim bar. Matching them makes the phone
 * dissolve into the page.
 */
.phone__shot {
  width: 100%;
  aspect-ratio: 1080 / 1920;
  background-color: var(--sunken);
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
  border-radius: 1.5rem;

  /* Light page gets the dark screenshot; see the note above. */
  background-image: var(--shot-dark);
}

/*
 * The switch has to be made on .phone__shot itself, not by pointing a
 * :root-level --shot at one of them: a custom property's var() is substituted on
 * the element that declares it, so a --shot declared on :root would look for
 * --shot-dark on :root, not find it, and invalidate background-image entirely.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .phone__shot {
    background-image: var(--shot-light);
  }
}

:root[data-theme='dark'] .phone__shot {
  background-image: var(--shot-light);
}

:root[data-theme='light'] .phone__shot {
  background-image: var(--shot-dark);
}

.phone--wide {
  max-width: 24rem;
}

/* --- Lightbox -----------------------------------------------------------
   Only interactive when JavaScript is running, so the affordance and the frame's
   pointer only appear then — a zoom cursor on something that cannot zoom is a
   worse lie than no zoom at all. */

.js .phone {
  cursor: zoom-in;
}

.js .phone:hover {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.16),
    0 14px 30px -14px rgba(0, 0, 0, 0.55),
    0 0 0 2px var(--amber);
}

.js .phone:focus-visible {
  outline: 3px solid var(--amber);
  outline-offset: 5px;
}

.lightbox {
  border: 0;
  padding: 0;
  background: none;
  max-width: min(92vw, 26rem);
  max-height: 92vh;
  overflow: visible;
}

.lightbox::backdrop {
  background: rgba(0, 0, 0, 0.84);
}

.lightbox__img {
  display: block;
  width: 100%;
  height: auto;
  border: 3px solid #2b2b2b;
  border-radius: 1.4rem;
  background: var(--sunken);
}

.lightbox__close {
  position: absolute;
  top: -3rem;
  right: 0;
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  background: none;
  border: 1px solid rgba(255, 255, 255, 0.5);
  color: #ffffff;
}

.lightbox__close:hover {
  background: var(--amber);
  border-color: var(--amber);
  color: var(--on-amber);
}

.lightbox__close svg {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

/* --- Privacy section ---------------------------------------------------
   The most credible thing on the page, so it is stated as data rather than as
   marketing. */

/* The argument on one side, the numbers backing it on the other. */
.privacy {
  display: grid;
  gap: clamp(2.5rem, 5vw, 4.5rem);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 62rem) {
  .privacy {
    grid-template-columns: 1.05fr 0.95fr;
  }
}

/* Spacing is done entirely with child selectors, and the measure entirely with a
   descendant one, so the two cannot fight. `.privacy__say p` would otherwise win
   on specificity (0,1,1 against 0,1,0) and zero the gap the sibling rule sets —
   which is exactly what it did, running the two paragraphs together. */
.privacy__say > * {
  margin: 0;
}

.privacy__say > * + * {
  margin-top: 1.35rem;
}

/* The display face sets at line-height 0.94, so its glyphs sit low in their box
   and swallow part of the following margin. The heading block gets extra. */
.privacy__say > div + p {
  margin-top: 1.9rem;
}

/* Kept to a readable measure even though the column is wider than one. */
.privacy__say p {
  max-width: 32rem;
}

/* Stacked rather than three tiles side by side: in this column a 3-up would give
   two-and-one, and read as a row that ran out. A numeral against its label reads
   like the spec sheet it is. */
.facts {
  display: grid;
  gap: 1px;
  background: var(--hairline);
  border: 1px solid var(--hairline);
  grid-template-columns: 1fr;
}

.fact {
  background: var(--surface);
  padding: 1rem 1.25rem;
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  gap: 0 1rem;
  align-items: baseline;
}

.fact__value {
  font-family: var(--display-face);
  font-weight: 900;
  font-stretch: 118%;
  font-size: 2.1rem;
  line-height: 1;
  color: var(--display);
  margin: 0;
  text-align: center;
}

.fact__label {
  font-size: 0.9rem;
  color: var(--muted);
  margin: 0;
}

.privacy__show h3 {
  margin-top: 2.25rem;
}

.perm {
  font-family: var(--mono);
  font-size: 0.8rem;
  list-style: none;
  padding: 0;
  margin: 1rem 0 0;
}

.perm li {
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--hairline);
  display: flex;
  gap: 0.75rem;
  align-items: baseline;
}

.perm li::before {
  content: '·';
  color: var(--amber);
  font-weight: 700;
}

/* --- Story --------------------------------------------------------------
   Where the app came from. The heading on one side and the answer on the other,
   which is how the section uses the page's full width without setting prose at
   the 135 characters a line that width would otherwise give it. */

.story {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 62rem) {
  /* The narrower column is the heading's: it sets in expanded caps and breaks
     over two or three lines here, which is the point — the paragraphs get the
     wider track and land near the 34rem measure by themselves. Same breakpoint
     as the privacy grid, so the two sections change shape together. */
  .story {
    grid-template-columns: 0.9fr 1.1fr;
  }
}

/* Spacing between the two paragraphs only. The gap above them is the grid's in
   both layouts, so the display face's low-sitting glyphs never have a margin to
   swallow — see the note on .privacy__say > div + p. */
.story__body > p {
  margin: 0;
}

.story__body > p + p {
  margin-top: 1.35rem;
}

/* --- Footer -------------------------------------------------------------
   Not a band. A dark slab of chrome at the end of a long page reads as weight
   rather than as a close, which is what the grey one did and the brown one did
   worse. This sits a shade off the page, keeps the page's own text colour, and
   lets the columns do the work. */

.foot {
  background: var(--sunken);
  color: var(--text);
  border-top: 1px solid var(--hairline);
  padding-block: clamp(3rem, 6vw, 5rem);
  font-size: 0.95rem;
}

.foot__grid {
  display: grid;
  gap: clamp(2.5rem, 5vw, 4rem);
  grid-template-columns: 1fr;
}

@media (min-width: 46rem) {
  .foot__grid {
    grid-template-columns: 1.3fr 1fr 1fr;
  }
}

.foot h2 {
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 1.15rem;
}

.foot ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.foot li + li {
  margin-top: 0.7rem;
}

.foot a {
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
}

.foot a:hover {
  border-bottom-color: var(--amber);
  border-bottom-width: 2px;
}

/* The mark and the name once more, so the page closes on the thing it is about —
   with the app's light plate, which is exactly what a saturated band gives it
   something to lift off. */
.foot__brand {
  display: grid;
  gap: 1.1rem;
  align-content: start;
}

.foot .brand {
  color: var(--text);
  margin-inline-end: 0;
}

/* The app's AppBar proportions, one step larger: 24pt mark, 4pt padding, 6pt
   radius — see home_screen.dart. */
.foot .brand svg {
  width: 1.85rem;
  height: 1.85rem;
  padding: 0.3rem;
  background: #e6e6e6;
  color: #000000;
  border-radius: 7px;
  box-sizing: content-box;
  /* The app's plate needs no outline on a grey app bar. Here the light theme's
     band is #ebebeb, nine levels off the plate, so without this it dissolves. */
  box-shadow: 0 0 0 1px var(--hairline);
}

.foot .brand__name {
  color: var(--text);
  font-size: 1.3rem;
}

.foot__tagline {
  margin: 0;
  max-width: 24rem;
  color: var(--muted);
  line-height: 1.55;
}

.foot__fine {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.74rem;
  line-height: 1.9;
  letter-spacing: 0.02em;
  color: var(--muted);
}

/* --- Social ---------------------------------------------------------------
   Just the handle. An icon chip would be a third rounded thing on a page whose
   subject draws everything with hard corners, and "@bierfux" already says both
   which service and which account. */

.social__handle {
  font-family: var(--mono);
  font-size: 0.85rem;
  letter-spacing: 0.02em;
}

.foot__contact {
  color: var(--muted);
}

.foot__contact a {
  color: var(--text);
}

/* --- Documents ---------------------------------------------------------
   The legal pages. No reveal animation anywhere near them: a privacy policy
   that fades in is a privacy policy that might not. */

.doc {
  padding-block: clamp(2.5rem, 6vw, 5rem);
}

.doc__inner {
  max-width: 44rem;
}

/* Narrower than the marketing headlines: a document title has to survive
   "Datenschutzerklärung" inside a 44rem measure, and the expanded width that
   suits a hero does not fit here. */
.doc h1 {
  font-size: clamp(1.6rem, 3.4vw, 2.5rem);
  font-stretch: 100%;
  letter-spacing: -0.005em;
  /* "Datenschutzerklärung" is one 20-character word and has to be allowed to
     break, which is the only place on the site that needs this. */
  hyphens: auto;
}

.doc__meta {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin: 1rem 0 2.5rem;
  padding-bottom: 1.5rem;
  border-bottom: 3px solid var(--display);
}

.doc h2 {
  font-family: var(--display-face);
  font-weight: 800;
  font-stretch: 112%;
  text-transform: uppercase;
  letter-spacing: 0;
  font-size: clamp(1.05rem, 1.7vw, 1.25rem);
  line-height: 1.2;
  color: var(--display);
  margin: 2.75rem 0 0.9rem;
}

.doc h3 {
  font-size: 1rem;
  font-weight: 600;
  margin: 1.75rem 0 0.5rem;
}

.doc p,
.doc li {
  max-width: 40rem;
}

.doc p {
  margin: 0 0 1.1em;
}

.doc ul,
.doc ol {
  margin: 0 0 1.2em;
  padding-inline-start: 1.4rem;
}

.doc li {
  margin-bottom: 0.5em;
}

.doc code,
.doc .num {
  font-family: var(--mono);
  font-size: 0.92em;
  background: var(--sunken);
  padding: 0.1em 0.35em;
  /* The policy quotes real endpoints and real filesystem paths. A monospace run
     like /data/data/at.bierfux.app/databases/ has no break opportunity of its
     own, and pushed the whole page into horizontal scroll on a phone. */
  overflow-wrap: anywhere;
}

/* A definition-style block for the concrete, checkable facts — the endpoint,
   the permissions, the database name. */
.doc dl {
  margin: 0 0 1.5em;
  border-top: 1px solid var(--hairline);
}

.doc dt {
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  padding-top: 0.85rem;
}

.doc dd {
  margin: 0.2rem 0 0.85rem;
  padding-bottom: 0.85rem;
  border-bottom: 1px solid var(--hairline);
  max-width: 40rem;
}

.callout {
  border-inline-start: 4px solid var(--amber);
  background: var(--surface);
  padding: 1rem 1.25rem;
  margin: 0 0 1.5em;
}

.callout p:last-child {
  margin-bottom: 0;
}

.toc {
  font-size: 0.92rem;
  border: 1px solid var(--hairline);
  background: var(--surface);
  padding: 1.25rem 1.5rem;
  margin-bottom: 2.5rem;
}

.toc ol {
  margin: 0.5rem 0 0;
  padding-inline-start: 1.3rem;
}

.toc li {
  margin-bottom: 0.3em;
}

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

.gone {
  min-height: 70vh;
  display: grid;
  align-content: center;
  padding-block: var(--band);
}

.gone__code {
  font-family: var(--display-face);
  font-weight: 900;
  font-stretch: 125%;
  font-size: clamp(4rem, 16vw, 9rem);
  line-height: 0.85;
  color: var(--amber);
  margin: 0;
}

/* --- Reduced motion ---------------------------------------------------
   Everything renders in place, tally already drawn. Nothing moves. */

@media (prefers-reduced-motion: reduce) {
  .js .reveal,
  .js .reveal.is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .js .tally path {
    stroke-dashoffset: 0;
    transition: none;
  }

  .js .compare--pending .offer__figure,
  .js .compare--pending .offer__rank {
    opacity: 1;
  }

  .js .offer__figure,
  .js .offer__rank {
    transition: none;
  }

  * {
    scroll-behavior: auto !important;
  }
}

/* --- Print -------------------------------------------------------------
   Someone will print the privacy policy. It should look like a document. */

@media print {
  .masthead,
  .foot,
  .toc {
    display: none;
  }

  body {
    background: #fff;
    color: #000;
    font-size: 11pt;
  }

  .doc__meta,
  .doc h2 {
    color: #000;
  }
}
