tasty.gallery
Overlays · Banners
← Design
Specs
Theme

Banners

See how banners bring a message or action into the work area. They stay below the tab bar and accept mouse input while leaving keyboard focus with your work.

This page uses a saved copy of the design system, last changed on . It may differ from the current design source.

Shell integration missing — action-less, TTL 10 s (2026-11-02)

shell-integration-missing — surface scope, countdown in the dismiss slot

Raised on a terminal surface when command-completion hooks need OSC 133 and no prompt marks were seen. Title + body, no buttons, so under the lifetime rule it is a TTL banner: the top-right slot shows the seconds, flips to × on hover, and the card closes at 0. The countdown starts when the card is shown, pauses while it is hovered or its scope is backgrounded, resumes where it stopped; re-raising the same id while shown resets it to 10. Card, glyph, title and body are unchanged — only the top-right slot changes.

Layout spec
scope
surface
actions
none → TTL
TTL
10 s — fixed design constant, not a setting
pause
hover · scope backgrounded
re-raise (shown)
countdown resets to 10
copy
unchanged — banner.shell_integration_missing.* (en/ko/ja)
Tokens used
banner-countdown-fg— secondsbanner-countdown-font— mono digits

Why 10 s: the body is two sentences plus a docs path (~25 words in en, longer in ja) — about 7–8 s of reading at a normal pace, plus time to notice a card that appears over live output. The 2 s toast default is for one-line confirmations and does not apply. Hover pauses the count, so a user who needs longer already has it. No setting: a per-banner TTL setting would be the only one of its kind; if users ask, a single global banner TTL setting is the place for it.

The glyph shown here is the specimen's; keep whatever the implemented card uses — this request changes only the top-right slot.

Mouse capture — the first banner instance

Anatomy & states

The canonical use: a TUI app turns on mouse tracking (DECSET 1000/1002/1003), so drag-to-select and right-click stop working. The banner explains why and the bypass. It is persistent (no TTL — it doesn't time out), fires once per tracking session and only on a real user click (never from agent/IPC). The top-right × is hidden until you hover; dismissing it suppresses the banner for that session. Leading glyph = a mouse (open decision resolved: keep it, in --tasty-banner-icon-fg tone). Card height is variable — body wraps to 1–3 lines depending on locale.

Layout spec
title
"Mouse input captured" · 13/600 --tasty-banner-fg
body
caption · --tasty-text-muted · 1–3 lines
glyph
mouse · --tasty-banner-icon-fg
affordance
⋯ + × on hover (no TTL / countdown)
fires
once per tracking session · user click only
dismiss
suppresses for the session
Tokens used
banner-bg— card fillbanner-icon-fg— mouse glyphbanner-recessed-opacity— dimmed variant

The bypass keys (Shift+drag / Shift+Right-click) are the message — keep them in the body. No inline action buttons: the body stays text-only, and the two per-app opt-outs live behind the ⋯ trigger next to the × (see Banner more menu below).

Position & hit-zone — card consumes, body passes through

The banner floats at the top of the surface content area, below the tab bar (never over it), full width minus 8px side margins. Crucially, the card rect — not the whole scope — is the mouse-consume + hover zone. Clicks on the surface body below the card pass through to the capturing app, so mouse reporting keeps working everywhere except the card itself.

Layout spec
anchor
surface content top, under tab bar
margins
8 top / 8 sides · 0 bottom
consume zone
the drawn card rect only
below card
pass-through to the app
focus
never steals keyboard focus
inactive surface
first click only focuses the surface
Tokens used
banner-margin— 8px gapaccent-info— consume-zone label

Banner more menu — per-app opt-outs behind ⋯

⋯ trigger — second slot in the affordance column

The mouse-capture banner's two per-app opt-outs are not inline buttons — they sit behind a ⋯ trigger in the banner's top-right affordance column, left of the × with a 4px gap. Both share the same reveal rule: hidden until the banner is hovered (this banner has no TTL, so no countdown competes for the slot), and the ⋯ additionally stays lit while its menu is open. The card's width and height are unchanged — only the body's right reserve grows from 28 to 56px, so a 1-line body still fits on one line.

Layout spec
trigger
IconButton ghost / sm · 24px · more glyph
placement
affordance column, left of ×
gap
4px between ⋯ and ×
reveal
banner hover — same rule as ×
open state
active (accent) while menu is open
body reserve
28 → 56px right padding
card size
unchanged
Tokens used
banner-more-column-gap— 4px ⋯ ↔ ×banner-more-reserve— 56px body right paddingoverlay-active— open-trigger washaccent-primary— open-trigger glyph

New glyph more (icons/more.svg — three dots, horizontal, same 2px round-cap language as M12 17h.01 style glyphs). A unicode ⋯ was rejected: the system renders every affordance through the Icon component so it inherits currentColor and the 16px sm size.

Don't keep the trigger permanently visible. A persistent banner with a permanently drawn ⋯ reads as a second dismiss control and adds chrome to a notice that is already at the top of live output — hover is where every other banner affordance lives.

The menu — 2 rows, anchored under the trigger

Clicking ⋯ opens a headless menu 4px below the trigger, right-aligned to it (the trigger sits at the card's right edge, so left-aligning would overflow), at a higher z-order than the banner. It flips above the trigger when the viewport can't fit it below. Same anchored-popup contract as the Tools menu: no scrim, outside click · Esc dismiss, ↑↓ move, ↵ select. Selecting a row acts immediately and closes the menu — to run the other one, reopen ⋯.

Layout spec
rows
2 × 28px MenuItem, leading icon
anchor
4px below ⋯, right-aligned
overflow
flip above the trigger
width
min 200 · max 288 (elastic)
scrim
none
dismiss
outside click · Esc · row select
keyboard
↑↓ · ↵ · Esc
Tokens used
banner-more-menu-bg— menu fillbanner-more-menu-border— edgebanner-more-menu-shadow— popover liftbanner-more-menu-offset— 4px below triggerbanner-more-menu-min-width— 200px floorbanner-more-menu-max-width— 288px ceiling

Both rows write a permanent, per-program Settings entry (copy: see Row copy below): Don't show again for {app} → mouse_capture_banner_blacklist (capture stays delegated, only the notice is suppressed) · Let Tasty handle clicks in {app} → mouse_capture_blacklist (tasty takes clicks and drags back; the wheel still goes to the program). “Don't show again” also closes the banner it was opened from — suppressing the notice and leaving it on screen would contradict itself. “Let Tasty handle clicks” leaves the banner up (capture is already released; the user dismisses when done reading). Icons: bell for the notice row, mouse for the capture row.

While the menu is open the banner is treated as hover-equivalent — a TTL banner would keep its countdown paused, and the affordance column stays drawn even if the pointer has left the card.

Elastic width — the interpolated program name

Every row interpolates a program name of arbitrary length, and word order differs by locale (en: name last; ko/ja: name first). So a row's label is two parts: the fixed text (never truncates) and the program name in mono (same row ink as the fixed text: text-secondary at rest, text-primary on hover — 2026-10-08), which is the part that shrinks and ellipsises. The menu grows with the content between 200px and 288px (a wider band than the Tools menu's 160–240, since every row carries a program name) and the full name is available as the row's tooltip.

Layout spec
label
fixed text + app name (2 spans)
truncates
the app name only
app name
mono · row ink (--tasty-banner-more-app-fg → menu-item-fg / -fg-hover)
width
content-sized, 200 ≤ w ≤ 288 — never widens past 288
wrap
only when the FIXED copy + 1 name glyph does not fit: whole label wraps, row grows (min 28, pad-y --tasty-menu-item-wrap-padding-y); the fixed copy never ellipsises
tooltip
full program name on the row
tone
both rows neutral (no danger)
Tokens used
banner-more-app-font— mono program namebanner-more-app-fg— program name = row inkbanner-more-app-fg-hover— hovermenu-item-height— 28px rowsaccent-danger— rejected variant only

Do keep the program name as the mono part of the row — it is the one thing the user must verify before writing a permanent per-app setting, and mono marks it as a process name rather than prose.

Don't paint the “Let Tasty handle clicks” row in danger tone (last card above, shown for the record). Nothing is destroyed and nothing is lost: the setting is reversible from Settings › Terminal, and it returns the mouse to tasty. Danger in this system means delete — spending it here would make the two rows look like different classes of action when they are the same class at two strengths.

Row copy — en / ko / ja (2026-11-02)

Each row names the program and what changes for it, in words a user already has: “show again” for the notice, “clicks” for capture. “Again” / “다시” / “今後” marks the first row as lasting; the second row reads as a standing rule (“in vim, Tasty handles clicks”). The old “~에 대해” and “disable mouse capture” (which read as turning off a feature of the app) are gone. Each row = prefix + {app} (mono) + suffix; en uses the prefix only, ko/ja the suffix only (leading space in ja before the particle). Width band 200–288 is unchanged; with this copy no row wraps for a short name in any locale (the old ja wrap is retired, the wrap rule stays for long names and future copy).

Layout spec
row 1 en
prefix Don't show again for · suffix —
row 1 ko
prefix — · suffix 에서 다시 보지 않기
row 1 ja
prefix — · suffix では今後表示しない
row 2 en
prefix Let Tasty handle clicks in · suffix —
row 2 ko
prefix — · suffix 에서 클릭은 Tasty가 처리
row 2 ja
prefix — · suffix ではクリックを Tasty で処理
keys
[popup.mouse_capture_banner_menu] prefix / suffix
Tokens used
banner-more-menu-max-width— 288 — unchanged

Settings › Terminal › Mouse Capture and the user guide take the same verbs: list 1 Don't show the capture notice for these programs / 캡처 안내를 다시 보지 않을 프로그램 / キャプチャ通知を表示しないプログラム · list 2 Let Tasty handle clicks in these programs / 클릭을 Tasty가 처리할 프로그램 / クリックを Tasty で処理するプログラム, with the existing description (clicks and drags go to selection and the menu; the wheel is still sent).

Program name unknown — rows shown disabled (2026-11-03)

When the program name is unknown (remote attach mirror — no local PTY; before the first 1 Hz poll; OS lookup failed) the actions still exist but can't run, so they are shown disabled, not hidden. The ⋯ trigger is drawn exactly as with a known name (same hover reveal, lit while open, same 56 body reserve). The menu opens with a non-interactive reason line on top, a separator, then the same two rows disabled — worded with “this program” in place of the name. No “(unknown)” placeholder: a mono “(unknown)” would read as a process called that. Clicking a disabled row does nothing: nothing is saved, the menu stays open, the banner stays. Esc / outside click close as usual. Keyboard arrow navigation skips disabled rows (with both disabled, focus stays on the menu). If the name arrives while the menu is open the menu does not change; the next open shows the normal rows. On a remote mirror the name is always unknown, so this is the menu there — a note, no separate example.

Layout spec
unknown when
remote mirror · before first poll · lookup failed
⋯ trigger
always drawn — same as known name
body reserve
56 (unchanged)
reason line
info glyph 14 (top --tasty-banner-more-reason-glyph-offset 1) · caption · --tasty-text-muted · wraps · not focusable, no hover
rows
MenuItem disabled as is (pointer-events none, text + icon --tasty-state-disabled-fg, no overlay) · wrap on
tooltip
none — the reason is in the menu, so pointer-events none costs nothing
click
no-op · menu stays open · nothing saved
keyboard
arrows skip disabled rows
live update
no — next open
lifetime
has ⋯ → no TTL (normal rule, no exception)
keys
[popup.mouse_capture_banner_menu] unknown_reason · notice_unknown · capture_unknown (whole strings, no prefix/suffix)
Tokens used
banner-more-reserve— 56 — alwaysstate-disabled-fg— disabled rowstext-muted— reason linefont-size-caption— reason line 11banner-more-reason-glyph-offset— reason glyph top 1menu-separator— reason ↔ rows

Do keep the rows the user would see with a known name, disabled, so the menu teaches what exists; the reason line says why it can't run now, without a hover.

Don't hide the ⋯ or the rows when the name is unknown (2026-11-02 option a, withdrawn). Don't carry the reason in a tooltip on a disabled row — pointer-events none on the row blocks it, and a tooltip is invisible to anyone who doesn't hover.

Mouse-capture blacklist (Settings › Terminal)

Per-program capture opt-out — list editor

The companion settings (labels aligned with the ⋯ rows 2026-11-02): Don't show the capture notice for these programs (mouse_capture_banner_blacklist) and Let Tasty handle clicks in these programs (mouse_capture_blacklist) — process-name patterns where mouse capture is disabled, so clicks/drags are handled locally (select / tasty menu) instead of being sent to the app. Promoted from the old newline textarea to a list editor (rows + Add) — resolved open decision. Grouped with the Show mouse-capture hint switch since both govern the same feature. Default is an empty list. Remove uses × (it un-lists a program, it doesn't delete anything).

Layout spec
lives in
Settings › Terminal
group
hint switch + blacklist (1 section)
row
28px · pattern (mono) + × remove
add
Input + Add button (disabled when empty)
match
case-insensitive substring or * wildcard
default
empty list
Tokens used
overlay-hover— row hover (8%)accent-warning— match-rule noticeinput-bg— add field

The wheel is always forwarded to the program even for blacklisted apps — only clicks/drags are intercepted. A blacklisted foreground app also suppresses the capture banner on that surface.

HTML script notice — the first inset banner (2026-09-28)

Placement — inset, scoped to one surface

The HTML page is a native WebView layered over tasty's GPU frame, so a floating banner would be hidden under it. Approach ⒜: tasty paints the banner and the surface's WebView rect shrinks by the banner's height. This is a formal inset placement in the banner system, for any scope whose content is a native layer: same shell, margin 8 on all four sides (--tasty-banner-inset-gap is the new bottom one), and the page starts below it. Floating stays the default everywhere else. Scope is the surface: the terminal beside it is untouched. Showing or clearing the banner resizes the page once; there is no animation on the rect.

Layout spec
approach
⒜ egui banner + WebView rect inset
placement
inset (new banner variant) · floating unchanged
margin
8 top / sides / bottom
scope
the HTML surface only
focus
never takes it · clicks don't reach the page
TTL
none — stays until allow or ×
Tokens used
banner-inset-gap— 8 — banner → page (new)banner-margin— 8 top / sidesbanner-bg— shellbanner-border— edge

Don't inject the notice into the document DOM (⒞): the page could hide or forge it, and its button can't run with scripts off. Don't build three native overlays (⒝) for one notice.

Banner states

Informational, not a warning: blocking is the default working as intended. Leading glyph lock in --tasty-html-script-banner-glyph (→ accent-info). One action, [Allow for this document], Secondary / Sm — a relaxation should not be the loudest thing on screen, and there is no second action (the global switch lives in Settings › Appearance › HTML). Title and body wrap and never clamp — the card grows (Tasty-owned copy, same rule as refusal / size sync / mouse capture, 2026-10-10 b12); the button never wraps. The scope ("this document", "until Tasty restarts") is in the body copy. After Allow the document reloads once: the action is replaced by a spinner + Reloading with scripts allowed, × is withheld, and the banner fades out (120ms) when the new load commits.

Layout spec
title
"Scripts in this document are blocked" · 13/600
body
caption · text-muted · wraps, never clamps (b12 — was ≤ 2 lines)
remote branch
network scripts stay blocked — says so
action
Secondary / Sm · no wrap
reloading
spinner + label · no × · fade out on commit
loading (still blocked)
Allow disabled from load start to commit · no delay · tooltip (top) "Available when the document finishes loading" · × kept
narrow
surface width < 440 (--tasty-banner-narrow-below) → action on its own line, body-aligned
glyph nudge
1 · --tasty-banner-glyph-offset
title ↔ body
2 · --tasty-banner-text-gap (every banner)
Tokens used
html-script-banner-glyph— lock → accent-infobanner-title-font-size— 13banner-body-font-size— 11

A button on the banner shell (2026-09-29)

The shell is --tasty-banner-bg = surface-raised, which is also the Secondary button's fill, and its edge (border-default) is the same ramp step. On a banner the button would be text only. Rule for every banner: a Secondary inside a banner shell (data-surface="banner") goes one ramp step up, fill --tasty-banner-button-bg (→ surface-hover) with edge --tasty-banner-button-border (→ border-frame). Hover is the normal Button overlay (8%) on top; the edge does not change on hover here. No new variant: the context decides. Disabled on a banner follows the disabled rule (neutral box, disabled ink). Plugin banners too (2026-10-06): the rule belongs to the banner, not to who paints it. The platform opens the banner context for the whole frame of a plugin banner's content, so a plugin author draws a plain Secondary and gets the banner box; the context never leaks to the same plugin's surfaces or popups.

Layout spec
where
Secondary inside any banner shell
fill
surface-hover (one step above banner-bg)
edge
border-frame (one step above banner-border)
hover
overlay-hover · edge unchanged
variant
none new — context rule in Button
Tokens used
banner-button-bg— → surface-hoverbanner-button-border— → border-framebanner-bg— shell

Load failed — no banner, no marker (2026-10-06)

When the document never commits (the web process dies first, or the load errors), the host chrome's Failed to load state takes the body. The script banner and the tab's lock / script marker are both removed in that state: they describe the previous document, and Allow would allow nothing. Only the failure state shows. A reload that commits decides banner and marker again from the new document. Same on every OS; only Linux is measured today.

Layout spec
banner
removed while failed
tab marker
lock / script glyph removed while failed
failure state
alertCircle md + Failed to load (accent-danger) · URL mono caption text-disabled
reload commits
banner + marker re-derived from the new document
Tokens used
accent-danger— glyph + titletext-disabled— URL

After × and after Allow — the tab-strip marker

× hides the notice for this load; scripts stay blocked. A lock marker then sits in the surface's tab, first in the tab's right-hand status cluster (Layouts › Pane tab strip; --tasty-html-script-marker-fg → glyph-dim, 12px). Clicking it shows the banner again — it re-explains before offering Allow, and never allows on its own. Mouse only, no focus change. After Allow, the tab carries a script glyph in text-muted with a tooltip; it is not clickable. Navigating to another document clears both, and the new document starts over. #fragment moves keep them.

Layout spec
dismissed
lock · glyph-dim · click → banner again
allowed
scriptFile · text-muted · tooltip only
size
12 (icon-size-xs) in the 24 strip
cleared by
navigation to another document · app restart
kept on
#fragment moves
no banner when
no runnable script · already allowed · sandbox off
Tokens used
html-script-marker-fg— → glyph-dimhtml-script-marker-allowed-fg— → text-mutedhtml-script-marker-size— → icon-size-xs

Firing: only when the user views the document (opened it, or selected the surface). Agent/IPC opens, session restore and background loads keep a "has scripts" flag and show the banner the first time the user looks at that surface.

Auto-attach refused — mapping notice (2026-10-07)

Persistent row mark + workspace banner, no toast

A workspace mapped with tasty set workspace --ssh / --ssh-profile whose target is this Tasty is not attached (a self-mirror would lock local input). Two signals, both persistent and neither takes focus: a warning glyph on the workspace's sidebar row (visible whether or not it is active, tooltip = target + reason), and a Workspace-scope banner at the top of that workspace's content while it is active — target in mono, the reason, and what to do. Remove mapping (banner button) clears the mapping as tasty set workspace --id … --clear-mapping would; × hides the banner for this activation only — the row mark stays until the mapping changes, is removed, or a profile re-check succeeds. No toast: the condition outlives a toast and must be findable later. The same pair covers the other mapping errors (profile not found, endpoint not resolvable) with their own reason line.

Layout spec
row mark
alertTriangle 14 · accent-warning · trailing slot of the workspace row · tooltip = target + reason
banner scope
Workspace · shown while that workspace is active
glyph
alertTriangle 16 · accent-warning
title
Remote not attached — {target} (target mono)
body
{reason} Change or remove the mapping for this workspace.
action
Remove mapping · banner button (secondary sm on banner-button tokens)
×
hides for this activation; the row mark stays
clears
mapping changed / removed · profile re-check succeeds
focus
never moved — agent-made mappings included
toast
none
Tokens used
accent-warning— glyph + row markbanner-bg— shellbanner-button-bg— Remove mappingbanner-body-font-size— reason 11

Strings (en): remote.refusal.title “Remote not attached — {target}” · remote.refusal.self “This mapping points at this Tasty, so it was not attached.” · remote.refusal.profile_missing “No SSH profile with this name.” · remote.refusal.unresolved “The endpoint could not be resolved.” · remote.refusal.hint “Change or remove the mapping for this workspace.” · remote.refusal.remove “Remove mapping”. No new tokens.

Collapsed rail · beside the attention badges (2026-10-07 batch 5)

Rail: the refusal takes the one free avatar corner, top-left, as a 12px chip (alertTriangle 8 on bg-sidebar) — top-right stays the attention dot, bottom-right the mirror chip, bottom-left the move chip; all four can show at once, so there is no priority rule. Same tooltip as the row. Row: order after the name is move glyph · refusal mark · badge group, gap = the row item spacing (space-sm). The badges stay rightmost so the counts line up down the list; the two persistent marks sit before them. The mark never replaces a badge.

Layout spec
rail chip
top-left · 12 · alertTriangle 8 · accent-warning on bg-sidebar
rail corners
TR attention dot · BR mirror · BL move · TL refusal — no priority
row order
name · move · refusal · badges
row gap
space-sm (row item spacing)
tooltip
same as the row · not clickable
Tokens used
attach-refusal-glyph— → accent-warningattach-refusal-chip-size— → move-source-chip-size 12attach-refusal-chip-glyph-size— → move-source-chip-glyph-size 8

Attach mirror — terminal size sync failed (2026-10-09)

Workspace banner · default · retrying · several surfaces

An attached mirror asks the remote to resize each terminal to the local pane. When the remote does not answer within 5 s (or refuses) Tasty retries once on its own; if that also fails this banner appears in the mirror workspace that owns the surface. Same family as the refusal banner: alertTriangle in accent-warning, a semibold title, one muted body line, one banner-button action and the × dismiss. Retry re-sends only the failed surfaces' size requests (the connection and other surfaces are untouched). While waiting the button is disabled with a leading Spinner and keeps its label, so the width does not jump between languages; success removes the banner, failure returns it to the default state. Several surfaces failing together share one card: “N surfaces — a, b +n”, and the action reads Retry all. The banner also clears by itself when a later resize succeeds. A reconnecting mirror shows the reconnect notice instead (it wins the slot). Queue rules are the normal Workspace-scope ones: one card per scope.

Layout spec
boxes
glyph 16 · text column (flex 1) · action group (Retry + ×, gap 4)
glyph
alertTriangle 16 · attach-sync-glyph → accent-warning (same as refusal)
title
banner-title 13 · semibold · never truncated
body
banner-body 11 · muted · TWO lines (b10): ① names — one line ② “The remote may still be using the old size.” — wraps, never cut
name
text-secondary · each name ≤ attach-sync-name-max-width 160, shrinks to fit the line down to attach-sync-name-min-width 40, then ellipsis · the floor applies only while shrinking: box = min(natural, max(40, shrunk)) — a short name keeps its own width (b11) · first two entries, then +n; separators, ×k and +n never shrink
same name (b11)
equal display names collapse into one entry + “ ×k” (text-muted, never shrinks) · “N surfaces” counts surfaces · +n counts remaining entries · Retry all retries every surface
body lines (b11 · b12)
Tasty-owned banner copy (refusal · size sync · mouse capture · HTML script) never clamps — the card grows · plugin-supplied body clamps at 3 lines, then ellipsis + full text in the tooltip
plugin body rows (b12)
rows = min(3, floor(content height ÷ body line height)) — a fixed size_hint that holds fewer lines clamps at what fits; the ellipsis sits on the last row that fits
plugin tooltip (b12)
drawn by the HOST, not inside the plugin mesh: the widget reports “truncated + full text” for the frame and the host shows the standard Tooltip BELOW the banner card (left = body x, offset tooltip-offset 4, max tooltip-max-width 240, delay tooltip-delay) — never over the pointer, never clipped by the content rect · interim (no channel yet): the widget opens its tooltip below the body inside the content rect, never above
retrying
Retry disabled + leading Spinner 14, label unchanged
many
one card · “N surfaces — a, b +n” · Retry all
×
IconButton sm · hides this card; a new failure shows it again
clears
Retry success · later resize success · surface closed
priority
reconnect notice wins the slot
Tokens used
attach-sync-glyph— → accent-warningattach-sync-name-max-width— → size-160attach-sync-name-min-width— → size-40 (new, b11)banner-button-bg— Retryspinner-indicator— retrying

Strings (en): remote.size_sync.title “Couldn't sync the terminal size with the remote” · remote.size_sync.names “{names}” (line 1) · remote.size_sync.hint “The remote may still be using the old size.” (line 2; replaces remote.size_sync.body) · remote.size_sync.many “{n} surfaces — {names}” · remote.size_sync.retry “Retry” · remote.size_sync.retry_all “Retry all”.

Narrow — action under the text, × stays top-right (2026-10-09 b10)

Every banner judges narrow on the content width of its scope, before the banner margins: a surface banner on that surface, a Pane / Tab banner on that pane / tab content, a Workspace banner on the union of panes under the tab bar (what the app does today). Under --tasty-banner-narrow-below (440) the action button moves to its own line under the text, starting at the body's left edge (glyph 16 + banner-gap), banner-gap above it. The × is a dismiss affordance, not an action: it stays in the top-right of the card, as on the HTML script banner. Retry all and the retrying state (disabled + Spinner, label kept) look the same on the wrapped line. Samples at 360.

Layout spec
judged on
scope content width before margins · workspace = pane union under the tab bar
threshold
banner-narrow-below 440
action
own line · left = body edge (glyph 16 + banner-gap) · banner-gap above
×
stays top-right of the card (not part of the wrapped group)
retrying / Retry all
same on the wrapped line
sample width
360
Tokens used
banner-narrow-below— → size-440banner-gap— glyph ↔ text · text ↔ action line

HTML mirror — large-file and limit notices (2026-10-12)

Two inset banners · Mocha / Latte · wide / narrow

The two remote-size questions of a mirrored HTML surface, as banners. Same inset shell and button boxes as the script notice; no × (each is a decision) and no keyboard focus. When both a size notice and the script notice show, the size notice sits first. Full surface placement (address bar band, receiving state, stacking) lives in Plugins › HTML viewer.

2026-11-01: the remote-content notice joins the same stack between the two — size → remote content → script. See HTML remote content notice.

Layout spec
shell
script-notice inset shell · no ×
large file
download (info) · title · path left-ellipsis + size Tag warning [+N more waiting] · Decline / Allow
limit
alertTriangle (warning) · total received · limit sentence · Stop / Continue
order
size notice above script notice
narrow
banner-narrow-below 440 → actions wrap
Tokens used
html-size-banner-glyph— → accent-infohtml-limit-banner-glyph— → accent-warning

HTML remote content notice (2026-11-01)

Banner — states

When the global Allow remote content is off and the document references http(s) stylesheets, images, fonts, frames, media or scripts, the HTML surface shows a second inset notice. Same shell, life rules and state names as the script notice; it is told apart by its glyph, globeOff (new), in the same informational tone (--tasty-html-remote-banner-glyph → accent-info). The title states the policy (“…is blocked”), which is true whether the reference was found by the static scan or seen blocked by the OS, so the copy never has to tell those apart. The body names the kinds, not counts: a static count can differ from what the page actually loads. Title and body wrap and never clamp (Tasty-owned copy, b12). One action [Allow remote content], Secondary / Sm — distinct from the script notice's “Allow for this document” so two stacked notices never show the same button label.

Layout spec
glyph
globeOff 16 · html-remote-banner-glyph → accent-info
title
“Remote content in this document is blocked” · 13/600 · wraps
body
kinds, no counts · 11 · text-muted · wraps
action
Allow remote content · Secondary / Sm · no wrap
loading
disabled from load start to commit · tooltip top “Available when the document finishes loading” · × kept
reloading
Spinner 14 + “Reloading with remote content allowed” · body opacity-dimmed · no × · fades out on commit
×
hover only · hides the notice for this document, block stays
focus
never taken · click only
Tokens used
html-remote-banner-glyph— → accent-infobanner-bg— shellbanner-button-bg— Allow

Strings (en): html.remote.title “Remote content in this document is blocked” · html.remote.body “Stylesheets, images, fonts, frames and scripts from the network don't load, so the page may look incomplete. Allowing applies to this document only.” · html.remote.allow “Allow remote content” · html.remote.reloading “Reloading with remote content allowed” · html.remote.allow_loading “Available when the document finishes loading” (same text as the script key) · html.remote.marker_blocked “Remote content blocked. Click for details.” · html.remote.marker_allowed “Remote content allowed for this document”.

The address-field marker — always findable

The persistent signal lives at the end of the address field, after the mirror's “n skipped” Tag: the band is where the document's address and its loading state already are, and the tab cell's right cluster is kept for the script marker. Shown whenever the notice is not on screen. Blocked (× dismissed, or a block the OS observed without a static reference) = globeOff in glyph-dim inside a 20 hit cell with the hover fill; a click shows the notice, it never allows on its own. Allowed = globe in text-muted, tooltip only. Tooltips open top. Agent IPC/CLI queries and allows never change focus, the active tab or the marker drawing beyond the reload result. Failed (uncommitted) documents: no notice, no marker.

Layout spec
slot
address field end · after skipped Tag · field ellipsises the address first
cell
20 (html-remote-marker-hit) · glyph 14 · radius-sm
blocked
globeOff · glyph-dim · hover fill · click → notice
allowed
globe · text-muted · tooltip only
shown when
notice not on screen · never with the notice
with script marker
independent places: script = tab cluster, remote = address field · never merged
cleared by
another document committing · failed load
Tokens used
html-remote-marker-fg— → glyph-dimhtml-remote-marker-allowed-fg— → text-mutedhtml-remote-marker-size— → icon-size-sm 14html-remote-marker-hit— → size-20html-remote-marker-hover-bg— → overlay-hover

Don't make the blocked marker allow directly. It re-shows the notice, which explains scope before offering Allow — the same rule as the script lock.

Surface variants 1–10 · stacking with the script and size notices

Stacking order under the address band, one inset stack (--tasty-banner-margin 8 above, --tasty-banner-inset-gap 8 between and below): mirror size notice → remote content → script. Size blocks rendering entirely, remote content decides how the page looks, scripts decide how it behaves — and allowing remote can change what the script notice says. The two notices stay separate cards: the allows are independent and either one can be left standing. Remote-src scripts need both allows, so the script notice is judged on runnable scripts: a document whose only scripts come from the network shows the remote notice alone (7); after remote Allow reloads, those scripts become runnable and the script notice appears with its plain body (7′). With inline and network scripts both present, the script notice uses the revised bodyRemote. Detection: a static reference fires the notice. A block the OS reports without a static reference (Windows; Linux pending) shows the marker only, no late notice (8a). With no OS signal and no reference (macOS) nothing is shown (8b); the global switch in Settings stays the path.

Layout spec
order
address band → size (mirror) → remote → script → WebView
gaps
banner-margin 8 top/sides · banner-inset-gap 8 between + below
merge
never — two cards, two independent allows
script notice
judged on runnable scripts (network scripts need remote allowed)
bodyRemote
kept as a key, copy revised: shown only alongside the remote notice
static ref
notice (when the user views)
observed block, no ref
marker only (Windows · Linux TBD)
no signal, no ref
nothing (macOS)
narrow
< 440 → each card wraps its action, stack order unchanged
global on
no notice · no marker
Tokens used
banner-inset-gap— 8 between / belowbanner-margin— 8 top / sidesbanner-narrow-below— → size-440

Script notice bodyRemote (en, revised): “Buttons and menus that need JavaScript may not respond. Allowing runs the scripts inside this document; scripts from the network also need remote content allowed.”