/* ============================================================
   kauke — design system · Retning A «Lun» (warm · round · editorial)
   Implemented from "kauke retninger.dc.html" (Claude Design).
   Tokens → components → layout → responsive.
   ============================================================ */

/* ---------- Tokens ----------
   Every colour in this stylesheet lives here and nowhere else (design-dark-mode.md §7.1), and
   DesignTokensTest fails the build if a hex literal reappears further down or in a template.
   That rule is what makes the dark block below possible at all: a hardcoded hex is exactly the
   value that cannot follow a theme. */
:root {
  color-scheme: light;

  /* Brand */
  /* Two reds, and the split only shows up in the dark palette. --red is the brand as *ink*:
     link text, an active tab, an eyebrow. --red-fill is the brand as a *surface*, with
     --on-dark text on top. In light they are the same colour; on dark the ink has to lift to
     stay readable (#c2362a on #1f1c19 is ~3.3:1) while the fill must not, because what the
     button needs is white-on-red and that pair does not change. */
  --red:        #c2362a;  /* Kaukerød — brand ink */
  --red-fill:   #c2362a;  /* Kaukerød — brand surface, under --on-dark */
  --red-fill-hover: #a82c21;
  --red-bright: #d6402f;  /* Lys rød — wordmark accent */
  --red-dark:   #a82c21;  /* the hover under --red, and destructive text */
  --red-ink:    #7c3b32;  /* text inside a red notice */
  --ink:        #1f2a3d;  /* Blekk — headings, strong text */
  --ink-hover:  #2b3852;  /* the pressed «Administrer» state and friends */

  /* Surfaces */
  --page:         #faf7f2;      /* Fløte — page background */
  --cream:        var(--page);  /* alias: the old name for the same role */
  --panel:        #f3eee6;      /* panels, neutral pills */
  --surface:      #fff;         /* card surface */
  --white:        var(--surface); /* alias */
  --surface-tint: #fffdf9;      /* the warm hover wash on secondary buttons and rows */
  /* What a scrolled toolbar's edge fades to. In light the page is nearly the card's white, so
     the fade aims well past it (the panel shade was still too close to white on a phone); in
     dark the page itself is already far from the surface, so it aims at the page. */
  --toolbar-fade: var(--disabled);
  --disabled:     #e7ddd4;      /* a button that cannot be pressed */
  /* Text on a filled brand colour, a scrim or a photo. Never themed: what it sits on is dark
     in both modes, so flipping it would put white text on white. */
  --on-dark:      #fff;

  /* Text */
  --slate:      #5a6473;  /* secondary text */
  --body:       #33404f;  /* long-form body */
  --body-soft:  #444c5a;
  /* Dempet, ikke utydelig: #8a8072 ga 3.63:1 mot --page, og AA krever 4.5:1 for tekst i denne
     størrelsen. Den mørke paletten var allerede over grensen (5.94:1), så det er bare denne
     verdien som flyttes. 5.02:1. Bunnteksten bærer selgeropplysningene Vipps krever «godt
     synlig», og 3.63:1 er en dårlig påstand om synlig. */
  --taupe:      #74695c;  /* muted labels, meta */
  --taupe-light:#a79f90;  /* placeholders, disabled text */
  --mark:       #bcae99;  /* empty-state glyph fill */
  --mark-soft:  #cdc6ba;

  /* Lines */
  --border:        #ece7df; /* card / section borders */
  --border-warm:   #e7e0d3;
  --border-input:  #ddd5c8;
  --border-strong: #cdc2b0; /* hover borders, dashed drop zones */
  --border-dashed: #e0d8cb; /* the empty state's dashed rule */
  --hair:          #f1ece4; /* table row hairline */

  /* Status */
  --green:        #2f7d5b; --green-soft:#e7f1ec; --green-border:#d4e5dc; /* Godkjent */
  --green-hover:  #266b4d;
  --amber:        #b07a1e; --amber-soft:#f6efe3; --amber-border:#ecdfc7; /* Venter */
  --amber-text:   #7a5514;
  --red-soft:     #fbeeec; --red-soft-border:#f3d9d4;
  --info-soft:    #eef0f3;

  /* Avatar palette — backgrounds for --on-dark initials */
  --av-navy:  #22304a;
  --av-brown: #7c3b32;
  --av-steel: #3f5066;
  --av-stone: #9a9182;

  /* Image placeholder stripes */
  --ph-a: #efe9df;
  --ph-b: #e7e0d3;

  /* Focus. The ring is an accessibility surface, so it has a name rather than four spellings. */
  --focus:       rgba(194,54,42,.16);
  --focus-ring:  rgba(194,54,42,.35);
  --focus-green: rgba(47,125,91,.12);

  /* Scrims. These lie over photographs, which are the same photographs in both modes, so they
     do not follow the theme — only --overlay does, because it dims the page itself. */
  --overlay:      rgba(31,42,61,.45);
  --scrim-soft:   rgba(31,42,61,.55);
  --scrim:        rgba(31,42,61,.66);
  --scrim-strong: rgba(31,42,61,.78);
  --glass:        rgba(255,255,255,.08);
  --glass-hover:  rgba(255,255,255,.16);
  --glass-soft:   rgba(255,255,255,.14);
  --glass-track:  rgba(255,255,255,.3);

  /* Lightbox chrome — dark in both modes: a picture is looked at against dark, always. */
  --lb-bg:    #171f2c;
  --lb-title: #cdd3dc;
  --lb-label: #6b7585;
  --lb-a:     #2a3445;
  --lb-b:     #222c3b;

  /* The <select> chevron, drawn in --taupe. A data URI, so the colour is baked into the
     image and the dark block has to hand over a second one. */
  --chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8'><path d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%238a8072' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>");

  /* The widget sandbox's paper. Light in both modes on purpose (design-dark-mode.md §5,
     option 2): a widget is content, like a photo, and it cannot be told which theme it is in. */
  --widget-paper: #fff;

  /* The wordmark is a picture, so it cannot follow a colour token — it needs a second file.
     Both twins already existed for the e-mail's dark mode: cream type, #d6402f leaf. */
  --wordmark:       url("../images/kauke-full-rBZgmdO.png");
  --wordmark-small: url("../images/kauke-small-I9smjqc.png");

  /* Elevation */
  --shadow-card:      0 14px 34px -28px rgba(31,42,61,.45);
  --shadow-lg:        0 22px 60px -30px rgba(31,42,61,.40);
  --shadow-pop:       0 18px 44px -28px rgba(31,42,61,.42);
  --shadow-btn:       0 8px 18px -8px rgba(194,54,42,.6);
  --shadow-btn-green: 0 8px 18px -10px rgba(47,125,91,.6);
  --shadow-soft:      0 14px 34px -30px rgba(31,42,61,.4);
  --shadow-toolbar:   0 8px 20px -16px rgba(31,42,61,.4);
  --shadow-toast:     0 10px 26px -18px rgba(31,42,61,.4);
  --shadow-red:       0 6px 16px -10px rgba(194,54,42,.4);

  /* Type */
  --serif: 'Newsreader', Georgia, 'Times New Roman', serif;
  --sans:  'Public Sans', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  --mono:  ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Radius */
  --r-sm: 8px; --r-md: 12px; --r-lg: 16px; --r-xl: 18px; --r-pill: 999px;
}

/* ---------- Mørk modus (design-dark-mode.md §3) ----------
   The same declarations twice, and the duplication is the whole trick. The media query serves
   «følg systemet»; the attribute serves an explicit choice from the footer toggle; and
   :not([data-theme="light"]) is what lets an explicit *light* choice beat a dark OS. There is no
   preprocessor to write it once with (asset-mapper, no build step), and two blocks a grep keeps
   honest beat adding one. **Edit both.** DesignTokensTest compares them declaration by
   declaration and fails if they drift.

   Two rules decide the values. Warm, never neutral: every dark surface keeps a brown cast, so
   the page reads as unlit paper rather than as a terminal. And hover reverses direction — in the
   light palette a hover goes darker, here it must go lighter, or every control looks like it is
   being switched off when you touch it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --red:        #e2705f;  /* #c2362a as ink on #1f1c19 is ~3.3:1. This is ~6.4:1. */
    --red-fill:   #c2362a;  /* unchanged: what matters here is --on-dark on top of it */
    --red-fill-hover: #d6402f;
    --red-dark:   #f08c7c;  /* hover reverses direction: lighter, not darker */
    --red-ink:    #eaa79b;
    --ink:        #f2ece2;
    --ink-hover:  #fff;

    --page:         #14120f;
    --panel:        #332e2b;
    --surface:      #1f1c19;
    --surface-tint: #252119;
    --disabled:     #2e2a25;
    --toolbar-fade: var(--page);

    --slate:      #c3bcb0;
    --body:       #e6e0d6;
    --body-soft:  #d6cfc3;
    --taupe:      #b2a694;
    --taupe-light:#7d7466;
    --mark:       #5c5347;
    --mark-soft:  #4a433a;

    --border:        #3a342d;
    --border-warm:   #423b33;
    --border-input:  #4c443b;  /* must stay findable: an input you cannot see is worse here */
    --border-strong: #534a40;
    --border-dashed: #3a342d;
    --hair:          #2a2621;

    --green:       #4f9d78; --green-soft:#172a22; --green-border:#234a3a;
    --green-hover: #63b28c;
    --amber:       #d19a3c; --amber-soft:#2a2115; --amber-border:#4a3a1e;
    --amber-text:  #e0b45f;
    --red-soft:    #2a1a17; --red-soft-border:#4a2a24;
    --info-soft:   #232c3a;

    --av-navy:  #3d5478;
    --av-brown: #9a5147;
    --av-steel: #5b7392;

    --ph-a: #241f1b;
    --ph-b: #2c2621;

    --focus:       rgba(226,112,95,.28);
    --focus-ring:  rgba(226,112,95,.45);
    --focus-green: rgba(79,157,120,.28);

    --overlay: rgba(0,0,0,.72);

    --wordmark:       url("../images/kauke-full-dark-JRxJ9no.png");
    --wordmark-small: url("../images/kauke-small-dark-URBzRtx.png");

    --chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8'><path d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%239a9080' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>");

    /* Two dark things cast no contrast on each other, so the shadow all but goes and the border
       does the work: --border is a full step lighter than --surface, and that edge is what makes
       a card read as a card at all. */
    --shadow-card:      0 10px 28px -24px rgba(0,0,0,.8);
    --shadow-lg:        0 22px 60px -34px rgba(0,0,0,.85);
    --shadow-pop:       0 18px 44px -30px rgba(0,0,0,.8);
    --shadow-btn:       0 8px 18px -10px rgba(194,54,42,.5);
    --shadow-btn-green: 0 8px 18px -12px rgba(47,125,91,.5);
    --shadow-soft:      none;
    --shadow-toolbar:   none;
    --shadow-toast:     0 10px 26px -20px rgba(0,0,0,.8);
    --shadow-red:       0 6px 16px -12px rgba(194,54,42,.6);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --red:        #e2705f;  /* #c2362a as ink on #1f1c19 is ~3.3:1. This is ~6.4:1. */
  --red-fill:   #c2362a;  /* unchanged: what matters here is --on-dark on top of it */
  --red-fill-hover: #d6402f;
  --red-dark:   #f08c7c;  /* hover reverses direction: lighter, not darker */
  --red-ink:    #eaa79b;
  --ink:        #f2ece2;
  --ink-hover:  #fff;

  --page:         #14120f;
  --panel:        #332e2b;
  --surface:      #1f1c19;
  --surface-tint: #252119;
  --disabled:     #2e2a25;
  --toolbar-fade: var(--page);

  --slate:      #c3bcb0;
  --body:       #e6e0d6;
  --body-soft:  #d6cfc3;
  --taupe:      #b2a694;
  --taupe-light:#7d7466;
  --mark:       #5c5347;
  --mark-soft:  #4a433a;

  --border:        #3a342d;
  --border-warm:   #423b33;
  --border-input:  #4c443b;  /* must stay findable: an input you cannot see is worse here */
  --border-strong: #534a40;
  --border-dashed: #3a342d;
  --hair:          #2a2621;

  --green:       #4f9d78; --green-soft:#172a22; --green-border:#234a3a;
  --green-hover: #63b28c;
  --amber:       #d19a3c; --amber-soft:#2a2115; --amber-border:#4a3a1e;
  --amber-text:  #e0b45f;
  --red-soft:    #2a1a17; --red-soft-border:#4a2a24;
  --info-soft:   #232c3a;

  --av-navy:  #3d5478;
  --av-brown: #9a5147;
  --av-steel: #5b7392;

  --ph-a: #241f1b;
  --ph-b: #2c2621;

  --focus:       rgba(226,112,95,.28);
  --focus-ring:  rgba(226,112,95,.45);
  --focus-green: rgba(79,157,120,.28);

  --overlay: rgba(0,0,0,.72);

  --wordmark:       url("../images/kauke-full-dark-JRxJ9no.png");
  --wordmark-small: url("../images/kauke-small-dark-URBzRtx.png");

  --chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8'><path d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%239a9080' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>");

  /* Two dark things cast no contrast on each other, so the shadow all but goes and the border
     does the work: --border is a full step lighter than --surface, and that edge is what makes
     a card read as a card at all. */
  --shadow-card:      0 10px 28px -24px rgba(0,0,0,.8);
  --shadow-lg:        0 22px 60px -34px rgba(0,0,0,.85);
  --shadow-pop:       0 18px 44px -30px rgba(0,0,0,.8);
  --shadow-btn:       0 8px 18px -10px rgba(194,54,42,.5);
  --shadow-btn-green: 0 8px 18px -12px rgba(47,125,91,.5);
  --shadow-soft:      none;
  --shadow-toolbar:   none;
  --shadow-toast:     0 10px 26px -20px rgba(0,0,0,.8);
  --shadow-red:       0 6px 16px -12px rgba(194,54,42,.6);
}

/* ---------- Reset / base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font: 400 16px/1.5 var(--sans);
  color: var(--body);
  background: var(--cream);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, h2, h3 { font-family: var(--serif); color: var(--ink); font-weight: 600; margin: 0; }
p { margin: 0; }
a { color: inherit; }
img { max-width: 100%; display: block; }
/* After the img rule on purpose: `hidden` must win over any display we set ourselves. */
[hidden] { display: none !important; }
::placeholder { color: var(--taupe-light); opacity: 1; }
:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); border-radius: var(--r-sm); }

/* ---------- Wordmark ---------- */
/* Image logo (288×126). Box is sized in em off font-size, so it scales with the
   surrounding text: 24px base → 42px tall; the admin header's 22px → ~38px. The
   "kauke" link text is kept for the accessible name but hidden off-canvas. */
.wordmark {
  display: inline-block; vertical-align: middle;
  font-size: 24px; width: 4em; height: 1.75em;
  background: var(--wordmark) no-repeat left center;
  background-size: contain;
  white-space: nowrap; text-decoration: none;
  text-indent: -9999px; overflow: hidden;
}
.wordmark::before { content: none; }
.wordmark--lg { font-size: 56px; }
/* Optical vertical alignment of the logo in the page header. Tweak the pixel value
   to taste: more negative = higher, 0 = geometrically centered. Only affects the
   site header — the login and register pages keep the centered logo. */
.site-header .wordmark { transform: translateY(var(--wordmark-nudge, -3px)); }

/* ---------- Privacy / lock glyph (reused everywhere) ----------
   A small "person" mark = "who can see this". fill = currentColor. */
.pico { width: 1em; height: 1em; display: inline-block; fill: currentColor; flex: none; }

/* ---------- Lucide-style line icons (24×24 viewBox, stroked, never filled) ---------- */
.icon { width: 1em; height: 1em; display: inline-block; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- Layout ---------- */
.wrap { width: 100%; max-width: 1000px; margin: 0 auto; padding: 0 24px; }
.wrap--wide   { max-width: 1120px; }
.wrap--read   { max-width: 760px; }
.wrap--feed   { max-width: 720px; }
.section      { padding: 48px 0 64px; }
.section--cream { background: var(--cream); }

/* ---------- Site header ---------- */
/* The header is a stack of rows, not a row itself: in admin mode a second row (role pill +
   admin nav) drops in under the first, so the padding lives on each row's wrap. */
.site-header { background: var(--cream); border-bottom: 1px solid var(--border); }
.site-header .wrap { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-top: 18px; padding-bottom: 18px; }
.site-header__admin-row { border-top: 1px solid var(--border); }
.site-header .wrap.site-header__admin { justify-content: flex-start; gap: 20px; padding-top: 0; padding-bottom: 0; }
/* min-width:0 so the row may shrink below its content: the group label is the one thing here
   that can give (it ellipsises), and without this the flexbox would refuse to shrink it and
   push the header sideways instead. */
.site-header__nav { display: flex; align-items: center; gap: 8px; min-width: 0; }
.navlink { font: 600 14px var(--sans); color: var(--slate); text-decoration: none; padding: 10px 14px; border-radius: var(--r-pill); }
.navlink:hover { color: var(--ink); }
.user-chip { display: flex; align-items: center; gap: 11px; font: 600 14px var(--sans); color: var(--slate); }
/* text-decoration must go on the link: the underline is painted by the <a>, and it is drawn
   straight across the avatar's circle. Setting it on .avatar itself would do nothing. */
.avatar-link { display: inline-flex; border-radius: 50%; text-decoration: none; }
/* The bell (varsler): an icon link with the unread pill hanging off its corner. */
.bell-link { position: relative; display: inline-flex; align-items: center; padding: 6px; border-radius: 50%; color: var(--ink); }
.bell-link:hover { background: var(--panel); }
.bell-link__count { position: absolute; top: -3px; right: -5px; }

/* ---------- Varsler (/varsler) ---------- */
.notif-list { list-style: none; margin: 0; padding: 0; }
.notif { border-bottom: 1px solid var(--border); }
.notif__link { display: block; padding: 14px 10px; text-decoration: none; border-radius: 8px; }
.notif__link:hover { background: var(--panel); }
.notif__line { display: block; color: var(--body); font-size: 15px; line-height: 1.45; }
.notif--unread .notif__line { font-weight: 600; color: var(--ink); }
.notif__meta { display: block; margin-top: 2px; color: var(--taupe); font-size: 13px; }

/* ---------- Group switcher (fase 3) ----------
   A <details> so it opens without JS; kauke.js only adds click-outside/Escape. */
/* The switcher is the flex item that absorbs a tight header — the button, bell and avatar all
   hold their content width, so the label ellipsises only once there is genuinely no room. It
   used to be capped at a flat 11ch on phones, which truncated «Gruppene dine» even with 150px
   of the header to spare (and the cap dated from when the full wordmark took 96px of it). */
.switcher { position: relative; min-width: 0; }
.switcher__summary {
  display: flex; align-items: center; gap: 7px; cursor: pointer; list-style: none;
  font: 600 14px var(--sans); color: var(--ink);
  /* padding + line-height tuned so the trigger is 40px tall — same as the adjacent
     .btn (Administrer/Skriv). Horizontal padding stays tighter than a button on
     purpose: the label ellipsis at 22ch already needs the room. */
  padding: 12px 13px; line-height: 1;
  border: 1px solid var(--border-warm); border-radius: var(--r-pill);
  background: var(--surface);
}
.switcher__summary::-webkit-details-marker { display: none; }
.switcher__summary:hover { border-color: var(--taupe-light); }
.switcher__summary:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
/* min-width is a floor, not a size: the label shrinks before anything else in the header, and
   without it a crowded header squeezes it to nothing at all rather than to a few letters. */
.switcher__label { max-width: 22ch; min-width: 5ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.switcher__caret {
  width: 0; height: 0; flex: none; margin-top: 1px;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-top: 5px solid var(--taupe-light);
}
.switcher[open] .switcher__caret { transform: rotate(180deg); }
.switcher__menu {
  position: absolute; z-index: 40; top: calc(100% + 8px); left: 0; min-width: 232px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-pop); padding: 6px; overflow: hidden;
}
.switcher__item {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 9px 12px; border-radius: var(--r-md); text-decoration: none;
  font: 600 14px var(--sans); color: var(--ink);
}
.switcher__item:hover { background: var(--panel); }
.switcher__item.is-current { color: var(--red); }
.switcher__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.switcher__role { font: 500 12px var(--sans); color: var(--taupe); flex: none; }
.switcher__item--foot {
  margin-top: 5px; padding-top: 12px; border-top: 1px solid var(--hair);
  border-radius: 0 0 var(--r-md) var(--r-md); font-weight: 500; color: var(--slate);
}
/* Same switcher, opening upward: in the footer the room is above. The caret is left alone —
   a closed menu shows a down triangle whichever way it will open, because that is what «trykk
   her for flere valg» looks like. */
.switcher--up .switcher__menu { top: auto; bottom: calc(100% + 8px); }
/* 34px tall, which is the temavelgerens pill: the two share the last row of the footer, and a
   40px header trigger beside a 34px pill reads as two rows that failed to line up. */
.switcher--small .switcher__summary { padding: 9px 12px; }
@media (max-width: 660px) {
  /* The menu is wider than the trigger, and the trigger sits mid-header — so anchoring the
     menu to it runs off one edge or the other depending on the page (the band feed puts the
     switcher at the left of the nav, «Dine sider» nearer the right). Anchor it to the header
     instead and let it span the page gutters, which is where a phone dropdown belongs. */
  .site-header .wrap { position: relative; }
  .switcher { position: static; }
  .switcher__menu { left: 16px; right: 16px; min-width: 0; }
}

/* ---------- Avatar ---------- */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 50%;
  background: var(--av-navy); color: var(--on-dark); flex: none;
  font: 600 14px var(--sans);
}
.avatar--sm { width: 32px; height: 32px; font-size: 12px; }
.avatar--md { width: 36px; height: 36px; font-size: 13px; }
.avatar--lg { width: 42px; height: 42px; font-size: 14px; }
.avatar--navy  { background: var(--av-navy); }
.avatar--brown { background: var(--av-brown); }
.avatar--steel { background: var(--av-steel); }
.avatar--stone { background: var(--av-stone); }
/* A profile picture is the same circle with a face in it: cover-cropped, never stretched. The
   background stays so the palette colour shows while the bytes are still on their way. */
img.avatar { object-fit: cover; }
/* Tap to peek (avatar_pop_controller.js): the popped picture is a fixed-position clone laid out
   at its big size and scaled down to start, which is what keeps it sharp mid-bounce. Origin at
   the left edge so a picture at the start of a table row grows over the name. */
img.avatar[data-controller~="avatar-pop"] { cursor: zoom-in; }
.avatar-ghost { position: fixed; z-index: 50; pointer-events: none; transform-origin: left center; object-fit: cover; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font: 600 14px var(--sans); line-height: 1; text-decoration: none;
  border: 1px solid transparent; border-radius: var(--r-pill);
  padding: 12px 18px; cursor: pointer; transition: background .15s, box-shadow .15s, color .15s;
  white-space: nowrap;
}
.btn--primary { background: var(--red-fill); color: var(--on-dark); box-shadow: var(--shadow-btn); }
.btn--primary:hover { background: var(--red-fill-hover); }
.btn--secondary { background: var(--surface); color: var(--ink); border-color: var(--border-input); }
.btn--secondary:hover { border-color: var(--border-strong); background: var(--surface-tint); }
/* Pressed: «Administrer» while you are inside admin. Filled, so it reads as a mode you are
   in rather than a place to go — clicking it again is the way out. */
.btn--secondary.is-active { position: relative; background: var(--ink); color: var(--surface); border-color: var(--ink); }
.btn--secondary.is-active:hover { background: var(--ink-hover); border-color: var(--ink-hover); }
/* Two arcs, one at each end, rather than a closed ring: they echo the pill's own rounded ends
   from the inside, which reads as switched on without drawing a second button around the label.
   Each is a half circle of r = 18, so it sits 2px inside a 40px-tall button all the way round. The
   box is 18 x 36 with its facing border dropped: the corner radii then use up the full width
   (18 + 0) and the full height (18 + 18), no straight segment is left over, and what remains is
   the bare arc. Against the pill's own radius of 20 it runs nearly concentric with the end cap it
   sits in. The offset is 1px rather than 2px because an absolute child is placed against the
   padding box, inside the button's own 1px border. They cost the label no room even though the
   arc's tips reach 20px in: the tips are at the very top and bottom, and across the band the text
   actually occupies the curve has fallen back to 2-4px. So the button keeps its padding at both
   header widths, and pressing it shifts nothing. currentColor, not a token — the fill flips from
   ink to cream between the themes, and the arcs have to follow the label they sit around rather
   than either palette. */
.btn--secondary.is-active::before,
.btn--secondary.is-active::after {
  content: ''; position: absolute; top: 50%; margin-top: -18px;
  width: 18px; height: 36px; pointer-events: none;
  border: 2px solid currentColor; opacity: .5;
}
.btn--secondary.is-active::before { left: 1px;  border-right: 0; border-radius: 18px 0 0 18px; }
.btn--secondary.is-active::after  { right: 1px; border-left: 0;  border-radius: 0 18px 18px 0; }
.btn--ghost { background: transparent; color: var(--slate); box-shadow: none; }
.btn--ghost:hover { color: var(--ink); }
.btn--approve { background: var(--green); color: var(--on-dark); box-shadow: var(--shadow-btn-green); }
.btn--approve:hover { background: var(--green-hover); }
.btn--danger { background: var(--surface); color: var(--red-dark); border-color: var(--red-soft-border); box-shadow: none; }
.btn--danger:hover { background: var(--red-fill); color: var(--on-dark); border-color: var(--red-fill); }
.btn--lg { padding: 14px 24px; font-size: 15px; }
.btn--block { width: 100%; }
.btn:disabled, .btn[aria-disabled="true"], .btn--disabled {
  background: var(--disabled); color: var(--taupe-light); border-color: transparent;
  box-shadow: none; cursor: not-allowed; pointer-events: none;
}

/* ---------- Pills / badges ---------- */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  font: 600 12px var(--sans); border-radius: var(--r-pill);
  padding: 4px 11px; background: var(--panel); color: var(--slate);
  border: 1px solid transparent;
}
.pill--red   { background: var(--red-soft); color: var(--red); }
.pill--admin { background: var(--red-soft); color: var(--red); }
.pill--ghost { background: var(--surface); border-color: var(--border-warm); color: var(--slate); }
.pill--private { background: var(--panel); border-color: var(--border-warm); color: var(--slate); padding: 7px 13px; font-size: 12.5px; }
.pill--soon  { background: var(--panel); color: var(--taupe); font-size: 10.5px; padding: 2px 8px; }
.count-badge { background: var(--red-fill); color: var(--on-dark); border-radius: var(--r-pill); padding: 1px 7px; font: 600 11px var(--sans); }

/* status dot */
.status { display: inline-flex; align-items: center; gap: 6px; font: 600 12.5px var(--sans); }
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.status--on   { color: var(--green); }
.status--off  { color: var(--taupe); }
.status--off::before { background: var(--mark-soft); }

/* ---------- Chips (trip filter) ---------- */
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
  font: 600 13px var(--sans); color: var(--slate);
  background: var(--surface); border: 1px solid var(--border-input);
  border-radius: var(--r-pill); padding: 8px 16px; cursor: pointer;
  text-decoration: none; white-space: nowrap;
}
.chip:hover { border-color: var(--border-strong); }
.chip--active { background: var(--red-fill); color: var(--on-dark); border-color: var(--red-fill); }
/* The reaction you gave. Deliberately not --chip--active: an emoji is a multicolour glyph
   drawn for light paper, and ❤️ or 👏 on a Kaukerød surface is a smudge in both themes,
   since --red-fill does not lift on dark. A tint plus a full-strength border says «valgt»
   without putting a surface behind the glyph. The hover has to be restated, or
   `.chip:hover` outranks the border here and the chip looks unselected under the cursor. */
.chip--reacted { background: var(--red-soft); color: var(--red); border-color: var(--red); font-weight: 700; }
.chip--reacted:hover { border-color: var(--red-dark); }

/* ---------- Reaction picker (comments) ----------
   The emoji sit in a popover, not in the row: opening the picker must not move the
   comment you are reading, nor the ones under it. Anchored to the row rather than to
   the smiley — the row spans the comment, so a picker opened from a toggle sitting far
   right (a comment with several reactions already) still cannot run off the edge. */
.react-row { position: relative; }
.react { position: static; display: inline-flex; }
.react__menu {
  position: absolute; z-index: 30; top: calc(100% + 8px); left: 0;
  display: flex; flex-wrap: wrap; gap: 6px; max-width: 100%;
  /* Cream, not white: the thread card underneath is white, and a white panel on white
     leans on the shadow alone to read as something floating above it. */
  background: var(--cream); border: 1px solid var(--border-warm); border-radius: var(--r-lg);
  box-shadow: var(--shadow-pop); padding: 6px;
}
.react__menu > form { display: flex; }
.react__menu .chip { padding: 7px 13px; font-size: 15px; line-height: 1.2; }

/* ---------- Who reacted ----------
   The chip says how many; hovering it says who, and on a touch screen a long press does
   (kauke.js) since there is no hover to have. Anchored to the row for the same reason the
   picker is, and opening upwards, so the two never sit on top of each other.

   Hidden with opacity alone, never `visibility` or `display`: the chip points at this
   element with aria-describedby, and either of those would take it out of the
   accessibility tree along with the names. */
.reactors {
  position: absolute; z-index: 31; bottom: calc(100% + 8px); left: 0;
  max-width: min(340px, 100%); width: max-content;
  background: var(--cream); border: 1px solid var(--border-warm); border-radius: var(--r-md);
  box-shadow: var(--shadow-pop); padding: 9px 12px;
  font: 500 12.5px/1.55 var(--sans); color: var(--slate); white-space: normal;
  opacity: 0; pointer-events: none; transition: opacity .1s ease;
}
.reactors strong { display: block; font-weight: 600; color: var(--ink); }
.chip[data-reactors]:hover + .reactors,
.chip[data-reactors]:focus-visible + .reactors { opacity: 1; transition-delay: .3s; }
/* Opened by long press; the class outlives the finger, so no delay and no hover needed. */
.reactors.is-open { opacity: 1; }
/* A long press on a chip means «who reacted» here, so it must not also mean «select this
   text» or «open the callout» — the browser's own gestures would fire over ours. */
.chip[data-reactors] { -webkit-touch-callout: none; user-select: none; }

/* A quiet round icon button — «Reager» under a comment, the bell that mutes a thread.
   Icon only, so every one of them carries its own aria-label. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; border-radius: var(--r-pill);
  border: none; background: none; color: var(--slate); cursor: pointer; list-style: none;
  transition: background-color .15s, color .15s;
}
.icon-btn:hover { background: var(--panel); color: var(--red); }
.icon-btn:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }

/* The picker's own trigger is a <summary>: hide the marker on both WebKit and the
   standard pseudo-element, or a disclosure triangle sits beside the smiley. */
.react-toggle::-webkit-details-marker { display: none; }
.react-toggle::marker { content: ""; }
details[open] > .react-toggle { background: var(--panel); color: var(--red); }

/* ---------- (i) beside a label (partials/_info.html.twig) ----------
   The details is inline so the toggle trails the label's words; the text is a block, so an
   open one drops below the label in normal flow rather than in a tooltip. */
.info { display: inline; }
.info__toggle {
  display: inline-flex; align-items: center; justify-content: center; vertical-align: 2px;
  padding-top: 1px; box-sizing: border-box; /* the circle sits 1px higher than the glyph */
  width: 17px; height: 17px; margin-left: 6px; border-radius: 50%;
  border: 1px solid var(--taupe-light); color: var(--slate); background: var(--surface);
  font: 600 11px/1 var(--serif); font-style: italic; cursor: pointer; list-style: none;
}
.info__toggle::-webkit-details-marker { display: none; }
.info__toggle::marker { content: ""; }
.info__toggle:hover, .info[open] > .info__toggle { color: var(--red); border-color: var(--red); }
.info__toggle:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.info__text {
  display: block; margin: 8px 0 4px; padding: 10px 12px; border-radius: var(--r-sm);
  background: var(--panel); color: var(--slate); font: 400 13px/1.55 var(--sans);
}
.info__text p { margin: 0 0 8px; }
.info__text p:last-child, .info__text dl { margin-bottom: 0; }
.info__text dt { font-weight: 600; color: var(--ink); margin-top: 8px; }
.info__text dd { margin: 0; }

/* Sticky save: the settings form is long, and the button was off-screen for most of it. */
.form-foot {
  position: sticky; bottom: 0; z-index: 2;
  /* Reaches the card's edge on every width, since it reads the same padding the card uses. */
  margin: 16px calc(-1 * var(--card-pad)) calc(-1 * var(--card-pad));
  padding: 12px var(--card-pad); background: var(--surface); border-top: 1px solid var(--hair);
  border-radius: 0 0 var(--r-xl) var(--r-xl);
}

/* ---------- Band section nav (Innlegg · Emneknagger · Arrangementer) ---------- */
.band-nav { display: flex; gap: 24px; margin-top: 18px; }
.band-nav__link { font: 600 14.5px var(--sans); color: var(--slate); text-decoration: none; padding-bottom: 9px; border-bottom: 2px solid transparent; }
.band-nav__link:hover { color: var(--ink); }
.band-nav__link.is-active { color: var(--red); border-bottom-color: var(--red); }

/* ---------- Simple list rows (topic index, event list) ---------- */
.list-rows { display: flex; flex-direction: column; gap: 10px; }
.list-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: 15px 20px; text-decoration: none; box-shadow: var(--shadow-card);
  transition: border-color .13s ease, transform .13s ease;
}
.list-row:hover { border-color: var(--border-warm); transform: translateY(-1px); }
.list-row__name { font: 600 19px var(--serif); color: var(--ink); }
.list-row__meta { font: 500 13px var(--sans); color: var(--taupe); white-space: nowrap; }
/* ---------- Event header: picture beside the title ----------
   The «Bilde» slot leads the title when no banner is carrying the page (design-events.md
   §15). Wraps to a stacked block on narrow screens; with no picture it is just the text. */
.event-head { margin-top: 16px; }
.event-head--media { display: flex; align-items: flex-start; gap: 20px; flex-wrap: wrap; }
.event-head__body { flex: 1 1 260px; min-width: 0; }
.event-head__photo {
  width: 148px; height: 111px; flex: none; object-fit: cover;
  border-radius: var(--r-lg); border: 1px solid var(--border-warm); background: var(--panel);
}
/* The band event card is a smaller header than the standalone page's, so is its picture. */
.event-head__photo--sm { width: 112px; height: 84px; }
@media (max-width: 560px) {
  .event-head__photo, .event-head__photo--sm { width: 100%; height: 180px; }
}

/* Rows that lead with a picture (event list): the tile is always there — the photo when the
   event has one, else the same quiet placeholder mark — so the column never goes ragged. */
.list-row--media { align-items: center; justify-content: flex-start; }
.list-row--media .list-row__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-row__thumb {
  width: 54px; height: 42px; flex: none; border-radius: 9px; position: relative; overflow: hidden;
  border: 1px solid var(--border-warm); background: var(--panel);
  display: flex; align-items: center; justify-content: center;
}
.list-row__thumb::before {
  content: ''; width: 15px; height: 15px; background: var(--av-stone);
  border-radius: 5px; transform: rotate(45deg);
}
/* The picture lies over the placeholder mark, so a missing file drops out (house `onerror`
   pattern) and the quiet mark is simply there again — the column never goes ragged. */
.list-row__thumb img {
  position: absolute; inset: -1px; width: calc(100% + 2px); height: calc(100% + 2px); object-fit: cover;
}
/* On a phone the date and the address ate the whole row and the title was ellipsised down to
   nothing — the one thing you read a list of events for. Two rows beside the tile instead: the
   title first, in full, and the when/where under it, free to wrap. Same move as the admin
   event row's actions, which stack under the title at 720px for the same reason. */
@media (max-width: 720px) {
  .list-row--media { display: grid; grid-template-columns: auto 1fr; gap: 3px 14px; }
  .list-row--media .list-row__thumb { grid-row: span 2; align-self: center; }
  .list-row--media .list-row__name { font-size: 17.5px; white-space: normal; overflow: visible; }
  .list-row--media .list-row__meta { grid-column: 2; white-space: normal; }
}
@media (prefers-reduced-motion: reduce) { .list-row { transition: none; } .list-row:hover { transform: none; } }

/* ---------- Card ---------- */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-xl); box-shadow: var(--shadow-card);
}
.card__pad { --card-pad: 22px; padding: var(--card-pad); }
/* The /a/ article card (event, public answer, event post) — drops to edge-to-edge on
   mobile (Claude Design «Svar mobil»): the card chrome goes and its white surface is
   carried full-bleed by the enclosing .section--paper. */
.card--article { padding: 44px 56px 40px; }
/* The comment thread below an article — same paper, its own card. */
.card--thread { margin-top: 22px; padding: 34px 56px 30px; }

/* ---------- Hub cards ---------- */
.cluster { display: grid; gap: 16px; grid-template-columns: repeat(2, 1fr); margin-top: 36px; }
/* Cards stretch to equal height in the grid row; push each card's action (button or
   form) to the bottom so buttons line up when one card's text is longer. */
.cluster > .card { display: flex; flex-direction: column; }
.cluster > .card > :last-child { margin-top: auto; }
/* The button and the line under it travel together, pinned to the foot of the card — so a
   button lands on the same line as its neighbour's however long the blurb above it ran.
   (Pinning the *last* child alone put the note at the foot and left the button floating
   under the text, which is why a card with a note and one without disagreed.)

   A card with nothing to disclaim keeps the note's space anyway, or its button would sit
   lower than the one beside it. The padding is the note's own margin plus one line of it;
   where :has() is unsupported the second rule is dropped and every card keeps the padding —
   the buttons still line up, which is the point. */
.card-action { margin-top: auto; padding-bottom: calc(10px + 12.5px * 1.45); }
.card-action:has(.card-action__note) { padding-bottom: 0; }
.card-action__note { margin: 10px 0 0; font: 400 12.5px/1.45 var(--sans); color: var(--taupe); }
.eyebrow { font: 600 12px var(--sans); color: var(--red); letter-spacing: .06em; text-transform: uppercase; }
.lead { font: 400 20px/1.5 var(--sans); color: var(--slate); }

/* ---------- Slug input ---------- */
.slug-input {
  display: flex; align-items: center; overflow: hidden;
  border: 1px solid var(--border-input); border-radius: var(--r-md);
  background: var(--cream);
}
.slug-input__prefix { padding-left: 13px; font: 500 14px var(--mono); color: var(--taupe-light); }
.slug-input input { flex: 1; min-width: 0; border: none; background: transparent; outline: none; padding: 11px 6px; font: 500 14px var(--mono); color: var(--ink); }

/* ---------- Forms ---------- */
.field { margin-bottom: 14px; }
.label { display: block; font: 600 13px var(--sans); color: var(--ink); margin-bottom: 7px; }
.label .muted { font-weight: 400; color: var(--taupe); }
.input, .textarea {
  width: 100%; border: 1px solid var(--border-input); border-radius: var(--r-md);
  padding: 13px 15px; font: 400 15px var(--sans); color: var(--ink);
  background: var(--cream); outline: none; transition: border-color .15s, box-shadow .15s;
}
.textarea { resize: vertical; min-height: 64px; line-height: 1.45; }
/* Native selects share the input look; a taupe chevron replaces the UA arrow */
select.input {
  appearance: none; -webkit-appearance: none; cursor: pointer; padding-right: 36px;
  background-image: var(--chevron);
  background-repeat: no-repeat; background-position: right 13px center;
}
select.input--sm { width: auto; padding: 6px 30px 6px 10px; font-size: 13px; background-position: right 10px center; }
/* background-color, not the `background` shorthand — the shorthand would reset the
   background-image select.input uses for its chevron. */
.input:focus, .textarea:focus { border-color: var(--red); box-shadow: 0 0 0 3px var(--focus); background-color: var(--surface); }
.field--error .input { border-color: var(--red); border-width: 1.5px; box-shadow: 0 0 0 3px var(--focus); background-color: var(--surface); }
.field--ok .input { border-color: var(--green); border-width: 1.5px; box-shadow: 0 0 0 3px var(--focus-green); background-color: var(--surface); }
/* «Del denne lenken»: the link and its two buttons. The input keeps the whole width until
   there is room for a button beside it, so on a narrow phone the buttons wrap under a
   readable URL instead of squeezing it to nothing. */
.share-link { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.share-link .input { flex: 1 1 220px; min-width: 0; }
.share-link .btn { flex: 0 0 auto; padding: 12px 16px; }

.field__msg { display: flex; align-items: center; gap: 6px; margin-top: 7px; font: 500 12.5px var(--sans); }
.field--error .field__msg { color: var(--red-dark); }
.field--ok .field__msg { color: var(--green); }
.field__badge { width: 15px; height: 15px; border-radius: 50%; background: var(--red-fill); color: var(--on-dark); display: inline-flex; align-items: center; justify-content: center; font: 700 10px var(--sans); flex: none; }

/* ---------- Form rows ----------
   Fields side by side where there is room, stacked where there isn't. A media query on its
   own can't do this: these rows used to carry `grid-template-columns` as an inline style,
   which no query can override — and `datetime-local` and `select` have an intrinsic minimum
   width, so a fixed `1fr 1fr` pushed the whole page sideways on a phone instead of shrinking.
   auto-fit lets the row count its own columns; unused tracks collapse, so a two-field row is
   still exactly two equal columns on desktop. Tune the breakpoint per row with --col. */
.form-row {
  display: grid; gap: 14px; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(var(--col, 180px), 1fr));
}
/* A row that ends in a button: the button sits on the inputs' baseline and keeps its own
   width rather than stretching to fill the track it landed in. */
.form-row--action { align-items: end; }
.form-row--action > .btn { justify-self: start; }

/* ---------- Schedule (dato + valgfritt klokkeslett) ----------
   Two controls where there used to be one `datetime-local`. That widget welded the two
   together: opening its calendar filled the time segment with the current time of day, so
   picking a date saved a clock time nobody typed. Split, the date picker has no time to
   invent — and a blank time box can then mean what it should, that the time isn't decided.
   Two blank ones across Fra and Til is exactly an all-day event, Monday to Friday included.
   The date track takes what's left (minmax(0,…) so it may shrink below its intrinsic width);
   the time box keeps its own, which is narrow enough that the pair survives a 320px phone.
   No «kl.» label between the two: an empty time control already shows --:-- beside a clock
   glyph, which says time of day without being told — except on iOS, which draws nothing at
   all in an empty one, so date_hint_controller lays that same hint over it there. */
.schedule { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; }
/* A box per control, so the hint has something to be positioned against. */
.schedule__box { position: relative; min-width: 0; }
/* A fixed width, not `auto`: an empty time input is narrower than a filled one, so letting
   it size to its content left the Fra and Til date boxes visibly different widths. */
.schedule__box--time { width: 104px; }
.schedule .input[type="time"] { padding-left: 11px; padding-right: 11px; }
/* minmax(0, …) frees the *track* to shrink, min-width the *box* inside it. Chrome needs
   neither — with a 1fr track the spec's content-based minimum doesn't apply — but they cost
   nothing and say what the row means: the pair has to survive a 320px phone. */
.schedule .input { min-width: 0; }
/* What iOS actually needed. Its date control holds a minimum size of ~244px (measured off the
   bug report) that belongs to the native control, not to the box — so no CSS min-width could
   undo it, and the row first slid the date box under the time box, then pushed the time box out
   past the card. The same theme centres the value, leaving «5. aug. 2026» on a different margin
   from the hint above it and from every other field on the page. Dropping the native appearance
   settles both: width binds, and the text sits at the padding like ordinary input text.
   Only where the controller found iOS — Chrome and desktop Safari keep their native controls,
   and with them their own empty-state hints.

   The height has to be pinned once the native appearance is gone: a control with no text has no
   line box either, so an empty box collapsed to its padding and stood shorter than a filled one
   beside it in the same row. The value element is given a line of its own instead of one it has
   to find in its content — the same line whether or not there is a value, so the two states are
   the same height by construction rather than by metrics agreeing. min-height says the same
   thing on the box, so an engine that ignores the inner element still can't collapse it. 19px is
   what Chrome's date control lands on inside this padding. */
.schedule--ios .input {
  -webkit-appearance: none; appearance: none;
  --line: 19px;
  min-height: calc(var(--line) + 28px);      /* 13px padding twice, 1px border twice */
}
.schedule--ios .input::-webkit-date-and-time-value {
  text-align: left; height: var(--line); line-height: var(--line);
}
/* And if a control still won't fit beside the time box, the time box goes under it rather than
   out past the card — set by measurement, not by guessing the engine (date_hint_controller). */
.schedule--stacked { grid-template-columns: minmax(0, 1fr); }
/* One control alone in the row — the poll's «Svarfrist», which stays a datetime-local because a
   frist is a point in time and nothing else. It is inside a .schedule anyway: the iOS work above
   is exactly what that control needs, both halves of it. No column gap, or fit() measures a box
   plus a gap that isn't drawn against the row and stacks a row that has nothing to stack. */
.schedule--one { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
/* Left to match the control's own text: the date box's 15px of padding, the time box's 11px.
   Shown only while the control is empty, and only on iOS — see date_hint_controller. */
.schedule__hint {
  position: absolute; left: 15px; top: 50%; transform: translateY(-50%);
  font: 400 15px var(--sans); color: var(--taupe-light); pointer-events: none; display: none;
}
.schedule__box--time .schedule__hint { left: 11px; }
.schedule__box.is-blank .schedule__hint { display: block; }
.schedule-hint { font: 400 12.5px var(--sans); color: var(--taupe); margin: 10px 0 0; }

/* ---------- One field with its own button («Nytt forslag», «Svarfrist») ----------
   Not a .form-row: the button acts on the single field beside it, and on a phone it has to end
   up under that field rather than beside a squeezed one. Sizing the row from its content is what
   broke, and it broke in both directions on the same screen. An empty date box on iOS draws
   nothing at all, so the pair measured as almost nothing and was pressed to 75px beside
   «Foreslå»; the deadline's datetime-local reports one width to the flex algorithm and paints at
   a minimum of its own, so it slid in under «Sett frist». The basis decides the width instead of
   the control's opinion of itself: wrapping happens before shrinking, so the field keeps a whole
   row and the button drops under it the moment the two no longer fit. */
.field-action { display: flex; gap: 8px; align-items: end; flex-wrap: wrap; }
/* max-width so a row that lands in a wide container doesn't stretch a date box across it. */
.field-action > .field { flex: 1 1 260px; min-width: 0; max-width: 360px; margin: 0; }
.field-action > .btn { flex: none; }
/* The poll's two organizer rows, side by side where there is room. They need a basis of their
   own: a row that has wrapped its button is only as wide as it happened to measure, and the two
   then stand at different widths on the same phone. */
.poll-admin { display: flex; flex-wrap: wrap; gap: 18px 24px; margin-top: 16px; }
.poll-admin > .field-action { flex: 1 1 320px; }

/* ---------- Adressevelger («Sted») ----------
   The list floats over what follows rather than pushing the form down: an organizer scanning
   six addresses should not have the Beskrivelse-field jump away under their eyes and back again
   on the next keystroke. It is anchored to the input's own box, so it lines up with the field
   and not with the label above it. */
.address-picker__field { position: relative; }
.address-picker__hits {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 30;
  margin: 0; padding: 4px; list-style: none; max-height: 260px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border-input); border-radius: 12px;
  box-shadow: var(--shadow-pop);
}
.address-picker__hit {
  padding: 9px 11px; border-radius: 8px; cursor: pointer;
  font: 500 14.5px var(--sans); color: var(--body);
}
/* Pointer and keyboard land on the same state, so arrowing down looks like hovering. */
.address-picker__hit:hover, .address-picker__hit.is-active { background: var(--panel); color: var(--ink); }

/* The two links an arrangement's meta-linje can carry: a sted with a point behind it
   (design-events.md §24) and «Legg i kalenderen» (§27). The icon is the only thing marking
   either as a link, so the text keeps the weight and colour of the line it sits in. */
.location-link, .meta-link {
  display: inline-flex; align-items: center; gap: 5px;
  color: inherit; text-decoration: none; border-bottom: 1px solid var(--border-strong);
}
.location-link:hover, .meta-link:hover { color: var(--red); border-bottom-color: var(--red); }

/* ---------- Image picker (gruppebilde · bilde på invitasjonssiden) ----------
   Every option is the same tile — a 4:3 thumb with its radio under it — so «Uten bilde» and
   the photos share one rhythm instead of wrapping ragged against each other, and the row
   reflows by itself from a phone to a wide settings card. */
.img-picker { display: grid; grid-template-columns: repeat(auto-fill, minmax(78px, 1fr)); gap: 12px 10px; }
/* Two rows only — thumb, then radio. A caption shares the *thumb's* cell rather than taking a
   row of its own, so it costs no height and every tile in the row is exactly as tall as its
   picture, captioned or not. That also keeps the radios on one line by construction. */
.img-picker__opt {
  display: grid; grid-template-rows: auto auto; justify-items: center; gap: 5px;
  cursor: pointer; text-align: center;
  /* min-width:auto would let a tile's own content set the column width — and a picture whose
     file is missing renders its alt text, which is wide. That stretched one column, and
     aspect-ratio then made that tile taller than the rest. Tiles never size the grid. */
  min-width: 0;
}
.img-picker__opt > .img-picker__thumb { grid-area: 1 / 1; }
.img-picker__opt > .img-picker__cap   { grid-area: 1 / 1; justify-self: stretch; }
.img-picker__opt > input              { grid-area: 2 / 1; }
/* A just-uploaded picture is resized async, so for a few seconds _slot_thumb renders a
   waiting label instead. It shares the thumb's cell too — the image beside it is [hidden]
   until the worker is done, and the controller then removes this label. */
.img-picker__wait {
  grid-area: 1 / 1; align-self: center;
  font: 500 10.5px var(--sans); color: var(--taupe);
}
.img-picker__thumb {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block;
  border-radius: 9px; border: 1px solid var(--border-warm); background: var(--panel);
  /* A missing file falls back to alt text — clip it to the tile rather than let it spill. */
  overflow: hidden;
}
/* «Uten bilde» is an empty tile — nothing to show, so its label is simply its content. */
.img-picker__none { background: var(--panel); }
.img-picker__opt:has(.img-picker__none) > .img-picker__cap {
  align-self: center; background: none; padding: 0; border-radius: 0;
  font: 500 11px var(--sans); color: var(--taupe); white-space: normal;
}
/* A caption over a real photo needs to stay readable, so it rides a scrim along the bottom. */
.img-picker__cap {
  align-self: end;
  font: 600 10px var(--sans); color: var(--on-dark); background: var(--scrim);
  padding: 3px 5px; border-radius: 0 0 8px 8px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The chosen tile should read as chosen — a lone radio under a thumbnail is easy to miss. */
.img-picker__opt:has(input:checked) .img-picker__thumb { border-color: var(--red); box-shadow: 0 0 0 2px var(--focus-ring); }

/* ---------- Combobox («folk du kjenner», fase 3) ---------- */
.combo { position: relative; }
.combo__list {
  position: absolute; z-index: 30; top: calc(100% + 5px); left: 0; right: 0;
  margin: 0; padding: 5px; list-style: none; max-height: 264px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-pop);
}
.combo__item {
  display: flex; flex-direction: column; gap: 2px; padding: 8px 11px;
  border-radius: var(--r-md); cursor: pointer;
}
.combo__item.is-active, .combo__item:hover { background: var(--panel); }
.combo__name { font: 600 14px var(--sans); color: var(--ink); }
.combo__meta { font: 500 12.5px var(--sans); color: var(--taupe); }

/* divider with label */
.or { display: flex; align-items: center; gap: 12px; margin: 22px 0; }
.or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.or span { font: 500 12px var(--sans); color: var(--taupe-light); }

/* ---------- Notices ---------- */
.notice {
  display: flex; gap: 12px; align-items: flex-start;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 16px 18px;
  font: 400 14px/1.55 var(--sans); color: var(--slate);
}
.notice--red    { background: var(--red-soft); border-color: var(--red-soft-border); color: var(--red-ink); }
.notice--soft   { background: var(--surface); border-color: var(--border); }
/* Caution, not error: the storage warning at 80 % (design-tiers.md §4) — nothing is blocked yet. */
.notice--amber  { background: var(--amber-soft); border-color: var(--amber-border); color: var(--amber-text); }
.notice strong  { font-weight: 700; }
.privacy-note   { font: 400 12.5px/1.5 var(--sans); color: var(--taupe); }

/* ---------- Språktilbud (design-i18n.md §3.1) ---------- */
/* A slim bar above everything else on a public page, offering the twin in the language this
   reader would otherwise have been given. Quiet on purpose: it sits over the header, so it must
   read as chrome and never as a message. */
.langbar {
  display: block; text-align: center; text-decoration: none;
  background: var(--panel); border-bottom: 1px solid var(--hair);
  padding: 9px 16px;
  font: 400 13.5px/1.4 var(--sans); color: var(--slate);
}
.langbar:hover, .langbar:focus-visible { color: var(--red); text-decoration: underline; }

/* ---------- Storage meter (group settings «Lagring») ---------- */
.meter { height: 8px; border-radius: 99px; background: var(--hair); overflow: hidden; }
.meter__fill { height: 100%; border-radius: 99px; background: var(--green); transition: width .3s ease; }
.meter__fill--warn { background: var(--amber); }
.meter__fill--full { background: var(--red); }

/* admin attention banner */
.alert {
  display: flex; align-items: center; gap: 13px;
  background: var(--surface); border: 1px solid var(--red-soft-border);
  border-left: 3px solid var(--red); border-radius: var(--r-md);
  padding: 14px 18px; font: 400 14.5px var(--sans); color: var(--body);
}
.alert a { margin-left: auto; font: 600 14px var(--sans); color: var(--red); text-decoration: none; white-space: nowrap; }

/* ---------- Image placeholder ---------- */
.ph {
  background: repeating-linear-gradient(135deg, var(--ph-a) 0 11px, var(--ph-b) 11px 22px);
  border-radius: var(--r-md);
  position: relative;
  overflow: hidden;
}
.ph > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.ph--label { display: flex; align-items: flex-end; padding: 9px; font: 500 10px var(--mono); color: var(--av-stone); }

/* ---------- Gallery ---------- */
.gallery { display: grid; gap: 8px; }
.gallery--2 { grid-template-columns: 1fr 1fr; }
.gallery--3 { grid-template-columns: 1fr 1fr 1fr; }
.gallery--3 .ph, .gallery--2 .ph { height: 130px; }
.gallery--quad { grid-template-columns: 1fr 1fr; }
.gallery--quad .ph { height: 120px; }
.gallery--feature { grid-template-columns: repeat(4, 1fr); grid-auto-rows: 118px; }
.gallery--feature .ph { height: auto; }
.gallery--feature .ph:first-child { grid-column: span 2; grid-row: span 2; }
.gallery__more { position: relative; overflow: hidden; cursor: zoom-in; }
.gallery__more > span:not(.ph__play) {
  position: absolute; inset: 0; background: var(--scrim);
  display: flex; align-items: center; justify-content: center;
  font: 600 22px var(--serif); color: var(--on-dark);
}
/* Video tile: centered play badge over the poster thumb. */
.ph__play {
  position: absolute; inset: 0; margin: auto; width: 44px; height: 44px; border-radius: 50%;
  background: var(--scrim); color: var(--on-dark);
  display: flex; align-items: center; justify-content: center; pointer-events: none;
}
.ph__play svg { margin-left: 2px; }
.gallery .ph { cursor: zoom-in; }

/* ---------- Post (feed card) ---------- */
.post { position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-xl); padding: 24px; box-shadow: var(--shadow-card); margin-bottom: 18px;
  transition: box-shadow .15s ease, border-color .15s ease; }
/* The head wraps as a whole: with enough labels the pill group drops to its own line
   below the byline rather than squeezing the name into one word per line. */
.post__head { display: flex; align-items: center; justify-content: space-between; gap: 10px 12px; flex-wrap: wrap; }
.post__labels { display: flex; flex-wrap: wrap; gap: 8px; }
/* Stretched title link makes the whole card clickable; byline and label pills are
   raised above the overlay so they keep their own destinations. */
.post__link { text-decoration: none; color: inherit; }
.post__link::after { content: ""; position: absolute; inset: 0; border-radius: var(--r-xl); z-index: 1; }
.post__head .byline, .post__head .pill { position: relative; z-index: 2; }
.post:has(.post__link:hover) { border-color: var(--border-warm); box-shadow: var(--shadow-pop); }
.byline { display: flex; align-items: center; gap: 11px; min-width: 0; }
.post__head .byline { flex: 1 1 auto; }
.byline__name { font: 600 14px var(--sans); color: var(--ink); overflow-wrap: anywhere; }
.byline__meta { font: 400 13px var(--sans); color: var(--taupe); }
.post__title { font: 600 25px/1.25 var(--serif); color: var(--ink); margin: 16px 0 8px; }
.post__body { font: 400 16px/1.6 var(--sans); color: var(--body-soft); margin: 0 0 16px; }
.post__body:last-child { margin-bottom: 0; }
.post__body strong { font-weight: 600; color: var(--ink); }
.post__widget { font: 400 13.5px/1.55 var(--sans); color: var(--taupe); margin: -6px 0 16px; }

/* ---------- Post (mini card, an innlegg listed inside an arrangement) ---------- */
/* Same three things as the feed card — tittel, et stykke av teksten, bildene — but a row
   rather than a sheet: an arrangement's innlegg sit inside a card already, and a card in a
   card reads as a mistake. Text left, at most two thumbs right. */
.postmini { display: flex; gap: 16px; align-items: flex-start; padding: 14px 0; border-bottom: 1px solid var(--hair); text-decoration: none; }
/* The seksjonen under has its own line, and two hairlines a few pixels apart read as a slip. */
.postmini:last-child { border-bottom: 0; }
.postmini__text { flex: 1 1 auto; min-width: 0; }
.postmini__head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.postmini__title { font: 600 19px/1.3 var(--serif); color: var(--ink); }
.postmini:hover .postmini__title { color: var(--red); }
.postmini__body {
  font: 400 14.5px/1.55 var(--sans); color: var(--body-soft); margin: 4px 0 0;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
.postmini__meta { font: 500 13.5px var(--sans); color: var(--taupe); margin-top: 5px; }
.postmini__shots { flex: 0 0 auto; display: flex; gap: 6px; }
.postmini__shots .ph { width: 92px; height: 70px; }
/* The tile macro is shared with the gallery, where «+3» is a zoom target. Here the whole
   row is one link to the innlegg, so the cursor must not promise a lightbox. */
.postmini__shots .gallery__more { cursor: inherit; }
.postmini__shots .gallery__more > span:not(.ph__play) { font-size: 17px; }
.postmini__shots .ph__play { width: 30px; height: 30px; }
.postmini__shots .ph__play svg { width: 14px; height: 14px; }

/* ---------- Band header ---------- */
.band-head { background: var(--surface); border-bottom: 1px solid var(--border); }
.band-head__row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.band-id { display: flex; align-items: center; gap: 16px; }
.band-logo { width: 60px; height: 60px; border-radius: var(--r-xl); border: 1px solid var(--border-warm);
  background: repeating-linear-gradient(135deg, var(--ph-a) 0 9px, var(--ph-b) 9px 18px); flex: none; }
.band-name { font: 600 30px/1.1 var(--serif); color: var(--ink); margin: 0; }
.band-sub { font: 400 14px var(--sans); color: var(--slate); margin: 5px 0 0; }

/* Editor's action row at the top of the feed. Right-aligned so the left edge stays a clean
   reading column; full width on phones, where a lone pill in the corner is easy to miss. */
.feed-actions { display: flex; justify-content: flex-end; margin-bottom: 18px; }

/* ---------- Long-form article body ----------
   The markdown in a post body (App\Content\Markdown) can produce any of these, so all of them
   need a place to land. Headings start at h2: the page's h1 is the post's title. */
.prose { font: 400 17.5px/1.75 var(--sans); color: var(--body); }
.prose p { margin: 0 0 20px; }
.prose > :last-child { margin-bottom: 0; }
.prose h2 { font: 600 25px/1.3 var(--serif); color: var(--ink); margin: 30px 0 12px; }
.prose h3 { font: 600 20px/1.35 var(--serif); color: var(--ink); margin: 26px 0 10px; }
.prose h4, .prose h5, .prose h6 { font: 600 16px/1.4 var(--sans); color: var(--ink); margin: 22px 0 8px; }
.prose h5, .prose h6 { color: var(--slate); }
.prose ul, .prose ol { margin: 0 0 20px; padding-left: 22px; }
.prose ol { padding-left: 24px; }
.prose li { margin-bottom: 6px; }
.prose li > ul, .prose li > ol { margin: 6px 0 0; }
.prose li > p { margin: 0 0 8px; }
/* Task lists («- [x] gjort»): the box is decoration, so it never takes focus. */
.prose li input[type="checkbox"] { display: inline-block; margin: 0 8px 0 -2px; accent-color: var(--red); }
.prose a { color: var(--red); font-weight: 600; text-decoration: underline; }
.prose strong { color: var(--ink); }
.prose em { font-style: italic; }
.prose del { color: var(--taupe); }
.prose blockquote {
  margin: 0 0 20px; padding: 4px 0 4px 18px;
  border-left: 3px solid var(--border-warm); color: var(--slate); font-style: italic;
}
.prose blockquote > :last-child { margin-bottom: 0; }
.prose hr { border: 0; border-top: 1px solid var(--border); margin: 30px 0; }
.prose code {
  font: 400 .87em/1.5 var(--mono); color: var(--ink);
  background: var(--panel); border-radius: var(--r-sm); padding: 2px 5px;
}
.prose pre {
  margin: 0 0 20px; padding: 14px 16px; overflow-x: auto;
  background: var(--panel); border: 1px solid var(--border-warm); border-radius: var(--r-md);
}
.prose pre code { font-size: 14px; background: none; padding: 0; white-space: pre; }
/* A wide table scrolls on its own rather than pushing the reading column sideways. */
/* A photo placed in the text with `![tekst](bilde:3)` (design-inline-images.md §3). The markup is
   all inline elements, so it is valid inside the <p> the parser built; these rules are the whole
   of what makes it a block. The striped ground is the same one a gallery tile uses, and it shows
   through for the minute before the worker has written the variants. */
.prose-photo { display: block; margin: 22px 0; }
.prose-photo__frame { display: block; }
.prose-photo__frame > img {
  display: block; width: 100%; height: auto; border-radius: var(--r-md);
  background: repeating-linear-gradient(135deg, var(--ph-a) 0 11px, var(--ph-b) 11px 22px);
}
/* A portrait photo is never taller than the column is wide, so it stands beside the text instead
   of pushing it a screen away. The frame is a container so `100cqw` is that width whatever the
   column becomes; the same picture, blurred and half-transparent, covers the width left over. */
.prose-photo--tall .prose-photo__frame {
  position: relative; overflow: hidden; border-radius: var(--r-md); container-type: inline-size;
}
.prose-photo--tall .prose-photo__frame > img {
  position: relative; width: auto; max-width: 100%; max-height: 100cqw; margin: 0 auto; border-radius: 0;
}
.prose-photo--tall .prose-photo__frame > img.prose-photo__ambient {
  position: absolute; inset: 0; width: 100%; height: 100%; max-height: none; margin: 0;
  object-fit: cover; filter: blur(24px); opacity: .5; transform: scale(1.1); background: none;
}
.prose-photo__caption { display: block; padding: 9px 2px 0; font: 400 13.5px/1.5 var(--sans); color: var(--taupe); }
/* Only clickable where a lightbox exists to open. The preview page has none. */
.prose[data-lightbox] .prose-photo__frame > img { cursor: zoom-in; }

/* «Skjult tekst» (design-inline-images.md §7): `![tekst](skjult "grunn")` folds the bracketed text
   under its reason until the reader opens it. A checkbox and a label rather than a <details>,
   because a <details> closes the <p> it lands in and this has to sit mid-paragraph. The body is
   `display: none` until checked, so a lazy-loaded photo inside it is not fetched before then. */
.veil { display: inline; }
.veil__toggle { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.veil__summary {
  display: inline-flex; align-items: baseline; gap: 8px; cursor: pointer;
  padding: 2px 10px; border: 1px dashed var(--border-strong); border-radius: var(--r-sm);
  font: 600 14px/1.6 var(--sans); color: var(--ink); background: var(--panel);
}
.veil__summary:hover { border-color: var(--taupe); }
.veil__toggle:focus-visible + .veil__summary { outline: 2px solid var(--red); outline-offset: 2px; }
.veil__show, .veil__hide { font-weight: 400; color: var(--red); text-decoration: underline; }
.veil__hide, .veil__body { display: none; }
.veil__toggle:checked ~ .veil__summary .veil__show { display: none; }
.veil__toggle:checked ~ .veil__summary .veil__hide { display: inline; }
.veil__toggle:checked ~ .veil__body { display: inline; margin-left: .3em; }

.prose table { width: 100%; margin: 0 0 20px; border-collapse: collapse; font-size: 16px; display: block; overflow-x: auto; }
.prose th, .prose td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--hair); }
.prose th { font-weight: 600; color: var(--ink); border-bottom-color: var(--border-warm); white-space: nowrap; }

/* Short user text (a comment, a reported comment): the inline half of the same markdown,
   rendered inside a <p>. A pasted URL is a link here too, so it needs the link colour. */
.rich-inline a { color: var(--red); font-weight: 600; text-decoration: underline; word-break: break-word; }
.rich-inline code { font: 400 .87em/1.5 var(--mono); background: var(--panel); border-radius: var(--r-sm); padding: 1px 5px; }
.rich-inline em { font-style: italic; }
.rich-inline del { color: var(--taupe); }

/* The markup cheat sheet under an editor's title field (partials/_markup_help.html.twig). */
.markup-help { font: 400 12.5px/1.6 var(--sans); color: var(--taupe); margin-bottom: 8px; }
.markup-help p { margin: 0; }
.markup-help code { font: 400 12px var(--mono); color: var(--body); background: var(--panel);
  border-radius: 6px; padding: 1px 5px; }
.markup-help summary { display: inline-block; margin-top: 6px; color: var(--slate); cursor: pointer;
  list-style: none; }
.markup-help summary::-webkit-details-marker { display: none; }
.markup-help summary::before { content: '▸ '; color: var(--taupe); }
.markup-help details[open] > summary::before { content: '▾ '; }
/* Two columns, but the first is capped: `max-content` alone let the longest example
   (`![skjult tekst](skjult "grunn")`) push the grid past a phone's width. */
.markup-help dl { display: grid; grid-template-columns: minmax(auto, max-content) 1fr; gap: 7px 16px;
  margin: 10px 0 0; padding: 14px 16px; background: var(--panel); border-radius: var(--r-md); min-width: 0; }
.markup-help dt, .markup-help dd { margin: 0; min-width: 0; }
.markup-help dt code { white-space: normal; overflow-wrap: anywhere; }
@media (max-width: 480px) {
  /* Stacked: the example on its own line, its meaning indented under it. */
  .markup-help dl { grid-template-columns: 1fr; gap: 2px; }
  .markup-help dd { padding-left: 12px; margin-bottom: 6px; }
}
.markup-help details code { background: var(--surface); }
.markup-help__note { margin-top: 10px; max-width: 62ch; }

/* ---------- Interaktiv visning (widget, design-widgets.md) ----------
   The frame starts pinned on top of the container at opacity 0, and the fallback text sits in
   the normal flow underneath it. That ordering is not decoration: it is what lets `loading=lazy`
   work (a display:none frame is never scrolled *into*, so it would never load), while the reader
   sees the fallback rather than a blank rectangle until the widget has told us how tall it is.
   The swap happens on the first height message, and only then. */
.widget { position: relative; margin: 32px 0 4px; }
.widget__frame {
  position: absolute; top: 0; left: 0;
  width: 100%; height: 80px; border: 0; display: block;
  opacity: 0; pointer-events: none;
  /* The widget is a light island in both themes (design-dark-mode.md §5, option 2). CSS cannot
     cross an opaque origin and the shell hardcodes near-black text on a transparent background,
     so on a dark page a widget would otherwise paint that text straight onto --surface. Painting
     the *frame element* rather than the shell is what keeps this a stylesheet change: the shell
     is served immutable for a year, so editing it would mean a version bump and a vhost change
     for what is a colour. Option 3 — telling the widget which theme it is in — is its own piece
     of work with its own deploy. */
  background: var(--widget-paper); border-radius: var(--r-lg); color-scheme: light;
}
.widget--live .widget__frame { position: static; opacity: 1; pointer-events: auto; }
.widget__fallback {
  margin: 0; padding: 18px 20px;
  background: var(--panel); border: 1px solid var(--border-warm); border-radius: var(--r-lg);
  font: 400 14.5px/1.6 var(--sans); color: var(--slate);
}
.widget__code { margin-top: 10px; }
.widget__code > summary {
  cursor: pointer; font: 500 13px var(--sans); color: var(--taupe);
  list-style: none; display: inline-block; padding: 4px 0;
}
.widget__code > summary::-webkit-details-marker { display: none; }
.widget__code > summary::before { content: '▸ '; }
.widget__code[open] > summary::before { content: '▾ '; }
.widget__meta { margin: 6px 0 10px; font: 400 12.5px/1.55 var(--sans); color: var(--taupe); }
.widget__code pre {
  margin: 0 0 10px; padding: 14px 16px; overflow-x: auto;
  background: var(--panel); border: 1px solid var(--border-warm); border-radius: 12px;
}
.widget__code code { font: 400 12.5px/1.6 var(--mono); color: var(--body); white-space: pre; }
/* Kjørerapporten (§4.3): bare redaktører ser denne, og den skal lese som en beskjed, ikke som en
   feilside. Derfor samme panel som resten av «Vis koden», med rødt bare på selve setningen. */
.widget__run {
  margin: 0 0 10px; padding: 12px 16px;
  background: var(--panel); border: 1px solid var(--border-warm);
  border-left: 3px solid var(--red); border-radius: 12px;
  font: 400 13px/1.6 var(--sans); color: var(--body);
}
.widget__run-line { margin: 0 0 6px; }
.widget__run-line code { color: var(--slate); white-space: pre-wrap; }

/* ---------- Forhåndsvisning for et program (design-mcp.md §7.5) ---------- */
.preview-note {
  margin: 0 0 10px; padding: 12px 16px;
  background: var(--panel); border: 1px solid var(--border-warm); border-radius: 12px;
  font: 400 13px/1.6 var(--sans); color: var(--taupe);
}
.preview-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
.preview-gallery img { width: 100%; height: 180px; object-fit: cover; border-radius: 12px; display: block; }

/* On paper there is no widget, so the paper gets the fallback (design-widgets.md §8). The
   `!important` is aimed at the `hidden` attribute the bridge sets once the frame is live. */
@media print {
  .widget__frame, .widget__code { display: none; }
  .widget__fallback { display: block !important; background: none; border: none; padding: 0; }
}

/* ---------- Centered hero panels (private notice / verify) ---------- */
.hero-panel { background: var(--cream); padding: 72px 0 84px; }
.hero-panel__inner { max-width: 520px; margin: 0 auto; text-align: center; }
.icon-circle {
  width: 84px; height: 84px; border-radius: 50%;
  background: var(--red-soft); border: 1px solid var(--red-soft-border);
  display: flex; align-items: center; justify-content: center; margin: 0 auto;
  color: var(--red);
}
.icon-circle--green { background: var(--green-soft); border-color: var(--green-border); color: var(--green); }
.icon-square { width: 80px; height: 80px; border-radius: 24px; }

/* ---------- Empty states ---------- */
.empty {
  border: 1.5px dashed var(--border-dashed); background: var(--surface-tint); border-radius: var(--r-lg);
  padding: 26px 22px; text-align: center;
  display: flex; flex-direction: column; align-items: center;
}
.empty__icon { width: 54px; height: 54px; border-radius: 50%; background: var(--panel); display: flex; align-items: center; justify-content: center; color: var(--av-stone); }
.empty__icon--green { background: var(--green-soft); color: var(--green); }
.empty h3 { font: 600 20px var(--serif); margin: 16px 0 0; }
.empty p { font: 400 13.5px/1.5 var(--sans); color: var(--slate); margin: 8px 0 16px; max-width: 30ch; }
.empty p:last-child { margin-bottom: 0; }

/* ---------- Toasts ---------- */
.toast {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--slate); border-radius: var(--r-md);
  padding: 14px 16px; box-shadow: var(--shadow-toast);
}
.toast + .toast { margin-top: 12px; }
.toast__icon { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex: none; font: 700 14px var(--sans); }
.toast__msg { flex: 1; font: 500 14.5px var(--sans); color: var(--ink); }
.toast__x { font: 400 16px var(--sans); color: var(--taupe-light); cursor: pointer; background: none; border: none; }
.toast--success { border-left-color: var(--green); }
.toast--success .toast__icon { background: var(--green-soft); color: var(--green); }
.toast--info { border-left-color: var(--av-steel); }
.toast--info .toast__icon { background: var(--info-soft); color: var(--av-steel); font-family: var(--serif); }
.toast--error { border-left-color: var(--red); }
.toast--error .toast__icon { background: var(--red-soft); color: var(--red); }

/* ---------- Dialog (native <dialog>, opened by kauke.js) ---------- */
.dialog { border: none; border-radius: var(--r-lg); background: var(--surface); box-shadow: var(--shadow-lg); padding: 24px; width: min(92vw, 420px); }
.dialog::backdrop { background: var(--overlay); }
.dialog__title { font: 600 21px/1.3 var(--serif); color: var(--ink); margin: 0 0 8px; }
.dialog__text { font: 400 14px/1.55 var(--sans); color: var(--slate); margin: 0 0 14px; }
.dialog__check { display: flex; gap: 8px; align-items: center; font: 500 13.5px var(--sans); color: var(--body); margin: 0 0 18px; cursor: pointer; }
.dialog__actions { display: flex; justify-content: flex-end; gap: 10px; }

/* ---------- Lightbox (the photo/video viewer behind a gallery) ---------- */
.lightbox { position: fixed; inset: 0; z-index: 50; background: var(--lb-bg); display: flex; flex-direction: column; }
.lightbox[hidden] { display: none; }
.lightbox__bar { display: flex; align-items: center; justify-content: space-between; padding: 18px 26px; position: relative; z-index: 2; }
.lightbox__title { font: 500 14px var(--sans); color: var(--lb-title); }
.lightbox__main { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 0 26px 26px; position: relative; z-index: 2; }
.lightbox__stage { flex: 1; min-height: 0; height: 100%; border-radius: 14px; display: flex; align-items: center; justify-content: center; padding: 16px; font: 500 11px var(--mono); color: var(--lb-label); background: repeating-linear-gradient(135deg, var(--lb-a) 0 16px, var(--lb-b) 16px 32px); overflow: hidden; }
.lightbox__stage img, .lightbox__stage video { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 8px; display: block; }
.lightbox__round { width: 36px; height: 36px; border-radius: 50%; border: none; background: var(--glass); color: var(--on-dark); cursor: pointer; font: 400 18px var(--sans); display: flex; align-items: center; justify-content: center; flex: none; }
.lightbox__round--lg { width: 48px; height: 48px; font-size: 26px; }
.lightbox__round:hover { background: var(--glass-hover); }
.lightbox__backdrop { position: absolute; inset: 0; z-index: 1; background: transparent; border: none; cursor: default; }
/* One picture, nowhere to page to. */
.lightbox.is-single .lightbox__round--lg { display: none; }
/* «Kommenter bildet» (design-comment-replies.md §4): a panel over the foot of the stage, an
   ordinary form. It is a light island on the dark viewer, like a widget, so the textarea keeps
   the page's own tokens. */
.lightbox__bar-actions { display: flex; gap: 8px; }
.lightbox__panel { position: absolute; left: 26px; right: 26px; bottom: 26px; z-index: 3; background: var(--surface); color: var(--ink); border-radius: 12px; padding: 14px; box-shadow: var(--shadow-lg); }
.lightbox__panel[hidden] { display: none; }
.lightbox__panel .comment__about { margin-top: 0; }
.lightbox__panel .comment__thumb { grid-row: 1 / span 2; }
.lightbox__panel textarea { margin-top: 8px; }
.lightbox__panel-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
.lightbox.is-composing .lightbox__round--lg { display: none; }
/* The peek (design-comment-replies.md §2.1): one picture, grown from a comment's thumb. Every
   box is positioned by kauke.js in window coordinates, so the CSS only names the transition. */
.peek { position: fixed; inset: 0; z-index: 45; }
.peek[hidden] { display: none; }
.peek__backdrop { position: absolute; inset: 0; background: var(--overlay); border: none; cursor: default; opacity: 0; transition: opacity .28s ease; }
.peek.is-open .peek__backdrop { opacity: 1; }
.peek__img { position: fixed; object-fit: cover; border-radius: 8px; box-shadow: var(--shadow-lg); background: repeating-linear-gradient(135deg, var(--ph-a) 0 16px, var(--ph-b) 16px 32px); transition: top .28s ease, left .28s ease, width .28s ease, height .28s ease, border-radius .28s ease; }
.peek.is-open .peek__img { border-radius: 12px; object-fit: contain; }
.peek__bar { position: fixed; display: flex; align-items: center; gap: 10px; opacity: 0; transition: opacity .2s ease .18s; }
.peek.is-open .peek__bar { opacity: 1; }
.peek__caption { flex: 1; min-width: 0; font: 400 13px var(--sans); color: var(--on-dark); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .peek__img, .peek__backdrop, .peek__bar { transition: none; } }

/* ---------- Admin shell ----------
   Only the post/event editor still has a header of its own: a slim back bar, deliberately
   without nav. Everything else administers under the ordinary site header. */
.admin-header { background: var(--cream); border-bottom: 1px solid var(--border); }
.admin-header .wrap { display: flex; align-items: center; justify-content: space-between; }
.badge-admin { flex: none; background: var(--ink); color: var(--surface); border-radius: 6px; padding: 3px 9px; font: 600 11px var(--sans); letter-spacing: .03em; }
/* Underline sits a natural distance under the text (mirrors .band-nav__link):
   spacing between items comes from gap, so the line matches the text width. */
.admin-nav { display: flex; gap: 24px; }
.admin-nav a { font: 600 14px var(--sans); color: var(--slate); text-decoration: none; padding: 9px 0; border-bottom: 2px solid transparent; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.admin-nav a:hover { color: var(--ink); }
.admin-nav a.is-active { color: var(--red); border-bottom-color: var(--red); }
/* .admin-nav inside a page rather than in the header band (a standalone event's two admin
   pages). It carries its own hairline, since there is no header edge under it. */
.page-tabs { border-bottom: 1px solid var(--hair); margin: 12px 0 4px; }
.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.page-head h1 { font: 600 32px/1.1 var(--serif); }
.page-head p { font: 400 15px var(--sans); color: var(--slate); margin: 8px 0 0; }
/* A jump button pointing at something already on screen; kauke.js marks it. It fades
   instead of being removed, so the head keeps its shape while you scroll. */
[data-jump] { transition: opacity .2s ease, visibility .2s; }
[data-jump].is-redundant { opacity: 0; visibility: hidden; }
@media (prefers-reduced-motion: reduce) { [data-jump] { transition: none; } }

/* ---------- People table ---------- */
.table { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow-card); }
.table__row { display: grid; grid-template-columns: 2.3fr 2fr 1.3fr 1fr 1.4fr; gap: 12px; align-items: center; padding: 15px 22px; border-bottom: 1px solid var(--hair); }
.table__row:last-child { border-bottom: none; }
.table__head { background: var(--cream); border-bottom: 1px solid var(--border); padding: 13px 22px; font: 600 11.5px var(--sans); color: var(--taupe); letter-spacing: .05em; text-transform: uppercase; }
.table__name { display: flex; align-items: center; gap: 11px; font: 600 14px var(--sans); color: var(--ink); }
.table--import .table__row { grid-template-columns: 1.5fr 1.8fr .9fr; }
/* Folk without the fase 5 foresatte opt-in: no Relasjon column. */
.table--no-relation .table__row { grid-template-columns: 2.3fr 2fr 1fr 1.4fr; }
.table__contact { font: 400 13.5px var(--sans); color: var(--slate); }
.table__contact--empty { color: var(--taupe-light); font-style: italic; }
.cell-label { display: none; font: 600 11px var(--sans); color: var(--taupe); text-transform: uppercase; letter-spacing: .04em; }
/* The quiet action at the end of a row. It reads as a link and carries no box of its own, which
   left the thing a finger has to hit exactly as big as the text: 15px tall. The padding grows
   that to 33px; the matching negative margin hands the space straight back to the layout, so no
   row moves and the desktop table keeps its rhythm.
   It grows on the vertical only, and that is the whole trick. Two of these sit side by side all
   over the admin (Endre · Fjern, Send invitasjon · Fjern), and .last-cell packs them into a
   fixed grid column with 8px between — so any horizontal padding either eats that gap until the
   two hit areas share an edge, or widens the cell past the column and clips it. Vertically there
   is nothing beside them: the row pitch is ~53px, so 33px leaves a 20px dead zone between rows
   and costs no width at all. Where two of them are genuinely dangerous together — «Send
   invitasjon» sends a mail that cannot be unsent — the *container* widens its gap instead. */
.row-action {
  font: 600 12.5px var(--sans); color: var(--red-dark); text-decoration: none;
  display: inline-block; padding: 9px 0; margin: -9px 0;
}
/* also used on form buttons — strip the UA chrome so they look like the links */
button.row-action { background: none; border: none; cursor: pointer; }
/* The quiet actions under a comment. Each one is its own <form> or <details>, so the row
   cannot rely on inline layout: a label that keeps a line box is baseline-aligned against
   its wrapper's much taller strut and hangs ~6px below its neighbours. Both wrappers have
   to lose that line box, or the two halves of the fix disagree: flexing only the <form>
   left «Slett» level with the row and «Rediger» beside it still sitting on one. So the
   <form> flexes, and the <summary> is blockified (fit-content, so the click target is the
   word and not the whole row). flex-start then keeps the labels level when an open
   <details> (rediger, meld) makes the row taller than one line. */
/* The reaction row above sits in pill-shaped chips that carry their own visual padding,
   so 8px read as tighter than it measured — hence 12. */
/* ---- Replies and photo comments (design-comment-replies.md §2) ----
   The thread stays flat. A reply wears a one-line strip saying what it answers; a photo comment
   wears the picture, 60px top-left, with everything else indented beside it. Both are the same
   markup in the compose box, so what you are about to send looks like what you sent. */
.ctx-strip { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding: 6px 10px 6px 12px; border-left: 3px solid var(--hair); background: var(--panel); border-radius: 0 8px 8px 0; font: 400 13px var(--sans); color: var(--slate); min-width: 0; }
.ctx-strip[hidden] { display: none; }
.ctx-strip__link { display: flex; gap: 8px; min-width: 0; flex: 1; color: inherit; text-decoration: none; }
.ctx-strip__link:hover .ctx-strip__who, .ctx-strip__link:focus-visible .ctx-strip__who { color: var(--red); }
.ctx-strip__who { font-weight: 600; color: var(--body); white-space: nowrap; }
.ctx-strip__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.ctx-strip__x { margin-left: auto; color: var(--slate); text-decoration: none; font-size: 15px; padding: 2px 4px; }
.ctx-strip__x:hover { color: var(--red); }
.comment__photo { display: flex; align-items: flex-start; gap: 14px; margin-top: 10px; }
.comment__beside { flex: 1; min-width: 0; }
.comment__beside > .comment__about { margin-top: 0; }
.comment__beside > .comment__body { margin-top: 6px; }
.comment__beside > .ctx-strip { margin-top: 6px; }
.comment__thumb { flex: none; }
.comment__thumb { display: block; width: 60px; height: 60px; border-radius: 8px; overflow: hidden; background: repeating-linear-gradient(135deg, var(--ph-a) 0 6px, var(--ph-b) 6px 12px); }
.comment__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
a.comment__thumb:hover, a.comment__thumb:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.comment__about { margin-top: 6px; font: 400 13px var(--sans); color: var(--slate); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.comment-actions { margin-top: 12px; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px; }
.comment-actions > form { display: flex; }
.comment-actions > details > summary { display: block; width: fit-content; }
.last-cell { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

/* ---------- Editor ---------- */
.editor-title { width: 100%; border: 1px solid var(--border-input); border-radius: var(--r-md); background: var(--surface); font: 600 30px/1.3 var(--serif); color: var(--ink); outline: none; padding: 12px 16px 6px; transition: border-color .15s, box-shadow .15s; box-shadow: var(--shadow-soft); }
.editor-title:focus { border-color: var(--red); box-shadow: 0 0 0 3px var(--focus); }
.editor-title::placeholder { color: var(--taupe-light); font-weight: 500; }
.toolbar { display: inline-flex; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); padding: 7px 8px; box-shadow: var(--shadow-toolbar); }
.toolbar__scroll { display: flex; align-items: center; gap: 6px; min-width: 0; }
.toolbar button { width: 34px; height: 34px; border: none; background: transparent; border-radius: var(--r-sm); color: var(--ink); cursor: pointer; font: 600 15px var(--sans); }
.toolbar button:hover, .toolbar button.is-active { background: var(--panel); }
.toolbar .sep { width: 1px; height: 20px; background: var(--border); }
.editor-body { margin-top: 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); padding: 24px 26px; min-height: 220px; font: 400 17px/1.7 var(--sans); color: var(--body); box-shadow: var(--shadow-soft); }
/* The widget editor, folded away under the post form (design-widgets.md §7.1). Most innlegg are
   text and pictures and should not have to scroll past a code box to reach the thumbnails. */
.editor-widget { margin-top: 28px; border-top: 1px solid var(--border); padding-top: 20px; }
.editor-widget > summary { cursor: pointer; font: 600 15px var(--sans); color: var(--ink); list-style: none; display: flex; align-items: center; gap: 8px; }
.editor-widget > summary::-webkit-details-marker { display: none; }
.editor-widget > summary::before { content: '▸'; color: var(--taupe); }
.editor-widget[open] > summary::before { content: '▾'; }
.editor-code {
  width: 100%; resize: vertical; background: var(--surface); border: 1px solid var(--border-input);
  border-radius: var(--r-md); padding: 14px 16px; outline: none;
  font: 400 13px/1.65 var(--mono); color: var(--body); tab-size: 2;
}
.editor-code:focus { border-color: var(--red); box-shadow: 0 0 0 3px var(--focus); }

.dropzone { border: 1.5px dashed var(--border-strong); background: var(--surface-tint); border-radius: var(--r-md); padding: 22px; text-align: center;
  transition: border-color .12s ease, background .12s ease; }
/* While files hover over the zone — the only feedback that a drop will land here. */
.dropzone--over { border-color: var(--red); background: var(--red-soft); }
.dropzone__hint { font: 600 15px var(--sans); color: var(--ink); }
.dropzone__hint a { color: var(--red); text-decoration: underline; cursor: pointer; }
.dropzone__sub { font: 400 13px var(--sans); color: var(--taupe); margin-top: 4px; }
/* A card that takes a dropped contact (contact_drop_controller): the same red the dropzone
   turns, on the card's own edge, while the drag is over it. */
.card.is-drop-target { outline: 1.5px dashed var(--red); outline-offset: -1.5px; background: var(--red-soft); }
.thumbs { display: flex; gap: 12px; margin-top: 18px; justify-content: center; flex-wrap: wrap; }
.thumb { position: relative; width: 118px; height: 88px; border-radius: 10px; overflow: hidden;
  background: repeating-linear-gradient(135deg, var(--ph-a) 0 10px, var(--ph-b) 10px 20px); }
.thumb__x { position: absolute; top: 5px; right: 5px; width: 22px; height: 22px; border-radius: 50%; background: var(--scrim-strong); color: var(--on-dark); display: flex; align-items: center; justify-content: center; font: 400 13px var(--sans); cursor: pointer; border: none; }
.thumb__cover { position: absolute; left: 5px; bottom: 5px; background: var(--red-fill); color: var(--on-dark); border-radius: 6px; padding: 2px 7px; font: 600 10px var(--sans); }
/* The ordinal an editor writes as `bilde:N` (design-inline-images.md §2). Rendered by Twig, not
   by JavaScript, so typing the marker by hand stays a real option. */
.thumb__n { position: absolute; top: 5px; left: 5px; min-width: 20px; height: 20px; border-radius: 50%; background: var(--scrim-strong); color: var(--on-dark); display: flex; align-items: center; justify-content: center; font: 600 11px var(--sans); pointer-events: none; }
/* Ships hidden; inline_photo_controller.js reveals it. Covers the tile's foot on hover only, so
   the strip still reads as pictures rather than as a wall of buttons. */
.thumb__insert { position: absolute; left: 0; right: 0; bottom: 0; padding: 5px 4px; border: none; background: var(--scrim-strong); color: var(--on-dark); font: 600 10.5px var(--sans); cursor: pointer; opacity: 0; transition: opacity .12s ease; }
.thumb:hover .thumb__insert, .thumb__insert:focus-visible { opacity: 1; }
.thumb__progress { position: absolute; inset: 0; background: var(--scrim-soft); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 7px; }
.thumb__progress span { font: 600 12px var(--sans); color: var(--on-dark); }
.thumb__bar { width: 78px; height: 5px; border-radius: 3px; background: var(--glass-track); overflow: hidden; }
.thumb__bar > i { display: block; height: 100%; background: var(--on-dark); }
.thumb--add { width: 118px; height: 88px; border-radius: 10px; border: 1.5px dashed var(--border-strong); display: flex; align-items: center; justify-content: center; font: 300 30px var(--sans); color: var(--mark); cursor: pointer; }

/* ---------- Vedlegg (design-documents.md §9) ----------
   A list, not a gallery. A document is a thing you fetch by its name, so the name is the link and
   the picture is only ever a hint: a rendered first page where we have one, a type badge where we
   don't. Nothing here opens in place — the server sends every one of these as an attachment. */
.attachments { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.attachment {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface-tint);
}
/* Same striped ground the photo placeholders use, so a document that is still waiting for its
   preview looks like the rest of the media rather than like a broken image. */
.attachment__thumb {
  width: 46px; height: 60px; flex: none; object-fit: cover; border-radius: 4px;
  background: repeating-linear-gradient(135deg, var(--ph-a) 0 10px, var(--ph-b) 10px 20px);
}
.attachment__badge {
  width: 46px; height: 60px; flex: none; border-radius: 4px;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel); color: var(--taupe);
  font: 600 11px var(--mono); letter-spacing: .04em;
}
.attachment__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.attachment__name { font: 600 15px var(--sans); color: var(--ink); text-decoration: none; overflow-wrap: anywhere; }
.attachment__name:hover { color: var(--red); text-decoration: underline; }
.attachment__meta { font: 400 13px var(--sans); color: var(--taupe); }
.attachment__x {
  flex: none; width: 26px; height: 26px; border-radius: 50%; border: none; cursor: pointer;
  background: transparent; color: var(--taupe); font: 400 14px var(--sans);
}
.attachment__x:hover { background: var(--panel); color: var(--ink); }

/* ---------- Date-poll grid (design-events.md §18 · Claude Design «Svar mobil» 1a) ----------
   Desktop: the matrix sits inside the article card. Mobile: the card is gone, the grid
   bleeds edge-to-edge and only the date columns scroll — the name column stays put. */
.poll-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin; }
.poll {
  border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%;
  font: 500 14px var(--sans); color: var(--ink);
  --poll-bg: var(--surface); /* sticky-column backdrop; the mobile query swaps it to cream */
}
.poll th, .poll td { padding: 12px 10px; text-align: center; border-bottom: 1px solid var(--hair); vertical-align: middle; }
.poll thead th { vertical-align: bottom; padding-bottom: 14px; }
.poll .cnr, .poll .rowlab {
  position: sticky; left: 0; z-index: 2; background: var(--poll-bg);
  text-align: left; padding-left: 0; box-shadow: 1px 0 0 var(--border);
}
.poll thead .cnr { z-index: 3; }
.poll .dcol { min-width: 96px; }
.poll .d-day { font: 600 13px var(--sans); color: var(--taupe); }
.poll .d-date { font: 600 18px var(--serif); color: var(--ink); line-height: 1.1; margin-top: 2px; white-space: nowrap; }
.poll .d-time { font: 500 13px var(--sans); color: var(--slate); margin-top: 1px; }
/* A proposal that is a whole day rather than a time of day. The line is never left blank —
   an empty slot between the date and the vote count reads as a broken column. */
.poll .d-time--allday { font-style: italic; color: var(--taupe); white-space: nowrap; }
.poll .d-cnt { display: inline-flex; align-items: center; gap: 4px; margin-top: 8px; font: 600 12px var(--sans); color: var(--green); background: var(--green-soft); border-radius: var(--r-pill); padding: 2px 9px; }
.poll .rowlab { font: 600 14px var(--sans); white-space: nowrap; }
.poll .rowlab .me { color: var(--taupe); font-weight: 500; }
.poll .mk--y { color: var(--green); }
.poll .mk--m { color: var(--amber); }
.poll .mk--n { color: var(--slate); }
.poll .mk--none { color: var(--taupe-light); }
.poll .yourrow td { padding-top: 14px; padding-bottom: 14px; }
.poll .yourrow .rowlab { color: var(--red); }
.poll .chosen { background: var(--green-soft); }

/* Compact answer toggle: an invisible native <select> covers an icon box — tapping it
   opens the platform picker with the full Kan / Kanskje / Kan ikke labels, and :has()
   paints the box from the chosen value. No JS, and the form field is the real control. */
.tog {
  position: relative; width: 52px; height: 44px; margin: 0 auto;
  border: 1.5px solid var(--border-input); background: var(--surface); border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  font: 600 15px var(--sans); color: var(--taupe-light);
}
.tog::before { content: "–"; }
.tog select { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; appearance: none; -webkit-appearance: none; border: none; }
.tog:focus-within { box-shadow: 0 0 0 3px var(--focus-ring); }
.tog:has(option[value="yes"]:checked) { border-color: var(--green); background: var(--green-soft); color: var(--green); }
.tog:has(option[value="yes"]:checked)::before { content: "✓"; }
.tog:has(option[value="maybe"]:checked) { border-color: var(--amber-border); background: var(--amber-soft); color: var(--amber); }
.tog:has(option[value="maybe"]:checked)::before { content: "(✓)"; }
.tog:has(option[value="no"]:checked) { border-color: var(--border-warm); background: var(--panel); color: var(--slate); }
.tog:has(option[value="no"]:checked)::before { content: "✕"; }

.poll-hint { display: none; font: 500 12.5px var(--sans); color: var(--taupe); margin: 14px 0 0; align-items: center; gap: 7px; }
.poll-hint svg { color: var(--taupe-light); flex: none; }
/* Only one of the two lines is ever shown; [hidden] takes care of the other. */
.poll-hint__line { display: inline-flex; align-items: center; gap: 7px; }

/* ---------- Path chooser («To veier», create event — Claude Design 1b) ----------
   Two radio inputs rendered as selectable cards; :checked sibling selectors reveal the
   chosen path's fields. No JS: the radios precede the cards and regions in the DOM. */
.path-picker { position: relative; }
/* Focusable for keyboard users, invisible to everyone. */
.path-choice { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.path-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
.path-card {
  display: block; text-align: center; padding: 16px 10px; border-radius: 14px; cursor: pointer;
  border: 1.5px solid var(--border); background: var(--surface); opacity: .72;
  transition: border-color .15s, background .15s, box-shadow .15s, opacity .15s;
}
.path-card svg { display: block; margin: 0 auto 6px; }
.path-card__title { display: block; font: 600 14px var(--sans); color: var(--ink); }
.path-card__sub { display: block; font: 400 12px var(--sans); color: var(--taupe); margin-top: 3px; }
#mode-fixed:checked ~ .path-cards .path-card--fixed,
#mode-poll:checked ~ .path-cards .path-card--poll {
  border-color: var(--red); background: var(--red-soft);
  box-shadow: var(--shadow-red); opacity: 1;
}
#mode-fixed:focus-visible ~ .path-cards .path-card--fixed,
#mode-poll:focus-visible ~ .path-cards .path-card--poll { box-shadow: 0 0 0 3px var(--focus-ring); }
.path-hint {
  border: 1px dashed var(--border-warm); border-radius: 12px; padding: 18px; text-align: center;
  font: 400 13.5px/1.5 var(--sans); color: var(--taupe-light);
}
.path-fields { display: none; }
#mode-fixed:checked ~ .path-fields--fixed,
#mode-poll:checked ~ .path-fields--poll { display: block; animation: riseIn .18s ease; }
#mode-fixed:checked ~ .path-hint,
#mode-poll:checked ~ .path-hint { display: none; }
@keyframes riseIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .path-fields { animation: none !important; } }

/* ---------- /avstemning as a landing page ----------
   The click from an advertisement lands on a form, and a form is not an argument. These three
   pieces are what the page shows above it: what the visitor will get (`poll-shot`, the answer
   grid the ad image also shows), and the two or three words that answer «hva er dette» before
   the first field (`trust-line`).

   `trust-line` is deliberately NOT a row of pills. A pill is kauke's shape for something you can
   press, and three of them under a heading read as a choice to make rather than a fact to
   register. Plain text with the house `·` separator says the same thing and asks for nothing. */
.poll-shot { border: 1px solid var(--border-warm); border-radius: var(--r-lg); overflow: hidden; background: var(--surface); }
.poll-shot__row { display: grid; grid-template-columns: 1fr 54px 54px 54px; align-items: center; }
.poll-shot__row + .poll-shot__row { border-top: 1px solid var(--hair); }
.poll-shot__row--head { background: var(--panel); border-bottom: 1px solid var(--border); }
.poll-shot__row--all { background: var(--red-soft); }
.poll-shot__name { font: 500 13.5px var(--sans); color: var(--ink); padding: 9px 12px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.poll-shot__cell { text-align: center; font: 600 13px var(--sans); padding: 9px 0; }
.poll-shot__row--head .poll-shot__name { font: 600 11px var(--sans); color: var(--taupe); }
.poll-shot__row--head .poll-shot__cell { font: 600 11px/1.3 var(--sans); color: var(--taupe); padding: 8px 0; }
.poll-shot__row--all .poll-shot__name,
.poll-shot__row--all .poll-shot__cell { color: var(--red-dark); font-weight: 600; }
.poll-shot__yes { color: var(--green); }
.poll-shot__no { color: var(--mark-soft); }
.trust-line { display: flex; align-items: center; gap: 8px; margin: 14px 0 0;
  font: 500 13px var(--sans); color: var(--taupe); }

/* ---------- /ny og /a/ny as landing pages ----------
   Same job as `poll-shot` above, for the two pages that used to answer an advertisement with a
   Vipps wall and nothing else. `feed-shot` is one shape for both: a group page with its own
   address, an event with its answers. What it draws is the product, not a logo, which is the
   same rule the ad images follow (ads/reddit/README.md).

   `gains` is three lines rather than a grid of cards. A card is a place to press, and nothing
   here is pressable — the only thing to press on either page is the Vipps button.

   No dark-mode block, for the same reason `poll-shot` has none: every value below is a token,
   so the shot follows the theme without being told. */
.feed-shot { border: 1px solid var(--border-warm); border-radius: var(--r-lg); overflow: hidden;
  background: var(--surface); }
.feed-shot__bar { background: var(--panel); border-bottom: 1px solid var(--border);
  font: 600 12px var(--sans); color: var(--taupe); padding: 9px 14px; }
.feed-shot__row { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 14px; }
.feed-shot__row + .feed-shot__row { border-top: 1px solid var(--hair); }
.feed-shot__row--stack { display: block; }
.feed-shot__title { font: 600 14px var(--sans); color: var(--ink); }
.feed-shot__excerpt { font: 400 13.5px/1.5 var(--sans); color: var(--body-soft); margin-top: 4px; }
.feed-shot__meta { font: 400 12.5px var(--sans); color: var(--taupe); margin-top: 3px; }
.feed-shot__photos + .feed-shot__meta { margin-top: 8px; }
.feed-shot__tag { display: inline-block; white-space: nowrap; border-radius: 999px;
  background: var(--panel); border: 1px solid var(--border); color: var(--taupe);
  font: 600 11.5px var(--sans); padding: 4px 9px; }
.feed-shot__tag--yes { background: var(--green-soft); border-color: var(--green-border);
  color: var(--green); }
.feed-shot__tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }
.feed-shot__photos { display: flex; gap: 6px; margin-top: 9px; }
/* The span stays the box and the photo goes inside it, rather than the <img> being the box: the
   play badge below is drawn with ::before/::after, and a replaced element has no pseudo-elements
   to draw them into. The two placeholder tints are still worth their line — they are what fills
   the strip while the pictures load. Photos are never filtered, so the strip is the one part of
   the shot that looks the same in both themes.

   The box is 16:9 rather than a fixed height, and the files are cut to the same ratio, so `cover`
   has nothing to crop and the whole picture is what a visitor sees. It is the ratio a phone
   shoots in, which is the point: the strip should look like the photos a group actually has. */
.feed-shot__photo { position: relative; flex: 1; aspect-ratio: 16 / 9; border-radius: var(--r-sm);
  background: var(--ph-a); border: 1px solid var(--hair); overflow: hidden; }
.feed-shot__photo:nth-child(2) { background: var(--ph-b); }
.feed-shot__photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* A clip in the strip, which for a korps is the whole argument: the recording from the concert
   has nowhere else to live but a public streaming service. `--on-dark` on purpose, and not
   themed — a play badge sits on a photo, which is the one place white stays white. */
.feed-shot__photo--video::before { content: ''; position: absolute; left: 50%; top: 50%;
  width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%;
  background: var(--scrim-soft); }
.feed-shot__photo--video::after { content: ''; position: absolute; left: 50%; top: 50%;
  margin: -5px 0 0 -3px; border-style: solid; border-width: 5px 0 5px 8px;
  border-color: transparent transparent transparent var(--on-dark); }
.gains { list-style: none; margin: 18px 0 0; padding: 0; }
.gains li { position: relative; padding: 0 0 0 20px; margin-bottom: 9px;
  font: 400 14.5px/1.55 var(--sans); color: var(--body); }
.gains li::before { content: ''; position: absolute; left: 2px; top: 8px; width: 7px; height: 7px;
  border-radius: 50%; background: var(--red); }
.gains strong { font-weight: 600; color: var(--ink); }

/* «Legg til sted og beskrivelse»: the optional half of a form, folded away in a <details> so it
   opens with no JavaScript. Same marker discipline as the markup cheat sheet — the native
   triangle is replaced, not left to look like a different control on every browser. */
.field-more > summary { display: inline-block; list-style: none; cursor: pointer;
  font: 600 13.5px var(--sans); color: var(--slate); }
.field-more > summary::-webkit-details-marker { display: none; }
.field-more > summary:hover { color: var(--ink); }
.field-more > summary::before { content: '+ '; color: var(--taupe); }
.field-more[open] > summary::before { content: '– '; }

/* Ghost add-button («+ Legg til forslag») and the dashed add-row disclosure. */
.btn-add {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--panel); border: 1px dashed var(--border-input); color: var(--slate);
  border-radius: var(--r-pill); padding: 9px 15px; font: 600 13px var(--sans); cursor: pointer;
}
.btn-add:hover { color: var(--ink); border-color: var(--border-strong); }
.add-row {
  display: flex; align-items: center; gap: 10px;
  border: 1.5px dashed var(--border-strong); background: var(--surface-tint); border-radius: 12px;
  padding: 12px 14px; cursor: pointer; font: 600 13.5px var(--sans); color: var(--slate);
}
.add-row__plus { width: 34px; height: 34px; border-radius: 9px; background: var(--panel); display: flex; align-items: center; justify-content: center; color: var(--taupe); font-size: 20px; flex: none; }
details.add-disclosure > summary { list-style: none; }
details.add-disclosure > summary::-webkit-details-marker { display: none; }
details.add-disclosure[open] > summary .add-row { border-style: solid; background: var(--surface); }

/* select-like control */
.select-box { display: flex; align-items: center; justify-content: space-between; background: var(--surface); border: 1px solid var(--border-input); border-radius: var(--r-md); padding: 13px 16px; font: 500 15px var(--sans); color: var(--ink); cursor: pointer; }
.select-box .caret { color: var(--taupe); font-size: 12px; }
.readonly-box { display: flex; align-items: center; gap: 9px; background: var(--panel); border: 1px solid var(--border-warm); border-radius: var(--r-md); padding: 13px 16px; font: 500 14.5px var(--sans); color: var(--slate); overflow-wrap: anywhere; }

/* ---------- «Sett av» (design-seen-by.md §6) ----------
   Collapsed by default: the number is the answer, the names are the follow-up. A <details>,
   so it opens without JS — same as the group switcher. Typography follows the RSVP block,
   including var(--taupe) for the quieter «who is missing» half. */
.seen-by > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: 7px; font: 600 13.5px var(--sans); color: var(--slate); }
.seen-by > summary::-webkit-details-marker { display: none; }
.seen-by__caret {
  width: 0; height: 0; flex: none; margin-top: 1px;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-top: 5px solid var(--taupe);
  transition: transform .15s;
}
.seen-by[open] .seen-by__caret { transform: rotate(180deg); }
.seen-by > summary:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; border-radius: 4px; }
.seen-by__names { margin-top: 9px; display: flex; flex-direction: column; gap: 5px; font: 400 13.5px/1.55 var(--sans); color: var(--slate); }
.seen-by__missing { color: var(--taupe); }

/* ---------- Footer ---------- */
.site-footer { border-top: 1px solid var(--border); background: var(--cream); }
.site-footer .wrap { display: flex; align-items: center; justify-content: space-between; gap: 6px 18px; flex-wrap: wrap; padding: 20px 24px; font: 400 13px var(--sans); color: var(--taupe); }
.site-footer__brand { font: 600 13px var(--sans); color: var(--slate); }
/* Lenkeraden er lengre enn før, så én regel her i stedet for style="color:inherit" på hver lenke. */
.site-footer a { color: inherit; text-decoration: none; }
.site-footer a:hover { text-decoration: underline; }
.site-footer__nav { display: inline; }
.site-footer__nowrap { white-space: nowrap; }
/* Nedtrekket er telefonens utgave av lenkeraden, så det er borte til det trengs. Tema- og
   språkvelgeren står sammen ytterst til høyre, som én gruppe småting man trykker på. */
.site-footer__menu { display: none; }
.site-footer__controls { display: flex; align-items: center; gap: 10px; }
/* `.site-footer a { color: inherit }` gjelder også menyen, og der er taupe for svakt: en rad i
   et nedtrekk leses som en knapp, ikke som bunntekst. */
.site-footer .switcher__item { color: var(--ink); }
.site-footer .switcher__item:hover { text-decoration: none; }

/* På telefon fikk de tre gruppene aldri plass på én rad, og `space-between` fordelte restene
   utover som halve linjer. Stablet er det tre rader som begynner på samme kant.
   Samme sted bytter lenkeraden mot nedtrekket: seks lenker med «·» mellom seg ga hitbokser
   som lå tettere enn en finger treffer, og hver rad i menyen er 38px høy. Menyen holdes
   forankret i sin egen knapp, i motsetning til toppens, som spenner over hele bredden. */
@media (max-width: 720px) {
  .site-footer .wrap { flex-direction: column; align-items: flex-start; gap: 14px; }
  .site-footer__nav { display: none; }
  .site-footer__menu { display: block; }
  .site-footer .switcher { position: relative; }
  .site-footer .switcher__menu { left: 0; right: auto; min-width: 200px; }
  /* «Naviger» og temavelgeren er begge småting man trykker på, så de deler den siste raden i
     stedet for å ta hver sin. De brytes til hver sin linje på en veldig smal skjerm. */
  .site-footer__controls {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px 14px; flex-wrap: wrap; width: 100%;
  }
}

/* ---------- Tema (design-dark-mode.md §3) ----------
   Three real buttons in a segmented pill, in the footer of every page and again on /konto. Quiet
   on purpose: it is a preference, not an action, and it sits beside «Om · Personvern · Kontakt».
   Icon-only, so the footer line has room for it; the name lives in aria-label and title. */
.theme { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.theme__label { font: 500 13px var(--sans); color: var(--taupe); }
.theme__opts {
  display: inline-flex; gap: 2px; padding: 2px;
  background: var(--panel); border: 1px solid var(--border-warm); border-radius: var(--r-pill);
}
.theme__opt {
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--slate);
  background: none; border: none; border-radius: var(--r-pill);
  padding: 6px 10px; cursor: pointer;
}
.theme__opt:hover { color: var(--ink); }
.theme__opt.is-active { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-toast); }
.theme__opt:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }

/* ---------- Språk (design-i18n.md §2) ----------
   A globe plus the current code (e.g. «NB») the height of the theme pill (34px), opening the footer's switcher upward. The items
   are submit buttons dressed as menu rows, so the button reset below is what makes them look
   like the links in «Naviger» beside it. */
.switcher--small .locale__summary { height: 34px; padding: 0 10px 0 9px; justify-content: center; gap: 5px; box-sizing: border-box; }
.locale__code { font: 600 12px var(--sans); letter-spacing: .04em; line-height: 1; }
/* It sits at the far right of the row, so the menu hangs from its right edge, or it runs off
   the page on a wide screen and the check on the current language is the first thing cut. */
.locale .switcher__menu { left: auto; right: 0; min-width: 160px; }
.locale__item { width: 100%; background: none; border: none; cursor: pointer; text-align: left; }
.locale__item[aria-current] .switcher__role { color: var(--red); }

/* ---------- Helpers ---------- */
.muted { color: var(--taupe); }
.stack-sm > * + * { margin-top: 10px; }
.section-label { font: 600 11.5px var(--sans); color: var(--taupe); letter-spacing: .08em; text-transform: uppercase; }
.back-link { font: 600 14px var(--sans); color: var(--taupe); text-decoration: none; }
.back-link:hover { color: var(--ink); }

/* ============================================================
   Responsive — collapse the desktop layouts to the mobile
   mockups (single column, stacked table rows, scrollable chips)
   ============================================================ */
@media (max-width: 720px) {
  .wrap { padding: 0 16px; }
  .section { padding: 28px 0 40px; }

  /* A card's inner padding matches the page gutter, so the gap outside the card and the gap
     inside it are the same 16px. They used to differ by a few pixels (22px in against 16px
     out), which reads as a mistake rather than a rhythm — and on a 390px screen every one of
     those pixels is content width. The thread is the loud case: 56px, set for a desktop
     column, survived here whenever it wasn't sitting on a .section--paper sheet. */
  .card__pad { --card-pad: 16px; }
  .post { padding: 18px 16px; }
  .card--thread { padding: 20px 16px; }
  .list-row { padding-left: 16px; padding-right: 16px; }

  .cluster { grid-template-columns: 1fr; }
  /* One card per row: there is no neighbour to line up with, so the space held for a missing
     note is just a gap under the button. */
  .card-action { padding-bottom: 0; }

  .band-name { font-size: 22px; }
  .band-logo { width: 46px; height: 46px; }
  .feed-actions .btn { width: 100%; }

  .post__title { font-size: 21px; }
  /* Two thumbs beside the text on a 390px screen leaves the tittel nothing, so the shots
     drop under it and take the full row instead. */
  .postmini { flex-direction: column; gap: 12px; }
  .postmini__title { font-size: 17.5px; }
  .postmini__shots { align-self: stretch; }
  .postmini__shots .ph { flex: 1 1 0; width: auto; height: 92px; }
  .gallery--feature { grid-template-columns: 1fr 1fr; grid-auto-rows: 100px; }
  .gallery--feature .ph:first-child { grid-column: span 2; grid-row: span 1; }

  .page-head { flex-direction: column; }
  .page-head h1 { font-size: 26px; }

  /* people table → stacked cards */
  .table { border-radius: var(--r-lg); }
  .table__head { display: none; }
  .table__row,
  .table--import .table__row,
  .table--no-relation .table__row {
    grid-template-columns: 1fr; gap: 6px; padding: 16px 18px;
  }
  .table__row > span:not(.table__name) { display: flex; align-items: center; gap: 8px; }
  .cell-label { display: inline-block; min-width: 84px; }
  /* The row is a stacked card here, not a grid column, so the gap is free — and this is the
     one width where 8px between «Endre» and «Fjern» is a thumb rather than a cursor. Written
     as `span.last-cell` to match the specificity of the rule above, which would otherwise keep
     handing every span in the row the same 8px. */
  .table__row > span.last-cell { justify-content: flex-start; gap: 22px; }

  .editor-title { font-size: 28px; }

  .hero-panel { padding: 44px 0 56px; }
  .hero-panel h1 { font-size: 28px; }

  /* The admin row is already its own row; on a phone it only needs to scroll sideways.
     min-width:0 lets it shrink past its content so the role pill keeps its place. */
  .site-header .wrap { padding-top: 12px; padding-bottom: 12px; }
  .site-header .wrap.site-header__admin { gap: 14px; }
  .admin-nav { min-width: 0; gap: 20px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
  .admin-nav::-webkit-scrollbar { display: none; }
  /* kauke.js toggles has-more on both while content is clipped off the right edge. The mask
     makes a stacking context but no containing block, so the poll's sticky name column still
     pins to the left inside it. */
  .admin-nav.has-more,
  .poll-scroll.has-more { -webkit-mask-image: linear-gradient(to right, black calc(100% - 32px), transparent); mask-image: linear-gradient(to right, black calc(100% - 32px), transparent); }
  .admin-nav a { padding: 12px 0; }

  /* ---- Lightbox on a phone ----
     The picture gets the whole width. Two 48px buttons with a 20px gap and 26px gutters
     were taking 188px of a 390px screen — more than half of it — so the arrows stop being
     columns of their own and become tall tap zones over the picture's edges, which is
     where a thumb lands anyway. Swiping does the same job (kauke.js). */
  .lightbox__bar { padding: 12px 16px; }
  .lightbox__title { font-size: 13px; }
  .lightbox__main { padding: 0 0 16px; gap: 0; }
  .lightbox__stage { padding: 0; border-radius: 0; background: none; }
  .lightbox__stage img, .lightbox__stage video { border-radius: 0; }
  .lightbox__round--lg {
    position: absolute; top: 0; bottom: 16px; width: 30%; height: auto;
    border-radius: 0; background: none; font-size: 34px; padding: 0 14px;
  }
  .lightbox__round--lg:hover { background: none; }
  .lightbox [data-lightbox-prev] { left: 0; justify-content: flex-start; }
  .lightbox [data-lightbox-next] { right: 0; justify-content: flex-end; }
  /* A clip owns its own surface: play, scrub and volume all live under those zones. Shrink
     them back to circles, centred clear of the control bar, and leave swipe to do the rest. */
  .lightbox.is-video .lightbox__round--lg {
    top: 50%; bottom: auto; transform: translateY(-50%);
    width: 44px; height: 44px; border-radius: 50%; padding: 0;
    background: var(--glass-soft); font-size: 24px; justify-content: center;
  }
  .lightbox.is-video [data-lightbox-prev] { left: 8px; }
  .lightbox.is-video [data-lightbox-next] { right: 8px; }
  .lightbox__panel { left: 8px; right: 8px; bottom: 8px; }

  .chips { overflow-x: auto; flex-wrap: nowrap; padding-bottom: 4px; }
  /* …except a reaction row: overflow-x on the scroll parent would clip the picker that
     hangs below it, so this row wraps instead of scrolling. */
  .chips.react-row { overflow: visible; flex-wrap: wrap; }

  .path-cards { grid-template-columns: 1fr; }

  /* «Svar mobil» 1a: the article card goes edge-to-edge, only the date columns scroll.
     The white surface moves up to the section, which carries it full-bleed from header
     to footer — a pseudo-card without corners, so controls, chips and text sit on the
     same paper as on desktop. */
  .section--paper { background: var(--surface); }
  .card--article { padding: 24px 0 32px; border: none; border-radius: 0; box-shadow: none; background: transparent; }
  /* The thread — and any padded panel below the article, e.g. «Foreslå bilder» — joins the
     same sheet: a hairline instead of card chrome, so they line up with the article's own
     sections rather than sitting inset on top of it. */
  .section--paper .card--thread,
  .section--paper .card__pad {
    margin-top: 0; padding: 24px 0 28px; background: transparent;
    border: none; border-top: 1px solid var(--border); border-radius: 0; box-shadow: none;
  }
  .poll-scroll { margin: 0 -16px; }
  /* Buy back the width three dates need to fit without a swipe. The sticky column's rule
     goes, and with it the padding that was only there to hold the name off that rule — the
     next column's own padding separates them. Cell padding halves, so the gap *between* two
     dates stops being double the gap at the edges; min-width drops by exactly what the
     padding gave back, so the date text keeps the room it had and only the slack is lost. */
  .poll .cnr, .poll .rowlab { padding-left: 16px; padding-right: 0; box-shadow: none; }
  .poll th, .poll td { padding-left: 5px; padding-right: 5px; }
  .poll .dcol { min-width: 86px; }
  /* «(deg)» goes under the name instead of beside it. It is a quiet marker, and inline it
     was the single widest thing in the column — the ~38px it cost is most of what the third
     date needs. The column is sized by its longest name, so this buys the width back for
     every row at once. */
  .poll .rowlab .me { display: block; font-size: 12px; line-height: 1.3; }
  .poll-hint { display: flex; }
  .poll-save { width: 100%; padding: 15px 24px; font-size: 15px; }

  .site-header__nav .navlink:not(:last-child) { display: none; }

  /* A phone header carries switcher · Administrer · bell · avatar. Tightening the gaps and
     the switcher's own side padding buys back the ~13px that was otherwise taken out of the
     group name, so it reads in full on the widths people actually use. */
  .site-header__nav { gap: 6px; }
  .switcher__summary { padding-left: 11px; padding-right: 11px; }
  .site-header__nav .btn { padding-left: 14px; padding-right: 14px; }

  /* ---- The narrow mark in crowded headers ----
     The full wordmark wants 4em. A logged-in header carries a group switcher, «Administrer»,
     the bell and the avatar beside it, and flex was shrinking the logo to fit — down to a
     17px smudge on the band feed. The narrow mark (just the k and its ring) needs a third of
     the width, so everything fits at full size instead. Keyed off the bell, which is exactly
     what a logged-in header has and a «Logg inn»/«Om» header hasn't — those keep the
     wordmark, and so does the big centred one on the login and landing pages. */
  .site-header .wordmark { flex: none; }
  .site-header:has(.bell-link) .wordmark {
    background-image: var(--wordmark-small);
    width: 1.25em;  /* 90 × 126 — the box keeps the wordmark's 1.75em height, so the mark
                       fills it and stays the same size as on desktop. */
    /* No optical nudge on this one. The lift exists because the full wordmark's weight sits
       low — «auke» is a mass of lowercase in the lower half, putting its ink centroid at
       58.9 % of the height. Drop the letters and the ring-and-k balances at 48.3 %, near
       enough dead centre, so the same lift would just push it off. */
    --wordmark-nudge: 0px;
  }
}

@media (max-width: 480px) {
  .grid-2, .grid-3 { grid-template-columns: 1fr !important; }
}

/* generic responsive grids used on the states page */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
@media (max-width: 900px) { .grid-4 { grid-template-columns: 1fr 1fr; } .grid-3 { grid-template-columns: 1fr; } }

/* ---------- Rik tekst (design-rich-editor.md §5) ----------
   TipTap mounts inside a second .editor-body, so the surface is the one the textarea already
   had; the padding moves onto the ProseMirror element so the whole box takes the caret. The base
   rules ProseMirror ships (pre-wrap, the hidden gapcursor) are written here rather than pulled
   from its stylesheet, so they follow the tokens and never load for a reader. */
.editor-mode-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; position: relative; }
.editor-mode-row .toolbar__scroll { flex-wrap: wrap; }
/* The toolbar follows the writer down a long post: the row sticks under the top of the viewport,
   on a strip of page background so the text scrolls under it rather than through it. The strip's
   padding is taken back out of the row's margins (the partial subtracts the 8px on top), so the
   editors sit where they sat. Sticky only while the toolbar is shown: in markdown mode the row
   holds nothing worth pinning. */
.editor-mode-row { padding: 8px 0 10px; margin-bottom: -10px; background: var(--cream); }
.editor-mode-row:has(> .toolbar:not([hidden])) { position: sticky; top: 0; z-index: 15; }
/* On a phone the toolbar keeps to one row and scrolls sideways instead of wrapping (the
   admin-nav pattern), so the stuck strip stays short; full basis puts the markdown switch on
   its own line under it. */
@media (max-width: 720px) {
  /* The card's side padding moves into the scroller: outside it, the 8px would clip a
     scrolled-past button before the border, and its 15% ghost with it. */
  .editor-mode-row .toolbar { flex: 1 1 100%; min-width: 0; padding-left: 0; padding-right: 0; }
  .editor-mode-row .toolbar__scroll { flex: 1; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; padding: 0 8px; }
  .editor-mode-row .toolbar__scroll::-webkit-scrollbar { display: none; }
  .editor-mode-row .toolbar button, .editor-mode-row .toolbar .sep { flex: none; }
  /* The admin-nav fade, on the scroller *inside* the card: kauke.js sets has-more/has-less
     while buttons are clipped past an edge, so the buttons dissolve toward the border that
     stays intact. The card is the viewport; the content is what moves. Under the dissolving
     buttons, a gradient toward --toolbar-fade (the pseudo-elements, inset past the border and
     below the scroller) fades the card's surface out too, so the hint reads at a glance. */
  .toolbar__scroll { --fade-l: 0px; --fade-r: 0px; position: relative; z-index: 1; }
  .toolbar__scroll.has-more { --fade-r: 56px; }
  .toolbar__scroll.has-less { --fade-l: 56px; }
  /* The mask bottoms out at 15%, not 0: a clipped button stays a readable hint of itself while
     the overlay behind it carries the full fade to --toolbar-fade. (color-mix, because the mask
     colour is only an alpha carrier and the colour-literal ban below the token block stands.) */
  .toolbar__scroll.has-more, .toolbar__scroll.has-less {
    -webkit-mask-image: linear-gradient(to right, color-mix(in srgb, black 15%, transparent), black var(--fade-l), black calc(100% - var(--fade-r)), color-mix(in srgb, black 15%, transparent));
    mask-image: linear-gradient(to right, color-mix(in srgb, black 15%, transparent), black var(--fade-l), black calc(100% - var(--fade-r)), color-mix(in srgb, black 15%, transparent));
  }
  .editor-mode-row .toolbar { position: relative; }
  .editor-mode-row .toolbar::before, .editor-mode-row .toolbar::after {
    content: ""; position: absolute; top: 1px; bottom: 1px; width: 56px; pointer-events: none; opacity: 0;
  }
  .editor-mode-row .toolbar::before { left: 1px; border-radius: calc(var(--r-md) - 1px) 0 0 calc(var(--r-md) - 1px); background: linear-gradient(to right, var(--toolbar-fade), transparent); }
  .editor-mode-row .toolbar::after { right: 1px; border-radius: 0 calc(var(--r-md) - 1px) calc(var(--r-md) - 1px) 0; background: linear-gradient(to left, var(--toolbar-fade), transparent); }
  .editor-mode-row .toolbar:has(> .toolbar__scroll.has-less)::before { opacity: 1; }
  .editor-mode-row .toolbar:has(> .toolbar__scroll.has-more)::after { opacity: 1; }
}
.editor-mode { margin: 0; margin-left: auto; }
.editor-mode__link { border: none; background: none; padding: 0; cursor: pointer; font: 500 13px var(--sans); color: var(--slate); text-decoration: underline; text-underline-offset: 2px; }
.editor-mode__link:hover { color: var(--ink); }
.toolbar button svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.toolbar button:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
.editor-body--rich { padding: 0; cursor: text; }
.editor-body--rich:focus-within { border-color: var(--red); box-shadow: 0 0 0 3px var(--focus); }
.editor-body--rich .ProseMirror { padding: 24px 26px; min-height: 260px; outline: none; white-space: pre-wrap; overflow-wrap: break-word; -webkit-font-variant-ligatures: none; font-variant-ligatures: none; }
.editor-body--rich .ProseMirror p { margin: 0 0 20px; }
/* The empty editor says what it is for, like the textarea's placeholder it replaced. */
.editor-body--rich .ProseMirror p.is-editor-empty:first-child::before { content: attr(data-placeholder); float: left; height: 0; pointer-events: none; color: var(--taupe-light); font-weight: 500; }
.editor-body--rich .ProseMirror > :last-child { margin-bottom: 0; }
.editor-body--rich .ProseMirror h1, .editor-body--rich .ProseMirror h2 { font: 600 25px/1.3 var(--serif); color: var(--ink); margin: 30px 0 12px; }
.editor-body--rich .ProseMirror h3 { font: 600 20px/1.35 var(--serif); color: var(--ink); margin: 26px 0 10px; }
.editor-body--rich .ProseMirror h4 { font: 600 16px/1.4 var(--sans); color: var(--ink); margin: 22px 0 8px; }
.editor-body--rich .ProseMirror > :first-child { margin-top: 0; }
.editor-body--rich .ProseMirror ul, .editor-body--rich .ProseMirror ol { margin: 0 0 20px; padding-left: 22px; }
.editor-body--rich .ProseMirror li { margin-bottom: 6px; }
.editor-body--rich .ProseMirror li > p { margin: 0 0 8px; }
.editor-body--rich .ProseMirror ul[data-type="taskList"] { list-style: none; padding-left: 2px; }
.editor-body--rich .ProseMirror ul[data-type="taskList"] li { display: flex; gap: 8px; align-items: baseline; }
.editor-body--rich .ProseMirror ul[data-type="taskList"] li > label { flex: none; }
.editor-body--rich .ProseMirror ul[data-type="taskList"] li > div { flex: 1; min-width: 0; }
.editor-body--rich .ProseMirror input[type="checkbox"] { accent-color: var(--red); margin: 0; }
.editor-body--rich .ProseMirror a { color: var(--red); font-weight: 600; text-decoration: underline; }
.editor-body--rich .ProseMirror strong { color: var(--ink); }
.editor-body--rich .ProseMirror s { color: var(--taupe); }
.editor-body--rich .ProseMirror blockquote { margin: 0 0 20px; padding: 4px 0 4px 18px; border-left: 3px solid var(--border-warm); color: var(--slate); font-style: italic; }
.editor-body--rich .ProseMirror hr { border: 0; border-top: 1px solid var(--border); margin: 30px 0; }
.editor-body--rich .ProseMirror code { font: 400 .87em/1.5 var(--mono); color: var(--ink); background: var(--panel); border-radius: var(--r-sm); padding: 2px 5px; }
.editor-body--rich .ProseMirror pre { margin: 0 0 20px; padding: 14px 16px; overflow-x: auto; background: var(--panel); border: 1px solid var(--border-warm); border-radius: var(--r-md); }
.editor-body--rich .ProseMirror pre code { font-size: 14px; background: none; padding: 0; }
.editor-body--rich .ProseMirror .ProseMirror-selectednode { outline: 2px solid var(--red); outline-offset: 2px; border-radius: var(--r-sm); }
.editor-body--rich .ProseMirror-gapcursor { display: none; pointer-events: none; position: relative; }
.editor-body--rich .ProseMirror-gapcursor::after { content: ''; display: block; position: absolute; top: -2px; width: 20px; border-top: 1px solid var(--ink); }
.editor-body--rich .ProseMirror-focused .ProseMirror-gapcursor { display: block; }
/* `![tekst](bilde:3)` in the editor: the thumbnail where the photo will be, the caption right under
   it and editable, the ordinal in the corner as on the thumbnail strip. */
.rich-photo { display: block; margin: 22px 0; position: relative; }
.rich-photo__frame { display: block; position: relative; height: 220px; border-radius: var(--r-md); overflow: hidden; background: repeating-linear-gradient(135deg, var(--ph-a) 0 11px, var(--ph-b) 11px 22px); }
.rich-photo__frame > img { display: block; width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
.rich-photo__n { position: absolute; top: 8px; left: 8px; min-width: 22px; height: 22px; border-radius: 50%; background: var(--scrim-strong); color: var(--on-dark); display: flex; align-items: center; justify-content: center; font: 600 11px var(--sans); pointer-events: none; }
.rich-photo__caption { display: block; padding: 9px 2px 0; min-height: 1.5em; font: 400 13.5px/1.5 var(--sans); color: var(--taupe); }
.rich-photo__caption:empty::before { content: attr(data-placeholder); color: var(--taupe-light); pointer-events: none; }
/* `![tekst](skjult "grunn")`: the reason as a chip, the folded text right after it, both in the
   flow of the paragraph the way the rendered veil is. */
.rich-veil { display: inline; background: var(--panel); border: 1px dashed var(--border-strong); border-radius: var(--r-sm); padding: 2px 8px; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
.rich-veil__reason { font: 600 13px var(--sans); color: var(--ink); margin-right: 6px; padding-right: 6px; border-right: 1px solid var(--border-strong); cursor: pointer; }
.rich-veil__reason:empty::before { content: '…'; }
.editor-pop { position: absolute; top: calc(100% + 6px); left: 0; z-index: 20; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); padding: 10px; box-shadow: var(--shadow-pop); width: min(420px, 100%); box-sizing: border-box; }
.editor-pop__label { display: block; font: 600 12.5px var(--sans); color: var(--slate); margin: 0 0 6px 2px; }
.editor-pop__row { display: flex; gap: 8px; align-items: center; }
.editor-pop__row .input { flex: 1; min-width: 0; padding: 8px 10px; font-size: 14px; }
.editor-pop__row .btn { padding: 9px 14px; font-size: 13px; }
/* The photo popover: the post's photos as a small grid, «Last opp nytt bilde» beside them. */
.editor-pop--photo { width: min(460px, 100%); }
.editor-pop__thumbs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.editor-pop__thumbs:empty { display: none; }
.editor-pop__thumb { position: relative; width: 64px; height: 64px; padding: 0; border: 2px solid transparent; border-radius: var(--r-sm); overflow: hidden; cursor: pointer; background: repeating-linear-gradient(135deg, var(--ph-a) 0 8px, var(--ph-b) 8px 16px); }
.editor-pop__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.editor-pop__thumb > span { position: absolute; top: 4px; left: 4px; min-width: 17px; height: 17px; border-radius: 50%; background: var(--scrim-strong); color: var(--on-dark); display: flex; align-items: center; justify-content: center; font: 600 10px var(--sans); pointer-events: none; }
.editor-pop__thumb.is-selected { border-color: var(--red); }
.editor-note { margin: 12px 0 0; padding: 10px 14px; border-radius: var(--r-sm); background: var(--amber-soft); border: 1px solid var(--amber-border); color: var(--amber-text); font: 400 13.5px/1.55 var(--sans); }
