/* global React, ReactDOM, TweaksPanel, useTweaks, TweakSection, TweakRadio, TweakColor */
const { useState, useEffect, useMemo } = React;

// ─────────────────────────────────────────────────────────────────────────────
// i18n
// ─────────────────────────────────────────────────────────────────────────────
const T = {
  fr: {
    locale: 'fr-CA',
    edition: 'CANADIAN PORTAL · ÉDITION FR-CA · MAI 2026',
    updated: 'Mise à jour\u00a0: 06.05.2026',
    notice: 'Site indépendant, non affilié au gouvernement',
    nav: [
      { label: 'Immigrer', href: '#immigrer' },
      { label: 'Travailler', href: '#travailler' },
      { label: 'Vivre', href: '#vivre' },
      { label: 'Provinces', href: '#provinces' },
      { label: 'Actualités', href: '#actu' },
      { label: 'Guides', href: '#guides' },
      { label: 'Citoyenneté', href: '#citoyennete' },
    ],
    subscribe: 'S’abonner',
    searchPlaceholder: 'Chercher un guide, une province, un programme…',
    live: 'EN DIRECT',
    eyebrow: 'Vol. 07 · Édition de mai',
    heroTitleA: 'Comprendre le',
    heroTitleEm: 'Canada',
    heroTitleB: 'avant de le',
    heroTitleC: 'choisir.',
    heroDek: 'Un journal indépendant pour préparer un projet d’immigration avec calme. Programmes officiels, vie quotidienne, finances, citoyenneté. Relus, sourcés, jamais alarmistes.',
    ctaExplore: 'Explorer les rubriques',
    ctaRead: 'Lire les guides récents',
    metaGuides: 'Guides publiés',
    metaProvinces: 'Provinces et territoires',
    metaEdition: 'Édition à jour',
    cardKicker: 'À LA UNE · 06.05.2026',
    cardTitleA: 'Entrée express, 2026',
    cardTitleB: 'les seuils des dernières rondes',
    cardDek: 'Lecture mesurée des invitations récentes. Ce que les chiffres permettent réellement de prévoir, et ce qu’ils ne disent pas.',
    cardReadTime: '10 min de lecture',
    cardCta: 'Lire l’article →',
    sec1Mono: '§ I · RUBRIQUES',
    sec1Title: 'Sept entrées, un seul fil rouge.',
    sec1Lead: 'Chaque rubrique regroupe des guides relus à la main, mis à jour, et croisés avec les sources officielles d’IRCC et des provinces.',
    sec2Mono: '§ II · GUIDES RÉCENTS',
    sec2Title: 'La rédaction cette semaine.',
    sec2Lead: 'Filtrez par rubrique. Tous les articles sont datés et révisés à chaque changement officiel signalé par IRCC.',
    all: 'Tout',
    results: 'résultats',
    result: 'résultat',
    fullArticle: 'Lire l’article complet →',
    minRead: 'min',
    emptyTitle: 'Aucun guide ne correspond à ce filtre pour le moment.',
    emptyCta: 'Tout afficher',
    pull: 'Immigrer, c’est traduire sa vie dans un nouveau cadre administratif. Ce portail aide à le faire',
    pullAccent: 'sans perdre la sienne.',
    pullCredit: 'La rédaction',
    sec3Mono: '§ III · INFOLETTRE',
    sec3Title: 'Une lettre, deux fois par mois.',
    sec3Lead: 'Les changements officiels qui méritent vraiment votre attention. Sans alarmisme, sans clickbait, avec les liens vers les sources.',
    nlLabel: 'VOTRE COURRIEL',
    nlPlaceholder: 'vous@exemple.ca',
    nlFine1: 'Aucun spam. Désabonnement en un clic. Voir notre',
    nlFineLink: 'politique de confidentialité',
    footerBrand: 'Un journal indépendant pour préparer un projet canadien avec calme. Pédagogique, vérifié, jamais alarmiste.',
    footerNotice: 'Site indépendant non affilié au gouvernement du Canada.',
    fSections: 'Rubriques',
    fAbout: 'À propos',
    fFollow: 'Suivre',
    fAboutLinks: ['Notre méthode', 'Contact', 'Mentions légales', 'Politique de confidentialité', 'Conditions d’utilisation'],
    fFollowLinks: ['Infolettre', 'Instagram', 'YouTube', 'Pinterest', 'RSS'],
    fCopy: '© 2026 CANADIAN PORTAL · TOUS DROITS RÉSERVÉS',
    fMade: 'FAIT AVEC SOIN POUR FUTURS NOUVEAUX ARRIVANTS',
    ticker: [
      { tag: 'NOUVEAU', text: 'Ontario PNP, nouveau tirage Tech Draw, seuil CRS 472' },
      { tag: 'IRCC', text: 'Entrée express, ronde du 06 mai, 4 500 invitations envoyées' },
      { tag: 'QUÉBEC', text: 'PEQ volet diplômés, ouverture des inscriptions le 15 mai 2026' },
      { tag: 'C.-B.', text: 'BC PNP Tech, 168 invitations cette semaine, score min. 105' },
      { tag: 'ALERTE', text: 'Permis postdiplôme, nouvelles règles dès juin 2026' },
      { tag: 'ALBERTA', text: 'AAIP rouvre la catégorie Tourism & Hospitality après 18 mois' },
      { tag: 'CITOYENNETÉ', text: 'Délais de traitement réduits à 10 mois en moyenne' },
      { tag: 'MANITOBA', text: 'MPNP, 312 candidats invités, secteurs prioritaires en santé' },
      { tag: 'NOUVEAU', text: 'Programme pilote pour les aides familiaux, ouverture confirmée' },
    ],
    topics: [
      { id: 'immigrer', label: 'Immigrer', kicker: '01', count: 14, hue: 25,
        title: 'Immigrer au Canada',
        desc: 'Programmes de résidence permanente, Entrée express, candidats des provinces, critères et étapes avant la demande.' },
      { id: 'travailler', label: 'Travailler', kicker: '02', count: 11, hue: 165,
        title: 'Travailler au Canada',
        desc: 'Permis de travail, droits des travailleurs étrangers, transition vers la résidence permanente, employeurs et offres.' },
      { id: 'vivre', label: 'Vivre', kicker: '03', count: 9, hue: 230,
        title: 'Vivre au Canada',
        desc: 'Préparer l’arrivée, services d’établissement, logement, santé, finances, impôts et intégration au quotidien.' },
      { id: 'provinces', label: 'Provinces', kicker: '04', count: 10, hue: 290,
        title: 'Provinces et territoires',
        desc: 'Comparer les provinces selon immigration, emploi, coût de la vie et installation. Panoramas pratiques.' },
      { id: 'actu', label: 'Actualités', kicker: '05', count: 8, hue: 50,
        title: 'Actualités IRCC',
        desc: 'Décryptages prudents des changements officiels, annonces, mises à jour de programmes et tendances à surveiller.' },
      { id: 'guides', label: 'Guides', kicker: '06', count: 16, hue: 130,
        title: 'Guides et conseils',
        desc: 'Conseils concrets pour organiser un dossier, éviter les pièges, préparer son budget, communiquer avec les autorités.' },
      { id: 'citoyennete', label: 'Citoyenneté', kicker: '07', count: 7, hue: 340,
        title: 'Citoyenneté canadienne',
        desc: 'Repères sur la présence physique, le test, les langues et les étapes après la résidence permanente.' },
    ],
    articles: [],  },
  en: {
    locale: 'en-CA',
    edition: 'CANADIAN PORTAL · EN-CA EDITION · MAY 2026',
    updated: 'Updated\u00a0: 05.06.2026',
    notice: 'Independent site, not affiliated with the Government of Canada',
    nav: [
      { label: 'Immigrate', href: '#immigrer' },
      { label: 'Work', href: '#travailler' },
      { label: 'Live', href: '#vivre' },
      { label: 'Provinces', href: '#provinces' },
      { label: 'News', href: '#actu' },
      { label: 'Guides', href: '#guides' },
      { label: 'Citizenship', href: '#citoyennete' },
    ],
    subscribe: 'Subscribe',
    searchPlaceholder: 'Search a guide, a province, a program…',
    live: 'LIVE',
    eyebrow: 'Vol. 07 · May Edition',
    heroTitleA: 'Understand',
    heroTitleEm: 'Canada',
    heroTitleB: 'before you',
    heroTitleC: 'choose it.',
    heroDek: 'An independent journal for planning your immigration project calmly. Official programs, daily life, finances, citizenship. Edited, sourced, never alarmist.',
    ctaExplore: 'Explore sections',
    ctaRead: 'Read recent guides',
    metaGuides: 'Published guides',
    metaProvinces: 'Provinces and territories',
    metaEdition: 'Up-to-date edition',
    cardKicker: 'FEATURED · 05.06.2026',
    cardTitleA: 'Express Entry, 2026',
    cardTitleB: 'thresholds of recent rounds',
    cardDek: 'A measured reading of recent invitations. What the numbers really tell us, and what they don’t.',
    cardReadTime: '10 min read',
    cardCta: 'Read the article →',
    sec1Mono: '§ I · SECTIONS',
    sec1Title: 'Seven entries, one common thread.',
    sec1Lead: 'Each section gathers guides reviewed by hand, kept up to date, and cross-checked with official IRCC and provincial sources.',
    sec2Mono: '§ II · RECENT GUIDES',
    sec2Title: 'The newsroom this week.',
    sec2Lead: 'Filter by section. All articles are dated and revised whenever IRCC announces an official change.',
    all: 'All',
    results: 'results',
    result: 'result',
    fullArticle: 'Read the full article →',
    minRead: 'min',
    emptyTitle: 'No guide matches this filter at the moment.',
    emptyCta: 'Show all',
    pull: 'Immigrating means translating your life into a new administrative frame. This portal helps you do that',
    pullAccent: 'without losing your own.',
    pullCredit: 'The editors',
    sec3Mono: '§ III · NEWSLETTER',
    sec3Title: 'A letter, twice a month.',
    sec3Lead: 'The official changes that truly deserve your attention. No alarmism, no clickbait, with links to the sources.',
    nlLabel: 'YOUR EMAIL',
    nlPlaceholder: 'you@example.ca',
    nlFine1: 'No spam. Unsubscribe in one click. See our',
    nlFineLink: 'privacy policy',
    footerBrand: 'An independent journal to plan a Canadian project calmly. Educational, fact-checked, never alarmist.',
    footerNotice: 'Independent site not affiliated with the Government of Canada.',
    fSections: 'Sections',
    fAbout: 'About',
    fFollow: 'Follow',
    fAboutLinks: ['Our method', 'Contact', 'Legal notice', 'Privacy policy', 'Terms of use'],
    fFollowLinks: ['Newsletter', 'Instagram', 'YouTube', 'Pinterest', 'RSS'],
    fCopy: '© 2026 CANADIAN PORTAL · ALL RIGHTS RESERVED',
    fMade: 'MADE WITH CARE FOR FUTURE NEWCOMERS',
    ticker: [
      { tag: 'NEW', text: 'Ontario PNP, new Tech Draw, CRS threshold 472' },
      { tag: 'IRCC', text: 'Express Entry, May 6 round, 4,500 invitations issued' },
      { tag: 'QUÉBEC', text: 'PEQ graduates stream, registrations open May 15, 2026' },
      { tag: 'B.C.', text: 'BC PNP Tech, 168 invitations this week, min. score 105' },
      { tag: 'ALERT', text: 'Postgraduate work permit, new rules from June 2026' },
      { tag: 'ALBERTA', text: 'AAIP reopens Tourism & Hospitality category after 18 months' },
      { tag: 'CITIZENSHIP', text: 'Processing times reduced to 10 months on average' },
      { tag: 'MANITOBA', text: 'MPNP, 312 candidates invited, priority sectors in healthcare' },
      { tag: 'NEW', text: 'Pilot program for caregivers, opening confirmed' },
    ],
    topics: [
      { id: 'immigrer', label: 'Immigrate', kicker: '01', count: 14, hue: 25,
        title: 'Immigrate to Canada',
        desc: 'Permanent residence programs, Express Entry, provincial nominees, criteria and steps before applying.' },
      { id: 'travailler', label: 'Work', kicker: '02', count: 11, hue: 165,
        title: 'Work in Canada',
        desc: 'Work permits, rights of foreign workers, transitioning to permanent residence, employers and job offers.' },
      { id: 'vivre', label: 'Live', kicker: '03', count: 9, hue: 230,
        title: 'Live in Canada',
        desc: 'Prepare your arrival, settlement services, housing, healthcare, finances, taxes and daily integration.' },
      { id: 'provinces', label: 'Provinces', kicker: '04', count: 10, hue: 290,
        title: 'Provinces and territories',
        desc: 'Compare provinces by immigration, employment, cost of living and settlement. Practical overviews.' },
      { id: 'actu', label: 'News', kicker: '05', count: 8, hue: 50,
        title: 'IRCC news',
        desc: 'Careful analysis of official changes, announcements, program updates and trends to watch.' },
      { id: 'guides', label: 'Guides', kicker: '06', count: 16, hue: 130,
        title: 'Guides and tips',
        desc: 'Concrete advice to organize a file, avoid pitfalls, plan a budget, and communicate with authorities.' },
      { id: 'citoyennete', label: 'Citizenship', kicker: '07', count: 7, hue: 340,
        title: 'Canadian citizenship',
        desc: 'Reference points on physical presence, the test, languages, and steps after permanent residence.' },
    ],
    articles: [],  },
};

if (window.ARTICLES_FR) T.fr.articles = window.ARTICLES_FR;
if (window.ARTICLES_EN) T.en.articles = window.ARTICLES_EN;

// Hash routing helpers
function parseHash(h) {
  if (!h || h === '#') return { kind: 'home' };
  const m = h.replace(/^#/, '').match(/^(article|rubrique|page)\/(.+)$/);
  if (!m) return { kind: 'home' };
  if (m[1] === 'article') return { kind: 'article', id: parseInt(m[2], 10) };
  if (m[1] === 'page') return { kind: 'page', id: m[2] };
  return { kind: 'topic', id: m[2] };
}
function slugArticle(idx) { return `#article/${idx}`; }
function slugTopic(id) { return `#rubrique/${id}`; }

// ─────────────────────────────────────────────────────────────────────────────
// Logo
// ─────────────────────────────────────────────────────────────────────────────
function CPLogo({ size = 38 }) {
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 12, color: 'var(--ink)' }}>
      <svg width={size} height={size} viewBox="0 0 64 64" aria-hidden="true">
        <circle cx="32" cy="32" r="30" fill="var(--accent)"/>
        <circle cx="32" cy="32" r="30" fill="none" stroke="var(--accent-deep)" strokeWidth="1.5"/>
        <circle cx="32" cy="32" r="24" fill="none" stroke="white" strokeWidth="0.6" opacity="0.35"/>
        <g fill="white">
          <rect x="31" y="40" width="2" height="10" rx="1"/>
          <path d="M16 42 L32 30 L48 42 L42 42 L32 34 L22 42 Z" opacity="0.78"/>
          <path d="M19 32 L32 22 L45 32 L40 32 L32 26 L24 32 Z" opacity="0.92"/>
          <path d="M24 24 L32 16 L40 24 L36 24 L32 20 L28 24 Z"/>
          <circle cx="32" cy="13" r="1.4"/>
        </g>
      </svg>
      <span style={{ fontFamily: 'var(--serif)', fontWeight: 700, fontSize: 20, letterSpacing: '-0.01em', display: 'flex', alignItems: 'baseline', gap: 1 }}>
        <span style={{ color: 'var(--ink)' }}>Canadian</span>
        <span style={{ color: 'var(--accent)', fontStyle: 'italic', fontWeight: 700 }}>Portal</span>
      </span>
    </span>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Article art (richer placeholder illustrations)
// ─────────────────────────────────────────────────────────────────────────────
// Photo gallery is provided by cp-articles.js (window.ART_PHOTOS)

function ArticleArt({ kind, large = false }) {
  const url = (window.ART_PHOTOS && (window.ART_PHOTOS[kind] || window.ART_PHOTOS.editorial)) || '';
  const wrapStyle = {
    width: '100%',
    aspectRatio: large ? '4 / 3' : '3 / 2',
    background: '#1a1815',
    borderRadius: 6,
    overflow: 'hidden',
    position: 'relative',
    border: '1px solid var(--line)',
  };
  const imgStyle = {
    position: 'absolute', inset: 0, width: '100%', height: '100%',
    objectFit: 'cover', display: 'block',
    filter: 'saturate(0.92) contrast(1.04)',
  };
  const overlay = (
    <div aria-hidden="true" style={{
      position: 'absolute', inset: 0,
      background: large
        ? 'linear-gradient(180deg, rgba(20,18,15,0) 0%, rgba(20,18,15,0) 50%, rgba(20,18,15,0.55) 100%)'
        : 'linear-gradient(180deg, rgba(20,18,15,0) 60%, rgba(20,18,15,0.35) 100%)',
      pointerEvents: 'none',
    }}/>
  );
  const corner = large ? (
    <div aria-hidden="true" style={{
      position: 'absolute', left: 16, top: 16,
      fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '0.14em',
      color: 'white', background: 'var(--accent)',
      padding: '5px 10px', borderRadius: 3, fontWeight: 700,
    }}>À LA UNE</div>
  ) : null;
  return (
    <div style={wrapStyle}>
      <img src={url} alt="" loading="lazy" style={imgStyle}/>
      {overlay}
      {corner}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// App
// ─────────────────────────────────────────────────────────────────────────────
function App() {
  const [tweaks, setTweak] = useTweaks(/*EDITMODE-BEGIN*/{
    "palette": "warm",
    "accentHue": 28,
    "density": "comfortable"
  }/*EDITMODE-END*/);

  const [lang, setLang] = useState('fr');
  const t = T[lang];

  useEffect(() => {
    const root = document.documentElement;
    const palettes = {
      warm:  { bg: '#f6f1e7', card: '#fbf7ef', ink: '#1a1815', ink2: '#3a3530', ink3: '#6b645c', line: '#e3dccc', line2: '#cdc4b0' },
      cool:  { bg: '#eef0f2', card: '#f7f8fa', ink: '#101418', ink2: '#2c333a', ink3: '#5b6470', line: '#d8dde2', line2: '#b9c1cb' },
      paper: { bg: '#f3eee5', card: '#ffffff', ink: '#161514', ink2: '#33312d', ink3: '#6c665b', line: '#e2dccd', line2: '#c7bea7' },
      dark:  { bg: '#15140f', card: '#1c1b16', ink: '#f3ede0', ink2: '#d6cfbe', ink3: '#8c8576', line: '#2e2c25', line2: '#3e3b32' },
    };
    const p = palettes[tweaks.palette] || palettes.warm;
    root.style.setProperty('--bg', p.bg);
    root.style.setProperty('--bg-card', p.card);
    root.style.setProperty('--ink', p.ink);
    root.style.setProperty('--ink-2', p.ink2);
    root.style.setProperty('--ink-3', p.ink3);
    root.style.setProperty('--line', p.line);
    root.style.setProperty('--line-2', p.line2);
    // Canada red — toned down further
    root.style.setProperty('--accent',      `oklch(0.52 0.18 ${tweaks.accentHue})`);
    root.style.setProperty('--accent-deep', `oklch(0.42 0.17 ${tweaks.accentHue})`);
    root.style.setProperty('--accent-soft', `oklch(0.94 0.04 ${tweaks.accentHue})`);
  }, [tweaks.palette, tweaks.accentHue]);

  const [activeTopic, setActiveTopic] = useState('all');
  const [search, setSearch] = useState('');
  const [navOpen, setNavOpen] = useState(false);
  const [showSearch, setShowSearch] = useState(false);
  const [route, setRoute] = useState(() => parseHash(window.location.hash));

  useEffect(() => { document.body.classList.add('ready'); }, []);

  // Hash routing — real pages via #article/<idx> and #rubrique/<id>
  useEffect(() => {
    const onHash = () => {
      setRoute(parseHash(window.location.hash));
      window.scrollTo({ top: 0, behavior: 'auto' });
    };
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  function navigate(hash) {
    if (window.location.hash === hash) {
      window.scrollTo({ top: 0, behavior: 'smooth' });
    } else {
      window.location.hash = hash;
    }
  }

  const openArticle = route.kind === 'article' ? t.articles[route.id] : null;
  const openTopic = route.kind === 'topic' ? t.topics.find(tp => tp.id === route.id) : null;

  const filtered = useMemo(() => {
    return t.articles.filter(a => {
      if (activeTopic !== 'all' && a.tag !== activeTopic) return false;
      if (search.trim()) {
        const q = search.toLowerCase();
        if (!a.title.toLowerCase().includes(q) && !a.dek.toLowerCase().includes(q) && !a.cat.toLowerCase().includes(q)) return false;
      }
      return true;
    });
  }, [activeTopic, search, t]);

  const featured = filtered.find(a => a.featured) || filtered[0];
  const rest = filtered.filter(a => a !== featured);

  return (
    <div className="cp-app" lang={t.locale}>
      <style>{cssText}</style>

      {/* Top utility bar */}
      <div className="utility">
        <div className="utility-inner">
          <span className="util-mono">{t.edition}</span>
          <span className="util-right">
            <span className="util-mono">{t.updated}</span>
            <span className="util-dot">•</span>
            <span className="util-mono util-notice">{t.notice}</span>
          </span>
        </div>
      </div>

      {/* Header */}
      <header className="site-header">
        <div className="header-inner">
          <a href="#" className="brand-link" aria-label="Canadian Portal">
            <CPLogo size={40}/>
          </a>
          <nav className="primary-nav" aria-label="Primary">
            {t.nav.map(item => (
              <a key={item.label} href={item.href}>{item.label}</a>
            ))}
          </nav>
          <div className="header-actions">
            <div className="lang-switch" role="group" aria-label="Language">
              <button className={lang==='fr'?'is-on':''} onClick={()=>setLang('fr')}>FR</button>
              <span className="lang-sep"/>
              <button className={lang==='en'?'is-on':''} onClick={()=>setLang('en')}>EN</button>
            </div>
            <button className="icon-btn" aria-label="Search" onClick={() => setShowSearch(s => !s)}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
            </button>
            <a className="cta-link" href="#newsletter">{t.subscribe}</a>
            <button className="hamburger" aria-label="Menu" onClick={() => setNavOpen(o => !o)}>
              <span/><span/><span/>
            </button>
          </div>
        </div>
        {showSearch && (
          <div className="search-drawer">
            <div className="search-inner">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
              <input
                autoFocus
                placeholder={t.searchPlaceholder}
                value={search}
                onChange={e => setSearch(e.target.value)}
              />
              <button className="search-close" onClick={() => { setSearch(''); setShowSearch(false); }} aria-label="Close">×</button>
            </div>
          </div>
        )}
        {navOpen && (
          <div className="mobile-nav">
            {t.nav.map(item => (
              <a key={item.label} href={item.href} onClick={() => setNavOpen(false)}>{item.label}</a>
            ))}
          </div>
        )}
      </header>

      {/* News Ticker (red band, under menu) */}
      <div className="news-ticker" aria-label="Live news">
        <div className="ticker-label">
          <span className="ticker-dot"/>
          <span>{t.live}</span>
        </div>
        <div className="ticker-track">
          <div className="ticker-content">
            {t.ticker.map((it, i) => (
              <span className="ticker-item" key={i}>
                <span className="ticker-tag">{it.tag}</span>
                <span className="ticker-text">{it.text}</span>
                <span className="ticker-sep">◆</span>
              </span>
            ))}
            {t.ticker.map((it, i) => (
              <span className="ticker-item" key={'b'+i} aria-hidden="true">
                <span className="ticker-tag">{it.tag}</span>
                <span className="ticker-text">{it.text}</span>
                <span className="ticker-sep">◆</span>
              </span>
            ))}
          </div>
        </div>
      </div>

      {/* Hero */}
      {route.kind === 'home' && (<>
      <section className="hero">
        <div className="hero-grid">
          <div className="hero-text">
            <p className="eyebrow"><span className="eyebrow-dot"/>{t.eyebrow}</p>
            <h1 className="hero-title">
              {t.heroTitleA} <em>{t.heroTitleEm}</em> <br/>{t.heroTitleB} <span className="hero-underline">{t.heroTitleC}</span>
            </h1>
            <p className="hero-dek">{t.heroDek}</p>
            <div className="hero-actions">
              <a href="#topics" className="btn-primary">
                {t.ctaExplore}
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
              </a>
              <a href="#latest" className="btn-ghost">{t.ctaRead}</a>
            </div>
            <ul className="hero-meta">
              <li><strong>92</strong><span>{t.metaGuides}</span></li>
              <li><strong>13</strong><span>{t.metaProvinces}</span></li>
              <li><strong>2026</strong><span>{t.metaEdition}</span></li>
            </ul>
          </div>
          <aside className="hero-card" onClick={() => navigate(slugArticle(t.articles.indexOf(featured)))} style={{cursor:'pointer'}}>
            <div className="hero-card-art">
              <ArticleArt kind="chart" large/>
            </div>
            <div className="hero-card-body">
              <div className="hero-card-top">
                <span className="card-mono">{t.cardKicker}</span>
                <span className="card-dot"/>
              </div>
              <h3 className="hero-card-title">{t.cardTitleA},<br/>{t.cardTitleB}</h3>
              <p className="hero-card-dek">{t.cardDek}</p>
              <div className="hero-card-foot">
                <span className="card-mono">{t.cardReadTime}</span>
                <span className="card-cta">{t.cardCta}</span>
              </div>
            </div>
          </aside>
        </div>
      </section>

      {/* Topics */}
      <section className="topics" id="topics">
        <div className="section-head">
          <span className="section-mono">{t.sec1Mono}</span>
          <h2>{t.sec1Title}</h2>
          <p>{t.sec1Lead}</p>
        </div>
        <div className="topic-grid">
          {t.topics.map((tp) => (
            <a key={tp.id} href={slugTopic(tp.id)} className="topic-card">
              <div className="topic-num">{tp.kicker}</div>
              <h3 className="topic-title">{tp.title}</h3>
              <p className="topic-desc">{tp.desc}</p>
              <div className="topic-foot">
                <span className="topic-count">{lang==='fr'?'Voir la rubrique':'View section'}</span>
                <span className="topic-arrow">→</span>
              </div>
            </a>
          ))}
        </div>
      </section>

      {/* Latest */}
      <section className="latest" id="latest">
        <div className="section-head">
          <span className="section-mono">{t.sec2Mono}</span>
          <h2>{t.sec2Title}</h2>
          <p>{t.sec2Lead}</p>
        </div>

        <div className="filter-bar">
          <div className="filter-tabs" role="tablist">
            <button
              className={`filter-tab ${activeTopic === 'all' ? 'is-active' : ''}`}
              onClick={() => setActiveTopic('all')}
            >
              {t.all}
            </button>
            {t.topics.map(tp => {
              const n = t.articles.filter(a => a.tag === tp.id).length;
              if (!n) return null;
              return (
                <button
                  key={tp.id}
                  className={`filter-tab ${activeTopic === tp.id ? 'is-active' : ''}`}
                  onClick={() => setActiveTopic(tp.id)}
                >
                  {tp.label}
                </button>
              );
            })}
          </div>
          <div className="filter-meta">
          </div>
        </div>

        {featured && (
          <a href={slugArticle(t.articles.indexOf(featured))} className="featured-card">
            <div className="featured-art">
              <ArticleArt kind={featured.img} large/>
            </div>
            <div className="featured-body">
              <div className="featured-meta">
                <span className="badge">{featured.cat}</span>
                <span className="meta-sep">/</span>
                <span className="card-mono">{featured.date}</span>
                <span className="meta-sep">/</span>
                <span className="card-mono">{featured.readTime} {t.minRead}</span>
              </div>
              <h3 className="featured-title">{featured.title}</h3>
              <p className="featured-dek">{featured.dek}</p>
              <span className="featured-cta">{t.fullArticle}</span>
            </div>
          </a>
        )}

        <div className={`article-grid ${tweaks.density === 'compact' ? 'is-compact' : ''}`}>
          {rest.map((a, i) => (
            <a key={i} href={slugArticle(t.articles.indexOf(a))} className="article-card">
              <div className="card-art">
                <ArticleArt kind={a.img}/>
              </div>
              <div className="card-body">
                <div className="card-meta">
                  <span className="badge badge-sm">{a.cat}</span>
                  <span className="card-mono">{a.readTime} {t.minRead}</span>
                </div>
                <h4 className="card-title">{a.title}</h4>
                <p className="card-dek">{a.dek}</p>
                <div className="card-foot">
                  <span className="card-mono">{a.date}</span>
                  <span className="card-arrow">→</span>
                </div>
              </div>
            </a>
          ))}
        </div>

        {filtered.length === 0 && (
          <div className="empty-state">
            <p>{t.emptyTitle}</p>
            <button className="btn-ghost" onClick={() => { setActiveTopic('all'); setSearch(''); }}>{t.emptyCta}</button>
          </div>
        )}
      </section>

      {/* Pull quote */}
      <section className="pull">
        <div className="pull-inner">
          <span className="quote-mark">“</span>
          <p>
            {t.pull} <span className="quote-accent">{t.pullAccent}</span>
          </p>
          <span className="pull-credit">· {t.pullCredit}</span>
        </div>
      </section>

      {/* Newsletter */}
      <section className="newsletter" id="newsletter">
        <div className="newsletter-grid">
          <div>
            <span className="section-mono">{t.sec3Mono}</span>
            <h2>{t.sec3Title}</h2>
            <p>{t.sec3Lead}</p>
          </div>
          <form className="newsletter-form" onSubmit={e => e.preventDefault()}>
            <label htmlFor="nl-email" className="card-mono">{t.nlLabel}</label>
            <div className="newsletter-input-row">
              <input id="nl-email" type="email" placeholder={t.nlPlaceholder} required/>
              <button type="submit" className="btn-on-red">{t.subscribe}</button>
            </div>
            <p className="newsletter-fine">
              {t.nlFine1} <a href="#">{t.nlFineLink}</a>.
            </p>
          </form>
        </div>
      </section>
      </>)}

      {/* Article page */}
      {route.kind === 'article' && openArticle && (
        <ArticlePage
          article={openArticle}
          articleIdx={route.id}
          allArticles={t.articles}
          t={t} lang={lang}
          ArticleArt={ArticleArt}
          navigate={navigate}
        />
      )}

      {/* Topic page */}
      {route.kind === 'topic' && openTopic && (
        <TopicPage
          topic={openTopic}
          allArticles={t.articles}
          t={t} lang={lang}
          ArticleArt={ArticleArt}
          navigate={navigate}
        />
      )}

      {/* Static pages (About / Privacy / Contact) */}
      {route.kind === 'page' && (
        <StaticPage pageId={route.id} lang={lang} navigate={navigate}/>
      )}

      {/* Footer */}
      <footer className="site-footer">
        <div className="footer-grid">
          <div className="footer-brand">
            <CPLogo size={42}/>
            <p>{t.footerBrand}</p>
            <p className="footer-notice">{t.footerNotice}</p>
          </div>
          <div>
            <strong>{t.fSections}</strong>
            {t.topics.slice(0,5).map(tp => <a key={tp.id} href={`#${tp.id}`}>{tp.label}</a>)}
          </div>
          <div>
            <strong>{t.fAbout}</strong>
            <a href="#page/about">{t.fAboutLinks[0]}</a>
            <a href="#page/contact">{t.fAboutLinks[1]}</a>
            <a href="#page/about">{t.fAboutLinks[2]}</a>
            <a href="#page/privacy">{t.fAboutLinks[3]}</a>
            <a href="#page/privacy">{t.fAboutLinks[4]}</a>
          </div>
          <div>
            <strong>{t.fFollow}</strong>
            {t.fFollowLinks.map(l => <a key={l} href="#">{l}</a>)}
          </div>
        </div>
        <div className="footer-bottom">
          <span className="card-mono">{t.fCopy}</span>
          <span className="card-mono">{t.fMade}</span>
        </div>
      </footer>

      {openArticle && false && (
        <ArticleDetail
          article={openArticle}
          allArticles={t.articles}
          t={t} lang={lang}
          onClose={() => setOpenArticle(null)}
          onOpenArticle={(a) => setOpenArticle(a)}
          ArticleArt={ArticleArt}
        />
      )}
      {openTopic && !openArticle && false && (
        <TopicDetail
          topic={openTopic}
          allArticles={t.articles}
          t={t} lang={lang}
          onClose={() => setOpenTopic(null)}
          onOpenArticle={(a) => setOpenArticle(a)}
          ArticleArt={ArticleArt}
        />
      )}

      {/* Tweaks */}
      <TweaksPanel title="Tweaks">
        <TweakSection title="Palette">
          <TweakRadio
            label="Tonalité"
            value={tweaks.palette}
            onChange={v => setTweak('palette', v)}
            options={[
              { value: 'warm', label: 'Chaud' },
              { value: 'cool', label: 'Froid' },
              { value: 'paper', label: 'Papier' },
              { value: 'dark', label: 'Sombre' },
            ]}
          />
        </TweakSection>
        <TweakSection title="Accent">
          <TweakColor
            label="Couleur d’accent"
            value={tweaks.accentHue}
            onChange={v => setTweak('accentHue', v)}
            options={[
              { value: 28,  color: 'oklch(0.56 0.22 28)',  label: 'Rouge Canada' },
              { value: 28,  color: 'oklch(0.50 0.16 28)',  label: 'Rouge vif' },
              { value: 50,  color: 'oklch(0.55 0.13 50)',  label: 'Brique' },
              { value: 230, color: 'oklch(0.46 0.12 230)', label: 'Atlantique' },
            ]}
          />
        </TweakSection>
        <TweakSection title="Densité">
          <TweakRadio
            label="Grille"
            value={tweaks.density}
            onChange={v => setTweak('density', v)}
            options={[
              { value: 'comfortable', label: 'Confortable' },
              { value: 'compact', label: 'Compacte' },
            ]}
          />
        </TweakSection>
      </TweaksPanel>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// CSS
// ─────────────────────────────────────────────────────────────────────────────
const cssText = `
.cp-app { color: var(--ink); background: var(--bg); min-height: 100vh; }
.cp-app a { color: inherit; text-decoration: none; }
.cp-app button { font: inherit; cursor: pointer; }

/* News Ticker */
.news-ticker {
  background: var(--accent);
  color: white;
  display: flex; align-items: stretch;
  border-bottom: 2px solid var(--accent-deep);
  overflow: hidden; position: relative;
  height: 44px;
}
.ticker-label {
  background: var(--accent-deep);
  color: white;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 0 18px;
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.16em;
  flex-shrink: 0; position: relative; z-index: 2;
  box-shadow: 6px 0 12px -6px rgba(0,0,0,0.4);
}
.ticker-dot {
  width: 8px; height: 8px; background: white; border-radius: 50%;
  animation: tickerPulse 1.2s infinite;
}
@keyframes tickerPulse {
  0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.8); }
  100% { box-shadow: 0 0 0 10px rgba(255,255,255,0); }
}
.ticker-track {
  flex: 1; overflow: hidden; display: flex; align-items: center;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%);
}
.ticker-content {
  display: inline-flex; align-items: center; white-space: nowrap;
  animation: ticker 90s linear infinite; padding-left: 32px;
}
.news-ticker:hover .ticker-content { animation-play-state: paused; }
@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.ticker-item { display: inline-flex; align-items: center; gap: 12px; font-size: 13px; font-weight: 500; }
.ticker-tag {
  font-family: var(--mono); font-size: 10.5px; font-weight: 700;
  background: rgba(255,255,255,0.18); color: white;
  padding: 3px 8px; border-radius: 3px; letter-spacing: 0.1em;
}
.ticker-text { color: rgba(255,255,255,0.96); }
.ticker-sep { color: rgba(255,255,255,0.45); margin: 0 18px; font-size: 10px; }

/* Utility bar */
.utility {
  background: var(--accent-deep);
  color: white;
  border-bottom: 1px solid var(--accent-deep);
}
.utility-inner {
  max-width: 1280px; margin: 0 auto; padding: 8px 32px;
  display: flex; align-items: center; justify-content: space-between;
  font-size: 11px; letter-spacing: 0.08em;
}
.util-mono { font-family: var(--mono); color: rgba(255,255,255,0.92); }
.util-right { display: flex; align-items: center; gap: 12px; }
.util-dot { opacity: 0.4; }
.util-notice { opacity: 0.7; }

/* Header */
.site-header {
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.header-inner {
  max-width: 1280px; margin: 0 auto; padding: 18px 32px;
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 32px;
}
.brand-link { display: inline-flex; }
.primary-nav { display: flex; gap: 28px; justify-content: center; flex-wrap: wrap; }
.primary-nav a {
  font-size: 14px; font-weight: 500; color: var(--ink-2);
  position: relative; padding: 4px 0; transition: color .15s;
}
.primary-nav a:hover { color: var(--accent); }
.primary-nav a::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: -2px;
  height: 1px; background: var(--accent); transition: right .2s ease;
}
.primary-nav a:hover::after { right: 0; }

.header-actions { display: flex; align-items: center; gap: 14px; }
.lang-switch {
  display: inline-flex; align-items: center; border: 1px solid var(--line-2);
  border-radius: 999px; padding: 2px; gap: 0;
}
.lang-switch button {
  border: 0; background: transparent; padding: 6px 12px; font-size: 12px;
  font-family: var(--mono); font-weight: 600; letter-spacing: 0.08em;
  color: var(--ink-3); border-radius: 999px;
}
.lang-switch button.is-on { background: var(--ink); color: var(--bg); }
.lang-sep { display: none; }

.icon-btn {
  width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--line-2);
  background: transparent; color: var(--ink); display: inline-flex; align-items: center; justify-content: center;
  transition: all .15s;
}
.icon-btn:hover { background: var(--ink); color: var(--bg); border-color: var(--ink); }

.cta-link {
  font-size: 13px; font-weight: 600; padding: 9px 18px;
  border: 1px solid var(--accent); border-radius: 999px; background: var(--accent); color: white;
  transition: all .15s;
}
.cta-link:hover { background: var(--accent-deep); border-color: var(--accent-deep); }

.hamburger { display: none; background: transparent; border: 0; width: 32px; height: 32px; flex-direction: column; gap: 4px; align-items: center; justify-content: center; }
.hamburger span { width: 22px; height: 1.5px; background: var(--ink); display: block; }

.search-drawer { border-top: 1px solid var(--line); background: var(--bg-card); animation: drop .2s ease; }
.search-inner { max-width: 1280px; margin: 0 auto; padding: 18px 32px; display: flex; align-items: center; gap: 14px; }
.search-inner input {
  flex: 1; border: 0; background: transparent; outline: none;
  font-family: var(--serif); font-size: 22px; color: var(--ink);
}
.search-inner input::placeholder { color: var(--ink-3); font-style: italic; }
.search-close { border: 1px solid var(--line-2); background: transparent; color: var(--ink-3); width: 32px; height: 32px; border-radius: 50%; font-size: 20px; line-height: 1; }
.mobile-nav { display: none; padding: 16px 32px; flex-direction: column; gap: 14px; border-top: 1px solid var(--line); }
.mobile-nav a { font-size: 16px; font-weight: 500; }
@keyframes drop { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }

/* Hero */
.hero { max-width: 1280px; margin: 0 auto; padding: 64px 32px 48px; border-bottom: 1px solid var(--line); }
.hero-grid { display: grid; grid-template-columns: 1.25fr 1fr; gap: 56px; align-items: stretch; }
.eyebrow {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em;
  color: var(--ink-3); text-transform: uppercase; margin: 0 0 24px;
}
.eyebrow-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); display: inline-block; }
.hero-title {
  font-family: var(--serif); font-weight: 500; letter-spacing: -0.02em;
  font-size: clamp(48px, 7vw, 96px); line-height: 0.96; margin: 0 0 28px;
  color: var(--ink); text-wrap: pretty;
}
.hero-title em { font-style: italic; font-weight: 400; color: var(--accent-deep); }
.hero-underline { position: relative; white-space: nowrap; }
.hero-underline::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 8px; height: 12px;
  background: var(--accent-soft); z-index: -1; border-radius: 2px;
}
.hero-dek { font-size: 19px; line-height: 1.55; color: var(--ink-2); max-width: 56ch; margin: 0 0 36px; text-wrap: pretty; }
.hero-actions { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 56px; }
.btn-primary {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 14px 26px; background: var(--accent); color: white;
  border: 1px solid var(--accent); border-radius: 999px; font-weight: 600; font-size: 14px;
  transition: transform .15s, background .15s, box-shadow .15s;
  box-shadow: 0 6px 20px -8px var(--accent);
}
.btn-primary:hover { background: var(--accent-deep); border-color: var(--accent-deep); transform: translateY(-1px); }
.btn-ghost {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 14px 24px; background: transparent; color: var(--ink);
  border: 1px solid var(--line-2); border-radius: 999px; font-weight: 500; font-size: 14px;
  transition: all .15s;
}
.btn-ghost:hover { border-color: var(--ink); }
.hero-meta {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px;
  list-style: none; padding: 24px 0 0; margin: 0; border-top: 1px solid var(--line);
}
.hero-meta li { display: flex; flex-direction: column; gap: 4px; }
.hero-meta strong { font-family: var(--serif); font-size: 36px; font-weight: 500; color: var(--ink); letter-spacing: -0.02em; }
.hero-meta span { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); }

.hero-card {
  border: 1px solid var(--line-2); border-radius: 6px;
  background: var(--bg-card); display: flex; flex-direction: column;
  position: relative; overflow: hidden;
}
.hero-card-art { width: 100%; }
.hero-card-art > div { aspect-ratio: 4 / 3; border-radius: 0; border: 0; border-bottom: 1px solid var(--line); }
.hero-card-body { padding: 28px 28px 26px; display: flex; flex-direction: column; flex: 1; }
.hero-card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
.card-mono { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.12em; color: var(--ink-3); text-transform: uppercase; }
.card-dot { width: 8px; height: 8px; background: var(--accent); border-radius: 50%; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
.hero-card-title { font-family: var(--serif); font-size: 28px; line-height: 1.15; margin: 0 0 16px; font-weight: 500; letter-spacing: -0.01em; }
.hero-card-dek { color: var(--ink-2); font-size: 15px; line-height: 1.55; margin: 0 0 20px; }
.hero-card-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 18px; margin-top: auto; border-top: 1px solid var(--line); }
.card-cta { font-weight: 600; font-size: 13px; color: var(--accent); }

/* Sections */
.topics, .latest, .pull, .newsletter { max-width: 1280px; margin: 0 auto; padding: 80px 32px; }
.section-head { max-width: 720px; margin: 0 auto 48px; text-align: center; }
.section-mono { font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; color: var(--ink-3); display: block; margin-bottom: 14px; }
.section-head h2 { font-family: var(--serif); font-size: clamp(36px, 4.5vw, 56px); font-weight: 500; margin: 0 0 18px; letter-spacing: -0.02em; line-height: 1.05; }
.section-head p { color: var(--ink-2); font-size: 17px; line-height: 1.55; margin: 0; }

/* Topics */
.topic-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.topic-card { background: var(--bg); padding: 32px 28px; min-height: 220px; display: flex; flex-direction: column; gap: 12px; transition: background .2s; position: relative; }
.topic-card:hover { background: var(--bg-card); }
.topic-card:hover .topic-arrow { transform: translateX(4px); color: var(--accent); }
.topic-num { font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; color: var(--ink-3); }
.topic-card:nth-child(1) .topic-num,
.topic-card:nth-child(7) .topic-num { color: var(--accent); }
.topic-title { font-family: var(--serif); font-size: 22px; font-weight: 500; margin: 0; letter-spacing: -0.01em; line-height: 1.2; }
.topic-desc { font-size: 14px; line-height: 1.55; color: var(--ink-2); margin: 0; flex: 1; }
.topic-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 16px; border-top: 1px solid var(--line); font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; color: var(--ink-3); text-transform: uppercase; }
.topic-arrow { font-size: 18px; transition: transform .2s, color .2s; }

/* Filter bar */
.filter-bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-bottom: 36px; padding-bottom: 18px; border-bottom: 1px solid var(--line); }
.filter-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
.filter-tab { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid var(--line-2); border-radius: 999px; background: transparent; color: var(--ink-2); font-size: 13px; font-weight: 500; transition: all .15s; }
.filter-tab:hover { border-color: var(--ink); color: var(--ink); }
.filter-tab.is-active { background: var(--accent); color: white; border-color: var(--accent); }
.tab-count { font-family: var(--mono); font-size: 10.5px; opacity: 0.7; }

/* Featured */
.featured-card { display: grid; grid-template-columns: 1.25fr 1fr; gap: 0; border: 1px solid var(--line-2); border-radius: 6px; overflow: hidden; background: var(--bg-card); margin-bottom: 60px; transition: transform .2s, box-shadow .2s; }
.featured-card:hover { transform: translateY(-2px); box-shadow: 0 18px 48px -28px rgba(0,0,0,0.35); }
.featured-art { background: var(--bg); border-right: 1px solid var(--line); }
.featured-art > div { aspect-ratio: 4/3; height: 100%; border: 0; border-radius: 0; }
.featured-body { padding: 48px 48px 44px; display: flex; flex-direction: column; gap: 18px; }
.featured-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.badge { display: inline-block; padding: 4px 10px; background: var(--accent-soft); color: var(--accent-deep); font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; border-radius: 3px; }
.badge-sm { padding: 3px 8px; font-size: 10px; }
.meta-sep { color: var(--ink-3); }
.featured-title { font-family: var(--serif); font-size: clamp(28px, 3vw, 42px); margin: 0; line-height: 1.1; font-weight: 500; letter-spacing: -0.01em; text-wrap: balance; }
.featured-dek { font-size: 17px; line-height: 1.6; color: var(--ink-2); margin: 0; max-width: 50ch; }
.featured-cta { font-weight: 600; color: var(--accent); margin-top: auto; padding-top: 18px; border-top: 1px solid var(--line); }

/* Article grid — bigger cards */
.article-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 56px 36px; }
.article-grid.is-compact { grid-template-columns: repeat(3, 1fr); gap: 44px 28px; }
.article-card { display: flex; flex-direction: column; gap: 20px; transition: transform .15s; }
.article-card:hover { transform: translateY(-3px); }
.article-card:hover .card-title { color: var(--accent); }
.card-art { overflow: hidden; border-radius: 8px; }
.card-art > div { transition: transform .4s; aspect-ratio: 4/3; }
.is-compact .card-art > div { aspect-ratio: 4/3; }
.article-card:hover .card-art > div { transform: scale(1.03); }
.card-body { display: flex; flex-direction: column; gap: 12px; }
.card-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.card-title { font-family: var(--serif); font-size: 26px; line-height: 1.18; margin: 0; font-weight: 500; letter-spacing: -0.01em; transition: color .2s; text-wrap: balance; }
.is-compact .card-title { font-size: 22px; }
.card-dek { color: var(--ink-2); font-size: 16px; line-height: 1.6; margin: 0; text-wrap: pretty; }
.is-compact .card-dek { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.card-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 14px; margin-top: auto; border-top: 1px solid var(--line); }
.card-arrow { color: var(--ink-3); font-size: 18px; transition: transform .2s, color .2s; }
.article-card:hover .card-arrow { transform: translateX(4px); color: var(--accent); }

.empty-state { text-align: center; padding: 60px 0; color: var(--ink-3); }
.empty-state p { margin: 0 0 18px; font-family: var(--serif); font-size: 22px; }

/* Pull quote */
.pull { padding: 60px 32px; }
.pull-inner { max-width: 880px; margin: 0 auto; text-align: center; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: 60px 0; position: relative; }
.quote-mark { font-family: var(--serif); font-size: 120px; line-height: 0.6; color: var(--accent); display: block; margin-bottom: -20px; }
.pull-inner p { font-family: var(--serif); font-size: clamp(28px, 3.6vw, 44px); line-height: 1.2; margin: 0 auto 24px; max-width: 26ch; font-weight: 500; letter-spacing: -0.01em; }
.pull-inner em { font-style: italic; color: var(--accent-deep); }
.quote-accent { background: linear-gradient(transparent 65%, var(--accent-soft) 65%); padding: 0 4px; }
.pull-credit { font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; color: var(--ink-3); }

/* Newsletter — softened red, white CTA for contrast */
.newsletter { background: var(--accent); color: white; max-width: none; padding: 80px 0; margin: 0; position: relative; overflow: hidden; }
.newsletter::before { content: ''; position: absolute; inset: 0; background: radial-gradient(circle at 80% 20%, var(--accent-deep) 0%, transparent 60%); opacity: 0.55; pointer-events: none; }
.newsletter-grid { max-width: 1280px; margin: 0 auto; padding: 0 32px; display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; position: relative; z-index: 1; }
.newsletter .section-mono { color: rgba(255,255,255,0.6); }
.newsletter h2 { font-family: var(--serif); font-size: clamp(36px, 4.5vw, 56px); font-weight: 500; margin: 14px 0 18px; letter-spacing: -0.02em; line-height: 1.05; color: white; }
.newsletter p { color: rgba(255,255,255,0.82); font-size: 17px; line-height: 1.55; margin: 0; }
.newsletter-form { display: flex; flex-direction: column; gap: 12px; }
.newsletter-form label { color: rgba(255,255,255,0.6); }
.newsletter-input-row { display: flex; gap: 10px; }
.newsletter-input-row input { flex: 1; padding: 16px 20px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.08); color: white; border-radius: 999px; font: inherit; font-size: 15px; outline: none; }
.newsletter-input-row input::placeholder { color: rgba(255,255,255,0.5); }
.newsletter-input-row input:focus { border-color: white; background: rgba(255,255,255,0.14); }
/* button on red — white background, accent text, never black */
.btn-on-red { padding: 14px 26px; background: white; color: var(--accent-deep); border: 1px solid white; border-radius: 999px; font-weight: 700; font-size: 14px; transition: transform .15s, background .15s, color .15s; }
.btn-on-red:hover { background: var(--accent-deep); color: white; border-color: var(--accent-deep); transform: translateY(-1px); }
.newsletter-fine { font-size: 12.5px; color: rgba(255,255,255,0.6) !important; }
.newsletter-fine a { color: white; text-decoration: underline; }

/* Footer */
.site-footer { border-top: 1px solid var(--line); background: var(--bg-card); padding: 80px 32px 40px; }
.footer-grid { max-width: 1280px; margin: 0 auto; display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 48px; }
.footer-grid > div { display: flex; flex-direction: column; gap: 10px; }
.footer-grid strong { font-family: var(--serif); font-size: 17px; margin-bottom: 6px; font-weight: 600; }
.footer-grid a { font-size: 14px; color: var(--ink-2); }
.footer-grid a:hover { color: var(--accent); }
.footer-brand p { font-size: 14px; color: var(--ink-2); line-height: 1.55; margin: 16px 0 0; max-width: 42ch; }
.footer-notice { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; color: var(--ink-3); text-transform: uppercase; }
.footer-bottom { max-width: 1280px; margin: 60px auto 0; padding-top: 24px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 16px; }

/* Article & Topic Pages (real inline pages) */
.article-page, .topic-page { background: var(--bg); padding: 32px 0 0; }
.page-inner { max-width: 920px; margin: 0 auto; padding: 0 32px 80px; }
.crumbs { display: flex; align-items: center; gap: 10px; font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; color: var(--ink-3); text-transform: uppercase; margin: 0 0 36px; flex-wrap: wrap; }
.crumbs a { color: var(--ink-3); }
.crumbs a:hover { color: var(--accent); }
.crumb-sep { opacity: 0.5; }
.crumb-current { color: var(--ink); }
.article-head { margin-bottom: 28px; }
.detail-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 22px; }
.article-title { font-family: var(--serif); font-size: clamp(36px, 5vw, 56px); line-height: 1.05; margin: 0 0 22px; font-weight: 500; letter-spacing: -0.02em; text-wrap: balance; }
.article-dek { font-family: var(--serif); font-size: 22px; line-height: 1.5; color: var(--ink-2); margin: 0; font-style: italic; max-width: 60ch; text-wrap: pretty; }
.article-hero { margin: 36px 0 44px; }
.article-hero > div { aspect-ratio: 16/9; border-radius: 6px; }
.article-body { max-width: 64ch; margin: 0 auto; }
.detail-lede { font-size: 20px; line-height: 1.6; color: var(--ink); font-weight: 500; margin: 0 0 32px; padding-bottom: 28px; border-bottom: 1px solid var(--line); }
.article-body h2 { font-family: var(--serif); font-size: 30px; font-weight: 500; letter-spacing: -0.01em; margin: 40px 0 18px; line-height: 1.2; }
.article-body p { font-size: 17.5px; line-height: 1.75; color: var(--ink-2); margin: 0 0 22px; }
.detail-quote { font-family: var(--serif); font-size: 24px; line-height: 1.4; color: var(--accent-deep); border-left: 3px solid var(--accent); padding: 10px 0 10px 26px; margin: 36px 0; font-style: italic; }
.detail-list { padding-left: 24px; margin: 22px 0 28px; }
.detail-list li { font-size: 16.5px; line-height: 1.65; color: var(--ink-2); margin-bottom: 12px; }
.detail-list li::marker { color: var(--accent); }
.article-related { margin-top: 72px; padding-top: 40px; border-top: 1px solid var(--line); }
.related-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 20px; }
.related-card { display: flex; flex-direction: column; gap: 10px; transition: transform .15s; }
.related-card:hover { transform: translateY(-2px); }
.related-card:hover .related-title { color: var(--accent); }
.related-art { overflow: hidden; border-radius: 4px; }
.related-art > div { aspect-ratio: 3/2; }
.related-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.related-title { font-family: var(--serif); font-size: 17px; line-height: 1.25; margin: 0; font-weight: 500; transition: color .2s; text-wrap: balance; }

.topic-page-head { margin-bottom: 56px; padding-bottom: 36px; border-bottom: 1px solid var(--line); }
.topic-page-head .article-dek { font-style: normal; font-family: var(--sans); font-size: 19px; }
.topic-stats { margin-top: 20px; }
.topic-page-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 44px 32px; }

.back-bar { margin-top: 72px; padding-top: 32px; border-top: 1px solid var(--line); display: flex; justify-content: flex-start; }

@media (max-width: 720px) {
  .page-inner { padding: 0 20px 60px; }
  .related-grid { grid-template-columns: 1fr; }
  .topic-page-grid { grid-template-columns: 1fr; }
}

/* Responsive */
@media (max-width: 1024px) {
  .topic-grid { grid-template-columns: repeat(2, 1fr); }
  .article-grid, .article-grid.is-compact { grid-template-columns: repeat(2, 1fr); }
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .hero-grid { grid-template-columns: 1fr; }
  .featured-card { grid-template-columns: 1fr; }
  .newsletter-grid { grid-template-columns: 1fr; gap: 32px; }
  .primary-nav { display: none; }
  .hamburger { display: inline-flex; }
  .mobile-nav { display: flex; }
}
@media (max-width: 600px) {
  .header-inner, .utility-inner { padding-left: 20px; padding-right: 20px; }
  .hero, .topics, .latest, .pull { padding-left: 20px; padding-right: 20px; }
  .hero { padding-top: 40px; }
  .topic-grid { grid-template-columns: 1fr; }
  .article-grid, .article-grid.is-compact { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr; }
  .hero-meta { grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
  .featured-body { padding: 28px; }
  .util-right { display: none; }
  .cta-link { display: none; }
}
`;

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
