  :root{
    --bg-0:#0a2440; --bg-1:#2c1a5c; --panel:#1e1f52; --line:rgba(255,255,255,0.16);
    --paper:#1e1f52; --amber:#9b7fe8; --amber-bright:#b39cf0; --amber-deep:#7a5cd6;
    --news-accent:#6f93f0; --bizfeed-accent:#e685c4; --transit-accent:#f0955f;
    --ink:#f2eefa; --ink-dim:#a8b4d0; --danger:#e0715a; --hero-border:rgba(255,255,255,0.4);
    --shadow-lg:0 24px 60px rgba(0,0,0,0.45); --shadow-md:0 10px 28px rgba(0,0,0,0.3);
    --glow:rgba(155,127,232,0.22); --panelT7:rgba(30,31,82,0.7); --panelT4:rgba(30,31,82,0.4);
    --sq-empty-bg:rgba(255,255,255,0.04); --sq-empty-border:rgba(255,255,255,0.06); --sq-plus:rgba(255,255,255,0.25);
  }
  [data-theme="light"]{
    --bg-0:#f3f2fa; --bg-1:#ece9f7; --panel:#ffffff; --line:#ddd8ef;
    --paper:#ffffff; --amber:#5847c9; --amber-bright:#7266d6; --amber-deep:#463699;
    --news-accent:#2f5bc9; --bizfeed-accent:#c93f96; --transit-accent:#c9622f;
    --ink:#211c38; --ink-dim:#6b6488; --danger:#c1503a; --hero-border:#c7bee8;
    --shadow-lg:0 20px 50px rgba(88,71,201,0.12); --shadow-md:0 8px 22px rgba(88,71,201,0.10);
    --glow:rgba(88,71,201,0.14); --panelT7:rgba(255,255,255,0.8); --panelT4:rgba(255,255,255,0.55);
    --sq-empty-bg:rgba(33,28,56,0.035); --sq-empty-border:rgba(33,28,56,0.10); --sq-plus:rgba(33,28,56,0.28);
  }
  *{box-sizing:border-box;}
  /* Custom line-icon set replacing emoji placeholders (📅/🚨/📰/📍 etc.)
     across the header, ask bar, and section headers -- sized/aligned to
     behave like an inline text glyph (1em square, baseline-nudged) so it
     drops into existing button/heading markup without layout changes,
     and uses currentColor so it always matches its surrounding text
     rather than needing its own color rules at every call site. */
  .icon{width:1em;height:1em;vertical-align:-0.16em;flex:none;color:currentColor;}
  .icon-lg{width:1.3em;height:1.3em;vertical-align:-0.22em;}

  /* ---- Skeleton loading states ----
     Previously these four cards just sat empty (no loading indicator at
     all) for however long their fetch took, or in the business card's
     case showed plain "Ladataan..." text. A shimmering placeholder that
     roughly mirrors the real content's shape reads as "this is loading"
     rather than "this is broken/empty", and reuses each section's real
     card classes (.eventCard, .newsRow, .feedItemCompact) so it sits
     exactly where the real content will land instead of causing a
     layout jump once it arrives. Each container's real render function
     replaces this markup outright, so it only ever shows once, briefly. */
  @keyframes skeletonShimmer{
    0%{background-position:200% 0;}
    100%{background-position:-200% 0;}
  }
  .skel{background:linear-gradient(90deg, var(--panelT4) 25%, var(--line) 37%, var(--panelT4) 63%);
    background-size:400% 100%;animation:skeletonShimmer 1.6s ease-in-out infinite;border-radius:6px;
    color:transparent!important;}
  .skelRow{display:flex;gap:12px;}
  .skelLine{height:12px;border-radius:4px;}
  .skelEventCard .eventCardPhoto{background:none;}
  .skelNewsRow{padding-bottom:14px;}

  /* Shared empty-state look: a muted icon over a short, plain-spoken
     message, instead of a bare line of text sitting in otherwise-empty
     space. An empty card isn't broken, so this stays quiet (no warning
     colors, no border) -- it just gives the empty space some shape. */
  .emptyState{display:flex;flex-direction:column;align-items:center;gap:10px;
    padding:30px 16px;text-align:center;color:var(--ink-dim);}
  .emptyState .icon-lg{width:34px;height:34px;opacity:0.45;}
  .emptyState p{margin:0;font-size:13px;line-height:1.5;max-width:260px;}
  html,body{margin:0;min-height:100%;max-width:100%;overflow-x:hidden;
    background:radial-gradient(1200px 800px at 15% -10%, var(--glow), transparent 60%), linear-gradient(160deg, var(--bg-0), var(--bg-1) 100%);
    font-family:'IBM Plex Sans',sans-serif;color:var(--ink); transition:background .2s ease, color .2s ease;}
  a{color:inherit;}

  header{display:flex;align-items:center;justify-content:space-between;padding:22px 28px;flex-wrap:wrap;gap:12px;
    max-width:1500px;margin:0 auto;}
  #townPill{display:flex;align-items:center;gap:4px;font-size:13.5px;color:var(--ink-dim);white-space:nowrap;}
  /* flex-wrap:wrap here specifically, not just relying on the outer
     header's own wrap -- this group (weather, login, theme, language)
     was previously one rigid non-wrapping row with no fallback of its
     own, so on a medium-width screen where Finnish text (e.g.
     "Kirjaudu" vs English "Login") tipped its total width past what
     actually fit, the language toggle at the end simply overflowed
     past the right edge instead of reflowing -- a real, reported bug
     specific to the Finnish UI. justify-content:flex-end keeps wrapped
     items aligned to the right, matching where this group already
     sits in the header. */
  #headerRightGroup{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;}
  #mobileBellBtn{display:none;}
  #brandTownMobile{display:none;}
  @media (max-width:900px){
    #townPill{display:none;}
    #mobileBellBtn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;flex:none;
      border:1px solid var(--line);background:var(--panelT7);border-radius:999px;font-size:15px;cursor:pointer;}
    /* !important here specifically, not just relying on cascade order --
       #accountBtn has a full base style rule later in this file (its
       actual button styling), and since that rule doesn't happen to be
       positioned before this one, plain display:none at equal
       specificity was silently losing to it regardless of viewport
       width. A genuine pre-existing bug, not something introduced by
       today's changes -- likely meant "Kirjaudu" never actually hid on
       mobile the way this rule always intended, confirmed by a real
       phone screenshot showing it visible where only the bell icon was
       supposed to be. */
    /* Icon-only, not hidden entirely -- a full "Kirjaudu"/"Login" text
       label takes real horizontal space on a crowded mobile header, but
       hiding the button completely removed the only way to reach
       login/account from mobile at all, a real functional regression a
       previous fix introduced without checking what it was actually
       removing. Matches the same compact circular size as the bell and
       settings buttons next to it. */
    #accountBtn{width:34px;height:34px;padding:0;justify-content:center;flex:none;}
    #accountBtnLabel{display:none;}
    #brandTownMobile{display:block;font-family:'IBM Plex Sans',sans-serif;font-size:12px;font-weight:500;
      color:var(--ink-dim);white-space:nowrap;}
    #brand .name{font-size:16px;line-height:1.3;}
    #brandMark{width:36px;height:42px;}
    #brandMark svg{width:36px;height:42px;}
  }
  /* Weather now lives in the header (matching the reference layout) instead
     of floating above the events card -- the forecast panel becomes a
     dropdown flyout anchored under the pill rather than a normal-flow
     block, since there's no room for it to push header content around. */
  #weatherWrap{position:relative;}
  #weatherForecast{position:absolute;top:calc(100% + 10px);right:0;z-index:30;
    background:var(--panel);border:1px solid var(--line);border-radius:12px;
    box-shadow:var(--shadow-md);padding:14px;margin:0 !important;max-width:none !important;}
  /* On mobile, right:0 anchors the dropdown to #weatherWrap's own right
     edge -- but #weatherWrap sits inside a header that's crowded with
     icons and can wrap onto its own line on narrow screens, so that
     anchor point isn't reliable there the way it is on a roomy desktop
     header. Confirmed showing up oddly positioned/overlapping other
     header content on some mobile devices. position:fixed spanning most
     of the viewport width (rather than being anchored to any specific
     header element at all) guarantees it always lands somewhere
     sensible and fully visible, regardless of how the header itself
     happens to be laid out on any given phone. */
  #weatherForecastCloseBtn{display:none;}
  @media (max-width:900px){
    #weatherForecast{position:fixed;top:96px;left:12px;right:12px;
      max-height:70vh;overflow-y:auto;overscroll-behavior:contain;}
    /* Only meaningfully needed on mobile (see the JS comment in
       renderWeatherForecast) -- hidden by the base rule above by
       default, shown only within this same mobile media query, so
       desktop's inline dropdown (which sits right under its own toggle
       button with no overlap risk) doesn't get an unnecessary close
       button cluttering it. Declared *after* the base display:none
       rule above on purpose: media queries don't add specificity, so
       at equal specificity the later rule in source order always wins
       regardless of viewport width -- placing this before the base
       rule (as an earlier version of this fix did) would have made
       the base rule silently win at every width, defeating this
       entirely regardless of whether the media query matched. */
    #weatherForecastCloseBtn{display:flex;position:absolute;top:10px;right:10px;
      width:28px;height:28px;border-radius:50%;border:none;background:var(--panelT4);
      color:var(--ink-dim);cursor:pointer;align-items:center;justify-content:center;
      font-size:14px;z-index:1;}
    #weatherForecastCloseBtn:hover{background:var(--panelT7);color:var(--ink);}
  }
  #brand{display:flex;align-items:center;gap:14px;cursor:pointer;}
  #brandMark{width:64px;height:76px;display:flex;align-items:center;justify-content:center;
    flex:none;filter:drop-shadow(0 3px 6px rgba(14,42,71,0.35));}
  #brandMark svg{width:64px;height:76px;}
  #brand .name{font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:clamp(21px,6.5vw,34px);white-space:nowrap;}
  #brand .name span{color:var(--amber);}

  #langToggle{display:flex;gap:2px;background:var(--panelT7);border:1px solid var(--line);
    border-radius:999px;padding:3px;}
  #langToggle button{border:none;background:none;color:var(--ink-dim);font-family:'Space Grotesk',sans-serif;
    font-size:12px;font-weight:500;padding:6px 14px;border-radius:999px;cursor:pointer;transition:all .12s ease;}
  #langToggle button.active{background:var(--amber);color:#fff;font-weight:700;}

  /* Same reasoning and same desktop/absolute vs mobile/fixed split as
     #weatherForecast above -- a dropdown anchored to a specific header
     element lands oddly on mobile, where the header itself is crowded
     enough to wrap onto its own line, so the anchor point isn't
     reliable there the way it is on a roomy desktop header. */
  #settingsWrap{position:relative;}
  #settingsBtn{border:none;background:var(--panelT7);border:1px solid var(--line);border-radius:999px;
    width:34px;height:34px;cursor:pointer;display:flex;align-items:center;justify-content:center;
    color:var(--ink);flex:none;}
  #settingsBtn:hover{background:var(--panelT4);}
  #settingsPanel{display:none;position:absolute;top:calc(100% + 10px);right:0;z-index:30;
    background:var(--panel);border:1px solid var(--line);border-radius:12px;
    box-shadow:var(--shadow-md);padding:14px;min-width:200px;}
  .settingsPanelRow{display:flex;align-items:center;justify-content:space-between;gap:16px;}
  .settingsPanelRow:not(:last-child){margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--line);}
  .settingsPanelRow > span{font-size:13px;color:var(--ink);font-weight:500;}
  @media (max-width:900px){
    #settingsPanel{position:fixed;top:96px;left:12px;right:12px;min-width:0;}
  }

  main{max-width:1500px;margin:0 auto;padding:0 24px 100px;}

  /* Hidden: this strip predates the "Mainostetut yritykset" card in the new
     3-col grid below the hero, which now shows the same business data.
     Kept in the DOM (not deleted) since renderLogoBanner()/swipe/rotation
     JS still references these elements -- hiding avoids touching that
     logic while removing it from what visitors actually see. */
  #logoBanner, #logoBannerDots, #companyInfoCta{display:none !important;}

  /* ---- vertically scrolling logo banner, between header and AI search ---- */
  #logoBanner{position:relative;height:210px;overflow:hidden;margin:20px 0 8px;
    border:1px solid var(--line);border-radius:14px;background:var(--panelT4);}
  /* On wide screens there's room to just show the whole board at once
     (up to however many businesses actually exist) rather than
     paginating -- matches "the card fills up as squares sell out"
     rather than always capping at 2 rows. Mobile keeps the fixed-height
     paginated behavior, since there generally isn't room for everyone
     on screen at once there regardless of the board's fill level. */
  @media (min-width:1000px){
    #logoBanner{height:auto;min-height:70px;overflow:visible;}
  }
  #logoBannerTrack{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;
    gap:12px;padding:14px;height:100%;box-sizing:border-box;transition:opacity .35s ease;}
  #logoBannerTrack.logoBannerFading{opacity:0;}
  .logoBannerItem{flex:none;box-sizing:border-box;display:flex;align-items:center;justify-content:center;
    border:3px solid transparent;}
  .logoBannerItem.logoBannerHighlight{border-color:var(--amber);border-radius:8px;overflow:hidden;}
  .logoBannerItem.logoBannerPremium{border-color:#f2c14e;border-radius:8px;overflow:hidden;
    animation:logoBannerShine 2.2s ease-in-out infinite;}
  @keyframes logoBannerShine{
    0%, 100%{box-shadow:0 0 0 0 rgba(242,193,78,0.45);}
    50%{box-shadow:0 0 16px 4px rgba(242,193,78,0.7);}
  }
  .logoBannerItem.logoBannerLegendary{border-radius:8px;overflow:hidden;position:relative;
    animation:logoBannerLegendaryGlow 2.5s ease-in-out infinite;}
  @keyframes logoBannerLegendaryGlow{
    0%{border-color:#ffd700;box-shadow:0 0 16px 4px rgba(255,215,0,0.6);}
    33%{border-color:#ff6b9d;box-shadow:0 0 16px 4px rgba(255,107,157,0.6);}
    66%{border-color:#4ecdc4;box-shadow:0 0 16px 4px rgba(78,205,196,0.6);}
    100%{border-color:#ffd700;box-shadow:0 0 16px 4px rgba(255,215,0,0.6);}
  }
  .logoBannerCrown{position:absolute;top:1px;right:1px;font-size:13px;line-height:1;z-index:2;
    filter:drop-shadow(0 1px 2px rgba(0,0,0,0.4));}
  .logoBannerItem img{width:100%;height:100%;object-fit:contain;}
  #logoBannerEmpty{display:flex;align-items:center;justify-content:center;height:100%;
    color:var(--ink-dim);font-size:13px;text-align:center;padding:0 20px;}
  #logoBannerDots{display:flex;justify-content:center;gap:6px;padding:0 0 10px;}
  #logoBannerDots span{width:6px;height:6px;border-radius:50%;background:var(--line);cursor:pointer;}
  #logoBannerDots span.active{background:var(--amber);}

  #companyInfoCta{display:block;margin:0 auto 20px;width:fit-content;background:color-mix(in srgb, var(--amber) 14%, transparent);
    border:1px solid color-mix(in srgb, var(--amber) 35%, transparent);color:var(--amber);font-family:'IBM Plex Mono',monospace;
    font-weight:400;font-size:12px;padding:8px 16px;border-radius:999px;cursor:pointer;}

  @media (min-width:1000px){
    #companyInfoCta{margin:0 auto 24px;width:fit-content;font-size:14.5px;font-weight:400;
      padding:12px 28px;background:color-mix(in srgb, var(--amber) 22%, transparent);border-width:1.5px;}
  }

  #companyInfoCta:hover{background:color-mix(in srgb, var(--amber) 24%, transparent);}

  #companyInfoOverlay{position:fixed;inset:0;background:rgba(4,9,16,0.72);z-index:3000;display:none;
    align-items:center;justify-content:center;padding:20px;}
  #companyInfoModal{position:relative;background:var(--paper);color:var(--ink);width:100%;max-width:780px;
    border-radius:14px;padding:40px 32px 32px;max-height:85vh;overflow-y:auto;overscroll-behavior:contain;box-shadow:var(--shadow-lg);
    font-family:'IBM Plex Sans',sans-serif;text-align:center;}
  #companyInfoModal .close{position:absolute;top:18px;right:20px;cursor:pointer;font-size:16px;color:var(--ink-dim);
    background:none;border:none;width:28px;height:28px;border-radius:50%;}
  #companyInfoModal .close:hover{background:rgba(0,0,0,0.06);}

  /* This modal is always a light card, regardless of the site's own
     dark/light theme toggle (same convention as #legalModal) -- so
     everything inside it needs fixed colors, not the theme variables
     (--ink, --ink-dim, --panelT4, --line) used when this content used
     to live inline on the page and shared the page's own background. */
  #companyPitchTitle{font-family:'Space Grotesk',sans-serif;font-size:clamp(24px,4vw,34px);margin:8px 0 16px;line-height:1.15;color:var(--ink);}
  #companyPitchTitle span{color:var(--amber-deep);}
  #companyInfoModal p.sub{color:var(--ink-dim);font-size:15px;max-width:640px;margin:0 auto 24px;line-height:1.6;}
  #companyInfoModal h2{color:var(--ink) !important;}
  #companyInfoModal .valueCard{color:var(--ink-dim);}
  #companyInfoModal .valueCard b{color:var(--ink);}
  #companyInfoModal .factCard, #companyInfoModal .hiwStep{background:rgba(0,0,0,0.035);border-color:var(--line);}
  #companyInfoModal .factCard span, #companyInfoModal .hiwStep span, #companyInfoModal .factsNote{color:var(--ink-dim);}
  #companyInfoModal .faqItem{border-bottom-color:var(--line);}
  #companyInfoModal .faqQ{color:var(--ink);}
  #companyInfoModal .faqA{color:var(--ink-dim);}
  #companyInfoModal #earlyBanner{color:var(--ink);}

  .pitchEyebrow{display:inline-flex;align-items:center;gap:6px;background:color-mix(in srgb, var(--amber) 14%, transparent);
    border:1px solid color-mix(in srgb, var(--amber) 35%, transparent);color:var(--amber);font-family:'IBM Plex Mono',monospace;
    font-size:12px;padding:6px 14px;border-radius:999px;margin-bottom:18px;}

  .valueRow{display:flex;gap:28px;justify-content:center;margin:8px 0 20px;flex-wrap:wrap;}
  #volumeNote{text-align:center;font-size:12.5px;color:var(--amber);font-family:'IBM Plex Mono',monospace;
    margin:0 0 8px;}
  .valueCard{max-width:220px;font-size:13px;color:var(--ink-dim);line-height:1.5;text-align:left;}
  .valueCard b{display:block;color:var(--ink);font-family:'Space Grotesk',sans-serif;font-size:22px;margin-bottom:6px;}

  #newsSection{margin-top:0;}
  #newsSection .feedSubhead{text-align:left;}
  #newsSection #newsSourceNote{text-align:left;}
  #eventsCountNote, #eventsSourceNoteWrap{display:none;}
  #newsCategoryFilter, #newsSourceNote{display:none !important;}

  #searchWrap{max-width:460px;margin:0 auto 40px;position:relative;}
  #searchRow{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;}
  #townInput{flex:1;min-width:220px;padding:14px 18px;border-radius:999px;border:1px solid var(--line);
    background:var(--panelT7);color:var(--ink);font-size:15px;font-family:'IBM Plex Sans',sans-serif;}
  #townInput:focus{outline:none;border-color:var(--amber);}
  #searchBtn{padding:14px 26px;border-radius:999px;border:none;cursor:pointer;
    background:linear-gradient(135deg,var(--amber-bright),var(--amber));color:#fff;
    font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:15px;white-space:nowrap;}
  #suggestions{position:absolute;top:calc(100% + 6px);left:0;right:0;background:var(--panel);
    border:1px solid var(--line);border-radius:12px;overflow:hidden;z-index:50;box-shadow:var(--shadow-md);display:none;}
  #suggestions.open{display:block;}
  .suggestion{padding:11px 18px;cursor:pointer;font-size:14px;display:flex;justify-content:space-between;
    align-items:center;border-bottom:1px solid var(--line);}
  .suggestion:last-child{border-bottom:none;}
  .suggestion:hover{background:color-mix(in srgb, var(--amber) 12%, transparent);}
  .suggestion .pop{color:var(--ink-dim);font-family:'IBM Plex Mono',monospace;font-size:11.5px;}
  #homeErr{color:var(--danger);margin:-24px auto 20px;font-size:13.5px;display:none;text-align:center;max-width:460px;}

  /* ---- board (now lives inside #companyInfoModal, not the main page) ---- */
  #boardCol{background:rgba(0,0,0,0.025);border:1px solid var(--line);border-radius:16px;
    padding:20px 18px 24px;margin:24px 0;}
  #boardHeader{padding:2px 0 8px;text-align:center;}
  #boardHeader h2{font-family:'Space Grotesk',sans-serif;font-size:24px;margin:0 0 4px;color:var(--ink);}
  #boardHeader .meta{color:#6b6248;font-family:'IBM Plex Mono',monospace;font-size:12.5px;}

  #qtyPicker{text-align:center;margin-top:14px;}
  #rectError{display:none;color:var(--danger);font-size:12.5px;font-weight:600;margin-top:10px;text-align:center;}
  #newsCategoryFilter{margin:6px 0 4px;padding:6px 12px;border-radius:999px;border:1px solid var(--line);
    background:var(--panelT7);color:var(--ink);font-size:12px;font-family:'IBM Plex Sans',sans-serif;cursor:pointer;
    max-width:100%;text-overflow:ellipsis;}

  /* ---- floating selection bar ---- */
  #selectionBar{
    position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:900;display:none;
    background:var(--paper);color:var(--ink);padding:14px 20px;border-radius:20px;
    box-shadow:var(--shadow-lg);align-items:center;gap:10px 16px;font-family:'Space Grotesk',sans-serif;
    max-width:min(92vw, 480px);flex-wrap:wrap;justify-content:center;
  }
  #selectionBar.open{display:flex;}
  /* The board now lives inside the company-info modal (a centered
     overlay), so the selection bar -- fixed to the viewport -- can
     safely stay simple and centered at all sizes; there's no separate
     desktop "board column" position to account for anymore. */
  @media (min-width:1000px){
    #selectionBar{position:static;transform:none;left:auto;bottom:auto;margin:16px 0 0;max-width:none;width:100%;}
  }
  #selectionBar .count{font-weight:700;font-size:14.5px;flex:1 1 100%;text-align:center;}
  #selectionBar button{border:none;border-radius:999px;padding:9px 18px;font-family:'Space Grotesk',sans-serif;
    font-weight:700;font-size:13.5px;cursor:pointer;white-space:nowrap;flex:0 0 auto;}
  #selectionBar .go{background:linear-gradient(135deg,var(--amber-bright),var(--amber));color:#fff;}
  #selectionBar .cancel{background:none;color:var(--ink-dim);text-decoration:underline;padding:9px 6px;}

  /* ---- modal ---- */
  #overlay{position:fixed;inset:0;background:rgba(4,9,16,0.72);z-index:2000;display:none;
    align-items:center;justify-content:center;padding:20px;backdrop-filter:blur(3px);}
  #modal{background:var(--paper);color:var(--ink);width:100%;max-width:440px;border-radius:14px;
    padding:30px 30px 24px;max-height:90vh;overflow-y:auto;overscroll-behavior:contain;box-shadow:var(--shadow-lg);position:relative;}
  #modal h2{font-family:'Space Grotesk',sans-serif;margin:0 0 4px;font-size:21px;}
  #modal .sub{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--ink-dim);margin-bottom:18px;}
  #modal label{display:block;font-size:12.5px;font-weight:600;margin:14px 0 6px;color:var(--ink);}
  #modal input[type=text],#modal input[type=email],#modal input[type=url]{
    width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:7px;
    font-family:'IBM Plex Sans',sans-serif;font-size:14px;background:var(--paper);}
  #modal textarea{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:7px;
    font-family:'IBM Plex Sans',sans-serif;font-size:14px;background:var(--paper);resize:vertical;min-height:44px;}
  #modal select{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:7px;
    font-family:'IBM Plex Sans',sans-serif;font-size:14px;background:var(--paper);color:var(--ink);cursor:pointer;}
  #modal select:focus{outline:none;border-color:var(--amber-deep);box-shadow:0 0 0 3px color-mix(in srgb, var(--amber) 18%, transparent);}
  #modal input:focus{outline:none;border-color:var(--amber-deep);box-shadow:0 0 0 3px color-mix(in srgb, var(--amber) 18%, transparent);}
  #modal .colorRow{display:flex;gap:9px;margin-top:6px;flex-wrap:wrap;}
  .logoBtn{padding:10px 16px;border:1px solid var(--line);border-radius:7px;background:var(--paper);
    color:var(--ink);font-family:'IBM Plex Sans',sans-serif;font-size:13.5px;cursor:pointer;}
  .logoBtn.primary{background:var(--amber);border-color:var(--amber-deep);color:#fff;font-weight:700;}
  .logoBtn:hover{background:var(--panelT7);}
  #modal .note{font-size:11.5px;color:var(--ink-dim);margin:8px 0;}
  #modal .swatch{width:30px;height:30px;border-radius:7px;cursor:pointer;border:2px solid transparent;}
  #modal .swatch.selected{border-color:var(--ink);box-shadow:0 0 0 2px var(--paper),0 0 0 3px var(--ink);}
  .additionalTownsBox{margin-top:16px;padding:12px 14px;background:var(--panelT7);border:1px solid var(--line);border-radius:9px;}
  .additionalTownsBox input[type=text]{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:7px;
    font-size:13.5px;background:var(--paper);font-family:'IBM Plex Sans',sans-serif;}
  #additionalTownSuggestions{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--paper);
    border:1px solid var(--line);border-radius:8px;z-index:20;display:none;max-height:180px;overflow-y:auto;overscroll-behavior:contain;}
  #additionalTownSuggestions.open{display:block;}
  #additionalTownSuggestions div{padding:8px 12px;cursor:pointer;font-size:13px;color:var(--ink);}
  #additionalTownSuggestions div:hover{background:#efe8d3;}
  #additionalTownsList{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;}
  .townChip{display:flex;align-items:center;gap:6px;background:var(--panelT7);color:var(--ink);font-size:12.5px;
    font-weight:600;padding:5px 6px 5px 12px;border-radius:999px;}
  .townChip .qtyBtn{cursor:pointer;background:rgba(0,0,0,0.08);border:none;border-radius:50%;width:20px;height:20px;
    display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:var(--ink);padding:0;}
  .townChip .qtyBtn:hover{background:rgba(0,0,0,0.16);}
  .townChip .qtyNum{min-width:14px;text-align:center;}
  .townChip span.rm{cursor:pointer;background:rgba(0,0,0,0.1);border-radius:50%;width:18px;height:18px;
    display:flex;align-items:center;justify-content:center;font-size:12px;}
  .planBox{margin-top:16px;}
  .planToggle{display:flex;gap:8px;margin-top:6px;}
  .planOpt{flex:1;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);
    font-family:'IBM Plex Sans',sans-serif;font-size:13px;font-weight:600;color:var(--ink-dim);cursor:pointer;}
  .planOpt.selected{background:var(--amber);border-color:var(--amber-deep);color:#fff;}
  #prepaidTermRow{display:flex;gap:8px;margin-top:8px;}
  .termOpt{flex:1;padding:9px 6px;border:1px solid var(--line);border-radius:8px;background:var(--paper);
    font-family:'IBM Plex Sans',sans-serif;font-size:12.5px;font-weight:600;color:var(--ink-dim);cursor:pointer;
    display:flex;flex-direction:column;align-items:center;gap:2px;}
  .termOpt .save{font-size:11px;font-weight:700;color:#4a7a5c;}
  .termOpt.selected{background:var(--amber);border-color:var(--amber-deep);color:#fff;}
  .termOpt.selected .save{color:#fff;}
  #modal .highlightBox{margin-top:16px;padding:12px 14px;background:#efe8d3;border:1px solid #ddd0a8;
    border-radius:9px;display:flex;gap:10px;align-items:flex-start;}
  #modal .highlightBox input{width:auto;margin-top:3px;}
  #modal .highlightBox p{margin:0;font-size:12px;line-height:1.45;color:var(--ink-dim);}
  #modal .price{font-size:24px;font-weight:700;margin:16px 0 4px;font-family:'Space Grotesk',sans-serif;}
  #modal .priceNote{font-size:11.5px;color:var(--ink-dim);margin-bottom:6px;}
  #trialNote{display:inline-block;background:#e8f0dc;color:#3d6b2f;font-size:12.5px;font-weight:700;
    padding:5px 12px;border-radius:999px;margin-top:14px;}
  #modal .submitBtn{width:100%;margin-top:16px;padding:14px;border:none;border-radius:8px;
    background:linear-gradient(135deg,var(--amber-bright),var(--amber));color:#fff;
    font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:15px;cursor:pointer;}
  #modal .close{position:absolute;top:18px;right:20px;cursor:pointer;font-size:16px;color:var(--ink-dim);
    background:none;border:none;width:28px;height:28px;border-radius:50%;}
  #modal .err{color:#b8452f;font-size:12.5px;margin-top:8px;display:none;font-weight:500;}

  footer{text-align:center;padding:8px 24px 24px;color:var(--ink-dim);font-family:'IBM Plex Mono',monospace;font-size:11px;}

  #legalBar{text-align:center;padding:16px 20px 4px;font-family:'IBM Plex Mono',monospace;font-size:11px;
    color:var(--ink-dim);}
  #legalBar .sep{margin:0 8px;opacity:0.5;}
  #legalBar a{color:var(--ink-dim);text-decoration:underline;}
  #legalBar a:hover{color:var(--amber);}

  #legalOverlay{position:fixed;inset:0;background:rgba(4,9,16,0.72);z-index:3000;display:none;
    align-items:center;justify-content:center;padding:20px;}
  #legalModal{position:relative;background:var(--paper);color:var(--ink);width:100%;max-width:560px;
    border-radius:14px;padding:32px;max-height:80vh;overflow-y:auto;overscroll-behavior:contain;box-shadow:var(--shadow-lg);
    font-family:'IBM Plex Sans',sans-serif;}
  #legalModal h2{font-family:'Space Grotesk',sans-serif;margin:0 0 16px;font-size:19px;}
  #legalModal .legalBody p{font-size:13.5px;line-height:1.6;margin:0 0 12px;color:var(--ink);}
  #legalModal .legalBody a{color:var(--amber-deep);}
  #legalModal .close{position:absolute;top:18px;right:20px;cursor:pointer;font-size:16px;color:var(--ink-dim);
    background:none;border:none;width:28px;height:28px;border-radius:50%;}
  #legalModal .close:hover{background:rgba(0,0,0,0.06);}

  #authOverlay{position:fixed;inset:0;background:rgba(4,9,16,0.72);z-index:3000;display:none;
    align-items:center;justify-content:center;padding:20px;}
  #authModal{position:relative;background:var(--paper);color:var(--ink);width:100%;max-width:380px;
    border-radius:14px;padding:32px;box-shadow:var(--shadow-lg);font-family:'IBM Plex Sans',sans-serif;}
  #authModal h2{font-family:'Space Grotesk',sans-serif;margin:0 0 6px;font-size:19px;}
  #authModal p.authSub{font-size:12.5px;color:var(--ink-dim);margin:0 0 18px;line-height:1.5;}
  #authModal .close{position:absolute;top:18px;right:20px;cursor:pointer;font-size:16px;color:var(--ink-dim);
    background:none;border:none;width:28px;height:28px;border-radius:50%;}
  #authModal .close:hover{background:rgba(0,0,0,0.06);}
  #authModal input[type=email], #authModal input[type=password]{width:100%;box-sizing:border-box;
    padding:10px 12px;margin-bottom:10px;border:1px solid #d8cfb8;border-radius:8px;font-size:13.5px;
    font-family:'IBM Plex Sans',sans-serif;}
  #authModal label.authConsent{display:flex;gap:8px;align-items:flex-start;font-size:12px;
    color:#5a5238;line-height:1.4;margin:2px 0 14px;cursor:pointer;}
  #authModal label.authConsent input{margin-top:2px;}
  #authModal button.authSubmit{width:100%;background:var(--amber);color:#fff;border:none;
    border-radius:8px;padding:11px;font-weight:700;font-size:13.5px;cursor:pointer;font-family:'IBM Plex Sans',sans-serif;}
  #authModal .authSwitch{text-align:center;font-size:12.5px;margin-top:14px;color:var(--ink-dim);}
  #authModal .authSwitch a{color:var(--amber-deep);cursor:pointer;text-decoration:underline;}
  #authModal .authErr{display:none;background:#fbe4e4;color:#8a2c2c;border-radius:8px;padding:8px 10px;
    font-size:12px;margin-bottom:10px;}
  #feedbackOverlay{position:fixed;inset:0;background:rgba(4,9,16,0.72);z-index:3000;display:none;
    align-items:center;justify-content:center;padding:20px;}
  #feedbackModal{position:relative;background:var(--paper);color:var(--ink);width:100%;max-width:380px;
    border-radius:14px;padding:32px;box-shadow:var(--shadow-lg);font-family:'IBM Plex Sans',sans-serif;}
  #feedbackModal h2{font-family:'Space Grotesk',sans-serif;margin:0 0 6px;font-size:19px;}
  #feedbackModal p.authSub{font-size:12.5px;color:var(--ink-dim);margin:0 0 18px;line-height:1.5;}
  #feedbackModal .close{position:absolute;top:18px;right:20px;cursor:pointer;font-size:16px;color:var(--ink-dim);
    background:none;border:none;width:28px;height:28px;border-radius:50%;}
  #feedbackModal .close:hover{background:rgba(0,0,0,0.06);}
  #feedbackModal textarea{width:100%;box-sizing:border-box;min-height:100px;padding:10px 12px;margin-bottom:10px;
    border:1px solid #d8cfb8;border-radius:8px;font-size:13.5px;font-family:'IBM Plex Sans',sans-serif;resize:vertical;}
  #feedbackModal input[type=email]{width:100%;box-sizing:border-box;padding:10px 12px;margin-bottom:10px;
    border:1px solid #d8cfb8;border-radius:8px;font-size:13.5px;font-family:'IBM Plex Sans',sans-serif;}
  #feedbackModal button.authSubmit{width:100%;background:var(--amber);color:#fff;border:none;
    border-radius:8px;padding:11px;font-weight:700;font-size:13.5px;cursor:pointer;font-family:'IBM Plex Sans',sans-serif;}
  #feedbackModal .err{display:none;background:#fbe4e4;color:#8a2c2c;border-radius:8px;padding:8px 10px;
    font-size:12px;margin-bottom:10px;}

  #nearbyOverlay, #favoritesOverlay, #digestOverlay, #accountVerifyOverlay, #transitStopMapOverlay{position:fixed;inset:0;background:rgba(4,9,16,0.72);z-index:3000;display:none;
    align-items:center;justify-content:center;padding:20px;}
  .simpleModal{position:relative;background:var(--paper);color:var(--ink);width:100%;max-width:480px;
    max-height:85vh;overflow-y:auto;overscroll-behavior:contain;
    border-radius:14px;padding:32px;box-shadow:var(--shadow-lg);font-family:'IBM Plex Sans',sans-serif;}
  .simpleModal h2{font-family:'Space Grotesk',sans-serif;margin:0 0 6px;font-size:19px;}
  .simpleModal p.authSub{font-size:12.5px;color:var(--ink-dim);margin:0 0 18px;line-height:1.5;}
  .simpleModal .close{position:absolute;top:18px;right:20px;cursor:pointer;font-size:16px;color:var(--ink-dim);
    background:none;border:none;width:28px;height:28px;border-radius:50%;}
  .simpleModal .close:hover{background:rgba(0,0,0,0.06);}
  .simpleModal button.authSubmit{width:100%;background:var(--amber);color:#fff;border:none;
    border-radius:8px;padding:11px;font-weight:700;font-size:13.5px;cursor:pointer;font-family:'IBM Plex Sans',sans-serif;}
  .simpleModal button.authSubmit:disabled{opacity:0.6;cursor:default;}
  .simpleModal .err{display:none;background:#fbe4e4;color:#8a2c2c;border-radius:8px;padding:8px 10px;
    font-size:12px;margin:12px 0 0;}
  .nearbyPrivacyNote{font-size:11.5px;color:var(--ink-dim);text-align:center;margin:14px 0 0;line-height:1.4;}
  #nearbyMap, #transitStopMap{height:220px;border-radius:12px;margin-bottom:14px;background:var(--panelT4);
    z-index:0;} /* z-index:0 keeps Leaflet's own internal panes (which use high z-indexes) from bleeding above this modal's own close button etc. */
  /* Leaflet's default div-icon adds its own background/border box --
     needs resetting to nothing so the custom SVG pin (see
     makeColoredPinIcon in app-board.js) renders on its own, not inside
     an extra white square. */
  .coloredPinIcon{background:transparent;border:none;}
  .nearbyListItem{display:flex;align-items:center;gap:12px;padding:10px 4px;
    border-radius:10px;transition:background 0.15s ease;}
  .nearbyListItem:hover{background:var(--panelT4);}
  .nearbyListItem:not(:last-child){border-bottom:1px solid var(--line);}
  /* Wraps only the clickable content -- the favorite button (see
     .bizFeedRowLink's CSS comment for the full reasoning) is a sibling
     of this, not nested inside it, so a heart click can't possibly
     reach the link underneath regardless of event-handling quirks. */
  .nearbyListItemLink{display:flex;align-items:center;gap:12px;flex:1;min-width:0;
    text-decoration:none;color:inherit;}
  .nearbyListItemLink img{width:40px;height:40px;border-radius:10px;object-fit:contain;background:#fff;
    border:1px solid var(--line);padding:4px;box-sizing:border-box;flex:none;}
  .nearbyListItemText{flex:1;min-width:0;}
  .nearbyListItemText b{display:block;font-size:13.5px;color:var(--ink);font-weight:700;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .nearbyListItemText span{display:block;font-size:11.5px;color:var(--ink-dim);}
  .nearbyDistance{flex:none;font-size:12px;font-weight:700;color:var(--amber);white-space:nowrap;}
  .nearbyFavoriteBtn{position:static;flex:none;}
  .nearbyEmptyMsg{text-align:center;color:var(--ink-dim);font-size:13px;padding:20px 0;}
  .favoritesTab{background:var(--bg-0);border:1px solid var(--line);color:var(--ink-dim);
    font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:12.5px;padding:8px 14px;
    border-radius:999px;cursor:pointer;transition:background 0.15s ease,color 0.15s ease,border-color 0.15s ease;}
  .favoritesTab:hover{border-color:var(--amber);color:var(--amber);}
  .favoritesTab.active{background:var(--amber);border-color:var(--amber);color:#fff;}

  #accountBtn{border:none;background:var(--panelT7);border:1px solid var(--line);border-radius:999px;
    padding:0 14px;height:34px;cursor:pointer;font-size:12.5px;color:var(--ink);font-family:'Space Grotesk',sans-serif;
    font-weight:400;display:flex;align-items:center;gap:6px;white-space:nowrap;}
  #accountBtn.accountBtnAdmin{background:var(--amber);border-color:var(--amber-deep);color:#fff;font-weight:700;cursor:default;}

  #howItWorks{margin-top:56px;text-align:center;}
  #howItWorks h2{font-family:'Space Grotesk',sans-serif;font-size:22px;color:var(--ink);margin:0 0 28px;}
  .hiwSteps{display:flex;gap:20px;justify-content:center;flex-wrap:wrap;}
  .hiwStep{max-width:200px;text-align:left;background:var(--panelT4);border:1px solid var(--line);
    border-radius:10px;padding:18px;}
  .hiwStep b{display:block;width:26px;height:26px;border-radius:50%;background:var(--amber);color:#fff;
    font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:13px;text-align:center;line-height:26px;margin-bottom:10px;}
  .hiwStep span{font-size:13px;color:var(--ink-dim);line-height:1.5;}

  #weatherWidget{display:flex;align-items:center;justify-content:center;gap:8px;width:fit-content;margin:0;
    background:var(--panelT4);border:1px solid var(--line);border-radius:999px;padding:7px 16px;
    cursor:pointer;transition:background-color 0.15s;}
  #weatherWidget:hover{background:var(--panelT7);}
  #weatherWidget .weatherChevron{font-size:10px;color:var(--ink-dim);transition:transform 0.15s;}
  #weatherWidget.expanded .weatherChevron{transform:rotate(180deg);}
  #weatherIcon{font-size:20px;line-height:1;display:inline-flex;align-items:center;}
  #weatherIcon img{width:28px;height:28px;display:block;}
  #weatherTemp{font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:15px;color:var(--ink);}
  #weatherForecast{display:flex;flex-direction:column;gap:12px;}
  #weatherHourlyRow, #weatherDaysRow{display:flex;gap:8px;overflow-x:auto;padding:2px;}
  .forecastDay, .forecastHour{flex:none;width:78px;background:var(--panelT4);border:1px solid var(--line);border-radius:10px;
    padding:10px 6px;text-align:center;}
  .forecastDay .dName, .forecastHour .hTime{font-family:'Space Grotesk',sans-serif;font-size:12.5px;font-weight:700;color:var(--ink);}
  .forecastDay .dIcon, .forecastHour .dIcon{font-size:32px;margin:4px 0;display:flex;align-items:center;justify-content:center;}
  .forecastDay .dIcon img, .forecastHour .dIcon img{width:46px;height:46px;}
  .forecastDay .dTemps, .forecastHour .dTemps{font-size:12px;color:var(--ink-dim);}
  .forecastDay .dTemps b, .forecastHour .dTemps b{color:var(--ink);}
  /* A thin top-edge accent + matching icon color per card, so the four
     card types read as distinct at a glance rather than all sharing the
     exact same neutral-ink icon: events takes the brand violet (it's the
     lead card), the transit card its own dedicated coral
     (--transit-accent), news a violet-leaning blue, and businesses pink.
     inset box-shadow (not border-top) so it follows the card's existing
     border-radius without needing separate corner math. */
  #eventsSection{background:var(--panel);border:1px solid var(--line);border-radius:20px;
    padding:18px 20px;box-shadow:var(--shadow-md), inset 0 3px 0 0 var(--amber);transition:box-shadow 0.2s ease;}
  #eventsSection .feedSubhead svg{color:var(--amber);}
  #eventsSection:hover, #newsSection:hover, #bizFeedCard:hover, #offersSection:hover{box-shadow:0 10px 28px rgba(88,71,201,0.14);}
  #newsSection{margin-bottom:0;background:var(--panel);border:1px solid var(--line);
    border-radius:20px;padding:18px 20px;box-shadow:var(--shadow-md), inset 0 3px 0 0 var(--news-accent);transition:box-shadow 0.2s ease;}
  #newsSection .feedSubhead svg{color:var(--news-accent);}
  #bizFeedCard{background:var(--panel);border:1px solid var(--line);border-radius:20px;
    padding:0;box-shadow:var(--shadow-md), inset 0 3px 0 0 var(--bizfeed-accent);overflow:hidden;display:flex;flex-direction:column;transition:box-shadow 0.2s ease;}
  #bizFeedCardHead h3 svg{color:var(--bizfeed-accent);}
  #bizFeedCardHead{padding:16px 18px;border-bottom:1px solid var(--line);
    display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;}
  #bizFeedCardHead > div{min-width:0;}
  #bizFeedCardHead h3{font-family:'Space Grotesk',sans-serif;font-size:15px;color:var(--ink);margin:0 0 3px;}
  #bizFeedCardHead p{font-size:11px;color:var(--ink-dim);margin:0;}
  /* Light mode (the default, matching the prototype) drops the border in favor
     of a bigger, softer shadow -- the border was added specifically to fix a
     dark-mode contrast issue, so it stays there but isn't needed here.
     Split into one rule per card (rather than one shared selector) so
     each can combine the same softer light-mode shadow with its own
     accent color -- a single shared rule can't set three different
     inset colors at once. */
  [data-theme="light"] #eventsSection{border:none;box-shadow:0 16px 40px rgba(30,20,70,0.10), inset 0 3px 0 0 var(--amber);}
  [data-theme="light"] #newsSection{border:none;box-shadow:0 16px 40px rgba(30,20,70,0.10), inset 0 3px 0 0 var(--news-accent);}
  [data-theme="light"] #bizFeedCard{border:none;box-shadow:0 16px 40px rgba(30,20,70,0.10), inset 0 3px 0 0 var(--bizfeed-accent);}
  #bizFeedScrollWindow{height:auto;overflow:hidden;position:relative;flex:1;
    display:flex;align-items:center;
    mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);}
  #bizFeedScrollTrack{display:flex;flex-direction:row;width:max-content;animation:bizFeedScroll 20s linear infinite;}
  /* Freezes the animation when there's only one business (see
     renderBizFeedCard) -- a class instead of an inline style, so it
     doesn't permanently outrank every hover-pause rule below no matter
     how they're written. Only one business means nothing to scroll
     through anyway, so this is never really "in conflict" with hover in
     practice, but it's still correct to let the cascade actually work
     instead of quietly locking it out. */
  #bizFeedScrollTrack.bizFeedScrollTrackStatic{animation-play-state:paused;}
  #bizFeedScrollWindow:hover #bizFeedScrollTrack{animation-play-state:paused;}
  /* Redundant with the rule above on paper (hovering a descendant also
     means its ancestors are :hover'd, so this "shouldn't" be necessary)
     -- kept anyway as a more direct safeguard with no dependency on
     that bubbling working exactly as expected on every browser/device,
     since a reported case of the container-hover pause not visibly
     taking effect couldn't be reproduced reliably enough here to
     pin down a single root cause. Costs nothing since both rules set
     the same value when they do apply. */
  #bizFeedScrollTrack:hover{animation-play-state:paused;}
  /* Touch devices have no :hover at all, so the row never paused there
     -- tapping the favorite heart mid-scroll meant tapping a target
     that was actively moving out from under your finger. This
     JS-driven class (see the pointerdown/pointerup listeners) covers
     touch the same way :hover already covers desktop mouse use. */
  #bizFeedScrollWindow.interacting #bizFeedScrollTrack{animation-play-state:paused;}
  @keyframes bizFeedScroll{ 0%{transform:translateX(0);} 100%{transform:translateX(-50%);} }
  .bizFeedRow{display:flex;flex-direction:column;align-items:center;padding:22px 24px;
    border-right:1px solid var(--line);flex:none;white-space:nowrap;position:relative;
    transition:background 0.15s ease;}
  .bizFeedRow:hover{background:var(--panelT4);}
  /* Wraps only the logo+text, not the whole row -- the favorite button
     is a sibling of this, not nested inside it (see the HTML in
     renderBizFeedCard). Previously the button *was* nested inside the
     row's own <a>, relying on stopPropagation()/preventDefault() in its
     click handler to stop the click from also triggering navigation --
     which tested correctly here but was reported as unreliable on at
     least one real device/browser combination. Structurally moving the
     button outside the link entirely removes any dependency on that
     event-handling behavior working a specific way: a click on the
     heart now simply cannot reach the link at all, regardless of
     browser quirks. */
  .bizFeedRowLink{display:flex;flex-direction:column;align-items:center;gap:10px;
    text-decoration:none;color:inherit;}
  .bizFeedLogo{width:76px;height:76px;border-radius:14px;object-fit:contain;flex:none;
    background:#fff;padding:7px;box-sizing:border-box;border:1px solid var(--line);box-shadow:0 2px 6px rgba(0,0,0,0.06);
    transition:transform 0.15s ease, box-shadow 0.15s ease;}
  .bizFeedRow:hover .bizFeedLogo{transform:translateY(-2px);box-shadow:0 8px 18px rgba(88,71,201,0.18);}
  .bizFeedRow b{font-size:13px;font-weight:700;color:var(--ink);display:block;line-height:1.25;text-align:center;}
  .bizFeedRowIndustry{display:block;font-size:11px;color:var(--ink-dim);text-align:center;margin-top:2px;}
  .bizFeedRowText{min-width:0;max-width:110px;}
  /* Sibling of .bizFeedRowLink now, not nested inside it -- see the CSS
     comment on .bizFeedRowLink above for why. Neutral outline by
     default, filled brand-color heart once saved, matching the same
     "small icon button" language used for the chat toolbar buttons. */
  .bizFeedFavoriteBtn{position:absolute;top:6px;right:6px;width:36px;height:36px;border-radius:50%;
    border:none;background:var(--panelT7);color:var(--ink-dim);cursor:pointer;
    display:flex;align-items:center;justify-content:center;z-index:5;
    transition:background 0.15s ease, color 0.15s ease, transform 0.15s ease;}
  .bizFeedFavoriteBtn:hover{background:var(--panelT4);transform:scale(1.1);}
  /* A solid filled heart, not just a different-colored outline -- the
     outline-only version was confirmed to actually work (the click,
     the state change, all of it) but was subtle enough on a small icon
     that a real click could look like nothing happened at a glance.
     A filled heart is the unambiguous, universally-understood
     "favorited" signal (matches how this pattern reads everywhere
     else -- Instagram, X/Twitter, etc.), and the brief scale pop makes
     the moment of toggling itself visible, not just the end state. */
  .bizFeedFavoriteBtn.favorited{color:#d6336c;background:#fce4ec;}
  .bizFeedFavoriteBtn.favorited svg{fill:currentColor;}
  .bizFeedFavoriteBtn.favorited{animation:favoritePop 0.3s ease;}
  @keyframes favoritePop{
    0%{transform:scale(1);}
    40%{transform:scale(1.3);}
    100%{transform:scale(1);}
  }
  .bizFeedFavoriteBtn svg{width:18px;height:18px;transition:fill 0.15s ease;}
  #bizFeedEmpty{font-size:12.5px;}
  #bizFeedLoading{padding:20px 18px;font-size:12.5px;color:var(--ink-dim);text-align:center;}
  #bizFeedCta{flex:none;white-space:nowrap;padding:8px 14px;border-radius:999px;border:none;cursor:pointer;
    background:var(--amber);color:#fff;font-weight:700;font-size:12px;font-family:'Space Grotesk',sans-serif;
    transition:box-shadow 0.15s ease, transform 0.15s ease;}
  #bizFeedCta:hover{box-shadow:0 4px 14px rgba(88,71,201,0.35);transform:translateY(-1px);}
  /* Events and news sit side by side as two cards from 1000px up -- each
     keeps a single-column internal item list at that width since they're
     now half-width, rather than the old 2-3 internal columns which were
     sized for each section spanning the full page width. */
  /* Pulled up over the photo's bottom edge (like the reference layout) --
     positive z-index + relative so the cards paint over the photo's
     ::before rather than being clipped by it. Note: if askHeroResults is
     open (a long AI answer pushes #heroPhotoWrap taller than the photo's
     fixed 380px), this fixed overlap can start cutting into the
     results text instead of just the photo -- worth an eye on that state
     specifically, not just the empty/default view. */
  /* align-items:stretch (not the old start) is what actually makes
     #offersSection (the transit card)/bizFeedCard match their row-sibling's height -- with
     start, each grid row is still sized to its tallest item (events,
     news), but a shorter sibling's own box just stops early instead of
     filling that reserved space, leaving bare page background in the
     gap. stretch fills it at the CSS level, independent of any JS
     load timing, so it can't be thrown off by events/news arriving in
     whatever order the network happens to return them. In the single-
     column mobile layout below there's only one item per row anyway, so
     stretch has nothing to visibly change. */
  #homeGrid{display:grid;grid-template-columns:minmax(0,1fr);gap:20px;align-items:stretch;
    position:relative;z-index:2;margin-top:-22px;}
  @media (min-width:1000px){
    #homeGrid{grid-template-columns:minmax(0,2.3fr) minmax(0,1fr);}
    /* Explicit placement instead of relying on implicit auto-placement
       (DOM order alone) -- needed specifically because #newsSection
       becomes position:absolute when expanded (see .feedCardExpanded
       below), which takes it out of the auto-placement algorithm
       entirely. Without an explicit assignment here, #bizFeedCard (the
       next item in DOM order) would silently auto-place itself into
       news's now-vacated cell instead of staying in column 2 -- exactly
       what was happening, confirmed by testing this in isolation. */
    #eventsSection{grid-column:1;grid-row:1;}
    #bizFeedCard{grid-column:2;grid-row:1;}
    #newsSection{grid-column:1;grid-row:2;}
    #offersSection{grid-column:2;grid-row:2;}
  }

  /* Mobile-only stacking order: events, news, businesses, transit --
     different from the DOM order (events, transit, news, businesses)
     that the desktop 2x2 grid pairs up row-by-row. `order` only needs to
     be set below 1000px -- once the desktop media query above kicks in,
     these simply stop applying (order isn't set anywhere else), and every
     item falls back to its default order:0, which resolves ties by DOM
     order -- exactly the original events/offers/news/biz pairing, so
     nothing needs to be explicitly reset for wider viewports. */
  @media (max-width:999px){
    #eventsSection{order:1;}
    #newsSection{order:2;}
    #bizFeedCard{order:3;}
    #offersSection{order:4;}
  }
  #offersSection{background:var(--panel);border:1px solid var(--line);border-radius:20px;
    padding:20px;box-shadow:var(--shadow-md), inset 0 3px 0 0 var(--transit-accent);transition:box-shadow 0.2s ease;
    display:flex;flex-direction:column;}
  #offersSection .feedSubhead svg{color:var(--transit-accent);}
  [data-theme="light"] #offersSection{border:none;box-shadow:0 16px 40px rgba(30,20,70,0.10), inset 0 3px 0 0 var(--transit-accent);}
  /* 4 standard rows at 140px + 3×18px gaps = 614px total. 5 of these
     compact rows + 4×18px gaps need to fit that same total, so
     (614 - 72) / 5 = ~108px each -- scaled down from the standard row
     since a transit stop row is just a stop name + distance + a row of
     departure chips, no description paragraph or photo, so it doesn't
     need the full 140px to read cleanly. */
  .feedItem.feedItemCompact{flex:1;height:auto;padding:12px 16px;display:flex;flex-direction:column;
    justify-content:center;align-items:stretch;}
  .feedItem.feedItemCompact b{font-size:13.5px;-webkit-line-clamp:2;}
  .transitStopRow{cursor:pointer;transition:background 0.15s ease;}
  .transitStopRow:hover{background:var(--panelT4);}
  /* Replaces the old single run-on text line (see the comment above
     makeTransitStopEl in app-board.js) with distinct per-departure
     chips -- wraps naturally onto a second line on narrow cards rather
     than truncating or overflowing. */
  .transitDepartures{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;}
  .transitDepartureChip{display:inline-flex;align-items:center;gap:5px;
    background:var(--panelT4);border:1px solid var(--line);border-radius:8px;
    padding:3px 8px 3px 3px;}
  /* Solid, confident color -- a real route-number badge, the same
     scannable pattern actual transit apps (HSL, Google Maps) already
     use, not a decorative wash. Pink specifically, not this card's own
     orange accent -- OSL's (Oulun Seudun Liikenne) real buses ("Onnikat")
     use a pink livery, so this is the one spot on the card where
     matching the real external operator's own color is more accurate
     than the card's general theme accent -- same reasoning already
     applied to the news card's real source logos. */
  .transitRouteBadge{display:inline-flex;align-items:center;justify-content:center;
    min-width:20px;height:20px;padding:0 5px;border-radius:6px;
    background:var(--bizfeed-accent);color:#fff;
    font-family:'Space Grotesk',sans-serif;font-size:11px;font-weight:700;}
  .transitDepartureRest{font-size:11.5px;color:var(--ink-dim);white-space:nowrap;}
  #featureTiles{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:8px;}
  @media (max-width:480px){
    .featureTile{padding:12px;gap:5px;}
    .featureTileIcon{width:32px;height:32px;font-size:14px;}
    .featureTile b{font-size:12.5px;}
    .featureTile>span:not(.featureTileIcon):not(.featureTileSoonBadge){font-size:11px;}
  }
  @media (min-width:700px){ #featureTiles{grid-template-columns:repeat(4,minmax(0,1fr));} }
  .featureTile{background:var(--panelT7);border:1px solid var(--line);border-radius:16px;padding:18px;
    display:flex;flex-direction:column;gap:8px;text-align:left;font-family:'IBM Plex Sans',sans-serif;
    cursor:pointer;position:relative;}
  .featureTile.featureTileSoon{cursor:default;}
  .featureTileIcon{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;
    font-size:18px;flex:none;}
  .featureTile b{font-family:'Space Grotesk',sans-serif;font-size:14.5px;color:var(--ink);}
  .featureTile>span:not(.featureTileIcon):not(.featureTileSoonBadge){font-size:12.5px;color:var(--ink-dim);line-height:1.4;}
  .featureTileSoonBadge{position:absolute;top:14px;right:14px;font-size:10.5px;font-weight:700;color:var(--amber);
    background:var(--panelT4);border:1px solid var(--line);border-radius:999px;padding:3px 9px;}
  #favoritesTileBadge.featureTileBadge{position:absolute;top:12px;right:12px;min-width:20px;height:20px;
    border-radius:999px;background:#d6336c;color:#fff;font-size:11px;font-weight:700;
    align-items:center;justify-content:center;padding:0 5px;box-sizing:border-box;}

  /* Events and news sit side by side as two cards from 1000px up -- each
     keeps a single-column internal item list at that width since they're
     now half-width, rather than the old 2-3 internal columns which were
     sized for each section spanning the full page width. */
  /* "Näytä kaikki" swaps a card's own content between collapsed and
     expanded views in place. On the >=1000px 2x2 grid, news shares its
     row with the business card (align-items:stretch, same principle
     applies elsewhere in this grid) -- expanding news in place would grow
     its own row's height, and stretch would then drag the business card's
     height along with it too, even though nothing about the business
     card actually changed.
     Two things this went through before landing here:
     (1) position:absolute + inset:0 sized to the *original* grid area
     kept the business card untouched, but forced news's own box to
     exactly that height regardless of its real (often much taller, now
     showing everything) content -- overflowing past its own box into
     whatever came next, and since an absolutely-positioned item doesn't
     count toward that grid's track sizing at all, the grid could end up
     *shorter* than before, pulling the feature tiles below it upward.
     (2) Switching to position:relative + grid-row:1/3 (spanning both
     rows, back in normal flow) fixed that shift, but reintroduced the
     first problem in a different shape: the business card's row is now
     partly sized by news's spanning height demand, so its height still
     changed with news's expand state -- just shrinking instead of
     growing, empirically confirmed while testing this fix, not assumed.
     The combination that actually satisfies both at once: position:
     absolute again (so news genuinely can't affect the business card's
     row), sized to fill its column's actual reserved height (see the
     inset/bottom note below) with a generous max-height + scroll for
     content taller than that -- and separately, JS freezes the business
     card's height at the exact moment news expands (see
     expandFeedCard/collapseFeedCard), since pulling news out of the
     row-sizing calculation is exactly what let the business card's
     height depend on news's state to begin with.
     Two more bugs surfaced from that, both about grid-column/grid-row
     values with an indefinite end (just "1" rather than "1 / 2"): (a)
     for an absolutely-positioned grid item specifically, an indefinite
     end line makes its containing block fall back to the *entire grid
     container's* width rather than just that one column -- so the
     overlay was stretching across both columns (covering the business
     card entirely) instead of just matching events' width, and (b)
     since #newsSection becomes position:absolute when expanded, it's
     removed from the auto-placement algorithm entirely, which let
     #bizFeedCard (the next item in DOM order) silently auto-place
     itself into news's now-vacant cell instead of staying in column 2 --
     both empirically confirmed in isolation before landing on this
     fix. All four cards now get an explicit grid-column/grid-row
     (immediately below), and the expanded state uses explicit "1 / 2"
     and "1 / 3" end lines rather than leaving them indefinite.
     One more round after that: with only top/left/right set (no
     bottom), news's box height was purely intrinsic to its own content
     -- fine when its content happened to be about as tall as the
     events+business column, but left a visible gap below the card
     whenever news's real content was *shorter* than that (its own
     reserved area, based on events+business's real height, doesn't
     automatically become news's height just because it's absolutely
     positioned within it -- inset properties only compute an automatic
     height from top+bottom together, not from top alone). Adding
     bottom:0 back is what actually fills that reserved area -- safe to
     do now that grid-column/grid-row have explicit end lines, so this
     only affects news's own box height, not whether it affects the
     business card's sizing (confirmed empirically: it doesn't). Content
     taller than that available height still scrolls internally rather
     than overflowing, via max-height + overflow-y:auto. Mobile doesn't
     need any of this: events/news aren't side by side there
     (see the mobile `order` rules below), so expanding news in place is
     already just normal, expected downward growth. */
  #newsExpandedView{animation:expandedViewIn 0.35s ease;transition:opacity 0.18s ease, transform 0.18s ease;}
  @keyframes expandedViewIn{
    from{opacity:0;transform:translateY(10px);}
    to{opacity:1;transform:translateY(0);}
  }
  @media (min-width:1000px){
    #newsSection.feedCardExpanded{
      position:absolute;
      grid-column:1 / 2;
      grid-row:1 / 3;
      top:0;left:0;right:0;bottom:0;
      z-index:60;
      max-height:92vh;
      overflow-y:auto;
      box-shadow:0 24px 60px rgba(20,15,40,0.32);
    }
  }
  #newsSourceColumnsGrid{display:grid;grid-template-columns:minmax(0,1fr);gap:20px;}
  @media (min-width:1000px){ #newsSourceColumnsGrid{grid-template-columns:repeat(3,minmax(0,1fr));} }
  /* --col-accent set per-column from JS (loadNewsSourceColumns in
     app-board.js) -- each source's real external brand color here
     (Kaleva orange, Yle cyan, Oulu purple), not the muted purple-family
     tones the collapsed card's row borders deliberately use. This is a
     detail/browsing view distinguishing three genuinely different real
     organizations, not the app's own top-level visual identity, so
     showing their real colors is the more useful choice here
     specifically. color-mix() gives a subtle, low-opacity tint of that
     color rather than a flat gray, without needing a second hardcoded
     color per source just for the tint. */
  .expandedNewsCol{background:color-mix(in srgb, var(--col-accent, var(--amber)) 5%, var(--panel));
    border:1px solid var(--line);border-top:3px solid var(--col-accent, var(--amber));
    border-radius:14px;padding:18px;}
  .expandedNewsColHead{display:flex;align-items:center;gap:8px;font-family:'Space Grotesk',sans-serif;
    font-weight:700;font-size:14.5px;margin:0 0 12px;}
  /* The label itself is the link -- tried a separate row of source
     chips below the columns first, but real feedback was that a whole
     extra section felt bolted-on compared to just making the existing
     header clickable. Underline only on hover, not by default, so it
     still reads primarily as a heading, not as obviously a link until
     you're actually interacting with it. */
  .expandedNewsColHeadLink{display:flex;align-items:center;gap:8px;color:var(--ink);text-decoration:none;}
  .expandedNewsColHeadLink:hover{text-decoration:underline;}
  .expandedNewsColLogo{width:22px;height:22px;border-radius:50%;flex:none;color:#fff;font-size:11px;font-weight:700;
    display:flex;align-items:center;justify-content:center;font-family:'Space Grotesk',sans-serif;}
  .expandedNewsColSelect{display:block;width:100%;margin:0 0 14px;padding:9px 12px;border-radius:9px;
    border:1px solid var(--line);background:var(--panelT7);color:var(--ink);font-size:13px;
    font-family:'IBM Plex Sans',sans-serif;cursor:pointer;}
  .feedSubhead{text-align:center;font-family:'Space Grotesk',sans-serif;font-size:15px;color:var(--ink);
    margin:0 0 4px;}
  .cardHeadRow{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:4px;}
  .cardHeadRow .feedSubhead{margin:0;text-align:left;}
  .cardHeadLink{font-size:11px;color:var(--amber);text-decoration:none;font-weight:700;white-space:nowrap;
    text-transform:none;letter-spacing:normal;flex:none;}
  #newsItems, #eventItems{margin-top:12px;}
  /* Flex column with each row set to flex:1 (see .feedItemCompact below)
     instead of a grid of fixed-size rows -- always exactly 3 items that
     stretch to fill whatever height JS sets on this container, rather
     than calculating how many fixed-size rows fit in a given space. */
  #transitItems{margin-top:12px;}
  /* Horizontal-scroll pages instead of a growing vertical list --
     overflow-x + scroll-snap gives real native swipe on mobile for free,
     no touch-event JS needed; the arrow buttons just scroll by one page
     width for desktop/non-touch use. */
  .pagedItemsContainer{display:flex;align-items:flex-start;overflow-x:auto;scroll-snap-type:x mandatory;
    scroll-behavior:smooth;scrollbar-width:none;-ms-overflow-style:none;}
  /* No padding here -- tried adding some specifically to give the
     highlighted event card's outward box-shadow ring room to render
     (see the .eventCard.highlighted comment below for why an inset
     shadow is used instead now), but padding on a
     scroll-snap-type:x mandatory container turned out to cause a
     different, worse problem: the initial render's scroll position
     didn't line up with where the padding shifted each snap-aligned
     page to, so the ring only rendered fully *after* an actual scroll
     event forced the browser to re-evaluate the snap position -- not
     visible on first load, confirmed by real device testing. Switching
     the highlight to an inset shadow sidesteps the whole problem: it
     renders entirely inside the card's own box and was never actually
     a scroll-container-clipping issue to begin with once inset. */
  .pagedItemsContainer::-webkit-scrollbar{display:none;}
  .itemsPage{flex:0 0 100%;scroll-snap-align:start;display:flex;flex-direction:column;gap:12px;min-width:0;}
  /* Event pages lay their cards out in a horizontal row (matching the
     reference design) instead of the vertical stack news uses -- :has()
     lets this happen automatically based on what's actually inside,
     rather than needing renderPagedList to know or care which visual
     style a given list uses. */
  .itemsPage:has(.eventCard){flex-direction:row;max-height:250px;}
  .pagerControls{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:14px;}
  .pagerArrow{background:var(--panelT7);border:1px solid var(--line);color:var(--ink);width:32px;height:32px;
    border-radius:50%;cursor:pointer;font-size:16px;flex:none;display:flex;align-items:center;justify-content:center;}
  .pagerArrow:disabled{opacity:0.35;cursor:default;}
  .pagerDots{display:flex;gap:6px;}
  .pagerDot{width:6px;height:6px;border-radius:50%;background:var(--line);cursor:pointer;border:none;padding:0;}
  .pagerDot.active{background:var(--amber);width:16px;border-radius:3px;}

  /* ---- Photo-forward event cards ---- */
  .eventCard{flex:0 0 calc((100% - 36px) / 4);min-width:0;max-height:250px;display:flex;flex-direction:column;text-decoration:none;color:inherit;
    background:var(--panel);border:1px solid var(--line);border-radius:16px;overflow:hidden;
    box-shadow:var(--shadow-md);transition:box-shadow 0.15s ease,transform 0.15s ease;}
  .eventCard:hover{box-shadow:0 8px 20px rgba(88,71,201,0.14);transform:translateY(-2px);}
  [data-theme="light"] .eventCard{border:none;box-shadow:0 12px 28px rgba(30,20,70,0.08);}
  .eventCard.highlighted{position:relative;}
  .eventCard.highlighted::after{
    content:'';position:absolute;inset:0;border:2px solid var(--amber);
    border-radius:inherit;pointer-events:none;z-index:2;
  }
  /* An overlay pseudo-element, not a box-shadow of either kind -- two
     different real failures ruled those out. An outward box-shadow
     (0 0 0 2px, no inset) extends past the card's own border box, at
     the mercy of whatever ancestor clips overflow (the horizontally
     scrolling row this card sits in); giving it room via padding on
     that scroll container caused a worse, confirmed problem instead --
     the container has scroll-snap-type:x mandatory, and the added
     padding left the *initial* render's scroll position out of sync
     with the snap points, so the ring only rendered fully after an
     actual scroll event forced the browser to recalculate. Switching
     to an inset box-shadow avoided that (nothing extends outside the
     box, no scroll-container interaction at all) but hit a different,
     equally real problem: an inset shadow paints behind the element's
     own content, the same layer a background-color would -- and the
     card's own photo is a fully opaque <img> covering most of that
     same area, so the ring was being completely hidden behind it,
     confirmed by a real screenshot showing the ring perfectly visible
     around the card's text body (no opaque content there) but entirely
     missing around the photo itself. This overlay approach sidesteps
     both failures at once: bounded within the card's own box like the
     inset version (so no scroll-container clipping or snap interaction
     ever applies to it), but painted via z-index on top of everything
     -- including the photo -- rather than behind it. border-radius:inherit
     keeps its corners matching the card's own rounding automatically.
  */
  .eventCard.ended{opacity:0.55;}
  .eventCard.ended .eventCardPhoto{filter:grayscale(0.6);}
  .eventCard.ended .eventCardMeta{font-style:italic;}
  .eventCardPhoto{position:relative;width:100%;aspect-ratio:4/3;max-height:130px;background:var(--panelT4);flex:none;}
  .eventCardPhoto img{width:100%;height:100%;object-fit:cover;display:block;}
  .eventCardPhotoFallback{display:flex;align-items:center;justify-content:center;font-size:32px;color:var(--ink-dim);
    background:linear-gradient(135deg,var(--panelT7),var(--panelT4));}
  .eventCardBadge{position:absolute;top:10px;right:10px;background:var(--amber);color:#fff;
    font-family:'Space Grotesk',sans-serif;font-size:10.5px;font-weight:700;padding:3px 9px;border-radius:6px;}
  /* Top-LEFT specifically, not top-right like .bizFeedFavoriteBtn it's
     modeled on -- .eventCardBadge above already occupies top-right, and
     a highlighted event can have both at once. Same visual language
     (filled-on-active, brief pop) as the business favorite button for
     consistency, just sized down slightly to fit this card's smaller
     130px-max photo area. */
  .eventInterestBtn{position:absolute;top:8px;left:8px;width:30px;height:30px;border-radius:50%;
    border:none;background:var(--panelT7);color:#fff;cursor:pointer;
    display:flex;align-items:center;justify-content:center;z-index:5;
    transition:background 0.15s ease, color 0.15s ease, transform 0.15s ease;}
  .eventInterestBtn svg{width:16px;height:16px;}
  .eventInterestBtn:hover{background:var(--panelT4);transform:scale(1.1);}
  .eventInterestBtn.interested{color:#d6336c;background:#fce4ec;}
  .eventInterestBtn.interested svg{fill:currentColor;}
  .eventInterestBtn.interested{animation:favoritePop 0.3s ease;}
  .eventCardBody{padding:12px 14px 14px;display:flex;flex-direction:column;gap:4px;}
  .eventCard b{font-family:'Space Grotesk',sans-serif;font-size:13.5px;color:var(--ink);line-height:1.3;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:35px;}
  .eventCardMeta{font-size:12px;color:var(--ink-dim);}
  .eventCardAddress{font-size:11px;color:var(--ink-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
  .eventCardShareBtn{align-self:flex-start;margin-top:4px;width:26px;height:26px;border-radius:50%;
    border:none;background:var(--panelT4);color:var(--ink-dim);cursor:pointer;
    display:flex;align-items:center;justify-content:center;transition:background 0.15s ease,color 0.15s ease;}
  .eventCardShareBtn svg{width:14px;height:14px;}
  .eventCardShareBtn:hover{background:var(--panelT7);color:var(--amber);}
  /* 4 cards per row leaves only ~85-95px each on a true phone width --
     too narrow for a readable title even with the 2-line clamp above,
     confirmed producing exactly the single-word-per-line, premature
     ellipsis look reported ("Somma Band...", cutting off
     "Musikläger" entirely). 3 cards fits meaningfully more per title.
     Matched by an equivalent change to EVENTS_PAGE_SIZE in app-board.js
     so each pager page actually contains 3 events here, not 4 -- the
     CSS alone would just leave an awkward gap for the 4th card's slot
     otherwise. 24px = 2 gaps of 12px (one fewer gap than the 4-card
     version's 36px = 3 gaps). */
  @media (max-width:640px){
    .eventCard{flex:0 0 calc((100% - 12px) / 2);}
  }

  /* ---- Compact news rows ---- */
  .newsRow{display:flex;align-items:center;gap:12px;padding:10px 14px 10px 12px;border-radius:10px;
    text-decoration:none;color:inherit;transition:background 0.15s ease,box-shadow 0.15s ease;
    border-left:3px solid transparent;width:100%;box-sizing:border-box;}
  .newsRow:not(:last-child){border-bottom:1px solid var(--line);padding-bottom:14px;}
  .newsRow:hover{background:var(--panelT4);box-shadow:0 2px 10px rgba(88,71,201,0.10);}
  /* Three tonal shades of the site's own purple, not each outlet's real
     external brand color -- see the comment above where these classes
     get assigned in app-board.js for why. Left border only, not a
     full-row background tint -- a low-opacity color-mix background was
     tried here and reverted: at a low enough opacity to not look
     garish, it didn't read as "color" at all, just as a slightly dirty
     off-white, and combined with the row's own rounded corners made
     each row look like its own separate gray box rather than one clean
     scannable list -- exactly the "something's off, boxes within a
     box" feeling reported. The badge and, where available, the real
     article photo already carry plenty of color on their own -- this
     doesn't need the row background doing it too. */
  .newsRow--kaleva{border-left-color:var(--amber-deep);}
  .newsRow--yle{border-left-color:var(--amber);}
  .newsRow--oulu{border-left-color:var(--amber-bright);}
  /* Bigger and rounded-square rather than a small circle -- the single
     biggest driver of the news card reading as flatter than the
     photo-forward events card right above it on the homepage. Still
     small enough to stay a "compact list row" rather than turning into
     a full photo card (see .eventCard for that treatment), but enough
     bigger to actually register as a real visual element, not a tiny
     dot next to the text. */
  .newsRowBadge{flex:none;width:52px;height:52px;border-radius:13px;color:#fff;object-fit:cover;
    display:flex;align-items:center;justify-content:center;font-family:'Space Grotesk',sans-serif;
    font-size:18px;font-weight:700;}
  /* Real source logos (Kaleva/Yle) are wide wordmarks, not square icons
     -- force-cropping one into the circular photo badge above would cut
     off most of it and make it unrecognizable. A white rounded-rect
     chip with contain-fit (never cropped) is the standard, sane way to
     show an arbitrary-shaped brand logo at a small fixed size. */
  .newsRowBadgeLogo{border-radius:11px;background:#fff;object-fit:contain;padding:7px;box-sizing:border-box;}
  .newsRow.newsRowLead .newsRowBadgeLogo{border-radius:15px;padding:10px;}
  .newsRowBody{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
  .newsRow b{font-family:'Space Grotesk',sans-serif;font-size:13px;color:var(--ink);line-height:1.35;
    display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;font-weight:600;}
  .newsRowMeta{font-size:11.5px;color:var(--ink-dim);}
  .newsRowArrow{flex:none;margin-left:auto;width:20px;height:20px;color:var(--ink-dim);
    opacity:0.5;transition:opacity 0.15s ease, color 0.15s ease, transform 0.15s ease;}
  .newsRowArrow svg{width:100%;height:100%;}
  .newsRow:hover .newsRowArrow{opacity:1;color:var(--amber);transform:translate(1px,-1px);}

  /* The lead story: same row structure, just turned up -- a visibly
     bigger photo/badge and larger title than the rest of the list, so
     the freshest item reads as the headline rather than blending in.
     Kept to size/weight only (no color/border tricks) to stay quiet
     alongside the rest of the card rather than looking like a separate
     component. */
  .newsRow.newsRowLead{padding-top:4px;padding-bottom:18px;gap:14px;}
  .newsRow.newsRowLead .newsRowBadge{width:78px;height:78px;font-size:26px;border-radius:16px;}
  .newsRow.newsRowLead b{font-size:15.5px;line-height:1.3;}

  .feedItem{background:var(--panelT4);border:1px solid var(--line);border-radius:10px;padding:14px 16px;
    display:flex;gap:12px;align-items:flex-start;transition:box-shadow 0.15s ease;height:140px;}
  .feedItem:hover{box-shadow:0 4px 14px rgba(88,71,201,0.10);}
  .feedItem .feedBody{flex:1;min-width:0;}
  .feedItem b{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
    font-family:'Space Grotesk',sans-serif;font-size:14.5px;color:var(--ink);margin-bottom:4px;overflow-wrap:break-word;}
  .feedItem p{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
    margin:0 0 6px;font-size:13px;color:var(--ink-dim);line-height:1.5;}
  .feedItem a{font-size:11.5px;color:var(--amber);text-decoration:underline;}
  #factsSection{margin:40px auto 8px;max-width:760px;text-align:center;}
  #factsSection h2{font-family:'Space Grotesk',sans-serif;font-size:20px;color:var(--ink);margin:0 0 22px;}
  .factsRow{display:flex;gap:20px;justify-content:center;flex-wrap:wrap;}
  .factCard{max-width:190px;background:var(--panelT4);border:1px solid var(--line);border-radius:10px;padding:18px 16px;}
  .factCard b{display:block;font-family:'Space Grotesk',sans-serif;font-size:30px;color:var(--amber);margin-bottom:6px;}
  .factCard span{font-size:12.5px;color:var(--ink-dim);line-height:1.5;}
  .factsNote{font-size:11.5px;color:var(--ink-dim);margin-top:16px;max-width:520px;margin-left:auto;margin-right:auto;line-height:1.5;}

  #earlyBanner{max-width:640px;margin:28px auto;background:color-mix(in srgb, var(--amber) 12%, transparent);border:1px solid color-mix(in srgb, var(--amber) 35%, transparent);
    border-radius:10px;padding:14px 20px;text-align:center;font-size:13.5px;color:var(--ink);line-height:1.5;}

  #faq{margin-top:56px;max-width:640px;margin-left:auto;margin-right:auto;}
  #faq h2{font-family:'Space Grotesk',sans-serif;font-size:22px;color:var(--ink);margin:0 0 20px;text-align:center;}
  .faqItem{padding:4px 0;border-bottom:1px solid var(--line);}
  .faqItem:last-child{border-bottom:none;}
  .faqQ{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;
    background:none;border:none;cursor:pointer;padding:14px 0;text-align:left;
    font-family:'Space Grotesk',sans-serif;font-size:14.5px;color:var(--ink);}
  .faqToggle{flex:none;font-size:18px;color:var(--amber);font-weight:400;transition:transform .15s ease;}
  .faqItem.open .faqToggle{transform:rotate(45deg);}
  .faqA{margin:0;font-size:13px;color:var(--ink-dim);line-height:1.55;
    max-height:0;overflow:hidden;transition:max-height .2s ease, padding .2s ease;}
  .faqItem.open .faqA{max-height:400px;padding-bottom:16px;}

  /* ---- PWA install banner ---- */
  /* position:fixed, not static in-flow -- this used to sit in normal
     document flow, display:none until shown. That's a real, severe CLS
     bug: it only becomes visible via an ASYNC event (Chrome/Android's
     beforeinstallprompt can fire seconds after the page has already
     rendered; see app-core.js), and an in-flow element suddenly gaining
     real height pushes everything below it (the entire hero and page
     content) down at that moment -- exactly the kind of unexpected,
     non-interaction-triggered shift Cumulative Layout Shift scoring
     penalizes most heavily. Fixed positioning removes it from the
     layout flow entirely, so toggling it can never move anything else
     on the page, regardless of when the async trigger fires. `top` is
     set dynamically in JS (see showInstallBanner in app-core.js) rather
     than hardcoded here, since the header's own height isn't a fixed
     number -- it varies by content and breakpoint. */
  /* top is a static value, not measured from the header via JS --
     that measurement approach (still used by the JS that shows this
     banner, kept for backwards compatibility but effectively
     overridden by this more specific rule) turned out fragile: if the
     header isn't itself position:fixed and a banner has any delay
     before appearing (the push banner deliberately waits 1.5s), the
     page can already be scrolled by the time the measurement runs,
     producing a wrong value -- a real, reported bug where the banner
     ended up mid-page instead of near the top. A fixed pixel value
     can never be wrong regardless of scroll position or timing. Sized
     generously for the header's tallest realistic content (the
     weather widget + login button row), not pixel-perfect -- a few
     px of gap is a far smaller problem than a banner in the wrong
     place entirely. */
  #installBanner{display:none;position:fixed;left:0;right:0;top:80px !important;z-index:180;
    align-items:center;gap:12px;justify-content:center;
    background:var(--amber);box-shadow:0 4px 16px rgba(20,15,40,0.25);
    padding:10px 20px;font-size:13px;color:#fff;text-align:center;flex-wrap:wrap;
    transform:translateZ(0);will-change:transform;}
  /* White, not var(--amber) -- this banner is now a solid purple block
     (see above), so a same-color button would have been invisible
     against its own background. White stands out clearly regardless of
     the banner's exact shade in either theme. */
  #installBannerBtn{background:#fff;color:var(--amber-deep);border:none;border-radius:999px;
    padding:6px 16px;font-weight:700;font-size:12.5px;cursor:pointer;font-family:'Space Grotesk',sans-serif;
    flex:none;}
  #installBannerClose{background:none;border:none;color:#fff;opacity:0.8;cursor:pointer;font-size:14px;
    padding:2px 6px;flex:none;}

  /* Same fixed-overlay treatment and reasoning as #installBanner above
     (mutually exclusive with it -- see the JS trigger conditions in
     app-core.js -- so they never compete for the same space or need
     different z-index handling). A slightly muted color, not the same
     bright purple, since this is a lower-urgency "FYI, optional"
     notice rather than an actionable prompt. */
  #reinstallBanner{display:none;position:fixed;left:0;right:0;top:80px !important;z-index:180;
    align-items:center;gap:12px;justify-content:center;
    background:var(--amber-deep);box-shadow:0 4px 16px rgba(20,15,40,0.25);
    padding:10px 20px;font-size:13px;color:#fff;text-align:center;flex-wrap:wrap;
    transform:translateZ(0);will-change:transform;}
  #reinstallBannerClose{background:none;border:none;color:#fff;opacity:0.8;cursor:pointer;font-size:14px;
    padding:2px 6px;flex:none;}

  /* ---- AI local guide: hero search (top of page, not a hidden corner bubble) ---- */
  /* Photo is a fixed-height layer behind the top of this wrapper (::before), not
     the wrapper's own background -- so long AI answers in #askHeroResults can
     grow the wrapper taller without stretching the photo along with them.
     A soft brand-violet glow sits between the photo and the darkening
     overlay (::after, painted after ::before so it layers on top of the
     photo but behind the actual text/ask-bar content at z-index:1) --
     the same accent color used for the events card and its icon
     elsewhere on the page, so the hero reads as *this* product's hero
     rather than a generic dark-photo-with-white-text treatment. */
  #heroPhotoWrap{position:relative;width:100vw;left:50%;margin-left:-50vw;
    padding:40px 20px 32px;margin-bottom:0;display:flex;flex-direction:column;align-items:center;}
  /* bottom:0 (not a fixed height) -- previously both of these used a
     guessed height:380px, which matched the content's real height on
     some devices but not others: different viewport widths wrap the
     headline/subhead differently, different font rendering engines
     measure text differently, and notched phones add real vertical
     space via safe-area insets. Whenever actual content (headline,
     subhead, ask bar, the 4 suggestion chips) added up to more than
     380px, whatever came last -- the suggestion chips -- spilled below
     the photo backdrop entirely, landing on the plain page background
     instead. bottom:0 makes both of these stretch to match
     #heroPhotoWrap's own real height exactly, on any device, since it's
     already position:relative (the containing block for these
     absolutely-positioned pseudo-elements) and top:0 + bottom:0
     together resolve to "exactly as tall as the parent actually is". */
  #heroPhotoWrap::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;z-index:0;
    background:linear-gradient(180deg,rgba(10,8,20,0.55) 0%,rgba(10,8,20,0.32) 40%,rgba(10,8,20,0.55) 100%),
      image-set(url('/hero-oulu.webp') type('image/webp'), url('/hero-oulu.jpg') type('image/jpeg'));
    background-size:cover;background-position:center;}
  @media (max-width:900px){
    /* Same photo, downscaled to the width this breakpoint actually
       displays -- background-size:cover was already cropping the full
       2048px-wide desktop image down on every phone, so a phone-sized
       source here is the same visible result for a fraction of the
       bytes, not a quality trade-off. */
    #heroPhotoWrap::before{
      background:linear-gradient(180deg,rgba(10,8,20,0.55) 0%,rgba(10,8,20,0.32) 40%,rgba(10,8,20,0.55) 100%),
        image-set(url('/hero-oulu-mobile.webp') type('image/webp'), url('/hero-oulu-mobile.jpg') type('image/jpeg'));
      background-size:cover;background-position:center;}
  }
  #heroPhotoWrap::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;z-index:0;
    background:radial-gradient(600px 320px at 50% 40%, rgba(124,92,255,0.22), transparent 70%);
    pointer-events:none;}
  #askHero{max-width:600px;width:100%;text-align:center;position:relative;z-index:1;
    padding-top:30px;}
  #askHero h1{font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:clamp(28px,5vw,44px);
    line-height:1.1;letter-spacing:-0.01em;margin:0 0 10px;color:#fff;text-shadow:0 2px 16px rgba(10,8,20,0.4);}
  #askHero .askHeroSub{color:#e8e4f7;font-size:14px;margin:0 0 20px;}
  #askHeroRow{display:flex;gap:10px;max-width:560px;margin:0 auto;background:rgba(255,255,255,0.97);
    border-radius:999px;padding:6px 6px 6px 6px;box-shadow:0 12px 30px rgba(10,8,20,0.35);
    transition:box-shadow 0.2s ease;}
  #askHeroRow:focus-within{box-shadow:0 12px 30px rgba(10,8,20,0.35), 0 0 0 3px rgba(124,92,255,0.45);}
  #askHeroInput{flex:1;padding:12px 18px;border-radius:999px;border:none;
    background:none;color:var(--ink);font-size:15px;font-family:'IBM Plex Sans',sans-serif;}
  #askHeroSendBtn{flex:none;padding:0 26px;border-radius:999px;border:none;cursor:pointer;
    background:linear-gradient(135deg,var(--amber-bright),var(--amber));color:#fff;font-weight:700;font-size:14.5px;
    font-family:'Space Grotesk',sans-serif;box-shadow:0 4px 14px rgba(88,71,201,0.35);transition:box-shadow 0.15s ease,transform 0.15s ease;}
  #askHeroSendBtn:hover{box-shadow:0 6px 18px rgba(88,71,201,0.45);transform:translateY(-1px);}
  #askHeroSendBtn:disabled{opacity:0.5;cursor:default;}
  /* AskHeroResults can now safely extend past the photo's fixed 380px height --
     each result already has its own opaque background (.askResultBlock below),
     but the echoed question line had none, so give it one for legibility here. */

  #askMicBtn, #askFollowupMicBtn{flex:none;width:44px;height:44px;border-radius:50%;border:1px solid var(--line);
    background:var(--panelT7);color:var(--ink-dim);font-size:17px;cursor:pointer;padding:0;
    display:flex;align-items:center;justify-content:center;}
  #askFollowupMicBtn{width:36px;height:36px;font-size:14px;}
  #askMicBtn:hover, #askFollowupMicBtn:hover{border-color:var(--amber);color:var(--amber);}
  #askMicBtn.listening, #askFollowupMicBtn.listening{background:rgba(216,78,78,0.15);border-color:#d84e4e;
    color:#d84e4e;animation:askMicPulse 1.3s ease-in-out infinite;}
  @keyframes askMicPulse{
    0%, 100%{box-shadow:0 0 0 0 rgba(216,78,78,0.4);}
    50%{box-shadow:0 0 0 8px rgba(216,78,78,0);}
  }

  #askHeroResults{max-width:640px;margin:0 auto;text-align:left;display:none;}
  /* Fills the empty space with real visible substance the moment the
     panel opens with nothing asked yet -- without this, the sheet
     breakpoint's max-height:60vh (see further down) is a CEILING, not a
     forced height, so an empty #askResultsList collapses toward zero
     height and the panel reads as "just a tiny bar with buttons on it",
     a real, reported point of confusion distinct from (but related to)
     the scroll-jump bug openAskPanel's own comment explains. min-height
     guarantees a substantial, obviously-a-chat-window size regardless
     of breakpoint, without needing separate per-breakpoint overrides. */
  .askEmptyPlaceholder{min-height:140px;display:flex;align-items:center;justify-content:center;
    text-align:center;color:var(--ink-dim);font-size:13.5px;line-height:1.5;padding:24px;margin:0;}
  #askSheetMinimizeBtn{display:none;}
  #askDesktopChatMinimizeBtn{display:none;}
  #askNewChatBtn{display:none;}
  #askDesktopChatHeader{display:none;}
  #askChatToolbarBtns{display:flex;align-items:center;gap:2px;}
  /* Shared icon-button look for new-chat/minimize/close -- one rule
     instead of repeating border/background/hover per button, since all
     three are the same small circular icon-button shape. Deliberately
     does NOT set display here: each button's own #id rule controls
     that per breakpoint (hidden by default, shown only where relevant)
     -- a `#askChatToolbarBtns button` selector has higher specificity
     than a plain `#askSheetMinimizeBtn` one (ID+type beats ID alone),
     so setting display:flex here would silently override every
     button's own hidden state at every width, regardless of source
     order -- confirmed happening exactly that way before removing it. */
  #askChatToolbarBtns button{border:none;background:none;color:var(--ink-dim);cursor:pointer;
    width:28px;height:28px;border-radius:50%;align-items:center;justify-content:center;flex:none;}
  #askChatToolbarBtns button:hover{background:var(--panelT4);color:var(--ink);}
  /* Shows text ("Tyhjennä"/"Clear"), not a single icon like its sibling
     buttons in this same toolbar -- the fixed 28px circle above would
     badly crop it, so this overrides width/height/radius/padding while
     keeping the shared color/hover/cursor behavior. Compound selector
     (#askChatToolbarBtns #askNewChatBtn, specificity 200) deliberately
     -- plain #askNewChatBtn alone (specificity 100) would lose to the
     shared #askChatToolbarBtns button rule above (specificity 101,
     ID+element beats ID alone) regardless of which rule comes later in
     the file, silently keeping the old circular sizing. */
  #askChatToolbarBtns #askNewChatBtn{width:auto;height:26px;border-radius:13px;padding:0 10px;font-size:12px;
    font-weight:600;font-family:'IBM Plex Sans',sans-serif;}
  #mobileTabBar{display:none;}
  @media (max-width:900px){
    body{padding-bottom:64px;}
    #mobileTabBar{display:flex;position:fixed;bottom:0;left:0;right:0;z-index:210;
      background:var(--panel);border-top:1px solid var(--line);padding:8px 12px calc(8px + env(safe-area-inset-bottom));}
    /* Hidden while the chat sheet is genuinely open with a conversation
       -- #askFollowupRow's own "Kysy jotain muuta..." input already
       covers "ask something" in that state, so having this permanent
       bar's identical-looking input still visible right underneath it
       read as two broken, redundant ask boxes stacked on each other.
       :not(.minimized) is the important part: this should still show
       normally whenever the sheet is minimized or closed, only hidden
       while there's an actual open conversation covering the same
       need. */
    body:has(#askHeroResults.open:not(.minimized)) #mobileTabBar{display:none;}
    #mobileAskForm{display:flex;align-items:center;gap:8px;width:100%;}
    #mobileAskInput{flex:1;min-width:0;padding:11px 16px;border-radius:999px;border:1px solid var(--line);
      background:var(--panelT7);color:var(--ink);font-size:14px;font-family:'IBM Plex Sans',sans-serif;}
    #mobileAskInput::placeholder{color:var(--ink-dim);}
    #mobileAskSendBtn{flex:none;width:42px;height:42px;border-radius:50%;border:none;cursor:pointer;
      background:linear-gradient(135deg,#7c5cff,#5847c9);color:#fff;font-size:19px;font-weight:700;
      box-shadow:0 3px 10px rgba(88,71,201,0.28);
      display:flex;align-items:center;justify-content:center;}
    #mobileAskSendBtn:disabled{opacity:0.5;cursor:default;}
  }
  #askSuggestRow{display:flex;flex-wrap:nowrap;justify-content:center;gap:8px;max-width:960px;margin:16px auto 0;}
  @media (max-width:900px){ #askSuggestRow{flex-wrap:wrap;max-width:560px;} }
  .askSuggestChip{background:rgba(20,15,40,0.55);border:1px solid rgba(255,255,255,0.25);color:#fff;
    font-size:12px;font-family:'IBM Plex Sans',sans-serif;padding:8px 13px;border-radius:999px;cursor:pointer;
    backdrop-filter:blur(4px);white-space:nowrap;flex:none;
    transition:background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;}
  .askSuggestChip:hover{background:rgba(20,15,40,0.7);transform:translateY(-1px);box-shadow:0 4px 14px rgba(10,8,20,0.35);}
  #askHeroResults.open{display:block;margin-top:24px;}
  /* ---- Desktop docked chat panel ----
     Previously the answer just grew #askHeroResults inline, permanently
     pushing the homepage cards further down the page every time --
     visible screen space it never gave back short of a refresh. `main`
     is centered with max-width:1500px, which on anything wider than
     that leaves an unused margin on both sides (see the padding-right
     trick below) -- more than enough room for a persistent, docked chat
     window there instead, closer to how a real chat assistant behaves:
     always reachable, never disturbing the page underneath it.
     body's padding-right (not main's own margin) is what makes room --
     main still centers itself via margin:0 auto, but now within body's
     *remaining* content box once padding-right eats into it, shifting
     main's centered position left exactly enough to clear the panel.
     Below 1300px there usually isn't enough spare margin for this to
     make sense at a readable width, so it falls back to the old inline
     block behavior instead of cramming a chat panel into a sliver of
     space. Mobile (<=900px) is a separate, untouched code path entirely
     (the bottom sheet elsewhere in this file) -- this block only ever
     applies above 1300px.
     Declared after the base #askHeroResults.open rule above, on purpose:
     media queries don't add specificity, so at the same specificity the
     *later* rule in source order wins whenever both apply -- placing
     this block earlier in the file (where it was first written) would
     have silently lost to the base rule every time, regardless of
     viewport width. */
  @media (min-width:1301px){
    /* Reserves space on #homeGrid and #featureTiles specifically, not
       body -- previously this was body.desktopChatOpen{padding-right:...},
       which shrank EVERYTHING on the page equally, including the
       decorative hero photo above (see the real, reported complaint:
       "the picture gets shrunk"). #heroPhotoWrap and #homeGrid are
       siblings, not nested, so scoping the padding to just the
       content sections that actually hold clickable things (event/
       business/news/transit cards, the feature tiles) means the hero
       -- which has nothing interactive in it worth protecting from the
       panel's overlap -- can stay genuinely full-width/full-bleed
       exactly as it looks with the chat closed, with the panel simply
       floating on top of its right edge instead of visually
       compressing it. */
    body.desktopChatOpen #homeGrid,
    body.desktopChatOpen #featureTiles{padding-right:clamp(300px, 27vw, 400px);transition:padding-right 0.25s ease;box-sizing:border-box;}
    #askHeroResults.open{
      display:flex;flex-direction:column;
      position:fixed;top:104px;right:20px;bottom:20px;
      width:clamp(280px, 25vw, 380px);max-width:none;margin:0;
      background:var(--panel);border:1px solid var(--line);border-radius:18px;
      box-shadow:var(--shadow-lg);z-index:150;overflow:hidden;
      transform:translateX(0);
      transition:transform 0.32s cubic-bezier(0.16,1,0.3,1);
    }
    /* Previously .minimized had no effect at all at this breakpoint --
       only the sheet's own media query defined the slide-away
       transform, so repurposing this button from close to minimize
       (see the HTML) needed this added too, or toggling the class here
       would have silently done nothing. Slides right (off toward the
       same edge the panel is docked to) rather than down, matching the
       direction it lives in. */
    #askHeroResults.open.minimized{ transform:translateX(150vw); }
    /* Same reasoning and same fix as the mobile sheet's translateY
       below -- 120% of the panel's own (shrinkable) width had the same
       risk of not fully clearing the viewport once the panel shrinks
       to just its header after clearing, even without a tab bar here
       to visibly collide with the way mobile had. */
    #askDesktopChatHeader{display:flex;align-items:center;justify-content:space-between;
      padding:14px 16px;border-bottom:1px solid var(--line);flex:none;}
    #askChatToolbarTitle{display:block;font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:14px;color:var(--ink);}
    #askNewChatBtn{display:flex;}
    #askDesktopChatMinimizeBtn{display:flex;}
    /* Explicit safeguard, not just relying on the transform that slides
       the whole sheet off-screen when minimized -- ensures Tyhjennä is
       never visible/clickable while minimized regardless of how that
       state was reached. */
    #askHeroResults.open.minimized #askNewChatBtn{display:none;}
    #askResultsList{flex:1;overflow-y:auto;overscroll-behavior:contain;padding:16px;display:flex;flex-direction:column;gap:10px;}
    /* Chat-bubble treatment: the question aligns right in the brand
       accent color (like a "sent" message), the answer aligns left in
       a neutral tone (like a "received" one) -- only while actually
       docked, so the exact same markup still renders as plain stacked
       cards in the <=1300px inline fallback and on mobile. */
    #askResultsList .askResultBlock{margin-bottom:0;max-width:88%;}
    #askResultsList .askResultBlock.askQuestionEcho{align-self:flex-end;background:var(--amber);
      color:#fff;font-weight:600;border-radius:16px 16px 4px 16px;padding:9px 16px;font-size:14px;}
    #askResultsList .askResultBlock:not(.askQuestionEcho){align-self:flex-start;border-radius:16px 16px 16px 4px;}
    #askFollowupRow{flex:none;padding:12px 16px;border-top:1px solid var(--line);margin:0;}
  }
  /* Turns the shared ask-results panel into a bottom sheet on mobile,
     instead of the plain inline block it is on desktop -- same element,
     same #askResultsList content, just repositioned. Sits just above
     #mobileTabBar (which stays visible and stable at the very bottom
     per the request) rather than covering it, so the input is always
     reachable to ask a follow-up or dismiss/reopen. Declared after the
     base #askHeroResults.open rule above, so at this same specificity
     it wins purely on source order while this media query is active --
     no !important needed. */
  /* Turns the shared ask-results panel into a bottom sheet on any
     screen too narrow for the docked chat panel above (see the
     >=1301px block elsewhere in this file) -- not just "mobile" in the
     narrow phone sense. This used to only kick in at <=900px, leaving
     everything from 901-1300px (typical tablets, like an iPad Pro in
     either orientation, and plenty of smaller laptop windows) stuck
     with the old plain-inline-push-down behavior: worse than either
     the sheet or the docked panel, and neither fully "mobile" nor fully
     "desktop" -- a real third state nobody asked for. Collapsing that
     gap into the sheet treatment means every viewport now lands in
     exactly one of two coherent designs, matching how the rest of the
     page already only really has two layouts (single-column vs 2x2
     grid, switching at 1000px -- unrelated to this breakpoint, and
     intentionally left alone here).
     The one real difference across this wider range: #mobileTabBar (the
     persistent bottom search bar) only actually exists at <=900px, so
     the sheet's bottom offset needs to clear it there specifically --
     at 901-1300px there's no bar to clear, so the sheet can just sit
     near the screen's bottom edge directly. See the narrower override
     just below for that one property. */
  /* Floating reopen affordance -- shown whenever there's a prior
     conversation but the chat isn't currently visible (minimized on the
     sheet, or closed via the X on the desktop docked panel). Positioned
     as a plain top-level element (sibling of #mobileTabBar, not nested
     inside #askHero) rather than inside the hero section -- past bugs
     in this exact file (see the z-index comments elsewhere) came from
     fixed-position elements nested inside ancestors that turned out to
     establish their own stacking context, so keeping this genuinely
     top-level avoids repeating that. JS toggles display via
     updateReopenButtonVisibility(), called after every state change
     that could affect it (asking a question, minimizing, closing). */
  #askReopenChatBtn{display:none;position:fixed;left:50%;transform:translateX(-50%);bottom:20px;
    z-index:190;align-items:center;gap:8px;padding:11px 20px;border-radius:999px;border:none;cursor:pointer;
    background:var(--amber);color:#fff;font-weight:700;font-family:'Space Grotesk',sans-serif;font-size:13px;
    box-shadow:0 8px 22px rgba(88,71,201,0.35);transition:box-shadow 0.15s ease, transform 0.15s ease;}
  #askReopenChatBtn:hover{box-shadow:0 10px 26px rgba(88,71,201,0.45);}
  #askReopenChatBtn.visible{display:flex;}
  @media (max-width:900px){
    /* Clears #mobileTabBar (the persistent search bar), same reasoning
       as the sheet's own bottom offset at this width. */
    #askReopenChatBtn{bottom:calc(64px + 14px + env(safe-area-inset-bottom));}
  }
  @media (min-width:1301px){
    /* Aligned to the same side the docked panel itself lives on, so
       reopening feels like it's coming back from where it went, rather
       than popping up in an unrelated spot. */
    #askReopenChatBtn{left:auto;right:20px;transform:none;}
  }
  @media (max-width:1300px){
    #askHeroResults.open{
      position:fixed;left:0;right:0;
      bottom:20px;
      max-width:none;margin:0;
      max-height:60vh;
      display:flex;flex-direction:column;
      background:var(--panel);border-top:1px solid var(--line);
      border-radius:20px 20px 0 0;
      box-shadow:0 -10px 30px rgba(20,15,40,0.25);
      z-index:200;
      transform:translateY(0);
      transition:transform 0.32s cubic-bezier(0.16,1,0.3,1);
      overflow:hidden;
    }
    /* Minimized slides the sheet fully out of view (rather than removing
       .open / clearing its content) so the last answer is still there,
       ready to slide back up instantly, without re-asking the question. */
    #askHeroResults.open.minimized{ transform:translateY(150vh); }
    /* 150vh, not a percentage of the sheet's own height (115% was here
       before) -- a percentage-of-self only reliably clears the
       viewport when the sheet is tall, which it usually is with a real
       conversation in it, but badly fails right after clearing: the
       sheet shrinks to just its ~55px header (no results, no followup
       row left to give it height), and 115% of that tiny height is
       only ~63px of movement -- not enough to escape the viewport when
       it starts just above the mobile tab bar, confirmed by direct
       measurement showing the "minimized" sheet still overlapping and
       intercepting clicks meant for the tab bar's own send button.
       viewport-relative units don't have this problem at any content
       height. */
    /* flex:none (not part of the scrolling area) -- previously the whole
       #askHeroResults.open box itself was the one scrollable region,
       with the minimize button as just its first child, so a tall
       conversation scrolled the button itself out of view along with
       everything else, needing a scroll back up inside the sheet just
       to reach it. Now #askResultsList below is the *only* thing that
       scrolls; this toolbar stays fixed at the top of the sheet
       regardless of how long the conversation gets. Same toolbar
       structure as the desktop panel's header, just compact -- no
       title, and minimize instead of close, since the sheet's whole
       point is instant reopening rather than a full dismiss. */
    #askDesktopChatHeader{display:flex;align-items:center;justify-content:flex-end;
      padding:8px 8px 4px;flex:none;}
    #askChatToolbarTitle{display:none;}
    #askNewChatBtn{display:flex;}
    /* Same safeguard as the 901-1300px range above. */
    #askHeroResults.open.minimized #askNewChatBtn{display:none;}
    #askSheetMinimizeBtn{display:flex;font-size:16px;line-height:1;}
    #askResultsList{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:0 18px 14px;}
    #askFollowupRow{flex:none;padding:12px 18px calc(12px + env(safe-area-inset-bottom));
      border-top:1px solid var(--line);margin:0;}
    /* #askHeroResults.open's own z-index:200 above only settles paint
       order *within* #askHero's stacking context (#askHero itself is
       position:relative;z-index:1) -- it says nothing about #askHero's
       subtree versus siblings outside it, like #homeGrid, which has its
       own z-index:2 (to pull the cards up over the hero photo's bottom
       edge). Since 1 < 2, the entire #askHero subtree -- sheet included,
       regardless of its internal z-index -- was painting *behind*
       #homeGrid's cards, which is exactly the "hidden under everything"
       bug. Bumping #askHero above #homeGrid only while the sheet is
       actually open (and at any width using the sheet treatment) fixes
       that without touching the deliberate small overlap at the
       hero/homeGrid boundary in any other state. */
    #askHero:has(#askHeroResults.open){ z-index:220; }
    /* Same chat-bubble look as the desktop docked panel -- a purple,
       right-aligned bubble for the echoed question, instead of the
       plain neutral label-style block it used elsewhere. #askResultsList
       isn't a flex column the way the desktop panel is (its own
       display:flex above is row-direction, since it's the flex *item*
       here, not laying its own children out that way), so margin-left:auto
       on a max-width'd block does the same right-alignment job here that
       align-self does on desktop. */
    #askResultsList .askResultBlock.askQuestionEcho{
      background:var(--amber);color:#fff;font-weight:600;
      border-radius:16px 16px 4px 16px;padding:9px 16px;font-size:14px;
      max-width:80%;margin:8px 0 8px auto;
    }
    #askResultsList .askResultBlock:not(.askQuestionEcho){margin-bottom:8px;}
  }
  /* #mobileTabBar (the persistent bottom search bar the sheet sits
     above) only actually renders at <=900px -- see its own CSS -- so
     only here does the sheet need bottom offset to clear it instead of
     the 901-1300px range's plain 20px above. */
  @media (max-width:900px){
    #askHeroResults.open{ bottom:calc(64px + env(safe-area-inset-bottom)); }
    /* Matches the exact same :not(.minimized) condition #mobileTabBar's
       own hide rule uses -- the 64px above exists purely to clear the
       tab bar, so once the tab bar hides itself (open, real
       conversation, not minimized), that reserved space becomes stale
       dead space at the bottom of the screen unless the sheet also
       extends down to fill it. Higher specificity than the plain .open
       rule above (an extra :not() pseudo-class), so this correctly
       wins whenever both could apply. */
    #askHeroResults.open:not(.minimized){ bottom:env(safe-area-inset-bottom); }
  }
  .askResultBlock{background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow-md);
    padding:16px 18px;margin-bottom:12px;font-size:14px;line-height:1.6;color:var(--ink);}
  .askTypingText{margin:0;white-space:pre-wrap;}
  [data-theme="light"] .askResultBlock{border:none;box-shadow:0 16px 40px rgba(30,20,70,0.10);}
  .askResultBlock.pending{color:var(--ink-dim);font-style:italic;}
  .thinkingDots{display:inline-flex;align-items:center;gap:5px;padding:2px 0;}
  .thinkingDots span{width:7px;height:7px;border-radius:50%;background:var(--amber);
    animation:thinkingBounce 1.2s infinite ease-in-out both;}
  .thinkingDots span:nth-child(1){animation-delay:-0.32s;}
  .thinkingDots span:nth-child(2){animation-delay:-0.16s;}
  @keyframes thinkingBounce{
    0%, 80%, 100% { transform:scale(0.6); opacity:0.5; }
    40% { transform:scale(1); opacity:1; }
  }
  .thinkingDotsCard{display:flex;justify-content:center;align-items:center;padding:24px 0;}
  .askResultBlock.askQuestionEcho{background:var(--panel);border-radius:8px;border:1px solid var(--line);box-shadow:var(--shadow-md);padding:6px 4px 6px 18px;margin-bottom:6px;
    font-weight:700;font-family:'Space Grotesk',sans-serif;color:var(--ink-dim);font-size:13.5px;}
  [data-theme="light"] .askResultBlock.askQuestionEcho{border:none;box-shadow:0 16px 40px rgba(30,20,70,0.10);}
  .askMentions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;}
  .askMentionsNote{font-size:12px;color:var(--ink-dim);margin:12px 0 0;}
  .askMentionChip{font-size:14px;padding:9px 18px;border-radius:999px;border:1px solid var(--amber);
    background:var(--amber);color:#fff;font-weight:700;text-decoration:none;}
  .askMentionChip:hover{background:var(--amber-deep);border-color:var(--amber-deep);}
  .askAdvertiserTag{display:inline-block;background:rgba(0,0,0,0.16);color:#fff;font-weight:700;
    font-size:10px;text-transform:uppercase;letter-spacing:0.03em;padding:2px 7px;border-radius:999px;
    margin:0 2px;vertical-align:middle;}
  .askWebResults{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;}
  .askMap{height:220px;border-radius:10px;margin-top:12px;overflow:hidden;border:1px solid var(--line);}
  .askWebResultChip{font-size:14px;padding:9px 18px;border-radius:999px;border:1px solid var(--line);
    background:none;color:var(--ink-dim);text-decoration:none;}
  .askWebResultChip:hover{border-color:var(--ink-dim);color:var(--ink);}
  .askFeedbackRow{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px;padding-top:12px;
    border-top:1px solid var(--line);}
  .askFeedbackLabel{font-size:12.5px;color:var(--ink-dim);margin-right:2px;}
  .askFeedbackBtn{background:none;border:1px solid var(--line);border-radius:999px;padding:5px 11px;
    font-size:14px;cursor:pointer;line-height:1;}
  .askFeedbackBtn:hover{border-color:var(--ink-dim);background:var(--panelT7);}
  .askFeedbackCommentBox{display:flex;flex-direction:column;gap:8px;margin-top:10px;width:100%;flex-basis:100%;}
  .askFeedbackTextarea{width:100%;min-height:56px;padding:9px 12px;border-radius:8px;border:1px solid var(--line);
    background:var(--panelT7);color:var(--ink);font-size:13px;font-family:'IBM Plex Sans',sans-serif;resize:vertical;
    box-sizing:border-box;}
  .askFeedbackThanks{font-size:12.5px;color:var(--ink-dim);}
  .askAnswerHeading{font-size:16px;font-family:'Space Grotesk',sans-serif;font-weight:700;color:var(--ink);
    margin:16px 0 8px;}
  .askAnswerHeading:first-child{margin-top:0;}
  .askAnswerList{margin:0 0 10px;padding-left:22px;}
  .askAnswerList li{margin-bottom:6px;line-height:1.5;}
  #askFollowupRow{display:flex;gap:8px;margin-top:4px;}
  #askFollowupInput{flex:1;padding:10px 16px;border-radius:999px;border:1px solid var(--line);
    background:var(--panelT7);color:var(--ink);font-size:13.5px;font-family:'IBM Plex Sans',sans-serif;}
  #askFollowupSendBtn{flex:none;width:36px;height:36px;border-radius:50%;border:none;cursor:pointer;
    background:var(--amber);color:#fff;font-size:15px;font-weight:700;}
  #askFollowupSendBtn:disabled{opacity:0.5;cursor:default;}
  @media (max-width:600px){
    #askHero{padding:20px 14px;}
    #askHeroRow{padding:4px 4px 4px 4px;gap:6px;}
    #askHeroInput{padding:10px 12px;font-size:13.5px;}
    #askHeroSendBtn{padding:0 14px;font-size:13px;}
    #askMicBtn{width:36px;height:36px;font-size:14px;}
    .askSuggestChip{font-size:11px;padding:6px 11px;}
  }
