/* Lukkari - kevyt, oma CSS. Ei kirjastoa, ei käännösvaihetta. */

/* METROPOLIS on OTSIKKOFONTTI, ja vain yksi paino ladataan (Bold, 26 kB).
   Lähde, lisenssi ja se miksi fonttia ei haeta kolmannelta osapuolelta:
   `core/static/core/fonts/README.md`.

   `font-display: swap` EIKÄ oletus `auto`: oletuksella otsikko on ladatessa
   NÄKYMÄTÖN (selainten "block period"), eli sivun suurin teksti puuttuu juuri
   sen ensimmäisen sekunnin ajan, jona lukija etsii mistä sivusta on kyse.
   `swap` näyttää järjestelmäfontin heti ja vaihtaa sen alla. */
@font-face {
  font-family: 'Metropolis';
  src: url("fonts/Metropolis-Bold.dc6926c36da3.woff2") format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --color-text: #1c1c1c;
  --color-bg: #ffffff;
  /* SIVUPOHJA ON OMA TOKENINSA EIKÄ `--color-bg`:n uusi arvo. `--color-bg` on
     myös kiinnitetyn sarakkeen, pudotusvalikon ja lomakekenttien tausta -
     sen vaihto harmaaksi värjäisi nekin, ja kenttä, joka on sivun pohjan
     värinen, lakkaa näyttämästä kentältä. Pohja vaihtuu `body`llä, ja
     taulukot, kortit ja kentät pysyvät valkoisina.
     Teksti sivupohjaa vasten 15,50:1. */
  --color-page-bg: #f3f4f7;
  --color-muted: #5f6470;
  --color-border: #d7dae2;
  /* KOROSTUS ON TOIMINNON VÄRI (ks. tilamerkkien kommentti alla). Valkoinen
     teksti korostuksella 13,43:1 ja tummalla korostuksella 15,92:1. */
  --color-accent: #27246e;
  --color-accent-dark: #1c1a52;
  --color-accent-contrast: #ffffff;
  /* TUMMAT PINNAT: yläpalkki ja navigaatiopalkki. Kaksi eri sävyä, jotta
     palkit erottuvat toisistaan ilman erotinviivaa. Valkoinen teksti
     yläpalkilla 16,63:1 ja navigaatiolla 14,44:1; himmennetty
     (`--color-on-dark-muted`) 8,84:1 ja 7,67:1. */
  --color-header-bg: #1a1846;
  --color-nav-bg: #232062;
  --color-on-dark: #ffffff;
  --color-on-dark-muted: #b9bbd8;
  /* LIME VAIN TUMMALLA POHJALLA (.claude/rules/kayttoliittyma.md, "Ilme on sukulainen, ei
     kopio"). Valkoisella se on 1,30:1 eli näkymätön; yläpalkilla 12,82:1 ja
     navigaatiolla 11,14:1. Vahtina `core/tests.py:LimeTokenScopeTests`. */
  --color-accent-on-dark: #d9ee1c;
  /* OTSIKKOFONTTI. Varalla järjestelmäfontti, joka on sama kuin `body`llä:
     jos woff2 ei lataudu, otsikko ei muutu kursiiviksi eikä serifiksi vaan
     näyttää siltä miltä sivu näytti ennen tätä työtä. */
  --font-heading: 'Metropolis', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* FOKUSRENGAS ON TOKEN, KOSKA POHJAN VÄRI VAIHTUU. Korostus näkyy vaalealla
     (13,43:1) muttei tummansinisellä palkilla, jossa se on käytännössä sama
     väri kuin pohja - ja näppäimistöllä liikkuva käyttäjä kadottaisi juuri
     siinä kohtaa sen, missä hän on. Tummat pinnat asettavat arvon uudelleen
     omassa säännössään; ne ovat ainoat kolme kohtaa, joissa arvo vaihtuu. */
  --color-focus: var(--color-accent);
  /* VALITUN ILMOITTAUTUMISTILAN VÄRIT (rinki/CLAUDE.md, "Väri kertoo VALITUN
     tilan"). IN EI KÄYTÄ --color-accentia, ja PERUSTELU EI OLE SÄVY VAAN
     MERKITYS: accent on sivun ENSISIJAISEN toiminnon väri, ja accent-värinen
     IN-nappi luettaisiin "paina tästä" -napiksi silloinkin kun mitään ei ole
     valittu. Omalla tokenilla valittu tila on oma merkityksensä eikä
     toiminnon kehotus. (Sääntö kirjoitettiin kun accent oli vihreä ja
     perustelu kuului "vaikka sekin on vihreä"; ilme B teki accentista
     sinisen, eikä sääntö silti muutu - se ei koskaan ollut sävystä kiinni.)
     Väri on KOLMAS kerros: napissa lukee aina IN tai OUT, ja valitussa on
     lisäksi merkki sanan edessä - ✓ IN-napissa, ✗ OUT-napissa - joten
     punavihreä ei yksin kanna tilaa eikä edes sitä, kumpi on valittu. */
  --color-signup-in: #2e7d32;
  --color-signup-out: #b3261e;
  --color-signup-contrast: #ffffff;
  /* RINGIN OSALLISTUJARYHMÄN SÄVYT. Samat arvot kuin --color-fill-full-surface
     ja --color-fill-low-surface alempana, mutta ERI TOKENIT TARKOITUKSELLA -
     sama kuvio kuin --color-danger vs --color-signup-out: ryhmän sävy ei ole
     täyttöaste, eikä toisen säätö saa liikuttaa toista. Otsikkorivin sävyt
     (`-head`) ovat hueen sovitettu vastine --color-surfacelle: pariteetti
     valkoisen taulukon otsikkoriviin (1,16:1), ei uusi korostus. Kontrastit:
     leipäteksti yli 14:1, --color-muted 5,19:1 OUT-sävyllä, otsikkoteksti yli
     12:1. Mittaukset: toteutusratkaisut.md, "Rinki: IN- ja OUT-ryhmien
     erottelu". */
  --color-signup-in-surface: #e4f2e6;
  --color-signup-in-head: #cbe4d1;
  --color-signup-out-surface: #fdeceb;
  --color-signup-out-head: #f7d5d1;
  --color-surface: #eceef3;
  /* PUNAINEN HUOMIO JA TUHOAVA TOIMINTO. Sama arvo kuin
     `--color-signup-out`illa, mutta ERI TOKEN TARKOITUKSELLA: erottelu on
     semanttinen (rinki/CLAUDE.md, "Väri kertoo VALITUN tilan"). OUT on
     valittu tila eikä varoitus, ja jos toisen sävyä joskus säädetään, toisen
     ei pidä liikkua mukana. */
  --color-danger: #b3261e;
  --color-danger-contrast: #ffffff;
  --color-danger-surface: #fdf1f0;
  /* HALUKKUUDEN TILAVÄRIT (shifts/CLAUDE.md, "Halukkuus ≠ vuoro"): solun
     pohja ja painetun 👍-painikkeen täyttö. Täyttö on SAMA ARVO kuin
     `--color-signup-in`, mutta ERI TOKEN TARKOITUKSELLA - sama kuvio kuin
     `--color-danger` vs `--color-signup-out`: Ringin ilmoittautumistila ja
     lakanan halukkuus ovat eri merkityksiä, eikä toisen säätö saa liikuttaa
     toista. Kontrasti valkoinen teksti / täyttö 5,1:1. */
  --color-availability-surface: #dff0e4;
  --color-availability-fill: #2e7d32;
  --color-availability-contrast: #ffffff;
  /* TILAMERKIN TÄYTTÖASTEVÄRIT (toteutusratkaisut.md, "Tilamerkit"). Kolme
     paria: vaalea tausta + tumma teksti, koska merkki on TILA eikä toiminto.

     TÄYSI EI OLE --color-accent: accent on ensisijaisen toiminnon väri, ja
     accent-värinen merkki luettaisiin napiksi - sama peruste kuin Rinkin
     IN-napin omalla tokenilla. KEVYT PUNAINEN EI OLE --color-dangerin täysi
     sävy: vajaa täyttöaste on tila eikä tuhoava toiminto.

     ILME B EI MUUTTANUT NÄITÄ: täyttöasteen merkit ovat tilavärejä, eivät
     ilmettä, ja ne pysyivät ennallaan kun accent vaihtui siniseksi. Sävyjen
     ero accentiin on nyt suurempi kuin ennen, mikä on sääntöä myöten oikein.

     KONTRASTIT MITATTU (tekstin ja merkin oman taustan välillä; vaatimus
     4,5:1): vihreä 6,80:1, oranssi 5,62:1, punainen 6,41:1, neutraali
     (--color-muted / --color-surface) 5,11:1 uusilla arvoilla. Reunat sivun
     VALKOISTA taustaa vasten (vaatimus 3:1): 3,61:1 / 4,39:1 / 4,36:1 -
     merkit istuvat kortin tai taulukon valkoisella, eivät sivupohjalla.
     Mittaukset ja menetelmä: toteutusratkaisut.md, "Tilamerkit". */
  --color-fill-full-surface: #e4f2e6;
  --color-fill-full-border: #5c9367;
  --color-fill-full-text: #1b5e20;
  --color-fill-mid-surface: #fdf0d8;
  --color-fill-mid-border: #9c7118;
  --color-fill-mid-text: #8a5300;
  --color-fill-low-surface: #fdeceb;
  --color-fill-low-border: #c05a4e;
  --color-fill-low-text: #a02b23;
  /* LAKANAN SOLU, JOHON KATSOJAN TUOMARIKORTTI RIITTÄÄ (shifts/CLAUDE.md,
     "Kortin riittävyys halukkaalle"). Oma token eikä --color-fill-mid-surface,
     vaikka sävyt ovat sukua: täyttöasteen oranssi on VAJAUDEN merkki ja tämä
     on KUTSU, eikä toisen säätö saa liikuttaa toista - sama kuvio kuin
     --color-availability-surfacella ja --color-signup-inillä. Keltainen eikä
     vihreä: vihreä on halukkuuden oma väri (--color-availability-surface), ja
     sama solu voi olla molempia. */
  --color-card-fit-surface: #fbf3cd;
  /* KOLMAS HUOMIOLAJI: VAROITUS, JOSTA EI JÄÄ MITÄÄN TEKEMÄTTÄ
     (.claude/rules/kayttoliittyma.md, "Punainen huomio tarkoittaa yhtä asiaa"). Punainen
     tarkoittaa tässä sovelluksessa yhtä asiaa ja neutraali `.notice-shared`
     toista; keltainen on se väliin jäänyt tapaus, joka on tiedettävä ennen
     kuin jatkaa mutta joka ei estä eikä vaadi toimenpidettä - tänään Rinkin
     päällekkäisyysvaroitus.

     OMA TOKEN EIKÄ --color-card-fit-surface, vaikka sävyt ovat sukua: se on
     lakanan solun KUTSU ("korttisi riittää tähän"), ja toisen säätö ei saa
     liikuttaa toista - sama kuvio kuin sillä itsellään. Teksti jää
     --color-textiin, jonka kontrasti tälle pohjalle on n. 15:1; merkissä on
     aina sana, väri on kolmas kerros. */
  --color-caution-surface: #fdf6d9;
  --color-caution-border: #b88a1a;
  /* KATETTU LAKANARIVI (shifts/CLAUDE.md, "RIVIN SÄVY RIIPPUU VAIN
     KATTEESTA"): ottelun jokaiseen tuomarirooliin on otettu kantaa. Sävy on
     RIVIN, ei solun, ja sen on erotuttava --color-surfacesta (lukitun solun
     harmaa), joka istuu samalla rivillä - ero todennettu ruudulla, ei
     koodista. */
  --color-sheet-covered: #dbe7f6;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --radius: 6px;
}

* {
  box-sizing: border-box;
}

html {
  font-size: 16px;
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--color-text);
  background: var(--color-page-bg);
  line-height: 1.5;
}

/* YLÄPALKKI ON TUMMA, ja sen reunaviiva on POISTETTU eikä muutettu: viivan
   tehtävä oli erottaa vaalea palkki vaaleasta sivusta, ja tumman pinnan ja
   harmaan pohjan raja näkyy itsestään. */
.site-header {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--color-header-bg);
  color: var(--color-on-dark);
}

/* NAVIGAATIOPALKKI (`core/templates/core/_site_nav.html`). Sama HTML kaikilla
   leveyksillä (.claude/rules/kayttoliittyma.md, "Yksi käyttöliittymä, joka mukautuu").

   RIVI KÄÄRIYTYY, EI VIERITY, ja se on sama peruste kuin lakanan
   sisäkkäisellä pystyvierityksellä: `overflow-x: auto` tekisi palkista
   leikkaavan säiliön, ja pudotusvalikko katkeaisi juuri silloin kun se on
   auki. Viisi ryhmää mahtuu kapealla kahdelle riville - se on huonompi mutta
   ehjä, ja katkaistu valikko ei ole kumpikaan. */
.site-nav {
  padding: 0 var(--space-3);
  background: var(--color-nav-bg);
  color: var(--color-on-dark);
}

/* TUMMILLA PINNOILLA FOKUS ON LIME. Sääntö asettaa vain muuttujan, jolloin
   ylätunnisteen ja palkin jokainen linkki, nappi ja avaaja saa sen
   periytymällä - yhtäkään komponenttikohtaista fokussääntöä ei kirjoiteta. */
.site-header,
.site-nav {
  --color-focus: var(--color-accent-on-dark);
}

/* LEVEÄLLÄ RUUDULLA PUDOTUSVALIKKO ON VALKOINEN PANEELI, ja lime on siellä
   1,30:1 eli näkymätön: arvo palautuu korostukseen. Kapealla ruudulla sama
   valikko on virrassa tummalla palkilla, ja 30rem-lohko kumoaa tämän. */
.site-nav__menu {
  --color-focus: var(--color-accent);
}

.site-nav__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Ryhmä on linkki JA sen viereinen pudotus samassa rivissä. `position:
   relative` on valikon ankkuri leveällä ruudulla. */
.site-nav__item {
  display: flex;
  align-items: center;
  position: relative;
}

/* KOSKETUSKOHTEEN KORKEUS 44 px, sama vaatimus kuin lakanan roolirivillä
   (shifts/CLAUDE.md): palkki on sovelluksen useimmin napautettu kohde. */
.site-nav__link {
  display: inline-block;
  padding: var(--space-2) 0;
  min-height: 44px;
  line-height: 1.75;
  color: var(--color-on-dark);
  text-decoration: none;
  font-weight: 600;
}

.site-nav__link:hover {
  text-decoration: underline;
}

/* AKTIIVINEN OSIO EI OLE VÄRIN VARASSA: alleviivaus kantaa tiedon myös
   harmaasävyisenä ja värisokealle, ja `aria-current` kertoo sen
   ruudunlukijalle. Väri on kolmas kerros - sama periaate kuin tilamerkeillä
   (.claude/rules/kayttoliittyma.md, "Tilamerkin muoto ja täyttöasteen rajat"). */
.site-nav__item--current .site-nav__link {
  color: var(--color-accent-on-dark);
  box-shadow: inset 0 -3px 0 0 var(--color-accent-on-dark);
}

/* Pudotuksen avaaja. `list-style: none` poistaa selaimen oman kolmion, jonka
   tilalla on oma merkki - selainten oma kolmio on eri kohdassa ja eri
   kokoinen eri selaimissa. */
.site-nav__summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  min-height: 44px;
  color: var(--color-on-dark-muted);
  cursor: pointer;
  list-style: none;
}

.site-nav__summary::-webkit-details-marker {
  display: none;
}

.site-nav__more[open] .site-nav__summary {
  color: var(--color-on-dark);
}

/* Auki oleva valikko LEVEÄLLÄ RUUDULLA on päällekkäinen paneeli: palkki ei saa
   hypätä pidemmäksi kohdistuksesta. Kapealla ruudulla sama valikko on
   virrassa, ks. 30rem-lohko. */
.site-nav__menu {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  min-width: max-content;
  margin: 0;
  padding: var(--space-2) 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
  list-style: none;
}

/* VIIMEINEN RYHMÄ AVAA VASEMMALLE. Ilman tätä oikeanpuoleisimman ryhmän
   ("Seuran ylläpito") paneeli voi ylittää ikkunan oikean reunan ja synnyttää
   SIVUTASON vaakavierityksen - juuri sen, jota kapean ruudun kierros mittaa. */
.site-nav__item:last-child .site-nav__menu {
  left: auto;
  right: 0;
}

.site-nav__menu a {
  display: block;
  padding: var(--space-2) var(--space-3);
  color: var(--color-text);
  text-decoration: none;
  white-space: nowrap;
}

.site-nav__menu a:hover,
.site-nav__menu a:focus {
  background: var(--color-surface);
  text-decoration: underline;
}

.site-nav__menu [aria-current="page"] {
  font-weight: 600;
  color: var(--color-accent);
}

/* OMA NIMI JA ULOSKIRJAUTUMINEN OMAT-PUDOTUKSESSA (R7a). Ne siirtyivät
   ylätunnisteesta tänne, koska ylätunniste ja palkki veivät yhdessä 375 x 667
   px:n ruudulla 184 px eli 27,6 % näkymän korkeudesta (`core/CLAUDE.md`, "OMA
   NIMI JA ULOSKIRJAUTUMINEN OVAT OMAT-PUDOTUKSESSA").

   NIMI ON EROTINVIIVAN YLÄPUOLELLA OLEVA TEKSTI EIKÄ LINKKI: se kertoo kuka on
   sisällä, eikä siitä ole mihinkään mentävää - oma henkilösivu on saman
   valikon "Henkilösivu". Viiva erottaa sivulistan tästä parista, jotta valikko
   ei näytä yhdeltä tasaiselta luettelolta, jossa nimi olisi kuudes sivu. */
.site-nav__user {
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-2);
  color: var(--color-muted);
  font-size: 0.85rem;
  white-space: nowrap;
}

/* Uloskirjautumisnappi latoutuu kuin valikon linkki: sama pehmuste ja sama
   hover, jotta rivi ei erotu napiksi keskellä linkkilistaa. */
.site-nav__menu form {
  margin: 0;
}

.site-nav__menu button {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  color: var(--color-text);
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
}

/* `:not(:disabled)` samasta syystä kuin toissijaisella napilla
   (.claude/rules/kayttoliittyma.md, "Estetty nappi näyttää estetyltä"): tämän valikon nappi
   ei tällä hetkellä voi olla estetty, mutta se on havainto eikä sääntö.
   `:focus` ei tarvitse ehtoa - estetty nappi ei ole fokusoitava. */
.site-nav__menu button:not(:disabled):hover,
.site-nav__menu button:focus {
  background: var(--color-surface);
  text-decoration: underline;
}

/* Alatunniste on tarkoituksella vaatimaton: sen ainoa sisältö on
   tietosuojaselosteen linkki, jonka on löydyttävä joka sivulta mutta joka ei
   saa kilpailla sivun oman sisällön kanssa. Sama pehmuste kuin
   ylätunnisteella, jotta sivu päättyy samaan rytmiin kuin alkaa. */
.site-footer {
  padding: var(--space-3);
  border-top: 1px solid var(--color-border);
  font-size: 0.85rem;
}

/* JULKAISUN VERSIO (core/release.py): pieni ja vaimea, oma rivinsä linkin
   alla. Tieto on ylläpitäjälle ja tukipyyntöön, ei lukijalle. */
.site-footer__version {
  display: block;
  margin-top: var(--space-1);
  color: var(--color-muted);
  font-size: 0.75rem;
}

/* TUOTENIMI ON OTSIKKOFONTISSA, vaikka se ei ole otsikko: se on ainoa
   Lukkaria itseään nimeävä teksti, joka on joka sivulla. */
.site-header__brand {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--color-on-dark);
  text-decoration: none;
}

.site-header__club {
  color: var(--color-on-dark-muted);
}

/* Paluu seuranvalintaan. Linkin näköinen mutta hillitty: se on navigointia,
   ei sivun toiminto. */
/* LIME ON TÄSSÄ, KOSKA POHJA ON TUMMA (.claude/rules/kayttoliittyma.md, "Ilme on sukulainen,
   ei kopio"). Valkoisella se olisi 1,30:1 eli näkymätön; yläpalkilla
   12,82:1. Linkki on palkin ainoa toiminto, joten se on myös ainoa kohta,
   jossa lime kantaa merkitystä eikä ole koristetta. */
.site-header__link {
  color: var(--color-accent-on-dark);
  font-size: 0.9rem;
  text-decoration: none;
}

.site-header__link:hover {
  text-decoration: underline;
}

/* Kirjautuneen nimi ja uloskirjautuminen työntyvät oikeaan reunaan. Nimi on
   himmennetty: se on tilannetieto, ei toiminto. */
.site-header__user {
  margin-left: auto;
  color: var(--color-on-dark-muted);
}

.site-header__logout {
  margin: 0;
}

/* Tekstipainike: uloskirjautuminen on POST-lomake, mutta sen ei pidä huutaa
   ylätunnisteessa kuin ensisijainen toiminto. */
.linklike {
  padding: 0;
  border: none;
  background: none;
  color: var(--color-accent);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

.linklike:hover {
  opacity: 0.8;
}

/* Ylätunnisteen uloskirjautuminen on TUMMALLA POHJALLA, eikä `--color-accent`
   näy siellä (13,43:1 valkoista vasten tarkoittaa, että se on lähes musta).
   Navigaatiovalikon sama nappi EI tarvitse tätä: `.site-nav__menu button` on
   tarkempi valitsin ja hoitaa sekä leveän valkoisen paneelin että kapean
   tumman virran. */
.site-header .linklike {
  color: var(--color-on-dark);
}

/* Sisältöpalstan leveys on PORRAS, ja portaat asetetaan muuttujaan eikä
   toistamalla max-width-riviä jokaisessa muunnelmassa. Oletus on tekstisivun
   40rem; muunnelmat (--auth, --landing, --data, --wide) vaihtavat vain arvon.
   Portaiden arvot ja niiden mittaukset: toteutusratkaisut.md, "Sisältöpalstan
   leveysporras datataulukoille". */
.site-main {
  --main-width: 40rem;
  max-width: var(--main-width);
  margin: 0 auto;
  padding: var(--space-4) var(--space-3);
}

/* OTSIKOT OVAT METROPOLISISSA, LEIPÄTEKSTI EI (.claude/rules/kayttoliittyma.md, "Ilme on
   sukulainen, ei kopio"). Rajaus on tarkoituksellinen eikä puolitiehen
   jäänyt: taulukko, lomake ja navigaatio ovat tiheää käyttöliittymätekstiä,
   jossa järjestelmäfontti on luettavampi ja joka tapauksessa jo ladattu.

   PAINO ON 700, ja se on ainoa ladattu leikkaus: `font-weight: bold` (700)
   osuu siihen suoraan, eikä selain joudu tekemään synteettistä lihavointia.
   Selaimen oletus `h1`-`h6`:lle on jo `bold`, joten painoa ei kirjoiteta
   tähän uudelleen. */
h1, h2, h3 {
  font-family: var(--font-heading);
  line-height: 1.25;
}

/* VERSAALIOTSIKOITA EI OLE, JA SE ON MITATTU PÄÄTÖS EIKÄ UNOHDUS.

   Ilme B:n suunnitelmassa `h1` ja `h2` olivat versaalilla (`text-transform`,
   ei pohjien tekstiä muuttaen), ja päätöksessä oli purkuehto: jos pisin
   otsikko katkeaa sanan keskeltä 390 px:n ruudulla 16 px:n juurifontilla,
   versaali puretaan. EHTO TÄYTTYI (mitattu 2026-09-11, 30 näkymää):

     PALLOJUNIORIKORVAUKSET   pisin sana 469 px / palsta 374 px
     TUOMARIVELVOITTEET       pisin sana 388 px / palsta 374 px

   Molemmat katkesivat VAIN versaalilla - pienaakkosin kumpikin mahtuu
   rivilleen. Suomen yhdyssana on se, joka ei siedä versaalin leveyttä, eikä
   vika ole yksittäisessä otsikossa: jokainen tuleva yhdyssanaotsikko osuu
   samaan. `letter-spacing`in poisto olisi tuonut noin 2 %, kun tarvittiin
   25 %.

   ÄLÄ PALAUTA VERSAALIA ILMAN UUTTA MITTAUSTA - ja mittaa silloin PISIN
   SANA, ei otsikon laatikkoleveyttä: laatikko on aina palstan levyinen, eli
   se näyttää mahtuvan silloinkin kun sana ei mahdu. Luvut ja menetelmä:
   toteutusratkaisut.md, "Ilme B: pesis-sukulaisilme kokeiluhaaraan". */

/* OTSIKKO EI SAA VIERITTÄÄ SIVUA VAAKAAN.

   Suomen yhdyssanat ovat pitkiä ja otsikot ovat sivun suurinta tekstiä, joten
   ne ovat se paikka, jossa yksi sana ei mahdu rivilleen. Mitattu: 390 px:n
   ruudulla 20 px:n juurifontilla palkkiokoosteen "Tuomaripalkkiokooste" oli
   418 px leveä 370 px:n palstassa ja vieritti KOKO SIVUA 39 px. Normaalilla
   juurifontilla sama otsikko mahtui tasan (374 vs. 374 px), eli vika oli
   piilossa yhden pikselin päässä - se löytyi vasta kahden tekstikoon
   kierroksella (.claude/rules/kayttoliittyma.md, "KAPEA RUUTU TODENNETAAN KAHDELLA
   TEKSTIKOOLLA").

   `break-word` EIKÄ `anywhere`: edellinen katkaisee sanan vasta kun se ei
   mahdu omalle rivilleen eikä vaikuta min-content-leveyteen. `anywhere` (jota
   kortin solut käyttävät, koska ne ovat flex-itemejä joiden ON kutistuttava)
   kaventaisi otsikoita myös silloin kun tilaa on. Sääntö on siis: muuta vain
   se, mikä on jo rikki.

   PIIRI ON h1-h6 EIKÄ h1-h3 kuten yllä olevan `line-height`in: se on
   tyylivalinta käytetyille tasoille, tämä on vikasuoja jokaiselle. */
h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

/* OSOITE `<code>`-ELEMENTISSÄ EI SAA VIERITTÄÄ SIVUA VAAKAAN.

   Sama vika kuin otsikoilla yllä, mutta pahempi ja eri syystä: URL on YKSI
   pitkä merkkijono ilman välilyöntejä, eikä siinä ole yhtäkään kohtaa, josta
   selain katkaisisi sen itsestään. Mitattu 2026-09-22 kalenterisyötteen
   osoitteella: 390 px:n ruudulla `<code>`in oikea reuna oli 527 px (ja 20 px:n
   juurifontilla 659 px), eli sivu vieritti vaakaan molemmilla tekstikoolla.
   Julkisen lakanan linkki on sama tapaus - siksi sääntö on ELEMENTILLÄ eikä
   sivukohtaisena luokkana.

   `anywhere` EIKÄ `break-word`, toisin kuin otsikoilla: `break-word` ei vaikuta
   min-content-leveyteen, joten se ei estä elementtiä vaatimasta koko URLin
   levyistä tilaa. Osoite ON juuri se sisältö, joka saa katketa mistä tahansa -
   se luetaan kopiointipainikkeella eikä silmällä. */
code {
  overflow-wrap: anywhere;
}

p {
  max-width: 65ch;
}

a {
  color: var(--color-accent);
}

/* Lomakkeet */

form p {
  margin: 0 0 var(--space-3);
}

label {
  display: block;
  font-weight: 600;
  margin-bottom: var(--space-1);
}

/* Kentän LEVEYSKATTO on eri asia kuin globaali width:100% (ks. .claude/rules/kayttoliittyma.md,
   "Globaali `input { width: 100% }` on ansa"): 100% on kapean palstan ratkaisu,
   ja se muuttuu haitaksi heti kun palsta levenee (.site-main--data 76rem).
   Nimikenttä koko palstan levyisenä on huonompi lopputulos kuin ennen
   leventämistä. Katto ei koske kenttiä, joille on annettu oma leveys
   (.sheet-filters, .fee-cell, .ride-search) - niiden omat säännöt ovat
   tarkempia ja voittavat. */
input,
select,
textarea {
  width: 100%;
  max-width: 32rem;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font: inherit;
  background: var(--color-bg);
  color: var(--color-text);
}

/* Valintaruutu ja radionappi EIVÄT ole tekstikenttiä: width:100% venyttää ne
   koko lomakkeen leveydeksi (allauthin "Muista minut" ui keskellä sivua).
   Tämä poistaa syyn .check-inline- ja .sheet-filters__check-korjauksilta,
   mutta ne jäävät paikoilleen - ne tekevät myös muuta (rivitys, nowrap). */
input[type="checkbox"],
input[type="radio"] {
  width: auto;
}

/* NUMEROKENTTÄ EI OLE PALSTAN LEVYINEN. Globaali `width: 100%` on kirjoitettu
   tekstikentille, mutta lukukenttä (rivien määrä, lisättävien paikkojen määrä,
   lukituksen viikot ja päivät, syntymävuosi) on kolmen merkin mittainen - koko
   palstan levyinen kenttä lupaa arvon, jota siihen ei mahdu kirjoittaa.

   TARKKUUS ON SAMA (0,1,1) KUIN `.fee-cell input`illa ja
   `.settings-form--inline input`illa, joten tämä on kirjoitettu ENNEN niitä:
   ne ovat myöhempinä vahvempia ja saavat pitää omat leveytensä. */
input[type="number"] {
  width: auto;
  max-width: 8rem;
}

/* KENTÄN APUTEKSTI OMALLE RIVILLEEN KENTÄN ALLE. Kenttä on `width: 100 %` ja
   `max-width: 32rem`, joten leveällä palstalla sen perään kirjoitettu inline-
   teksti alkaa kentän VIERESTÄ ja katkeaa keskeltä seuraavalle riville - ja
   näyttää silloin kuuluvan seuraavaan kenttään.

   KAKSI VALITSINTA, YKSI SÄÄNTÖ: `.helptext` on Djangon `as_p`:n oma luokka
   (esteen lomake lakanalla), `.field-help` käsin kirjoitettujen lomakkeiden
   (Rinki). Kolmatta ei lisätä - uusi lomake käyttää jompaakumpaa. */
.field-help,
.helptext {
  display: block;
  font-size: 0.85rem;
  color: var(--color-muted);
}

input:focus,
select:focus,
textarea:focus {
  outline: 2px solid var(--color-focus);
  outline-offset: 1px;
}

/* YLÄPALKIN JA NAVIGAATION FOKUS KIRJOITETAAN AUKI, eikä selaimen omaan
   renkaaseen luoteta: Chromen oletus on tummalla pohjalla tumma eikä osu
   `--color-focus`-tokeniin lainkaan. `outline-offset` on positiivinen, jotta
   rengas ei jää linkin oman tekstin päälle. */
.site-header a:focus-visible,
.site-header button:focus-visible,
.site-nav a:focus-visible,
.site-nav summary:focus-visible,
.site-nav button:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

button,
.button {
  display: inline-block;
  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius);
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}

button:hover,
.button:hover {
  opacity: 0.9;
}

/* NEUTRAALIN OHJAIMEN TÄYTTÖ ON `--color-bg` (.claude/rules/kayttoliittyma.md, "NEUTRAALIN
   OHJAIMEN TÄYTTÖ ON VALKOINEN"). `--color-surface` oli oikea valinta niin
   kauan kuin sivupohja oli valkoinen, mutta harmaalla pohjalla se on lähes
   pohjan sävy: mitattu dE 2,38 (valkoisella 4,12). Nappi näytti himmeältä,
   melkein estetyltä, juuri siellä missä se on tavallisin - etusivun "Kaikki
   tapahtumat", lakanan jaksonapit ja suodatinlomakkeen "Näytä".

   REUNA KANTAA VALKOISELLA KORTILLA, jossa nappi on pohjansa värinen: reuna
   erottuu siellä dE 13,66 (ennen napin oma täyttö reunasta 7,23). Nappi ei
   siis katoa kortille vaan muuttuu tavalliseksi reunalliseksi napiksi. */
button.secondary,
.button.secondary {
  background: var(--color-bg);
  color: var(--color-text);
  border: 1px solid var(--color-border);
}

/* TUHOAVA TOIMINTO KAHDELLA TASOLLA (.claude/rules/kayttoliittyma.md, "Tuhoavalla napilla on
   kaksi tasoa"). `.danger` yksin on peruuttamattoman vahvistuksen nappi;
   `.secondary.danger` on rivikohtainen Poista pitkässä listassa.

   MIKSI KAKSI: pelkkä täyspunainen kaikkialla hallitsisi joukkuesivun
   kaltaista listaa, jossa jokaisella rivillä on Poista - punaisesta sarakkeesta
   tulisi sivun päävärialue, eikä se enää erottaisi mitään. Pelkkä punainen
   teksti taas ei riitä vahvistussivulle, jossa nappi on sivun ainoa ja
   peruuttamaton toiminto.

   JÄRJESTYS ON SÄÄNTÖ: `button.danger` on yhtä tarkka kuin `button.secondary`
   (0,1,1), joten se on kirjoitettava sen JÄLKEEN. `.secondary.danger` (0,2,1)
   voittaa molemmat tarkkuudellaan, mutta se on silti viimeisenä, jotta
   lukujärjestys vastaa voimajärjestystä.

   VÄRI EI OLE AINOA MERKKI: napissa lukee aina mitä se tekee ("Poista",
   "Poista sarjan tulevat"), ja peruuttamattomilla on lisäksi oma
   vahvistussivunsa tai `data-confirm`. Sama kolmikerroksisuus kuin IN/OUT-
   napeilla. */
button.danger,
.button.danger {
  background: var(--color-danger);
  color: var(--color-danger-contrast);
  border: 1px solid var(--color-danger);
}

button.secondary.danger,
.button.secondary.danger {
  background: var(--color-bg);
  color: var(--color-danger);
  border: 1px solid var(--color-danger);
}

/* HOVER SAA NÄKYVÄN TÄYTÖN, koska `opacity: 0.9` EI NÄY valkoisen napin
   päällä: mitattu dE 0,35 harmaalla sivupohjalla ja 0,00 valkoisella
   kortilla, kun täyttövaihto on 6,49. Läpinäkyvyys toimi harmaalla napilla,
   ei enää valkoisella - sama muutos, joka teki napista näkyvän, teki sen
   hoverista näkymättömän.

   `:not(:disabled)` ON OSA SÄÄNTÖÄ EIKÄ VARMUUDEN VUOKSI (.claude/rules/kayttoliittyma.md,
   "Estetty nappi näyttää estetyltä"): ilman sitä estetty toissijainen nappi
   vaihtaisi väriä hiiren alla eli näyttäisi reagoivan juuri silloin kun se ei
   reagoi.

   SYY EI OLE TARKKUUS VAAN ERI OMINAISUUS. `button:disabled:hover` asettaa
   `opacity`n ja `cursor`in muttei taustaa lainkaan, joten se ei voi kumota
   tätä millään tarkkuudella - kaskadi ratkaisee ominaisuus kerrallaan.
   Estetyn napin voi siis sulkea pois VAIN valitsimessa. Sama koskee jokaista
   tulevaa hover-sääntöä, joka vaihtaa taustan.

   EHTO ON MOLEMMISSA OSISSA, EIKÄ VAIN `button`-ALKUISESSA. Luokkamuoto
   `.button.secondary` on tällä hetkellä aina `<a>`, jolla ei ole estettyä
   tilaa - mutta se on HAVAINTO TYÖPUUSTA eikä sääntö, jota mikään valvoo:
   `<button class="button secondary">` on täysin kelvollinen ja lakkaisi
   hiljaa noudattamasta estetyn napin sääntöä. Toinen `:not(:disabled)` maksaa
   yhden sanan ja poistaa oletuksen kokonaan.

   YKSI SÄÄNTÖ RIITTÄÄ MYÖS `.secondary.danger`ILLE: se asettaa vain taustan,
   joten punainen teksti ja reuna jäävät voimaan. */
button.secondary:not(:disabled):hover,
.button.secondary:not(:disabled):hover {
  background: var(--color-surface);
}

/* ESTETTY NAPPI NÄYTTÄÄ ESTETYLTÄ - YLEISENÄ SÄÄNTÖNÄ, ei komponentti
   kerrallaan (.claude/rules/kayttoliittyma.md, "Estetty nappi näyttää estetyltä"). Tähän asti
   tyyli oli vain `.confirm-toggle`illa, joten mm. julkaisunappi ilman
   tietosuojayhteystietoa ja seuravastaavan oma oikeudenpoisto näyttivät
   tavallisilta napeilta, vaikka pohjassa on `disabled`.

   HOVER KUMOTAAN ERIKSEEN: `button:hover { opacity: 0.9 }` on yhtä tarkka
   (0,1,1) kuin tämä, joten ilman omaa hover-valitsintaan estetty nappi
   KIRKASTUISI hiiren alla - eli näyttäisi reagoivan juuri silloin kun se ei
   reagoi.

   SYY EI OLE VÄRISSÄ VAAN TEKSTISSÄ: jokainen estetty nappi tässä
   sovelluksessa kertoo syyn viereisessä tekstissä (`.confirm-reason`,
   `.notice-warning`, asetussivun `block_reason`). Himmennys kertoo vain, ettei
   nappi ole käytettävissä. */
button:disabled,
button:disabled:hover,
.button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Sivun alalaidan navigointi: YKSI JAETTU KOMPONENTTI, ei sivukohtaisia
   sääntöjä (.claude/rules/kayttoliittyma.md).

   Linkkirivi on useimmilla sivuilla ainoa poistumisreitti, ja paljaana
   <p><a>-luettelona se ei erottunut leipätekstistä lainkaan. Jos jokin sivu
   tarvitsee poikkeuksen, se on löydös eikä uusi luokka.

   `flex-wrap` EI OLE HIENOSÄÄTÖÄ vaan vikasuoja: napit ovat sivun leveimpiä
   yksittäisiä elementtejä, ja isommalla tekstikoolla (20 px juurifontti)
   rivittymätön nappirivi vierittäisi koko SIVUN vaakaan - sama ansa kuin
   kausirivin kentillä (.settings-form--inline).

   `gap` erottaa napit toisistaan, joten &middot;-erottimia ei tarvita eikä
   niitä ole.

   SÄÄNTÖ ON ENNEN @media print -lohkoa TARKOITUKSELLA: osa riveistä kantaa
   myös `print-hide`-luokan (palkkiokooste, tapahtumasivu), ja koska molemmat
   ovat yhden luokan tarkkuisia, myöhempi voittaa. Jos tämä sääntö siirtyisi
   printtilohkon jälkeen, nappirivi ilmestyisi tulosteeseen. */
.page-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin: var(--space-4) 0 var(--space-3);
}

/* OSIORIVI (`core/templates/core/_section_page_nav.html`) ON OMA LOHKONSA,
   EI KOLMAS PERÄKKÄINEN `.page-nav`-RIVI. Sivun omat painikkeet ("Kopioi
   taksat", "Pallojuniorikorvaus") ovat tämän sivun TOIMINTOJA; osiorivi on
   OSOITE osion muille sivuille. Ennen tätä ne olivat samannäköisiä nappeja
   allekkain (palkkiosivulla kolme riviä), eikä rajaa näkynyt.

   ERO ON RAKENTEESSA, EI SIVUKOHTAISESSA SÄÄNNÖSSÄ: reunaviiva ja otsake
   erottavat rivin sivun sisällöstä, ja napit ovat kevyempiä (pienempi
   pehmuste, normaali paino). Linkit ovat yhä `.page-nav`in `.button
   secondary` -nappeja, joten komponentin eheysvahti koskee niitä
   (`core/test_docs.py:TemplateStructureTests.test_every_page_nav_link_is_a_button`).

   KEVEÄT NAPIT KOSKEVAT KAIKKIA LEVEYKSIÄ: ne rivittyvät vierekkäin, ja
   kosketuskohde on yli 24 px (WCAG 2.5.8) myös 16 px:n juurifontilla.
   KAPEALLA RIVI ON LISÄKSI SULJETTAVA `<details>`, koska keveys ei riittänyt:
   kahdeksan nappia vei 390 px:llä ja 20 px:n juurifontilla seitsemän riviä
   eli lähes puolet ruudusta (mitattu 2026-09-23). Rakenne on sama kuin
   `.filter-details`illä alempana, ja sama rappeutuma pätee: ilman
   `::details-content`-tukea leveä ruutu näyttää avaajan eikä piilota
   linkkejä saavuttamattomiin. Lyhyt rivi renderöidään auki
   (`core/templatetags/lukkari_nav.py:SECTION_NAV_OPEN_MAX`). */
.section-nav {
  margin: var(--space-4) 0 var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.section-nav__details > summary {
  cursor: pointer;
  padding: var(--space-2) 0;
  color: var(--color-accent);
  font-weight: 600;
}

/* Leveän ruudun otsake. Perustilassa piilossa: ilman `::details-content`-
   tukea avaaja on näkyvissä kaikilla leveyksillä, eikä sama sana saa
   renderöityä kahdesti. */
.section-nav__title {
  display: none;
  margin: 0 0 var(--space-2);
  font-size: 0.85rem;
  color: var(--color-muted);
}

.section-nav .page-nav {
  gap: var(--space-2);
  margin: 0;
}

.section-nav .button {
  padding: var(--space-1) var(--space-2);
  font-size: 0.9rem;
  font-weight: 400;
}

@media (min-width: 30.0625rem) {
  .section-nav__details::details-content {
    content-visibility: visible;
  }
}

@supports selector(::details-content) {
  @media (min-width: 30.0625rem) {
    .section-nav__details > summary {
      display: none;
    }

    .section-nav__title {
      display: block;
    }
  }
}

ul.errorlist {
  color: var(--color-danger);
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-1);
  font-size: 0.9rem;
}

/* KENTTÄKOHTAINEN VIRHE TAULUKON SOLUSSA (R7b). `ul.errorlist` on
   lomakepystyrivin virhelista; tämä on sen taulukkovastine yhdelle kentälle,
   jonka vieressä ei ole tilaa listalle. Sama väri ja sama koko - ero on vain
   siinä, että tämä latoutuu kentän alle omalle rivilleen. */
.field-error {
  display: block;
  margin-top: var(--space-1);
  color: var(--color-danger);
  font-size: 0.9rem;
}

/* HYLÄTTY RIVI EROTTUU REUNAVIIVALLA, EI TAUSTAVÄRILLÄ. Taustaväri olisi
   peittänyt solujen kentät, ja punainen tarkoittaa tässä sovelluksessa yhtä
   asiaa (.claude/rules/kayttoliittyma.md, "Punainen huomio"): tästä rivistä jää jotain
   tekemättä, koska tallennus hylättiin. */
.settings-row--error > * {
  border-bottom-color: var(--color-danger);
}

/* AVATTAVA OHJE ("Miten luvut lasketaan"). Laskusäännöt olivat sivujen alussa
   kolmena kappaleena taulukon ja lukijan välissä (R7b); ne ovat luettavissa
   mutta eivät tiellä. `<details>` on natiivi eikä vaadi JavaScriptiä - sama
   peruste kuin navigaatiopalkin pudotuksella. */
.page-help {
  margin: 0 0 var(--space-3);
  font-size: 0.9rem;
  color: var(--color-muted);
}

.page-help > summary {
  cursor: pointer;
  color: var(--color-accent);
  /* Kosketuskohde: pelkkä tekstirivi on 20 px korkea. */
  padding: var(--space-2) 0;
}

.page-help > p {
  margin: 0 0 var(--space-2);
}

/* MONIKENTTÄINEN SUODATINLOMAKE ON KAPEALLA AVATTAVASSA KOHDASSA (R7b,
   2026-09-11). Neljän kentän suodatinrivi latoutuu 375 px:llä kahdeksalle
   riville ja työntää sivun sisällön ruudun ulkopuolelle; leveällä ruudulla
   sama rivi mahtuu yhdelle riville, eikä sitä saa piilottaa.

   RAKENNE ON PÄINVASTAINEN KUIN MILTÄ NÄYTTÄÄ: `<details>` on markkauksessa
   SULJETTU (ei `open`-attribuuttia), ja LEVEÄ ruutu avaa sen CSS:llä. Toisin
   päin tehtynä kapea ruutu ei voisi sulkea sitä - `open` on attribuutti, jota
   CSS ei voi poistaa. POIKKEUS: ottelu- ja tuomarilakana renderöi `open`in,
   kun paneelin omat suodattimet ovat voimassa (shifts/CLAUDE.md,
   "SUODATINPANEELI PYSYY AUKI") - leveällä se ei muuta mitään, koska sisältö
   on siellä joka tapauksessa näkyvissä.

   `::details-content { content-visibility: visible }` ON AINOA TAPA, JOKA
   TOIMII (mitattu 2026-09-11, Chrome 152): vanha kikka
   `details:not([open]) > *:not(summary) { display: block }` EI avaa sisältöä
   lainkaan - `<details>`in sisältö on UA-varjopuun slotissa, jota
   elementtisääntö ei tavoita.

   RAPPEUTUMA ON EHJÄ, JA SE ON EHTO: `@supports selector(::details-content)`
   piilottaa summaryn leveällä ruudulla VAIN siinä selaimessa, joka osaa avata
   sisällön. Ilman tukea leveä ruutu näyttää "Suodattimet"-avaajan, jota
   klikkaamalla lomake aukeaa - huonompi mutta ehjä, sama laji kuin
   `:has()`-rappeutumat (.claude/rules/kayttoliittyma.md). Ilman
   `@supports`ia lomake olisi tukemattomassa selaimessa SAAVUTTAMATON. */
.filter-details {
  margin-bottom: var(--space-3);
}

.filter-details > summary {
  cursor: pointer;
  padding: var(--space-2) 0;
  color: var(--color-accent);
  font-weight: 600;
}

@media (min-width: 30.0625rem) {
  .filter-details::details-content {
    content-visibility: visible;
  }
}

@supports selector(::details-content) {
  @media (min-width: 30.0625rem) {
    .filter-details > summary {
      display: none;
    }
  }
}

/* LAKANAN SUODATINPANEELIN YLÄNAPPI VAIN KAPEALLA RUUDULLA (shifts/CLAUDE.md,
   "SUODATINPANEELI PYSYY AUKI"). Auki jäävä paneeli on puhelimessa kymmenkunta
   riviä pitkä, joten alalaidan Näytä-painike jää ruudun alle; ylänappi on
   saman painikkeen toinen sisällytys samasta osapohjasta.

   PERUSTILA ON PIILOSSA, JA SE ON MEDIA QUERYN ULKOPUOLELLA: leveällä ruudulla
   nappeja on yksi kuten ennenkin, eikä yksikään leveän ruudun sääntö saa tuoda
   ylänappia takaisin (vahti shifts/tests.py:SheetFilterPanelTests).

   NÄYTTÖSÄÄNTÖ ON TIEDOSTON ENSIMMÄISESSÄ 30rem-LOHKOSSA, EI OMASSA LOHKOSSAAN
   TÄSSÄ: useampi lähdevahti pitää ensimmäistä kapean ruudun media queryn
   esiintymää kapeana lohkona ja kaikkea ennen sitä perustilana, joten uusi
   lohko ennen sitä siirtäisi niiden lukeman alueen hiljaa. Vahdit hakevat
   RAAKAMERKKIJONOLLA, joten sama pätee kommenttiin: tämän kohdan yläpuolelle
   ei kirjoiteta media queryn otsaketta sanatarkasti edes kommenttina. */
.sheet-filters__submit--top {
  display: none;
}

/* ILMOITTAUTUMISAJAN VIRKE on Oma ilmoittautuminen -lohkossa painikkeiden
   vieressä NORMAALINA tekstinä (R7b): himmennys kertoi vähemmän tärkeää, ja
   tämä on ehto sille, toimiiko nappi. */
.signup-deadline {
  margin: 0 0 var(--space-2);
}

/* Taulukot */

/* TAULUKKO ON VALKOINEN, KOSKA SIVUPOHJA ON HARMAA. Ilman tätä taulukko
   sulautuisi pohjaan eikä erottuisi sivun sisällöstä; nyt se on kortti muiden
   joukossa. Reuna EI ole tarpeen - vaalean pinnan raja harmaata vasten näkyy
   itsestään, ja reuna kilpailisi solujen omien viivojen kanssa. */
table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: var(--space-4);
  background: var(--color-bg);
}

th, td {
  text-align: left;
  padding: var(--space-2);
  border-bottom: 1px solid var(--color-border);
  vertical-align: top;
}

th {
  background: var(--color-surface);
  font-weight: 600;
}

/* Ottelu- ja tuomarilakana */

.site-main--wide {
  --main-width: none;
}

/* Datataulukkosivut: lista, jonka sarakkeet eivät mahdu tekstisivun palstaan.
   YKSI porras, ei kahta ("lista" / "leveä taulukko") - kaksi vaatisi arvion
   joka kerta kun sivulle lisätään taulukko, ja arvio ajautuisi erilleen.
   Porras lisätään vain mitatusti: jos taulukon luonnollinen leveys mahtuu
   oletukseen, sivu jää ennalleen eikä kolmen sarakkeen taulukko veny turhaan.
   Arvo tulee ylärajasta: 1440 px:n ikkunassa 76rem mahtuu palstaan ilman
   vaakavieritystä. ARVO EI OLE KATTAVUUSLUPAUS - neljä näkymää ylittää sen
   (mitattu 2026-09-07, levein /seura/tehtavat/ 1922 px), ja ne vierittyvät
   omassa .table-scroll-säiliössään. Se ei ole peruste kolmannelle portaalle
   vaan merkki siitä, että kyseisen taulukon tiheys on seuraava vipu; luvut ja
   aukko: toteutusratkaisut.md ja shifts/CLAUDE.md. */
.site-main--data {
  --main-width: 76rem;
}

/* Vieritys VAIN vaakaan: pystysuunta on sivun normaali vieritys (ks.
   shifts/CLAUDE.md "Sarakkeen tunnistus lakanassa"). Pystyakselia EI aseteta
   tässä - leikkaus kuuluu taulukolle, ei vierityskontille (ks. .sheet). */
.table-scroll {
  overflow-x: auto;
  /* POSITIOITU ESI-ISÄ ON OSA VIERITYSTÄ, EI KORISTETTA.

     `overflow` leikkaa absoluuttisesti positioidun jälkeläisen VAIN jos säiliö
     on sen sisältävä lohko. Ilman `position: relative`ä leveän taulukon
     `.visually-hidden`-labelit (position: absolute, ei left/top) jäävät
     staattiselle paikalleen kaukana oikealla, karkaavat kääröstä ja kasvattavat
     SIVUN vaakavieritystä - vaikka taulukko itse vierii säiliössään oikein.

     Sama juurisyy ja sama korjaus kuin `.sheet-cell`illä ("ilman positioitua
     esi-isää tyhjän tilaviestin position:absolute vuotaa sivutason
     vaakavieritykseksi"). Oire on petollinen, koska kääri ON paikallaan ja
     mittaus näyttää sen oikean levyisenä: vuoto ei ole taulukossa vaan
     labelissa. Mitattu asetussivun kausiosiossa: 88 px oletustekstillä,
     208 px 1,25x tekstillä. Ks. `docs/arkisto/toteutusrupeamat-2026-09.md`,
     "Pallojuniorikorvaus ja maksettu-tila". */
  position: relative;
}

/* KELLUVA VAAKAVIERITYSPALKKI (core/static/core/table_scroll.js). Skripti
   luo palkin vain ylivuotavalle kääreelle ja vain (pointer: fine) -laitteella,
   ja asettaa leveyden ja vaakasijainnin itse. Palkki on parannus eikä ehto:
   ilman JavaScriptiä kääre vierii kuten ennenkin. Perustelu:
   toteutusratkaisut.md, "Kelluva vaakavierityspalkki leveille taulukoille".

   `[hidden]` KIRJOITETAAN AUKI: luokan oma display-arvo voittaisi selaimen
   oletussäännön, ja piilotettu palkki jäisi ruudulle. `overflow-x: scroll`
   piirtää palkin aina, kun elementti näkyy.

   MINIMIKORKEUS ON OVERLAY-PALKKEJA VARTEN: kun käyttöjärjestelmän palkki
   ei vie tilaa (macOS, piilotetut palkit), elementin korkeus olisi sisällön
   1 px eikä palkkiin voisi tarttua. Tavallisella palkilla korkeus tulee
   palkin paksuudesta, ja minimi jää sen alle. */
.floating-scrollbar {
  position: fixed;
  bottom: 0;
  z-index: 10;
  min-height: 0.75rem;
  overflow-x: scroll;
  overflow-y: hidden;
  background: var(--color-bg);
  box-shadow: 0 -1px 0 var(--color-border);
}

.floating-scrollbar[hidden] {
  display: none;
}

.floating-scrollbar__inner {
  height: 1px;
}

/* Minimileveys, jonka alle sarakkeet eivät purista: tilan loppuessa (selaimen
   skaalaus tai kapea ikkuna) .table-scroll vierii vaakaan sen sijaan että
   jokainen sarake ahtautuisi ja rivittyisi.

   Arvo on mitattu selaimessa (taulukon sisäinen minimileveys, seed-data):
   tuomarivastaava 1724 px, tiivistila 1597 px, lukija 1551 px. Valittu 96rem
   (1536 px) on juuri kapeimman variantin alla, joten se ei pakota vieritystä
   jota sisältö ei jo itse vaatisi - se on regressiovahti sille päivälle, jona
   jokin sarake muuttuu kutistuvaksi. Aiempi 70rem (1120 px) jäi KAIKKIEN
   varianttien alle eli ei tehnyt mitään. */
.sheet {
  min-width: 96rem;
  /* Leikkaus on TAULUKOLLA eikä .table-scrollilla, ja se on sarakekorostuksen
     ehto. Overlay venyy 100vh solun ylä- ja alapuolelle; ilman leikkausta
     alaspäin menevä osa on VIERITETTÄVÄÄ ylivuotoa vierityskontissa - mitattu
     selaimessa 876 px, kun korostettu solu on taulukon alalaidassa. Se on
     täsmälleen se sisäinen pystyvieritys, joka purettiin.

     .table-scrollille asetettuna clip ei auta: pariksi jäävä overflow-x: auto
     pakottaa clipin laskemaan arvokseen hidden (CSS Overflow 3), jolloin
     ylivuoto jää olemassa ja kontti vierii kohdistuksesta - mitattu, ei
     arvattu. Taulukolla clip säilyy clipinä eikä kontti synny lainkaan:
     mitattu 0 px pystyylivuotoa, vaakavieritys ennallaan (732 px 1440 px:n
     ikkunassa) ja PVM-sarakkeen kiinnitys toimii yhä. */
  overflow: clip;
}

/* SARAKKEEN TUNNISTUS: kolme keinoa, jotka eivät ole toistensa vaihtoehtoja
   (ks. shifts/CLAUDE.md "Sarakkeen tunnistus lakanassa - akselit on eroteltu").
   Otsikkorivin KIINNITYS ON KUMOTTU 2026-08-07: se vaati .table-scrollille
   korkeusrajan, koska sticky kiinnittyy lähimpään vierivään esi-isään ja
   .table-scroll on sellainen jo overflow-x: auton takia - ja se korkeusraja oli
   se sisäinen pystyvieritys, joka kaappasi hiiren rullan. Perustelu ja hylätyt
   vaihtoehdot: toteutusratkaisut.md, "Sarakeotsikoiden kiinnitys (2026-08-07)".
   Regressiovahtina shifts/tests.py:SheetColumnIdentityTests.

   1. HAAMUTEKSTI tyhjässä solussa. Sarakkeen roolilyhenne ("2T") himmennettynä
   - juuri tyhjä solu on se, jota etsitään, ja siinä on tila kertoa itse mikä se
   on. Ei display:none-tyylistä piilotusta: teksti on solun ainoa sisältö. */
.cell-ghost {
  color: var(--color-muted);
  font-size: 0.85rem;
}

/* 2. PVM-SARAKE KIINNITETÄÄN VAAKASUUNTAAN. Tämä toimii nimenomaan siksi, että
   .table-scroll on vaakavieritettävä kontti - sama mekanismi, joka esti
   otsikkorivin kiinnityksen, mahdollistaa tämän.

   Reuna on box-shadow eikä border: border-collapse: collapse -taulukossa
   koottu reunaviiva piirtyy taulukolle eikä solulle, joten se jäisi
   vieritettäessä paikalleen ja kiinnitetty sarake jäisi ilman oikeaa reunaa.

   LÄPINÄKYMÄTÖN TAUSTA ON PAKOLLINEN: kiinnitetty solu asettuu vieritettäessä
   myös .cell--wanted- ja .cell--assigned-värjättyjen solujen päälle, ja ilman
   omaa taustaa ne kuultaisivat sen läpi. z-index on korkeampi kuin
   sarakekorostuksen overlaylla (1), jotta korostus ei maalaudu kiinnitetyn
   sarakkeen päälle. */
.sheet th:first-child,
.sheet td:first-child {
  position: sticky;
  left: 0;
  z-index: 3;
  box-shadow: inset -1px 0 0 var(--color-border);
}

.sheet td:first-child {
  background: var(--color-bg);
}

.sheet th:first-child {
  background: var(--color-surface);
}

/* KATETTU RIVI: jokaiseen tuomarirooliin on otettu kantaa (shifts/CLAUDE.md,
   "RIVIN SÄVY RIIPPUU VAIN KATTEESTA"). Sävy on RIVIN eikä solun väri, joten
   se maalautuu solujen ALLE: solutilat (.cell--wanted, .cell--assigned,
   lukitun harmaa) piirtyvät sen päälle omine taustoineen, eikä niiden
   :not()-ehtoihin kosketa. Näkyy molemmissa näkymissä; merkin SANA on vain
   kapean ruudun tiivistetyllä rivillä, leveällä sama tieto luetaan soluista.

   KIINNITETTY PVM-SARAKE TARVITSEE SÄVYN ERIKSEEN: `.sheet td:first-child`
   asettaa läpinäkymättömän oman taustan, joka on vaakavierityksen ehto eikä
   poistettavissa. Voitto tulee TARKKUUDESTA eikä tiedostojärjestyksestä
   (shifts/CLAUDE.md, "SOLUN VÄRISÄÄNTÖJEN TARKKUUS ON OSA SÄÄNTÖÄ"):
   `.sheet-row--covered > td:first-child` olisi (0,2,1) eli tasapeli
   `.sheet td:first-child`in kanssa (`:first-child` on pseudoluokka), ja
   tasapelin ratkaisisi järjestys. `.sheet .sheet-row--covered > td:first-child`
   on (0,3,1). Vahti laskee tarkkuudet: shifts/tests.py:SummaryStateBadgeTests. */
.sheet .sheet-row--covered {
  background: var(--color-sheet-covered);
}

.sheet .sheet-row--covered > td:first-child {
  background: var(--color-sheet-covered);
}

/* 3. OTSIKKORIVIN TOISTO taulukon sisällä. Rivi on <tr> tbodyn sisällä (useampi
   thead ei ole validia HTML:ää), ja sen solut tulevat samasta includesta kuin
   varsinainen otsikko. Toimii ilman JS:ää, tulostuu oikein eikä riipu
   vierityksen tilasta. Toistoväli: shifts/referee_sheet.py. */
.sheet-repeat th {
  background: var(--color-surface);
  border-top: 2px solid var(--color-border);
}

/* SARAKEKOROSTUS ilman JavaScriptiä - julkisessa lakanassa ei saa olla yhtään
   <script>-tagia (turvaraja), ja sisäiselläkin puolella skriptit ladataan vain
   ehdollisesti, joten JS-pohjainen korostus tarkoittaisi kolmea eri käytöstä
   samalla sivulla.

   Overlay venyy pystysuunnassa yli solun rajojen ja leikkautuu .sheetin
   overflow: clipiin - siksi se ei synnytä vieritettävää ylivuotoa. Sävy on
   LÄPINÄKYVÄ ja hyvin kevyt: tila on tieto (.cell--wanted, .cell--assigned,
   .cell--not-needed) ja korostus vain vihje, joten se ei saa kumota solun
   pohjaväriä vaan tummentaa sitä hitusen. pointer-events: none pitää valikon ja
   painikkeet klikattavina. Vain .sheet-cell eli vuorosarakkeet: PVM on
   kiinnitetty sarake, jonka päälle korostus ei kuulu. */
.sheet-cell:hover::after {
  content: '';
  position: absolute;
  top: -100vh;
  bottom: -100vh;
  left: 0;
  right: 0;
  z-index: 1;
  /* SÄVY JOHDETAAN KOROSTUKSESTA eikä ole oma literaalinsa: aiemmin tässä oli
     `rgba(31, 92, 61, 0.07)` eli korostuksen vanha arvo auki kirjoitettuna,
     ja se olisi jäänyt vihreäksi kun korostus vaihtui. `color-mix` on
     hyväksytty rappeutuma: jos selain ei tue sitä, sääntö hylätään ja
     korostus jää pois - hover-vihje katoaa, mutta solun oma tila (väri JA
     teksti) on tallella eikä mitään tietoa menetetä. */
  background: color-mix(in srgb, var(--color-accent) 7%, transparent);
  pointer-events: none;
}

/* position:relative on tyhjän tilaviestin takia, ei asettelun: .cell-status:empty
   on position:absolute, ja ilman positioitua esi-isää sen sijaintilaatikko on
   koko dokumentti - jolloin vaakaan vieritetyn taulukon oikeanpuoleisten
   solujen viestit työntyvät ulos sivusta ja koko SIVU saa vaakavierityksen
   .table-scrollin sisäisen vierityksen PÄÄLLE. Mitattu selaimessa 1280 px:n
   leveydellä: 177 px sivutason vieritystä, tällä säännöllä 0. */
.sheet-cell {
  min-width: 8rem;
  position: relative;
}

/* Solun valikon teksti keskitetään: valikko on width:100%, ja lyhyt sisältö
   (halukkaiden määrä "(2)" tyhjässä solussa) jäi vasempaan laitaan roikkumaan
   leveän kentän reunaan. Keskitys koskee koko lakanan solunvalikkoja, ei vain
   tuomariroolisoluja - muuten sarakkeet olisivat keskenään eri linjassa. */
.sheet-cell select {
  text-align: center;
}

/* Kapeat tekstisarakkeet (PVM, Kello, Kenttä, Sarja) eivät katkea: "la 6.6."
   kahdella rivillä sitoisi koko ottelurivin korkeuden, ja juuri rivittyminen
   räjäytti rivikorkeuden selaimen skaalauksella (ks. shifts/CLAUDE.md,
   "Lakanan on kestettävä selaimen skaalaus"). Ottelusarake on tietoisesti
   ILMAN tätä -
   pitkien joukkuenimien on katkettava jossain, muuten taulukko levenee
   loputtomiin. Tilan loppuessa .table-scroll vierii vaakaan. */
.col-nowrap {
  white-space: nowrap;
}

/* ESTEKOLMIO LAKANASSA (ks. shifts/CLAUDE.md, "Esteet ja saatavuus").

   Solu on kapea: sen ainoa sisältö suljettuna on kolmio. Este ei ole
   vuorosarake, joten se EI saa luokkaa .sheet-cell - se toisi 8rem
   minimileveyden ja sarakekorostuksen overlayn, joista kumpikaan ei kuulu
   tietosarakkeelle. */
.sheet-obstacle {
  white-space: nowrap;
  vertical-align: top;
}

/* Kolmio on PAINIKE eikä merkki (ks. sama luku), mutta se ei saa näyttää
   lomakepainikkeelta lakanan solussa: painikkeen oma tausta ja reunus veisivät
   koko kapean sarakkeen. Fokusrengas jätetään selaimen omaksi - se on ainoa
   vihje näppäimistökäyttäjälle siitä, mihin fokus osui. */
.obstacle__toggle {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  width: auto;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  color: inherit;
}

/* POPOVER ON NORMAALISSA VIRRASSA, EI position:absolute - ja se on mitattu
   päätös eikä laiskuus. Taulukolla on `overflow: clip` (sarakekorostuksen
   ehto, ks. .sheet), joten absoluuttisesti positioitu popover LEIKKAUTUISI
   taulukon alareunassa: viimeisen ottelurivin este olisi lukukelvoton juuri
   silloin kun jakso on lyhyt ja rivejä on yksi. Virrassa oleva popover
   levittää solua auki ollessaan - rivi kasvaa hetkeksi, mutta sisältö näkyy
   jokaisella rivillä.

   ESTETEKSTI ON DOM:ISSA MYÖS SULJETTUNA (palvelin renderöi sen, JS ei luo
   sitä), joten tuloste löytää sen ilman että popoveria on avattu. */
.obstacle__list {
  display: none;
  white-space: normal;
  max-width: 16rem;
  font-size: 0.85rem;
}

.obstacle__list ul {
  margin: var(--space-1) 0 0;
  padding-left: 1.1rem;
}

/* KLIKKAUS ON AINOA AVAUSTAPA KUN JAVASCRIPT ON LADATTU (ks. shifts/CLAUDE.md,
   "KOLMIO ON PAINIKE, EI MERKKI"). .obstacle--open tulee unavailability.js:stä
   eikä riipu osoittimen sijainnista, joten avaustila kestää sen, että popover
   levittää solua ja sarakeleveydet jaetaan uudelleen.

   :hover EI OLE AVAUSTAPA, JA SE ON KUMOTTU PÄÄTÖS EIKÄ UNOHDUS: popover on
   virrassa (yllä), joten hover-avaus siirsi kolmion pois osoittimen alta,
   hover katkesi, popover sulkeutui ja kolmio palasi osoittimen alle - silmukka
   värähteli auki ja kiinni lukukelvottomana (toteutusratkaisut.md,
   "Päätöshistoria: hover poistettiin avaustavoista"). Vahti:
   shifts/tests.py:SheetObstacleOpeningTests. */
.obstacle--open .obstacle__list {
  display: block;
}

/* FOKUSAVAUS VAIN ILMAN JAVASCRIPTIÄ. unavailability.js ilmoittaa läsnäolostaan
   lisäämällä taulukolle luokan `sheet--obstacle-click` (sama kuvio kuin
   sheet_cards.js ja `sheet--cards`), ja fokusavaus annetaan vain kun merkintää
   ei ole. Skriptin kanssa fokus ei saa pitää popoveria auki: klikkaus sekä
   asettaa avaustilan että fokusoi painikkeen, joten fokusavaus tekisi toisesta
   klikkauksesta ja Escapesta näkymättömiä - avaustila purettaisiin, mutta
   popover jäisi fokuksen varassa näkyviin. */
.sheet:not(.sheet--obstacle-click) .obstacle:focus-within .obstacle__list {
  display: block;
}

/* flex-wrap on pakollinen, ei kosmetiikkaa: ilman sitä suodatinrivi (kaksi
   valikkoa + täppä + painike) ei mahdu kapealle ruudulle vaan työntää KOKO
   SIVULLE vaakavierityksen - mitattu selaimessa 500 px:n leveydellä: 91 px
   ylivuotoa, säännöllä 0. Sama vika kuin .cell-status:emptyllä aikanaan:
   taulukon oma .table-scroll ei auta, koska ylivuoto tulee sen ulkopuolelta. */
.sheet-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

/* width:auto antaa valikolle sen LEVEIMMÄN vaihtoehdon leveyden, ja
   kenttävalikossa se on "Autofix1.com Areena, Kempele" - 368 px, eli enemmän
   kuin puhelimen koko sisältöleveys. Ilman max-widthiä suodatinrivi työnsi
   siitä 9 px:n ylivuodon ja koko SIVU sai vaakavierityksen (mitattu 375 px:n
   leveydellä: 1 px sivutason vieritystä, säännöillä 0). Sama vika kuin
   flex-wrapin puuttuessa - ylivuoto tulee taulukon .table-scrollin
   ulkopuolelta, joten se ei voi sitä korjata. min-width:0 on pakollinen
   pari: ilman sitä flex-item ei suostu kutistumaan sisältönsä alle. */
.sheet-filters select {
  width: auto;
  max-width: 100%;
}

.sheet-filters > span {
  max-width: 100%;
  min-width: 0;
}

.sheet-periods {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.sheet-periods__nav {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: var(--space-3);
}

/* "Ei tarvita" on päätös, ei puute - himmennetty, ei tyhjän näköinen.

   KAKSI SÄÄNTÖÄ, EI YKSI (shifts/CLAUDE.md, ":not()-EHDOT KOSKEVAT VAIN
   TAUSTAA"): himmennys on EHDOTON ja koskee jokaista "Ei tarvita" -solua,
   myös sitä, joka on samalla oma halukkuus. Vain TAUSTA sulkee
   .cell--wantedin ja .cell--assignedin pois valitsimessaan, koska se on
   tilavärisääntö ("LUKITUS EI POISTA TIETOA"): jokainen solun taustan
   asettava .cell--*-sääntö väistää halukkuutta ja myönnettyä vuoroa
   VALITSIMESSAAN, ei sääntöjärjestyksellä. Ehto kirjoitettiin kertaalleen
   koko sääntöön, jolloin himmennys - jolla ei ole kilpailijaa - lakkasi
   osumasta halukkuussoluun. Vahti
   (shifts/tests.py:AvailabilityToggleMarkupTests) väittää molemmista
   puoliskoista. */
.cell--not-needed {
  opacity: 0.45;
}

.cell--not-needed:not(.cell--wanted):not(.cell--assigned) {
  background: var(--color-surface);
}

.cell--saving {
  opacity: 0.6;
}

/* Halukkuustoggle (shifts/CLAUDE.md, "Halukkuus ≠ vuoro"): 👍 MOLEMMISSA
   tiloissa, ja merkitty tila on painetun näköinen (tumma vihreä täyttö,
   valkoinen teksti) ja kantaa sanan "Peru" toisena rivinä. Tila luetaan
   `aria-pressed`istä eikä erillisestä luokasta, jotta ruudunlukija ja silmä
   saavat saman viestin. Koko kauden lakanasta tila luetaan silti solujen
   pohjaväreistä (.cell--wanted / .cell--assigned).

   "PERU" ON PAINIKKEEN SISÄLLÄ, EI SEN ALLA: napin alle sijoitettu sana olisi
   kuollut kosketusalue täsmälleen siinä kohdassa, johon sormi osuu. Sana on
   pohjassa aina ja piilotetaan täällä - selain ei kirjoita painikkeen
   sisältöä. Solu kasvaa merkittäessä; se on hyväksytty hinta siitä, ettei
   jokainen kauden rivi ole pysyvästi korkeampi.

   Aiemmat muodot: (1) oletuksena X, klikattuna peukku - näytti kiellolta;
   (2) 👍 / punainen ✕ - `aria-pressed` kertoi tilan samalla kun ✕ kertoi
   teon. Kumpikaan ei palaa. */
.avail-toggle {
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg);
  color: var(--color-text);
  font-size: 0.9rem;
  line-height: 1.6;
  font-weight: 400;
}

.avail-toggle[aria-pressed="true"] {
  border-color: var(--color-availability-fill);
  background: var(--color-availability-fill);
  color: var(--color-availability-contrast);
  font-weight: 600;
}

.avail-toggle__cancel {
  display: none;
  font-size: 0.75rem;
  line-height: 1.3;
  padding-bottom: 0.15rem;
}

.avail-toggle[aria-pressed="true"] .avail-toggle__cancel {
  display: block;
}

/* TYHJÄ TUOMARIPAIKKA LAKANASSA: katkoviiva ja vajaan sävy
   (toteutusratkaisut.md, "Tilamerkit"). Haamuteksti (roolin lyhenne) pysyy,
   koska lakanan minimileveys on mitattu sen kanssa - tämä on solun tila,
   ei sen sisältö.

   KATKOVIIVA KANTAA TIEDON ILMAN VÄRIÄ: harmaasävyisenä ja tulosteessa
   vaalea sävy katoaa, katkoviiva ei. Sama laji kuin negatiivisen summan
   lihavoinnilla (.fee-amount--negative).

   `outline` EIKÄ `border`, ja se on mitattu ansa: taulukko on
   `border-collapse: collapse`, jossa naapurisolun SOLIDI reuna voittaa
   dashedin - reuna jäisi näkymättömäksi juuri niillä sivuilla, joita varten
   se kirjoitettiin. `outline-offset: -1px` pitää viivan solun sisällä eikä
   siirrä ainuttakaan saraketta.

   HALUKKUUS VOITTAA VAPAUDEN, JA EHTO ON VALITSIMESSA (shifts/CLAUDE.md,
   "SOLUN VÄRISÄÄNTÖJEN TARKKUUS ON OSA SÄÄNTÖÄ"): tyhjä paikka voi olla yhtä
   aikaa oma halukkuusmerkintä, ja silloin pelaajan oman merkinnän vihreä
   pohja on painavampi tieto kuin paikan vapaus. Sääntöjärjestys EI riitä:
   `:not(.cell--locked)` alla nostaa tarkkuuden (0,2,0):aan, joka voittaa
   `.cell--wanted`in (0,1,0) riippumatta siitä kumpi on tiedostossa
   myöhemmin - ja juuri niin jokainen tyhjä tuomaripaikka jäi kertaalleen
   vajaan punertavaksi halukkuudesta huolimatta (toteutusratkaisut.md,
   "Halukkuuspainike toggleksi ja `.cell--free`n tarkkuusvika"). Siksi
   taustasäännössä on myös `:not(.cell--wanted)`. Katkoviiva jää näkyviin
   molemmissa.

   `:not(.cell--assigned)` on samassa valitsimessa VAHDIN takia, ei
   käytöksen: vapaa ja myönnetty ovat toisensa poissulkevia, joten ehto ei
   osu koskaan - mutta tilavärisäännön vahti vaatii molemmat ehdot
   jokaiselta taustan asettavalta .cell--*-säännöltä, eikä poikkeuslistaa
   haluta (shifts/CLAUDE.md, "LUKITUS EI POISTA TIETOA").

   LUKITTU SOLU EI SAA VAJAAN SÄVYÄ, JA EHTO ON VALITSIMESSA EIKÄ
   SÄÄNTÖJÄRJESTYKSESSÄ (mitattu selaimessa 2026-09-10: 10 lukittua vapaata
   solua lakanalla). Kuitatun ottelun tyhjää tuomaripaikkaa ei voi täyttää
   perumatta kuittausta ensin, joten vajaan sävy lupaisi tekemistä, jota siinä
   solussa ei ole - lukituksen himmennys on siellä oikea viesti.
   `:not(.cell--locked)` nostaa tarkkuuden (0,2,0):aan, joten lopputulos ei
   riipu siitä kummassa järjestyksessä `.cell--free` ja `.cell--locked`
   sattuvat olemaan tiedostossa. **Katkoviiva jää lukittuunkin soluun** -
   tieto "tämä paikka on tyhjä" ei katoa, vain sen kutsu toimintaan. */
.cell--free {
  outline: 1px dashed var(--color-fill-low-border);
  outline-offset: -1px;
}

.cell--free:not(.cell--locked):not(.cell--wanted):not(.cell--assigned):not(.cell--card-short) {
  background: var(--color-fill-low-surface);
}

/* KORTIN RIITTÄVYYS KATSOJALLE (toteutusratkaisut.md, "Kortin riittävyys
   halukkaalle lakanassa"). Kolme sääntöä, ja jokaisella on oma tehtävänsä:

   1. `:not(.cell--card-short)` YLLÄ on se, joka toteuttaa päätöksen "vajaan
      punertava sävy jää vain niille, joiden kortti riittää". Ehto on
      VALITSIMESSA eikä sääntöjärjestyksessä: `.cell--free:not(...)` on
      tarkkuudeltaan (0,4,0), joten myöhempi (0,3,0)-sääntö ei voittaisi sitä
      (shifts/CLAUDE.md, "SOLUN VÄRISÄÄNTÖJEN TARKKUUS ON OSA SÄÄNTÖÄ").
   2. **VAIMEUS ON TEKSTIN VÄRI, EI `opacity` (korjattu 2026-09-21).** Ensimmäinen
      toteutus himmensi solun `opacity: 0.45`:llä, ja se rikkoi kolme sääntöä
      kerralla: solun AKTIIVINEN halukkuuspainike näytti estetyltä
      (.claude/rules/kayttoliittyma.md, "Estetty nappi näyttää estetyltä"), oma halukkuus
      vaimeni vaikka se on fakta ("LUKITUS EI POISTA TIETOA, VAIN KUTSUN
      TOIMINTAAN"), ja `--color-text` 45 %:n peittävyydellä jäi noin 3:1:een eli
      alle AA-rajan 0,75 rem:n merkintätekstille. `opacity` periytyy jokaiseen
      lapseen eikä sitä voi kumota lapsessa - siksi vaimeus on VÄRI, jonka
      painike ja merkki asettavat itse uudelleen. **Molemmat ominaisuudet ovat
      samassa ehdollisessa säännössä**, koska kummallakin on kilpailija:
      halukas solu näyttää halukkaalta.
   3. Keltainen korostus on sidottu `.cell--freehen` VALITSIMESSA eikä
      palvelimen ehdossa. Elävä täyttö poistaa `cell--freen` luokkana
      (shifts/static/shifts/referee_sheet.js), jolloin korostus katoaa
      itsestään - palvelimen laskemaan lippuun sidottuna se jäisi keltaiseksi
      soluun, joka ei ole enää vapaa. Tarkkuus (0,5,0) voittaa kohdan 1. */
.cell--card-short:not(.cell--wanted):not(.cell--assigned) {
  background: var(--color-surface);
  color: var(--color-muted);
}

.cell--free.cell--card-ok:not(.cell--locked):not(.cell--wanted):not(.cell--assigned) {
  background: var(--color-card-fit-surface);
}

/* Sana solussa ("kortti ei riitä", "Vanhemmat", "Liitolta"). Omalla
   rivillään ja pienempänä: se on solun tila eikä sen sisältö, ja
   roolilyhenne tai tekijän nimi on yhä rivin tärkein tieto. VÄRIN SE PERII
   solulta (`--color-muted`, 5,1:1 pinnalla), eikä sitä kirjoiteta tähän:
   halukkaassa solussa solu EI vaimenna, ja sanan oma harmaa jäisi silloin
   väittämään vaimeutta, jota solussa ei ole. */
.cell-card-note {
  display: block;
  font-size: 0.75rem;
  line-height: 1.3;
}

/* Halukas: vaaleanvihreä pohja. Koko kauden lakanasta näkee yhdellä
   silmäyksellä mihin on ilmoittautunut. Väri on tila eikä korostus, joten
   se asuu omassa tokenissaan (.claude/rules/kayttoliittyma.md, "Korostus ei ole tilaväri"). */
.cell--wanted {
  background: var(--color-availability-surface);
}

/* Valittu vuoroon: päätyylin tummansininen pohja (--color-accent), valkoinen
   teksti. Vahvempi kuin halukkuus, koska se on päätös eikä toive. */
.cell--assigned {
  background: var(--color-accent);
  color: var(--color-accent-contrast);
}

.cell--assigned a {
  color: var(--color-accent-contrast);
}

/* Lakanan solun hakukenttä ("Hae…"- ja "Muu vastaava…" -komennot, ks.
   shifts/referee_menu.py): korvaa solun valikon väliaikaisesti, joten se on
   saman levyinen eikä siirrä taulukon sarakkeita hakua avattaessa. */
.cell-search {
  width: 100%;
  font-size: 0.9rem;
}

.avail-all {
  white-space: nowrap;
  font-size: 0.85rem;
}

/* LAKANAN TIIVISTETTY RIVI on OLEMASSA VAIN KAPEALLA RUUDULLA (ks.
   shifts/CLAUDE.md, "Lakana kapealla ruudulla - ottelu on rivi, ei
   sivullinen"). Perustila on `none`, ja solu herää henkiin vasta alempana
   olevassa 30rem-lohkossa - näin leveä näkymä JA TULOSTE eivät näe sitä
   lainkaan, eikä `@media print` tarvitse omaa piilotussääntöään.

   Solu on rivin VIIMEINEN eikä ensimmäinen, koska `.sheet td:first-child` on
   vaakaan kiinnitetty PVM-sarake: ensimmäiseksi lisätty solu kaappaisi sen
   kiinnityksen leveällä ruudulla. Kapealla se nostetaan `order`illa
   ensimmäiseksi. */
.sheet-summary {
  display: none;
}

/* Tiivistila: pelkkä kontrollien piilotus EI lyhennä riviä lainkaan (mitattu:
   655px -> 654px), koska rivin korkeuden määrää ottelusarakkeen rivittyvä
   teksti ja valikon oma korkeus - toggle mahtuu solussa niiden viereen. Jotta
   täppä tekee sen mitä lupaa ("ruudulle mahtuu enemmän rivejä"), tiivistila
   pienentää myös solujen pystypehmustetta. */
.sheet--compact th,
.sheet--compact td {
  padding-top: var(--space-1);
  padding-bottom: var(--space-1);
}

/* Täppä ja sen teksti samalle riville. Ilman tätä globaali input{width:100%}
   venyttää valintaruudun koko leveyteen ja teksti valuu sen alle - label on
   lohkotason elementti (label{display:block}). Koskee jokaista "Valitse
   kaikki" -tyyppistä täppää lomakkeen ulkopuolella. */
.check-inline {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
}

.check-inline input {
  width: auto;
}

/* "Vain vuorotiedot" -täppä kausivalinnan vieressä: sama rivi, ei omaa
   lomakettaan - yksi Näytä-painike riittää molempiin näkymäasetuksiin. */
.sheet-filters__check {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  white-space: nowrap;
}

.sheet-filters__check input {
  width: auto;
}

/* Kuittaus: painike on tarkoituksella tekstinä eikä ikonina - se on rahaan
   johtava todiste, ei pikatoiminto. Kuitattuna sävy vaihtuu ja teksti kertoo
   mitä painike nyt tekee (peruu), ei mitä tila on. */
.confirm-cell {
  min-width: 11rem;
}

.confirm-toggle {
  font-size: 0.85rem;
  padding: var(--space-1) var(--space-2);
}

.confirm-toggle--on {
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
}

.confirm-mark {
  display: inline-block;
  margin-left: var(--space-1);
  font-size: 0.85rem;
  color: var(--color-accent);
  white-space: nowrap;
}

/* PASSIIVISEN KUITTAUSNAPIN SYY NÄKYVÄNÄ TEKSTINÄ (shifts/CLAUDE.md,
   "KUITTAUKSEN AIKARAJA ON OTTELUN ALKUHETKI"). Nappi ei katoa vaan
   himmenee (himmennys tulee yleisestä `button:disabled`-säännöstä yltä), ja
   syy lukee sen vieressä: pelkkä title-attribuutti ei ole puhelimessa
   olemassa, eikä himmeä nappi ilman selitystä eroa rikkinäisestä.
   `display: block` antaa LEVEÄLLÄ ruudulla oman rivin kuten maksumerkinnällä;
   kapean ruudun kortissa solu on flex-kontti, jolloin teksti asettuu napin
   viereen. Molemmat mahtuvat 375 px:iin myös 20 px:n juurifontilla (mitattu
   2026-09-02), joten eroa ei pakoteta pois - pakotus kaventaisi nappia. */
.confirm-reason {
  display: block;
  font-size: 0.8rem;
  color: var(--color-muted);
  white-space: nowrap;
}

/* Maksumerkintä kuittausmerkin alla (shifts/CLAUDE.md, MAKSETTU-TILA).
   OMALLA RIVILLÄÄN (`display: block`) eikä kuittausmerkin perässä: solu on jo
   kapea, ja vierekkäin ne luettaisiin yhdeksi merkinnäksi. HIMMENNETTY, koska
   maksu on kirjanpidon tieto eikä lakanan päivittäinen kysymys - kuittaus on
   se, jota tuomarivastaava tällä sivulla tekee. */
.paid-mark {
  display: block;
  font-size: 0.8rem;
  color: var(--color-muted);
  white-space: nowrap;
}

/* Lukittu solu: kuitattua ottelua ei muokata perumatta ensin. Himmennys
   kertoo miksi kontrolli ei reagoi - pelkkä disabled näyttäisi rikkinäiseltä.

   LUKITUS EI POISTA TIETOA, VAIN KUTSUN TOIMINTAAN (shifts/CLAUDE.md,
   "Halukkuus ≠ vuoro"): halukkuus (.cell--wanted) ja katseltavalle
   henkilölle myönnetty vuoro (.cell--assigned) ovat faktoja, ja lukittu solu
   pitää niiden pohjavärin. Ilman :not()-ehtoja tämä sääntö voitti
   .cell--assignedin pelkällä JÄRJESTYKSELLÄ mutta jätti sen valkoisen
   tekstivärin voimaan: kuitatun ottelun oma vuoro oli valkoista tekstiä
   --color-surfacen päällä, kontrasti 1,16:1. Vajaan sävyn poistuminen
   lukitusta VAPAASTA solusta on eri päätös ja jää voimaan (.cell--free
   yllä): se lupasi tekemistä, jota lukitussa solussa ei ole. */
.cell--locked:not(.cell--wanted):not(.cell--assigned) {
  background: var(--color-surface);
}

.muted {
  color: var(--color-muted);
}

/* KAKSI KIRJOITUSASUA SAMASTA TIEDOSTA, EI KAHTA TIETOA. Kapea ruutu tarvitsee
   paikoin lyhyemmän sanan kuin leveä ("Tuomarivelvoite (kuitatut)" on
   yksinään leveämpi kuin koko muu koontitaulukko), eikä tekstiä voi lyhentää
   CSS:llä: `content`iin kirjoitettu sana jäisi käännösten ulkopuolelle ja
   ruudunlukijalle. Molemmat asut renderöidään palvelimella, ja CSS valitsee
   kumpi näkyy.

   ÄLÄ KÄYTÄ TÄTÄ KAHDEN ERI TIEDON NÄYTTÄMISEEN - silloin toinen niistä
   katoaa toiselta ruudulta jäljettömiin. */
.narrow-only {
  display: none;
}

/* Nimi-arvo-pareja yhdelle kohteelle (henkilösivun perustiedot). Sama
   kaksisarakkeinen ristikko kuin palkkiokoosteen omalla listalla, mutta
   luokka on geneerinen - koosteen `.fee-summary__meta` jää sen tulostusasun
   takia omakseen eikä sitä yhdistetä tähän. */
.detail-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  margin-bottom: var(--space-3);
}

.detail-list dt {
  font-weight: 600;
}

/* PITKÄ ARVO KATKEAA, ja `anywhere` EIKÄ `break-word`: vain edellinen
   pienentää min-content-leveyttä, jonka mukaan `1fr`-sarake mitoittuu.
   Katkeamaton sähköpostiosoite venytti henkilösivun Perustiedot-listan ja
   vieritti koko sivua vaakaan 20 px:n juurifontilla 375 px:n ruudulla
   (mitattu 2026-09-18). */
.detail-list dd {
  margin: 0;
  overflow-wrap: anywhere;
}

/* Solun tilaviesti: "…" tallennuksen ajan, "✓" onnistuneesta, "✗" virheestä
   (itse virheteksti menee sivun ylälaidan banneriin, ei soluun). */
.cell-status {
  display: inline-block;
  min-width: 1em;
  font-size: 0.85rem;
  color: var(--color-accent);
}

/* Valikko on width:100%, joten viesti on aina omalla rivillään solussa - ja
   TYHJÄNÄKIN se muodosti rivilaatikon, joka varasi ~19 px jokaisesta
   tuomariroolisolusta (mitattu selaimessa; elementin oma korkeus on 0, mutta
   rivilaatikon korkeuden määrää emon line-height). Tyhjä viesti siirretään
   siksi pois normaalista virrasta.

   EI display:none: aria-live-alueen on oltava saavutettavuuspuussa JO ENNEN
   muutosta, muuten ruudunlukija ei ilmoita ilmestyvää viestiä - piilotettu ja
   samalla hetkellä täytetty alue luetaan uutena elementtinä, ei muutoksena.
   Sama kuvio kuin tutussa visually-hidden-luokassa: elementti pysyy
   renderöitynä, se ei vain vie tilaa. Kun teksti tulee, :empty lakkaa
   täsmäämästä ja viesti näkyy normaalisti (rivi kasvaa viestin ajaksi).

   Ehto nojaa siihen, ettei pohjassa ole väliä merkkiäkään span-elementin
   sisällä - :empty ei täsmää edes yhteen välilyöntiin. Regressiovahtina
   shifts/tests.py:n CellStatusMarkupTests. */
.cell-status:empty {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.sheet-error {
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius);
  color: var(--color-danger);
  background: var(--color-danger-surface);
}

/* SIVUN OSIO ON LOHKO: valkoinen kortti harmaalla sivupohjalla
   (toteutusratkaisut.md, "Testipalautteen korjaukset 22.9.", kohta 4). YKSI
   KOMPONENTTI, kuten `.page-nav`: sivukohtaista osiotyyliä ei kirjoiteta.

   NIMI ON YLEINEN EIKÄ ASETUSSIVUN OMA (vaihdettu 2026-09-23): sama rakenne kantaa
   joukkuesivun, henkilösivun, `/omat/`in ja Rinki-tapahtuman osioita, eikä
   nimi saa luvata "asetuksia" (vahti `core/test_docs.py:TemplateStructureTests`,
   vanha nimi on kielletty).

   EI AIHEKOHTAISTA VÄRIÄ. Täyttö on neutraali `--color-bg`: talkoolohkon
   vihreä luettaisiin Ringin IN:ksi (.claude/rules/kayttoliittyma.md, "Korostus ei ole
   tilaväri").

   ENSIMMÄISEN JA VIIMEISEN LAPSEN MARGINAALI NOLLATAAN, koska lohkon oma
   pehmuste hoitaa välin: otsikon ylämarginaali ja taulukon alamarginaali
   kasvattaisivat muuten korttia epäsymmetrisesti. Kapean ruudun sääntö on
   tiedoston ensimmäisessä 30rem-lohkossa ja tulosteen sääntö `@media print`
   -lohkossa (paperilla lohko on neutraali). */
.page-section {
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg);
}

.page-section > :first-child {
  margin-top: 0;
}

.page-section > :last-child {
  margin-bottom: 0;
}

/* SIVUN SISÄLLYSLUETTELO (`core/templates/core/club_settings.html`).
   EI `.page-nav`: se vie sivun SISÄLLÄ eikä pois, joten linkit ovat
   kevyitä painikkeita eivätkä lihavoituja nappeja. Neutraalin ohjaimen sääntö
   (.claude/rules/kayttoliittyma.md, "NEUTRAALIN OHJAIMEN TÄYTTÖ ON VALKOINEN"): lepotilassa
   `--color-bg`, hoverissa `--color-surface`. Fokus kirjoitetaan auki, koska
   reunallisella painikkeella selaimen oma rengas jää reunan alle.
   `flex-wrap` rivittää kapealla - se on vikasuoja, ei hienosäätö. */
.page-toc ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  padding: 0;
  list-style: none;
  font-size: 0.9rem;
}

.page-toc a {
  display: inline-block;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg);
  color: var(--color-text);
  text-decoration: none;
}

.page-toc a:hover {
  background: var(--color-surface);
}

.page-toc a:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Valikko ei saa venyä sivun levyiseksi leveällä ruudulla, mutta EI myöskään
   ylivuotaa kapealla: globaali select{width:100%} hoitaa jälkimmäisen (vrt.
   .sheet-filters, jossa width:auto pakotti erillisen max-width-säännön - siellä
   pisin kenttänimi oli 368 px eli enemmän kuin puhelimen sisältöleveys). */
.settings-form {
  margin-bottom: var(--space-3);
}

.settings-form select,
.settings-form input {
  max-width: 32rem;
}

.settings-form button {
  margin-top: var(--space-2);
}

/* Pohjien kuvaukset pohjavalikon alla (shifts/_template_notes.html).

   SÄILIÖ RENDERÖIDÄÄN AINA, myös tyhjänä: ehto "onko yhdelläkään pohjalla
   kuvausta" olisi muuten laskettava näkymässä, ja sama laskenta olisi neljässä
   kutsupaikassa. Tyhjänä lohkolla ei ole omaa taustaa eikä kehystä, joten se ei
   näy ruudulla - vain marginaali, jonka viimeinen sääntö poistaa. */
.template-notes {
  margin: 0 0 var(--space-3);
}

.template-notes:empty {
  margin: 0;
}

.template-notes__item {
  margin: 0 0 var(--space-1);
}

/* Nimi ja kuvaus samalle riville kun tilaa on, allekkain kun ei ole. Kuvaus on
   vapaata tekstiä, joten sen on saatava katketa - `strong` ei. */
.template-notes__item .muted {
  margin-left: var(--space-1);
}

/* Riviladottu lomake taulukon solussa: kentät ja painike samalle riville, kun
   tilaa on. KAUSIRIVI EI ENÄÄ KÄYTÄ TÄTÄ - sen kentät ovat omissa
   sarakkeissaan ja lomake taulukon ulkopuolella (.season-field alla).

   Käyttäjiä on NELJÄ, ja jokaisessa solussa on YKSI lomake: tehtäväpohjan
   paikkasivu (shifts/club_template.html), joukkuesivun lakanapaikkojen
   kuittaus (core/team_page.html) sekä maksujen summa- ja maksutyyppirivit
   (charges/team_charges.html, charges/team_charge_types.html). Seuran tehtäväkatalogi käytti tätä
   seitsemälle kentälle ja siirtyi pois 2026-09-07 samasta syystä kuin
   kausirivi - yhden solun lomake ei jousta, ja rivin levein solu määrää koko
   taulukon leveyden (.task-field alla).

   Kentät ovat kapeampia kuin .settings-formin oletus (32rem), koska ne ovat
   solussa eivätkä palstalla - ja ne KUTISTUVAT, koska solun leveys ei jousta.
   Kapealla ruudulla flex rivittää ne allekkain, mikä on sama lopputulos kuin
   lomakkeella muutenkin. */
.settings-form--inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-bottom: 0;
}

.settings-form--inline input {
  /* `min-width: 0` ON PAKOLLINEN, EI HIENOSÄÄTÖÄ: flex-itemin oletus on
     `min-width: auto`, joka kieltäytyy kutistumasta sisältöä pienemmäksi -
     ja isommalla tekstikoolla (20 px juurifontti) kolme kenttää ja painike
     vierittivät koko SIVUN vaakaan 405 px:iin (mitattu selaintestissä
     2026-08-28). Perusleveys on `flex-basis`issa, jotta kentät jakavat tilan
     ja kutistuvat kun sitä ei ole. */
  flex: 1 1 6rem;
  width: auto;
  min-width: 0;
  max-width: 10rem;
}

.settings-form--inline button {
  margin-top: 0;
}

/* Kausirivin kentät seuran asetussivulla (core/club_settings.html).

   LEVEYS ANNETAAN SARAKKEITTAIN, koska globaali `input { width: 100% }` ei rajaa
   taulukossa mitään: auto-layoutissa sarakkeen leveys tulee kentän OMASTA koosta
   (oletus 20 merkkiä), joten viisi kenttäsaraketta olisi yhdessä leveämpi kuin
   mikään niiden sisällöstä. `max-width: 100%` pitää kentän solussaan silloinkin
   kun taulukko on kenttää kapeampi. */
.season-field {
  max-width: 100%;
}

.season-field--name {
  width: 9rem;
}

.season-field--date {
  width: 10rem;
}

.season-field--id {
  width: 8rem;
}

.season-field--fee {
  width: 7rem;
}

/* Tehtävätyypin rivin kentät (shifts/club_tasks.html). Sama peruste kuin
   kausirivillä yllä: auto-layoutissa sarakkeen leveys tulee kentän OMASTA
   koosta, joten globaali `width: 100%` ei rajaa taulukossa mitään.

   VALIKOT SAAVAT `width: auto` eivätkä kiinteää leveyttä: silloin ne
   kutistuvat pisimmän vaihtoehtonsa mittaisiksi ("ottelun jälkeen"), eikä
   kiinteä leveys voi leikata vaihtoehdon tekstiä umpimähkään. Kiinteä leveys
   annetaan vain niille kahdelle, joiden sisältö on vapaata tekstiä tai
   numero. */
.task-field {
  width: auto;
  max-width: 100%;
}

.task-field--name {
  width: 11rem;
}

.task-field--order {
  width: 4.5rem;
}

/* TIHEÄ TAULUKKO: kapeampi VAAKApehmuste, pystypehmuste ennallaan.

   Tiheys on tässä projektissa se vipu, jolla leveä taulukko mahtuu
   sisältöpalstaan - uutta leveysporrasta ei lisätä (.claude/rules/kayttoliittyma.md,
   "Sisältöpalstan leveys on PORRAS"). Mitattu 2026-09-10 seuran
   tehtäväkatalogilla: kymmenen saraketta 1240 px -> 1184 px, eli
   toimintosarake lakkasi leikkautumasta.

   YLEINEN LUOKKA EIKÄ SIVUKOHTAINEN SÄÄNTÖ: sama vipu on tarjolla seuraavalle
   leveälle taulukolle, eikä kahta eri tiheyttä synny vahingossa. Pystypehmuste
   EI kiristy - rivien erottuminen toisistaan on eri kysymys kuin sarakkeiden
   mahtuminen. */
.table--dense th,
.table--dense td {
  padding: var(--space-2) var(--space-1);
}

/* Toimintosarake: painikkeet rivittyvät RAJATUN leveyden sisällä.

   `max-width` EI OLE KOSMETIIKKAA vaan se, mikä pitää sarakkeen kapeana:
   ilman sitä kolme painiketta rinnakkain tekisi toimintosarakkeesta rivin
   leveimmän solun - eli juuri sen, minkä poistaminen on tämän taulukon koko
   tarkoitus. Sama kate koskee jaetun rivin perustetta (.task-note), joka on
   yhtä pitkä virke. */
.task-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  max-width: 13rem;
}

.task-note {
  display: inline-block;
  max-width: 13rem;
}

.settings-list {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-3);
}

.settings-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}

/* Nimi ja sähköposti vievät jäljelle jäävän tilan. overflow-wrap on pakollinen,
   ei kosmetiikkaa: pitkä sähköpostiosoite on yksi katkeamaton sana, joka
   työntäisi kapealla ruudulla koko sivulle vaakavierityksen. min-width:0 on
   sen pari - ilman sitä flex-item ei suostu kutistumaan sisältönsä alle. */
.settings-list li > span:first-child {
  flex: 1 1 12rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

.settings-list form {
  margin: 0;
}

@media (max-width: 30rem) {
  /* Lakanan suodatinpaneelin ylänappi näkyy vain kapealla (perustila ja
     peruste: `.sheet-filters__submit--top` yllä). OMA TÄYSLEVEÄ RIVI heti
     paneelin otsikon alla: flex-rivissä kentän vieressä napin paikka vaihtui
     tekstikoon mukana (mitattu 16 px vs. 20 px juurifontilla). */
  .sheet-filters__submit--top {
    display: block;
    flex-basis: 100%;
  }

  /* OSIOLOHKON VAAKAPEHMUSTE PIENENEE (perustila: `.page-section` yllä),
     jottei kortti syö sisältöleveyttä: 390 px:n ruudulla palstan ja kortin
     pehmusteet veisivät muuten yli kymmenesosan leveydestä, ja juuri kortin
     sisällä ovat taulukot. Taulukko vierittyy yhä omassa `.table-scroll`issaan
     kortin sisällä, ei sivua. */
  .page-section {
    padding: var(--space-3) var(--space-2);
  }

  /* NAVIGAATIOPALKKI KAPEALLA RUUDULLA.

     VALIKKO ON VIRRASSA EIKÄ PÄÄLLEKKÄIN, ja tämä on sama päätös samasta
     syystä kuin lakanan estepopoverilla (shifts/CLAUDE.md, "POPOVER ON
     NORMAALISSA VIRRASSA"): 375 px:n ruudulla oikeaan reunaan ankkuroitu
     paneeli ylittää ikkunan, ja seuraus on SIVUTASON vaakavieritys. Virrassa
     oleva valikko levittää palkkia auki ollessaan - se on hyväksytty hinta,
     koska vaihtoehto on paneeli, jota ei voi lukea.

     RIVI KÄÄRIYTYY (sääntö on komponentissa), joten viisi ryhmää latoutuu
     kahdelle riville eikä yksikään nimi katkea. Nimiä ei lyhennetä:
     "Tuomarivuorot" on osion nimi, ja lyhenne olisi toinen nimi samalle
     osiolle (juuri-CLAUDE.md, Vakiotermistö). */
  .site-nav {
    padding: 0 var(--space-2);
  }

  .site-nav__item {
    /* Auki oleva valikko tarvitsee koko rivin leveyden, joten ryhmä on
       pystysuora lohko: linkki ja avaaja rinnakkain, valikko niiden alle. */
    flex-wrap: wrap;
  }

  .site-nav__menu {
    position: static;
    flex: 1 0 100%;
    border: none;
    border-left: 2px solid var(--color-border);
    border-radius: 0;
    box-shadow: none;
    background: transparent;
    /* LEVEYS EI SAA TULLA PISIMMÄSTÄ NIMESTÄ. Leveän ruudun `min-width:
       max-content` + `nowrap` on siellä oikein (paneeli leijuu sisällön
       päällä), mutta kapealla se sitoo palkin leveyden nimen pituuteen: yksi
       uusi pitkä painikenimi työntäisi SIVUA vaakaan, eikä vika näkyisi
       yhdessäkään yksikkötestissä.

       MITATTU 2026-09-11: sääntö ei muuta nykyistä asettelua lainkaan (samat
       leveydet 217/124/130 px ja 271/155/162 px molemmilla tekstikoilla) -
       se on vikasuoja tulevalle nimelle, ei korjaus nykyiseen. */
    min-width: 0;
    max-width: 100%;
  }

  /* VALIKKO ON TÄSSÄ TUMMALLA POHJALLA (ks. yllä: `position: static` ja
     `background: transparent` jättävät sen palkin väriin), joten leveän
     ruudun valkoisen paneelin värit olisivat siellä lukukelvottomat -
     `--color-text` tummansinisellä on 1,7:1. Kaikki kolme riviä palautetaan
     tummalle pinnalle, ja fokusrengas limeen. */
  .site-nav__menu {
    --color-focus: var(--color-accent-on-dark);
    border-left-color: var(--color-on-dark-muted);
  }

  .site-nav__menu a {
    white-space: normal;
    color: var(--color-on-dark);
  }

  .site-nav__menu button {
    color: var(--color-on-dark);
  }

  .site-nav__user {
    color: var(--color-on-dark-muted);
    border-top-color: var(--color-on-dark-muted);
  }

  /* Hover ja fokus: vaalea `--color-surface` peittäisi tumman pinnan ja jäisi
     tekstin alle vaaleana laatikkona, jossa valkoinen teksti katoaa. */
  /* `:not(:disabled)` ON TÄSSÄ TARKKUUDEN TAKIA, ei estetyn napin takia.
     Leveän ruudun sääntö sai ehdon (.claude/rules/kayttoliittyma.md, "Estetty nappi näyttää
     estetyltä"), mikä nosti sen tarkkuuden (0,2,2):een - ja ilman samaa ehtoa
     tämä (0,1,2) HÄVIÄISI sille myöhemmästä sijainnistaan huolimatta. Mitattu
     2026-09-11 selaimessa: kapean ruudun valikon hover muuttui vaaleaksi
     (`--color-surface`) tummalla palkilla, eli valkoinen teksti katosi
     vaaleaan laatikkoon - juuri se, jonka tämä sääntö on kirjoitettu
     estämään. **Tarkkuuden on pysyttävä samana molemmissa.** */
  .site-nav__menu a:hover,
  .site-nav__menu a:focus,
  .site-nav__menu button:not(:disabled):hover,
  .site-nav__menu button:focus {
    background: var(--color-accent-dark);
  }

  .site-nav__menu [aria-current="page"] {
    color: var(--color-accent-on-dark);
  }

  .site-nav__item:last-child .site-nav__menu {
    right: auto;
  }


  /* Kapealla ruudulla rivin muokkauslomake latoutuu allekkain: kolme
     päivämäärä- ja tekstikenttää vierekkäin ei mahdu, ja isolla tekstillä
     ne veivät koko sivun vaakavieritykseen. */
  .settings-form--inline {
    flex-direction: column;
    align-items: stretch;
  }

  .settings-form--inline input {
    flex: none;
    width: 100%;
    max-width: none;
  }

  .sheet {
    min-width: 0;
  }

  /* Puhelimessa taulukko on korttiasettelu (ks. alempi 30rem-lohko): jokainen
     solu on oma lohkonsa otsikkoineen, joten rivikorkeutta ei ole sitomassa
     mitään - nowrapin ainoa vaikutus olisi työntää pitkä sarjanimi ulos
     ruudusta ja tuoda koko sivulle vaakavieritys. */
  .col-nowrap {
    white-space: normal;
  }

  /* Puhelimessa taulukko on korttiasettelu (rivi = kortti, solu = lohko
     otsikkoineen), joten sarakkeita ei ole eikä sarakkeen tunnistusta tarvita:
     PVM-solu on kortin ensimmäinen rivi eikä kiinnitettävä sarake, ja
     korostusoverlay venyisi koko kortin yli. Toistetut otsikkorivit piilotetaan
     samoin kuin thead - ne olisivat kortteja, joissa lukee pelkkiä
     sarakeotsikoita. */
  .sheet th:first-child,
  .sheet td:first-child {
    position: static;
    box-shadow: none;
    background: none;
  }

  .sheet-repeat {
    display: none;
  }

  /* Haamuteksti on TURHA korttiasettelussa: td::before nostaa saman
     sarakeotsikon kortin riviotsikoksi, joten solussa lukisi "2T" kahdesti
     allekkain (todettu selaimessa). Tyhjä kortin rivi otsikkoineen kertoo
     saman - sarakkeen tunnistus on tässä asettelussa jo ratkaistu. */
  .cell-ghost {
    display: none;
  }

  .sheet-cell:hover::after {
    content: none;
  }

  /* --- Kortti: ottelu on rivi, ei sivullinen -------------------------------

     KAIKKI TÄMÄN OSAN SÄÄNNÖT OVAT `.sheet--cards`-LUOKAN TAKANA, ja luokan
     lisää `sheet_cards.js`. Se on tarkoituksellinen ehto eikä varmistelu:
     ilman JavaScriptiä kapea ruutu näyttää kortit auki niin kuin ennenkin -
     huonompi mutta ehjä. Pelkkään media queryyn sidottu tiivistys jättäisi
     JS:ttömän selaimen lakanaan, jonka yhtäkään ottelua ei voi avata.

     Mitattu lähtötila oli ~700 px per ottelu 390 px:n ruudulla eli tasan yksi
     ruudullinen: selaaminen ei ollut heikkoa vaan mahdotonta. */

  /* Rivi on pystysuora flex-kontti, jotta viimeisenä oleva tiivistetty rivi
     voidaan nostaa `order`illa ensimmäiseksi. `:not(.sheet-repeat)` on
     pakollinen: tämä valitsin on tarkempi kuin `.sheet-repeat { display: none }`
     ja toisi toistetut otsikkorivit takaisin näkyviin. */
  .sheet--cards tbody tr:not(.sheet-repeat) {
    display: flex;
    flex-direction: column;
  }

  .sheet--cards .sheet-summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    order: -1;
    padding: 0;
  }

  /* Korttiasettelun `td::before` nostaisi data-labelin riviotsikoksi, mutta
     tiivistetty rivi ON kortin otsikko - "Ottelu" sen yläpuolella olisi
     kohinaa. Attribuutti on silti pakollinen (mekaaninen vahti
     core/test_docs.py), ja se kuvaa solun sisältöä oikein. */
  .sheet--cards .sheet-summary::before {
    content: none;
  }

  /* Koko tiivistetty rivi on avauspainike: kosketuskohteen on oltava rivin
     kokoinen, ei pelkkä nuoli. Painikkeen oma ulkoasu nollataan - se on rivi,
     ei lomakepainike. */
  .sheet--cards .sheet-summary__toggle {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 2.75rem;
    padding: var(--space-2) 0;
    border: none;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
  }

  .sheet--cards .sheet-summary__lines {
    flex: 1 1 auto;
    min-width: 0;
  }

  .sheet--cards .sheet-summary__when,
  .sheet--cards .sheet-summary__who,
  .sheet--cards .sheet-summary__more {
    display: block;
  }

  .sheet--cards .sheet-summary__when {
    font-size: 0.85rem;
    color: var(--color-muted);
  }

  .sheet--cards .sheet-summary__who {
    font-weight: 600;
  }

  /* Kenttä ja sarja ovat kortin kolmas otsikkorivi - tiivistetyllä rivillä on
     vain se, mistä ottelun tunnistaa. Kuusi tietosaraketta latistuu näin
     kolmeksi tekstiriviksi (mitattu ~280 px -> ~70 px). */
  .sheet--cards .sheet-summary__more {
    display: none;
    font-size: 0.85rem;
    color: var(--color-muted);
  }

  .sheet--cards tr.is-open .sheet-summary__more {
    display: block;
  }

  .sheet--cards .sheet-summary__caret {
    flex: 0 0 auto;
    color: var(--color-muted);
    transition: transform 120ms ease;
  }

  .sheet--cards tr.is-open .sheet-summary__caret {
    transform: rotate(90deg);
  }

  /* Tiivistetyn rivin omat MERKIT: tuomarivastaavalle täyttöaste, tuomarille
     oma halukkuustila. Molemmat ovat merkkejä eivätkä painikkeita - tässä oli
     "Kaikki käy", ja se on kumottu (ks. shifts/CLAUDE.md, "Tiivistetyllä
     rivillä on TILAMERKKI, EI TEKOA"). Merkki EI saa näyttää painikkeelta:
     harmaa reagoimaton painike luetaan rikkinäiseksi, merkki luetaan merkiksi.

     KÄÄRE ON PELKKÄ LAYOUT-KOUKKU: sana, väri ja tasaleveät numerot tulevat
     jaetusta komponentista (.state-badge), eikä niitä kirjoiteta tähän
     toiseen kertaan. Kääre on olemassa siksi, että flex-item on se, jonka
     kasvu estetään - ja siksi, että JavaScript vaihtaa merkin sen sisältä
     (shifts/static/shifts/availability.js). */
  .sheet--cards .sheet-summary__fill,
  .sheet--cards .sheet-summary__state {
    flex: 0 0 auto;
  }

  /* Suljettu kortti on VAIN tiivistetty rivi. */
  .sheet--cards tbody tr:not(.sheet-repeat):not(.is-open) > td:not(.sheet-summary) {
    display: none;
  }

  /* Avatussa kortissa otsikkotiedot tulevat tiivistetystä rivistä, joten
     taulukon kuusi tietosaraketta jäävät pois - muuten sama tieto lukisi
     kortissa kahdesti. Valitsin on MERKITYKSEEN eikä sijaintiin perustuva
     (`nth-child` rikkoutuisi hiljaa heti kun sarakejoukko muuttuu): kortin
     omat rivit ovat vuorosolut, estesolu ja tiivistetty rivi, kaikki muut ovat
     otsikkotietoa. */
  .sheet--cards tbody td:not(.sheet-cell):not(.sheet-obstacle):not(.sheet-summary) {
    display: none;
  }

  /* Roolirivi on YKSI rivi: tunnus, tekijä ja painike vierekkäin. Aiemmin
     `td::before` oli oma lohkonsa arvon yläpuolella, eli jokainen rooli vei
     kaksi riviä. */
  .sheet--cards .sheet-cell {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 2.75rem;
    padding: var(--space-1) 0;
  }

  .sheet--cards .sheet-cell::before {
    flex: 0 0 3.5rem;
    margin: 0;
  }

  /* Valikko JÄÄ KORTTIIN myös kapealla (tuomarivastaava korjaa vuoroja
     puhelimella), mutta roolitunnuksen VIERESSÄ eikä omana täysleveänä
     rivinään. min-width:0 on pakollinen pari: ilman sitä flex-item ei suostu
     kutistumaan sisältönsä alle ja pitkä nimi työntää sivulle vaakavierityksen. */
  .sheet--cards .sheet-cell select,
  .sheet--cards .sheet-cell .cell-search {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
  }

  /* KOSKETUSKOHTEEN VÄHIMMÄISKOKO ON 44 px. Sivun tärkein toiminto oli sen
     pienin kohde (28 px), ja rivi lyhenee silti - korkeuden söi ennen
     riviotsikon oma rivi. */
  .sheet--cards .avail-toggle {
    margin-left: auto;
    min-width: 2.75rem;
    min-height: 2.75rem;
  }

  .sheet--cards .confirm-toggle {
    min-height: 2.75rem;
  }

  .sheet--cards .avail-all {
    min-height: 2.75rem;
  }

  /* TYHJÄ RIVI PIILOTETAAN, kun sillä ei ole arvoa, valikkoa EIKÄ kontrollia
     tälle katsojalle (views._build_sheet_rows laskee `void`in ja
     `card_void`in, pohja valitsee kumpaa tiivistila lukee).

     TARVITTU TYHJÄ TUOMARIVUORO EI OLE ENÄÄ TÄSSÄ JOUKOSSA (muutettu
     2026-08-27): se on juuri se rivi, jonka takia sivulle tullaan, ja
     "Näytä vain vapaat tuomarivuorot" -suodatin lupasi sen näkyvän. Piilotus
     jää voimaan siellä missä se ei valehtele - tiivistila, estesolu ilman
     estettä ja joukkuevastuurivit (ks. shifts/CLAUDE.md, "TYHJÄ TUOMARIVUORO
     NÄKYY AINA"). Leveällä ruudulla tyhjä solu on pakko näyttää sarakkeen
     tunnistuksen vuoksi - kortissa sarakkeen nimi ON rivi.

     Sääntö on `.sheet-cell`-säännön JÄLKEEN: valitsimet ovat yhtä tarkkoja
     (0,2,0), joten järjestys ratkaisee. */
  .sheet--cards .sheet-cell--void {
    display: none;
  }
}

/* Tuomarivuorojen koonti */

.summary__count {
  text-align: right;
  white-space: nowrap;
}

/* Nollarivi himmennetään mutta EI piiloteta: kuorman epätasaisuus on koonnin
   viesti, ja se näkyy vain kun myös nollat ovat listalla. */
.summary__row--empty {
  color: var(--color-muted);
}

/* KAPEALLA RUUDULLA KOONTI PYSYY TAULUKKONA (shifts/CLAUDE.md, "Tuomarikoonti").

   AUKI KIRJOITETTU POIKKEUS ON POISTETTU, koska sitä ei enää ole
   mistä poiketa: korttimuunnos on opt-in `card-rows`- ja `.sheet`-koukkujen
   takana (ks. "Korttimuunnos on OPT-IN"), eikä koonnin taulukossa ole
   kumpaakaan. Aiemmin tässä oli seitsemän display-lohkoa, jotka palauttivat
   taulukon taulukoksi - ne olisivat opt-inin jälkeen kumonneet säännön, joka ei
   enää osu tähän. Regressiovahti kysyy nyt päinvastaista: shifts/tests.py:
   RefereeSummaryNarrowLayoutTests väittää, ETTEI koonnissa ole koukkua.

   Miksi koonti ei kortteja kestä: kysymys on "kuka kantaa liikaa, kuka liian
   vähän", ja vastaus luetaan vertaamalla lukusarakkeita allekkain; kortteina
   yksi henkilö vei ~480 px ja lukuja saattoi verrata vain muistista.

   ALLA JÄLJELLÄ OLEVAT SÄÄNNÖT OVAT TIHEYTTÄ, EIVÄT POIKKEUSTA. Ne kumoavat
   PERUSTYYLIN (`table { width: 100% }`, `th, td { padding: var(--space-2) }`),
   joka on voimassa leveydestä riippumatta - eivät korttimuunnosta. Siksi ne
   jäävät vaikka poikkeus poistui. */
@media (max-width: 30rem) {
  .summary {
    /* Sarakemäärä ei ole kiinteä vaan `task_types`in mittainen. Perustyylin
       `table { width: 100% }` puristaisi sarakkeet ruudun leveyteen ja
       rivittäisi otsikot; auto + min-width antaa taulukon kasvaa sisältönsä
       mittaiseksi, jolloin vaakavieritys tapahtuu taulukon OMASSA
       .table-scroll-säiliössä eikä sivulla. */
    width: auto;
    min-width: 100%;
    /* Kuusi kaksimerkkistä lukusaraketta ja nimi mahtuvat 390 px:ään vasta
       tiiviimmällä ladelmalla - tiheys on tämän taulukon vipu, ei uusi
       leveysporras (.claude/rules/kayttoliittyma.md, "Sisältöpalstan leveys on PORRAS"). */
    font-size: 0.85rem;
  }

  /* Perustyylin `var(--space-2)` on tässä liikaa: sama mitattu 390 px:n
     mahtuminen kuin font-sizella yllä. */
  .summary th,
  .summary td {
    padding: var(--space-1);
  }

  /* Yhteensä-sarake on se luku, jota oikeasti verrataan riviltä toiselle
     (shifts/CLAUDE.md, "Tuomarikoonti"). Korostus on VAIN kapealla ruudulla:
     leveällä sarakkeita on väljästi eikä lukija eksy, ja työpöytänäkymä ei saa
     muuttua. Vasen reunaviiva eikä taustaväri - taustaväri kilpailisi
     nollarivin himmennyksen ja `th`-solujen oman pinnan kanssa. */
  .summary__total {
    border-left: 2px solid var(--color-border);
  }

  /* PALKKIOKOOSTEEN LUETTELO ON SAMAA LAJIA KUIN TUOMARIKOONTI: kapea
     numeromatriisi, joka PYSYY TAULUKKONA (.claude/rules/kayttoliittyma.md, "ovat kapeita
     numeromatriiseja: koukkua ei lisätä yhteenkään niistä"). Vipu on siis
     tiheys eikä korttimuunnos.

     MITATTU 2026-09-10: 375 px:n ruudulla taulukko oli 497 px, eli Tilitettävä
     jäi kokonaan vierityksen taakse - ja juuri se on sarake, jonka takia
     tuomarivastaava sivun avaa. Tiheys ja lyhennetyt sarakeotsikot mahtuvat
     ilman että työpöytänäkymä muuttuu.

     SAMAT KOLME SÄÄNTÖÄ KUIN `.summary`llä, ei kopio vaan sama kuvio: nämä
     kumoavat PERUSTYYLIN (`table { width: 100% }`, solupehmusteen), eivät
     korttimuunnosta - koukkua tässä taulukossa ei ole eikä tule. */
  .fee-index {
    width: auto;
    min-width: 100%;
    font-size: 0.85rem;
  }

  .fee-index th,
  .fee-index td {
    padding: var(--space-1);
  }

  /* Tilitettävä on tämän taulukon "Yhteensä": se luku, jota riviltä toiselle
     luetaan. Sama korostus ja sama peruste kuin `.summary__total`illa.

     SARAKE ON LISÄKSI KIINNITETTY OIKEAAN REUNAAN, ja se on ISON TEKSTIN takia
     eikä varotoimi: normaalilla juurifontilla tiheys riitti (taulukko 359 px =
     kääre), mutta 20 px:n juurifontilla viisi saraketta on 432 px eikä 355
     px:iin mahdu millään tiheydellä - ja juuri Tilitettävä olisi taas se, joka
     jää vierityksen taakse (mitattu 2026-09-10 molemmilla tekstikooilla).
     Sama mekanismi kuin lakanan kiinnitetyllä PVM-sarakkeella: `.table-scroll`
     on vaakavierivä kontti, joten `right: 0` kiinnittyy siihen.

     LÄPINÄKYMÄTÖN TAUSTA ON PAKOLLINEN, sama ansa kuin lakanassa - muuten
     vierivät solut kuultavat kiinnitetyn sarakkeen läpi. `th`-soluilla on oma
     pintansa (`--color-surface`), joten ne saavat sen sellaisenaan. */
  .fee-index__total {
    border-left: 2px solid var(--color-border);
    position: sticky;
    right: 0;
    background: var(--color-bg);
  }

  .fee-index th.fee-index__total {
    background: var(--color-surface);
  }
}

/* Tuomaripalkkiot: matriisieditori, koosteet ja tuloste */

.fee-cell input {
  width: 5rem;
  text-align: right;
}

.fee-cell--error input {
  border-color: var(--color-danger);
}

.fee-cell__error {
  display: block;
  font-size: 0.8rem;
  color: var(--color-danger);
}

/* VALIKKOSOLUN VÄHIMMÄISLEVEYS (sarjojen korttivaatimukset).

   Globaali `select { width: 100% }` antaa valikon kutistua solun mukana, ja
   kapealla ruudulla taulukon sarakkeet puristuvat sisältöä pienemmiksi:
   mitattuna 375 px:n ruudulla valikko oli 29 px eli PELKKÄ NUOLI - valittu
   arvo ("N (jatkava)") ei näkynyt lainkaan, eikä matriisia voinut lukea.

   MIN-WIDTH EIKÄ WIDTH: leveällä ruudulla sarake saa yhä joustaa sisältönsä
   mukaan. Seuraus kapealla on VAAKAVIERITYS taulukon omassa
   `.table-scroll`-säiliössä, ja se on tässä oikea lopputulos: matriisi on
   matriisi, ja luettava arvo on tärkeämpi kuin vierityksetön rivi. */
.fee-cell select {
  min-width: 8rem;
}

/* Puuttuva palkkio on 0 € - mutta ei hiljaa. Sama sävy kuin virhebannerissa,
   koska kyse on rahasta joka jää maksamatta.

   `.notice-warning` on SAMA SÄÄNTÖ YLEISNIMELLÄ, ei kopio: puuttuva
   tietosuojayhteystieto asetussivulla on sama laji huomiota (jotain jää
   tekemättä, eikä se saa jäädä hiljaiseksi), mutta `.fee-warning` sillä olisi
   väärä nimi. Kaksi identtistä sääntöä erkaantuisi ensimmäisessä
   värisäädössä, joten valitsimia on kaksi ja sääntö yksi.

   SÄÄNNÖN KOTI ON NYT JUURI-CLAUDE.md ("Punainen huomio tarkoittaa yhtä
   asiaa"), ja tämä kommentti on sen toteutuspuoli. Sääntö oli aiemmin vain
   tässä, ja katselmus 2026-09-10 löysi kolme rikkomusta - luokan valinta on
   arvio, jota mikään vahti ei tarkista. */
.fee-warning,
.notice-warning {
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius);
  color: var(--color-danger);
  background: var(--color-danger-surface);
}

.fee-warning ul {
  margin: var(--space-1) 0;
}

/* JAETUN DATAN HUOMIO on OMA SÄÄNTÖNSÄ eikä `.notice-warning`in uudelleenkäyttö.
   Punainen sävy tarkoittaa
   tässä projektissa "jotain jää tekemättä" (puuttuva palkkio, puuttuva
   tietosuojayhteystieto); jaettu data ei ole vika vaan ominaisuus, josta on
   tiedettävä ennen muokkaamista. Punaisena varoitus kertoisi väärän asian ja
   turruttaisi samalla ne kaksi, jotka oikeasti vaativat toimenpiteen. */
/* SUODATTIMEN TILARIVI ("Näytetään vain vajaat: 7 / 23 ottelua") on SAMA
   SÄÄNTÖ TOISELLA VALITSIMELLA, ei kopio - sama kuvio kuin
   `.fee-warning` / `.notice-warning` -parilla yllä. Laji on sama kuin jaetun
   datan huomiolla: neutraali tieto, joka on luettava ennen kuin sivun
   sisällöstä tekee johtopäätöksiä. Punainen sävy väittäisi, että jotain jää
   tekemättä - suodatin on käyttäjän oma valinta eikä vika. */
/* KOLMAS HUOMIOLAJI, KELTAINEN (.claude/rules/kayttoliittyma.md, "Punainen huomio tarkoittaa
   yhtä asiaa"): varoitus, joka ei estä eikä jätä mitään tekemättä, mutta joka
   on tiedettävä ennen kuin jatkaa. Punaisena se väittäisi väärää ja
   turruttaisi samalla ne huomiot, jotka oikeasti vaativat toimenpiteen;
   neutraalina se katoaisi leipätekstiin, ja juuri se oli Rinkin
   päällekkäisyysvaroituksen vika.

   SISÄLTÖ ON KAPPALEITA, ei yhtä virkettä: aiheuttajia voi olla useampi, ja
   kukin on oma rivinsä. Kappaleiden oletusmarginaali purettaisiin muuten
   laatikon pehmusteen päälle. */
.notice-caution {
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  border: 1px solid var(--color-caution-border);
  border-radius: var(--radius);
  background: var(--color-caution-surface);
}

.notice-caution p {
  margin: 0;
}

.notice-caution p + p {
  margin-top: var(--space-1);
}

.notice-shared,
.sheet-notice {
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-accent);
  border-radius: var(--radius);
  background: var(--color-surface);
}

/* ASENNUSKEHOTE JA -LINKKI (`core/CLAUDE.md`, "PWA: asennettava sovellus
   seuran osoitteessa"). Etusivun kehote on `.notice-shared`, eli neutraali
   tieto: mitään ei jää tekemättä.

   KEHOTE NÄKYY VAIN KAPEALLA RUUDULLA TAI KOSKETUSLAITTEELLA: asennus on
   puhelimen asia, ja työpöydällä kehote olisi kohinaa. Palvelin ei tiedä
   laitteesta mitään, joten ehto on tässä eikä näkymässä; näkymä kertoo vain,
   onko kehote suljettu tässä istunnossa.

   ASENNETUSSA SOVELLUKSESSA KEHOTE JA OMAT-VALIKON LINKKI PIILOTETAAN. Sääntö
   on viimeisenä, jotta se voittaa näyttösäännön samalla tarkkuudella.
   iPhonen `display-mode`-tukea ei ole todennettu laitteella (`core/CLAUDE.md`,
   AVOIN AUKKO iPhonesta); jos se ei riitä, varareitti on `install.js`in
   `navigator.standalone`. */
.install-notice {
  display: none;
}

@media (max-width: 30rem), (pointer: coarse) {
  .install-notice {
    display: block;
  }
}

.install-notice p {
  margin: 0;
}

.install-notice__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.install-notice__actions form {
  margin: 0;
}

@media (display-mode: standalone) {
  .install-notice,
  .install-link {
    display: none;
  }
}

/* PUSH-ILMOITUSTEN KYTKIN (`push/templates/push/_toggle.html`). `[hidden]`
   KIRJOITETAAN AUKI samasta syystä kuin kelluvalla vierityspalkilla: napin oma
   `display: inline-block` voittaa selaimen oletussäännön, ja piilotettu
   "Ota käyttöön" näkyisi "Poista käytöstä" -napin rinnalla. Mitattu
   selaimessa 2026-09-28, ennen kuin sääntö lisättiin. */
.push-toggle [hidden] {
  display: none;
}

.fee-totals {
  list-style: none;
  padding: 0;
}

.fee-amount {
  text-align: right;
  white-space: nowrap;
  /* TASALEVYISET NUMEROT: summat ovat allekkain sarakkeessa, ja
     suhteutusleveä "1" siirtää desimaalipilkun rivi riviltä eri kohtaan.
     Ominaisuutta tukemattomassa selaimessa sarake näyttää siltä kuin ennen. */
  font-variant-numeric: tabular-nums;
}

/* NEGATIIVINEN TILITETTÄVÄ tarkoittaa, että henkilö on velkaa seuralle -
   sama laji huomiota kuin puuttuva palkkiorivi, joten sama sävy kuin
   `.fee-warning`illa.

   EI PELKÄN VÄRIN VARASSA: luku alkaa miinusmerkillä (`|euros` kirjoittaa
   U+2212:n), ja lihavointi erottaa sen myös harmaasävyisenä ja tulosteessa.
   Väri on kolmas kerros, ei ainoa. */
.fee-amount--negative {
  color: var(--color-danger);
  font-weight: 700;
}

.fee-amount__missing {
  color: var(--color-danger);
  font-weight: 600;
}

.fee-summary__meta {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  margin-bottom: var(--space-3);
}

.fee-summary__meta dt {
  font-weight: 600;
}

.fee-summary__meta dd {
  margin: 0;
}

.fee-summary__note {
  font-size: 0.9rem;
}

.fee-summary__signature {
  margin-top: var(--space-4);
}

/* Tyhjä viiva kynällä täytettäväksi - IBANia ja henkilötunnusta EI tallenneta
   kantaan (ks. shifts/CLAUDE.md, "Palkkiokooste"). */
.fee-summary__line {
  display: inline-block;
  min-width: 16rem;
  border-bottom: 1px solid var(--color-text);
}

/* Tuloste: selaimen tulostustoiminto riittää, ei PDF-kirjastoa. */
@media print {
  /* NAVIGAATIOPALKKI ON TÄSSÄ LISTASSA EIKÄ VAIN YLÄTUNNISTE. Palkin teksti
     on VALKOISTA, ja selaimet eivät oletuksena tulosta taustavärejä: ilman
     tätä riviä jokaisen tulosteen ylälaitaan jäisi kahden rivin verran
     näkymätöntä tekstiä, joka vie tilaa muttei näy. Vika on sitä lajia, joka
     löytyy vasta paperilta. */
  .site-header,
  .site-nav,
  .print-hide,
  .messages {
    display: none;
  }

  .site-main {
    max-width: none;
    padding: 0;
  }

  .fee-summary__table {
    width: 100%;
  }

  /* OSIOLOHKO ON PAPERILLA NEUTRAALI: ei täyttöä, ei reunaa eikä
     pehmustetta. Kortti on ruudun keino erottaa osiot harmaasta pohjasta;
     paperilla pohja on valkoinen, ja reunat kaventaisivat taulukoita. */
  .page-section {
    padding: 0;
    border: none;
    border-radius: 0;
    background: none;
  }

  /* Talkootapahtuman tuloste on YKSI LAPPU KENTÄLLE (shifts/talkoot.md, "Mitä
     tapahtumasivulla näkyy ja mikä lasketaan"): sama näkymä @media printillä,
     ei erillistä pohjaa. Vaakavieritys on ruudun ratkaisu ahtaaseen tilaan -
     paperilla se leikkaisi sarakkeita pois. */
  .table-scroll {
    overflow: visible;
  }

  /* Kelluva palkki on ruudun ohjain, ei paperin sisältöä. */
  .floating-scrollbar {
    display: none;
  }

  table {
    width: 100%;
  }

  /* Rivi ei saa katketa kahdelle sivulle, ja sarakeotsikot toistuvat jokaisella
     sivulla: 40 rivin lappu on käytännössä aina monisivuinen. */
  tr {
    break-inside: avoid;
  }

  thead {
    display: table-header-group;
  }

  /* Vaihe- ja otteluotsikko ei jää sivun viimeiseksi riviksi ilman taulukkoaan. */
  .phase-heading,
  .event-match h3 {
    break-after: avoid;
  }

  /* ESTEET AUKI PAPERILLA. Tulosteessa ei ole hoveria eikä fokusta, joten
     suljettu popover tarkoittaisi tyhjää saraketta - ja lakana on nimenomaan
     paperilakanan korvaaja (shifts/CLAUDE.md, "Esteet ja saatavuus").
     Kolmio jää näkyviin merkiksi, mutta painikkeena se on paperilla turha. */
  .obstacle__list {
    display: block;
  }
}

/* Ottelulista (jaettu lista + isännöintivalinta) */

/* Rivit, jotka helpottavat valintaa: oma joukkue pelaa tai ottelu on omalla
   kentällä. Väri on VIHJE eikä tieto - sama asia lukee rivillä myös tekstinä
   (.match-hint), koska pelkkä väri ei kerro mitään ruudunlukijalle eikä
   värisokealle. Jälkimmäinen (oma kenttä) on se tapaus, joka Excelissä
   unohtuu: turnausottelu meidän kentällämme, jossa oma joukkue ei pelaa. */
.match-row--hint {
  /* Sama peruste ja sama rappeutuma kuin lakanan sarakekorostuksella: tämä
     oli `#f2f7f4`, korostuksen vanhaan vihreään sävytetty valkoinen. Väri on
     VIHJE, ja sama asia lukee rivillä tekstinä (`.match-hint`) - jos
     `color-mix` ei toimi, rivi on valkoinen ja teksti kertoo saman. */
  background: color-mix(in srgb, var(--color-accent) 6%, var(--color-bg));
}

/* Jo lakanassa oleva rivi on himmeämpi: siitä ei enää tehdä valintaa, vaan
   sen ainoa toiminto on purku. */
.match-row--hosted {
  background: var(--color-surface);
}

.match-hint {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 0 var(--space-2);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius);
  font-size: 0.8rem;
  color: var(--color-accent);
  white-space: nowrap;
}

/* LASKEUTUMISSIVUN LOHKOT (`core/templates/core/home_club.html`).

   LOHKO ON LISTA EIKÄ TAULUKKO, ja se on päätös: rivillä on kolme osaa
   (linkki, lisätieto, tilamerkki), joista kaksi ovat vapaata tekstiä. Taulukko
   olisi vaatinut kapealla ruudulla korttimuunnoksen ja `data-label`in joka
   soluun (.claude/rules/kayttoliittyma.md, "KORTTIASETTELU EI YKSIN RIITÄ") — lista latoutuu
   samaan lopputulokseen ilman kumpaakaan.

   SAMA HTML KAIKILLA LEVEYKSILLÄ: rivi on pystysuora flex-kontti, joka ei
   tarvitse media queryä lainkaan. Kapealla ruudulla tilamerkki putoaa omalle
   rivilleen itsestään, koska `flex-wrap` on päällä. */
.landing-block {
  margin-bottom: var(--space-4);
}

.landing-block__title {
  margin-bottom: var(--space-2);
  font-size: 1.1rem;
}

/* HOITAMATTA-LOHKO EROTTUU REUNAVIIVALLA, EI PUNAISELLA. Punainen tarkoittaa
   tässä sovelluksessa "jotain jää tekemättä" (juuri-CLAUDE.md), ja juuri sitä
   lohko tarkoittaa — mutta koko lohkon punaaminen turruttaisi sen, ja
   yksittäinen rivi on jo linkki tekemiseen. Sama laji kuin `.notice-shared`in
   neutraalilla huomiolla: reuna kertoo rajan, sisältö kertoo asian. */
.landing-block--pending {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-danger);
  border-radius: var(--radius);
  background: var(--color-surface);
}

.landing-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* RIVI ON YKSI KOSKETUSKOHDE, JONKA KORKEUS ON 44 px. Linkki on rivin
   ensimmäinen osa ja vie koko leveyden, joten napautus osuu siihen myös
   lisätiedon kohdalta (`flex: 1 0 100%`). */
.landing-list__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}

.landing-list__item:last-child {
  border-bottom: none;
}

/* RIVI NÄYTTÄÄ LINKILTÄ ILMAN HIIRTÄ (R7a): otsikko on linkin värillä ja rivin
   lopussa on nuoli. Aiemmin otsikko oli leipätekstin värinen ja ainoa vihje oli
   alleviivaus hoverissa — kosketusnäytöllä hover-tilaa ei ole olemassa, joten
   koko vihje puuttui juuri siltä laitteelta, jolla sivua eniten katsotaan. */
.landing-list__link {
  flex: 1 1 auto;
  min-width: 60%;
  color: var(--color-accent);
  font-weight: 600;
  text-decoration: none;
  /* Suomen yhdyssanat ovat pitkiä, ja rivin linkki on sen pisin osa - sama
     peruste kuin otsikoiden `break-word`illa. */
  overflow-wrap: break-word;
}

.landing-list__link:hover {
  text-decoration: underline;
}

.landing-list__detail {
  flex: 1 1 auto;
  color: var(--color-muted);
  font-size: 0.85rem;
  overflow-wrap: anywhere;
}

/* NUOLI ON `aria-hidden`: se ei kanna tietoa, jota linkkirooli ei jo kerro, ja
   ruudunlukijalle luettuna se olisi kohinaa jokaisen rivin perässä. Se ei ole
   LINKIN sisällä vaan rivin viimeinen osa, jotta linkin oma rivitys
   (`overflow-wrap: break-word`) pysyy ennallaan; `margin-left: auto` vie sen
   rivin oikeaan laitaan myös silloin kun rivi on kääriytynyt. */
.landing-list__arrow {
  margin-left: auto;
  color: var(--color-accent);
  font-size: 1.1rem;
  line-height: 1;
}

/* TÄYTTÖASTELUKUJEN RIVI (talkootapahtuman `_event_fill_counts.html`):
   otsikko ja tilamerkki pareittain. Flex-käärö, koska pareja on kaksi ja
   kapealla ruudulla ne eivät mahdu rinnakkain - katkeava rivi on luettava,
   vaakaan vierittävä sivu ei. */
.fill-counts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
}

/* TILAMERKKI - YKSI SÄÄNTÖ KOKO SOVELLUKSEEN (toteutusratkaisut.md,
   "Tilamerkit"). Runko määrää muodon, muunnelma VAIN värit: uusi käyttökohde
   ei voi keksiä omaa merkkiään, ja rajan säätö osuu jokaiseen kerralla.

   MERKISSÄ ON AINA SANA (core/badges.py), joten väri on kolmas kerros eikä
   tiedon ainoa kantaja - sama periaate kuin Rinkin IN/OUT-napeilla.

   EI `white-space: nowrap`: teksti on luku JA sana ("6/19 · 13 vapaana"), ja
   375 px:n kortissa 1,25x-tekstikoolla se ei mahdu yhdelle riville. Katkeava
   merkki on luettava, vaakaan vierittävä sivu ei (.claude/rules/kayttoliittyma.md, "KAPEA
   RUUTU TODENNETAAN KAHDELLA TEKSTIKOOLLA").

   NEUTRAALI ON RUNGON OMA VÄRI eikä oma muunnelma: `--neutral` on olemassa
   vakiona (core/badges.py:NEUTRAL), jotta kutsupaikka nimeää tason aina
   ääneen, mutta se ei ylikirjoita mitään. */
.state-badge {
  display: inline-block;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

.state-badge--full {
  border-color: var(--color-fill-full-border);
  background: var(--color-fill-full-surface);
  color: var(--color-fill-full-text);
}

.state-badge--mid {
  border-color: var(--color-fill-mid-border);
  background: var(--color-fill-mid-surface);
  color: var(--color-fill-mid-text);
}

.state-badge--low {
  border-color: var(--color-fill-low-border);
  background: var(--color-fill-low-surface);
  color: var(--color-fill-low-text);
}

/* RINGIN OMA ILMOITTAUTUMISTILA käyttää Rinkin omia tokeneita eikä
   täyttöasteen värejä (rinki/CLAUDE.md, "Väri kertoo VALITUN tilan"):
   ilmoittautuminen ei ole täytettävä paikka. Sävy on TEKSTISSÄ JA REUNASSA
   eikä täytenä taustana - täysi sävy on Rinkin oman napin väri, ja /omat/
   -sivulla merkki ei ole nappi. */
.state-badge--in {
  border-color: var(--color-signup-in);
  color: var(--color-signup-in);
}

.state-badge--out {
  border-color: var(--color-signup-out);
  color: var(--color-signup-out);
}


.match-hosted {
  margin-right: var(--space-2);
  font-weight: 600;
  color: var(--color-accent);
  white-space: nowrap;
}

/* Rivin lomake taulukon tai toisen lomakkeen ULKOPUOLELLA (HTML ei salli
   sisäkkäisiä lomakkeita eikä <form>ia <tr>:n ja <td>:n välissä), ja rivin
   kentät ja painike osoittavat siihen form-attribuutilla - itse lomakkeissa ei
   ole mitään näytettävää. Käyttäjät: isännöinnin purku (matches/match_list.html)
   ja asetussivun kausirivi (core/club_settings.html). */
.hidden-form {
  display: none;
}

/* Talkoiden kuittaus: tilamerkintä ja painike samalla rivillä. Lomake on oma
   elementtinsä (POST), joten se ei voi olla <p>:n sisällä - flex pitää sen
   silti tekstin vieressä ja rivittää kapealla ruudulla. */
.event-confirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-3) 0;
}

.event-confirm form {
  margin: 0;
}

/* Tuontisivu (pesistulokset-JSONit) */

/* Linkki näytetään tekstinä eikä pelkkänä painikkeena: siinä on API-avain,
   ja käyttäjän on voitava nähdä ja korjata se itse silloin kun avainta ei ole
   asetettu. Katkeaa mistä tahansa kohdasta - URL on pitkä eikä siinä ole
   välilyöntejä.

   REUNA, KOSKA LAATIKKO ISTUU SUORAAN SIVUPOHJALLA. `--color-surface` erottuu
   harmaasta sivupohjasta vain ΔE 2,38 eli käytännössä ei lainkaan, joten
   pelkkä täyttö ei rajaa laatikkoa mitenkään. Tämä oli ainoa sivupohjalle
   jäävä surface-pinta ilman omaa reunaa - seitsemällä muulla se oli jo
   (`docs/arkisto/toteutusrupeamat-2026-09.md`, "Surface-pinnat, jotka jäävät suoraan
   sivupohjalle"). Neutraali `--color-border` eikä `--color-accent`: laatikko
   on koodia, ei huomio - viereinen `.import-note` on korostettu juuri siksi,
   että se on huomio. */
.import-url {
  display: block;
  padding: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.85rem;
  word-break: break-all;
}

.import-source {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.import-source .import-url {
  flex: 1 1 20rem;
}

.import-upload {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

/* Huomio, ei virhe: puuttuva tunniste tai kytkemättä jäänyt joukkue on asia
   jonka ihminen käy katsomassa, ei epäonnistunut toiminto. */
.import-note {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius);
  background: var(--color-surface);
}

.import-report {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  margin-bottom: var(--space-4);
}

.import-report ul {
  margin: 0 0 var(--space-2);
  padding-left: var(--space-4);
}

/* HERÄTETEKSTI (shifts/templates/shifts/_nudge_text.html ja
   referee_nudge.html). Sivu avataan puhelimella, joten ehdokkaat ovat
   valintaruutujen luettelo eivätkä taulukko, ja tekstikenttä on KOKO LEVYINEN:
   kentän yleinen leveyskatto (32rem) on nimikenttää varten, ja herätetekstin
   pisin rivi on ottelun otsikkorivi. Rivin teksti rivittyy valintaruudun
   viereen eikä sen alle. */
.nudge-choices {
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-3);
}

.nudge-choices ul {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
}

.nudge-choices li + li {
  margin-top: var(--space-2);
}

.nudge-choices label {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  font-weight: normal;
}

.nudge-choices .muted {
  display: block;
}

.nudge-text textarea {
  max-width: none;
  font: inherit;
}

/* Raportin nimiluettelot listana, ei pilkkuproosana: pesistulosten kenttänimi
   sisältää kaupungin ("POP-kenttä, Siilinjärvi"), joten pilkulla eroteltua
   luetteloa ei voi jäsentää silmillä. */
.import-list {
  margin: var(--space-1) 0 var(--space-2);
  padding-left: var(--space-4);
}

/* Sarjataulukon skaalauskestävyys, sama sääntö kuin lakanassa: minimileveys,
   jonka alle sarakkeet eivät purista, ja nowrap niissä sarakkeissa joiden
   sisältö ei saa katketa. Ilman näitä tiedostovalitsimen teksti leikkautui
   ("Valitse tiedost…") heti kun ikkunaa kavennettiin tai sivua skaalattiin. */
.import-table {
  min-width: 64rem;
}

/* Painikkeet omassa nowrap-kääreessään - itse solu EI ole nowrap, koska sen
   sisällä oleva pitkä URL kuuluu katketa (.import-url). */
/* PAINIKEPARI KÄÄRIYTYY, MUTTA PAINIKETEKSTI EI KATKEA (korjattu R7b:ssa
   2026-09-11). `white-space: nowrap` on täällä perustellusti - solun sisällä
   oleva pitkä URL (`.import-url`) kuuluu katketa, painiketeksti ei - mutta
   ilman `flex-wrap`ia se piti myös PARIN yhdellä rivillä: 390 px:n ruudulla
   kääreen oikea reuna oli 421 px:ssä eli 31 px ruudun ulkopuolella, eikä sivu
   vierittynyt (matches/CLAUDE.md, mitattu 2026-09-07). Jälkimmäisen painikkeen
   oikea laita jäi siis saavuttamatta. `flex-wrap` kääntää parin kahdelle
   riville; `nowrap` jää koskemaan tekstiä painikkeen sisällä. */
.import-buttons {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  white-space: nowrap;
}

.import-action form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

/* Tiedostovalitsin on globaalisti width:100%, mikä kutistaa sen solun leveyden
   mukana ja leikkaa painikkeen tekstin. Taulukossa se saa oman leveytensä. */
.import-action input[type="file"] {
  width: auto;
}

/* Manuaalinen varareitti: supistettuna omaan osioonsa, koska sitä tarvitaan
   vain jos automaattihaku ei toimi. Avaimettomassa tilassa pohja avaa sen
   valmiiksi (details[open]) - silloin se on ainoa polku. */
.import-manual {
  margin: var(--space-4) 0;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}

.import-manual summary {
  cursor: pointer;
  font-weight: 600;
}

.import-manual[open] summary {
  margin-bottom: var(--space-3);
}

@media (max-width: 30rem) {
  .import-table {
    min-width: 0;
  }
}

/* Rinki: tapahtumaluettelon lohkolista, sivutus ja joukkuevalinta
   (rinki/CLAUDE.md, "Luettelo on lohkolista" ja Sivutus).

   KOMPONENTILLA ON YKSI KOTI, eikä sivukohtaisia sääntöjä kirjoiteta: luettelo
   on yhdellä sivulla, mutta juuri siitä sivukohtainen sääntö alkaa - toinen
   lukija kopioi sen ja kaksi kirjoitusasua erkaantuu.

   TAULUKON KORTTIMUUNNOS EI ENÄA KOSKE TÄTÄ. Muunnos (card-rows) antoi kapean
   ruudun asettelun ilmaiseksi ja vaati vastineeksi data-labelin joka solulta;
   lohkolista on lohkolista jo leveällä, joten @media-sääntöjä ei tarvita
   lainkaan. Se on menetys vahdeissa eikä säästö: kapea ruutu on nyt tämän
   komponentin oma todennettava asia. */
.event-list {
  list-style: none;
  margin: var(--space-3) 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* KOKO LOHKO ON LINKKI, ei lohkon sisällä oleva linkki: klikkialue on
   puhelimessa se, joka ratkaisee (sama peruste kuin .tile-korteilla). Grid
   yhdellä sarakkeella pitää rivit allekkain ilman että jokainen span on
   erikseen lohkoelementti. */
.event-card {
  display: grid;
  gap: var(--space-1);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  background: var(--color-bg);
  color: var(--color-text);
  text-decoration: none;
  transition: border-color 0.12s, background-color 0.12s;
  /* Suomen yhdyssanat ovat pitkiä ja paikannimi voi olla katkeamaton: ilman
     tätä pitkä nimi vierittäisi sivua vaakaan kapealla ruudulla. */
  overflow-wrap: anywhere;
}

.event-card:hover {
  border-color: var(--color-accent);
  background: var(--color-surface);
}

.event-card:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Otsake on lohkon tunniste ja sivun raskain teksti lohkon sisällä. */
.event-card__name {
  font-weight: 600;
  font-size: 1.05rem;
}

/* Tyyppi on tarkenne, ei otsake - ja se renderöityy VAIN kun nimi on annettu
   (rinki/CLAUDE.md, "Tapahtuman nimi ja varasanan yksi koti"). */
.event-card__kind,
.event-card__where,
.event-card__locked {
  color: var(--color-muted);
  font-size: 0.9rem;
}

.event-card__when {
  font-variant-numeric: tabular-nums;
}

/* Luvut ovat lohkon viimeinen rivi: ne ovat syy avata tapahtuma, mutta eivät
   syy löytää sitä listasta. */
.event-card__counts {
  color: var(--color-muted);
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

/* Sivutusrivi EI ole .page-nav: se liikkuu saman luettelon sisällä eikä vie
   sivulta pois (.claude/rules/kayttoliittyma.md). Sama flex-vikasuoja
   kuitenkin - isommalla tekstikoolla rivittymätön nappirivi vierittäisi
   sivun vaakaan. */
.pager {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin: var(--space-3) 0;
}

/* Joukkuevalinta: isot painikkeet pinossa, ei linkkirivi. Muoto tulee
   `.button`-komponentista - tämä vain pinoaa ne ja antaa niille leveyden,
   jotta ne ovat puhelimessa peukalon kokoisia maaleja. Katto on sama kuin
   lomakekentillä (32rem), jottei painike veny 76 remin levyiseksi. */
.team-choices {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: stretch;
  max-width: 32rem;
  margin: var(--space-3) 0;
}

.team-choices .button {
  padding: var(--space-3);
  font-size: 1.05rem;
  text-align: center;
}

/* Rinki: merkintänapit ja oman merkinnän ylälohko
   (rinki/CLAUDE.md, "Väri kertoo VALITUN tilan" ja "Ylälohko ja ryhmänappien
   ehto").

   VALITSEMATON NAPPI ON NEUTRAALI, VALITTU TÄYSVÄRINEN. Kumpikin nappi on
   sama koko molemmissa tiloissa, joten valinta ei liikuta riviä - vaihtuva
   koko olisi tehnyt napista liikkuvan maalin juuri sille, joka on jo kerran
   painanut väärää. */
.signup-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin: 0;
}

/* Globaali `input { width: 100% }` työntäisi napit omalle rivilleen myös
   silloin kun tilaa on; `flex` antaa kentän kutistua ja kasvaa rivin mukana.
   `min-width` pitää kentän käyttökelpoisena, ja kapealla ruudulla rivi
   rivittyy - se on haluttu tila eikä ylivuoto. */
.signup-form input[type="text"] {
  flex: 1 1 10rem;
  width: auto;
  min-width: 8rem;
  max-width: 24rem;
}

.signup-choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* VALITSEMATON IN/OUT ON NEUTRAALI OHJAIN, ei tila: sama peruste ja sama
   arvo kuin toissijaisella napilla (.claude/rules/kayttoliittyma.md, "NEUTRAALIN OHJAIMEN
   TÄYTTÖ ON VALKOINEN"). VALITUN napin täyttö ei muutu - se on tila, ja sen
   värin omistaa `--color-signup-in`/`--color-signup-out` (rinki/CLAUDE.md,
   "Väri kertoo VALITUN tilan"). Ero valitsemattoman ja valitun välillä KASVOI
   tästä: valkoinen vs. täysvärinen erottuu enemmän kuin harmaa vs.
   täysvärinen. */
/* `min-width` KATTAA LEVEIMMÄN TILAN, EI VALITSEMATONTA. Valittu nappi saa
   merkin eteensä ("✗ OUT"), joten sen sisältö on leveämpi kuin valitsemattoman
   ("OUT") - ja jos katto on mitoitettu valitsemattoman mukaan, nappi LEVENEE
   klikkauksesta ja rivi hyppää. Vanha 4,5rem (72 px) kattoi vain IN-puolen:
   "✓ IN" mahtui, "✗ OUT" ei (teksti 49,6 px + pehmuste 32 px + reuna 2 px =
   83,6 px). Mitat: `docs/arkisto/toteutusrupeamat-2026-09.md`, "Valitun napin
   merkki ✗ OUT, toteutus".

   ARVO ON REMEINÄ, joten se skaalautuu juurifontin mukana - sekä teksti että
   pehmuste ovat suhteellisia, joten sama katto pätee 20 px:n tekstillä.
   Vahti `rinki/tests.py:SignupChoiceWidthTests` katsoo VAKIOTA, ei ruudulla
   mitattua leveyttä: renderöity leveys näkyy vain selaintestissä. */
.signup-choice {
  background: var(--color-bg);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  min-width: 5.5rem;
  text-align: center;
}

/* Hover vain VALITSEMATTOMALLE: valitun napin täyttö on tila, eikä hover saa
   peittää sitä. Valittu nappi säilyttää yleisen `opacity`-hoverinsa, joka
   näkyy täysvärisen päällä toisin kuin valkoisen. */
.signup-choice:not(.is-chosen):not(:disabled):hover {
  background: var(--color-surface);
}

.signup-choice--in.is-chosen {
  background: var(--color-signup-in);
  border-color: var(--color-signup-in);
  color: var(--color-signup-contrast);
}

.signup-choice--out.is-chosen {
  background: var(--color-signup-out);
  border-color: var(--color-signup-out);
  color: var(--color-signup-contrast);
}

/* RINGIN OSALLISTUJARYHMÄN KEHYS JA SÄVY (rinki/CLAUDE.md, "Väri kertoo
   VALITUN tilan"; mittaukset toteutusratkaisut.md, "Rinki: IN- ja OUT-ryhmien
   erottelu"). EROTTELUN KANTAA KEHYS, EI SÄVY: IN yhtenäinen viiva, OUT
   katkoviiva. Viivatyyli on kuvio eikä sävy, joten se erottuu harmaasävyisenä,
   kaikilla värinäön muodoilla ja paperilla - selain pudottaa taustat
   tulosteesta, reunat se piirtää. Vaalea sävy on KOLMAS kerros: IN- ja
   OUT-sävyn ero harmaasävyssä on 1,01:1, eikä sitä voi kasvattaa ilman että
   himmeä teksti putoaa 4,5:1:n alle. Tieto on joka tapauksessa
   ryhmäotsikossa ja Tila-sarakkeessa.

   KÄSITTELY ON RYHMÄN, EI RIVIN TILAN: luokka tulee event.html:n
   include-riviltä, eikä "Ei vastausta"- tai henkilökuntaryhmä saa kumpaakaan -
   henkilökunnan IN-merkintä ei saa vihreää.

   KÄÄRE ON .table-scroll:n ULKOPUOLELLA: sisäpuolella kehys vierisi
   vaakavierityksen mukana pois. SÄVY ON tr:LLÄ EIKÄ KÄÄREELLÄ: `table` on
   nimenomaisesti valkoinen (`background: var(--color-bg)`), joten kääreen
   sävy jäisi sen alle - ja tr on ainoa elementti, joka kantaa sävyn
   molemmissa asetteluissa (kapealla tr on kortti, eikä korttimuunnos aseta
   taustaa). Kääreen pehmuste ja taulukon alamarginaalin nollaus ovat pari:
   ilman edellistä kortin nollattu vaakapehmuste tuo tekstin kiinni viivaan,
   ilman jälkimmäistä kehykseen jää rako alareunaan.

   TARKKUUDET LASKETTU: `.signup-group--in tr` on (0,1,1) ja voittaa
   korttimuunnoksen `:where(...) tr`in (0,0,1); `.card-rows tr` (0,1,1) ei
   aseta taustaa, joten kilpailua ei synny. `thead th` (0,1,2) voittaa `th`in
   (0,0,1) surface-täytön. Tekstiväri ei muutu: säännöt asettavat vain taustan
   ja reunan, joten leipäteksti ja `.card-sub`in --color-muted periytyvät
   ennallaan. `@media print` ei aseta yhtäkään näistä ominaisuuksista.
   Vahti: rinki/tests.py:ParticipantGroupStyleTests. */
.signup-group {
  border-width: 2px;
  border-radius: var(--radius);
  padding: var(--space-2);
  margin-bottom: var(--space-4);
}

.signup-group table {
  margin-bottom: 0;
}

.signup-group--in {
  border-style: solid;
  border-color: var(--color-signup-in);
}

.signup-group--out {
  border-style: dashed;
  border-color: var(--color-signup-out);
}

.signup-group--in tr {
  background: var(--color-signup-in-surface);
}

.signup-group--out tr {
  background: var(--color-signup-out-surface);
}

/* OTSIKKORIVI SAA RYHMÄN OMAN SÄVYN: --color-surface on sävyä vasten 1,00:1
   eli katoaisi siihen. Tavoite on PARITEETTI valkoisen taulukon otsikkorivin
   kanssa, ei uusi korostus. */
.signup-group--in thead th {
  background: var(--color-signup-in-head);
}

.signup-group--out thead th {
  background: var(--color-signup-out-head);
}

@media (max-width: 30rem) {
  /* Viimeisen kortin erotinviiva ja väli pois kehyksen sisällä: kehys on jo
     raja, ja jäljelle jäävä väli tekisi alareunan pehmusteesta yläreunaa
     isomman. (0,3,1) voittaa `.card-rows tr`in (0,1,1). */
  .signup-group .card-rows tr:last-child {
    border-bottom: none;
    margin-bottom: 0;
    padding-bottom: 0;
  }
}

.own-signup {
  margin: var(--space-4) 0;
}

.own-signup__person {
  padding: var(--space-3);
  margin-bottom: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
}

.own-signup__name {
  margin: 0 0 var(--space-2);
  /* Pitkä sukunimi ja tila samalla rivillä: rivittyy eikä vieritä sivua. */
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: baseline;
  overflow-wrap: anywhere;
}

.own-signup__state {
  color: var(--color-muted);
}

/* ISOT NAPIT VAIN YLÄLOHKOSSA. Ryhmärivin nappi on vastuuhenkilön työkalu
   taulukon solussa, ja samankokoisena se olisi levittänyt jokaisen rivin.
   `min-width` on remeinä, joten se kasvaa juurifontin mukana - 20 px:n
   tekstillä nappi ei jää tekstiään kapeammaksi.

   KATTO ON TÄÄLLÄKIN LEVEIMMÄN TILAN MUKAAN (ks. `.signup-choice` yllä):
   isompi fontti ja pehmuste venyttivät valitun OUTin 105 px:iin, kun vanha
   6rem antoi 96 px. */
.own-signup .signup-choice {
  padding: var(--space-3) var(--space-4);
  font-size: 1.1rem;
  min-width: 7rem;
}

.own-signup .signup-form input[type="text"] {
  flex-basis: 100%;
}

/* RINGIN KOKOONPANO (rinki/CLAUDE.md, Kokoonpano).

   VALINTALISTA POISTUI 2026-09-22: täpät ovat nyt IN-ryhmän taulukon omassa
   sarakkeessa, jossa pelaajat muutenkin luetaan (toteutusratkaisut.md,
   "Testipalautteen korjaukset 22.9.", kohta 1). `.lineup-choices`-säännöt
   poistettiin samassa muutoksessa, koska orvoksi jäänyt sääntö näyttää
   käytössä olevalta.

   JULKAISTU NIMILISTA ON YHÄ LISTA: siinä on yksi tieto riviä kohti eikä
   yhtään ohjainta, joten taulukko antaisi sille saraketta jota ei ole. */
.lineup-list {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-3);
}

/* KOSKETUSKOHDE ON SOLU, EI 20 PX:N RUUTU. Kokoonpano täytetään ottelupäivänä
   kentän laidalla puhelimella, ja paljas valintaruutu on maali, jota ei osu.

   PEHMUSTE ON `label`ISSA EIKÄ SOLUSSA EIKÄ RUUDUN MARGINAALISSA, ja se on
   koko säännön idea (korjattu 2026-09-22). Solun oma pehmuste ei ole
   klikkausaluetta, eikä ruudun `margin` sitäkään vähää: kohde kutistui
   ruudun kokoiseksi vaikka viereinen kommentti väitti muuta. Soluntäyttävä
   `label` on klikattava kaikkialta, joten pehmuste kuuluu sen sisään ja
   solun oma pehmuste nollataan.

   `width: auto` tulee globaalista input-säännöstä (input-kohta yllä), joka
   venyttäisi muuten ruudun koko sarakkeen levyiseksi. */
.lineup-pick-cell {
  text-align: center;
  padding: 0;
}

/* Globaali `label` on jo `display: block`, mutta sen `margin-bottom` on
   kentän otsikkoa varten - täpän kohteessa se olisi klikkaamatonta rakoa
   kohteen alla. */
.lineup-pick-cell label {
  padding: var(--space-2) var(--space-3);
  margin: 0;
  cursor: pointer;
}

.lineup-pick-cell input[type="checkbox"] {
  width: auto;
  min-width: 1.25rem;
  min-height: 1.25rem;
  margin: 0;
}

/* Laskeutumissivut: seuran etusivu, seuranvalinta ja kirjautuminen */

/* Painikeruudukko tarvitsee enemmän tilaa kuin tekstisivun 40rem: kolme 15rem:n
   korttia rinnakkain ei mahdu kapeampaan, ja kahden sarakkeen ruudukossa kortit
   jäisivät leveämmiksi kuin niiden teksti vaatii. EI --wide (rajaton), koska
   ruudukko ei ole taulukko - loputon leveys hajottaisi rivit toisistaan. */
.site-main--landing {
  --main-width: 52rem;
}

.landing-hero {
  margin-bottom: var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}

.landing-hero__name {
  margin: 0 0 var(--space-2);
  font-size: 1.9rem;
}

.landing-hero__lead {
  margin: 0;
  color: var(--color-muted);
}

.landing-hero__roles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0;
}

/* Rooli on tunnistetieto, ei toiminto - himmeä pilleri, ei painikkeen näköinen. */
.role-chip {
  display: inline-block;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-muted);
  font-size: 0.8rem;
  white-space: nowrap;
}

/* TESTISEURAN MERKKI (clubs/CLAUDE.md, "Testiseura"): ylätunnisteen alla
   koko leveydeltä, ja seuranvalinnassa sama sana pillerinä. KELTAINEN, koska
   tieto ei estä mitään mutta se on tiedettävä ennen kuin syöttää mitään
   (--color-caution-*, sama laji kuin .notice-caution). Teksti on
   --color-textiä, jonka kontrasti pohjalle on n. 15:1. Merkki on erillinen
   rivi eikä ylätunnisteen osa: ylätunniste on tumma, ja kapealla ruudulla se
   kääriytyy jo seuran nimen ja linkkien kanssa. */
.test-club-banner {
  margin: 0;
  padding: var(--space-1) var(--space-3);
  border-bottom: 1px solid var(--color-caution-border);
  background: var(--color-caution-surface);
  color: var(--color-text);
  font-weight: 600;
  text-align: center;
}

.test-club-chip {
  display: inline-block;
  /* Seuranvalinnan kortin runko on flex-sarake: ilman tätä pilleri venyisi
     koko kortin levyiseksi. */
  align-self: flex-start;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-caution-border);
  border-radius: 999px;
  background: var(--color-caution-surface);
  color: var(--color-text);
  font-size: 0.8rem;
  white-space: nowrap;
}

.landing-section {
  margin-bottom: var(--space-4);
}

/* Ryhmän otsikko on lokerointia eikä sisältöä: pieni ja himmeä, jotta
   painikkeet pysyvät sivun raskaimpana elementtinä. */
.landing-section__title {
  margin: 0 0 var(--space-2);
  font-size: 0.85rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--space-3);
}

/* Koko kortti on linkki, ei kortin sisällä oleva linkki: klikkialue on
   puhelimessa se, joka ratkaisee. Kuvake on vasemmassa sarakkeessa ja venyy
   molempien tekstirivien yli (grid-row: span 2). */
.tile {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  column-gap: var(--space-2);
  row-gap: var(--space-1);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  background: var(--color-bg);
  color: var(--color-text);
  text-decoration: none;
  transition: border-color 0.12s, background-color 0.12s;
}

.tile:hover {
  border-color: var(--color-accent);
  background: var(--color-surface);
}

.tile:focus-visible,
.club-card:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.tile__icon {
  grid-row: 1 / span 2;
  width: 1.75rem;
  height: 1.75rem;
  /* Piirtoasetukset ovat täällä eivätkä jokaisessa polussa (core/_icons.html).
     currentColor: kuvake perii painikkeen tekstin värin, joten korostettu
     painike ei tarvitse omaa ikonisarjaansa. */
  color: var(--color-accent);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.tile__title {
  font-weight: 600;
  color: var(--color-accent);
  /* Kaksirivinen otsikko jakautuu tasan sen sijaan että viimeiselle riville
     jäisi yksi sana. Selain joka ei tue tätä rivittää tavalliseen tapaan. */
  text-wrap: balance;
}

.tile__text {
  grid-column: 2;
  color: var(--color-muted);
  font-size: 0.9rem;
}

/* Roolin päivittäinen työkalu: yksi korostettu painike, joka vie kaksi
   saraketta. Ilman korostusta sivu on kuusi samanarvoista laatikkoa, joista
   käyttäjä joutuu lukemaan joka kerta minkä hän oikeasti tarvitsee. */
.tile--primary {
  grid-column: span 2;
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-accent-contrast);
}

.tile--primary:hover {
  background: var(--color-accent-dark);
  border-color: var(--color-accent-dark);
}

.tile--primary .tile__icon,
.tile--primary .tile__title {
  color: var(--color-accent-contrast);
}

.tile--primary .tile__text {
  color: rgba(255, 255, 255, 0.85);
}

/* Seuranvalinta */

.club-list {
  list-style: none;
  display: grid;
  gap: var(--space-2);
  padding: 0;
  margin: 0 0 var(--space-3);
}

.club-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  background: var(--color-bg);
  color: var(--color-text);
  text-decoration: none;
  transition: border-color 0.12s, background-color 0.12s;
}

.club-card:hover {
  border-color: var(--color-accent);
  background: var(--color-surface);
}

/* min-width: 0 on pakollinen pari flex: 1:lle - ilman sitä pitkä seuran nimi ei
   suostu kutistumaan ja työntää nuolen ulos kortista kapealla ruudulla. */
.club-card__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.club-card__name {
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--color-accent);
}

.club-card__roles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.club-card__go {
  flex: 0 0 auto;
  font-size: 1.3rem;
  color: var(--color-accent);
}

.landing-note {
  font-size: 0.9rem;
}

/* Tyhjä tila (ei seuroja, ei pääsyä) on oma laatikkonsa eikä irrallista
   tekstiä: se on sivun sisältö, ei huomautus sen puutteesta. */
.empty-state {
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  background: var(--color-surface);
}

.empty-state > :first-child {
  margin-top: 0;
}

.empty-state > :last-child {
  margin-bottom: 0;
}

/* Kirjautuminen ja muut allauthin sivut (templates/allauth/layouts/base.html) */

.site-main--auth {
  --main-width: 26rem;
}

.auth-card {
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: 12px;
  background: var(--color-bg);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}

.auth-card__brand {
  margin: 0;
  font-size: 1.6rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--color-accent);
}

.auth-card__club,
.auth-card__tagline {
  margin: var(--space-1) 0 var(--space-4);
  color: var(--color-muted);
}

/* allauthin oma sivuotsikko ("Kirjaudu sisään") on kortin sisällä tuotenimen
   alla, joten se on tavallista h1:tä pienempi - muuten kortissa on kaksi
   kilpailevaa otsikkoa. */
.auth-card h1 {
  margin: 0 0 var(--space-3);
  font-size: 1.15rem;
}

.auth-card form {
  margin-bottom: 0;
}

/* "Muista minut" yhdelle riville. Djangon as_p tuottaa järjestyksen
   label-then-input ja label on lohkotason elementti, joten rivi syntyy vain
   kääntämällä suunta: row-reverse asettaa syötteen ensin ja flex-end pakkaa
   parin vasempaan reunaan. :has() on ehto, jota ei voi kiertää ilman omaa
   lomakepohjaa - jos selain ei tue sitä, rivi vain jää kahdelle riville. */
.auth-card form p:has(input[type="checkbox"]) {
  display: flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
  align-items: center;
  gap: var(--space-2);
}

.auth-card form p:has(input[type="checkbox"]) label {
  margin: 0;
  font-weight: 400;
}

/* SVG-sprite (core/_icons.html) pois normaalista virrasta. EI display:none -
   piilotetun SVG:n symboleihin viittaava <use> ei renderöi kaikissa
   selaimissa. */
.icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

@media (max-width: 30rem) {
  /* Puhelimessa yksi sarake: korostettu painike ei voi viedä kahta. */
  .tiles {
    grid-template-columns: 1fr;
  }

  .tile--primary {
    grid-column: auto;
  }

  .landing-hero__name {
    font-size: 1.5rem;
  }
}

/* Talkoot */

/* Ruudunlukijalle näkyvä, silmälle näkymätön selite. Talkoolistan jokainen
   valikko on oma lomakkeensa eikä peri saraketta otsikkorivistä, joten se
   tarvitsee oman label-elementtinsä - näkyvänä se toistaisi tehtävän nimen
   viereisestä solusta. Vrt. .cell-status:empty, joka tekee saman tempun
   aria-live-alueelle. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Valikko ja tallennuspainike samalle riville solussa. Globaali
   select{width:100%} venyttäisi valikon koko solun levyiseksi ja työntäisi
   painikkeen omalle rivilleen jokaisella rivillä. */
.volunteer-cell {
  display: flex;
  gap: var(--space-1);
  align-items: center;
  /* Kapealla ruudulla (korttiasettelu) valikko ja painike menevät allekkain
     sen sijaan että valikko kutistuisi lukukelvottomaksi. */
  flex-wrap: wrap;
}

.volunteer-cell select {
  width: auto;
  min-width: 12rem;
  flex: 1 1 auto;
}

/* "Kenen puolesta" on LISÄVALINNAN TAKANA (shifts/talkoot.md, "Kenen puolesta on
   lisävalinnan takana"): toisen puolesta merkitseminen on harvinaista, joten
   rivin oletus on oma merkintä ja valikko avataan tarvittaessa. `<details>` on
   selaimen oma, joten paljastus ei vaadi JavaScriptiä - ja suljettunakin sen
   sisältö lähtee lomakkeen mukana, eli tallennus on yhä yksi POST.

   AUKI OLLESSAAN OMA RIVINSÄ: `flex-basis: 100%` estää valikkoa puristumasta
   tekijävalikon ja painikkeiden väliin. Suljettuna summary on kapea eikä varaa
   riviä, koska `flex-basis` koskee vain auki olevaa elementtiä. */
.beneficiary {
  flex: 0 1 auto;
  min-width: 0;
}

.beneficiary[open] {
  flex-basis: 100%;
}

.beneficiary > summary {
  cursor: pointer;
  color: var(--color-muted);
  /* Kosketuskohde: sama vähimmäiskorkeus kuin muillakin rivin kontrolleilla. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* Valikko omalle rivilleen summaryn alle, ei sen viereen: auki oleva `<details>`
   on lohko, ja valikon on saatava koko leveys kapealla ruudulla. */
.beneficiary > select {
  margin-top: var(--space-1);
}

/* Kyytivalikon "Muu seuran jäsen…" -komennon avaama nimihaku. Osumat tulevat
   palvelimelta pyynnöstä - koko seuran luetteloa ei ole sivulla, joten tämä ei
   ole <datalist> vaan oma tuloslistansa (ks. shifts/CLAUDE.md). */
.ride-search {
  display: inline-block;
}

.ride-search__input {
  width: auto;
  min-width: 10rem;
}

.ride-search__results {
  list-style: none;
  margin: var(--space-1) 0 0;
  padding: 0;
  /* Osumia on rajattu määrä, mutta kapealla ruudulla nekin vievät tilaa. */
  max-height: 12rem;
  overflow-y: auto;
}

.ride-search__results li {
  margin: 0 0 var(--space-1);
}

.event-match {
  margin-bottom: var(--space-4);
}

/* Oman joukkueen ottelu korostetaan kevyesti - PELKKÄÄ esittämistä, mikään
   logiikka ei haaraudu siitä pelaako oma joukkue (shifts/CLAUDE.md). */
.event-match--own {
  border-left: 4px solid var(--color-accent);
  padding-left: var(--space-3);
}

.badge {
  display: inline-block;
  padding: 0 var(--space-2);
  border-radius: var(--radius);
  background: var(--color-surface);
  border: 1px solid var(--color-accent);
  font-size: 0.8rem;
  font-weight: normal;
  vertical-align: middle;
}

/* Django messages */

.messages {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-3);
}

.messages li {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  margin-bottom: var(--space-1);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

.messages li.success {
  border-color: var(--color-accent);
}

.messages li.error {
  border-color: var(--color-danger);
}

/* Puhelimen näyttö */

@media (max-width: 30rem) {
  .wide-only {
    display: none;
  }

  .narrow-only {
    display: inline;
  }

  .site-main {
    padding: var(--space-3) var(--space-2);
  }

  .site-header {
    flex-wrap: wrap;
  }

  /* --- Korttimuunnos on OPT-IN, ei kaikkien taulukoiden kohtalo ------------

     Sääntö: .claude/rules/kayttoliittyma.md, "KORTTIASETTELU EI SOVI JOKAISELLE
     TAULUKOLLE". Nämä kuusi sääntöä osuivat aiemmin JOKAISEEN <table>iin
     jokaisella sivulla, myös sellaiseen jolle kortit ovat haitaksi: kapea
     numeromatriisi (tuomarikoonti, palkkiokoosteen hakemisto,
     tuomarivelvoitteet, korttivaatimukset) menettää kortteina koko arvonsa eli
     lukujen kohdistuksen allekkain. Poikkeuksia olisi ollut neljä auki
     kirjoitettua display-lohkoa; nyt on yksi koukku.

     KOUKKUJA ON KAKSI JA MOLEMMAT OVAT ENTUUDESTAAN OLEMASSA - uutta luokkaa
     ei keksitty:
       `<tbody class="card-rows">`  yksitoista pohjaa, sanasto elää sen sisällä
                                    (ks. "Omat vuorot: kortti tiivistyy")
       `<table class="sheet">`      ottelu- ja tuomarilakana, sisäinen ja
                                    julkinen. Lakana EI voi kantaa `card-rows`ia,
                                    koska sanastovahti vaatisi silloin jokaiselta
                                    sen solulta yhden sanaston luokan - lakanalla
                                    on oma solusanastonsa (`.sheet-cell`,
                                    `.sheet-summary`, `.sheet-obstacle`).
                                    Koukku on `.sheet` eikä `.sheet--cards`,
                                    koska jälkimmäisen lisää JavaScript: ilman
                                    JS:ää kapea lakana näyttää kortit auki
                                    (ks. "Kortti: ottelu on rivi").

     `:has()` ON PAKKO `table`- ja `thead`-säännöissä, koska ne ovat koukun
     YLÄPUOLELLA eikä CSS:ssä ole edeltäjävalitsinta. Hyväksytty rappeutuma:
     ilman `:has()`-tukea `card-rows`-taulukko jää taulukoksi ja vierittyy
     vaakaan omassa `.table-scroll`-säiliössään - huonompi mutta ehjä, ja sama
     tila jonka hallintanäkymät saavat muutenkin. Rappeutuma on
     KAIKKI-TAI-EI-MITÄÄN tarkoituksella: `:has()` on jokaisessa kuudessa
     valitsimessa, myös niissä jotka pärjäisivät ilman, jottei selain jäisi
     puolitiehen (tbody lohkona taulukon sisällä). Sama kuvio ja sama
     hyväksytty rappeutuma kuin kirjautumislomakkeella, ks. "Muista minut
     yhdelle riville".

     `:where()` PITÄÄ TARKKUUDEN ENNALLAAN, ja se on korjaus eikä koristelu:
     paljaat `tr`/`td`-valitsimet olivat (0,0,1), ja luokkaan sidottu muoto
     olisi noussut (0,1,x):ään - jolloin esimerkiksi
     `.sheet-repeat { display: none }` (0,1,0) olisi hävinnyt uudelle
     `tr`-säännölle ja lakanan toistetut otsikkorivit olisivat palanneet
     näkyviin. `:where()` ei kerrytä tarkkuutta, joten jokainen sääntö painaa
     tasan sen verran kuin ennenkin.

     `tfoot` EI OLE MUKANA, eikä koskaan ollutkaan: yhteenvetorivi on jo
     otsikko-arvo-pari (palkkiokoosteen "Palkkiot yhteensä"). Sama rajaus kuin
     sanastovahdilla. */

  table:where(:has(> .card-rows), .sheet) {
    display: block;
  }

  :where(table:has(> .card-rows), .sheet) thead {
    display: none;
  }

  :where(table:has(> .card-rows), .sheet) tbody {
    display: block;
  }

  :where(table:has(> .card-rows), .sheet) tr {
    display: block;
    border-bottom: 2px solid var(--color-border);
    padding-bottom: var(--space-2);
    margin-bottom: var(--space-2);
  }

  :where(table:has(> .card-rows), .sheet) th,
  :where(table:has(> .card-rows), .sheet) td {
    display: block;
  }

  /* `th` EI saa reunan ja pehmusteen nollausta, eikä koskaan saanutkaan: sen
     oma pinta ja pystypehmuste nollataan vasta sanastossa (`.card-rows th`),
     jotta rivin tunniste ei jää kortin sisällä ainoaksi taulukkomaiseksi
     soluksi. Lakanassa `th` on sarakeotsikko piilotetussa `thead`ssa. */
  :where(table:has(> .card-rows), .sheet) td {
    border-bottom: none;
    padding: var(--space-1) 0;
  }

  :where(table:has(> .card-rows), .sheet) td::before {
    content: attr(data-label);
    display: block;
    font-weight: 600;
    font-size: 0.85rem;
    color: var(--color-muted);
  }
}

/* --- Omat vuorot: kortti tiivistyy ---------------------------------------

   Säännöt: shifts/CLAUDE.md, "Kapea ruutu: tiivistys ILMAN avattavaa korttia".
   Mitattu lähtötila: yksi tuomarivuoro ~500 px 390 px:n ruudulla eli ruudulle
   mahtui puolitoista vuoroa.

   EI JAVASCRIPTIÄ EIKÄ AVATTAVAA KORTTIA, ja se on rajaus eikä unohdus.
   Lakanassa (`.sheet--cards`, `sheet_cards.js`) ongelma on selaaminen:
   neljäkymmentä ottelua, joista etsitään yhtä. Täällä rivejä on kymmenkunta ja
   käyttäjä tuli lukemaan ne kaikki - sulkeutuva kortti piilottaisi juuri sen,
   minkä takia sivulle tultiin.

   Globaali korttimuunnos JÄÄ VOIMAAN (toisin kuin koonnissa, ks.
   "Tuomarivuorojen koonti"): tässä rivit ovat leveitä ja sekalaisia, mikä on
   juuri se tapaus, jolle kortit ovat. Alla vain päätetään, mitkä solut ovat
   kortin OTSAKE ja mitkä jäävät otsikollisiksi kaveritiedoiksi.

   KOUKKU `.card-rows` ON `<tbody>`SSÄ, ei `<table>`issa eikä
   `.table-scroll`-kääreessä. Sanasto koskee vain RUNKORIVEJÄ - `thead` on
   kapealla ruudulla piilossa muutenkin - ja kumpikin ylempi paikka olisi
   rikkonut vahdin, joka väittää sekä paljaasta `<table>`-merkkijonosta että
   tasan `<div class="table-scroll">` -kääreestä (`shifts/tests.py`,
   `test_every_table_is_wrapped_and_every_cell_is_labelled`).

   VOKABULAARI (yksi luokka per solu, my_shifts.html):
     .card-lead   otsakerivin alkuun, otsikko piilossa (päivä, kello)
     .card-mark   otsakerivin oikeaan reunaan, otsikko piilossa (rooli)
     .card-title  rivin tunniste omalle rivilleen (ottelu, tehtävä, tapahtuma)
     .card-sub    himmennetty lisärivi (sarja, kohde, tyyppi)
     .card-fact   otsikko JÄÄ ja tulee arvon eteen samalle riville (paljas luku)
     .card-state  tilamerkki omalle rivilleen (kuittaus, tekijä, vastuu)
     .card-control  lomakesolu omalle rivilleen, otsikko JÄÄ omalle rivilleen

   SEITSEMÄS LUOKKA `.card-control` tuli talkoonäkymien myötä (shifts/talkoot.md,
   "Talkoonäkymät kapealla ruudulla - koko sivu, ei yksi taulukko"). Se on solu,
   jonka sisältö on LOMAKE eikä luettavaa tietoa, ja sen otsikko JÄÄ - siinä ero
   `.card-state`en, jossa otsikko piilotetaan. Painike kertoo mitä se tekee
   muttei mitä riviä se koskee, ja talkoorivin merkintävalikon nykyinen arvo on
   sama nimi kuin viereisen Tekijä-solun arvo: kaksi samaa nimeä allekkain ilman
   otsikoita näyttäisi toistolta eikä kontrollilta. */

@media (max-width: 30rem) {
  /* RYHMITTELY SYNTYY VÄLEISTÄ, JA VÄLIN ON OLTAVA NELINKERTAINEN
     (mitattu 2026-08-27, shifts/CLAUDE.md "RYHMITTELY SYNTYY VÄLEISTÄ").
     Kahden faktaparin väli oli `var(--space-2)` (8 px) ja saman parin otsikon
     ja arvon väli 0.35em (4,76 px) - suhde 1,7:1, eli käytännössä sama luku.
     Silmä ei ryhmitä mitään, ja "Tili On Huoltajia 1" luettiin yhtenä
     merkkijonona.

     LUVUT: erottava väli `var(--space-3)` (1rem), yhdistävä `var(--space-1)`
     (0.25rem) - suhde tasan 4:1. Molemmat ovat projektin omalla
     välistysasteikolla ja molemmat rem-yksikköjä, joten SUHDE EI MUUTU
     tekstikoon mukana: 20 px:n juurifontilla luvut ovat 20 px ja 5 px.
     Luettuna yhdistävä väli on yksi sananväli ja erottava neljä.

     `column-gap` osuu vain niihin soluihin, jotka jakavat rivin: faktoihin ja
     otsakerivin `card-lead`-soluihin (päivä, kello). Muut sanaston solut ovat
     `flex-basis: 100%` eli yksin rivillään.

     PYSTYVÄLIT: solun oma pehmuste on nollattu alempana, joten rivin sisäinen
     väli tulee `row-gap`ista - se on JO asteikon pienin (`var(--space-1)`)
     eikä sitä kiristetty, koska asteikon alle meneminen olisi vienyt sanaston
     projektin omien mittojen ulkopuolelle 2 px:n hyödystä. Korttien VÄLINEN
     tila kiristettiin sen sijaan puoleen (8+8 -> 4+4 px). Erotinviiva
     (`border-bottom: 2px`, opt-in-lohko) EI muutu: se on se, joka kertoo
     missä kortti vaihtuu, eikä kiristys saa sulauttaa kortteja toisiinsa. */
  .card-rows tr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: var(--space-3);
    row-gap: var(--space-1);
    padding-bottom: var(--space-1);
    margin-bottom: var(--space-1);
  }

  /* Solun oma pystypehmuste pois: rivivälin hoitaa `row-gap`, ja kuudella
     solulla pehmuste yksin oli yli kolmasosa kortin korkeudesta. min-width ja
     overflow-wrap ovat pari: ilman edellistä flex-item ei suostu kutistumaan
     sisältönsä alle, ja pitkä sarjanimi työntäisi koko SIVULLE
     vaakavierityksen.

     `th` ON MUKANA, koska rivin tunniste voi olla `th[scope="row"]` eikä `td`
     (tuomarikorttien nimisarake). Sanaston LUOKKASÄÄNNÖT kattoivat sen jo -
     nämä elementtisäännöt eivät, ja ilman niitä nimi olisi ainoa solu, jolla on
     yhä pystypehmuste. */
  .card-rows td,
  .card-rows th {
    padding: 0;
    min-width: 0;
    overflow-wrap: anywhere;
  }

  /* `th`in oma pinta ja reunaviiva kuuluvat SARAKEOTSIKOLLE eivätkä kortin
     riville: kapealla ruudulla ne piirtäisivät nimen taakse harmaan palkin
     koko leveydeltä ja viivan kortin sisään. Lihavointi jää - rivin tunniste
     on lihavoitu muutenkin (`.card-title`). */
  .card-rows th {
    background: none;
    border-bottom: none;
  }

  /* OTSIKKO PIILOTETAAN KUN ARVO KERTOO ITSE MIKÄ SE ON (päivä, kello, ottelu,
     rooli) ja JÄÄ KUN ARVO ON PALJAS LUKU, joka voisi olla mitä tahansa (Euroa,
     Vuoroja, Matkustajia, Maksu). Sama päättely kuin lakanan tyhjän rivin
     piilotuksessa. `data-label` säilyy markkauksessa jokaisessa solussa -
     piilotus on CSS:ää eikä attribuutin poisto (juuri-CLAUDE.md). */
  .card-rows .card-lead::before,
  .card-rows .card-mark::before,
  .card-rows .card-title::before,
  .card-rows .card-sub::before,
  .card-rows .card-state::before {
    content: none;
  }

  /* Otsikollinen kaveritieto: otsikko ja arvo SAMALLE riville. Globaali
     `td::before { display: block }` antaisi jokaiselle niistä kaksi riviä.
     Väli on parin YHDISTÄVÄ väli - ks. `.card-rows tr` yllä. */
  .card-rows .card-fact::before {
    display: inline;
    margin-right: var(--space-1);
  }

  /* OTSIKKO JA ARVO OVAT SAMAA KOKOA, ja se sekä ryhmittää että tiivistää.
     Otsikko oli 0.85rem (globaali `td::before`) ja arvo 1rem, jolloin parin
     kaksi puoliskoa eivät näyttäneet kuuluvan yhteen ja rivi hyppi. Yhteinen
     koko on PIENEMPI eikä isompi: fakta on paljas luku tai lyhyt sana
     ("On", "1", "45,00 €"), joka on luettava 0.85rem:llä, ja samalla kortin
     faktarivi lyhenee. Molemmat puoliskot kirjoitetaan tähän auki - pelkkä
     arvon pienennys jättäisi parin riippumaan siitä, että globaali sääntö
     sattuu olemaan sama luku. Erottelun hoitaa väri: otsikko on himmennetty
     (globaali `td::before`), arvo ei. */
  .card-rows .card-fact,
  .card-rows .card-fact::before {
    font-size: 0.85rem;
  }

  /* Rivi 1: päivä ja kello vasemmalla, merkki oikeassa reunassa. Merkki on
     markkauksessa vasta neljäntenä (Rooli on Ottelun ja Sarjan jäljessä), joten
     järjestys tulee `order`ista - EI markkauksen sarakejärjestyksen
     muuttamisesta, joka muuttaisi työpöytänäkymää. */
  .card-rows .card-lead {
    order: 1;
  }

  .card-rows .card-mark {
    order: 2;
    margin-left: auto;
    font-weight: 600;
  }

  /* MERKKI ILMAN OTSAKERIVIÄ ASETTUU TUNNISTEEN VIEREEN, ei sen yläpuolelle.

     `.card-mark` on kirjoitettu `.card-lead`in pariksi (rivi 1: päivä ja kello
     vasemmalla, merkki oikealla). Kun kortissa ei ole `card-lead`ia - kuten
     joukkuesivun jäsenluettelossa, jossa rivi on nimi ja rooli - merkki jäi
     yksin ENSIMMÄISELLE riville eli tunnisteen yläpuolelle, ja rooli luettiin
     edellisen henkilön kortin lopuksi. Mitattu 2026-09-10: `top` 554 vs.
     nimen 582.

     `:has()` ON PAKKO, koska ehto koskee sisarusta eikä CSS:ssä ole
     edeltäjävalitsinta. Rappeutuma on sama hyväksytty kuin korttimuunnoksella
     itsellään (.claude/rules/kayttoliittyma.md, "KOUKKUJA ON KAKSI"): ilman tukea kortti on
     täsmälleen se, mikä se oli ennen tätä sääntöä. */
  .card-rows tr:not(:has(.card-lead)) .card-title {
    order: 2;
    /* `flex: 1` EIKÄ pelkkä `flex-basis: auto`: tunniste kasvaa rivin
       levyiseksi, jolloin merkki päätyy `margin-left: auto`llaan oikeaan
       reunaan JA seuraava solu rivittyy. Pelkällä `auto`lla huoltajan nimi
       mahtui vielä samalle riville ja rivistä tuli kolmen tiedon jono. */
    flex: 1 1 auto;
  }

  .card-rows tr:not(:has(.card-lead)) .card-mark {
    order: 3;
  }

  /* SEURAAVA SOLU ALOITTAA UUDEN RIVIN, muuten se mahtuu tunnisteen ja merkin
     perään ja rivistä tulee kolmen tiedon jono ("Kaskinen Vieno pelaaja
     Huoltajat Kaskinen Marjut"). Mitattu 2026-09-10 molemmilla tekstikooilla:
     ilman tätä merkki ei myöskään päädy oikeaan reunaan, koska rivillä ei ole
     vapaata tilaa jaettavaksi. */
  .card-rows tr:not(:has(.card-lead)) .card-fact {
    flex-basis: 100%;
  }

  /* Rivi 2: rivin tunniste. `flex-basis: 100%` on se, mikä pakottaa
     rivinvaihdon - ei <br> markkauksessa, joka näkyisi myös työpöydällä. */
  .card-rows .card-title {
    order: 3;
    flex-basis: 100%;
    font-weight: 600;
  }

  /* Rivi 3: himmennetty kaveritieto. Solun sisällä oleva linkki pitää oman
     värinsä (`a { color: … }` on tarkempi kuin peritty), joten kohde pysyy
     tunnistettavana linkkinä vaikka rivi on himmennetty. */
  .card-rows .card-sub {
    order: 4;
    flex-basis: 100%;
    font-size: 0.85rem;
    color: var(--color-muted);
  }

  .card-rows .card-fact {
    order: 5;
  }

  /* Tilamerkki omalle rivilleen: "Kuittaus: -" vei kaksi riviä kertoakseen
     ettei mitään ole tapahtunut. TIETO EI KATOA - kuitattu ja kuittaamaton
     ovat molemmat näkyviä tiloja, ei toinen merkin puuttumista. */
  .card-rows .card-state {
    order: 6;
    flex-basis: 100%;
  }

  /* Lomakesolu viimeisenä omalle rivilleen. OTSIKKO JÄÄ eikä sitä kumota
     tässä: globaali `td::before { display: block }` antaa sille oman rivinsä
     kontrollin yläpuolelle, ja juuri se erottaa kontrollin viereisen solun
     arvosta. Solun oma pystypehmuste on nollattu ylempänä, joten lomakkeiden
     väli tulee `row-gap`ista. */
  .card-rows .card-control {
    order: 7;
    flex-basis: 100%;
  }

  /* "Kenen puolesta" -lisävalinta saa kapealla OMAN RIVINSÄ myös suljettuna, ja
     se on mitattu eikä makuasia: isolla tekstillä (1,25x) summary ja "Tallenna"
     täyttivät rivin yhdessä, jolloin "Varaa" putosi kolmannelle riville YKSIN -
     ja yksin jäänyt painike on juuri se, jonka lukija tulkitsee eri rivin
     kontrolliksi (shifts/talkoot.md, "Talkoonäkymät kapealla ruudulla").
     Omalla rivillään lisävalinta jättää "Tallenna" ja "Varaa" samalle riville.
     Leveällä ruudulla sääntöä ei ole: siellä koko lomake on yhdellä rivillä. */
  .volunteer-cell > .beneficiary {
    flex-basis: 100%;
  }
}
