:root {
  /* The search field's height: 1rem text, 0.75rem padding top and bottom, 1px border. */
  --search-field-h: calc(1.15rem + 1.5rem + 2px);
  color-scheme: light dark;
  --card-bg: #ffffff;
  --card-bg-translucent: rgba(255, 255, 255, 0.85);
  --card-border: #e2e2e2;
  --text: #1a1a1a;
  --muted: #6b6b6b;
  --accent: #468da3; /* the compass needle's blue (Andreas 2026-09-21); was #2d6cdf */
  /* The Sök button: the compass needle's blue, in light and dark mode (Andreas 2026-09-21). */
  --search-button: #468da3;
  /* Ordinary links (hover colours of link-like things): the needle's blue instead of the plain
     accent blue. The gold login link is separate and unchanged. */
  --link: #468da3;
  --panel-bg: #f7f7f7;
  /* Andreas 2026-09-11: "logga in med google = guldfonten" - the same
     gold-link convention already used on privacy.html/tos.html/faq.html
     (a deeper, readable gold on a light background; the brand's own
     brighter #ffd400 - the compass ring colour, theme-color, favicon -
     only once the background goes dark too, where it stays readable). */
  --gold: #b58a00;
  /* Header picture + footer band (Andreas 2026-10-01): the sand of the beach picture. */
  --sand: #efe5c6;
  --footer-bg: #f1e8cd;
  --footer-line: #e2d6b0;
  --footer-brand: #6b5a3a;
  --footer-muted: #8a7a58;
  --plate-bg: rgba(251, 247, 232, 0.94);
  --plate-border: rgba(0, 0, 0, 0.14);
}

@media (prefers-color-scheme: dark) {
  :root {
    --card-bg: #1e1e1e;
    --card-bg-translucent: rgba(30, 30, 30, 0.85);
    --card-border: #333333;
    --text: #f0f0f0;
    --muted: #a0a0a0;
    --panel-bg: #191919;
    --gold: #ffd400;
    --sand: #2f2a1e;
    --footer-bg: #24211a;
    --footer-line: #34301f;
    --footer-brand: #cbb889;
    --footer-muted: #9a8f72;
    --plate-bg: rgba(40, 36, 26, 0.92);
    --plate-border: rgba(255, 255, 255, 0.14);
  }
}

* {
  box-sizing: border-box;
}

html {
  /* Always a scrollbar (where scrollbars take room at all), so it never pops in/out and shifts the
     page sideways when the page is momentarily short (e.g. results cleared while a new search
     loads). Andreas 2026-10-01: this replaced scrollbar-gutter: stable, whose reserved-but-empty
     strip stopped the full-width header picture and footer band 15px short of the right edge. */
  overflow-y: scroll;
  /* Lets body's min-height:100vh below actually reach the full viewport
     height - a percentage height on body is otherwise relative to
     html's own (content-sized, not viewport-sized) height. */
  height: 100%;
}

body {
  font-family: system-ui, sans-serif;
  margin: 0;
  /* No bottom padding: the footer band runs to the very bottom (Andreas 2026-10-01). */
  padding: 0 1.5rem 0;
  color: var(--text);
  background: transparent;
  /* Andreas 2026-09-12: "Går det att göra footern till en äkta footer?
     Nu är den väldigt långt upp om sidan saknar innehåll" - a normal
     block-flow footer just sits right after whatever content happens to
     be there, so a short page (e.g. the login-gate alone, or few
     results) left it stranded partway up the viewport instead of
     pinned to the bottom. Standard flex sticky-footer pattern: body
     becomes a full-viewport-tall flex column, and .site-footer's own
     margin-top:auto (see its rule) eats all the LEFTOVER space above
     it, pushing it - and only it, whatever precedes it stays in normal
     flow - down to the very bottom. A page with enough real content to
     already exceed one viewport is completely unaffected: auto margin
     has nothing left to consume there, footer just sits after the
     content exactly as before. */
  min-height: 100vh;
  /* Andreas 2026-09-30: in Chrome on a phone 100vh is the height WITHOUT the address bar, so with
     the bar showing the "bottom" footer sat below the edge of the screen. svh is the height with
     the bar showing - the footer is really on screen. (100vh above stays for older browsers.) */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

/* Andreas 2026-09-30: the panel column's contents (sources, pickers) and the account page's lists
   are built by app.js - the HTML only has empty, folded panels and empty lists. Until app.js has
   drawn them (it removes .booting from <html>, see the end of init()) they stay invisible, their
   space kept, so the first frame never shows that empty skeleton. If app.js never gets that far,
   they show anyway after 3s. */
html.booting .layout-sources,
html.booting #account-view {
  visibility: hidden;
  animation: boot-failsafe 0s 3s forwards;
}
@keyframes boot-failsafe {
  to { visibility: visible; }
}

/* Andreas 2026-10-01: a calm beach seen from above (sea, oasis, palms, a grove, a wreck) behind
   the header and the search field, the full width of the window. The picture is centred and fills
   the band's height, so on a desktop the logo always sits on the same calm patch of it. The band
   ends 14px under the search field (--header-band-h, set by app.js syncHeaderBand); the icons and
   counts of a search sit below it, on the plain page. Only the waves move, slowly (inside the SVG,
   off for prefers-reduced-motion). */
html {
  overflow-x: clip; /* the full-width footer band is 100vw wide, scrollbar included */
}
body {
  position: relative;
}
body::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--header-band-h, 206px);
  /* Always the picture's own size (206px tall), the band just cuts it off where it ends - so on a
     page with a lower band (/account: no search field) it doesn't shrink, and the shoreline still
     runs through the logo. */
  background: var(--sand) url("/static/header-bg.svg?v=2") center top / auto 206px no-repeat;
  pointer-events: none;
}
@media (prefers-color-scheme: dark) {
  body::before {
    background-image: url("/static/header-bg-dark.svg?v=2");
  }
}
/* Phone: the band is lower (until app.js has measured it: 160px). The picture keeps its desktop
   size (see body::before), moved up 27px, so the shoreline runs through the
   logo here too, as on a desktop (Andreas 2026-10-01: "Går det att göra på mobil också?"). The
   logo's middle sits at ~y 48 on a phone and ~y 75 on a desktop; the picture's shore is at y 82. */
@media (max-width: 860px) {
  body::before {
    height: var(--header-band-h, 160px);
    background-position: center -27px;
  }
}
/* No outline round the name, day or night - it reads well on the picture as it is (Andreas
   2026-10-01). Night: the name in the compass's beige. */
@media (prefers-color-scheme: dark) {
  .brand h1 {
    color: #ede9ca;
  }
}
/* Positioned (no z-index) so they paint over the picture, which is the body's first layer. */
header,
#login-gate,
#account-view,
#layout {
  position: relative;
}

/* Every direct child above the footer centers ITSELF the classic way
   (max-width + margin:auto) - fine as a normal block, but as a FLEX
   item (body just became one, see above) a pair of `auto` side margins
   takes priority over align-items' default stretch, so each one
   shrink-wrapped down to its own content width instead of filling the
   line first and THEN centering within that. header's internal
   "logo left, version/Konto/menu/cog spread toward the right edge"
   layout assumes a properly wide box to spread across - shrunk to
   content width, everything crammed together instead (Andreas
   2026-09-12: "99 sabbade headern"). #layout's own CSS GRID column
   split (1fr 240px) would be computed against that same too-narrow box
   too. Forcing width back to 100% here restores each one to exactly
   the sizing it already had as a plain block, before body became flex -
   they still center via their own max-width+auto margins same as ever,
   just from a starting width that's actually the full line again. */
header,
#login-gate,
#account-view,
#layout {
  width: 100%;
}

header {
  max-width: 1200px;
  margin: 2rem auto 1rem;
  text-align: center;
}

/* Andreas 2026-09-12, third round, after PR #101's .header-actions group
   turned out to wrap too eagerly AND still let #pages-menu/#sidebar-
   toggle drift off the logo's row together with Konto: "Meny-knappen och
   settings ska ALLTID vara i samma höjd som loggan. [...] 'konto' och
   eventuella nya saker som tillkommer kan få radbrytas om det är trångt
   [...] och centrerras höger". A flexbox `order` trick could prioritize
   #pages-menu/#sidebar-toggle for line 1, but `order` also changes VISUAL
   position within a line - it would render menu/gear to the LEFT of
   Konto even when everything fits on one line, which Andreas never
   asked for. Grid solves this without that side effect: .brand,
   #pages-menu and #sidebar-toggle sit in their own named areas that
   never move, and only .header-meta (Konto - version-tag itself now
   lives in the footer, see .site-footer, so this basically never needs
   to wrap in practice any more) is free to reflow into row 2. */
/* Andreas 2026-09-30: order Filter, Konto, Meny - Filter isn't always there (e.g. the account
   page), so it goes first and Konto and the menu keep their place at the right edge. */
.header-top {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr auto auto auto;
  grid-template-areas: "brand . gear meta menu";
  align-items: center;
  column-gap: 1rem;
  margin-bottom: 1rem;
}

.header-top .brand {
  grid-area: brand;
}

/* Only child left here is #account-link (Konto) - version-tag moved to
   the footer. justify-content:flex-end keeps it flush against #pages-
   menu, matching where it always sat in the old flat row. */
.header-meta {
  grid-area: meta;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 0.4rem 0.75rem;
  min-width: 0;
}

#pages-menu {
  grid-area: menu;
}

#sidebar-toggle {
  grid-area: gear;
}

/* Below this, .brand can no longer share a row with menu+gear even with
   Konto's text gone (44px logo + wordmark leaves too little). Drop
   .header-meta to its own full-width row, right-aligned under menu/gear
   - never left-stranded like the old flat-row overflow used to be.
   Andreas 2026-09-12: measured live (mobile .brand=153.5px + Konto
   34.8px + #pages-menu 32px + #sidebar-toggle 40px + 3x mobile
   column-gap 0.4rem + body's 2x0.75rem padding =~ 304px) rather than
   guessed - single row genuinely holds down to ~300-305px before
   Konto starts overlapping .brand, confirmed live pixel-by-pixel. The
   initial 400px breakpoint here was an unmeasured guess (wrapped far
   earlier than necessary); 320px CSS-px is the conventional floor for
   real visitors (iPhone SE 1st-gen; most modern phones are 360px+), so
   300px leaves a real margin below that floor - single row should hold
   for every realistic visitor, not just most of them. */
@media (max-width: 300px) {
  .header-top {
    grid-template-columns: auto 1fr auto auto;
    grid-template-areas:
      "brand . gear menu"
      "meta meta meta meta";
    row-gap: 0.4rem;
  }
}

/* Safety net, not a width we test against or design for (320px is the
   real floor - see above). Andreas: "jag vill ju inte att det
   overlapar, ens i extrema fall" - below ~270px even .brand+#pages-menu
   +#sidebar-toggle alone (~245px content + 24px body padding) no longer
   fit next to each other, so the 300px tier above would start
   overlapping too. This tier drops .brand to its own full-width row so
   nothing ever overlaps, at any width - #pages-menu/#sidebar-toggle
   still stay paired together (just no longer literally on .brand's
   row), .header-meta (Konto) right-aligned on its own row below that. */
@media (max-width: 270px) {
  .header-top {
    grid-template-columns: 1fr auto auto;
    grid-template-areas:
      "brand brand brand"
      ". gear menu"
      "meta meta meta";
  }
}

h1 {
  font-family: "am-sirte", sans-serif;
  font-weight: 400;
  font-style: normal;
  /* Sized so the "t" ascender (the tallest glyph that still sits exactly
     on the baseline, no descender) matches the compass logo height - same
     reference-glyph baseline logic used for the source logos elsewhere in
     the app. "t" ascent is ~71% of font-size in this font; started at
     64/0.71=~90px, then Andreas asked for 5px shorter (59/0.71=~83px) and
     4px lower (position:relative below, two rounds of +2px). Capitals/j
     overshoot above/below the line by design, same as e.g. Plick's "p". */
  font-size: 83px;
  line-height: 1;
  position: relative;
  top: 4px;
  margin: 0;
}

.brand {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.75rem;
}

/* The logo + wordmark link back to the start page. Inherits colour and
   drops the underline so it looks identical to the old non-link markup;
   carries .brand's flex+gap since it's now the flex parent of the two. */
.brand-link {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  text-decoration: none;
  color: inherit;
  /* Andreas 2026-09-29: no blue flash around logo + name when tapped on mobile (the browser's
     own tap highlight). Keyboard focus still shows its outline. */
  -webkit-tap-highlight-color: transparent;
}

/* Persistent footer with the legal links - shown on both the login gate
   and the logged-in app, so /privacy and /tos are always reachable (and
   match what Google's consent screen links to). */
.site-footer {
  /* margin-top:auto (body is a flex column) pins this to the bottom of the screen on a short page;
     on a long one it just follows the content. */
  margin: auto auto 0;
  padding-top: 2rem;
  width: 100%;
  max-width: 1200px;
  font-size: 0.8rem;
  color: var(--footer-muted);
}
/* Andreas 2026-10-01: a low, quiet band the full width of the window, all the way down, in the
   header's sand colours but calmer - only "Skattjakt – version N". The band is this row's own
   full-bleed backdrop, so the footer keeps its centred 1200px content. */
.footer-row {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0.7rem 0 0.8rem;
}
.footer-row::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 0;
  bottom: 0;
  left: calc(50% - 50vw);
  width: 100vw;
  /* Sparse sand grains, like the header's beach (Andreas 2026-10-01). */
  background: var(--footer-bg) url("/static/footer-sand.svg?v=2") left top / 240px 48px repeat;
  border-top: 1px solid var(--footer-line);
}
@media (prefers-color-scheme: dark) {
  .footer-row::before {
    background-image: url("/static/footer-sand-dark.svg?v=2");
  }
}
/* The wordmark in the logo's own typeface (am-sirte, loaded from Adobe Fonts in index.html). */
.footer-brand {
  font-family: "am-sirte", sans-serif;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--footer-brand);
}
/* With a sponsor card right above it (see app.js placeSponsorCard) the footer's usual 3rem of
   air would push the card far up: the two sit close together instead. Keyed to the start page
   itself (#discover shown) rather than to the card having arrived, so the footer - and with it
   the panel box above it, see syncSidebarScrollHeight - doesn't change size as the card loads. */
body:has(#discover:not([hidden])) .site-footer,
body.has-sponsor .site-footer {
  /* Andreas 2026-09-30: "liiite längre ned" for the card. */
  padding-top: 0.25rem;
}
/* The empty search-status line keeps its height on the start page - between the card and the
   footer that's just a gap. has-sponsor is gone the moment a search starts. */
body.has-sponsor #status:empty {
  display: none;
}
.site-footer a { color: inherit; }

.brand-logo {
  position: relative;
  width: 64px;
  height: 64px;
  flex-shrink: 0;
}

/* Small muted label identifying which build of the live site this is -
   same size/weight/color as the old per-version snapshots' own "(version
   N)" tag next to their title (see e.g. localhost:8001). Moved from the
   header into the footer (Andreas 2026-09-12, third round) to remove it
   from the header-width equation entirely - inherits .site-footer's own
   font-size/color like its sibling links, this just adds the nowrap. */
.version-tag {
  white-space: nowrap;
}

/* #67: replaces the old header account row (name + 3 buttons) - a single
   link to the /konto page. Normal flex child of .header-meta. */
#account-link {
  font-family: system-ui, sans-serif;
  font-size: 0.8rem;
  /* A light plate, like Filter's (Andreas 2026-10-01): readable on any part of the picture. */
  color: var(--text);
  padding: 6px 10px;
  background: var(--plate-bg);
  border: 1px solid var(--plate-border);
  border-radius: 8px;
  text-decoration: none;
  white-space: nowrap;
}

#account-link:hover {
  color: var(--link);
}

/* Small "…" menu for the standalone pages (Tips & frågor / Integritet /
   Villkor) - sits between the version tag and the settings cog. `relative`
   (not the old `absolute`) only so #pages-menu-list below still anchors to
   THIS box rather than the nearest other positioned ancestor - it's a
   normal flex child of .header-top otherwise, see .version-tag above. */
#pages-menu {
  position: relative;
}

#pages-menu-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--plate-border);
  border-radius: 8px;
  background: var(--plate-bg);
  color: var(--text);
  cursor: pointer;
}

#pages-menu-toggle svg {
  width: 20px;
  height: 20px;
}

#pages-menu-list[hidden] {
  display: none;
}

#pages-menu-list {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  display: flex;
  flex-direction: column;
  min-width: 190px;
  padding: 0.35rem 0;
  background: var(--panel-bg);
  border: 1px solid var(--card-border);
  border-radius: 8px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
  z-index: 50;
}

#pages-menu-list a {
  padding: 0.5rem 0.9rem;
  font-size: 0.9rem;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

/* Cookie-inställningar opens the consent banner, so it is a button - styled as the links. */
#pages-menu-list .pages-menu-button {
  padding: 0.5rem 0.9rem;
  font: inherit;
  font-size: 0.9rem;
  color: var(--text);
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  white-space: nowrap;
}
#pages-menu-list .pages-menu-button:hover,
#pages-menu-list a:hover {
  background: rgba(127, 127, 127, 0.14);
}

/* "Filter" button (Andreas 2026-09-21: the cog read as 'settings', the panel is really the
   filters; icon + word so nobody has to guess). Still the sidebar show/hide toggle. */
#sidebar-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--card-border);
  border-radius: 8px;
  background: var(--plate-bg);
  color: var(--text);
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
}
#sidebar-toggle:hover { border-color: var(--text); }
/* Round badge in the top right corner: how many filter panels differ from their default. Plain:
   an outline, not filled with a dark colour, no accent colour (Andreas 2026-09-21). */
#sidebar-toggle { position: relative; }
.sidebar-toggle-badge {
  position: absolute;
  top: -8px;
  right: -8px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  box-sizing: border-box;
  border-radius: 9px;
  background: Canvas;
  border: 1px solid var(--muted);
  color: var(--text);
  font-size: 11px;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.sidebar-toggle-badge[hidden] { display: none; }
/* On the narrowest phones the header row has no room for the word (Konto would drop to its own
   row): icon only there. Larger phones and desktop keep "Filter". */
@media (max-width: 384px) {
  .sidebar-toggle-label { display: none; }
  #sidebar-toggle { padding: 0.35rem 0.5rem; }
}

/* Same hidden-vs-explicit-display bug as #account/#layout elsewhere in
   this file - the unconditional `display: flex` above outranks the UA
   stylesheet's [hidden]{display:none}, so app.js setting the `hidden`
   PROPERTY (see init(), hides this on /konto - "settingsknappen behövs
   inte på konto-sidan") would otherwise do nothing visually without this
   explicit override. */
#sidebar-toggle[hidden] {
  display: none;
}

#account {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  font-size: 0.8rem;
  color: var(--muted);
}

/* Same hidden-vs-explicit-display bug as #layout above - see its comment. */
#account[hidden] {
  display: none;
}

#account-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 260px;
}

/* Andreas 2026-09-11: "Logga ut" ska se ut som "Radera min data" bredvid
   den - samma .account-link-button-klass nu istf en egen accent-färgad,
   inte-understruken-förrän-hover-stil. */
.account-link-button {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 0.8rem;
  color: var(--muted);
  text-decoration: underline;
  white-space: nowrap;
  cursor: pointer;
}

.account-link-button:hover {
  color: var(--link);
}

#discover[hidden] {
  display: none;
}

.discover-section {
  margin-bottom: 1.5rem;
}

.discover-section h2 {
  font-size: 1rem;
  margin: 0 0 0.75rem;
  color: var(--text);
}

.discover-categories {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 0.75rem;
}

.discover-category {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 10px;
  padding: 0.75rem 0.9rem;
}

.discover-category h3 {
  font-size: 0.85rem;
  margin: 0 0 0.5rem;
  color: var(--muted);
  font-weight: 600;
}

.discover-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.discover-chip {
  border: 1px solid var(--card-border);
  background: var(--panel-bg);
  color: var(--text);
  border-radius: 999px;
  padding: 0.3rem 0.75rem;
  font-size: 0.8rem;
  cursor: pointer;
}

.discover-chip:hover {
  border-color: var(--link);
  color: var(--link);
}

/* Shown above the search to logged-out visitors: a one-line pitch + the
   login call-to-action. app.js hides it once you're signed in. */
#login-gate {
  max-width: 1200px;
  /* Nudged ~2px further from the header/logo above (was 1.5rem). */
  margin: 1.65rem auto 0.5rem;
  padding: 0 1.5rem;
  text-align: left;
  color: var(--text);
}

/* Andreas 2026-09-11: condensed from a bold headline + a separate lead
   paragraph (source list, "spara bevakningar och bli mejlad...") down to
   one line - the login link is now inline in the headline itself, not a
   second element below it. */
.gate-headline {
  /* Andreas 2026-09-12: "behöver inte vara tjockmarkerad". */
  font-weight: 400;
  font-size: 0.95rem;
  line-height: 1.6;
  margin: 0;
  color: var(--text);
}

.gate-headline a {
  /* Andreas 2026-09-12: "'logga in med Google' är ju tjocktext.." - the
     600 weight was left over from before, never actually asked for -
     the other gold links (privacy.html etc.) are plain weight, just
     gold. Matches that now. */
  color: var(--gold);
}

#sidebar-toggle svg {
  width: 20px;
  height: 20px;
}

.brand-logo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* White backdrop blob, dark-mode only - the compass ring's black outline
   otherwise disappears into the dark page background. Canvas is 551x501
   against the compass artwork's own 501x501 canvas (109.98% x 100% at
   1:1 scale) - bumped 10% past that (120.98% x 110%), still centered via
   the matching negative left/top offset, regardless of .brand-logo's px
   size at each breakpoint. Doubled selector (.brand-logo .brand-logo-blob)
   so this outranks the generic .brand-logo img rule, which also sets
   width/height/position and would otherwise win on specificity and
   silently force this back to inset:0/100%/100%. */
.brand-logo-blob {
  display: none;
}

.brand-logo .brand-logo-blob {
  position: absolute;
  width: 120.98%;
  height: 110%;
  left: -10.49%;
  top: -5%;
}

/* Night (Andreas 2026-10-01): the white blob is gone - on the beach picture the compass instead
   gets a round 2px ring in its own beige (#ede9ca), a full circle that also closes the gap in the
   yellow ring. The ring's outer edge is 94.41% of the image, offset 2.69% / 2.40%. */
@media (prefers-color-scheme: dark) {
  .brand-logo::before {
    content: "";
    position: absolute;
    left: calc(2.69% - 2px);
    top: calc(2.40% - 2px);
    width: calc(94.41% + 4px);
    height: calc(94.41% + 4px);
    border-radius: 50%;
    background: #ede9ca;
  }
}

@keyframes compass-needle-seek {
  0%   { transform: rotate(0deg); }
  18%  { transform: rotate(-14deg); }
  42%  { transform: rotate(10.5deg); }
  62%  { transform: rotate(-6.3deg); }
  80%  { transform: rotate(3.08deg); }
  100% { transform: rotate(0deg); }
}

.brand-logo-needle {
  animation: compass-needle-seek 6.5s ease-in-out infinite;
  will-change: transform;
}

/* Alternate one-shot "landing" animation (spin-in and settle, e.g. on
   page load) - not currently used anywhere (the loop above is active),
   kept here ready to swap in. To use: add class="brand-logo-needle-land"
   to the needle <img> instead of "brand-logo-needle". Pulled from the
   Compass Logo Animation Claude Design project 2026-07-20; the -720deg
   start is Andreas's tweak (was -540deg) for a fuller spin before it
   settles. */
@keyframes compass-needle-land {
  0%   { transform: rotate(-720deg); }
  70%  { transform: rotate(24deg); }
  85%  { transform: rotate(-9deg); }
  94%  { transform: rotate(3deg); }
  100% { transform: rotate(0deg); }
}

.brand-logo-needle-land {
  animation: compass-needle-land 1.6s cubic-bezier(0.33, 0, 0.35, 1) forwards;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .brand-logo-needle {
    animation: none;
  }
}

/* All the sidebar button-pickers stack the same way, with the same gap
   between choices. */
#sort-picker,
#scope-picker,
#seller-picker,
#sale-type-picker,
#shipping-picker {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

/* Toast: bevakning-tillagd feedback. Fixed + centered so it's visible
   regardless of sidebar state (hidden via the cog, or off-screen below the
   fold on mobile) - the thing the "Notis vid bevakning" backlog item was
   about (adding a watch was otherwise silent unless you happened to be
   looking at the sidebar). Same opacity-transition fade pattern as
   #search-progress elsewhere in this file, just on its own element instead
   of shared icon state. */
#toast {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%) translateY(8px);
  z-index: 20;
  padding: 0.65rem 1.1rem;
  border-radius: 8px;
  background: var(--text);
  color: var(--card-bg);
  font-size: 0.9rem;
  font-weight: 600;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
}

#toast.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

#lan-select {
  width: 100%;
  padding: 0.5rem 0.75rem;
  font-size: 0.9rem;
  border-radius: 6px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text);
}

#price-fields {
  display: flex;
  gap: 0.5rem;
}

#price-fields label {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--muted);
}

/* In the narrow desktop sidebar "Från [input] Till [input]" on one row is
   cramped - stack each field so the label sits under its own box. Below
   861px the sidebar is full-width (single-column layout) and the inline
   version fits fine. column-reverse: the label is first in the DOM, the
   input second, so this puts the input on top and the word beneath it. */
@media (min-width: 861px) {
  #price-fields label {
    flex-direction: column-reverse;
    align-items: stretch;
    gap: 0.25rem;
    /* "Från" / "Till" centred under their own input box. */
    text-align: center;
  }
}

#price-fields input {
  width: 100%;
  min-width: 0;
  padding: 0.5rem 0.6rem;
  font-size: 0.9rem;
  border-radius: 6px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text);
}

.sort-option {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.65rem;
  font-size: 0.85rem;
  border-radius: 6px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text);
  cursor: pointer;
}

.sort-option.active {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}

/* Groups the search form with the per-source "searching" row directly below
   it, so both stay together as one unit - including on mobile, where
   .layout-main dissolves (display: contents) and each child would otherwise
   become its own grid item placed into a named area. Without this wrapper
   the progress row had no area assigned and got auto-placed at the very
   bottom of the page, off-screen in portrait. */
.search-block {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 0;
}

/* Plain wrapper otherwise - flex only so #search-progress-status-row (the
   "Söker..." label + #status-live) and #search-progress (flex-basis:100%
   below) each get their own line - Andreas 2026-09-14: "söker..." belongs
   on the same row as the "N träffar för ..." text, not the favicon row. */
#search-progress-header {
  display: flex;
  flex-wrap: wrap;
  padding: 0.6rem 1.5rem;
  /* Andreas 2026-09-17: "Jag tror vi hade det bättre i tidigare versioner
     där ikoner + siffror var längst upp (inte i en header). Och
     resultaten och hoppen skedde EFTER att vi gjort alla sökningarna.
     Tråkigare men färre buggar." - position:sticky (and the coordination
     it needed with .section-header's own sticky offset, and the
     mid-search auto-scroll it existed to support) is gone as of this
     date. This is a plain in-flow block now - a search starting/finishing
     just grows/shrinks it like any other content, no scroll-anchoring or
     viewport-pinning involved. The two transitions below still make that
     grow/shrink gentle rather than an instant snap (Andreas 2026-09-16:
     "mittspalten hoppar", 2026-09-17: "det bara blixtsnabbt försvinner,
     blinkar bort") - padding-top matches #search-progress's own 0.35s
     opacity fade (see below) so the box and its icons finish together. */
  transition: padding-top 0.3s ease, opacity 0.35s ease;
}

#search-progress-header > #search-progress {
  flex-basis: 100%;
}

/* Andreas 2026-09-18: "om sökordet är jättelångt så hamnar 'Söker...' på
   en rad, och söktermen på en annan" - #search-progress-label and
   #status-live used to be plain siblings inside #search-progress-header,
   free to wrap onto separate flex lines from EACH OTHER (not just from
   the icon row) the moment #status-live's "N träffar för '<long
   query>'" text got wide enough that the two no longer fit side by side.
   Grouping them into this own nowrap row instead - a single flex item of
   #search-progress-header, so it still gets its own line below the icon
   row exactly as before - means the label and the count/query text can
   never separate: #status-live truncates with an ellipsis internally
   (see below) rather than ever pushing the label down. flex-basis:100%
   for the same reason #search-progress gets it above - alone on its own
   line regardless of how short its content is. min-width:0 lets it
   actually shrink below its children's combined natural width instead of
   overflowing the header's padding. */
#search-progress-status-row {
  display: flex;
  flex-wrap: nowrap;
  flex-basis: 100%;
  min-width: 0;
  /* baseline, not center - Andreas 2026-09-14, second round: "söker" and
     #status-live's "N träffar för ..." text need to actually sit at the
     same height, not just be in the same flex line - center aligns the
     two items' BOXES, not their text, which read as slightly offset once
     their font-sizes matched (see #search-progress-label's own comment). */
  align-items: baseline;
}

#search-progress-header.is-active,
#search-progress-header.is-streaming {
  padding-top: 40px;
}

/* Fades the WHOLE bar (background, label, counter), not just the icon
   row's own separate opacity rule (#search-progress.is-fading below) -
   without this, only the favicons faded and the bar's background/text
   just snapped away instantly once the state classes dropped. */
#search-progress-header.is-fading {
  opacity: 0;
}

/* "Söker" + three dots, right before the favicon row (see index.html) -
   Andreas 2026-09-14: the row alone read as static during a slow search,
   this is purely a "still working" signal. A sibling of #search-progress,
   not nested inside it (see index.html), so gated on the WRAPPER's
   mirrored state classes rather than #search-progress's own. No own
   font-size/line-height (was 0.85rem - Andreas 2026-09-14, second round:
   "ska så klart vara samma font som resten av meningen samt på samma
   höjd") - inherits the exact same as #status-live's own text right next
   to it, which sets none of its own either. */
#search-progress-label {
  display: none;
  color: var(--muted);
  margin-right: 0.4rem;
  /* Own inline-flex layout (this label + its nested .search-progress-dots
     span) - baseline-aligns the dots against the "Söker" text next to
     them. Unrelated to (and not to be confused with) matching THIS
     label's own font-size/line-height against #status-live's, which is
     a wrapper-level align-items concern instead - see
     #search-progress-header. */
  align-items: baseline;
  /* Never the one that shrinks/truncates - #status-live (the long,
     variable-length part) does that instead. Without this, the label's
     own text could get squeezed by #search-progress-status-row's nowrap
     flex layout on a narrow screen, same bug as before just moved one
     level down. */
  flex-shrink: 0;
}

#search-progress-header.is-active #search-progress-label,
#search-progress-header.is-streaming #search-progress-label {
  display: inline-flex;
}

.search-progress-dots span {
  animation: search-progress-dot 1.4s infinite;
  animation-fill-mode: both;
}

.search-progress-dots span:nth-child(2) {
  animation-delay: 0.2s;
}

.search-progress-dots span:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes search-progress-dot {
  0%, 80%, 100% {
    opacity: 0.2;
  }
  40% {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .search-progress-dots span {
    animation: none;
    opacity: 1;
  }
}

/* Per-source "searching" row: enabled sources' icons pop in below the search
   box while a search runs, then app.js fades them out once results arrive.
   Hidden entirely when there's nothing to show (no .is-active) - display:none
   so it collapses cleanly out of the .search-block flex flow, leaving no gap.
   The row is ALWAYS displayed (even idle) and its icons always sit in the DOM
   - just visibility:hidden when no search is running (see .search-progress-icon
   below). That keeps the row's height permanently reserved, so the results
   below never jump up a few pixels when the icons fade out at the end of a
   search. */
#search-progress {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  /* Now a flex ITEM of #search-progress-header (alongside
     #search-progress-label) rather than a plain block spanning
     .search-block's full width - without flex-grow it shrinks to fit only
     its current (possibly still-empty, mid-populating) content, which
     throws off lockSearchProgressHeight's own per-row math below (it
     reads clientWidth to decide how many icons fit per row - measured too
     narrow there = overestimates how many rows are needed). min-width:0
     is the standard flex-item companion to flex-grow, letting it actually
     shrink below its content's natural width when the label needs room
     too, instead of overflowing the line. */
  flex: 1 1 auto;
  min-width: 0;
  /* Floor only - covers the fallback path (icons all present up front,
     see populateSearchProgress) and the idle/no-sources-enabled state.
     Streaming mode (see below) starts with an EMPTY row and appends icons
     one at a time as they arrive, so this 30px alone isn't enough to stop
     the row growing taller mid-search as icons wrap onto a 2nd line -
     app.js's lockSearchProgressHeight sets a bigger inline min-height up
     front for that case, sized for the actual number of enabled sources,
     so the row never visibly grows/shrinks over the course of one search
     (Andreas 2026-09-11: the "träffar"-text below was jumping as a NEW
     search's row snapped back down to one line before regrowing). */
  min-height: 30px;
  opacity: 1;
  transition: opacity 0.35s ease;
}

/* app.js adds this on completion to fade the row out (opacity->0) as the
   results take over, then drops back to idle once the fade lands. */
#search-progress.is-fading {
  opacity: 0;
}

/* Wraps one icon so a failed source (#69) can get a small "✕" overlay
   positioned over it - the wrapper is the positioning context, the icon
   itself stays a plain circular image. */
.search-progress-item {
  position: relative;
  width: 30px;
  height: 30px;
}

.search-progress-icon {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
  /* Idle: invisible but still occupying its 30x30 layout box (visibility, not
     display:none), so the row keeps its reserved height between searches. */
  visibility: hidden;
}

/* Only while a search runs do the icons show and pop in. `backwards` holds
   the from-state (scale 0) during the per-icon stagger delay set inline in
   app.js, so each stays invisible until its turn. Losing is-active at the end
   returns them to the hidden-but-space-reserving idle state above. This is
   the fallback path (no live per-source signal - keepResults' fetch, or the
   client falling back after an SSE error): a rough progress feel, not real
   progress. See #search-progress.is-streaming below for the real thing. */
#search-progress.is-active .search-progress-icon {
  visibility: visible;
  animation: source-pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
}

/* Streaming mode (#69, Trello "progressiv sökning"): app.js's
   appendSearchProgressIcon adds ONE icon per real SSE completion event,
   always at the END of the row - so the row always fills strictly
   1st/2nd/3rd... in COMPLETION order, never a fixed position matching the
   sidebar's Källor order (Andreas 2026-09-11: positions popping in out of
   left-to-right order, e.g. #5 landing before #3, read as messy). Nothing
   to hide-then-reveal here - each icon simply doesn't exist until its
   source is done, so it can just play the pop-in animation on arrival.
   A source that completes near-instantly (cache hit) just appears
   near-instantly too - there's no artificial delay to skip. */
#search-progress.is-streaming .search-progress-icon {
  visibility: visible;
  animation: source-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* A source that failed (or was skipped, breaker open) lands greyed out
   with a small "✕" instead of popping in normally - still counts as
   "landed", just visibly not contributing to the count. */
.search-progress-icon.is-failed {
  filter: grayscale(1);
  opacity: 0.55;
}
/* Gated on the SAME #search-progress.is-active/.is-streaming states as
   the icon's own visibility above - without this, the "✕" (unlike the
   icon it sits on, which correctly returns to visibility:hidden once the
   row fades back to idle) stayed fully opaque and visible FOREVER after
   a search with a failed source finished, since it's a plain ::after
   with no visibility of its own to reset (Andreas 2026-09-11: "'x' på
   'kunde inte nå' ligger kvar i evighet" - the icon itself had already
   faded away, leaving only this floating in its empty slot). */
#search-progress.is-active .search-progress-item.is-failed::after,
#search-progress.is-streaming .search-progress-item.is-failed::after {
  content: "✕";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Andreas 2026-09-12: "'x' för trasig källa är för litet i desktop ...
     skulle föredra om det är samma procentenhet på båda [mobil och
     desktop]". The favicon it sits on (.search-progress-item/-icon) is a
     fixed 30px on every breakpoint - no media query touches it - and
     this rule was equally unconditional, so the OLD 0.8rem was already
     the exact same absolute size (and so the same % of the favicon)
     everywhere; what differs between mobile and desktop is only how big
     that fixed 30px favicon reads on a phone screen held closer versus a
     desktop monitor, not the CSS itself. Bumped to 1.85rem - measured via
     canvas text metrics in the site's own font (system-ui, 700 weight):
     the rendered "✕" glyph is ~22.56px wide against the 30px favicon,
     ~75.2%. */
  font-size: 1.85rem;
  font-weight: 700;
  color: var(--text);
  text-shadow: 0 0 3px var(--card-bg), 0 0 3px var(--card-bg), 0 0 3px var(--card-bg);
}

@keyframes source-pop {
  0%   { transform: scale(0); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  #search-progress.is-active .search-progress-icon,
  #search-progress.is-streaming .search-progress-icon {
    animation: none;
  }
}

#search-form {
  display: flex;
  gap: 0.5rem;
  /* +14px (Andreas 2026-10-01): the header picture runs 14px past the search field before it
     ends - everything below moves down as much, so the search counts stay clear of its edge. */
  margin-bottom: calc(1rem + 14px);
}

/* Wraps the input so the history menu can be absolutely positioned
   against it. Takes over the flex-grow the input used to have. */
#search-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
}

#search-input {
  width: 100%;
  /* A fixed height, not the browser's own "normal" line height: the account block on /konto
     takes exactly the same height, so the picture band ends at the same place on both pages. */
  height: var(--search-field-h);
  padding: 0.75rem 1rem;
  font-size: 1rem;
  border-radius: 8px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--text);
}

/* Custom history dropdown - replaces the native <datalist>, whose
   option-picked event isn't reliable across browsers (kept regressing
   to "fills the box but doesn't search"). Real buttons, real clicks. */
#search-history-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 40;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  max-height: 320px;
  overflow-y: auto;
}

#search-history-menu[hidden] {
  display: none;
}

.search-history-opt {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 0.7rem;
  border-radius: 6px;
  color: var(--text);
  font-size: 0.95rem;
  cursor: pointer;
}

.search-history-opt:hover,
.search-history-opt.is-active {
  background: rgba(127, 127, 127, 0.14);
}

.search-history-opt svg {
  width: 15px;
  height: 15px;
  flex: none;
  color: var(--muted);
}

#search-form button {
  padding: 0.75rem 1.25rem;
  font-size: 1rem;
  border-radius: 8px;
  border: none;
  background: var(--search-button, var(--accent));
  color: white;
  cursor: pointer;
}

/* .layout-main wraps the search form + results together as ONE grid item
   (see .layout-main below) rather than giving the sidebar its own
   grid-area spanning two separate "search"/"results" rows like before.
   That row-spanning arrangement made an Android tablet (reproduced on
   Chrome, Firefox AND Opera there, so it's not one engine's bug)
   inconsistently inflate the search row's height to make room for the
   tall spanning sidebar, leaving a large blank gap between the search bar
   and the results - implicit grid row-sizing with a multi-row-spanning
   item is inherently a bit ambiguous, and different engines/versions can
   resolve it differently. A single-row 2-column grid has no such
   ambiguity: the sidebar only ever needs to fit its own one row. */
/* #67 account page (/konto) - a client-side view, not a real navigation
   (see app.js's isAccountPage), so it lives as a sibling of #layout that
   swaps visibility with it. Same max-width/centering as #layout so the
   page doesn't visually jump width when switching between them. */
#account-view[hidden] {
  display: none;
}

#account-view {
  max-width: 1200px;
  /* No top margin: the "Inloggad som" block sits in the picture at the same height as the
     search field on the start page (Andreas 2026-10-01). */
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* On mobile the body's own side padding is the page margin (same edge as
   the logo and the search row); the extra 1.5rem only belongs on wide screens. */
@media (max-width: 860px) {
  #account-view {
    padding: 0;
  }
}

/* Andreas 2026-10-01: the "Konto" heading isn't shown - you know where you are. It stays for
   screen readers. */
.account-view-header h2 {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  margin: 0;
}

/* Exactly as high as the search field on the start page: "Inloggad som" at its top edge, the
   links at its bottom edge, so the picture band ends at the same place (Andreas 2026-10-01). */
.account-view-header {
  /* min-: should the links wrap onto two lines on a narrow phone, the block grows instead of spilling. */
  min-height: var(--search-field-h);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  margin-bottom: 2rem;
}

#account-signed-in-as {
  color: var(--muted);
  margin: 0;
}

.account-view-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: center;
}

#account-saved-empty,
#account-watches-empty {
  color: var(--muted);
}

/* Text left, switches at the right edge: Mejl alone on the web, Push | Mejl
   in the app (.with-push). */
.notify-prefs {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0.6rem 1rem;
  margin-bottom: 2rem;
}

.notify-prefs.with-push {
  grid-template-columns: 1fr auto auto;
}

.notify-prefs:not(.with-push) [data-push-only] {
  display: none;
}

.notify-col {
  justify-self: center;
  align-self: end;
  font-size: 0.85rem;
  color: var(--muted);
}

.notify-grid-heading {
  margin: 0;
}

/* Sits right below the saved-listings grid; give it the same breathing
   room as the other section headings on the page. */
#account-saved-list + .notify-prefs {
  margin-top: 2rem;
}

.notify-prefs input {
  appearance: none;
  flex: none;
  position: relative;
  width: 2.5rem;
  height: 1.4rem;
  margin: 0;
  border-radius: 999px;
  background: var(--card-border);
  cursor: pointer;
  transition: background 0.15s;
}

.notify-prefs input::after {
  content: "";
  position: absolute;
  top: 0.2rem;
  left: 0.2rem;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  background: #ffffff;
  transition: transform 0.15s;
}

.notify-prefs input:checked {
  background: var(--accent);
}

.notify-prefs input:checked::after {
  transform: translateX(1.1rem);
}

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

/* #account-saved-list is a plain .section-grid of renderCard() cards now
   (Andreas 2026-09-11) - see app.js's renderSavedListings. It used to be
   a row+filter-tags layout of its own; that turned out to carry no
   useful information (a saved listing's filter snapshot doesn't do
   anything after the fact) and was dropped after one round. The
   Bevakningar list below inherited that same row+tags layout instead - a
   watch's filters stay genuinely meaningful (they define what it
   actually monitors), so its tags earn their place. */
.account-watch-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.account-watch-row {
  position: relative;
  display: grid;
  grid-template-columns: fit-content(45%) minmax(0, 1fr);
  cursor: pointer;
  gap: 0.5rem;
  align-items: center;
  /* Same compact look as the startpage sidebar's .watch-row (Andreas
     2026-09-19: "Följ den stilen"): 6px radius, tight padding, 0.35rem
     between rows. padding-right leaves room for the absolute trash button. */
  padding: 0.35rem 0.4rem;
  padding-right: 2.1rem;
  border: 1px solid var(--card-border);
  border-radius: 6px;
  background: var(--card-bg);
  margin-bottom: 0.35rem;
}

.account-watch-query {
  /* Same as .watch-row .watch-word: 0.85rem/500. */
  font-size: 0.85rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Default-valued tags only show once the row is expanded (click). */
.account-watch-row:not(.is-open) .filter-tag.is-default {
  display: none;
}

.account-watch-row.is-open .account-watch-tags {
  grid-column: 1 / -1;
}

.account-watch-tags {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-width: 0;
}

.account-watch-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-content: flex-start;
  min-width: 0;
}

/* Collapsed = always ONE line (Andreas 2026-09-19): chips never wrap, the
   ones that don't fit are clipped at the right edge and counted by the
   "+N" badge - click the row to see them all. */
.account-watch-row:not(.is-open) .account-watch-chips {
  flex-wrap: nowrap;
  overflow: hidden;
}

/* Right-aligned (Andreas 2026-09-19): the wrapper shrinks to its content
   and hugs the right edge; when the chips don't fit it's capped at the
   column width instead and they're clipped from the right (start stays
   visible). */
.account-watch-row:not(.is-open) .account-watch-tags {
  justify-self: end;
  max-width: 100%;
}

/* The fade only when something is really clipped (.is-clipped, set from
   app.js) - otherwise a right-aligned last chip would be faded for no
   reason. */
.account-watch-row:not(.is-open) .account-watch-chips.is-clipped {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.25rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 1.25rem), transparent);
}

.account-watch-more {
  flex: none;
}

.account-watch-row.is-open .account-watch-more {
  display: none;
}

.filter-tag {
  font-size: 0.75rem;
  color: var(--muted);
  background: var(--panel-bg);
  border: 1px solid var(--card-border);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  white-space: nowrap;
}

.account-watch-remove {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: 0.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.account-watch-remove:hover {
  /* Red like the sidebar's .watch-remove:hover. */
  color: #d33;
}

.account-watch-remove svg {
  width: 16px;
  height: 16px;
}

#layout {
  display: grid;
  grid-template-columns: 1fr 240px;
  grid-template-areas: "main sidebar";
  gap: 1.5rem;
  max-width: 1200px;
  margin: 0 auto;
  align-items: start;
}

/* Author styles always beat the browser's own [hidden]{display:none} UA
   rule, regardless of specificity - an explicit `display` here for the
   visible case silently wins over `hidden` too, so app.js's
   `layoutEl.hidden = true` (shown to every logged-out visitor - see
   init()) had NO visual effect: the full search UI rendered anyway,
   just alongside the "please log in" message instead of being replaced
   by it. Confirmed live 2026-09-03 via a real browser check. Same fix
   already existed for #discover below - #account has the identical bug
   (display: flex) and gets the same fix. */
#layout[hidden] {
  display: none;
}

/* Sidebar hidden via the settings-cog toggle (#sidebar-toggle in app.js) -
   reclaims the 240px column on desktop. Left alone below the 860px
   breakpoint further down: the mobile layout is already a single column,
   so hiding .sidebar-slot there just collapses its row. */
@media (min-width: 861px) {
  #layout.sidebar-hidden {
    grid-template-columns: 1fr;
    grid-template-areas: "main";
  }
}

#layout.sidebar-hidden .sidebar-slot {
  display: none;
}

.layout-main {
  grid-area: main;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  min-width: 0;
}

.layout-results {
  min-width: 0;
}

/* A sidebar filter change refreshes the results in place (app.js keepResults) -
   dim the old set while the new one loads instead of clearing it, so the page
   never collapses and the scroll position never jumps. */
#results.is-refreshing {
  opacity: 0.45;
  transition: opacity 0.12s ease;
}

#status {
  text-align: center;
  color: var(--muted);
  min-height: 1.2em;
}

/* #69 live counter (Andreas 2026-09-11) - paired with the favicon
   progress row above it, see index.html's comment. Empty (cleared by
   app.js) outside of an active streaming search, so :empty collapses it
   to zero height instead of reserving a permanently-visible blank line
   next to the search box. */
#status-live {
  /* left, not center - Andreas 2026-09-18: this used to be the sole
     content of its own full-width line, where centering vs. left-align
     was invisible either way (its box always matched its own content
     width). Now it shares #search-progress-status-row with the "Söker..."
     label, shrinks to whatever width is left next to it, and truncates
     with an ellipsis (see below) - centering a truncated single line
     inside a shrunk box reads as cut off on both ends instead of one. */
  text-align: left;
  color: var(--muted);
  margin: 0;
  /* A bit of extra breathing room above the count text itself, rather
     than touching the shared .search-block flex gap other rows rely on
     too (Andreas 2026-09-11: "nuddar nästan pixlar" - back when the +N
     popup still floated up from THIS line; it now floats above its own
     favicon instead - see .hit-count-plop - but the spacing still reads
     fine on its own, so left as-is). */
  margin-top: 0.5rem;
  /* Andreas 2026-09-18: "låt hellre söktermen brytas av med '…' på slutet
     om det inte får plats på skärmen" - a long query used to just push
     this whole line wide enough that #search-progress-status-row's nowrap
     forced #search-progress-label onto a line of its own (the actual
     bug). min-width:0 lets this flex item shrink below its own text's
     natural width instead of overflowing the row; overflow/white-space/
     text-overflow then turn that shrink into a single-line ellipsis
     rather than a clipped-with-nothing-shown cutoff. */
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

#status-live:empty {
  display: none;
}

/* Andreas 2026-09-11 through 13: three rounds chasing a #status-live
   height jump on mobile, each reserving space under a narrower condition
   than the last, before landing on the fix below (originally scoped to
   the single-column breakpoint only - see the git history on this rule
   for that earlier version). Andreas 2026-09-16, live-video-instrumented:
   the SAME collapse (:empty -> display:none, dropping this line's height
   in one frame) also happens on a normal two-column DESKTOP layout now -
   #status-live is written to on every layout since bumpStatusCount
   stopped special-casing mobile (see its own comment in app.js), so the
   old "on desktop #status-live is never written to at all" assumption
   this rule used to rely on no longer holds. Scoped to the header's own
   engaged states (is-active/is-streaming) rather than a viewport width,
   so it reserves space consistently everywhere a search is genuinely
   running, and correctly reverts to the plain :empty/display:none
   collapse the rest of the time - no permanent empty gap under the
   search box before/after a search, on any layout. Sized for the worst
   case (two lines, once a "(kunde inte nå: ...)" suffix wraps in) rather
   than the common one-line case, so that doesn't still jump either -
   measured live against real rendered text, not guessed. */
#search-progress-header.is-active #status-live,
#search-progress-header.is-streaming #status-live {
  display: block;
  min-height: 38px;
}

/* #69 live-ticking count. Andreas 2026-09-11 (second round of feedback):
   the number itself must NOT scale/bounce/recolour - "tråkiga fonten",
   plain and unstyled like the rest of #status, only the digits may
   change. So no animation and no font-weight here at all any more - the
   only thing this rule still does is stop the digit BOX from reflowing
   the (centered) line as the number grows a digit. */
.status-count-pop {
  position: relative;
  display: inline-block;
  /* the whole centered #status line visibly slid sideways as the number
     grew a digit (1 -> 12 -> 123), because a wider number shifts where a
     CENTERED line's text starts. tabular-nums makes every digit the same
     rendered width, and the reserved min-width keeps the number's own box
     from changing size at all up to 3 digits (the overwhelmingly common
     case - a source rarely returns 4+ digits worth of matches) - so in
     practice the whole line stays put while only the number's own glyphs
     change, instead of "träffar för X" sliding too. */
  min-width: 3ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The floating "+N" that pops up and fades as it rises - a classic
   damage-number treatment. app.js appends it into the COMPLETING
   SOURCE'S OWN .search-progress-item (Andreas 2026-09-11, second round:
   "flytta den så att de hamnar ovanför respektive favicon" - it used to
   live inside .status-count-pop, floating above the shared total number
   instead, which read as messy once several could be alive at once) -
   .search-progress-item is already `position: relative` (see the
   is-failed "✕" overlay above), so this needs no CSS changes to work
   there instead: centered via left:50%/translateX(-50%) works the same
   regardless of which positioned parent it lands in. Colour is set
   inline per-call in app.js, matching the completing source's own
   section colour rather than one fixed colour here. */
.hit-count-plop {
  position: absolute;
  /* Andreas 2026-09-11 (fifth round): "siffrorna behöver komma några få
     pixlar högre upp, ovanför favicons också" - a few px more clearance
     above the icon (was -1.1em). */
  top: -1.5em;
  /* Andreas 2026-09-11 (fourth round): "sifferpopupen för inte överskrida
     favicons storlek ... blir rörigt" - a wide "+N" (e.g. "+119") under
     the old left:50%/translateX(-50%) auto-width centering could render
     wider than the 30px favicon it floats above and spill into the NEXT
     icon's space. Pinning it to the parent's own full width (the parent
     IS the 30px .search-progress-item) instead means it can never be
     wider than the icon itself - text-align centers within that fixed
     box, overflow:hidden clips the rare case a 3-digit "+N" still doesn't
     quite fit rather than spilling sideways. */
  left: 0;
  width: 100%;
  text-align: center;
  overflow: hidden;
  font-size: 0.72em;
  font-weight: 700;
  /* --plop-color/-dark set inline per-call in app.js's bumpStatusCount -
     the SAME per-source colour table (section-colors.json) the result
     section headers use, so a source's plop always matches its own
     section line. --text is just the fallback for a source with no
     entry there. */
  color: var(--plop-color, var(--text));
  white-space: nowrap;
  pointer-events: none;
  /* Andreas 2026-09-11: sources often complete within a second of each
     other, so the old 0.9s (which also started fading almost immediately
     after popping in - see the keyframes) read as "gone before I could
     read it". Fixed length independent of how fast the favicons tick -
     pop in, HOLD fully visible for a while, then fade.
     Fourth round: a short animation-delay so the favicon's OWN pop-in
     (source-pop, 0.3s) gets a beat to land first, THEN the number pops -
     "favicon kommer först, sedan ploppar en siffra in" - rather than both
     appearing in the exact same instant. `both` fill-mode holds the 0%
     keyframe (invisible) through that delay instead of the plop briefly
     flashing at its default (fully visible) style before the animation
     proper begins. */
  animation-name: hit-count-plop-float;
  animation-duration: 1.6s;
  animation-timing-function: ease-out;
  animation-delay: 0.18s;
  animation-fill-mode: both;
}

@media (prefers-color-scheme: dark) {
  .hit-count-plop {
    color: var(--plop-color-dark, var(--plop-color, var(--text)));
  }
}

/* Andreas 2026-09-11 (fourth round): "helt ok om den animeras lite mer
   livligt" - a bigger overshoot (1.15 -> 1.4) on the pop-in now that the
   delay above gives it its own clear moment instead of competing with the
   favicon for attention. No translateX any more - centering is the
   plop's own fixed width + text-align now (see above), not a transform,
   so only translateY/scale need animating. */
@keyframes hit-count-plop-float {
  0%   { opacity: 0; transform: translateY(4px) scale(0.8); }
  10%  { opacity: 1; transform: translateY(0) scale(1.4); }
  25%  { transform: translateY(0) scale(1); }
  70%  { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-14px) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  /* Belt-and-suspenders: bumpStatusCount is only ever called from the
     streaming path, which useStreamingSearch() already keeps off for
     reduced-motion - this just means a freshly-loaded stylesheet can
     never show the animation even if that gate is ever bypassed. */
  .hit-count-plop {
    animation: none;
  }
}

/* Contextual nudge shown under the status line on a very broad result set
   (see app.js). */
#search-tip {
  text-align: center;
  color: var(--muted);
  font-size: 0.85rem;
  margin: 0.25rem 0 0;
}

#search-tip code {
  background: var(--panel-bg);
  border: 1px solid var(--card-border);
  border-radius: 4px;
  padding: 0.05rem 0.3rem;
  font-size: 0.9em;
  /* Keep "sökord -tillbehör" on one line so the minus never wraps away
     from its word and gets read as a stray dash. */
  white-space: nowrap;
}

#search-tip a { color: inherit; }

/* Sidebar */

/* The wrapper owns grid placement + stickiness; .layout-sources inside it
   owns its own scroll box (max-height + overflow). Keeping those two jobs
   on separate elements avoids a Chromium quirk where a stuck `position:
   sticky` element under-reports how far its OWN overflow content can
   scroll. */
.sidebar-slot {
  grid-area: sidebar;
  position: sticky;
  top: 1rem;
}

.layout-sources {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* The sidebar is its own scroll box so you can page through the filter
   panels without moving the results, and vice versa (min-width: 861px
   only - the mobile breakpoint below drops it to a plain inline column).

   Its height must be capped so its bottom never falls past the viewport,
   or its last panels (Frakt, Län) sit off-screen with no way to scroll to
   them. `calc(100vh - 2rem)` is only the right cap once .sidebar-slot is
   actually pinned at top:1rem; before you've scrolled down far enough for
   that it sits ~230px lower (under the header, and the login gate when
   logged out) and that static calc overflows the fold. app.js recomputes
   --sidebar-scroll-h from the slot's real top on every scroll/resize so
   the box always ends just above the fold; this calc() is the pre-JS
   fallback. */
@media (min-width: 861px) {
  .layout-sources {
    max-height: var(--sidebar-scroll-h, calc(100vh - 2rem));
    overflow-y: auto;
    /* Without this, hitting the top/bottom of the sidebar's own scroll
       hands the rest of the wheel/trackpad gesture off to the page scroll
       underneath (scroll chaining) - contain stops it right at the
       boundary instead. */
    overscroll-behavior: contain;
  }
  .layout-sources.has-scrollbar {
    padding-right: 4px;
  }
  /* Andreas 2026-09-30: Chrome's wide square scrollbar here, styled like Safari's - a slim grey
     thumb with rounded ends, no track. (Safari's own overlay scrollbar ignores this and stays as
     it is. Only this panel box - the page's own scrollbar is left alone.) */
  .layout-sources::-webkit-scrollbar {
    width: 10px;
  }
  .layout-sources::-webkit-scrollbar-track {
    background: transparent;
  }
  .layout-sources::-webkit-scrollbar-thumb {
    background: #9a9a9a;
    background-clip: padding-box;
    border: 2px solid transparent;
    border-radius: 5px;
  }
  .layout-sources::-webkit-scrollbar-thumb:hover {
    background-color: #7a7a7a;
  }
}

.side-panel {
  background: var(--panel-bg);
  border: 1px solid var(--card-border);
  border-radius: 10px;
  padding: 1rem;
}

.side-panel h2 {
  font-size: 1rem;
  margin: 0 0 0.35rem;
}

/* Källor, Bevakningar and Versioner all use <details>/<summary> instead of
   a plain <h2> so each collapses natively (no JS needed) - open/closed
   state is restored from localStorage per panel, see
   PANEL_OPEN_STORAGE_PREFIX in app.js. The default disclosure triangle is
   suppressed (list-style + ::-webkit-details-marker/::marker) since the
   summary text itself is the click target. */
/* The whole summary is the drag handle for reordering the sidebar panels
   themselves (Användare/Källor/Bevakningar/Versioner) - see
   PANEL_ORDER_STORAGE_KEY in app.js - same as a whole source-row is below,
   rather than a separate small grip icon. touch-action: none stops touch
   drags from also scrolling the page - it has to be set unconditionally
   like this rather than toggled on/off by app.js, since real mobile
   browsers resolve whether a touch may pan at touchstart, not dynamically
   mid-gesture. With native scrolling blocked outright, app.js instead
   scrolls the page by hand in JS during a plain swipe, until a long-press
   arms an actual drag - see the "phase" state machine around
   PANEL_ORDER_STORAGE_KEY. */
.side-panel summary {
  display: flex;
  /* baseline so the smaller .panel-dev annotation sits on the same line as
     the title rather than floating half a line up. */
  align-items: baseline;
  font-size: 1rem;
  font-weight: bold;
  cursor: grab;
  list-style: none;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

.side-panel summary:active {
  cursor: grabbing;
}

.side-panel summary::-webkit-details-marker {
  display: none;
}

.side-panel summary::marker {
  content: "";
}

/* One consistent gap under the title of an open panel - same size whether
   what follows is the hint paragraph or the controls directly (Källor,
   Frakt, Sortering and Pris have no hint). Only when open, so a collapsed
   panel's title still sits tight inside its own padding.
   .source-panel-hint then adds the matching gap between hint and controls. */
.side-panel[open] summary {
  margin-bottom: 0.75rem;
}

/* Non-default filter value, shown after a panel's title so it's visible
   even when the panel is collapsed (see updateFilterTags in app.js). */
.panel-dev {
  margin-left: 0.5rem;
  min-width: 0;
  font-weight: normal;
  font-size: 0.8rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "Nollställ filter" - the last item inside the sidebar scroll box, after
   the Län panel, centred, in the same dark weight as the panel titles.
   Scrolls with the panels rather than floating pinned below them. Shown
   (app.js unsets [hidden]) only when some filter deviates from its default;
   .layout-sources' 1rem gap gives it its top spacing. */
#filter-reset {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0.3rem 0;
  background: none;
  border: none;
  font: inherit;
  /* Match the sidebar hint text (.source-panel-hint) - Andreas wants it
     no bigger than the body copy in this column. */
  font-size: 0.75rem;
  font-weight: bold;
  color: var(--text);
  text-align: center;
  cursor: pointer;
}

#filter-reset:hover {
  text-decoration: underline;
}

.side-panel.dragging {
  position: relative;
  z-index: 5;
  opacity: 0.4;
}

.side-panel.drag-over-before {
  border-top: 2px solid var(--text);
}

.side-panel.drag-over-after {
  border-bottom: 2px solid var(--text);
}

#versions-list {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

#versions-list a {
  display: block;
  padding: 0.35rem 0.4rem;
  border-radius: 6px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  color: var(--text);
  text-decoration: none;
  font-size: 15px;
  font-weight: 700;
}

#versions-list a:hover {
  border-color: var(--link);
}

.source-panel-hint {
  font-size: 0.75rem;
  color: var(--muted);
  /* Negative top pulls the hint up under the title: line-height 1.3 leaves
     ~2px of leading above the first line, which otherwise makes the
     title→hint gap look bigger than the equal hint→controls gap below.
     Nudge this if the balance still looks off. */
  margin: -0.15rem 0 0.75rem;
  line-height: 1.3;
}

#source-list,
#watch-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.watch-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.4rem;
  border-radius: 6px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
}

.watch-row .watch-word {
  flex: 1;
  min-width: 0;
  /* Was a lone 15px, the only leftover px font-size in the sidebar (breaks
     text-zoom scaling) - and heavier (700) than warranted for a row of
     saved search terms, not a heading. 0.85rem/500 (Andreas 2026-09-09). */
  font-size: 0.85rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}

.watch-row .watch-word:hover {
  color: var(--link);
}

/* Doubled selector (#search-form #watch-add) so this outranks the
   generic blue #search-form button rule that styles the Sök button. */
#search-form #watch-add {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 40px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text);
  cursor: pointer;
}

#search-form #watch-add svg {
  width: 28px;
  height: 28px;
}

/* Filled while the query in the box is one of your watches (see updateWatchIcon in app.js). */
#search-form #watch-add.is-watched svg {
  fill: currentColor;
}

/* Real hover only (Andreas 2026-09-29): on touch, mobile browsers keep :hover after a tap until
   the next tap elsewhere, so the icon looked watched when it wasn't. Same guard as .card-save. */
@media (hover: hover) and (pointer: fine) {
  #search-form #watch-add:hover svg {
    fill: currentColor;
  }
}

/* Icon-only on narrow screens (the tooltip/aria-label still carry the
   "Bevaka" meaning there) - a visible text label only once there's room
   next to the search field, matching fyndar.se's always-labeled button.
   See DESIGN.md's Konkurrenter-sektion. */
.watch-add-label {
  display: none;
}

@media (min-width: 861px) {
  #search-form #watch-add {
    width: auto;
    padding: 0 0.75rem 0 0.5rem;
  }

  .watch-add-label {
    display: inline;
    font-size: 0.9rem;
  }
}


.watch-remove {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.watch-remove svg {
  width: 16px;
  height: 16px;
}

.watch-remove:hover {
  color: #d33;
}

.source-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.4rem;
  border-radius: 6px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  cursor: grab;
}

.source-row.dragging {
  opacity: 0.4;
}

.source-row.drag-over-before {
  border-top: 2px solid var(--text);
}

.source-row.drag-over-after {
  border-bottom: 2px solid var(--text);
}

.visibility-toggle {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.visibility-toggle svg {
  width: 18px;
  height: 18px;
}

.visibility-toggle:hover {
  color: var(--text);
}

/* The disabled-row dimming (opacity on .source-row.disabled) already
   greys the slashed eye out along with everything else. */

.source-row .source-badge {
  flex: 1;
  min-width: 0;
}

.source-row.disabled {
  opacity: 0.45;
}

/* Source badge (favicon + optional name), used on result cards and in
   the sidebar rows. No background of its own - it always sits on an
   already-themed surface (the card, or the sidebar row), so a white chip
   here just looked like a stray light box in dark mode. */

.source-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  line-height: 0;
}

.source-badge img {
  height: 14px;
  width: auto;
  max-width: 100px;
  display: block;
}

/* Square favicon-style variant, used on individual result cards - a
   uniform square reads better in a tight row than mixed logo proportions. */
/* `.card .source-badge-favicon img`, not just `.source-badge-favicon img`: `.card img { width: 100% }`
   further down has the same specificity and wins otherwise, so the icon took 100 % of its badge.
   That is 24 px normally, but the "BIL" tag next to Blocket Bilar's icon made the badge 57 px wide
   and stretched the icon into an ellipse (prod, 2026-09-21). */
.source-badge-favicon img,
.card .source-badge-favicon img {
  width: 24px;
  height: 24px;
  max-width: none;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

/* Sidebar rows (icon-text variant): the favicon is a fixed 24px circle
   regardless of source, so the name text is sized to visually balance
   against it instead of the tiny default badge-name size. */
.source-badge-favicon .source-badge-name {
  /* Was a lone 15px (breaks text-zoom scaling, see .watch-word above for
     the same fix and reasoning) - 0.9rem is the nearest rem equivalent. */
  font-size: 0.9rem;
}

/* Trello #67 follow-up (Andreas 2026-09-13): the account page's saved-
   listing cards recheck each URL after loading (see startSavedRecheck in
   app.js) - a card's favicon starts invisible until ITS OWN check
   resolves, same "doesn't exist until its source is done" language the
   search page's own favicon row already uses (see .search-progress-icon
   / #search-progress.is-streaming), so the two features read as the same
   system rather than inventing new visual language. Scoped to
   #account-saved-list so ordinary search-result cards (never carrying
   .is-pending-check) are completely unaffected. */
#account-saved-list .card.is-pending-check .source-badge-favicon {
  visibility: hidden;
}

/* Andreas 2026-09-14: "priset... borde också vara synligt på samma
   sätt som ikonen. är det inte färdigsökt=osynlig" - the price hides
   right alongside the favicon while its check is pending, for the same
   reason: showing a number that might get silently corrected a moment
   later is worse than a brief blank. Same visibility:hidden mechanism
   (not display:none) so the card's layout doesn't jump when it appears -
   removing .is-pending-check (see startSavedRecheck's onmessage) reveals
   both at once. No pop-in animation added here on purpose - only the
   favicon's reveal was ever asked to bounce. */
#account-saved-list .card.is-pending-check .card-price {
  visibility: hidden;
}

/* Positioning context for .recheck-plop below - every favicon in this
   list gets it unconditionally, harmless on its own (no visible effect
   without an absolutely-positioned child), needed so .recheck-plop below
   has something to anchor to regardless of which card it lands on. */
#account-saved-list .card .source-badge-favicon {
  position: relative;
}

/* The pop-in bounce itself, same one the search page's favicons use once
   a card's check resolves - but NOT declared directly on a class present
   since the card's initial render (that was the bug, 2026-09-14: a CSS
   animation keeps running on its own clock even while the element is
   invisibility:hidden by is-pending-check above, so by the time a real
   check resolved seconds later and the favicon actually became visible,
   the 0.3s animation had already finished long ago - Andreas: "Favicon
   går från osynlig till synlig utan någon animation"). Fixed by having
   startSavedRecheck's onmessage handler (app.js) add THIS class fresh,
   at the exact moment it reveals the favicon, and remove it once the
   animation ends - so the animation's clock starts when the favicon
   actually appears, not when the card was first inserted into the DOM. */
#account-saved-list .source-badge-favicon.is-favicon-revealing {
  animation: source-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Gone (status "removed", "ended_sold" or "ended_unsold" from the
   recheck - see GONE_LISTING_STATUSES in app.js): grayscale the product photo, the source
   favicon AND the heart (Andreas 2026-09-22, reversing his own 2026-09-16 "ikonen ska inte vara
   gråmarkerad" - now every icon on a gone card reads as inactive). Andreas's earlier `> img`
   scoping (to dodge the favicon on purpose) is no longer needed since the favicon is included now
   too - a plain `.is-removed img` descendant selector covers the product photo and the favicon in
   one rule. */
#account-saved-list .card.is-removed img {
  filter: grayscale(1);
}
#account-saved-list .card.is-removed .card-save {
  filter: grayscale(1);
  opacity: 0.6;
}
/* Title stays plain text, no strikethrough (Andreas 2026-09-16: "rubriken
   ska inte strykas över") - just muted, matching the price notice's own
   treatment below. */
#account-saved-list .card.is-removed .card-title {
  color: var(--muted);
}

/* Andreas 2026-09-16: the price element is repurposed to hold
   LISTING_GONE_NOTICE's text instead of a real price (see renderCard/
   startSavedRecheck in app.js) - muted and un-bolded so it reads as a
   note, not as a (stale, misleading) number. */
#account-saved-list .card.is-removed .card-price {
  color: var(--muted);
  font-weight: 400;
}

/* The "+1" (still active) / "✕" (removed) badge popping up above a
   card's favicon once its recheck resolves - same animation/timing as
   the search page's .hit-count-plop (including its 0.18s delay, so the
   favicon's own pop-in above gets a beat to land first), just
   repositioned for this 24px favicon instead of the search row's 30px
   one. Colour is set inline per-call in app.js from the SAME
   SECTION_COLORS table bumpStatusCount already uses, so a listing's
   badge is always its own source's colour. */
#account-saved-list .recheck-plop {
  position: absolute;
  top: -1.5em;
  left: 0;
  /* Andreas 2026-09-13 (confirmed Safari-only via AskUserQuestion, same
     pattern as the Källor-panel jump the day before): a fixed 24px, NOT
     width:100% of the badge - the badge briefly measures 0x0 right at
     the instant this element is inserted (its favicon is only just now
     switching from is-pending-check's visibility:hidden to visible, one
     JS tick before the browser's own layout catches up to the 24px it
     settles at moments later - confirmed live via getBoundingClientRect
     inside the very MutationObserver callback that fires on insertion).
     Chrome's animation engine re-reads a percentage width on every
     frame regardless, so the same 0->24px badge resize self-heals there
     within one frame either way - Safari's apparently doesn't, and
     locks the animating element to whatever it measured at start,
     leaving "+1"/"✕" permanently invisible in a 0-width box. A fixed
     px value never depends on the parent's own live layout state at
     all, in any browser - matches the favicon's own fixed size
     (.source-badge-favicon img is a hardcoded 24px too). */
  width: 24px;
  text-align: center;
  overflow: hidden;
  font-size: 0.72em;
  font-weight: 700;
  white-space: nowrap;
  /* Found 2026-09-14 (Andreas: "+1 syns fortfarande inte"): without this,
     the "+1"/"✕" text's line box inherits .source-badge's line-height: 0
     (see that rule's own comment - a deliberate zero for the favicon-only
     badge, to avoid a stray gap under the icon) - collapsing THIS
     element's rendered height to 0px regardless of its font-size, so
     overflow: hidden above clipped the text away completely, animation
     or no animation. Same fix .source-badge-name and .source-badge-tag
     already use for the same reason, just missed here since this badge
     didn't exist yet when those were written. */
  line-height: 1;
  pointer-events: none;
  color: var(--plop-color, var(--text));
  animation-name: hit-count-plop-float;
  animation-duration: 1.6s;
  animation-timing-function: ease-out;
  animation-delay: 0.18s;
  animation-fill-mode: both;
}
@media (prefers-color-scheme: dark) {
  #account-saved-list .recheck-plop {
    color: var(--plop-color-dark, var(--plop-color, var(--text)));
  }
}
/* Andreas 2026-09-23: the "0" badge for a gone listing should read as inactive, same treatment as
   the favicon/photo/heart it floats above (.card.is-removed img, .card.is-removed .card-save) -
   not the source's own colour (that's only ever set for the "+1" still-active case now, see
   app.js). Two classes beat the plain .recheck-plop rule above without needing !important, and
   this deliberately skips the dark-mode override above too - --muted already adapts on its own. */
#account-saved-list .recheck-plop.is-gone-plop {
  color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
  #account-saved-list .card.is-pending-check .source-badge-favicon,
  #account-saved-list .card.is-pending-check .card-price {
    /* No pop-in to wait for without animation - reveal immediately
       rather than staying hidden forever for someone who never sees the
       transition play. Same treatment for the price (2026-09-14) as for
       the favicon it was copied from - one hidden-until-checked rule for
       both, so it gets the same reduced-motion exception too. */
    visibility: visible;
  }
  #account-saved-list .card .source-badge-favicon,
  #account-saved-list .recheck-plop {
    animation: none;
  }
}

.source-badge .source-badge-name {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--text);
  line-height: 1;
  white-space: nowrap;
}

.source-badge-tag {
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: white;
  background: var(--accent);
  border-radius: 4px;
  padding: 2px 5px;
  line-height: 1;
  white-space: nowrap;
}

/* Result sections */

.section-header {
  display: grid;
  /* headline left, count pill in a centred middle track, empty track
     right so the pill sits in the true middle of the header. */
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.5rem;
  margin: 1.75rem 0 0.75rem;
  padding: 0.6rem 0.75rem;
  /* Single-sided accent, so no border-radius (a rounded corner needs a
     border on all sides to land right). */
  border-top: 3px solid var(--section-line, var(--accent));
  border-radius: 0;
  position: sticky;
  /* Small gap above a pinned header so the content scrolling past stays
     visible behind it rather than the header sitting flush to the
     viewport edge (most noticeable in mobile view). Andreas 2026-09-17:
     #search-progress-header lost its own position:sticky (and with it,
     the need to coordinate who's on top with THIS header) - back to a
     plain fixed gap. */
  top: 4px;
  z-index: 3;
  background: var(--card-bg-translucent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  /* position: sticky + backdrop-filter has known GPU compositing glitches
     on some Android tablets, where the blurred translucent background
     paints as an oversized, wrongly-placed blank rectangle instead of
     staying pinned to the header itself. Forcing its own compositing
     layer is the standard mitigation - cheap and harmless if it turns out
     not to be the cause. */
  transform: translateZ(0);
  will-change: transform;
}

.section:first-child .section-header {
  /* Only trim the outer gap under the search bar - the accent line and
     its padding should look the same as every other section's header. */
  margin-top: 0.5rem;
}

.section-headline {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.section-favicon {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 4px;
  object-fit: contain;
}

.section-name {
  font-weight: 600;
  font-size: 1rem;
  color: var(--text);
}

.section-tag {
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: #fff;
  background: var(--section-line, var(--accent));
  border-radius: 4px;
  padding: 2px 5px;
  line-height: 1;
  white-space: nowrap;
}

.section-count {
  grid-column: 2;
  justify-self: center;
  /* Soft chip tinted from the source colour - not a solid shouty pill.
     The count text keeps the same weight and brightness as the source
     name beside it (var(--text)), never brighter. Mixed into --card-bg
     (not transparent) so it reads as a chip in both themes; fallback for
     no color-mix support is a neutral grey chip. */
  background: rgba(127, 127, 127, 0.14);
  background: color-mix(in srgb, var(--section-pill, var(--muted)) 16%, var(--card-bg));
  color: var(--text);
  font-size: 0.78rem;
  font-weight: 600;
  padding: 2px 10px;
  border-radius: 999px;
  white-space: nowrap;
}

.section-note {
  /* Full width, its own row under the headline/pill. */
  grid-column: 1 / -1;
  justify-self: start;
  color: var(--muted);
  font-size: 0.75rem;
  font-style: italic;
}

.new-section-title {
  /* Match the per-source .section-name exactly - the "Nytt" / pooled
     headers should read as the same kind of header, not a bigger one. */
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
  margin: 0;
}

/* The "Nytt" section's sparkle icon - fixed two-tone (compass red + blue),
   so it needs no theme handling; just size it like a favicon. */
.section-new-icon {
  flex: none;
  line-height: 0;
}

@media (prefers-color-scheme: dark) {
  .section-header {
    border-top-color: var(--section-line-dark, var(--section-line, var(--accent)));
  }
}

.section-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem;
}

.flat-grid {
  margin-top: 0.5rem;
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--card-border);
  border-radius: 10px;
  background: var(--card-bg);
  overflow: hidden;
  text-decoration: none;
  color: var(--text);
  /* The card itself is now the long-press-to-save target on touch (see
     app.js's bindSaveToggle) - same callout/selection suppression as
     .card img below, on the link itself rather than just the image. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

.new-badge {
  position: absolute;
  top: 10px;
  left: -30px;
  z-index: 2;
  background: #22c55e;
  color: #ffffff;
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 32px;
  transform: rotate(-45deg);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}

/* #67: save-toggle heart, top-right of the card image (mirrors .new-badge
   at top-left). Sits above the image (z-index) with its own translucent
   backdrop so it stays legible over any photo. `stroke` heart outline by
   default; .is-saved switches it to a solid fill via `fill` (see the SVG
   itself - fill:none/currentColor toggled here, not two different icons).

   touch-action:manipulation (Andreas 2026-09-11 bug report) - without it,
   mobile browsers can mistake a tap here for the start of a double-tap-
   to-zoom gesture and zoom the whole page in. This tells the browser
   "single taps here are just clicks, never a zoom gesture".

   Hidden by default on desktop, revealed on :hover (Andreas: declutter
   the card until you're actually looking at it) - see the hover rule and
   the reduced-motion/mobile overrides below. Always visible once saved
   regardless of hover, so a glance at the grid shows what's already
   saved without needing to mouse over every card. */
.card-save {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--card-bg-translucent);
  color: var(--text);
  cursor: pointer;
  touch-action: manipulation;
  /* The long-press-to-save gesture lives on the whole .card now, not
     just this button (see app.js's bindSaveToggle) - kept here too
     defensively, since this sits directly on top of the card/image and
     a touch landing exactly on it should behave the same way. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  opacity: 0;
  transition: opacity 0.15s ease, transform 0.15s ease;
}

.card-save svg {
  width: 18px;
  height: 18px;
  fill: none;
  transition: fill 0.15s ease;
}

.card-save.is-saved {
  color: #d0432f; /* compass-needle red - matches the brand's own accent red */
}

.card-save.is-saved svg {
  fill: currentColor;
}

/* Desktop only (hover has no real equivalent on touch - see the mobile
   long-press rule below): reveal on hovering the card, or the heart
   itself (keyboard/focus-visible users tabbing to the button directly). */
@media (hover: hover) and (pointer: fine) {
  .card:hover .card-save,
  .card-save:focus-visible {
    opacity: 1;
  }
}

/* Touch devices have no hover, and (Andreas 2026-09-11, revised) the
   heart is no longer a permanently-visible discoverability hint there
   either - fully invisible unless already saved, full stop. Saving on
   touch is a long-press on the whole card now (app.js), not a tap on
   this button - .is-pressing below (on .card, not this element) reveals
   it with a pulse purely as feedback WHILE held. */

.card-save.is-saved {
  opacity: 1;
}

/* Long-press feedback while held (app.js toggles .is-pressing on the
   CARD, not the heart - the gesture works anywhere on it now, see
   bindSaveToggle). Reveals the otherwise-invisible heart with a pulse
   so a touch user gets confirmation they're about to toggle it, then it
   either settles into .is-saved or disappears again if released early/
   cancelled. */
.card.is-pressing .card-save {
  opacity: 1;
  animation: card-save-pulse 0.5s ease-in-out infinite;
}

@keyframes card-save-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.2); }
}

@media (prefers-reduced-motion: reduce) {
  .card.is-pressing .card-save {
    animation: none;
  }
}

.card img {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--card-border);
  /* Best-effort against Android/iOS's own long-press-on-image menu
     preempting the card's long-press-to-save gesture (app.js) - see the
     contextmenu listener there for the known limitation this doesn't
     fully close on Android. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}

.card-body {
  /* Fills the full height of .card (which the grid row already stretches
     to match its tallest sibling), so .card-meta below can be pinned to
     the bottom regardless of how many lines .card-title wraps to. */
  flex: 1;
  padding: 0.6rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.card-title {
  font-size: 0.9rem;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.card-meta {
  /* Sticks to the bottom of .card-body's flex column, so price/favicon
     line up across cards in the same row even when titles wrap to a
     different number of lines. */
  margin-top: auto;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.8rem;
}

.card-price {
  font-weight: 600;
}

/* Vinted-only "~" prefix (see app.js's renderPriceInto/formatPrice) - a
   live currency-converted price, not the exact number .card-price's bold
   600 otherwise implies. Andreas 2026-09-18: "kan du ändra tildetecknet
   så att det ej är bold?" - normal weight only for the tilde itself, the
   digits right after it stay exactly as bold as every other price. */
.card-price-approx {
  font-weight: 400;
}

/* #67 follow-up (2026-09-14): a saved listing's price just before its
   last genuine change (history.saved_listings.previous_price) - sits
   right after the current price. Andreas was explicit about exactly
   three things here: struck through, grey, to the right - the current
   price's own font is untouched (.card-price above), and this
   deliberately does NOT add its own font-weight either, so it just
   inherits .card-price's 600 like plain text would. */
.card-price-old {
  margin-left: 0.4em;
  color: var(--muted);
  text-decoration: line-through;
}

@media (max-width: 860px) {
  #layout {
    grid-template-columns: 1fr;
    grid-template-areas:
      "search"
      "results";
  }

  /* Below this breakpoint, .layout-main's own grouping isn't wanted - the
     sidebar sits between the search bar and results here, not beside a
     combined block of both. display: contents drops the wrapper out of
     the box tree entirely so its children (the search block, results)
     rejoin #layout's grid directly, exactly as if .layout-main didn't
     exist. The search block (form + progress row) stays a single grid item
     so the icons remain pinned right under the search bar, above the
     sidebar - not auto-placed into an unnamed cell at the page bottom. */
  .layout-main {
    display: contents;
  }

  .search-block {
    grid-area: search;
  }

  .layout-results {
    grid-area: results;
  }

  /* The filter panels are a layer on top of the page (Andreas 2026-09-21), opened with the
     Filter button, not a block between the search bar and the results: the results start right
     under the search zones instead of ~1 500 px down. */
  .sidebar-slot {
    display: none;
    position: static;
  }
  /* Starts under the header (--sheet-top, set by placeFiltersLayer) so the logo and the Filter
     button stay where they are. Opaque (a see-through test was tried and dropped, Andreas 2026-09-21). */
  #layout.filters-open .sidebar-slot {
    display: block;
    position: fixed;
    inset: var(--sheet-top, 4.5rem) 0 0 0;
    z-index: 100;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: Canvas;
    color: CanvasText;
    padding: 0 0.75rem 2rem;
  }
  body.filters-open header {
    position: relative;
    z-index: 101; /* the "..." menu drops down over the layer */
  }
  .sidebar-sheet-head {
    display: none;
  }
  #layout.filters-open .sidebar-sheet-head {
    display: grid;
    grid-template-columns: 1fr auto 1fr; /* title left, the reset exactly centred like the one below */
    align-items: center;
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 0.75rem 0;
    margin-bottom: 0.5rem;
    background: Canvas;
    border-bottom: 1px solid var(--card-border);
  }
  .sidebar-sheet-title {
    font-size: 1.1rem;
    font-weight: 600;
  }
  /* Same size and place as #filter-reset at the bottom: 0.75rem bold, centred. */
  #filter-reset-top {
    grid-column: 2;
    border: 0;
    background: none;
    color: var(--text);
    font: inherit;
    font-size: 0.75rem;
    font-weight: bold;
    text-align: center;
    padding: 0.3rem 0;
    cursor: pointer;
  }
  #filter-reset-top:hover { text-decoration: underline; }
  #filter-reset-top[hidden] { display: none; }
  /* While the layer is open ONLY the layer scrolls (Andreas 2026-09-22: on the phone the page behind
     could be scrolled, the Filter button went off screen and the layer could not be closed).
     overflow:hidden on <body> is ignored by iOS Safari, so the page is pinned with position:fixed. */
  body.filters-open {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    overflow: hidden;
  }
  html.filters-open {
    overflow: hidden;
    overscroll-behavior: none;
  }

  /* Andreas 2026-09-26: kontosidans kort visade bara 1 kolumn på vissa
     telefoner (t.ex. OnePlus 8 Pro) men 2 på andra (t.ex. Pixel 7), trots
     att båda är "moderna telefoner". Orsaken var att denna regel tidigare
     bara gällde under 480px - en telefon vars CSS-viewport hamnar strax
     över det (vanligt förekommande, ~481-550px beroende på tillverkarens
     egen visningsstorlek/DPI-mappning i Chrome) föll tillbaka på skrivbords-
     kortstorleken (minmax 220px) utan att vara bred nog för 2 sådana kort -
     ett dödläge där bredare telefoner fick FÄRRE kolumner än smalare.
     Flyttat hit till den redan existerande 860px-gränsen (samma gräns som
     redan lägger om HELA sidan till enkelkolumn-layout ovan) tar bort det
     dödläget helt - 150px-korten ryms i minst 2 kolumner över hela det
     telefon-bredd-intervallet appen redan behandlar som "mobil". */
  .section-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 0.75rem;
  }
}
@media (min-width: 861px) {
  .sidebar-sheet-head { display: none; }
}

@media (max-width: 480px) {
  body {
    padding: 0 0.75rem 0;
  }

  header {
    margin: 1.25rem auto 0.75rem;
  }

  h1 {
    /* Same "t"-ascent-matches-logo-height logic as desktop, scaled down
       proportionally: desktop is 83px for a 64px logo (ratio 1.297), so
       44px mobile logo * 1.297 =~ 57px. top offset scaled the same way
       (4px * 44/64 =~ 3px). */
    font-size: 57px;
    top: 3px;
  }

  .brand-logo {
    width: 44px;
    height: 44px;
  }

  .header-top {
    /* Grid areas/breakpoint now live on the base .header-top rule (see
       style.css near .brand/.header-meta) - nothing left to override
       here except tightening the gap and margin for mobile. */
    margin-bottom: 0.75rem;
    column-gap: 0.4rem;
    row-gap: 0.4rem;
  }

  #search-form {
    flex-wrap: wrap;
  }
}

/* Staging-only corner ribbon (app.js adds this element only when the page
   is loaded from staging.skattjakt.app - see there for why hostname-based
   detection instead of a server flag). Fixed (not absolute) so it stays
   put while scrolling; pointer-events:none so it can never block a click
   on whatever's underneath; a high z-index above everything else on the
   page, including the sticky section headers. Deliberately loud (bright
   red, impossible to miss) - the whole point is nobody mistakes this for
   the live site. */
#staging-ribbon {
  position: fixed;
  top: 22px;
  right: -46px;
  z-index: 9999;
  width: 170px;
  padding: 4px 0;
  background: #e0332f;
  color: #ffffff;
  font-family: system-ui, sans-serif;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-align: center;
  text-transform: uppercase;
  transform: rotate(45deg);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}


/* PROTOTYPE category chips (admin only, see categories.py / app.js): one
   scrollable row under the search box, no wrap so it never grows taller
   than one line on a phone. .is-locked = the choice has nothing left to
   split (no sub-level with 2+ options), so it is colour-marked instead of
   offering further choices. */
.cat-chips {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: 0.4rem;
  overflow-x: auto;
  overflow-y: hidden; /* only sideways movement scrolls the row */
  padding: 0.35rem 0;
  scrollbar-width: none;
}

.cat-chips::-webkit-scrollbar {
  display: none;
}

.cat-chips.is-dragging {
  user-select: none;
  -webkit-user-select: none;
}

.cat-chips[hidden] {
  display: none;
}

.cat-chip {
  flex: none;
  font: inherit;
  font-size: 0.85rem;
  color: var(--text);
  background: var(--panel-bg);
  border: 1px solid var(--card-border);
  border-radius: 999px;
  padding: 0.25rem 0.75rem;
  cursor: pointer;
  white-space: nowrap;
}

.cat-chip:hover {
  border-color: var(--accent);
}

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

.cat-chip .cat-chip-count {
  display: none; /* Andreas 2026-09-20: counts are not shown */
}

.cat-chip.is-selected {
  border-color: var(--accent);
  color: var(--accent);
}

.cat-chip.is-locked {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  cursor: default;
}

.cat-chip.is-locked .cat-chip-count {
  color: rgba(255, 255, 255, 0.8);
}

.cat-chip-group {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
}

.cat-chip.is-sub {
  font-size: 0.78rem;
  margin-left: 0.9rem;
}

.cat-chip.is-info {
  cursor: default;
  color: var(--muted);
  border-style: dashed;
}

@font-face {
  font-family: "Barlow";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url("/static/fonts/Barlow-400.woff2") format("woff2");
}

@font-face {
  font-family: "Barlow Condensed";
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  src: url("/static/fonts/BarlowCondensed-600.woff2") format("woff2");
}

/* Category chips (Andreas 2026-09-20, variant "C" of three compared): no
   rounded frames, no counts, a parent's sub-choices sit physically under it. */
.cat-chips {
  --chip-hi: var(--gold);
  gap: 0.9rem 1.6rem;
}
.cat-chip-group { gap: 0; }
.cat-chip { border-radius: 0; background: transparent; }
.cat-chip.is-sub { margin-left: 0; font-size: 0.8rem; }
.cat-chip.is-info { border-style: none; }

/* Kolumner: condensed caps under a top rule, subs as a ruled list */
.cat-chips { gap: 0.9rem 1.8rem; }
.cat-chips .cat-chip-group,
.cat-chips > .cat-chip { min-width: 7rem; }
.cat-chips .cat-chip {
  border: 0;
  border-top: 3px solid var(--card-border);
  padding: 0.45rem 0 0.4rem;
  text-align: left;
  font-family: "Barlow Condensed", system-ui, sans-serif;
  font-weight: 600;
  font-size: 1.05rem;
  letter-spacing: 0.02em; /* was 0.07em for all caps */
  text-transform: none; /* level 1 in normal case, so it does not compete with the hit count */
  line-height: 1;
  color: var(--text);
}
.cat-chips .cat-chip:hover { border-top-color: var(--chip-hi); }
.cat-chips .cat-chip.is-selected,
.cat-chips .cat-chip.is-locked {
  background: transparent;
  color: var(--chip-hi);
  border-top-color: var(--chip-hi);
}
.cat-chips .cat-chip.is-sub {
  width: 100%;
  border-top: 0;
  border-bottom: 1px solid var(--card-border);
  font-family: "Barlow", system-ui, sans-serif;
  font-weight: 400;
  font-size: 0.8rem;
  letter-spacing: 0;
  text-transform: none;
  padding: 0.35rem 0;
  color: var(--muted);
}
.cat-chips .cat-chip.is-sub:hover { color: var(--text); }
.cat-chips .cat-chip.is-sub.is-selected {
  color: var(--chip-hi);
  border-bottom-color: var(--chip-hi);
}

/* Columns of choices next to the chosen ones (two or more levels picked) */
.cat-chips .cat-chip-tree {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.cat-chips .cat-chip-cols {
  margin-left: calc(var(--cols-lvl, 1) * var(--indent));
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: 0.25rem 1.8rem;
}
/* plain choices: as few rows as fit the width, never more than three (--leaf-rows, set by fitLeafRows in
   app.js); when even three rows are too wide the row scrolls sideways */
.cat-chips .cat-chip-cols.is-leaf {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(var(--leaf-rows, 3), auto);
}
/* Only Barlow 400 and Barlow Condensed 600 are self-hosted. Asking Barlow for 600 made Safari
   synthesize a fat bold (Andreas 2026-09-21), so the column heads use the real weight and are set
   apart by the full text colour alone. */
.cat-chips { font-synthesis: none; }
.cat-chips .cat-chip.is-colhead {
  font-weight: 400;
  color: var(--text);
}

/* One indent per level: a level-n choice is pushed in n steps (text only, so the
   ruled line keeps spanning the whole column). */
.cat-chips { --indent: 0.9rem; }
.cat-chips .cat-chip.is-sub { padding-left: calc(var(--lvl, 0) * var(--indent)); }

/* ---- Layout zones under the search row (Andreas 2026-09-21: "inget ska hoppa") ----
   Order: icon row (with the tip in its slot) + count line, categories, results. Every zone keeps
   a fixed height from the first search on, so only the results may grow. The same on desktop
   and on phones; only the way the zones are stacked differs (see the two media blocks after). */
/* Icon row: the floating +N reaches at most ~31 px above the icons (top -1.5em, scale 1.4,
   rises 14 px while fading). The 28 px gap under the search row plus this padding cover it. */
#layout.has-searched #search-progress-header {
  margin-top: 0.75rem;
  padding-bottom: 0;
  padding-top: 0.3rem;
  transition: none;
}
/* The icons fade on their own (#search-progress.is-fading); the header must not, it holds
   the permanent count line. */
#layout.has-searched #search-progress-header.is-fading { opacity: 1; }

/* Count line: same slot before, during and after the search. "Söker..." keeps its width
   when it goes, so the text does not slide sideways either. */
#layout.has-searched #status-live {
  display: block;
  min-height: 1.5em;
}
/* An empty line has no text baseline, so "Söker..." sat 4 px lower until the first count
   arrived. A zero-width space gives it a real line from the start. */
#layout.has-searched #status-live:empty::before {
  content: "\200b";
}
/* Opacity, not visibility - Andreas 2026-09-22: "och fadar 'söker...'". The HEADER itself is
   deliberately excluded from is-fading's opacity (see #search-progress-header.is-fading above -
   it holds the permanent count line), so the label needs its own transition to fade rather than
   inheriting one from its ancestor. */
#layout.has-searched #search-progress-label {
  display: inline-flex;
  opacity: 0;
  transition: opacity 0.35s ease;
}
/* "Söker..." tied to is-active/is-streaming, same as the icon row itself (not to
   .search-block.is-searching, which comes off at the true fetch-complete moment) - Andreas
   2026-09-22: "ordet 'söker' ska försvinna samtidigt som ikonerna". An earlier version of this
   rule (same day) tied the label to is-searching instead, so it vanished early while the icons
   were still finishing their hold animation (see PLOP_LIFECYCLE_MS in app.js) - Andreas caught
   that as wrong: label and icon row hide together, both when the hold ends. */
#layout.has-searched #search-progress-header.is-active #search-progress-label,
#layout.has-searched #search-progress-header.is-streaming #search-progress-label {
  opacity: 1;
}
/* Comes after the is-active/is-streaming rule above (same specificity - source order breaks the
   tie) so it wins while is-fading is on alongside them, starting the label's own fade at the
   exact same moment the icon row's (#search-progress.is-fading) starts, instead of snapping only
   once is-active/is-streaming are removed at the very end. */
#layout.has-searched #search-progress-header.is-fading #search-progress-label {
  opacity: 0;
}
/* Count line centered in the column, like the pills in the source headers. Three columns
   put the text exactly in the middle and "Söker..." to its left, so neither moves when the
   label goes. The number is the point of the line: bold, full text colour, larger. */
#layout.has-searched #search-progress-status-row {
  display: grid;
  grid-template-columns: 1fr minmax(0, auto) 1fr;
  align-items: baseline;
}
#layout.has-searched #search-progress-label {
  grid-column: 1;
  justify-self: end;
}
#layout.has-searched #status-live {
  grid-column: 2;
  text-align: center;
  min-height: 2em;
}
/* No 'för "sökord"' on desktop (Andreas 2026-09-21): the query is in the search box. The
   '(kunde inte nå: ...)' warning stays, it is a separate node. */
#layout.has-searched #status-live .status-query { display: none; }
#layout.has-searched #status-live .status-count-pop {
  font-size: 1.25em;
  font-weight: 700;
  color: var(--text);
  line-height: 1;
}
/* #status stays for screen readers only; the sighted line is #status-live above. */
#layout.has-searched #status {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Tip: lives in the icon row's own slot (Andreas 2026-09-21: they are never shown at the same
   time, icons during the search, the tip after it). Icons and tip share grid row 1, so the
   tip needs no row of its own. It fades in only after the icons have faded out (0.35 s). */
#layout.has-searched #search-progress-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}
#layout.has-searched #search-progress-header > #search-progress { grid-area: 1 / 1; }
#layout.has-searched #search-progress-header > #search-tip {
  grid-area: 1 / 1;
  align-self: center;
  justify-self: center;
  margin: 0;
  max-width: 100%;
}
#layout.has-searched #search-progress-header > #search-progress-status-row { grid-area: 2 / 1; }
#layout.has-searched #search-tip:not([hidden]) {
  animation: search-tip-in 0.25s ease 0.35s both;
}

/* Category row: reserves what the FIRST level needs, 120 px at the most (measured on real
   searches: 95-120 px, three sub-choices under each heading). Going deeper (a click) grows the row
   to 148-181 px and pushes the results down then; that is the price of not leaving up to 60 px
   empty on every search (Andreas 2026-09-21: rarely are all the rows visible). Only for someone
   who has the row (admin prototype, see chipsZoneKnown). */
#layout.chips-zone.has-searched #category-chips {
  min-height: 120px;
}
#layout.has-searched #category-chips { margin-top: 0.25rem; }
#layout.has-searched #discover { margin-top: 0.75rem; }
#layout.chips-zone.has-searched #category-chips[hidden] {
  display: block;
  visibility: hidden;
}

/* Desktop: .layout-main and .search-block dissolve so all zones become siblings that can be
   ordered; spacing is set per zone (margins), not by one shared gap. */
@media (min-width: 861px) {
  #layout.has-searched .layout-main { gap: 0; }
  #layout.has-searched .search-block,
  #layout.has-searched .layout-results { display: contents; }
  #layout.has-searched #search-form { order: 0; }
  #layout.has-searched #search-progress-header { order: 1; }
  #layout.has-searched #category-chips { order: 3; }
  #layout.has-searched #discover { order: 4; }
  #layout.has-searched #results { order: 5; margin-top: 1.5rem; }
}

/* Phones: the search block keeps its own column (its zones are already in the right order), the
   grid gives the results their 1.5rem; spacing per zone as on desktop. The tip can wrap to two
   lines here, so the icon row's slot is always two lines tall. */
@media (max-width: 860px) {
  #layout.has-searched .search-block { gap: 0; }
  #layout.has-searched #search-progress-header {
    grid-template-rows: minmax(2.5rem, auto) auto;
  }
}

@keyframes search-tip-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The category row fades in the first time it shows for a search (opacity only: it must not
   move anything). */
.cat-chips.is-entering {
  animation: cat-chips-in 0.35s ease both;
}
@keyframes cat-chips-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  #layout.has-searched #search-tip:not([hidden]),
  .cat-chips.is-entering {
    animation: none;
  }
}

/* Andreas 2026-09-28: the app's own "Få notiser om nya fynd?" card, shown once before Android's
   permission dialog (see askPushOnFirstStart in app.js). Deliberately the same look as the cookie
   card (consent.js builds that one's CSS inline). */
#push-ask {
  position: fixed; left: 50%; bottom: 1rem; transform: translateX(-50%) translateY(12px);
  z-index: 9998; width: calc(100% - 2rem); max-width: 34rem; background: #fff; color: #1a1a1a;
  border: 1px solid #e2e2e2; border-radius: 10px; padding: 1rem 1.15rem;
  box-shadow: 0 8px 28px rgba(0, 0, 0, .18);
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  opacity: 0; pointer-events: none; transition: opacity .2s ease, transform .2s ease;
}
#push-ask.is-visible { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
#push-ask p { margin: 0 0 .75rem; }
#push-ask .push-ask-title { font-weight: 700; margin-bottom: .35rem; }
#push-ask .push-ask-actions { display: flex; gap: .6rem; flex-wrap: wrap; }
#push-ask button {
  flex: 1; min-width: 7rem; padding: .55rem .9rem; font-size: .92rem; font-weight: 600;
  border-radius: 7px; cursor: pointer; border: 1px solid transparent;
}
#push-ask .push-ask-yes { background: #468da3; color: #fff; }
#push-ask .push-ask-no { background: transparent; color: #1a1a1a; border-color: #c9c9c9; }
@media (prefers-color-scheme: dark) {
  #push-ask { background: #1e1e1e; color: #f0f0f0; border-color: #333; }
  #push-ask .push-ask-no { color: #f0f0f0; border-color: #444; }
}

/* Sponsor card on the start page (Andreas 2026-09-30, see sponsors.py and app.js renderSponsorCard):
   one calm row at the bottom of the screen - pale paper with rounded corners, a small icon, the name, "Sponsorplats", one sentence and the link. No shadow, tilt or
   stamp: it must never pull focus from the search. */
/* Andreas 2026-09-30: full width, so the sentence wraps as little as possible. */
#sponsor-slot {
  margin-top: 2rem;
}
.sponsor-card {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.9rem;
  background: #fbf9ef;
  color: #3a3a3a;
  text-decoration: none;
  font-size: 0.85rem;
  line-height: 1.35;
  /* Andreas 2026-09-30: the same edge on both sides - plain rounded corners, no pointed tag edge. */
  border-radius: 6px;
  -webkit-tap-highlight-color: transparent;
}
.sponsor-icon {
  flex: 0 0 26px;
  width: 26px;
  height: 26px;
}
.sponsor-icon img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.sponsor-icon.is-text {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: #2b3a55;
  color: #fff;
  font: 600 0.62rem "Barlow Condensed", system-ui, sans-serif;
  letter-spacing: 0.02em;
}
.sponsor-body {
  flex: 1;
  min-width: 0;
}
.sponsor-name {
  font: 600 0.8rem "Barlow Condensed", system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #2b3a55;
}
.sponsor-label {
  margin-left: 0.4rem;
  font: 600 0.62rem "Barlow Condensed", system-ui, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #9a8f5a;
}
.sponsor-go {
  display: block; /* the link on its own line (Andreas 2026-09-30) */
  margin-top: 0.15rem;
  color: #468da3;
  font-weight: 600;
  white-space: nowrap;
}
/* Night (Andreas 2026-10-01): the pale card glared on the dark page - the footer band's dark sand
   instead, with light text. */
@media (prefers-color-scheme: dark) {
  .sponsor-card {
    background: #24211a;
    color: #d8d2c0;
    box-shadow: inset 0 0 0 1px #34301f;
  }
  .sponsor-name { color: #cbb889; }
  .sponsor-label { color: #9a8f72; }
  .sponsor-go { color: #7fb6c8; }
}
.sponsor-card.is-open .sponsor-name {
  text-transform: none;
  letter-spacing: 0.02em;
  font-size: 0.9rem;
}
