<script>
  import { onMount } from 'svelte';
  import { getJSON } from '$lib/api.js';
  import * as P from '$lib/prefs.js';
  import { prefs, initPrefs } from '$lib/prefs.svelte.js';
  import HubBar from '$lib/components/HubBar.svelte';
  import Footer from '$lib/components/Footer.svelte';

  // The homepage hub (Claude Design "Frame A" — editorial, with colour): a news teaser,
  // Daily Art, Play, and the rotating small-joys rail, wired to live data. Legacy
  // root-query links (/?view=…) are redirected to /news by +page.js before this renders.
  let news = $state(null);   // {id, title, summary, image, topic, ...}
  let art = $state(null);    // {title, artist, year, image}
  let newsFit = $state('cover');   // 'cover' = full-bleed photo; 'contain' = framed-plate figure
  let newsImgOk = $state(false);   // reveal the photo only once it's truly loaded (else the typo cover stays)

  // Pictureless articles (~half the feed) get a typographic category cover instead of a blank
  // well: the topic word on a soft topic-tinted field, color-coded across the feed.
  const TOPIC_TINT = {
    science:     { bg: '#e2edf1', ink: '#2f6d86' },
    technology:  { bg: '#e8e9f4', ink: '#5a5fa0' },
    environment: { bg: '#e6efe2', ink: '#3a7d5b' },
    health:      { bg: '#f4e8e8', ink: '#a8566a' },
    community:   { bg: '#f3ece0', ink: '#a9763f' },
    culture:     { bg: '#ece5f3', ink: '#7e51b0' },
    world:       { bg: '#e6ecf3', ink: '#4a6da0' },
    space:       { bg: '#e7e7f2', ink: '#4a4e8a' },
  };
  const TOPIC_DEFAULT = { bg: '#e6edef', ink: '#3f6378' };
  let topicTint = $derived(TOPIC_TINT[(news?.topic || '').toLowerCase()] || TOPIC_DEFAULT);
  let word = $state(null);   // /api/word/today
  let quote = $state(null);  // /api/quote/today
  let fact = $state(null);   // /api/onthisday/today

  // small-joys display helpers
  const POS = { noun: 'n.', adjective: 'adj.', verb: 'v.', adverb: 'adv.', pronoun: 'pron.',
                preposition: 'prep.', conjunction: 'conj.', interjection: 'interj.' };
  const cap = (s) => (s ? s[0].toUpperCase() + s.slice(1) : '');
  const clip = (s, n) => {
    if (!s || s.length <= n) return s || '';
    const cut = s.slice(0, n), i = cut.lastIndexOf(' ');
    return (i > 0 ? cut.slice(0, i) : cut).replace(/[\s,;:.]+$/, '') + '…';
  };

  // truncation handled by CSS (-webkit-line-clamp:2) — breaks on whole words, fills 2 full lines
  let headline = $derived(news?.title ?? 'What went right this week: the good news that actually matters');

  // Sell the value proposition right on the card: the quick UB brief vs. the full article.
  //   "1 min brief · ~10 min full story"  (both known)
  //   "1 min brief"                       (full-story time unknown — never fake it)
  //   hidden                              (no brief/summary yet)
  const briefMins = (t) => Math.max(1, Math.round((t || '').trim().split(/\s+/).filter(Boolean).length / 200));
  let readBadge = $derived.by(() => {
    if (!news?.summary) return '';
    const brief = `${briefMins(news.summary)} min brief`;
    return news.source_read_minutes ? `${brief} · ~${news.source_read_minutes} min full story` : brief;
  });

  // small-joys shelf: a single-row, swipeable rail (all 3 cards live in the row; the next
  // peeks). Arrows scroll it; the counter/dots track the snapped card from scroll position.
  // Phones get a swipeable rail (all 3 cards in a scroll row, the next peeking). Wider
  // screens keep the original two-up view with the arrows rotating which two show — a
  // rail there felt like a broken carousel. isNarrow drives BOTH the DOM and the layout
  // so they never disagree (no "3 cards wrapping in a 2-col grid").
  let railEl = $state(null);
  let joyIdx = $state(0);
  let isNarrow = $state(false);
  $effect(() => {
    const mq = window.matchMedia('(max-width: 520px)');
    const sync = (e) => { isNarrow = e.matches; };
    sync(mq);
    mq.addEventListener('change', sync);
    return () => mq.removeEventListener('change', sync);
  });

  function railTo(i) {
    joyIdx = Math.max(0, Math.min(2, i));
    const card = railEl?.children?.[joyIdx];
    if (card) railEl.scrollTo({ left: card.offsetLeft, behavior: 'smooth' });
  }
  let scrollRAF = 0;
  function onRailScroll() {
    if (!railEl) return;
    cancelAnimationFrame(scrollRAF);
    scrollRAF = requestAnimationFrame(() => {
      const x = railEl.scrollLeft;
      let best = 0, bestD = Infinity;
      for (let i = 0; i < railEl.children.length; i++) {
        const d = Math.abs(railEl.children[i].offsetLeft - x);
        if (d < bestD) { bestD = d; best = i; }
      }
      joyIdx = best;
    });
  }
  // desktop = rotate which two are shown; mobile = scroll the rail
  const prevJoy = () => (isNarrow ? railTo(joyIdx - 1) : (joyIdx = (joyIdx + 2) % 3));
  const nextJoy = () => (isNarrow ? railTo(joyIdx + 1) : (joyIdx = (joyIdx + 1) % 3));

  onMount(async () => {
    initPrefs();   // load the reader's saved boundaries so the teaser honors them (like the News Brief)
    try {
      const a = await getJSON('/api/art/today');
      if (a) art = { title: a.title, artist: a.artist, year: a.date_text, image: a.image_url };
    } catch { /* fall back to gradient swatch */ }

    let homeq = '';
    try {
      const hv = localStorage.getItem('goodnews:home') || '';
      const hs = localStorage.getItem('goodnews:homeScope') || 'nearby';
      if (hv && hs !== 'world') homeq = `&home=${encodeURIComponent(hv)}&scope=${hs}`;
    } catch { /* global brief */ }
    const q = P.param(prefs.data);   // the reader's boundaries (excluded topics, ceilings)
    try {
      const it = (await getJSON(`/api/brief?limit=1${homeq}${q ? '&' + q : ''}`))?.items?.[0];
      // image_url is already resolved server-side per the source's image policy (our
      // cached copy for cleared sources, else the publisher URL); the probe below loads
      // it with retries either way.
      if (it) news = { id: it.id, title: it.title, summary: it.summary || it.description || '', image: it.image_url || null, topic: it.topic || null, source_read_minutes: it.source_read_minutes };
      // News images are hotlinked from the source, so a single fetch can transiently
      // fail (slow CDN, rate limit, hiccup) and leave a blank plate until you refresh.
      // Probe with a couple of retries and reveal the photo only once it's truly loaded
      // (and thus cached) — otherwise the typographic topic cover stays. The probe also
      // tells us cover (photo) vs. framed plate (wide/tall figure) from real dimensions.
      if (news?.image) {
        const url = news.image;
        let tries = 0;
        const load = () => {
          const probe = new Image();
          probe.onload = () => {
            const a = probe.naturalWidth / probe.naturalHeight;
            newsFit = a >= 0.85 && a <= 1.9 ? 'cover' : 'contain';
            newsImgOk = true;
          };
          probe.onerror = () => { if (++tries <= 2) setTimeout(load, 500 * tries); };
          probe.src = url;
        };
        load();
      }
    } catch { /* fall back to design copy */ }

    // small joys (each falls back to its placeholder if the engine has nothing yet)
    try { word = await getJSON('/api/word/today'); } catch { /* placeholder */ }
    try { quote = await getJSON('/api/quote/today'); } catch { /* placeholder */ }
    try { fact = await getJSON('/api/onthisday/today'); } catch { /* placeholder */ }
  });
</script>

<svelte:head>
  <title>upbeatBytes — a calmer, brighter corner of the internet</title>
  <meta name="description" content="A calmer, brighter corner of the internet: good news, daily art, small games, and little resets." />
</svelte:head>

{#snippet joyCard(i)}
  {#if i === 0}
    <a class="joy joy-word" href="/word">
      <span class="wm" aria-hidden="true">{word ? cap(word.word)[0] : 'S'}</span>
      <div class="joy-in">
        <div class="tag"><span class="rule"></span><span class="tag-label">Word of the day</span></div>
        <p class="word-line"><span class="word">{word ? cap(word.word) : 'Serene'}</span> <span class="word-pos">{word ? (POS[word.part_of_speech] ?? word.part_of_speech ?? '') : 'adj.'}</span></p>
        <p class="word-pron">{word?.phonetic ?? '/səˈriːn/'}</p>
        <p class="def">{word ? clip(word.definition, 78) : 'Calm, peaceful, and untroubled. The quiet after a storm passes.'}</p>
      </div>
    </a>
  {:else if i === 1}
    <a class="joy joy-quote" href="/quote">
      <span class="wm wm-q" aria-hidden="true">“</span>
      <div class="joy-in">
        <div class="tag"><span class="rule"></span><span class="tag-label">Quote of the day</span></div>
        <p class="quote">{quote ? clip(quote.text, 92) : 'Very little is needed to make a happy life.'}</p>
        <div class="attrib"><span class="attrib-rule"></span><span class="attrib-by">{quote?.author ?? 'Marcus Aurelius'}</span></div>
      </div>
    </a>
  {:else}
    <a class="joy joy-fact" href="/onthisday">
      <div class="joy-in">
        <div class="tag"><span class="rule"></span><span class="tag-label">On this day</span></div>
        <p class="fact-hero"><span class="year">{fact?.year ?? '1928'}</span> <span class="onthis">in history</span></p>
        <p class="fact">{fact ? clip(fact.text, 96) : 'Penicillin was discovered by a happy accident.'}</p>
      </div>
    </a>
  {/if}
{/snippet}

<div class="page">
  <HubBar active="home" />

  <section class="hero">
    <h1>A <span class="t">calmer</span>, <span class="b">brighter</span> corner of the internet.</h1>
    <p class="sub">Good news, daily art, small games, and little resets.</p>
  </section>

  <main class="bento">
    <!-- Good News (tall) — a card with TWO links, so it's a div, not a single anchor -->
    <div class="card news">
      <a class="news-photo-a" href={news?.id ? `/a/${news.id}` : '/'} aria-label="Read this article">
        {#if news?.image && newsImgOk}
          <div class="news-photo {newsFit}">
            <div class="news-plate" style={`background-image:url(${news.image})`}></div>
          </div>
        {:else}
          <!-- pictureless: typographic category cover -->
          <div class="news-typo" style={`background:${topicTint.bg}`}>
            <span class="news-typo-word" style={`color:${topicTint.ink}`}>{news?.topic || 'good news'}</span>
          </div>
        {/if}
      </a>
      <div class="news-body">
        <span class="label" style="color:#0083ad">GOOD NEWS</span>
        <a class="headline-a" href={news?.id ? `/a/${news.id}` : '/'}><h2>{headline}</h2></a>
        <a class="summary-a" href={news?.id ? `/a/${news.id}` : '/'}>
          <p class="summary">{news?.summary || "We read the week so you don't have to doomscroll it. Five quietly hopeful stories, summarised to the gist."}</p>
        </a>
        <div class="news-foot">
          {#if readBadge}<span class="meta">{readBadge}</span>{/if}
        </div>
        <hr class="news-div" />
        <a class="news-more" href="/news">Read more good news →</a>
      </div>
    </div>

    <div class="rightcol">
    <!-- Daily Art (wide) -->
    <a class="card art" href="/art">
      <div class="art-body">
        <span class="label" style="color:#8857C2">DAILY ART</span>
        <h3>A masterwork a day</h3>
        <p class="art-today">
          {#if art}Today: <span class="ital">{art.title}</span>{#if art.artist} — {art.artist}{/if}{#if art.year}, {art.year}{/if}.
          {:else}Today: <span class="ital">Among the Sierra Nevada</span> — Bierstadt, 1868.{/if}
        </p>
        <span class="link art-link">View today</span>
      </div>
      <div class="art-swatch" style={art?.image ? `--art:url(${art.image})` : ''}></div>
    </a>

    <!-- bottom pair — vertically centered in the space beneath the pinned Art card -->
    <div class="pair-wrap">
    <div class="pair">
      <a class="card play" href="/play">
        <div class="play-top">
          <span class="label" style="color:#A8650F">PLAY</span>
          <h3>A little daily puzzle</h3>
        </div>

        <!-- "bleeding boards": three game motifs clipping at the card edges (decorative) -->
        <div class="play-band" aria-hidden="true">
          <div class="wb">
            <div class="wb-row"><span class="wb-t wb-a">E</span><span class="wb-t wb-n">A</span><span class="wb-t wb-a">T</span></div>
            <div class="wb-row"><span class="wb-t wb-g">Y</span><span class="wb-t wb-g">T</span><span class="wb-t wb-g">E</span></div>
            <div class="wb-row"><span class="wb-t wb-e"></span><span class="wb-t wb-e"></span><span class="wb-t wb-d"></span></div>
          </div>
          <div class="ws">
            <span>K</span><span>R</span><span>O</span><span>A</span><span>E</span><span>S</span>
            <span class="hl">B</span><span class="hl">Y</span><span class="hl">T</span><span class="hl">E</span><span class="hl">S</span><span>W</span>
            <span>T</span><span>I</span><span>M</span><span>U</span><span>H</span><span>P</span>
            <span>G</span><span>E</span><span>B</span><span>O</span><span>R</span><span>L</span>
            <span>F</span><span>I</span><span>N</span><span>D</span><span>C</span><span>A</span>
            <span>Z</span><span>O</span><span>S</span><span>E</span><span>K</span><span>Y</span>
          </div>
          <div class="mm">
            <span class="mm-a"></span>
            <span class="mm-w"><span class="mm-dot" style="background:#6bbf8c"></span></span>
            <span class="mm-a"></span>
            <span class="mm-w"><span class="mm-dot" style="background:#6bbf8c"></span></span>
            <span class="mm-a"></span>
            <span class="mm-a"></span>
            <span class="mm-a"></span>
            <span class="mm-w"><span class="mm-dot" style="background:#D2861B"></span></span>
            <span class="mm-a"></span>
          </div>
        </div>

        <div class="play-foot"><span class="link play-link">Enter</span></div>
      </a>
      <div class="card moment">
        <div class="moment-top">
          <span class="label" style="color:#3F9A66">ENTERTAINMENT</span>
          <span class="soon">SOON</span>
        </div>
        <div class="moment-mid">
          <div class="ent-icons" aria-hidden="true">
            <span class="ent-icon">
              <svg viewBox="0 0 24 24"><circle cx="8" cy="9" r="3" fill="#e3a24c" /><circle cx="16" cy="9" r="3" fill="#5aa0c8" /><circle cx="12" cy="15.5" r="3" fill="#5bbf86" /></svg>
            </span>
            <span class="ent-icon">
              <svg viewBox="0 0 24 24" fill="none" stroke="#3F9A66" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9" /><path d="M8.5 14a4.5 4.5 0 0 0 7 0" /><circle cx="9" cy="10" r="1" fill="#3F9A66" stroke="none" /><circle cx="15" cy="10" r="1" fill="#3F9A66" stroke="none" /></svg>
            </span>
            <span class="ent-icon">
              <svg viewBox="0 0 24 24" fill="#3F9A66"><path d="M12 3l1.6 7.4L21 12l-7.4 1.6L12 21l-1.6-7.4L3 12l7.4-1.6z" /></svg>
            </span>
          </div>
          <p class="moment-line">A little something to enjoy.</p>
        </div>
        <span class="moment-meta">Coloring, characters, and curiosities. Coming soon.</span>
      </div>
    </div>
    </div>

    <!-- "small joys" — a single swipeable row; all three live in the rail, the next peeks -->
    <div class="joys-shelf">
      <div class="joys-head">
        <span class="jt-label">Small joys for today</span>
        <span class="jt-line"></span>
        <span class="jt-count">{joyIdx + 1} / 3</span>
        <div class="joys-arrows">
          <button class="arrow" onclick={prevJoy} aria-label="Previous small joy">‹</button>
          <button class="arrow" onclick={nextJoy} aria-label="Next small joy">›</button>
        </div>
      </div>
      <div class="joys" class:rail={isNarrow} bind:this={railEl} onscroll={onRailScroll}>
        {#if isNarrow}
          {@render joyCard(0)}
          {@render joyCard(1)}
          {@render joyCard(2)}
        {:else}
          {@render joyCard(joyIdx)}
          {@render joyCard((joyIdx + 1) % 3)}
        {/if}
      </div>
    </div>
    </div>
  </main>

  <Footer />
</div>

<style>
  @font-face { font-family: 'Hanken Grotesk'; src: url('/fonts/hanken-var.woff2') format('woff2'); font-weight: 400 700; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Newsreader'; src: url('/fonts/newsreader-var.woff2') format('woff2'); font-weight: 400 600; font-style: normal; font-display: swap; }
  @font-face { font-family: 'Newsreader'; src: url('/fonts/newsreader-italic-var.woff2') format('woff2'); font-weight: 400 500; font-style: italic; font-display: swap; }
  @font-face { font-family: 'Work Sans'; src: url('/fonts/work-sans-var.woff2') format('woff2'); font-weight: 400 700; font-style: normal; font-display: swap; }

  .page {
    --ink: #1c1916; --body: #6b6256; --muted: #a89e8c; --teal: #0083ad;
    --canvas: #FFF9EF; --news-border: #f2e7d3;
    min-height: 100vh; background: var(--canvas); color: #23201b;
    font-family: 'Hanken Grotesk', ui-sans-serif, system-ui, sans-serif;
    display: flex; flex-direction: column;
    overflow-x: clip;   /* seatbelt: a wide child (e.g. the joys rail) can never scroll the page */
  }
  .page :global(*) { box-sizing: border-box; }

  /* Hero — spacing tuned per the /home2 pass: pulled up a touch, more air before cards */
  .hero { text-align: center; max-width: 1180px; width: 100%; margin: 0 auto; padding: clamp(24px, 4vw, 34px) clamp(18px, 5vw, 44px) clamp(38px, 5vw, 48px); }
  .hero h1 {
    font-family: 'Newsreader', Georgia, serif; font-weight: 500;
    font-size: clamp(2.1rem, 5vw, 50px); line-height: 1.04; letter-spacing: -0.015em; margin: 0; color: var(--ink);
  }
  .hero h1 .t { color: #0083ad; }
  .hero h1 .b { color: #E0852C; }
  .hero .sub { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: clamp(1rem, 2vw, 19px); color: #857b6c; margin: 14px 0 0; }

  /* Bento grid. The cards use Work Sans (per CD's mockup — a touch bolder/darker than our
     body Hanken); headings stay Newsreader (set on h2,h3 below). */
  .bento {
    max-width: 1180px; width: 100%; margin: 0 auto; box-sizing: border-box;
    padding: 0 clamp(18px, 5vw, 44px) 16px;
    display: grid; grid-template-columns: minmax(0, 1.18fr) minmax(0, 1.82fr); gap: 16px;
    font-family: 'Work Sans', 'Hanken Grotesk', ui-sans-serif, system-ui, sans-serif;
  }
  /* Containment: the grid items + flex chain must be allowed to shrink below their content,
     or the phone joys rail forces the whole page wider than the viewport. (NOT .bento — it
     keeps its own max-width:1180px; listing it here would override that and go full-width.) */
  .news, .rightcol, .pair-wrap, .joys-shelf, .joys { min-width: 0; max-width: 100%; }
  /* right column matches the News height; Art stays pinned to the TOP and the Play/Moment
     pair to the BOTTOM, with the extra space distributed BETWEEN them (FIX1). The cards
     themselves keep their natural size and never stretch. */
  .rightcol { display: flex; flex-direction: column; gap: 16px; }
  .rightcol .art { flex: none; }                              /* Art pinned to the top */
  .pair-wrap { flex: 1; display: flex; align-items: center; } /* fill the rest; pair vertically centered */
  .card {
    border-radius: 18px; overflow: hidden; text-decoration: none; color: inherit;
    transition: transform 0.18s ease, box-shadow 0.18s ease;
  }
  a.card:hover { transform: translateY(-2px); }
  /* every card opens with the same "eyelash" — a short dash in the card's accent (via
     currentColor, so each label's inline colour drives it) + a tracked uppercase label */
  .label {
    display: inline-flex; align-items: center; gap: 9px;
    font-size: 11px; font-weight: 600; letter-spacing: 0.18em; line-height: 1;
  }
  .label::before { content: ''; width: 20px; height: 2px; border-radius: 2px; background: currentColor; flex: none; }
  .link { font-size: 14px; font-weight: 600; padding-bottom: 2px; align-self: flex-start; }
  /* card titles: Newsreader, a calmer medium weight (per CD's mockup) */
  h2, h3 { font-family: 'Newsreader', Georgia, serif; font-weight: 500; letter-spacing: -0.01em; color: var(--ink); }

  /* Good News — photo on top, equal height to the right column. The gist flex-fills the
     card (basis:0 so it never drives the row height → the right column always sets it, never
     stretched) and fades softly into a comfortable margin above the read-time. */
  .news {
    background: #fff; border: 1px solid var(--news-border);
    display: flex; flex-direction: column; box-shadow: 0 6px 20px -14px rgba(0, 131, 173, 0.4);
  }
  /* photo + headline both link to the article (clickable, not just the text links) */
  .news-photo-a { display: block; }
  .news-photo-a:hover { filter: brightness(0.97); }
  .headline-a { display: block; text-decoration: none; color: inherit; }
  .headline-a:hover h2 { color: var(--teal); }
  /* Photos fill edge-to-edge (cover, no box). Only figures/diagrams (detected by their
     wide/tall shape) get the soft tinted matte + white framed plate, so labels stay whole. */
  .news-photo { aspect-ratio: 5/4; animation: news-photo-in 0.45s ease both; }
  @keyframes news-photo-in { from { opacity: 0; } to { opacity: 1; } }
  @media (prefers-reduced-motion: reduce) { .news-photo { animation: none; } }
  .news-plate { background-position: center; background-repeat: no-repeat; }
  .news-photo.cover .news-plate { width: 100%; height: 100%; background-size: cover; }
  .news-photo.contain {
    /* silvery at top, fading down into the card's white so the matte isn't a hard band */
    background: linear-gradient(180deg, #e6edef 0%, #f3f6f5 55%, #ffffff 100%);
    display: flex; align-items: center; justify-content: center; padding: 20px;
  }
  .news-photo.contain .news-plate {
    width: 100%; height: 100%; box-sizing: border-box; padding: 12px;
    background-color: #fff; border: 1px solid #e7edee; border-radius: 8px;
    box-shadow: 0 6px 18px -10px rgba(30, 60, 70, 0.28);
    background-size: contain; background-origin: content-box;
  }
  /* pictureless fallback: topic word on a soft topic-tinted field, same footprint as the photo */
  .news-typo { aspect-ratio: 5/4; display: flex; align-items: center; justify-content: center; padding: 16px; box-sizing: border-box; }
  .news-typo-word {
    font-family: 'Newsreader', Georgia, serif; font-weight: 500; font-size: clamp(1.9rem, 5vw, 2.9rem);
    line-height: 1.05; letter-spacing: -0.01em; text-transform: lowercase; text-align: center; max-width: 100%;
  }

  .news-body { padding: 24px 26px; flex: 1; display: flex; flex-direction: column; }
  .news h2 {
    font-size: clamp(1.55rem, 2.6vw, 30px); line-height: 1.14; margin: 12px 0 0;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
  }
  /* Desktop: the gist grows to fill the card (basis:0 so it never inflates the row height),
     softly fading out so it never jams against the read-time — a comfortable margin either way. */
  .summary-a {
    flex: 1 1 0; min-height: 0; overflow: hidden; display: block; text-decoration: none; color: inherit;
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.5em), transparent);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 1.5em), transparent);
  }
  .summary { font-size: 15px; line-height: 1.5; color: #5a5346; margin: 12px 0 0; }
  .news-foot { display: flex; align-items: center; justify-content: flex-end; padding-top: 16px; }
  .meta { font-size: 12px; color: var(--muted); line-height: 1.35; overflow-wrap: anywhere; }
  /* divider sets the secondary "feed" link apart as its own thing */
  .news-div { border: none; border-top: 1px solid #e6d9bf; margin: 14px 0 12px; }
  .news-more { display: inline-block; font-size: 13px; font-weight: 600; color: var(--teal); text-decoration: none; }
  .news-more:hover { text-decoration: underline; }
  .news-more:hover { color: var(--teal); }

  /* Daily Art — wide, text left + artwork swatch right */
  .art { background: #F3EEF9; border: 1px solid #e4d8f1; display: flex; min-height: 188px; }
  .art-body { flex: 1; padding: 24px 26px; display: flex; flex-direction: column; }
  .art h3 { font-size: clamp(1.35rem, 2.1vw, 25px); line-height: 1.16; margin: 10px 0 0; color: #2a1c3d; }
  .art-today { font-size: 14.5px; line-height: 1.5; color: #6f6280; margin: 11px 0 0; }
  .ital { font-style: italic; font-family: 'Newsreader', Georgia, serif; }
  /* a little breathing room before the link (per CD), not pinned tight under the caption */
  .art-link { margin-top: 20px; color: #8857C2; border-bottom: 2px solid #c9aef0; }
  /* swatch crops a few px off every edge (::after inset) so scanned paintings don't show
     their ragged/black canvas edge at the top */
  .art-swatch {
    width: 46%; min-width: 130px; position: relative; overflow: hidden;
    background: linear-gradient(170deg, #bfe0f0 0%, #a9cf9a 50%, #d89a4e 100%);
  }
  .art-swatch::after {
    content: ""; position: absolute; inset: -6px;
    background-image: var(--art); background-size: cover; background-position: center;
  }

  /* bottom pair */
  .pair { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  .play { background: #FFF3DC; border: 1px solid #f6e2b8; display: flex; flex-direction: column; }
  .play-top { padding: 22px 24px 0; }
  .play h3 { font-size: clamp(1.25rem, 1.9vw, 23px); margin: 14px 0 0; color: #5c3d0c; }
  .play-foot { margin-top: auto; padding: 16px 24px 22px; }
  .play-link { color: #A8650F; border-bottom: 2px solid #e0a94f; }

  /* "bleeding boards" — three game motifs clipping at the card edges (decorative) */
  /* Word search is the centred highlight; the two side games are the SAME size and each
     bleeds ~half a column off its edge (consistent both sides) to imply "more under the hood". */
  .play-band { position: relative; height: 124px; margin-top: 24px; overflow: hidden; }
  .wb { position: absolute; top: 50%; left: -12px; transform: translateY(-50%); display: flex; flex-direction: column; gap: 4px; }
  .wb-row { display: flex; gap: 4px; }
  .wb-t { width: 24px; height: 24px; border-radius: 5px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 11px; color: #fff; }
  .wb-g { background: #6bbf8c; } .wb-a { background: #E6A02C; } .wb-n { background: #d9c39a; }
  .wb-e { background: #fff; border: 1.5px solid #ecca84; } .wb-d { background: #fff; border: 1.5px dashed #e0bb6f; }
  .ws {
    position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    display: grid; grid-template-columns: repeat(6, 14px); gap: 3px;
    font-weight: 600; font-size: 10.5px; line-height: 14px; color: #d4b576; text-align: center;
    background: #fff; border: 1.5px solid #f0d597; border-radius: 10px; padding: 9px;
    box-shadow: 0 5px 16px -8px rgba(210, 134, 27, 0.55);
  }
  .ws .hl { color: #B5701A; font-weight: 800; }
  .mm { position: absolute; top: 50%; right: -12px; transform: translateY(-50%); display: grid; grid-template-columns: repeat(3, 24px); grid-auto-rows: 24px; gap: 4px; }
  .mm > span { border-radius: 6px; }
  .mm-a { background: #E6A02C; }
  .mm-w { background: #fff; border: 1.5px solid #f0d597; display: flex; align-items: center; justify-content: center; }
  .mm-dot { width: 7px; height: 7px; border-radius: 50%; display: block; }

  .moment { background: #E6F3E9; border: 1px solid #cee6d3; padding: 22px 24px; display: flex; flex-direction: column; }
  .moment-top { display: flex; align-items: center; justify-content: space-between; }
  .soon { font-size: 10px; font-weight: 700; letter-spacing: 0.08em; color: #3F9A66; background: #fff; border-radius: 999px; padding: 3px 8px; }
  /* centered motif: three small enjoy-icons + tagline fill the middle, caption at the foot */
  .moment-mid { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; text-align: center; }
  .ent-icons { display: flex; gap: 10px; }
  .ent-icon { width: 46px; height: 46px; border-radius: 13px; background: #fff; border: 1px solid #d3e4d8; box-shadow: 0 4px 12px -8px rgba(40, 90, 60, 0.4); display: flex; align-items: center; justify-content: center; }
  .ent-icon svg { width: 24px; height: 24px; display: block; }
  .moment-line { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 18px; line-height: 1.3; color: #214a35; margin: 0; }
  .moment-meta { margin-top: 14px; font-size: 13px; color: #6f9683; text-align: center; }

  /* "small joys" rail — little jewels: one big focal point per card, a faint oversized
     watermark glyph, an accent-tag label, soft diagonal gradient + long low shadow. */
  .joys-shelf { flex: none; }
  /* header: italic title · hairline rule · counter · arrows (the gesture is genuinely
     horizontal now, so the 1/3 + ‹ › affordance is honest) */
  .joys-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
  .jt-label { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 18px; color: #3a342b; }
  .jt-line { flex: 1; height: 1px; background: #e6dcc8; }
  .jt-count { font-size: 12px; color: #b0a690; white-space: nowrap; }
  .joys-arrows { display: flex; gap: 8px; }
  .arrow {
    width: 30px; height: 30px; border-radius: 50%; border: 1px solid #e0d3b8; background: transparent;
    color: #b09a6e; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center;
    padding: 0; line-height: 1; transition: background 0.15s ease, color 0.15s ease;
    -webkit-tap-highlight-color: transparent;
  }
  .arrow:hover { background: #fff; color: #9a7b3e; }
  /* Desktop/tablet: the original two-up view (arrows rotate which two show). */
  .joys { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
  .joy {
    min-width: 0; position: relative; overflow: hidden; border-radius: 20px; padding: 18px 22px;
    min-height: 170px; box-sizing: border-box; display: block; text-decoration: none; color: inherit;
    transition: transform 0.16s ease, box-shadow 0.16s ease;
  }
  .joy:hover { transform: translateY(-2px); }
  /* Phone: a single swipeable row; the next card peeks. Scrollbar hidden; snap per card. */
  .joys.rail {
    display: flex; gap: 16px; overflow-x: auto; scroll-snap-type: x mandatory;
    padding-bottom: 6px; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .joys.rail::-webkit-scrollbar { display: none; }
  .joys.rail .joy { flex: 0 0 min(86%, 344px); scroll-snap-align: start; }
  .joy-in { position: relative; }   /* content sits above the watermark */
  .wm { position: absolute; font-family: 'Newsreader', Georgia, serif; line-height: 1; pointer-events: none; }

  /* a fresh trio, distinct from the doors above (teal/plum/amber/green): sky · rose · clay */
  .joy-word { background: linear-gradient(165deg, #EAF2F9, #DBE8F4); border: 1px solid #d2e1f0; box-shadow: 0 10px 30px -22px rgba(60, 100, 145, 0.55); --accent: #4f7da8; --rule: #4f7da8; }
  .joy-word .wm { right: -14px; bottom: -30px; font-size: 150px; font-weight: 400; color: rgba(79, 125, 168, 0.13); }
  .joy-quote { background: linear-gradient(165deg, #F9EDF1, #F1DEE6); border: 1px solid #eed6df; box-shadow: 0 10px 30px -22px rgba(150, 85, 115, 0.5); --accent: #b06a86; --rule: #b06a86; }
  .joy-quote .wm { left: 14px; top: -26px; font-size: 120px; color: rgba(176, 106, 134, 0.16); }
  .joy-fact { background: linear-gradient(165deg, #F7EAE1, #EFDACB); border: 1px solid #ecd5c4; box-shadow: 0 10px 30px -22px rgba(150, 90, 55, 0.5); --accent: #9a5a38; --rule: #9a5a38; }

  .tag { display: flex; align-items: center; gap: 8px; }
  .tag .rule { width: 18px; height: 2px; border-radius: 2px; background: var(--rule); }
  .tag-label { font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent); }

  .word-line { margin: 8px 0 0; display: flex; align-items: baseline; gap: 9px; }
  .joy .word { font-family: 'Newsreader', Georgia, serif; font-weight: 500; font-size: 32px; line-height: 1; letter-spacing: -0.01em; color: #2c3a48; }
  .word-pos { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 13px; color: #7d93a8; }
  .word-pron { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 13px; color: #5f7791; margin: 2px 0 0; }
  .joy .def { font-size: 13.5px; color: #45535d; margin: 8px 0 0; line-height: 1.45; }

  .joy .quote { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 21px; line-height: 1.3; color: #3e2c36; margin: 16px 0 0; }
  .attrib { display: flex; align-items: center; gap: 9px; margin-top: 12px; }
  .attrib-rule { width: 22px; height: 1px; background: #d8afc1; }
  .attrib-by { font-family: 'Newsreader', Georgia, serif; font-size: 13px; color: #97667f; }

  .fact-hero { display: flex; align-items: baseline; gap: 8px; margin: 12px 0 0; }
  .year { font-family: 'Newsreader', Georgia, serif; font-weight: 500; font-size: 30px; color: #7a4a30; line-height: 0.9; }
  .onthis { font-size: 11px; color: #9e7a64; letter-spacing: 0.04em; }
  .joy .fact { font-family: 'Newsreader', Georgia, serif; font-size: 16px; color: #5e4636; margin: 8px 0 0; line-height: 1.34; }


  /* responsive — collapse the bento on narrow screens */
  @media (max-width: 860px) {
    .bento { grid-template-columns: minmax(0, 1fr); }   /* minmax(0,…) so a wide child can't widen the page */
    .news { grid-row: auto; }
    /* phone: a short, clean 3-line taste (no fill/fade — the card is natural height here) */
    .summary-a { flex: none; -webkit-mask-image: none; mask-image: none; }
    .summary { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; }
    .news-foot { padding-top: 12px; }   /* snug under the text, not a line's gap */
  }
  @media (max-width: 520px) {
    /* Art becomes an image-first card: the painting on top in a proper landscape frame
       (aspect-ratio, not a stubby fixed-height band that crop the work to a sliver),
       caption beneath. */
    .art { flex-direction: column; min-height: 0; }
    .art-swatch { width: 100%; min-width: 0; order: -1; aspect-ratio: 3 / 2; }
    .pair { grid-template-columns: 1fr; }
    /* Entertainment: when stacked it loses the height it borrowed from Play on desktop,
       so the content felt crowded. Give it room to breathe (not as tall as desktop). */
    .moment { padding: 30px 24px; gap: 6px; }
    .moment-mid { gap: 18px; padding: 12px 0; }
    .moment-meta { margin-top: 18px; }
  }
</style>
