/* ---------------------------------------------------------------- Fonts --
   Self-hosted and subset (ASCII + Polish diacritics + typographic marks).
   Self-hosted means no request leaves the visitor's browser to a third
   party, so no IP addresses reach Google — one less RODO disclosure.

   These live here rather than in a separate file loaded with @import:
   an @import forces the browser to fetch this stylesheet, parse it, and
   only THEN discover the fonts — a serial round trip that delayed first
   paint. Inline, the fonts are discovered as soon as this file arrives.

   ONE FILE PER FAMILY+SUBSET, NOT ONE PER WEIGHT. Both families are
   VARIABLE fonts with a `wght` axis (Karla 400–800, Newsreader 200–800),
   so the 400 file and the 700/600 file were byte-for-byte identical — the
   browser was downloading the same 61 KB twice under two names. Each face
   below still pins an exact weight (that is what a single `font-weight`
   value does to a variable axis, so the rendering is unchanged), they just
   all point at the one file. Verified with fontTools: `fvar` present.
   Do not re-split these per weight.
   -------------------------------------------------------------------- */

/* The two subsets, repeated on every face below.
   latin-ext = U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
               U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
               U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF
               — this is where ą ć ę ł ń ś ź ż live, so Polish needs it.
   latin     = everything else the page uses. */

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

@font-face {
  font-family: 'Karla';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/karla-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

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

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

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

@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/newsreader-italic-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

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

@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/newsreader-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

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

@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(../fonts/newsreader-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   Korepetycje z angielskiego — single-page site

   Direction
   ---------
   Palette : burgundy as the brand colour (her call — the logo will match it)
             on a warm blush-white paper. Wine, deliberately NOT clay or
             terracotta: it stays on the red/purple side of the wheel.
             The butter-yellow highlighter stays — yellow against burgundy is
             what keeps the page warm and a little playful rather than formal.
   Type    : Newsreader (a bookish reading serif) for headings and the pull
             quote; Karla (humanist grotesque) for body and UI.
   Signature: the highlighter stroke on one word of the headline. The bilingual
             dictionary-entry section labels that used to be the other
             signature were removed on 2026-08-13 at Wero's request.
   ========================================================================== */

:root {
  /* Colour ---------------------------------------------------------------- */
  --paper:        #FDF7F5;   /* warm blush white — pink-leaning, not cream   */
  --surface:      #FFFFFF;
  --surface-alt:  #F1E9E4;   /* warm greige — the quieter band          */
  --ink:          #2B1A1E;   /* warm near-black, red undertone               */
  --ink-soft:     #5C474B;
  --ink-faint:    #736067;   /* 5.1:1 even on --surface-alt, the lightest bg */
  --wine:         #8E0E32;   /* sampled from her cherry logo — exact match    */
  --wine-deep:    #6B0A22;
  --cocoa:        #2B1A1E;   /* secondary: the quiet dark, for big calm blocks */
  --cocoa-wash:   #F1E9E4;   /* tint for icon chips on white cards             */
  --cocoa-band:   #EDE4DD;   /* the deeper of the two section bands            */
  --marker:       #F7C948;   /* highlighter — marks words, never fills boxes    */
  --marker-soft:  #FDF0C8;
  --line:         #EEDCD8;
  --line-soft:    #F5E8E5;
  --danger:       #A3231B;

  /* Type ------------------------------------------------------------------ */
  --font-display: "Newsreader", Georgia, "Times New Roman", serif;
  --font-body:    "Karla", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* Spacing rhythm (4 / 8 scale) ------------------------------------------ */
  --s-1: 0.25rem;  --s-2: 0.5rem;  --s-3: 0.75rem;  --s-4: 1rem;
  --s-5: 1.5rem;   --s-6: 2rem;    --s-7: 3rem;     --s-8: 4rem;
  --s-9: 6rem;

  /* Rounder than the first pass — she asked for warmer and more inviting,
     and corner radius does more of that work than any other single value. */
  --radius:    16px;
  --radius-lg: 26px;
  --nav-h:     74px;

  --shadow-sm: 0 1px 2px rgba(43, 26, 30, 0.05), 0 2px 10px rgba(43, 26, 30, 0.05);
  --shadow-md: 0 2px 4px rgba(43, 26, 30, 0.05), 0 14px 36px rgba(43, 26, 30, 0.09);

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

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

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

html {
  scroll-behavior: smooth;
  /* Zero, not the nav's height. Offsetting by the nav parks the target
     section just below it, which leaves the previous section's band showing
     through the translucent bar — a strip of the wrong colour across the top
     of every jump. At zero the section's own top edge is at the top of the
     viewport, so what shows through the bar is the section you asked for.
     The sections you can jump to reserve nav-height of padding, so nothing
     ends up underneath it. */
  scroll-padding-top: 0;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }
/* blockquote carries a browser default of `1em 40px`, which was insetting
   the quote panel on all four sides and stopping it lining up. */
p, figure, blockquote { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
a { color: inherit; }

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.14;
  letter-spacing: -0.012em;
}
/* Headings otherwise break wherever the column runs out, which left the hero
   reading "Lekcje / angielskiego dla / każdego." and stranded single words on
   second lines elsewhere. Balancing spreads them evenly. Browsers without
   support just wrap as before, so there is nothing to fall back to.
   Not on a phone: there the column is too narrow for balancing to have any
   room, and on the pricing heading it only moved the em dash down to start
   the second line, which reads like a stray list item. */
@media (min-width: 721px) {
  h1, h2, h3, h4 { text-wrap: balance; }
}

/* Focus. The ring has to be visible on every surface it can land on, and the
   page has three: pale paper, the filled burgundy trial card, and the dark
   brown footer. A burgundy ring vanishes on the last two (burgundy on
   burgundy is invisible; burgundy on near-black is ~1.9:1, well under the
   3:1 WCAG asks of a focus indicator), so dark surfaces re-declare the token
   rather than every element re-declaring the outline. */
:focus-visible {
  outline: 3px solid var(--focus-ring, var(--wine));
  outline-offset: 3px;
  border-radius: 3px;
}
.footer, .quote, .price-card--trial { --focus-ring: #FFFFFF; }

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

/* Bot trap. Parked far off-screen rather than clipped to 1px, so it can't
   contribute phantom geometry to the layout on narrow screens. */
.honeypot {
  position: absolute; left: -9999px; top: 0;
  width: 1px; height: 1px; overflow: hidden;
}

.skip-link {
  position: absolute; left: var(--s-4); top: -100px; z-index: 200;
  background: var(--wine); color: #FFFFFF;
  padding: var(--s-3) var(--s-4); border-radius: var(--radius);
  font-weight: 700; text-decoration: none;
  transition: top 180ms var(--ease);
}
.skip-link:focus { top: var(--s-4); }

/* ------------------------------------------------------------- Layout -- */

.wrap {
  width: 100%;
  max-width: 1140px;
  margin-inline: auto;
  padding-inline: var(--s-5);
}

.section { padding-block: var(--s-9); }

/* A section you can jump to from the nav gets the whole screen: land on it and
   it is the only thing in view, with no band of the section above or below
   showing at either edge. Top padding is at least the nav's height so the
   heading never sits under the bar, since the anchor now lands the section's
   true top edge at the top of the viewport. */
.section--screen {
  min-height: 100svh;
  display: flex;
  align-items: center;
  padding-block: calc(var(--nav-h) + var(--s-5)) var(--s-7);
}
.section--screen > .wrap { width: 100%; }

/* The offer section carries the most: a heading, two formats and four prices.
   At full spacing it stands 930px and was cut off at the bottom of a laptop
   screen. On anything shorter than ~900px of viewport its internals tighten
   so the whole thing still lands in one view. Nothing moves on a tall screen. */
@media (min-width: 941px) and (max-height: 900px) {
  .section--screen { padding-block: calc(var(--nav-h) + var(--s-4)) var(--s-6); }
  .section--screen .section__head { margin-bottom: var(--s-5); }
  .section--screen .formats { margin-bottom: var(--s-5); }
  .section--screen .format { padding: var(--s-5); }
  .section--screen .price-card { padding: var(--s-5); }
  .section--screen .price-card__amount { margin: var(--s-4) 0; padding-top: var(--s-4); }
  .section--screen .price-card__amount b { font-size: 2rem; }
  .section--screen .pricing__note { margin-top: var(--s-5); }
}
/* The contact panel grew when the name field went in and when the error slots
   started holding their space, which pushed that section past one screen on a
   laptop. Tightened here rather than by giving anything back, since both of
   those earn their keep: the name is what tells Wero who wrote, and the
   reserved slots are what stop the form twitching as you type. */
@media (min-width: 941px) and (max-height: 900px) {
  .form-panel { padding: var(--s-5); }
  .form-panel .field { margin-bottom: var(--s-3); }
  .form-panel .field label { margin-bottom: var(--s-1); }
  .form-panel .textarea { min-height: 92px; }
  .form-panel .consent { margin-bottom: var(--s-3); }
  .form-panel .error--consent { margin-bottom: var(--s-3); }
}
@media (min-width: 941px) and (max-height: 760px) {
  .section--screen { padding-block: calc(var(--nav-h) + var(--s-3)) var(--s-5); }
  .section--screen .formats { margin-bottom: var(--s-4); }
  .section--screen .price-card { padding: var(--s-4) var(--s-5); }
}
.section--alt { background: var(--surface-alt); }
/* Bands alternate between the red family and the brown family so the page has
   some rhythm instead of one repeated tint. */
.section--warm { background: var(--cocoa-band); }
/* Every section on the page introduces itself the same way: an h2, sometimes
   a lead paragraph, then one fixed gap before the content. That is worth
   holding to now the bilingual labels are gone and each head is a bare
   heading — the two sections that carried their own one-off margins instead
   were the two that read as mistakes.
   The measure is 52rem rather than 40 because at 40 the pricing heading was
   forced onto two lines, and balancing those put the em dash at the start of
   the second — a heading opening with a dash reads like a stray list item.
   Widening it costs the prose nothing, since .lead caps itself far narrower. */
.section__head { max-width: 52rem; margin-bottom: var(--s-7); }
.section__head p { margin-top: var(--s-4); }
/* Where a head sits beside something with a hard top edge — a picture, the
   form panel — its line box puts a few pixels of leading above the cap, so
   flush tops measure as a heading sitting low. Pulling it up by that leading
   levels them optically, which is what the eye actually reads. Only while
   they are genuinely side by side: stacked, this would just put one heading
   nearer the top of its band than all the others. */
@media (min-width: 941px) {
  .contact__intro .section__head h2 { margin-top: -0.13em; }
}

/* Ceiling is 3.7rem, not 4.1rem, and the number is measured rather than
   chosen: "Lekcje angielskiego" needs 551px at 4.1rem, while the hero's text
   column is only 523-540px across desktop widths — so the first line wrapped
   and the heading ran to three lines instead of the two it is written as.
   (The column gets NARROWER as the viewport grows, because the grid gap is a
   vw clamp, so 1920 is the tightest case, not 1280.) At 3.7rem the line needs
   ~497px and clears the narrowest column by ~26px. */
h1 { font-size: clamp(2.5rem, 5.4vw, 3.7rem); line-height: 1.06; }
h2 { font-size: clamp(1.9rem, 3.7vw, 2.7rem); }
h3 { font-size: 1.1875rem; letter-spacing: -0.008em; }

.lead {
  font-size: clamp(1.0625rem, 1.5vw, 1.1875rem);
  color: var(--ink-soft);
  max-width: 35rem;
  line-height: 1.72;
}
/* Her intro runs to two paragraphs. Without this they close up into one
   block and the break she wrote disappears. */
.lead + .lead { margin-top: var(--s-4); }
/* The last spacing helper went with the testimonials block on 2026-08-20.
   .mt-4 had exactly one user, inside that commented-out section, so removing
   the section left it dead; there are now no utility classes at all and the
   markup carries no inline style attributes, which is what keeps an injected
   style from ever rendering under the Content-Security-Policy. */

/* The bilingual dictionary-entry section labels ("ABOUT ME — o mnie") were
   removed on 2026-08-13 at Wero's request. Section headings now carry the
   sections on their own. */

/* ------------------------------------------------- Signature: marker -- */
/* A hand-drawn highlighter stroke, not a flat rectangle: the edges are
   uneven and the ends are slightly angled, the way a real marker lands. */

.mark {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 44' preserveAspectRatio='none'%3E%3Cpath d='M2.5 36.5c34-3.4 70 2.1 104-1.2 40-3.9 78 3.6 118 .4 25-2 48 1.8 73-1.1L296 8.4c-30 2.6-58-1.6-88 1.1-42 3.8-82-3.2-124 .3-27 2.2-52-1.4-79 1.2z' fill='%23F5C63F'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 100% 0.66em;
  background-position: 0 82%;
  padding-inline: 0.1em;
}

/* --------------------------------------------------------------- Nav -- */

.nav {
  position: sticky; top: 0; z-index: 100;
  height: var(--nav-h);
  display: flex; align-items: center;
  background: rgba(253, 247, 245, 0.88);
  backdrop-filter: saturate(1.5) blur(12px);
  -webkit-backdrop-filter: saturate(1.5) blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color 220ms var(--ease), box-shadow 220ms var(--ease);
}
.nav.is-scrolled {
  border-bottom-color: var(--line);
  box-shadow: 0 1px 14px rgba(43, 26, 30, 0.06);
}
.nav__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4);
}

/* The lockup is a single piece of vector artwork carrying both the cherry
   mark and the name, so there is nothing to align by hand — and the mark in
   it is the exact same geometry as the standalone logo. */
.brand__lockup {
  height: 32px;
  width: auto;
  display: block;
}

/* Contains nothing but the lockup image, so it carries no type rules — the
   name used to be live text here before the vector lockup replaced it. */
.brand {
  display: inline-flex; align-items: center;
  min-width: 0;                     /* let it shrink instead of pushing the
                                       menu button off a narrow screen */
  text-decoration: none;
  padding-block: var(--s-3);        /* takes the tap area past 44px tall */
}

.nav__links { display: flex; align-items: center; gap: var(--s-6); }

/* :not(.btn) matters — without it these rules out-specify .btn--primary and
   repaint the CTA's white label in muted ink, making it unreadable on green. */
.nav__links a:not(.btn) {
  position: relative;
  text-decoration: none;
  font-size: 0.9375rem; font-weight: 500;
  color: var(--ink-soft);
  padding-block: var(--s-3);        /* 44px+ tap target, not just 41px */
  transition: color 180ms var(--ease);
}
.nav__links a:not(.btn)::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 2px; background: var(--marker);
  transform: scaleX(0); transform-origin: left;
  transition: transform 220ms var(--ease);
}
.nav__links a:not(.btn):hover { color: var(--ink); }
.nav__links a:not(.btn):hover::after,
.nav__links a:not(.btn)[aria-current="true"]::after { transform: scaleX(1); }
.nav__links a:not(.btn)[aria-current="true"] { color: var(--ink); }

.nav__toggle {
  display: none;
  width: 44px; height: 44px;
  align-items: center; justify-content: center;
  background: none; border: 1px solid var(--line);
  border-radius: 10px; cursor: pointer; color: var(--ink);
}

/* --------------------------------------------------------------- Btns -- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: 46px;
  padding: var(--s-3) var(--s-5);
  border: 1px solid transparent;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 0.9375rem; font-weight: 700;
  text-align: center;               /* flex centring alone does not centre a
                                       label that wraps onto a second line */
  text-decoration: none; cursor: pointer;
  transition: background-color 180ms var(--ease), color 180ms var(--ease),
              border-color 180ms var(--ease), transform 140ms var(--ease);
}
.btn:active { transform: translateY(1px); }

.btn--primary { background: var(--wine); color: #FFFFFF; }
.btn--primary:hover { background: var(--wine-deep); }

.btn--ghost { background: transparent; color: var(--ink); border-color: var(--line); }
.btn--ghost:hover { border-color: var(--ink); background: var(--surface); }

.btn--lg { min-height: 54px; padding: var(--s-4) var(--s-6); font-size: 1rem; }
.btn--block { width: 100%; }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; transform: none; }

/* --------------------------------------------------------------- Hero -- */
/* Doubles as "O mnie" — the brief has the About section acting as the hero. */

/* The hero owns the first screen. Without this the next section's tinted band
   showed as a strip under the fold, which reads as the page having started
   badly rather than as an invitation to scroll. Filling the viewport also
   means the content is centred in the space rather than pushed to the top.
   `svh` is the small viewport height — on a phone, `100vh` measures the
   screen with browser chrome hidden, so a `100vh` hero overflows by the
   height of the address bar the moment the page loads. */
.hero {
  padding-block: var(--s-8) var(--s-9);
  min-height: calc(100svh - var(--nav-h));
  display: flex;
  align-items: center;
  /* "O mnie" has to land at the very top of the document, not at the hero's
     own top edge — the nav sits above it in flow, so jumping to the edge
     scrolls 74px past the start and pulls the next section into view at the
     bottom. This margin gives the anchor the nav back. */
  scroll-margin-top: var(--nav-h);
}
.hero > * { width: 100%; }

/* The logo links to #top, an empty span at the very start of <main> — which
   begins one nav-height down the document. With scroll-padding-top at 0, the
   jump put that span at the top of the viewport and so left the page parked
   74px down: the headline sat tucked under the bar instead of the page being
   at its actual top. Giving the span the nav back lands it at scrollY 0. */
#top { scroll-margin-top: var(--nav-h); }

/* Short laptop screens: the whole hero has to be visible without scrolling,
   including the three facts under the intro, which were being cut off. A
   1366x768 laptop leaves roughly 660-680px of viewport once browser chrome
   is taken out, so the spacing tightens and the portrait is capped by
   viewport height rather than by its 4:5 ratio. Height-based, not width-based:
   the problem is short screens, and a wide 1920x700 window has it too. */
/* Tier boundaries raised from 850/745 to 1000/800 on 2026-08-18, when her
   Cambridge C2 paragraph made the hero ~75px taller. A 1440x900 window sat in
   the gap between the old tiers: too short for the full-spacing hero, too tall
   to qualify for any tightening, so the hero ran 49px past the fold and the
   three facts were cut off. Measured at 1000/900/800/745 after the change. */
@media (min-width: 941px) and (max-height: 1000px) {
  .hero { padding-block: var(--s-6) var(--s-7); }
  .hero__title { margin-bottom: var(--s-4); }
  .hero__actions { margin-top: var(--s-5); }
  .hero__meta { margin-top: var(--s-5); padding-top: var(--s-4); }
  .portrait { max-height: 62vh; }
}
/* Shorter still — a 1366x768 laptop lands near 680px of usable viewport once
   the browser's own chrome is taken out, and the three facts were the last
   thing to clear the fold. */
@media (min-width: 941px) and (max-height: 870px) {
  .hero { padding-block: var(--s-5) var(--s-6); }
  .hero__title { margin-bottom: var(--s-3); }
  .hero__actions { margin-top: var(--s-4); }
  .hero__meta { margin-top: var(--s-4); padding-top: var(--s-3); }
  .portrait { max-height: 56vh; }
  /* Her intro is the tallest block left in the column, so its leading is
     where the last 20px come from. 1.55 is still comfortable for reading;
     this is the only place on the site that departs from 1.72. */
  .hero .lead { line-height: 1.55; }
  .hero .lead + .lead { margin-top: var(--s-3); }
}
/* A third tier, added with the Cambridge C2 paragraph. On a 1366x768 laptop
   (~745px of usable viewport) the tier above bottoms out at 727px of hero and
   the last of the three facts was cut. The picture is the only thing here that
   can give height back without touching her words. */
@media (min-width: 941px) and (max-height: 810px) {
  /* Capping the picture does nothing here — measured, the hero's height is set
     by the TEXT column (671px against the picture's 430px), so the height has
     to come out of the type. The headline loses ~21px and the three paragraphs
     ~21px between them, which is the 33px needed and no more. */
  .hero__title { font-size: 3.05rem; }
  .hero .lead { line-height: 1.45; }
  .hero { padding-block: var(--s-4) var(--s-5); }
}
.hero__grid {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(var(--s-6), 5vw, var(--s-9));
  align-items: center;
}
.hero__title { margin-bottom: var(--s-5); }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-6); }

.hero__meta {
  display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-5);
  margin-top: var(--s-6); padding-top: var(--s-5);
  border-top: 1px solid var(--line);
  /* 14px, not the 15px used elsewhere for small type. Her 2026-08-19 revision
     made the middle fact "Dzieci, młodzież i dorośli" 54px wider, which pushed
     the third onto its own line at every desktop width and added 38px to a
     hero that is tuned to clear the fold on a short laptop. One step down in
     size buys 33px across the three and puts them back on one line from
     1100px up — the same band that fitted before her change. It also reads
     correctly: this strip is secondary to the two buttons above it.
     The mobile rule re-declares 0.9375rem, so a phone is unaffected. */
  font-size: 0.875rem; color: var(--ink-soft);
}
.hero__meta li { display: inline-flex; align-items: center; gap: var(--s-2); }
.hero__meta svg { color: var(--cocoa); flex: none; }

/* Portrait frame — the real photo drops straight in here. */
.portrait {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow-md);
}
.portrait img { width: 100%; height: 100%; object-fit: cover; }
.portrait__placeholder {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--s-3);
  background: radial-gradient(115% 85% at 50% 0%, #FFFFFF 0%, #F3EAE4 58%, #E8DCD4 100%);
  color: var(--ink-faint); text-align: center; padding: var(--s-5);
}
.portrait__placeholder svg { color: #C2AB9E; }
.portrait__placeholder span { font-size: 0.875rem; font-weight: 500; }

/* --------------------------------------------------------- Media slots -- */
/* Reusable space for a photo or graphic. She asked for more room for images
   through the page — these hold that space now so the layout is already
   correct, and each one becomes a real picture by deleting the placeholder
   div and uncommenting the <img> above it. Nothing else has to change. */

.media {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}
/* 16:10 — a shape photographs actually come in. It is taller than the two
   short paragraphs beside it and that is fine, because the text is centred
   against it rather than pinned to its top. */
.media--wide   { aspect-ratio: 16 / 10; }
.media--square { aspect-ratio: 1 / 1; }
/* Absolutely positioned, exactly like the placeholder it replaces, so the
   picture fills its slot without ever contributing intrinsic height to it.
   Left in normal flow, a real photo's own height drove the layout instead:
   the contact slot is a flex item sized from the space left over beside the
   form, and dropping a 2:3 portrait into it stretched the whole grid row to
   the photo's natural height and blew the section open.
   Cropped from above centre, so a face stays in frame rather than the middle
   of the picture. */
.media img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 55%;
}

.media__placeholder {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--s-3);
  background: radial-gradient(120% 90% at 50% 0%, #FFFFFF 0%, #F6EEE8 55%, #EBE0D7 100%);
  color: var(--ink-faint); text-align: center; padding: var(--s-5);
}
.media__placeholder svg { color: #C2AB9E; }
.media__placeholder span { font-size: 0.875rem; font-weight: 500; }

/* Head on one side, picture on the other, sharing one top edge and one bottom
   edge exactly.
   This used to be centred instead, because the old two-line intro was roughly
   half the picture's height and no honest alignment existed — centring was the
   least-bad option. Her longer text (2026-08-18) removed the problem: measured
   across 1024-1680px the text column now runs 265-294px against the picture's
   269-300px, so the two genuinely are the same block. Stretching them is now
   the truthful alignment rather than a compromise, and it holds at every width
   because the picture takes its height from the text rather than from a cap.
   The 16:10 ratio and the 300px cap that used to do that job are therefore
   only needed once the grid stacks — see the single-column rule below. */
.split {
  display: grid;
  grid-template-columns: 1.08fr 0.92fr;
  gap: clamp(var(--s-6), 4vw, var(--s-7));
  align-items: stretch;
  /* s-8, not the s-6 this used to be. 32px was tighter than the site's own
     standard gap between a heading block and the grid under it (s-7, 48px),
     and this block is heavier than a plain heading because it carries a
     photograph — so the cards were reading as clustered against it rather
     than as the next thing down. */
  margin-bottom: var(--s-8);
}
.split .section__head { margin-bottom: 0; }
/* aspect-ratio has to be cleared, not just overridden: left set, it fights
   the stretched row height and wins, which puts the letterbox back. */
.split .media { aspect-ratio: auto; height: 100%; }

/* --------------------------------------------------------------- Grid -- */

.grid { display: grid; gap: var(--s-5); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
/* Four prices: 4 across, then 2 x 2, then stacked. Never 3 + 1, which would
   orphan one plan on its own line in the middle of a comparison. */
.grid--prices { grid-template-columns: repeat(4, 1fr); }
/* The rail's dots exist only where the rail does. main.js builds them on
   every screen, so they are hidden by default and switched on inside the
   phone breakpoint — that is what keeps the desktop pixel-identical. */
.rail-dots { display: none; }
/* Four across needs roughly 1070px of viewport before "130 zł / zajęcia" and
   "Umów lekcję próbną" stop fitting on one line inside a card. Below that it
   is 2 x 2 — which is why this sits above the 940px block rather than in it:
   between 941 and 1100 the row was four cramped columns with wrapped buttons
   and staggered prices. Never 3 + 1, which would orphan a card mid-row. */
@media (max-width: 1100px) {
  .grid--prices { grid-template-columns: repeat(2, 1fr); }
}

.card {
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: var(--s-6);
  transition: border-color 220ms var(--ease), box-shadow 220ms var(--ease);
}
.card:hover { border-color: var(--line); box-shadow: var(--shadow-sm); }
.card__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-bottom: var(--s-4);
  border-radius: 11px;
  background: var(--cocoa-wash); color: var(--cocoa);
}
.card h3 { margin-bottom: var(--s-2); }
.card p { color: var(--ink-soft); font-size: 0.9688rem; }

/* ------------------------------------------------------- How lessons -- */

.lessons__grid {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(var(--s-6), 5vw, var(--s-8));
  align-items: stretch;             /* the panel matches the list beside it */
}

.checklist { display: grid; gap: var(--s-5); }
.checklist li { display: flex; gap: var(--s-4); align-items: flex-start; }
.checklist svg { flex: none; margin-top: 3px; color: var(--cocoa); }
.checklist h3 { margin-bottom: var(--s-1); }
.checklist p { color: var(--ink-soft); font-size: 0.9688rem; }

.quote {
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: var(--cocoa);
  color: #FFFFFF;
  border-radius: var(--radius-lg);
  padding: clamp(var(--s-6), 4vw, var(--s-7));
}
.quote p {
  font-family: var(--font-display);
  font-size: clamp(1.375rem, 2.5vw, 1.75rem);
  line-height: 1.34;
}
/* No highlighter in here on purpose — the marker is the hero's device, and
   using it twice makes it decoration instead of a signature. */
/* ------------------------------------------------------------ Pricing -- */

/* ------------------------------------------------------- Lesson formats -- */
/* Was two small pill "bubbles", which she rightly called weak: they looked
   like tags rather than a real choice, and carried no information beyond
   their own labels. Now a proper pair of cards that actually answer the
   question a parent is asking — what does each option mean for us? */

.formats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s-5);
  margin-bottom: var(--s-7);
}

.format {
  display: flex; align-items: flex-start; gap: var(--s-5);
  padding: var(--s-6);
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  transition: border-color 220ms var(--ease), box-shadow 220ms var(--ease),
              transform 220ms var(--ease);
}
.format:hover {
  border-color: var(--line);
  box-shadow: var(--shadow-sm);
  transform: translateY(-2px);
}
.format__icon {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px;
  border-radius: 15px;
  background: var(--cocoa-wash); color: var(--cocoa);
}
.format h3 { margin-bottom: var(--s-2); }
.format p { color: var(--ink-soft); font-size: 0.9375rem; }
.format__meta {
  display: block; margin-top: var(--s-3);
  font-size: 0.8438rem; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--wine);
}

.price-card {
  display: flex; flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s-6);
}
/* The 25-minute trial is the cheapest way in, so it is the card that should
   catch the eye of someone who has not decided yet — filled, not outlined. */
.price-card--trial {
  position: relative;
  background: var(--wine);
  border-color: var(--wine);
  color: #FFFFFF;
}
.price-card--trial .price-card__desc { color: rgba(255, 255, 255, 0.82); }
.price-card--trial .price-card__amount {
  border-top-color: rgba(255, 255, 255, 0.22);
}
.price-card--trial .price-card__amount span { color: rgba(255, 255, 255, 0.72); }
/* On the filled red card the button inverts — white fill, red label — so it
   still reads as the same filled button as the other three. */
.price-card--trial .btn {
  background: #FFFFFF; color: var(--wine); border-color: #FFFFFF;
}
.price-card--trial .btn:hover {
  background: var(--paper); color: var(--wine-deep); border-color: var(--paper);
}

.price-card h3 { margin-bottom: var(--s-2); }
/* The description absorbs the slack rather than a fixed min-height reserving
   it: her four descriptions run from two lines to four, and a reserved height
   tuned for one of them left the four amounts stepping up and down across the
   row. Growing here pins amount and button to the bottom, so the numbers —
   the one thing anyone scans this row for — sit on a single line. */
.price-card__desc { flex: 1 1 auto; color: var(--ink-soft); font-size: 0.9375rem; }
/* Set at the same offset and weight as the footer and in-body link underlines,
   so it reads as this page's emphasis rather than a browser default rule. */
.price-card__desc u {
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.price-card__amount {
  display: flex; align-items: baseline; gap: var(--s-2);
  margin: var(--s-5) 0;
  padding-top: var(--s-5);
  border-top: 1px solid var(--line-soft);
}
/* All four amounts are set at the same size, so the row aligns by
   construction — there is no trick holding it together.

   It briefly had one: the surcharge was set smaller, which made its box
   shorter and dropped it out of line, and that was compensated with
   `line-height: 0` on the figure. A zero-height line box only behaves while
   the text stays on ONE line — at 1024 the cards got narrow enough for
   "130 zł" to break between figure and currency, and the two halves drew
   exactly on top of each other as an unreadable glyph pile. Equal type
   removes the whole problem; `nowrap` below is a guard, not a fix, because
   a price should never break between the figure and the currency. */
.price-card__amount b {
  font-family: var(--font-display);
  font-size: 2.4rem; font-weight: 600; letter-spacing: -0.02em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.price-card__amount span {
  color: var(--ink-faint); font-size: 0.9375rem;
  white-space: nowrap;
}
.price-card .btn {
  margin-top: auto;
  padding-inline: var(--s-4);       /* lets "Umów lekcję próbną" sit on one
                                       line, so all four buttons match */
}
/* The weekend surcharge gets no modifier at all: it is the same card as the
   two beside it, by Santi's call (2026-08-13). It had picked up a dashed
   outline, a transparent fill and a lighter figure to mark it a top-up, and
   the leading "+" already does that on its own. Only the trial is treated
   differently, and only because it is the filled card. */

.pricing__note { margin-top: var(--s-6); font-size: 0.9063rem; color: var(--ink-faint); }

/* ------------------------------------------------------------ Contact -- */

.contact__grid {
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  gap: clamp(var(--s-6), 5vw, var(--s-8));
  align-items: stretch;
}

.contact__intro { display: flex; flex-direction: column; }
/* The picture now sits after the form panel in the markup, because that is
   the order a phone wants and source order is the one mechanism that cannot
   go wrong. Above the phone breakpoint it belongs back under the intro, so
   both layouts place it explicitly rather than relying on where it happens
   to fall. Below 721px neither rule applies and source order stands. */
@media (min-width: 721px) {
  .contact__intro { order: 1; }
  #kontakt .media   { order: 2; }
  #kontakt .form-panel { order: 3; }
  /* The picture used to live inside .contact__intro and so began immediately
     under the heading with no gap at all. As a grid sibling it inherits the
     grid's row gap, which pushed it down 64px. Zero the row gap to put it
     back exactly where it was; the column gap between the two columns is set
     separately below and is unaffected. */
  .contact__grid { row-gap: 0; }
}
@media (min-width: 721px) and (max-width: 940px) {
  /* One column at these widths, so the row gap that was just zeroed is the
     only thing separating the picture from the form. Give it back on the
     form alone. */
  #kontakt .form-panel { margin-top: clamp(var(--s-6), 5vw, var(--s-8)); }
}
@media (min-width: 941px) {
  /* Two columns: the intro sits at the top of the left one, the picture
     takes the rest of it, and the form spans both rows on the right — which
     is what makes the picture's bottom edge finish level with the form's,
     the pairing the eye actually reads. */
  .contact__grid { grid-template-rows: auto 1fr; }
  .contact__intro   { grid-column: 1; grid-row: 1; }
  #kontakt .media   { grid-column: 1; grid-row: 2; align-self: stretch; }
  #kontakt .form-panel { grid-column: 2; grid-row: 1 / span 2; }
}
/* The picture takes whatever height is left over, so its bottom edge finishes
   exactly level with the bottom edge of the form panel beside it.
   It was briefly aligned to the send button instead, which left a visible
   gap below it against the panel edge and looked worse — the two bottom
   edges are what the eye pairs, so they are what must match.
   Only while the two are side by side: stacked, there is no form to line up
   with, and dropping the aspect ratio there turned a slot marked square into
   a 200px letterbox, so the same photo would have been cropped two different
   ways depending on the device. */
@media (min-width: 941px) {
  /* Selector was .contact__intro .media until the figure moved out to become
     a grid sibling; it fills its grid row now instead of a flex remainder,
     so the aspect ratio is dropped here exactly as before. */
  #kontakt .media { aspect-ratio: auto; min-height: 200px; }
}

.form-panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(var(--s-5), 3vw, var(--s-7));
  box-shadow: var(--shadow-sm);
}

.field { margin-bottom: var(--s-5); }
.field label { display: block; font-size: 0.9375rem; font-weight: 700; margin-bottom: var(--s-2); }

.input, .textarea {
  width: 100%;
  min-height: 48px;
  padding: var(--s-3) var(--s-4);
  font-family: var(--font-body);
  font-size: 1rem;                 /* 16px — stops iOS zooming in on focus */
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 10px;
  transition: border-color 160ms var(--ease), box-shadow 160ms var(--ease);
}
.textarea { min-height: 150px; resize: vertical; line-height: 1.6; }
/* Placeholders are still text: they have to clear 4.5:1 like everything else.
   They read as hints rather than values because the labels above are bold
   and the real value renders at 15:1, not because they are washed out. */
.input::placeholder, .textarea::placeholder { color: var(--ink-faint); opacity: 1; }
.input:focus, .textarea:focus {
  outline: none;
  border-color: var(--wine);
  box-shadow: 0 0 0 3px rgba(142, 14, 50, 0.14);
}
.input[aria-invalid="true"], .textarea[aria-invalid="true"] { border-color: var(--danger); }

/* Always in the layout, hidden with `visibility` rather than removed with
   `display`. Toggling display made every error message change the panel's
   height by 31px as it appeared, and the picture beside the form is sized
   from the space that panel leaves over — so it resized by 32px too, every
   time someone typed. Holding one line of space here keeps both still. */
.error {
  display: flex; align-items: flex-start; gap: var(--s-2);
  margin-top: var(--s-2);
  /* Slightly more than one rendered line of this text plus the icon's 3px
     nudge — measured, not guessed. Under-reserving left a 3px twitch. */
  min-height: 1.5rem;
  font-size: 0.875rem; color: var(--danger);
  visibility: hidden;
}
.error.is-visible { visibility: visible; }
.error svg { flex: none; margin-top: 3px; }

/* RODO consent */
.consent { display: flex; gap: var(--s-3); align-items: flex-start; margin-bottom: var(--s-5); }
/* Drawn rather than left as the browser default: everything else in this
   panel is rounded and warm, and a stock square checkbox was the one piece
   of raw operating-system UI on the page. Behaviour is untouched — it is
   still a real checkbox, still keyboard-operable, still 24px (WCAG 2.5.8),
   and the whole label is clickable so the real target is far bigger. */
.consent input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  flex: none; width: 24px; height: 24px; margin: 1px 0 0;
  background: var(--surface);
  border: 1.5px solid var(--ink-faint);
  border-radius: 7px;
  cursor: pointer;
  display: grid; place-content: center;
  transition: background-color 140ms var(--ease), border-color 140ms var(--ease);
}
.consent input[type="checkbox"]::before {
  content: "";
  width: 13px; height: 13px;
  clip-path: polygon(14% 44%, 0 60%, 39% 100%, 100% 18%, 84% 4%, 37% 70%);
  background: #FFFFFF;
  transform: scale(0);
  transition: transform 140ms var(--ease);
}
.consent input[type="checkbox"]:hover { border-color: var(--wine); }
.consent input[type="checkbox"]:checked {
  background: var(--wine); border-color: var(--wine);
}
.consent input[type="checkbox"]:checked::before { transform: scale(1); }
.consent input[type="checkbox"][aria-invalid="true"] { border-color: var(--danger); }
.consent label { font-size: 0.875rem; line-height: 1.55; color: var(--ink-soft); cursor: pointer; }
.consent a { color: var(--wine); text-decoration: underline; text-underline-offset: 2px; }
/* the consent error sits below the whole row, not indented under the label */
.error--consent { margin: calc(-1 * var(--s-3)) 0 var(--s-5); }
/* a whole-form error (a failed send) sits directly above the button */
/* This one collapses instead of holding space. It only appears when a send
   actually fails, which is not something that happens while someone types —
   so it costs nothing to keep it out of the layout until it is needed, and
   holding 40px of permanent blank for it pushed the section past a screen. */
.error--form { display: none; margin: 0 0 var(--s-4); }
.error--form.is-visible { display: flex; }

/* Success replaces the form in place — no redirect, as the brief asks. */
.form-success {
  display: none;
  flex-direction: column; align-items: center; justify-content: center;
  text-align: center; min-height: 330px; padding: var(--s-6);
}
.form-panel.is-sent .form-panel__form { display: none; }
/* The panel is frozen at the height it had as a form, so the thank-you has to
   stretch into that whole height for its own centring to mean anything —
   otherwise it sits at its natural 330px against the top and leaves the rest
   of the panel empty below it. */
.form-panel.is-sent { display: flex; }
.form-panel.is-sent .form-success { display: flex; flex: 1; min-height: 0; }
.form-success__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 60px; height: 60px; margin-bottom: var(--s-5);
  border-radius: 999px; background: var(--cocoa-wash); color: var(--wine);
}
.form-success h3 { font-size: 1.5rem; margin-bottom: var(--s-3); }
/* Focus is moved here on send so a screen reader announces the confirmation
   instead of leaving the visitor on a button that vanished. It carries
   tabindex="-1" and cannot be tabbed to, so the ring has no navigational
   meaning — on screen it just reads as a box drawn around the heading. The
   focus itself stays; only its outline goes. */
.form-success h3:focus,
.form-success h3:focus-visible { outline: none; }
.form-success p { color: var(--ink-soft); max-width: 26rem; }

/* -------------------------------------------------------------- Footer -- */

.footer {
  background: var(--cocoa);
  color: rgba(255, 255, 255, 0.82);
  padding-block: var(--s-7);
  font-size: 0.9063rem;
}
/* Three columns rather than space-between across three flex items. With
   space-between the middle group lands wherever the two outer widths leave
   it — measured 20px right of the page's centre line, which reads as a near
   miss rather than a decision. An equal 1fr either side centres it on the
   page exactly, and the outer two stay locked to the content edges. */
.footer__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: var(--s-4) var(--s-6);
  align-items: center;
}
.footer__inner > :last-child { justify-self: end; }
.footer a { color: #FFFFFF; text-decoration: none; }
.footer a:hover { text-decoration: underline; text-underline-offset: 3px; }
/* Same file as the header, turned white in CSS. A single-colour mark inverts
   exactly, so this ships one lockup instead of two near-identical downloads. */
/* The cherry sits above the wordmark inside this lockup, so centring the IMAGE
   box puts the wordmark's baseline below the baseline of the two text items
   beside it — measured at 4px on a 26px lockup, which is visible. Nudged up
   to put all three on one baseline. Re-measure if the logo height changes:
   it is the gap between the wordmark's baseline and the box's centre. */
.footer__logo {
  height: 26px; width: auto; display: block;
  transform: translateY(-4px);
  filter: brightness(0) invert(1);
}
.footer__links { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-5); }
/* standalone links, so they get a real 44px tap area — not an inline
   exception like the one inside the consent sentence */
.footer__links a { display: inline-flex; align-items: center; min-height: 44px; }

/* ---------------------------------------------------------- Legal page -- */

/* The article keeps the site's own container, and the measure is capped on
   the text inside it. Capping the container instead re-centred the whole
   column ~220px inboard, so the heading of this page lined up with nothing —
   not the lockup above it, not the lockup below it. */
.legal { padding-block: var(--s-8) var(--s-9); }
.legal > * { max-width: 44rem; }
.legal h1 { font-size: clamp(2rem, 4vw, 2.75rem); margin-bottom: var(--s-3); }
.legal h2 { font-size: 1.3125rem; margin-top: var(--s-7); margin-bottom: var(--s-3); }
.legal p, .legal li { color: var(--ink-soft); }
.legal p + p { margin-top: var(--s-4); }
/* A list followed by more prose needs the gap too — `p + p` alone left the
   paragraph after each list touching the final bullet, so it read as part of
   the list. */
.legal ul { margin-top: var(--s-3); display: grid; gap: var(--s-2); }
.legal ul + p { margin-top: var(--s-4); }
.legal li { padding-left: var(--s-5); position: relative; }
.legal li::before {
  content: ""; position: absolute; left: 0; top: 0.68em;
  width: 6px; height: 6px; border-radius: 999px; background: var(--cocoa);
}
.legal__updated { font-size: 0.9063rem; color: var(--ink-faint); margin-bottom: var(--s-6); }
.legal a { color: var(--wine); }

/* ------------------------------------------------------------ Reveal -- */

.reveal { opacity: 0; transform: translateY(14px); }
.reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity 560ms var(--ease), transform 560ms var(--ease);
}

/* -------------------------------------------------------- Responsive -- */

@media (max-width: 940px) {
  .hero__grid, .lessons__grid, .contact__grid, .split { grid-template-columns: 1fr; }
  .hero__grid { gap: var(--s-7); }
  .portrait { max-width: 420px; }
  /* Stacked, there is no text column to take a height from, so the picture
     needs its own shape back — 16:10, which is what it is cropped to. */
  .split .media { aspect-ratio: 16 / 10; height: auto; max-width: 540px; }
  .grid--3 { grid-template-columns: repeat(2, 1fr); }
  /* Three items in a two-column grid leave the third stranded beside a hole.
     Letting it run the full width fills the row and looks like a decision. */
  .grid--3 > :last-child:nth-child(3) { grid-column: 1 / -1; }
  .grid--prices { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 720px) {
  /* ══════════════════════════════════════════════════════════════════════
     THE PHONE LAYOUT — rebuilt from zero, 2026-08-18.

     What was here before was a compression pass. Every size on the site had
     been pushed down until three sections fitted one screen: the headline
     dropped from 3.7rem to 2rem, the cards were rebuilt as cramped rows, the
     four price cards were squeezed into a 2 x 2 grid of 170px boxes with
     their buttons deleted, and every picture was capped by height. The page
     read as a desktop layout squeezed sideways, which is exactly what Santi
     said when he saw it on his phone.

     This one works the other way round. The type and the spacing come back
     UP toward the desktop's proportions — same components, same voice — and
     the three sections that have to fit one screen are solved by changing
     the ORDER and the SHAPE of what is in them rather than by shrinking it:

       hero     her portrait and the two buttons move above her intro, so
                the first screen is headline, portrait, both actions. The
                intro and the three facts follow below, at full size.
       oferta   the four price cards become one horizontally swipeable rail
                instead of a 2 x 2 grid. Each card gets its whole desktop
                treatment back, buttons included, because only one is in
                view at a time.
       kontakt  the picture goes and the form is the section.

     Everything else — the two middle sections and the footer — is given
     MORE room than it had, not less.
     ══════════════════════════════════════════════════════════════════ */

  /* --- The phone's own scale ------------------------------------------- */
  /* Spacing sits between the old phone values and the desktop's: seams are
     48px rather than 32px, which is what stops the page feeling squeezed,
     without spending a whole desktop 96px band on a 390px screen.
     1rem stays the floor for anything the user types into — iOS zooms the
     viewport on focus for any input under 16px, which throws the layout out.
     See .form-panel below: its fields deliberately do not shrink. */
  :root {
    --s-9: 4.5rem;   --s-8: 3rem;      --s-7: 2.5rem;
    --s-6: 1.75rem;  --s-5: 1.25rem;   --s-4: 1rem;
  }

  /* The headline ceiling is measured, not chosen. "Lekcje angielskiego"
     needs about 8.4px of width for every 1px of font-size, so 2.375rem
     (38px) asks for ~319px against a 350px column and clears it by 31px.
     2.5rem does not. Everything else is pitched to keep the serif's weight
     in the page rather than flattening it to app-sized headings. */
  body { font-size: 1rem; line-height: 1.65; }
  h1 { font-size: 2.375rem; line-height: 1.06; }
  h2 { font-size: 1.75rem; }
  h3 { font-size: 1.125rem; }
  .lead { font-size: 1rem; line-height: 1.65; }

  /* --- Nav ------------------------------------------------------------- */
  /* The drawer used to be display:none -> display:flex, and `display` cannot
     be transitioned, so it appeared and vanished in a single frame — the
     harshest thing on the site on a phone, on a page where everything else
     eases. visibility instead of display keeps it out of the tab order and
     untappable while closed, and animates.
     visibility is delayed by the fade on the way OUT (0s linear 200ms) so the
     drawer stays visible long enough to fade, and is instant on the way IN. */
  .nav__links {
    display: flex;
    position: fixed; inset: var(--nav-h) 0 auto 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    padding: var(--s-3) var(--s-5) var(--s-5);
    box-shadow: var(--shadow-md);
    visibility: hidden; opacity: 0; transform: translateY(-10px);
    transition: opacity 200ms var(--ease), transform 200ms var(--ease),
                visibility 0s linear 200ms;
  }
  .nav__links.is-open {
    visibility: visible; opacity: 1; transform: none;
    transition: opacity 220ms var(--ease), transform 220ms var(--ease),
                visibility 0s;
  }
  .nav__links a:not(.btn) { padding-block: var(--s-4); font-size: 1.0625rem; }
  .nav__links a:not(.btn)::after { display: none; }
  /* The enquiry button belongs in the drawer too — it is the whole point of
     the site, and hiding it on phones left the menu with three grey links
     and no way to act on any of them. */
  .nav__links .btn { margin-top: var(--s-4); }

  .nav__toggle { display: inline-flex; }
  .brand__lockup { height: 27px; }

  /* The privacy page has two links and no hamburger, so it opts out of the
     drawer entirely and keeps its row on screen. The "Strona główna" link
     goes — the logo beside it already does that job — leaving the one thing
     someone reading the consent text actually wants: the way back to the
     form. */
  .nav__links--simple {
    display: flex; position: static; flex-direction: row;
    background: none; border: 0; box-shadow: none; padding: 0;
    gap: var(--s-4);
    /* This page has no hamburger to open the drawer, so it has to opt out of
       the hidden-by-default state above or its links vanish entirely. */
    visibility: visible; opacity: 1; transform: none; transition: none;
  }
  .nav__links--simple .nav__home { display: none; }
  .nav__links--simple .btn {
    margin-top: 0;
    padding-inline: var(--s-4);
    font-size: 0.875rem;
    white-space: nowrap;            /* two lumpy lines inside a pill is worse
                                       than a slightly smaller pill */
  }

  /* --- Hero: headline, her face, both buttons — then the words ---------- */
  /* The reorder is the whole fix. Read down the markup the hero is headline,
     three paragraphs, two buttons, three facts — which puts the buttons
     roughly 850px down a 390px phone, below the fold, on a page whose only
     job is getting someone to write to her.

     display:contents on BOTH wrappers dissolves them, so every piece of the
     hero becomes a flex item of .hero__grid in its own right and `order` can
     place them freely. Desktop never enters this block, so its two-column
     layout is untouched.

     Reading order on screen becomes: title, portrait, actions, intro, facts.
     The DOM order is unchanged, so the tab order still runs title -> intro ->
     buttons and the two buttons are the only focusable things here, both on
     screen together. */
  .hero { padding-block: var(--s-5) var(--s-7); }
  .hero__grid { display: flex; flex-direction: column; gap: 0; }
  .hero__col, .hero__body { display: contents; }

  .hero__title { order: 1; margin-bottom: 0; }
  /* 300px, not the 200px this had become. At 200 in a 350px column the
     portrait read as a thumbnail floating in white space; at 300 it is the
     block it is on desktop, centred, with an even 25px margin either side.
     4:5 is the crop the file already is, so the slot never re-crops it. */
  .hero .portrait { order: 2; max-width: 300px; margin: var(--s-6) auto 0; }
  /* Two full-width stacked buttons cost 123px for two lines of text. Side by
     side they cost 54px and still clear the 44px touch target with room. */
  .hero__actions {
    order: 3; margin-top: var(--s-6);
    flex-wrap: nowrap; gap: var(--s-3);
  }
  .hero__actions .btn { flex: 1 1 0; padding-inline: var(--s-4); white-space: nowrap; }
  .hero .lead { order: 4; }
  .hero .lead:first-of-type { margin-top: var(--s-6); }
  .hero .lead + .lead { margin-top: var(--s-4); }
  .hero__meta {
    order: 5;
    margin-top: var(--s-6); padding-top: var(--s-5);
    gap: var(--s-3) var(--s-5); font-size: 0.9375rem;
  }

  /* --- Sections: air back ---------------------------------------------- */
  .section { padding-block: var(--s-8); }
  .section__head { margin-bottom: var(--s-6); }
  .card { padding: var(--s-6); }

  /* --- "Od zadań domowych po maturę" ------------------------------------ */
  /* The picture leads the section. Two reasons, and the second is the one
     that matters: it is the last line of the head — "Najczęściej pomagam
     w:" — that introduces the three cards, so anything sitting between the
     two breaks the sentence away from what it points at. Above the heading,
     the photograph opens the band instead and the line lands where it
     belongs, directly on the cards.

     It is sized by its ratio and nothing else. It used to carry
     `max-height: 165px`, and because the figure owns its aspect ratio a
     height cap shrinks the WIDTH with it: the picture rendered 264px wide
     inside a 354px column with a quarter of the column dead beside it, which
     is why it read as broken rather than as small. Full column width at the
     16:10 the file is actually cropped to. */
  .split {
    display: flex; flex-direction: column;
    gap: var(--s-6); margin-bottom: var(--s-7);
  }
  .split .media {
    order: -1;
    aspect-ratio: 16 / 10; height: auto; max-width: none;
  }
  .split .section__head { margin-bottom: 0; }

  /* One column, and the desktop card comes back whole — icon above the
     heading, full padding. These had been rebuilt as icon-left rows at
     0.9375rem to save height; the section is allowed the height now. */
  .grid--3 { grid-template-columns: 1fr; gap: var(--s-5); }

  /* --- "Jak się uczymy" ------------------------------------------------- */
  .lessons__grid { gap: var(--s-7); }
  .checklist { gap: var(--s-5); }
  .quote { padding: var(--s-6); }

  /* --- The three sections a nav link points at -------------------------- */
  /* align-items goes to flex-start: centred content in a min-height box
     overflows in BOTH directions once it is taller than the box, and the
     overflow above the top edge cannot be scrolled to. Starting at the top
     is also what makes a nav jump land flush.
     min-height moves from svh to lvh. svh is the viewport with the browser's
     chrome fully SHOWN, so a section sized to it comes up short the moment
     Safari's address bar collapses on scroll — and a strip of the next
     section's band appears under it, which is the one thing these sections
     must never do. lvh is the tall measurement, so the band always covers
     the screen whichever state the chrome is in. Browsers without lvh keep
     the svh value above and behave as before. */
  .section--screen {
    min-height: 100lvh;
    align-items: flex-start;
    padding-block: calc(var(--nav-h) + var(--s-4)) var(--s-7);
  }
  .section--screen .section__head { margin-bottom: var(--s-4); }

  /* --- Oferta: two formats, then a rail of four prices ------------------ */
  /* The formats pair up. One short sentence each does not need a full-width
     card, and stacked they cost 292px of the offer's one screen. */
  .formats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-4); margin-bottom: var(--s-4);
  }
  .format { flex-direction: column; gap: var(--s-2); padding: var(--s-4); }
  .format__icon { width: 40px; height: 40px; border-radius: 12px; }
  .format h3 { font-size: 1.0625rem; }
  .format p { font-size: 0.875rem; line-height: 1.45; }
  .format__meta { margin-top: var(--s-1); font-size: 0.75rem; letter-spacing: 0.04em; }

  /* Four cards across a 350px column is 170px each: "Zajęcia jednorazowe"
     wraps to two lines, the amounts crowd their units, and the buttons had
     to be deleted outright. A rail gives every card its full width back —
     and its button back with it, because only one card is in view at a time,
     so a call to action costs 54px once rather than four times.

     78% is deliberate: the following card shows about 60px past the edge,
     which is the affordance. There is no arrow, no dot row and no caption,
     because the peek says it on its own and a caption would need new Polish.
     It is a native scroll container, so it keeps its keyboard and
     screen-reader behaviour and honours reduced-motion for free.

     The negative margin bleeds the rail to the screen edges so cards scroll
     under them; the matching padding puts the first and last card back on
     the page's own gutter, and scroll-padding lands every snap there too. */
  .grid--prices {
    display: flex;
    grid-template-columns: none;
    gap: var(--s-4);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--s-5);
    margin-inline: calc(-1 * var(--s-5));
    padding: 2px var(--s-5) var(--s-3);
    scrollbar-width: none;
  }
  .grid--prices::-webkit-scrollbar { width: 0; height: 0; }
  .grid--prices .price-card {
    flex: 0 0 78%;
    scroll-snap-align: start;
    padding: var(--s-5);
  }
  /* The rail scrolls sideways, so the third and fourth cards sit outside the
     viewport and the reveal observer never intersects them — measured at
     360px, both stayed at opacity 0 until the safety sweep happened to run
     while the section was on screen, which is a coin flip. An invisible price
     is a bug; a missing 560ms fade is not, so on a phone the row simply
     arrives with the section instead of card by card. */
  .grid--prices .price-card.reveal { opacity: 1; transform: none; }

  /* Dots under the rail, so it is obvious there are four cards and which one
     you are on — a horizontal rail gives no other clue how much is left.
     Built by main.js rather than written into the markup, which keeps
     index.html byte-identical and means the dots simply never appear if
     scripting is off, where they would be a lie anyway.

     Deliberately not buttons: a tappable dot wants a 44px touch target,
     which would either make the row far too heavy or ship a target too
     small to pass. The rail is already swipeable and keyboard-scrollable,
     so these are an indicator and are hidden from screen readers. */
  .rail-dots {
    display: flex;
    justify-content: center;
    gap: var(--s-2);
    margin-top: var(--s-4);
  }
  .rail-dots span {
    width: 7px; height: 7px;
    border-radius: 999px;
    background: var(--ink-faint);
    opacity: 0.3;
    transition: opacity 200ms var(--ease), width 200ms var(--ease),
                background-color 200ms var(--ease);
  }
  .rail-dots span[data-current] {
    width: 20px;
    opacity: 1;
    background: var(--wine);
  }
  .price-card__desc { font-size: 0.875rem; line-height: 1.45; }
  .price-card__amount { margin: var(--s-3) 0 var(--s-4); padding-top: var(--s-4); }
  .price-card__amount b { font-size: 2.25rem; }
  .pricing__note { margin-top: var(--s-5); font-size: 0.875rem; line-height: 1.5; }

  /* --- Kontakt: the form leads, the picture closes ---------------------- */
  /* Santi asked for the picture back (2026-08-18), below the form rather
     than above it. That keeps the reasoning that removed it — the form is
     what the section is for, so it gets the first screen — while giving the
     section a proper ending instead of stopping dead at a button.

     The figure is a direct child of .contact__grid in the markup, so this
     needs nothing but source order. The first attempt gave .contact__intro
     display: contents to promote the figure into a grid item and ordered it
     third; that rendered correctly in Chrome at every size and the picture
     came out MISSING on a real iPhone. The same .media pattern — a figure
     sized by aspect-ratio holding an absolutely positioned image — renders
     fine elsewhere on the same page on the same device, which leaves
     display: contents as the one differing ingredient. Moving the element is
     cheaper than defending the technique.

     4:3 rather than the desktop's near-portrait slot or the old 16:9. The
     source is a 2:3 portrait, so any landscape crop cuts it; 4:3 is the
     shallowest cut that still reads as a photograph on a phone rather than
     as a letterbox, and object-position keeps the crop high. */
  /* height: auto and align-self: start are the load-bearing half of this
     rule, not tidiness. .contact__grid sets align-items: stretch, which
     gives every item a definite height — and a box with a definite height
     ignores aspect-ratio. The figure's only child is absolutely positioned,
     so its own content height is 0, and it collapses to nothing. Chrome
     resolves the conflict in aspect-ratio's favour and WebKit does not, so
     the picture rendered here and was MISSING on Santi's iPhone, twice.
     The .split picture on the same page never had the bug because its
     mobile rule already carries height: auto — that difference is what
     identified the cause. */
  #kontakt .media {
    aspect-ratio: 4 / 3;
    height: auto;
    align-self: start;
  }
  .contact__grid { gap: var(--s-6); }
  .form-panel { padding: var(--s-5); }
  .field { margin-bottom: var(--s-4); }
  .textarea { min-height: 108px; }
  .consent { margin-bottom: var(--s-4); }
  .error--consent { margin: calc(-1 * var(--s-2)) 0 var(--s-4); }

  /* --- Footer ----------------------------------------------------------- */
  /* One column on a phone, everything left-aligned against the page edge. */
  .footer__inner { grid-template-columns: 1fr; justify-items: start; }
  .footer__inner > :last-child { justify-self: start; }
}

@media (max-width: 380px) {
  /* Smallest phones: shrink the lockup so it and the menu button always fit
     on one line, and give the headline back the width it needs — 2.375rem
     wants ~319px and a 360px screen only leaves 312px of column. */
  .brand__lockup { height: 23px; }
  h1 { font-size: 2.125rem; }
  .hero .portrait { max-width: 275px; }
  /* A wider card on a narrower screen, not a narrower one: the descriptions
     are what set the card's height, so more width means fewer lines. 88% of
     a 320px column still leaves ~38px of the next card showing. */
  .grid--prices .price-card { flex: 0 0 88%; }
}

/* ----------------------------------------------- Page-to-page motion -- */
/* The jump between the front page and the privacy policy used to be the one
   hard cut on the site. Two earlier attempts to soften it both made the site
   feel slower than the cut did, and the shape of this one is a reaction to
   each of them:

   1. @view-transition. The browser abandons a cross-document view transition
      whenever the incoming page cannot reach first render in time - measured
      10/10 warm, 2/6 on a cold cache over a slow connection. Firefox does not
      implement it at all.

   2. Fading the outgoing page to opacity 0 over 200ms while a timer in JS held
      the navigation back. Measured 2026-08-18: that put 200ms of dead waiting
      in front of a round trip that had not started, and then left the screen
      EMPTY for 1.1s (returning visitor, slow connection) to 1.75s (cold) while
      the next page loaded. Blank is not a transition.

   So the outgoing page now DIMS and stays readable, on a curve slow enough
   that it never reaches a state that looks like a blank page, and the
   navigation is native and immediate (main.js prevents nothing). The browser
   keeps showing this page until the next one can paint, so the dim is
   overlapped with the network instead of being charged on top of it.

   Only main and the footer move - the header is identical on both pages and
   holds still, which keeps the eye anchored. Deliberately NOT on <body>: a
   transform on body would become the containing block for the fixed mobile
   nav drawer and break it. */
@keyframes ww-page-in {
  from { opacity: 0; transform: translateY(6px); }
}
main, .footer {
  /* 220ms, not the 420ms this started at. The incoming content is held below
     full opacity for the whole run, so the duration is charged directly to how
     soon the page can be read - it was the single largest thing still standing
     between the click and the words. `backwards` rather than `both`: with
     `both` the animation keeps filling forever after it ends, which leaves
     main and the footer promoted to their own compositing layers for the life
     of the page for no reason. */
  animation: ww-page-in 220ms var(--ease) backwards;
}
/* .is-leaving is set by the click handler as the browser navigates, so the
   outgoing page acknowledges the tap. Opacity only, and only part of the way:
   a page that fades to nothing and then waits looks broken, and a translate on
   the way out is motion the reader never finishes watching. */
body.is-leaving main,
body.is-leaving .footer {
  opacity: 0.55;
  transition: opacity 260ms var(--ease);
  animation: none;
}

/* --------------------------------------------------- Reduced motion -- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* main.js checks this same query and skips the page transition entirely. */
  main, .footer { animation: none; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  .reveal { opacity: 1; transform: none; }
}

@media print {
  .nav, .footer, .btn { display: none; }
  body { background: #FFFFFF; }
}
