/* updn design package 1.0.10 — generated by design/build.py, do not edit.
   Source: updn-web/static/site.css and updn-web/design/app.css. */
/* updn-app.css — the application layer: the same paper, now with controls on it.
   Loaded AFTER site.css, which owns the tokens, the reset, the serif body, links, :focus-visible,
   colour-scheme, reduced motion and print. Nothing here re-declares any of that.

   The two Flask apps keep writing Bootstrap's class names — `btn btn-outline-secondary btn-sm`,
   `mt-2 alert alert-info`, `col-md-4` — from ~20 runtime JavaScript sites we do not want to rewrite.
   So this file defines that vocabulary again, in the book's language. It is a reimplementation,
   not a renaming: the names survive, the look does not. Bootstrap's drop shadows become hairlines,
   its six greys become one muted ink, its radius becomes the book's single 2 px, and its eight
   semantic colours become the four box identities the PDF already owns.

   HOW THIS FILE IS BUILT — read this before adding the 208th class.

   1. THE SCALES. Three of them, and nothing outside them: --rom-0..5 (space), the control size slot
      (--kontroll-h / -pad-y / -pad-x / -fs), and --vekt-* (weight). No component invents a number.
      If you need a value that is not on a scale, the honest move is to question the component,
      not to add a magic number.

   2. THE ROLE TRIO. Every tinted thing — alert, badge, card accent, status card, callout — reads
      --rolle-bg / --rolle-kant / --rolle-tekst and paints itself from them. A variant class sets
      those three and nothing else. That is why `.alert-info`, `.bg-info`, `.text-bg-info` and
      `.status-processing` are one line each and are guaranteed to agree with one another.
      A new role is: three properties, one selector list. A new tinted component is: read the trio.

   3. THE SIZE SLOT. Every control (button, field, select, input-group affix, nav link) declares the
      four --kontroll-* properties on itself and then reads them. A size modifier (`.btn-sm`,
      `.form-control-lg`) only re-sets the slot; it never touches padding or font-size directly.
      Custom properties resolve at computed-value time, so the modifier works regardless of the
      order the classes appear in the markup.

   4. THE GRID IS ONE FORMULA. `.col-md-4` sets `--kol: 4`. One rule turns any --kol into the exact
      width that makes N columns plus their gaps sum to 100 %. Twelve one-line facts per breakpoint,
      one piece of arithmetic, no negative margins and no gutter padding.

   Everything below is either a scale, a role, a component reading those, or a utility naming one. */


/* ---- 1. registered properties -------------------------------------------------------------------
   The role trio and --kol are registered with `inherits: false`. Without that, an `.alert-danger`
   would hand its red down to every card, badge and button nested inside it, and a `.col-md-4` would
   hand its width arithmetic to the nested row inside it — the classic "why is this panel pink"
   bug. `syntax: "*"` lets us omit an initial-value, so an unset property stays guaranteed-invalid
   and `var(--rolle-bg, <fallback>)` falls through to the component's own default. That default can
   therefore itself be a token, which is what keeps dark mode working.
   The size slot is registered for the same reason: a `.btn-sm` must not shrink the text of a field
   that happens to sit inside it. */
@property --rolle-bg     { syntax: "*"; inherits: false; }
@property --rolle-kant   { syntax: "*"; inherits: false; }
@property --rolle-tekst  { syntax: "*"; inherits: false; }
@property --kol          { syntax: "*"; inherits: false; }
@property --kontroll-h     { syntax: "*"; inherits: false; }
@property --kontroll-pad-y { syntax: "*"; inherits: false; }
@property --kontroll-pad-x { syntax: "*"; inherits: false; }
@property --kontroll-fs    { syntax: "*"; inherits: false; }


/* ---- 2. token extension -------------------------------------------------------------------------
   Three colours the book did not need on paper but a form does, each with a dark counterpart in the
   block at the foot of this file. A colour token without one is the bug, so they are added in pairs
   and nowhere else. Measured against --papir: --dempet-svak 5.1:1, --ib-kant 5.2:1. */
:root {
  --dempet-svak: #6e6757;  /* third ink tier: table captions, disabled text, .text-body-tertiary.
                              Lighter than --dempet (6.7:1) but still over 4.5:1 on both papers. */
  --ib-kant: #7a6410;      /* the amber notice had a background and a text colour but no rule;
                              the warning role needs one to sit beside the other three. */
  --slor: rgba(30, 27, 23, .45);  /* ::backdrop scrim: warm, because a neutral grey veil over warm
                                     paper reads as a dirty screen rather than a dimmed page. */

  /* the space scale. Bootstrap's ratios kept deliberately: `mb-3` appears 50 times and means
     "one paragraph", and site.css already sets `p { margin-bottom: 1rem }`. Changing the ratios
     would silently re-space 138 existing `mb-*` sites. */
  --rom-0: 0;
  --rom-1: .25rem;
  --rom-2: .5rem;
  --rom-3: 1rem;     /* one paragraph */
  --rom-4: 1.5rem;   /* one section break */
  --rom-5: 3rem;     /* one page break */

  /* the type scale, taken from the book's own headings rather than Bootstrap's, so `fs-3` in an app
     template and `h2` in a chapter are the same size. */
  --fs-1: 2.15rem;  --fs-2: 1.6rem;   --fs-3: 1.32rem;
  --fs-4: 1.15rem;  --fs-5: 1.08rem;  --fs-6: 1rem;
  --fs-liten: .875em;   /* relative, so it shrinks with whatever it sits in */

  /* the weight scale. The text face is variable 400–700; site.css uses 500 and 600 for headings. */
  --vekt-normal: 400; --vekt-medium: 500; --vekt-semi: 600; --vekt-fet: 700;

  --radius: 2px;        /* the book's only radius, and there is no second one */
  --grid-gutter: var(--rom-4);
  --g-x: var(--grid-gutter);   /* declared at the root as well as on .row, so a stray col outside a
                                  row still has a length to subtract and never computes to garbage */
  --g-y: var(--grid-gutter);
}


/* ---- 3. the roles -------------------------------------------------------------------------------
   Five identities, four of them straight from the PDF. Each is a background, a rule and an ink, and
   the ink is always --blekk because every tint in the token block was chosen to carry body text
   (14:1 in light, 11:1 in dark). --rboks-kant is NOT used as text anywhere: it measures 4.2:1 on
   paper and would fail AA. The neutral role uses --dempet for secondary text instead.

   The long selector lists are the point. `bg-success`, `text-bg-success`, `alert-success` and
   `status-done` are four names the templates already use for one idea, and binding them here means
   they can never drift apart. To add a role: copy one line. */
.rolle-info,    .alert-info,    .bg-info,    .text-bg-info,    .badge.bg-info,
.border-info,   .status-processing {
  --rolle-bg: var(--eksempel-bg); --rolle-kant: var(--eksempel-kant); --rolle-tekst: var(--blekk);
}
.rolle-success, .alert-success, .bg-success, .text-bg-success, .badge.bg-success,
.border-success, .status-done {
  --rolle-bg: var(--oppgaver-bg); --rolle-kant: var(--oppgaver-kant); --rolle-tekst: var(--blekk);
}
.rolle-danger,  .alert-danger,  .bg-danger,  .text-bg-danger,  .badge.bg-danger,
.border-danger, .status-failed {
  --rolle-bg: var(--viktig-bg); --rolle-kant: var(--viktig-kant); --rolle-tekst: var(--blekk);
}
.rolle-warning, .alert-warning, .bg-warning, .text-bg-warning, .badge.bg-warning,
.border-warning {
  --rolle-bg: var(--ib-bg); --rolle-kant: var(--ib-kant); --rolle-tekst: var(--ib-tekst);
}
/* Neutral. `bg-dark` and `bg-secondary` land here on purpose: the book owns exactly two surfaces,
   paper and deep paper, and no black panel. A "dark" badge is a quiet badge, not an inverted one. */
.rolle-neutral, .alert-secondary, .alert-light, .alert-dark,
.bg-secondary, .bg-dark, .bg-light, .bg-body-tertiary,
.text-bg-secondary, .text-bg-light, .text-bg-dark, .border-secondary, .status-queued {
  --rolle-bg: var(--rboks-bg); --rolle-kant: var(--rboks-kant); --rolle-tekst: var(--blekk);
}
/* `bg-primary` is the one accent. It is a fill, not a tint, so its ink is paper. */
.rolle-primary, .alert-primary, .bg-primary, .text-bg-primary, .border-primary {
  --rolle-bg: var(--lenke); --rolle-kant: var(--lenke); --rolle-tekst: var(--papir);
}
.bg-white, .bg-body { --rolle-bg: var(--papir); --rolle-kant: var(--regel); --rolle-tekst: var(--blekk); }

/* The utility form of a role: paint the background only. The tinted panels (alert, card, badge) do
   their own painting further down, because they also want the rule and the ink. */
.bg-primary, .bg-secondary, .bg-success, .bg-danger, .bg-warning, .bg-info,
.bg-light, .bg-dark, .bg-white, .bg-body, .bg-body-tertiary {
  background-color: var(--rolle-bg);
  color: var(--rolle-tekst);
}
/* Our tints ARE the 10 % version of the colour already; there is no louder tier to fade down from.
   So `bg-opacity-10` mixes the rule colour into paper at the stated strength, which is the only
   reading of the class that means anything here — and it inverts, because both ends are tokens. */
.bg-opacity-10 { background-color: color-mix(in srgb, var(--rolle-kant, var(--regel)) 10%, var(--papir)); }
.bg-opacity-25 { background-color: color-mix(in srgb, var(--rolle-kant, var(--regel)) 25%, var(--papir)); }
.border-opacity-25 { border-color: color-mix(in srgb, var(--rolle-kant, var(--regel)) 25%, var(--papir)); }
.border-opacity-50 { border-color: color-mix(in srgb, var(--rolle-kant, var(--regel)) 50%, var(--papir)); }


/* ---- 4. layout: container and the one-formula grid ---------------------------------------------- */
/* The app container, and the bar above it, both measure `--bred + 2.5rem`.
   Two things were wrong before. site.css gives nav.hovednav the PROSE measure by default and only
   widens it for body.kurs-side/hub-side/oppslag-side — classes the apps do not carry — so an app's
   navbar sat at 44rem above 76rem of content: narrower than its own page, and narrower than the
   same bar on updn.no. And `--bred` alone is not the book's content box: main.hub is
   `--bred + 2.5rem` wide with a 1.25rem gutter each side, so matching it needs the same sum.
   An app is lists, tables and forms — never running prose — so the wide measure is the right one
   everywhere here. */
nav.hovednav,
.container, .container-fluid {
  inline-size: 100%; max-inline-size: calc(var(--bred) + 2.5rem); margin-inline: auto;
  padding-inline: 1.25rem;   /* matches site.css's own `main` gutter, so app and book pages align */
}
/* site.css's own padding shorthand on the bar would otherwise be reset by the padding-inline above. */
nav.hovednav { padding-block: .85rem .75rem; }
.container-fluid { max-inline-size: none; }

/* .row is a wrapping flex line with a real `gap`. Bootstrap uses negative margins and per-column
   padding because it predates `gap`; we do not have to. The consequence is that `justify-content-*`
   and `align-items-*` on a .row behave exactly as the nine login/register templates already assume. */
.row {
  display: flex; flex-wrap: wrap;
  --g-x: var(--grid-gutter); --g-y: var(--grid-gutter);
  gap: var(--g-y) var(--g-x);
}

/* THE FORMULA. A column spanning N of 12 must be N/12 of the space that is left after the gaps, plus
   the gaps it swallows internally:  N/12 · 100 % − (1 − N/12) · gap.  Check it: three col-4s come to
   100 % − 2·gap, and the two gaps between them put it back to exactly 100 %. N = 12 gives 100 % − 0.
   Unset --kol falls back to 12, which is why `col-md-4` needs no separate mobile rule: below md it
   simply never sets --kol, and a full-width column is the mobile-first default. */
.col, [class*="col-"] {
  flex: 0 0 auto;
  inline-size: calc(var(--kol, 12) / 12 * 100% - (1 - var(--kol, 12) / 12) * var(--g-x));
  min-inline-size: 0;   /* a long unbroken token (a share URL, a Sqid) must not widen its column */
}
/* Anything dropped into a row without a col class takes the full line. `[class*="col-"]` is safe
   here: `collapse`, `flex-column` and `text-columns` contain "col" but not "col-".
   The :where() is load-bearing. `:not()` takes the specificity of its most specific argument, so a
   bare `:not(.col, [class*="col-"])` would make this (0,2,0) and beat any `.w-25`/`.w-50`/`.w-75`
   on the same element — forcing it back to full width. `:where()` contributes zero, leaving the
   whole selector at the one class of `.row`, which is what a utility must be able to override. */
.row > :not(:where(.col, [class*="col-"])) { inline-size: 100%; }
.col, .col-auto { flex: 1 1 0; inline-size: auto; }
.col-auto { flex: 0 0 auto; }

.col-1{--kol:1} .col-2{--kol:2} .col-3{--kol:3} .col-4{--kol:4} .col-5{--kol:5} .col-6{--kol:6} .col-7{--kol:7} .col-8{--kol:8} .col-9{--kol:9} .col-10{--kol:10} .col-11{--kol:11} .col-12{--kol:12}
@media (min-width: 576px) {
  .col-sm-1{--kol:1} .col-sm-2{--kol:2} .col-sm-3{--kol:3} .col-sm-4{--kol:4} .col-sm-5{--kol:5} .col-sm-6{--kol:6} .col-sm-7{--kol:7} .col-sm-8{--kol:8} .col-sm-9{--kol:9} .col-sm-10{--kol:10} .col-sm-11{--kol:11} .col-sm-12{--kol:12}
  .col-sm, .col-sm-auto { inline-size: auto; } .col-sm { flex: 1 1 0; } .col-sm-auto { flex: 0 0 auto; }
}
@media (min-width: 768px) {
  .col-md-1{--kol:1} .col-md-2{--kol:2} .col-md-3{--kol:3} .col-md-4{--kol:4} .col-md-5{--kol:5} .col-md-6{--kol:6} .col-md-7{--kol:7} .col-md-8{--kol:8} .col-md-9{--kol:9} .col-md-10{--kol:10} .col-md-11{--kol:11} .col-md-12{--kol:12}
  .col-md, .col-md-auto { inline-size: auto; } .col-md { flex: 1 1 0; } .col-md-auto { flex: 0 0 auto; }
}
@media (min-width: 992px) {
  .col-lg-1{--kol:1} .col-lg-2{--kol:2} .col-lg-3{--kol:3} .col-lg-4{--kol:4} .col-lg-5{--kol:5} .col-lg-6{--kol:6} .col-lg-7{--kol:7} .col-lg-8{--kol:8} .col-lg-9{--kol:9} .col-lg-10{--kol:10} .col-lg-11{--kol:11} .col-lg-12{--kol:12}
  .col-lg, .col-lg-auto { inline-size: auto; } .col-lg { flex: 1 1 0; } .col-lg-auto { flex: 0 0 auto; }
}
@media (min-width: 1200px) {
  .col-xl-1{--kol:1} .col-xl-2{--kol:2} .col-xl-3{--kol:3} .col-xl-4{--kol:4} .col-xl-5{--kol:5} .col-xl-6{--kol:6} .col-xl-7{--kol:7} .col-xl-8{--kol:8} .col-xl-9{--kol:9} .col-xl-10{--kol:10} .col-xl-11{--kol:11} .col-xl-12{--kol:12}
  .col-xl, .col-xl-auto { inline-size: auto; } .col-xl { flex: 1 1 0; } .col-xl-auto { flex: 0 0 auto; }
}
/* The gutter utilities feed the same formula, so g-2 narrows the columns by exactly as much as it
   narrows the gaps and the row still sums to 100 %. */
.g-0{--g-x:var(--rom-0);--g-y:var(--rom-0)} .g-1{--g-x:var(--rom-1);--g-y:var(--rom-1)} .g-2{--g-x:var(--rom-2);--g-y:var(--rom-2)} .g-3{--g-x:var(--rom-3);--g-y:var(--rom-3)} .g-4{--g-x:var(--rom-4);--g-y:var(--rom-4)} .g-5{--g-x:var(--rom-5);--g-y:var(--rom-5)}
.gx-0{--g-x:var(--rom-0)} .gx-1{--g-x:var(--rom-1)} .gx-2{--g-x:var(--rom-2)} .gx-3{--g-x:var(--rom-3)} .gx-4{--g-x:var(--rom-4)} .gx-5{--g-x:var(--rom-5)}
.gy-0{--g-y:var(--rom-0)} .gy-1{--g-y:var(--rom-1)} .gy-2{--g-y:var(--rom-2)} .gy-3{--g-y:var(--rom-3)} .gy-4{--g-y:var(--rom-4)} .gy-5{--g-y:var(--rom-5)}


/* ---- 5. chrome: navbar, breadcrumb ---------------------------------------------------------------
   Bootstrap's `navbar navbar-dark bg-dark` is a black slab. The book's own header is paper with a
   hairline under it and the page links set in small letterspaced capitals (site.css, header.topp /
   nav.hovednav). This is that header, wearing Bootstrap's class names. `navbar-dark` is therefore a
   deliberate no-op: there is no dark bar to opt into. */
.navbar {
  background: var(--papir); border-block-end: 1px solid var(--regel);
  display: flex; flex-wrap: wrap; align-items: center;
  padding-block: .55rem; padding-inline: 0;
}
.navbar > .container, .navbar > .container-fluid {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--rom-2) var(--rom-3);
}
/* Not a no-op by omission but by statement: `navbar-dark` asks for the inverted bar Bootstrap had,
   and the answer is that there isn't one. Stated explicitly so a future reader greps `navbar-dark`,
   finds this, and does not conclude the class was simply forgotten. */
.navbar.navbar-dark, .navbar.bg-dark { background: var(--papir); color: var(--blekk); }
.navbar-brand {
  color: var(--blekk); text-decoration: none; font-weight: var(--vekt-medium);
  font-size: 1.05rem; letter-spacing: .02em; margin-inline-end: var(--rom-3);
  display: inline-flex; align-items: center; min-block-size: 2.5rem;
}
.navbar-brand:hover { color: var(--lenke); }
.navbar-nav { display: flex; flex-wrap: wrap; align-items: center; gap: .2rem 1.1rem; list-style: none; margin: 0; padding: 0; }
.navbar-nav .nav-item { margin: 0; }
/* Same treatment as nav.hovednav: capitals, quiet ink, the underline appearing on hover rather than
   a background swatch. Small caps do not exist in the text face, so letterspaced capitals stand in. */
.navbar .nav-link {
  color: var(--dempet); text-decoration: none; font-size: .68rem;
  letter-spacing: .09em; text-transform: uppercase; font-weight: var(--vekt-medium);
  border-block-end: 1px solid transparent;
}
.navbar .nav-link:hover, .navbar .nav-link[aria-current], .navbar .nav-link.active {
  color: var(--blekk); border-block-end-color: var(--blekk);
}
/* The toggler: three ink rules, drawn from borders. No url(), not even a data: one — the CSP is
   being tightened to 'self' and an inlined SVG would need img-src data: to survive it. */
.navbar-toggler {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 2.5rem; block-size: 2.5rem; margin-inline-start: auto;
  background: var(--papir); border: 1px solid var(--regel); border-radius: var(--radius);
  color: var(--blekk); cursor: pointer; padding: 0;
}
.navbar-toggler-icon {
  position: relative; display: block; inline-size: 1.1rem; block-size: 1px;
  background: currentColor;
}
.navbar-toggler-icon::before, .navbar-toggler-icon::after {
  content: ""; position: absolute; inset-inline: 0; block-size: 1px; background: currentColor;
}
.navbar-toggler-icon::before { inset-block-start: -.34rem; }
.navbar-toggler-icon::after  { inset-block-start:  .34rem; }
.navbar-collapse { flex-basis: 100%; }
/* Below the expand breakpoint the menu is a disclosure; at and above it, it is simply the bar. The
   rule that opens it lives with the other .collapse rules further down, because it is an exception
   to them and would otherwise be overruled by source order — there is no !important to fall back on. */
@media (min-width: 992px) {
  .navbar-expand-lg .navbar-toggler { display: none; }
}

.breadcrumb {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .5rem;
  list-style: none; margin: 0 0 var(--rom-3); padding: 0;
  font-size: .8rem; color: var(--dempet);
}
.breadcrumb-item { display: inline-flex; align-items: baseline; gap: .5rem; }
/* A thin slash rather than Bootstrap's chevron: the separator is punctuation, not an icon. */
.breadcrumb-item + .breadcrumb-item::before { content: "/"; color: var(--regel-sterk); }
/* .8 rem type with no padding is a ~14 px tall target, across 12 breadcrumb links. Padding the
   block axis grows the hit area without moving the text or opening the row up. */
.breadcrumb-item a {
  color: var(--dempet); text-decoration-color: var(--regel-sterk);
  display: inline-block; padding-block: .55rem; margin-block: -.55rem;
}
.breadcrumb-item a:hover { color: var(--blekk); }
.breadcrumb-item.active, .breadcrumb-item[aria-current] { color: var(--blekk); }

.sticky-top { position: sticky; inset-block-start: 0; z-index: 10; }


/* ---- 6. buttons ---------------------------------------------------------------------------------
   One component, one size slot, one role trio. `.btn` declares its defaults — quiet paper with a
   hairline, which is what 29 `btn-outline-secondary` sites want — and every variant below re-sets
   only --btn-bg / --btn-tekst / --btn-kant.
   The minimum block size is 2.5 rem on every variant including `btn-sm`: a small button is small in
   type and in horizontal padding, never in target. 61 `btn-sm` sites sit in dense teacher tables and
   are the ones most likely to be tapped on a phone at the front of a lecture hall. */
.btn {
  --kontroll-h: 2.5rem; --kontroll-pad-y: .45rem; --kontroll-pad-x: .95rem; --kontroll-fs: .92rem;
  --btn-bg: var(--papir); --btn-tekst: var(--blekk); --btn-kant: var(--regel-sterk);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--rom-2);
  min-block-size: var(--kontroll-h);
  padding: var(--kontroll-pad-y) var(--kontroll-pad-x);
  font-family: inherit; font-size: var(--kontroll-fs); font-weight: var(--vekt-medium);
  line-height: 1.25; text-align: center; text-decoration: none; white-space: nowrap;
  background: var(--btn-bg); color: var(--btn-tekst);
  border: 1px solid var(--btn-kant); border-radius: var(--radius);
  cursor: pointer; user-select: none;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
/* Hover deepens the paper and darkens the rule; it never moves the button or drops a shadow. */
.btn:hover { background: var(--papir-dyp); border-color: var(--blekk); color: var(--btn-tekst); }
.btn:active { background: var(--regel); }
.btn:disabled, .btn.disabled, .btn[aria-disabled="true"] {
  color: var(--dempet-svak); background: var(--papir-dyp); border-color: var(--regel);
  cursor: not-allowed; opacity: 1;   /* opacity would drag the text under 4.5:1; a real colour will not */
}

/* Filled variants: the role colour as the surface, paper as the ink. Every pair measures over 5.9:1
   in light and over 8:1 in dark, and every one is a token, so the whole set inverts at lights-out. */
.btn-primary { --btn-bg: var(--lenke);          --btn-tekst: var(--papir); --btn-kant: var(--lenke); }
.btn-danger  { --btn-bg: var(--viktig-kant);    --btn-tekst: var(--papir); --btn-kant: var(--viktig-kant); }
.btn-success { --btn-bg: var(--oppgaver-kant);  --btn-tekst: var(--papir); --btn-kant: var(--oppgaver-kant); }
.btn-info    { --btn-bg: var(--eksempel-kant);  --btn-tekst: var(--papir); --btn-kant: var(--eksempel-kant); }
.btn-warning { --btn-bg: var(--ib-bg);          --btn-tekst: var(--ib-tekst); --btn-kant: var(--ib-kant); }
.btn-dark    { --btn-bg: var(--blekk);          --btn-tekst: var(--papir); --btn-kant: var(--blekk); }
.btn-secondary, .btn-light { --btn-bg: var(--papir-dyp); --btn-tekst: var(--blekk); --btn-kant: var(--regel-sterk); }
/* A heading dropped into a flex row of controls — "Emnenavn  [Rediger] [Slett]".
   site.css gives headings a generous TOP margin (h2 is 2.8rem) because in running prose a heading
   needs air above it. Inside `align-items: center` the flex algorithm centres MARGIN boxes, not
   text boxes, so that 2.8rem lifts the buttons clear of the heading's own baseline and the row
   looks broken. `mb-0`, the obvious reach, only touches the other end.
   Zeroing the block margins for a heading that is a DIRECT flex child fixes it everywhere at
   once: in an app a heading in a control row is positioned by the row, never by prose spacing. */
.d-flex > h1, .d-flex > h2, .d-flex > h3,
.d-flex > h4, .d-flex > h5, .d-flex > h6 { margin-block: 0; }

/* A link that is a button must win against whatever is colouring links around it.
   `.btn` alone is one class (0,1,0), and site.css's `nav.hovednav a` is a class plus an element
   (0,1,1) — so `<a class="btn btn-primary">Registrer deg</a>` in the navbar came out with
   --dempet ink on the ink-blue fill: 1.20:1, measured on the live page, i.e. invisible. Matching
   that specificity with the element selector fixes it here and anywhere else a container decides
   how its links look, which is the general form of the same bug.
   The border-bottom reset goes with it: the same navbar rule underlines links on hover, and a
   button is not a link even when it is one. */
a.btn, button.btn { color: var(--btn-tekst); }
/* The navbar link rule also sets `padding-bottom: .05rem` (to sit the hover underline close to the
   text). At (0,1,1) that beats .btn's own `padding: var(--kontroll-pad-y) …`, leaving the button
   with 5.4px above its label and 0.9px below — the label rode 2px low in its own box. Restore the
   symmetric padding along with the colour and the border. */
nav.hovednav a.btn, nav.hovednav a.btn:hover {
  border-bottom: 0;
  color: var(--btn-tekst);
  padding: var(--kontroll-pad-y) var(--kontroll-pad-x);
}

/* A filled button must not go pale on hover — it goes one step deeper into its own colour. */
.btn-primary:hover, .btn-danger:hover, .btn-success:hover, .btn-info:hover, .btn-dark:hover {
  background: color-mix(in srgb, var(--btn-bg) 82%, var(--blekk));
  border-color: color-mix(in srgb, var(--btn-bg) 82%, var(--blekk));
  color: var(--btn-tekst);
}
.btn-warning:hover, .btn-secondary:hover, .btn-light:hover {
  background: color-mix(in srgb, var(--btn-bg) 88%, var(--blekk)); border-color: var(--blekk);
}

/* Outline variants: paper, a hairline, and the role colour carried by the text only. This is the
   book's default register — 29 `btn-outline-secondary` and 22 `btn-outline-danger` sites mean most
   buttons in these apps are quiet ones, which is right for a page made of prose. */
.btn-outline-primary   { --btn-tekst: var(--lenke);          --btn-kant: var(--lenke); }
.btn-outline-danger    { --btn-tekst: var(--viktig-kant);    --btn-kant: var(--viktig-kant); }
.btn-outline-success   { --btn-tekst: var(--oppgaver-kant);  --btn-kant: var(--oppgaver-kant); }
.btn-outline-info      { --btn-tekst: var(--eksempel-kant);  --btn-kant: var(--eksempel-kant); }
.btn-outline-warning   { --btn-tekst: var(--ib-tekst);       --btn-kant: var(--ib-kant); }
.btn-outline-dark      { --btn-tekst: var(--blekk);          --btn-kant: var(--blekk); }
.btn-outline-secondary { --btn-tekst: var(--dempet);         --btn-kant: var(--regel-sterk); }
/* `btn-outline-light` exists only because Bootstrap needed a button for a black bar. Our bar is
   paper, so the quietest thing on it is the secondary treatment, and that is what this becomes. */
.btn-outline-light     { --btn-tekst: var(--dempet);         --btn-kant: var(--regel); }
/* On hover the outline fills with the faintest wash of its own colour, so the identity is confirmed
   rather than replaced, and the text colour — the thing carrying the meaning — never moves. */
.btn-outline-primary:hover, .btn-outline-danger:hover, .btn-outline-success:hover,
.btn-outline-info:hover, .btn-outline-warning:hover, .btn-outline-dark:hover,
.btn-outline-secondary:hover, .btn-outline-light:hover {
  background: color-mix(in srgb, var(--btn-kant) 12%, var(--papir));
  border-color: var(--btn-kant); color: var(--btn-tekst);
}

/* A button that is really a link. Underlined, because colour alone is not a link cue. */
.btn-link {
  --btn-bg: transparent; --btn-tekst: var(--lenke); --btn-kant: transparent;
  text-decoration: underline; text-decoration-color: var(--regel-sterk);
  text-underline-offset: .16em; text-decoration-thickness: 1px;
}
.btn-link:hover { background: transparent; border-color: transparent; text-decoration-color: currentColor; }

/* Size modifiers touch the slot and nothing else. */
.btn-sm { --kontroll-pad-y: .3rem; --kontroll-pad-x: .6rem; --kontroll-fs: .78rem; }
.btn-lg { --kontroll-h: 2.9rem; --kontroll-pad-y: .6rem; --kontroll-pad-x: 1.35rem; --kontroll-fs: 1.02rem; }

/* A group reads as one object: shared rules collapse, only the outer corners keep the radius. */
.btn-group { display: inline-flex; flex-wrap: wrap; }
.btn-group > .btn { border-radius: 0; }
.btn-group > .btn:not(:first-child) { margin-inline-start: -1px; }
.btn-group > .btn:first-child { border-start-start-radius: var(--radius); border-end-start-radius: var(--radius); }
.btn-group > .btn:last-child  { border-start-end-radius: var(--radius);  border-end-end-radius: var(--radius); }
.btn-group > .btn:hover, .btn-group > .btn:focus-visible { z-index: 1; }   /* so the live rule wins the overlap */

/* The dismiss control. A typographic multiplication sign, not an icon file — again, no url(). */
.btn-close {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 2.5rem; block-size: 2.5rem; flex: 0 0 auto; padding: 0;
  background: transparent; border: 1px solid transparent; border-radius: var(--radius);
  color: var(--dempet); cursor: pointer; line-height: 1;
}
.btn-close::before { content: "\00d7"; font-size: 1.3rem; }
.btn-close:hover { color: var(--blekk); background: var(--papir-dyp); }
/* The small close sits in a chip inside a row of chips; it keeps a 2.5 rem target by overflowing its
   own box with negative margin rather than by actually shrinking the hit area. */
.btn-close-sm { inline-size: 1.4rem; block-size: 1.4rem; margin: -.55rem; padding: .55rem; box-sizing: content-box; }
.btn-close-sm::before { font-size: 1rem; }


/* ---- 7. forms -----------------------------------------------------------------------------------
   The field is the navbar search field from site.css, grown to a form's proportions: paper inside a
   hairline, the rule going to ink on focus. site.css's global :focus-visible ring sits outside it. */
.form-label {
  display: block; margin-block-end: var(--rom-1);
  font-size: .78rem; letter-spacing: .06em; text-transform: uppercase;
  font-weight: var(--vekt-medium); color: var(--dempet);
}
.form-control, .form-select {
  --kontroll-h: 2.5rem; --kontroll-pad-y: .45rem; --kontroll-pad-x: .65rem; --kontroll-fs: .95rem;
  display: block; inline-size: 100%; min-block-size: var(--kontroll-h);
  padding: var(--kontroll-pad-y) var(--kontroll-pad-x);
  font-family: inherit; font-size: var(--kontroll-fs); line-height: 1.5; color: var(--blekk);
  background: var(--papir); border: 1px solid var(--regel-sterk); border-radius: var(--radius);
  transition: border-color .15s ease;
}
.form-control::placeholder { color: var(--dempet); opacity: 1; }   /* Firefox dims placeholders by default */
.form-control:hover, .form-select:hover { border-color: var(--dempet); }
.form-control:focus, .form-select:focus { border-color: var(--blekk); }
/* background-COLOR, not the shorthand: the shorthand would wipe the select's drawn caret below. */
.form-control:disabled, .form-select:disabled, .form-control[readonly] {
  background-color: var(--papir-dyp); color: var(--dempet); border-color: var(--regel);
}
textarea.form-control { min-block-size: 6rem; line-height: 1.6; resize: vertical; }
.form-control-sm { --kontroll-pad-x: .5rem; --kontroll-fs: .82rem; }
.form-control-lg { --kontroll-h: 2.9rem; --kontroll-pad-x: .8rem; --kontroll-fs: 1.05rem; }

/* The select caret is two gradient half-squares forming a chevron. It uses currentColor, so it
   follows the ink into dark mode, and it is not a file, so it cannot be blocked by img-src. */
.form-select {
  appearance: none; -webkit-appearance: none;
  padding-inline-end: 2rem;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 1.05rem center, right .75rem center;
  background-size: .3rem .3rem, .3rem .3rem;
  background-repeat: no-repeat;
}

/* Checkboxes and radios stay native. The browser already knows how to draw, focus and announce one;
   all it needs from us is which blue to use, and `accent-color` inherits the colour scheme for free.
   The 2.5 rem target lives on the row, where the label — which is also the hit area — sits. */
.form-check {
  display: flex; align-items: center; gap: var(--rom-2);
  min-block-size: 2.5rem; padding-block: var(--rom-1);
}
/* These also appear loose in table cells, where the 2.5 rem row above them is not there to help, so
   the box itself has to clear WCAG 2.2's 24 px minimum. 1.4 rem does that at the desktop root
   (site.css:74 sets html to 112.5% = 18 px, giving 25.2 px) but NOT on a phone, where site.css:478
   drops the root to 106.25% = 17 px and 1.4 rem is only 23.8 px. An explicit floor in px is the
   honest way to state a requirement that is itself in px. */
.form-check-input {
  accent-color: var(--lenke);
  inline-size: 1.4rem; block-size: 1.4rem;
  min-inline-size: 24px; min-block-size: 24px;
  flex: 0 0 auto; margin: 0; cursor: pointer;
}
.form-check-label { cursor: pointer; margin: 0; }
.form-text { display: block; margin-block-start: var(--rom-1); font-size: .8rem; color: var(--dempet); font-style: italic; }
/* An error needs a second cue beside its colour, so it opens with a mark the eye finds in a column
   of fields. `invalid-feedback` is hidden until the field is marked invalid, as Bootstrap's was. */
.invalid-feedback {
  display: none; margin-block-start: var(--rom-1); font-size: .82rem; color: var(--viktig-kant);
}
.invalid-feedback::before { content: "\2715\00a0"; }
.is-invalid ~ .invalid-feedback, .was-validated :invalid ~ .invalid-feedback,
.invalid-feedback.d-block { display: block; }
.form-control.is-invalid, .form-select.is-invalid { border-color: var(--viktig-kant); }

/* An input group is one object made of parts: the rules between them collapse to a single hairline
   and only the ends are rounded, the same trick as .btn-group. */
.input-group { display: flex; flex-wrap: nowrap; align-items: stretch; }
.input-group > * { border-radius: 0; }
.input-group > :first-child { border-start-start-radius: var(--radius); border-end-start-radius: var(--radius); }
.input-group > :last-child  { border-start-end-radius: var(--radius);  border-end-end-radius: var(--radius); }
.input-group > :not(:first-child) { margin-inline-start: -1px; }
.input-group > .form-control, .input-group > .form-select { flex: 1 1 auto; inline-size: 1%; min-inline-size: 0; }
.input-group > .form-control:focus, .input-group > .btn:focus-visible { z-index: 1; }
.input-group-text {
  --kontroll-pad-y: .45rem; --kontroll-pad-x: .65rem; --kontroll-fs: .88rem;
  display: flex; align-items: center; min-block-size: 2.5rem;
  padding: var(--kontroll-pad-y) var(--kontroll-pad-x);
  font-size: var(--kontroll-fs); color: var(--dempet); white-space: nowrap;
  background: var(--papir-dyp); border: 1px solid var(--regel-sterk);
}


/* ---- 8. panels: card, alert, list group ---------------------------------------------------------
   `card` is `.boks` from site.css with Bootstrap's name on it: a hairlined panel on paper, no
   shadow, no lift, 2 px corners. `shadow` and `shadow-sm` therefore have nothing left to do here,
   and are defined further down as what a shadow was always standing in for — an edge. */
.card {
  display: flex; flex-direction: column;
  background: var(--papir); border: 1px solid var(--regel); border-radius: var(--radius);
  margin-block-end: 0;
}
.card-body { padding: var(--rom-3) 1.2rem; flex: 1 1 auto; }
.card-body > :last-child { margin-block-end: 0; }
.card-header, .card-footer {
  padding: .7rem 1.2rem; background: var(--papir-dyp); border-color: var(--regel);
  font-size: .8rem; color: var(--dempet);
}
.card-header { border-block-end: 1px solid var(--regel); border-start-start-radius: var(--radius); border-start-end-radius: var(--radius); }
.card-footer { border-block-start: 1px solid var(--regel); border-end-start-radius: var(--radius); border-end-end-radius: var(--radius); }
/* The card title is the box label from site.css: letterspaced capitals, quiet, not a heading size. */
/* On a plain paper card the title is an eyebrow, so --dempet. On a TINTED card (the four
   `card text-bg-*` KPI panels in SRS/app/templates/admin/dashboard.html) --dempet lands within
   1.2:1 of the tint and the title all but vanishes — so follow the role's own text colour when the
   card has one, exactly as .card-text and .alert-heading do. */
.card-title {
  margin: 0 0 var(--rom-2); font-size: .72rem; letter-spacing: .13em; text-transform: uppercase;
  font-weight: var(--vekt-semi); color: var(--rolle-tekst, var(--dempet)); line-height: 1.3;
}
.card-text { margin: 0 0 var(--rom-3); }
.card-text:last-child { margin-block-end: 0; }
/* `border-0` on a card is 9 templates asking for a panel without a frame — usually a login card
   centred on the page, where the hairline is redundant with the page edge. Honour it exactly. */
.card.border-0 { border-color: transparent; }
.card.border-0 > .card-header { border-block-end-color: var(--regel); }
/* A card with an identity uses the same grammar as an alert and a status card, and for the same
   reason: in the book a coloured panel is a tint with ONE coloured edge, never a frame in full
   strength. Binding the three components to one grammar is what keeps a tinted card and a tinted
   alert from looking like they came from different stylesheets. */
/* The `text-bg-*` family is listed alongside `bg-*` because SRS/app/templates/admin/dashboard.html
   uses `card text-bg-primary` (and -success, -info, -warning) for its four KPI panels. `text-bg-*`
   sets the role trio but nothing on a card read it, so those four cards fell through to the plain
   `.card { background: var(--papir) }` and rendered as four identical blank panels. */
.card:is(.bg-info, .bg-success, .bg-danger, .bg-warning, .bg-secondary, .bg-light, .bg-primary,
         .text-bg-info, .text-bg-success, .text-bg-danger, .text-bg-warning, .text-bg-secondary,
         .text-bg-light, .text-bg-primary) {
  background: var(--rolle-bg); color: var(--rolle-tekst);
  border-color: color-mix(in srgb, var(--rolle-kant) 45%, transparent);
  border-inline-start: 3px solid var(--rolle-kant);
  border-start-start-radius: 0; border-end-start-radius: 0;
}
/* The role trio is registered `inherits: false` on purpose — a .btn-sm inside a card must not pick
   up the card's slot values. The consequence is that a DESCENDANT cannot read --rolle-tekst: it
   resolves to guaranteed-invalid and falls back. .card-title's fallback is --dempet, which on a
   solid .text-bg-primary card measured 1.2:1 — the title all but disappeared.
   The card has already put the right colour on itself, so the children take it by inheritance
   rather than by reading a property that was deliberately made unreadable to them. */
.card:is(.bg-info, .bg-success, .bg-danger, .bg-warning, .bg-secondary, .bg-light, .bg-primary,
         .text-bg-info, .text-bg-success, .text-bg-danger, .text-bg-warning, .text-bg-secondary,
         .text-bg-light, .text-bg-primary) :is(.card-title, .card-header, .card-text, .card-body) {
  color: inherit;
}

/* The alert is the book's four-box identity, exactly as the PDF prints it: a tint with a coloured
   rule down the leading edge. That rule is what makes info, success, warning and danger tell apart
   without relying on hue, and it is the reason these were already dark-mode aware.
   The bare `alert-` in the JavaScript (`"alert-" + type` with an empty type) lands on the plain
   `.alert` here and gets the neutral grey identity rather than nothing at all. */
/* NOTE ON THE DEFAULTS. A component must never *declare* the role trio to get its own default — a
   `.alert { --rolle-bg: … }` and a `.alert-info { --rolle-bg: … }` have the same specificity, and
   whichever is written second wins for every alert on the site. The default belongs in the var()
   fallback instead, which is precisely what the `syntax: "*"` registration at the head of the file
   buys us: unset means guaranteed-invalid, guaranteed-invalid means the fallback, and the fallback
   is a token, so the untinted alert inverts in dark mode like everything else. */
.alert {
  position: relative;
  margin-block: 0 var(--rom-3); padding: .85rem 1.1rem .85rem 1.2rem;
  background: var(--rolle-bg, var(--rboks-bg));
  color: var(--rolle-tekst, var(--blekk));
  border: 0; border-inline-start: 3px solid var(--rolle-kant, var(--rboks-kant));
  border-start-end-radius: var(--radius); border-end-end-radius: var(--radius);
}
.alert > :last-child { margin-block-end: 0; }
.alert a { color: var(--rolle-tekst, var(--blekk)); text-decoration-color: var(--regel-sterk); }
/* The fallback is --dempet, not --rboks-kant. On a neutral alert (bare `.alert`, `-secondary`,
   `-light`, `-dark`, and `.status-queued`) --rboks-kant would paint #7a7365 on #ede9df = 3.88:1,
   under AA at this size. site.css:339 had already hit the same wall and solved it the same way:
   `.boks.rboks > .boks-tittel { color: var(--dempet) }`. The tinted roles still use their own edge
   colour, which is where the contrast was checked. */
.alert-heading {
  margin: 0 0 var(--rom-2); font-size: .72rem; letter-spacing: .13em; text-transform: uppercase;
  font-weight: var(--vekt-semi); color: var(--rolle-kant, var(--dempet));
}
/* Room for the close button, which sits in the padding rather than in the text column. */
.alert-dismissible { padding-inline-end: 3.2rem; }
.alert-dismissible .btn-close { position: absolute; inset-block-start: .2rem; inset-inline-end: .2rem; }

/* A list group is the book's hairlined list (ul.oppslag in site.css): rules between rows, nothing
   around them, no boxes. */
.list-group { list-style: none; margin: 0; padding: 0; border-block-start: 1px solid var(--regel); }
.list-group-item {
  display: block; padding: .6rem 0; border-block-end: 1px solid var(--regel);
  background: transparent; color: var(--blekk);
}
/* The -sm variant tightens the type, not the target: as a link this is still something a thumb has
   to hit, and .3rem of padding around a .85rem line came to about 31 px. */
.list-group-item-sm { padding-block: .3rem; font-size: .85rem; }
a.list-group-item-sm, button.list-group-item-sm { min-block-size: 2.5rem; display: flex; align-items: center; }
.list-group-item.active, .list-group-item[aria-current] { background: var(--papir-dyp); font-weight: var(--vekt-medium); }
a.list-group-item, button.list-group-item { inline-size: 100%; text-align: start; text-decoration: none; border-inline: 0; cursor: pointer; }
a.list-group-item:hover, button.list-group-item:hover { background: var(--papir-dyp); }


/* ---- 9. status cards --------------------------------------------------------------------------
   Job states for the reader's speech queue. These are referenced in the markup and were defined in
   no stylesheet at all, so this is the first time they have had a look.
   Each is a role plus a shape. The shape matters: four tints that differ only in hue would be a
   colour-only status indicator, which fails for the reader who cannot separate the greens from the
   reds — and these four states are exactly the ones a user needs to tell apart at a glance. The
   marks are a circle (waiting), a half-filled circle (working), a filled circle (done) and a
   triangle (failed), so the state survives greyscale, dark mode and a bad projector. */
/* The mark is positioned, not given a grid column. A grid here looked tidier but broke on the real
   markup: a status card is usually `<strong>Ferdig</strong> — 18 minutter lyd.`, and that trailing
   bare text is an ANONYMOUS grid item, which `.status-card > *` cannot select. It auto-placed into
   the 1.4 rem icon column and wrapped one word per line. Ordinary block flow with the mark in the
   padding takes any markup shape — text nodes, elements, a progress bar — without caring. */
.status-card {
  position: relative;
  margin-block: var(--rom-3); padding: var(--rom-3) 1.2rem var(--rom-3) 2.9rem;
  background: var(--rolle-bg, var(--rboks-bg));
  color: var(--rolle-tekst, var(--blekk));
  border-inline-start: 3px solid var(--rolle-kant, var(--rboks-kant));
  border-start-end-radius: var(--radius); border-end-end-radius: var(--radius);
}
.status-card::before {
  content: "\25cb"; color: var(--rolle-kant, var(--rboks-kant)); font-size: 1rem; line-height: 1.5;
  position: absolute; inset-inline-start: 1rem; inset-block-start: var(--rom-3);
}
.status-card > * { margin-block: 0; }
.status-card > * + * { margin-block-start: var(--rom-2); }
.status-card .error { color: var(--viktig-kant); margin: 0; }
.status-card audio { inline-size: 100%; }
.status-queued::before     { content: "\25cb"; }   /* ○ open: nothing has happened yet */
.status-processing::before { content: "\25d0"; }   /* ◐ half: work under way */
.status-done::before       { content: "\25cf"; }   /* ● filled: complete */
.status-failed::before     { content: "\25b2"; }   /* ▲ a different shape entirely, not a darker dot */


/* ---- 10. the progress bar ------------------------------------------------------------------------
   The reader polls a job queue and had no bar at all. The difficulty is that the app's CSP carries
   no 'unsafe-inline' and no style-src nonce, so `style="width:42%"` — the way every progress bar on
   the web is built — is simply blocked. Two mechanisms here, both CSP-safe, neither needing a style
   attribute, a nonce or an injected <style> element.

   MECHANISM A, preferred: the native <progress> element.
     <progress class="fremdrift" value="42" max="100">42 %</progress>
   The number lives in an HTML *content* attribute, which CSP does not police — style-src governs
   stylesheets and style attributes, and `value` is neither. The browser also gives us the correct
   role, the correct announcement and an indeterminate state (omit `value`) at no cost. This is the
   right answer whenever the bar does not need a label inside it.

   MECHANISM B, for a bar that must carry a caption or its own ink: a two-digit decomposition.
     <div class="fremdrift" role="progressbar" aria-valuenow="42" aria-valuemin="0" aria-valuemax="100"
          data-tiere="4" data-enere="2"><span class="fremdrift-linje"></span></div>
   Twenty-one attribute selectors — tens 0–10, ones 0–9 — set two custom properties, and one calc
   turns them back into a percentage. That buys 1 % resolution for 21 rules, where the obvious
   `[data-prosent="0".."100"]` would need 101; five-per-cent steps would need 21 rules for a fifth of
   the precision. The JavaScript contract is two lines:
     el.dataset.tiere = Math.floor(p / 10);  el.dataset.enere = p % 10;
   and 100 % is tiere=10, enere=0. Set aria-valuenow to the true number in the same breath: the
   attributes above drive pixels, aria-valuenow drives the announcement, and they must agree. */
progress.fremdrift {
  appearance: none; -webkit-appearance: none;
  inline-size: 100%; block-size: .45rem; display: block;
  background: var(--papir-dyp); border: 1px solid var(--regel); border-radius: var(--radius);
  overflow: hidden; color: var(--lenke);
}
/* These three cannot be grouped into one selector list: a browser that does not know one vendor
   pseudo-element discards the whole rule, and we would lose the bar in the other two. */
progress.fremdrift::-webkit-progress-bar { background: var(--papir-dyp); }
progress.fremdrift::-webkit-progress-value { background: var(--lenke); transition: inline-size .3s ease; }
progress.fremdrift::-moz-progress-bar { background: var(--lenke); }

.fremdrift {
  --ti: 0; --en: 0;   /* inheriting on purpose: the attribute lands on the track, the fill reads it */
  display: block; inline-size: 100%; block-size: .45rem; overflow: hidden;
  background: var(--papir-dyp); border: 1px solid var(--regel); border-radius: var(--radius);
}
.fremdrift-linje {
  display: block; block-size: 100%;
  inline-size: calc((var(--ti) * 10 + var(--en)) * 1%);
  background: var(--fremdrift-farge, var(--lenke));
  transition: inline-size .3s ease;
}
.fremdrift[data-tiere="0"]{--ti:0} .fremdrift[data-tiere="1"]{--ti:1} .fremdrift[data-tiere="2"]{--ti:2} .fremdrift[data-tiere="3"]{--ti:3} .fremdrift[data-tiere="4"]{--ti:4} .fremdrift[data-tiere="5"]{--ti:5} .fremdrift[data-tiere="6"]{--ti:6} .fremdrift[data-tiere="7"]{--ti:7} .fremdrift[data-tiere="8"]{--ti:8} .fremdrift[data-tiere="9"]{--ti:9} .fremdrift[data-tiere="10"]{--ti:10}
.fremdrift[data-enere="0"]{--en:0} .fremdrift[data-enere="1"]{--en:1} .fremdrift[data-enere="2"]{--en:2} .fremdrift[data-enere="3"]{--en:3} .fremdrift[data-enere="4"]{--en:4} .fremdrift[data-enere="5"]{--en:5} .fremdrift[data-enere="6"]{--en:6} .fremdrift[data-enere="7"]{--en:7} .fremdrift[data-enere="8"]{--en:8} .fremdrift[data-enere="9"]{--en:9}
/* A failed job's bar should not stay accent-blue under a red status card. */
.status-failed .fremdrift { --fremdrift-farge: var(--viktig-kant); }
.status-done .fremdrift   { --fremdrift-farge: var(--oppgaver-kant); }
/* Indeterminate: a short stripe that travels. site.css kills every animation under
   prefers-reduced-motion, so the resting state has to be meaningful on its own — hence a stripe
   parked at the start covering a third of the track, which reads as "under way, extent unknown"
   rather than as an empty or a finished bar. */
.fremdrift.ubestemt .fremdrift-linje { inline-size: 34%; animation: fremdrift-vandre 1.6s ease-in-out infinite; }
@keyframes fremdrift-vandre {
  0%   { transform: translateX(0); }
  50%  { transform: translateX(194%); }
  100% { transform: translateX(0); }
}

/* The spinner: a hairline ring with one ink arc. Under reduced motion it stops turning and stays a
   ring with a mark on it, which still reads as a token beside "Processing…" rather than vanishing. */
.spinner-border {
  display: inline-block; inline-size: 1.1rem; block-size: 1.1rem; flex: 0 0 auto;
  border: 2px solid var(--regel); border-block-start-color: currentColor;
  border-radius: 50%; animation: spinner-snurr .8s linear infinite;
  vertical-align: -.15em;
}
.spinner-border-sm { inline-size: .8rem; block-size: .8rem; border-width: 1.5px; }
@keyframes spinner-snurr { to { transform: rotate(360deg); } }


/* ---- 11. dialog ---------------------------------------------------------------------------------
   28 window.confirm() calls become one native <dialog>. Native, because the browser gives us the
   modal semantics, the focus trap, the Escape key and the inert background for free, and because a
   hand-rolled modal would want inline positioning styles the CSP will not allow.
   The panel is a card that has come forward: the same paper, the same hairline, the same 2 px — the
   only new thing is the veil behind it, which is warm rather than neutral grey so the page under it
   still looks like paper in a dim room. */
dialog {
  /* A floor as well as a ceiling: a dialog sizes to its content, and a two-word confirmation left to
     do that becomes a cramped little box in the middle of the screen. */
  min-inline-size: min(20rem, calc(100vw - 2rem));
  max-inline-size: min(32rem, calc(100vw - 2rem));
  max-block-size: calc(100dvh - 4rem);
  margin: auto;   /* centres the box in the top layer without a transform */
  padding: 0;
  background: var(--papir); color: var(--blekk);
  border: 1px solid var(--regel-sterk); border-radius: var(--radius);
  font-family: inherit;
}
/* Scoped to [open] so the UA's own `dialog:not([open]) { display: none }` still governs the closed
   state — an unscoped `display` here would leave every dialog on the page permanently visible. */
dialog[open] { display: flex; flex-direction: column; }
dialog::backdrop { background: var(--slor); }
/* The body scrolls, the actions do not: a long list of consequences must never push the button that
   dismisses it off the bottom of the screen. */
.dialog-innhold { padding: 1.3rem 1.4rem; overflow-y: auto; }
.dialog-tittel {
  margin: 0 0 var(--rom-2); font-size: .72rem; letter-spacing: .13em; text-transform: uppercase;
  font-weight: var(--vekt-semi); color: var(--dempet);
}
.dialog-tekst { margin: 0 0 var(--rom-4); }
.dialog-tekst:last-child { margin-block-end: 0; }
/* The button row: the confirming action last, where the eye and the thumb finish. */
.dialog-valg {
  display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--rom-2);
  padding: var(--rom-3) 1.4rem; margin: 0;
  background: var(--papir-dyp); border-block-start: 1px solid var(--regel);
}
/* A destructive dialog says so with a rule down its edge, the same grammar as the viktig box. */
dialog.dialog-danger { border-inline-start: 3px solid var(--viktig-kant); }
/* The page behind a modal must not scroll under it; :has keeps this out of the JavaScript. */
html:has(dialog[open]) { overflow: hidden; }


/* ---- 12. empty states ---------------------------------------------------------------------------
   The dashed rule is `.merknad` from site.css — the book's way of saying "this is an editorial note,
   not content". An empty list is exactly that: a frame with the reason it is empty written in it,
   and room for the one button that would fill it. */
.tom {
  display: flex; flex-direction: column; align-items: center; gap: var(--rom-2);
  padding: var(--rom-5) var(--rom-4);
  border: 1px dashed var(--regel-sterk); border-radius: var(--radius);
  color: var(--dempet); text-align: center;
}
.tom-tittel { margin: 0; font-size: var(--fs-4); font-weight: var(--vekt-medium); color: var(--blekk); }
.tom-tekst { margin: 0; max-inline-size: 28rem; font-style: italic; }
.tom .btn { margin-block-start: var(--rom-2); }
/* The compact form, for an empty panel inside a card rather than a whole page. */
.tom.tom-liten { padding: var(--rom-4) var(--rom-3); border-style: dashed; }
.tom.tom-liten .tom-tittel { font-size: 1rem; }


/* ---- 13. tables ---------------------------------------------------------------------------------
   Booktabs, as the book sets them: an ink rule above the head, an ink rule under the body, a hairline
   between rows, and not one vertical line anywhere. Figures are lining and tabular so columns of
   numbers line up, which is the whole reason a results table exists. */
.table {
  inline-size: 100%; border-collapse: collapse; margin-block-end: var(--rom-3);
  font-size: .92rem; font-variant-numeric: lining-nums tabular-nums;
}
/* MIDDLE, not Bootstrap's `top`. Bootstrap defaults to top because it assumes content tables with
   long wrapping cells; these are data rows where nearly every row carries a 2.5rem button, so top
   left «Quick Quiz», the status badges and the date pinned to the ceiling of a row whose height
   was set by the buttons beside them. `.align-top` is still there for a table that wants the
   other behaviour. */
.table th, .table td { padding: .45em .7em; vertical-align: middle; text-align: start; border: 0; }
.table thead th {
  font-weight: var(--vekt-semi); font-size: .74rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--dempet); white-space: nowrap;
  border-block: 1px solid var(--blekk);
}
.table tbody tr + tr > * { border-block-start: 1px solid var(--regel); }
.table tbody tr:last-child > * { border-block-end: 1px solid var(--blekk); }
.table tfoot td { border-block-start: 1px solid var(--blekk); color: var(--dempet); font-size: .85rem; }
.table caption { caption-side: bottom; padding-block-start: var(--rom-2); color: var(--dempet); font-size: .84rem; text-align: start; }
.table-hover tbody tr:hover > * { background: var(--papir-dyp); }
.table-sm th, .table-sm td { padding: .25em .5em; }
.table-light th, .table-light td, tr.table-light > * { background: var(--papir-dyp); }
/* `table-bordered` asked for a grid of lines. The book does not draw them, so it gets the hairline
   between rows it really wanted and keeps its columns open. */
.table-bordered tbody tr > * { border-block-start: 1px solid var(--regel); }
.table-responsive { overflow-x: auto; }   /* the same escape hatch as .tabellrull in site.css */
.align-middle, .align-middle > * { vertical-align: middle; }


/* ---- 14. small parts ----------------------------------------------------------------------------- */
/* The badge is a label, not a pill: letterspaced capitals in a tint with a hairline of its own role
   colour. Capitals rather than a bold sans, because the book has no sans and no small caps. */
.badge {
  display: inline-flex; align-items: center; gap: var(--rom-1);
  padding: .12em .5em; border-radius: var(--radius);
  font-size: .68rem; letter-spacing: .08em; text-transform: uppercase;
  font-weight: var(--vekt-semi); line-height: 1.5; white-space: nowrap;
  background: var(--rolle-bg, var(--rboks-bg));
  color: var(--rolle-tekst, var(--blekk));
  border: 1px solid color-mix(in srgb, var(--rolle-kant, var(--rboks-kant)) 45%, transparent);
  font-variant-numeric: lining-nums;
}
/* A badge that is a link or a button still needs a real target, and 1.6 rem (27 px) was not one.
   The visible pill stays small — the target is grown around it with padding-block, so the badge
   still reads as a badge while clearing 44 px in the block direction. */
a.badge, button.badge {
  text-decoration: none;
  display: inline-flex; align-items: center;
  min-block-size: 2.5rem; padding-block: .55rem;
}

/* A tab or pill row, outside the navbar: the book's letter index (nav.bokstaver) grammar — a rule
   under the item that appears on hover and stays for the current one. */
.nav { display: flex; flex-wrap: wrap; gap: .2rem 1.2rem; list-style: none; margin: 0 0 var(--rom-3); padding: 0; border-block-end: 1px solid var(--regel); }
.nav-item { margin: 0; }
.nav-link {
  display: inline-flex; align-items: center; min-block-size: 2.5rem;
  padding: .2rem 0; color: var(--dempet); text-decoration: none;
  border-block-end: 2px solid transparent; margin-block-end: -1px;
  font-size: .88rem; background: none; border-inline: 0; border-block-start: 0; cursor: pointer;
}
.nav-link:hover { color: var(--blekk); border-block-end-color: var(--regel-sterk); }
.nav-link.active, .nav-link[aria-current], .nav-link[aria-selected="true"] { color: var(--blekk); border-block-end-color: var(--blekk); }
.nav-link:disabled, .nav-link.disabled { color: var(--dempet-svak); cursor: not-allowed; }

/* A dropdown is the search panel from site.css (.navsok-treff): paper, a hairline, rows divided by
   hairlines. The one shadow in the book is on that panel, because a floating layer must read as
   floating; it is a soft drop, not a Material card. */
.dropdown { position: relative; }
.dropdown-menu {
  display: none;   /* opened by `.show`, or by the hover group at the foot of section 15 */
  position: absolute; inset-block-start: calc(100% + .4rem); inset-inline-start: 0; z-index: 30;
  min-inline-size: 12rem; max-inline-size: min(92vw, 22rem);
  max-block-size: min(70vh, 24rem); overflow-y: auto;
  margin: 0; padding: 0; list-style: none;
  background: var(--papir); border: 1px solid var(--regel); border-radius: var(--radius);
  box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .13);
  font-size: .85rem; text-align: start;
}
.dropdown-menu.show { display: block; }
.dropdown-item, .dropdown-item-text {
  display: block; inline-size: 100%; min-block-size: 2.5rem;
  padding: .5rem .75rem; border: 0; border-block-end: 1px solid var(--regel);
  background: transparent; color: var(--blekk); text-align: start; text-decoration: none;
  font: inherit; font-size: .85rem; line-height: 1.4;
}
.dropdown-item { cursor: pointer; }
.dropdown-item:hover, .dropdown-item:focus-visible { background: var(--papir-dyp); }
.dropdown-item-text { color: var(--dempet); cursor: default; }
.dropdown-header {
  padding: .45rem .75rem; border-block-end: 1px solid var(--regel);
  font-size: .66rem; letter-spacing: .13em; text-transform: uppercase;
  font-weight: var(--vekt-semi); color: var(--dempet);
}
.dropdown-menu > :last-child { border-block-end: 0; }

/* Collapse. Bootstrap's JavaScript is going away, so this is the CSS half of the contract: the
   panel is hidden until something adds `.show`. `.collapsing` is kept as a synonym for open so a
   mid-transition state never flashes the panel away. */
.collapse:not(.show) { display: none; }
.collapse.show, .collapsing { display: block; }
.navbar-collapse.collapse.show { display: flex; flex-wrap: wrap; }
/* Above the expand breakpoint the navbar's panel is not a disclosure at all, so it ignores `.show`.
   Three classes, to outrank `.collapse:not(.show)` by specificity as well as by position. */
@media (min-width: 992px) {
  .navbar-expand-lg .navbar-collapse.collapse { display: flex; flex-wrap: wrap; flex-basis: auto; }
}


/* ---- 14b. the icon ------------------------------------------------------------------------------
   <svg class="ikon" aria-hidden="true"><use href="#dokument"></use></svg>

   This replaces bootstrap-icons (86 kB of CSS plus a 130 kB webfont, for 43 glyphs) with a sprite
   inlined once per page.

   The stroke setup lives HERE, not on the sprite, and that is not a stylistic choice. <use> clones
   the referenced symbol into a shadow tree, and presentation ATTRIBUTES on the sprite's own <svg>
   root do not reach the clone — but inherited CSS properties from the referencing element do.
   fill, stroke, stroke-width, stroke-linecap and stroke-linejoin are all inherited SVG properties,
   so setting them on .ikon is what actually draws the icon. A sprite that looks correct on its own
   will render as invisible or as solid black blobs without this rule.

   Sized in em so an icon matches whatever text it sits in — a .btn-sm and an h2 both get the right
   one — and nudged off the baseline by the usual .125em so it sits optically centred beside text.
   currentColor throughout, so it inverts in dark mode with everything else. */
/* The sprite itself. It is inlined once per page and must not render — but it cannot say so with
   style="display:none", because the CSP that this re-skin tightened has no 'unsafe-inline'. A
   class is the only way to hide it. */
.ikon-ark { display: none; }

.ikon {
  inline-size: 1em; block-size: 1em;
  vertical-align: -.125em;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* A couple of glyphs read better filled than stroked — a play triangle and a stop square are
   shapes, not outlines. They opt in by carrying their own fill, so the symbol says so, not a
   second class at every call site. */
.ikon .fylt { fill: currentColor; stroke: none; }
/* An icon that is the only thing in a button still needs the button's target, which .btn already
   gives it; this only stops the glyph being squashed by a tight flex row. */
.btn > .ikon, .nav-link > .ikon, .input-group-text > .ikon { flex: 0 0 auto; }


/* ---- 15. what the two apps call their own ------------------------------------------------------
   Classes that exist only in these templates. They are components like any other — each one reads
   the same scales — and they are gathered here rather than scattered so the next person can see at
   a glance what is app-specific and what is vocabulary. */

/* The live text-response panel: answers arriving during a quiz. A hairlined well, scrolling, with
   one response per hairlined row — the book's list idiom at speed. */
.text-resp-panel {
  max-block-size: 18rem; overflow-y: auto;
  padding: var(--rom-2) .8rem;
  background: var(--papir-dyp); border: 1px solid var(--regel); border-radius: var(--radius);
}
.text-resp-row {
  padding: .35rem 0; border-block-end: 1px solid var(--regel);
  font-size: .9rem; line-height: 1.45; overflow-wrap: anywhere;
}
.text-resp-row:last-child { border-block-end: 0; }

/* The question editor's row of small actions. They inherit everything from .btn; all they add is
   the promise that a row of them wraps rather than overflowing on a phone. */
.btn-add-opt, .btn-remove-opt, .btn-clone-q, .btn-remove-q, .btn-preview-q, .btn-grade { white-space: nowrap; }
/* Remove is a single character, so it needs a square target rather than a wide one. */
.btn-remove-opt { --kontroll-pad-x: 0; min-inline-size: 2.5rem; }
/* Grading is a two-way decision made many times in a row, and the teacher's eye needs to see what
   has already been graded without re-reading it. The mark goes on the side NOT chosen: it steps
   back to the outline treatment, leaving the filled button as the record of the decision. Doing it
   the other way round — dimming the chosen one — would leave paper-coloured text (`btn-success`
   sets --btn-tekst to paper) on a pale tint, which is how a "selected" state becomes unreadable.
   The app has to set aria-pressed for this to fire, which it should anyway: these are toggles. */
.btn-grade[aria-pressed="false"] {
  --btn-bg: var(--papir); --btn-tekst: var(--dempet); --btn-kant: var(--regel-sterk);
}
/* The clone menu floating out of a course row. It borrows .dropdown-menu for its looks and differs
   only in what opens it: hover or keyboard focus inside the group, rather than a `.show` class from
   JavaScript. :focus-within is what makes it reachable by keyboard at all, and it is stated here
   rather than left to the app's own custom.css so the menu cannot be orphaned when that file goes. */
.clone-hover-menu { inset-inline-start: auto; inset-inline-end: 0; }
.clone-hover-group { position: relative; }
.clone-hover-group:hover > .clone-hover-menu,
.clone-hover-group:focus-within > .clone-hover-menu { display: block; }


/* ---- 16. utilities ------------------------------------------------------------------------------
   Every one of these names a scale step. There is no !important anywhere in this file, so a utility
   beats a component only by being later in the source — which is why this section is last, and why
   `none` is last within each display tier: `d-none` has to win over `d-flex` on the same element,
   because 61 JavaScript sites toggle exactly that pair. */

/* space */
.m-0{margin:var(--rom-0)} .m-1{margin:var(--rom-1)} .m-2{margin:var(--rom-2)} .m-3{margin:var(--rom-3)} .m-4{margin:var(--rom-4)} .m-5{margin:var(--rom-5)} .m-auto{margin:auto}
.mt-0{margin-block-start:var(--rom-0)} .mt-1{margin-block-start:var(--rom-1)} .mt-2{margin-block-start:var(--rom-2)} .mt-3{margin-block-start:var(--rom-3)} .mt-4{margin-block-start:var(--rom-4)} .mt-5{margin-block-start:var(--rom-5)} .mt-auto{margin-block-start:auto}
.mb-0{margin-block-end:var(--rom-0)} .mb-1{margin-block-end:var(--rom-1)} .mb-2{margin-block-end:var(--rom-2)} .mb-3{margin-block-end:var(--rom-3)} .mb-4{margin-block-end:var(--rom-4)} .mb-5{margin-block-end:var(--rom-5)} .mb-auto{margin-block-end:auto}
.ms-0{margin-inline-start:var(--rom-0)} .ms-1{margin-inline-start:var(--rom-1)} .ms-2{margin-inline-start:var(--rom-2)} .ms-3{margin-inline-start:var(--rom-3)} .ms-4{margin-inline-start:var(--rom-4)} .ms-5{margin-inline-start:var(--rom-5)} .ms-auto{margin-inline-start:auto}
.me-0{margin-inline-end:var(--rom-0)} .me-1{margin-inline-end:var(--rom-1)} .me-2{margin-inline-end:var(--rom-2)} .me-3{margin-inline-end:var(--rom-3)} .me-4{margin-inline-end:var(--rom-4)} .me-5{margin-inline-end:var(--rom-5)} .me-auto{margin-inline-end:auto}
.mx-0{margin-inline:var(--rom-0)} .mx-1{margin-inline:var(--rom-1)} .mx-2{margin-inline:var(--rom-2)} .mx-3{margin-inline:var(--rom-3)} .mx-4{margin-inline:var(--rom-4)} .mx-5{margin-inline:var(--rom-5)} .mx-auto{margin-inline:auto}
.my-0{margin-block:var(--rom-0)} .my-1{margin-block:var(--rom-1)} .my-2{margin-block:var(--rom-2)} .my-3{margin-block:var(--rom-3)} .my-4{margin-block:var(--rom-4)} .my-5{margin-block:var(--rom-5)} .my-auto{margin-block:auto}
.p-0{padding:var(--rom-0)} .p-1{padding:var(--rom-1)} .p-2{padding:var(--rom-2)} .p-3{padding:var(--rom-3)} .p-4{padding:var(--rom-4)} .p-5{padding:var(--rom-5)}
.pt-0{padding-block-start:var(--rom-0)} .pt-1{padding-block-start:var(--rom-1)} .pt-2{padding-block-start:var(--rom-2)} .pt-3{padding-block-start:var(--rom-3)} .pt-4{padding-block-start:var(--rom-4)} .pt-5{padding-block-start:var(--rom-5)}
.pb-0{padding-block-end:var(--rom-0)} .pb-1{padding-block-end:var(--rom-1)} .pb-2{padding-block-end:var(--rom-2)} .pb-3{padding-block-end:var(--rom-3)} .pb-4{padding-block-end:var(--rom-4)} .pb-5{padding-block-end:var(--rom-5)}
.ps-0{padding-inline-start:var(--rom-0)} .ps-1{padding-inline-start:var(--rom-1)} .ps-2{padding-inline-start:var(--rom-2)} .ps-3{padding-inline-start:var(--rom-3)} .ps-4{padding-inline-start:var(--rom-4)} .ps-5{padding-inline-start:var(--rom-5)}
.pe-0{padding-inline-end:var(--rom-0)} .pe-1{padding-inline-end:var(--rom-1)} .pe-2{padding-inline-end:var(--rom-2)} .pe-3{padding-inline-end:var(--rom-3)} .pe-4{padding-inline-end:var(--rom-4)} .pe-5{padding-inline-end:var(--rom-5)}
.px-0{padding-inline:var(--rom-0)} .px-1{padding-inline:var(--rom-1)} .px-2{padding-inline:var(--rom-2)} .px-3{padding-inline:var(--rom-3)} .px-4{padding-inline:var(--rom-4)} .px-5{padding-inline:var(--rom-5)}
.py-0{padding-block:var(--rom-0)} .py-1{padding-block:var(--rom-1)} .py-2{padding-block:var(--rom-2)} .py-3{padding-block:var(--rom-3)} .py-4{padding-block:var(--rom-4)} .py-5{padding-block:var(--rom-5)}
.gap-0{gap:var(--rom-0)} .gap-1{gap:var(--rom-1)} .gap-2{gap:var(--rom-2)} .gap-3{gap:var(--rom-3)} .gap-4{gap:var(--rom-4)} .gap-5{gap:var(--rom-5)}

/* flex */
.flex-row{flex-direction:row} .flex-column{flex-direction:column} .flex-wrap{flex-wrap:wrap} .flex-nowrap{flex-wrap:nowrap}
.flex-grow-0{flex-grow:0} .flex-grow-1{flex-grow:1} .flex-shrink-0{flex-shrink:0} .flex-shrink-1{flex-shrink:1} .flex-fill{flex:1 1 auto}
.justify-content-start{justify-content:flex-start} .justify-content-end{justify-content:flex-end} .justify-content-center{justify-content:center} .justify-content-between{justify-content:space-between} .justify-content-around{justify-content:space-around} .justify-content-evenly{justify-content:space-evenly}
.align-items-start{align-items:flex-start} .align-items-end{align-items:flex-end} .align-items-center{align-items:center} .align-items-baseline{align-items:baseline} .align-items-stretch{align-items:stretch}
.align-self-start{align-self:flex-start} .align-self-end{align-self:flex-end} .align-self-center{align-self:center} .align-self-baseline{align-self:baseline} .align-self-stretch{align-self:stretch}
.align-top{vertical-align:top} .align-bottom{vertical-align:bottom} .align-baseline{vertical-align:baseline}
/* `float` is the one place the logical keywords are not usable: inline-start/inline-end are
   Firefox-only, and everywhere else the declaration is simply dropped, so `.float-end` silently did
   nothing. Used at SRS/app/templates/teacher/quiz_results.html:19. The rest of this file's logical
   properties are fine — those have universal support. */
.float-start{float:left} .float-end{float:right} .float-none{float:none}

/* size */
.w-25{inline-size:25%} .w-50{inline-size:50%} .w-75{inline-size:75%} .w-100{inline-size:100%} .w-auto{inline-size:auto}
.h-25{block-size:25%} .h-50{block-size:50%} .h-75{block-size:75%} .h-100{block-size:100%} .h-auto{block-size:auto}
.mw-100{max-inline-size:100%} .mh-100{max-block-size:100%} .vh-100{block-size:100vh}

/* type */
.text-start{text-align:start} .text-center{text-align:center} .text-end{text-align:end}
.text-nowrap{white-space:nowrap} .text-wrap{white-space:normal} .text-break{overflow-wrap:anywhere}
.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-inline-size:0}
.text-uppercase{text-transform:uppercase;letter-spacing:.08em} /* capitals need the extra space in a serif */
.text-lowercase{text-transform:lowercase} .text-capitalize{text-transform:capitalize}
.text-decoration-none{text-decoration:none} .text-decoration-underline{text-decoration:underline}
.font-monospace{font-family:var(--mono);font-size:.86em} /* the mono face runs large beside STIX */
.fst-italic{font-style:italic} .fst-normal{font-style:normal}
.fw-normal{font-weight:var(--vekt-normal)} .fw-medium{font-weight:var(--vekt-medium)} .fw-semibold{font-weight:var(--vekt-semi)} .fw-bold{font-weight:var(--vekt-fet)} .fw-light{font-weight:var(--vekt-normal)}
.fs-1{font-size:var(--fs-1)} .fs-2{font-size:var(--fs-2)} .fs-3{font-size:var(--fs-3)} .fs-4{font-size:var(--fs-4)} .fs-5{font-size:var(--fs-5)} .fs-6{font-size:var(--fs-6)}
.small, small { font-size: var(--fs-liten); }
/* `lead` is the book's ingress: a shade larger, quieter, italic — the paragraph that introduces. */
.lead { font-size: 1.08rem; color: var(--dempet); font-style: italic; line-height: 1.55; }

/* text colour. Every one of these is a token, so every one inverts. There is no second grey scale:
   `text-muted`, `text-body-secondary` and `text-secondary` are three names Bootstrap gave the same
   idea, and here they are one ink. */
.text-muted, .text-body-secondary, .text-secondary { color: var(--dempet); }
.text-body-tertiary { color: var(--dempet-svak); }
.text-body, .text-dark, .text-black { color: var(--blekk); }
.text-primary { color: var(--lenke); }
.text-success { color: var(--oppgaver-kant); }
.text-danger, .text-danger-emphasis { color: var(--viktig-kant); }
.text-warning { color: var(--ib-tekst); }
.text-info { color: var(--eksempel-kant); }
/* `text-light` and `text-white` were for text on a black bar. There is no black bar, so they are
   the quiet ink — the alternative is invisible text on paper. */
.text-light, .text-white { color: var(--dempet); }
.text-reset { color: inherit; }

/* edges. `shadow` and `shadow-sm` become hairlines: a drop shadow was always a way of saying "this
   is a separate surface", and on paper that is said with a rule. The 15 shadow sites are cards and
   a navbar, all of which already have an edge — so these mostly resolve to the edge they meant. */
.shadow, .shadow-sm, .shadow-lg { border: 1px solid var(--regel); }
.shadow-none { border: 0; }
.border { border: 1px solid var(--regel); }
.border-0 { border: 0; }
.border-top { border-block-start: 1px solid var(--regel); }
.border-bottom { border-block-end: 1px solid var(--regel); }
.border-start { border-inline-start: 1px solid var(--regel); }
.border-end { border-inline-end: 1px solid var(--regel); }
.border-primary, .border-success, .border-danger, .border-warning, .border-info, .border-secondary { border-color: var(--rolle-kant); }
.rounded { border-radius: var(--radius); }        /* 2 px, the only radius the book owns */
.rounded-0 { border-radius: 0; }
.rounded-circle { border-radius: 50%; }
.overflow-auto { overflow: auto; } .overflow-hidden { overflow: hidden; }
.position-relative { position: relative; } .position-absolute { position: absolute; }
.visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.user-select-all { user-select: all; }

/* display. `none` last in every tier — see the note at the head of this section. */
.d-block{display:block} .d-inline{display:inline} .d-inline-block{display:inline-block} .d-flex{display:flex} .d-inline-flex{display:inline-flex} .d-table{display:table} .d-grid{display:grid} .d-none{display:none}
/* `d-grid` is 2 templates wrapping a single button so it fills the line; a bare grid does that. */
.d-grid > .btn { inline-size: 100%; }

@media (min-width: 576px) {
  .col-sm-auto{--kol:auto}
  .text-sm-start{text-align:start} .text-sm-center{text-align:center} .text-sm-end{text-align:end}
  .d-sm-block{display:block} .d-sm-inline{display:inline} .d-sm-inline-block{display:inline-block} .d-sm-flex{display:flex} .d-sm-inline-flex{display:inline-flex} .d-sm-table{display:table} .d-sm-grid{display:grid} .d-sm-none{display:none}
  .mt-sm-0{margin-block-start:var(--rom-0)} .mt-sm-1{margin-block-start:var(--rom-1)} .mt-sm-2{margin-block-start:var(--rom-2)} .mt-sm-3{margin-block-start:var(--rom-3)} .mt-sm-4{margin-block-start:var(--rom-4)} .mt-sm-5{margin-block-start:var(--rom-5)} .mt-sm-auto{margin-block-start:auto}
  .mb-sm-0{margin-block-end:var(--rom-0)} .mb-sm-1{margin-block-end:var(--rom-1)} .mb-sm-2{margin-block-end:var(--rom-2)} .mb-sm-3{margin-block-end:var(--rom-3)} .mb-sm-4{margin-block-end:var(--rom-4)} .mb-sm-5{margin-block-end:var(--rom-5)} .mb-sm-auto{margin-block-end:auto}
  .ms-sm-0{margin-inline-start:var(--rom-0)} .ms-sm-1{margin-inline-start:var(--rom-1)} .ms-sm-2{margin-inline-start:var(--rom-2)} .ms-sm-3{margin-inline-start:var(--rom-3)} .ms-sm-4{margin-inline-start:var(--rom-4)} .ms-sm-5{margin-inline-start:var(--rom-5)} .ms-sm-auto{margin-inline-start:auto}
  .me-sm-0{margin-inline-end:var(--rom-0)} .me-sm-1{margin-inline-end:var(--rom-1)} .me-sm-2{margin-inline-end:var(--rom-2)} .me-sm-3{margin-inline-end:var(--rom-3)} .me-sm-4{margin-inline-end:var(--rom-4)} .me-sm-5{margin-inline-end:var(--rom-5)} .me-sm-auto{margin-inline-end:auto}
}
@media (min-width: 768px) {
  .col-md-auto{--kol:auto}
  .text-md-start{text-align:start} .text-md-center{text-align:center} .text-md-end{text-align:end}
  .d-md-block{display:block} .d-md-inline{display:inline} .d-md-inline-block{display:inline-block} .d-md-flex{display:flex} .d-md-inline-flex{display:inline-flex} .d-md-table{display:table} .d-md-grid{display:grid} .d-md-none{display:none}
  .mt-md-0{margin-block-start:var(--rom-0)} .mt-md-1{margin-block-start:var(--rom-1)} .mt-md-2{margin-block-start:var(--rom-2)} .mt-md-3{margin-block-start:var(--rom-3)} .mt-md-4{margin-block-start:var(--rom-4)} .mt-md-5{margin-block-start:var(--rom-5)} .mt-md-auto{margin-block-start:auto}
  .mb-md-0{margin-block-end:var(--rom-0)} .mb-md-1{margin-block-end:var(--rom-1)} .mb-md-2{margin-block-end:var(--rom-2)} .mb-md-3{margin-block-end:var(--rom-3)} .mb-md-4{margin-block-end:var(--rom-4)} .mb-md-5{margin-block-end:var(--rom-5)} .mb-md-auto{margin-block-end:auto}
  .ms-md-0{margin-inline-start:var(--rom-0)} .ms-md-1{margin-inline-start:var(--rom-1)} .ms-md-2{margin-inline-start:var(--rom-2)} .ms-md-3{margin-inline-start:var(--rom-3)} .ms-md-4{margin-inline-start:var(--rom-4)} .ms-md-5{margin-inline-start:var(--rom-5)} .ms-md-auto{margin-inline-start:auto}
  .me-md-0{margin-inline-end:var(--rom-0)} .me-md-1{margin-inline-end:var(--rom-1)} .me-md-2{margin-inline-end:var(--rom-2)} .me-md-3{margin-inline-end:var(--rom-3)} .me-md-4{margin-inline-end:var(--rom-4)} .me-md-5{margin-inline-end:var(--rom-5)} .me-md-auto{margin-inline-end:auto}
}
@media (min-width: 992px) {
  .col-lg-auto{--kol:auto}
  .text-lg-start{text-align:start} .text-lg-center{text-align:center} .text-lg-end{text-align:end}
  .d-lg-block{display:block} .d-lg-inline{display:inline} .d-lg-inline-block{display:inline-block} .d-lg-flex{display:flex} .d-lg-inline-flex{display:inline-flex} .d-lg-table{display:table} .d-lg-grid{display:grid} .d-lg-none{display:none}
  .mt-lg-0{margin-block-start:var(--rom-0)} .mt-lg-1{margin-block-start:var(--rom-1)} .mt-lg-2{margin-block-start:var(--rom-2)} .mt-lg-3{margin-block-start:var(--rom-3)} .mt-lg-4{margin-block-start:var(--rom-4)} .mt-lg-5{margin-block-start:var(--rom-5)} .mt-lg-auto{margin-block-start:auto}
  .mb-lg-0{margin-block-end:var(--rom-0)} .mb-lg-1{margin-block-end:var(--rom-1)} .mb-lg-2{margin-block-end:var(--rom-2)} .mb-lg-3{margin-block-end:var(--rom-3)} .mb-lg-4{margin-block-end:var(--rom-4)} .mb-lg-5{margin-block-end:var(--rom-5)} .mb-lg-auto{margin-block-end:auto}
  .ms-lg-0{margin-inline-start:var(--rom-0)} .ms-lg-1{margin-inline-start:var(--rom-1)} .ms-lg-2{margin-inline-start:var(--rom-2)} .ms-lg-3{margin-inline-start:var(--rom-3)} .ms-lg-4{margin-inline-start:var(--rom-4)} .ms-lg-5{margin-inline-start:var(--rom-5)} .ms-lg-auto{margin-inline-start:auto}
  .me-lg-0{margin-inline-end:var(--rom-0)} .me-lg-1{margin-inline-end:var(--rom-1)} .me-lg-2{margin-inline-end:var(--rom-2)} .me-lg-3{margin-inline-end:var(--rom-3)} .me-lg-4{margin-inline-end:var(--rom-4)} .me-lg-5{margin-inline-end:var(--rom-5)} .me-lg-auto{margin-inline-end:auto}
}
@media (min-width: 1200px) {
  .col-xl-auto{--kol:auto}
  .text-xl-start{text-align:start} .text-xl-center{text-align:center} .text-xl-end{text-align:end}
  .d-xl-block{display:block} .d-xl-inline{display:inline} .d-xl-inline-block{display:inline-block} .d-xl-flex{display:flex} .d-xl-inline-flex{display:inline-flex} .d-xl-table{display:table} .d-xl-grid{display:grid} .d-xl-none{display:none}
  .mt-xl-0{margin-block-start:var(--rom-0)} .mt-xl-1{margin-block-start:var(--rom-1)} .mt-xl-2{margin-block-start:var(--rom-2)} .mt-xl-3{margin-block-start:var(--rom-3)} .mt-xl-4{margin-block-start:var(--rom-4)} .mt-xl-5{margin-block-start:var(--rom-5)} .mt-xl-auto{margin-block-start:auto}
  .mb-xl-0{margin-block-end:var(--rom-0)} .mb-xl-1{margin-block-end:var(--rom-1)} .mb-xl-2{margin-block-end:var(--rom-2)} .mb-xl-3{margin-block-end:var(--rom-3)} .mb-xl-4{margin-block-end:var(--rom-4)} .mb-xl-5{margin-block-end:var(--rom-5)} .mb-xl-auto{margin-block-end:auto}
  .ms-xl-0{margin-inline-start:var(--rom-0)} .ms-xl-1{margin-inline-start:var(--rom-1)} .ms-xl-2{margin-inline-start:var(--rom-2)} .ms-xl-3{margin-inline-start:var(--rom-3)} .ms-xl-4{margin-inline-start:var(--rom-4)} .ms-xl-5{margin-inline-start:var(--rom-5)} .ms-xl-auto{margin-inline-start:auto}
  .me-xl-0{margin-inline-end:var(--rom-0)} .me-xl-1{margin-inline-end:var(--rom-1)} .me-xl-2{margin-inline-end:var(--rom-2)} .me-xl-3{margin-inline-end:var(--rom-3)} .me-xl-4{margin-inline-end:var(--rom-4)} .me-xl-5{margin-inline-end:var(--rom-5)} .me-xl-auto{margin-inline-end:auto}
}


/* ---- 17. phones ---------------------------------------------------------------------------------
   site.css drops the root size and tightens the page gutter below 641 px; these are the app's own
   adjustments at that width. Nothing here is new design, only the same components with less room. */
@media (max-width: 640px) {
  .container, .container-fluid { padding-inline: 1rem; }
  .card-body { padding: var(--rom-3) 1rem; }
  .alert, .status-card { padding-inline: 1rem .9rem; }
  /* ...but not the dismissible variant. This rule and `.alert-dismissible` above are both one
     class — a media query adds no specificity — and this one comes later, so it used to win and
     collapse the end padding to .9rem while the 2.5rem-wide .btn-close stayed absolutely
     positioned on top of the text. That is the flash banner on every page of BOTH apps
     (SRS/app/templates/base.html:70, article-reader/app/templates/base.html:68). */
  .alert-dismissible { padding-inline-end: 3.2rem; }
  .dialog-innhold, .dialog-valg { padding-inline: 1rem; }
  /* A row of actions on a phone becomes a column of full-width ones rather than three cramped ones. */
  .d-grid .btn, .dialog-valg .btn { inline-size: 100%; }
  /* `column`, not `column-reverse`. The DOM order already puts the confirming action last — where
     the eye finishes and, stacked, where the thumb rests. `column-reverse` renders the DOM-last
     child at the TOP, which put confirm furthest from the thumb and mirrored the desktop order. */
  .dialog-valg { flex-direction: column; }
  .table { font-size: .86rem; }
}


/* ---- 18. dark: the tokens this layer added --------------------------------------------------------
   Everything else in this file is built from site.css's tokens and inverts with them. These three
   are the only colours added here, so these are the only ones that need a counterpart — and they
   are stated here rather than at the point of use precisely so the pairing cannot be forgotten.
   Measured against the dark --papir: --dempet-svak 5.7:1, --ib-kant 8.6:1. */
@media (prefers-color-scheme: dark) {
  :root {
    --dempet-svak: #9a9285;
    --ib-kant: #c9b566;
    --slor: rgba(0, 0, 0, .62);
  }
  /* The floating panel's drop needs more weight on a near-black page to read as a layer at all. */
  .dropdown-menu { box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .5); }
}


/* ---- 19. print ----------------------------------------------------------------------------------
   site.css already hides the book's own chrome and flattens the page to black on white. The app adds
   its own furniture, so the app hides it: nobody prints a navigation bar or a submit button, and a
   collapsed panel that is holding the content being printed has to open. */
@media print {
  .navbar, .breadcrumb, .btn, .btn-group, .dropdown-menu, .navbar-toggler, .btn-close { display: none; }
  .collapse, .collapse:not(.show) { display: block; }
  .card, .alert, .status-card, .table, .tom { break-inside: avoid; }
  .card, .alert, .status-card { background: #fff; border-color: #000; }
  .text-resp-panel { max-block-size: none; overflow: visible; }
  /* [open], not bare `dialog`: a media query adds no specificity, so a plain `dialog` here would
     lose to the `dialog[open]` rule in section 11 and print the modal over the page. */
  dialog, dialog[open] { display: none; }
}