:root {
  --bg: #08080f;
  --panel: rgba(255, 255, 255, .04);
  /* Topprads-kontrollerna ligger över splash art utan panel bakom sig.
     En ljus glaspanel där tappar --muted till ~5,7:1; den här ytan är
     mörk nog att platshållaren håller 7:1 och blurren gör den till glas. */
  --field: rgba(9, 9, 17, .78);
  --panel-solid: #11111c;
  --line: rgba(255, 255, 255, .10);
  --text: #e8ecf4;
  /* Uppgift 8:s fixrunda mätte --muted mot panelen över enbart --bg
     (#121219, 7,04:1). Men .splash är fixed och .splash::after är
     genomskinlig över de översta 78 % av viewporten, så splash art lyser
     igenom varje glaspanel som passerar där: uppmätt 6,49–6,99:1 mot
     verklig splash art på alla tre verifieringskontona. #a9b1c5 mäter
     7,58:1 även mot en påtvingat vit splash och 9,31:1 mot --bg. */
  --muted: #a9b1c5;
  --cyan: #22d3ee;
  --magenta: #f0338a;
  /* Magenta i textvikt. --magenta är en kantfärg: som text mäter den
     3,47-4,53:1 mot en matchrad i vyns övre tredjedel över en ljus
     splash, alltså under WCAG AA, medan cyanen bredvid ligger på
     10,33:1. Samma ton, ljusare, redan i systemet via .badge. */
  --magenta-text: #ffa7cd;
  /* Remake-grått är en KANT- och dekorfärg, aldrig en textfärg. Som text
     på en matchrad i vyns övre tredjedel mäter den 3,03:1 mot en
     påtvingat vit splash och 3,20:1 mot verklig splash art - långt under
     7:1 även vid full opacitet. Ordet "Remake" bär --muted i stället
     (7,21:1 mot verklig splash art, 6,83:1 mot den påtvingat vita - samma
     band som .sub och .ratio på varje annan matchrad); här blir --remake
     kvar på .match.r:s vänsterkant och på formstripens remake-rutor, som
     båda är dekor och inte text. */
  --remake: #6b7280;
  --tier: #8b93a7;
  --display: 'Chakra Petch', system-ui, sans-serif;
  --body: 'Inter', system-ui, sans-serif;
  --radius: 14px;
}

* { box-sizing: border-box; }
html, body { margin: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font: 16px/1.6 var(--body);
  min-height: 100vh;
  overflow-x: hidden;
}
img { max-width: 100%; display: block; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: 4px; }

/* Text bara för skärmläsare. position: absolute är inte kosmetik här:
   .vh-elementen ligger inuti rutnät (matchradens knapp), och ett
   absolutpositionerat barn är inte ett rutnätsobjekt och kan därför inte
   råka ockupera en kolumn. */
.vh {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Splash art bakom allt. Mörkad och blurrad så att den aldrig
   konkurrerar med text. */
.splash {
  position: fixed; inset: 0; z-index: -1;
  background: var(--splash, none) center top / cover no-repeat;
  filter: blur(22px) saturate(1.3);
  opacity: 0; transform: scale(1.1);
}
.splash.on { opacity: .28; }
.splash::after {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(120% 80% at 20% 0%, rgba(34, 211, 238, .22), transparent 60%),
    radial-gradient(100% 70% at 85% 10%, rgba(240, 51, 138, .20), transparent 60%),
    linear-gradient(180deg, rgba(8, 8, 15, .55) 0%, var(--bg) 78%);
}

.wrap { max-width: 1160px; margin: 0 auto; padding: 22px 20px 72px; }

/* Topprad */
.top { display: flex; gap: 16px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
.brand { font: 700 20px/1 var(--display); letter-spacing: .14em; text-decoration: none; }
.brand span { color: var(--cyan); }
.tools { display: flex; gap: 10px; align-items: center; flex: 1 1 340px; min-width: 0; justify-content: flex-end; }
.search { display: flex; gap: 8px; flex: 1 1 auto; min-width: 0; max-width: 400px; }
/* Fältet krymper med raden i stället för att ha en fast bredd. Utan
   min-width: 0 går en flexitem aldrig under sin innehållsbredd: raden
   sprack 63 px utanför viewporten vid 390 px och knappens etikett bröt
   till två rader. */
.search input {
  background: var(--field); border: 1px solid var(--line); color: var(--text);
  border-radius: 10px; padding: 11px 14px; font: 500 15px var(--body);
  flex: 1 1 auto; min-width: 0;
  backdrop-filter: blur(12px);
}
.search input::placeholder { color: var(--muted); }
.search input:focus { border-color: var(--cyan); }
.search button {
  background: linear-gradient(135deg, var(--cyan), #0ea5c4);
  color: #04141a; border: 0; border-radius: 10px;
  padding: 11px 20px; font: 600 15px var(--display); letter-spacing: .04em;
  cursor: pointer; text-transform: uppercase; flex: none; white-space: nowrap;
}
.search button:hover { filter: brightness(1.12); }

/* Språkväxlare. Två textknappar där den aktiva bär accentchippet —
   SV/EN är språkkoder och översätts inte. */
.langs {
  display: flex; gap: 2px; flex: none;
  padding: 3px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--field); backdrop-filter: blur(12px);
}
.langs button {
  background: none; border: 0; cursor: pointer; color: var(--text);
  font: 500 12px/1 var(--display); letter-spacing: .1em;
  padding: 9px 11px; border-radius: 7px;
}
.langs button:hover { background: rgba(255, 255, 255, .07); }
.langs button[aria-pressed="true"] {
  background: rgba(34, 211, 238, .16); color: var(--cyan); font-weight: 700;
  box-shadow: inset 0 0 0 1px rgba(34, 211, 238, .38);
}

/* Smal vy: sökfältet får egen rad. Språkväxlaren står redan före
   formuläret i DOM:en, så ingen order-deklaration behövs — tab-ordningen
   följer den visuella ordningen i båda layouterna (WCAG 2.4.3). */
@media (max-width: 580px) {
  .tools { display: contents; }
  .search { flex: 1 1 100%; max-width: none; }
}

/* Notis om datans ålder */
.staleflag {
  margin: 18px 0 0; padding: 10px 14px; border-radius: 10px;
  background: var(--panel); border: 1px solid var(--line);
  color: var(--muted); font-size: 14px; backdrop-filter: blur(12px);
}

/* Hero */
.hero { margin: 40px 0 36px; }

/* Lokal skärm bakom identitet och rank. De raderna har ingen panel under
   sig och ligger högst upp i vyn, där .splash::after mörkar som svagast
   (.55 vid 0 %-stoppet) — mot en ljus del av en splash mäter --muted där
   ~5,7:1 istället för 7:1. Att mörka hela vyn (~.84 i toppstoppet) eller
   sänka .splash.on till ~.10 hade i praktiken raderat splash-arten, som
   är designens signatur.

   Skärmen är därför lokal: full bredd så att den inte får någon synlig
   sidkant, stark under texten till vänster och utfadad mot höger där det
   inte står något, med en mjuk vertikal ut- och intoning via mask-image.
   KPI-korten, matchlistan och resten av vyn ligger kvar mot orörd splash. */
.heroid { position: relative; }
.heroid::before {
  content: ''; position: absolute; z-index: -1; pointer-events: none;
  top: -62px; bottom: -48px;
  left: calc(50% - 50vw); right: calc(50% - 50vw);
  background: linear-gradient(to right,
    rgba(4, 4, 10, .92) 0%,
    rgba(4, 4, 10, .92) 56%,
    rgba(4, 4, 10, .58) 73%,
    rgba(4, 4, 10, 0) 90%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 50px, #000 calc(100% - 40px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 50px, #000 calc(100% - 40px), transparent 100%);
}
/* Smal vy: texten fyller hela bredden, så skärmen gör det också. */
@media (max-width: 580px) {
  .heroid::before { background: rgba(4, 4, 10, .92); }
}

.identity { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; }
.identity .icon { position: relative; width: 76px; height: 76px; flex: none; }
.identity .icon img {
  width: 76px; height: 76px; border-radius: 16px;
  border: 1px solid var(--line); box-shadow: 0 0 28px rgba(34, 211, 238, .18);
}
.identity .icon b {
  position: absolute; bottom: -9px; left: 50%; transform: translateX(-50%);
  background: var(--panel-solid); border: 1px solid var(--line); border-radius: 7px;
  font: 600 12px/1 var(--display); padding: 4px 7px;
}
.identity h1 { font: 700 clamp(32px, 6vw, 52px)/1 var(--display); margin: 0; letter-spacing: .01em; }
.identity h1 span { color: var(--muted); font-weight: 500; }
.identity .lvl { color: var(--muted); font-size: 14px; margin: 6px 0 0; }

/* Rank */
.rank { display: flex; gap: 22px; align-items: center; flex-wrap: wrap; margin-top: 30px; }
.rank .emblem { width: 108px; height: 108px; flex: none; filter: drop-shadow(0 6px 22px rgba(0, 0, 0, .6)); }
.rank .tierword { font: 700 clamp(34px, 7vw, 60px)/1 var(--display); color: var(--tier); margin: 0; }
.rank .tierword em { font-style: normal; font-weight: 500; opacity: .65; }
.rank .lp { color: var(--muted); font-size: 15px; margin: 6px 0 0; }
.rank .bar { height: 6px; background: var(--line); border-radius: 3px; overflow: hidden; margin-top: 10px; max-width: 300px; }
.rank .bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--tier), var(--cyan)); border-radius: 3px; width: 0; }

/* KPI-kort. Rubriken säger vilket fönster korten räknas över: rank-raden
   strax ovanför bär Riots säsongssiffra (t.ex. 252-229) och korten de
   senaste spelade matcherna, och utan rubrik läses de två som en
   motsägelse i stället för som två olika fönster. Samma h2-stil som
   sektionsrubrikerna, men med egna marginaler eftersom den sitter tätt
   ihop med kortraden under sig. */
.kpihead { margin: 30px 0 0; }
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-top: 12px; }
.kpi {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 18px; backdrop-filter: blur(14px); position: relative; overflow: hidden;
}
.kpi::before {
  content: ''; position: absolute; inset: 0 0 auto 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--cyan), transparent); opacity: .5;
}
.kpi b { display: block; font: 700 clamp(26px, 4vw, 34px)/1 var(--display); }
.kpi small { display: block; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .1em; margin-top: 7px; }

/* Formstrip */
.formstrip { display: flex; gap: 4px; margin-top: 26px; flex-wrap: wrap; }
.formstrip i {
  width: 16px; height: 26px; border-radius: 3px; background: var(--magenta);
  box-shadow: 0 0 10px rgba(240, 51, 138, .45);
}
.formstrip i.w { background: var(--cyan); box-shadow: 0 0 10px rgba(34, 211, 238, .45); }
.formstrip i.r { background: var(--remake); box-shadow: none; }

/* Innehåll */
.grid { display: grid; grid-template-columns: minmax(0, 1fr) 330px; gap: 26px; margin-top: 42px; }
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }

/* Kolumnerna har olika struktur: vänster inleds med en naken <h2> och sedan
   matchkorten, medan sidokolumnens rubriker ligger INUTI sina glaspaneler.
   Utan kompensation börjar första panelen i linje med rubriktexten i stället
   för med första matchkortet. Offseten är h2:ans radhöjd plus dess
   undermarginal — skrivet som calc så att härledningen syns om någon ändrar
   h2-regeln ovan. Gäller bara i tvåkolumnsläget; under 900px staplas de. */
@media (min-width: 901px) {
  aside { padding-top: calc(13px + 16px); }
}
h2 { font: 600 13px/1 var(--display); letter-spacing: .18em; text-transform: uppercase; color: var(--muted); margin: 0 0 16px; }

/* Matchkortet är sedan uppgift 4 bara skalet: rutnätet flyttade in i
   .matchrow, som är en riktig <button>, så att hela raden är ett
   klick- och fokusmål och scoreboarden kan ligga i full bredd under den. */
.match {
  background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--magenta);
  border-radius: 12px; margin-bottom: 9px; backdrop-filter: blur(14px);
  position: relative; overflow: hidden;
}
.matchrow {
  display: grid; grid-template-columns: 58px minmax(0, 1fr) auto 18px; gap: 15px; align-items: center;
  width: 100%; margin: 0; padding: 13px 16px; text-align: left;
  background: none; border: 0; color: inherit; font: inherit; cursor: pointer;
}
/* Ett äldre svar utan lag-array ger ingen scoreboard och därför ingen
   knapp - bara raden, utan pekare och utan pilkolumn. */
.matchrow.plain { grid-template-columns: 58px minmax(0, 1fr) auto; cursor: default; }
/* Hovern MÖRKAR raden. Den självklara riktningen - rgba(255,255,255,.045),
   som resten av sidan använder på knappar UTAN brödtext i sig - lyfte ytan
   bakom radens egna --muted-texter (.sub, .ratio, .outcome small) från
   6,98-7,12:1 till 6,18-6,32:1 mot verklig splash art. En hovereffekt är
   ett tillstånd som WCAG gäller i, inte ett undantag från den. Mörkare
   glas höjer i stället samma texter till 8,12-8,22:1, och hela raden är
   nu ett klickmål - så effekten ska synas över hela raden, inte bara på
   pilen. */
.matchrow:hover { background: rgba(4, 4, 10, .38); }
/* Kortet klipper sitt innehåll (overflow: hidden), så en fokusring
   UTANFÖR knappen hade kapats på tre sidor. Den ritas innanför i stället. */
.matchrow:focus-visible { outline-offset: -3px; }
.matchrow .mid { display: block; min-width: 0; }
.matchrow .sub, .matchrow .outcome { display: block; }
/* Pilen: ren dekor, därför aria-hidden i markupen. Riktningen byts direkt
   på aria-expanded; bara övergången mellan lägena är rörelse och ligger
   i reduced-motion-guarden längst ned. */
.chev {
  width: 8px; height: 8px; margin-top: -3px; justify-self: center;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
}
.matchrow:hover .chev { border-color: var(--text); }
.matchrow[aria-expanded="true"] .chev { transform: rotate(225deg); margin-top: 3px; }
.match.w { border-left-color: var(--cyan); }
/* Remake-raden dämpas av sin grå vänsterkant och av att utgångsordet
   tappar accentfärgen - INTE av element-opacity. opacity dämpar texten
   och glaspanelen bakom den i samma svep, så kontrastkvoten kollapsar i
   stället för att hålla: uppmätt 1,96:1 för ordet "Remake" och 3,50:1
   för .sub och de små raderna, på en remake-rad i vyns övre tredjedel.
   Att behålla opacity och ljusa upp färgen hjälper inte - det är
   dämpningen av bakgrunden som är problemet. Dämpningen var dessutom
   ojämn: .reveal-animationens forwards-fyllning skrev över opacity till
   1 igen, så bara besökare med prefers-reduced-motion: reduce såg den. */
.match.r { border-left-color: var(--remake); }
.match::after {
  content: ''; position: absolute; left: 0; bottom: 0; height: 1px; width: 0;
  background: linear-gradient(90deg, var(--magenta), transparent);
}
.match.w::after { background: linear-gradient(90deg, var(--cyan), transparent); }
.match .champ {
  width: 58px; height: 58px; flex: none;
  clip-path: polygon(14% 0, 100% 0, 100% 86%, 86% 100%, 0 100%, 0 14%);
}
.match .l1 { display: flex; gap: 11px; align-items: baseline; flex-wrap: wrap; }
.kda { font: 700 21px/1 var(--display); }
.kda em { font-style: normal; color: var(--magenta-text); }
.ratio { color: var(--muted); font-size: 13px; }
.badge {
  font: 600 11px/1 var(--display); letter-spacing: .08em; text-transform: uppercase;
  padding: 4px 8px; border-radius: 5px; background: rgba(240, 51, 138, .16);
  color: var(--magenta-text); border: 1px solid rgba(240, 51, 138, .4);
}
.sub { color: var(--muted); font-size: 13px; margin-top: 3px; }
/* Djupare mått (guld/min, skadeandel, CS vid 10 min): en egen, mindre rad
   under den vanliga sub-raden, samma --muted-ton men lite mindre och tätare
   ihop mot den så att de två raderna läses som ETT sekundärt block, inte
   två separata rader med samma vikt som konkurrerar med kda-raden ovanför.
   Utelämnad helt (ingen rad, inget bindestreck) på matcher utan
   challenges-block - se hasDeep i app.js. */
.sub.deep { font-size: 12px; margin-top: 1px; }
.items { display: flex; gap: 3px; margin-top: 9px; }
.items img, .items s {
  width: 27px; height: 27px; border-radius: 5px;
  background: rgba(255, 255, 255, .05); border: 1px solid var(--line);
}
.outcome { text-align: right; font: 600 14px/1.3 var(--display); letter-spacing: .06em; text-transform: uppercase; }
.outcome.w { color: var(--cyan); }
.outcome.l { color: var(--magenta-text); }
.outcome.r { color: var(--muted); }
.outcome small { display: block; color: var(--muted); font: 400 12px var(--body); text-transform: none; letter-spacing: 0; margin-top: 4px; }
@media (max-width: 580px) {
  /* Pilen får en egen, uttryckligt placerad kolumn på rad 1. Utan
     grid-row hade den auto-placerats EFTER .outcome, som flyttar ner till
     rad 2 här, och hamnat längst ned till höger i stället för uppe vid
     raden den styr. */
  .matchrow { grid-template-columns: 48px minmax(0, 1fr) 16px; gap: 12px; padding: 12px 13px; }
  .matchrow.plain { grid-template-columns: 48px minmax(0, 1fr); }
  .matchrow .chev { grid-column: 3; grid-row: 1; }
  .match .champ { width: 48px; height: 48px; }
  .outcome { grid-column: 2; text-align: left; }
}

/* Scoreboard: fälls ut ur matchraden, byggd av lag-arrayen som redan låg
   i svaret.

   Invarianten är NOLL anrop mot /api/* och NOLL mot Riots API - det är de
   som äter den delade nyckelbudgeten. Championporträtten (.pportrait
   nedan) hämtas däremot när raden fälls ut, från Data Dragon: en publik
   CDN utan nyckel och utan kvot mot oss. De räknas för sig och är
   förväntade. Ta inte bort porträtten för att "återställa" noll anrop -
   det är precis det misstaget den här kommentaren en gång beskrev som en
   funktion. Hela resonemanget står i app.js, ovanför scoreboardbygget. */
.board { padding: 0 16px 14px; border-top: 1px solid var(--line); }
/* table-layout: fixed är inte kosmetik. Med automatisk tabellayout styrs
   bredden av cellernas MIN-CONTENT, och `white-space: nowrap` på
   sifferkolumnerna plus ett 30 px porträtt i namncellen gör det minimumet
   bredare än en 390 px vy har plats för. Tabellen sköt då hela
   matchsektionen 24 px utanför viewporten - osynligt för besökaren bara
   för att body har overflow-x: hidden, alltså precis den sortens fel som
   ligger kvar tills någon mäter. Med fast layout äger `width: 100%` och
   kolumnbredderna nedan saken, och innehållet kapas med ellips i stället
   för att knuffa. */
.sb { width: 100%; border-collapse: collapse; margin-top: 13px; table-layout: fixed; }
.sb thead th:nth-child(2) { width: 98px; }
.sb thead th:nth-child(3) { width: 62px; }
.sb thead th:nth-child(4) { width: 78px; }
.sb caption {
  text-align: left; padding-bottom: 8px;
  font: 600 11px/1 var(--display); letter-spacing: .16em; text-transform: uppercase; color: var(--muted);
}
.sb th, .sb td { padding: 6px 5px; }
.sb thead th {
  font: 600 10px/1 var(--display); letter-spacing: .12em; text-transform: uppercase; color: var(--muted);
  text-align: right; border-bottom: 1px solid var(--line); padding-bottom: 8px;
}
.sb thead th:first-child { text-align: left; padding-left: 9px; }
.sb tbody th { font-weight: 400; text-align: left; padding-left: 9px; }
.sb tbody td {
  text-align: right; color: var(--muted); font-size: 13px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.sb tbody td:last-child { padding-right: 0; }
.sb tbody tr + tr th, .sb tbody tr + tr td { border-top: 1px solid rgba(255, 255, 255, .05); }
.pcell { display: flex; align-items: center; gap: 9px; min-width: 0; }
/* Championporträttet. Hämtas från Data Dragon när raden fälls ut - en
   publik CDN utan nyckel och utan kvot mot oss, alltså inget som rör den
   budget de två klicknivåerna finns för. Det är /api/* och Riots API som
   ska stå på noll vid en utfällning, inget annat. */
.pportrait { width: 26px; height: 26px; flex: none; border-radius: 7px; border: 1px solid var(--line); }
/* Anonymiserad rad: porträttet gråskalas med. En bild ser klickbar ut, och
   namnet bredvid den är det inte - affordansen måste stämma för hela
   cellen, inte bara för textdelen. Gråskala och inte opacity: opacity
   hade dämpat ytan bakom lika mycket som bilden, och det är precis den
   mekanismen som en gång kollapsade kontrasten på .match.r. */
.pcell.anon .pportrait { filter: grayscale(1) brightness(.72); }
/* Texten bär basline-radningen; .pcell självt centrerar mot porträttet. */
.ptext { display: flex; align-items: baseline; gap: 9px; min-width: 0; flex: 1 1 auto; }
.pchamp {
  color: var(--muted); font-size: 12px; flex: none;
  max-width: 78px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pname { display: block; flex: 1 1 auto; min-width: 0; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pname .pt { color: var(--muted); }
/* Namnet självt står i --text; accentfärgen bär understrykningen, alltså
   en kant och inte glyfen - samma regel som .champs-toggle följer. */
a.pname {
  color: var(--text); text-decoration: underline; text-decoration-thickness: 1px;
  text-decoration-color: rgba(34, 211, 238, .55); text-underline-offset: 3px;
}
a.pname:hover, a.pname:focus-visible { text-decoration-color: var(--cyan); }
/* Anonymiserad deltagare: ingen länk, ingen understrykning, ingen pekare,
   dämpad och kursiv. Ska läsas som "den här går inte att öppna", inte som
   en trasig cell - i en typisk high-elo-match är flera rader sådana, och
   det är normalt. */
.pname.anon { color: var(--muted); font-style: italic; cursor: default; }
/* Raden för profilen som visas. Markeringen är visuell; skärmläsare får
   samma sak sagt i klartext via .vh-texten i cellen.

   Tonen MÖRKAR raden, den ljusar inte upp den, och accenten ligger som en
   kant i cellens vänsterkant - aldrig som en tonad yta bakom siffrorna.
   Första försöket var rgba(34, 211, 238, .075) över hela raden: det lyfte
   ytan bakom --muted-siffrorna till 6,19:1 mot verklig splash art, alltså
   UNDER 7:1 och sämre än varje annan rad i samma tabell (7,12:1). En
   markering får inte kosta läsbarhet i det den markerar. */
.sb tr.me th, .sb tr.me td { background: rgba(4, 4, 10, .42); }
.sb tr.me th { box-shadow: inset 2px 0 0 var(--cyan); }
.sb tr.me .pname { font-weight: 600; }

/* Scoreboardens smala läge. Det här blocket MÅSTE stå efter basreglerna
   ovan, inte i matchradens 580-block längre upp: flera av reglerna här har
   exakt samma specificitet som sin basregel (.board, .sb th, och
   kolumnbredderna teckenidentiskt), och vid lika specificitet vinner den
   som står sist. Placerade högre upp var de tysta - kolumnerna behöll sina
   breda värden och namnkolumnen krympte till en bokstav per spelare, vilket
   syntes först när bredderna mättes i webbläsaren. */
@media (max-width: 580px) {
  .board { padding: 0 10px 12px; }
  /* Championnamnet får en egen rad över spelarnamnet när bredden inte
     räcker till båda på en. Porträttet står kvar till vänster om de två
     raderna, så radhöjden är densamma som utan det. */
  .ptext { display: block; }
  .pchamp { display: block; max-width: none; }
  .pportrait { width: 26px; height: 26px; border-radius: 7px; }
  .pcell { gap: 8px; }
  /* Sifferkolumnerna ner till det de faktiskt behöver, så att
     namnkolumnen får resten. Bredderna är mätta, inte gissade: inget
     sifferfält spiller över sin kolumn vid 390 px. */
  .sb thead th:nth-child(2) { width: 84px; }
  .sb thead th:nth-child(3) { width: 44px; }
  .sb thead th:nth-child(4) { width: 58px; }
  .sb th, .sb td { padding: 6px 3px; }
  .sb thead th:first-child, .sb tbody th { padding-left: 6px; }
  /* Etiketten för en anonymiserad spelare är ingen identifierare utan en
     förklaring, och den får därför radbrytas i stället för att kapas mitt
     i ordet - "Anonymous playe..." säger mindre än två hela rader gör.
     Riot ID:n bredvid kapas fortfarande med ellips: ett namn som bryts på
     mitten läses som två namn. Att raden blir något högre är inget att
     dölja; den ska se annorlunda ut. */
  .pname.anon { font-size: 12px; white-space: normal; }
}

/* Live-indikator. Syns bara när /api/live svarar med ett pågående spel -
   annars står #livewrap tom och tar ingen plats alls. */
.live {
  display: flex; align-items: center; gap: 13px; flex-wrap: wrap;
  margin-top: 24px; padding: 11px 16px; max-width: max-content;
  background: rgba(34, 211, 238, .09); border: 1px solid rgba(34, 211, 238, .34);
  border-radius: 12px; backdrop-filter: blur(14px);
}
.empty .live { margin-inline: auto; text-align: left; }
.livedot {
  position: relative; flex: none; width: 9px; height: 9px; border-radius: 50%;
  background: var(--cyan); box-shadow: 0 0 10px rgba(34, 211, 238, .75);
}
.livechamp { width: 36px; height: 36px; flex: none; border-radius: 9px; border: 1px solid var(--line); }
.livetext { min-width: 0; }
.livelabel { display: block; font: 700 12px/1 var(--display); letter-spacing: .14em; text-transform: uppercase; color: var(--cyan); }
.livemeta { display: block; color: var(--muted); font-size: 13px; margin-top: 5px; }
.liveclock { padding-left: 6px; text-align: right; }
.liveclock small { display: block; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .1em; }
.livetime { display: block; margin-top: 4px; font: 700 19px/1 var(--display); font-variant-numeric: tabular-nums; }

/* Matchen tog slut medan sidan stod öppen. Samma plats, samma form och
   samma prick som live-rutan, men neutral panel i stället för cyanfylld
   och en släckt prick i stället för en pulserande - den ska läsas som att
   indikatorn LAGT SIG, inte som att något nytt dykt upp. Därför ingen
   accentkant, ingen neon och ingen knapp. */
.liveover {
  display: flex; align-items: center; gap: 11px; flex-wrap: wrap;
  margin: 24px 0 0; padding: 11px 16px; max-width: max-content;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 12px; backdrop-filter: blur(14px);
  color: var(--muted); font-size: 13px;
}
.empty .liveover { margin-inline: auto; text-align: left; }
/* Släckt prick: dekorfärg, aldrig textfärg - se --remake-kommentaren överst. */
.livedot.off { background: var(--remake); box-shadow: none; }

aside section {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 20px; margin-bottom: 14px; backdrop-filter: blur(14px);
}
.roles p { display: grid; grid-template-columns: 74px 1fr 26px; gap: 11px; align-items: center; font-size: 14px; margin: 9px 0; }
.roles .bar { height: 5px; background: var(--line); border-radius: 3px; overflow: hidden; }
.roles .bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--cyan), var(--magenta)); width: 0; }
.roles span:last-child { text-align: right; color: var(--muted); }
.mast { display: flex; gap: 12px; align-items: center; margin: 12px 0; }
.mast img { width: 42px; height: 42px; border-radius: 50%; border: 1px solid var(--line); }
.mast div { flex: 1; min-width: 0; }
.mast b { font-weight: 600; font-size: 15px; }
.mast .pts { font: 700 17px var(--display); color: var(--cyan); }

/* Championpanel: samma flex-rad-mönster som mastery ovan (porträtt, namn,
   en sub-rad), men en egen klass - de två panelerna visar olika fönster på
   datan (matchhistorik kontra livstids mastery-poäng) och ska kunna ändras
   oberoende av varandra. */
.champrow { display: flex; gap: 12px; align-items: center; margin: 12px 0; }
.champrow img { width: 42px; height: 42px; border-radius: 10px; border: 1px solid var(--line); flex: none; }
.champrow div { flex: 1; min-width: 0; }
.champrow b { font-weight: 600; font-size: 15px; }
/* "Visa fler": ingen nätverksanrop, bara en hidden-flagga på #champs-extra
   (se lyssnaren i app.js) - stats.byChampion ligger redan i minnet. Kanten
   bär accentfärgen i vila (dekor, inte text) och texten själv står i
   --muted, samma ton som rubriken ovanför den; hover/fokus lyfter båda till
   en starkare kant och --text, aldrig till neon i sig. */
.champs-toggle {
  display: block; width: 100%; margin-top: 6px;
  background: none; border: 1px dashed var(--line); border-radius: 8px;
  color: var(--muted); font: 600 12px/1 var(--display); letter-spacing: .06em; text-transform: uppercase;
  padding: 9px; cursor: pointer;
}
.champs-toggle:hover, .champs-toggle:focus-visible { border-color: var(--cyan); color: var(--text); }

/* Tillstånd */
.state { padding: 90px 0; max-width: 580px; }
.state h1 { font: 700 clamp(28px, 5vw, 40px)/1.1 var(--display); margin: 0 0 12px; }
.state p { color: var(--muted); margin: 0; }

/* Tomt läge: kontot finns men saknar matcher, rank och mastery (t.ex.
   ett helt nytt konto). Ska läsas som en avsiktlig inbjudan att söka,
   inte som ett fel eller en vägg av nollor. Centrerad och fristående
   från .state, med en accentglyf istället för grå felruta. */
.empty { max-width: 480px; margin: 0 auto; padding: 100px 0 90px; text-align: center; }
.empty .glyph {
  width: 88px; height: 88px; margin: 0 auto 28px; position: relative;
  clip-path: polygon(16% 0, 100% 0, 100% 84%, 84% 100%, 0 100%, 0 16%);
  background: linear-gradient(135deg, rgba(34, 211, 238, .16), rgba(240, 51, 138, .14));
  border: 1px solid var(--line);
  box-shadow: 0 0 34px rgba(34, 211, 238, .2);
}
.empty .glyph::after {
  content: ''; position: absolute; inset: 24px;
  border: 2px solid var(--cyan); border-radius: 50%; opacity: .85;
}
.empty h1 { font: 700 clamp(24px, 4.5vw, 32px)/1.25 var(--display); margin: 0 0 12px; }
.empty p { color: var(--muted); margin: 0; }
.empty strong { color: var(--text); font-weight: 600; }

footer { color: var(--muted); font-size: 12px; margin-top: 56px; border-top: 1px solid var(--line); padding-top: 18px; }

/* Rörelse */
@media (prefers-reduced-motion: no-preference) {
  .splash { transition: opacity 1.2s ease; }
  .rank .bar i, .roles .bar i { transition: width .9s cubic-bezier(.2, .8, .2, 1); }
  .match::after { transition: width 1s ease .1s; }
  .match.in::after { width: 100%; }
  .reveal { opacity: 0; transform: translateY(14px); animation: rise .55s cubic-bezier(.2, .8, .2, 1) forwards; }
  @keyframes rise { to { opacity: 1; transform: none; } }
  .chev { transition: transform .25s cubic-bezier(.2, .8, .2, 1), margin-top .25s cubic-bezier(.2, .8, .2, 1); }

  /* Live-pulsen. Ringen finns BARA här inne - content, storlek och
     animation i samma regel - så att en besökare med reduced-motion får en
     stilla prick och inte en fryst ring runt den.

     Ringen är dekor utan text i sig, och det är med flit: en
     `animation: ... forwards` skriver över en vanlig opacity-deklaration
     för alla UTOM dem med reduced-motion, och exakt den växelverkan dolde
     ett kontrastfel i .match.r i veckor (se kommentaren där). Ingen text i
     live-rutan rör opacity, varken här eller utanför guarden. */
  /* :not(.off) - den släckta pricken i "matchen är slut"-noteringen återanvänder
     .livedot för form och placering, men ska inte pulsera. Utan undantaget
     hade noteringen blinkat lika ivrigt som ett pågående spel. */
  .livedot:not(.off)::after {
    content: ''; position: absolute; inset: 0; border-radius: 50%;
    border: 1px solid var(--cyan);
    animation: livepulse 1.9s cubic-bezier(.2, .8, .2, 1) infinite;
  }
  @keyframes livepulse {
    0% { transform: scale(1); opacity: .9; }
    70% { transform: scale(3); opacity: 0; }
    100% { transform: scale(3); opacity: 0; }
  }
}
