<#
.SYNOPSIS
    PowerShell port of the Terminal Panel Design System (docs/TERMINAL-DESIGN.md).

.DESCRIPTION
    Mirror of skills/_lib/term.sh for PowerShell scripts in the claude-mods
    family. Provides chrome rendering (panels, sections, leaves), glyph
    registries (brand, health, diagram icon), color tokens, and width-aware
    text utilities so PowerShell-based skills produce output that's visually
    coherent with bash and Python siblings (fleet-ops, summon, etc.).

    Source from any PowerShell skill script:

        $LibDir = Join-Path $PSScriptRoot '..\..\_lib'
        . (Join-Path $LibDir 'term.ps1')
        Initialize-Term

    All component helpers return strings. The caller decides where to write
    them (stderr for chrome via [Console]::Error.WriteLine, stdout for data
    payloads). This keeps the ATP stream-separation contract intact.

    Honors: $env:NO_COLOR, $env:FORCE_COLOR, $env:TERM_ASCII, $env:TERM=dumb.
    Spec: ../docs/TERMINAL-DESIGN.md.
#>

# Guard against double-sourcing
if ($Script:__TermPs1Loaded) { return }
$Script:__TermPs1Loaded = $true

# ─── Globals (populated by Initialize-Term) ──────────────────────────────────
$Script:TermTty       = $false
$Script:TermColor     = $false
$Script:TermAsciiMode = $false
$Script:TermWidth     = 100   # claude-mods default (TERMINAL-DESIGN open question #1)

# ANSI escape codes (empty when color disabled)
$Script:TC_Green   = ''
$Script:TC_Yellow  = ''
$Script:TC_Orange  = ''
$Script:TC_Red     = ''
$Script:TC_Cyan    = ''
$Script:TC_Magenta = ''
$Script:TC_Dim     = ''
$Script:TC_Off     = ''

# Tree connectors (set by Initialize-Term)
$Script:T_Branch = ''   # ├─  / +-
$Script:T_Last   = ''   # └─  / `-
$Script:T_Vert   = ''   # │   / |

# Panel chrome
$Script:P_TL     = ''   # ╭   / +
$Script:P_BL     = ''   # ╰   / +
$Script:P_HRule  = ''   # ─   / -
$Script:P_Term   = ''   # ●   / *

# Header / alert / tip glyphs
$Script:G_Branch = ''   # ⎇ / (b)
$Script:G_Alert  = ''   # ▲ / !
$Script:G_Tip    = ''   # 💡 / (i)

# Spinner frame banks
$Script:Spin_Working   = @()
$Script:Spin_Heartbeat = @()

# ─── Registries (Unicode | ASCII fallback) ──────────────────────────────────
$Script:TermBrand = @{
    fleet      = '⚡|[F]'
    forge      = '🔨|[B]'
    psql       = '🐘|[P]'
    watch      = '📡|[M]'
    deploy     = '🚀|[D]'
    git        = '🌿|[G]'
    'windows-ops' = '🩺|[H]'   # stethoscope — diagnostics is the verb
}

$Script:TermHealthGlyph = @{
    healthy  = '•|(+)'
    pending  = '•|(.)'
    warning  = '•|(!)'
    critical = '•|(!!)'
    busted   = '⬤|(X)'
    unknown  = '•|(?)'
}

$Script:TermDiagramIcon = @{
    user     = '👤|(U)'
    web      = '🌐|(W)'
    mobile   = '📱|(M)'
    auth     = '🔐|(A)'
    database = '🗄|(D)'
    cache    = '⚡|(C)'
    queue    = '📨|(Q)'
    storage  = '📦|(P)'
    service  = '⚙|*'
    api      = '🔌|(I)'
    search   = '🔍|(S)'
    timer    = '⏱|(T)'
    build    = '🔨|(B)'
    hook     = '🪝|(H)'
    log      = '📄|(F)'
}

# ─── Initialize-Term ─────────────────────────────────────────────────────────
function Initialize-Term {
    <#
    .SYNOPSIS
        Detect terminal capabilities and populate global glyph/color state.
        Idempotent — safe to call multiple times.
    #>
    [CmdletBinding()]
    param()

    # TTY detection — stdout (not stderr; rendering targets stdout-ish)
    try {
        $Script:TermTty = -not [Console]::IsOutputRedirected
    } catch {
        $Script:TermTty = $false
    }

    # ASCII fallback: explicit env, or non-UTF environment
    $asciiEnv = $env:TERM_ASCII -eq '1' -or $env:FLEET_ASCII -eq '1'
    $lang = if ($env:LC_ALL) { $env:LC_ALL } elseif ($env:LANG) { $env:LANG } else { '' }
    $nonUtf = $lang -and ($lang -notmatch '[Uu][Tt][Ff]') -and ($env:TERM -eq 'dumb')
    $Script:TermAsciiMode = $asciiEnv -or $nonUtf

    # Color: TTY + not NO_COLOR, or FORCE_COLOR overrides
    if ($env:FORCE_COLOR) {
        $Script:TermColor = $true
    } elseif ($env:NO_COLOR -or -not $Script:TermTty -or $env:TERM -eq 'dumb') {
        $Script:TermColor = $false
    } else {
        $Script:TermColor = $true
    }

    # Terminal width — fall back to 100 (claude-mods default)
    if ($Script:TermTty) {
        try {
            $cols = [Console]::WindowWidth
            if ($cols -ge 40) { $Script:TermWidth = $cols }
        } catch {
            # WindowWidth throws when no console attached; keep default
        }
    }

    # Allow explicit override
    if ($env:TERM_WIDTH -match '^\d+$' -and [int]$env:TERM_WIDTH -ge 40) {
        $Script:TermWidth = [int]$env:TERM_WIDTH
    }

    # Glyphs by mode
    if ($Script:TermAsciiMode) {
        $Script:T_Branch = '+-'
        $Script:T_Last   = '`-'
        $Script:T_Vert   = '|'
        $Script:P_TL     = '+'
        $Script:P_BL     = '+'
        $Script:P_HRule  = '-'
        $Script:P_Term   = '*'
        $Script:G_Branch = '(b)'
        $Script:G_Alert  = '!'
        $Script:G_Tip    = '(i)'
        $Script:Spin_Working   = @('|', '/', '-', '\')
        $Script:Spin_Heartbeat = @('.', ':', '*', ':')
    } else {
        $Script:T_Branch = '├─'
        $Script:T_Last   = '└─'
        $Script:T_Vert   = '│'
        $Script:P_TL     = '╭'
        $Script:P_BL     = '╰'
        $Script:P_HRule  = '─'
        $Script:P_Term   = '●'
        $Script:G_Branch = '⎇'
        $Script:G_Alert  = '▲'
        $Script:G_Tip    = '💡'
        $Script:Spin_Working   = @('⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏')
        $Script:Spin_Heartbeat = @('·','∙','•','●','•','∙')
    }

    # ANSI escapes
    if ($Script:TermColor) {
        $esc = [char]27
        $Script:TC_Green   = "${esc}[32m"
        $Script:TC_Yellow  = "${esc}[33m"
        $Script:TC_Orange  = "${esc}[38;5;208m"
        $Script:TC_Red     = "${esc}[31m"
        $Script:TC_Cyan    = "${esc}[36m"
        $Script:TC_Magenta = "${esc}[35m"
        $Script:TC_Dim     = "${esc}[2m"
        $Script:TC_Off     = "${esc}[0m"

        # On Windows, ensure VT processing is enabled so ANSI works in PS 5.1
        if ($PSVersionTable.PSVersion.Major -le 5) {
            try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
        }
    } else {
        $Script:TC_Green = ''; $Script:TC_Yellow = ''; $Script:TC_Orange = ''
        $Script:TC_Red = ''; $Script:TC_Cyan = ''; $Script:TC_Magenta = ''
        $Script:TC_Dim = ''; $Script:TC_Off = ''
    }
}

# ─── Color helper ────────────────────────────────────────────────────────────
function Get-TermColor {
    <#
    .SYNOPSIS
        Wrap text in an ANSI color escape. Returns plain text when color disabled.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('green','yellow','orange','red','cyan','magenta','dim')]
        [string]$Token,
        [Parameter(Mandatory)][AllowEmptyString()][string]$Text
    )
    if (-not $Script:TermColor) { return $Text }
    $code = switch ($Token) {
        'green'   { $Script:TC_Green }
        'yellow'  { $Script:TC_Yellow }
        'orange'  { $Script:TC_Orange }
        'red'     { $Script:TC_Red }
        'cyan'    { $Script:TC_Cyan }
        'magenta' { $Script:TC_Magenta }
        'dim'     { $Script:TC_Dim }
    }
    return "${code}${Text}$($Script:TC_Off)"
}

# ─── Registry lookup ─────────────────────────────────────────────────────────
function Get-TermGlyph {
    <#
    .SYNOPSIS
        Return registered Unicode glyph (or ASCII fallback when in ASCII mode).
    .PARAMETER Registry
        Which registry to consult: Brand | Health | Diagram
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('Brand','Health','Diagram')]$Registry,
        [Parameter(Mandatory)][string]$Key
    )
    $map = switch ($Registry) {
        'Brand'   { $Script:TermBrand }
        'Health'  { $Script:TermHealthGlyph }
        'Diagram' { $Script:TermDiagramIcon }
    }
    $entry = $map[$Key]
    if (-not $entry) { return '?' }
    $parts = $entry -split '\|', 2
    if ($Script:TermAsciiMode) { return $parts[1] } else { return $parts[0] }
}

# ─── Display-width helpers ───────────────────────────────────────────────────
function Get-TermDisplayWidth {
    <#
    .SYNOPSIS
        Approximate display column count for a string, accounting for emoji
        double-width and ignoring ANSI color escapes.
    #>
    [CmdletBinding()]
    param([Parameter(ValueFromPipeline)][AllowEmptyString()][string]$Text = '')
    process {
        if (-not $Text) { return 0 }
        # Strip ANSI escapes (CSI sequences)
        $esc = [char]27
        $clean = $Text -replace "${esc}\[[0-9;]*m", ''
        $width = 0
        # EnumerateRunes handles surrogate pairs correctly
        foreach ($rune in [System.Globalization.StringInfo]::GetTextElementEnumerator($clean)) {
            $cp = if ([string]$rune.Current.Length -gt 0) { [int][char]([string]$rune.Current)[0] } else { 0 }
            # Simple wide-emoji heuristic: rune length >1 (surrogate pair) OR in known wide ranges
            $s = [string]$rune.Current
            if ($s.Length -gt 1) {
                $width += 2   # surrogate pair — almost always wide
            } elseif (($cp -ge 0x2600 -and $cp -le 0x27BF) -or
                      ($cp -ge 0x2B00 -and $cp -le 0x2BFF) -or
                      $cp -eq 0x26A1 -or         # ⚡
                      $cp -eq 0x2728 -or         # ✨
                      $cp -eq 0x2B24) {          # ⬤
                $width += 2
            } else {
                $width += 1
            }
        }
        return $width
    }
}

function Get-TermTruncated {
    <#
    .SYNOPSIS
        Ellipsis-truncate text to fit in MaxCols display columns.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position=0)][AllowEmptyString()][string]$Text,
        [Parameter(Mandatory, Position=1)][int]$MaxCols
    )
    $w = Get-TermDisplayWidth $Text
    if ($w -le $MaxCols) { return $Text }
    $ell = if ($Script:TermAsciiMode) { '..' } else { '…' }
    $ellW = Get-TermDisplayWidth $ell
    # Naive truncation by character count — close enough for our needs
    $maxChars = $MaxCols - $ellW
    if ($maxChars -lt 0) { $maxChars = 0 }
    if ($Text.Length -le $maxChars) { return $Text + $ell }
    return $Text.Substring(0, $maxChars) + $ell
}

# ─── Panel chrome ────────────────────────────────────────────────────────────
function New-TermPanelOpen {
    <#
    .SYNOPSIS
        Render the panel header bar: ╭── 🩺 brand · subtitle ─── INDICATOR ───●
    .PARAMETER Brand
        Brand key from the registry (e.g. 'windows-ops').
    .PARAMETER Name
        Tool name shown after the brand emoji (e.g. 'windows-ops').
    .PARAMETER Subtitle
        Optional subtitle after the name (e.g. 'health-audit').
    .PARAMETER Indicator
        Optional right-side context indicator (e.g. 'TITAN', 'Y / Disk 1').
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Brand,
        [Parameter(Mandatory)][string]$Name,
        [string]$Subtitle = '',
        [string]$Indicator = ''
    )
    $emoji = Get-TermGlyph -Registry Brand -Key $Brand
    $title = if ($Subtitle) {
        "$Name $(Get-TermColor dim "· $Subtitle")"
    } else {
        $Name
    }
    $titleVis = if ($Subtitle) { "$Name · $Subtitle" } else { $Name }

    $leftRaw = "$($Script:P_TL)$($Script:P_HRule)$($Script:P_HRule) $emoji "
    $left = "$leftRaw$(Get-TermColor cyan $title) "
    $leftVis = "$leftRaw${titleVis} "

    if ($Indicator) {
        $right = " $(Get-TermColor dim $Indicator) $($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$(Get-TermColor cyan $Script:P_Term)"
        $rightVis = " $Indicator $($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$($Script:P_Term)"
    } else {
        $right = "$($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$(Get-TermColor cyan $Script:P_Term)"
        $rightVis = "$($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$($Script:P_Term)"
    }

    $leftW  = Get-TermDisplayWidth $leftVis
    $rightW = Get-TermDisplayWidth $rightVis
    $fill = $Script:TermWidth - $leftW - $rightW
    if ($fill -lt 4) { $fill = 4 }
    $rule = Get-TermColor cyan ($Script:P_HRule * $fill)
    return "${left}${rule}${right}"
}

function New-TermPanelClose {
    <#
    .SYNOPSIS
        Render the panel footer bar: ╰── hotkeys ─── health1  health2 ───●
    .PARAMETER Hotkeys
        Pre-rendered hotkey string (use New-TermHotkey + joining with ' · ').
    .PARAMETER Healths
        Pre-rendered health-indicator string (use New-TermHealth + joining with '  ').
    #>
    [CmdletBinding()]
    param(
        [string]$Hotkeys = '',
        [string]$Healths = ''
    )
    $leftRaw = "$($Script:P_BL)$($Script:P_HRule)$($Script:P_HRule) "
    $left = "$leftRaw$Hotkeys "
    # For width calc strip ANSI from the rendered hotkeys/healths
    $hotkeysVis = ($Hotkeys -replace "$([char]27)\[[0-9;]*m", '')
    $leftVis = "$leftRaw$hotkeysVis "

    if ($Healths) {
        $right = " $Healths $($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$(Get-TermColor cyan $Script:P_Term)"
        $healthsVis = ($Healths -replace "$([char]27)\[[0-9;]*m", '')
        $rightVis = " $healthsVis $($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$($Script:P_Term)"
    } else {
        $right = "$($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$(Get-TermColor cyan $Script:P_Term)"
        $rightVis = "$($Script:P_HRule)$($Script:P_HRule)$($Script:P_HRule)$($Script:P_Term)"
    }

    $leftW  = Get-TermDisplayWidth $leftVis
    $rightW = Get-TermDisplayWidth $rightVis
    $fill = $Script:TermWidth - $leftW - $rightW
    if ($fill -lt 4) { $fill = 4 }
    $rule = Get-TermColor cyan ($Script:P_HRule * $fill)
    return "${left}${rule}${right}"
}

function New-TermPanelVert {
    <# Body-line spacer: a single │ on its own line. #>
    [CmdletBinding()]
    param()
    return Get-TermColor dim $Script:T_Vert
}

# ─── Body components ─────────────────────────────────────────────────────────
function New-TermSection {
    <#
    .SYNOPSIS
        Section header: ├── LABEL (n)  with label colored by state.
    .PARAMETER State
        State token (FAILING/WARN/PASS/INFO or fleet-style RUNNING/READY/FAILED/CONFLICT).
    .PARAMETER Label
        Section label text.
    .PARAMETER Count
        Item count. Pass -1 to omit the (n).
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$State,
        [Parameter(Mandatory)][string]$Label,
        [int]$Count = -1
    )
    $color = switch -Regex ($State) {
        '^(RUNNING|PENDING|WARN|warning|WATCHLIST)$' { 'yellow' }
        '^(READY|PASS|LANDED|DONE|OK|healthy)$'      { 'green' }
        '^(FAILING|FAILED|ERROR|CRITICAL|critical|alarm|busted)$' { 'red' }
        '^(CONFLICT)$' { 'magenta' }
        default { '' }
    }
    $renderedLabel = if ($color) { Get-TermColor $color $Label } else { $Label }
    # Section is a panel-edge attachment — '├──' IS the left edge, no leading '│' prefix.
    $conn = Get-TermColor dim "$($Script:T_Branch)$($Script:P_HRule)"
    $countStr = if ($Count -ge 0) { ' ' + (Get-TermColor dim "($Count)") } else { '' }
    return "${conn} ${renderedLabel}${countStr}"
}

function New-TermSummary {
    <#
    .SYNOPSIS
        Summary line: ├── text  (all dim, metadata-only branch).
    #>
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Text)
    # Summary attaches at the panel edge like a section.
    $conn = Get-TermColor dim "$($Script:T_Branch)$($Script:P_HRule)"
    $body = Get-TermColor dim $Text
    return "${conn} ${body}"
}

function New-TermLeaf {
    <#
    .SYNOPSIS
        Single leaf row: │   ├── name              ●─●─◉    meta    age
    .PARAMETER Name
        Leaf name (ellipsis-truncated to fit name column).
    .PARAMETER Rail
        Pre-rendered leaf-glyph string (rail or pip bar or plain text).
    .PARAMETER Meta
        Meta column content (e.g. 'M4 ?1', 'clean', error count).
    .PARAMETER Age
        Age column content (right-aligned, e.g. '12m', '2h').
    .PARAMETER IsLast
        Use └── connector instead of ├── (for last sibling in a section).
    .PARAMETER NameColWidth
        Override name column width (default 32).
    .PARAMETER RailColWidth
        Override rail column width (default 14).
    .PARAMETER MetaColWidth
        Override meta column width (default 12).
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Name,
        [string]$Rail = '',
        [string]$Meta = '',
        [string]$Age  = '',
        [switch]$IsLast,
        [int]$NameColWidth = 32,
        [int]$RailColWidth = 14,
        [int]$MetaColWidth = 12
    )
    $vert = Get-TermColor dim $Script:T_Vert
    $connRaw = if ($IsLast) { $Script:T_Last } else { $Script:T_Branch }
    $conn = Get-TermColor dim "$connRaw$($Script:P_HRule)"

    $truncName = Get-TermTruncated -Text $Name -MaxCols $NameColWidth
    $nameW = Get-TermDisplayWidth $truncName
    $namePad = ' ' * [Math]::Max(0, $NameColWidth - $nameW)

    $railW = Get-TermDisplayWidth ($Rail -replace "$([char]27)\[[0-9;]*m", '')
    $railPad = ' ' * [Math]::Max(0, $RailColWidth - $railW)

    $metaColored = if ($Meta) { Get-TermColor dim $Meta } else { '' }
    $metaW = Get-TermDisplayWidth $Meta
    $metaPad = ' ' * [Math]::Max(0, $MetaColWidth - $metaW)

    $ageColored = if ($Age) { Get-TermColor dim $Age } else { '' }

    return "${vert}   ${conn} ${truncName}${namePad}  ${Rail}${railPad}  ${metaColored}${metaPad}  ${ageColored}"
}

function New-TermAlert {
    <#
    .SYNOPSIS
        Inline alert sub-row: │   │   ▲ message  (orange/red).
    .PARAMETER Severity
        warning (orange) or critical (red).
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('warning','critical')]$Severity,
        [Parameter(Mandatory)][string]$Text
    )
    $color = if ($Severity -eq 'critical') { 'red' } else { 'orange' }
    $vert  = Get-TermColor dim $Script:T_Vert
    $vert2 = Get-TermColor dim $Script:T_Vert
    $tri = Get-TermColor $color $Script:G_Alert
    # Per design § 4.7: panel-vert, 3-space section indent, leaf-continuation vert,
    # 3-space sub-indent (aligns the alert under the leaf's tree connector).
    return "${vert}   ${vert2}   ${tri} ${Text}"
}

function New-TermHint {
    <#
    .SYNOPSIS
        Hint row with the tip glyph: │   💡 text  (dim, no tree connector).
        Used for "to get started" / "did you know" rows in empty states.
    #>
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Text)
    $vert = Get-TermColor dim $Script:T_Vert
    $tip = $Script:G_Tip
    return "${vert}   ${tip} $(Get-TermColor dim $Text)"
}

function New-TermToast {
    <#
    .SYNOPSIS
        Toast row: ├── 🩺 message  (dim cyan emoji + default fg text).
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Brand,
        [Parameter(Mandatory)][string]$Text
    )
    $emoji = Get-TermGlyph -Registry Brand -Key $Brand
    $vert = Get-TermColor dim $Script:T_Vert
    $conn = Get-TermColor dim "$($Script:T_Branch)$($Script:P_HRule)"
    $msg = "$(Get-TermColor cyan $emoji) $Text"
    return "${vert}${conn} ${msg}"
}

# ─── Leaf-glyph builders ─────────────────────────────────────────────────────
function New-TermRail {
    <#
    .SYNOPSIS
        Build a commit-graph rail: ●─●─●─◉ (HEAD) or ●─●─⊗ (CONFLICT).
    .PARAMETER Commits
        Number of landed commits (including the HEAD position).
    .PARAMETER Head
        HEAD | CONFLICT | EMPTY
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateRange(0,99)][int]$Commits,
        [ValidateSet('HEAD','CONFLICT','EMPTY')][string]$Head = 'HEAD'
    )
    $commit   = if ($Script:TermAsciiMode) { '*' } else { '●' }
    $link     = if ($Script:TermAsciiMode) { '-' } else { '─' }
    $headg    = if ($Script:TermAsciiMode) { '@' } else { '◉' }
    $conflict = if ($Script:TermAsciiMode) { 'X' } else { '⊗' }

    if ($Commits -le 0 -and $Head -eq 'EMPTY') {
        return $link
    }

    $out = ''
    for ($i = 0; $i -lt ($Commits - 1); $i++) {
        $out += "$(Get-TermColor green $commit)$link"
    }
    switch ($Head) {
        'HEAD' {
            if ($Commits -ge 1) { $out += "$(Get-TermColor green $commit)$link" }
            $out += Get-TermColor yellow $headg
        }
        'CONFLICT' {
            if ($Commits -ge 1) { $out += "$(Get-TermColor green $commit)$link" }
            $out += Get-TermColor red $conflict
        }
        'EMPTY' {
            if ($Commits -ge 1) { $out += Get-TermColor green $commit }
        }
    }
    return $out
}

function New-TermPipBar {
    <#
    .SYNOPSIS
        Build a pip bar: ▰▰▰▰▱▱▱▱▱▱  with state-color based on metric type.
    .PARAMETER Type
        progress | score | capacity (drives color selection per design § 4.10).
    .PARAMETER Filled
        Filled count.
    .PARAMETER Total
        Total / denominator.
    .PARAMETER Width
        Pip count (default 10 = clean 10% increments).
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('progress','score','capacity')]$Type,
        [Parameter(Mandatory)][int]$Filled,
        [Parameter(Mandatory)][int]$Total,
        [int]$Width = 10
    )
    $pipFull  = if ($Script:TermAsciiMode) { '#' } else { '▰' }
    $pipEmpty = if ($Script:TermAsciiMode) { '-' } else { '▱' }

    # Natural denominator override: if total <= 12 and not 100, use total as width
    if ($Total -ne 100 -and $Total -gt 0 -and $Total -le 12) {
        $Width = $Total
    }

    $pct = if ($Total -gt 0) { [int](100 * $Filled / $Total) } else { 0 }
    $pips = if ($Total -eq 100) { [int]($Filled / 10) } else { $Filled }
    if ($pips -lt 0)      { $pips = 0 }
    if ($pips -gt $Width) { $pips = $Width }

    $color = switch ($Type) {
        'progress' {
            if ($pct -ge 100) { 'green' } else { 'yellow' }
        }
        'score' {
            if ($pct -lt 33)      { 'red' }
            elseif ($pct -lt 66)  { 'yellow' }
            else                  { 'green' }
        }
        'capacity' {
            if ($pct -ge 80)      { 'red' }
            elseif ($pct -ge 60)  { 'yellow' }
            else                  { 'green' }
        }
    }

    $out = ''
    for ($i = 0; $i -lt $pips;  $i++) { $out += Get-TermColor $color $pipFull }
    for ($i = $pips; $i -lt $Width; $i++) { $out += Get-TermColor dim $pipEmpty }
    return $out
}

# ─── Right-side furniture ────────────────────────────────────────────────────
function New-TermHealth {
    <#
    .SYNOPSIS
        Health indicator: • text  (with ⬤ for busted state).
    .PARAMETER State
        healthy | pending | warning | critical | busted | unknown
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('healthy','pending','warning','critical','busted','unknown')]$State,
        [Parameter(Mandatory)][string]$Text
    )
    $glyph = Get-TermGlyph -Registry Health -Key $State
    $color = switch ($State) {
        'healthy'  { 'green' }
        'pending'  { 'yellow' }
        'warning'  { 'orange' }
        'critical' { 'red' }
        'busted'   { 'dim' }
        default    { 'dim' }
    }
    return "$(Get-TermColor $color $glyph) $Text"
}

function New-TermHotkey {
    <#
    .SYNOPSIS
        Hotkey hint: "R refresh" with R in cyan.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Key,
        [Parameter(Mandatory)][string]$Verb
    )
    return "$(Get-TermColor cyan $Key) $Verb"
}

function Join-TermHotkeys {
    <# Combine hotkeys with the dot separator. Accumulates from pipeline OR -InputObject array. #>
    [CmdletBinding()]
    param([Parameter(ValueFromPipeline, Position=0)][string]$InputObject)
    begin { $items = New-Object System.Collections.Generic.List[string] }
    process { if ($InputObject) { $items.Add($InputObject) } }
    end { return ($items -join ' · ') }
}

function Join-TermHealths {
    <# Combine health indicators with two-space separator (per design § 4.3). #>
    [CmdletBinding()]
    param([Parameter(ValueFromPipeline, Position=0)][string]$InputObject)
    begin { $items = New-Object System.Collections.Generic.List[string] }
    process { if ($InputObject) { $items.Add($InputObject) } }
    end { return ($items -join '  ') }
}

# ─── Spinners ────────────────────────────────────────────────────────────────
function Get-TermSpinnerFrame {
    <#
    .SYNOPSIS
        Return the spinner glyph for the given tick (frame index).
    .PARAMETER Family
        working (fast, task-progress) or heartbeat (slow, daemon-alive).
    .PARAMETER Tick
        Frame index; modded by family frame count.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('working','heartbeat')]$Family,
        [Parameter(Mandatory)][int]$Tick
    )
    $frames = switch ($Family) {
        'working'   { $Script:Spin_Working }
        'heartbeat' { $Script:Spin_Heartbeat }
    }
    if (-not $frames) { return '?' }
    return $frames[$Tick % $frames.Count]
}

# ─── Convenience: Write a panel-block to stderr ──────────────────────────────
function Write-TermLine {
    <#
    .SYNOPSIS
        Write a pre-rendered chrome line to stderr. Use for panel rows so
        stdout remains data-only (ATP stream separation).
    #>
    [CmdletBinding()]
    param([Parameter(ValueFromPipeline)][AllowEmptyString()][string]$Line)
    process { [Console]::Error.WriteLine($Line) }
}

function Write-TermData {
    <#
    .SYNOPSIS
        Write payload data to stdout (the data product of the script).
        Use for JSON, machine-readable records, anything downstream tooling
        will consume.
    #>
    [CmdletBinding()]
    param([Parameter(ValueFromPipeline)][AllowEmptyString()][string]$Line)
    process { [Console]::Out.WriteLine($Line) }
}
