/* The palette, 2026-09-20.

   The ground is neutral and follows the system: grey-white in the light,
   grey-black in the dark - never pure white or pure black, the way Apple's
   own grouped backgrounds are (#F2F2F7 / #1C1C1E). It used to be Midnight
   Navy everywhere; the blue stays for what it is FOR - the button, the
   active state, the logo's accents - and stops being the wall behind
   everything.

     Call Blue     #0461EA  the button, the active state
     Bright Blue   #2F80FF  highlights and hover
     Sky Blue      #76B6FF  the layer behind
     Ice Blue      #D9EBFF  very light surfaces

   The ramp keeps its old names because half the stylesheet refers to them,
   and the names were never about a hue: g900 is the ground, g100 the type
   on it. In the light the ramp simply runs the other way. */
:root{
  color-scheme:light dark;
  --g900:#1C1C1E; --g800:#2C2C2E; --g700:#3A3A3C; --g600:#8E8E93;
  --g500:#AEAEB2; --g400:#C7C7CC; --g200:#E5E5EA; --g100:#F2F2F7;
  /* The ground as a triplet, for the translucent pills and chips that sit
     over a picture; and the two ends of the page's own gradient. */
  --veil:28 28 30; --ground-top:#2C2C2E; --ground-bottom:#151516;
  /* The panel behind a face on the stage: a step off the ground, in
     whichever direction the ground is. */
  --panel-top:#2C2C2E; --panel-bottom:#1C1C1E;
  /* Hairlines and fills over the ground: light on dark, dark on light. */
  /* Hairlines and fills over the ground: light on dark, dark on light.
     Real values, not references to themselves: a palette rewrite once
     turned these into `--line:var(--line)`, which is invalid, and every
     border and every button fill in the dark vanished with it. */
  --line:rgba(255,255,255,.12); --fill:rgba(255,255,255,.1); --fill-strong:rgba(255,255,255,.14);
  --primary:#0461EA; --primary-bright:#2F80FF; --sky:#76B6FF;
  --error:#FF5630; --warning:#FFAB00; --success:#36B37E; --info:#00B8D9;
  /* The ten a line can wear. Ten navies once, which satisfied D-91 by the
     letter and nothing by the eye: side by side they were one colour.
     Robert, 28.09: the colours may be green and violet too.
     So it is the wheel, minus the band between yellow and olive where a
     colour dark enough for white lettering stops being a colour and
     becomes mud. Every one of the ten is tuned to the SAME contrast with
     white - 5:1 - so no line's initials are harder to read than another's,
     and the hues are placed to be as far apart from each other as ten
     hues can be. It opens on a blue beside the brand's, because the
     product is blue and the first line anybody makes should look it. */
  --avatar-1:#3969D9; --avatar-2:#8157D0; --avatar-3:#A24AB6;
  --avatar-4:#BC3E8C; --avatar-5:#C8395E; --avatar-6:#C84016;
  --avatar-7:#0E8202; --avatar-8:#068053; --avatar-9:#057B8C;
  --avatar-10:#0475B5;
  --sans:"Public Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  --disp:"Barlow","Public Sans",ui-sans-serif,system-ui,sans-serif;
  --mono:ui-monospace,SFMono-Regular,Menlo,monospace;
  /* The bar's edge: a line the eye can find, where --line is a whisper. */
  --line-strong:rgba(255,255,255,.38);
  /* What lifts a panel off the ground. */
  --lift:0 8px 28px rgba(0,0,0,.45);
  /* The voice, as light: cyan on a dark ground; a triplet so the alpha
     can be a calc() of the level. */
  --glow:0 184 217;
  --safe-b:env(safe-area-inset-bottom,0px);
  /* The browser's own chrome, as a length. `lvh` is the tall viewport with
     the address bar hidden, `svh` the short one with it showing; the
     difference is what a bar at the bottom has to clear so it is never under
     the browser's own. viewport-call carries the same token, and it is the
     reason its controls never end up behind Safari's. */
  --chrome:calc(100lvh - 100svh);
  --bar-space:calc(var(--chrome) + var(--safe-b) + 96px);
  --safe-t:env(safe-area-inset-top,0px);
}
@media (prefers-color-scheme:light){
  :root{
    --g900:#F2F2F7; --g800:#E5E5EA; --g700:#D1D1D6; --g600:#8E8E93;
    --g500:#6E6E73; --g400:#48484A; --g200:#2C2C2E; --g100:#1C1C1E;
    --veil:242 242 247; --ground-top:#FBFBFD; --ground-bottom:#E9E9EE;
    --panel-top:#D1D1D6; --panel-bottom:#C7C7CC;
    --line:rgba(0,0,0,.1); --fill:rgba(0,0,0,.06); --fill-strong:rgba(0,0,0,.09);
    --line-strong:rgba(0,0,0,.24);
    --lift:0 8px 28px rgba(0,0,0,.16);
    --glow:4 97 234;
  }
}
*{box-sizing:border-box}

/* The page appears once, complete. The script's first pass decides what
   is shown - the name field, the message button, the closed-for-the-night
   line - and drawing the HTML first and then correcting it was the second
   of three things that arrived one after another. Held back until the
   script says it has drawn, with a fallback that shows the page anyway
   if the script never gets to say so. */
#stage:not([data-ready]){opacity:0;animation:reveal-anyway 0s 1.2s forwards}
#stage[data-ready]{animation:reveal .18s ease-out}
@keyframes reveal-anyway{to{opacity:1}}
@keyframes reveal{from{opacity:0}to{opacity:1}}

/* [hidden] comes from the user agent stylesheet and loses against any rule
   of ours that sets display - which an id selector with display:grid is.
   One line, and the attribute means what it says everywhere. */
[hidden]{display:none!important}

/* Depth rather than a wall of one colour. A single flat navy over a whole
   phone screen reads as something that has gone wrong, which is exactly what
   Robert called it on 2026-09-19. The light comes from above, as it does in
   the logo's own shading. */
/* Pinned, like an app. No page scroll, no rubber band at the edges, no
   double-tap zoom: the one thing on this screen is a button that rings a
   telephone, and a page that slides around under the thumb while somebody
   is looking for it is a page, not a product. */
html,body{margin:0;position:fixed;inset:0;height:100%;overflow:hidden;
  overscroll-behavior:none;touch-action:manipulation;color:var(--g100);
  background:linear-gradient(180deg, var(--ground-top) 0%, var(--g900) 45%, var(--ground-bottom) 100%);
  background-attachment:fixed;
  font-family:var(--sans);-webkit-font-smoothing:antialiased;
  -webkit-text-size-adjust:100%;touch-action:manipulation}

/* One column, centred, with room for a thumb at the bottom. Every screen of
   this app is the same shape: who you are calling, what is happening, and
   one or two buttons. */
/* Full height on a phone, and the tall one: the picture fills the screen and
   the controls are lifted clear of the browser's chrome rather than the
   whole layout shrinking whenever the address bar appears. */
/* Content and button are ONE block, centred together. The first version
   gave the content all the leftover height and pinned the button to the
   bottom edge, which on a tall phone put half a screen of nothing between
   the name field and the thing to press. The first row can still shrink
   to nothing and scroll on a short screen with the keyboard up; the
   button stays below it either way. */
/* One column. The card lives BETWEEN the browser's bars (the small
   viewport) and is built to fit rather than to scroll: a flexible region
   holds the content and the bar together, centred, and scrolls as one
   piece only if it must; the footer sits at the bottom. The grid that was
   here - two empty flexible rows around the content - gave the empty rows
   everything the moment the content did not fit, and the content nothing:
   a circle cut in half, the bar under it, and a hole below. */
main{height:100svh;display:flex;flex-direction:column;
  padding:calc(var(--safe-t) + 16px) 20px calc(var(--safe-b) + 12px);
  max-width:460px;margin:0 auto;text-align:center}
main>footer{flex:none;margin-top:14px}
/* The scroller is here rather than on the page: on a short screen with the
   keyboard open this part moves and the button below stays put. */
/* NO `-webkit-overflow-scrolling: touch` HERE, and that is the point of
   this comment. It stood here until 10.10.2026 and it is not a scrolling
   hint on any iOS that still gets updates - momentum has been the default
   for overflow scrollers since iOS 13. What it still does in WebKit is
   give this element a composited layer, and a composited layer is a
   stacking context: everything inside it is painted at THIS element's
   level, whatever z-index it carries.
   Everything this page overlays the screen with lives inside here - the
   message thread, the chat, the end card, the nudge, the device list. So
   the line quietly made the scroller a lid: the moment one single element
   sat OUTSIDE it, that element painted over all of them, and a tap meant
   for the thread's writing line landed on the bar underneath instead
   (measured in Safari 18.4, iPhone 16 Pro). `tests/ebenen.test.js` keeps
   the lid off. */
#top{flex:1 1 auto;min-height:0;display:grid;align-content:center;justify-items:center;gap:14px;
  overflow:auto;overscroll-behavior:contain}
/* The bar is part of the centred block now, a breath under the content. */
#top>#actions{margin-top:4px}

.eyebrow{margin:0;font:500 .78rem/1.2 var(--sans);color:var(--g600);
  letter-spacing:.06em;text-transform:uppercase}
h1{margin:0;font-family:var(--disp);font-weight:700;letter-spacing:-.015em;
  font-size:clamp(1.7rem,1.2rem+2.6vw,2.5rem);line-height:1.12;
  text-wrap:balance;overflow-wrap:anywhere}
.sub{margin:0;max-width:34ch;font-size:.92rem;line-height:1.5;color:var(--g500)}
.slug{font:500 .82rem/1 var(--mono);color:var(--g600);word-break:break-all}

/* The dial. A ring that pulses while the far end rings, and nothing else
   moving on the page - a phone ringing is one event, not a light show. */
#dial{position:relative;width:clamp(88px,16svh,132px);height:clamp(88px,16svh,132px);display:grid;place-content:center;
  border-radius:50%;border:1px solid var(--fill);background:var(--g800);
  transition:transform .12s ease-out, box-shadow .12s ease-out;
  transform:scale(calc(1 + var(--level,0) * .06))}
/* No initials in it: this is the line being CALLED, and a telephone says
   that in one glyph, in any language, to somebody who has never seen this
   product before.

   The circle carries the LINE's colour, not the brand's (D-91). What the
   day before had got right, and what has to survive that change, is that
   people press this: it is round, it is big, it has a handset in it, and
   it sits above a button that does the thing. So the tinted circle is
   built the way the blue one was - a brighter top edge, the colour in the
   middle, a darker foot, a rim brighter than the fill - and only the hue
   comes from the line. A flat wash of a navy off the ramp reads as a
   label; this reads as a key. The light stops there: a fully modelled
   sphere was tried once and made the one thing somebody has to press look
   like a decoration. */
#dial{cursor:pointer}
#dial:not([data-tint]):not([data-photo]){
  background:linear-gradient(180deg, var(--primary-bright) 0%, var(--primary) 55%, color-mix(in srgb, var(--primary) 82%, black) 100%);
  border-color:color-mix(in srgb, var(--primary) 70%, white)}
#dial:active{transform:scale(calc(.97 + var(--level,0) * .06))}
/* background-COLOR and background-IMAGE separately, never the `background`
   shorthand: the shimmer that runs while a photograph is still coming
   replaces background-image, and a shorthand here would have taken the
   colour with it and left a transparent hole for as long as the wire is
   slow. */
#dial[data-tint]{
  background-color:var(--tint);
  background-image:linear-gradient(180deg,
    /* Lit with the line's OWN colour, not the brand's bright blue. That
       mix was free while all ten tints were navy; against the green and
       the violet it pulled the top of the circle back toward blue and the
       circle read as two colours. `--primary-bright` is `--primary` about
       a fifth of the way to white, so this is the same light. */
    color-mix(in srgb, var(--tint) 66%, white) 0%,
    var(--tint) 55%,
    color-mix(in srgb, var(--tint) 84%, black) 100%);
  border-color:color-mix(in srgb, var(--tint) 52%, white)}
/* A picture in the circle is a CROP of a photograph, not the top left
   corner of one at its natural size - which for a 1296x2304 picture of a
   plate on a white table is a plain white circle, and looked like a photo
   that had failed to load. Robert saw exactly that. */
#dial[data-photo]{background-size:cover;background-position:center}
/* While somebody is talking: the same idea as viewport-call's tiles, scaled
   and lit rather than a bar moving somewhere else on the screen. */
main[data-phase="live"] #dial,main[data-phase="ended"] #dial{
  box-shadow:0 0 0 calc(2px + var(--level,0) * 3px) rgba(0,184,217,calc(.25 + var(--level,0) * .5)),
             0 0 calc(18px + var(--level,0) * 30px) rgba(0,184,217,calc(var(--level,0) * .45))}
#dial[data-tint="1"]{--tint:var(--avatar-1)} #dial[data-tint="2"]{--tint:var(--avatar-2)}
#dial[data-tint="3"]{--tint:var(--avatar-3)} #dial[data-tint="4"]{--tint:var(--avatar-4)}
#dial[data-tint="5"]{--tint:var(--avatar-5)} #dial[data-tint="6"]{--tint:var(--avatar-6)}
#dial[data-tint="7"]{--tint:var(--avatar-7)} #dial[data-tint="8"]{--tint:var(--avatar-8)}
#dial[data-tint="9"]{--tint:var(--avatar-9)} #dial[data-tint="10"]{--tint:var(--avatar-10)}
/* With a picture the circle IS the picture: the glyph goes, the photograph
   fills the whole of it, and the ring around it stays so it still reads as
   the same object. */
/* The photograph comes in over the tint rather than replacing a circle
   that was already drawn: the third of the three arrivals, made a fade. */
/* The photograph arrives the way pictures arrive everywhere now: soft
   while it is still coming, then it settles sharp into place - and until
   then a light sweeps across the tinted circle so it reads as "loading",
   not as "empty". Nothing moves in the layout; only the picture inside.
   The sweep is a background layer on the circle itself: ::after belongs
   to the ringing pulse and must stay free, and nothing here may clip it. */
#dial::before{content:"";position:absolute;inset:0;border-radius:50%;
  background-image:var(--photo,none);background-size:cover;background-position:center;
  opacity:0;filter:blur(12px);transition:opacity .45s ease,filter .7s ease}
#dial[data-photo-ready]::before{opacity:1;filter:blur(0)}
#dial[data-photo]:not([data-photo-ready]){
  background-image:linear-gradient(110deg,transparent 30%,rgba(255,255,255,.16) 50%,transparent 70%);
  background-size:220% 100%;animation:shimmer 1.4s ease-in-out infinite}
@keyframes shimmer{from{background-position:120% 0}to{background-position:-120% 0}}
@media (prefers-reduced-motion:reduce){
  #dial::before{filter:none;transition:opacity .3s ease}
  #dial[data-photo]:not([data-photo-ready]){animation:none;background-image:none}
}
#dial[data-photo] .ico{display:none}
/* Behind a photograph: no line colour. The tint is for initials, where it
   tells one line from another; under a picture with transparent parts it
   showed as a blue disc behind the logo. A neutral ground, one step off
   the page's own, lit from above like the tile panels in a call. */
#dial[data-photo]{background-color:var(--g800);
  background-image:radial-gradient(120% 90% at 50% 20%,rgb(255 255 255 / .08),transparent 65%)}
#dial[data-photo]:not([data-photo-ready]){
  background-image:linear-gradient(110deg,transparent 30%,rgba(255,255,255,.16) 50%,transparent 70%),
    radial-gradient(120% 90% at 50% 20%,rgb(255 255 255 / .08),transparent 65%);
  background-size:220% 100%,100% 100%}
#about,#reassure{max-width:22rem;margin:0 auto;text-align:center}
/* The one sentence that only belongs to the wait: shown while it rings,
   gone in idle, live and ended. No colour and no size of its own - it is
   the same quiet .sub as the line's own description above it. A live line
   rings at nobody, so there it stays away too. */
#reassure{display:none}
main[data-phase="ringing"]:not([data-mode="live"]) #reassure{display:block}
#dial .ico{width:46px;height:46px}
#dial::after{content:"";position:absolute;inset:-10px;border-radius:50%;
  border:2px solid var(--primary);opacity:0;pointer-events:none}
main[data-phase="ringing"] #dial::after{animation:pulse 1.5s ease-out infinite}
main[data-phase="ringing"] #dial{background:color-mix(in srgb,var(--primary) 22%,var(--g800))}
/* Answered, media on their way: the card stays, the ring stops pulsing,
   and the circle takes the "on" tint - the stage follows in one piece
   when the sound is actually up. */
main[data-phase="connecting"] #dial,
main[data-phase="live"] #dial{background:color-mix(in srgb,var(--success) 20%,var(--g800))}
@keyframes pulse{0%{opacity:.8;transform:scale(.94)}100%{opacity:0;transform:scale(1.22)}}

/* The far end's voice, read from the receiver's RTP report - not from a Web
   Audio analyser on received media. It is a check that something is coming
   through, so it must not be able to light up when nothing is. */
#level{width:132px;height:4px;border-radius:2px;background:var(--line);overflow:hidden}
#level i{display:block;height:100%;width:0;background:var(--success);
  transition:width .12s linear}

.status{display:flex;gap:9px;align-items:center;justify-content:center;
  font-size:.88rem;color:var(--g400)}
.status:empty,.status:not(:has(:not([hidden]))){display:none}
.status .spinner{flex:none}
#clock{font:500 1.05rem/1 var(--mono);color:var(--g400);letter-spacing:.02em}
/* `auto` margins, not only centred text: with a 34ch cap and no margins the
   block sat at the left edge of the card and its centred lines read as
   off-centre (Robert, 16.10.2026, on the "lot of calls" notice). */
#notice{margin:0 auto;min-height:1.2em;font-size:.86rem;color:var(--error);
  max-width:34ch;text-align:center}
#notice[data-tone="info"]{color:var(--g500)}
/* Something that worked reads in the colour of things that worked, not in
   the red of things that did not. */
#notice[data-tone="good"]{color:var(--success)}

/* One spinner everywhere: a ring with one bright quarter reads as turning
   at any size, where a row of dots does not. */
.spinner{display:inline-block;width:15px;height:15px;border-radius:50%;
  border:2px solid rgba(255,255,255,.18);border-top-color:var(--primary);
  animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

label{display:block;font-size:.8rem;color:var(--g500);text-align:left;margin-bottom:6px}
/* One short thing goes in here, a name, so the field is the size of a name
   rather than the width of the screen. The app puts it in a list row, which
   is the same idea in Apple's vocabulary: a line, not a panel. Centred for
   the same reason, and the label sits on the same line where there is room.
*/
#notefield,#reasonfield{display:flex;flex-direction:column;align-items:center;gap:6px}
/* The owner's suggestions under the reason: tap to use one. */
#reasonchips{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;max-width:20rem}
#reasonchips[hidden]{display:none}
.reasonchip{font:inherit;font-size:.85em;color:inherit;background:var(--fill);border:1px solid var(--line);
  border-radius:999px;padding:5px 12px;cursor:pointer}
.reasonchip[aria-pressed="true"]{background:var(--primary-bright);border-color:transparent;color:#fff}
/* The reason wears the name's clothes exactly: two fields that look
   different read as two different kinds of thing, and these are one
   sentence in two halves. */
#note,#reason{text-align:center;max-width:15rem;padding:6px 4px;
  font-size:1.05rem;background:transparent;border:0;border-radius:0;
  /* --line-strong, not white at .22: on the light ground that line was
     invisible and a filled field read as plain text (31.10.2026). */
  border-bottom:1px solid var(--line-strong)}
#note:focus,#reason:focus{outline:none;border-bottom-color:var(--primary-bright)}
#notefield label{font-size:.82rem;color:var(--g500)}

/* The bell number, and it is a DOOR rather than a field on a form.
   Robert, 28.09: it should be seen first, and then the name and the call -
   so ring.js keeps everything below it hidden until it is answered, and this
   gives it the weight that goes with standing alone. It had no rules at all
   until now, which is why a page with a designed circle and a blue pill
   underneath had a grey browser box in the middle of it.
   The field borrows the line under #note and #reason so the three read as
   one family, and the button is the primary pill at the size the page uses
   everywhere else. */
#guard{display:flex;flex-direction:column;align-items:center;gap:10px;
  margin:6px 0 2px;width:100%}
#guardwhy,#guardask{max-width:19rem;text-align:center;margin:0}
#guardask{font-size:1.05rem;color:var(--g100)}
#guardrow{display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  justify-content:center;width:100%}
#guardanswer{text-align:center;width:min(11rem,60vw);padding:8px 4px;
  font-size:1.25rem;letter-spacing:.06em;background:transparent;color:var(--g100);
  border:0;border-radius:0;border-bottom:1px solid var(--line-strong)}
#guardanswer::placeholder{letter-spacing:.01em;font-size:1rem;color:var(--g500)}
#guardanswer:focus{outline:none;border-bottom-color:var(--primary-bright)}
#guardgo{height:46px;padding:0 20px;border-radius:999px;border:0;cursor:pointer;
  color:#fff;font:inherit;font-size:1rem;letter-spacing:.01em;
  background:linear-gradient(180deg, var(--primary-bright) 0%, var(--primary) 55%, color-mix(in srgb, var(--primary) 82%, black) 100%);
  box-shadow:0 8px 22px rgba(4,97,234,.3), inset 0 1px 0 rgba(255,255,255,.18)}
#guardgo:active{transform:scale(.98)}
/* A wrong answer is said quietly and in the warning colour, under the field
   it belongs to - not as a banner, because getting a number wrong once is
   ordinary and this page is somebody standing at a door. */
#guardnote{color:var(--warning);margin:0}
textarea,input[type=text]{width:100%;background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.16);color:var(--g100);border-radius:12px;
  padding:12px 14px;font:inherit;font-size:1rem;resize:none}
textarea::placeholder,input::placeholder{color:var(--g600)}
input:focus::placeholder,textarea:focus::placeholder{color:transparent}
.count{margin:6px 2px 0;font-size:.74rem;color:var(--g600);text-align:right}

/* One panel, always there, whatever state the call is in: idle it holds the
   Call button, ringing it holds Cancel, in a call the two circles. The thing
   under the thumb does not move or change its material when the state
   changes - only what is in it changes. */
#actions{display:flex;flex-direction:column;align-items:stretch;gap:10px;
  width:fit-content;max-width:min(520px,100%);margin-inline:auto;padding:10px;
  /* The radius is not a taste: for the gap around the buttons to be the
     same everywhere, it has to be the button's own radius plus the padding.
     27 (half of a 54 circle) + 10 = 37. Anything smaller pinches the
     corners, which is exactly what 30 did. */
  border-radius:37px;background:rgb(var(--veil) / .62);box-shadow:var(--lift);
  /* One border, and the visible one. The old faint line at 10 % survived
     the palette change as `var(--fill)` two declarations further down
     and won by being last - so the frame was there, at six per cent,
     which is to say not there. */
  border:1px solid var(--line-strong);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px)}
/* Everything in the panel is a row of circles now, so it keeps its shape
   in every state. */
/* In a call the two controls float as one pill, the way viewport-call's do:
   dark material with a blur behind it, so they stay legible over anything. */
/* The panel is on `#actions` now, so this is only the row inside it. */
#incall{display:flex;gap:12px;justify-content:center}
/* 52, as in viewport-call: six of them plus their gaps fit a 402pt iPhone
   exactly, and two look like a control rather than a pair of targets. */
#incall .round{width:52px;height:52px}
#incall .round .ico{width:23px;height:23px}
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
/* The "nobody answered" block needs the same grid as the actions it sits
   inside. As a plain div its rows stacked flush against each other, and
   "Call again" - a bare button rather than a .row - shrank to the width of
   its own text while every other control was full width. */
#vm{display:grid;gap:10px}
#vm > button{width:100%}
/* A player with controls has an intrinsic width of its own and will happily
   push the page sideways on a narrow screen. */
#preview{width:100%;min-width:0;display:block}
.row{display:flex;gap:10px;min-width:0}
button{border:0;border-radius:12px;padding:15px 20px;font:600 1rem/1 var(--sans);
  cursor:pointer;color:#fff;background:var(--g700);
  display:flex;align-items:center;justify-content:center;gap:9px}
button:disabled{opacity:.4;cursor:default}
/* The one button of the product, sized like the control it turns into.
   74px is the height of the in-call pill - 52 for the circles plus 10 of
   padding either side - so pressing Call does not make the bar jump, and
   the two read as the same object in two states. Fully round for the same
   reason. */
button.primary{height:54px;padding:0 22px;border-radius:999px;
  font-size:1.08rem;letter-spacing:.01em;
  background:linear-gradient(180deg, var(--primary-bright) 0%, var(--primary) 55%, color-mix(in srgb, var(--primary) 82%, black) 100%);
  box-shadow:0 10px 30px rgba(4,97,234,.35), inset 0 1px 0 rgba(255,255,255,.18)}
button.primary .ico{width:25px;height:25px}
button.primary:active{transform:translateY(1px)}
button.danger{background:var(--error)}
button.ghost{background:var(--fill-strong);color:var(--g100)}
/* Cancel is a cross, like every other control in here. A word in a row of
   circles is the odd one out, and this row is one press wide. */
#cancel{width:54px;height:54px;margin-inline:auto}
/* Call and the answering machine, side by side in the panel, the way a
   telephone puts them. */
/* 16 rather than 10: these two do different things, and a thumb aiming at
   Call must not land on the recorder. */
#idlerow{gap:16px;align-items:center;justify-content:center}
/* The recorder's own row: one round stop, centred, at the size of every
   other circle in this panel. */
#vm-start,#vm-recording,#vm-review{justify-content:center;gap:12px}
#vm .round{width:54px;height:54px}
/* One size for every glyph in the panel, measured rather than eyeballed:
   the paper plane and the cross are drawn to the edge of their box while
   the microphone and the receiver are not, so at one nominal size the plane
   reads LARGER. These two come down to match. */
#cancel .ico,#vm-drop .ico,#retry .ico{width:21px;height:21px}
/* The paper plane is a closed shape, so outlining it at 1.9 leaves a thin
   little arrow in a 54 circle: filled, not stroked. And larger than the
   others rather than equal to them - a triangle covers less of its own box
   than a receiver does, so at the same number it reads smaller. Rendered
   side by side at 24 against the receiver's 25 it still looked thin.
   Measured rather than argued about: the old dart covered 15.9% of its own
   box, the plane it was replaced with covers 26.1%, and the outlined
   triangle next to it has less ink than either. 26 filled. */
#send .ico{width:26px;height:26px;fill:currentColor;stroke-width:0}
/* Round and filled, like the Call button, so "send" is unmistakably the
   one that finishes this. */
.round.primary{background:linear-gradient(180deg, var(--primary-bright) 0%, var(--primary) 55%, color-mix(in srgb, var(--primary) 82%, black) 100%)}
#idlerow .round{width:54px;height:54px}
button.grow{flex:1 1 0;min-width:0}
button:hover:not(:disabled){filter:brightness(1.08)}
button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--primary);outline-offset:3px}

/* A round control, for mute and hang up during a call: the same shape the
   call app uses, so the gesture is already learned. */
/* Centred by the grid, not by the baseline: an inline SVG in a button sits
   on the text baseline with the line's own slack around it, which put the
   answering-machine glyph a hair left of and below the middle. */
.round{width:60px;height:60px;border-radius:50%;padding:0;flex:none;
  display:grid;place-content:center;line-height:0}
.round .ico{width:25px;height:25px}
.round[aria-pressed="true"],button.ghost[aria-pressed="true"]{
  background:var(--g100);color:var(--g900);border-color:transparent}

/* The level meter is the microphone glyph filling up, as it is in
   viewport-call. Not a separate bar: the question a person has while
   talking is "is any of this going out", and the answer belongs on the
   control that decides it.

   `currentColor` rather than a colour of its own, so the glyph goes from
   line to solid instead of something else arriving inside it. The clip is
   measured from the top of the 24-unit box: the capsule runs from 3 to 15,
   so 62.5% is empty and 12.5% is full. */
#mute{position:relative}
#mute .ico-level{position:absolute;left:50%;top:50%;margin:-11.5px 0 0 -11.5px;
  fill:currentColor;stroke:none;
  clip-path:inset(calc(62.5% - var(--level,0) * 50%) 0 0 0);
  transition:clip-path .09s linear}
#mute[aria-pressed="true"] .ico-level{display:none}

.ico{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.9;
  stroke-linecap:round;stroke-linejoin:round;pointer-events:none}
.round.danger .ico,#dial .ico{stroke-width:0;fill:currentColor}

/* Three small lines, each given its own room: the store link, then the
   legal links, then the build. Stacked tight they read as one grey lump. */
footer{font-size:.74rem;color:var(--g700);text-align:center;padding-top:8px}
footer a{color:var(--g600)}
/* The store link on its own line above, the legal links on ONE line, the
   build below - a grid here put "Imprint", the dot and "Privacy" on three
   lines of their own. */
footer .getapp{display:block;margin-bottom:12px;font-size:.86rem}
.build{display:block;margin-top:8px;font:400 .66rem/1 var(--mono);color:var(--g700);opacity:.7}

/* Reduced motion still has to say "something is happening" - it breathes
   instead of turning, rather than becoming a static ring that reads as a
   spinner that has failed. */
@media (prefers-reduced-motion:reduce){
  *{transition:none!important}
  .spinner{animation:breathe 1.6s ease-in-out infinite}
  #dial::after{animation:breathe 1.6s ease-in-out infinite!important}
}
@keyframes breathe{0%,100%{opacity:.35}50%{opacity:1}}

/* The development device page. Deliberately plain: it is a test harness
   standing in for a phone, and it should never be mistaken for the app. */
.dev{max-width:720px;text-align:left}
.dev main{text-align:left}
.dev h1{font-size:1.5rem}
.dev .card{background:var(--g800);border:1px solid rgba(255,255,255,.08);
  border-radius:14px;padding:16px;margin:0 0 14px}
.dev .card h2{margin:0 0 10px;font:600 .95rem/1.3 var(--sans)}
.dev .grid{display:grid;gap:8px}
.dev .grid.two{grid-template-columns:1fr 1fr}
.dev label{margin-top:4px}
.dev pre{margin:0;padding:10px;background:#0d1117;border-radius:10px;
  font:400 .74rem/1.5 var(--mono);color:var(--g400);max-height:230px;
  overflow:auto;white-space:pre-wrap;word-break:break-all}
.dev .pill{display:inline-block;padding:3px 9px;border-radius:99px;
  font:600 .7rem/1.5 var(--sans);background:var(--g700)}
.dev .pill[data-on="yes"]{background:var(--success);color:#04291c}
.dev .pill[data-on="ring"]{background:var(--primary)}
.dev button{padding:11px 14px;font-size:.9rem}

/* Utility classes rather than style="" attributes. The pages ship with
   style-src 'self', which blocks an inline style attribute exactly as it
   blocks an inline script - and "just add 'unsafe-inline' for the two
   margins" is how a policy stops being one. Script-driven styling through
   el.style is a different thing and stays allowed. */
.sprite{display:none}
.dim{color:var(--g600)}
.wide{width:100%}
.free{max-width:none}
.gap-sm{margin-top:8px}
.gap-md{margin-top:10px}
.tight{margin:0 0 6px}
.tight-md{margin:0 0 10px}
.flat{margin:0}
.row-flex{display:flex;gap:8px;align-items:center}
.grow-1{flex:1}
.devmain{max-width:720px;display:block;text-align:left}

/* The imprint and the privacy page.

   Everything above is built for one sentence in the middle of a screen: a
   460px column, centred, in quiet grey. That is right for "who are you
   calling" and wrong for two thousand words of law, which came out as a
   narrow grey ribbon nobody would read. These pages get their own width,
   their own alignment and enough contrast to be read rather than skimmed. */
/* A legal page is a document, and it scrolls like one: the whole page,
   with the footer at its end. It used to sit in the ring page's fixed
   four-row grid, where a text taller than the screen ate the footer's
   row and the links at the bottom were cut off. */
body.legal{overflow:auto;-webkit-overflow-scrolling:touch}
.legal main{max-width:none;text-align:left;padding:0;height:auto;min-height:100%;display:block}
.legal #top{overflow:visible;min-height:0}
.legal #top{display:block;max-width:44rem;margin:0 auto;
  padding:clamp(28px,6vw,64px) clamp(20px,5vw,32px) 8px}
/* Left, all of it. `#top` centres its text for the caller's card, and on
   these pages that left a short paragraph in the middle and a long one
   apparently on the left, under centred headings (31.10.2026). */
.legal #top{text-align:left;justify-items:start}
.legal #top ul.sub{max-width:68ch;margin:0 0 12px;padding-left:1.2em;font-size:1rem;line-height:1.65;color:var(--g400)}
.legal #top ul.sub li{margin:0 0 6px}
.legal .eyebrow{margin-bottom:10px}
.legal h1{font-size:clamp(2rem,1.4rem+2.4vw,2.9rem);margin-bottom:6px}
.legal h2{margin:34px 0 10px;font:600 1.12rem/1.3 var(--disp);
  letter-spacing:-.01em;color:var(--g200)}
/* Body text people are expected to actually read: bigger, lighter on the
   eye, and a measure that does not run past 75 characters. */
.legal p.sub{max-width:68ch;font-size:1rem;line-height:1.65;color:var(--g400);
  margin:0 0 12px}
.legal p.sub strong{color:var(--g100);font-weight:600}
.legal a{color:var(--primary)}
.legal hr{width:100%;border:0;border-top:1px solid var(--g800);margin:44px 0 4px}
/* An address is several lines and means them. */
.legal .block{white-space:pre-line;color:var(--g200)}
/* The lead paragraph under the title: the short answer, before the long one. */
.legal .lede{font-size:1.1rem;line-height:1.6;color:var(--g200);max-width:64ch;
  margin-bottom:8px}
.legal footer{max-width:44rem;margin:0 auto;padding:0 clamp(20px,5vw,32px)
  calc(var(--safe-b) + 36px);text-align:center}
/* The footer is the site's, not the text's: grey like on every other page. */
.legal footer a{color:var(--g600)}

/* The one place the app is offered. Quiet on the ring page - somebody there
   is trying to reach a person, not shopping - and the only thing to do on
   the home page. */
.getapp img{width:18px;height:18px;vertical-align:-4px;margin-right:7px}
.getapp{display:inline-block;color:var(--primary);text-decoration:none;
  border-bottom:1px solid color-mix(in srgb, var(--primary) 40%, transparent)}
.getapp:hover{border-bottom-color:var(--primary)}
.getapp.big{font-size:1rem;padding:10px 0}
/* Said in one word beside the link, not hidden in a footnote: the app is
   a public test until the listing exists. */
.beta{display:inline-block;margin-left:8px;padding:2px 8px;border-radius:999px;vertical-align:1px;
  font:600 .68rem/1.3 var(--sans);letter-spacing:.06em;text-transform:uppercase;
  color:var(--primary);border:1px solid color-mix(in srgb,var(--primary) 55%,transparent)}
.sub.small{font-size:.82rem;color:var(--g500)}
#stores .getapp{font-size:.95rem;padding:8px 0}

/* Paused, or shut for the night. Not an error - the line works, it is just
   not ringing right now - so it is the ordinary text colour on a quiet
   ground rather than the red `#notice` uses. */
#shut {
  margin: 10px auto 0;
  padding: 7px 14px;
  max-width: 30ch;
  border-radius: 12px;
  background: rgb(255 255 255 / 0.08);
  color: var(--text);
  font-size: 0.92rem;
}

/* ================================================== the stage ==========
   From "connected" onwards the page is a stage: the form of viewport-call's
   single tile and self view, in this product's colours. docs/CALL-UI.md
   is the description; where the two disagree, viewport-call is right. */

/* The stage takes the whole screen - the tall viewport, edge to edge. */
#stage[data-phase="live"] #video {
  position: fixed; inset: 0; width: 100%; height: 100lvh; margin: 0; max-width: none;
  z-index: 1; background: var(--g900); overflow: hidden;
}
#stage[data-phase="live"] #top { padding: 0; gap: 0; }
#stage[data-phase="live"] #eyebrow, #stage[data-phase="live"] #label,
#stage[data-phase="live"] #dial, #stage[data-phase="live"] #about,
#stage[data-phase="live"] #asname,
#stage[data-phase="live"] #notefield, #stage[data-phase="live"] #reasonfield,
#stage[data-phase="live"] #shut,
#stage[data-phase="live"] #status { display: none; }
#video[hidden] { display: none; }

#theirvideo {
  width: 100%; height: 100%; object-fit: cover; object-position: center;
  display: block; background: var(--g900);
}
#video:not([data-video="on"]) #theirvideo { display: none; }

/* No picture: the line's circle in a panel - the tile-avatar of
   viewport-call. The circle swells and glows with their voice; 5 % is
   enough to read without the layout appearing to move.
   The glow is viewport-call's, at viewport-call's values. It was turned
   up once for the navy ground and was too much: "das war vorher schon
   besser" (Robert, 2026-09-20). */
#theirsoff {
  position: absolute; inset: 0; display: grid; place-content: center;
  pointer-events: none; container-type: size;
  /* Centred in what is actually free: the stage runs behind the browser's
     bars and under our own, and a circle centred in all of that landed
     under the clock and the bar on a phone. viewport-call's cover pads
     the same way. */
  padding-bottom: calc(var(--bar-space) + 24px);
  background:
    radial-gradient(115% 95% at 50% 32%, color-mix(in srgb, var(--tint, var(--avatar-1)) 30%, transparent) 0%, transparent 72%),
    linear-gradient(168deg, var(--panel-top) 0%, var(--panel-bottom) 62%);
}
#video[data-video="on"] #theirsoff { display: none; }
#theirface {
  --circle: min(40cqw, 55cqh, 96px);
  width: var(--circle); aspect-ratio: 1; display: grid; place-content: center;
  border-radius: 50%; letter-spacing: 0.02em; color: var(--g100);
  font: 600 calc(var(--circle) * 0.38) / 1 var(--sans);
  background: var(--tint, var(--avatar-1));
  background-size: cover; background-position: center;
  /* The circle this page already had, and that was right: a ring that is
     there at rest and grows with the voice, a bloom that starts at 18px.
     viewport-call's own numbers - a ring of level x 2.5px - come out at
     under a pixel for ordinary speech, which read as "hardly any
     activity" (Robert, 2026-09-20) next to a bar that showed 30 %. */
  /* More than the old circle: „darf deutlicher sein“ (Robert). The scale
     is the part that reads across a room. */
  transform: scale(calc(1 + var(--level, 0) * 0.12));
  box-shadow: 0 0 0 calc(2px + var(--level, 0) * 5px) rgb(var(--glow) / calc(0.25 + var(--level, 0) * 0.6)),
    0 0 calc(18px + var(--level, 0) * 48px) calc(var(--level, 0) * 4px) rgb(var(--glow) / calc(var(--level, 0) * 0.55));
}
#theirsoff[data-photo] #theirface { font-size: 0; background-color: var(--g800); }
#theirsoff[data-tint="1"]{--tint:var(--avatar-1)} #theirsoff[data-tint="2"]{--tint:var(--avatar-2)}
#theirsoff[data-tint="3"]{--tint:var(--avatar-3)} #theirsoff[data-tint="4"]{--tint:var(--avatar-4)}
#theirsoff[data-tint="5"]{--tint:var(--avatar-5)} #theirsoff[data-tint="6"]{--tint:var(--avatar-6)}
#theirsoff[data-tint="7"]{--tint:var(--avatar-7)} #theirsoff[data-tint="8"]{--tint:var(--avatar-8)}
#theirsoff[data-tint="9"]{--tint:var(--avatar-9)} #theirsoff[data-tint="10"]{--tint:var(--avatar-10)}

/* A picture needs the same signal: inset, so it reads as this picture
   lighting up rather than as a gap opening around it. */
#video[data-video="on"]::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  box-shadow: inset 0 0 0 calc(1px + var(--level, 0) * 3px) rgb(var(--glow) / calc(0.15 + var(--level, 0) * 0.6)),
              inset 0 0 calc(10px + var(--level, 0) * 40px) calc(var(--level, 0) * 6px) rgb(var(--glow) / calc(var(--level, 0) * 0.45));
}
/* While your tile is a card in the middle, theirs is dimmed, not hidden:
   it should stay visible that the call is still on. */
#stage[data-self="focused"] #theirvideo,
#stage[data-self="focused"] #theirsoff { filter: brightness(0.45) saturate(0.75); }

/* Whom you are calling: top centre, over the picture. */
#theirname {
  position: absolute; left: 50%; top: calc(var(--safe-t) + 14px); transform: translateX(-50%); z-index: 2;
  max-width: calc(100% - 120px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 6px 11px; border-radius: 999px;
  background: rgb(var(--veil) / 0.66); backdrop-filter: blur(12px); border: 1px solid var(--line-strong);
  box-shadow: var(--lift);
  font: 500 0.78rem / 1 var(--mono); letter-spacing: 0.03em; color: var(--g100);
}

#theirpaused {
  position: absolute; inset: 0; z-index: 2; display: grid; gap: 10px;
  place-content: center; justify-items: center; text-align: center;
  padding: 20px 20px calc(var(--bar-space) + 24px);
  background: rgb(var(--veil) / 0.38);
  backdrop-filter: blur(16px) saturate(0.6); -webkit-backdrop-filter: blur(16px) saturate(0.6);
}
#theirpaused .ico { width: 34px; height: 34px; opacity: 0.85; stroke-width: 2.4; }
#theirpaused p { margin: 0; font-weight: 600; font-size: 0.95rem; }
#theirpaused span { display: block; margin-top: 3px; font-weight: 400; font-size: 0.8rem; opacity: 0.68; }

/* ---- the self view: four sizes ---------------------------------------
   tile / smaller while the bar is hidden or it is dragged / a card in the
   middle when tapped. The easing overshoots slightly and settles. */
:root { --tile-top: calc(var(--safe-t) + 62px); --tile-bottom: calc(var(--bar-space) + 8px); }
#stage[data-hud="hidden"] { --tile-top: calc(var(--safe-t) + 14px);
  --tile-bottom: calc(var(--chrome) + var(--safe-b) + 16px); }
#me {
  position: absolute; right: 16px; top: var(--tile-top); z-index: 3;
  /* 22vw, not 26: on a 375-point phone the wider tile reached into the
     line's circle in the middle of the stage. */
  width: clamp(84px, 22vw, 180px); aspect-ratio: 3 / 4; border-radius: 14px; overflow: hidden;
  background: var(--g800); box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
  touch-action: none; cursor: pointer; user-select: none; -webkit-user-select: none;
  transition: left .38s cubic-bezier(.22,1.12,.36,1), top .38s cubic-bezier(.22,1.12,.36,1),
              right .38s cubic-bezier(.22,1.12,.36,1), bottom .38s cubic-bezier(.22,1.12,.36,1),
              width .3s cubic-bezier(.22,1,.36,1), height .3s cubic-bezier(.22,1,.36,1),
              border-radius .3s ease, box-shadow .3s ease;
}
#me.dragging { cursor: grabbing; transition: none; }
#stage[data-hud="hidden"] #me:not(.focused):not(.dragging) { width: clamp(76px, 19vw, 132px); }
/* Centred in the room between the badges and the bar, not in the whole
   picture: centred on the picture it ran under the bar on a small phone
   (iPhone mini, 2026-09-20). Width from the room's height, 3:4. */
#me.focused {
  /* 48 more at the bottom: the line-and-clock pill rides above the bar
     and the tile reached into it (Robert, 2026-09-20). */
  --room: calc(100lvh - var(--tile-top) - var(--tile-bottom) - 48px);
  left: 50% !important; top: calc(var(--tile-top) + var(--room) / 2) !important; right: auto !important; bottom: auto !important;
  transform: translate(-50%, -50%); width: min(78vw, var(--room) * 0.75 - 16px); border-radius: 22px; z-index: 7;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.7); cursor: default;
}
#myvideo { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Only the camera pointing at your own face is mirrored - see showMyself()
   in ring.js. On the rear one a mirror image shows the world the wrong way
   round, and any writing in it is unreadable. */
#myvideo.gespiegelt { transform: scaleX(-1); }
#me:not([data-video="on"]) #myvideo { display: none; }
/* Your own picture lights up with what is LEAVING this machine, only while
   the camera is on - with it off the circle underneath already carries it. */
#me[data-video="on"]::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 4; border-radius: inherit;
  /* The same gain the circle has, on a small tile: a thin bright line
     that is there at rest, and a bloom that reaches a third of the way
     in at full voice. */
  box-shadow: inset 0 0 0 calc(1px + var(--level, 0) * 3px) rgb(var(--glow) / calc(0.2 + var(--level, 0) * 0.7)),
              inset 0 0 calc(6px + var(--level, 0) * 28px) calc(var(--level, 0) * 5px) rgb(var(--glow) / calc(var(--level, 0) * 0.6));
}
#selfoff {
  position: absolute; inset: 0; display: grid; place-content: center; container-type: size;
  background:
    radial-gradient(115% 95% at 50% 32%, color-mix(in srgb, var(--g700) 30%, transparent) 0%, transparent 72%),
    linear-gradient(168deg, var(--panel-top) 0%, var(--panel-bottom) 62%);
}
#me[data-video="on"] #selfoff { display: none; }
#selfinitials {
  --circle: min(46cqw, 62cqh, 120px);
  width: var(--circle); aspect-ratio: 1; display: grid; place-content: center; border-radius: 50%;
  letter-spacing: 0.02em; color: var(--g100); font: 600 calc(var(--circle) * 0.38) / 1 var(--sans);
  background: var(--g700);
  /* The circle this page already had, and that was right: a ring that is
     there at rest and grows with the voice, a bloom that starts at 18px.
     viewport-call's own numbers - a ring of level x 2.5px - come out at
     under a pixel for ordinary speech, which read as "hardly any
     activity" (Robert, 2026-09-20) next to a bar that showed 30 %. */
  /* More than the old circle: „darf deutlicher sein“ (Robert). The scale
     is the part that reads across a room. */
  transform: scale(calc(1 + var(--level, 0) * 0.12));
  box-shadow: 0 0 0 calc(2px + var(--level, 0) * 5px) rgb(var(--glow) / calc(0.25 + var(--level, 0) * 0.6)),
    0 0 calc(18px + var(--level, 0) * 48px) calc(var(--level, 0) * 4px) rgb(var(--glow) / calc(var(--level, 0) * 0.55));
}
/* The head, for a caller who gave no name - and it has to be measured
   the same way the circle beside it is, in the tile's own units.
   `width: 38%` looked equivalent and was not: the percentage resolves
   against the grid area, the grid area is `place-content: center` and so
   is sized by this item, and an item sized by the thing it sizes has no
   answer. The track collapsed and the head was drawn up and to the left
   of the middle - only in the no-name case, because the initials circle
   is measured in `cqw`/`cqh` against `#selfoff` (`container-type: size`)
   and those are definite. Robert saw it on the call, 28.09.
   The three numbers are the circle's own, scaled by 38/46, so the head is
   the same share of every tile the circle would have been. */
#selfnobody { width: min(38cqw, 51cqh, 99px); height: min(38cqw, 51cqh, 99px); opacity: 0.55; }
/* One tool, top left, only while the tile is the card: back to the corner.
   The camera switch lived bottom right and is in the camera's chevron
   now: on a small phone it was too big for the tile and, made smaller,
   too small to hit (Robert, 2026-09-20). */
#selftools { position: absolute; inset: 0; pointer-events: none; }
#selftools button {
  position: absolute; width: 38px; height: 38px; border-radius: 50%; border: 0; cursor: pointer;
  display: grid; place-content: center; pointer-events: auto;
  background: rgb(var(--veil) / 0.62); color: #fff; backdrop-filter: blur(10px);
  transition: width .2s ease, height .2s ease;
}
#selftools .ico { width: 18px; height: 18px; }
#selftools #shrink { left: 9px; top: 9px; display: none; }
#me.focused #selftools #shrink { display: grid; }
#me.focused #selftools button { width: 40px; height: 40px; }
#me.focused #selftools .ico { width: 20px; height: 20px; }
#me.focused #selftools #shrink { left: 16px; top: 16px; }
#stage[data-hud="hidden"] #me:not(.focused) #selftools { opacity: 0; pointer-events: none; }

/* ---- the bar --------------------------------------------------------- */
/* Already where they will be: while the audio connects the bar sits at
   the stage's bottom edge, so the jump from the card to the stage moves
   the picture in but never the controls. */
#stage[data-phase="connecting"] #actions,
#stage[data-phase="live"] #actions {
  pointer-events: auto;
  position: fixed; left: 50%; transform: translateX(-50%);
  /* From the visible bottom edge: a fixed element is anchored above the
     browser's toolbar already, so adding the toolbar's height here (the
     formula viewport-call uses for a bar that is ABSOLUTE inside a 100lvh
     stage) put ours a toolbar too high. */
  bottom: calc(var(--safe-b) + 22px); z-index: 5; width: auto;
  padding: 10px; border-radius: 999px;
  background: rgb(var(--veil) / 0.62); backdrop-filter: blur(16px);
  border: 1px solid var(--line-strong); box-shadow: var(--lift);
  transition: opacity .3s ease, transform .3s ease;
}
#stage[data-phase="connecting"] #incall,
#stage[data-phase="live"] #incall { position: relative; }
#stage[data-hud="hidden"][data-phase="live"] #actions {
  opacity: 0; pointer-events: none; transform: translateX(-50%) translateY(8px);
}
/* The line and the clock, 10px above the bar, out of flow: they cost the
   bar no width on a phone that six buttons already fill exactly. */
#chip-time {
  position: absolute; bottom: calc(100% + 20px); left: 50%; transform: translateX(-50%);
  pointer-events: none; white-space: nowrap; display: flex; gap: 8px; align-items: baseline;
  max-width: min(320px, 86vw); font: 500 0.78rem / 1 var(--mono); letter-spacing: 0.03em;
  padding: 6px 11px; border-radius: 999px;
  background: rgb(var(--veil) / 0.66); backdrop-filter: blur(12px);
  border: 1px solid var(--line-strong); box-shadow: var(--lift);
}
#chip-time .room { color: var(--g400); overflow: hidden; text-overflow: ellipsis; min-width: 0; }
#chip-time .clock { color: var(--g100); font-variant-numeric: tabular-nums; flex: none; }
#chip-time .clock::before { content: "·"; margin-right: 8px; color: var(--g600); }
#stage[data-phase="live"] #notice {
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 6;
  /* Above the line-and-clock chip, which stands 20px over the bar: at
     122px the two were drawn on top of each other (screen tree, 30.10.2026). */
  bottom: calc(var(--safe-b) + 150px); margin: 0;
  max-width: min(92vw, 460px); text-align: center; text-shadow: 0 2px 14px rgba(0, 0, 0, 0.85);
}
/* The path, bottom right, out of the way: a fact about the call, not a
   warning about it. Small, no ground, no word. */
#stage[data-phase="live"] #callinfo {
  position: fixed; bottom: calc(var(--safe-b) + 26px); right: 14px; left: auto; top: auto;
  z-index: 5; pointer-events: none;
}
#stage[data-hud="hidden"][data-phase="live"] #callinfo { opacity: 0; }
/* The shield alone: filled is direct, hollow is relayed. No word and no
   ground - it is a mark in a corner, not a chip to read. */
#stage[data-phase="live"] #path { margin: 0; padding: 0; opacity: 0.45; background: none; border: 0; }
#stage[data-phase="live"] #path span { display: none; }
#stage[data-phase="live"] #shield { width: 14px; height: 14px; filter: drop-shadow(0 1px 5px rgba(0, 0, 0, 0.8)); }

/* ---- the bubble, and the button it points at ----------------------- */
#nudge {
  position: fixed; z-index: 8; max-width: min(300px, 86vw); padding: 11px 14px; border-radius: 14px;
  background: var(--g100); color: var(--g900); font-size: 0.87rem; line-height: 1.35; font-weight: 500;
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.55); transform: translate(-50%, -100%);
}
#nudge::after {
  content: ""; position: absolute; left: 50%; bottom: -7px; width: 14px; height: 14px;
  margin-left: -7px; background: var(--g100); transform: rotate(45deg); border-radius: 3px;
}
#nudge button {
  margin-left: 10px; border: 0; background: none; color: var(--g600); cursor: pointer;
  font: 600 0.82rem / 1 var(--sans); padding: 2px 4px;
}
#incall .round.wants { background: var(--primary-bright); color: #fff; animation: wants 1.6s ease-in-out infinite; }
@keyframes wants {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--primary-bright) 70%, transparent); }
  50%      { box-shadow: 0 0 0 12px color-mix(in srgb, var(--primary-bright) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) { #incall .round.wants { animation: none; } }

/* Their state, top left: viewport-call's tile badges, in navy. Each one
   is a button - the badge is how you ask. */
#theirbadges { position: absolute; left: 10px; top: calc(var(--safe-t) + 10px); z-index: 2;
  display: flex; gap: 6px; align-items: center; }
#theirbadges span[role="button"] {
  display: grid; place-content: center; cursor: pointer; width: 30px; height: 30px; border-radius: 50%;
  background: rgb(var(--veil) / 0.72); backdrop-filter: blur(12px);
  border: 1px solid var(--line-strong); box-shadow: var(--lift); color: var(--g200);
}
#theirbadges .ico { width: 16px; height: 16px; }
#theirbadges span[role="button"]:hover { border-color: rgba(255, 255, 255, 0.4); }
#theirbadges span[role="button"]:active { transform: scale(0.97); }
#theirbadges span[data-pending] { border-color: rgb(var(--glow) / 0.6); color: rgb(var(--glow)); }
/* What came back, for a few seconds: a refusal, or that we are waiting. */
#ask-note { font: 500 0.72rem / 1 var(--mono); color: var(--g400); padding: 6px 10px; border-radius: 999px;
  background: rgb(var(--veil) / 0.66); border: 1px solid var(--line); }

/* Your own circle above the name, changing as you type: what the other
   end will see of you. Above rather than beside - a face and a field in
   one row read as a form with a decoration, one over the other read as a
   person being named. */
#namerow { display: flex; flex-direction: column; align-items: center; gap: 10px; }
#namerow input { width: 100%; min-width: 0; max-width: 100%; text-overflow: ellipsis; }
#mepreview { flex: none; width: 56px; height: 56px; border-radius: 50%; display: grid; place-content: center;
  background: var(--g700); color: var(--g100); font: 600 1.15rem / 1 var(--sans); letter-spacing: 0.02em;
  box-shadow: inset 0 0 0 1px var(--line); }
#mepreview .ico { width: 24px; height: 24px; opacity: 0.6; }

/* The end of a call, said over the page it happened on: the idle card is
   there behind the blur, ready to ring again, and a cross puts the sheet
   away. It blocks on purpose - the line "Call ended" used to sit quietly
   under the button, and a caller who looked up a second late did not
   know whether the call was still on. */
#endcard {
  position: fixed; inset: 0; z-index: 20; display: grid; place-content: center; gap: 14px;
  justify-items: center; text-align: center; padding: 24px;
  background: rgb(var(--veil) / 0.55); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
}
#endcard p { margin: 0; font: 600 1.25rem / 1.3 var(--sans); color: var(--g100); max-width: 26ch; }
#endcard-close {
  position: absolute; top: calc(var(--safe-t) + 16px); right: 16px;
  width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--line-strong);
  background: rgb(var(--veil) / 0.7); color: var(--g100); cursor: pointer;
  display: grid; place-content: center;
}
#endcard-close .ico { width: 20px; height: 20px; }
#endcard-action { margin-top: 6px; }
/* The line that says abuse can be reported. Quiet on purpose: `#endcard p`
   is the sheet's headline - 600 weight, 1.25rem - and a notice about abuse
   set in the same type would read as an accusation over a call that was
   almost always fine. It is the smallest thing on the sheet, under
   everything else, and a caller who needs it will still find it. */
#endcard .report { margin-top: 2px; max-width: 32ch;
  font: 400 0.78rem / 1.35 var(--sans); color: var(--g500); }
#endcard .report a { color: var(--g400); text-decoration: underline; }
#leave { display: grid; gap: 8px; justify-items: stretch; width: min(100%, 22rem); margin-top: 8px; }
#leave button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
#write { display: grid; gap: 10px; width: min(100%, 24rem); margin-top: 8px; }
#write textarea { width: 100%; box-sizing: border-box; resize: none; font: 400 0.95rem / 1.4 var(--sans);
  color: var(--g100); background: var(--fill); border: 1px solid var(--line-strong); border-radius: 12px; padding: 10px 12px; }
#write .write-row { display: flex; gap: 10px; justify-content: flex-end; }
#write .write-row button { display: inline-flex; align-items: center; gap: 8px; }

/* The live chat on a chat line: a log of bubbles, theirs on the left in
   the page's fill, mine on the right in the brand blue, and one row to
   type in. It takes the stage's middle where a picture would be. */
#chat { display: grid; grid-template-rows: auto 1fr auto; gap: 10px; width: min(100%, 26rem);
  height: min(52vh, 420px); margin-top: 8px; }
/* The head: the cross that ends it, top left, and the line with the clock
   beside it. Well away from Send. */
#chat-head { display: flex; align-items: center; gap: 12px; }
#chat-end { flex: none; width: 40px; height: 40px; border-radius: 50%; padding: 0;
  display: grid; place-content: center; background: var(--fill-strong); color: var(--g100);
  border: 1px solid var(--line-strong); cursor: pointer; }
#chat-end .ico { width: 20px; height: 20px; }
#chat-end:hover { background: var(--fill); }
#chat-title { display: flex; gap: 8px; align-items: baseline; min-width: 0;
  font: 500 0.82rem / 1 var(--mono); letter-spacing: 0.03em; }
#chat-room { color: var(--g400); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#chat-clock { color: var(--g100); font-variant-numeric: tabular-nums; flex: none; }
#chat-clock:not(:empty)::before { content: "·"; margin-right: 8px; color: var(--g600); }
#chat-log { overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding: 4px 2px;
  scroll-behavior: smooth; }
#chat-log .msg { max-width: 82%; padding: 8px 12px; border-radius: 16px; font: 400 0.95rem / 1.35 var(--sans);
  white-space: pre-wrap; overflow-wrap: anywhere; }
#chat-log .msg.them { align-self: flex-start; background: var(--fill-strong); color: var(--g100);
  border-bottom-left-radius: 6px; }
#chat-log .msg.me { align-self: flex-end; background: var(--primary); color: #fff;
  border-bottom-right-radius: 6px; }
#chat-log .sys { align-self: center; font: 400 0.8rem / 1.3 var(--sans); color: var(--g500); }
#chat-row { display: flex; gap: 8px; align-items: center; }
#chat-row input { flex: 1; min-width: 0; font: 400 1rem / 1.3 var(--sans); color: var(--g100);
  background: var(--fill); border: 1px solid var(--line-strong); border-radius: 22px; padding: 10px 14px; }
#chat-row input:focus { outline: none; border-color: var(--primary-bright); }
#chat-row .round { width: 44px; height: 44px; }
#chat-row .round .ico { width: 20px; height: 20px; }
/* Live, the chat IS the stage: it takes the whole screen the way the
   picture would, with the log filling it and the message row sitting just
   above the bar. Fixed and behind the bar (z-index 2 under its 5), so the
   page's footer is covered rather than collided with - which is what put
   the red button on top of the "get the app" line. Robert, 2026-09-21. */
#stage[data-phase="live"][data-mode="live"] #chat {
  position: fixed; top: var(--vv-top, 0px); left: 0; right: 0; z-index: 2;
  width: auto; height: var(--vvh, 100dvh); max-width: none; margin: 0;
  display: grid; grid-template-rows: auto 1fr auto; gap: 10px;
  padding: calc(var(--safe-t) + 14px) 16px calc(var(--safe-b) + 16px);
  background: linear-gradient(180deg, var(--ground-top) 0%, var(--ground-bottom) 100%);
}
#stage[data-phase="live"][data-mode="live"] #chat-log,
#stage[data-phase="live"][data-mode="live"] #chat-row {
  width: min(100%, 34rem); margin-inline: auto;
}
#stage[data-phase="live"][data-mode="live"] #chat-log { padding-bottom: 8px; }
#stage[data-phase="live"][data-mode="live"] #chat-head { width: min(100%, 34rem); margin-inline: auto; }
/* No bar in a chat: ending it is the cross in the head, and the line and
   the clock sit beside it. The handset bar below said "call". */
#stage[data-phase="live"][data-mode="live"] #actions { display: none; }
/* Nor the path's shield: in a chat it lay over the send button. */
#stage[data-phase="live"][data-mode="live"] #callinfo { display: none; }

/* A live line: the chat is the page from the first moment, over
   everything, whatever phase the call underneath is in. The line sent
   first starts that call, silently. Robert, 2026-09-22. */
#stage[data-chat="on"] #chat {
  position: fixed; top: var(--vv-top, 0px); left: 0; right: 0; z-index: 7;
  width: auto; height: var(--vvh, 100dvh); max-width: none; margin: 0;
  display: grid; grid-template-rows: auto 1fr auto; gap: 10px;
  padding: calc(var(--safe-t) + 14px) 16px calc(var(--safe-b) + 16px);
  background: linear-gradient(180deg, var(--ground-top) 0%, var(--ground-bottom) 100%);
}
#stage[data-chat="on"] #chat[hidden] { display: none; }
#stage[data-chat="on"] #chat-head, #stage[data-chat="on"] #chat-log, #stage[data-chat="on"] #chat-row {
  width: min(100%, 34rem); margin-inline: auto;
}
#stage[data-chat="on"] #actions, #stage[data-chat="on"] #endcard { display: none; }
/* What a live line's empty chat is for, until the first line (ring.js,
   openLiveStage). */
#chat-log .chat-hint { margin: auto 0; padding: 0 12px; text-align: center; color: var(--g400); font-size: 0.92rem; }
#chat-log .chat-hint:not(:only-child) { display: none; }

/* The written message's thread, on the sheet: the message as a bubble,
   the three moments under it (sent, on their phone, seen), the replies,
   and a row to write more. Same bubbles as the chat. */
/* The whole screen, like the chat: fixed over everything, the head with
   the cross top left, the message pinned, the replies scrolling, the row
   at the bottom. */
#thread { position: fixed; top: var(--vv-top, 0px); left: 0; right: 0; height: var(--vvh, 100dvh);
  z-index: 7; display: grid; grid-template-rows: auto 1fr auto; gap: 6px;
  padding: calc(var(--safe-t) + 8px) 16px calc(var(--safe-b) + 10px);
  /* The head's buttons must never widen the whole panel past the screen: the
     grid columns stay at the viewport and anything over spills inside, not
     out. Robert, 2026-09-22. */
  overflow-x: hidden;
  background: linear-gradient(180deg, var(--ground-top) 0%, var(--ground-bottom) 100%); }
#thread[hidden] { display: none; }
#thread-head, #thread-log, #thread-row { width: min(100%, 34rem); margin-inline: auto; min-width: 0; }
#thread-head { display: flex; align-items: center; gap: 10px; }
#thread-close { flex: none; width: 40px; height: 40px; border-radius: 50%; padding: 0;
  display: grid; place-content: center; background: var(--fill-strong); color: var(--g100);
  border: 1px solid var(--line-strong); cursor: pointer; }
#thread-close .ico { width: 20px; height: 20px; }
#thread-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 500 0.9rem / 1.2 var(--sans); color: var(--g300); }
/* Turn the chat into a call: round buttons at the end of the head. */
.head-call { flex: 0 0 38px; width: 38px; height: 38px; padding: 0; border-radius: 50%;
  display: grid; place-content: center; aspect-ratio: 1;
  background: var(--fill-strong); color: var(--g100); border: 1px solid var(--line-strong); cursor: pointer; }
.head-call[hidden] { display: none; }
.head-call .ico { width: 19px; height: 19px; }
/* The owner is in the thread: a green dot and two words, at the end of the head. */
/* No auto margin: with the title on flex:1 it fought for the free space and
   shoved the call buttons off the right edge. The title absorbs the slack and
   truncates; the dot and buttons keep their place. Robert, 2026-09-22. */
#thread-online { flex: none; display: inline-flex; align-items: center; gap: 6px;
  font: 500 0.78rem / 1 var(--sans); color: var(--g300); white-space: nowrap; }
#thread-online[hidden] { display: none; }
#thread-online i { width: 8px; height: 8px; border-radius: 50%; background: #34C759;
  box-shadow: 0 0 0 3px rgba(52, 199, 89, 0.22); }
/* The same under everything this page wrote later, in the scrolling log. */
#thread-log .state { align-self: flex-end; display: inline-flex; align-items: center; gap: 6px;
  font: 500 0.78rem / 1.3 var(--sans); color: var(--g400); margin-top: -2px; padding-right: 4px; }
#thread-log .state[data-stage="seen"] { color: var(--primary-bright); }
#thread-log .ticks { display: inline-flex; align-items: center; }
#thread-log .ticks .ico { width: 15px; height: 15px; stroke-width: 2.2; }
#thread-log .ticks .ico + .ico { margin-left: -9px; }
#thread-log .state[data-stage="sent"] .ticks { color: var(--g400); }
/* Held for the line's opening (D-177): one grey tick, like sent. */
#thread-log .state[data-stage="held"] .ticks { color: var(--g400); }
#thread-log .state[data-stage="delivered"] .ticks { color: var(--g400); }
#thread-log .state[data-stage="seen"] .ticks { color: var(--primary-bright); }
/* And in the live chat, under this page's own lines: the other side's
   acknowledgement on the channel is the tick. */
#chat-log .state { align-self: flex-end; display: inline-flex; align-items: center; gap: 6px;
  font: 500 0.78rem / 1.3 var(--sans); color: var(--g400); margin-top: -2px; padding-right: 4px; }
#chat-log .state[data-stage="seen"] { color: var(--primary-bright); }
#chat-log .ticks { display: inline-flex; align-items: center; }
#chat-log .ticks .ico { width: 15px; height: 15px; stroke-width: 2.2; }
#chat-log .ticks .ico + .ico { margin-left: -9px; }
#chat-log .state[data-stage="sent"] .ticks { color: var(--g400); }
#chat-log .state[data-stage="seen"] .ticks { color: var(--primary-bright); }
/* The time under every line, on the line's own side - a conversation then
   reads as two columns of times rather than a ladder down the middle.
   Robert, 11.10.2026: "so wie es halt bei chats wie imessage und so ist". */
#thread-log .msg-at, #chat-log .msg-at { font: 500 0.7rem/1.2 var(--sans); color: var(--g400);
  font-variant-numeric: tabular-nums; margin: -2px 2px 2px; }
#thread-log .msg-at.me, #chat-log .msg-at.me { align-self: flex-end; }
#thread-log .msg-at.them, #chat-log .msg-at.them { align-self: flex-start; }
/* The last line of a closed chat, in place of the composer (D-175): in the
   middle, quiet, in the time stamps' voice. */
#thread-log .msg-closed { align-self: center; font: 500 0.78rem/1.3 var(--sans); color: var(--g400);
  margin: 8px 0 4px; text-align: center; }
#thread-row[hidden] { display: none; }
/* Collapsed: the same state in a row shows once, under the last line. */
#chat-log .state[hidden], #thread-log .state[hidden] { display: none; }
#thread-log { display: flex; flex-direction: column; gap: 6px; min-height: 0;
  overflow-y: auto; padding: 2px; scroll-behavior: smooth;
  /* At rest the bar is out of the way; it shows only while the log is being
     scrolled, added and removed as a class by the page. Robert, 2026-09-22. */
  scrollbar-width: none; }
#thread-log::-webkit-scrollbar { width: 0; }
#thread-log.scrolling { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
#thread-log.scrolling::-webkit-scrollbar { width: 6px; }
#thread-log.scrolling::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }
/* Not at the bottom: a round button jumps the log back to the newest line. */
#thread-down { position: absolute; right: 18px; bottom: calc(var(--safe-b) + 74px); z-index: 8;
  width: 40px; height: 40px; border-radius: 50%; display: grid; place-content: center;
  background: var(--panel-top); color: var(--g100); border: 1px solid var(--line-strong);
  box-shadow: var(--lift); cursor: pointer; opacity: 0; transform: translateY(6px);
  transition: opacity .16s ease, transform .16s ease; pointer-events: none; }
#thread-down.show { opacity: 1; transform: none; pointer-events: auto; }
#thread-down .ico { width: 20px; height: 20px; transform: rotate(180deg); }
#thread-down[hidden] { display: none; }
/* Leave-or-keep, asked when the cross is pressed. */
.ask-back { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 9;
  display: flex; align-items: center; justify-content: center;
  padding: 24px; background: rgba(0, 0, 0, 0.5);
  /* The page behind blurs, the way it does under a call. Robert, 2026-09-22. */
  backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px); }
.ask-back[hidden] { display: none; }
.ask-card { width: min(100%, 20rem); background: var(--panel-top); border: 1px solid var(--line-strong);
  border-radius: 18px; padding: 18px; box-shadow: var(--lift); }
.ask-title { margin: 0 0 6px; font: 600 1.05rem / 1.3 var(--sans); color: var(--g100); }
.ask-sub { margin: 0 0 16px; font: 400 0.9rem / 1.4 var(--sans); color: var(--g500); }
.ask-row { display: flex; gap: 10px; }
.ask-row button { flex: 1; padding: 11px; border-radius: 12px; font: 600 0.95rem / 1 var(--sans); cursor: pointer; }
.ask-keep { background: var(--primary); color: #fff; border: 1px solid transparent; }
.ask-leave { background: var(--fill-strong); color: var(--g100); border: 1px solid var(--line-strong); }
/* The chat's composer, on the start card: the chat look, a size smaller. */
#chatstart { display: flex; gap: 8px; align-items: center; width: min(100%, 24rem); margin-inline: auto; }
#chatstart[hidden] { display: none; }
#chatstart input { flex: 1; min-width: 0; font: 400 1rem / 1.3 var(--sans); color: var(--g100);
  background: var(--fill); border: 1px solid var(--line-strong); border-radius: 22px; padding: 11px 16px; }
#chatstart input:focus { outline: none; border-color: var(--primary-bright); }
#chatstart .round { width: 44px; height: 44px; flex: none; }
#chatstart .round .ico { width: 20px; height: 20px; }
/* Opening the full chat: a brief grow from the small composer into the whole
   thread, so it reads as the same field getting bigger. Robert, 2026-09-22. */
@keyframes thread-grow { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: none; } }
#thread:not([hidden]).grew { animation: thread-grow 0.18s ease-out; }
@media (prefers-reduced-motion: reduce) { #thread.grew { animation: none; } }
#thread-log .msg { max-width: 82%; padding: 8px 12px; border-radius: 16px; font: 400 0.95rem / 1.35 var(--sans);
  white-space: pre-wrap; overflow-wrap: anywhere; text-align: left; }
#thread-log .msg.them { align-self: flex-start; background: var(--fill-strong); color: var(--g100);
  border-bottom-left-radius: 6px; }
#thread-log .msg.me { align-self: flex-end; background: var(--primary); color: #fff;
  border-bottom-right-radius: 6px; }
#thread-log .state { align-self: flex-end; font: 500 0.78rem / 1.3 var(--sans); color: var(--g400);
  margin-top: -2px; padding-right: 4px; }
#thread-log .state[data-stage="replied"], #thread-log .state[data-stage="seen"] { color: var(--primary-bright); }
#thread-row { display: flex; gap: 8px; align-items: center; }
#thread-row input { flex: 1; min-width: 0; font: 400 1rem / 1.3 var(--sans); color: var(--g100);
  background: var(--fill); border: 1px solid var(--line-strong); border-radius: 22px; padding: 10px 14px; }
#thread-row input:focus { outline: none; border-color: var(--primary-bright); }
#thread-row .round { width: 44px; height: 44px; }
#thread-row .round .ico { width: 20px; height: 20px; }
#rate { display: grid; gap: 8px; justify-items: center; margin-top: 4px; }
#rate p { margin: 0; font: 500 0.95rem / 1.3 var(--sans); color: var(--g100); }
#rate-thanks { color: var(--g500); }
#stars { display: flex; gap: 6px; }
#stars button {
  border: 0; background: none; cursor: pointer; padding: 4px; font-size: 2rem; line-height: 1;
  color: var(--g600); transition: color .12s ease, transform .12s ease;
}
/* Everything up to the one under the pointer lights up, which is how a
   row of stars is read. */
#stars button:hover, #stars button.lit { color: var(--warning); }
#stars button:active { transform: scale(0.94); }
#stars:has(button:hover) button:hover ~ button { color: var(--g600); }
#rate-ask span { display: block; margin-top: 4px; font: 400 0.8rem / 1.35 var(--sans); color: var(--g500); }
/* The stores, with room around them: the small link used to sit right
   under the stars and read as part of the rating. */
/* The badges. A page that does not know the reader's store shows them side
   by side as equals (a Windows PC, a Linux box). A page that does
   (data-store-mine on <html>, set by the store script) stacks them: the
   reader's own store first and large - the thing to press - then room,
   then the other platforms small, each with a line saying whose it is,
   so nobody lands in the wrong store and everybody sees where else the
   app lives (Robert, 16.10.2026). */
/* The invitation to the app at the end of a call or a chat: the icon, the
   reader's own free code in bold, where it is made under it (server/appcta.js). */
#stores-app { margin: 18px auto 10px; text-align: center; }
#stores-app img { display: block; margin: 0 auto 10px; }
#stores-app p { margin: 0; color: var(--g100); }
#stores-app p b { display: block; font-size: 1.15rem; }
#stores-app p span { display: block; margin-top: 4px; font-size: .9rem; color: var(--g400); }
#stores { display: flex; gap: 12px; align-items: center; justify-content: center; flex-wrap: wrap; margin-top: 2px; }
#stores a { display: inline-flex; line-height: 0; }
#stores img { display: block; height: 44px; width: auto; }
#stores a.store-badge { display: inline-flex; flex-direction: column; align-items: center; gap: 5px; line-height: 1.3; text-decoration: none; }
#stores .store-other { display: none; font: 500 0.78rem / 1.3 var(--sans); color: var(--g500); }
html:not([data-store-mine="none"]) #stores { flex-direction: column; gap: 0; }
/* Every badge at one height, as Apple and Google both ask when their badges
   stand together; the phone's own store only comes first (30.10.2026). */
html:not([data-store-mine="none"]) #stores [data-store-mine="false"] { margin-top: 18px; }
html:not([data-store-mine="none"]) #stores [data-store-mine="false"] + [data-store-mine="false"] { margin-top: 14px; }
html:not([data-store-mine="none"]) #stores [data-store-mine="false"] .store-other { display: block; }
#stores [data-store-mine="true"] .store-other, html[data-store-mine="none"] #stores .store-other { display: none; }
#stores .store-mine { display: none; }
#stores [data-store-mine="true"] .store-mine { display: inline; }
#stores [data-store-mine="true"] .appcard-text > .store-other, #stores [data-store-mine="false"] .appcard-text > .store-mine { display: none; }
#stores [data-store-mine="false"] .appcard-text > .store-other { display: inline; }
/* The app as a card: its icon with the iOS corner, the name, one line
   under it. One card on the home page and on the sheet after a call. */
.appcard { display: inline-flex; align-items: center; gap: 14px; text-align: left; text-decoration: none;
  padding: 10px 16px 10px 10px; border-radius: 18px; color: var(--g100);
  background: var(--fill); border: 1px solid var(--line-strong); box-shadow: var(--lift); line-height: 1.25; }
.appcard:hover { border-color: rgb(var(--glow) / 0.5); }
.appcard img { display: block; flex: none; width: 60px; height: 60px; border-radius: 22.37%; }
.appcard-text { display: flex; flex-direction: column; gap: 3px; }
.appcard-text b { font: 600 1.02rem / 1.2 var(--sans); color: var(--g100); }
.appcard-text span:last-child { font: 400 0.8rem / 1.3 var(--sans); color: var(--g500); }
#stores .appcard { padding: 8px 14px 8px 8px; line-height: 1.25; }
#stores .appcard img { width: 48px; height: 48px; }
#stores .appcard-text b { font-size: 0.95rem; }

/* Where the call goes, under the clock. Quiet: it is a fact about the
   call, not a warning about it. */
#path {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  font-size: 0.78rem; opacity: 0.55; margin-top: 4px;
}
#shield { width: 13px; height: 13px; }
/* Direct: filled, nothing of ours in between. Relayed: the same outline,
   hollow. */
#shield path { fill: currentColor; stroke: none; }
#shield[data-relayed="true"] path { fill: none; stroke: currentColor; stroke-width: 1.6; }

/* Short screens - the mini, the SE, a phone in landscape: everything a
   size smaller, so the whole card is on screen without scrolling.
   LAST in the file on purpose: a media query adds no specificity, so a
   rule in here only wins over the base rule if it comes after it. It sat
   near the top for a day and only the one rule with no later rival
   (#about) ever applied. */
/* ---- choosing a device: browser only -------------------------------
   A small chevron on the lower right of the microphone and of the camera,
   the way Meet attaches one to each medium, and a list above it in the
   bar's own material. The app has none of this: there the system's route
   list does the job. */
.withmore { position: relative; display: inline-grid; flex: none; }
.withmore > .round[hidden] + .more { display: none; }
.more {
  /* In the corner of the square around the circle: the arrow's lower right
     end sits on that corner (the path runs 6,14.5 to 18,14.5 in a 24 box,
     drawn at 12 px), so it reads as belonging to the button, not to the
     gap beside it. */
  position: absolute; right: -7px; bottom: -9px; width: 22px; height: 22px; padding: 0; border-radius: 50%;
  display: grid; place-content: center; line-height: 0; cursor: pointer;
  /* No material of its own: a filled disc read as a fifth control
     (Robert, 2026-09-20). The glyph alone, and no shadow either: the page
     has enough of those. */
  background: none; border: 0; color: var(--g200);
}
.more .ico { width: 12px; height: 12px; stroke-width: 2.6; transition: transform 0.15s ease; }
.more[aria-expanded="true"] .ico { transform: rotate(180deg); }
.more:hover { color: var(--g100); }
#devicelist {
  position: fixed; z-index: 8; left: 50%; transform: translateX(-50%);
  width: min(320px, 92vw); text-align: left; max-height: 60vh; overflow-y: auto; padding: 8px; border-radius: 16px;
  background: rgb(var(--veil) / 0.9); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--line-strong); box-shadow: var(--lift); color: var(--g100);
}
#devicelist section + section { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--line); }
#devicelist h4 { margin: 4px 10px 2px; font: 600 0.68rem / 1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--g500); }
#devicelist .options button {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; border: 0; background: none;
  color: var(--g100); padding: 9px 10px; border-radius: 10px; font: 500 0.9rem / 1.2 var(--sans); cursor: pointer;
}
#devicelist .options button:hover { background: var(--fill); }
#devicelist .options button .ico { width: 16px; height: 16px; flex: none; visibility: hidden; }
#devicelist .options button[aria-checked="true"] .ico { visibility: visible; color: rgb(var(--glow)); }
#devicelist .options button span { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* What to work on, after a low rating: a small field and one button. */
#rate-more { display: grid; gap: 8px; justify-items: center; width: min(100%, 22rem); margin-top: 6px; }
#rate-more label { font: 500 0.9rem / 1.3 var(--sans); color: var(--g200); }
#rate-more textarea { font: 400 0.9rem / 1.35 var(--sans); resize: none; }
#rate-more button { padding: 8px 18px; }

@media (max-height:860px){
  main{padding-top:calc(var(--safe-t) + 8px);padding-bottom:calc(var(--safe-b) + 8px)}
  main>footer{margin-top:8px}
  #top{gap:8px}
  .eyebrow{font-size:.7rem}
  h1{font-size:clamp(1.4rem,1rem+1.8vw,1.8rem);line-height:1.1}
  #dial{width:96px;height:96px}
  #dial .ico{width:38px;height:38px}
  #mepreview{width:44px;height:44px;font-size:.95rem}
  #mepreview .ico{width:20px;height:20px}
  .sub{font-size:.86rem;line-height:1.4}
  #about{display:none}
  #notefield{margin-top:2px}
  #reasonfield{margin-top:2px}
  button.primary{height:50px}
  #idlerow .round{width:50px;height:50px}
  footer{padding-top:0;font-size:.7rem}
  footer .getapp{margin-bottom:4px;font-size:.8rem}
  .build{margin-top:4px}
}

/* ===================================================== identity colour =
   Two colours on this page, and they are two because they say different
   things. The brand's Call Blue is what YOU are and what you press: your
   own circle, the buttons, the controls. The line's own colour off the
   ramp is what the line IS - the thing you came here to reach, the same
   colour it wears in the owner's list on three platforms. On 2026-09-20
   these were collapsed into one blue, on the reading that a page with one
   line and one caller has nothing to tell apart; that was true of TELLING
   APART and wrong about identity. A line's colour is not decoration, it
   is the line, the way a contact's picture is the person - which is why
   it does not compete with the brand blue that stays on the controls
   (D-91, Robert 2026-09-28: "die farben der links könnten variieren,
   somit auch mal das mit dem hörer").
   What survives from that day unchanged: the caller's own circle is Call
   Blue, and a photograph beats any colour.
   LAST in the file, and it has to stay last: it was written once above
   the stage rules, lost to them, and then lost altogether in a later
   edit. tests/stylesheet.test.js checks it is here. */
#mepreview, #selfinitials { background: var(--primary); color: #fff; }
#theirface { color: #fff; }
/* White on the circle whatever the page's own type colour is: in the
   light the handset came out black on blue. */
#dial { color: #fff; }

/* ---- home: open a link by its code, or scan its QR. Robert, 2026-09-23. */
.codebox{margin:18px 0 6px;padding:16px;border:1px solid var(--line);border-radius:16px;background:var(--fill);max-width:34ch}
.codebox h2{margin:0 0 2px;font:600 1rem/1.2 var(--sans);color:var(--g100)}
.codeform{display:flex;flex-direction:column;align-items:stretch;gap:12px;margin-top:12px}
.cells{display:flex;align-items:center;gap:5px;width:100%}
.cell{flex:1 1 0;min-width:0;height:2.7rem;text-align:center;font:600 1.05rem/1 var(--mono);text-transform:lowercase;
  color:var(--g100);background:var(--fill-strong);border:1px solid var(--line-strong);border-radius:8px;padding:0;caret-color:var(--primary)}
.cell:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 25%,transparent)}
/* What separates the three blocks is a GAP, not a character. The hyphen
   went out of the code in D-82 and out of this box with it: a person
   typing here should never be shown a character that is not part of
   what they are typing. */
.cellgap{flex:none;width:.5rem}
.codego{align-self:flex-end}
.codego{padding:10px 18px;border:0;border-radius:999px;font:600 .9rem/1 var(--sans);
  color:#fff;background:var(--primary);cursor:pointer}
.codego:disabled{opacity:.4;cursor:default}
.scanbtn{margin-top:10px;padding:9px 14px;border:1px solid var(--line-strong);border-radius:999px;
  font:600 .88rem/1 var(--sans);color:var(--g100);background:transparent;cursor:pointer}
.codeerr{margin:8px 0 0;font-size:.85rem;color:#ff9f0a}
.scanner{position:fixed;inset:0;z-index:50;background:#000}
.scanner video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.scan-frame{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:min(70vw,260px);aspect-ratio:1;
  border:3px solid #fff;border-radius:20px;box-shadow:0 0 0 100vmax rgba(0,0,0,.5)}
.scan-hint{position:absolute;left:0;right:0;bottom:16%;color:#fff;font:500 .95rem/1.3 var(--sans);text-align:center;padding:0 24px;margin:0}
.scanclose{position:absolute;top:16px;right:16px;padding:9px 16px;border:0;border-radius:999px;
  font:600 .9rem/1 var(--sans);color:#000;background:#fff;cursor:pointer}

/* ---- desktop beta downloads on the home page. Robert, 2026-09-23. */
.downloads{margin:12px 0 0;max-width:34ch}
.dlrow{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}
.dlbtn{flex:1;min-width:5.5rem;text-align:center;padding:10px 12px;border:1px solid var(--line-strong);border-radius:12px;
  font:600 .9rem/1 var(--sans);color:var(--g100);text-decoration:none;background:var(--fill)}
.dlbtn:hover{border-color:var(--primary);color:var(--primary)}
.dlnote{margin-top:8px;opacity:.85}

/* The home content column: bound to the container so the code card cannot
   push the page wider than the phone. Home only; the call page is untouched.
   Robert, 2026-09-23. */
.home #top{grid-template-columns:minmax(0,1fr);align-content:safe center}
.home #top>*{max-width:min(34ch,100%);justify-self:center}

/* A code shown to be typed, not clicked: the monospace of the link rows, a
   size up, and the letter-spacing that makes three blocks of three read as
   three blocks. */
.codeline{font:600 1.35rem/1.4 var(--mono);letter-spacing:.06em}

/* The buyer's page for a batch. The cards themselves are printed from a
   window of their own (web/sheet.js), so nothing here is print-only. */
.batchactions{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}
.batchtable{width:100%;border-collapse:collapse;margin-top:8px;font-size:.95rem}
.batchtable th{text-align:left;font-weight:600;color:var(--g500);padding:6px 10px 6px 0;border-bottom:1px solid var(--line)}
.batchtable td{padding:6px 10px 6px 0;border-bottom:1px solid var(--line)}
.batchtable .mono{font-family:var(--mono);letter-spacing:.03em}

/* ---- the shop -------------------------------------------------------------
   Its own layout, because the rest of this site is one screen with one thing
   on it: `main` is 100svh with a centred column and paragraphs 34 characters
   wide, which turned three sections into a squeezed strip. A shop is a page
   you scroll. */
/* The body is fixed to the viewport for the app-like pages - `html,body` up
   at the top of this file - and the ONLY thing that scrolls there is #top.
   The first version of this page set `#top{overflow:visible}` and so threw
   that scroller away: everything below the fold was clipped, unreachable and
   unclickable. A page you scroll has to say so on the BODY, the way the
   legal pages do. */
body.shoppage{overflow:auto;-webkit-overflow-scrolling:touch}
.shop{max-width:none;text-align:left;padding:0;height:auto;min-height:100%;display:block}
.shop #top{display:block;max-width:56rem;margin:0 auto;padding:40px 20px 0;
  align-content:start;justify-items:stretch;overflow:visible;min-height:0}
.shop .lede{max-width:62ch;margin:0 0 12px;font-size:1rem;line-height:1.6;color:var(--g400)}
.shop .lede.small{font-size:.9rem;color:var(--g500)}
.shop h1{margin:6px 0 14px}
.shop h2{margin:38px 0 14px;font-size:1.25rem;font-weight:650}
.shop section{margin-bottom:30px}
.shophead{margin-bottom:8px}

.packs{display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:14px}
.pack{border:1px solid var(--line);border-radius:14px;padding:18px;background:var(--fill)}
.pack h3{margin:0 0 6px;font-size:1rem;font-weight:650}
.price{margin:0 0 10px;font-size:1.6rem;font-weight:700;letter-spacing:-.02em}
.price .per{font-size:.8rem;font-weight:500;color:var(--g500)}
.packwhat{margin:0 0 8px;font-size:.92rem}
.packabout{margin:0 0 10px;font-size:.88rem;color:var(--g500);line-height:1.5}
.packunit{margin:0;font-size:.8rem;color:var(--g600);font-family:var(--mono)}
.fineprint{margin:14px 0 0;font-size:.82rem;color:var(--g600);max-width:62ch;line-height:1.5}

.orderform{display:grid;grid-template-columns:1fr 1fr;gap:14px;max-width:44rem;margin-top:6px}
.field{display:flex;flex-direction:column;gap:6px}
.field.wide,.submitrow{grid-column:1 / -1}
.field.narrow{max-width:12rem}
.field label{font-size:.85rem;color:var(--g400)}
.field .opt{color:var(--g600);font-weight:400}
.orderform input,.orderform select,.orderform textarea{
  font:inherit;font-size:.95rem;color:var(--g100);background:var(--fill);
  border:1px solid var(--line);border-radius:10px;padding:10px 12px;width:100%}
.orderform textarea{resize:vertical}
.orderform input:focus,.orderform select:focus,.orderform textarea:focus{
  outline:none;border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 22%,transparent)}
.total{grid-column:1 / -1;margin:0;padding:12px 14px;border-radius:10px;
  background:var(--fill-strong);font-size:.95rem}
.submitrow{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.orderform button{font:inherit;font-weight:600;color:#fff;background:var(--primary);
  border:0;border-radius:10px;padding:12px 22px;cursor:pointer}
.orderform button[disabled]{opacity:.55;cursor:default}
.done{border:1px solid var(--line);border-radius:14px;padding:20px;background:var(--fill)}
.done h3{margin:0 0 10px;font-size:1.05rem}
.done .mono{font-family:var(--mono)}
.steps{max-width:62ch;padding-left:1.1rem;color:var(--g400);line-height:1.6;font-size:.95rem}
.steps li{margin-bottom:8px}
@media (max-width:640px){
  .orderform{grid-template-columns:1fr}
  .shop #top{padding:28px 16px 0}
}
.blockbtn{font:inherit;font-size:.82rem;color:var(--bad,#ff453a);background:transparent;
  border:1px solid var(--line);border-radius:8px;padding:4px 10px;cursor:pointer}
.blockbtn:hover{border-color:var(--bad,#ff453a)}

/* The buyer's page uses the shop's wide layout, and `.codebox` was drawn for
   a narrow column on a one-screen page: on a 56rem page it left three
   letterbox panels on the left and half the page empty, with a part's link
   running out of its own box. */
.shop .codebox{max-width:none;width:100%;box-sizing:border-box;margin:0 0 18px;text-align:left}
.shop .codebox h2{margin-top:0}
.shop .codebox .sub{max-width:68ch}
.shop .codebox .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.shop .codebox input{font:inherit;font-size:.95rem;color:var(--g100);background:var(--fill);
  border:1px solid var(--line);border-radius:10px;padding:9px 12px}
.batchtable td .mono,.batchtable .mono{word-break:break-all}

/* The emergency notice on the caller page.
   Readable but not loud: it is meant to be read before anybody needs it,
   and not to wrestle the call button for attention. Its own line above the
   legal links, so it does not blur into a fourth link. */
footer .no-emergency {
  margin: 0 0 6px;
  font-size: 12px;
  line-height: 17px;
  color: var(--ink-muted, #4A5568);
}

/* The ten tints as a choice, on the buyer page.
   Robert, 04.10.2026: *"bei der color waere entweder ein color picker gut
   oder die farben die wir haben"* - they are the ones we have, and they
   stand a hand's breadth further up in this file. Hence HERE and not in
   `shop.css`: that is where they started, and the buyer page does not load
   `shop.css` at all. In the picture from 04.10.2026 the circles were
   stacked and grey, because `--avatar-N` and the rules lay in two different
   files and only one of them was loaded. */
.tintrow{display:inline-flex;gap:6px;flex-wrap:wrap;align-items:center;vertical-align:middle}
/* `display` and `padding` spelled out: the general rule for `button`
   further up turns every button into a flex box with 15px of padding, and
   without these two lines the circles are not circles. The colour is set by
   `web/batch.js` directly - see there for why not through a variable. */
.tintrow button{width:26px;height:26px;min-width:26px;border-radius:50%;
  border:2px solid transparent;padding:0;cursor:pointer;flex:0 0 auto;
  display:inline-block;background:#888;
  -webkit-appearance:none;appearance:none}
.tintrow button[aria-checked="true"]{border-color:#fff;outline:2px solid rgba(255,255,255,.45);
  outline-offset:1px}
/* No colour: the code tints itself. As a chequer pattern, because an empty
   circle looks like an eleventh colour. */
.tintrow button.none{background:repeating-linear-gradient(45deg,
  rgba(255,255,255,.25) 0 5px,rgba(255,255,255,.06) 5px 10px)}
/* The picture that currently applies - small, round, beside the file
   field. Without a size the browser draws it at its original size, and a
   logo a thousand pixels wide shoves half the page away. */
.presetshot{width:44px;height:44px;border-radius:50%;object-fit:cover;
  border:1px solid var(--line);flex:0 0 auto}

/* Google's badge, at Google's own proportions.
   =========================================
   Their brand guidelines ask for the artwork unaltered, not recoloured and
   with clear space around it; the file already carries that space, so the
   only thing done here is a size. The `width`/`height` attributes in the
   HTML say the file's real 646x250, which is what reserves the right box
   before the image arrives - see `tests/inline-styles.test.js` for the day
   a wrong pair of those numbers stretched a photograph on /v2. */
.playbadge { display: inline-block; line-height: 0; }
.playbadge img { display: block; height: 68px; width: auto; }
/* Apple's badge carries no margin of its own, Google's PNG does: 52px
   here stands as tall as the 68px Play image beside it. */
.appbadge { display: inline-block; line-height: 0; }
.appbadge img { display: block; height: 52px; width: auto; }

/* The front page's own mark, where a beta card used to stand by accident
   (it was moved there by the store script until 30.10.2026). */
.home-logo { display: block; width: 72px; height: 72px; margin: 0 auto 18px; border-radius: 22.37%; }

/* The store badges on the front page and the unused-code page, laid out by
   the same rule as #stores at the end of a call: side by side as equals on
   a desktop - a Mac too, which the call page stacks - and on a phone
   (data-store on <html>, set by the store script) the phone's own
   store first and large, the other under it, smaller, saying whose it is. */
.stores-row { display: flex; gap: 16px; align-items: center; justify-content: center; flex-wrap: wrap; margin: 26px 0 6px; }
.stores-row a.store-badge { display: inline-flex; flex-direction: column; align-items: center; gap: 5px; line-height: 1.3; text-decoration: none; }
.stores-row .store-other { display: none; font: 500 0.78rem / 1.3 var(--sans); color: var(--g500); }
html:not([data-store="none"]) .stores-row { flex-direction: column; gap: 0; }
html:not([data-store="none"]) .stores-row [data-store-mine="false"] { margin-top: 26px; opacity: .8; }
html:not([data-store="none"]) .stores-row [data-store-mine="false"] .store-other { display: block; }

/* ---- The front page, Scanner First (30.10.2026). Two languages in one
   page: the one not chosen is not drawn. */
:root[lang="de"] .front [lang="en"], :root[lang="en"] .front [lang="de"],
:root[lang="de"] .scanner [lang="en"], :root[lang="en"] .scanner [lang="de"] { display: none; }
.front #top > * { max-width: min(40ch, 100%); width: 100%; }
.front-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 26px; }
.front-brand { display: inline-flex; align-items: center; gap: 10px; color: var(--g100); text-decoration: none; font: 600 1rem/1 var(--sans); }
.front-brand img { width: 40px; height: 40px; display: block; }
.front-lang { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; padding: 2px; }
.front-lang button { min-width: 44px; min-height: 36px; border: 0; border-radius: 999px; background: transparent;
  color: var(--g500); font: 600 .78rem/1 var(--sans); cursor: pointer; }
.front-lang button[aria-pressed="true"] { background: var(--fill-strong); color: var(--g100); }
.front-hero h1 { margin: 0 0 12px; font: 700 clamp(1.6rem, 6.4vw, 2.1rem)/1.15 var(--sans); color: var(--g100); text-align: center; }
.front-hero .sub { max-width: none; text-align: center; margin: 0 auto 8px; }
.front-do { color: var(--g400); font-weight: 500; }
.front-actions { display: flex; flex-direction: column; gap: 10px; margin: 18px 0 6px; }
.front-scan, .front-enter, .front-go { min-height: 48px; padding: 0 20px; border-radius: 999px; cursor: pointer;
  font: 600 1rem/1 var(--sans); }
.front-scan { border: 0; color: #fff; background: var(--primary); }
.front-scan:hover { background: var(--primary-bright); }
.front-enter { border: 1px solid var(--line-strong); color: var(--g100); background: transparent; }
.front-scan.small, .front-enter.small { min-height: 44px; font-size: .9rem; }
.front button:focus-visible, .front a:focus-visible, .front input:focus-visible, .scanner button:focus-visible {
  outline: 3px solid var(--sky); outline-offset: 2px; }
.front-camerr { margin: 10px 0 4px; padding: 14px 16px; border: 1px solid rgba(255, 159, 10, .45); border-radius: 14px;
  background: rgba(255, 159, 10, .08); text-align: left; }
.front-camerr h3 { margin: 0 0 6px; font: 600 .98rem/1.3 var(--sans); color: var(--g100); outline: none; }
.front-camerr p { margin: 0 0 8px; font-size: .88rem; line-height: 1.45; color: var(--g400); }
.front-camerr-acts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
/* "Enter a code" stands right above as the page's own button; a second one
   in the notice said it twice (screen tree, 31.10.2026). Either that
   button or the field it opens is always above the notice. */
.front-camerr-acts [data-enter-code] { display: none; }
.front-camerr-acts button { flex: 1 1 10rem; }
.front-form { margin: 0 0 6px; text-align: left; }
.front-form[hidden], .front-enter[hidden] { display: none; }
.front-row { display: flex; gap: 8px; }
.front-row input { flex: 1 1 auto; min-width: 0; min-height: 48px; padding: 0 14px; border: 1px solid var(--line-strong);
  border-radius: 12px; background: var(--fill); color: var(--g100); font: 500 1rem/1 var(--sans); }
.front-row input:focus { border-color: var(--primary); }
.front-go { flex: none; border: 0; color: #fff; background: var(--primary); font-size: .92rem; }
/* On a phone the field takes the whole width, so the printed link fits. */
@media (max-width: 480px) { .front-row { flex-direction: column; } }
.front-trust { text-align: center; margin: 14px auto 0; color: var(--g500); }
.front-how, .front-own { margin-top: 40px; padding-top: 28px; border-top: 1px solid var(--line); text-align: left; }
.front-how h2, .front-own h2 { margin: 0 0 14px; font: 650 1.2rem/1.25 var(--sans); color: var(--g100); }
.front-how ol { margin: 0 0 14px; padding-left: 1.4em; color: var(--g400); font-size: .95rem; line-height: 1.5; }
.front-how li { margin-bottom: 8px; }
.front-how li b, .front-own b { color: var(--g100); font-weight: 600; }
.front-how .sub, .front-own .sub { max-width: none; margin: 0 0 10px; }
.front-cta { margin: 18px 0 0; text-align: center; font: 600 .95rem/1.3 var(--sans); color: var(--g100); }
.front .stores-row { margin-top: 12px; }
.front-price { margin-top: 34px; text-align: center; max-width: none; }
.scan-status { position: absolute; left: 16px; right: 16px; top: 14%; margin: 0; padding: 10px 14px; border-radius: 12px;
  background: rgba(0, 0, 0, .7); color: #fff; font: 500 .95rem/1.35 var(--sans); text-align: center; }
.scan-acts { position: absolute; left: 0; right: 0; bottom: 6%; display: flex; gap: 10px; justify-content: center; padding: 0 16px; }
.scan-acts .scanclose { position: static; }
.scanmanual { min-height: 44px; padding: 0 16px; border: 1px solid rgba(255, 255, 255, .6); border-radius: 999px;
  background: rgba(0, 0, 0, .4); color: #fff; font: 600 .9rem/1 var(--sans); cursor: pointer; }
.scan-acts .scanclose { min-height: 44px; }
@media (prefers-reduced-motion: reduce) { .front * { transition: none !important; scroll-behavior: auto !important; } }
/* The scroller spans the window and the column stays in its middle, so the
   scroll bar sits at the window's edge and not against the text (Robert,
   30.10.2026). */
main.front { max-width: none; padding-left: 0; padding-right: 0; }
.front #top { padding: 0 20px; }
.front footer { padding: 0 20px; }

/* The front page's two ways in (30.10.2026, third take): a scan button
   with its mark, then the code typed the way it is printed - one large
   field in the code's own type, falling into threes - and a wide button. */
.front-actions { gap: 0; margin: 22px 0 4px; }
.front-scan { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 54px;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--primary) 35%, transparent); }
.front-ico { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex: none; }
.front-form { margin: 26px 0 0; display: flex; flex-direction: column; gap: 10px; }
.front-label { text-align: center; font: 500 .9rem/1.3 var(--sans); color: var(--g500); }
input.front-code { width: 100%; box-sizing: border-box; min-height: 64px; padding: 0 16px; border: 1px solid var(--line-strong);
  border-radius: 16px; background: var(--fill); color: var(--g100); text-align: center;
  font: 600 1.7rem/1 var(--mono); letter-spacing: .14em; text-transform: lowercase;
  transition: border-color .15s, box-shadow .15s; }
.front-code::placeholder { color: var(--g700); letter-spacing: .2em; }
.front-code.is-link { font: 500 1rem/1 var(--sans); letter-spacing: 0; text-transform: none; }
.front-code:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 22%, transparent); }
.front-form .front-go { width: 100%; min-height: 50px; border: 1px solid var(--line-strong); background: transparent;
  color: var(--g100); font: 600 1rem/1 var(--sans); transition: background .15s, border-color .15s, color .15s; }
.front-code:not(:placeholder-shown) ~ .front-go { background: var(--primary); border-color: var(--primary); color: #fff; }
.front-form .codeerr { text-align: center; margin: 0; }
/* Below the way in, one alignment: every section reads from the left, its
   badges and the price included, so nothing below the hero sits centred
   under a left-aligned heading (Robert, 30.10.2026: "wirkt unten
   unsymmetrisch"). And both badges stand the same height. */
.front .front-cta, .front .front-price { text-align: left; }
.front .stores-row { justify-content: flex-start; }
.front-code::placeholder { color: var(--g500); }
/* `ch` follows the font size, so the small print came out narrower than
   the column above it; it takes the column's width instead. */
.front #top > .front-price { max-width: min(400px, 100%); font-size: .82rem; }
.front #top > section { max-width: min(400px, 100%); }

/* "Code eingeben" as its own button under the scan button; the field comes
   up in its place (Robert, 30.10.2026). The scan button carries a QR mark. */
.front-actions > .front-enter { display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 54px; margin-top: 12px; }
.front-actions > .front-enter[hidden], .front-form[hidden] { display: none; }
.front-form { margin-top: 12px; }
.front-qr rect { fill: none; }
.front-qr .dot { fill: currentColor; stroke: none; }

/* The store badges, by the stores' own rules (30.10.2026). Both stand at
   exactly the same height - Apple and Google each ask for that when their
   badge appears beside another store's - and never below Apple's minimum of
   40 px. The Google artwork is used cropped to the badge itself
   (web/badges/google-play*-trim.png): the files Google ships carry their
   own transparent margin, and a different one per language, which made
   the same CSS height look like two different sizes. The space between
   them is at least a quarter of the badge height, Apple's clear space. */
.stores-row { gap: 14px; }
.stores-row img { display: block; height: 48px; width: auto; margin: 0; }
html:not([data-store="none"]) .stores-row { gap: 0; }
html:not([data-store="none"]) .stores-row [data-store-mine="false"] { margin-top: 18px; }

/* The three steps, each with its mark - bare, no tile behind it (Robert,
   30.10.2026). */
.front-how .front-steps { list-style: none; margin: 0 0 16px; padding: 0; display: grid; gap: 16px; counter-reset: none; }
.front-how .front-steps li { display: grid; grid-template-columns: 44px 1fr; gap: 14px; align-items: start; margin: 0;
  color: var(--g400); font-size: .95rem; line-height: 1.5; }
.front-step-ico { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center;
  color: var(--sky); }
.front-step-ico svg { width: 28px; height: 28px; fill: currentColor; stroke: none; }
.front-step-ico .dot { fill: currentColor; stroke: none; }
.front-steps li > span[lang] { padding-top: 10px; }

/* The hero reads from the left like every section under it, and is
   weighted: the question large, the explanation quiet, the instruction -
   what to do next, right above the buttons - in full strength
   (Robert, 30.10.2026). */
.front-hero h1 { text-align: left; font-size: clamp(1.9rem, 7.4vw, 2.4rem); line-height: 1.1; letter-spacing: -.01em; margin-bottom: 14px; }
.front-hero .sub { text-align: left; margin: 0 0 10px; font-size: 1rem; line-height: 1.5; color: var(--g500); }
.front-hero .front-do { color: var(--g100); font-weight: 600; margin-top: 4px; }
.front-hero .front-trust { text-align: center; margin: 12px 0 0; font-size: .82rem; color: var(--g500); }
/* No lone word on a line of its own: the question and the instruction
   break into lines of even length, the paragraphs avoid a short last line
   (Robert, 30.10.2026: "blöder Umbruch"). */
.front .sub, .front-steps li { text-wrap: pretty; }
.front-hero h1, .front .front-hero .front-do, .front h2 { text-wrap: balance; }

/* The front page in the light scheme: what read well on dark was too faint
   on light - the field's edge, the orange of a mistake, the pale blue of
   the step marks (30.10.2026). */
@media (prefers-color-scheme: light) {
  input.front-code { background: #fff; border-color: rgba(0, 0, 0, .28); }
  .front .codeerr { color: #b45309; }
  .front-step-ico { color: var(--primary); }
  .front-camerr { background: rgba(180, 83, 9, .06); border-color: rgba(180, 83, 9, .35); }
}

/* "Also for Android" announces the second store, so it stands above that
   badge, with clear room to the first one - not under it, where it read as
   a caption of the badge it follows (Robert, 30.10.2026). */
#stores .store-other, .stores-row .store-other, .v3-info-stores .store-other { order: -1; }
html:not([data-store-mine="none"]) #stores [data-store-mine="false"] { margin-top: 30px; gap: 8px; }
html:not([data-store="none"]) .stores-row [data-store-mine="false"] { margin-top: 30px; gap: 8px; }
/* On the front page the stacked badges follow the section's left edge. */
html:not([data-store="none"]) .front .stores-row { align-items: flex-start; }
.front .stores-row a.store-badge { align-items: flex-start; }

/* The page for a code that does not exist (30.10.2026): the real mark at
   the top as the way home, the message, and the two ways back to what the
   visitor came for - built from the front page's own pieces. */
.gone #top > * { max-width: min(400px, 100%); width: 100%; }
.gone h1 { margin: 0 0 6px; text-wrap: balance; }
.gone .sub { text-wrap: balance; }
.gone-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 18px; }
.gone-actions a { display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 52px; text-decoration: none; box-sizing: border-box; }
.gone-actions .front-scan { box-shadow: 0 6px 20px color-mix(in srgb, var(--primary) 35%, transparent); }
.gone-trust { margin-top: 8px; text-align: center; }
/* The same frame as the front page: the scroller spans the window and the
   column keeps 20 px to its edge, so the scan button's glow is drawn whole
   instead of being cut off at the sides (Robert, 30.10.2026). */
main.gone { max-width: none; padding-left: 0; padding-right: 0; }
.gone #top { padding: 0 20px; }
.gone footer { padding: 0 20px; }
/* The error page's head is the front page's: mark left, language right.
   Its switch is made of links (the server renders one language), styled
   as the front page's buttons. */
.front-lang a { min-width: 44px; min-height: 36px; display: inline-grid; place-items: center; border-radius: 999px;
  color: var(--g500); font: 600 .78rem/1 var(--sans); text-decoration: none; }
.front-lang a[aria-current="page"] { background: var(--fill-strong); color: var(--g100); }

/* Mark and language belong at the top of the screen, on every page, and
   not inside the block that centres itself (Robert, 30.10.2026): the head
   is the first child of <main>, above the scroller, at the column's width. */
main.front > .front-head, main.gone > .front-head {
  flex: none; width: min(400px, calc(100% - 40px)); margin: 0 auto 18px; box-sizing: border-box; }
.front #top { align-content: start; padding-top: 8px; }

/* ---- The front page on a desktop (30.10.2026). Mobile first stays - most
   readers hold a phone - but a wide screen gets the same pieces side by
   side instead of one long column: the question beside its two buttons, the
   three steps in a row, the app's text beside its badges. */
@media (min-width: 960px) {
  main.front > .front-head { width: min(960px, calc(100% - 80px)); margin-bottom: 32px; }
  main.front #top { padding: 0 40px; }
  main.front #top > *, main.front #top > section, main.front #top > .front-price { max-width: min(960px, 100%); }
  main.front .front-hero { display: grid; grid-template-columns: minmax(0, 1fr) 380px; column-gap: 72px;
    align-items: center; margin-top: 24px; }
  main.front .front-hero > h1 { grid-column: 1; font-size: 3.1rem; line-height: 1.05; margin-bottom: 18px; }
  main.front .front-hero > p.sub:not(.front-trust) { grid-column: 1; max-width: 46ch; font-size: 1.08rem; }
  main.front .front-hero > .front-actions { grid-column: 2; grid-row: 1 / span 2; align-self: end; margin: 0; }
  main.front .front-hero > .front-trust { grid-column: 2; grid-row: 3; align-self: start; margin-top: 12px; }
  main.front .front-hero > .front-camerr { grid-column: 2; }
  main.front .front-how, main.front .front-own { margin-top: 64px; padding-top: 40px; }
  main.front .front-how .front-steps { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px; }
  main.front .front-how .front-steps { align-items: start; }
  main.front .front-how .front-steps li { grid-template-columns: 1fr; gap: 12px; align-content: start; }
  main.front .front-steps li > span[lang] { padding-top: 0; }
  main.front .front-own { display: grid; grid-template-columns: minmax(0, 1fr) 380px; column-gap: 72px; align-items: start; }
  main.front .front-own > h2, main.front .front-own > p.sub { grid-column: 1; max-width: 52ch; }
  main.front .front-own > .front-cta { grid-column: 2; grid-row: 1; margin-top: 0; }
  main.front .front-own > .stores-row { grid-column: 2; grid-row: 2 / span 3; align-self: start; }
  main.front #top > .front-price { margin-top: 56px; max-width: min(960px, 100%); justify-self: center; text-align: left; }
}

/* A legal page has the front page's head too (Robert, 30.10.2026): the
   mark home on the left, the language on the right, at the text's width.
   The German text says it came through an AI with a badge beside its date,
   and which version counts in a note at its end. */
.legal main > .legal-head { max-width: 44rem; margin: 0 auto; box-sizing: border-box;
  padding: clamp(16px,3vw,28px) clamp(20px,5vw,32px) 0; }
.legal main > .legal-head ~ #top { padding-top: clamp(16px,4vw,40px); }
/* The wordmark is the mark, not a link in the text: `.legal a` would paint it blue. */
.legal a.front-brand { color: var(--g100); }
.legal .eyebrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.legal .ai-badge { display: inline-block; padding: 3px 8px; border: 1px solid var(--line); border-radius: 999px;
  font: 600 .7rem/1 var(--sans); letter-spacing: .02em; color: var(--g400); text-decoration: none; }
.legal .ai-badge:hover { color: var(--g200); }
.legal .legal-note { margin: 28px 0 12px; color: var(--g500); font-size: .82rem; }

/* The app's icon wherever a page shows it, at any size, with the corner an
   app icon has on an iPhone (Robert, 30.10.2026: "überall"): 22.37% of the
   side, the usual reading of Apple's mask. One rule by the file, so an icon
   added to a page later is rounded without anybody remembering to. */
img[src^="/static/icon-180.png"], img[src^="/static/favicon-48.png"] { border-radius: 22.37%; }
