/* ============================================================================
   components.css — the public-site kit. Every page builds from these pieces;
   page CSS only lays them out. Twig wrappers live in
   templates/web/components/Shared/ (each documents its params).

   Naming: block / block__element / block--modifier. State classes: .is-*.
   Short names: dialog = .dlg, toast = .snack, tooltip = .tip.

   Rules for every piece: colours from tokens.css only; media queries only
   the device ranges (tokens.css); hovers of cards, list rows and tiles —
   and every hover that moves or glows — inside
   @media (hover: hover) and (pointer: fine); a transform animation on text
   has a Firefox fade fallback (tokens.css header).

   Contents
     1. Button        .btn (+ --glow, --glow-sm: the violet underlight)
     2. Card          .card (+ --raised, --pad*, --sheen, --shadow, --hover, --hover-raised, --lift)
     3. Chip          .chip (+ __label, --mono, --dot, --lg, --wrap, --on-media)
     4. Icon tile     .icon-tile
     5. Stat          .stat (+ __foot), .stats
     6. Headers       .section-header, .page-header (+ --mark: text · watermark)
     7. Breadcrumb    .crumbs (Shared/Breadcrumb)
     8. Page mark     .page-mark (engraved watermark word in a page header)
     9. Toolbar       .toolbar (Shared/SearchSortToolbar; JS: LS.sortToolbar)
    10. Pagination    .pagination (Shared/Pagination; JS twin LS.pagination)
    11. Empty state   .empty-state
    12. Accordion     .acc (+ .acc--editorial)
    13. Cover         .cover
    14. Avatar        .avatar (+ --xxl, --square)
    15. Voice line    .voiceline
    16. Caption       .caption (subtitle line: types a line, the lektor "speaks" it)
    17. Frame         .frame-corners (capture-frame corner brackets)
    18. Dialog        .dlg (Shared/Modal, LS.modal)
    19. Toast         .snack (LS.toast)
    20. Tooltip       .tip ([data-tooltip])
    21. Notice        .notice (+ __actions; Shared/Notice)
    22. Table         .table (+ --hover, --stack, __rank; paged by [data-paginate])
    23. Forms         .field, .input, .textarea
    24. Skeleton      .skeleton
    25. Depth/light   .card--lift, .spotlight, .tilt, .chip--live, .voiceline--drift, .horizon
    26. Carousel      .carousel (driven by LS.carousel in main.js; --carousel-slide-min)
    27. Game card     .game-card (Shared/GameCard: catalog grid, similar packs, home carousel)
    28. Facts         .facts (label/value list)
    29. Well          .well (mono value + copy button)
    30. Segmented     .segmented (option track)
    31. Steps         .steps (+ --cards; Shared/Steps)
    32. Split         .split, .split__head (header left, content right; not sticky)
    33. News          .news-media (16:9 cover box), .news-grid, .news-card (+ --timeline, __more; Shared/NewsCard)
    34. CTA band      .cta-band (Shared/CtaBand: closing section of an inner page)
    35. Call-out      .callout (+ --media: copy left, art right)
    36. Meter         .meter (progress bar)
    37. Spinner       .spinner (ring), .spin (a turning icon)
    38. Entrance      .is-entering (an item / row that just appeared)
    39. Contents      .toc (Shared/Toc, LS.toc: table of contents of a text)
    40. Social links  .socials (+ --muted; Shared/SocialLinks)
    41. Video         .video (+ --inline; Shared/VideoFacade, LS.video: YouTube click-to-play)
    42. Help card     .help-cards, .help-card (HelpPage/ArticleCard: Help Center lists, /download guides)
    43. Tones         shared --tone variables (chip, notice, avatar, snack) — last on purpose
   ========================================================================== */


/* ── 1. Button ──────────────────────────────────────────────────────────── */
.btn {
  --btn-bg:         var(--surface-3);
  --btn-fg:         var(--text-strong);
  --btn-line:       var(--line-strong);
  --btn-bg-hover:   var(--surface-4);
  --btn-fg-hover:   var(--text-strong);
  --btn-line-hover: var(--line-hover);
  --btn-shadow:     none;


  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 2.5rem;
  max-width: 100%;
  padding: 0 1.125rem;
  margin: 0;
  color: var(--btn-fg);
  background-color: var(--btn-bg);
  border: 1px solid var(--btn-line);
  border-radius: var(--radius-md);
  box-shadow: var(--btn-shadow);
  font-family: var(--font-main);
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  vertical-align: middle;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.btn:hover {
  color: var(--btn-fg-hover);
  background-color: var(--btn-bg-hover);
  border-color: var(--btn-line-hover);
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible {
  outline: none;
  box-shadow: var(--btn-shadow), var(--focus-ring);
}
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}
.btn svg { width: 1em; height: 1em; font-size: 1.0625em; }
.btn__suffix { transition: transform var(--dur-fast) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {   /* hover motion: mouse only (tokens.css) */
  .btn:hover .btn__suffix { transform: translateX(2px); }
}
/* Firefox re-snaps a transformed element's text and icons to whole pixels
   when the motion ends (a visible jump). There a button has no press nudge
   and the suffix arrow moves by its `left` offset instead of a transform
   (layout, whole pixels, nothing to re-snap). (-moz-appearance = Firefox.) */
@supports (-moz-appearance: none) {
  .btn:active { transform: none; }
  .btn__suffix {
    position: relative;
    left: 0;
    transition: left var(--dur-fast) var(--ease-out);
  }
  @media (hover: hover) and (pointer: fine) {
    .btn:hover .btn__suffix { left: 2px; transform: none; }
  }
}

.btn--primary {
  --btn-bg:         var(--accent-500);
  --btn-fg:         var(--on-accent);
  --btn-line:       var(--accent-500);
  --btn-bg-hover:   var(--accent-450);
  --btn-fg-hover:   var(--on-accent);
  --btn-line-hover: var(--accent-450);
  --btn-shadow:     var(--highlight-btn);
}
.btn--secondary { /* the defaults: surface-3 + strong line */ }
.btn--ghost {
  --btn-bg:         transparent;
  --btn-fg:         var(--text-2);
  --btn-line:       transparent;
  --btn-bg-hover:   var(--surface-hover);
  --btn-fg-hover:   var(--text-strong);
  --btn-line-hover: transparent;
}
.btn--soft {
  --btn-bg:         var(--accent-soft);
  --btn-fg:         var(--accent-300);
  --btn-line:       var(--accent-line);
  --btn-bg-hover:   var(--accent-soft-hover);
  --btn-fg-hover:   var(--accent-300);
  --btn-line-hover: var(--accent-line);
}
.btn--discord {
  --btn-bg:         var(--discord);
  --btn-fg:         var(--on-accent);
  --btn-line:       var(--discord);
  --btn-bg-hover:   var(--discord-hover);
  --btn-fg-hover:   var(--on-accent);
  --btn-line-hover: var(--discord-hover);
  --btn-shadow:     var(--highlight-btn);
}
/* The main action of a hero / closing band: a violet underlight under the
   button (--glow: the large one, --glow-sm: scaled to a card). Through
   --btn-shadow, so the focus ring adds to it. Primary buttons only. */
.btn--glow    { --btn-shadow: var(--highlight-btn), var(--underlight); }
.btn--glow-sm { --btn-shadow: var(--highlight-btn), var(--underlight-sm); }

.btn--sm { min-height: 2rem; padding: 0 0.75rem; gap: 0.375rem; font-size: 0.875rem; }
.btn--lg { min-height: 3rem; padding: 0 1.5rem;   gap: 0.625rem; font-size: 1rem; }
.btn--block { display: flex; width: 100%; }

.btn--icon { width: 2.5rem; padding: 0; }
.btn--icon.btn--sm { width: 2rem; }
.btn--icon.btn--lg { width: 3rem; }
/* A copy button whose icon turns into a check (main.js [data-copy-swap]):
   the check is green while it shows. */
[data-copy-swap].is-copied,
[data-copy-swap].is-copied:hover { color: var(--success-text); }

/* Two-line download button: "Pobierz dla" over "Windows". */
.btn--split {
  gap: 0.75rem;
  min-height: 3rem;
  padding: 0.4375rem 1.25rem 0.4375rem 1rem;
  text-align: left;
}
.btn--split .btn__icon { font-size: 1.375rem; }
.btn__label { display: flex; flex-direction: column; line-height: 1.15; }
.btn__hint { font-size: var(--fs-xs); font-weight: 500; opacity: 0.8; }
.btn__name { font-size: 1rem; }


/* ── 2. Card ────────────────────────────────────────────────────────────────
   The one surface for boxed content. A page lays a card out (grid / flex
   direction, gaps) but never re-skins it; a look a page needs is one of
   these modifiers:
     --raised        surface-2 instead of surface-1
     --pad           24px padding (--pad-md 20px, --pad-sm 16px, --pad-xs 12px)
     --sheen         the faint top light (var(--sheen)) over the fill
     --shadow        a soft drop shadow under the hairline (floating tiles)
     --hover         a link / button card: the edge lightens on hover
     --hover-raised  …and the fill turns surface-2 (with --hover)
     --lift          section 25: the edge, a 2px lift and a deeper shadow
   Hovers are mouse only (tokens.css). */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  height: auto;
  margin: 0;
  color: var(--text-1);
  background-color: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--highlight-top);
  overflow: hidden;
  transition: border-color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard);
}
.card--raised { background-color: var(--surface-2); }
.card--pad    { padding: var(--space-6); }
.card--pad-md { padding: var(--space-5); }
.card--pad-sm { padding: var(--space-4); }
.card--pad-xs { padding: var(--space-3); }
.card--sheen  { background-image: var(--sheen); }
.card--shadow { box-shadow: var(--highlight-top), var(--shadow-md); }
@media (hover: hover) and (pointer: fine) {
  .card--hover:hover { border-color: var(--line-hover); }
  .card--hover-raised:hover { background-color: var(--surface-2); }
}

.card__media {
  position: relative;
  display: block;
  background-color: var(--surface-2);
}
/* A cover filling the media slot edge to edge: the card already has the edge. */
.card__media > .cover { border-radius: 0; }
.card__media > .cover::after { display: none; }

.card__body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
}
.card__title {
  margin: 0;
  color: var(--text-strong);
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
}
.card__text {
  margin: 0;
  color: var(--text-2);
  font-size: 0.9375rem;
  line-height: 1.6;
}
.card__meta {
  margin: 0;
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
}
.card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: auto;
  padding: 0 var(--space-5) var(--space-5);
}

/* Whole-card link: the title link stretches over the card. Other links and
   buttons inside the card need position: relative + z-index to stay usable
   (.card__foot children get it here). */
.card__link { color: inherit; text-decoration: none; }
.card__link:hover { color: inherit; }
.card__link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
}
.card__link:focus-visible { outline: none; }
.card__link:focus-visible::after { box-shadow: inset 0 0 0 2px var(--focus-outline); }
.card__foot > * { position: relative; z-index: 2; }


/* ── 3. Chip (Shared/Chip) ──────────────────────────────────────────────────
   One line, never wraps (unless --wrap). A long label that must fit its
   box (a game name on a cover) sits in .chip__label and ends in an
   ellipsis (Chip `truncate`). Modifiers: --mono, --dot, --lg, --wrap,
   --on-media; tones in section 41. */
.chip {
  --tone: var(--text-2);
  --tone-soft: var(--surface-active);
  --tone-line: var(--line);
  --chip-px: 0.625rem;   /* side padding; the .chip--live ring is placed by it */
  --chip-h: 1.5rem;

  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  height: var(--chip-h);
  max-width: 100%;
  padding: 0 var(--chip-px);
  color: var(--tone);
  background-color: var(--tone-soft);
  border: 1px solid var(--tone-line);
  border-radius: var(--radius-full);
  font-family: var(--font-main);
  font-size: var(--fs-xs);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0;
  white-space: nowrap;
  text-decoration: none;
}
.chip svg { width: 1em; height: 1em; font-size: 0.9em; }
.chip__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.chip--mono { font-family: var(--font-mono); letter-spacing: 0.02em; }
/* --lg: 28px tall, roomier sides (a row of facts in a hero). --wrap: a
   long label (a requirement, a role) wraps onto more lines; the chip
   grows from its height. */
.chip--lg { --chip-h: 1.75rem; --chip-px: 0.75rem; }
.chip--wrap {
  height: auto;
  min-height: var(--chip-h);
  padding-block: 0.25rem;
  line-height: 1.3;
  white-space: normal;
}
/* On a photo (a cover, a blurred hero): the tone tint laid over a dark
   base, so the chip stays legible on any picture (Chip `onMedia`). The
   chips on a news card cover (section 33) get it by their place. */
.chip--on-media,
.news-card__chips .chip {
  background-color: var(--cover-scrim);
  background-image: linear-gradient(var(--tone-soft), var(--tone-soft));
}
.chip--dot::before {
  content: '';
  width: 0.375rem;
  height: 0.375rem;
  flex-shrink: 0;
  border-radius: 50%;
  background-color: currentColor;
}
a.chip,
button.chip { transition: border-color var(--dur-fast) var(--ease-standard); }
a.chip:hover,
button.chip:hover { color: var(--tone); border-color: var(--tone); }
button.chip { cursor: pointer; }
button.chip:focus-visible { outline: none; box-shadow: var(--focus-ring); }


/* ── 4. Icon tile ───────────────────────────────────────────────────────── */
.icon-tile {
  display: inline-grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  flex-shrink: 0;
  color: var(--accent-300);
  background-color: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  font-size: 1.125rem;
}
.icon-tile--accent {
  background-color: var(--accent-soft);
  border-color: var(--accent-line);
}
.icon-tile--sm { width: 2.25rem; height: 2.25rem; border-radius: var(--radius-sm); font-size: 0.9375rem; }
.icon-tile--lg { width: 3.5rem; height: 3.5rem; border-radius: var(--radius-lg); font-size: 1.375rem; }


/* ── 5. Stat ────────────────────────────────────────────────────────────── */
.stat {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
}
.stat__value {
  margin: 0;
  color: var(--text-strong);
  font-family: var(--font-mono);
  font-size: 2rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.stat__label {
  margin: 0;
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1.4;
}
.stat__affix {
  margin-left: 0.16em;
  color: var(--accent-text);
  font-size: 0.5em;
  font-weight: 500;
  letter-spacing: 0;
}
.stat__affix--pre {
  margin-left: 0;
  margin-right: 0.45em;
  color: var(--text-2);
  font-family: var(--font-main);
  font-size: 0.34em;
  letter-spacing: 0.01em;
}
.stat--center { align-items: center; text-align: center; }
/* Stat `foot` block: a row of chips / a meter under the label. */
.stat__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.stat__foot:empty { display: none; }
.stat--center .stat__foot { justify-content: center; }
/* LS.countUp(el, {keepWidth: true}) holds the final width while the number
   runs up (--count-width, set inline), so a unit next to it stands still. */
.is-counting {
  display: inline-block;
  min-width: var(--count-width);
  text-align: right;
}

/* A hairline row of stats: stacked on phones, one row from tablet up. */
.stats {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border-block: 1px solid var(--line);
}
.stats > .stat { padding: var(--space-6) var(--space-2); }
.stats > .stat + .stat { border-top: 1px solid var(--line); }
@media (min-width: 768px) {
  .stats { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
  .stats > .stat { padding: var(--space-8) var(--space-6); }
  .stats > .stat + .stat { border-top: 0; border-left: 1px solid var(--line); }
}


/* ── 6. Headers ─────────────────────────────────────────────────────────── */
.section-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  max-width: 46rem;
  margin: 0 0 var(--space-12);
  text-align: left;
}
.section-header__title {
  margin: 0;
  color: var(--text-strong);
  font-size: clamp(2rem, 3.4vw, 2.75rem);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
.section-header__desc {
  margin: 0;
  color: var(--text-2);
  font-size: 1.0625rem;
  line-height: 1.65;
  text-wrap: pretty;
}
.section-header__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
}
.section-header__actions:empty { display: none; }

/* The text wrapper only matters in the split layout; otherwise its children
   sit straight in the header's column. */
.section-header__text { display: contents; }

/* Split: title block left, actions right (from tablet up). */
@media (min-width: 768px) {
  .section-header--split {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-8);
    max-width: none;
  }
  .section-header--split .section-header__text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
    max-width: 46rem;
  }
  .section-header--split .section-header__actions { margin-top: 0; flex-shrink: 0; }
}

/* Page header (top of every inner page). Padding in :where() so a page can
   override it with a plain class rule. */
:where(.page-header) { padding-block: var(--space-16) var(--space-10); }
@media (max-width: 767px) {
  :where(.page-header) { padding-block: var(--space-10) var(--space-8); }
}
.page-header {
  position: relative;
  text-align: left;
  overflow: visible;
}
/* Inner: the breadcrumb, the text column (__main), the optional watermark
   (__mark) and the toolbar, stacked. The watermark only shows from 1024px:
   a header with one (.page-header--mark, set by Shared/PageHeader when
   `watermark` is passed) puts text 7fr and the word 5fr side by side, the
   word centred against the text block, with the breadcrumb above and the
   toolbar below, both across the two columns. Below 1024px the header is
   the plain single column.
   Spacing is margins (not gap), so every gap is set where it is used. */
.page-header__inner {
  display: flex;
  flex-direction: column;
}
.page-header__crumbs { margin-bottom: var(--space-4); }
.page-header__toolbar { margin-top: var(--space-8); }
.page-header__main {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  min-width: 0;
}
.page-header__mark { display: none; }
@media (min-width: 1024px) {
  .page-header--mark .page-header__inner {
    display: grid;
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    align-items: center;
    column-gap: var(--space-16);
  }
  .page-header--mark .page-header__crumbs,
  .page-header--mark .page-header__toolbar { grid-column: 1 / -1; }
  .page-header--mark .page-header__crumbs { align-self: start; }
  .page-header--mark .page-header__mark {
    display: flex;
    justify-content: flex-end;
    min-width: 0;
    container-type: inline-size;
    pointer-events: none;
    user-select: none;
  }
}
.page-header__title {
  margin: 0;
  max-width: 26ch;
  color: var(--text-strong);
  font-size: var(--fs-h1);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.025em;
  text-wrap: balance;
}
.page-header__desc {
  margin: 0;
  max-width: 42rem;
  color: var(--text-2);
  font-size: var(--fs-lead);
  line-height: 1.65;
  text-wrap: pretty;
}
.page-header__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  color: var(--text-2);
  font-size: var(--fs-sm);
}
.page-header__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
}
.page-header__toolbar { width: 100%; }
.page-header__meta:empty,
.page-header__actions:empty,
.page-header__toolbar:empty { display: none; }


/* ── 7. Breadcrumb (.crumbs, Shared/Breadcrumb) ────────────────────────────
   "⌂ / Aktualności / Tytuł" at the top of a page header: one small line in
   text-2, links → text-strong on hover, "/" in text-3. It never wraps: a
   long LAST item is cut with an ellipsis (the parents keep their width). */
.crumbs {
  max-width: 100%;
  min-width: 0;
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.crumbs__list {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
.crumbs__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
  white-space: nowrap;
}
.crumbs__item--current { flex: 0 1 auto; min-width: 0; }
.crumbs__link {
  color: var(--text-2);
  text-decoration: none;
  border-radius: var(--radius-xs);
  transition: color var(--dur-fast) var(--ease-standard);
}
.crumbs__link:hover { color: var(--text-strong); }
/* The house: 0.95em, standing on the baseline of the words beside it (the
   1em icon box centres on the line; a hair lower reads as on the baseline). */
.crumbs__home {
  display: inline-flex;
  align-items: center;
  padding-top: 0.0625em;
}
.crumbs__icon { width: 0.95em; height: 0.95em; }
.crumbs__sep { color: var(--text-3); }
.crumbs__current,
.crumbs__text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}


/* ── 8. Page mark (.page-mark, Shared/PageHeader `watermark`) ──────────────
   A short mono word engraved on the right of a page header: outline only
   (1px --line-strong over an almost clear fill), the first character in
   violet outline, framed by the kit capture-frame corners (.frame-corners)
   — the language of the @nick mark on the home team cards (.player__mark).
   Markup: Shared/PageMark (PageHeader `watermark`; the error / maintenance
   pages, web/solo.html.twig). Decorative and static; in a page header
   desktop only (the column shows from 1024px, see 7). The word is as large
   as --mark-size (default clamp(4rem, 9vw, 9rem), settable on an
   ancestor) allows while it still fits its container: --mark-chars (set
   by PageMark) × 0.62em is the width of a JetBrains Mono line (0.6em
   advance + 0.02em tracking), measured against the nearest inline-size
   container (container query units).
   .page-mark--icon (PageMark `icon`): an icon engraved instead of a word —
   its shape in the same 1px outline (non-scaling stroke), all in violet,
   one em square. */
.page-mark {
  --mark-pad-x: var(--space-8);
  --frame-len: 1.125rem;
  position: relative;
  display: inline-block;
  max-width: 100%;
  padding: var(--space-6) var(--mark-pad-x);
}
.page-mark__word {
  display: block;
  color: var(--engrave-fill);
  -webkit-text-stroke: 1px var(--line-strong);
  font-family: var(--font-mono);
  font-size: min(
    var(--mark-size, clamp(4rem, 9vw, 9rem)),
    calc((100cqi - 2 * var(--mark-pad-x)) / (var(--mark-chars, 8) * 0.62))
  );
  font-weight: 600;
  line-height: 0.9;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.page-mark__accent { -webkit-text-stroke-color: var(--accent-line); }
.page-mark__icon {
  display: block;
  width: 1em;
  height: 1em;
  overflow: visible;
}
.page-mark__icon path {
  fill: var(--engrave-fill);
  stroke: var(--accent-line);
  stroke-width: 1px;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}


/* ── 9. Toolbar: search + sort ──────────────────────────────────────────────
   Shared/SearchSortToolbar; behaviour: LS.sortToolbar (main.js, bound to
   every [data-sort-toolbar]). The sort widget is a select-only combobox:
   .toolbar-sort-select[data-value] (.is-open) › .toolbar-sort-trigger
   (role=combobox, focus stays on it, aria-activedescendant) ›
   .toolbar-sort-label + .toolbar-sort-dropdown (role=listbox) ›
   .toolbar-sort-option[data-value] (role=option; .selected = chosen,
   .is-active = the one the arrow keys / the pointer are on). Selectors
   are prefixed with .toolbar. */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
}
.toolbar__search {
  position: relative;
  flex: 1 1 18rem;
  min-width: 0;
  margin: 0;
}
.toolbar__search .toolbar__input { padding-right: 3rem; }
.toolbar__search--clearable .toolbar__input { padding-right: 5.25rem; }
/* × inside the field while a phrase is active (next to the search icon). */
.toolbar__clear {
  position: absolute;
  top: 50%;
  right: 2.625rem;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  color: var(--text-3);
  border-radius: var(--radius-full);
  transform: translateY(-50%);
  font-size: 0.8125rem;
  transition: color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard);
}
.toolbar__clear:hover { color: var(--text-strong); background-color: var(--surface-active); }
.toolbar__clear:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.toolbar__clear::after {   /* hairline between × and the search icon */
  content: '';
  position: absolute;
  top: 0.375rem;
  bottom: 0.375rem;
  right: -0.4375rem;
  width: 1px;
  background-color: var(--line);
}
/* "N wyników dla „phrase”" — a quiet line under the toolbar. */
.toolbar__summary {
  margin: var(--space-3) 0 0;
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.toolbar__summary strong {
  color: var(--text-strong);
  font-family: var(--font-mono);
  font-weight: 600;
}
.toolbar__phrase { color: var(--text-1); font-weight: 500; overflow-wrap: anywhere; }
.toolbar__submit {
  position: absolute;
  top: 50%;
  right: 0.25rem;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  color: var(--text-2);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  transform: translateY(-50%);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard);
}
.toolbar__submit:hover { color: var(--text-strong); background-color: var(--surface-hover); }
.toolbar__submit:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.toolbar__sort {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}
.toolbar__sort-label {
  color: var(--text-2);
  font-size: var(--fs-sm);
  white-space: nowrap;
}

.toolbar .toolbar-sort-select {
  position: relative;
  z-index: 10;
  min-width: 12rem;
  font-family: var(--font-main);
  font-size: var(--fs-sm);
  user-select: none;
}
.toolbar .toolbar-sort-select.is-open { z-index: 60; }

.toolbar .toolbar-sort-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  height: 2.5rem;
  padding: 0 0.75rem 0 0.875rem;
  color: var(--text-1);
  background-color: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  box-shadow: none;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-standard);
}
.toolbar .toolbar-sort-trigger:hover { border-color: var(--line-hover); }
.toolbar .toolbar-sort-trigger:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.toolbar .toolbar-sort-trigger svg {
  color: var(--text-2);
  font-size: 0.75rem;
  transition: transform var(--dur-fast) var(--ease-standard);
}
.toolbar .toolbar-sort-select.is-open .toolbar-sort-trigger {
  border-color: var(--accent-line);
  box-shadow: none;
}
/* keyboard focus stays visible while the list is open */
.toolbar .toolbar-sort-select.is-open .toolbar-sort-trigger:focus-visible { box-shadow: var(--focus-ring); }
.toolbar .toolbar-sort-select.is-open .toolbar-sort-trigger svg { transform: rotate(180deg); }

.toolbar .toolbar-sort-dropdown {
  position: absolute;
  top: calc(100% + 0.375rem);
  left: 0;
  right: 0;
  z-index: 50;
  padding: 0.25rem;
  background-color: var(--surface-overlay);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-standard),
              transform var(--dur-fast) var(--ease-out),
              visibility 0s linear var(--dur-fast);
}
.toolbar .toolbar-sort-select.is-open .toolbar-sort-dropdown {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
  transition-delay: 0s;
}
/* Firefox re-snaps the list to whole pixels when the slide ends, so its
   options jumped sideways/up by a fraction of a pixel. There it only fades
   (no transform in either state). (-moz-appearance = Firefox.) */
@supports (-moz-appearance: none) {
  .toolbar .toolbar-sort-dropdown,
  .toolbar .toolbar-sort-select.is-open .toolbar-sort-dropdown,
  .tip,
  .tip.is-below,
  .tip.is-visible { transform: none; }   /* tooltips: same snap, same fix */
}
.toolbar .toolbar-sort-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 0.5rem 0.625rem;
  color: var(--text-2);
  background: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard);
}
.toolbar .toolbar-sort-option.is-active {
  color: var(--text-strong);
  background-color: var(--surface-hover);
}
@media (hover: hover) and (pointer: fine) {   /* a list row: mouse only (tokens.css) */
  .toolbar .toolbar-sort-option:hover {
    color: var(--text-strong);
    background-color: var(--surface-hover);
  }
}
.toolbar .toolbar-sort-option.selected {
  color: var(--accent-300);
  background-color: var(--accent-soft);
}
/* Check mark drawn with a mask so it takes the text colour. */
.toolbar .toolbar-sort-option.selected::after {
  content: '';
  display: block;
  width: 0.75rem;
  height: 0.75rem;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z'/%3E%3C/svg%3E") center / contain no-repeat;
}

@media (max-width: 767px) {
  .toolbar__search { flex-basis: 100%; }
  .toolbar__sort { width: 100%; }
  .toolbar .toolbar-sort-select { flex: 1 1 auto; min-width: 0; }
}


/* ── 10. Pagination ──────────────────────────────────────────────────────── */
.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  margin: var(--space-12) 0 0;
  padding: 0;
  list-style: none;
}
.pagination__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.5rem;
  height: 2.5rem;
  padding: 0 0.625rem;
  color: var(--text-2);
  background-color: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: 500;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  transition: color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard);
}
.pagination__btn:hover {
  color: var(--text-strong);
  border-color: var(--line-hover);
}
.pagination__btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.pagination__btn.is-active {
  color: var(--accent-300);
  background-color: var(--accent-soft);
  border-color: var(--accent-line);
  cursor: default;
}
.pagination__btn.is-disabled {
  opacity: 0.4;
  pointer-events: none;
}
.pagination__gap {
  padding: 0 0.25rem;
  color: var(--text-3);
  font-family: var(--font-mono);
  user-select: none;
}
/* Tablet portrait: larger touch targets. Phones: prev + 5 slots + next
   still fit one row at 360px. */
@media (min-width: 768px) and (max-width: 1023px) {
  .pagination__btn { min-width: 2.75rem; height: 2.75rem; }
}
@media (max-width: 767px) {
  .pagination { gap: 0.25rem; }
  .pagination__btn { padding: 0 0.5rem; }
}


/* ── 11. Empty state ────────────────────────────────────────────────────────
   A calm card, not a dashed hole: soft violet light from the top, an accent
   icon tile inside thin capture corners (.frame-corners, the OCR motif),
   title, text and optional actions. --compact for use inside panels. */
.empty-state {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  padding: clamp(2.5rem, 6vw, 4rem) var(--space-6);
  overflow: hidden;
  text-align: center;
  background:
    radial-gradient(60% 70% at 50% 0%, var(--ambient-violet-soft), transparent 70%),
    var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-xl);
  box-shadow: var(--highlight-top);
  animation: none;
}
.empty-state__visual {
  position: relative;
  display: grid;
  place-items: center;
  width: 6rem;
  height: 6rem;
  margin-bottom: var(--space-5);
}
.empty-state__frame { --frame-inset: 0px; --frame-len: 0.875rem; }
.empty-state__icon {
  display: grid;
  place-items: center;
  width: 3.75rem;
  height: 3.75rem;
  color: var(--accent-300);
  background-color: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  box-shadow: var(--highlight-top), var(--underlight-sm);
  font-size: 1.5rem;
}
.empty-state__title {
  margin: 0;
  color: var(--text-strong);
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
}
.empty-state__desc {
  margin: var(--space-2) 0 0;
  max-width: 30rem;
  color: var(--text-2);
  font-size: 0.9375rem;
  line-height: 1.6;
}
.empty-state__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
  margin-top: var(--space-6);
}
/* Inside a panel or card: smaller, no own frame. */
.empty-state--compact {
  padding: var(--space-8) var(--space-5);
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.empty-state--compact .empty-state__visual { width: 4.5rem; height: 4.5rem; margin-bottom: var(--space-4); }
.empty-state--compact .empty-state__icon { width: 2.75rem; height: 2.75rem; font-size: 1.125rem; }
.empty-state--compact .empty-state__title { font-size: 1.0625rem; }
.empty-state--compact .empty-state__desc { font-size: var(--fs-sm); }
.empty-state--compact .empty-state__actions { margin-top: var(--space-4); }


/* ── 12. Accordion (native <details>) ───────────────────────────────────────
   Works without JS. With JS (html.ls-js) main.js drives it: .is-open flips
   at once (the chevron / plus and the rail turn with it, --acc-dur), and the
   panel's height runs 0 → its measured height on open, back to 0 on close
   (280ms --ease-out, with opacity; Web Animations, then height is auto
   again). `open` is removed only after the close has finished. While a
   panel moves, <html> carries .acc-sizing (a hook only: native scroll
   anchoring stays on and keeps the page steady). Reduced motion: instant.
   The hover light of an item (a list row) is mouse only (tokens.css). */
.acc {
  --acc-dur: 280ms;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.acc__item {
  background-color: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--highlight-top);
  transition: border-color var(--dur-fast) var(--ease-standard);
}
.acc__item.is-open,
html:not(.ls-js) .acc__item[open] { border-color: var(--line-strong); }
@media (hover: hover) and (pointer: fine) {
  .acc__item:hover { border-color: var(--line-strong); }
}
.acc__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  color: var(--text-strong);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  list-style: none;
  cursor: pointer;
  border-radius: inherit;
}
.acc__summary::-webkit-details-marker { display: none; }
.acc__summary::marker { content: ''; }
.acc__summary:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--focus-outline); }
.acc__chev {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
  color: var(--text-2);
  background-color: var(--surface-2);
  border-radius: var(--radius-sm);
  font-size: 0.75rem;
  transition: transform var(--acc-dur) var(--ease-out), color var(--dur-fast) var(--ease-standard);
}
.acc__item[open] > .acc__summary .acc__chev { transform: rotate(180deg); color: var(--accent-text); }
.ls-js .acc__item:not(.is-open) > .acc__summary .acc__chev { transform: none; color: var(--text-2); }
/* Firefox: the chevron turns at once (only its colour eases) — a rotating
   tile on a sub-pixel row re-snapped to whole pixels when the turn ended.
   (-moz-appearance = Firefox.) */
@supports (-moz-appearance: none) {
  .acc__chev { transition: color var(--dur-fast) var(--ease-standard); }
}

/* The panel clips its content while main.js animates its height. */
.acc__panel { overflow: hidden; }
.acc__clip { min-height: 0; }
/* html.acc-sizing is only a JS hook now: native scroll anchoring stays on. */
.acc__body {
  padding: 0 var(--space-5) var(--space-5);
  color: var(--text-2);
  font-size: 0.9375rem;
  line-height: 1.7;
}
.acc__body > * + * { margin-top: var(--space-3); }
.acc__body a { color: var(--accent-text); text-decoration: underline; text-decoration-color: var(--accent-line); text-underline-offset: 0.2em; }
.acc__body a:hover { color: var(--accent-300); }
.acc__body ul, .acc__body ol { padding-left: 1.25rem; }
.acc__body strong { color: var(--text-1); }

/* Editorial variant (Accordion `variant: 'editorial'`): no boxes, hairlines
   between items, a mono index, a plus that turns into ×, a 2px accent rail
   on the open item, the open item lit from that rail (a soft violet light
   fading out to the right) and an answer that fades / slides in after the
   height opens. Use it on a section background, not inside a card. The
   hover colours of a question are mouse only (tokens.css). */
.acc--editorial {
  gap: 0;
  border-bottom: 1px solid var(--line);
}
.acc--editorial .acc__item {
  position: relative;
  background: none;
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
  box-shadow: none;
  transition: background-color var(--dur-base) var(--ease-standard);
}
.acc--editorial .acc__item.is-open,
html:not(.ls-js) .acc--editorial .acc__item[open] {
  background: linear-gradient(90deg, var(--ambient-violet-soft), transparent 55%);
}
.acc--editorial .acc__item::before {
  content: '';
  position: absolute;
  top: -1px;
  bottom: 0;
  left: 0;
  width: 2px;
  background-color: var(--accent-500);
  border-radius: var(--radius-full);
  transform: scaleY(0);
  transform-origin: 50% 0;
  transition: transform var(--acc-dur) var(--ease-out);
}
.acc--editorial .acc__item[open]::before { transform: none; }
.ls-js .acc--editorial .acc__item:not(.is-open)::before { transform: scaleY(0); }
.acc--editorial .acc__summary {
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr) 2rem;
  align-items: start;
  gap: var(--space-4);
  padding: var(--space-6) 0 var(--space-6) var(--space-5);
  color: var(--text-1);
  font-size: 1.125rem;
  line-height: 1.45;
  border-radius: 0;
}
.acc__index {
  padding-top: 0.25rem;
  color: var(--accent-text);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.6;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
.acc--editorial .acc__question { transition: color var(--dur-fast) var(--ease-standard); }
.acc--editorial .acc__item[open] .acc__question { color: var(--text-strong); }
.ls-js .acc--editorial .acc__item:not(.is-open) .acc__question { color: var(--text-1); }
@media (hover: hover) and (pointer: fine) {
  .acc--editorial .acc__summary:hover .acc__question,
  .ls-js .acc--editorial .acc__item .acc__summary:hover .acc__question { color: var(--text-strong); }
}
.acc__plus {
  position: relative;
  display: block;
  width: 2rem;
  height: 2rem;
  color: var(--text-2);
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  transition: transform var(--acc-dur, 280ms) var(--ease-out),
              color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.acc__plus::before,
.acc__plus::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0.75rem;
  height: 1.5px;
  margin: -0.75px 0 0 -0.375rem;
  background-color: currentColor;
  border-radius: var(--radius-full);
}
.acc__plus::after { transform: rotate(90deg); }
@media (hover: hover) and (pointer: fine) {
  .acc--editorial .acc__summary:hover .acc__plus { color: var(--text-strong); border-color: var(--line-hover); }
}
.acc--editorial .acc__item[open] .acc__plus {
  color: var(--accent-300);
  background-color: var(--accent-soft);
  border-color: var(--accent-line);
  transform: rotate(45deg);
}
.ls-js .acc--editorial .acc__item:not(.is-open) .acc__plus {
  color: var(--text-2);
  background-color: transparent;
  border-color: var(--line-strong);
  transform: none;
}
@media (hover: hover) and (pointer: fine) {
  .ls-js .acc--editorial .acc__item:not(.is-open) .acc__summary:hover .acc__plus { color: var(--text-strong); border-color: var(--line-hover); }
}
/* Firefox: like the chevron above, the plus turns into × at once (its
   colours still ease) — turning back to upright on close ended with the
   circle and its cross re-snapping to whole pixels. (-moz-appearance =
   Firefox.) */
@supports (-moz-appearance: none) {
  .acc__plus {
    transition: color var(--dur-fast) var(--ease-standard),
                background-color var(--dur-fast) var(--ease-standard),
                border-color var(--dur-fast) var(--ease-standard);
  }
}
.acc--editorial .acc__body {
  max-width: 62ch;
  padding: 0 calc(2rem + var(--space-4)) var(--space-6) calc(var(--space-5) + 2.5rem + var(--space-4));
  font-size: 1rem;
}
@media (max-width: 767px) {
  .acc--editorial .acc__summary {
    grid-template-columns: 1.75rem minmax(0, 1fr) 2rem;
    gap: var(--space-3);
    padding: var(--space-5) 0 var(--space-5) var(--space-4);
    font-size: 1.0625rem;
  }
  .acc--editorial .acc__body {
    padding: 0 0 var(--space-5) calc(var(--space-4) + 1.75rem + var(--space-3));
    font-size: 0.9375rem;
  }
}


/* ── 13. Cover (2:3 game art, source 300×450) ───────────────────────────────
   A fixed 2:3 box on a static --surface-2 placeholder; the image simply
   appears over it when loaded (main.js adds .is-loaded; .is-error on a
   broken file shows the fallback). Only the home page fades the image in
   and pulses the box while it loads. Without an image, .cover--empty shows
   the fallback tile. Never wider than ~300 CSS px. */
.cover {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  background-color: var(--surface-2);
  border-radius: var(--radius-md);
  isolation: isolate;
}
.cover::after {
  /* Hairline on top of the art so light covers keep an edge. */
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  border: 1px solid var(--line);
  border-radius: inherit;
  pointer-events: none;
}
.cover__img {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  transition: opacity var(--dur-slow) var(--ease-out);   /* only changes on home */
}
.ls-js [data-page="home"] .cover:not(.is-loaded) .cover__img { opacity: 0; }
.ls-js [data-page="home"] .cover:not(.is-loaded):not(.is-error):not(.cover--empty) {
  animation: ls-pulse 1.6s var(--ease-in-out) infinite;
}
.cover.is-error .cover__img { display: none; }

.cover__fallback {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-4);
  color: var(--text-3);
  text-align: center;
  font-size: 1.75rem;
}
.cover--empty .cover__fallback,
.cover.is-error .cover__fallback { display: flex; }
.cover__name {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--text-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: 1.35;
}


/* ── 14. Avatar ─────────────────────────────────────────────────────────── */
.avatar {
  --tone: var(--accent-text);
  --tone-soft: var(--accent-soft);
  --tone-line: var(--accent-line);
  --avatar-size: 3rem;

  position: relative;
  display: inline-grid;
  place-items: center;
  width: var(--avatar-size);
  height: var(--avatar-size);
  flex-shrink: 0;
  overflow: hidden;
  color: var(--tone);
  background-color: var(--tone-soft);
  border: 1px solid var(--tone-line);
  border-radius: 50%;
  font-family: var(--font-main);
  font-size: calc(var(--avatar-size) * 0.36);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.02em;
  user-select: none;
}
.avatar--sm { --avatar-size: 2rem; }
.avatar--md { --avatar-size: 3rem; }
.avatar--lg { --avatar-size: 4.5rem; }
.avatar--xl { --avatar-size: 5.75rem; }  /* 92px */
.avatar--xxl { --avatar-size: 7rem; }    /* 112px — team player cards */
/* Rounded square instead of a disc (portraits). */
.avatar--square { border-radius: calc(var(--avatar-size) * 0.24); }
.avatar__initials { position: relative; z-index: 0; }
.avatar__img {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}
.avatar.is-error .avatar__img { display: none; }


/* ── 15. Voice line (waveform motif) ────────────────────────────────────────
   Thin bars (2px wide, 3px apart, rounded ends): a dim row (.14 white) and a
   violet copy on top (accent-400 with a faint accent-300 core). Bar heights
   follow a fixed 16-step pattern. The row (--voiceline-bars bars, set by
   VoiceLine.html.twig) is centred and clipped, and its ends fade out.
   While main.js marks it [data-loop].is-visible:
     · the bars breathe: a slow wave runs left → right through the row
       (2.6s per 16 bars, like the admin login);
     · default: the lit part sweeps open left → right and fades (9s);
     · .voiceline--drift (section 25): a wide soft-edged lit window glides
       across at a constant pace.
   Offscreen, under reduced motion and without JS it stands still: full
   heights, 40% lit (or the window in the middle).

   Built to cost the GPU next to nothing: there is no element per bar. Each
   row of bars is ONE box painted through a mask — --voiceline-shape is an
   80px SVG tile of the 16 bars (heights in %, so mask-size scales them),
   --voiceline-core-shape the same bars carrying the core gradient. Masks
   sit only on boxes that never move on their own, so they are rasterised
   once; all motion is transform-only, on four small layers:
     .voiceline__track       the dim bars (static; 46% tall while breathing)
     .voiceline__wave        the breathing: an alpha wave (its mask, period
       > .voiceline__tips    16 bars) slides right over the bar tips (full
                             height minus the 46%), which slide back by the
                             same distance, so they stay put
     .voiceline__beam        the lit part (it moves; overflow clips it) —
       > .voiceline__lit     the lit row inside slides back the same way
   A page may swap the bar pattern by setting both shape variables.
   Never animate anything per bar or put a mask on a moving box here.
   Keep it rare: the home hero, the "Graj z lektorem" step, the home outro
   (and the mobile menu). Everywhere else the subtitle line (.caption,
   section 16) speaks, or a .frame-corners accent (17) frames. */
.voiceline {
  --voiceline-fade: 6%;
  --voiceline-shape: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect x='0' y='35%25' width='2' height='30%25' rx='1'/><rect x='5' y='22.5%25' width='2' height='55%25' rx='1'/><rect x='10' y='10%25' width='2' height='80%25' rx='1'/><rect x='15' y='27.5%25' width='2' height='45%25' rx='1'/><rect x='20' y='17.5%25' width='2' height='65%25' rx='1'/><rect x='25' y='2.5%25' width='2' height='95%25' rx='1'/><rect x='30' y='20%25' width='2' height='60%25' rx='1'/><rect x='35' y='32.5%25' width='2' height='35%25' rx='1'/><rect x='40' y='40%25' width='2' height='20%25' rx='1'/><rect x='45' y='25%25' width='2' height='50%25' rx='1'/><rect x='50' y='12.5%25' width='2' height='75%25' rx='1'/><rect x='55' y='0%25' width='2' height='100%25' rx='1'/><rect x='60' y='15%25' width='2' height='70%25' rx='1'/><rect x='65' y='30%25' width='2' height='40%25' rx='1'/><rect x='70' y='21%25' width='2' height='58%25' rx='1'/><rect x='75' y='37.5%25' width='2' height='25%25' rx='1'/></svg>");
  --voiceline-core-shape: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><linearGradient id='c' x2='0' y2='1'><stop offset='.2' stop-opacity='0'/><stop offset='.5'/><stop offset='.8' stop-opacity='0'/></linearGradient><g fill='url(%23c)'><rect x='0' y='35%25' width='2' height='30%25' rx='1'/><rect x='5' y='22.5%25' width='2' height='55%25' rx='1'/><rect x='10' y='10%25' width='2' height='80%25' rx='1'/><rect x='15' y='27.5%25' width='2' height='45%25' rx='1'/><rect x='20' y='17.5%25' width='2' height='65%25' rx='1'/><rect x='25' y='2.5%25' width='2' height='95%25' rx='1'/><rect x='30' y='20%25' width='2' height='60%25' rx='1'/><rect x='35' y='32.5%25' width='2' height='35%25' rx='1'/><rect x='40' y='40%25' width='2' height='20%25' rx='1'/><rect x='45' y='25%25' width='2' height='50%25' rx='1'/><rect x='50' y='12.5%25' width='2' height='75%25' rx='1'/><rect x='55' y='0%25' width='2' height='100%25' rx='1'/><rect x='60' y='15%25' width='2' height='70%25' rx='1'/><rect x='65' y='30%25' width='2' height='40%25' rx='1'/><rect x='70' y='21%25' width='2' height='58%25' rx='1'/><rect x='75' y='37.5%25' width='2' height='25%25' rx='1'/></g></svg>");
  /* The painted band: N bars of 5px minus the last gap, centred; the bar
     tile starts at its left edge, snapped to a whole pixel (a tile on a
     half pixel smears every 2px bar over 3). The band's hard edges get
     1.5px of slack for that snap — they cut inside the 3px gaps. */
  --vl-band: calc(var(--voiceline-bars, 48) * 5px - 3px);
  --vl-x: calc(50% + 40px - var(--vl-band) / 2);
  --vl-edges: linear-gradient(90deg, transparent calc(50% - var(--vl-band) / 2 - 1.5px), #000 0 calc(50% + var(--vl-band) / 2 + 1.5px), transparent 0);
  --vl-ends: linear-gradient(90deg, transparent, #000 var(--voiceline-fade), #000 calc(100% - var(--voiceline-fade)), transparent);

  position: relative;
  display: block;
  width: 100%;
  height: var(--voiceline-h, 3rem);
  overflow: hidden;
}
/* The lit row's box is the line's width give or take 1/64px of layout
   rounding; the +0.25px keeps both rounding the same way (the centre is
   on a whole or a half pixel). */
@supports (left: round(nearest, 1.5px, 1px)) {
  .voiceline { --vl-x: round(nearest, 50% + 40px - var(--vl-band) / 2 + 0.25px, 1px); }
}
/* The static leaves: each one is the size of the line and holds no moving
   child, so its mask is painted once into its layer. */
.voiceline__track,
.voiceline__tips::before,
.voiceline__lit::before,
.voiceline__lit::after {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--voiceline-dim);
  -webkit-mask: var(--vl-edges), var(--vl-ends), var(--voiceline-shape) var(--vl-x) 50% / 80px var(--vl-h, 100%) repeat-x;
  -webkit-mask-composite: source-in;
          mask: var(--vl-edges), var(--vl-ends), var(--voiceline-shape) var(--vl-x) 50% / 80px var(--vl-h, 100%) repeat-x;
          mask-composite: intersect;
}
/* The tips: the full bars minus their 46% middle. */
.voiceline__tips::before {
  -webkit-mask: var(--vl-edges), var(--vl-ends),
    var(--voiceline-shape) var(--vl-x) 50% / 80px 100% repeat-x,
    var(--voiceline-shape) var(--vl-x) 50% / 80px 46% repeat-x;
  -webkit-mask-composite: source-in, source-in, xor, source-over;
          mask: var(--vl-edges), var(--vl-ends),
    var(--voiceline-shape) var(--vl-x) 50% / 80px 100% repeat-x,
    var(--voiceline-shape) var(--vl-x) 50% / 80px 46% repeat-x;
          mask-composite: intersect, intersect, exclude, add;
}
.voiceline__lit::before { background-color: var(--voiceline-lit); }
.voiceline__lit::after {
  background-color: var(--voiceline-core);
  -webkit-mask: var(--vl-edges), var(--vl-ends), var(--voiceline-core-shape) var(--vl-x) 50% / 80px 100% repeat-x;
  -webkit-mask-composite: source-in;
          mask: var(--vl-edges), var(--vl-ends), var(--voiceline-core-shape) var(--vl-x) 50% / 80px 100% repeat-x;
          mask-composite: intersect;
}

/* Breathing: one period of the wave is 80px = 16 bars; it moves one
   period per 2.6s (162.5ms per bar, later bars behind earlier ones). */
.voiceline__wave {
  position: absolute;
  top: 0;
  bottom: 0;
  left: -80px;
  right: 0;
  display: none;
  -webkit-mask-image: repeating-linear-gradient(90deg, transparent 0, rgba(0, 0, 0, 0.15) 10px, rgba(0, 0, 0, 0.5) 20px, rgba(0, 0, 0, 0.85) 30px, #000 40px, rgba(0, 0, 0, 0.85) 50px, rgba(0, 0, 0, 0.5) 60px, rgba(0, 0, 0, 0.15) 70px, transparent 80px);
          mask-image: repeating-linear-gradient(90deg, transparent 0, rgba(0, 0, 0, 0.15) 10px, rgba(0, 0, 0, 0.5) 20px, rgba(0, 0, 0, 0.85) 30px, #000 40px, rgba(0, 0, 0, 0.85) 50px, rgba(0, 0, 0, 0.5) 60px, rgba(0, 0, 0, 0.15) 70px, transparent 80px);
}
.voiceline__tips {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 80px;
  right: 0;
}

/* The lit part: at rest 40% of the line is lit (the beam is pushed 60% to
   the left, the lit row inside 60% back). */
.voiceline__beam {
  position: absolute;
  inset: 0;
  overflow: hidden;
  transform: translateX(-60%);
  pointer-events: none;
}
.voiceline__lit {
  position: absolute;
  inset: 0;
  transform: translateX(60%);
}

@media (prefers-reduced-motion: no-preference) {
  .voiceline.is-visible .voiceline__track { --vl-h: 46%; }
  .voiceline.is-visible .voiceline__wave {
    display: block;
    animation: ls-wave 2.6s linear infinite;
  }
  .voiceline.is-visible .voiceline__tips { animation: ls-wave-back 2.6s linear infinite; }
  /* The sweep (default): the beam opens left → right while the lit row
     holds still, then the lit row fades. The negative delay starts it a
     third of the way in, so the very first frame already shows a lit part. */
  .voiceline.is-visible .voiceline__beam { animation: ls-sweep 9s var(--ease-in-out) -3s infinite; }
  .voiceline.is-visible .voiceline__lit  { animation: ls-sweep-back 9s var(--ease-in-out) -3s infinite; }
}
@keyframes ls-wave {
  from { transform: none; }
  to   { transform: translateX(80px); }
}
@keyframes ls-wave-back {
  from { transform: none; }
  to   { transform: translateX(-80px); }
}
@keyframes ls-sweep {
  0%        { transform: translateX(-100%); }
  78%, 100% { transform: none; }
}
@keyframes ls-sweep-back {
  0%   { transform: translateX(100%); opacity: 1; }
  78%  { transform: none; }
  90%  { opacity: 1; }
  100% { transform: none; opacity: 0; }
}

/* ── 16. Caption (subtitle line motif) ─────────────────────────────────────
   The product in one line, "napis → głos": a small mono subtitle bar that
   types a short dialogue line behind a caret; when the line is complete the
   caret gives way to a three-bar pulse — the lektor speaking it — then the
   line fades and the next one is typed. Shared/Caption.html.twig renders it
   (lines); main.js (initCaptions) types only while it is on
   screen. Without JS, under reduced motion and offscreen it shows the first
   line in full with a still pulse. Its width is set by an invisible copy of
   the longest line (.caption__sizer), so typing never moves the layout. The
   copy is painted from data-sizer by ::before (content: attr()), so it is
   not text in the page — the line is never read or indexed twice.
   One line, ≤ 40 characters; on a narrow screen it ends in an ellipsis.
   .caption--wrap (Caption `wrap: true`): a longer line wraps instead; the
   sizer wraps the same way, so the height is still reserved up front.
     p.caption[data-caption="[…lines…]"][aria-hidden]
       span.caption__line
         span.caption__sizer[data-sizer]    (longest line via ::before, invisible)
         span.caption__typed
           span.caption__chars              (the first line, server-rendered)
           span.caption__caret
           span.caption__pulse > i × 3
   States (main.js): .is-live (JS drives it), .is-typing (caret, no pulse),
   .is-speaking (the pulse moves), .is-clearing (the line fades out). */
.caption {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  max-width: 100%;
  min-width: 0;
  margin: 0;
  padding: 0.4375rem 0.75rem;
  color: var(--text-2);
  background-color: var(--sunken);
  border: 1px solid var(--line-subtle);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  line-height: 1.4;
  letter-spacing: 0.01em;
  white-space: nowrap;
  text-align: left;
}
.caption__line {
  display: grid;
  min-width: 0;
  overflow: hidden;
}
.caption__sizer,
.caption__typed { grid-area: 1 / 1; min-width: 0; }
/* padding-right: the room the pulse takes after the longest line */
.caption__sizer { padding-right: 1.5rem; overflow: hidden; text-overflow: ellipsis; visibility: hidden; }
.caption__sizer::before { content: attr(data-sizer); }
.caption__typed { display: flex; align-items: center; }
/* Wrapping variant: the sizer keeps a little more room than the caret +
   pulse take, so the typed line never wraps to more lines than it does. */
.caption--wrap { white-space: normal; }
.caption--wrap .caption__sizer { padding-right: 2rem; }
.caption--wrap .caption__typed { align-items: flex-end; }
/* centre the caret and the pulse on the last line, not the whole block */
.caption--wrap .caption__caret,
.caption--wrap .caption__pulse { margin-bottom: 0.15rem; }
.caption__chars {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: opacity var(--dur-slow) var(--ease-standard);
}
.caption__caret {
  display: none;
  flex: none;
  width: 0.5em;
  height: 1.05em;
  margin-left: 1px;
  background-color: var(--accent-400);
  border-radius: 1px;
  opacity: 0.85;
}
.caption__pulse {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 2px;
  height: 0.75rem;
  margin-left: 0.5rem;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.caption__pulse i {
  display: block;
  width: 2px;
  height: 100%;
  background-color: var(--accent-400);
  border-radius: var(--radius-full);
  transform: scaleY(0.5);
}
.caption__pulse i:nth-child(2) { transform: scaleY(0.9); }
.caption__pulse i:nth-child(3) { transform: scaleY(0.65); }

.caption.is-live .caption__pulse { opacity: 0; }
.caption.is-typing .caption__caret { display: block; }
.caption.is-speaking .caption__pulse { opacity: 1; }
.caption.is-speaking .caption__pulse i { animation: ls-speak 0.9s var(--ease-in-out) infinite alternate; }
.caption.is-speaking .caption__pulse i:nth-child(2) { animation-delay: -0.3s; }
.caption.is-speaking .caption__pulse i:nth-child(3) { animation-delay: -0.6s; }
.caption.is-clearing .caption__chars { opacity: 0; }
@keyframes ls-speak {
  from { transform: scaleY(0.3); }
  to   { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .caption.is-speaking .caption__pulse i { animation: none; }
}


/* ── 17. Capture frame (.frame-corners) ────────────────────────────────────
   Four thin corner brackets in --line-strong — the OCR capture box — around
   an illustration area. A quiet, static accent for a card that needs
   "something". A decorative span inside a positioned parent:
     <span class="frame-corners" aria-hidden="true"></span>
   Tune with --frame-inset (default 0), --frame-len (0.875rem) and
   --frame-color. */
.frame-corners {
  --fc: var(--frame-color, var(--line-strong));
  --fl: var(--frame-len, 0.875rem);
  position: absolute;
  inset: var(--frame-inset, 0);
  z-index: 1;
  background:
    linear-gradient(var(--fc), var(--fc)) 0 0 / var(--fl) 1px,
    linear-gradient(var(--fc), var(--fc)) 0 0 / 1px var(--fl),
    linear-gradient(var(--fc), var(--fc)) 100% 0 / var(--fl) 1px,
    linear-gradient(var(--fc), var(--fc)) 100% 0 / 1px var(--fl),
    linear-gradient(var(--fc), var(--fc)) 0 100% / var(--fl) 1px,
    linear-gradient(var(--fc), var(--fc)) 0 100% / 1px var(--fl),
    linear-gradient(var(--fc), var(--fc)) 100% 100% / var(--fl) 1px,
    linear-gradient(var(--fc), var(--fc)) 100% 100% / 1px var(--fl);
  background-repeat: no-repeat;
  pointer-events: none;
}


/* ── 18. Dialog ─────────────────────────────────────────────────────────────
   Shared/Modal; opened and closed by LS.modal (main.js), shown / hidden
   through the [hidden] attribute. Phones: no backdrop blur. */
.dlg {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
}
.dlg[hidden] { display: none; }
.dlg__backdrop {
  position: absolute;
  inset: 0;
  background-color: var(--scrim);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
  animation: ls-fade-in var(--dur-fast) var(--ease-standard);
}
.dlg__dialog {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 32.5rem;
  max-height: calc(100vh - 2rem);
  max-height: calc(100dvh - 2rem);
  overflow: hidden;
  color: var(--text-1);
  background-color: var(--surface-overlay);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg), var(--highlight-top);
  animation: ls-rise-in var(--dur-base) var(--ease-out);
}
.dlg__dialog:focus { outline: none; }
/* Firefox re-snaps the centred dialog to whole pixels when the rise-in ends,
   so its whole content jumped. There the dialog only fades in (no transform).
   (-moz-appearance = Firefox.) */
@supports (-moz-appearance: none) {
  .dlg__dialog { animation-name: ls-fade-in; }
}
.dlg--sm .dlg__dialog { max-width: 24rem; }
.dlg--lg .dlg__dialog { max-width: 45rem; }
.dlg__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
  padding: var(--space-4) var(--space-4) var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--line);
}
.dlg__title {
  flex: 1;
  min-width: 0;
  margin: 0;
  color: var(--text-strong);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.35;
}
.dlg__body {
  flex: 1;
  padding: var(--space-5);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.dlg__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
  flex-shrink: 0;
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--line);
}
.dlg__foot:empty { display: none; }
@media (max-width: 767px) {
  .dlg { padding: var(--space-3); }
  .dlg__backdrop { -webkit-backdrop-filter: none; backdrop-filter: none; }
  .dlg__dialog {
    max-height: calc(100vh - 1.5rem);
    max-height: calc(100dvh - 1.5rem);
  }
}


/* ── 19. Toast (LS.toast) ───────────────────────────────────────────────── */
.snack {
  --tone: var(--success-text);

  position: fixed;
  left: 50%;
  bottom: var(--space-6);
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: calc(100vw - 2rem);
  padding: 0.75rem 1rem 0.75rem 1.125rem;
  overflow: hidden;
  color: var(--text-1);
  background-color: var(--surface-overlay);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-sm);
  line-height: 1.45;
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 8px);
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard),
              transform var(--dur-base) var(--ease-out),
              visibility 0s linear var(--dur-base);
}
.snack::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 3px;
  background-color: var(--tone);
}
.snack.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
  transition-delay: 0s;
}
/* Firefox: translate(-50%) put the toast on half a pixel and the slide-up
   ended with its text re-snapping. There it is centred by auto margins (no
   translate) and only fades. (-moz-appearance = Firefox.) */
@supports (-moz-appearance: none) {
  .snack,
  .snack.is-visible {
    right: 0;
    left: 0;
    width: fit-content;
    margin-inline: auto;
    transform: none;
  }
}
.snack__icon { display: inline-flex; color: var(--tone); }
.snack__text { min-width: 0; }


/* ── 20. Tooltip ([data-tooltip], positioned by main.js) ────────────────── */
.tip {
  position: fixed;
  top: 0;
  left: 0;
  z-index: var(--z-tooltip);
  max-width: 260px;
  padding: 0.5rem 0.75rem;
  color: var(--text-1);
  background-color: var(--surface-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  font-family: var(--font-main);
  font-size: var(--fs-xs);
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: normal;
  text-align: left;
  text-transform: none;
  white-space: normal;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(3px);
  transition: opacity var(--dur-fast) var(--ease-standard),
              transform var(--dur-fast) var(--ease-standard),
              visibility 0s linear var(--dur-fast);
}
.tip.is-below { transform: translateY(-3px); }
.tip.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}
/* Arrow drawn twice: the outer one in the border colour, the inner in the
   fill. --tt-arrow (set by main.js) keeps it on the trigger. */
.tip::before,
.tip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: var(--tt-arrow, 50%);
  width: 0;
  height: 0;
  border: 6px solid transparent;
}
.tip::before { margin-left: -7px; border-width: 7px; border-top-color: var(--line-strong); }
.tip::after  { margin-left: -6px; border-top-color: var(--surface-3); }
.tip.is-below::before,
.tip.is-below::after { top: auto; bottom: 100%; border-top-color: transparent; }
.tip.is-below::before { border-bottom-color: var(--line-strong); }
.tip.is-below::after  { border-bottom-color: var(--surface-3); }


/* ── 21. Notice ─────────────────────────────────────────────────────────── */
.notice {
  --tone: var(--accent-text);
  --tone-soft: var(--accent-soft);
  --tone-line: var(--accent-line);

  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  color: var(--text-1);
  background-color: var(--tone-soft);
  border: 1px solid var(--tone-line);
  border-radius: var(--radius-md);
  font-size: 0.9375rem;
  line-height: 1.6;
}
.notice__icon {
  display: inline-flex;
  flex-shrink: 0;
  margin-top: 0.2em;
  color: var(--tone);
  font-size: 1.0625rem;
}
.notice__body { display: flex; flex: 1 1 auto; flex-direction: column; gap: var(--space-1); min-width: 0; }
/* The title reads at the notice's size whatever its element (Notice
   `titleTag`: p, or a heading in the outline). */
.notice__title { margin: 0; color: var(--tone); font-size: inherit; font-weight: 600; line-height: inherit; }
.notice__body p { margin: 0; }
/* Notice `actions` block: a button beside the text, centred on it; on
   phones under it, full width. Empty (no block): not there. */
.notice__actions {
  display: flex;
  flex-wrap: wrap;
  flex-shrink: 0;
  align-self: center;
  gap: var(--space-2);
  margin-left: var(--space-4);
}
.notice__actions:empty { display: none; }
@media (max-width: 767px) {
  .notice:has(> .notice__actions:not(:empty)) { flex-wrap: wrap; }
  .notice:has(> .notice__actions:not(:empty)) > .notice__body { flex-basis: 0; }
  .notice__actions { flex-basis: 100%; margin: var(--space-1) 0 0; }
  .notice__actions > .btn { flex: 1 1 100%; }
}


/* ── 22. Table ──────────────────────────────────────────────────────────────
   A .table in a .table-wrap card. Modifiers:
     --hover    rows light up under the pointer (mouse only, tokens.css)
     --inset    from 768px the first / last cells keep 24px off the card edge
     --stack    phones: no table grid — every row is a two-column grid of
                its own (main cells left, a value right, page CSS places
                the cells with grid-template-areas / hides columns), the
                header row goes (thead hidden: label the cells in markup
                where a value needs one)
   .table__rank   a narrow mono "#" column in muted text.
   Paged in the browser: table[data-paginate="<rows per page>"][id] + an
   empty, hidden [data-pager="<table id>"] after the wrap (main.js, kit
   LS.pagination). The switch: the old rows fade (tbody.is-leaving), the
   wrap glides to the new height (.table-wrap.is-resizing, height inline),
   the new rows enter (.is-entering, section 38, staggered by
   --enter-delay); the last row of a page has .is-page-end (no bottom
   rule, like :last-child). Reduced motion: an instant swap. */
.table-wrap {
  overflow-x: auto;
  background-color: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--highlight-top);
}
.table {
  width: 100%;
  margin: 0;
  border-collapse: collapse;
  color: var(--text-1);
  font-size: 0.9375rem;
}
.table th,
.table td {
  padding: 0.875rem 1rem;
  text-align: left;
  vertical-align: middle;
  background: none;
  border-bottom: 1px solid var(--line);
  box-shadow: none;
}
.table th {
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  border-bottom-color: var(--line-strong);
}
.table tbody tr:last-child > *,
.table tbody tr.is-page-end > * { border-bottom: 0; }
.table .is-num {
  font-family: var(--font-mono);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.table__rank {
  width: 3.5rem;
  color: var(--text-3);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.table--hover tbody tr { transition: background-color var(--dur-fast) var(--ease-standard); }
@media (hover: hover) and (pointer: fine) {
  .table--hover tbody tr:hover { background-color: var(--surface-hover); }
}
@media (min-width: 768px) {
  .table--inset th:first-child,
  .table--inset td:first-child { padding-left: var(--space-6); }
  .table--inset th:last-child,
  .table--inset td:last-child { padding-right: var(--space-6); }
}
@media (max-width: 767px) {
  .table__rank { width: 2.5rem; }
  .table--stack,
  .table--stack tbody { display: block; }
  .table--stack thead { display: none; }
  .table--stack tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    gap: var(--space-3) var(--space-4);
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--line);
  }
  .table--stack tbody tr:last-child,
  .table--stack tbody tr.is-page-end { border-bottom: 0; }
  .table--stack td {
    display: block;
    width: auto;
    height: auto;
    padding: 0;
    border: 0;
  }
}

/* Paging (main.js [data-paginate]). */
.table[data-paginate] tbody { transition: opacity 140ms var(--ease-standard); }
.table[data-paginate] tbody.is-leaving { opacity: 0; }
.table tbody tr.is-entering { --enter-y: 4px; --enter-dur: 260ms; }
.table-wrap.is-resizing {
  overflow: hidden;
  transition: height 280ms var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .table[data-paginate] tbody,
  .table-wrap.is-resizing { transition: none; }
}


/* ── 23. Forms ──────────────────────────────────────────────────────────── */
.field {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  min-width: 0;
}
.field__label {
  color: var(--text-1);
  font-size: var(--fs-sm);
  font-weight: 500;
}
.field__hint  { margin: 0; color: var(--text-2); font-size: 0.8125rem; }
.field__error { margin: 0; color: var(--danger-text); font-size: 0.8125rem; }

.input,
.textarea {
  display: block;
  width: 100%;
  height: 2.5rem;
  margin: 0;
  padding: 0 0.875rem;
  color: var(--text-strong);
  background-color: var(--sunken);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  box-shadow: none;
  font-family: var(--font-main);
  font-size: 0.9375rem;
  line-height: 1.4;
  transition: border-color var(--dur-fast) var(--ease-standard),
              box-shadow var(--dur-fast) var(--ease-standard);
}
.input::placeholder,
.textarea::placeholder { color: var(--text-3); opacity: 1; }
.input:hover,
.textarea:hover { border-color: var(--line-hover); }
.input:focus,
.textarea:focus {
  outline: none;
  border-color: var(--accent-line);
  box-shadow: var(--focus-ring);
}
.input[aria-invalid="true"],
.textarea[aria-invalid="true"] { border-color: var(--danger-line); }
.input:disabled,
.textarea:disabled { opacity: 0.5; cursor: not-allowed; }
/* Touch screens: 16px text, or iOS Safari zooms the page in when the field
   takes focus (and the on-screen keyboard opens). */
@media (hover: none), (pointer: coarse) {
  .input,
  .textarea { font-size: 1rem; }
}

.textarea {
  height: auto;
  min-height: 6rem;
  padding: 0.625rem 0.875rem;
  line-height: 1.55;
  resize: vertical;
}


/* ── 24. Skeleton ───────────────────────────────────────────────────────── */
.skeleton {
  display: block;
  background-color: var(--surface-2);
  border-radius: var(--radius-xs);
  animation: ls-pulse 1.6s var(--ease-in-out) infinite;
}
.skeleton--text { height: 0.875rem; }


/* ── 25. Depth & light ─────────────────────────────────────────────────────
   Opt-in pieces for pages that need more presence than a flat card (the
   home page first). All of them move transform/opacity only and stand still
   under prefers-reduced-motion (base.css kill switch + the rules below).
   The hover parts (edge, lift, shadow, light, tilt) are mouse only — see
   the hover rule in tokens.css; on touch a card does not react to a tap.

   .card--lift        hover: lighter edge, lifts 2px, deeper shadow. Uses
                      the individual `translate` property, so it composes with
                      the .reveal entrance animation (which owns `transform`).
   .spotlight         a soft violet light that follows the cursor. main.js
                      sets --mx / --my on it from one delegated pointermove
                      listener (fine pointers only). The element must be
                      positioned (a .card already is); the light sits between
                      its background and its content. --spot-size tunes it.
   .tilt              3D tilt toward the cursor (max 6deg): main.js sets
                      --rx / --ry on the [data-tilt] wrapper around it.
   .chip--live        with --dot: the dot sends out a slow ring while the
                      chip (or an ancestor) is [data-loop].is-visible.
   .voiceline--drift  a lit WINDOW gliding left → right at a constant pace
                      instead of the sweep (VoiceLine.html.twig `drift: true`
                      adds the class). --drift-dur sets the pace (7s).
   .horizon           a section seam: hairline across the top, brighter in
                      the middle, with a soft violet bloom falling below it.
                      Put it on a positioned block (.section is). */
.card--lift {
  transition: border-color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard),
              translate var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out);
}
/* The edge, the lift and its shadow: mouse only (tokens.css). */
@media (hover: hover) and (pointer: fine) {
  .card--lift:hover {
    border-color: var(--line-hover);
    translate: 0 -2px;
    box-shadow: var(--highlight-top-strong), var(--shadow-lift);
  }
}
/* Firefox re-snaps text to whole pixels when a moved element settles, so
   the title visibly jumped at the end of the 2px lift. There the card keeps
   still and only its edge and shadow change. (-moz-appearance = Firefox.) */
@supports (-moz-appearance: none) {
  .card--lift:hover { translate: none; }
}
/* Reduced motion: the edge and the shadow, no lift. */
@media (prefers-reduced-motion: reduce) {
  .card--lift:hover { translate: none; }
}

.spotlight { isolation: isolate; }
.spotlight::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(var(--spot-size, 22rem) circle at var(--mx, 50%) var(--my, 0%), var(--spotlight), transparent 70%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard);
}
@media (hover: hover) and (pointer: fine) {
  .spotlight:hover::before { opacity: 1; }
}

/* Tilt: main.js sets --rx / --ry on a [data-tilt] wrapper from the cursor
   (fine pointers, no reduced motion) and .is-tilting while it follows; a
   .tilt inside (or the wrapper itself) turns with them. Elements that also
   have their own transitions list them in --tilt-extra. */
.tilt {
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform var(--tilt-dur, 600ms) var(--ease-out), var(--tilt-extra, opacity 0s);
}
.is-tilting .tilt,
.tilt.is-tilting { --tilt-dur: 140ms; }
@media (prefers-reduced-motion: reduce) {
  .tilt { transform: none; }
}

.chip--live { position: relative; }
.chip--live::after {
  content: '';
  position: absolute;
  top: 50%;
  left: var(--chip-px, 0.625rem);   /* over the dot, whatever the chip's padding */
  width: 0.375rem;
  height: 0.375rem;
  margin-top: -0.1875rem;
  border-radius: 50%;
  background-color: currentColor;
  opacity: 0;
  pointer-events: none;
}
.is-visible.chip--live::after,
.is-visible .chip--live::after {
  animation: ls-live-ring 2.4s var(--ease-out) infinite;
}
@keyframes ls-live-ring {
  0%   { opacity: 0.55; transform: scale(1); }
  70%  { opacity: 0;    transform: scale(3.2); }
  100% { opacity: 0;    transform: scale(3.2); }
}

/* Drift: the beam is a window 36% of the line wide whose edges fade softly
   (mask). It glides, linear, from where only its faded tail reaches into
   the line's own faded end (-80% of itself, left edge at -28.8% of the
   line) to the mirror position on the right (257.78%, left edge at 92.8%),
   so it is on show for most of the loop and never pops in; the lit row
   inside (277.78% of the beam = the line) slides the opposite way by the
   same distance, so its bars stay exactly on top of the dim ones. One timing for
   both, so they never part. The lit bars stand at full height (the dim
   row breathes around them): the lektor speaking up.
   At rest (offscreen, reduced motion, no JS) the window sits in the middle
   (32–68% of the line). The beam is the only masked box that moves in the
   voice line — a small one (36% × the line height). */
.voiceline--drift .voiceline__beam {
  right: auto;
  width: 36%;
  transform: translateX(88.89%);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 38%, #000 62%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 38%, #000 62%, transparent);
}
.voiceline--drift .voiceline__lit {
  right: auto;
  width: calc(100% * 25 / 9);   /* 277.78%: exactly the line */
  transform: translateX(-32%);
}
@media (prefers-reduced-motion: no-preference) {
  .voiceline--drift.is-visible .voiceline__beam {
    animation: ls-beam var(--drift-dur, 7s) linear infinite;
  }
  .voiceline--drift.is-visible .voiceline__lit {
    animation: ls-beam-inner var(--drift-dur, 7s) linear infinite;
  }
}
@keyframes ls-beam {
  from { transform: translateX(-80%); }
  to   { transform: translateX(257.78%); }
}
@keyframes ls-beam-inner {
  from { transform: translateX(28.8%); }
  to   { transform: translateX(-92.8%); }
}

.horizon::before,
.horizon::after {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  pointer-events: none;
  transform: translateX(-50%);
}
.horizon::before {
  z-index: 1;
  width: min(100%, 72rem);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-strong) 25%, var(--horizon-core) 50%, var(--line-strong) 75%, transparent);
}
.horizon::after {
  z-index: 0;
  width: min(100%, 60rem);
  height: 16rem;
  background: radial-gradient(50% 100% at 50% 0%, var(--ambient-violet-soft), transparent);
}
.horizon > * { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .is-visible.chip--live::after,
  .is-visible .chip--live::after { animation: none; }
}


/* ── 26. Carousel ──────────────────────────────────────────────────────────
   A sliding row of cards driven by LS.carousel(root, opts) in main.js
   (transform-based, drag/swipe, optional loop + autoplay). Markup:

     .carousel                               role="region" + a label
       .carousel__stage                      positions the arrows
         .carousel__viewport  [data-carousel-viewport] tabindex="0" role="group" + aria-label
           .carousel__track   [data-carousel-track]  (ul; moves)
             .carousel__slide × n                    (li; holds one card)
         button.carousel__nav.carousel__nav--prev [data-carousel-prev] hidden
         button.carousel__nav.carousel__nav--next [data-carousel-next] hidden
       .carousel__dots        [data-carousel-dots] hidden   (JS fills it)

   Page CSS sets the layout on the .carousel, per breakpoint:
     --carousel-per-view   slides in view (integer; JS reads it, default 1)
     --carousel-slide-min  optional (px): JS shows one slide more than
                           --carousel-per-view each time every slide would
                           still be at least this wide (fluid on wide
                           screens instead of wide-desktop breakpoints)
     --carousel-gap        gap between slides (default --grid-gap)
     --carousel-bleed      how far the clip box reaches past the content
                           edges, so a lifted card and its shadow are not
                           cut (default: the gap minus 4px, so not even the
                           edge or shadow of a neighbour peeks in)
     --carousel-air-top / --carousel-air-bottom   the same room, vertically
     --carousel-nav-y      arrow centre, from the top of the track (50%)
   JS sets --carousel-pv (the slides in view it settled on; the slides are
   sized by it, by --carousel-per-view without JS), --carousel-slide-w
   (px, one slide) and --carousel-speed, plus
   the states .is-ready (JS runs: clipped + translated; before that the
   track simply scrolls sideways), .is-locked (every slide fits: no arrows,
   dots or drag), .is-looping, .is-dragging, .is-jumping.
   Arrows hang half outside the track edges from 1024px; below that the
   track is swiped. */
.carousel {
  --carousel-per-view:   1;
  --carousel-gap:        var(--grid-gap);
  --carousel-bleed:      calc(var(--carousel-gap) - 4px);
  --carousel-air-top:    var(--space-4);
  --carousel-air-bottom: var(--space-8);
  position: relative;
}
.carousel__stage { position: relative; }

.carousel__viewport {
  margin: calc(var(--carousel-air-top) * -1) calc(var(--carousel-bleed) * -1) 0;
  padding: var(--carousel-air-top) var(--carousel-bleed) var(--carousel-air-bottom);
  border-radius: var(--radius-lg);
  /* No JS: a native sideways scroll with snap points. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--carousel-bleed);
  scrollbar-width: none;
}
.carousel__viewport::-webkit-scrollbar { display: none; }
.carousel__viewport:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--focus-outline);
}

.carousel__track {
  display: flex;
  align-items: stretch;
  gap: var(--carousel-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.carousel__slide {
  display: flex;
  flex: 0 0 calc((100% - (var(--carousel-pv, var(--carousel-per-view)) - 1) * var(--carousel-gap)) / var(--carousel-pv, var(--carousel-per-view)));
  min-width: 0;
  scroll-snap-align: start;
}
.carousel__slide > * { flex: 1 1 auto; min-width: 0; }

/* JS running: clip instead of scroll (clip also keeps focus from scrolling
   the box), the track slides with a transform. touch-action pan-y keeps a
   vertical swipe a native page scroll (and pinch-zoom); a sideways one is
   taken by main.js, which then stops the page scroll for that gesture. */
.carousel.is-ready .carousel__viewport {
  overflow: hidden;
  overflow: clip;
  scroll-snap-type: none;
  touch-action: pan-y;              /* fallback if pinch-zoom is not understood */
  touch-action: pan-y pinch-zoom;
  cursor: grab;
}
.carousel.is-ready .carousel__track {
  transition: transform var(--carousel-speed, 500ms) var(--ease-out);
  will-change: transform;
}
.carousel.is-ready .carousel__track img,
.carousel.is-ready .carousel__track a { -webkit-user-drag: none; }
.carousel.is-dragging .carousel__track,
.carousel.is-jumping .carousel__track { transition: none; }
.carousel.is-dragging .carousel__viewport,
.carousel.is-dragging .carousel__viewport * {
  cursor: grabbing;
  -webkit-user-select: none;
          user-select: none;
}
.carousel.is-locked .carousel__viewport { cursor: auto; touch-action: auto; }

/* Arrows: round, floating half over the track edges. */
.carousel__nav {
  position: absolute;
  top: var(--carousel-nav-y, 50%);
  z-index: 3;
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: -22px 0 0;
  padding: 0;
  color: var(--text-1);
  font-size: 0.875rem;
  background-color: var(--surface-float);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-full);
  box-shadow: var(--topbar-sheen), var(--shadow-md);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard),
              opacity var(--dur-fast) var(--ease-standard);
}
.carousel__nav svg { width: 1em; height: 1em; }
.carousel__nav--prev { left: -22px; }
.carousel__nav--next { right: -22px; }
@media (min-width: 1024px) {
  .carousel__nav { display: inline-flex; }
}
.carousel__nav:hover {
  color: var(--accent-300);
  background-color: var(--surface-float-accent);
  border-color: var(--accent-line);
}
.carousel__nav:active { transform: translateY(1px); }
.carousel__nav:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring), var(--shadow-md);
}
.carousel__nav:disabled { opacity: 0.35; cursor: default; pointer-events: none; }

/* Dots: 6px, the active one a 24px accent pill. The button is the larger
   hit area; the dot is its ::before. */
.carousel__dots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: calc(var(--space-2) * -1);
}
.carousel__dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1rem;
  height: 1.5rem;
  padding: 0 0.25rem;
  background: none;
  border: 0;
  cursor: pointer;
}
.carousel__dot::before {
  content: '';
  width: 6px;
  height: 6px;
  background-color: var(--line-hover);
  border-radius: var(--radius-full);
  transition: width var(--dur-slow) var(--ease-out),
              background-color var(--dur-fast) var(--ease-standard);
}
.carousel__dot:hover::before { background-color: var(--text-3); }
.carousel__dot.is-active::before { width: 24px; background-color: var(--accent-500); }
.carousel__dot:focus-visible { outline: none; }
.carousel__dot:focus-visible::before { box-shadow: var(--focus-ring); }

@media (prefers-reduced-motion: reduce) {
  .carousel.is-ready .carousel__track { transition: none; }
}


/* ── 27. Game card (.game-card, Shared/GameCard) ───────────────────────────
   One VoicePack in the library grid and in the "Podobne VoicePacki"
   carousel. A kit .card (+ .card--lift, .spotlight): the 2:3 cover fills
   the top and zooms 1.04 inside its frame on hover, a "Nowy" chip floats
   over it, then the name, a 2×2 grid of mono stats (engine · lines /
   downloads · version), the date it was added and an optional foot
   (GameCard block `foot`: e.g. the home carousel's "Zainstaluj" button,
   above the stretched link). The title's .card__link stretches over the
   whole card. Phones (two narrow cards): tighter padding, one stat per
   line. Hovers are mouse only (tokens.css). */
.game-card {
  height: 100%;
  box-shadow: var(--highlight-top), var(--shadow-sm);
}
.game-card .cover__img { transition: scale 450ms var(--ease-standard); }
.game-card .cover--empty {
  background: radial-gradient(80% 55% at 50% 35%, var(--ambient-violet-soft), transparent), var(--surface-2);
}
@media (hover: hover) and (pointer: fine) {
  .game-card:hover .cover__img { scale: 1.04; }
  .game-card:hover .game-card__title { color: var(--accent-300); }
}

/* The chip sits over the art, so it gets an opaque floating backing. */
.game-card__badge {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  z-index: 3;
  background-color: var(--surface-float-accent);
  -webkit-backdrop-filter: blur(8px);
          backdrop-filter: blur(8px);
  box-shadow: var(--shadow-sm);
}

.game-card__body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  container-type: inline-size;
}
/* Up to two lines; the second line is reserved, so the stats of the cards
   in one grid row line up. */
.game-card__title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  min-height: 2.8em;
  margin: 0;
  overflow: hidden;
  color: var(--text-strong);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
  /* Plain greedy wrapping, not the headings' balance (base.css): the carousel
     sizes its slides in JS to a fraction of a pixel and re-measures them, and
     balancing a 2-line clamped title re-ran on every pass — the same title
     broke "Assassin's Creed / Black Flag Resynced" one moment and "… Black
     Flag / Resynced" the next. Greedy wrapping depends on the width only. */
  text-wrap: wrap;
  transition: color var(--dur-fast) var(--ease-standard);
}
.game-card__stats {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.375rem var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}
.game-card__stat {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  min-width: 0;
  white-space: nowrap;
}
.game-card__stat:nth-child(even) { justify-self: end; }
.game-card__stat svg,
.game-card__date svg { flex: none; width: 1em; height: 1em; color: var(--text-3); }
.game-card__val { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.game-card__date {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  margin: auto 0 0;
  padding-top: var(--space-3);
  color: var(--text-2);
  border-top: 1px solid var(--line-subtle);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}
/* GameCard block `foot`: actions under the stats, above the card link. */
.game-card__foot { position: relative; z-index: 2; margin-top: auto; }
.game-card__date + .game-card__foot { margin-top: 0; }
.game-card__foot:empty { display: none; }
/* The line count shows the number only ("kwestie" is for screen readers);
   narrow cards (two on a phone) put one stat per line. */
@container (max-width: 10.5rem) {
  .game-card__stats { grid-template-columns: minmax(0, 1fr); }
  .game-card__stat:nth-child(even) { justify-self: start; }
}
@media (max-width: 767px) {
  .game-card__body { gap: var(--space-2); padding: var(--space-3); }
  .game-card__title { font-size: 0.9375rem; }
  .game-card__stats { gap: 0.3125rem var(--space-2); font-size: 0.6875rem; }
  .game-card__stat { gap: 0.375rem; }
  .game-card__date { padding-top: var(--space-2); font-size: 0.6875rem; }
  .game-card__badge { top: var(--space-2); left: var(--space-2); }
}
@media (prefers-reduced-motion: reduce) {
  .game-card:hover .cover__img { scale: none; }
}


/* ── 28. Facts (.facts) ────────────────────────────────────────────────────
   A label/value list (<dl> with .facts__row wrappers): label left in
   secondary text, value right in strong text (add .mono on numbers,
   versions, dates), hairlines between rows. */
.facts {
  display: flex;
  flex-direction: column;
  margin: 0;
}
.facts__row {
  display: grid;
  grid-template-columns: minmax(6.5rem, auto) minmax(0, 1fr);
  align-items: baseline;
  gap: var(--space-4);
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--line-subtle);
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.facts__row:first-child { padding-top: 0; }
.facts__row:last-child { padding-bottom: 0; border-bottom: 0; }
.facts dt { color: var(--text-2); }
.facts dd {
  margin: 0;
  color: var(--text-strong);
  text-align: right;
  overflow-wrap: anywhere;
}
.facts dd.mono { font-size: 0.8125rem; }


/* ── 29. Well (.well) ──────────────────────────────────────────────────────
   Mono text in a sunken box with a copy button (checksums, terminal
   commands, keys). __code wraps anywhere, __prompt is an optional "$",
   __copy a .btn--ghost.btn--icon.btn--sm; --code for commands. */
.well {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  background-color: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}
.well__prompt {
  flex-shrink: 0;
  color: var(--accent-400);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  user-select: none;
}
.well__code {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--space-1) 0;
  color: var(--text-1);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.6;
  letter-spacing: 0.02em;
  overflow-wrap: anywhere;
  word-break: break-all;
}
.well--code .well__code { font-size: var(--fs-sm); word-break: normal; }
.well__copy { flex-shrink: 0; align-self: flex-start; color: var(--text-2); }
.well__copy.is-copied,
.well__copy.is-copied:hover { color: var(--success-text); }


/* ── 30. Segmented control (.segmented) ────────────────────────────────────
   Mutually exclusive options in a sunken track; the chosen one
   (.is-active or [aria-pressed="true"]) is raised. Options stack icon /
   name / optional desc (desc shows from 768px). Set --seg-n for a count
   other than 3. */
.segmented {
  display: grid;
  grid-template-columns: repeat(var(--seg-n, 3), minmax(0, 1fr));
  gap: 4px;
  padding: 4px;
  background-color: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}
.segmented__opt {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  min-width: 0;
  min-height: 4rem;
  padding: var(--space-2) var(--space-2);
  color: var(--text-2);
  background-color: transparent;
  border: 1px solid transparent;
  border-radius: calc(var(--radius-md) - 4px);
  text-align: center;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.segmented__opt:hover { color: var(--text-strong); background-color: var(--surface-hover); }
.segmented__opt:active { transform: translateY(1px); }
@supports (-moz-appearance: none) {
  .segmented__opt:active { transform: none; }   /* Firefox: no press nudge (see .btn) */
}
.segmented__opt:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.segmented__opt.is-active,
.segmented__opt[aria-pressed="true"] {
  color: var(--text-strong);
  background: var(--sheen), var(--surface-3);
  border-color: var(--line-strong);
  box-shadow: var(--highlight-top-strong), var(--shadow-sm);
}
.segmented__icon {
  display: inline-flex;
  color: var(--text-3);
  font-size: 1.0625rem;
  transition: color var(--dur-fast) var(--ease-standard);
}
.segmented__opt.is-active .segmented__icon,
.segmented__opt[aria-pressed="true"] .segmented__icon { color: var(--accent-300); }
.segmented__name {
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.25;
}
.segmented__desc {
  display: none;
  color: var(--text-2);
  font-size: var(--fs-xs);
  line-height: 1.35;
}
@media (min-width: 768px) {
  .segmented__desc { display: block; }
}


/* ── 31. Steps (.steps, Shared/Steps) ──────────────────────────────────────
   A numbered vertical path (the home "Jak to działa" idea, stacked): mono
   numbers in accent nodes, joined by a line. Static.
     ol.steps > li.steps__item > span.steps__node (01…) + div.steps__body
   .steps--cards: every step's text sits in a card (.steps__card, a kit
   .card--sheen holding an optional .steps__icon and .steps__text), larger
   nodes centred on the card's first line, a brighter line between them.
   The node is opaque: it takes --steps-floor (the section background;
   --bg, --bg-alt inside .section--alt) under its accent tint. */
.steps {
  --node: 2.25rem;
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}
.steps__item {
  position: relative;
  display: grid;
  grid-template-columns: var(--node) minmax(0, 1fr);
  gap: var(--space-4);
  padding-bottom: var(--space-6);
}
.steps__item:last-child { padding-bottom: 0; }
.steps__item:not(:last-child)::after {
  content: '';
  position: absolute;
  top: calc(var(--node) + 6px);
  bottom: 6px;
  left: calc(var(--node) / 2);
  width: 1px;
  background: linear-gradient(var(--accent-line), var(--line));
  transform-origin: 50% 0;
}
.steps__node {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: var(--node);
  height: var(--node);
  color: var(--accent-300);
  background-color: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.steps__body {
  min-width: 0;
  padding-top: 0.375rem;
  color: var(--text-1);
  font-size: 0.9375rem;
  line-height: 1.65;
  text-wrap: pretty;
}
.steps__body > * + * { margin-top: var(--space-3); }
.steps__body strong { color: var(--text-strong); }

.steps--cards {
  --node: 2.75rem;
  --node-top: 0.625rem;   /* centres the node on the card's first text line */
  gap: var(--space-5);
}
.section--alt .steps--cards { --steps-floor: var(--bg-alt); }
.steps--cards .steps__item {
  align-items: start;
  gap: var(--space-5);
  padding-bottom: 0;
}
.steps--cards .steps__item:not(:last-child)::after {
  top: calc(var(--node-top) + var(--node));
  bottom: calc((var(--space-5) + var(--node-top)) * -1);
  background: linear-gradient(var(--accent-500), var(--accent-line));
}
.steps--cards .steps__node {
  margin-top: var(--node-top);
  background: linear-gradient(var(--accent-soft), var(--accent-soft)), var(--steps-floor, var(--bg));
  font-size: 0.8125rem;
  font-weight: 500;
}
.steps--cards .steps__body { padding-top: 0; }
.steps__card {
  flex-direction: row;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-5);
}
.steps__icon {
  display: inline-flex;
  margin-top: 0.1875rem;
  color: var(--text-3);
  font-size: 1.125rem;
}
.steps__text {
  margin: 0;
  color: var(--text-1);
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.5;
}


/* ── 32. Split (.split) ─────────────────────────────────────────────────────
   A section in two columns from 1024px: the header (and whatever goes with
   it, in .split__head) on the left, the content on the right; the head
   stays at the top of its column — never sticky (owner). One column below
   1024px. A bare SectionHeader can be the first child instead of
   .split__head. */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-10);
}
.split > * { min-width: 0; }
.split__head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-8);
}
.split__head .section-header,
.split > .section-header { margin-bottom: 0; }
@media (min-width: 1024px) {
  .split {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    align-items: start;
    gap: var(--space-16);
  }
}


/* ── 33. News: media box, grid, card ────────────────────────────────────────
   .news-media (16:9): a news cover (source 640×360) on the news list, the
   home timeline, the post hero and the post sidebar. The fallback
   (category icon on a faint light) sits under the image, so a post
   without a cover (or with a broken file) still has its tile, and the
   image simply appears over it when loaded (no fade, no pulse). main.js
   (markImage) marks a box .is-loaded / .is-error. */
.news-media {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  margin: 0;
  overflow: hidden;
  background-color: var(--surface-2);
  isolation: isolate;
}
.news-media__img {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  transition: scale 0.6s var(--ease-out);
}
.news-media.is-error .news-media__img { display: none; }
.news-media__fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--text-3);
  background: radial-gradient(60% 70% at 50% 40%, var(--ambient-violet-soft), transparent);
  font-size: 2rem;
}


/* News card (.news-card, Shared/NewsCard) in a .news-grid: 1 column, 2
   from 768px, 3 from 1024px (.news-grid--3: 3 from 768px). 16:9 cover
   with a bottom scrim and the category chip on it, title (3 lines),
   excerpt (3 lines), author · date in mono and the "Czytaj dalej" pill;
   the whole card is a link. The wide feature variant (.news-card--feature:
   no foot, a "Czytaj wpis" line, .news-card__more, instead) is laid out
   by news.css; the timeline variant (.news-card--timeline: no chips and no
   scrim on the cover, the author alone in the foot) by the home page. The
   hovers (cover zoom, title colour, the pill lighting up) are mouse only
   (tokens.css); keyboard focus lights the pill too. */
.news-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--grid-gap);
}
@media (min-width: 768px) {
  .news-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .news-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .news-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.news-card {
  box-shadow: var(--highlight-top), var(--shadow-md);
}
/* The cover sits above the card's stretched link but lets clicks through to
   it; only the game chip on it takes the pointer. */
.news-card__media { z-index: 2; pointer-events: none; }
/* The cover sits above the stretched link, so the link's own focus ring
   would only show under it: the whole card carries the ring instead. */
.news-card:has(.card__link:focus-visible) { box-shadow: var(--highlight-top), var(--focus-ring); }
.news-card .card__link:focus-visible::after { box-shadow: none; }
/* Scrim at the bottom of the cover, under the chips. */
.news-card__media::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(to top, var(--cover-scrim-mid), transparent 55%);
  pointer-events: none;
}
/* The timeline card has no chips on its cover: no scrim either. */
.news-card--timeline .news-card__media::after { display: none; }
@media (hover: hover) and (pointer: fine) {   /* cover zoom: mouse only (tokens.css) */
  .news-card:hover .news-media__img { scale: 1.03; }
}
.news-card__chips {
  position: absolute;
  left: var(--space-4);
  right: var(--space-4);
  bottom: var(--space-3);
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
/* The chips on the cover take the .chip--on-media look (section 3). */
.news-card__game {
  min-width: 0;
  max-width: 100%;
  pointer-events: auto;
}
@media (hover: hover) and (pointer: fine) {   /* no tap effect on touch (tokens.css) */
  .news-card__game:hover { color: var(--text-strong); border-color: var(--line-hover); }
}

.news-card__body { gap: var(--space-3); }
.news-card__title {
  display: -webkit-box;
  overflow: hidden;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  transition: color var(--dur-fast) var(--ease-standard);
}
.news-card__excerpt {
  display: -webkit-box;
  overflow: hidden;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}
.news-card__foot {
  flex-wrap: nowrap;
  justify-content: space-between;
  margin-inline: var(--space-5);
  padding-top: var(--space-3);
  padding-inline: 0;
  border-top: 1px solid var(--line-subtle);
  /* Nothing in the foot is interactive (author · date, the pill): clicks
     fall through to the card link (.card__foot > * sits above it). */
  pointer-events: none;
}
.news-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
}
/* "Czytaj dalej" — a small pill in the card foot (the card itself is the
   link). Quiet at rest, accent on card hover / focus; the arrow does not
   move. */
.news-card__read {
  pointer-events: none;   /* decoration: a click falls through to the card link */
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  gap: 0.375rem;
  padding: 0.3125rem 0.75rem;
  color: var(--text-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
@media (hover: hover) and (pointer: fine) {
  .news-card:hover .news-card__title { color: var(--accent-300); }
  .news-card:hover .news-card__read {
    color: var(--accent-text);
    background-color: var(--accent-soft);
    border-color: var(--accent-line);
  }
}
.news-card:focus-within .news-card__read {
  color: var(--accent-text);
  background-color: var(--accent-soft);
  border-color: var(--accent-line);
}
/* "Czytaj wpis" — the feature card's cue under its excerpt (it has no
   foot); the arrow nudges on card hover (mouse only). */
.news-card__more {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-1) 0 0;
  color: var(--accent-text);
  font-size: var(--fs-sm);
  font-weight: 600;
}
.news-card__more svg { transition: transform var(--dur-fast) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .news-card:hover .news-card__more svg { transform: translateX(2px); }
}

@media (prefers-reduced-motion: reduce) {
  .news-card:hover .news-media__img { scale: none; }
  .news-card:hover .news-card__more svg { transform: none; }
}


/* ── 34. CTA band (.cta-band, Shared/CtaBand) ───────────────────────────────
   The closing section of an inner page, on the deep floor: the dot grid in
   a soft oval, a violet light in the middle (.cta-band__light), centred
   copy (an icon tile or an eyebrow, a big title, one line, the actions)
   and a subtitle line (Shared/Caption, .caption) under them that types a
   few lines of the page's "dialogue". Shared/CtaBand renders it (embed,
   block `actions`):
     <section class="cta-band">
       <span class="cta-band__light" aria-hidden="true"></span>
       <div class="wrap cta-band__inner"> icon/eyebrow, __title, __text, __actions,
         Shared/Caption (extraClass: cta-band__caption) </div>
     </section>
   The primary button in __actions glows (the .btn--glow underlight). The
   home page closes with the same band, hand-built in HomePage/Outro
   (.cta-band.outro: more room, the display title, a voice line between
   two .cta-band__inner rows; home.css lays it out). */
.cta-band {
  position: relative;
  isolation: isolate;
  overflow: clip;
  padding-block: calc(var(--section-y) * 1.1);
  background-color: var(--bg-deep);
  text-align: center;
}
.cta-band::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -2;
  background-image: var(--dot-grid);
  background-size: var(--dot-grid-size);
  -webkit-mask-image: radial-gradient(60% 70% at 50% 45%, #000, transparent);
          mask-image: radial-gradient(60% 70% at 50% 45%, #000, transparent);
  pointer-events: none;
}
.cta-band__light {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(36% 50% at 50% 46%, var(--ambient-violet), transparent 72%),
    radial-gradient(20% 26% at 50% 24%, var(--ambient-warm), transparent 70%);
  pointer-events: none;
}
.cta-band__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
}
.cta-band__icon { margin-bottom: var(--space-2); box-shadow: var(--underlight-sm); }
.cta-band__title {
  max-width: 20ch;
  margin: 0;
  color: var(--text-strong);
  font-size: clamp(2.125rem, 4.6vw, 3.5rem);
  font-weight: 600;
  line-height: 1.04;
  letter-spacing: -0.035em;
  text-wrap: balance;
}
.cta-band__text {
  max-width: 40rem;
  margin: 0;
  color: var(--text-2);
  font-size: var(--fs-lead);
  line-height: 1.65;
  text-wrap: pretty;
}
.cta-band__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}
.cta-band__actions > .btn--primary { --btn-shadow: var(--highlight-btn), var(--underlight); }
.cta-band__caption { margin-top: var(--space-12); }
@media (max-width: 767px) {
  .cta-band__text { font-size: 1rem; }
  .cta-band__caption { margin-top: var(--space-10); }
  .cta-band__actions { width: 100%; }
  .cta-band__actions > .btn { flex: 1 1 100%; white-space: normal; }
}


/* ── 35. Call-out (.callout) ────────────────────────────────────────────────
   A wide card that closes a section with one offer: icon tile + copy on the
   left (.callout__main > .icon-tile + .callout__copy > __title, __text),
   the way on to the right (.callout__side holding buttons, or
   .callout__actions + a .callout__note). surface-2 with a soft violet light
   from the left. Stacks on phones. The primary button in __actions glows
   (.btn--glow-sm). .callout--media (below): copy left, framed art right. */
.callout {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-8);
  padding: var(--space-8) var(--space-6);
  background:
    radial-gradient(40% 130% at 0% 50%, var(--ambient-violet), transparent 72%),
    var(--sheen),
    var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-xl);
  box-shadow: var(--highlight-top-strong), var(--shadow-xl);
}
.callout__main {
  display: flex;
  align-items: flex-start;
  gap: var(--space-5);
  min-width: 0;
}
.callout__copy { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.callout__title {
  margin: 0;
  color: var(--text-strong);
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.02em;
}
.callout__text {
  max-width: 52ch;
  margin: 0;
  color: var(--text-1);
  font-size: 1rem;
  line-height: 1.65;
  text-wrap: pretty;
}
.callout__text strong { color: var(--text-strong); }
.callout__side {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-6);
  min-width: 0;
}
.callout__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.callout__actions > .btn--primary { --btn-shadow: var(--highlight-btn), var(--underlight-sm); }
.callout__note {
  flex-basis: 100%;
  margin: 0;
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1.55;
}
.callout__note strong { color: var(--text-strong); font-weight: 600; }
@media (min-width: 1024px) {
  .callout {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-12);
    padding: var(--space-10) var(--space-12);
  }
}
@media (max-width: 767px) {
  .callout__main { flex-direction: column; }
  .callout__side > .btn,
  .callout__actions > .btn { flex: 1 1 100%; }
}

/* .callout--media: copy on the left (.callout__copy: an icon tile, an
   eyebrow, __title, __text, __actions under each other), a framed piece of
   art on the right (.callout__art: a link or a box holding an <img>, with
   a violet underlight); the light comes from the right. Two equal columns
   from 1024px (the art turned slightly towards the copy), stacked below.
   Hover (mouse only): the frame takes the accent edge and the image
   zooms 1.03. */
.callout--media {
  align-items: center;
  overflow: hidden;
  background:
    radial-gradient(50% 120% at 100% 50%, var(--ambient-violet), transparent 72%),
    var(--sheen),
    var(--surface-2);
}
.callout--media .callout__copy {
  align-items: flex-start;
  gap: var(--space-3);
}
.callout--media .callout__copy > .icon-tile { margin-bottom: var(--space-2); }
.callout--media .callout__title { font-size: clamp(1.5rem, 2.6vw, 2rem); line-height: 1.2; }
.callout--media .callout__text { max-width: 50ch; }
.callout--media .callout__actions { margin-top: var(--space-3); }
.callout__art {
  display: block;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xl), var(--underlight);
  transition: border-color var(--dur-fast) var(--ease-standard);
}
.callout__art img {
  width: 100%;
  height: auto;
  transition: scale 0.6s var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .callout--media:hover .callout__art { border-color: var(--accent-line); }
  .callout--media:hover .callout__art img { scale: 1.03; }
}
@media (min-width: 1024px) {
  .callout--media {
    grid-template-columns: minmax(0, 6fr) minmax(0, 6fr);
    gap: var(--space-12);
    padding: var(--space-12);
  }
  .callout--media .callout__art { transform: perspective(1200px) rotateY(-8deg) rotateX(3deg); }
}
@media (prefers-reduced-motion: reduce) {
  .callout--media:hover .callout__art img { scale: none; }
}


/* ── 36. Meter (.meter) ─────────────────────────────────────────────────────
   A thin progress bar: a track and its fill (accent).
     <span class="meter" role="img" aria-label="…"><span class="meter__fill" style="--meter: 0.42"></span></span>
   --meter (0…1, set inline on the fill or the meter) is how far it is
   filled. A native <progress class="meter" value max> gets the same look.
     --lg       6px instead of 4px
     --success  a green fill (e.g. a goal reached)
     --grow     the fill grows in once on load (clip-path, 1.2s
                --ease-out-cubic after 300ms — the timing of
                LS.countUp(el, {delay: 300}), so a number counting beside it
                ends with the bar)
     --sheen    a slow light band sweeps over the filled part (4.8s loop)
   --meter-w caps the width (default: the container). Reduced motion: the
   fill simply stands at its value. */
.meter {
  --meter-h: 4px;
  position: relative;
  display: block;
  width: 100%;
  max-width: var(--meter-w, none);
  height: var(--meter-h);
  margin: 0;
  overflow: hidden;
  color: var(--accent-500);
  background-color: var(--surface-3);
  border: 0;
  border-radius: var(--radius-full);
  -webkit-appearance: none;
          appearance: none;
}
.meter--lg { --meter-h: 6px; }
.meter__fill {
  position: relative;
  display: block;
  width: calc(var(--meter, 0) * 100%);
  height: 100%;
  overflow: hidden;
  background-color: var(--accent-500);
  border-radius: inherit;
}
.meter--success .meter__fill { background-color: var(--success); }
.meter--grow .meter__fill { animation: ls-meter-grow 1200ms var(--ease-out-cubic) 300ms both; }
.meter--sheen .meter__fill::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--sheen-sweep), transparent) no-repeat;
  background-size: 60% 100%;
  background-position: -150% 0;   /* parked left of the fill */
  pointer-events: none;
  animation: ls-meter-sheen 4.8s var(--ease-in-out) 1.8s infinite;
}
progress.meter::-webkit-progress-bar { background-color: var(--surface-3); border-radius: var(--radius-full); }
progress.meter::-webkit-progress-value { background-color: var(--accent-500); border-radius: var(--radius-full); }
progress.meter::-moz-progress-bar { background-color: var(--accent-500); border-radius: var(--radius-full); }
@keyframes ls-meter-grow {
  from { clip-path: inset(0 100% 0 0 round var(--radius-full)); }
  to   { clip-path: inset(0 0 0 0 round var(--radius-full)); }
}
/* The band crosses in the first 40% of the loop, then rests off the fill. */
@keyframes ls-meter-sheen {
  0%   { background-position: -150% 0; }
  40%  { background-position: 250% 0; }
  100% { background-position: 250% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .meter--grow .meter__fill,
  .meter--sheen .meter__fill::after { animation: none; }
}


/* ── 37. Spinner ────────────────────────────────────────────────────────────
   .spinner: a ring (2px --line-strong, the top quarter in accent) turning
   once every 0.8s; --spinner-size sets it (default 1.25rem). Decorative:
   pair it with text, or aria-busy on the region that loads.
   .spin: put it on an icon (e.g. fa6-solid:spinner) to turn it. Reduced
   motion: both stand still. */
.spinner {
  display: inline-block;
  flex: none;
  width: var(--spinner-size, 1.25rem);
  height: var(--spinner-size, 1.25rem);
  border: 2px solid var(--line-strong);
  border-top-color: var(--accent-400);
  border-radius: 50%;
  animation: ls-spin 0.8s linear infinite;
}
.spin { animation: ls-spin 0.9s linear infinite; }
@media (prefers-reduced-motion: reduce) {
  .spinner,
  .spin { animation: none; }
}


/* ── 38. Entrance (.is-entering) ────────────────────────────────────────────
   Something that has just appeared because of a click (a page of table
   rows, a panel switched in) rises 8px and fades in once (300ms). Add the
   class, remove it to replay (force a reflow in between). Tune it with
   --enter-y, --enter-dur and --enter-delay (a stagger, set inline; table
   rows: 4px, 260ms — section 22). Firefox: a fade only (the rise ending
   would re-snap the text). Reduced motion: no animation. Subpages animate
   on interaction only — this is never a scroll reveal. */
.is-entering {
  animation: ls-enter var(--enter-dur, 300ms) var(--ease-out) var(--enter-delay, 0ms) both;
}
@keyframes ls-enter {
  from { opacity: 0; transform: translateY(var(--enter-y, 8px)); }
  to   { opacity: 1; transform: none; }
}
@supports (-moz-appearance: none) {
  .is-entering { animation-name: ls-fade-in; }
}
@media (prefers-reduced-motion: reduce) {
  .is-entering { animation: none; }
}


/* ── 39. Table of contents (.toc, Shared/Toc + LS.toc) ──────────────────────
   "W tym wpisie" / "Spis treści": LS.toc (main.js) builds the list from
   the headings of the text it points at; until then (and when the text has
   fewer than two sections) the nav stays hidden.
     nav.toc[data-toc][hidden]
       button.card.toc__toggle[data-toc-toggle]   (.toc--sticky only)
       div.card.card--pad-md.toc__panel
         .eyebrow.eyebrow--muted.toc__title
         ol.toc__list[data-toc-list] > li > a.toc__link (.is-current)
                                              > span.toc__num + span.toc__label
   A link: the mono number, the heading's text, a 2px violet rail on the
   left edge of the current section (it grows in). Default: an in-flow card
   (the news post sidebar). .toc--sticky (the legal documents): from 1024px
   a card that sticks under the header (a long list scrolls inside it);
   below that a sticky "Spis treści" toggle with the list dropping down
   under it (.is-open; Esc, a click outside or a choice closes it).
   Firefox: the drop-down only fades (no slide to re-snap). */
.toc { min-width: 0; }
.toc__toggle { display: none; }
.toc__title { margin: 0 0 var(--space-3); }
.toc__list {
  position: relative;
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
  border-left: 1px solid var(--line);
}
.toc__link {
  position: relative;
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr);
  gap: var(--space-2);
  padding: 0.4375rem 0 0.4375rem var(--space-4);
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1.45;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-standard);
}
.toc__link::before {
  content: '';
  position: absolute;
  top: 0.375rem;
  bottom: 0.375rem;
  left: -1px;
  width: 2px;
  background-color: var(--accent-500);
  border-radius: var(--radius-full);
  transform: scaleY(0);
  transition: transform var(--dur-base) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .toc__link:hover { color: var(--text-strong); }
}
.toc__link:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--focus-outline);
  border-radius: var(--radius-sm);
}
.toc__link.is-current { color: var(--accent-300); }
.toc__link.is-current::before { transform: none; }
.toc__num {
  color: var(--text-3);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.95;
  font-variant-numeric: tabular-nums;
}
.toc__link.is-current .toc__num { color: var(--accent-text); }
.toc__label { min-width: 0; }

/* Sticky variant: the toggle is a kit card as a button. */
.toc--sticky .toc__toggle {
  flex-direction: row;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 3rem;
  padding: 0 var(--space-5);
  color: var(--text-1);
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.toc--sticky .toc__toggle > svg:first-child { color: var(--accent-text); }
/* The toggle shows below 1024px — mostly touch, where a tap must not leave
   its edge lit: the hover edge is for a mouse only. */
@media (hover: hover) and (pointer: fine) {
  .toc--sticky .toc__toggle:hover { border-color: var(--line-hover); }
}
.toc--sticky .toc__toggle:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.toc__chev {
  margin-left: auto;
  color: var(--text-2);
  font-size: 0.75rem;
  transition: transform var(--dur-base) var(--ease-out);
}
.toc--sticky.is-open .toc__toggle { border-color: var(--accent-line); }
.toc--sticky.is-open .toc__chev { transform: rotate(180deg); }
@media (max-width: 1023px) {
  .toc--sticky {
    position: sticky;
    top: calc(var(--head-h) + var(--space-2));
    z-index: 5;
  }
  .toc--sticky .toc__toggle { display: flex; }
  .toc--sticky .toc__panel {
    position: absolute;
    top: calc(100% + var(--space-2));
    left: 0;
    right: 0;
    z-index: 1;
    max-height: min(60vh, 28rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    background-color: var(--surface-overlay);
    border-color: var(--line-strong);
    box-shadow: var(--shadow-lg);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity var(--dur-fast) var(--ease-standard),
                transform var(--dur-base) var(--ease-out),
                visibility 0s linear var(--dur-fast);
  }
  .toc--sticky.is-open .toc__panel {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 0s;
  }
  .toc--sticky .toc__title { display: none; }
  @supports (-moz-appearance: none) {
    .toc--sticky .toc__panel,
    .toc--sticky.is-open .toc__panel { transform: none; }
  }
}
@media (min-width: 1024px) {
  .toc--sticky {
    position: sticky;
    top: calc(var(--head-h) + var(--space-6));
  }
  .toc--sticky .toc__panel {
    max-height: calc(100vh - var(--head-h) - var(--space-12));
    overflow-y: auto;
    scrollbar-width: thin;
  }
}


/* ── 40. Social links (.socials, Shared/SocialLinks) ────────────────────────
   A wrapping row of kit icon buttons (.btn--icon) to the project's
   channels (or a person's profiles). --muted: quieter icons (text-2)
   that light up (text-strong) under the mouse only — a row of profile
   links at the foot of a card. */
.socials {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
.socials--muted .btn { --btn-fg: var(--text-2); --btn-fg-hover: var(--text-2); }
@media (hover: hover) and (pointer: fine) {
  .socials--muted .btn { --btn-fg-hover: var(--text-strong); }
}


/* ── 41. Video (.video, Shared/VideoFacade + LS.video) ──────────────────────
   A YouTube video behind a click-to-play facade: the 16:9 box with the
   thumbnail (dimmed at rest so the play button leads), a scrim, the violet
   play button and an optional caption (title + mono hint). Nothing is
   requested from YouTube until the visitor presses play; LS.video (main.js)
   then swaps the facade for a youtube-nocookie.com iframe. The thumbnail can
   be served from this site (Help Center) or from i.ytimg.com (/download).
   The play button is centred with negative margins, never translate(-50%)
   (Firefox re-snaps a half-pixel). Variant --inline: inside running text
   (.prose) — a quieter frame without the violet underlight. Firefox: the
   thumbnail only brightens on hover, no zoom (the zoom ending re-snapped
   the play button and caption above it). */
.video {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background-color: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--highlight-top-strong), var(--shadow-xl), var(--underlight);
}
.video--inline {
  border-color: var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--highlight-top), var(--shadow-md);
}
.video__facade {
  position: absolute;
  inset: 0;
  display: block;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.video__facade:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--focus-outline);
}
.video__thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  filter: brightness(0.8) saturate(0.95);
  transition: opacity var(--dur-slow) var(--ease-standard),
              scale 0.6s var(--ease-out),
              filter var(--dur-slow) var(--ease-standard);
}
.video__facade.is-loaded .video__thumb { opacity: 1; }
.video__facade:focus-visible .video__thumb {
  scale: 1.03;
  filter: brightness(0.95) saturate(1);
}
.video__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, var(--cover-scrim) 0%, var(--cover-scrim-mid) 26%, transparent 52%);
  pointer-events: none;
}
.video__play {
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  place-items: center;
  width: 4.5rem;
  height: 4.5rem;
  margin: -2.25rem 0 0 -2.25rem;
  color: var(--on-accent);
  background-color: var(--accent-500);
  border-radius: 50%;
  box-shadow: var(--highlight-btn), var(--underlight-sm);
  font-size: 1.375rem;
  transition: background-color var(--dur-fast) var(--ease-standard);
}
.video__play svg { margin-left: 0.15em; }
.video--inline .video__play { width: 3.75rem; height: 3.75rem; margin: -1.875rem 0 0 -1.875rem; font-size: 1.125rem; }
.video__facade:focus-visible .video__play { background-color: var(--accent-450); }
@media (hover: hover) and (pointer: fine) {
  .video__facade:hover .video__thumb {
    scale: 1.03;
    filter: brightness(0.95) saturate(1);
  }
  .video__facade:hover .video__play { background-color: var(--accent-450); }
}
.video__caption {
  position: absolute;
  right: var(--space-6);
  bottom: var(--space-5);
  left: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  pointer-events: none;
}
.video__title {
  color: var(--text-strong);
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.3;
}
.video--inline .video__title { font-size: 1rem; }
.video__hint {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}
.video__iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
@media (max-width: 767px) {
  .video__play { width: 3.5rem; height: 3.5rem; margin: -1.75rem 0 0 -1.75rem; font-size: 1.125rem; }
  .video__caption { right: var(--space-4); bottom: var(--space-3); left: var(--space-4); }
  .video__title { font-size: 0.9375rem; }
}
@supports (-moz-appearance: none) {
  .video__facade:hover .video__thumb,
  .video__facade:focus-visible .video__thumb { scale: none; }
}


/* ── 42. Help article card (.help-cards, .help-card; HelpPage/ArticleCard) ──
   One Help Center article as a kit card: the Help Center lists and the
   install guides on /download. */
.help-cards {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--grid-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 768px) {
  .help-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .help-cards { grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr)); }
}
.help-card { gap: var(--space-3); }
.help-card__cat {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--accent-text);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
}
.help-card__title {
  margin: 0;
  color: var(--text-strong);
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.01em;
  text-wrap: balance;
  transition: color var(--dur-fast) var(--ease-standard);
}
.help-card__desc {
  margin: 0;
  color: var(--text-2);
  font-size: 0.9375rem;
  line-height: 1.6;
  text-wrap: pretty;
}
.help-card__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
.help-card__go {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-2);
  color: var(--accent-text);
  font-size: var(--fs-sm);
  font-weight: 600;
}
.help-card__arrow { transition: transform var(--dur-fast) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .help-card:hover .help-card__title { color: var(--accent-300); }
  .help-card:hover .help-card__arrow { transform: translateX(2px); }
}
/* Firefox: the arrow does not nudge (a transform ending re-snaps the text
   next to it; tokens.css). (-moz-appearance = Firefox.) */
@supports (-moz-appearance: none) {
  .help-card:hover .help-card__arrow { transform: none; }
}


/* ── 43. Tones ──────────────────────────────────────────────────────────────
   Last in the file on purpose: the component bases above set default
   --tone values, and these modifiers must override them. One definition
   per tone; chips, notices, avatars and toasts read --tone (text/icon),
   --tone-soft (fill) and --tone-line (border). Avatars take the kit tone
   names too, plus the colour names stored on TeamMember / CommunityHero
   (purple = accent, cyan = teal, green = success, amber / yellow =
   warning, red = danger). Toasts: accent, success, danger. */
.chip--accent, .notice--accent, .snack--accent,
.avatar--accent, .avatar--purple {
  --tone: var(--accent-text); --tone-soft: var(--accent-soft); --tone-line: var(--accent-line);
}
.chip--teal, .notice--teal,
.avatar--teal, .avatar--cyan {
  --tone: var(--teal-text); --tone-soft: var(--teal-soft); --tone-line: var(--teal-line);
}
.chip--success, .notice--success, .snack--success,
.avatar--success, .avatar--green {
  --tone: var(--success-text); --tone-soft: var(--success-soft); --tone-line: var(--success-line);
}
.chip--warning, .notice--warning,
.avatar--warning, .avatar--amber, .avatar--yellow {
  --tone: var(--warning-text); --tone-soft: var(--warning-soft); --tone-line: var(--warning-line);
}
.chip--danger, .notice--danger, .snack--danger,
.avatar--danger, .avatar--red {
  --tone: var(--danger-text); --tone-soft: var(--danger-soft); --tone-line: var(--danger-line);
}
.chip--blue, .avatar--blue {
  --tone: var(--cat-blue); --tone-soft: var(--cat-blue-soft); --tone-line: var(--cat-blue-line);
}
.chip--indigo, .avatar--indigo {
  --tone: var(--cat-indigo); --tone-soft: var(--cat-indigo-soft); --tone-line: var(--cat-indigo-line);
}
.chip--pink, .avatar--pink {
  --tone: var(--cat-pink); --tone-soft: var(--cat-pink-soft); --tone-line: var(--cat-pink-line);
}
.chip--orange, .avatar--orange {
  --tone: var(--cat-orange); --tone-soft: var(--cat-orange-soft); --tone-line: var(--cat-orange-line);
}
.chip--muted, .avatar--muted {
  --tone: var(--text-2); --tone-soft: var(--surface-active); --tone-line: var(--line);
}
