"""
First-run module — F1 / F2.

Recreates the Claude Design handoff (final-GHVscC) first-boot flow on the
640×480 stage, in the H1 console grammar but stripped of chrome where the
design asks for silence.

Screens (class ⇄ handoff):
  F1Welcome   ⇄ F1-welcome.html         Apple/PS5-style minimal welcome:
                                         gradient "AA" wordmark, one line,
                                         one glowing CTA, ambient glow,
                                         tiny version anchor. No chrome,
                                         no feature ad, no build/sha.
  F2Checklist ⇄ F2-setup-checklist.html  quick-setup 2×2 step grid
                                         (done / current / locked) + a
                                         progress bar + a Continue CTA.

Per the product owner's swept spec the F2 steps are the four things a fresh
R36S actually needs — Bluetooth, Wi-Fi (optional, since the R36S has no
built-in radio), Input, BIOS — in consumer language, no geek terms.

Every class shares the screen shape: name, __init__(self, app, ...),
on_action(self, action), update(self, dt_ms), draw(self, surf). Actions are
strings: up/down/left/right/a/b/x/y/l/r.

This module only defines screens + module-level helpers; it never edits
main.py or widgets.py. Helpers not in the shared kit (ambient glow,
gradient pill CTA, step card, A-glyph chip) are defined below.
"""

from __future__ import annotations

import math

import pygame

from ..design import tokens as t
from ..shared import widgets as w
from ..shared import fonts, icons, render

SAFE = t.Space.SAFE_AREA            # 16
STAGE_W = t.VIEWPORT_WIDTH          # 640
STAGE_H = t.VIEWPORT_HEIGHT         # 480
BODY_W = STAGE_W - SAFE * 2         # 608
FOOTER_Y = 448                      # hairline above footer hints


# ─── module-level helpers ─────────────────────────────────────────────


def _ambient_glow(surf, cx, cy, radius=260):
    """
    Soft rose→cyan→transparent radial bloom centered at (cx, cy). Matches
    F1's `.glow` (radial-gradient rose .10 → cyan .06 → transparent). Drawn
    as concentric translucent rings so it stays cheap and reads as ambient.
    """
    layers = 22
    glow = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
    for i in range(layers, 0, -1):
        frac = i / layers                 # 1.0 (outer) → ~0 (inner)
        r = int(radius * frac)
        # rose dominates the core, cyan the mid; both fade out by the edge
        col = render._lerp(t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, frac * 0.55)
        # alpha peaks at the center and falls toward the rim
        a = int(14 * (1.0 - frac) ** 1.4)
        if a <= 0:
            continue
        pygame.draw.circle(glow, (col[0], col[1], col[2], a),
                           (radius, radius), r)
    surf.blit(glow, (cx - radius, cy - radius))


def _welcome_lockup(*, size_aa=52, size_ctrl=34):
    """
    F1's large wordmark: gradient "AA" + primary "head" on a baseline, then
    a lighter, smaller "Control" to the side. Returns a transparent surface
    sized to the full lockup with baseline-aligned parts.
    """
    big = t.TextStyle(t.SPACE_GROTESK_BOLD, size_aa, "Bold", -1.4)
    ctrl_style = t.TextStyle(t.SPACE_GROTESK_SEMIBOLD, size_ctrl, "SemiBold", -0.5)
    aa = w.gradient_text(big, "AA")
    head = fonts.render(big, "head", t.Color.TEXT_PRIMARY)
    ctrl = fonts.render(ctrl_style, "Control", t.Color.TEXT_SECONDARY)

    gap_word = 4       # AA→head (baseline lockup, tight)
    gap_ctrl = 14      # head→Control
    h = max(aa.get_height(), head.get_height(), ctrl.get_height())
    total_w = aa.get_width() + gap_word + head.get_width() + gap_ctrl + ctrl.get_width()
    s = pygame.Surface((total_w, h), pygame.SRCALPHA)

    # baseline = bottom of the big glyphs; smaller "Control" sits on it too
    base = h
    x = 0
    s.blit(aa, (x, base - aa.get_height())); x += aa.get_width() + gap_word
    s.blit(head, (x, base - head.get_height())); x += head.get_width() + gap_ctrl
    # nudge Control up a hair so it reads as set on the same baseline
    s.blit(ctrl, (x, base - ctrl.get_height() - 2))
    return s


def _a_chip(diameter=22):
    """Small rose-outlined circular 'A' button glyph used inside the CTA."""
    s = pygame.Surface((diameter, diameter), pygame.SRCALPHA)
    pygame.draw.circle(s, t.with_alpha(t.Color.ACCENT_ROSE, 0.5),
                       (diameter // 2, diameter // 2), diameter // 2, 2)
    style = t.TextStyle(t.SPACE_GROTESK_BOLD, 11, "Bold", 0.0)
    lbl = fonts.render(style, "A", t.Color.ACCENT_ROSE)
    s.blit(lbl, ((diameter - lbl.get_width()) // 2,
                 (diameter - lbl.get_height()) // 2))
    return s


def _glow_pill(surf, rect, *, parts, glow=True):
    """
    A rounded glass pill with the focus-glow gradient hairline border and a
    soft outer bloom — F1's `.start` / F2's `.next`. `parts` is a list of
    pre-rendered surfaces laid out left→right, centered, with an 8px gap.
    """
    if glow:
        g, gpos = render.focus_glow(rect, t.Radius.PILL)
        surf.blit(g, gpos)
    body = render.rounded_gradient(
        (rect.w, rect.h), t.Color.SURFACE_GLASS[:3], t.Color.SURFACE_BASE,
        t.Radius.PILL, 135)
    # gradient hairline ring (rose→cyan), like the ::after mask border
    body.blit(render.gradient_border((rect.w, rect.h), t.Radius.PILL, width=2),
              (0, 0))
    gap = 8
    total = sum(p.get_width() for p in parts) + gap * (len(parts) - 1)
    x = (rect.w - total) // 2
    cy = rect.h // 2
    for p in parts:
        body.blit(p, (x, cy - p.get_height() // 2))
        x += p.get_width() + gap
    surf.blit(body, rect.topleft)


def _step_card(surf, rect, *, num, label, optional, detail, meta, icon_name,
               status, t_ms=0):
    """
    F2 step card. `status` ∈ {"done","current","locked"} drives the whole
    look: mint accents for done, the rose→cyan signature for current, a dim
    veil for locked. Layout: numbered badge + label (+ optional tag) +
    leading status icon on the top row, a prose detail block, and a mono
    meta line pinned to the bottom.
    """
    radius = t.Radius.CARD
    pad_x, pad_t = 14, 12

    if status == "current":
        body = render.rounded_gradient(
            (rect.w, rect.h), t.Color.SURFACE_ELEVATED, t.Color.SURFACE_BASE,
            radius, 160)
    else:
        body = render.rounded_gradient(
            (rect.w, rect.h), t.Color.SURFACE_BASE, t.Color.BG_MID, radius, 160)
        border = (t.with_alpha(t.Color.SUCCESS, 0.35) if status == "done"
                  else t.Color.BORDER_SOFT)
        pygame.draw.rect(body, border, (0, 0, rect.w, rect.h),
                         width=1, border_radius=radius)

    dim = (status == "locked")
    label_col = t.Color.TEXT_PRIMARY
    icon_col = (t.Color.SUCCESS if status == "done"
                else t.Color.ACCENT_ROSE if status == "current"
                else t.Color.TEXT_MUTED)

    # ── numbered badge (top-left) ──
    nb = 24
    nbg = pygame.Surface((nb, nb), pygame.SRCALPHA)
    if status == "done":
        pygame.draw.circle(nbg, t.with_alpha(t.Color.SUCCESS, 0.18),
                           (nb // 2, nb // 2), nb // 2)
        pygame.draw.circle(nbg, t.with_alpha(t.Color.SUCCESS, 0.4),
                           (nb // 2, nb // 2), nb // 2, 1)
        chk = icons.icon("ph-check", 12, t.Color.SUCCESS)
        nbg.blit(chk, ((nb - 12) // 2, (nb - 12) // 2))
    elif status == "current":
        grad = render.rounded_gradient((nb, nb), t.Color.ACCENT_ROSE,
                                       t.Color.ACCENT_CYAN, nb // 2, 135)
        nbg.blit(grad, (0, 0))
        nlbl = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 11, "Regular", 0.0),
                            str(num), t.Color.BG_MID)
        nbg.blit(nlbl, ((nb - nlbl.get_width()) // 2,
                        (nb - nlbl.get_height()) // 2))
    else:
        pygame.draw.circle(nbg, t.with_alpha(t.Color.SURFACE_GLASS[:3], 0.6),
                           (nb // 2, nb // 2), nb // 2)
        pygame.draw.circle(nbg, t.Color.BORDER_SOFT, (nb // 2, nb // 2),
                           nb // 2, 1)
        nlbl = fonts.render(t.Type.META_SM, str(num), t.Color.TEXT_SECONDARY)
        nbg.blit(nlbl, ((nb - nlbl.get_width()) // 2,
                        (nb - nlbl.get_height()) // 2))
    body.blit(nbg, (pad_x, pad_t))

    # ── leading status icon (top-right) ──
    ic = icons.icon(icon_name, 22, icon_col)
    body.blit(ic, (rect.w - pad_x - 22, pad_t + (nb - 22) // 2))

    # ── label (+ optional tag) ──
    lx = pad_x + nb + 8
    lbl = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, 15, "Bold", -0.2),
                       label, label_col)
    lbl_y = pad_t + (nb - lbl.get_height()) // 2
    # clip label so it never collides with the icon / optional tag
    body.blit(lbl, (lx, lbl_y))
    tx = lx + lbl.get_width() + 6
    if optional:
        otag = fonts.render(t.Type.META_SM, "optional", t.Color.TEXT_MUTED)
        body.blit(otag, (tx, lbl_y + (lbl.get_height() - otag.get_height())))

    # ── detail prose (wrapped) ──
    dy = pad_t + nb + 10
    _wrap_text(body, detail, t.Type.BODY_SM, t.Color.TEXT_SECONDARY,
               pad_x, dy, rect.w - pad_x * 2, line_h=15, max_lines=3,
               emphasis=t.Color.TEXT_PRIMARY)

    # ── meta line (mono, bottom) ──
    meta_text, meta_kind = meta
    my = rect.h - 12 - 12
    if meta_kind == "done":
        dr = 4
        pygame.draw.circle(body, t.Color.SUCCESS, (pad_x + dr, my + 6), dr)
        ms = fonts.render(t.Type.META_SM, meta_text, t.Color.SUCCESS)
        body.blit(ms, (pad_x + dr * 2 + 6, my + 6 - ms.get_height() // 2))
    elif meta_kind == "current":
        # pulsing cyan dot
        pulse = 0.5 + 0.5 * math.sin(t_ms / 360.0)
        dr = 3
        col = render._lerp(t.Color.BG_MID, t.Color.ACCENT_CYAN, 0.4 + 0.6 * pulse)
        pygame.draw.circle(body, col, (pad_x + dr, my + 6), dr)
        ms = fonts.render(t.Type.META_SM, meta_text, t.Color.ACCENT_CYAN)
        body.blit(ms, (pad_x + dr * 2 + 6, my + 6 - ms.get_height() // 2))
    else:
        ms = fonts.render(t.Type.META_SM, meta_text, t.Color.TEXT_MUTED)
        body.blit(ms, (pad_x, my + 6 - ms.get_height() // 2))

    if dim:
        body.set_alpha(int(255 * 0.55))

    if status == "current":
        # signature: outer glow + gradient ring over the elevated body
        g, gpos = render.focus_glow(rect, radius)
        surf.blit(g, gpos)
        surf.blit(body, rect.topleft)
        surf.blit(render.gradient_border((rect.w, rect.h), radius), rect.topleft)
    else:
        surf.blit(body, rect.topleft)


def _wrap_text(surf, text, style, color, x, y, max_w, *, line_h,
               max_lines=3, emphasis=None):
    """
    Minimal greedy word-wrap. `**bold**` spans render in `emphasis` color.
    Returns the y below the last drawn line.
    """
    words = text.split(" ")
    line = ""
    drawn = 0

    def flush(s):
        nonlocal y, drawn
        _render_rich(surf, s, style, color, x, y, emphasis)
        y += line_h
        drawn += 1

    for word in words:
        trial = (line + " " + word).strip()
        # measure with markers stripped
        plain = trial.replace("**", "")
        if fonts.measure(style, plain)[0] <= max_w or not line:
            line = trial
        else:
            flush(line)
            line = word
            if drawn >= max_lines - 1:
                break
    if line and drawn < max_lines:
        flush(line)
    return y


def _render_rich(surf, text, style, color, x, y, emphasis):
    """Render a single line honoring `**bold**` → emphasis color spans."""
    if emphasis is None or "**" not in text:
        surf.blit(fonts.render(style, text, color), (x, y))
        return
    cx = x
    for i, chunk in enumerate(text.split("**")):
        if not chunk:
            continue
        col = emphasis if (i % 2 == 1) else color
        g = fonts.render(style, chunk, col)
        surf.blit(g, (cx, y))
        cx += g.get_width()


# ═════════════════════════════════════════════════════════════════════
# F1 — Welcome (Apple / PS5-style minimal)
# ═════════════════════════════════════════════════════════════════════


class F1Welcome:
    """
    First boot. Just the wordmark + one short line + one glowing CTA, over
    an ambient radial bloom, with a tiny version anchor bottom-left. No
    status bar, no footer-hint chrome, no feature list, no build/sha — the
    old salesy welcome was explicitly rejected.

    A (or Start) advances to the quick-setup checklist; B exits.
    """
    name = "first-welcome"

    def __init__(self, app, version="1.4.0"):
        self.app = app
        self.version = version
        self._t = 0.0          # ms, for the gentle glow breathe

    # ── input ──────────────────────────────────────────────────────────
    def on_action(self, action):
        if action in ("a", "start"):
            self.app.push(F2Checklist(self.app))
        elif action == "b":
            self.app.request_exit()

    def update(self, dt_ms):
        self._t += dt_ms

    # ── draw ───────────────────────────────────────────────────────────
    def draw(self, surf):
        cx = STAGE_W // 2
        glow_cy = int(STAGE_H * 0.48)
        # ambient bloom breathes very slightly
        breathe = 1.0 + 0.03 * math.sin(self._t / 1400.0)
        _ambient_glow(surf, cx, glow_cy, radius=int(260 * breathe))

        # center column: wordmark, tagline, CTA — vertically grouped
        lockup = _welcome_lockup()
        tag = fonts.render(t.TextStyle(t.INTER_REGULAR, 15, "Regular", 0.0),
                           "Let's set things up.", t.Color.TEXT_SECONDARY)

        gap1 = 16        # wordmark → tag
        gap2 = 22        # tag → CTA
        cta_h = 48
        # CTA content sizes
        a_chip = _a_chip(22)
        start_lbl = fonts.render(
            t.TextStyle(t.SPACE_GROTESK_SEMIBOLD, 15, "SemiBold", -0.2),
            "Start", t.Color.TEXT_PRIMARY)
        cta_w = 26 + a_chip.get_width() + 12 + start_lbl.get_width() + 26

        block_h = (lockup.get_height() + gap1 + tag.get_height()
                   + gap2 + cta_h)
        top = (STAGE_H - block_h) // 2

        y = top
        surf.blit(lockup, (cx - lockup.get_width() // 2, y))
        y += lockup.get_height() + gap1
        surf.blit(tag, (cx - tag.get_width() // 2, y))
        y += tag.get_height() + gap2

        cta_rect = pygame.Rect(cx - cta_w // 2, y, cta_w, cta_h)
        _glow_pill(surf, cta_rect, parts=[a_chip, start_lbl])

        # tiny version anchor, bottom-left
        ver = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 10, "Regular", 0.3),
                           f"v {self.version}", t.Color.TEXT_MUTED)
        surf.blit(ver, (SAFE, STAGE_H - 14 - ver.get_height()))


# ═════════════════════════════════════════════════════════════════════
# F2 — Quick setup checklist (2×2 steps)
# ═════════════════════════════════════════════════════════════════════

# body grid geometry (between status bar 48 and footer 448)
_HERO_Y = 56
_PROG_Y = 92
_GRID_TOP = 108
_CTA_H = 40
_CTA_Y = FOOTER_Y - 8 - _CTA_H          # 400
_GRID_BOTTOM = _CTA_Y - 10              # 390
_GAP = 10
_CARD_W = (BODY_W - _GAP) // 2          # 299
_CARD_H = (_GRID_BOTTOM - _GRID_TOP - _GAP) // 2  # 136
_COL_X = [SAFE, SAFE + _CARD_W + _GAP]
_ROW_Y = [_GRID_TOP, _GRID_TOP + _CARD_H + _GAP]


class F2Checklist:
    """
    Quick-setup checklist: a 2×2 grid of the four things a fresh R36S needs.
    One step is "current" (the rose→cyan signature), earlier ones are
    "done" (mint), later ones are "locked" (dim). A progress bar with a
    rose-ringed marker sits up top; a glowing Continue pill sits bottom-right.

    The Wi-Fi step is flagged optional — the R36S has no built-in Wi-Fi, so a
    user without a USB dongle can skip it with X.

    D-pad moves focus across unlocked steps; A enters the current step
    (coming-soon toast for now); X skips the current step; B goes back.
    """
    name = "first-checklist"

    def __init__(self, app, current=0):
        self.app = app
        self._t = 0.0
        # (key, label, optional, detail, (meta, kind), icon)
        # status is derived from `current`: < current = done, == current =
        # current, > current = locked.
        self.steps = [
            ("bluetooth", "Connect Bluetooth", False,
             "Pair a controller, headset, or speaker. Most gamepads pair in "
             "a few seconds.",
             ("ready to pair", "current"), "ph-bluetooth"),
            ("wifi", "Connect to Wi-Fi", True,
             "The R36S has no built-in Wi-Fi. **Skip if you don't have a "
             "dongle** — plug a USB adapter to connect.",
             ("optional · skip if no dongle", "locked"), "ph-wifi-high"),
            ("input", "Set up controls", False,
             "A quick stick calibration maps your range and clears any "
             "drift. Takes about 12 seconds.",
             ("queued · sticks + buttons", "locked"), "ph-game-controller"),
            ("bios", "Add BIOS files", False,
             "Some consoles need a BIOS to play. We'll show which are "
             "missing and where to drop them.",
             ("queued · PS1 · Saturn · 3DO", "locked"), "ph-file-text"),
        ]
        self.current = current      # index of the active step
        self.focus = current        # which card the d-pad is on

    # ── status helpers ──────────────────────────────────────────────────
    def _status(self, i):
        if i < self.current:
            return "done"
        if i == self.current:
            return "current"
        return "locked"

    def _meta_for(self, i):
        """Override the seeded meta so it matches the derived status."""
        key, label, optional, detail, meta, icon = self.steps[i]
        st = self._status(i)
        if st == "done":
            return ("done · saved", "done")
        if st == "current":
            base = meta[0] if meta[1] != "locked" else "in progress…"
            return (base, "current")
        return meta if meta[1] == "locked" else (meta[0], "locked")

    # ── input ──────────────────────────────────────────────────────────
    def on_action(self, action):
        r, c = divmod(self.focus, 2)
        if action == "left":
            self.focus = r * 2 + max(0, c - 1)
        elif action == "right":
            self.focus = r * 2 + min(1, c + 1)
        elif action == "up":
            self.focus = max(0, r - 1) * 2 + c
        elif action == "down":
            self.focus = min(1, r + 1) * 2 + c
        elif action == "a":
            self._enter(self.steps[self.current][0])
        elif action == "x":
            self._skip()
        elif action == "b":
            self.app.pop()

    def _enter(self, key):
        label = self.steps[self.current][1]
        self.app.toast(f"{label} — coming soon")

    def _skip(self):
        if self.current < len(self.steps) - 1:
            self.current += 1
            self.focus = self.current
            self.app.toast("Step skipped")
        else:
            self.app.toast("Setup complete")

    def update(self, dt_ms):
        self._t += dt_ms

    # ── draw ───────────────────────────────────────────────────────────
    def draw(self, surf):
        self._draw_status_bar(surf)
        self._draw_hero(surf)
        self._draw_progress(surf)

        for i in range(len(self.steps)):
            key, label, optional, detail, _meta, icon = self.steps[i]
            r, c = divmod(i, 2)
            rect = pygame.Rect(_COL_X[c], _ROW_Y[r], _CARD_W, _CARD_H)
            _step_card(surf, rect, num=i + 1, label=label, optional=optional,
                       detail=detail, meta=self._meta_for(i), icon_name=icon,
                       status=self._status(i), t_ms=self._t)
            # focus ring on a non-current focused card (subtle, no scale)
            if i == self.focus and self._status(i) != "current":
                surf.blit(render.gradient_border((rect.w, rect.h),
                                                 t.Radius.CARD), rect.topleft)

        self._draw_cta(surf)
        w.footer_hints(surf, [("A", "continue"), ("X", "skip step"),
                              ("B", "back")])

    def _draw_status_bar(self, surf):
        # left: plain section title (this flow has its own title, not the
        # brand wordmark — matches F2's <span class="sb-title">).
        title = fonts.render(t.Type.TITLE, "Quick setup", t.Color.TEXT_PRIMARY)
        surf.blit(title, (SAFE, 14))
        # right cluster: "first-time setup" tag · battery · clock
        rx = STAGE_W - SAFE
        clk = fonts.render(t.Type.MONO_TECH, "14:32", t.Color.TEXT_PRIMARY)
        rx -= clk.get_width(); surf.blit(clk, (rx, 18))
        rx -= t.Space.L
        batt = w.battery_chip(87)
        rx -= batt.get_width(); surf.blit(batt, (rx, 16))
        rx -= t.Space.L
        tag = fonts.render(t.Type.META_SM, "first-time setup", t.Color.TEXT_MUTED)
        rx -= tag.get_width(); surf.blit(tag, (rx, 18))

    def _draw_hero(self, surf):
        h1 = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, 22, "Bold", -0.5),
                          "Let's get you ready", t.Color.TEXT_PRIMARY)
        surf.blit(h1, (SAFE, _HERO_Y))
        # right: "step N of 4 · ~90 s"
        done = sum(1 for i in range(len(self.steps)) if self._status(i) == "done")
        n = self.current + 1
        rx = STAGE_W - SAFE
        tail = fonts.render(t.Type.META_SM, f" of {len(self.steps)} · ~90 s",
                            t.Color.ACCENT_CYAN)
        num = fonts.render(t.Type.LABEL_SM, str(n), t.Color.TEXT_PRIMARY)
        lead = fonts.render(t.Type.META_SM, "step ", t.Color.ACCENT_CYAN)
        cy = _HERO_Y + (h1.get_height() - tail.get_height()) // 2
        x = rx - tail.get_width(); surf.blit(tail, (x, cy))
        x -= num.get_width(); surf.blit(num, (x, _HERO_Y + (h1.get_height() - num.get_height()) // 2))
        x -= lead.get_width(); surf.blit(lead, (x, cy))

    def _draw_progress(self, surf):
        # progress reflects completed steps over total
        total = len(self.steps)
        frac = (self.current) / total + (1 / total) * 0.5  # mid of current step
        frac = max(0.04, min(1.0, frac))
        w.progress_bar(surf, SAFE, _PROG_Y, BODY_W, frac, h=4)
        # rose-ringed marker dot at the fill head
        mx = SAFE + int(BODY_W * frac)
        my = _PROG_Y + 2
        pygame.draw.circle(surf, t.Color.TEXT_PRIMARY, (mx, my), 5)
        pygame.draw.circle(surf, t.Color.ACCENT_ROSE, (mx, my), 6, 2)

    def _draw_cta(self, surf):
        # left: "X skip this step" secondary hint
        skip_x = fonts.render(t.Type.META_SM, "X", t.Color.TEXT_SECONDARY)
        skip_l = fonts.render(t.Type.META_SM, " skip this step", t.Color.TEXT_MUTED)
        cy = _CTA_Y + _CTA_H // 2
        surf.blit(skip_x, (SAFE, cy - skip_x.get_height() // 2))
        surf.blit(skip_l, (SAFE + skip_x.get_width(), cy - skip_l.get_height() // 2))

        # right: glowing Continue pill with a caret
        lbl = fonts.render(
            t.TextStyle(t.INTER_SEMIBOLD, 13, "SemiBold", 0.0),
            "Continue", t.Color.TEXT_PRIMARY)
        caret = icons.icon("ph-caret-right", 14, t.Color.ACCENT_CYAN)
        cta_w = 18 + lbl.get_width() + 8 + caret.get_width() + 18
        rect = pygame.Rect(STAGE_W - SAFE - cta_w, _CTA_Y, cta_w, _CTA_H)
        _glow_pill(surf, rect, parts=[lbl, caret])
