/* Inter — self-hosted variable font (SIL OFL), kept within the CSP's
   font-src 'self'. Covers every weight the stylesheet uses.

   The ?v= is hand-written and has to stay that way: assets/.htaccess caches
   everything under assets/ for a year, PHP versions the URLs it emits on the
   file's mtime, and a stylesheet can do neither. So replacing this font (or
   hero.jpg below) means bumping its number in the same edit — otherwise
   everyone who has the old file keeps it. Editing this file changes the
   stylesheet's own URL, so the bumped inner URL is the one browsers fetch. */
@font-face {
  font-family: "Inter";
  src: url("/assets/fonts/Inter-Variable.woff2?v=1") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Cool marine neutrals — a deep-navy near-black over a faintly blue off-white,
     so the marine accent reads as intentional rather than a default grey. */
  --ink: #10222f;
  --ink-soft: #4b5f6e;
  --ink-faint: #90a2af;
  --paper: #fff;
  --page: #f3f7fb;
  --line: #dde8ef;
  --field-border: #c3d3de;
  --surface-muted: #e8f0f6;

  /* Marine-blue accent — sea rather than sunset. */
  --accent: #1a6ea3;
  --accent-strong: #0f4f78;
  --accent-tint: #e2eef6;

  --danger: #c0392b;
  --danger-tint: #fbeceb;
  --warn: #a4670f;
  --warn-tint: #f7ecd8;
  --ok-tint: #d8ece4;
  --ok-text: #12695a;

  --hero-fallback: #123047;

  /* Events belong to everyone, so they are drawn in one shared colour instead
     of their creator's. A petrol marine: same sea family as the accent, but
     green-leaning enough that a chip never reads as a button, and outside the
     per-user palette so it is never mistaken for someone's stay. */
  --event-color: #0f5563;
  --event-color-fg: #fff;

  --radius: 10px;
  --radius-sm: 6px;
  --content-width: 1180px;

  /* Spacing scale */
  --space-1: .25rem;
  --space-2: .5rem;
  --space-3: .75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  /* Type scale */
  --fs-xs: .75rem;
  --fs-sm: .88rem;
  --fs-base: 1rem;
  --fs-lg: 1.25rem;
  --fs-xl: 1.5rem;

  font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
    Roboto, "Helvetica Neue", Arial, sans-serif;
  color: var(--ink);
  background: var(--paper);
}

* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; }
a { color: inherit; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Only ever rendered by dev.grimsudden.se (see includes/layout.php). Deliberately
   outside the marine palette — it is a warning, not part of the design, and it
   has to be unmistakable at a glance from the real site. */
.env-banner {
  position: sticky;
  top: 0;
  z-index: 100;
  padding: .4rem 1rem;
  background: var(--warn);
  color: #fff;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-align: center;
  text-transform: uppercase;
}

.site-header { background: var(--paper); border-bottom: 1px solid var(--line); }
.site-brand {
  min-height: 114px;
  display: grid;
  place-items: center;
  border-bottom: 1px solid var(--line);
}
.site-brand a {
  display: inline-flex;
  align-items: center;
  gap: .42em;
  color: var(--ink);
  font-size: clamp(2rem, 3vw, 2.55rem);
  font-weight: 700;
  letter-spacing: -.04em;
  text-decoration: none;
}
/* Sized in em so the mark tracks the wordmark's clamp() and the shorter mobile
   header without a media query of its own. It carries no colour: the inline SVG
   is drawn in currentColor, which is what lets one file serve both header states. */
.site-brand__mark {
  width: 1.45em;
  height: 1.45em;
  flex: none;
}
/* Signed in, the brand band shows the hero photo behind "Grimsudden". A dark
   gradient over the image keeps the white wordmark legible on any part of it. */
.site-brand--hero {
  min-height: 220px;
  border-bottom: 0;
  background:
    linear-gradient(180deg, rgb(16 34 47 / 30%), rgb(16 34 47 / 62%)),
    var(--hero-fallback) url('/assets/images/hero.jpg?v=1') center 52% / cover no-repeat;
}
.site-brand--hero a {
  color: #fff;
  text-shadow: 0 2px 18px rgb(9 24 36 / 55%);
}
/* The wordmark's text-shadow does not apply to the SVG, so the mark gets the
   matching drop-shadow — without it, the coastline dissolves into the darker
   parts of the photo. */
.site-brand--hero .site-brand__mark {
  filter: drop-shadow(0 2px 14px rgb(9 24 36 / 55%));
}
.login-link {
  display: block;
  width: max-content;
  margin: 0 auto;
  padding: 1.25rem 1.5rem;
  font-size: .92rem;
  letter-spacing: .12em;
  text-decoration: none;
  text-transform: uppercase;
}
.login-link:hover, .site-nav a:hover { text-decoration: underline; text-underline-offset: .3em; }

/* Skip link: the first thing in the tab order and off-screen until focused,
   which is the only moment it is wanted.

   Focusing it puts it back into the normal flow rather than floating it over
   the top of the page. That is deliberate: anything pinned to the viewport's
   top-left lands on top of the sticky .env-banner, and covering the band that
   says "this is not the real site" — even for as long as one Tab press — is
   the one overlap this page cannot have. In flow it simply appears under it. */
.skip-link {
  position: absolute;
  left: -9999px;
  padding: .7rem 1.1rem;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  background: var(--accent-strong);
  color: #fff;
  font-size: .9rem;
  font-weight: 650;
  text-decoration: none;
}
.skip-link:focus { position: static; display: inline-block; margin: .5rem; }
/* The link's target wraps the whole content region, so focus lands inside it
   and a screen reader reads on from there. No ring: outlining the entire page
   reads as breakage, and the heading the reader was sent to is right there. */
.skip-target:focus, .skip-target:focus-visible { outline: none; }

/* The nav is the bounding box; .nav-links is the row. The two are separate so
   that below 800px the row can become a disclosure panel without the button
   that opens it being laid out as one more link. */
.site-nav {
  max-width: var(--content-width);
  min-height: 76px;
  padding: 0 1.25rem;
  margin: 0 auto;
  display: flex;
  align-items: stretch;
}
.nav-links {
  flex: 1;
  display: flex;
  align-items: stretch;
  /* flex-start, with .nav-links taking the free space, is what holds the routes
     left and the account entry hard right — the job the account link's own
     margin-left:auto used to do while it was still one of these children. */
  justify-content: flex-start;
  gap: clamp(.85rem, 2.4vw, 2.25rem);
}
/* Only ever visible below the breakpoint, and only once assets/js/site.js has
   set data-menu — see the max-width block further down. */
.nav-toggle { display: none; }
.site-nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  font-size: .94rem;
  letter-spacing: .1em;
  text-decoration: none;
  white-space: nowrap;
}
.site-nav a.is-active::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 3px;
  background: var(--accent);
}
.site-nav .account-link { font-size: .78rem; letter-spacing: .06em; display: inline-flex; align-items: center; gap: .5rem; }

/* The unseen-changes badge (#60). Sits between the search field and the account
   entry, and like both of those it lives outside .nav-links so it stays in the
   bar when the routes collapse behind the Meny button. */
.nav-notiser { display: inline-flex; align-items: center; gap: .3rem; padding: 0 .4rem; }
.nav-notiser-icon svg { display: block; width: 20px; height: 20px; }

/* Only the label is hidden, not the field: the input needs an accessible name
   and the bar has no room for a visible one. Not `display: none`, which would
   take it out of the accessibility tree along with the layout. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Sits between the routes and the account link. It does not grow: .nav-links
   already takes the free space, and a search field that stretches with the
   viewport would push the account entry around as the window resizes. */
.nav-search {
  display: flex;
  align-items: center;
  gap: .25rem;
  /* .nav-links stops growing once the bar is full, and then nothing separates
     the last route from the search field — "Historia" ends up against the
     input's edge. The gap between routes is theirs; this one is the field's. */
  margin-left: 1.5rem;
  margin-right: 1rem;
  align-self: center;
}
.nav-search input {
  width: clamp(7rem, 14vw, 12rem);
  padding: .4rem .6rem;
  font: inherit;
  font-size: .82rem;
  color: inherit;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 999px;
}
.nav-search input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.nav-search button {
  padding: .3rem .45rem;
  font-size: .9rem;
  line-height: 1;
  background: none;
  border: 0;
  cursor: pointer;
}
/* The live preview panel, built by assets/js/search.js. Positioned against the
   form, which is why the form is the relative box. Never rendered server-side:
   with no JavaScript there is no panel and the field just submits. */
.nav-search { position: relative; }
.nav-search-results {
  position: absolute;
  top: calc(100% + .4rem);
  right: 0;
  z-index: 20;
  width: min(22rem, 90vw);
  max-height: min(60vh, 28rem);
  overflow-y: auto;
  padding: .35rem;
  text-align: left;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: .6rem;
  box-shadow: 0 12px 28px rgb(16 34 47 / 18%);
}
.nav-search-group {
  margin: .5rem .5rem .25rem;
  font-size: .68rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.nav-search-group:first-child { margin-top: .25rem; }
/* These are inside .site-nav, so the nav's own link rules reach them — the
   tracking meant for six uppercase routes, and (below 800px) the 50px tap
   target. Neither belongs on a list of result titles, so both are undone here
   rather than by narrowing the selectors those rules were written with. */
.nav-search-results a {
  display: block;
  min-height: 0;
  padding: .45rem .5rem;
  font-size: .88rem;
  letter-spacing: normal;
  line-height: 1.3;
  color: var(--ink);
  text-decoration: none;
  border-radius: .35rem;
  /* A long title truncates rather than wrapping the panel into a wall. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nav-search-results a:hover,
.nav-search-results a:focus-visible { background: var(--accent-tint); outline: none; }
.nav-search-empty { margin: .6rem .5rem; font-size: .85rem; color: var(--ink-soft); }
/* Scoped through the panel so it outranks `.nav-search button` above, which
   is (0,1,1) to this rule's (0,1,0) — source order does not decide it, and
   without this the divider and sizing below are silently dropped in favour of
   the magnifier icon's. */
.nav-search-results .nav-search-all {
  display: block;
  width: 100%;
  margin-top: .35rem;
  padding: .5rem;
  font: inherit;
  font-size: .8rem;
  color: var(--accent);
  text-align: center;
  background: none;
  border: 0;
  border-top: 1px solid var(--line);
  cursor: pointer;
}
.account-name-first { display: none; }
.avatar-thumb { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; }
.avatar-thumb.avatar--lg, .avatar-initials.avatar--lg { width: 72px; height: 72px; border: 1px solid var(--line); }
.avatar-field { display: flex; align-items: center; gap: 1rem; margin: .25rem 0 .5rem; }
.avatar-remove { font-weight: 500; }

.site-footer { background: var(--paper); border-top: 1px solid var(--line); padding: 1.5rem 1.25rem; text-align: center; font-size: .85rem; color: var(--ink-soft); }
.site-footer p { margin: 0; }

.hero {
  min-height: calc(100vh - 190px);
  min-height: calc(100svh - 190px);
  background: var(--hero-fallback) url('/assets/images/hero.jpg?v=1') center 54% / cover no-repeat;
}
.private-page {
  max-width: var(--content-width);
  min-height: calc(100vh - 190px);
  padding: clamp(2rem, 5vw, 5rem) 1.25rem;
  margin: 0 auto;
}
.private-page h1 { margin-top: 0; font-size: clamp(2rem, 5vw, 3.5rem); }
/* A reading measure for the pages that are pure running text. At the full
   --content-width a line of body copy runs to around 180 characters, where
   comfortable reading is 65-75. The measure is set on the text rather than on
   the page, so the column still starts where every other page's content does —
   narrowing the whole <main> would centre it away from the left-aligned nav. */
.prose p { max-width: 68ch; }
.auth-page { display: grid; min-height: calc(100vh - 115px); min-height: calc(100svh - 115px); padding: clamp(2rem, 7vw, 6rem) 1.25rem; place-items: start center; background: var(--page); }
.auth-card { width: min(100%, 480px); padding: clamp(1.5rem, 4vw, 2.5rem); border: 1px solid var(--line); background: #fff; }.auth-card h1 { margin: 0; font-size: clamp(2rem, 6vw, 3.25rem); }.auth-intro { margin: .75rem 0 1.5rem; color: var(--ink-soft); line-height: 1.6; }.auth-form { max-width: none; padding: 0; margin: 0; background: transparent; }.auth-form button { width: 100%; }.auth-back { margin: 1.5rem 0 0; font-size: .9rem; }.auth-back a { text-underline-offset: .25em; }

/* The two dead ends — the 404 in index.php and the 500 in includes/errors.php.
   Centred like the auth pages above rather than left-flush like a content page:
   the wordmark, the nav and the footer are all centred, and an error page has
   no content column for a left edge to line up with, so .prose's reason for
   staying left does not apply here. The min-height is .private-page's, which is
   what keeps the footer at the bottom instead of floating mid-viewport. */
.error-page { display: grid; min-height: calc(100vh - 190px); min-height: calc(100svh - 190px); padding: clamp(2rem, 7vw, 6rem) 1.25rem; place-items: start center; background: var(--page); text-align: center; }
/* The mid-render variant is appended to a half-drawn page, so the viewport
   height is already spoken for and the surrounding markup may be anything: keep
   the centring and the measure, drop the height, and rule it off so it reads as
   a notice about the page rather than more of it. */
.error-page--inline { min-height: 0; padding: clamp(1.5rem, 5vw, 3rem) 1.25rem; border-top: 1px solid var(--line); }
.error-card { width: min(100%, 34rem); }
.error-status { margin: 0; color: var(--ink-faint); font-size: clamp(3.5rem, 14vw, 6rem); font-weight: 700; line-height: 1; letter-spacing: -.03em; }
.error-card h1 { margin: .25rem 0 0; font-size: clamp(1.75rem, 5vw, 2.5rem); }
.error-card p { margin: 1rem 0 0; color: var(--ink-soft); line-height: 1.6; }
/* The reported code exists to be read aloud over the phone, which is the one
   job the browser's default monospace does badly. Tinted, spaced wide enough to
   tell 8 from B, and big enough to be worth tapping to select. The right pad is
   short by the letter-spacing, which is applied after the last character too. */
.error-card code { display: inline-block; padding: .25rem .4rem .25rem .6rem; border-radius: var(--radius-sm); background: var(--surface-muted); color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 1.15em; font-weight: 600; letter-spacing: .2em; }
.error-action { display: inline-block; margin-top: 2rem; text-decoration: none; }

.eyebrow { margin: 0 0 .5rem; color: var(--ink-soft); font-size: .75rem; font-weight: 650; letter-spacing: .14em; text-transform: uppercase; }
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.25rem; margin-top: 2rem; }
.module-card, .item-list article, .event-list article { display: block; padding: 1.5rem; border: 1px solid var(--line); border-radius: var(--radius); color: inherit; text-decoration: none; background: #fff; }
.module-card:hover { border-color: var(--accent); box-shadow: 0 8px 24px rgb(164 74 44 / 10%); }
.module-card h2, .item-list h2, .event-list h2 { margin: 0 0 .45rem; font-size: 1.25rem; }
.module-card p, .item-list p, .event-list p { margin: .25rem 0; line-height: 1.6; }
/* The one figure that makes a hub card worth glancing at (#55) — set apart
   from the description above it so the eye lands on it first. */
.module-card .module-card-count { margin-top: .75rem; color: var(--accent-strong); font-weight: 650; }
.item-list, .event-list { display: grid; gap: 1rem; margin-top: 2rem; }
.item-list article, .event-list article { transition: border-color .15s ease, box-shadow .15s ease; }
.item-list article:hover, .event-list article:hover { border-color: var(--accent); box-shadow: 0 8px 24px rgb(164 74 44 / 10%); }
/* Dashboard cards link into the page that owns the item; the linked card is
   marked with :target so it is obvious which one was followed. */
.card-link { display: block; color: inherit; text-decoration: none; }
.card-link:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* Search results (#48) link at a row, not at the page it is on, so every
   linkable row needs the landing treatment — not just the ones that happen to
   be an <article> in one of these two lists. An inventory item sits in a
   ul.tree and a subtask is a div.subtask-card; without naming them here, two
   of the five result kinds land with no highlight and no scroll margin. */
.item-list article, .event-list article, .tree article { scroll-margin-top: 1.5rem; }
.item-list article:target, .event-list article:target, .tree article:target {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}
/* The dashboard is one tile grid rather than a stack of full-width blocks
   (#T26). auto-fit with a minmax floor means one column on a phone and as many
   as fit on a wide screen, with no breakpoint per width; the min() floor is
   what stops a 20rem track from overflowing a 320px viewport. */
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); gap: var(--space-5); margin-top: var(--space-6); align-items: start; }
.attention-panel { background: var(--warn-tint); padding: 1.5rem; border-radius: var(--radius); }
.attention-panel h2 { color: var(--warn); }
.my-tasks { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.my-tasks li { padding: .6rem .8rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff; }
.my-tasks .task-project { color: var(--ink-soft); font-size: .82rem; }
.user-cell { display: inline-flex; align-items: center; gap: .5rem; }
.avatar-initials { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%; font-size: .8rem; font-weight: 700; flex: none; }
.avatar-initials.avatar--lg { font-size: 1.6rem; }
.editor-form { display: grid; gap: 1rem; max-width: 720px; padding: 1.5rem; margin: 1.5rem 0; background: var(--accent-tint); border-radius: var(--radius); }
.editor-form.compact { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); max-width: none; }
.editor-form label { display: grid; gap: .45rem; font-size: .88rem; font-weight: 650; }
.editor-form label span { color: var(--ink-soft); font-weight: 400; }
.editor-form input, .editor-form textarea, .editor-form select { width: 100%; padding: .65rem .7rem; border: 1px solid var(--field-border); border-radius: var(--radius-sm); font: inherit; background: #fff; }
.editor-form input:focus, .editor-form textarea:focus, .editor-form select:focus { border-color: var(--accent); }
.editor-form textarea { min-height: 5rem; resize: vertical; }
/* .error-action is an <a>, not a <button>, but it is the same affordance and
   must not drift from it — so it rides the same two rules rather than copying
   their values (.error-action adds only what an anchor needs, further up). */
button, .error-action { width: max-content; padding: .7rem 1.1rem; border: 1px solid var(--accent-strong); border-radius: var(--radius-sm); background: var(--accent-strong); color: #fff; font: inherit; font-weight: 600; cursor: pointer; transition: background .15s ease; }
button:hover, .error-action:hover { background: var(--accent); border-color: var(--accent); }
.text-button { padding: 0; border: 0; background: transparent; color: var(--accent-strong); text-decoration: underline; }
.notice { max-width: 720px; padding: .9rem 1rem; border-radius: var(--radius-sm); }.notice.error { background:var(--danger-tint); color:var(--danger); }.notice.success { background:var(--ok-tint); color:var(--ok-text); }.notice.warn { background:var(--warn-tint); color:var(--warn); }
.status { display: inline-block; padding: .2rem .5rem; margin-bottom: .75rem; border-radius: 999px; font-size: .72rem; font-weight: 700; text-transform: uppercase; }.status.active,.status.ok { background:var(--ok-tint); color:var(--ok-text); }.status.planned,.status.unknown { background:var(--page); color: var(--ink-soft); }.status.complete { background:var(--surface-muted); color: var(--ink-faint); }.status.attention { background:var(--warn-tint); color:var(--warn); }.status.alert { background:var(--danger-tint); color:var(--danger); }
.create-toggle { margin: 1.5rem 0 0; }
.create-toggle > summary { display: inline-block; cursor: pointer; padding: .7rem 1.1rem; border: 1px solid var(--accent-strong); border-radius: var(--radius-sm); background: var(--accent-strong); color: #fff; font-weight: 650; font-size: .9rem; list-style: none; }
.create-toggle > summary::-webkit-details-marker { display: none; }
.create-toggle > summary:hover { background: var(--accent); border-color: var(--accent); }
.create-toggle[open] > summary { background: var(--accent); border-color: var(--accent); }
.create-toggle .editor-form { margin-top: .75rem; }
/* Collapsible list sections (calendar's upcoming/past, the dashboard's lists):
   the heading is the summary, so the fold keeps the h1 → h2 → h3 hierarchy. */
.fold-section { margin-top: 2rem; border-top: 1px solid var(--line); }
.fold-section > summary { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .9rem 0; cursor: pointer; list-style: none; }
.fold-section > summary::-webkit-details-marker { display: none; }
.fold-section > summary::after { content: '▾'; color: var(--ink-soft); font-size: .9rem; line-height: 1; transition: transform .15s ease; }
.fold-section[open] > summary::after { transform: rotate(180deg); }
.fold-section > summary:hover h2 { color: var(--accent-strong); }
.fold-section > summary:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.fold-section > summary h2 { margin: 0; font-size: 1.1rem; }
.fold-count { color: var(--ink-soft); font-weight: 400; }
.fold-section .event-list, .fold-section .item-list { margin-top: 0; }
/* The admin panel folds a whole page of sections (#133), so summaries there
   carry an h3 (a section) or an h4 (one table inside it) instead of an h2, and
   the folds nest one level. The sizes are the ones those headings had before
   they moved into a summary, so the step down the page reads the same. */
.fold-section > summary h3, .fold-section > summary h4 { margin: 0; }
.fold-section > summary h3 { font-size: 1rem; }
.fold-section > summary h4 { font-size: .95rem; }
.fold-section > summary:hover h3, .fold-section > summary:hover h4 { color: var(--accent-strong); }
.fold-section .fold-section { margin-top: 1rem; }
.fold-section > .table-wrap { margin-top: 1rem; }
/* A search box over one table's rows, revealed by assets/js/table-filter.js. */
.table-filter { margin-top: 1.25rem; }
.table-filter label { display: grid; gap: .35rem; max-width: 22rem; font-size: .82rem; font-weight: 650; color: var(--ink-soft); }
.table-filter input { padding: .5rem .6rem; border: 1px solid var(--field-border); border-radius: var(--radius-sm); font: inherit; background: #fff; }
.table-filter-count { margin: .5rem 0 0; font-size: .82rem; color: var(--ink-soft); }
.table-filter + .table-wrap { margin-top: 1rem; }
/* The browser's own [hidden] rule loses to any display the table sets, and a
   filtered-out row must not merely be transparent. */
.table-wrap tr[hidden] { display: none; }
/* A date reads as one thing; wrapping it mid-timestamp made the user table's
   rows different heights depending on the name beside them. */
.table-wrap .date-cell, .table-wrap .phone-cell { white-space: nowrap; }
/* A sortable column heading. assets/js/table-sort.js makes the button, so the
   heading is plain text until something can actually act on a click; the styling
   here undoes the site's button look and keeps the header's own. */
.table-wrap th .sort-button { display: inline-flex; align-items: center; gap: .35rem; padding: 0; border: 0; border-radius: 0; background: transparent; color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }
.table-wrap th .sort-button:hover { background: transparent; color: var(--accent-strong); }
.table-wrap th .sort-button:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.table-wrap th .sort-button::after { content: '↕'; font-size: .9em; color: var(--ink-faint); }
.table-wrap th[aria-sort] .sort-button::after { color: var(--accent-strong); }
.table-wrap th[aria-sort="ascending"] .sort-button::after { content: '▲'; font-size: .7em; }
.table-wrap th[aria-sort="descending"] .sort-button::after { content: '▼'; font-size: .7em; }
/* Calendar colour key: one swatch per person who has a stay. */
.calendar-legend { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1.5rem; }
.legend-item { display: inline-flex; align-items: center; gap: .4rem; font-size: .82rem; color: var(--ink-soft); }
.legend-swatch { display: inline-block; width: .8rem; height: .8rem; border-radius: 2px; flex: none; }
/* Who is coming to an event, and the sign-up button when it is open to all. */
.event-attendees { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; margin: .6rem 0 .4rem; }
.event-attendees-label { font-size: .82rem; color: var(--ink-soft); }
.event-attendees .assignee-badge { margin: 0; }
.event-attendee-fields { grid-column: 1 / -1; display: grid; gap: .6rem; }
.event-attendee-fields[hidden] { display: none; }
/* The grid display above (and on .editor-form label) beats the browser's own
   [hidden] rule, so a hidden field needs saying twice. */
.editor-form label[hidden] { display: none; }
button.secondary { background: transparent; color: var(--accent-strong); }
button.secondary:hover { background: var(--accent-tint); border-color: var(--accent-strong); }
.water-param-row { grid-column: 1 / -1; display: flex; gap: .6rem; align-items: end; }
.water-param-row > label:first-child { flex: 1 1 auto; }
.water-param-row .water-status { flex: 0 0 12rem; }
.subtasks { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.subtasks h3 { margin: 0 0 .5rem; font-size: 1rem; }
.subtask-empty { margin: 0 0 .5rem; color: var(--ink-soft); font-size: .9rem; }
.subtask-list { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; gap: .5rem; }
.subtask-row { display: flex; align-items: baseline; gap: .5rem; }
.subtask-row .toggle-form { margin: 0; }
.subtask-check { width: auto; padding: 0; border: 0; background: transparent; color: var(--accent-strong); font-size: 1.15rem; line-height: 1; cursor: pointer; }
.subtask-check:hover { background: transparent; color: var(--accent); }
.subtask-main { flex: 1; }
.subtask-title.done { text-decoration: line-through; color: var(--ink-faint); }
.subtask-edit { margin-top: .35rem; }
.subtask-edit > summary { cursor: pointer; font-size: .82rem; color: var(--accent-strong); }
.subtask-edit .editor-form { margin: .5rem 0 0; }
.assignee-badge { display: inline-block; padding: .1rem .5rem; margin: 0 .25rem .2rem 0; border-radius: 999px; font-size: .72rem; font-weight: 650; vertical-align: middle; }
.event-time { color:var(--ink-soft); font-size:.86rem; }.checkbox { display:flex!important; align-items:center; grid-template-columns:auto 1fr; }.checkbox input { width:auto!important; }.admin-edit { margin-top:3rem; }.admin-edit summary { cursor:pointer; font-weight:650; }.admin-action { display:inline-block; margin:0 .35rem .35rem 0; }.admin-action summary { cursor:pointer; font-weight:650; padding:.45rem .65rem; border:1px solid var(--accent-strong); border-radius: var(--radius-sm); background:var(--accent-strong); color:#fff; font-size:.82rem; }.admin-action[open] summary { background:var(--accent); border-color: var(--accent); }.admin-action .inline-form { display:flex; gap:.35rem; align-items:flex-start; margin:0; background:transparent; padding:0; }.admin-note { margin-top:2rem; color:var(--ink-soft); }
.table-wrap { overflow-x: auto; margin-top: 2rem; }.table-wrap table { width: 100%; border-collapse: collapse; background: #fff; }.table-wrap th, .table-wrap td { padding: .85rem; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }.table-wrap th { font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }.inline-form { display: inline-block; margin: 0 .35rem .35rem 0; }.inline-form button { padding: .45rem .65rem; font-size: .82rem; }.danger { border-color: var(--danger); background: var(--danger); }

/* Project management: filters, progress and the step-based subtask plan. */
.filter-bar { display: flex; flex-wrap: wrap; gap: .75rem; align-items: end; margin-top: 1.5rem; }
.filter-bar label { display: grid; gap: .35rem; font-size: .82rem; font-weight: 650; color: var(--ink-soft); }
.filter-bar input, .filter-bar select { padding: .5rem .6rem; border: 1px solid var(--field-border); border-radius: var(--radius-sm); font: inherit; background: #fff; }
.filter-bar button { padding: .55rem .9rem; font-size: .85rem; }
.filter-bar .text-button { align-self: center; }
.badge { display: inline-block; padding: .1rem .5rem; margin: 0 .25rem .2rem 0; border-radius: 999px; font-size: .72rem; font-weight: 650; vertical-align: middle; }
.blocked-badge { background: var(--danger-tint); color: var(--danger); }
/* A standing identity/permission indicator, not a warning — deliberately not
   styled like .blocked-badge's alarm colours. */
.badge-admin { background: var(--accent-tint); color: var(--accent-strong); }
/* The unseen count (#60), styled as an alert rather than as .badge-admin's
   accent — it is a number to act on, not a label saying who you are. The colour
   is decoration on top of the number, never the thing carrying the meaning.
   After .badge, not before it: same specificity, so the later rule is the one
   whose margin survives. */
.badge-unseen { margin: 0; background: var(--danger-tint); color: var(--danger); }
.project-meta { display: flex; flex-wrap: wrap; gap: .35rem 1rem; margin: .5rem 0 .75rem; font-size: .9rem; color: var(--ink-soft); align-items: center; }
.depends-hint { display: inline-block; margin-left: .35rem; font-size: .78rem; color: var(--ink-faint); font-style: italic; }
.progress-bar { position: relative; height: 1.1rem; margin: 0 0 1rem; background: var(--surface-muted); border-radius: 999px; overflow: hidden; }
.progress-bar__fill { position: absolute; inset: 0 auto 0 0; background: var(--ok-text); border-radius: 999px; transition: width .2s ease; }
.progress-bar__label { position: relative; display: block; text-align: center; font-size: .7rem; font-weight: 700; line-height: 1.1rem; color: var(--ink); }
/* Advisory password-strength meter (#49). Colour alone never carries the
   state: the bar's width and the label's text both change with it too. Good
   and strong share --ok-text on purpose — the label and width tell them apart. */
.password-strength { margin: .4rem 0 1rem; }
.password-strength .progress-bar { height: .6rem; margin: 0 0 .3rem; }
.password-strength__label { margin: 0; font-size: .8rem; color: var(--ink-soft); }
.password-strength--weak .progress-bar__fill { background: var(--danger); }
.password-strength--fair .progress-bar__fill { background: var(--warn); }
.plan-step-label { margin: .9rem 0 .4rem; font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }
/* Subtasks are stacked cards that echo the project cards above them. */
.plan-step { display: grid; gap: .6rem; margin-bottom: .25rem; }
.subtask-card { padding: 1rem 1.15rem; border: 1px solid var(--line); border-radius: var(--radius); background: #fff; transition: border-color .15s ease, box-shadow .15s ease; }
.subtask-card:hover { border-color: var(--accent); box-shadow: 0 8px 24px rgb(164 74 44 / 10%); }
.subtask-card.done { background: var(--page); }
.subtask-card.blocked { border-color: var(--danger); background: var(--danger-tint); }
/* A search result links at the subtask, so the one that was followed says so.
   After .done/.blocked deliberately: those match at the same specificity, and
   landing on a blocked subtask should still show which one you landed on. */
.subtask-card { scroll-margin-top: 1.5rem; }
.subtask-card:target { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint); }
.subtask-assignees { display: inline; }
.subtask-notes { margin: .25rem 0; font-size: .86rem; color: var(--ink-soft); white-space: normal; }
.assignee-field { margin: 0; padding: .6rem .8rem; border: 1px solid var(--field-border); border-radius: var(--radius-sm); background: #fff; }
.assignee-field legend { padding: 0 .35rem; font-size: .82rem; font-weight: 650; color: var(--ink-soft); }
.assignee-field .assignee-search { width: 100%; margin-bottom: .4rem; padding: .4rem .55rem; border: 1px solid var(--field-border); border-radius: var(--radius-sm); font: inherit; font-size: .85rem; }
/* A scrollable checklist — the member list can grow long. The extra .assignee-field
   qualifier is needed to beat the generic `.editor-form label` grid/bold rule. */
.assignee-field .assignee-checks { display: flex; flex-direction: column; gap: .1rem; max-height: 11rem; overflow-y: auto; padding-right: .35rem; }
.assignee-field .assignee-check { display: flex; flex-direction: row; align-items: center; gap: .55rem; padding: .3rem .4rem; border-radius: var(--radius-sm); font-size: .95rem; font-weight: 400; cursor: pointer; }
.assignee-field .assignee-check:hover { background: var(--accent-tint); }
.assignee-field .assignee-check input { width: 1.05rem; height: 1.05rem; flex: none; margin: 0; cursor: pointer; }
/* "Alla" ticks everyone at once, so it reads as a header for the list below. */
.assignee-field .assignee-all { margin-bottom: .25rem; padding-bottom: .4rem; border-bottom: 1px solid var(--line); border-radius: 0; font-weight: 650; }
.assignee-empty { margin: 0; font-size: .85rem; color: var(--ink-soft); }
.assignee-badge.unassigned { background: var(--surface-muted); color: var(--ink-faint); font-style: italic; }
/* Adding subtasks: a "+ Lägg till deluppgift" button opens a popup (projects.js). */
.subtask-create-field { display: grid; gap: .5rem; }
.subtask-create-field .field-label { font-size: .88rem; font-weight: 650; }
.add-subtask-row { width: max-content; margin-top: .5rem; padding: .5rem .9rem; border: 1px solid var(--accent-strong); border-radius: var(--radius-sm); background: #fff; color: var(--accent-strong); font: inherit; font-weight: 600; font-size: .85rem; cursor: pointer; }
.add-subtask-row:hover { background: var(--accent-tint); border-color: var(--accent); }
/* Subtasks queued on the create form before the project is saved. */
.pending-subtasks { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
.pending-subtask { display: flex; align-items: center; gap: .6rem; padding: .45rem .65rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff; }
.pending-subtask-label { flex: 1; font-size: .9rem; }
.remove-pending { width: auto; padding: 0 .5rem; border: 0; background: transparent; color: var(--danger); font-size: 1.15rem; line-height: 1; cursor: pointer; }
.remove-pending:hover { background: transparent; color: var(--danger); }
/* Subtask-creation popups. */
.subtask-dialog { width: min(30rem, 92vw); padding: 0; border: 1px solid var(--line); border-radius: var(--radius); background: #fff; color: var(--ink); }
.subtask-dialog::backdrop { background: rgb(36 28 25 / 45%); }
.subtask-dialog .editor-form { margin: 0; max-width: none; }
.subtask-dialog h3 { margin: 0; font-size: 1.15rem; }
.dialog-actions { display: flex; align-items: center; gap: 1rem; }
/* Checklist step-by-step guide: numbered ordered steps. */
.guide-heading { margin: 1.1rem 0 .5rem; font-size: .95rem; }
.guide-steps { margin: 0; padding: 0; list-style: none; counter-reset: step; display: grid; gap: .5rem; }
.guide-steps > li { position: relative; display: flex; align-items: baseline; gap: .7rem; padding: .55rem .7rem .55rem .5rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--paper); }
.guide-steps > li::before { counter-increment: step; content: counter(step); flex: none; display: grid; place-items: center; width: 1.6rem; height: 1.6rem; border-radius: 999px; background: var(--accent-tint); color: var(--accent-strong); font-size: .8rem; font-weight: 700; }
.guide-step-text { flex: 1; line-height: 1.5; }
.guide-steps .inline-form { margin: 0; }
/* Inventory attachment link. */
.attachment { margin: .5rem 0 0; }
.attachment a { color: var(--accent-strong); font-weight: 600; text-decoration: none; }
.attachment a:hover { text-decoration: underline; text-underline-offset: .2em; }

/* Documents and figures attached to a project (#191) or to a checklist step
   (#192): one card per document, holding its preview, its metadata line, the
   note saying what it is for, and the links to open or download it. */
.project-documents { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.project-documents h3 { margin: 0 0 .5rem; font-size: 1rem; }
.doc-attachments { display: grid; gap: .6rem; margin: .5rem 0 0; }
.doc-attachment { padding: .6rem .75rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--paper); }
.doc-attachment .attachment { margin: 0; }
.doc-attachment .event-time { margin: .15rem 0 0; }
.doc-attachment-note { margin: .35rem 0 0; font-size: .92rem; }
.doc-attachment-actions { margin: .35rem 0 0; font-size: .86rem; }
.doc-attachment .inline-form { margin: .35rem 0 0; }
/* An image IS its own preview — capped so a phone photo cannot push a project
   card off the screen, and never wider than the card it sits in. */
.doc-thumb { display: block; margin-bottom: .5rem; }
.doc-thumb img { display: block; max-width: 100%; max-height: 11rem; border: 1px solid var(--line); border-radius: var(--radius-sm); }
/* A guide step's own row (text, delete) stays on one line; the attachments and
   the attach form it can now carry wrap to full width underneath it. */
.guide-steps > li { flex-wrap: wrap; }
.guide-steps > li > .doc-attachments, .guide-steps > li > details { flex: 1 0 100%; }

/* Home status bar: El and the latest water test, side by side. */
.status-bar-section { margin: 1.5rem 0 2rem; }
/* Each tile needs ~30rem to keep its text, pill and toggle on one line, so the
   pair only splits into two columns when both halves are that wide. */
.status-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 30rem), 1fr)); gap: 1rem; align-items: start; }
/* The admin panel's storage tiles carry no pill and no toggle, so they need far
   less than 30rem — without this the three of them lay out as two and a stray. */
.status-bar.is-compact { grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); }
.status-tile { display: flex; align-items: center; gap: 1rem; width: 100%; padding: 1.05rem 1.15rem; background: #fff; border: 1px solid var(--line); border-left: 5px solid var(--tile-line, var(--line)); border-radius: var(--radius); text-align: left; }
.status-tile.is-ok { --tile-line: #7cc0a9; }
.status-tile.is-alert { --tile-line: #e39b93; }
.tile-icon { flex: none; width: 42px; height: 42px; display: grid; place-items: center; border-radius: 50%; background: var(--accent-tint); color: var(--accent-strong); }
.tile-icon svg { width: 22px; height: 22px; display: block; }
.tile-body { flex: 1; min-width: 0; }
.tile-label { margin: 0; font-size: .72rem; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; color: var(--ink-soft); }
.tile-value { margin: .1rem 0 0; font-size: 1.15rem; font-weight: 650; }
.tile-meta { margin: .15rem 0 0; font-size: .8rem; color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-pill { flex: none; display: inline-flex; align-items: center; gap: .45rem; padding: .3rem .7rem; border-radius: 999px; font-size: .78rem; font-weight: 650; }
.status-pill .dot { width: .55rem; height: .55rem; border-radius: 50%; background: currentColor; }
.status-pill.is-ok { background: var(--ok-tint); color: var(--ok-text); }
.status-pill.is-alert { background: var(--danger-tint); color: var(--danger); }
/* The Vatten tile is a disclosure control that opens the full water test. */
.water-tile { margin: 0; }
.water-tile > summary { cursor: pointer; list-style: none; transition: border-color .15s ease; }
.water-tile > summary::-webkit-details-marker { display: none; }
.water-tile > summary:hover { border-color: var(--accent); }
.water-tile > summary:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.tile-chevron { flex: none; color: var(--accent-strong); font-size: .82rem; font-weight: 650; white-space: nowrap; }
.tile-chevron::after { content: "Visa värden ▾"; }
.water-tile[open] .tile-chevron::after { content: "Dölj värden ▴"; }
/* Full-width water table below the row, shown when Vatten is open. */
.water-panel { grid-column: 1 / -1; display: none; }
.status-bar:has(.water-tile[open]) .water-panel { display: block; }
@supports not (selector(:has(*))) { .water-panel { display: block; } }
.water-panel .table-wrap { margin-top: 0; }
.water-assessment { margin-bottom: 1rem; font-size: .9rem; }
.water-assessment p { margin: .15rem 0; }
.water-assessment-text { margin-top: .5rem; color: var(--ink-soft); white-space: normal; }
.water-panel caption { text-align: left; padding: 0 0 .5rem; font-size: .74rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); font-weight: 700; }
/* On a phone the pill and the "Visa värden" toggle no longer fit beside the
   text — none of them may shrink, so .tile-body was collapsing and clipping the
   value. Move them to a second row instead. */
@media (max-width: 34rem) {
  .status-tile { display: grid; grid-template-columns: auto 1fr auto; column-gap: .8rem; row-gap: .6rem; align-items: center; }
  .tile-icon { grid-area: 1 / 1 / 2 / 2; }
  .tile-body { grid-area: 1 / 2 / 2 / 4; }
  .status-pill { grid-area: 2 / 2 / 3 / 3; justify-self: start; }
  .tile-chevron { grid-area: 2 / 3 / 3 / 4; justify-self: end; }
  .tile-meta { white-space: normal; }
}

/* Inventory tree: items grouped under a reference (folder), or standing alone. */
.tree, .tree ul { list-style: none; margin: 0; padding: 0; }
.tree { margin-top: 2rem; display: grid; gap: 1.25rem; }
.tree-ref { display: inline-block; font-weight: 650; font-size: 1.05rem; padding: .3rem .7rem; border-radius: var(--radius-sm); background: var(--accent-tint); color: var(--accent-strong); letter-spacing: .01em; }
.tree-branch > ul { margin: .75rem 0 0 .7rem; padding-left: 1.25rem; border-left: 2px solid var(--line); display: grid; gap: .75rem; }
.tree-branch > ul > .tree-leaf { position: relative; }
.tree-branch > ul > .tree-leaf::before { content: ""; position: absolute; left: -1.25rem; top: 1.6rem; width: 1.05rem; border-top: 2px solid var(--line); }
.tree article { display: block; padding: 1.25rem; border: 1px solid var(--line); border-radius: var(--radius); background: #fff; transition: border-color .15s ease, box-shadow .15s ease; }
.tree article:hover { border-color: var(--accent); box-shadow: 0 8px 24px rgb(164 74 44 / 10%); }
.tree article h3 { margin: 0 0 .35rem; font-size: 1.15rem; }
.tree article p { margin: .25rem 0; line-height: 1.6; }

/* Month navigation above the calendar grid, built in pages/kalender/index.php. */
.cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.cal-nav button { padding: .5rem 1rem; white-space: nowrap; }
.cal-nav h2 { margin: 0; font-size: 1.3rem; }

@media (max-width: 800px) {
  .site-brand { min-height: 90px; }
  .site-nav { min-height: auto; flex-wrap: wrap; padding: .25rem .5rem; }
  .nav-links { flex-wrap: wrap; gap: 0; }
  .site-nav a { min-height: 50px; padding: 0 .65rem; font-size: .78rem; letter-spacing: .05em; }
  /* Who you are stays in the bar: Meny on the left, the account entry hard
     right, and the routes in a panel underneath when the button opens them.
     The order swap is what puts that panel below both rather than between
     them — .nav-links takes a full row of its own, so without it the account
     entry would be pushed onto a third line. The DOM keeps the account last,
     so tabbing still runs through the routes first. */
  .site-nav .account-link { margin-left: auto; padding-right: 0; }
  .site-nav .nav-links { order: 1; }
  /* Stays in the bar beside Meny rather than joining the panel — the whole
     point of keeping it out of .nav-links. It shrinks instead of wrapping, so
     the bar holds Meny, the field and the account entry on one row. */
  .nav-search { order: 0; margin-right: .5rem; margin-left: auto; }
  .nav-search input { width: clamp(5rem, 26vw, 10rem); }
  /* The panel is anchored to the field on a wide screen, which is exactly wrong
     on a narrow one: the field sits hard right, so a panel wider than it runs
     off the left edge. Dropping .nav-search out of the positioning chain hands
     the job to .site-nav, and the panel spans the bar instead. */
  .nav-search { position: static; }
  .site-nav { position: relative; }
  .nav-search-results {
    top: calc(100% + .25rem);
    right: .5rem;
    left: .5rem;
    width: auto;
  }
  .nav-search-results a { min-height: 0; padding: .5rem; font-size: .85rem; letter-spacing: normal; }
  .hero { min-height: calc(100svh - 142px); }

  /* The disclosure menu. Gated on data-menu, which only exists once
     assets/js/site.js has run: without it the button never appears and the
     links are never hidden, so the row above still works with no JavaScript. */
  .site-nav[data-menu] .nav-toggle { display: inline-flex; align-items: center; gap: .5rem; margin: .35rem 0; padding: .5rem .9rem; font-size: .85rem; }
  .site-nav[data-menu] .nav-links { flex: 1 0 100%; flex-direction: column; align-items: stretch; }
  .site-nav[data-menu="closed"] .nav-links { display: none; }
  .nav-toggle-icon { font-size: 1.1rem; line-height: 1; }

  /* A long name has less room once the nav wraps, so only the first name
     shows — the full name is still the account link's accessible name. */
  .account-name-full { display: none; }
  .account-name-first { display: inline; }
}

@media (max-width: 30rem) {
  /* The first name and the Admin chip stay down here. They used to be dropped
     for the avatar alone, from when the account entry was inside the menu and
     this only decided what an opened panel looked like. Now it is the one thing
     in the bar saying who is signed in, which is worth the width.

     What gives instead is the spacing around it, and the badge, which is a
     label rather than a word being read. */
  .site-nav .account-link { gap: .35rem; }
  .badge-admin { padding: .1rem .3rem; font-size: .6rem; }
  /* Same trade as .badge-admin above: the bell and its number stay, the
     padding around them goes. Qualified with .site-nav because the mobile
     `.site-nav a` rule above sets padding at a higher specificity, and a bare
     .nav-notiser would lose to it. */
  .site-nav .nav-notiser { padding: 0 .15rem; }
  .badge-unseen { padding: .1rem .3rem; font-size: .6rem; }

  /* On a phone the spelled-out month buttons wrap onto two lines and dwarf
     the month heading, so drop the wording and keep the arrows. */
  .cal-nav .cal-nav-label { display: none; }
  .cal-nav button { padding: .4rem .8rem; font-size: 1.1rem; line-height: 1.2; }
  .cal-nav h2 { font-size: 1.05rem; }
}

/* ---------------------------------------------------------------------------
   What used to be style="" attributes.

   The CSP allows no inline styles at all (see .htaccess), and style-src does
   not care whether an attribute sets a whole rule or one custom property —
   the attribute is what gets blocked. So nothing this site renders may carry
   one, and everything below is where those declarations went.

   Values that genuinely vary per element — a progress percentage, a calendar
   bar's position — are written through the CSSOM from JavaScript instead,
   which style-src does not govern. See assets/js/site.js and calendar.js.
   --------------------------------------------------------------------------- */
.muted { color: var(--ink-soft); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-6 { margin-top: var(--space-6); }
.mt-7 { margin-top: var(--space-7); }
.auth-hint { margin-top: var(--space-4); font-size: .85rem; color: var(--ink-soft); }
.calendar-note { margin-top: .6rem; font-size: .8rem; color: var(--ink-soft); }

/* The calendar subscription panel. The URL field is deliberately wide and
   monospaced: it is 64 hex characters plus a host, and somebody checking that
   they copied the right one has nothing but the shape of it to go on. It stays
   a real <input readonly> rather than text so selecting it works everywhere,
   including the phones that ignore a click-to-select on a <code>. */
.feed-url { display: flex; align-items: center; gap: .5rem; margin-top: var(--space-3); }
.feed-url-input { flex: 1 1 auto; min-width: 0; padding: .45rem .6rem; border: 1px solid var(--field-border); border-radius: var(--radius-sm); background: #fff; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .8rem; }
.feed-hint { margin-top: var(--space-3); font-size: .85rem; color: var(--ink-soft); }
.feed-legend { margin: 0; font-size: .85rem; color: var(--ink-soft); }
.feed-form { margin-top: var(--space-4); }
/* Stacked, not .editor-form.compact's auto-fit columns: these four rows are one
   short sentence, one checkbox, a person list and a button, and side by side
   they read as four unrelated controls rather than one set of choices. The
   button sits at its own width instead of stretching down the whole row. */
.feed-preferences { gap: .75rem; max-width: 420px; }
.feed-preferences button { justify-self: start; }
/* Sits directly above a .table-wrap, whose own 2rem top margin it cancels.
   A heading inside a <summary> is sized by the .fold-section rules instead. */
.table-heading { margin: var(--space-6) 0 -1rem; font-size: .95rem; }
/* The heading of a card in .item-list / .event-list — smaller than the h2 rule
   those lists set, and specific enough to beat it. */
.item-list .card-title, .event-list .card-title { margin: var(--space-1) 0; font-size: 1.05rem; }
/* The admin user table's reference columns — E-post, Mobil, Skapad (#57). The
   table wants ~670px and a phone gives it ~335, and Åtgärder being last means
   the buttons are what falls off the end. Dropping these three brings it inside
   the viewport, so the controls are reachable without scrolling sideways.

   The media query is the default and works with no JavaScript; the explicit
   [data-columns] states come from assets/js/table-columns.js and win in both
   directions, at any width, because they are declared after it. Hidden here
   means display:none, so the row keeps the cell — assets/js/table-filter.js
   reads cell text, and searching by e-mail goes on working while the e-mail
   column is not on screen. */
@media (max-width: 40rem) {
  table:not([data-columns="all"]) .col-extra { display: none; }
}
table[data-columns="compact"] .col-extra { display: none; }
.column-toggle { margin-top: .75rem; font-size: .82rem; }
.column-toggle + .table-wrap { margin-top: 1rem; }

/* The fold-out actions in that table. A summary is display:list-item, so a label
   that wraps leaves its marker stranded on the first line — "Nollställ lösenord"
   does exactly that once the cell is narrow. Laying the summary out as a flex
   row with a marker of our own keeps the two together at any width. */
.admin-action summary { display: flex; align-items: center; gap: .4rem; list-style: none; }
.admin-action summary::-webkit-details-marker { display: none; }
.admin-action summary::before { content: '\25B8'; font-size: .9em; }
.admin-action[open] summary::before { content: '\25BE'; }
.row-self { display: inline-block; margin-right: .35rem; color: var(--ink-soft); }

@media (max-width: 40rem) {
  /* Four controls that will not sit beside each other on a phone. Stacked, the
     cell is as wide as its widest control instead of their sum, and an opened
     fold puts Spara under the field rather than off the right-hand edge. */
  .row-actions { display: flex; flex-direction: column; align-items: stretch; gap: .4rem; }
  .row-actions .inline-form, .row-actions .admin-action { display: block; margin: 0; }
  .row-actions button { width: 100%; }
  .row-actions .admin-action summary { justify-content: center; }
  .row-actions .admin-action .inline-form { flex-wrap: wrap; gap: .4rem; margin-top: .4rem; }
  .row-actions .admin-action .inline-form input { flex: 1 1 100%; min-width: 0; }
}

/* /kontakter/ renders the hand-maintained contacts as one .item-list per
   category under an <h3>. The list's own 2rem top margin is meant to separate
   it from the text above it, which the heading now does instead — without this
   every group heading floats a third of the way up its own gap. */
.category-group { margin-top: var(--space-2); }
/* The name row of a directory card: an avatar chip beside the name, so the
   heading has to lay itself out as a flex row rather than inherit .user-cell's
   inline-flex, which would leave the chip and the text off each other's
   baseline at the larger heading size. */
.item-list h3.user-cell { display: flex; }

/* One class per entry in includes/helpers.php's user_swatch_palette(), so a
   person's colour reaches the page as a class instead of a style attribute.
   user_swatch_class() emits the name; tests/HelpersTest.php fails if the two
   lists drift apart. --swatch-tint is the same colour at 13% alpha, used by
   the calendar's presence bands. */
.swatch-0 { --swatch-bg: #2a78d6; --swatch-fg: #0b0b0b; --swatch-tint: #2a78d622; }
.swatch-1 { --swatch-bg: #008300; --swatch-fg: #ffffff; --swatch-tint: #00830022; }
.swatch-2 { --swatch-bg: #e87ba4; --swatch-fg: #0b0b0b; --swatch-tint: #e87ba422; }
.swatch-3 { --swatch-bg: #eda100; --swatch-fg: #0b0b0b; --swatch-tint: #eda10022; }
.swatch-4 { --swatch-bg: #1baf7a; --swatch-fg: #0b0b0b; --swatch-tint: #1baf7a22; }
.swatch-5 { --swatch-bg: #eb6834; --swatch-fg: #0b0b0b; --swatch-tint: #eb683422; }
.swatch-6 { --swatch-bg: #4a3aa7; --swatch-fg: #ffffff; --swatch-tint: #4a3aa722; }
.swatch-7 { --swatch-bg: #e34948; --swatch-fg: #0b0b0b; --swatch-tint: #e3494822; }
.avatar-initials, .assignee-badge { background: var(--swatch-bg, var(--surface-muted)); color: var(--swatch-fg, var(--ink-faint)); }
/* No swatch class means "not a person": an event belongs to everyone, so the
   key beside it is drawn in the shared event colour. */
.legend-swatch { background: var(--swatch-bg, var(--event-color)); }
.event-swatch { vertical-align: middle; margin-right: .35rem; }
/* A stay on the dashboard, edged in whose stay it is. Scoped to the tile so it
   outranks the `border` shorthand on `.tile-cards article`, which would
   otherwise reset the left edge back to a hairline. */
.tile-cards .presence-card { border-left: 4px solid var(--swatch-bg, var(--line)); }

/* Konto-import: the format explainer and the per-row result table. The table
   itself is a plain .table-wrap now, like every other table on the site. */
.import-intro { margin-bottom: var(--space-6); }
.import-intro h2 { margin-top: 0; }
.import-intro h2, .import-results h2 { font-size: 1.1rem; }
.import-sample { padding: var(--space-4); background: var(--surface-muted); overflow-x: auto; font-size: .85rem; }
.import-results { margin-top: var(--space-6); }
.import-results .table-wrap { margin-top: var(--space-4); }
.import-ok { color: var(--ok-text); font-weight: 600; }
.import-fail { color: var(--danger); font-weight: 600; }

/* The admin mail sender's review step (#58). The body preview WRAPS rather than
   scrolling sideways like .import-sample: this is prose an admin is proofreading
   before an irreversible send, and a line that runs off the edge is a line that
   does not get read. */
.mail-preview { padding: var(--space-4); background: var(--surface-muted); font-size: .9rem; white-space: pre-wrap; overflow-wrap: anywhere; }
.mail-recipients { max-width: 720px; margin: 0 0 var(--space-4); padding-left: 1.25rem; }
.mail-recipients li { margin-bottom: .2rem; }

/* The looked-up error entry in the admin panel (#115). The trace is the one
   thing on the site that must not wrap: a stack frame is read by scanning down
   the left edge, and folding the paths turns that into a wall. So it scrolls
   sideways in its own box instead, like .import-sample. */
.log-entry-head { margin-bottom: var(--space-2); font-weight: 600; }
.log-trace { padding: var(--space-4); background: var(--surface-muted); overflow-x: auto; font-size: .8rem; line-height: 1.5; }

/* ---------------------------------------------------------------------------
   The month grid, built by assets/js/calendar.js.

   Everything static about it lives here. What the script still writes itself is
   only lane geometry — a bar's left/width/top and the height of the layer they
   sit in — set through element.style, which a strict style-src permits.
   --------------------------------------------------------------------------- */
/* There is no .cal-wrap rule here any more, and its absence is the fix for #91.
   The grid used to sit in a wrapper capped at 900px and centred — inside
   .private-page's left-aligned 1180px column, which put it ~140px right of
   where the heading, the "+ Lägg till" button and the legend start. That ragged
   left edge, not the whitespace, is what made the page look broken. It now uses
   the whole content column, which also widens the day columns and is half of
   what made the bars cramped. The wrapper element went with the rule. */
.cal-grid { border: 1px solid var(--line); }
/* Eight tracks, not seven: a leading gutter for the week number, zero-wide
   until a desktop monitor gives --cal-gutter a size (the min-width block at the
   foot of this file). Declared once, unconditionally, so the column mapping
   below stays in ONE place — the alternative is a second copy of every rule
   inside the media query, and a mapping in two places is one that eventually
   gets half-updated, silently putting a day's cells in the wrong column. */
.cal-row { display: grid; grid-template-columns: var(--cal-gutter, 0px) repeat(7, 1fr); }
.cal-week { display: grid; grid-template-columns: var(--cal-gutter, 0px) repeat(7, 1fr); grid-auto-rows: min-content; }
.cal-dayname { padding: .75rem; font-weight: 600; text-align: center; background: var(--surface-muted); border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
/* Which of the seven day columns an element sits in — track 1 is the gutter. */
.cal-col-1 { grid-column: 2; }
.cal-col-2 { grid-column: 3; }
.cal-col-3 { grid-column: 4; }
.cal-col-4 { grid-column: 5; }
.cal-col-5 { grid-column: 6; }
.cal-col-6 { grid-column: 7; }
.cal-col-7 { grid-column: 8; }
/* The day cell sits behind, spanning both rows (dates, then bars), so the week
   keeps one uniform surface; its min-height is what gives an event-free week
   its height. Holidays and Sundays are röda dagar, and a holiday's red tint
   beats today's accent tint so they never lose their meaning. */
/* The week-number gutter is desktop-only — a phone's day columns are ~50px and
   have no room for one. Everything but its display is settled here; the
   min-width block at the foot of this file switches it on and widens its
   track. */
.cal-weeknum, .cal-weeknum-head { display: none; grid-column: 1; }
.cal-weeknum { grid-row: 1 / span 2; place-items: center; font-size: .72rem; font-weight: 600; color: var(--ink-faint); background: var(--page); border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.cal-cell { grid-row: 1 / span 2; min-height: 80px; background: var(--paper); border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.cal-cell--today { background: var(--accent-tint); }
.cal-cell--holiday { background: var(--danger-tint); }
.cal-cell--outside { background: var(--page); }
.cal-date { grid-row: 1; min-width: 0; padding: .5rem .5rem .35rem; }
.cal-datenum { font-weight: 600; font-size: .9rem; }
.cal-datenum--red { color: var(--danger); }
/* Today's number always gets the accent circle, even on a red day, so "today"
   never disappears into the holiday styling. */
.cal-datenum--today { display: inline-flex; align-items: center; justify-content: center; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--accent); color: #fff; }
.cal-holiday { font-size: .7rem; color: var(--danger); margin-top: .25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* One box across the whole week, so a bar's left/width percentages measure the
   same seven days the cells above divide up. */
/* Spans the seven day columns and not the week-number gutter, so a bar's
   left/width percentages measure the same days the cells above divide up. */
.cal-bars { grid-column: 2 / -1; grid-row: 2; position: relative; margin-bottom: 4px; }
/* Each bar is an <a> to the entry's own card (#93), so it is focusable, has an
   accessible name and can be activated from a keyboard. min-width is the tap
   target's floor and has to stay in step with MIN_BAR_PX in
   assets/js/calendar.js, which reserves the same width when it packs lanes —
   tests/CalendarGridMarkupTest.php fails if the two drift apart. It used to be
   6px, which on a phone was a target nobody could hit. */
.cal-bar { position: absolute; min-width: 24px; padding: 0 .3rem; border-radius: 2px; font-size: .72rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: var(--event-color); color: var(--event-color-fg); text-decoration: none; }
/* A ring rather than an underline: the bar clips its own overflow, and at this
   height an underline lands on the descenders. z-index lifts the highlighted
   bar over the ones drawn after it, or a later lane paints across the ring.
   Focus keeps the site-wide outline but pulled tight — a lane is only 2px
   clear of the next one, so the usual 2px offset would put the ring through
   the bar above. */
.cal-bar:hover, .cal-bar:focus-visible { z-index: 1; }
.cal-bar:hover { box-shadow: 0 0 0 2px var(--accent-strong); }
.cal-bar:focus-visible { outline-offset: 0; }
/* On a bar only a few pixels wide the usual side padding is wider than the bar
   itself, leaving no room for even one character. */
.cal-bar--tight { padding: 0 1px; }
/* A presence entry reads as a tinted band with that person's colour on the
   edge; an event is a solid chip in the one shared event colour, since it
   belongs to everyone rather than to whoever created it. */
.cal-bar--presence { background: var(--swatch-tint); color: var(--ink); }
.cal-bar--presence:not(.cal-bar--from-before) { border-left: 4px solid var(--swatch-bg); }
/* A continuation edge loses its rounding, so the bar runs into the edge of the
   week row and reads as carrying on. */
.cal-bar--from-before { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.cal-bar--to-after { border-top-right-radius: 0; border-bottom-right-radius: 0; }

/* ---------------------------------------------------------------------------
   Desktop monitors, and only them (#92).

   Every other media query in this file is a max-width — mobile overrides on one
   desktop layout that served 801px to 5120px identically. This is the first
   min-width rule, and that is what makes the wide-screen work structurally
   unable to reach a phone or either iPad orientation rather than merely
   intended not to: portrait (768px) already falls under the 800px treatment,
   landscape (1024px) is below this threshold and keeps exactly today's layout.

   Nothing here fills space for its own sake. A calm, wide page is not a defect;
   what was wrong was a useful list displaced by empty cells.
   --------------------------------------------------------------------------- */
@media (min-width: 1100px) {
  /* Grid left, what is coming up right. On a 2560px display "Kommande
     händelser" used to sit below six rows of mostly-empty cells and fall off
     the fold, while blank grid occupied the view. */
  .calendar-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(18rem, 22rem); gap: var(--space-6); align-items: start; }
  /* .fold-section's own 2rem lines the list up below the grid, not beside it;
     the grid next to it carries .mt-4. */
  .calendar-columns > .fold-section { margin-top: var(--space-4); }

  /* The week-number gutter, which exists as a zero-width track at every size
     and only gets a width here. For a summer property where people say "vecka
     29" rather than a date, that is information rather than filler — and a
     better answer to the spare width than stretching the cells to swallow it. */
  .cal-grid { --cal-gutter: 3rem; }
  .cal-weeknum-head { display: block; }
  .cal-weeknum { display: grid; }
}

/* ---------------------------------------------------------------------------
   Dashboard tiles and the noticeboard (#T26)

   One card language for both: the homepage feed, the dashboard's other
   sections and /anslagstavla/ share the same border, radius, hover and focus
   treatment, so the feed reads as part of the dashboard rather than a widget
   dropped into it.
   --------------------------------------------------------------------------- */
.tile { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-5); border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper); box-shadow: 0 1px 2px rgb(16 34 47 / 4%); }
.tile-head { display: flex; align-items: center; gap: var(--space-2); }
.tile-head h2 { margin: 0; font-size: 1.05rem; }
/* Pushed to the far end of the header row, whichever of the two is present. */
.tile-count, .tile-action { margin-left: auto; }
.tile-count { padding: .1rem .5rem; border-radius: 999px; background: var(--surface-muted); color: var(--ink-soft); font-size: var(--fs-xs); font-weight: 700; }
.tile-action { color: var(--accent-strong); font-size: var(--fs-sm); font-weight: 650; }
.tile-glyph { display: inline-flex; color: var(--accent-strong); }
.tile-glyph svg { width: 20px; height: 20px; }
.tile-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.tile-cards { display: grid; gap: var(--space-3); }
/* The tile's own padding already separates these from the header, so the
   generic .item-list/.event-list card padding would read as double-boxed. */
.tile-cards article { display: block; padding: var(--space-3) var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--paper); transition: border-color .15s ease, box-shadow .15s ease; }
.tile-cards .card-link:hover article { border-color: var(--accent); box-shadow: 0 6px 18px rgb(16 34 47 / 8%); }
.tile-cards .card-title { margin: var(--space-1) 0 0; font-size: 1rem; }
.tile-cards p { margin: 0; }
/* The tile's own my-tasks list keeps its cards flat — the tile is the box. */
.tile .my-tasks li { border: 0; padding: 0; background: transparent; }
.tile-foot { margin-top: auto; padding-top: var(--space-2); border-top: 1px solid var(--line); font-size: var(--fs-sm); }
.tile-foot a { color: var(--accent-strong); font-weight: 650; }
.tile-empty { margin: 0; color: var(--ink-soft); font-size: var(--fs-sm); }
/* The way back into a board that has history but nothing recent. Quieter than
   a feed panel on purpose — it is a pointer, not news. */
.tile-empty-latest { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2); margin: 0; font-size: var(--fs-sm); }
.tile-empty-latest a { color: var(--accent-strong); font-weight: 650; }

/* The feed is the loudest tile on the dashboard, so it gets two tracks where
   there are two to give. 1100px is the stylesheet's existing wide-screen
   threshold, and every min-width query here has to clear 1024px so a phone and
   both iPad orientations are structurally unable to reach it — see #92 and
   tests/CalendarGridMarkupTest.php::testTheWideScreenRulesCannotReachAPhoneOrAnIpad.
   The grid itself is auto-fit and reflows below that on its own. */
@media (min-width: 1100px) {
  /* Two rows as well as two tracks. The feed is taller than any one tile, so
     without the row span the right-hand column ends after a single short tile
     and leaves a hole beside the feed, with the remaining tiles pushed onto a
     row of their own below it. Spanning two rows lets the two smallest tiles —
     the tasks and the projects — stack into that column instead. */
  .tile--feed { grid-column: span 2; grid-row: span 2; }
}

/* ------------------------------------------------------ the feed switcher

   Selecting a feed item is a radio group and nothing more: the inputs come
   first in the DOM so the tab strip and the panels are their following
   siblings, which is what makes `:checked ~` able to reveal one panel with no
   JavaScript. The inputs are hidden from sight but not from focus, so the
   strip is operable from the keyboard as the radio group it actually is. */
.feed-switcher { display: grid; gap: var(--space-3); }
.feed-radio { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.feed-tabs { display: grid; gap: var(--space-2); }
.feed-tab { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); border: 1px solid var(--line); border-left: 4px solid var(--line); border-radius: var(--radius-sm); cursor: pointer; }
.feed-tab:hover { border-color: var(--accent); }
.feed-tab-text { display: grid; min-width: 0; }
.feed-tab-title { font-weight: 650; font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feed-tab-time { color: var(--ink-soft); font-size: var(--fs-xs); }
.feed-panel { display: none; }
.feed-panel article { padding: var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--accent-tint); }
.feed-panel .card-title { margin: var(--space-2) 0; font-size: 1.15rem; }

/* One pair of rules per switchable item. HOMEPAGE_FEED_ITEMS in
   pages/hem/index.php is 3; a fourth item would need a fourth pair here, which
   is the trade for having no JavaScript in this at all. :nth-of-type counts
   inputs among the switcher's children, and the radios are the only ones. */
.feed-radio:nth-of-type(1):checked ~ .feed-panels > .feed-panel:nth-child(1),
.feed-radio:nth-of-type(2):checked ~ .feed-panels > .feed-panel:nth-child(2),
.feed-radio:nth-of-type(3):checked ~ .feed-panels > .feed-panel:nth-child(3) { display: block; }
.feed-radio:nth-of-type(1):checked ~ .feed-tabs > .feed-tab:nth-child(1),
.feed-radio:nth-of-type(2):checked ~ .feed-tabs > .feed-tab:nth-child(2),
.feed-radio:nth-of-type(3):checked ~ .feed-tabs > .feed-tab:nth-child(3) { border-left-color: var(--accent-strong); background: var(--accent-tint); }
/* Focus follows the hidden input, so it has to be painted on the label the
   reader is actually looking at. */
.feed-radio:nth-of-type(1):focus-visible ~ .feed-tabs > .feed-tab:nth-child(1),
.feed-radio:nth-of-type(2):focus-visible ~ .feed-tabs > .feed-tab:nth-child(2),
.feed-radio:nth-of-type(3):focus-visible ~ .feed-tabs > .feed-tab:nth-child(3) { outline: 3px solid var(--accent); outline-offset: 2px; }

/* ------------------------------------------------------ /anslagstavla/ */
.thread-list { display: grid; gap: var(--space-4); margin-top: var(--space-6); }
.thread-card, .thread-full, .comment-card { padding: var(--space-5); border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper); }
.thread-card { transition: border-color .15s ease, box-shadow .15s ease; }
.card-link:hover .thread-card { border-color: var(--accent); box-shadow: 0 8px 24px rgb(16 34 47 / 8%); }
.thread-card .card-title { margin: var(--space-2) 0; font-size: 1.15rem; }
.thread-byline { display: flex; align-items: center; gap: var(--space-2); margin: 0; font-size: var(--fs-sm); }
.thread-author { font-weight: 650; }
.thread-time { color: var(--ink-soft); font-size: var(--fs-xs); }
.thread-preview { margin: var(--space-2) 0; color: var(--ink-soft); line-height: 1.6; }
.thread-meta { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); margin: 0; color: var(--ink-soft); font-size: var(--fs-xs); }
.thread-count { font-weight: 650; }
.thread-full { margin-top: var(--space-5); }
.thread-full h1 { margin: var(--space-3) 0; }
.thread-actions { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--line); }
.thread-actions .admin-edit { margin-top: 0; }
.thread-comments { margin-top: var(--space-6); display: grid; gap: var(--space-4); }
.thread-comments h2 { margin: 0; font-size: 1.05rem; }
.comment-card { padding: var(--space-4); }
.comment-card p { margin: var(--space-2) 0 0; line-height: 1.6; }
.comment-form { margin: 0; }
.empty-state { margin-top: var(--space-6); padding: var(--space-5); border: 1px dashed var(--line); border-radius: var(--radius); color: var(--ink-soft); text-align: center; }

/* --------------------------------------------- /information/ekonomi/ (#52, #54)
   Budget, electricity and water. Three ideas carry the whole section:

   - `tabular-nums` everywhere a figure appears. These pages exist to be
     compared column against column, and proportional digits make a 1 narrower
     than a 7, so the columns visibly fail to line up. Nothing else in the
     stylesheet needed it; every table here does.
   - Wide tables scroll inside `.table-wrap`, never the page body. Twelve
     months across several years exceeds the content width by design.
   - Status colour comes from the existing --ok / --warn / --danger tokens
     rather than new ones, so a variance reads the same as every other piece of
     good and bad news on the site. */

.lead { max-width: 60ch; color: var(--ink-soft); line-height: 1.6; }
.field-hint { max-width: 70ch; color: var(--ink-soft); font-size: var(--fs-sm); line-height: 1.6; }
.form-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; margin-top: var(--space-4); }
.form-actions .field-hint { margin: 0; }
.checkbox-label { display: flex; gap: var(--space-2); align-items: center; }
.pill { display: inline-block; padding: .1rem .45rem; border-radius: 999px; background: var(--surface-muted); color: var(--ink-soft); font-size: var(--fs-xs); font-weight: 650; white-space: nowrap; }
.danger-text { color: var(--danger); }

/* Every numeric cell in the section. `.num` is the class, and it is what makes
   the right-alignment and the tabular digits one decision instead of thirty. */
.budget-table .num, .comparison-table .num, .consumption-table .num, .yoy-table .num,
.figure-value, .money-input {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.money-input { width: 100%; min-width: 6rem; max-width: 9rem; padding: .4rem .5rem; border: 1px solid var(--field-border); border-radius: var(--radius-sm); font: inherit; background: var(--paper); }
.money-input.is-invalid { border-color: var(--danger); background: var(--danger-tint); }
.order-input { width: 5rem; }
/* The number wraps under the name on a narrow screen, where the column
   header no longer explains it. The label rides with the field instead. */
.order-label { display: inline-flex; align-items: center; gap: .4rem; font-size: .78rem; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); }

/* An archived category or party stays legible — it still labels real history —
   but is visibly not part of the live list. */
.is-archived > th, .is-archived > td { color: var(--ink-soft); }

/* Year picker: the same row of links on the budget and both utility pages. */
.year-picker { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin: var(--space-5) 0; }
.year-picker a { padding: .35rem .7rem; border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); text-decoration: none; font-size: var(--fs-sm); font-weight: 650; }
.year-picker a:hover { border-color: var(--accent); color: var(--accent); }
.year-picker a.is-active { border-color: var(--accent); background: var(--accent); color: var(--paper); }
.year-picker .year-picker-link { border: 0; margin-left: auto; color: var(--accent); }
.year-jump { display: flex; align-items: center; gap: .4rem; font-size: var(--fs-sm); color: var(--ink-soft); margin: 0; }
.year-jump select { padding: .3rem .5rem; font-size: var(--fs-sm); }
.year-jump button { padding: .35rem .7rem; font-size: var(--fs-sm); }

/* Headline figures — the hub's "this year at a glance" and the utilities'
   reconciliation panel share one component. */
.ekonomi-glance, .reconciliation { margin: var(--space-6) 0; }
.figure-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); gap: var(--space-4); margin-top: var(--space-4); }
.figure-tile { display: grid; gap: var(--space-1); padding: var(--space-4); border: 1px solid var(--line); border-left: 4px solid var(--line); border-radius: var(--radius); background: var(--paper); }
.figure-label { color: var(--ink-soft); font-size: var(--fs-xs); font-weight: 650; letter-spacing: .06em; text-transform: uppercase; }
.figure-value { font-size: 1.4rem; font-weight: 700; text-align: left; }
.figure-note { color: var(--ink-soft); font-size: var(--fs-xs); }
.figure-tile.is-ok { border-left-color: var(--ok-text); }
.figure-tile.is-warn { border-left-color: var(--warn); }
.figure-tile.is-danger { border-left-color: var(--danger); }

.ekonomi-share-note { max-width: 70ch; padding: var(--space-3) var(--space-4); border-left: 3px solid var(--accent); background: var(--accent-tint); color: var(--ink); font-size: var(--fs-sm); line-height: 1.6; }

/* Variance: the figures are coloured, and a bar repeats them at a glance.
   Symmetric on purpose — an estimate 30 % too high missed by as much as one
   30 % too low, which is what the page is measuring. See App\Ekonomi\Variance. */
.budget-table .is-ok, .comparison-table .is-ok { color: var(--ok-text); }
.budget-table .is-warn, .comparison-table .is-warn { color: var(--warn); }
.budget-table .is-danger, .comparison-table .is-danger { color: var(--danger); }

.variance-bar { position: relative; width: 100%; min-width: 4rem; height: .5rem; border-radius: 999px; background: var(--surface-muted); overflow: hidden; }
/* Width is written by assets/js/ekonomi.js through element.style: the policy
   blocks style= attributes, and a variance percentage is genuinely continuous. */
.variance-bar__fill { position: absolute; inset: 0 auto 0 0; width: 0; height: 100%; border-radius: 999px; }
/* An underspend grows from the right, an overspend from the left, so a column
   of bars shows at a glance which way each category missed — the colour only
   encodes how far off it was, never the direction. */
.variance-bar[data-direction="under"] .variance-bar__fill { inset: 0 0 0 auto; }
.variance-bar__fill.is-ok { background: var(--ok-text); }
.variance-bar__fill.is-warn { background: var(--warn); }
.variance-bar__fill.is-danger { background: var(--danger); }

/* The figures behind the bars. Tighter than the entry tables above it:
   this one is read, never typed into. */
/* Same pills as the year picker, at the size the chart heading wants. */
/* The way back out of a page reached from one place. Not the eyebrow —
   that is a breadcrumb, read as location rather than as an exit. */
.back-link { margin: var(--space-3) 0 0; font-size: var(--fs-sm); font-weight: 650; }
/* A negative top margin here tucked the pills under the heading above and,
   on the el page, straight over the Spara button of the form before it.
   The switch belongs to the chart below it, so it clears what precedes it
   and sits close to what it controls. */
.chart-switch { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin: var(--space-6) 0 var(--space-2); }
/* The chart's own heading follows the pills, and its top margin would
   otherwise collapse through the wrapper and push them apart.
   Every view, not `.chart-switch + .chart-view`: that adjacent-sibling form
   reached only the first one, so Förbrukning sat tight against the pills and
   Kostnad — the second sibling, whether or not the first is hidden — started
   lower. Switching views must not move the heading. */
.chart-view > h2 { margin-top: 0; }
.chart-switch a { padding: .3rem .65rem; border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); text-decoration: none; font-size: var(--fs-sm); font-weight: 650; }
.chart-switch a:hover { border-color: var(--accent); color: var(--accent); }
.chart-switch a.is-active { border-color: var(--accent); background: var(--accent); color: var(--paper); }
.usage-table { margin-top: 0; }
.usage-table tfoot th, .usage-table tfoot td { border-top: 2px solid var(--line); font-weight: 650; }
.household-row > th, .household-row > td { border-top: 2px solid var(--line); color: var(--ink-soft); font-size: var(--fs-sm); }

/* The el line's actual is derived from the electricity page, so it renders as
   a value plus its provenance rather than as an editable field (#103). */
.derived-value { display: block; font-weight: 650; }
.derived-source { display: block; font-size: var(--fs-xs); }
.derived-partial { display: block; color: var(--warn); font-size: var(--fs-xs); }

/* Comparison: two header rows, so the year spans its estimate/actual pair. */
.comparison-table .year-head { border-bottom: 0; text-align: center; }
.comparison-table .sub-head { padding-top: 0; font-size: var(--fs-xs); }
.accuracy-abs { display: block; font-weight: 700; }
.accuracy-signed, .accuracy-years { display: block; color: var(--ink-soft); font-size: var(--fs-xs); }

/* Consumption grids. The dates actually read sit under each figure, because a
   period is never stretched to a calendar month. */
.consumption-table th, .consumption-table td, .yoy-table th, .yoy-table td { padding: .5rem .6rem; white-space: nowrap; }
.cell-span { display: block; color: var(--ink-faint); font-size: var(--fs-xs); }
.total-cell { font-weight: 700; border-left: 1px solid var(--line); }
.cost-row > th, .cost-row > td { border-top: 2px solid var(--line); color: var(--ink-soft); }


/* Usage chart: twelve month groups, one bar per year inside each. Grouped by
   month because January against January is the only comparison that means
   anything for a seasonal property. Heights come from assets/js/ekonomi.js —
   the policy blocks style=, and a bar height is genuinely continuous. */
.usage-chart-wrap { margin: var(--space-4) 0 var(--space-6); }
.chart-legend { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0 0 var(--space-3); padding: 0; list-style: none; font-size: var(--fs-xs); color: var(--ink-soft); }
.chart-legend-item { display: flex; align-items: center; gap: var(--space-1); font-weight: 650; }
.chart-legend-item::before { content: ""; width: .7rem; height: .7rem; border-radius: 2px; background: var(--series); }
.usage-chart { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--space-1); align-items: end; height: 9rem; padding: var(--space-3) 0 0; border-bottom: 1px solid var(--line); overflow-x: auto; }
.chart-group { display: grid; grid-template-rows: 1fr auto; gap: var(--space-1); height: 100%; min-width: 0; }
.chart-bars { display: flex; gap: 1px; align-items: flex-end; height: 100%; }
.chart-bar { flex: 1; min-width: 2px; height: 0; border-radius: 2px 2px 0 0; background: var(--series); }
/* A year with no figure for this month is a gap, not a zero — the two are
   different facts everywhere else in this section and must look different here. */
.chart-bar.is-empty { align-self: stretch; border-radius: 0; background: repeating-linear-gradient(45deg, var(--surface-muted), var(--surface-muted) 2px, transparent 2px, transparent 4px); }
.chart-label { font-size: var(--fs-xs); color: var(--ink-soft); text-align: center; }
/* Five series, cycled. Marine-family hues that stay distinguishable in the
   order they are handed out, rather than a rainbow. */
.series-0 { --series: #1a6ea3; }
.series-1 { --series: #0f5563; }
.series-2 { --series: #7cc0a9; }
.series-3 { --series: #a4670f; }
.series-4 { --series: #90a2af; }

/* Budget comparison chart: estimate against actual, per year. */
.budget-chart { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--space-3); align-items: end; height: 10rem; margin: var(--space-4) 0; padding-top: var(--space-3); border-bottom: 1px solid var(--line); }
.budget-chart-group { display: grid; grid-template-rows: 1fr auto; gap: var(--space-1); height: 100%; }
.budget-chart-bars { display: flex; gap: .2rem; align-items: flex-end; justify-content: center; height: 100%; }
.budget-chart-bar { width: 1.1rem; height: 0; border-radius: 2px 2px 0 0; }
.budget-chart-bar.is-estimate { background: #90a2af; }
.budget-chart-bar.is-actual { background: #1a6ea3; }
.budget-chart-bar.is-empty { align-self: stretch; background: repeating-linear-gradient(45deg, var(--surface-muted), var(--surface-muted) 2px, transparent 2px, transparent 4px); }

/* /notiser/ — an area heading with its own "Markera som läst" beside it (#60).
   The button is part of the heading row rather than of the list below it,
   because what it acknowledges is the area, not any one item in it. */
.notice-area-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-top: 2rem; }
.notice-area-head h2 { margin: 0; }

/* The notification settings save as you tick them (#60). assets/js/notiser.js
   sets data-live once it has taken over, so the Spara button is only hidden on
   a page where something is actually listening — with no JavaScript the form
   posts and reloads as usual. */
.editor-form[data-live] .settings-save { display: none; }
.settings-status { min-height: 1.2em; margin: .25rem 0 0; font-size: .85rem; color: var(--ink-soft); }

/* The arrival (#56) — the coastline drawing itself when you sign in.
   ------------------------------------------------------------------------

   The logo is not an emblem, it is the actual coastline of Grymsö traced from
   OpenStreetMap (docs/brand.md). So on the first dashboard render of a session
   the island draws itself, large and centred over the water in the hero band,
   then fills and settles into the ordinary lockup. You are watching the place
   appear, which is why it is one object throughout and never a large mark
   cross-fading into a small one.

   Nothing below matches unless <body> carries data-arrival, which
   includes/layout.php emits only when pages/hem/index.php spends the one-time
   session flag. Every other render — every inner page, every later visit to the
   dashboard — is untouched by all of it.

   The base declarations are the FINAL state and the keyframes are only the
   journey. That is what makes reduced motion a matter of setting five animation
   names to none: the elements are already where they should end up, with no
   second set of "final" rules to keep in step. */

/* The dashboard's taller band. Not gated on data-arrival — it is the shape of
   the page on every visit, not something that appears once and collapses. The
   height is chosen against the 5x coastline (~296px) with water above and
   below it, so the two numbers move together or not at all. */
.site-brand--dashboard { min-height: 380px; }

body[data-arrival] {
  --anim-coast-draw: coast-draw;
  --anim-coast-fill: coast-fill;
  --anim-settle: arrival-settle;
  --anim-word: arrival-word;
  --anim-rise: arrival-rise;

  /* How far above its resting size the island is drawn. The mobile figure is
     not a lesser preference, it is the same proportion of a smaller band. */
  --arrival-scale: 5;
}

@media (max-width: 800px) {
  .site-brand--dashboard { min-height: 210px; }
  body[data-arrival] { --arrival-scale: 3.5; }
}

/* The whole of the reduced-motion branch. */
@media (prefers-reduced-motion: reduce) {
  body[data-arrival] {
    --anim-coast-draw: none;
    --anim-coast-fill: none;
    --anim-settle: none;
    --anim-word: none;
    --anim-rise: none;
  }
}

/* The lockup during the arrival.
   Centring the enlarged island needs one number CSS cannot derive: half the
   width of the rest of the lockup. assets/js/greeting.js measures it once and
   sets --arrival-shift. Without the script the fallback is 0px, so the island
   still draws large and still settles — it is simply centred on itself rather
   than on the band. */
body[data-arrival] .site-brand--hero .site-brand__mark {
  /* Final state: resting size, resting place. */
  transform: none;
  transform-origin: 50% 50%;
  animation: var(--anim-settle, arrival-settle) 340ms 900ms cubic-bezier(.3, .7, .25, 1) both;
}
@keyframes arrival-settle {
  from { transform: translateX(var(--arrival-shift, 0px)) scale(var(--arrival-scale, 5)); }
  to   { transform: none; }
}

body[data-arrival] .site-brand--hero .site-brand__word {
  opacity: 1;
  animation: var(--anim-word, arrival-word) 320ms 920ms ease-out both;
}
@keyframes arrival-word {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The coastline drawing itself. The path keeps fill="currentColor" and no
   colour of its own — over the hero band currentColor is white, so what draws
   is a white coastline on the evening water.

   stroke-width divides by the scale because the whole draw happens while the
   mark is enlarged: a flat value would render as a wide band at 5x, which is a
   marker pen rather than a chart. Dividing holds the line at ~1.6px on screen
   at either scale. By the time the mark is back at 1x the stroke has faded to
   nothing, so the divided value is never seen at resting size. */
body[data-arrival] .site-brand__mark path {
  stroke: currentColor;
  stroke-width: calc(.9 / var(--arrival-scale, 5));
  stroke-linejoin: round;
  stroke-linecap: round;
  stroke-dasharray: 1;
  /* Final state: outline gone, island filled. */
  stroke-dashoffset: 0;
  stroke-opacity: 0;
  fill-opacity: 1;
  animation:
    var(--anim-coast-draw, coast-draw) 680ms cubic-bezier(.33, .66, .27, 1) both,
    var(--anim-coast-fill, coast-fill) 340ms 560ms cubic-bezier(.4, 0, .3, 1) both;
}

/* One trip round the shore, west end first — the direction the OSM way is
   traced in. Eased out only: it decelerates into place and does not accelerate
   away from the start, which is what stops it reading as a swipe. */
@keyframes coast-draw {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: 0; }
}

/* Land coming up under the line. Starts while the last of the coastline is
   still being drawn, so the two phases overlap rather than hand over — the
   fill arrives as the shore closes, not after it. */
@keyframes coast-fill {
  from { fill-opacity: 0;   stroke-opacity: .9; }
  55%  { fill-opacity: .45; stroke-opacity: .5; }
  to   { fill-opacity: 1;   stroke-opacity: 0; }
}

/* The staggered entrance. Opacity and transform only — no layout property is
   animated, so nothing reflows and nothing moves under a finger already
   reaching for a tile. The elements are in the document and hit-testable the
   whole time, and the last has settled at ~920ms, while the island is still
   travelling into the lockup: the dashboard never waits on the animation
   running above it. */
body[data-arrival] [data-arrival-step] {
  animation: var(--anim-rise, arrival-rise) 460ms cubic-bezier(.22, .68, .3, 1) both;
}
body[data-arrival] [data-arrival-step="1"] { animation-delay: 120ms; }
body[data-arrival] [data-arrival-step="2"] { animation-delay: 220ms; }
body[data-arrival] [data-arrival-step="3"] { animation-delay: 340ms; }
body[data-arrival] [data-arrival-step="4"] { animation-delay: 460ms; }

@keyframes arrival-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------------------
   The 16px floor on touch text fields (#194).

   iOS Safari zooms the page in whenever it focuses a text field computing to
   under 16px, and it zooms centred on the field. Below 800px the header search
   sits hard right (`margin-left: auto` above), so the zoom pans the viewport
   right — while the results panel is deliberately anchored to .site-nav and
   spans the whole bar, which puts its left-aligned result titles off the left
   edge of what is now on screen. The panel is drawn correctly and cannot be
   read. 16px is the threshold Safari checks, not a design preference.

   Gated on a coarse pointer rather than a width, because iPadOS Safari zooms
   the same way and an iPad in landscape is well past any phone breakpoint.
   max() rather than a flat 16px so a reader who has raised their browser's
   default text size still gets the larger value.

   The compact sizes these override are still what a mouse-driven screen gets;
   this is only about not moving the page under someone's thumb.
   --------------------------------------------------------------------------- */
@media (pointer: coarse) {
  .nav-search input,
  .table-filter input,
  .assignee-field .assignee-search,
  .feed-url-input,
  .year-jump select { font-size: max(1rem, 16px); }
}

/* ---------------------------------------------------------------------------
   Historia: chapters, their tables of contents and their photographs (#190).

   /historia/ is a landing page with an introduction and a card grid of
   chapters — both already-existing patterns (.card-grid, .module-card), so
   nothing here styles the list itself. What is new is everything on a chapter's
   own page.
   --------------------------------------------------------------------------- */

.history-chapters { margin-top: var(--space-6); }

/* Every chapter, on every chapter, so moving between them is one click and the
   reader can always see how much there is. A wrapping row rather than a
   scrolling strip: the family site will have a handful of chapters, not thirty,
   and a row that wraps needs no affordance to tell you it can be scrolled. */
.chapter-strip { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-5); }
.chapter-chip {
  padding: .4rem .8rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  color: inherit;
  font-size: var(--fs-sm);
  text-decoration: none;
}
.chapter-chip:hover { border-color: var(--accent); }
.chapter-chip.is-active { border-color: var(--accent-strong); background: var(--accent-strong); color: #fff; }

/* The table of contents, generated from the chapter's own headings.

   The numbers come from counters rather than from the markup: an <ol> already
   means "in order", so a number written into the text would be announced twice
   by a screen reader. counters(toc, ".") is what turns the nested list into
   "2.1" without the page having to know how deep it is. */
.chapter-toc {
  max-width: 68ch;
  margin: var(--space-5) 0;
  padding: var(--space-4) var(--space-5);
  border-left: 3px solid var(--accent-strong);
  background: var(--surface-muted);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.chapter-toc h2 { margin: 0 0 var(--space-3); font-size: 1rem; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-soft); }
.chapter-toc ol { margin: 0; padding-left: 0; list-style: none; counter-reset: toc; }
.chapter-toc ol ol { margin-top: .25rem; padding-left: var(--space-4); }
.chapter-toc li { counter-increment: toc; margin: .25rem 0; line-height: 1.5; }
.chapter-toc li::before { content: counters(toc, ".") ". "; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

/* A quotation, written with '> '. The rule down the left is the whole of the
   decoration — it reads as quoted at a glance without borrowing the accent
   colour the table of contents already uses to mean something else. */
.chapter-quote {
  max-width: 68ch;
  margin: var(--space-5) 0;
  padding: var(--space-2) 0 var(--space-2) var(--space-5);
  border-left: 3px solid var(--line);
  color: var(--ink-soft);
  font-style: italic;
}
.chapter-quote p { margin: 0; }

/* A break between two parts of the story, written with '---'. Short and
   centred rather than full width: a rule across the whole prose column reads
   as the end of the page, which is what the chapter pager below it is. */
.chapter-divider {
  width: 6rem;
  max-width: 40%;
  margin: var(--space-6) auto;
  border: 0;
  border-top: 1px solid var(--line);
}

/* A photograph placed in the text with [bild: …]. Width-constrained to the
   prose column so an image and the paragraph above it share a left edge.

   `width: auto` rather than `100%`, which is what this was first written as and
   what made every photograph dominate the page: 100% stretches a picture to the
   full column whatever its real size, so a small scan was upscaled into a
   blurry wall and an ordinary one still arrived bigger than the story it
   illustrates. Auto plus a max lets a picture be its own size, up to the width
   of the text.

   The height cap is what keeps a PORTRAIT photograph in proportion. A tall
   image constrained only by width is as tall as the screen — the reader scrolls
   past a single picture to reach the next paragraph — so the long edge is
   capped and the aspect ratio follows from `height: auto`. */
.chapter-figure { max-width: 68ch; margin: var(--space-6) 0; }
.chapter-figure img {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: 30rem;
  border-radius: var(--radius);
}
.chapter-figure figcaption { margin-top: var(--space-2); color: var(--ink-soft); font-size: var(--fs-sm); line-height: 1.5; }

/* Previous / next, at the foot of a chapter. space-between puts them on
   opposite edges; margin-left on the next link is what keeps it on the right
   when there is no previous one to push it there. Wrapping so two long chapter
   titles stack on a phone instead of being squeezed into two narrow columns. */
.chapter-pager {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: space-between;
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
  font-weight: 650;
}
.chapter-pager-next { margin-left: auto; text-align: right; }

/* The admin list of a chapter's uploaded images: thumbnail, the exact
   [bild: …] to paste into the body, and the caption. */
.chapter-image-list { margin: 0 0 var(--space-5); padding: 0; list-style: none; }
.chapter-image-list li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: center;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}
.chapter-image-list li > div { flex: 1 1 12rem; }
.chapter-image-list p { margin: 0; font-size: var(--fs-sm); }
.chapter-image-thumb { width: 84px; height: 84px; object-fit: cover; border-radius: var(--radius-sm); }

/* The syntax reminder under the chapter editor's textarea. */
.field-help { max-width: 68ch; margin: calc(var(--space-2) * -1) 0 var(--space-4); color: var(--ink-soft); font-size: var(--fs-sm); line-height: 1.6; }
