/*
 * keholuonto.fi — the app's own theme, in a stylesheet.
 *
 * Every value in the token block below is copied from `src/theme/theme.ts` and
 * `src/design/skin.ts`. Nothing generates this file: a theme change in the app
 * has to be carried across by hand, exactly as `web/app/tyyli.css` already says
 * of the two policy pages. The two sheets are deliberately separate — those
 * pages open in a sheet *over* the app and are read against the screen that
 * opened them, so they are the app's furniture; this is a website, read on a
 * laptop by somebody who may never have seen the app at all.
 *
 * ONE DELIBERATE DEPARTURE, and it is `--page-title`. The app's ladder is anchored
 * at body copy and tops out at `typography.title` — 32 — which is right on a
 * phone, where a title runs the width of the screen. On a 1440-point display a
 * 32px hero reads as a phone screenshot pasted into a page. So the page title
 * alone is a `clamp()` from that 32 up to 46, and *nothing else on the site
 * moves*: body stays 16, caption stays 14, a card title stays 19. The ladder is
 * the app's; the top rung is allowed the room a browser window has and a phone
 * does not.
 */

/* --------------------------------------------------------------------------
   The faces
   --------------------------------------------------------------------------
   Cut by `tools/make-web-fonts.py` out of `assets/fonts/` and the npm package,
   subset to what a Finnish page sets. Fraunces here is the SOFT 100 / WONK 1
   instance the app draws — `assets/fonts/README.md` is emphatic that the one on
   npm is a different typeface, and that holds on the web too.

   `swap` on every one of them. `src/design/fonts.ts` makes the same bargain in
   the app: until the faces arrive the words are drawn in the platform's own
   type, because a meditation site that showed a blank where its writing goes
   while a serif decompressed would be a worse site than one in Georgia.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Fraunces';
  src: url('/fonts/Fraunces-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Fraunces';
  src: url('/fonts/Fraunces-500.woff2') format('woff2');
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: 'Fraunces';
  src: url('/fonts/Fraunces-600.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}

/* Its own family because it is its own optical size — 144 against the text
   cuts' 60. `design/Wordmark.tsx` is the rule: nothing but the name is set in
   it, and the moment anything else is, the name is one of the labels again. */
@font-face {
  font-family: 'FrauncesWordmark';
  src: url('/fonts/FrauncesWordmark-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: 'SchibstedGrotesk';
  src: url('/fonts/SchibstedGrotesk-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'SchibstedGrotesk';
  src: url('/fonts/SchibstedGrotesk-500.woff2') format('woff2');
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: 'SchibstedGrotesk';
  src: url('/fonts/SchibstedGrotesk-600.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: 'SchibstedGrotesk';
  src: url('/fonts/SchibstedGrotesk-700.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}

/* Subset to the two dozen names `site/src/icons.json` keeps. An icon row whose
   name is not in it loses its badge and keeps its text — the rule the app
   follows for a glyph its own build has never heard of. */
@font-face {
  font-family: 'MaterialDesignIcons';
  src: url('/fonts/MaterialDesignIcons.woff2') format('woff2');
  font-weight: 400;
  font-display: block;
}

/* --------------------------------------------------------------------------
   The palette and the ladder — `src/theme/theme.ts`, copied
   -------------------------------------------------------------------------- */

:root {
  color-scheme: light dark;

  /* lightTheme.colors */
  --mist: #f5f8f2;
  --mist-deep: #ecf0e5;
  --lake: #3f5a63;
  --pine: #173507;
  --moss: #718c4b;
  --bark: #765f4e;
  --sand: #e2d5c3;
  --text-primary: #222917;
  --text-secondary: #454e3b;
  --card: #fbfcf8;
  --card-elevated: #ffffff;
  --border: #d5dccb;
  --border-strong: #b8c2a8;
  --accent-soft: #ecf1e4;
  --control-surface: #eef2e8;
  --primary-control-text: #f9fbf4;
  --image-placeholder: #e4e9dd;
  --warning: #8f5f40;
  --overlay: rgba(23, 53, 7, 0.09);
  /* Text over a published photograph, where the page palette does not apply.
     White in both schemes — the ground under it is a picture, not the page. */
  --on-image: #fafcf6;

  /* The mark's own light.
     -------------------------------------------------------------------------
     `assets/icon.png` is one idea — a warm glow on a `pine` ground — and both
     of these are sampled along its own radius. `--glow-soft` is the core of
     it exactly; `--glow` is one step out, where the light has taken enough
     colour to read as warm without turning gold. They are deliberately near
     the pale end of that gradient: further out it becomes butter and then
     amber, which is a different, heavier idea than light through a canopy.
     Nothing else in the palette is warm and light at once — `sand` is warm
     and dull, `moss` is light and green, and the app has no yellow at all.

     **It belongs on a photograph and nowhere else.** Against the page's own
     `mist` it is a 1.05:1 fill — invisible as a shape — so it can never be a
     button on the pale ground without a border doing all the work. Over a
     photograph it is nearly 16:1 against the shade it is read in — the card's
     scrim, or on a hero the four-layer shadow that replaced one — within a
     whisker of the white it replaces, so it costs no legibility at all where
     it is actually used.
     Both schemes get the same values, for `--on-image`'s reason: the ground
     is a picture rather than the page. */
  --glow: #fff1be;
  --glow-soft: #fffbd8;
  /* And what is written *on* the glow, for the same reason and in the same
     breath. A pill filled with a colour that does not invert cannot take a
     label that does: `--pine` is the dark green in light mode and a pale one
     in dark, so the cover's primary button and the chips beside it read at
     11.3:1 on one scheme and near 1.5:1 on the other — the words all but gone
     on the very ground the fill was chosen to be legible against. This is
     `pine`'s light-mode value by number as well as by intent, so nothing
     changes for the reader who was already seeing it correctly. */
  --on-glow: #173507;
  /* --glow: #FFBE00;
  --glow-soft: #FFBE00; */


  /* baseTheme.typography — the app's ladder, read up for a browser.
     -------------------------------------------------------------------------
     The app anchors its ladder at 16, and 16 is right on a phone held at arm's
     length. A laptop is twice that distance away, and the same 16 there is a
     size a reader leans in for. So `--body` ramps from the app's 16 on a narrow
     window to 18 on a wide one, and everything set from it — `--caption` and
     `--micro` below, `--subtitle` above — keeps its step, which is the rule
     `baseTheme` already states: caption moves whenever body does, or the two on
     one card stop reading as a hierarchy and start reading as a bug.

     `--display` and `--page-title` are headings rather than reading sizes and
     ramp on their own. */
  --body: clamp(16px, 0.4vw + 13.2px, 18px);
  --caption: calc(var(--body) * 0.875);
  --micro: calc(var(--body) * 0.75);
  --subtitle: calc(var(--body) * 1.19);
  --display: clamp(26px, 2.2vw, 32px);
  --title: 32px;
  --page-title: clamp(34px, 4.6vw, 52px);
  /* A thing being listed, over its own photograph. */
  --card-title: clamp(21px, 1.9vw, 26px);

  /* baseTheme.lineHeightScale */
  --leading: 1.45;
  /* baseTheme.spacing */
  --sp-xs: 6px;
  --sp-sm: 10px;
  /* The sticky bar's whole height: the `min-height` of the row inside it plus
     its own hairline. It is a token because two rules need to agree about it
     and they are 250 lines apart — the bar itself, and the `scroll-padding-top`
     that keeps a linked-to heading from landing underneath it. */
  --topbar: 65px;
  --sp-md: 16px;
  --sp-lg: 24px;
  --sp-xl: 32px;
  /* baseTheme.radius */
  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 22px;
  --r-pill: 999px;

  /* `design/layout.ts`. The measure travels as two max-widths and nothing more,
     which is what `src/content/README.md` says a web renderer owes.
     -------------------------------------------------------------------------
     **They are set from `--body` rather than in pixels, and that is the whole
     point.** The app's 520 and 680 are the widths that give its own 16-point
     body about 68 and 89 characters; written as pixels here they would give the
     18px body 60 and 78, and the line would get *narrower* as the type grew —
     the opposite of what is wanted. As multiples of the body size the character
     count is constant at every window, which is the thing a measure is for.

     **40 × the body, which is about 80 characters, and it is a step past the
     66–75 the textbooks give.** It was 34 — 72 characters, dead centre of that
     range and right by every rule — and on a laptop it read as a narrow ribbon
     of text down the middle of a wide page with more empty margin either side
     than column. The range is written for a printed page, where the margin is
     paper and the eye has nothing else to do; a 1280-wide window is not that
     page, and a measure that is correct in isolation can still be wrong for the
     thing it is drawn in. 80 is the far end of comfortable rather than past it,
     and it is the number this site is set at deliberately. Anything beyond it
     is a long line, whatever the page around it looks like.

     `--reading` keeps the app's own ratio to it (680 / 520 = 1.31), so a
     picture still breaks out past the words by the same fraction it does in the
     app. `--page` is a plain cap on the window, and is not a measure. */
  --measure: calc(var(--body) * 40);
  --reading: calc(var(--measure) * 1.32);
  --page: 1280px;

  /* `skin.ts` cardShadow, soft. */
  --shadow: 0 2px 10px rgba(23, 53, 7, 0.06), 0 1px 2px rgba(23, 53, 7, 0.04);
  --shadow-raised: 0 6px 22px rgba(23, 53, 7, 0.1), 0 2px 6px rgba(23, 53, 7, 0.05);

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

@media (prefers-color-scheme: dark) {
  :root {
    /* darkTheme.colors */
    --mist: #13160d;
    --mist-deep: #1b2013;
    --lake: #8eb2bf;
    --pine: #a0c48d;
    --moss: #b4ce8a;
    --bark: #d0b39c;
    --sand: #bca78f;
    --text-primary: #eef3e7;
    --text-secondary: #c4cbb9;
    --card: #202717;
    --card-elevated: #252d1b;
    --border: #38402b;
    --border-strong: #576345;
    --accent-soft: #232a19;
    --control-surface: #2d3722;
    --primary-control-text: #10190a;
    --image-placeholder: #2b321f;
    --overlay: rgba(0, 0, 0, 0.35);
    --shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
    --shadow-raised: 0 6px 22px rgba(0, 0, 0, 0.45);
  }
}

/* --------------------------------------------------------------------------
   Ground
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /*
    Room for the sticky bar whenever the page scrolls something into view.

    Without it a link to `#somaattinen-transformaatio` puts that heading at the
    very top of the viewport, which is exactly where the bar is — so the reader
    follows a link to a section and arrives at the paragraph after it, with the
    heading they were promised hidden behind the header. The article is linked
    to that way from `valmennus.md`, and every heading in every article has an
    id, so this is every one of them.

    `scroll-padding-top` on the scroll container rather than `scroll-margin-top`
    on the headings, because it is the container that has a bar over it: this
    covers a `:target` on load, an in-page link, `scrollIntoView`, and find-in-
    page, for anything the page ever scrolls to rather than for the elements
    somebody remembered to mark. A gutter of `--sp-md` on top, so the heading
    lands just clear of the hairline rather than welded to it.
  */
  scroll-padding-top: calc(var(--topbar) + var(--sp-md));
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  /* `skin.ts` pageGround: `mistDeep` washed over `mist`. In the app that is an
     opacity over a colour; here it is the same two values as a gradient that
     settles into `mist` before the fold, so a long page does not keep getting
     paler as it is read. */
  background: linear-gradient(var(--mist-deep), var(--mist) 900px) no-repeat var(--mist);
  color: var(--text-secondary);
  font-family: 'SchibstedGrotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: var(--body);
  line-height: var(--leading);
  font-weight: 400;
}

/* --------------------------------------------------------------------------
   Type
   --------------------------------------------------------------------------
   The weights are `baseTheme`'s own and are not guessed: `displayWeight` 400 is
   a page title, `headingWeight` 500 is a section heading, `cardTitleWeight` 500
   is a thing being listed. That the largest type is the lightest looks upside
   down written down and is right on the screen — at 28 points and up Fraunces
   has already said it, and bold on top of large reads as shouting.
   -------------------------------------------------------------------------- */

h1,
h2,
h3,
h4 {
  font-family: 'Fraunces', Georgia, 'Times New Roman', serif;
  color: var(--text-primary);
  letter-spacing: 0;
  margin: 0;
  /* Fraunces at display sizes wants less air than 1.45 gives it. */
  line-height: 1.18;
  text-wrap: balance;
}

h1 {
  font-size: var(--page-title);
  font-weight: 400;
}

/* `kit.ShelfHeading` — the label over a shelf of cards, and what published
   `richContent` sets its own headings level with. Display size, `displayWeight`,
   and `pine` where the page title is `textPrimary`: the two are told apart by
   colour, the size and the weight being deliberately the same. */
h2 {
  font-size: var(--display);
  font-weight: 400;
  color: var(--pine);
}

/* `kit.SectionHeading` — a heading *inside* a card, which is a different job
   from the one above and the reason the two are separate components in the app
   rather than one that grew. */
h3 {
  font-size: var(--subtitle);
  font-weight: 500;
  line-height: 1.3;
}

h4 {
  font-size: var(--body);
  font-weight: 500;
  line-height: 1.35;
}

p {
  margin: 0 0 var(--sp-md);
}

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

strong {
  font-weight: 600;
  color: var(--text-primary);
}

a {
  color: var(--lake);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

a:hover {
  color: var(--pine);
}

:focus-visible {
  outline: 2px solid var(--lake);
  outline-offset: 3px;
  border-radius: 4px;
}

img {
  max-width: 100%;
  height: auto;
}

hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: var(--sp-xl) 0;
}

/* `kit.Eyebrow` — small, tracked out, uppercase, in the lake blue. Every browse
   card in the app carries one, kept even where the heading above already says
   the same word: a label that came and went by list would read as meaning
   something. */
.eyebrow {
  display: block;
  margin: 0 0 var(--sp-xs);
  color: var(--lake);
  font-size: var(--micro);
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
}

/* `kit.Lead` — the sentence under a title. Primary colour rather than
   secondary, and one rung up. */
.lead {
  color: var(--text-primary);
  font-size: var(--subtitle);
  font-weight: 400;
  line-height: 1.4;
  margin-bottom: var(--sp-lg);
}

.quiet {
  font-size: var(--caption);
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   Measure
   -------------------------------------------------------------------------- */

.page {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: var(--sp-md);
}

@media (min-width: 700px) {
  .page {
    padding-inline: var(--sp-lg);
  }
}

.reading {
  max-width: var(--reading);
  margin-inline: auto;
}

/* A block set *after* a passage — a call to action, the note under it, the two
   store buttons — is the passage's own last line, so it takes the passage's
   edge and not the column's.

   `--reading` is the column and `--measure` is the measure, and the difference
   between them is what a picture may take: `.rich > *` caps every paragraph at
   the measure and centres it, so a photograph breaks out past the words on both
   sides and nothing else does. A button set at the column's full width does not
   break out symmetrically — it is left-aligned in a box 32% wider than the
   prose, so it starts 16% of a measure to the *left* of the sentence above it,
   which reads as the page having lost its edge rather than as a wider element.

   `RichCard`'s rule read onto this site: a thing that is pressed is part of what
   the passage is saying, and one wider than the sentence that introduced it
   reads as furniture the page added rather than as something the writing points
   at. A picture is the one thing published past that edge, and a button is not
   a picture. */
.reading--measure {
  max-width: var(--measure);
}

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

/* --------------------------------------------------------------------------
   The name
   --------------------------------------------------------------------------
   `design/Wordmark.tsx`: capitals at *normal* spacing, which is the deliberate
   part. Capitals are usually tracked out because they are drawn with
   sidebearings meant for mixed-case work, and this cut has generous enough ones
   to be set as it is. `.eyebrow` above keeps its tracking and must keep it — the
   moment the label style moves with the name, the name is one of the labels.
   -------------------------------------------------------------------------- */

.wordmark {
  font-family: 'FrauncesWordmark', Georgia, serif;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--pine);
  line-height: 1;
}

.logo {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

.logo .wordmark {
  /* `WORDMARK_SIZE` is 17 in the app, chosen to sit on the eyebrow's row beside
     the Premium badge. Nothing here shares a row with a badge, and a site
     header is read from further away than a phone is held. */
  font-size: 20px;
}

/* --------------------------------------------------------------------------
   Header
   -------------------------------------------------------------------------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--mist) 88%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  /* A hairline under the bar, always. It was going to appear only once the page
     had moved — so a hero photograph is not cut off by a line drawn across the
     top of it — and that needs either a scroll listener or a scroll-driven
     animation, which is a script and a moving part for a one-pixel line. The
     bar is translucent and blurred, so the line is what says it is a bar at
     all rather than words floating over the picture. */
  border-bottom: 1px solid var(--border);
}

.topbar > .page {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-md);
  /* Less the hairline, so `--topbar` is the height of the whole bar rather than
     of the row inside it — which is what the scroll padding has to clear. */
  min-height: calc(var(--topbar) - 1px);
}

.menu {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  list-style: none;
  margin: 0;
  padding: 0;
}

.menu a {
  color: var(--text-secondary);
  font-size: var(--caption);
  font-weight: 500;
  text-decoration: none;
  padding: 6px 2px;
  border-bottom: 2px solid transparent;
}

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

/* The mobile menu is a `<details>`: it opens and closes with no script at all,
   it is a button and a group to a screen reader without a line of ARIA, and it
   keeps working in a browser that failed to run anything. */
.menu-toggle {
  display: none;
}

.menu-toggle > summary {
  list-style: none;
  cursor: pointer;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--card);
  color: var(--text-primary);
  font-size: var(--caption);
  font-weight: 500;
}

.menu-toggle > summary::-webkit-details-marker {
  display: none;
}

@media (max-width: 899px) {
  .menu {
    display: none;
  }

  .menu-toggle {
    display: block;
    position: relative;
  }

  .menu-toggle[open] > .menu {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    position: absolute;
    right: 0;
    top: calc(100% + 8px);
    min-width: 220px;
    padding: var(--sp-xs);
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-raised);
  }

  .menu-toggle[open] > .menu a {
    display: block;
    padding: 11px var(--sp-sm);
    border-bottom: 0;
    border-radius: var(--r-sm);
    font-size: var(--body);
  }

  .menu-toggle[open] > .menu a[aria-current='page'] {
    background: var(--accent-soft);
  }
}

/* --------------------------------------------------------------------------
   The cover
   --------------------------------------------------------------------------
   `HeroDetailHeader` and `design/CoverScrim.tsx`, ported. The scrim is depth
   rather than a dimmer: a faint cap at the top under the eyebrow and a ramp
   easing in over the bottom half under the title, with the middle of the
   picture left exactly as published — which is where the subject of a cover
   almost always is.

   **A card wears it and a hero does not**, and the two were tried both ways.
   What a hero lays on a photograph is an eyebrow and one title at the foot of
   something 620px tall, and a shadow under those words holds them without
   taking the light off the whole forest — which on this site is most of what
   there is to look at. A card is that same object at a fifth of the size with
   three times as much on it: an eyebrow, a chip, a title and a description of
   two lines, packed into the bottom third of a small bright picture, and with
   no scrim under them they are unreadable rather than merely soft. So the
   scrim stays here and the hero's own four-layer shadow replaces it there.
   -------------------------------------------------------------------------- */

.cover {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--r-lg);
  background: var(--image-placeholder);
}

.cover > picture,
.cover > img {
  position: absolute;
  inset: 0;
}

.cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.scrim::before,
.scrim::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  z-index: 1;
  pointer-events: none;
}

.scrim::before {
  top: 0;
  height: 34%;
  background: linear-gradient(to bottom, rgba(10, 22, 4, 0.38), rgba(10, 22, 4, 0));
}

.scrim::after {
  bottom: 0;
  height: 68%;
  background: linear-gradient(
    to top,
    rgba(10, 22, 4, 0.82) 0%,
    rgba(10, 22, 4, 0.62) 26%,
    rgba(10, 22, 4, 0.26) 58%,
    rgba(10, 22, 4, 0) 100%
  );
}

@media (prefers-color-scheme: dark) {
  /* A notch stronger in dark mode, as the app draws it. */
  .scrim::before {
    background: linear-gradient(to bottom, rgba(4, 10, 2, 0.5), rgba(4, 10, 2, 0));
  }
  .scrim::after {
    background: linear-gradient(
      to top,
      rgba(4, 10, 2, 0.9) 0%,
      rgba(4, 10, 2, 0.72) 26%,
      rgba(4, 10, 2, 0.32) 58%,
      rgba(4, 10, 2, 0) 100%
    );
  }
}

/* Increase Contrast trades the depth for a flat veil over the whole picture —
   the guarantee, rather than the drawing. In the app that is the one thing that
   reader gets and nobody else; here it is the same bargain. */
@media (prefers-contrast: more) {
  .scrim::after {
    inset: 0;
    height: auto;
    background: rgba(8, 18, 4, 0.62);
  }
}

.cover-body {
  position: relative;
  z-index: 2;
}

/* Everything laid on a photograph is in the mark's own light rather than in
   white, and it is one scale wherever a picture is the ground: the heading in
   `--glow`, the words under it in `--glow-soft`. A hero and a browse card are
   the same object at two sizes — the app's own rule — so a warm hero above a
   row of cold white cards would read as the two having come from different
   places. */
.cover-body,
.cover-body p {
  color: var(--on-image);
}

.cover-body h1,
.cover-body h2,
.cover-body h3,
.cover-body .wordmark {
  color: var(--glow);
}

/* The description under a card's title. A step quieter than the title without
   leaving the glow — a grey over a photograph is not quieter, it is harder to
   read, and the scrim underneath is what buys the contrast. */
.cover-body .card-text {
  color: color-mix(in srgb, var(--glow-soft) 90%, transparent);
}

.cover-body .eyebrow {
  color: color-mix(in srgb, var(--glow-soft) 86%, transparent);
}

/* A lift under every word laid straight on a photograph — the app draws this
   contrast with the scrim alone, but a scrim tuned for a title at the foot
   leaves the eyebrow row up top thinner cover to read against. A shadow on
   the text itself is the web-only top-up, and it is two layers rather than
   one: a tight, nearly-opaque one that draws the letterform's own edge, and a
   wide, faint one that darkens the picture around the whole word. Either
   alone fails on a photograph like the mossy forest behind Valmennus — the
   tight one has nothing to hold on to where the ground is busy rather than
   dark, and the wide one alone is a smudge with no edge in it. Never on
   `.chip` or `.button`: both already sit on their own solid pill and a shadow
   under those reads as a second, redundant edge. */
.cover-body h1,
.cover-body h2,
.cover-body h3,
.cover-body .eyebrow,
.cover-body .lead,
.cover-body .card-text,
.cover-body .wordmark {
  text-shadow:
    0 1px 2px rgba(4, 10, 2, 0.62),
    0 2px 10px rgba(4, 10, 2, 0.48);
}

/* The hero carries no scrim, so its shadow is the whole of the contrast rather
   than a top-up on one — which is why it is five layers where a card's is two.
   A scrim is a gradient over the *picture*: it darkens the photograph evenly
   whether a word is standing there or not, and on a hero that is a third of
   the cover dimmed to hold two short lines. What a shadow darkens is only what
   is behind the letters, so the photograph is published as it was taken and
   the words are still read against it.

   The five are one idea at five radii. The first has no offset at all and is
   the letterform's own edge, drawn evenly all round — that one is what stops a
   stem dissolving where a bright stalk of moss crosses behind it, and an
   offset copy cannot do it because the ground is as likely to be bright above
   a letter as below. Then a tight pair that thickens that edge, and a wide,
   fainter pair that is the pool of shade a scrim would otherwise have laid
   across the whole card. Either half alone fails on the mossy forest behind
   Valmennus — the tight ones have nothing to hold on to where the ground is
   bright rather than dark, and the wide ones alone are a smudge with no edge
   in them. Never on `.chip` or `.button`: both sit on their own solid pill
   already. */
.hero .cover-body h1,
.hero .cover-body h2,
.hero .cover-body h3,
.hero .cover-body .eyebrow,
.hero .cover-body .lead,
.hero .cover-body .card-text,
.hero .cover-body .wordmark {
  text-shadow:
    0 0 2px rgba(4, 10, 2, 0.7),
    0 1px 3px rgba(4, 10, 2, 0.95),
    0 3px 10px rgba(4, 10, 2, 0.88),
    0 6px 26px rgba(4, 10, 2, 0.8),
    0 12px 60px rgba(4, 10, 2, 0.7);
}

/* Increase Contrast is the one reader who gets a veil back, and over the whole
   picture rather than as a gradient: that reader is trading the photograph for
   the guarantee, which is the same bargain `.scrim` already makes for them. */
@media (prefers-contrast: more) {
  .hero::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: rgba(8, 18, 4, 0.62);
  }
}

/* The hero itself: the cover with the page's own name and lead laid over it.
   Capped and rounded like every other card — `.page` around it in `hero()` is
   what caps it, so the picture reads as the same width as the content under
   it instead of a full-bleed band with a narrow column floating inside it. */
.hero {
  position: relative;
  /* One height for every hero, the front page included. It used to open on a
     taller card than the pages under it, which on a laptop was most of the
     first screen spent on a photograph before a word of the site was read.
     `hero--tall` below is the one exception, and it is asked for by a
     photograph rather than by a page. */
  min-height: min(50vh, 420px);
  display: flex;
  /* A card now, not a full-bleed band — it needs air between it and the bar
     above it, the way any other card would. */
  margin-top: var(--sp-lg);
  margin-bottom: var(--sp-xl);
}

/* The one picture that cannot live in that height, and it is the picture that
   says so rather than the page — `hero: "tall"` in `images/sources.json`. A
   forest is the same forest whatever band is cut out of it; a face fills most
   of its frame, and the band a 420px card cuts lands across the middle of one.
   620 is not a taste: it is what it takes for this photograph to hold a head
   from the crown to the chin at the width the page caps a hero to, with the
   `focus` travelling with it putting the crop on the face rather than the
   centre. The `vh` term is the same guard the height above has — a short
   window keeps its own proportions rather than being filled by a photograph.

   Only above 700px, which is where the crop is a horizontal one. Narrower than
   that the hero is already taller than the picture is wide, so the whole height
   of the photograph is on screen and it is the sides that are cut: there is no
   room to buy, and buying it would spend most of a phone's first screen. */
@media (min-width: 700px) {
  .hero--tall {
    min-height: min(74vh, 620px);
  }
}

/* The eyebrow-and-chip row at the top, the title block at the foot — the same
   split `.card-body` already draws, stretched across a taller card, and the
   same even padding on all four sides it uses too. The foot used to be a
   `clamp(var(--sp-xl), 6vw, 72px)`, which on a laptop is 72 against a 24 at
   the side: the words sat a third of the way up the card with a band of bare
   photograph under them, and the wider the window the worse it read. That
   number was set when the hero was a full-bleed band whose bottom edge was
   the fold rather than a card's own corner. */
.hero .cover-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
  width: 100%;
  padding: var(--sp-md);
}

/* Pushed to the foot of the card with its own auto margin rather than
   `justify-content: space-between` on the parent — `space-between` only
   works once there are two children, and the front page's hero has no
   eyebrow row, so it is down to this one. With `space-between` that left it
   sitting at the top of the card with the whole rest of the photograph empty
   underneath it. `margin-top: auto` pushes it to the bottom regardless of
   whether a `.card-top` sibling exists above it. */
.hero-bottom {
  margin-top: auto;
}

@media (min-width: 700px) {
  .hero .cover-body {
    padding: var(--sp-lg);
  }
}

.hero .wordmark {
  display: block;
  font-size: clamp(34px, 7vw, 76px);
  margin-bottom: var(--sp-md);
  /* The name in the mark's own light rather than in `onImage` white — it is
     the one word on the cover that is the brand rather than a line of copy.
     It was tried in `pine`, the green the bar and the footer draw it in, and
     that is a 1.3:1 against a scrimmed photograph: the page's own green is
     legible on the page's own ground and disappears on a picture. */
  color: var(--glow);
}

.hero h1 {
  /* Wide enough that the title breaks where a Finnish sentence breaks. It was
     18ch, which is half the card on a laptop: "Kehollisen mindfulnessin ja
     somaattisen transformaation yksilö- ja pienryhmävalmennukset" came out as
     four lines with `trans-` left hanging at the end of one of them, and the
     other half of the card empty beside it. 28ch is about three quarters of
     it, and it is `ch` rather than a fraction because what a line wants is a
     number of characters — the same cap holds when the type grows. `balance`
     above does the rest: given the room, it takes that title to three even
     lines that each end on a word.

     Full width was the other candidate and reads worse at the short end. A
     title like "Tilausmeditaatiot tapahtumiin ja tyhy-päiviin" would come out
     as one 1184px line of 52px type, which is a banner rather than a heading,
     and the page under it caps its own prose at less than half that. */
  max-width: 28ch;
  /* A Finnish compound does not break at a space to save it: without a hyphen
     to fall back on, "luontoyhteysmeditaatiot" alone ran past the card's own
     edge on a phone. `lang="fi"` on `<html>` is what lets `hyphens` pick a
     real break point instead of an arbitrary mid-word cut.

     But left to itself it hyphenates far more than that — it broke
     "transformaation" at 52px with most of a line still free, which is the
     weird break rather than the rescue. `hyphenate-limit-chars` says only a
     word of 18 characters or more may be broken at all, which is the length
     at which a single Finnish word genuinely outgrows a phone's card. A
     browser that has not implemented it hyphenates as before, which is what
     this line improves on rather than depends on. */
  overflow-wrap: break-word;
  hyphens: auto;
  hyphenate-limit-chars: 18 9 6;
}

/* The front page's is the one line under the wordmark rather than the name of
   the page, so it takes the card's whole width — three short lines under a
   large wordmark read as a stack of headings, and one wide line reads as the
   sentence it is. It steps down to `--glow-soft` for the same reason: the
   wordmark directly above it is already carrying the colour, and two lines of
   the same yellow stacked read as one block shouting. Every other hero's `h1`
   *is* the naming line and keeps `--glow`. The wordmark is only ever drawn on
   the front page, so its presence is what tells the two apart without a class
   of its own. */
.hero .wordmark + h1 {
  max-width: none;
  color: var(--glow-soft);
}

.hero .lead {
  max-width: var(--measure);
  /* `--glow-soft` at the same .92 the white was drawn at, so a hero that
     carries both a title and a lead keeps them in one family. */
  color: color-mix(in srgb, var(--glow-soft) 92%, transparent);
}

/* --------------------------------------------------------------------------
   Cards
   --------------------------------------------------------------------------
   `kit.Surface` and the three browse cards. A card is two zones and nothing
   more: the row that *names* the thing — the cover beside the eyebrow, the
   title and the meta — and the description under the pair at the card's full
   width. Spacing belongs to the card and never to the list; sideways is the
   grid's, which is `design/CardGrid.tsx`'s own rule read onto a web layout.
   -------------------------------------------------------------------------- */

.card {
  display: block;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: var(--sp-md);
  color: inherit;
  text-decoration: none;
}

/* `skin.ts` cardPress — a press shrinks the card a fraction and dims it a
   fraction, and lets go of both when the finger does. The number is small on
   purpose: a card is a large surface and the movement is read at its edges. A
   transform, so nothing reflows and nothing shifts under a finger about to
   land. Hover is the web's own addition and lifts rather than presses. */
a.card,
button.card {
  transition:
    transform 160ms var(--ease),
    box-shadow 160ms var(--ease),
    opacity 160ms var(--ease);
}

a.card:hover {
  box-shadow: var(--shadow-raised);
  transform: translateY(-2px);
}

a.card:active {
  transform: scale(0.985);
  opacity: 0.88;
}

.card h3,
.card h2 {
  margin-bottom: var(--sp-xs);
}

/* The browse card with a photograph is a cover, and the words go **on** it.
   -------------------------------------------------------------------------
   This is the one place the site deliberately parts company with the app. The
   app's rule is that a cover in a list carries nothing over it — and it is
   right there, because there the cover is a 104-point thumbnail *beside* the
   words on the card's own surface, and darkening every thumbnail in a list to
   keep legible text that is not on it was exactly the mistake `CoverScrim`
   exists to have stopped.

   Here the card is some 600 points wide and the picture is the whole head of
   it. That is not a thumbnail; it is a cover, and the app already has one
   answer for a cover — `HeroDetailHeader`, which lays the eyebrow and the
   chips at the top, the title and the description at the foot, and gathers a
   gradient where the words are while leaving the middle of the photograph as
   published. So the card is drawn as the page's own hero at card size, and the
   site says one thing about photography instead of two.

   `ArticleCard` is untouched: it has no cover, which is the whole of what says
   it is writing rather than something to look at. */
.card--cover {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 0;
  display: flex;
  min-height: 22rem;
  background: var(--image-placeholder);
}

.card--cover > picture {
  position: absolute;
  inset: 0;
}

.card--cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 700ms var(--ease);
}

/* The lift a card already answers a hover with, carried into the picture. It
   is a transform, so nothing reflows and the card's own edges stay put. */
a.card--cover:hover img {
  transform: scale(1.04);
}

.card-body {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--sp-xl);
  width: 100%;
  padding: var(--sp-lg);
}

/* The eyebrow at the leading edge and the chip at the trailing one, on one row
   — the cover header's own arrangement. `align-items: flex-start` so a chip
   that wraps to two lines does not drag the eyebrow down with it.

   **And the row wraps.** Neither of the two will give: an eyebrow is one word
   and cannot shrink below it, and a chip is `nowrap` because a badge broken
   across two lines inside its own pill is a worse object than a badge on a line
   of its own. On the narrowest phone a long eyebrow and a long badge —
   "Verkkokurssi" and "Tulossa syksyllä 2026" — want more than a 238-point row
   has, and with nothing allowed to wrap the chip simply ran off the card, which
   `overflow: hidden` then cut in half. Wrapping is the only give left. */
.card-top {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-sm);
}

.card-top .eyebrow,
.card-top .chip-row {
  margin: 0;
}

/* What holds the chip at the trailing edge once the row can wrap:
   `space-between` has nothing to distribute on a line holding one item, so a
   wrapped chip would drop to the leading edge and sit under the eyebrow it is
   meant to be opposite. The auto margin is the same arrangement on either
   line. */
.card-top .chip-row {
  margin-left: auto;
}

.card--cover h3 {
  /* Larger than a card title in the app, and it can be: it is laid across a
     photograph rather than squeezed into a column beside one. */
  font-size: var(--card-title);
  line-height: 1.15;
  margin-bottom: var(--sp-xs);
}

.card-text {
  color: var(--text-secondary);
  margin: 0;
}

/* `ArticleCard` — deliberately not a fourth browse card. It keeps the eyebrow,
   a title that wraps and never shrinks, and the description at the card's full
   width; the missing cover is the whole of what says "this is writing" in a
   shelf of photographs, and it carries no meta line because an article has no
   fact worth weighing before opening.

   It needs no rule of its own: it is the plain `.card` above, and what makes it
   an article card is that `.card--cover` was not added — so the picture, the
   scrim and the words over it are all simply absent. */

.grid {
  display: grid;
  /* `design/CardGrid.tsx` uses `spacing.sm` — ten points — and that is right in
     the app, where two cards on a row are a phone's width apart and the gap is
     read at a glance. Here a card is 600 points wide with a photograph in it,
     and ten points between two of those reads as one picture torn in half.
     The gutter grows with the window rather than sitting at a single value,
     since the cards themselves do. */
  gap: clamp(var(--sp-md), 1.8vw, var(--sp-xl));
  /* `MIN_CARD_WIDTH` is 340 in the app, and it is the number a browse card
     stops being one below: the description goes and the title breaks mid-word.
     The column count is worked out from it and the width actually available,
     never from a breakpoint. */
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
}

.grid--wide {
  grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr));
}

/* --------------------------------------------------------------------------
   Chips and badges — `design/Chip.tsx`
   --------------------------------------------------------------------------
   "Uusi", "Premium" and a duration are all one component in the app, and a tone
   may only change its colours. They used to be three lookalikes with three
   paddings, which read as a crooked row the moment two sat side by side.
   -------------------------------------------------------------------------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--text-secondary);
  font-size: var(--micro);
  font-weight: 600;
  letter-spacing: 0.3px;
  white-space: nowrap;
}

/* The filled tone. In the app it is Premium's and nothing else's, because that
   word has to look the same on a card as it does in the header badge. Here it
   is the one thing on a page that is genuinely an announcement — a price, a
   date something opens. */
.chip--solid {
  background: var(--pine);
  border-color: var(--pine);
  color: var(--primary-control-text);
}

/* Over a photograph a `surface` chip is retinted rather than rebuilt —
   `ChipsOnImage`. It was a dark pill at 42% with a half-lit border, which is
   the app's own treatment and reads there because it sits on a scrim over a
   *cover* the app chose. Every photograph on this site is a Finnish forest —
   mid-green, high detail, and busy at exactly the size a chip is — and a
   translucent dark pill on one is a smudge. So it is the mark's own pairing
   instead, the glow on the forest ground: a near-solid light pill with `pine`
   text, which is the most legible thing that can sit on any of these pictures
   and is still the palette rather than a new colour. The contour stays,
   because every tone here has one.

   Drawn in `--glow-soft` rather than in white, for the reason the cover's
   button is: this paragraph called it the glow on the forest ground while the
   pill was white, and a chip left cold beside a warm title and a warm button
   is the one object on the cover that looks like it came from somewhere else.
   The label is `--on-glow` rather than `--pine` — the fill does not invert, so
   the word on it may not either. It is 12.9:1 on the pill in both schemes. */
/* And a rung larger, `--micro` to `--caption`, with the padding grown to keep
   the pill's shape. That is a size rather than a tone — every chip on a cover
   grows together, so the row cannot go crooked — and it is the one place the
   site asks for it. A chip on the page's own ground is a footnote beside body
   copy and is the size it has always been; here it is one of four things laid
   over a whole photograph, beside a title at `--card-title` or larger, and at
   the footnote's size it read as a caption that had drifted onto the picture.
   The chip is also the smallest object on the cover and the only one with the
   picture showing right up against its edges, so it is where the extra size
   buys the most legibility. */
.cover-body .chip {
  background: color-mix(in srgb, var(--glow-soft) 94%, transparent);
  border-color: color-mix(in srgb, var(--glow-soft) 94%, transparent);
  color: var(--on-glow);
  font-size: var(--caption);
  padding: 4px 10px;
  font-weight: 700;
  backdrop-filter: blur(6px);
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs);
  margin-bottom: var(--sp-sm);
}

/* --------------------------------------------------------------------------
   Buttons — `kit.PrimaryButton` / `kit.SecondaryButton`
   --------------------------------------------------------------------------
   The primary is the single thing a card is asking for and is bare on purpose:
   a glyph beside it decorates a decision already made. The outline one is the
   one that comes in stacks and is the one that carries an icon.
   -------------------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px var(--sp-md);
  border-radius: var(--r-md);
  border: 1px solid var(--pine);
  background: var(--pine);
  color: var(--primary-control-text);
  font-family: inherit;
  font-size: var(--body);
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition:
    transform 140ms var(--ease),
    opacity 140ms var(--ease);
}

.button:hover {
  color: var(--primary-control-text);
  opacity: 0.92;
}

.button:active {
  transform: scale(0.97);
}

/* The outline button is **green**, and that is what makes it read as a button.
   It was `--border-strong` around `--text-primary` — the app's own secondary,
   where a button sits on a card that has already announced itself and a hairline
   is enough to say "press this". On a page it is not: an outline in the border
   colour around body-coloured words, beside body copy, is a box drawn around a
   sentence. `pine` on both the contour and the label is the same green the
   filled one is made of, so the two are one button seen at two weights rather
   than two ideas — and on the pale ground it is *greener* than the filled pill,
   which at this size reads nearly black.

   `pine` on `mist` is 12.7:1, so the words are past AA without help; the
   contour is the same colour and therefore never the thing carrying the
   legibility. Dark mode needs nothing said here — `--pine` is the pale green
   there, and both halves invert with it. */
.button--outline {
  padding-block: 13px;
  background: transparent;
  border-color: var(--pine);
  color: var(--pine);
}

.button--outline:hover {
  background: var(--control-surface);
  color: var(--pine);
}

/* Over a photograph the filled button turns light.
   `pine` is a dark green and a published forest photograph is mostly dark
   green, so the app's own primary — pine fill, pale label — read as a hole in
   the picture rather than as the thing the page is asking for. Inverted it is
   the brightest object on the cover, which is what a primary button is for. It
   is also the mark's own pairing: the glow on the forest ground — and it is
   drawn in the glow itself rather than in white, which is what makes that
   sentence true rather than merely a description of it. `--on-glow` on `--glow`
   is 11.3:1, so the warmth costs the label nothing.

   **The label is `--on-glow` and never `--pine`**, which is what this was for
   as long as the button existed. Both are the same dark green in light mode and
   only one of them stays it: `--pine` is the pale green in dark mode, so the
   hero's button on the front page — the one thing that page is asking for — was
   light green on pale yellow, at about 1.5:1. The fill is fixed in both schemes
   on purpose, and a label read against a fixed ground has to be fixed with it.

   Nothing about the button on the page's own surface changes, and must not:
   `kit.PrimaryButton` is pine there, the two are not read side by side, and a
   pale yellow on the page's own `mist` is a 1.1:1 fill — a shape with no
   edge. The glow is a colour for photographs, not for the page. */
.cover-body .button {
  background: var(--glow);
  border-color: var(--glow);
  color: var(--on-glow);
}

.cover-body .button:hover {
  color: var(--on-glow);
}

/* Over a photograph the outline button has the page's ground taken out from
   under it, so it borrows the cover's own contour the way a chip does. */
.cover-body .button--outline {
  border-color: rgba(250, 252, 246, 0.6);
  color: var(--on-image);
  background: rgba(12, 26, 6, 0.3);
  backdrop-filter: blur(6px);
}

.cover-body .button--outline:hover {
  background: rgba(12, 26, 6, 0.48);
  color: var(--on-image);
}

.buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
  margin-top: var(--sp-lg);
}

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

.section {
  padding-block: clamp(var(--sp-xl), 6vw, 72px);
}

.section--soft {
  background: var(--accent-soft);
  border-block: 1px solid var(--border);
}

.section > .page > h2,
.section > .page > .section-head {
  margin-bottom: var(--sp-lg);
}

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-sm);
}

/* "Katso kaikki" — a capped shelf's link is a field on the section and never a
   test in the header, and it appears only once the shelf is holding back more
   than it drew, so it is never a link to nothing. */
.section-head a {
  font-size: var(--caption);
  font-weight: 500;
}

/* --------------------------------------------------------------------------
   Rich content — the blocks `src/content/richBlocks.ts` reads
   --------------------------------------------------------------------------
   Set at the size of the `textContent` beside it, with headings level with
   `kit.ShelfHeading`. Published prose is the page's own words: a passage
   written into a catalog and one written into a screen are the same text, and a
   notch of difference read as a footnote appended to the page.

   Running prose stops at `--measure` and a picture keeps the whole of `--reading`,
   so on a wide window a photograph breaks out past the words on both sides.
   That is the width buying a bigger picture rather than a longer line.
   -------------------------------------------------------------------------- */

.rich {
  max-width: var(--reading);
  margin-inline: auto;
  color: var(--text-primary);
}

.rich > * {
  max-width: var(--measure);
  margin-inline: auto;
}

.rich > h2,
.rich > h3 {
  margin-top: var(--sp-xl);
  margin-bottom: var(--sp-sm);
}

.rich > h2:first-child,
.rich > h3:first-child {
  margin-top: 0;
}

/* The lead is the one rung published prose may leave the body size for, and it
   is `>` in Markdown. `subtitle`, in the body face at 600 — the step the ladder
   already has above `body`. It was set at the body's own size in semibold for
   as long as the construct existed, which made the one thing an author has for
   opening a piece indistinguishable from a bolded sentence. */
.rich .lead {
  font-size: var(--subtitle);
  font-weight: 600;
  color: var(--text-primary);
}

.rich .footnote {
  font-size: var(--caption);
  font-style: italic;
  color: var(--text-secondary);
}

.rich ul,
.rich ol {
  margin: 0 0 var(--sp-md);
  padding-left: 1.3em;
}

.rich li {
  margin-bottom: var(--sp-xs);
}

.rich li::marker {
  color: var(--moss);
}

/* A group is a run of blocks set apart from the next by space alone — the
   quietest division there is, and the right default for prose that simply moves
   on. A mark is louder and is asked for by name. */
.rich .group + .group {
  margin-top: var(--sp-xl);
}

/* `design/ChapterMark.tsx` — the dinkus and the dash-dot, centred, in the
   heading face. The passage publishes which division it wants; the app draws
   its own em dash whatever characters were typed. */
.chapter-mark {
  max-width: none;
  text-align: center;
  color: var(--moss);
  font-family: 'Fraunces', Georgia, serif;
  font-size: var(--subtitle);
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  margin-block: var(--sp-xl);
}

/* Rows led by an icon badge, and rows led by a round label badge. */
.rows {
  list-style: none;
  margin: 0 0 var(--sp-md);
  padding: 0;
}

.rows li {
  display: flex;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-md);
}

.row-badge {
  flex: none;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  border: 1px solid var(--border);
  color: var(--pine);
  font-size: var(--caption);
  font-weight: 700;
}

.row-badge .icon {
  /* `CONTROL_GLYPH_SIZE` — a glyph inside a control the reader presses, which
     is deliberately not derived from `typography`: the disc is a fixed 38 and a
     glyph that grew while the disc did not would push itself out of it. */
  font-size: 20px;
}

.row-title {
  display: block;
  font-weight: 600;
  color: var(--text-primary);
}

.icon {
  font-family: 'MaterialDesignIcons';
  font-style: normal;
  font-weight: 400;
  line-height: 1;
  speak: none;
  -webkit-font-smoothing: antialiased;
}

/* Pictures. The shape is published — `"square"`, `"portrait"`, or 16:9 for
   everything else — and the two that stand taller than they are wide are capped
   at the width one thing is drawn at and centred, since either is one thing
   standing on its own and would otherwise grow into a page and a half of
   photograph. */
.rich figure {
  max-width: var(--reading);
  margin: var(--sp-xl) auto;
}

.rich figure img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-lg);
  background: var(--image-placeholder);
}

.rich figcaption {
  margin-top: var(--sp-sm);
  max-width: var(--measure);
  margin-inline: auto;
  font-size: var(--caption);
  color: var(--text-secondary);
  text-align: center;
}

/* The cap is the frame's and the ratio is the picture's — the app's own split,
   for the reason written out there: a ratio on the image itself takes its width
   from a box that is not settled yet and draws a square as a rectangle.

   The shape is what the passage published, so it is a crop and not a hope about
   the file: `object-fit: cover` fills the frame and `object-position` — written
   per picture by `focusRules` — says which part of it survives. Without the
   crop the keyword decided nothing on the web, since a `<img>` at `height:
   auto` is whatever ratio the photograph already had, and the same passage read
   square in the app and portrait here. */
.rich figure.image--square,
.rich figure.image--portrait {
  max-width: var(--measure);
}

.rich figure img {
  object-fit: cover;
}

.rich figure.image--wide img {
  aspect-ratio: 16 / 9;
}

.rich figure.image--square img {
  aspect-ratio: 1;
}

.rich figure.image--portrait img {
  aspect-ratio: 4 / 5;
}

/* Two images on one line are a row, and the row is what decides whether they
   sit side by side. Each asks for `PAIR_MIN_WIDTH` and grows into an equal
   share of what the row actually has, so a wrap is what turns a wide window's
   pair into a narrow one's column — no measuring, and the same rule holds in a
   prose column and in a phone. */
.image-pair {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
  max-width: var(--reading);
  margin: var(--sp-xl) auto;
}

.image-pair figure {
  flex: 1 1 300px;
  margin: 0;
}

/* A card inside a passage takes the same measure the prose does: it is part of
   what the passage is saying, pressed rather than looked at, and a card wider
   than the sentence that introduced it reads as furniture the page added. */
.rich .card {
  max-width: var(--measure);
  margin-block: var(--sp-lg);
}

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */

.form {
  display: grid;
  gap: var(--sp-md);
}

.field {
  display: grid;
  gap: var(--sp-xs);
}

/* A field opens on a bold caption naming it. `.field-label` is the same caption
   drawn as a span, for the consent row — which already spends its `<label>` on
   the sentence beside the box and so has nothing left to name itself with. */
.field label,
.field-label {
  font-size: var(--caption);
  font-weight: 600;
  color: var(--text-primary);
}

/* The requirement is said in words, in the caption, quieter than the name of
   the field: an asterisk needs a legend to mean anything, and a legend is
   furniture on a form of two rows. Every required field carries one — a marker
   on one field and not the other makes the other read as optional. */
.field-required {
  font-weight: 400;
  color: var(--text-secondary);
}

.field input,
.field textarea,
.field select {
  font: inherit;
  color: var(--text-primary);
  padding: 12px var(--sp-sm);
  background: var(--card-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}

.field textarea {
  min-height: 140px;
  resize: vertical;
}

/* The newsletter's e-mail carries its caption inside the field and is the only
   field on this site that does, on the page's form and in the footer's alike —
   the reasoning is in `newsletterForm`. So the placeholder is drawn as a
   caption rather than left at the browser's own washed-out grey:
   `--text-secondary` is the colour every other quiet line is set in, and it
   clears AA on both grounds where a default placeholder does not. The rule is
   stated over every field on the site rather than over the newsletter's alone,
   because a placeholder left at the browser's grey is the failure whatever
   field it is in — but the newsletter's e-mail is the only field that has one,
   and it is meant to stay that way: the contact form's four keep their
   captions above them, where a name that vanishes on the first keystroke would
   cost a reader something. */
.field input::placeholder,
.form--compact .field-row input::placeholder {
  color: var(--text-secondary);
  opacity: 1;
}

/* A checkbox field carries the consent sentence as its label, so it reads as
   running text rather than as a bold caption over an empty box. */
.field--checkbox label {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-xs);
  font-size: var(--body);
  font-weight: 400;
  color: var(--text-primary);
}

.field--checkbox input[type='checkbox'] {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  margin-top: 0.2em;
  accent-color: var(--pine);
}

/* The compact form — the footer's, and nowhere else's.
   -------------------------------------------------------------------------
   One row: the word naming the offer, the address, the button. It sits under
   the menu in the footer's first column rather than in a band of its own, so
   the quiet end of the page stays one block — a heading, a lead and a rule
   above it made a second page opening where the last one had just closed.

   Everything steps down to the footer's own type. `--caption` on the caption
   and the field, `--micro` on the consent under it, and the padding comes off
   with the size — but not off the *targets*: the field and the button keep a
   full press area and the checkbox keeps its box, because type is what a
   footer makes smaller and a control the thumb has to find is not. */
.form--compact {
  gap: var(--sp-sm);
  /* Clear of the menu above it. The links are the footer's list and this is the
     one thing in the column asking for something back, so it wants more air
     between them than the rows of links keep between each other. */
  margin-top: var(--sp-lg);
}

/* The row wraps rather than squeezing: the address grows into whatever is left
   and the button takes what it needs, so a column too narrow for both puts the
   button under the field instead of shrinking the field past an address. It is
   capped where the consent under it is not — a field and a button stretched
   across a wide footer read as a form that has come loose from the column,
   where a sentence is meant to run the width it is given. */
.field-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-xs);
  max-width: 30rem;
}

.form--compact .field-row input {
  flex: 1 1 9rem;
  min-width: 0;
  font: inherit;
  font-size: var(--caption);
  color: var(--text-primary);
  padding: 7px var(--sp-sm);
  background: var(--card-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}

/* The compact button keeps `pine` and its label colour: this is still the one
   thing the footer is asking for, and an outline here would put it at the
   weight of the links above it. */
.button--compact {
  font-size: var(--caption);
  font-weight: 600;
  padding: 7px var(--sp-sm);
}

/* The consent is the second line and the only thing here that did not
   compress — unticked and required, the same affirmative act the page's own
   form asks for, at the footer's smallest size and across the whole column. */
.consent {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: var(--micro);
  line-height: 1.45;
}

.consent input[type='checkbox'] {
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  margin-top: 0.15em;
  accent-color: var(--pine);
}

/* The honeypot: a field no person can see, reach by tab, or be told about, and
   which every form robot fills in anyway. A submission carrying it is dropped.
   `display: none` is what the cruder robots check for, so it is moved off the
   page instead. */
.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   Footer
   --------------------------------------------------------------------------
   `design/FooterLinks.tsx`: the quiet end of the page. The separator is drawn
   *inside* the label it follows and never as a child of its own — the row wraps
   at large text sizes, and a "·" that is its own item can be pushed to the
   start of the second line with nothing in front of it.
   -------------------------------------------------------------------------- */

.sitefoot {
  margin-top: clamp(var(--sp-xl), 6vw, 72px);
  padding-block: var(--sp-xl);
  border-top: 1px solid var(--border);
  background: var(--mist-deep);
  font-size: var(--caption);
}

.sitefoot .page {
  display: grid;
  gap: var(--sp-lg);
}

@media (min-width: 700px) {
  .sitefoot .page {
    grid-template-columns: 1fr auto;
    align-items: start;
  }

  /* The wordmark spans both columns so the two blocks under it start on the
     same line: the address opposite the menu rather than opposite the name.
     `justify-self` is what keeps the link the width of the word — a grid item
     stretches to its column otherwise, and the whole row would be clickable. */
  .sitefoot .logo {
    grid-column: 1 / -1;
  }
}

.sitefoot .logo {
  justify-self: start;
}

/* The form is inside the footer's first column, under the menu, so it inherits
   `.sitefoot a`'s secondary colour on every link it holds. The consent sentence
   is running text rather than a link and keeps `--text-primary`; the
   tietosuojaseloste inside it is a link and is meant to look like one. */
.sitefoot .consent span {
  color: var(--text-primary);
}

.sitefoot .consent a {
  text-decoration: underline;
}

.sitefoot .button {
  color: var(--primary-control-text);
  text-decoration: none;
}

.sitefoot .button:hover {
  color: var(--primary-control-text);
}

.sitefoot nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm) var(--sp-md);
}

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

.sitefoot a:hover {
  color: var(--pine);
}

/* No margin over the menu: the wordmark is a grid row of its own now, so the
   space under it is the footer's own row gap — a margin here would be added to
   it, and only on this side of the footer. */
.sitefoot-links {
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   Prose page furniture
   -------------------------------------------------------------------------- */

/* The article's place in the site, above its eyebrow. It is a `.rich > *` like
   every other row on the page, so `max-width: var(--measure)` and the centring
   that comes with it are inherited and must not be overridden here — the whole
   point of the row is that it starts at the same left edge the prose does. The
   `← Artikkelit` link this replaced was an inline-block, and `margin-inline:
   auto` does nothing to one of those: it sat at the left edge of the *reading*
   column while the words sat centred in the narrower measure inside it, which
   is the misalignment that gave the page two left edges. `display: flex` is
   what makes the centring apply. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs);
  margin-bottom: var(--sp-md);
  font-size: var(--caption);
  color: var(--text-secondary);
}

.crumbs a {
  font-weight: 500;
}

/* The page you are on, said in the row and not linked. */
.crumbs-here {
  color: var(--text-secondary);
}

/* `margin-block` rather than the `margin: X 0 Y` shorthand this used to be.
   The shorthand set `margin-inline: 0`, which beat the `margin-inline: auto`
   every `.rich > *` is given — so the date alone was pinned to the left edge of
   the reading column, some 98px outside the column its own article was set in.
   A shorthand that touches an axis it has no opinion about is how one row ends
   up disagreeing with every other row on the page. */
.article-meta {
  margin-block: var(--sp-sm) var(--sp-xl);
  font-size: var(--caption);
  color: var(--text-secondary);
}

/* A band with a picture on one side and words on the other. `DetailBand`'s two
   columns are capped and the pair is centred, which is the same arithmetic:
   extra width goes to the page's own margins, never into wider rows. */
.band {
  display: grid;
  gap: var(--sp-lg);
  align-items: center;
}

@media (min-width: 900px) {
  .band {
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr);
    gap: clamp(var(--sp-lg), 5vw, 64px);
  }

  .band > * {
    max-width: var(--measure);
  }

  .band--image-first > :first-child {
    justify-self: end;
  }
}

/* A portrait photograph is 4:5, so at a column's full width it stands 650
   points tall beside four lines of text. Capped, the band reads as a picture
   and a paragraph rather than as a portrait with a caption under it. */
.portrait {
  max-width: 400px;
  margin-inline: auto;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow);
  background: var(--image-placeholder);
}

.portrait img {
  display: block;
  width: 100%;
  height: auto;
}

/* --------------------------------------------------------------------------
   Skip link — the first thing in the tab order and the only thing on the page
   that is invisible until it has focus.
   -------------------------------------------------------------------------- */

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

.skip:focus {
  left: var(--sp-md);
  top: var(--sp-md);
  padding: 10px var(--sp-md);
  background: var(--card);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}

@media print {
  .topbar,
  .sitefoot,
  .buttons {
    display: none;
  }
}

/* Written by src/images.ts — see images/sources.json. */
.focus--johanna-sammalella { object-position: 50% 65%; }
