// Blog — index + single-post reading view
// Posts are mocked; each is operator-focused (playbook | case study | team note).

const POSTS = [
  {
    slug: 'cost-per-converted-user',
    cat: 'Playbook',
    title: 'Why we measure cost-per-converted-user, not cost-per-install',
    dek: 'Installs are vanity. The number that pays the bills is the user who actually does the thing. Here is how we re-wire every funnel around the conversion that matters — and what it costs us to learn.',
    cover: 'assets/chipsy.png',
    coverObject: 'top',
    coverBg: '#0c0e15',
    author: 'IVO CADENAS',
    authorRole: 'Head of Growth',
    authorPhoto: 'assets/ivo.png',
    date: 'APR 18, 2026',
    read: 7,
    tags: ['GROWTH', 'FUNNEL', 'METRICS'],
    featured: true,
    body: [
      { kind: 'p', text: 'Most Telegram-miniapp decks lead with installs. We stopped doing that two clients ago. The number that actually moves the P&L is cost-per-converted-user — whatever conversion the client cares about — and once you point the funnel at it, every other decision gets simpler.' },
      { kind: 'h2', text: 'Installs are vanity, conversions are bookings' },
      { kind: 'p', text: 'When our first operator launched, we shipped with the install-per-dollar dashboard most agencies use. The chart looked great. The P&L didn\'t. The gap between an install and a paying, retained user was a 17-step funnel we had not modeled — and we were optimizing the cheapest step instead of the one that paid us.' },
      { kind: 'pull', text: 'A miniapp is not a download. It is a converted user.' },
      { kind: 'h2', text: 'What we rebuilt' },
      { kind: 'p', text: 'We collapsed the funnel from "install → register → convert" into a single dashboard. Every cohort gets one number stamped on it: $-cost per qualified conversion. Below the target, we scale. Above, we stop. No middle.' },
      { kind: 'p', text: '\n\nOn our first operator, that single rule got us to a low cost-per-registration and a strong cost-per-conversion at volume. On the second, it cut wasted spend by ~40% in the first 6 weeks because we killed creative that was driving cheap registrations who never converted.' },
      { kind: 'h2', text: 'The three metrics that matter' },
      { kind: 'list', items: [
        '$ / Reg — top-of-funnel sanity check, but never the goal.',
        '$ / Converted user — the only number that matters to clients.',
        'D7 retention of the converted cohort — proves we did not buy a one-and-done wonder.',
      ]},
      { kind: 'p', text: 'If you cannot get the unit economics to work on a Telegram miniapp, you do not have a creative problem. You have a funnel problem. Fix the funnel first.' },
    ],
  },
  {
    slug: 'incaverse-s2',
    cat: 'Case study',
    title: 'Incaverse S2: shipping a narrative miniapp at 170k registered',
    dek: 'Season 2 — Chinchaysuyu — went from prototype to live in 11 weeks. Here is what we learned about building a Telegram-native game that people actually finish.',
    cover: 'assets/incaverse-hero.png',
    coverObject: 'center',
    coverBg: '#0c0e15',
    author: 'JUAN SANCHEZ',
    authorRole: 'CTO',
    authorPhoto: 'assets/juan.png',
    date: 'APR 11, 2026',
    read: 9,
    tags: ['INCAVERSE', 'WEB3', 'GAMING'],
    featured: true,
    body: [
      { kind: 'p', text: 'Season 1 of Incaverse was a Tap2Earn — funded by the Ministerio de Cultura of Spain, simple loop, fast onboarding. It hit 170k registered and 15k actives. Season 2 was a bet: could we keep that audience inside a narrative game?' },
      { kind: 'h2', text: 'The constraint: 11 weeks' },
      { kind: 'p', text: 'We had the Madrid-in-Game milestone fixed. 11 weeks from prototype to live. That meant cutting half the design surface area before we wrote a line of code.' },
      { kind: 'pull', text: 'Telegram is a chat client first. Every animation we shipped had to load before the keyboard does.' },
      { kind: 'h2', text: 'What we cut' },
      { kind: 'list', items: [
        'Open-world exploration → fixed-camera scenes.',
        'Real-time PvP → asynchronous match resolution.',
        'On-chain combat → off-chain with on-chain settlement only.',
      ]},
      { kind: 'p', text: 'These were not compromises. They were the right answers for the medium. A miniapp is opened in 4G in a taxi. If your hero shot does not render in 1.5s, you lose them to the chat above.' },
      { kind: 'h2', text: 'What we kept' },
      { kind: 'p', text: 'The narrative. The Quechua naming. The token holders cohort — currently 1,500 — kept their S1 progression as a soulbound badge. That single decision drove our D30 retention 22 points above industry benchmark.' },
    ],
  },
  {
    slug: 'design-system-dark',
    cat: 'Team note',
    title: 'Why our design system is dark by default (and stays that way)',
    dek: 'Operators run dashboards at 2am. Players open Telegram in bed. We do not have a light-mode problem because we never had a light-mode audience.',
    cover: 'assets/amelia.png',
    coverObject: 'top',
    coverBg: '#1a1428',
    author: 'AMELIA VERGARAY',
    authorRole: 'Front-end · UI/UX',
    authorPhoto: 'assets/amelia.png',
    date: 'APR 04, 2026',
    read: 4,
    tags: ['DESIGN', 'SYSTEM'],
    body: [
      { kind: 'p', text: 'Every quarter someone asks if we should ship a light theme. Every quarter the answer is the same: no, and here is the data.' },
      { kind: 'h2', text: 'When our users open us' },
      { kind: 'p', text: '64% of Telegram miniapp opens happen between 8pm and 2am local time. Operators on the dashboard side cluster between 9am and noon, then again from 9pm to midnight. Both audiences are in low-light environments more than half the time they are with us.' },
      { kind: 'pull', text: 'A bright UI on a phone in bed is a UI that gets closed.' },
      { kind: 'h2', text: 'The system, simply' },
      { kind: 'list', items: [
        'Five ink levels — ink-0 through ink-4 — covering page, surface, card, hover.',
        'One accent at a time. Gold by default, violet on Web3, green on growth.',
        'Mono for data, Space Grotesk for display, Inter for body. Three families, no exceptions.',
      ]},
      { kind: 'p', text: 'When you constrain the palette this hard, every color decision becomes obvious. That is the whole point of a system.' },
    ],
  },
  {
    slug: 'plump-launch',
    cat: 'Case study',
    title: 'Plump: the second operator on the playbook',
    dek: 'Compounding returns are the point. Here is how we moved from Chipsy to Plump in under 6 weeks — and why operator #3 will be faster.',
    cover: 'assets/plump.png',
    coverObject: 'top',
    coverBg: '#0c0e15',
    author: 'RAFAEL BONNELLY',
    authorRole: 'Founder · CEO',
    authorPhoto: 'assets/rafael-bonnelly.png',
    date: 'MAR 27, 2026',
    read: 5,
    tags: ['OPERATOR', 'LATAM', 'PLAYBOOK'],
    body: [
      { kind: 'p', text: 'Studios sell time. Playbooks sell leverage. The first operator we ship is a project. The second is a product.' },
      { kind: 'h2', text: 'What carried over' },
      { kind: 'p', text: 'From Chipsy to Plump we re-used: the funnel telemetry, the conversion attribution model, the LatAm payment integration, the keyboard-first onboarding flow, and the retention loop. That is roughly 70% of the codebase and 90% of the operations playbook.' },
      { kind: 'h2', text: 'What was new' },
      { kind: 'list', items: [
        'Brand. Plump is its own world — not a Chipsy reskin.',
        'Audience mix. Different cohorts, different content tiers.',
        'Acquisition channels. Different creator partners, different geos.',
      ]},
      { kind: 'pull', text: 'The studio is not the product. The playbook is.' },
      { kind: 'p', text: 'Client #3 is in scoping. We expect time-to-launch to be half of Plump\'s. That is the entire thesis.' },
    ],
  },
  {
    slug: 'spin-the-wheel',
    cat: 'Playbook',
    title: 'Building Spin the Wheel as a top-of-funnel engine',
    dek: 'A miniapp does not have to be a destination. The next thing we are shipping is a pipe — white-label, plugs into anything, designed to feed registrations.',
    cover: 'assets/incaverse.png',
    coverObject: 'top',
    coverBg: '#0c0e15',
    author: 'IVO CADENAS',
    authorRole: 'Head of Growth',
    authorPhoto: 'assets/ivo.png',
    date: 'MAR 19, 2026',
    read: 6,
    tags: ['SPIN', 'FUNNEL', 'WHITE-LABEL'],
    body: [
      { kind: 'p', text: 'We kept getting the same request: "Can you build us a top-of-funnel?" Clients did not need another full product — they needed a way to get a Telegram audience to pull a lever once. That is Spin the Wheel.' },
      { kind: 'h2', text: 'The shape' },
      { kind: 'p', text: 'One screen. One wheel. One CTA. The whole thing exists to register a user and hand them off — to a brand, a creator, a Web3 product, whatever the client points us at.' },
      { kind: 'pull', text: 'A funnel is not a feature. It is the entire product.' },
      { kind: 'h2', text: 'Why white-label' },
      { kind: 'list', items: [
        'Clients want their brand on it, not ours.',
        'Telemetry stays consistent — we own the funnel data across deploys.',
        'Each new operator is a 2-week deploy, not a 12-week build.',
      ]},
      { kind: 'p', text: 'Q2 2026 launch. If you run something that needs registrations and you have a Telegram audience, talk to us before then.' },
    ],
  },
  {
    slug: 'hiring-community',
    cat: 'Team note',
    title: 'How we hire for community in a remote, async studio',
    dek: 'Community is a craft. Rohit joined us because he had shipped one — twice — at miniapp scale. Here is what we look for when nobody is in the room.',
    cover: 'assets/rohit.png',
    coverObject: 'top',
    coverBg: '#1a1428',
    author: 'RAFAEL BONNELLY',
    authorRole: 'Founder · CEO',
    authorPhoto: 'assets/rafael-bonnelly.png',
    date: 'MAR 12, 2026',
    read: 4,
    tags: ['HIRING', 'TEAM'],
    body: [
      { kind: 'p', text: 'We are 7 people across 4 time zones. Hiring for community is the hardest role we have because the work happens at the edges — Telegram, Discord, X — not in the standup.' },
      { kind: 'h2', text: 'Three things we look for' },
      { kind: 'list', items: [
        'Has shipped a community to a measurable outcome (DAUs, retention, conversions).',
        'Writes the way our players talk. Not the way agencies do.',
        'Treats the bot inbox as a product surface, not a chore.',
      ]},
      { kind: 'pull', text: 'The community lead owns the loudest UI we have: the bot reply.' },
      { kind: 'h2', text: 'Why Rohit' },
      { kind: 'p', text: 'He had run two miniapp communities to 50k+ before us, and both retained above benchmark a year later. The interview was a 2-hour audit of what he would change about the Incaverse Telegram channel. He came back with 14 specific calls. We hired him the next day.' },
    ],
  },
];

window.__BLOG_POSTS = POSTS;

function BlogCard({ post, onOpen, size = 'md' }) {
  const big = size === 'lg';
  return (
    <Reveal>
      <a href="#" onClick={(e) => { e.preventDefault(); onOpen(post.slug); }} className="card card-hover blog-card" style={{
        padding: 0, overflow: 'hidden', textDecoration: 'none', color: 'inherit',
        display: 'flex', flexDirection: 'column', height: '100%',
      }}>
        <div style={{
          aspectRatio: big ? '16/9' : '4/3',
          background: post.coverBg || '#0c0e15',
          position: 'relative',
          overflow: 'hidden',
          borderBottom: '1px solid var(--line)',
        }}>
          <img src={post.cover} alt={post.title} style={{
            position: 'absolute', inset: 0, width: '100%', height: '100%',
            objectFit: 'cover', objectPosition: post.coverObject || 'top',
            opacity: 0.92,
          }}/>
          <div style={{
            position: 'absolute', inset: 0,
            background: 'linear-gradient(180deg, rgba(7,8,12,0) 40%, rgba(7,8,12,0.85) 100%)',
          }}/>
          <div style={{ position: 'absolute', top: 14, left: 14, display: 'flex', gap: 6 }}>
            <span className={`tag ${post.cat === 'Playbook' ? 'tag-flag' : post.cat === 'Case study' ? 'tag-live' : 'tag-dev'}`}>{post.cat.toUpperCase()}</span>
          </div>
          <div style={{ position: 'absolute', bottom: 14, right: 14 }}>
            <span className="mono" style={{ fontSize: 10, color: 'var(--text-2)', letterSpacing: '0.14em', background: 'rgba(7,8,12,0.7)', padding: '4px 8px', borderRadius: 4, border: '1px solid var(--line)' }}>{post.read} MIN READ</span>
          </div>
        </div>
        <div style={{ padding: big ? 32 : 22, display: 'flex', flexDirection: 'column', flex: 1 }}>
          <div className="mono" style={{ fontSize: 10, color: 'var(--text-3)', letterSpacing: '0.16em', textTransform: 'uppercase' }}>{post.date}</div>
          <h3 className="display" style={{
            fontSize: big ? 'clamp(28px, 3vw, 40px)' : 22,
            letterSpacing: '-0.02em',
            marginTop: 12,
            lineHeight: 1.05,
            textWrap: 'pretty',
          }}>{post.title}</h3>
          <p style={{ color: 'var(--text-2)', fontSize: big ? 17 : 14, marginTop: 12, textWrap: 'pretty', flex: 1 }}>{post.dek}</p>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 22, paddingTop: 18, borderTop: '1px solid var(--line)' }}>
            <div style={{ width: 32, height: 32, borderRadius: '50%', background: '#0c0e15', overflow: 'hidden', flexShrink: 0, border: '1px solid var(--line-2)' }}>
              <img src={post.authorPhoto} alt={post.author} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top' }}/>
            </div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div className="mono" style={{ fontSize: 11, color: 'var(--text)', letterSpacing: '0.1em' }}>{post.author}</div>
              <div className="mono" style={{ fontSize: 9, color: 'var(--text-3)', letterSpacing: '0.14em', textTransform: 'uppercase', marginTop: 2 }}>{post.authorRole}</div>
            </div>
            <span className="mono" style={{ fontSize: 11, color: 'var(--accent)', letterSpacing: '0.1em' }}>READ →</span>
          </div>
        </div>
      </a>
    </Reveal>
  );
}

function CategoryHeading({ label, count }) {
  return (
    <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 24, marginBottom: 24, paddingBottom: 14, borderBottom: '1px solid var(--line)' }}>
      <h2 className="display" style={{ fontSize: 'clamp(28px, 4vw, 44px)', letterSpacing: '-0.03em' }}>{label}</h2>
      <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)', letterSpacing: '0.16em', textTransform: 'uppercase' }}>{count} {count === 1 ? 'POST' : 'POSTS'}</span>
    </div>
  );
}

function CategoryFilter({ active, setActive, cats }) {
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 28 }}>
      {['All', ...cats].map((c) => (
        <button
          key={c}
          onClick={() => setActive(c)}
          className="cat-chip"
          data-active={active === c}
        >
          {c}
        </button>
      ))}
    </div>
  );
}

function Blog({ setPage, openPost }) {
  const [activeCat, setActiveCat] = useState('All');
  const cats = ['Playbook', 'Case study', 'Team note'];
  const featured = POSTS.filter(p => p.featured);
  const filtered = activeCat === 'All' ? POSTS : POSTS.filter(p => p.cat === activeCat);

  // Group by category for the sectioned layout (only when filter is "All")
  const byCat = cats.map(c => ({ cat: c, posts: POSTS.filter(p => p.cat === c) }));

  return (
    <div className="page">
      <Breadcrumbs setPage={setPage} current="Blog"/>
      <section className="sec sec-after-crumbs">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow">Field notes · {POSTS.length} posts</div>
            <h1 className="display" style={{ fontSize: 'clamp(56px, 9vw, 130px)', marginTop: 20, maxWidth: 1100 }}>
              Playbooks, case studies, <span className="u-accent">team notes.</span>
            </h1>
            <p style={{ fontSize: 19, color: 'var(--text-2)', maxWidth: 620, marginTop: 24 }}>
              What we learn shipping miniapps to operators. Numbers we report on. Bets we make. The studio in the open.
            </p>
            <CategoryFilter active={activeCat} setActive={setActiveCat} cats={cats}/>
          </Reveal>
        </div>
      </section>

      {activeCat === 'All' && (
        <section className="sec-sm">
          <div className="wrap">
            <div className="eyebrow" style={{ marginBottom: 24 }}>Featured</div>
            <div className="grid g-2" style={{ gap: 24 }}>
              {featured.map(p => <BlogCard key={p.slug} post={p} onOpen={openPost} size="lg"/>)}
            </div>
          </div>
        </section>
      )}

      {activeCat === 'All' ? (
        byCat.map(({ cat, posts }) => (
          <section key={cat} className="sec-sm">
            <div className="wrap">
              <CategoryHeading label={cat} count={posts.length}/>
              <div className="grid g-3" style={{ gap: 20 }}>
                {posts.map(p => <BlogCard key={p.slug} post={p} onOpen={openPost}/>)}
              </div>
            </div>
          </section>
        ))
      ) : (
        <section className="sec-sm">
          <div className="wrap">
            <CategoryHeading label={activeCat} count={filtered.length}/>
            <div className="grid g-3" style={{ gap: 20 }}>
              {filtered.map(p => <BlogCard key={p.slug} post={p} onOpen={openPost}/>)}
            </div>
          </div>
        </section>
      )}

      <section className="sec">
        <div className="wrap">
          <NewsletterBlock variant="lg"/>
        </div>
      </section>
    </div>
  );
}

function NewsletterBlock({ variant = 'lg' }) {
  const [email, setEmail] = useState('');
  const [done, setDone] = useState(false);
  const submit = (e) => { e.preventDefault(); if (email) setDone(true); };
  return (
    <div className="card" style={{
      padding: variant === 'lg' ? 48 : 32,
      background: 'radial-gradient(ellipse at 0% 0%, rgba(255,201,60,0.10), var(--ink-3))',
      borderColor: 'rgba(255,201,60,0.25)',
    }}>
      <div className="grid g-2" style={{ gap: 32, alignItems: 'center' }}>
        <div>
          <span className="tag tag-flag">FIELD NOTES</span>
          <h3 className="display" style={{ fontSize: variant === 'lg' ? 'clamp(28px, 3.5vw, 44px)' : 26, letterSpacing: '-0.03em', marginTop: 14 }}>
            One miniapp post a week. <span className="u-accent">No fluff.</span>
          </h3>
          <p style={{ color: 'var(--text-2)', fontSize: 15, marginTop: 12, maxWidth: 520 }}>
            What we shipped, what it cost, what we'd do again. Sent Mondays. Unsubscribe with one click.
          </p>
        </div>
        <div>
          {!done ? (
            <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <label className="mono" style={{ fontSize: 10, color: 'var(--text-3)', letterSpacing: '0.14em', textTransform: 'uppercase' }}>Email</label>
              <div style={{ display: 'flex', gap: 8 }}>
                <input
                  type="email"
                  required
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  placeholder="you@operator.com"
                  style={{
                    flex: 1, padding: '12px 14px',
                    background: 'var(--ink-2)', border: '1px solid var(--line-2)',
                    borderRadius: 999, color: 'var(--text)',
                    fontFamily: 'Inter', fontSize: 15, outline: 'none',
                  }}
                />
                <button type="submit" className="btn btn-primary">Subscribe <span className="chev">→</span></button>
              </div>
              <span className="mono" style={{ fontSize: 10, color: 'var(--text-4)', letterSpacing: '0.12em', textTransform: 'uppercase' }}>~2,400 operators reading</span>
            </form>
          ) : (
            <div style={{ padding: 16, background: 'var(--ink-2)', borderRadius: 12, border: '1px solid rgba(36,225,164,0.4)' }}>
              <span className="tag tag-live">SUBSCRIBED</span>
              <div className="display" style={{ fontSize: 22, marginTop: 10 }}>You're on the list.</div>
              <div style={{ color: 'var(--text-2)', fontSize: 14, marginTop: 6 }}>Next post lands Monday.</div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function ShareRow({ post }) {
  const url = typeof window !== 'undefined' ? window.location.href : '';
  const items = [
    ['Telegram', `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(post.title)}`],
    ['X', `https://twitter.com/intent/tweet?text=${encodeURIComponent(post.title)}&url=${encodeURIComponent(url)}`],
    ['LinkedIn', `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`],
    ['Copy link', '#'],
  ];
  const [copied, setCopied] = useState(false);
  const handle = (label, e) => {
    if (label === 'Copy link') {
      e.preventDefault();
      try { navigator.clipboard.writeText(url); setCopied(true); setTimeout(() => setCopied(false), 1600); } catch {}
    }
  };
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
      <span className="mono" style={{ fontSize: 10, color: 'var(--text-3)', letterSpacing: '0.16em', textTransform: 'uppercase' }}>Share</span>
      {items.map(([label, href]) => (
        <a
          key={label}
          href={href}
          target={label === 'Copy link' ? '_self' : '_blank'}
          rel="noopener"
          onClick={(e) => handle(label, e)}
          className="share-chip"
        >
          {label === 'Copy link' && copied ? '✓ Copied' : label}
        </a>
      ))}
    </div>
  );
}

function Post({ slug, setPage, openPost }) {
  const post = POSTS.find(p => p.slug === slug) || POSTS[0];
  const others = POSTS.filter(p => p.slug !== post.slug).slice(0, 3);

  useEffect(() => { try { window.scrollTo(0, 0); } catch {} }, [slug]);

  return (
    <div className="page">
      <Breadcrumbs setPage={setPage} items={[['Blog', 'blog']]} current={post.title}/>
      {/* Hero */}
      <section className="sec-sm" style={{ paddingTop: 24 }}>
        <div className="wrap" style={{ maxWidth: 880 }}>
          <Reveal>
            <button onClick={() => setPage('blog')} className="mono" style={{
              background: 'transparent', border: 'none', color: 'var(--text-3)',
              fontSize: 11, letterSpacing: '0.16em', textTransform: 'uppercase',
              cursor: 'pointer', padding: 0, marginBottom: 24,
            }}>← All field notes</button>

            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 20 }}>
              <span className={`tag ${post.cat === 'Playbook' ? 'tag-flag' : post.cat === 'Case study' ? 'tag-live' : 'tag-dev'}`}>{post.cat.toUpperCase()}</span>
              {post.tags.map(t => <span key={t} className="tag">{t}</span>)}
            </div>

            <h1 className="display" style={{
              fontSize: 'clamp(40px, 6.5vw, 84px)',
              letterSpacing: '-0.03em',
              maxWidth: 900,
              textWrap: 'balance',
              lineHeight: 1.02,
            }}>{post.title}</h1>

            <p style={{
              fontSize: 'clamp(18px, 2vw, 22px)',
              color: 'var(--text-2)',
              marginTop: 24,
              maxWidth: 720,
              textWrap: 'pretty',
              lineHeight: 1.4,
            }}>{post.dek}</p>

            <div style={{ display: 'flex', alignItems: 'center', gap: 18, marginTop: 36, paddingTop: 24, borderTop: '1px solid var(--line)', flexWrap: 'wrap' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                <div style={{ width: 44, height: 44, borderRadius: '50%', background: '#0c0e15', overflow: 'hidden', border: '1px solid var(--line-2)' }}>
                  <img src={post.authorPhoto} alt={post.author} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top' }}/>
                </div>
                <div>
                  <div className="mono" style={{ fontSize: 12, color: 'var(--text)', letterSpacing: '0.1em' }}>{post.author}</div>
                  <div className="mono" style={{ fontSize: 10, color: 'var(--text-3)', letterSpacing: '0.14em', textTransform: 'uppercase', marginTop: 2 }}>{post.authorRole}</div>
                </div>
              </div>
              <span className="mono" style={{ fontSize: 11, color: 'var(--text-4)' }}>·</span>
              <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)', letterSpacing: '0.14em' }}>{post.date}</span>
              <span className="mono" style={{ fontSize: 11, color: 'var(--text-4)' }}>·</span>
              <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)', letterSpacing: '0.14em' }}>{post.read} MIN READ</span>
              <div style={{ marginLeft: 'auto' }}>
                <ShareRow post={post}/>
              </div>
            </div>
          </Reveal>
        </div>
      </section>

      {/* Cover */}
      <section className="sec-sm" style={{ paddingTop: 0 }}>
        <div className="wrap" style={{ maxWidth: 1080 }}>
          <Reveal>
            <div style={{
              aspectRatio: '16/9',
              background: post.coverBg || '#0c0e15',
              borderRadius: 16,
              border: '1px solid var(--line)',
              overflow: 'hidden',
              position: 'relative',
            }}>
              <img src={post.cover} alt={post.title} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: post.coverObject || 'center' }}/>
            </div>
          </Reveal>
        </div>
      </section>

      {/* Body */}
      <section className="sec-sm">
        <div className="wrap" style={{ maxWidth: 720 }}>
          <article className="post-body">
            {post.body.map((b, i) => {
              if (b.kind === 'p') return <p key={i}>{b.text}</p>;
              if (b.kind === 'h2') return <h2 key={i} className="display">{b.text}</h2>;
              if (b.kind === 'pull') return (
                <blockquote key={i} className="pull">
                  <span className="pull-mark">&ldquo;</span>
                  <span>{b.text}</span>
                </blockquote>
              );
              if (b.kind === 'list') return (
                <ul key={i}>
                  {b.items.map((it, j) => <li key={j}>{it}</li>)}
                </ul>
              );
              return null;
            })}
          </article>

          <div style={{ marginTop: 48, paddingTop: 24, borderTop: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
            <ShareRow post={post}/>
            <button onClick={() => setPage('blog')} className="btn btn-ghost">← All field notes</button>
          </div>
        </div>
      </section>

      {/* Newsletter */}
      <section className="sec-sm">
        <div className="wrap" style={{ maxWidth: 1080 }}>
          <NewsletterBlock variant="lg"/>
        </div>
      </section>

      {/* Related */}
      <section className="sec-sm">
        <div className="wrap">
          <div className="eyebrow" style={{ marginBottom: 24 }}>Keep reading</div>
          <div className="grid g-3" style={{ gap: 20 }}>
            {others.map(p => <BlogCard key={p.slug} post={p} onOpen={openPost}/>)}
          </div>
        </div>
      </section>
    </div>
  );
}

Object.assign(window, { Blog, Post, NewsletterBlock });
