/* ---------------------------------------------------------------------------
 * Krucemburk volby 2026 — KDU-ČSL
 * Colours and the photo aspect ratio live in :root — don't change them elsewhere.
 * ------------------------------------------------------------------------- */

/* --- typefaces ------------------------------------------------------------
 * Self-hosted on purpose: no request leaves the visitor's browser. Both files
 * are variable fonts, so one file per subset covers every weight we use.
 * Czech needs BOTH subsets — á í é ý ó ú live in "latin", ř ě ů č š ž ď ť ň in
 * "latin-ext". Dropping either breaks words mid-glyph.
 * ---------------------------------------------------------------------- */

@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 200 800;   /* variable; also carries an opsz 6–72 axis */
  font-display: swap;
  src: url(assets/fonts/newsreader-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url(assets/fonts/newsreader-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 100 700;   /* variable */
  font-display: swap;
  src: url(assets/fonts/plex-sans-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url(assets/fonts/plex-sans-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* Official KDU-ČSL palette — Grafický manuál KDU-ČSL 2022 (v0.3), § 0.3
     "Základní barevnost": https://www.kdu.cz/o-nas/grafika/graficky-manual
       MODRÁ  CMYK 100/70/0/50   RGB 0/50/100     #003264
       ŽLUTÁ  CMYK 0/15/90/0     RGB 255/215/25   #FFD719
       BÍLÁ   CMYK 0/0/0/0       RGB 255/255/255  #FFFFFF
     Held as channels so the scrims further down can vary alpha off the same
     source instead of hardcoding the colour again. */
  --primary-rgb: 0 50 100;
  --primary-dark-rgb: 0 31 62; /* derived — the manual defines no dark blue, and
                                  the photo needs a deeper ground than MODRÁ */
  --accent-rgb: 255 215 25;

  --color-primary: rgb(var(--primary-rgb));
  --color-primary-dark: rgb(var(--primary-dark-rgb));
  --color-accent: rgb(var(--accent-rgb));

  --color-text: #12253d;
  --color-paper: #f6f3ec; /* warm stock — the card is printed matter, not screen white.
                             The manual's BÍLÁ is for logo grounds, not every surface. */

  /* Ink for the profile card. It is a ballot slip, so it is set in ink:
     the party yellow deliberately never appears inside the card — on this
     paper it measures 1.26:1, far below any legible threshold. */
  --ink-rgb: 22 35 58;
  --ink: rgb(var(--ink-rgb));
  --ink-muted: #5d6b7e;
  --rule: rgb(var(--ink-rgb) / .16);
  --rule-strong: rgb(var(--ink-rgb) / .42);

  --font-display: "Newsreader", Georgia, "Times New Roman", serif;
  --font-ui: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* Aspect ratio of the photo. Set this to the real photo's ratio after swapping. */
  --photo-ratio: 16/9;

  /* The portraits' aspect ratio, 3:4 (1200×1600 and the like), as two numbers
     so calc() can use it either way up. Every rule that has to meet the
     portrait's edge — the panel, the shading box, the card's top on a phone —
     reads these; the one thing that can't is the @media bound in the
     "portrait" section, which repeats 3/4 as a literal. If the portraits are
     ever reshot to another ratio, change it here and there. Two of the fifteen
     are narrower than 3:4 (09, and 14 at 2:3) and leave a strip of bare paper
     inside that edge rather than overflowing it. */
  --portrait-w: 3;
  --portrait-h: 4;

  /* Width the contain-fitted portrait actually takes: its height decides it.
     Here that height is the window; the wide-landscape block subtracts the
     card from it, since there the photo stops above the card instead of
     running behind it. */
  --portrait-width: calc(100dvh * var(--portrait-w) / var(--portrait-h));

  --fade-duration: 720ms;
  --ease-out: cubic-bezier(.22, .61, .36, 1);
  --banner-height: 0px; /* height of the dev banner, set further down */
  /* height of the profile card — app.js measures it and sets the real value
     on <html> whenever a profile is open; the guess here only covers rules
     evaluated before then */
  --card-height: 40dvh;
  /* left edge of the profile card. On the wide layout app.js sets it to the
     kandidátka's own left edge, so the card sits under that list and the
     portrait alone, and the letter column beside it keeps the full height.
     0 — the card spanning the window — is every other layout. */
  --card-left: 0px;
}

* { box-sizing: border-box; }

/* Every type size in this sheet is a clamp() whose ceiling is in rem, and the
   ceilings all bind somewhere between 780px and 1200px wide. Past that the text
   was frozen at the browser's 16px while the layout kept growing — the portrait
   is calc(100dvh * 3/4) — so a 4K window read as a big photo with a 1440p
   caption stuck to it. Scaling the root scales every rem ceiling with it, so the
   whole sheet grows in proportion and no individual rule needs retuning.
   Both axes, not width alone: the layout is fitted to the window in both
   directions (the portrait's width comes off the window height, the panel takes
   what is left, the card is capped at 62dvh), so a 3840×1200 ultrawide must take
   the height's scale, not the width's, or the card and the panel programme
   overflow. The shorter side decides. Both terms are 16px at 2560×1440, which is
   the reference this is pinned to.
   The floor is 1em, not 16px: on the root element that is the reader's own
   default text size, so a reader who raised it keeps it, and nothing at or below
   the reference moves. It also keeps dvh from ever binding on a phone, where it
   jitters as the URL bar hides — the min() term is about 2px there. */
html { font-size: max(1em, min(100vw / 160, 100dvh / 90)); }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

body {
  background: var(--color-primary-dark);
  color: var(--color-text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* --- the photo ------------------------------------------------------------ */

.scroller {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start; /* the photo is never narrower than the screen */
  overflow: hidden;
  overflow-x: auto; /* pan when the photo is wider than the screen */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  background: var(--color-primary-dark);
}

.scroller::-webkit-scrollbar { display: none; }

/* nothing to pan while a profile covers the photo */
body.profile-open .scroller { overflow-x: hidden; }

/* The photo covers the screen — it is never letterboxed. A screen wider than
   the photo crops the top and bottom, a narrower one overflows sideways and
   .scroller pans. */
.stage {
  position: relative;
  flex: none;
  aspect-ratio: var(--photo-ratio);
  width: max(100vw, calc(100dvh * var(--photo-ratio)));
}

#photo {
  display: block;
  width: 100%;
  height: 100%;
  user-select: none;
  -webkit-user-drag: none;
}

/* The candidate's portrait, crossfaded over the group photo (app.js).
   Inside .profile rather than within #stage: #stage is sized to the group
   photo's aspect ratio and can be wider than the screen (it pans on phones),
   which would off-centre a differently-shaped photo positioned inside it —
   and possibly mid-scroll. .profile is fixed to the viewport with inset: 0,
   so absolute here is the same geometry fixed used to give, and on a portrait
   phone — where the profile is one scrolling page — the portrait travels up
   with the card instead of standing still behind it.
   z-index: -2 keeps it under .profile::after (its own shading, -1), the card
   and the controls; the negative depth stays inside .profile's stacking
   context, so nothing outside the dialog is affected. */
#person-photo {
  position: absolute;
  z-index: -2;
  top: 0;
  left: 0;
  width: 100%;
  /* The full window, not just the band above the profile card: the portrait
     fills the screen and the card sits over its lower part. Set explicitly
     rather than with inset: 0 — a replaced element like <img> ignores
     top+bottom insets and sizes to its intrinsic dimensions instead. */
  height: 100dvh;
  /* Portrait photos are much narrower than the stage's landscape box —
     cover would crop most of the person away to fill it. contain shows
     the whole photo instead, and that leaves a band beside it on any window
     wider than 3:4. background-color on the <img> itself paints those bands
     over the group photo still on the stage behind — paper, not navy, so
     they read as the sheet the portrait is mounted on and carry straight into
     the card below. On a wide window the panel covers this entirely. */
  object-fit: contain;
  /* On a narrow phone the box is taller than the portrait can fill, and
     centring would split the leftover height into a band above the head and
     one below. Hang the photo from the top instead: all the slack falls to
     the bottom, where the card covers it anyway. No effect on a wide window,
     where the photo is already height-constrained. */
  object-position: top;
  background: var(--color-paper);
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
  opacity: 0;
  /* Never display: none — the layer is always laid out, so the fade has a
     start state to run from. Hidden with visibility instead, once the
     fade-out is over; on the way in the flip is immediate. (The same pattern
     hides the three overlays further down.) */
  visibility: hidden;
  transition: opacity var(--fade-duration) var(--ease-out),
              visibility 0s linear var(--fade-duration);
}

body.showing-person #person-photo {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

/* hotspots belong to the group photo — they fade out with it */
.hotspot { transition: opacity .35s ease; }
body.showing-person .hotspot { opacity: 0; pointer-events: none; }

/* --- clickable faces ------------------------------------------------------ */

.hotspot {
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  width: calc(var(--d) * 1%);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.hotspot::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: .18em solid var(--color-accent);
  box-shadow: 0 0 0 .35em rgb(var(--primary-rgb) / .22);
  opacity: 0;
  transform: scale(.82);
  transition: opacity .25s ease, transform .25s var(--ease-out);
}

.hotspot:hover::before,
.hotspot:focus-visible::before { opacity: 1; transform: scale(1); }
.hotspot:focus { outline: none; }

/* name label, desktop only */
.hotspot::after {
  content: attr(data-name);
  position: absolute;
  left: 50%;
  bottom: -.4em;
  transform: translate(-50%, 100%);
  padding: .35em .7em;
  border-radius: 999px;
  background: var(--color-primary);
  color: #fff;
  font-size: clamp(.6875rem, 1.05vw, .9375rem);
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  transition: opacity .2s ease;
  pointer-events: none;
}

.hotspot:hover::after,
.hotspot:focus-visible::after { opacity: 1; }

/* a few pulses after load, to show the faces are clickable */
.hotspot.pulsing::before { animation: hotspot-pulse 2.6s ease-out 3; }
@keyframes hotspot-pulse {
  0%, 60%, 100% { opacity: 0; transform: scale(.82); }
  72% { opacity: .75; transform: scale(1); }
}

/* --- title and slogan ----------------------------------------------------- */

.site-title, .slogan {
  position: fixed;
  left: 0;
  right: 0;
  z-index: 3;
  margin: 0;
  padding: clamp(.7rem, 2.4vh, 1.6rem) clamp(1rem, 4vw, 2.5rem);
  text-align: center;
  pointer-events: none;
  transition: opacity .35s ease, transform .45s var(--ease-out);
}

.site-title {
  top: var(--banner-height);
  background: linear-gradient(to bottom, rgb(var(--primary-dark-rgb) / .82), rgb(var(--primary-dark-rgb) / .55) 55%, transparent);
  padding-top: calc(clamp(.7rem, 2.4vh, 1.6rem) + env(safe-area-inset-top));
}

.site-title h1 {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: .12em;
  line-height: 1.05;
}

.site-title-main {
  font-family: var(--font-ui);
  font-size: clamp(1.35rem, 4.2vw, 3.1rem);
  font-weight: 600;
  letter-spacing: 0;
  color: #fff;
  text-shadow: 0 2px 14px rgb(var(--primary-dark-rgb) / .55);
}

/* Official horizontal logotype (assets/logo.svg) in place of a typed "KDU-ČSL".
   It leads the header, above the title and larger than it.
   h1 is a column flexbox, so the image needs align-self or it stretches. */
.site-title .logo {
  align-self: center;
  width: auto;
  height: clamp(2.2rem, 9vw, 4.6rem);
  margin-bottom: .2em;
  filter: drop-shadow(0 2px 10px rgb(var(--primary-dark-rgb) / .6));
}

.slogan {
  bottom: 0;
  background: linear-gradient(to top, rgb(var(--primary-dark-rgb) / .88), rgb(var(--primary-dark-rgb) / .6) 55%, transparent);
  padding-bottom: calc(clamp(.7rem, 2.4vh, 1.6rem) + env(safe-area-inset-bottom));
}

.slogan p {
  margin: 0;
  font-family: var(--font-ui);
  font-size: clamp(.92rem, 2.75vw, 2rem);
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #fff;
  text-shadow: 0 2px 12px rgb(var(--primary-dark-rgb) / .6);
}

.slogan p span { white-space: nowrap; }

/* title and slogan step aside while a profile is open */
body.profile-open .site-title { opacity: 0; transform: translateY(-40%); }
body.profile-open .slogan { opacity: 0; transform: translateY(60%); }

/* --- swipe hint (only when the photo hangs off the sides) ----------------- */

.scroll-hint {
  position: fixed;
  left: 50%;
  bottom: clamp(4.2rem, 12vh, 6rem);
  transform: translateX(-50%);
  z-index: 3;
  margin: 0;
  padding: .5em 1em;
  border-radius: 999px;
  background: rgb(var(--primary-dark-rgb) / .72);
  color: #fff;
  font-family: var(--font-ui);
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .04em;
  white-space: nowrap;
  pointer-events: none;
  display: none;
  transition: opacity .5s ease;
}

body.pannable .scroll-hint { display: block; }
.scroll-hint.hidden { opacity: 0; }

/* the photo can be dragged sideways with a mouse */
body.pannable .scroller { cursor: grab; }
body.pannable.dragging .scroller,
body.pannable.dragging .hotspot { cursor: grabbing; }

/* The portrait's own shading, sized to the photo rather than to the window:
   an edge and an inner shadow, so the print reads as sitting in the paper
   instead of floating flat on it — and no navy touches the paper itself.
   The box is where object-fit: contain puts the photo — as wide as the
   window or as wide as 3:4 of its height, whichever is less, hung from the
   top (object-position) and centred until the panel pins it right.
   z-index: -1 keeps it under the panel, the card and the controls while
   staying above the photo, which is the -2 layer of the same context. */
.profile::after {
  content: "";
  position: absolute;
  z-index: -1;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(100vw, var(--portrait-width));
  height: min(100dvh, calc(100vw * var(--portrait-h) / var(--portrait-w)));
  box-shadow:
    inset 0 0 0 1px rgb(var(--primary-dark-rgb) / .14),
    inset 0 0 clamp(2.5rem, 7vw, 7rem) rgb(var(--primary-dark-rgb) / .3);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--fade-duration) var(--ease-out);
}

body.showing-person .profile::after { opacity: 1; }

/* A window wider than the portrait's own 3:4 can't be filled by contain, and
   without the panel there is nothing to put beside it — so fill the box and
   crop instead of leaving a band down each side. object-position: top means
   it is the feet that go, never the face. Below this ratio (any phone held
   upright) contain already fills the width and nothing here applies; the wide
   window puts contain back, because there the panel takes the rest.
   The other two bounds are where the crop stops being worth it. Cropping to
   fill costs zoom of ratio ÷ (3/4), and the card takes the bottom of what is
   left, so on a wide or a short window the only thing still on screen is the
   top of someone's head — 1000x650 and 844x390 (a phone held sideways) crop
   to a scalp, and both fall outside these bounds; 768x1024 is the portrait's
   own ratio, so it does not crop at all. The squarer windows inside them
   cropped to a scalp as well (820x800 kept 30%), and the "squarish windows"
   block further down takes those back. Better a small whole portrait on paper
   than a large piece of one. */
/* 3/4 here is the portrait ratio — a media query can't read --portrait-w/h */
@media (min-aspect-ratio: 3/4) and (max-aspect-ratio: 5/4) and (min-height: 600px) {
  #person-photo { object-fit: cover; }

  /* the photo is the whole window now, so its shading is too */
  .profile::after {
    left: 0;
    transform: none;
    width: 100vw;
    height: 100dvh;
  }
}

/* --- profile -------------------------------------------------------------- */

.profile {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  /* Laid out while closed, hidden with visibility: a display:none element has
     nothing to transition from, and app.js measures the card while the
     profile is shut. visibility drops the overlay from the tab order and the
     accessibility tree the same way; the flip waits for the card to slide out
     and the back button to fade (.22s + .5s below) and for the portrait's
     fade (--fade-duration), whichever is longer. */
  visibility: hidden;
  transition: visibility 0s linear 780ms;
}

/* While a profile is open the overlay doubles as the backdrop: it sits above
   the photo, so clicking it dismisses the profile (app.js). It stops catching
   clicks the moment .profile-open comes off, i.e. as soon as the card starts sliding
   out. */
body.profile-open .profile {
  visibility: visible;
  transition-delay: 0s;
  pointer-events: auto;
  cursor: zoom-out;
}

/* The navy glass chip every control on the photo is set in: the back button,
   the arrows, the board's Zavřít and the opener in the header (index.html
   gives each the class). A button adds only its own place and size. */
.chip {
  border: 1px solid rgb(255 255 255 / .3);
  border-radius: 2px;
  background: rgb(var(--primary-dark-rgb) / .78);
  color: #fff;
  font-family: var(--font-ui);
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .09em;
  text-transform: uppercase;
  cursor: pointer;
  pointer-events: auto;
  backdrop-filter: blur(6px);
  transition: background .2s ease;
}

.chip:hover { background: var(--color-primary); }
.chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }

.back-button {
  position: absolute;
  top: calc(var(--banner-height) + clamp(.8rem, 2.5vh, 1.6rem) + env(safe-area-inset-top));
  left: clamp(.8rem, 3vw, 2rem);
  padding: .7em 1.1em;
  opacity: 0;
  transform: translateY(-.6rem);
  transition: opacity .3s ease .2s, transform .3s var(--ease-out) .2s, background .2s ease;
}

body.profile-open .back-button { opacity: 1; transform: none; }

/* Previous / next on the kandidátka, flanking the portrait — the same chip as
   the back button, so the controls of the open profile read as one set.
   Anchored to the viewport, not to the card: the card's height follows the
   bio and it scrolls its own overflow, which would drag the arrows along with
   it. */
.profile-nav {
  position: absolute;
  top: 34dvh;
  display: grid;
  place-items: center;
  width: 2.9rem;
  height: 2.9rem;
  /* a glyph, not a label: the chip's label type would push it off centre */
  font-size: 1.6rem;
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1;
  opacity: 0;
  transform: translateY(-50%) scale(.92);
  transition: opacity .3s ease .2s, transform .3s var(--ease-out) .2s, background .2s ease;
}

/* display: grid above would otherwise beat the hidden attribute */
.profile-nav[hidden] { display: none; }

/* ‹ and › are cut to sit in running text, so their ink rides .091em below the
   middle of the line box place-items centres — that is the glyph's own bbox
   (80–488) against Plex's ascent and descent (1025/-275 per 1000 em), not a
   number picked by eye. Hard-coding it is safe now that the glyph comes from
   our own file on every device instead of from whatever the phone had. */
.profile-nav span { translate: 0 -.091em; }

.profile-nav-prev { left: clamp(.8rem, 3vw, 2rem); }
.profile-nav-next { right: clamp(.8rem, 3vw, 2rem); }

body.profile-open .profile-nav { opacity: 1; transform: translateY(-50%); }

/* --- the paper panel beside the portrait ---------------------------------- */

/* The portrait is contain-fitted, so a wide window can't be filled by it: it
   bleeds off the right edge at its own 3:4 and everything left of it is a
   second slip off the same hlasovací lístek, carrying the whole kandidátka
   with this candidate's line the only one fully inked — and the programme
   beside it when there is room. Panel and photo meet exactly on
   --portrait-width, so no navy is left over between them. */
.ballot-panel { display: none; } /* only a wide landscape window has the room */

.ballot-margin {
  display: grid;
  align-content: center;
  justify-items: start; /* flush left, ragged toward the portrait */
  /* Fifteen names are a fixed height; the paper they sit on is not — the card
     takes the same bite out of a short window as a tall one, so what is left
     runs out before the list does. The leading is the slack: .5em of it is
     right on a tall window and pure surplus on a short one, so it ramps down
     with the height instead of the last names going behind the card. Above
     about 1025px it clamps to .5em and nothing has moved. */
  gap: clamp(.15em, 1.6vh - .6rem, .5em);
  margin: 0;
  padding: 0;
  list-style: none;
  white-space: nowrap; /* a long name overhangs its measure rather than wrapping */
  font-family: var(--font-ui);
  font-size: clamp(.72rem, .95vw, .85rem);
  line-height: 1.15;
  letter-spacing: .04em;
  color: rgb(var(--ink-rgb) / .5);
}

.ballot-margin-num {
  margin-right: .6em;
  font-variant-numeric: tabular-nums;
  color: rgb(var(--ink-rgb) / .35);
}

/* the one you're on — the only line at full strength. No gold: on this page
   gold means "you can click this", and the list is set as printed matter. */
.ballot-margin .is-current { color: var(--ink); font-weight: 600; }
.ballot-margin .is-current .ballot-margin-num { color: var(--ink); }

/* Not one block of type any more but two sheets of paper dividing the panel
   between them: the programme's on the left, the candidate's — kandidátka,
   portrait, card — on the right. They are stretched to a common height, and
   the boundary between them is what the card's left edge is cut along, drawn
   the window's full height in the wide block below.
   The gap is the air left of that cut, and it is the panel's own left-padding
   token, so the letter column ends up with the same margin on both sides —
   which is what the old centring was there to give it. */
.ballot-sheet {
  display: flex;
  align-items: stretch;
  gap: clamp(1.5rem, 4vw, 4rem);
  width: 100%;
  max-height: 100%;
  min-height: 0;
}

/* The candidate's sheet above the card: all the paper the letter column
   leaves, with the names set at its far end, against the portrait — the photo
   and the bio are what the kandidátka belongs to, not the programme.
   No min-width: 0. The default min-width: auto floors this at the names' own
   measure, so where the paper is too narrow for both columns it is .panel-main
   that gives up the width and never the list. */
.ballot-side {
  flex: 1 1 auto;
  display: flex;
  justify-content: flex-end;
  /* the air right of the cut, the same token as the gap left of it */
  padding-left: clamp(1.5rem, 4vw, 4rem);
}

/* The address with the programme set between its opening and closing words,
   left of the kandidátka — but only when the panel measures wide enough to set
   it (the @container rule further down). On a short window the column scrolls
   as one page of paper — opening words, programme and closing words moving
   together — so all fifteen names stay on the sheet beside it. */
.panel-main {
  display: none;
  /* the sheet's width goes to this column, the bio being on the card: a
     programme point runs to about 58ch, so on a wide window most set on one
     line, and a column that cuts them a word short reads as a mistake. A width
     of its own, not the programme's: the address adds nothing to it (below),
     and the programme is not always there. */
  flex: 0 1 auto;
  width: 96ch;
  min-width: 0;
  flex-direction: column;
  min-height: 0;
}

/* The column itself: the three blocks as one running page, and the one part
   of the panel that gives up height — everything past the window's bottom
   scrolls into view rather than being cut off. It stays a flex column so the
   blocks' own flex: none and the margins between them read as they did. */
.panel-scroll {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  /* the page behind is the group photo and the stepping gesture — a swipe
     that runs past the end of the column must not carry on into it */
  overscroll-behavior: contain;
  /* an overlay scrollbar takes no width and would sit on the line ends — this
     is its lane; a classic scrollbar just gets a little air beside it */
  padding-right: 1rem;
}

/* It reuses the notice board's own list styling, printed scroll shadows and
   all; here it sets out in full and the column around it scrolls. */
.panel-program {
  display: flex;
  flex: none;
  flex-direction: column;
}

.panel-program[hidden] { display: none; } /* no points to print */

/* with no programme between them the address's two halves are one letter:
   only the intro's margin between them, not both */
.panel-program[hidden] + .panel-outro { margin-top: 0; }

/* and nothing to promise below the fold either — the hint sits outside the
   scroller now, so it is the column, not the programme, it is a sibling of */
.panel-main:has(.panel-program[hidden]) .program-more { display: none; }

/* The party's address above the programme and its closing words below it —
   set in the bio's voice, as printed matter: no gold, no box. */
.panel-intro,
.panel-outro {
  flex: none;
  /* stretches to the column but adds nothing to its width — a paragraph's
     one-line max-content would otherwise ask for the whole window */
  contain: inline-size;
  font-family: var(--font-display);
  font-size: clamp(.95rem, 1.05vw, 1.12rem);
  line-height: 1.6;
  text-align: justify;
  hyphens: auto; /* lang="cs": without it a justified line opens holes */
  color: var(--ink);
}

.panel-intro { margin-bottom: clamp(1rem, 3.5vh, 3rem); }
.panel-outro { margin-top: clamp(1rem, 3.5vh, 3rem); }

.panel-intro-salutation { margin: 0 0 .5em; font-size: 1.12em; font-weight: 700; }
.panel-intro-text,
.panel-outro-text { margin: 0; }
.panel-outro-text + .panel-outro-text { margin-top: .5em; }
/* where to find us on the ballot — the one line in party blue */
.panel-outro-vote { font-weight: 700; color: var(--color-primary); text-align: center; }

/* set a little tighter than on the letter — the panel has one window's height
   to work with, and the column reads better with the programme a shade
   smaller than the address around it. */
.panel-program .program-list {
  font-size: clamp(.82rem, .92vw, .95rem);
  line-height: 1.45;
}

.panel-program .program-list li { margin-bottom: .4em; }

/* The shadows' replacement: app.js marks an edge only while there is more
   column past it, and the text there fades out like a page running on. The
   top fade runs from the very first pixel: on the list it used to start 1px
   down to keep that block's printed rule inked, but the rule scrolls with the
   column now and the reserved pixel only left a hairline of unfaded text
   standing above the fade. */
.panel-main.more-below .panel-scroll {
  -webkit-mask-image: linear-gradient(#000 calc(100% - 3em), transparent);
  mask-image: linear-gradient(#000 calc(100% - 3em), transparent);
}
.panel-main.more-above .panel-scroll {
  -webkit-mask-image: linear-gradient(transparent, #000 3em);
  mask-image: linear-gradient(transparent, #000 3em);
}
.panel-main.more-above.more-below .panel-scroll {
  -webkit-mask-image: linear-gradient(transparent, #000 3em, #000 calc(100% - 3em), transparent);
  mask-image: linear-gradient(transparent, #000 3em, #000 calc(100% - 3em), transparent);
}

/* and in words, in the column headings' voice — it keeps its line when
   hidden, so reaching the end doesn't make the column jump */
.program-more {
  margin: .5em 0 0;
  font-family: var(--font-ui);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--color-primary);
  opacity: 0;
  transition: opacity .25s ease;
}
.panel-main.more-below .program-more { opacity: 1; }

/* Every candidate is a link in both lists — inside the modal this is the
   only way to reach someone who isn't the next or previous one. It inherits
   the list's type, so only the cursor and the hover colour give it away.
   The ring is party blue rather than the gold used on the photo: gold on this
   paper measures 1.26:1, i.e. an indicator you cannot see. */
.ballot-link {
  display: block;
  margin: 0;
  padding: .15em 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  text-align: inherit;
  text-decoration: none;
  cursor: pointer;
}

/* The current line is set bold, and bold is wider: without this the widest
   names widen the kandidátka when they're current and the columns beside it
   rewrap. Every piece of text reserves its bold width up front. */
.ballot-weight {
  display: inline-flex;
  flex-direction: column;
}
.ballot-weight::after {
  content: attr(data-text);
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: 600;
}

.ballot-link:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
}

.ballot-margin .ballot-link:hover,
.ballot-margin .ballot-link:hover .ballot-margin-num,
.ballot-strip .ballot-link:hover { color: var(--color-primary); }

/* The second ramp, for windows shorter than the gap alone can answer: the
   line's own padding goes the same way. It holds its full .15em until about
   727px, where the gap above has reached its own floor and has nothing left
   to give, and is gone by about 640px — so a window with room to spare keeps
   the whole hit target. It is 2px a side at most, and what it gives up at the
   bottom of the range the tightened leading had already given up. */
.ballot-margin .ballot-link { padding-block: clamp(0em, 2.33vh - .93rem, .15em); }

/* Only where there is a band wide enough to be a slip of paper — and only in
   the person-photo state, since it is the portrait's edge the panel meets. */
@media (min-width: 1024px) and (orientation: landscape) {
  /* The photo stops where the card starts, so the whole portrait is on screen:
     the card spans the window here, and a full-height photo hid everything
     below the waist behind it. The box is the window minus the card, the photo
     is fitted to its height, and --portrait-width — which the panel, the
     shading and the prev arrow all measure off — follows it down. */
  :root {
    --portrait-width: calc((100dvh - var(--card-height)) * var(--portrait-w) / var(--portrait-h));
    /* How far each half of the cut sheet stands off the window while the
       profile is shut. The programme's goes out sideways, the width of its own
       paper; the candidate's goes down the window's whole height, and the blur
       of its edge past that, so nothing of it shows along the bottom.
       One length each, not a percentage of the boxes that carry them: the
       letter column, the names, the card and the paper under them are four
       different sizes, and a percentage gives each of them a different
       distance. They arrive together or they are not one sheet. */
    --sheet-slide: calc(-1 * var(--card-left) - 1px);
    --sheet-drop: calc(100dvh + 2.125rem);
  }

  /* the portrait bleeds off the right edge instead of sitting centred, so the
     panel has the whole of the rest to itself */
  /* contain again, not the crop above: here the panel fills what the portrait
     leaves, so the whole picture can be shown */
  #person-photo {
    object-fit: contain;
    object-position: right top;
    height: calc(100dvh - var(--card-height));
  }

  /* the shading follows the photo to the right edge — and to its new bottom */
  .profile::after {
    left: auto;
    right: 0;
    transform: none;
    width: var(--portrait-width);
    height: calc(100dvh - var(--card-height));
  }

  /* The card used to throw its top edge up onto the print; now that it is one
     sheet with the kandidátka, joined by a rule and nothing more, the photo
     lost the shadow along its bottom — the paper below simply stopped. Put it
     back on the print itself, at the offset and blur the card used to cast, so
     it falls only where the photo is and not on the paper beside it. The panel
     still throws its own edge across the photo's left, so that side keeps the
     shading it always had. */
  body.panel-card-inset .profile::after {
    box-shadow:
      inset 0 0 0 1px rgb(var(--primary-dark-rgb) / .14),
      inset 0 0 clamp(2.5rem, 7vw, 7rem) rgb(var(--primary-dark-rgb) / .3),
      inset 0 -.875rem 2.125rem rgb(var(--primary-dark-rgb) / .26);
  }

  .ballot-panel {
    /* The sheet takes the paper's whole width — the two sheets inside it are
       what divide it up, and the kandidátka only reaches the portrait if its
       half runs that far. (It used to be a max-content column centred here,
       which sized the sheet to its type and left the names stranded in the
       middle.) The row may still shrink to what the card leaves, and the
       programme inside it gives up the difference. */
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, auto);
    align-content: center;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: var(--portrait-width);
    /* full-height paper, but its contents sit in what the card leaves of it —
       centring on the window would push them behind the card */
    /* the top is a floor, not a clamp: the columns start at the top of the
       sheet now, so on a short window the first heading would run in under the
       back button. Block padding, so the @container rules below — which measure
       the inline size — keep the breakpoints they were tuned at.
       The bottom is not the card's any more: the card starts at the kandidátka
       now, so the letter column runs to the foot of the window and it is the
       list alone that reserves the card's height (below). */
    padding: max(4rem, clamp(1.5rem, 5vh, 3.5rem)) clamp(1.25rem, 3vw, 2.5rem)
             clamp(1.5rem, 5vh, 3.5rem)
             clamp(1.5rem, 4vw, 4rem);
    /* the card's printed edge turned ninety degrees: the cut on the right,
       its hairline 3px inside it, the shadow thrown across the photo */
    background:
      linear-gradient(var(--rule), var(--rule)) right 3px top / 1px 100% no-repeat,
      var(--color-paper);
    border-right: 1px solid var(--rule-strong);
    box-shadow: 1px 0 0 rgb(var(--primary-dark-rgb) / .55),
                .875rem 0 2.125rem rgb(var(--primary-dark-rgb) / .26);
    /* paper, so it must not carry the backdrop's dismiss-on-click either */
    pointer-events: auto;
    cursor: auto;
    /* in from its own edge, the way the card comes up from the bottom */
    transform: translateX(-101%);
    transition: transform .5s var(--ease-out) .22s;
    /* the panel measures itself for the programme below: how much room it has
       depends on the window's height as much as its width, since the portrait
       is fitted to the height — no viewport query can answer that */
    container-type: inline-size;
  }

  /* Centred against the address and programme beside it. Fifteen names can
     still outgrow a very short sheet: `safe` centring packs the list to the top
     then, so the overflow goes down into the panel's own bottom margin — clear
     paper — rather than up under the back button.
     The card covers this column and no longer the one beside it, so the height
     it takes is reserved here rather than on the panel: the names centre in
     what is left above it. */
  .ballot-margin {
    align-content: safe center;
    padding-bottom: var(--card-height);
  }

  /* The paper is cut in two at the card's left edge and the halves come in from
     different directions — so neither of them can be the panel's own sheet any
     more. The panel keeps its box and gives up every mark it made: the paper is
     the two pseudo-elements below, each with its own half of the print and its
     own way in. It must not travel itself either, or everything on it would
     travel twice.
     The border stays, only colourless: it is 1px of the panel's width, and
     without it the sheet inside would shift and the cut with it. */
  body.panel-card-inset .ballot-panel {
    background: none;
    border-right-color: transparent;
    box-shadow: none;
    transform: none;
    isolation: isolate;
  }

  /* The programme's sheet: the paper from the window's edge to the cut, nothing
     printed down either side — the cut is the other sheet's own edge and lies on
     top of this one. In from the left, the way the whole panel used to come. */
  body.panel-card-inset .ballot-panel::before {
    content: "";
    position: absolute;
    z-index: -1;
    top: 0;
    bottom: 0;
    left: 0;
    width: var(--card-left);
    background: var(--color-paper);
    pointer-events: none;
    transform: translateX(var(--sheet-slide));
    transition: transform .5s var(--ease-out) .22s;
  }

  /* The candidate's sheet: the paper from the cut to the portrait, the window's
     full height, the kandidátka standing on it and the card at its foot. Both
     its edges are printed — on the left the cut, its hairline 3px inside it and
     the shadow thrown across the programme's paper; on the right the edge
     against the photo, which used to be the panel's and belongs to this half
     now. It is set back over the panel's colourless border (right: -1px) so it
     lands on the same pixels it always did.
     Behind the names, not over them, so the paper and the type on it are one
     thing that moves: the two lines are the leftmost 5px of the sheet and the
     names are set flush right against the portrait, so nothing of the list was
     ever standing on them. */
  body.panel-card-inset .ballot-panel::after {
    content: "";
    position: absolute;
    z-index: -1;
    top: 0;
    bottom: 0;
    left: var(--card-left);
    right: -1px;
    background:
      linear-gradient(var(--rule-strong), var(--rule-strong)) 0 0 / 1px 100% no-repeat,
      /* 4px, not the 3px the cut edges use: theirs is measured from inside a
         1px border, this line is the border */
      linear-gradient(var(--rule), var(--rule)) 4px 0 / 1px 100% no-repeat,
      linear-gradient(var(--rule), var(--rule)) right 3px top / 1px 100% no-repeat,
      var(--color-paper);
    border-right: 1px solid var(--rule-strong);
    box-shadow: -1px 0 0 rgb(var(--primary-dark-rgb) / .55),
                -.875rem 0 2.125rem rgb(var(--primary-dark-rgb) / .26),
                1px 0 0 rgb(var(--primary-dark-rgb) / .55),
                .875rem 0 2.125rem rgb(var(--primary-dark-rgb) / .26);
    pointer-events: none;
    transform: translateY(var(--sheet-drop));
    transition: transform .5s var(--ease-out) .22s;
  }

  /* What is set on each half goes the same length as the paper under it. */
  body.panel-card-inset .panel-main {
    transform: translateX(var(--sheet-slide));
    transition: transform .5s var(--ease-out) .22s;
  }

  body.panel-card-inset .ballot-side {
    transform: translateY(var(--sheet-drop));
    transition: transform .5s var(--ease-out) .22s;
  }

  /* and the card is the foot of that sheet, so it waits where the rest of the
     sheet waits — a window below, not a card's height below. It comes further,
     and so faster, than it did as a slip laid on top; the names above it would
     tear off otherwise. */
  body.panel-card-inset .profile-card { transform: translateY(var(--sheet-drop)); }

  /* .showing-person, not .profile-open: the panel is the paper the portrait is
     mounted on, so it comes in with the portrait — and stays away if that
     photo failed to load (app.js) and the group photo is all there is */
  body.showing-person .ballot-panel { transform: none; }

  /* Where the paper is cut, the halves are let go in the same frame — app.js
     adds both classes inside one open().
     The programme's half goes with the portrait, like the panel it came off.
     The candidate's half goes with the card, because the card is on that sheet
     and the card comes in whether the photo arrived or not. */
  body.panel-card-inset.showing-person .ballot-panel::before,
  body.panel-card-inset.showing-person .panel-main { transform: none; }

  body.panel-card-inset.profile-open .ballot-panel::after,
  body.panel-card-inset.profile-open .ballot-side,
  body.panel-card-inset.profile-open .profile-card { transform: none; }

  /* on the portrait with its partner, not in the paper: the pair flanks the
     candidate they step between */
  body.showing-person .profile-nav-prev {
    left: calc(100% - var(--portrait-width) + clamp(.8rem, 3vw, 2rem));
  }
}

/* Wide enough to set the address and programme as well as the list. Not a
   viewport width: a tall window makes the portrait wide and the panel narrow
   whatever the window's width is. Browsers without container queries simply
   never show it. */
@container (min-width: 46rem) {
  .panel-main { display: flex; }
}

/* --- kandidátka as numbers ------------------------------------------------ */

/* Where the window is too narrow for the panel, the list survives as bare
   numbers across the head of the card: enough to place this candidate among
   the fifteen without scrolling for it. Set in the text column rather than
   spanning both — a spanning row would inflate the `auto` number gutter and
   drag the name off to the right. */
.ballot-strip {
  grid-column: text;
  grid-row: 1;
  display: flex;
  flex-wrap: wrap;
  /* a numbering rail across the text measure: the fifteen divide the width
     between them, which both spreads them evenly and gives each number a
     tappable cell rather than a two-digit sliver */
  gap: .4em .2em;
  margin: 0 0 clamp(.9rem, 2.5vh, 1.4rem);
  padding: 0 0 .7rem;
  border-bottom: 1px solid var(--rule);
  list-style: none;
  font-family: var(--font-ui);
  font-size: clamp(.62rem, 2.3vw, .72rem);
  font-variant-numeric: tabular-nums;
  letter-spacing: .05em;
  color: rgb(var(--ink-rgb) / .32);
}

.ballot-strip li { flex: 1 1 auto; }

.ballot-strip .ballot-link {
  width: 100%;
  padding: .35em 0;
  text-align: center;
}

/* marked the way you mark a ballot: a rule under the number, in party blue.
   box-shadow rather than a border so the line doesn't shift the row. */
.ballot-strip .is-current {
  color: var(--ink);
  font-weight: 600;
  box-shadow: 0 2px 0 var(--color-primary);
}

/* after the rule above, not inside the block that sets up the panel — same
   specificity, so whichever comes last in the file wins */
@media (min-width: 1024px) and (orientation: landscape) {
  .ballot-strip { display: none; } /* the panel says it better */
}

/* The card is a slip off the hlasovací lístek: cut edges, printed rules, ink.
   The gold accent stays outside — on the photo hotspots and the party lockup —
   so gold reads as "interactive" and nothing else. */
.profile-card {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* Auto height, capped only as a safety net for an unexpectedly long bio —
     the min-height app.js puts on .profile-bio (reserveCardHeight) is what
     actually keeps the card a constant height across candidates, sized to the
     tallest card rather than a viewport fraction that's either too tall on
     short ones or too short to fit the longest without scrolling. */
  max-height: 62dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: clamp(1.2rem, 3.5vh, 2.2rem) clamp(1.2rem, 5vw, 3rem)
           calc(clamp(1.4rem, 4vh, 2.4rem) + env(safe-area-inset-bottom));
  /* the printed double rule that heads the sheet: the cut edge, then a hairline
     3px under it. A background layer, not a pseudo-element — the default
     `scroll` attachment keeps it pinned while a long bio scrolls past. */
  background:
    linear-gradient(var(--rule), var(--rule)) 0 3px / 100% 1px no-repeat,
    var(--color-paper);
  border-top: 1px solid var(--rule-strong);
  box-shadow: 0 -1px 0 rgb(var(--primary-dark-rgb) / .55), 0 -.875rem 2.125rem rgb(var(--primary-dark-rgb) / .26);
  pointer-events: auto;
  cursor: auto; /* the backdrop's zoom-out cursor must not bleed onto the card */
  transform: translateY(101%);
  transition: transform .5s var(--ease-out) .22s;

  /* gutter holds the ballot number, the text column hangs off it — one shared
     measure and one shared left edge, which per-block max-widths can't give */
  display: grid;
  grid-template-columns: [gutter] auto [text] minmax(0, 58ch);
  justify-content: center;
  align-content: start;
  column-gap: clamp(.9rem, 3vw, 1.6rem);
}

body.profile-open .profile-card { transform: none; }

/* rows 2–4 throughout: .ballot-strip takes row 1 above them, and collapses to
   nothing on the wide screens where it is display: none (the grid has no
   row-gap, so the empty row costs no height) */
.profile-order {
  grid-column: gutter;
  grid-row: 2 / span 2;
  margin: 0;
  text-align: center;
}

/* The printed number box — on the lístek it is a box, not a numeral. One
   rule for both places it is set: the gutter, and (narrow phones only, below)
   run in before the name. */
.profile-order-num,
.profile-name-num {
  display: inline-grid; /* sizes to the digit — the label must not stretch it */
  place-items: center;
  min-width: 2.1em;
  aspect-ratio: 1;
  border: 1px solid var(--rule-strong);
  font-family: var(--font-ui);
  font-weight: 600;
  font-variant-numeric: tabular-nums; /* 1 and 11 keep the same box */
  line-height: 1;
  color: var(--color-primary);
}

.profile-order-num { font-size: clamp(1.3rem, 4vw, 1.8rem); }

/* readable by screen readers, invisible on screen */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Short on purpose: this label sizes the whole gutter track, and a longer word
   ("na kandidátce") pushed it to 90px, cutting the bio to ~19 characters on a
   320px phone. The full ordinal is still announced via .visually-hidden. */
.profile-order-label {
  display: block;
  margin-top: .5em;
  font-family: var(--font-ui);
  font-size: .6rem;
  font-weight: 500;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.profile-card h2 {
  grid-column: text;
  grid-row: 2;
  margin: 0 0 .25rem;
  font-family: var(--font-display);
  font-size: clamp(1.55rem, 4.6vw, 2.5rem);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: 0;
  color: var(--color-primary);
}

/* Only where the gutter's number block is dropped — the narrow-phone block at
   the end of this section turns it on. */
.profile-name-num { display: none; }

.profile-job {
  grid-column: text;
  grid-row: 3;
  margin: 0;
  font-family: var(--font-ui);
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.profile-bio {
  grid-column: text;
  grid-row: 4;
  margin: clamp(1rem, 2.6vh, 1.5rem) 0 0;
  padding-top: clamp(1rem, 2.6vh, 1.5rem);
  border-top: 1px solid var(--rule);
  font-family: var(--font-display);
  font-size: clamp(1rem, 2.3vw, 1.12rem);
  line-height: 1.65;
  text-align: justify;
  hyphens: auto; /* lang="cs": without it a justified line opens holes */
  color: var(--ink);
  /* app.js (reserveCardHeight) sets an inline min-height here, so every
     candidate's card comes out the same height and a short bio just leaves
     blank space below it. Measured there rather than written here as a line
     count: the count depends on the width — the same texts wrap to 4 lines on
     a desktop and 7 on a 320px phone, and on a narrow phone the profession
     line above wraps for some people too — and on bios still being written. */
}

/* Beside the panel every line the card spends is taken off the paper above it,
   which has the address, the programme and fifteen names to fit. So the bio
   runs wider — the card spans the whole window here, and a 58ch column left
   most of it empty — and the card is set tighter. */
@media (min-width: 1024px) and (orientation: landscape) {
  .profile-card {
    padding-block: clamp(.8rem, 2vh, 1.3rem)
                   calc(clamp(.9rem, 2.2vh, 1.4rem) + env(safe-area-inset-bottom));
    grid-template-columns: [gutter] auto [text] minmax(0, 100ch);
  }

  /* The card pulled in to the kandidátka's own left edge — one line running
     from the list straight down into the card — so everything left of it is
     letter column, full height. app.js measures the edge (it moves with the
     window's height, through the portrait's width) and sets the class; without
     the class the card spans the window as it always did, which is what the
     windows too narrow for the letter column still get. */
  body.panel-card-inset .profile-card {
    left: var(--card-left);
    /* The cut on the left, with its hairline 3px inside it — the card paints
       over .ballot-panel::after, so it has to carry the lines itself where the
       two meet.
       No shadow of its own, though: the pseudo-element casts one edge the
       window's full height, and a second one here only doubled it over the
       card's rows and left a step at the seam.
       The top is not an edge at all any more. The kandidátka above and the bio
       below are the same sheet, so what divides them is one printed rule, the
       same one that runs between the programme's points — no cut line, no
       second hairline under it, and no shadow thrown up over the names, which
       is what made the card read as a separate slip laid on top. */
    background:
      linear-gradient(var(--rule), var(--rule)) 3px 0 / 1px 100% no-repeat,
      var(--color-paper);
    border-top-color: var(--rule);
    border-left: 1px solid var(--rule-strong);
    box-shadow: none;
  }

  .profile-card h2 { font-size: clamp(1.55rem, 2.6vw, 2.2rem); }

  .profile-bio {
    margin-top: clamp(.5rem, 1.3vh, .8rem);
    padding-top: clamp(.5rem, 1.3vh, .8rem);
    line-height: 1.55;
  }
}

/* On a narrow phone the gutter costs more than the hanging indent is worth: it
   ate ~28% of the card and cut the bio to ~19 characters. Drop to one column.
   The number block goes with it — measured at 55px of a card the portrait-phone
   block below caps at ~324px, spent on a number the strip above already marks
   in blue. It runs in before the name instead, where it costs nothing: the
   numeral is shorter than the name's line box.
   (Spanning the bio across both tracks instead would let it inflate the `auto`
   gutter and push the name off to the right.) */
@media (max-width: 430px) {
  /* Every millimetre above the bio is a line of bio once the cap bites, so the
     card is set tighter here than it is beside the panel. */
  .profile-card {
    grid-template-columns: minmax(0, 1fr);
    padding-top: clamp(.9rem, 2.4vh, 1.4rem);
    padding-bottom: calc(clamp(1rem, 2.8vh, 1.7rem) + env(safe-area-inset-bottom));
  }

  .ballot-strip,
  .profile-card h2,
  .profile-job,
  .profile-bio { grid-column: 1; }

  .ballot-strip { margin-bottom: clamp(.55rem, 1.5vh, .85rem); }

  .profile-bio {
    margin-top: clamp(.7rem, 1.7vh, 1rem);
    padding-top: clamp(.7rem, 1.7vh, 1rem);
  }

  /* row 2 is left empty and costs nothing — the grid has no row-gap */
  .profile-order { display: none; }

  /* The number keeps the printed box it had in the gutter — on the lístek it is
     a box, not a numeral — but hung off the name's first baseline instead of a
     row of its own. Flex rather than an inline box so a name that wraps runs as
     a block beside the number rather than back under it. */
  .profile-card h2 {
    display: flex;
    align-items: baseline;
    gap: .5em;
  }

  .profile-name-num {
    display: inline-grid;
    flex: none;
    font-size: .6em;
  }
}

/* --- volební program -------------------------------------------------------- */

/* The programme has no window of its own: it is set on the margin panel
   beside the kandidátka, and on the letter. What follows is the type the two
   share — each sets it in its own measure further down. */

/* The programme's headline. With the hairline under it gone, this is the one
   thing that marks the second half of the sheet off from the address above
   it, so it is set in the text face several steps up and in full ink — a
   headline on the page, not a label over a rule. Sentence case and untracked:
   the capitals belong to the group labels and the points' lead-ins below, and
   a heading set the same way would only compete with them. */
.program-kicker {
  margin: 0 0 .7rem;
  font-family: var(--font-display);
  font-size: clamp(1.45rem, 3.2vw, 1.95rem);
  font-weight: 600;
  line-height: 1.15;
  color: var(--ink);
}

/* the points under their headline. It never scrolls on its own — the panel's
   column and the letter's sheet are the pages that do. */
.program-list {
  margin: 0;
  padding: 0 0 .2rem;
  font-family: var(--font-display);
  font-size: clamp(1rem, 2.3vw, 1.1rem);
  line-height: 1.55;
  font-variant-numeric: tabular-nums; /* multi-digit point numbers align */
  color: var(--ink);
  /* the measure the two-column rule below asks about. It is the programme's
     own width that decides, not the window's: the panel's column is wide on a
     desktop but the letter's sheet is 40rem wherever it is read. */
  container: program / inline-size;
}

/* "Budeme prosazovat" / "Podpoříme" — a heading per group, in the kicker's voice */
.program-group {
  margin: 0 0 .5em;
  font-family: var(--font-ui);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--color-primary);
}

/* No bullets and no indent: the label in capitals opens every point, so a
   marker beside it only repeats what the label already says — and on a
   phone's measure the indent it costs is text. */
.program-list ul { margin: 0 0 1em; padding-left: 0; list-style: none; }
.program-list ul:last-child { margin-bottom: 0; }
.program-list li { margin: 0 0 .55em; }

/* The point's lead-in, set in capitals a step above the text it opens, so the
   eye can run down the labels alone. Letter-spacing because capitals set tight.
   On a narrow measure it takes the line above its text, where it is what
   marks the point off from the one before it. */
.program-list strong {
  display: block;
  font-weight: 600;
  font-size: 1.12em;
  text-transform: uppercase;
  letter-spacing: .03em;
}

/* a block in both layouts — a grid cell in the two-column one, its own line
   under the label in the stacked one. Ragged right here: stacked means a
   narrow measure, and the address beside it is set ragged for the same
   reason. The column below justifies it. */
.program-text { display: block; }

/* Where there is measure for it, the label moves out of the text and into a
   column of its own. The grid is on the <ul> and each <li> a subgrid of it,
   so every label in a group starts on the same line down the page — which is
   what makes the capitals read as a column rather than as bold run-ins.
   Each group is a <ul> of its own, so the column is set as a share of the
   measure rather than from the widest label: sized to content, "Podpoříme"
   would take a narrower column than "Budeme prosazovat" and the text beside
   them would start in two different places. A long label wraps instead. */
@container program (min-width: 42rem) {
  .program-list ul {
    display: grid;
    grid-template-columns: 26% minmax(0, 1fr);
    column-gap: clamp(1.2rem, 2.5cqi, 2.2rem);
  }

  /* Each point is one ruled row of the form: a hairline above it, and enough
     room either side of that rule that the row reads as one block rather than
     running on into the next. The panel sets its own tighter row spacing
     (see `.panel-program .program-list li` above); it is named here so this
     wins on specificity, not just on order.
     The inline padding stays 0: `subgrid` subgrids the inline axis only, and
     padding on a subgridded axis would inset the tracks out of line with the
     parent grid. Block padding and a top border do not. */
  .program-list li,
  .panel-program .program-list li {
    display: grid;
    grid-template-columns: subgrid;
    grid-column: 1 / -1;
    margin: 0;
    padding: .55em 0;
    border-top: 1px solid var(--rule);
  }

  /* back to its cell; the extra leading above it lines the capitals up with
     the first line of text beside them rather than riding above it */
  .program-list strong { display: block; line-height: 1.35; }

  /* there is measure for it now, and the address above and below the
     programme is set the same way on this column */
  .program-text {
    text-align: justify;
    hyphens: auto; /* lang="cs": without it a justified line opens holes */
  }
}

/* Takes the first step down from the photo, to whichever page this screen
   sets the programme on. Lives in the header, so it shares the title's
   centring — no corner to collide with on narrow phones. The header itself is
   pointer-events: none; the chip opts back in. */
.program-open-button {
  margin-top: .6rem;
  min-height: 44px;
  padding: .7em 1.1em;
}

/* the step is taken once a profile is up — display:none also drops the chip
   from the tab order. The letter covers the header on its own. */
body.profile-open .program-open-button { display: none; }

/* --- the letter ----------------------------------------------------------- */

/* The panel's left column on a sheet of its own, for phones, narrow windows
   and the wide ones where the panel can't set the programme: the same paper,
   the same type, the same order — address, programme, closing words. It is
   not a window over the photo but the page after it, so it takes the whole
   screen and the page itself scrolls. */
.letter {
  position: fixed;
  inset: 0;
  z-index: 5;
  background: var(--color-paper);
  color: var(--ink);
  pointer-events: none;
  opacity: 0;
  visibility: hidden; /* after the fade, as .profile */
  transform: translateY(1.5rem);
  transition: opacity .35s ease, transform .45s var(--ease-out), visibility 0s linear .5s;
}

body.letter-open .letter {
  pointer-events: auto;
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

.letter-sheet {
  /* starts under the Zpět chip, so the text never scrolls beneath it */
  position: absolute;
  inset: calc(var(--banner-height) + env(safe-area-inset-top) + 3.5rem) 0 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-block: 1.25rem calc(3rem + env(safe-area-inset-bottom));
  /* lines scrolled up fade out under the chip instead of being cut off; the
     fade is no taller than the padding, so the salutation starts unfaded */
  -webkit-mask-image: linear-gradient(transparent, #000 1.25rem);
  mask-image: linear-gradient(transparent, #000 1.25rem);
  /* one measure of text, centred on a tablet */
  padding-inline: max(clamp(1.25rem, 6vw, 3rem), calc((100% - 40rem) / 2));
}

/* the address in the panel's voice, ragged right: justified on a phone's
   measure it opens holes */
.letter .panel-intro,
.letter .panel-outro {
  display: block;
  contain: none;
  padding-left: 0;
  font-size: clamp(1rem, 2.3vw, 1.1rem);
  text-align: left;
}

/* --- dev banner (delete before publishing) -------------------------------- */

.dev-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 6;
  margin: 0;
  padding: .3rem .8rem;
  background: repeating-linear-gradient(135deg, #b3341f 0 12px, #97290f 12px 24px);
  color: #fff;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-align: center;
  pointer-events: none;
}

body:has(.dev-banner) { --banner-height: 1.5rem; }

/* --- squarish windows ------------------------------------------------------ */

/* Neither a portrait phone (its card meets the photo's own bottom edge, below)
   nor wide enough for the panel or for the card to stand beside the portrait.
   The card is pinned to the bottom at whatever height the bios need — half of
   a 1000x1000 window — and the crop-to-fill above has zoomed the photo by
   then: 1000x1000 kept the top 38% of the portrait, 820x800 the top 30%, a
   scalp either way. So the wide layout's answer without the panel: the photo
   stops where the card starts and is fitted whole into what is left, centred
   on the paper. Portrait windows only from 4/5 up — below that the full-width
   photo still pays (1024x1366 keeps 64% of it clear) and stays as it was.
   1249/1000 is "under 5/4": from 5/4 a landscape window stands the card
   beside the portrait instead (the last block of "phones held sideways"). */
@media (orientation: landscape) and (max-aspect-ratio: 1249/1000) and (max-width: 1023px) and (min-height: 501px),
       (orientation: portrait) and (min-aspect-ratio: 4/5) and (min-width: 901px) {
  :root {
    --portrait-width: calc((100dvh - var(--card-height)) * var(--portrait-w) / var(--portrait-h));
  }

  #person-photo {
    object-fit: contain;
    height: calc(100dvh - var(--card-height));
  }

  /* back to the photo's own box, which the crop-to-fill rule had widened */
  .profile::after {
    left: 50%;
    transform: translateX(-50%);
    width: var(--portrait-width);
    height: calc(100dvh - var(--card-height));
  }

  /* centred on the photo that is left, not at a fraction of the window */
  .profile-nav { top: calc((100dvh - var(--card-height)) / 2); }
}

/* --- portrait phones ------------------------------------------------------ */

@media (orientation: portrait) and (max-width: 900px) {
  .hotspot::after { display: none; } /* name labels are useless on touch */

  /* The two bands the screen splits into at rest, and the whole of this
     layout's geometry.

     The card stops where the portrait does. The portraits are cut 3:4 and
     fitted with contain, so on any window narrower than that the print is
     exactly 100vw × 4/3 tall, hung from the top (object-position) — the calc
     is its own bottom edge, and the card meets it with neither an overlap nor
     a band of bare paper between them, at whatever width the phone is.
     Measured at 62dvh before this, the card cut the candidate off at the chin.
     4/3 is the widest ratio in the set on purpose: a narrower photo (09 is
     0.57, 14 is 2:3) simply runs on behind the card, which is the harmless
     way to be wrong — a wider one would stop short and leave bare paper.
     The bounds are for the phones the calc can't serve: a stubby 4:3-ish one
     leaves less than the card's chrome needs (375×667 → 167px), a very tall
     narrow one more than the card should be allowed to take.

     Read from CSS rather than from app.js's --card-height, which measures the
     tallest bio: nothing here follows the text any more, because nothing here
     is capped by the window.

     svh, not dvh: a full-screen scroller is the page as far as the browser is
     concerned, so Chrome hands it the address bar — it slides away under the
     finger and settles on release. dvh settles with it, and since the portrait
     hangs from top: 0 and doesn't care, the card and the chips alone jumped
     against it — 37px on a 375×667, where the 34svh bound is what's binding.
     svh is the viewport with the bar still showing: the same number at rest,
     and the same number once the bar has gone. */
  .profile {
    --card-band: clamp(34svh, calc(100svh - 100vw * var(--portrait-h) / var(--portrait-w)), 50svh);
    --photo-band: calc(100svh - var(--card-band));
  }

  /* One sheet of paper: a long bio scrolls the profile, not a slit inside it —
     the portrait, the back button and the arrows travel up with the card the
     way a page does. The scroller is the dialog, so the document below it
     stays put (html, body are overflow: hidden). Only while it is open: the
     closed card sits a screen further down on its slide-out transform, which
     would otherwise be scrollable overflow. */
  .profile { overscroll-behavior: contain; }
  body.profile-open .profile { overflow-y: auto; }

  /* In normal flow under the photo band instead of pinned to the bottom of the
     window. min-height is what bottom: 0 used to give it — a short bio still
     reaches the bottom edge — and no max-height, so a long one runs on and the
     page scrolls instead of the card. The slide-in travels one card band, not
     101%: a percentage of a card several screens tall would fly. */
  .profile-card {
    position: static;
    max-height: none;
    overflow: visible;
    margin-top: var(--photo-band);
    min-height: var(--card-band);
    transform: translateY(calc(var(--card-band) + 2px));
  }

  /* Centred in the band of photo the card leaves rather than at a fixed
     fraction of the window: on a short phone the card takes most of the
     screen, and a window fraction put the arrows down where the back button
     now sits. */
  .profile-nav {
    top: calc(var(--photo-band) * .45);
  }

  /* Off the top-left corner: with the portrait running the full height of the
     window the button landed across the candidate's face, and the corner is
     the far end of a one-handed reach anyway. Hang it just above the card
     instead — clear of the head, low enough for a thumb.

     Hung off the card's top edge rather than off the bottom of the screen: a
     bottom offset resolves against .profile's padding box, which is the
     viewport, and the viewport is the one length the address bar still moves
     once the bands are svh — the button alone kept jumping on release. top
     puts its top edge where the card starts, and translate lifts it by its
     own height, which no stylesheet can name. translate, not transform: the
     entry animation below owns transform, and the two compose. */
  .back-button {
    top: calc(var(--photo-band) - clamp(.8rem, 2.5vh, 1.6rem));
    translate: 0 -100%;
    transform: translateY(.6rem); /* rises into place, the way the card does */
  }
}

/* --- phones held sideways -------------------------------------------------- */

/* A window this short has no height to stack anything in: the header's three
   lines stood across the faces, and the card — 62dvh of 390px — left a scalp
   of the portrait above it. So everything goes side by side instead: the
   header on one line, and the profile as the wide layout in miniature — the
   portrait at its full height on the left, the card the paper beside it,
   scrolling its own overflow. max-width keeps this clear of the wide layout
   (1024px and up), which has its own answer to a short window. */
@media (orientation: landscape) and (max-height: 500px) and (max-width: 1023px) {
  .hotspot::after { display: none; } /* name labels are useless on touch */

  /* logo, title and the programme's opener on one line */
  .site-title {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: .3rem 1.1rem;
    padding-block: calc(.55rem + env(safe-area-inset-top)) 1.1rem;
  }

  .site-title h1 {
    flex-direction: row;
    align-items: center;
    gap: .8rem;
  }

  .site-title .logo { height: 1.7rem; margin-bottom: 0; }
  .site-title-main { font-size: 1.3rem; }
  .program-open-button { margin-top: 0; min-height: 0; padding: .55em 1em; }

  .slogan { padding-block: .9rem calc(.5rem + env(safe-area-inset-bottom)); }
  .slogan p { font-size: .92rem; }
}

/* The side-by-side profile is not only for a window that short. Any landscape
   window under the wide layout's 1024px had the same scalp: the card is pinned
   to the bottom at whatever height the bios need, the portrait runs the full
   height behind it, and 800x600, 960x600 and 1023x768 — a small tablet held
   sideways, a desktop window pulled narrow — all kept the top 38% of the
   photo, which is hair. From 5/4 up the portrait's 3:4 leaves the card 40% of
   the width or more (350px of an 800x600), so it stands beside the photo
   there too. Squarer than that and the card would be a ribbon; those windows
   fit the photo above the card instead (the block before "portrait phones").

   A class and not a media query, because the wide layout falls back here too.
   From 1024px up the card is meant to be pulled in beside the programme, and
   where it cannot be — the bio too narrow, or fifteen names not clearing a
   card that tall — it used to span the window on top of a programme it was
   never meant to cover. Nothing in a media query can name that boundary: it
   runs through 1280x800, 1366x768 and 1536x864, a candidate's longest bio on
   one side of it and the next window pixel on the other. app.js measures it
   and sets .profile-side, and those windows get this layout, with the
   programme moved to the letter. So the rules below are written for both, and
   the ones at the top take back what the wide block would otherwise leak in. */
body.profile-side {
  /* the portrait is the window's full height again: the wide block fits it
     into what the card leaves, and here the card is beside it, not under it */
  --portrait-width: calc(100dvh * var(--portrait-w) / var(--portrait-h));
}

body.profile-side .ballot-panel { display: none; } /* the letter has the programme */
body.profile-side .ballot-strip { display: flex; } /* so the numbers are back */

/* the portrait: full height against the left edge, clear of a notch */
body.profile-side #person-photo {
  width: calc(env(safe-area-inset-left) + var(--portrait-width));
  height: 100dvh;
  object-position: right top;
}

body.profile-side .profile::after {
  left: env(safe-area-inset-left);
  right: auto; /* the wide block pins the shading to the right edge */
  transform: none;
  width: var(--portrait-width);
  height: 100dvh;
}

/* the card is the rest of the window, in from the right the way it comes up
   from the bottom elsewhere; its printed edge turns ninety degrees with it */
body.profile-side .profile-card {
  top: 0;
  left: calc(env(safe-area-inset-left) + var(--portrait-width));
  max-height: none;
  padding: calc(1rem + env(safe-area-inset-top))
           calc(clamp(1rem, 3vw, 1.75rem) + env(safe-area-inset-right))
           calc(1.2rem + env(safe-area-inset-bottom))
           clamp(1rem, 3vw, 1.75rem);
  background:
    linear-gradient(var(--rule), var(--rule)) 3px 0 / 1px 100% no-repeat,
    var(--color-paper);
  border-top: 0;
  border-left: 1px solid var(--rule-strong);
  box-shadow: -1px 0 0 rgb(var(--primary-dark-rgb) / .55),
              -14px 0 34px rgb(var(--primary-dark-rgb) / .26);
  transform: translateX(101%);
  /* One column, the number run in before the name — as on a narrow phone, and
     for the same reason: the gutter costs more than it is worth. Capped at the
     card's normal measure and centred in what is left (justify-content on the
     base rule): on a 1536px window the card is over 900px wide, and the bio
     would have run at a hundred characters to the line. The cap does not bind
     on a phone, where the whole card is narrower than 58ch. */
  grid-template-columns: minmax(0, 58ch);
}

/* and slides in when the profile opens. Spelled out here because the rule that
   does it elsewhere (body.profile-open .profile-card) carries the same weight
   as the standing-off transform above and is further up the file. */
body.profile-side.profile-open .profile-card { transform: none; }

body.profile-side .ballot-strip,
body.profile-side .profile-card h2,
body.profile-side .profile-job,
body.profile-side .profile-bio { grid-column: 1; }

body.profile-side .ballot-strip { margin-bottom: .7rem; font-size: .68rem; }
body.profile-side .profile-order { display: none; }

body.profile-side .profile-card h2 {
  display: flex;
  align-items: baseline;
  gap: .5em;
  font-size: clamp(1.35rem, 3.4vw, 1.75rem);
}

body.profile-side .profile-name-num { display: inline-grid; flex: none; font-size: .6em; }

body.profile-side .profile-bio {
  margin-top: .7rem;
  padding-top: .7rem;
  font-size: 1rem;
  line-height: 1.55;
}

/* the controls stay on the portrait: Zpět in its top corner, the arrows in
   its bottom corners, under the face rather than beside it */
body.profile-side .back-button {
  top: calc(var(--banner-height) + .7rem + env(safe-area-inset-top));
  left: calc(env(safe-area-inset-left) + .7rem);
  padding: .55em .9em;
}

body.profile-side .profile-nav {
  top: auto;
  bottom: calc(.7rem + env(safe-area-inset-bottom));
  width: 2.6rem;
  height: 2.6rem;
  transform: scale(.92);
}

body.profile-side.profile-open .profile-nav { transform: none; }

/* the prev arrow: same specificity as the wide block's own placement of it
   (body.showing-person .profile-nav-prev), and later in the file, so it wins */
body.profile-side .profile-nav-prev { left: calc(env(safe-area-inset-left) + .7rem); }
body.profile-side .profile-nav-next {
  left: calc(env(safe-area-inset-left) + var(--portrait-width) - 2.6rem - .7rem);
  right: auto;
}


/* --- reduced motion ------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  /* the cut sheet's four pieces are all named here and the panel itself is not:
     where the paper is one sheet it still slides, and where it is two, all four
     have to stop at once or the sheet tears */
  #person-photo, .hotspot, .profile, .profile::after, .profile-card,
  .ballot-panel::before, .ballot-panel::after, .panel-main, .ballot-side,
  .back-button, .profile-nav, .letter, .site-title, .slogan, .scroll-hint {
    transition-duration: .01ms !important;
    transition-delay: 0ms !important;
  }
  .hotspot, .hotspot::before { animation: none !important; }
}

/* skips the animation when a #/name link is opened directly */
body.no-animation * {
  transition: none !important;
  animation: none !important;
}

/* the star above reaches elements only, and the two sheets of paper are
   pseudo-elements */
body.no-animation .ballot-panel::before,
body.no-animation .ballot-panel::after {
  transition: none !important;
}
