<script lang="ts">
  import { onMount } from 'svelte';

  // ── Types ──────────────────────────────────────────────────────────────────

  type NodeId = 'browser' | 'dns-server' | 'website' | 'ad-server';

  interface NodeLayout {
    x: number;
    y: number;
    w: number;
    h: number;
    title: string;
    sub: string;
  }

  interface Step {
    label: string;
    desc: string;
    from: NodeId;
    to: NodeId;
    lineType: 'dns' | 'http' | 'blocked';
    isReturn: boolean;
    packetLabel?: string;
    blockedInAdBlockMode?: boolean;
    skipInAdBlockMode?: boolean;
    hitNode?: NodeId;
    hitText?: string;
    hitColor?: 'blue' | 'green' | 'red';
  }

  // ── Constants ──────────────────────────────────────────────────────────────

  const SITE_DOMAIN = 'news.com';
  const AD_DOMAIN = 'ads.tracker.net';
  const SITE_IP = '93.184.216.34';
  const AD_IP = '203.0.113.50';
  const BLOCKED_IP = '0.0.0.0';

  // Desktop layout - Diamond shape
  // Website at top, Browser left, DNS right, Ad server bottom
  const NODES_H: Record<NodeId, NodeLayout> = {
    'browser':    { x: 20,  y: 100, w: 120, h: 60, title: 'Browser',    sub: 'Your device'  },
    'dns-server': { x: 380, y: 100, w: 120, h: 60, title: 'DNS Server', sub: 'dnsmasq'      },
    'website':    { x: 200, y: 10,  w: 120, h: 60, title: 'news.com',   sub: SITE_IP        },
    'ad-server':  { x: 200, y: 190, w: 120, h: 60, title: 'Ad Server',  sub: AD_DOMAIN      },
  };

  // Mobile layout - 2x2 grid
  // Top row: Browser (left), DNS Server (right)
  // Bottom row: Website (left), Ad Server (right)
  const NODES_V: Record<NodeId, NodeLayout> = {
    'browser':    { x: 10,  y: 10,  w: 110, h: 50, title: 'Browser',    sub: 'Your device'  },
    'dns-server': { x: 150, y: 10,  w: 110, h: 50, title: 'DNS Server', sub: 'dnsmasq'      },
    'website':    { x: 10,  y: 90,  w: 110, h: 50, title: 'news.com',   sub: SITE_IP        },
    'ad-server':  { x: 150, y: 90,  w: 110, h: 50, title: 'Ad Server',  sub: AD_DOMAIN      },
  };

  // ── Reactive layout ────────────────────────────────────────────────────────

  let isVertical = $state(false);
  let NODES = $derived(isVertical ? NODES_V : NODES_H);
  let viewBoxWidth = $derived(isVertical ? 270 : 520);
  let viewBoxHeight = $derived(isVertical ? 175 : 340);

  function checkLayout() {
    if (typeof window !== 'undefined') {
      isVertical = window.innerWidth < 640;
    }
  }

  onMount(() => {
    checkLayout();
    window.addEventListener('resize', checkLayout);
    return () => window.removeEventListener('resize', checkLayout);
  });

  // ── State ──────────────────────────────────────────────────────────────────

  let adBlockEnabled = $state(false);
  let current = $state(-1);
  let busy = $state(false);
  let done = $state(false);

  // Colors
  const COLOR_DNS = 'oklch(0.65 0.19 240)';      // Blue
  const COLOR_HTTP = 'oklch(0.65 0.19 145)';     // Green
  const COLOR_BLOCKED = 'oklch(0.65 0.19 25)';   // Red

  // ── Helper functions ───────────────────────────────────────────────────────

  function ncx(id: NodeId) { return NODES[id].x + NODES[id].w / 2; }
  function ncy(id: NodeId) { return NODES[id].y + NODES[id].h / 2; }
  function nright(id: NodeId) { return NODES[id].x + NODES[id].w; }
  function nleft(id: NodeId) { return NODES[id].x; }
  function ntop(id: NodeId) { return NODES[id].y; }
  function nbottom(id: NodeId) { return NODES[id].y + NODES[id].h; }

  // ── Steps ──────────────────────────────────────────────────────────────────

  const STEPS: Step[] = [
    {
      label: 'Step 1 — DNS query for main site',
      desc: `You navigate to ${SITE_DOMAIN}. The browser needs to resolve this domain to an IP address, so it sends a DNS query to your DNS server.`,
      from: 'browser', to: 'dns-server',
      lineType: 'dns', isReturn: false,
      packetLabel: `DNS: ${SITE_DOMAIN}?`,
      hitNode: 'dns-server', hitText: 'Looking up...', hitColor: 'blue',
    },
    {
      label: 'Step 2 — DNS response',
      desc: `The DNS server looks up ${SITE_DOMAIN} and responds with the IP address ${SITE_IP}. This is a legitimate site, so it resolves normally.`,
      from: 'dns-server', to: 'browser',
      lineType: 'dns', isReturn: true,
      packetLabel: SITE_IP,
      hitNode: 'browser', hitText: `Resolved: ${SITE_IP}`, hitColor: 'green',
    },
    {
      label: 'Step 3 — HTTP request to site',
      desc: `The browser now knows the IP. It opens a TCP connection to ${SITE_IP} and sends an HTTP GET request for the homepage.`,
      from: 'browser', to: 'website',
      lineType: 'http', isReturn: false,
      packetLabel: 'GET /',
      hitNode: 'website', hitText: 'Request received', hitColor: 'blue',
    },
    {
      label: 'Step 4 — Site returns HTML',
      desc: `The server responds with the HTML page. Embedded in this HTML is a script tag: <script src="https://${AD_DOMAIN}/tracker.js">. The browser needs to fetch this too.`,
      from: 'website', to: 'browser',
      lineType: 'http', isReturn: true,
      packetLabel: 'HTML + JS',
      hitNode: 'browser', hitText: 'Page loaded!', hitColor: 'green',
    },
    {
      label: 'Step 5 — DNS query for ad domain',
      desc: `To load the ad script, the browser must first resolve ${AD_DOMAIN}. It sends another DNS query to your DNS server.`,
      from: 'browser', to: 'dns-server',
      lineType: 'dns', isReturn: false,
      packetLabel: `DNS: ${AD_DOMAIN}?`,
      hitNode: 'dns-server', hitText: 'Checking block list...', hitColor: 'blue',
    },
    {
      label: 'Step 6 — DNS response (blocked!)',
      desc: `The DNS server checks its block list and finds ${AD_DOMAIN}. Instead of resolving it, the server returns ${BLOCKED_IP} — a dead end. The request stops here.`,
      from: 'dns-server', to: 'browser',
      lineType: 'blocked', isReturn: true,
      packetLabel: BLOCKED_IP,
      blockedInAdBlockMode: true,
      hitNode: 'browser', hitText: 'BLOCKED!', hitColor: 'red',
    },
    {
      label: 'Step 6 — DNS response (allowed)',
      desc: `The DNS server resolves ${AD_DOMAIN} to ${AD_IP}. Without a block list, the ad domain resolves like any other.`,
      from: 'dns-server', to: 'browser',
      lineType: 'dns', isReturn: true,
      packetLabel: AD_IP,
      skipInAdBlockMode: true,
      hitNode: 'browser', hitText: `Resolved: ${AD_IP}`, hitColor: 'green',
    },
    {
      label: 'Step 7 — HTTP request to ad server',
      desc: `The browser connects to the ad server and requests the tracking script. This script will set cookies, fingerprint your browser, and load display ads.`,
      from: 'browser', to: 'ad-server',
      lineType: 'http', isReturn: false,
      packetLabel: 'GET /tracker.js',
      skipInAdBlockMode: true,
      hitNode: 'ad-server', hitText: 'Tracking you...', hitColor: 'red',
    },
    {
      label: 'Step 8 — Ad content returned',
      desc: `The ad server sends back JavaScript, tracking pixels, and ad creatives. Your data is collected, bandwidth is consumed, and ads clutter the page.`,
      from: 'ad-server', to: 'browser',
      lineType: 'http', isReturn: true,
      packetLabel: 'Ads + Trackers',
      skipInAdBlockMode: true,
      hitNode: 'browser', hitText: 'Ads loaded :(', hitColor: 'red',
    },
  ];

  // Filter steps based on ad-block mode
  let activeSteps = $derived.by(() => {
    return STEPS.filter(step => {
      if (adBlockEnabled && step.skipInAdBlockMode) return false;
      if (!adBlockEnabled && step.blockedInAdBlockMode) return false;
      return true;
    });
  });

  let totalSteps = $derived(activeSteps.length);

  // ── Line state tracking ────────────────────────────────────────────────────

  interface LineState {
    from: NodeId;
    to: NodeId;
    color: string;
    isReturn: boolean;
    progress: number;
  }

  let completedLines = $state<LineState[]>([]);
  let animatingLine = $state<LineState | null>(null);
  let litNodes = $state<Record<NodeId, string>>({
    'browser': '', 'dns-server': '', 'website': '', 'ad-server': ''
  });
  let showBlockedIndicator = $state(false);
  let badge = $state<{ node: NodeId; text: string; color: 'blue' | 'green' | 'red' } | null>(null);
  let dot = $state<{ x: number; y: number; color: string; visible: boolean; label: string }>({
    x: 0, y: 0, color: '', visible: false, label: ''
  });

  let stepLabel = $state('');
  let desc = $state('');

  // ── Line geometry ──────────────────────────────────────────────────────────

  function getLineEndpoints(from: NodeId, to: NodeId, isReturn: boolean) {
    let x1: number, y1: number, x2: number, y2: number;

    if (isVertical) {
      // Mobile 2x2 grid layout
      // Top row: Browser (left), DNS Server (right)
      // Bottom row: Website (left), Ad Server (right)
      const offset = isReturn ? 6 : -6;

      if ((from === 'browser' && to === 'dns-server') || (from === 'dns-server' && to === 'browser')) {
        // Horizontal line across top row
        x1 = from === 'browser' ? nright(from) : nleft(from);
        y1 = ncy(from) + offset;
        x2 = to === 'browser' ? nright(to) : nleft(to);
        y2 = ncy(to) + offset;
      } else if ((from === 'browser' && to === 'website') || (from === 'website' && to === 'browser')) {
        // Vertical line down left column
        const xOff = isReturn ? 10 : -10;
        x1 = ncx(from) + xOff;
        y1 = from === 'browser' ? nbottom(from) : ntop(from);
        x2 = ncx(to) + xOff;
        y2 = to === 'browser' ? nbottom(to) : ntop(to);
      } else if ((from === 'browser' && to === 'ad-server') || (from === 'ad-server' && to === 'browser')) {
        // Diagonal from top-left to bottom-right
        x1 = from === 'browser' ? nright(from) : nleft(from);
        y1 = from === 'browser' ? nbottom(from) : ntop(from);
        x2 = to === 'browser' ? nright(to) : nleft(to);
        y2 = to === 'browser' ? nbottom(to) : ntop(to);
      } else {
        x1 = ncx(from); y1 = ncy(from);
        x2 = ncx(to); y2 = ncy(to);
      }
    } else {
      // Desktop diamond layout
      if ((from === 'browser' && to === 'dns-server') || (from === 'dns-server' && to === 'browser')) {
        // Horizontal line between browser and DNS
        const offset = isReturn ? 8 : -8;
        x1 = from === 'browser' ? nright(from) : nleft(from);
        y1 = ncy(from) + offset;
        x2 = to === 'browser' ? nright(to) : nleft(to);
        y2 = ncy(to) + offset;
      } else if ((from === 'browser' && to === 'website') || (from === 'website' && to === 'browser')) {
        // Diagonal: browser to website (top)
        x1 = from === 'browser' ? nright(from) - 20 : ncx(from);
        y1 = from === 'browser' ? ntop(from) : nbottom(from);
        x2 = to === 'browser' ? nright(to) - 20 : ncx(to);
        y2 = to === 'browser' ? ntop(to) : nbottom(to);
      } else if ((from === 'browser' && to === 'ad-server') || (from === 'ad-server' && to === 'browser')) {
        // Diagonal: browser to ad-server (bottom)
        x1 = from === 'browser' ? nright(from) - 20 : ncx(from);
        y1 = from === 'browser' ? nbottom(from) : ntop(from);
        x2 = to === 'browser' ? nright(to) - 20 : ncx(to);
        y2 = to === 'browser' ? nbottom(to) : ntop(to);
      } else {
        x1 = ncx(from); y1 = ncy(from);
        x2 = ncx(to); y2 = ncy(to);
      }
    }

    return { x1, y1, x2, y2 };
  }

  function getLineColor(type: 'dns' | 'http' | 'blocked') {
    switch (type) {
      case 'dns': return COLOR_DNS;
      case 'http': return COLOR_HTTP;
      case 'blocked': return COLOR_BLOCKED;
    }
  }

  function getLineLength(from: NodeId, to: NodeId, isReturn: boolean) {
    const { x1, y1, x2, y2 } = getLineEndpoints(from, to, isReturn);
    return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
  }

  // ── Animation ──────────────────────────────────────────────────────────────

  function ease(t: number) { return t < 0.5 ? 2*t*t : -1+(4-2*t)*t; }

  function animateStep(step: Step, cb: () => void) {
    const { from, to, lineType, isReturn, packetLabel } = step;
    const color = getLineColor(lineType);
    const { x1, y1, x2, y2 } = getLineEndpoints(from, to, isReturn);

    dot = { x: x1, y: y1, color, visible: true, label: packetLabel || '' };
    litNodes = { ...litNodes, [from]: color };
    badge = null;

    // Start animating line
    animatingLine = { from, to, color, isReturn, progress: 0 };

    let t0: number | null = null;
    const duration = 600;

    function frame(ts: number) {
      if (!t0) t0 = ts;
      const raw = Math.min((ts - t0) / duration, 1);
      const t = ease(raw);

      const px = x1 + (x2 - x1) * t;
      const py = y1 + (y2 - y1) * t;
      dot = { x: px, y: py, color, visible: true, label: packetLabel || '' };

      // Update line progress
      animatingLine = { from, to, color, isReturn, progress: t };

      if (raw < 1) {
        requestAnimationFrame(frame);
      } else {
        dot = { ...dot, visible: false };
        litNodes = { ...litNodes, [to]: color };

        // Move to completed lines
        completedLines = [...completedLines, { from, to, color, isReturn, progress: 1 }];
        animatingLine = null;

        if (step.blockedInAdBlockMode && adBlockEnabled) {
          showBlockedIndicator = true;
        }

        // Show badge
        if (step.hitNode && step.hitText && step.hitColor) {
          badge = { node: step.hitNode, text: step.hitText, color: step.hitColor };
        }

        setTimeout(() => {
          cb();
        }, 200);
      }
    }
    requestAnimationFrame(frame);
  }

  // ── Badge positioning ──────────────────────────────────────────────────────

  function badgePos(id: NodeId) {
    const n = NODES[id];
    const w = isVertical ? 100 : 140;
    const h = isVertical ? 20 : 24;
    // Always position badge below the node
    return {
      x: n.x + (n.w - w) / 2,
      y: n.y + n.h + 4,
      cx: ncx(id),
      cy: n.y + n.h + 4 + h / 2,
      w,
      h
    };
  }

  // ── Step logic ─────────────────────────────────────────────────────────────

  function getStateAtStep(stepIndex: number) {
    const nodes: Record<NodeId, string> = {
      'browser': '', 'dns-server': '', 'website': '', 'ad-server': ''
    };
    const lines: LineState[] = [];
    let blocked = false;
    let lastBadge: { node: NodeId; text: string; color: 'blue' | 'green' | 'red' } | null = null;

    for (let i = 0; i <= stepIndex; i++) {
      const step = activeSteps[i];
      const color = getLineColor(step.lineType);

      nodes[step.from] = color;
      nodes[step.to] = color;
      lines.push({ from: step.from, to: step.to, color, isReturn: step.isReturn, progress: 1 });

      if (step.blockedInAdBlockMode && adBlockEnabled) {
        blocked = true;
      }

      if (step.hitNode && step.hitText && step.hitColor) {
        lastBadge = { node: step.hitNode, text: step.hitText, color: step.hitColor };
      }
    }

    return { nodes, lines, blocked, badge: lastBadge };
  }

  function applyStateAtStep(stepIndex: number) {
    const state = getStateAtStep(stepIndex);
    litNodes = state.nodes;
    completedLines = state.lines;
    animatingLine = null;
    showBlockedIndicator = state.blocked;
    badge = state.badge;

    const step = activeSteps[stepIndex];
    stepLabel = step.label;
    desc = step.desc;
  }

  function advance() {
    if (busy || done) return;
    current++;
    busy = true;
    badge = null;

    const step = activeSteps[current];
    stepLabel = step.label;
    desc = step.desc;

    animateStep(step, () => {
      busy = false;
      if (current === totalSteps - 1) done = true;
    });
  }

  function goBack() {
    if (busy || current < 0) return;
    current--;
    done = false;

    if (current < 0) {
      reset();
    } else {
      applyStateAtStep(current);
    }
  }

  function reset() {
    current = -1;
    busy = false;
    done = false;
    stepLabel = '';
    showBlockedIndicator = false;
    badge = null;
    dot = { x: 0, y: 0, color: '', visible: false, label: '' };
    litNodes = { 'browser': '', 'dns-server': '', 'website': '', 'ad-server': '' };
    completedLines = [];
    animatingLine = null;
    desc = adBlockEnabled
      ? `Ad-blocker is ON. Press "Start" to see how blocked requests work.`
      : `Ad-blocker is OFF. Press "Start" to see a typical page load with ads.`;
  }

  function toggleAdBlock() {
    adBlockEnabled = !adBlockEnabled;
    reset();
  }

  // Initialize description
  $effect(() => {
    if (current === -1) {
      desc = adBlockEnabled
        ? `Ad-blocker is ON. Press "Start" to see how blocked requests work.`
        : `Ad-blocker is OFF. Press "Start" to see a typical page load with ads.`;
    }
  });

  // ── Render line with progress ──────────────────────────────────────────────

  function renderLine(line: LineState) {
    const { x1, y1, x2, y2 } = getLineEndpoints(line.from, line.to, line.isReturn);
    const length = getLineLength(line.from, line.to, line.isReturn);
    const drawn = length * line.progress;
    const dashArray = `${drawn} ${length - drawn}`;
    return { x1, y1, x2, y2, dashArray };
  }
</script>

<!-- ── Markup ──────────────────────────────────────────────────────────────── -->

<div class="mx-auto my-8 w-full max-w-2xl font-sans">

  <svg
    width="100%"
    viewBox="0 0 {viewBoxWidth} {viewBoxHeight}"
    aria-label="Interactive ad-blocking diagram"
    class="overflow-visible"
  >
    <defs>
      <marker id="arr-dns-ab" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
        <path d="M2 1L8 5L2 9" fill="none" stroke={COLOR_DNS} stroke-width="1.5" stroke-linecap="round"/>
      </marker>
      <marker id="arr-http-ab" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
        <path d="M2 1L8 5L2 9" fill="none" stroke={COLOR_HTTP} stroke-width="1.5" stroke-linecap="round"/>
      </marker>
      <marker id="arr-blocked-ab" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
        <path d="M2 1L8 5L2 9" fill="none" stroke={COLOR_BLOCKED} stroke-width="1.5" stroke-linecap="round"/>
      </marker>
    </defs>

    <!-- Completed lines -->
    {#each completedLines as line}
      {@const { x1, y1, x2, y2 } = getLineEndpoints(line.from, line.to, line.isReturn)}
      <line
        {x1} {y1} {x2} {y2}
        stroke={line.color}
        stroke-width="2"
        marker-end={line.color === COLOR_HTTP ? 'url(#arr-http-ab)' : line.color === COLOR_BLOCKED ? 'url(#arr-blocked-ab)' : 'url(#arr-dns-ab)'}
      />
    {/each}

    <!-- Animating line (draws progressively, no arrow until complete) -->
    {#if animatingLine}
      {@const rendered = renderLine(animatingLine)}
      <line
        x1={rendered.x1} y1={rendered.y1}
        x2={rendered.x2} y2={rendered.y2}
        stroke={animatingLine.color}
        stroke-width="2"
        stroke-dasharray={rendered.dashArray}
      />
    {/if}

    <!-- Nodes -->
    {#each Object.entries(NODES) as [id, n]}
      {@const nodeId = id as NodeId}
      {@const isAdServer = nodeId === 'ad-server'}
      {@const isBlocked = isAdServer && adBlockEnabled && showBlockedIndicator}
      {@const lit = litNodes[nodeId]}

      <g class="transition-opacity duration-300" opacity={isBlocked ? 0.4 : 1}>
        <rect
          x={n.x} y={n.y} width={n.w} height={n.h} rx="8"
          fill="var(--card)"
          stroke={isBlocked ? COLOR_BLOCKED : lit || 'var(--border)'}
          stroke-width={lit || isBlocked ? 2 : 0.5}
          stroke-dasharray={isBlocked ? '4,2' : 'none'}
        />

        <!-- Icon based on node type -->
        {#if nodeId === 'browser'}
          <g transform="translate({n.x + n.w/2}, {n.y + (isVertical ? 18 : 22)})">
            <rect x="-10" y="-10" width="20" height="14" rx="2" fill="none" stroke="var(--muted-foreground)" stroke-width="1.2"/>
            <line x1="0" y1="4" x2="0" y2="8" stroke="var(--muted-foreground)" stroke-width="1.2"/>
            <line x1="-5" y1="8" x2="5" y2="8" stroke="var(--muted-foreground)" stroke-width="1.2"/>
          </g>
        {:else if nodeId === 'dns-server'}
          <g transform="translate({n.x + n.w/2}, {n.y + (isVertical ? 18 : 22)})">
            <circle cx="0" cy="0" r="9" fill="none" stroke={adBlockEnabled ? COLOR_HTTP : 'var(--muted-foreground)'} stroke-width="1.2"/>
            {#if adBlockEnabled}
              <text x="0" y="4" font-size="12" font-weight="bold" fill={COLOR_HTTP} text-anchor="middle">✓</text>
            {:else}
              <text x="0" y="3" font-size="8" font-weight="500" fill="var(--muted-foreground)" text-anchor="middle">DNS</text>
            {/if}
          </g>
        {:else if nodeId === 'website'}
          <g transform="translate({n.x + n.w/2}, {n.y + (isVertical ? 18 : 22)})">
            <rect x="-10" y="-8" width="20" height="16" rx="2" fill="none" stroke="var(--muted-foreground)" stroke-width="1"/>
            <line x1="-10" y1="-4" x2="10" y2="-4" stroke="var(--muted-foreground)" stroke-width="0.8"/>
            <circle cx="-6" cy="-6" r="1.5" fill="var(--muted-foreground)"/>
          </g>
        {:else if nodeId === 'ad-server'}
          <g transform="translate({n.x + n.w/2}, {n.y + (isVertical ? 18 : 22)})">
            <rect x="-10" y="-8" width="20" height="16" rx="2" fill="none" stroke={isBlocked ? COLOR_BLOCKED : 'var(--muted-foreground)'} stroke-width="1"/>
            <text x="0" y="4" font-size="9" font-weight="bold" fill={isBlocked ? COLOR_BLOCKED : 'var(--muted-foreground)'} text-anchor="middle">AD</text>
          </g>
        {/if}

        <text x={n.x + n.w/2} y={n.y + n.h - (isVertical ? 6 : 8)} class="node-title" text-anchor="middle">{n.title}</text>
      </g>
    {/each}

    <!-- Travelling dot + packet label -->
    {#if dot.visible}
      <circle cx={dot.x} cy={dot.y} r="5" fill={dot.color}/>
      {#if dot.label}
        {@const pw = dot.label.length * 5.5 + 14}
        <rect x={dot.x - pw/2} y={dot.y - 24} width={pw} height="18" rx="4" fill={dot.color} opacity="0.95"/>
        <text x={dot.x} y={dot.y - 12} class="packet-label">{dot.label}</text>
      {/if}
    {/if}

    <!-- Badge -->
    {#if badge}
      {@const bp = badgePos(badge.node)}
      <g class="badge-pop">
        <rect
          x={bp.x} y={bp.y} width={bp.w} height={bp.h} rx={isVertical ? 4 : 6}
          fill={badge.color === 'green' ? 'oklch(0.95 0.05 145)'
              : badge.color === 'red'   ? 'oklch(0.95 0.05 25)'
              :                           'oklch(0.95 0.05 240)'}
          stroke={badge.color === 'green' ? 'oklch(0.75 0.15 145)'
                : badge.color === 'red'   ? 'oklch(0.75 0.15 25)'
                :                           'oklch(0.75 0.15 240)'}
          stroke-width="0.5"
        />
        <text
          x={bp.cx} y={bp.cy}
          text-anchor="middle"
          font-size={isVertical ? 8 : 10}
          font-weight="500"
          fill={badge.color === 'green' ? 'oklch(0.40 0.15 145)'
              : badge.color === 'red'   ? 'oklch(0.40 0.15 25)'
              :                           'oklch(0.40 0.15 240)'}
        >{badge.text}</text>
      </g>
    {/if}

    <!-- Step label (desktop only, positioned at very bottom) -->
    {#if stepLabel && !isVertical}
      <text x={viewBoxWidth / 2} y={viewBoxHeight - 12} class="step-label">{stepLabel}</text>
    {/if}
  </svg>

  <!-- Mobile step label -->
  {#if isVertical && stepLabel}
    <p class="mt-2 text-center text-xs font-medium text-muted-foreground">{stepLabel}</p>
  {/if}

  <!-- Description -->
  <p class="mb-2 mt-2 h-20 overflow-y-auto rounded-lg bg-muted px-3 py-2 text-sm leading-relaxed text-muted-foreground sm:mb-3 sm:mt-4 sm:h-auto sm:min-h-16 sm:px-3.5 sm:py-2.5">
    {desc}
  </p>

  <!-- Controls -->
  <div class="flex flex-wrap items-center gap-2 pb-2 sm:pb-0">
    <!-- Back button -->
    <button
      onclick={goBack}
      disabled={busy || current < 0}
      class="inline-flex h-8 w-16 items-center justify-center gap-1 whitespace-nowrap rounded-full border bg-background text-xs font-medium shadow-xs transition-all hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 sm:h-9 sm:w-20 sm:text-sm dark:border-input dark:bg-input/30 dark:hover:bg-input/50"
    >
      ← Back
    </button>

    <!-- Next/Start button -->
    {#if !done}
      <button
        onclick={advance}
        disabled={busy}
        class="inline-flex h-8 w-16 items-center justify-center gap-1 whitespace-nowrap rounded-full border bg-background text-xs font-medium shadow-xs transition-all hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 sm:h-9 sm:w-20 sm:text-sm dark:border-input dark:bg-input/30 dark:hover:bg-input/50"
      >
        {current < 0 ? 'Start' : 'Next →'}
      </button>
    {:else}
      <button
        onclick={reset}
        class="inline-flex h-8 w-16 items-center justify-center gap-1 whitespace-nowrap rounded-full border bg-background text-xs font-medium shadow-xs transition-all hover:bg-accent hover:text-accent-foreground sm:h-9 sm:w-20 sm:text-sm dark:border-input dark:bg-input/30 dark:hover:bg-input/50"
      >
        Restart
      </button>
    {/if}

    <!-- Reset button -->
    <button
      onclick={reset}
      disabled={current < 0}
      class="inline-flex h-8 w-8 items-center justify-center whitespace-nowrap rounded-full border bg-background text-xs font-medium shadow-xs transition-all hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 sm:h-9 sm:w-9 sm:text-sm dark:border-input dark:bg-input/30 dark:hover:bg-input/50"
      title="Reset"
    >
      ↺
    </button>

    <!-- Step counter -->
    <span class="text-xs text-muted-foreground">
      {Math.max(current + 1, 0)} / {totalSteps}
    </span>

    <!-- Spacer -->
    <div class="flex-1"></div>

    <!-- Ad-blocker toggle -->
    <div class="flex items-center gap-1.5 sm:gap-2">
      <span class="text-xs text-muted-foreground hidden sm:inline">Blocker:</span>
      <button
        onclick={toggleAdBlock}
        class="relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 sm:h-6 sm:w-11"
        class:bg-green-500={adBlockEnabled}
        class:bg-gray-300={!adBlockEnabled}
        class:dark:bg-green-600={adBlockEnabled}
        class:dark:bg-gray-600={!adBlockEnabled}
        aria-pressed={adBlockEnabled}
        aria-label="Toggle ad-blocker"
      >
        <span
          class="inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-md transition-transform duration-200 sm:h-4 sm:w-4"
          style="transform: translateX({adBlockEnabled ? (isVertical ? '1.125rem' : '1.5rem') : '0.125rem'})"
        ></span>
      </button>
      <span class="text-xs font-semibold" class:text-green-600={adBlockEnabled} class:dark:text-green-400={adBlockEnabled} class:text-gray-500={!adBlockEnabled}>
        {adBlockEnabled ? 'ON' : 'OFF'}
      </span>
    </div>
  </div>

</div>

<!-- ── Styles ─────────────────────────────────────────────────────────────── -->

<style>
  .node-title {
    font-size: 10px;
    font-weight: 500;
    fill: var(--foreground);
    font-family: var(--font-sans);
  }

  .packet-label {
    font-size: 9px;
    font-family: var(--font-mono);
    fill: white;
    text-anchor: middle;
  }

  .step-label {
    font-size: 11px;
    fill: var(--muted-foreground);
    text-anchor: middle;
    font-family: var(--font-sans);
  }

  .badge-pop {
    animation: badge-pop 0.28s ease forwards;
  }

  @keyframes badge-pop {
    0%   { opacity: 0; transform: translateY(5px); }
    65%  { opacity: 1; transform: translateY(-1px); }
    100% { opacity: 1; transform: translateY(0); }
  }
</style>
