import { PageHeader } from "@/components/ui";
import { ChevronDownIcon } from "@/components/icons";
import RulebookPanel from "@/components/panels/RulebookPanel";
import GuideInteractivity from "./GuideInteractivity";

export const metadata = { title: "User Guide — AI Data Aggregation" };

const TOC = [
  ["overview", "What this application does"],
  ["lifecycle", "The product lifecycle"],
  ["pipeline", "How the pipeline works"],
  ["quality", "The Quality Score"],
  ["images", "The image pipeline"],
  ["backoffice", "Using the Admin Backoffice"],
  ["users", "Users, roles & signing in"],
  ["golive", "Connecting Ergonode & Vertex"],
  ["control", "Services, runners, cost & kill-switch"],
  ["csv", "The ai-product-log CSV"],
  ["glossary", "Glossary"],
  ["faq", "Troubleshooting"],
] as const;

export default function GuidePage() {
  return (
    <article className="max-w-3xl">
      <PageHeader
        title="User Guide"
        description="What the AI Data Aggregation service does, how it works, the terms it uses, and how to operate it."
      />
      <GuideInteractivity />

      <p className="mb-6 text-sm text-slate-500">
        Each chapter is collapsed — click a title (or a Contents entry) to open it. Sections with sub-topics expand the
        same way.
      </p>

      <nav aria-label="Contents" className="card mb-8">
        <h2 className="mb-2 text-sm font-semibold text-slate-900">Contents</h2>
        <ol className="grid gap-1 text-sm sm:grid-cols-2">
          {TOC.map(([id, label], i) => (
            <li key={id}>
              <a className="text-brand hover:underline" href={`#${id}`}>
                {i + 1}. {label}
              </a>
            </li>
          ))}
        </ol>
      </nav>

      <div className="border-t border-slate-200 text-slate-700">
        <Section id="rulebook" title="Active rulebook">
          <p>
            A plain-language, always-current summary of <strong>every active enrichment rule</strong>, written by the AI.
            It regenerates automatically whenever a rule changes.
          </p>
          <RulebookPanel />
        </Section>

        <Section id="overview" title="1 · What this application does">
          <p>
            This service automatically enriches products in <strong>Ergonode</strong> (the Product Information
            Management system). When a product is created or moved to status <Code>New</Code>, the service researches
            and fills in its missing attributes, sources and generates product images, scores everything for quality,
            and writes the accepted values back to Ergonode — turning a <Code>New</Code> product into{" "}
            <Code>Done</Code> (fully enriched) or <Code>Review</Code> (needs a human) with no manual data entry.
          </p>
          <p>
            Every value it writes is backed by a source and a quality score, and the full reasoning is attached to the
            product as a downloadable <strong>ai-product-log</strong> CSV, so Operations can defend or correct any
            AI-generated value.
          </p>
          <Callout>
            Connecting the real Ergonode and Google Vertex AI is a configuration step done in <strong>Admin Settings</strong>
            (see <a className="text-brand hover:underline" href="#golive">Connecting Ergonode &amp; Vertex</a>). Until then the
            pipeline calls fail loudly rather than using mock data, so you can always see exactly what is and isn’t working.
          </Callout>
        </Section>

        <Section id="lifecycle" title="2 · The product lifecycle">
          <p>Each product moves through these Ergonode statuses:</p>
          <Flow
            steps={[
              ["New", "Created or reset; waiting to be picked up."],
              ["AI Research", "Claimed by the service and being enriched."],
              ["Done", "All required attributes passed quality — pushed to Ergonode."],
              ["Review", "Something needs a human — partial values pushed, see the CSV."],
            ]}
          />
          <p>
            The service <strong>polls</strong> Ergonode on a configurable interval (default 15 min). A product enters
            the pipeline whenever it is <Code>New</Code>, or whenever any AI-relevant attribute changes (a change to the
            log file or the status itself does <em>not</em> re-trigger it). Each product is{" "}
            <strong>claimed atomically</strong>, so it is processed exactly once even with several workers running.
          </p>
        </Section>

        <Section id="pipeline" title="3 · How the pipeline works">
          <p>For each claimed product the service runs these steps in order:</p>
          <ol className="ml-5 list-decimal space-y-2">
            <li><strong>Requirement resolution</strong> — works out which attributes are Required / Optional / Skip from the product’s Template, Type and category (with per-category overrides).</li>
            <li><strong>Context loading</strong> — gathers existing attributes, the parent product’s binding attributes (for variants), any Content-Link assets, and the effective free-text category rules.</li>
            <li><strong>Per attribute: research → manipulation → scoring.</strong> The AI researches a value <em>with a source</em>, anchored on the product’s known identifiers (product name, brand, GTIN, MPN, article number) to lock onto the right product, then manipulation rules clean/format it and the Quality Score is computed. Research is restricted to the trusted-source whitelist; if the score stays below the threshold (0.8) it retries once, then logs it. When “AI fallback beyond trusted sources” is enabled (Enrichment Rules → Trusted Sources), the retry may also consult sources outside the whitelist — which score lower on source trust.</li>
            <li><strong>Strict identifiers</strong> — GTIN/MPN/SKU/Article Nr. are only accepted if they appear verbatim in a trusted source; a GTIN must also pass its checksum, or it is dropped.</li>
            <li><strong>Image pipeline</strong> — sources and generates images, normalises them and writes multilingual alt text (see below).</li>
            <li><strong>Product-level second pass</strong> — checks completeness, variant-group consistency (a variant’s binding attributes match its parent) and the aggregate score.</li>
            <li><strong>Push</strong> — writes accepted attributes + images back to Ergonode, uploads the ai-product-log CSV, and sets the status to <Code>Done</Code> or <Code>Review</Code>. On a passing product everything is pushed; on a failing product only attributes that scored ≥ 0.8 are pushed.</li>
          </ol>
          <p>Transient AI/network errors retry with exponential backoff; a terminal Ergonode push failure alerts the IT Slack channel and routes the product to <Code>Review</Code>.</p>
        </Section>

        <Section id="quality" title="4 · The Quality Score (AQS)">
          <p>
            Every enriched attribute gets an <strong>Attribute Quality Score</strong> between 0 and 1. Business-Rule
            Compliance is a knock-out gate: if a value breaks a rule, BRC = 0 and the whole AQS becomes 0. Otherwise:
          </p>
          <p className="rounded-md bg-slate-100 p-3 font-mono text-sm">
            AQS = 0.35·TSQ + 0.35·FPV + 0.25·CSC + 0.05·VFQ
          </p>
          <Terms
            rows={[
              ["BRC", "Business-Rule Compliance — KO gate (0 forces AQS 0)."],
              ["TSQ", "Trustworthy Source Quality — how authoritative the source is (manufacturer 1.0 … unknown 0.40). Weight 35%."],
              ["FPV", "Factual Plausibility — is the value sensible for the product? Weight 35%."],
              ["CSC", "Cross-Source Consistency — do multiple sources agree? Weight 25%."],
              ["VFQ", "Visual Formatting Quality — units, encoding, clean HTML. Weight 5%."],
            ]}
          />
          <p>An attribute needs AQS ≥ 0.8 to be pushed. A product reaches <Code>Done</Code> only if every required attribute passes and the product-level checks hold.</p>
        </Section>

        <Section id="images" title="5 · The image pipeline">
          <p>For every product the service:</p>
          <ul className="ml-5 list-disc space-y-1">
            <li><strong>Sources</strong> candidate images from the trusted-source whitelist, and <strong>always generates</strong> additional ones via the image model (Nano Banana), seeded with the sourced references.</li>
            <li><strong>Normalises</strong> each product image: segmentation-based background removal (handles white-on-white), optical centring on a white canvas with the configured padding, then WebP encoding under a file-size guard.</li>
            <li><strong>Mood images</strong> use a separate canvas with a geometric-centre crop (no background removal).</li>
            <li>Produces a configurable number of <em>hero</em>, <em>additional</em> and <em>mood</em> images, rejects images with forbidden overlays (price tags, logos…), and writes <strong>alt text in DE/EN/FR/IT</strong> for every accepted image.</li>
          </ul>
          <p>All image rules (canvas sizes, counts, padding, file-size cap, naming pattern, forbidden overlays) are edited under <strong>Enrichment Rules → Manipulation Rules → Image specs</strong> and apply on the next product. To review what a product actually produced — every sourced and generated image, each marked background-segmented or not with a clean ✓/✗ — open the <strong>Gallery</strong> tab of its history modal (Monitoring → click the ⓘ on a product).</p>
        </Section>

        <Section id="backoffice" title="6 · Using the Admin Backoffice">
          <Terms
            rows={[
              ["Dashboard", "Live pipeline health: queue depth, throughput, average quality per attribute, cost today, average cost per product, failure rate, the Ergonode status counts and the kill-switch."],
              ["Monitoring", "Three tabs — Product Queue (products in processing order with their live AI status; tick pending products and Force pickup to enrich them immediately instead of waiting for the next poll), Processed (every finished product: Done / Review / Failed) and Log (a step-by-step processing log). Click the ⓘ on any product to open its history — a progress tracker plus Attribute overview, Gallery and Log tabs."],
              ["Enrichment Rules", "Three tabs — Manipulation Rules (versioned cleanup, image, tone-of-voice and image-spec rules), Per-Category Rules (a collapsible Category Tree with requirement overrides + free-text per node, merging Main → Parent → Child; lists only research-enabled attributes), and Trusted Sources (research & image whitelist with TSQ weights, plus an “AI fallback beyond trusted sources” toggle)."],
              ["Admin Settings", "Credentials (Ergonode, Vertex AI, Slack), Runtime (language, polling, concurrent products, retries), Cost & Quality (ceilings, price table, AQS threshold & weights — superadmin), Actions (danger-zone resets), Audit Log (who / what / before → after) and Users & Roles (superadmin)."],
              ["Services", "Three tabs — Services (switch each pipeline service active/passive and schedule it via its ⋯ menu, plus the global kill-switch), Attributes (the Ergonode attribute overview + import/sync and category sync) and, on the Services tab, a Process runners table to start/stop the Worker & Poller with a live CLI log."],
              ["User Guide", "This page — open it any time from the book icon in the header."],
            ]}
          />
          <p>Edits are batched: a floating <strong>Save&nbsp;N&nbsp;changes</strong> button appears at the bottom-right whenever you have unsaved changes across a page, and nothing is persisted until you click it.</p>
          <p>The sidebar collapses to an icon rail via the hamburger at its base (hover an icon for its label). Every change is versioned and audited, and takes effect on the <strong>next product processed</strong> — no redeploy.</p>
        </Section>

        <Section id="users" title="7 · Users, roles & signing in">
          <p>
            Access to the backoffice is governed by a single <strong>Users &amp; Roles</strong> directory
            (<strong>Admin Settings → Users &amp; Roles</strong>). Each person signs in with one of the methods below, and the
            <strong> roles</strong> on their account decide which sections they can open.
          </p>

          <Sub title="Ways to sign in">
            <ul className="ml-5 list-disc space-y-1">
              <li><strong>Username &amp; password</strong> — a local user whose password an admin has set signs in with their e-mail and password.</li>
              <li><strong>Sign in with Google</strong> — when Google OAuth is configured (<strong>Admin Settings → Credentials → Google Auth</strong>), a user whose e-mail is listed in the directory can sign in with their Google account, no password needed.</li>
              <li><strong>Developer login</strong> — a bootstrap escape-hatch that grants <strong>Superadmin</strong> without a password. Disable it in production with <Code>ALLOW_DEV_LOGIN=false</Code>.</li>
            </ul>
            <Callout>
              The directory is the allow-list for both methods. While it is <em>empty</em>, any Google account may sign in (with
              the <strong>Monitoring</strong> role) and the developer login works — so you are never locked out during setup. Add
              the first user and the allow-list takes effect.
            </Callout>
          </Sub>

          <Sub title="Roles & what they can access">
            <Terms
              rows={[
                ["Superadmin", "Full access to everything, including the two superadmin-only areas: Cost & Quality and Users & Roles."],
                ["Tech-Admin", "Everything except the superadmin-only areas (Cost & Quality and Users & Roles)."],
                ["Service Manager", "Dashboard, Monitoring, Enrichment Rules and Services — no Admin Settings."],
                ["Monitoring", "Dashboard and Monitoring only — read-only oversight."],
              ]}
            />
          </Sub>

          <Sub title="Managing users">
            <p>
              In <strong>Users &amp; Roles</strong> an admin adds a user (e-mail + name), ticks one or more roles, and optionally
              sets a <strong>password</strong> — leave it blank for a Google-only user, or to keep an existing password unchanged.
              A <strong>local&nbsp;+&nbsp;password</strong> or <strong>Google-only</strong> badge shows how each user signs in, and
              deleting a user removes their access. Because this screen can grant any role and set passwords, it is
              <strong> Superadmin-only</strong>.
            </p>
          </Sub>
        </Section>

        <Section id="golive" title="8 · Connecting Ergonode & Vertex">
          <Sub title="Ergonode">
            <p>In <strong>Admin Settings → Credentials → Ergonode</strong>: enter the base URL, the content language (e.g. <Code>en_GB</Code> or <Code>de_CH</Code>) and a <strong>GraphQL API key with write access</strong> (created in Ergonode under System Settings → API keys). Click <strong>Test connection</strong>. The integration is <strong>GraphQL-only</strong> (<Code>POST /api/graphql/</Code> with an <Code>X-API-KEY</Code> header) — there is no username/password. Products are matched by <Code>SKU</Code> and attributes by their code. The API key is write-only: it is stored for the service to use and never shown again.</p>
            <p>Ergonode data operations live under <strong>Services → Attributes</strong>: the <strong>attribute overview</strong> (every Ergonode attribute, each with an <em>In&nbsp;research</em> toggle), <strong>Import / sync attributes</strong> (Ergonode is the single source of truth — new attributes are added, existing ones keep their config, and attributes no longer in Ergonode are removed) and <strong>Pull categories &amp; overwrite</strong> (imports the category tree into <strong>Enrichment Rules → Per-Category Rules</strong>, overwriting the local tree and dropping rules whose category no longer exists).</p>
          </Sub>
          <Sub title="Vertex AI">
            <p>In <strong>Admin Settings → Credentials → Vertex AI</strong>: set the GCP project id, then a model and (optionally) a region <em>per pipeline step</em> — useful when a model is only served in some regions. The service-account key is uploaded here (write-only) or mounted at <Code>secrets/vertex-sa.json</Code>; the page shows whether it is present and valid. Use <strong>Run smoke test</strong> to confirm every step works against the live project.</p>
            <Callout>Until both are configured, the pipeline calls fail loudly with a clear message — there are no mock fallbacks — so you always see exactly what works and what doesn’t.</Callout>
          </Sub>
        </Section>

        <Section id="control" title="9 · Services, runners, cost & kill-switch">
          <p>On the <strong>Services</strong> page each pipeline service (Ingestion, Research, Manipulation, Scoring, Image Sourcing, Image Creation, Push) is a row you switch <strong>active/passive</strong> and schedule — <em>always-on</em> or a weekly time window — from its <strong>⋯</strong> menu. A passive service becomes a no-op and is logged, useful for staged rollout or a quick rollback. The same page has a <strong>Process runners</strong> table to start and stop the two background processes — the <strong>Worker</strong> (consumes the queue and enriches products) and the <strong>Poller</strong> (auto-discovers <Code>New</Code> products on the configured interval) — each with a live CLI log. How many products the Worker enriches in parallel is set by <strong>Concurrent products</strong> under <strong>Admin Settings → Runtime</strong>.</p>
          <p>Every AI call records its cost. A <strong>per-product ceiling</strong> stops enriching a product once it gets too expensive; a <strong>daily ceiling</strong> trips the <strong>kill-switch</strong>, which halts all new AI work (products already in flight finish). Pause or resume everything from the <strong>Services</strong> page, or reset the kill-switch from the <strong>Dashboard</strong>. The Dashboard also shows the <strong>average cost per product</strong>; you can reset it — together with the queue and processing log — under <strong>Admin Settings → Actions</strong>. Every action is audited.</p>
        </Section>

        <Section id="csv" title="10 · The ai-product-log CSV">
          <p>For every processed product, a CSV is attached to the product in Ergonode with one row per attribute and per image, sorted by quality (lowest first). Reviewers download it to see exactly why each value was accepted or rejected. Columns:</p>
          <p className="rounded-md bg-slate-100 p-3 font-mono text-xs">
            row_type · attribute_name/image_position · candidate_value/image_filename · aqs · brc · tsq · fpv · csc · vfq ·
            source_url · source_excerpt · model_id · prompt_version · manipulation_rule_version · alt_text_de/en/fr/it ·
            rejection_reason · timestamp
          </p>
        </Section>

        <Section id="glossary" title="11 · Glossary">
          <Sub title="Abbreviations & codes">
          <Terms
            rows={[
              ["PIM", "Product Information Management — the system that stores products. Ergonode is the PIM this service enriches."],
              ["SKU", "Stock Keeping Unit — the unique code Ergonode uses to identify and match a product."],
              ["GTIN", "Global Trade Item Number — the product barcode number (EAN/UPC). Accepted only if it appears verbatim in a trusted source and passes its checksum."],
              ["MPN", "Manufacturer Part Number — the maker’s own part code for the product."],
              ["Article Nr.", "The retailer’s internal article number for the product."],
              ["AQS", "Attribute Quality Score — the 0–1 score every enriched value gets (see The Quality Score)."],
              ["BRC", "Business-Rule Compliance — a knock-out (KO) gate: if a value breaks a rule, the AQS is forced to 0."],
              ["TSQ / FPV / CSC / VFQ", "The four weighted parts of the AQS: Trustworthy Source Quality, Factual Plausibility, Cross-Source Consistency and Visual Formatting Quality (see The Quality Score)."],
              ["KO gate", "“Knock-out” gate — a check that, if failed, forces the score to 0 regardless of the rest."],
              ["RBAC", "Role-Based Access Control — the four roles (Superadmin, Tech-Admin, Service Manager, Monitoring) that decide what each user can open."],
              ["OAuth", "The protocol behind “Sign in with Google”."],
              ["GCP", "Google Cloud Platform — Vertex AI runs inside a GCP project."],
              ["Vertex AI", "Google’s managed AI platform that runs the research, scoring and image models."],
              ["API key", "A secret token that authorises the service to call an external system (e.g. Ergonode). Always stored write-only — entered once, never shown again."],
              ["GraphQL", "The query API used to talk to Ergonode (POST /api/graphql/ with an X-API-KEY header). The integration is GraphQL-only."],
              ["CSV", "Comma-Separated Values — the spreadsheet format of the ai-product-log."],
              ["WebP", "The compressed web image format product images are encoded to."],
              ["Locale", "A language + region code (e.g. de_CH, en_GB) set as Ergonode’s content language."],
              ["DE / EN / FR / IT", "The four content languages — German, English, French, Italian — that image alt text is written in."],
            ]}
          />
          </Sub>

          <Sub title="Concepts & features">
          <Terms
            rows={[
              ["Product Template", "Physical Product, Coupon or Voucher — determines which attributes are required."],
              ["Product Type", "Simple Product, Product with Variant, or Grouping (bundles are not supported in v1)."],
              ["Variant group", "A parent product and its variant children (e.g. sizes or colours) that share the parent’s binding values."],
              ["Binding attribute", "A variant-defining attribute (e.g. colour). A variant child reuses the parent’s shared binding values for consistency."],
              ["Requirement level", "Whether an attribute is Required, Optional or Skip for a template/category — a Skip attribute is never researched."],
              ["In-research toggle", "Per-attribute switch (Services → Attributes) that includes or excludes an attribute from AI research."],
              ["Per-Category Rules", "Admin rules attached to the Category Tree with Main → Parent → Child inheritance."],
              ["Category Tree", "The hierarchy of categories (Main → Parent → Child) that per-category rules and requirement overrides attach to."],
              ["Trusted source", "An approved domain the AI may research and source images from; each carries a TSQ weight (Enrichment Rules → Trusted Sources)."],
              ["AI fallback", "Optional retry that may consult sources outside the trusted whitelist — they score lower on source trust (TSQ)."],
              ["Manipulation rules", "Versioned rules that clean and format a researched value before it is scored."],
              ["Tone of voice", "Manipulation rules that enforce a consistent writing style for text attributes."],
              ["Alt text", "The short text describing an image (for accessibility and SEO), written per language (DE/EN/FR/IT)."],
              ["Image roles", "Hero (main pack-shot), additional (extra angles) and mood (lifestyle/atmosphere) images."],
              ["Background removal", "Segmentation that isolates the product from its background (handles white-on-white) before centring it on a white canvas."],
              ["Pipeline services", "The seven stages you can switch active/passive: Ingestion, Research, Manipulation, Scoring, Image Sourcing, Image Creation and Push."],
              ["Active / passive", "An active service runs; a passive one becomes a logged no-op — useful for staged rollout or a quick rollback."],
              ["Poller", "The background process that checks Ergonode for New products on the configured interval."],
              ["Worker", "The background process that consumes the queue and enriches products; Concurrent products sets how many it does in parallel."],
              ["Atomic claim", "Locking a product so exactly one worker processes it, even with several workers running."],
              ["Force pickup", "Manually queue selected New products right away instead of waiting for the next poll (Monitoring → Product Queue)."],
              ["Cost ceilings", "A per-product ceiling stops enriching one product when it gets too expensive; a daily ceiling trips the kill-switch."],
              ["Kill-switch", "A hard server-side gate that halts new AI work when the daily cost ceiling is reached."],
              ["Smoke test", "A quick end-to-end check that every Vertex AI step works against the live project."],
              ["Connection test", "Verifies the Ergonode base URL + API key by making a real call."],
              ["Content-Link", "Assets linked to a product in Ergonode that the pipeline can use as extra context."],
              ["Provenance", "The source URL, source excerpt, model id and prompt version stored with every AI value."],
              ["ai-product-log", "The per-product audit CSV uploaded to Ergonode as a file attribute (one row per attribute and image)."],
              ["Nano Banana", "Google’s Gemini image-generation model used for image creation."],
              ["Save bar", "The floating “Save N changes” button (bottom-right) that persists all your pending edits across a page at once."],
              ["Audit log", "The record of who changed what, with before → after values (Admin Settings → Audit Log)."],
              ["Exponential backoff", "A retry strategy that waits progressively longer between attempts after transient errors."],
              ["Developer login", "A bootstrap sign-in that grants Superadmin without a password; disable in production with ALLOW_DEV_LOGIN=false."],
            ]}
          />
          </Sub>
        </Section>

        <Section id="faq" title="12 · Troubleshooting">
          <Terms
            rows={[
              ["“Connection test failed” (Ergonode)", "Check the base URL and the GraphQL API key in Admin Settings → Credentials. The key must have WRITE access (created in Ergonode → System Settings → API keys). The integration uses GraphQL (POST /api/graphql/, X-API-KEY) — there is no username/password."],
              ["A product went to Review instead of Done", "Open its ai-product-log CSV (top rows = lowest scores) or the history modal’s Attribute overview. Common causes: a required attribute scored < 0.8 or is missing, a variant's binding attributes don't match its parent, or a free-text category rule failed."],
              ["Nothing is being processed", "On Services → Process runners, start the Worker (and the Poller, for auto-pickup). Check the kill-switch is off and the Ingestion service is active. New products are picked up on the next polling cycle."],
              ["Products finish with empty values / everything goes to Review", "Make sure the relevant attributes are enabled for research (Services → Attributes) and have a requirement set for the product’s template under Per-Category Rules — an attribute that is Skip / not-required for a template is never researched."],
              ["Vertex calls fail", "On the Vertex AI page, confirm the credential status is “configured” (key file present + valid, project id set) and run the smoke test."],
              ["Someone can’t sign in", "Add their e-mail under Admin Settings → Users & Roles and give them at least one role. For username/password login, set a password there; for Google, their e-mail must be listed and Google Auth configured under Credentials. There is no self-service reset — an admin sets a new password. Locked out entirely? Use the developer login (if enabled) to get back in as Superadmin."],
            ]}
          />
        </Section>
      </div>
    </article>
  );
}

/**
 * A collapsible chapter. Native <details> → closed by default, opens on a click
 * of the title (or when a Table-of-Contents link targets its id, wired up by
 * GuideInteractivity). The disclosure marker is a chevron that flips when open.
 */
function Section({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
  return (
    <details id={id} className="group scroll-mt-6 border-b border-slate-200">
      <summary className="flex cursor-pointer list-none items-center justify-between gap-3 py-3 hover:text-brand [&::-webkit-details-marker]:hidden">
        <h2 className="text-xl font-semibold text-slate-900">{title}</h2>
        <ChevronDownIcon className="h-5 w-5 shrink-0 text-slate-400 transition-transform group-open:rotate-180" />
      </summary>
      <div className="space-y-3 pb-6 pt-1">{children}</div>
    </details>
  );
}

/** A collapsible sub-topic inside a chapter (closed by default). */
function Sub({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <details className="group/sub rounded-md border border-slate-200">
      <summary className="flex cursor-pointer list-none items-center justify-between gap-2 rounded-md px-3 py-2 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
        <h3 className="font-semibold text-slate-900">{title}</h3>
        <ChevronDownIcon className="h-4 w-4 shrink-0 text-slate-400 transition-transform group-open/sub:rotate-180" />
      </summary>
      <div className="space-y-3 px-3 pb-3 pt-1">{children}</div>
    </details>
  );
}

function Code({ children }: { children: React.ReactNode }) {
  return <code className="rounded-sm bg-slate-100 px-1 py-0.5 text-sm text-slate-800">{children}</code>;
}

function Callout({ children }: { children: React.ReactNode }) {
  return <div className="rounded-md border-l-4 border-brand bg-blue-50 p-3 text-sm text-slate-700">{children}</div>;
}

function Flow({ steps }: { steps: [string, string][] }) {
  return (
    <ol className="space-y-2">
      {steps.map(([s, d], i) => (
        <li key={s} className="flex gap-3">
          <span className="mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand text-xs font-semibold text-white">{i + 1}</span>
          <span><strong className="text-slate-900">{s}</strong> — {d}</span>
        </li>
      ))}
    </ol>
  );
}

function Terms({ rows }: { rows: [string, string][] }) {
  return (
    <dl className="divide-y divide-slate-100 rounded-md border border-slate-200">
      {rows.map(([term, def]) => (
        <div key={term} className="grid gap-1 p-3 sm:grid-cols-[12rem_1fr] sm:gap-4">
          <dt className="font-semibold text-slate-900">{term}</dt>
          <dd className="text-sm text-slate-700">{def}</dd>
        </div>
      ))}
    </dl>
  );
}
