Files
DodoSSH/src/DodoSSH.Client.Android/Theme/Phone.axaml
T
jaap-jan 0cecf1afc4
ci / build and test (push) Successful in 4m14s
ci / api image (push) Successful in 10s
ci / deploy ssh.dodotech.cloud (push) Successful in 22s
ci / android head (push) Successful in 5m6s
ci / desktop nightly (push) Successful in 3m2s
Android: ask what to connect to on Connections, the same prompt the desktop's empty terminal draws
The phone's Connections screen with nothing open had a box that took an address alone, which left a
saved machine several screens away. It now draws the desktop's prompt over the same view-model state:
"connect to:" takes a saved host's name or user@host[:port], and a password line follows only when
one is needed. Names, addresses and refusals are read exactly as on the desktop, and nothing typed
here is saved.

What a phone changes: the hosts matching so far are thumb-sized buttons that take a host outright,
since there is no Tab; CONNECT is always drawn, since the IME's return key may only close the
keyboard; BACK, and the system back gesture, step off the password line rather than leaving the
screen; and the password's label sits on its own line. The keyboard follows the question onto the
password line, but is not raised when the screen opens, where it would cover the recent list.

Recent rows now go onto the prompt too. Covered by a new ShellFlowTests case for a tapped match on
top of the shared prompt cases; manual check 11.14.
2026-10-16 14:07:13 +02:00

1041 lines
61 KiB
XML

<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
The phone's own control styles. The desktop head has an equivalent block in its App.axaml and the two
deliberately do not share one: the palette is shared because a colour must not drift, but a control's
size is a property of the input device. Every touch target here is at least 44 effective pixels tall,
which is the number the desktop's 28-pixel rows would fail.
Classes rather than implicit styles for the buttons, because the shell needs three that differ in
meaning rather than in degree: the accent one is what the screen wants you to do, the outlined one is
the alternative, and the red one is the one that costs something.
── v2 ──────────────────────────────────────────────────────────────────────────────────────────────
The second design rounded everything on its own ladder — 4 for a tag, 9 for a button or a pill, 10 for
a list row, 11 for the search well, 12 for a card, 14 for a block of monospaced output — and the numbers
below carried it for three passes: the radius said how big the thing was, so a 12 on a chip or a 4 on a
card read as the wrong size before it read as the wrong shape. Left as a record of that reasoning rather
than deleted, because the reasoning still holds; only the numbers it was reasoning about have moved.
── v5 ──────────────────────────────────────────────────────────────────────────────────────────────
This pass takes the desktop's v5 ladder instead of v2's own, on the same reversal recorded for the
fonts: the phone now matches the desktop's shape as well as its face. Three rungs rather than six —
cards and sections stay 12, a button or a field is 10, a chip or a tag is 6 — collapsing v2's 9/10/11
into the one value the desktop's buttons and fields already use, and moving its 4 up to 6 and its 14
down to 12 to land on the desktop's own chip and card numbers. The ladder still says how big a thing
is before it says what shape it is; it is just a shorter ladder now, because the desktop it is copying
never drew v2's 11-radius search well or a card any rounder than a chip's neighbour a step away, and the
same case that closed the gap between 9, 10 and 11 closes the one between 12 and 14.
Not every radius on this head belongs to this ladder. The floating action button is 28 — half its own
56, which is a circle rather than a ladder rung — and the sheets stay at 22 on their top corners only,
which is a phone idiom this codebase's own bottom sheets have used since v2 and the desktop draws
nothing like. Both are documented where they are set rather than here, for the reason FAB and sheet
radii are always documented locally: a reader who only ever meets one of them should not have to find
this paragraph to learn it was deliberate.
-->
<!--
◆ FLUENT'S OWN BRUSHES, ANSWERED ONCE HERE RATHER THAN CONTROL BY CONTROL.
The 2026-10-02 audit (M5) found the controls this file never styled still drawing Fluent's defaults
among the ones it did: a dropdown with a bright white border on a black fill beside 10-radius fields on
#101019, a checkbox in Fluent's blue, a switch drawn as a bare circle, and a focused box whose fill went
to pure black. Each of those reads its colours through a DynamicResource key, and a key defined here is
found before the theme's own, because Avalonia searches an application's styles last-first and this file
is included after FluentTheme. So one block of keys restyles every instance on the head — the ones on
screens written later included — where a selector per control would restyle only the ones somebody
remembered.
Every value is a palette brush, never a new colour. The focused border is the accent rather than a
grey: a box with the keyboard up is the one control the user is currently writing into, and the
redesign gives it the ring that says so.
-->
<Styles.Resources>
<StaticResource x:Key="TextControlBackground" ResourceKey="Field" />
<StaticResource x:Key="TextControlBackgroundPointerOver" ResourceKey="Field" />
<StaticResource x:Key="TextControlBackgroundFocused" ResourceKey="Field" />
<StaticResource x:Key="TextControlBorderBrush" ResourceKey="BorderMid" />
<StaticResource x:Key="TextControlBorderBrushPointerOver" ResourceKey="BorderMid" />
<StaticResource x:Key="TextControlBorderBrushFocused" ResourceKey="TerminalTabAccent" />
<StaticResource x:Key="TextControlForegroundFocused" ResourceKey="Text" />
<StaticResource x:Key="TextControlPlaceholderForeground" ResourceKey="TextGhost" />
<StaticResource x:Key="TextControlPlaceholderForegroundFocused" ResourceKey="TextGhost" />
<StaticResource x:Key="TextControlPlaceholderForegroundPointerOver" ResourceKey="TextGhost" />
<StaticResource x:Key="ComboBoxBackground" ResourceKey="Field" />
<StaticResource x:Key="ComboBoxBackgroundPointerOver" ResourceKey="Field" />
<StaticResource x:Key="ComboBoxBackgroundPressed" ResourceKey="Hover" />
<StaticResource x:Key="ComboBoxBackgroundUnfocused" ResourceKey="Field" />
<StaticResource x:Key="ComboBoxBackgroundDisabled" ResourceKey="Raised" />
<StaticResource x:Key="ComboBoxBorderBrush" ResourceKey="BorderMid" />
<StaticResource x:Key="ComboBoxBorderBrushPointerOver" ResourceKey="BorderMid" />
<StaticResource x:Key="ComboBoxBorderBrushPressed" ResourceKey="TerminalTabAccent" />
<StaticResource x:Key="ComboBoxBorderBrushDisabled" ResourceKey="Border" />
<StaticResource x:Key="ComboBoxBackgroundBorderBrushFocused" ResourceKey="TerminalTabAccent" />
<StaticResource x:Key="ComboBoxBackgroundBorderBrushUnfocused" ResourceKey="BorderMid" />
<StaticResource x:Key="ComboBoxForeground" ResourceKey="Text" />
<StaticResource x:Key="ComboBoxForegroundFocused" ResourceKey="Text" />
<StaticResource x:Key="ComboBoxForegroundFocusedPressed" ResourceKey="Text" />
<StaticResource x:Key="ComboBoxDropDownGlyphForeground" ResourceKey="TextFaint" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocused" ResourceKey="TextFaint" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocusedPressed" ResourceKey="TextFaint" />
<StaticResource x:Key="ComboBoxDropDownBackground" ResourceKey="Chip" />
<StaticResource x:Key="ComboBoxDropDownBorderBrush" ResourceKey="BorderHover" />
<StaticResource x:Key="ComboBoxItemBackgroundPointerOver" ResourceKey="Hover" />
<StaticResource x:Key="ComboBoxItemBackgroundPressed" ResourceKey="Hover" />
<StaticResource x:Key="ComboBoxItemBackgroundSelected" ResourceKey="Active" />
<StaticResource x:Key="ComboBoxItemBackgroundSelectedPointerOver" ResourceKey="Active" />
<StaticResource x:Key="ComboBoxItemBackgroundSelectedPressed" ResourceKey="Active" />
<StaticResource x:Key="ComboBoxItemForegroundSelected" ResourceKey="AccentText" />
<StaticResource x:Key="ComboBoxItemForegroundSelectedPointerOver" ResourceKey="AccentText" />
<StaticResource x:Key="ComboBoxItemForegroundSelectedPressed" ResourceKey="AccentText" />
<StaticResource x:Key="CheckBoxCheckBackgroundFillUnchecked" ResourceKey="Field" />
<StaticResource x:Key="CheckBoxCheckBackgroundFillUncheckedPointerOver" ResourceKey="Field" />
<StaticResource x:Key="CheckBoxCheckBackgroundFillUncheckedPressed" ResourceKey="Hover" />
<StaticResource x:Key="CheckBoxCheckBackgroundStrokeUnchecked" ResourceKey="TextGhost" />
<StaticResource x:Key="CheckBoxCheckBackgroundStrokeUncheckedPointerOver" ResourceKey="TextGhost" />
<StaticResource x:Key="CheckBoxCheckBackgroundStrokeUncheckedPressed" ResourceKey="TextFaint" />
<StaticResource x:Key="CheckBoxCheckBackgroundFillChecked" ResourceKey="Accent" />
<StaticResource x:Key="CheckBoxCheckBackgroundFillCheckedPointerOver" ResourceKey="Accent" />
<StaticResource x:Key="CheckBoxCheckBackgroundFillCheckedPressed" ResourceKey="Accent" />
<StaticResource x:Key="CheckBoxCheckGlyphForegroundChecked" ResourceKey="AccentInk" />
<StaticResource x:Key="CheckBoxCheckGlyphForegroundCheckedPointerOver" ResourceKey="AccentInk" />
<StaticResource x:Key="CheckBoxCheckGlyphForegroundCheckedPressed" ResourceKey="AccentInk" />
<StaticResource x:Key="CheckBoxForegroundUnchecked" ResourceKey="Text" />
<StaticResource x:Key="CheckBoxForegroundUncheckedPointerOver" ResourceKey="Text" />
<StaticResource x:Key="CheckBoxForegroundUncheckedPressed" ResourceKey="Text" />
<StaticResource x:Key="CheckBoxForegroundChecked" ResourceKey="Text" />
<StaticResource x:Key="CheckBoxForegroundCheckedPointerOver" ResourceKey="Text" />
<StaticResource x:Key="CheckBoxForegroundCheckedPressed" ResourceKey="Text" />
<StaticResource x:Key="ToggleSwitchFillOn" ResourceKey="Accent" />
<StaticResource x:Key="ToggleSwitchFillOnPointerOver" ResourceKey="Accent" />
<StaticResource x:Key="ToggleSwitchFillOnPressed" ResourceKey="Accent" />
<StaticResource x:Key="ToggleSwitchStrokeOn" ResourceKey="Accent" />
<StaticResource x:Key="ToggleSwitchStrokeOnPointerOver" ResourceKey="Accent" />
<StaticResource x:Key="ToggleSwitchStrokeOnPressed" ResourceKey="Accent" />
<StaticResource x:Key="ToggleSwitchKnobFillOn" ResourceKey="AccentInk" />
<StaticResource x:Key="ToggleSwitchKnobFillOnPointerOver" ResourceKey="AccentInk" />
<StaticResource x:Key="ToggleSwitchKnobFillOnPressed" ResourceKey="AccentInk" />
<StaticResource x:Key="ToggleSwitchFillOff" ResourceKey="Hover" />
<StaticResource x:Key="ToggleSwitchFillOffPointerOver" ResourceKey="Hover" />
<StaticResource x:Key="ToggleSwitchFillOffPressed" ResourceKey="Hover" />
<StaticResource x:Key="ToggleSwitchStrokeOff" ResourceKey="TextGhost" />
<StaticResource x:Key="ToggleSwitchStrokeOffPointerOver" ResourceKey="TextGhost" />
<StaticResource x:Key="ToggleSwitchStrokeOffPressed" ResourceKey="TextFaint" />
<StaticResource x:Key="ToggleSwitchKnobFillOff" ResourceKey="TextFaint" />
<StaticResource x:Key="ToggleSwitchKnobFillOffPointerOver" ResourceKey="TextFaint" />
<StaticResource x:Key="ToggleSwitchKnobFillOffPressed" ResourceKey="TextFaint" />
</Styles.Resources>
<!--
Touch has no hover, so a pointer-over fill is one that sticks after a tap until the next touch lands
somewhere else — the grey square the audit found left behind on Vaults' refresh button (M5). The
transparent shapes keep their resting fill under the pointer; pressed is the feedback a finger gets.
-->
<Style Selector="Button.icon:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Button.row:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Button.secondary:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
</Style>
<!--
◆ A selected row in a list is not lit. The hosts list says why in HostsScreen.axaml — a lit row would be
a mark with no action behind it — but only its own Border.hostrow.chosen was ever changed, and Fluent's
ListBoxItem:selected went on painting #3D486B, a slate blue the palette has never held, under whichever
row had been tapped last (audit M3). Pointer-over goes with it for the reason the button keys above do.
-->
<Style Selector="ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="ListBoxItem:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Hover}" />
</Style>
<!--
The dropdown, on TextBox.field's numbers so the two sit in one form as one kind of box: 44 tall, a
10 radius, Field on BorderMid. The colours come from the keys above; these are the geometry Fluent sets
as plain values.
-->
<Style Selector="ComboBox">
<Setter Property="MinHeight" Value="44" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="14,0,10,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<!--
A checkbox's caption on the box's own line. Fluent tops the content of a CheckBox taller than its box,
so in a 44-pixel row the label sat below the tick's centre (audit M5).
-->
<Style Selector="CheckBox">
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.primary">
<Setter Property="Height" Value="48" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="SemiBold" />
<!--
◆ Off, because Fluent's Button theme turns it on, and the glow below is drawn outside the control by
definition. With the clip on, the blur was cut at the button's square bounds and showed as a lighter
rectangle behind the rounded fill on every primary button and the FAB (audit M1). Measured in the
desktop's layout harness: ClipToBounds reads True under the theme, and cleared, the glow renders whole.
The desktop's Button.accent carries the same setter for the same reason.
-->
<Setter Property="ClipToBounds" Value="False" />
</Style>
<!--
◆ Gradient and glow, replacing a flat Accent fill — the same swap App.axaml's Button.accent made on the
desktop, and for the same reason: this design draws every primary button top-to-bottom from a lighter
violet into the accent proper, lifted off the surface with a soft violet glow rather than a border. Both
live in Palette.axaml as AccentGradient and AccentGlow, already resolvable here because the palette is
shared — this is a resource-key swap, not new colour.
Background and BoxShadow are set here rather than as plain Setters above, because Fluent's default
button template only lets a style reach the fill and the glow through the ContentPresenter it draws
itself around — the same reason the desktop's rule targets /template/ ContentPresenter rather than the
Button. Height, radius and the rest stay ordinary Setters on Button.primary itself; only the two the
template intercepts move down here.
-->
<Style Selector="Button.primary /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
<Setter Property="BoxShadow" Value="{StaticResource AccentGlow}" />
</Style>
<Style Selector="Button.primary:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
<Setter Property="Opacity" Value="0.82" />
</Style>
<!--
Disabled is drawn as flat and unlit rather than merely dimmed. The design's CONTINUE button on the
recovery screen is disabled until the checkbox is ticked, and a user who cannot tell it is disabled
reads the screen as broken rather than as waiting for them.
BoxShadow has to be cleared here too, as "none" rather than left unset — see the remark on
Button.accent:disabled in the desktop's App.axaml for why the literal string is required and an empty
BoxShadows is not: the base rule's glow Setter is still in effect wherever a more specific one does not
override it, and a disabled primary button lit with a glow would read as wanting to be pressed.
-->
<Style Selector="Button.primary:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Raised}" />
<Setter Property="BoxShadow" Value="none" />
<Setter Property="TextElement.Foreground" Value="{StaticResource TextFaint}" />
</Style>
<!--
The alternative to a primary button, and in a row of the two it is on the LEFT: Android puts the action
that confirms on the right, and every two-button row on this head had it the other way round until the
2026-10-04 redesign (audit M12) — CANCEL | SAVE, KEEP | DELETE, CANCEL | CONNECT.
-->
<Style Selector="Button.secondary">
<Setter Property="Height" Value="48" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="11.5" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
◆ Disabled is quieter than enabled, which Fluent had the wrong way round: with no rule here its #3C3C43
disabled slab filled the outline, so SFTP's unavailable SAVE FILE was the heaviest button in its row
(audit M4). The same flat-and-unlit treatment Button.primary:disabled gets, on an outline.
-->
<Style Selector="Button.secondary /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Button.secondary:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Hover}" />
</Style>
<Style Selector="Button.secondary:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="TextElement.Foreground" Value="{StaticResource TextGhost}" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Height" Value="48" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource DangerSoft}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource Danger}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10.5" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
A row in a list: the whole row is the target, and it is 54 tall because a thumb is not a mouse.
◆ VerticalContentAlignment, because that height is the whole point of this class and Avalonia's default
for content alignment is Stretch — so the content presenter stretched the caption to the full row and a
TextBlock draws its line at the TOP of what it is given. Most rows here never showed it, having a
StackPanel or a Grid of already-centred children in them, which is what made the four that did look
like four unrelated mistakes: the breadcrumb chips and the up-one-directory button on FilesScreen, and
TerminalScreen's CLOSE THIS TAB, each a bare TextBlock in a row 36 or 44 tall with no vertical padding.
Measured at those numbers, the caption sat flush against the top edge with 21 to 33 pixels below it.
The desktop head's App.axaml carries the same setter on its own shapes for the same reason, and excludes
two of them — see the remark on Button.ghost there. Nothing is excluded here: no row's content depends
on being stretched, there being no full-height strip inside any of the thirty-three, and the Grids that
stop filling hold only children that already centre themselves, so they land where they always did.
-->
<Style Selector="Button.row">
<Setter Property="MinHeight" Value="54" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="16,6" />
</Style>
<Style Selector="Button.row:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Hover}" />
</Style>
<!--
The 44-pixel square in a header — a back arrow, an add, an upload. Square rather than stretched because
it sits beside a title that must keep the width it has, and 44 rather than the glyph's own size because
a 17-pixel arrow is a 17-pixel target.
-->
<Style Selector="Button.icon">
<Setter Property="Width" Value="44" />
<Setter Property="Height" Value="44" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="17" />
</Style>
<Style Selector="Button.icon.accent">
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
<Setter Property="FontSize" Value="20" />
</Style>
<!--
◆ THE FILL BELOW IS NOT DECORATION. IT IS UNDOING ONE FLUENT PUTS THERE.
`accent` is not a name this theme invented — Fluent ships a `Button.accent` style of its own, and a
button classed `icon accent` matches it. So these three buttons were being painted with Fluent's
system-accent slab, #6275AC, a blue that appears nowhere in Palette.axaml and reads as a control from
a different application sitting in this one's header. Measured off a screenshot rather than guessed:
the fill was #6275AC where the accent is #5D42DE.
The rule above could not stop it, because it sets Foreground and FontSize and Fluent's fill is on the
ContentPresenter — which is also why `Button.icon`'s own Background="Transparent" does not reach it.
App.axaml has carried the desktop's answer to exactly this since v5b: override the ContentPresenter,
not the Button. This head simply never had one, so the desktop looked right and the phone did not.
Transparent rather than the desktop's gradient, and the difference is the control rather than an
inconsistency. The desktop's Button.accent is a filled primary slab — this head has one of those too,
and it is Button.primary. `icon accent` is a 44-pixel glyph in a header row beside other glyphs; what
"accent" asks for there is the accent *colour on the mark*, which is what the rule above gives it. A
slab would make one header icon a button and its neighbours not.
Pressed is deliberately not restated: Button.icon:pressed below sets ChromeHover on every icon button
and, coming later in this file, wins over this rule for the moment a finger is down. That is the
feedback the plain icon buttons give and there is no reason this one should differ.
-->
<Style Selector="Button.icon.accent /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<Style Selector="Button.icon.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<Style Selector="Button.icon.accent:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
</Style>
<Style Selector="Button.icon:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
</Style>
<!--
============ one key of the terminal's accessory keypad ============
A class rather than eleven sets of attributes assigned in code, and the move out of the code-behind is
most of what makes the latched state honest: Ctrl on and Ctrl off used to be two Background values
written by hand in TerminalScreen.axaml.cs, which meant the row named two of this palette's brushes
itself and any third state would have named a third. It is `latched` here, and the code toggles a class.
── the numbers, and why they are these ─────────────────────────────────────────────────────────────────
40 tall, where the row this replaced was 30. Android asks for 48 on a touch target and every terminal on
the platform lands near it; 30 was chosen to save a strip of the terminal above and took the keypad past
the point of working, which is the wrong end of that trade. 40 with a 2-pixel margin gives 44 of pitch,
which is the same figure TextBox.field uses for a box a thumb has to hit.
HorizontalAlignment Stretch, because these live in UniformGrid cells and the cell is the share of the
width the key is entitled to. Left to Center a key would shrink to its label and "|" would be a sliver
between two wide neighbours — which is the layout the flexed row exists to stop.
── pressed has to be visible, and it was not ───────────────────────────────────────────────────────────
These keys are Focusable="False" so that using one does not take the keyboard away from the terminal
(see TerminalScreen.axaml.cs), and a button that never takes focus also never shows the focus adorner
Fluent would otherwise give it. That left a press with no feedback at all on a control whose entire job
is to be pressed while somebody is looking at the line of output above it. The pressed fill below is the
only thing that says a key was hit — and on a keypad over a remote shell, "did that send?" is a question
worth answering in the interface rather than in the scrollback.
-->
<Style Selector="Button.hotkey">
<Setter Property="Height" Value="40" />
<Setter Property="MinHeight" Value="40" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="Padding" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="{StaticResource Panel}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.hotkey /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Panel}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
</Style>
<Style Selector="Button.hotkey:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Panel}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
</Style>
<Style Selector="Button.hotkey:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Active}" />
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
</Style>
<!--
Latched Ctrl. The same fill a press gives, held rather than flashed — a modifier that is on and does not
look on is how somebody sends ^L to a database prompt believing they typed an l.
-->
<Style Selector="Button.hotkey.latched">
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<Style Selector="Button.hotkey.latched /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Active}" />
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
</Style>
<Style Selector="Button.hotkey:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Panel}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
</Style>
<Style Selector="Button.hotkey:disabled">
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
</Style>
<!--
The floating action button the design has asked for since v2, and which this theme refused twice — on
the grounds that it would open an editor this head had not got, and that an unused style is a claim the
control exists somewhere. Both were true. The second is why the style arrives in the same commit as the
button rather than ahead of it.
56 rather than the 44 the header icons use. It is the only control on the screen that floats over
content rather than sitting in a bar, so it has no neighbouring chrome to borrow an edge from, and a
thumb reaching the bottom-right corner of a 360dp screen is doing it without looking.
Accent-filled, which it shares with Button.primary and with nothing else — and it means the same thing
in both places: the one action on the surface that is not a choice between peers. Circular by radius
rather than by a Path, so the pressed state the template draws is the same shape as the button — 28,
exactly half its own 56, which is a geometric constraint rather than a ladder rung and does not move
with the rest of this file's radii.
◆ Gradient and glow since v5, matching Button.primary rather than staying a flat fill once that one
moved. The two are the only accent-filled controls on this head and are read as one idea — "the thing
this surface wants you to do" — so a flat FAB beside a gradient primary button would be the seam this
codebase's palette file keeps warning about, one screenshot over from the button it echoes. The glow
reads as well on a floating circle as it does on a bar-anchored rectangle: if anything a control that
already floats over content earns a lift more than one sitting in a row of chrome does, so it is kept
rather than dropped for the FAB's own shape.
Still only on HOSTS. The design puts a second one on S3 and that editor genuinely does not exist yet, so
the style being here is not permission to draw one there.
-->
<Style Selector="Button.fab">
<Setter Property="Width" Value="56" />
<Setter Property="Height" Value="56" />
<Setter Property="Padding" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="28" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="FontWeight" Value="SemiBold" />
<!-- Off for the glow; see the remark on Button.primary's own setter, which is the same fix. -->
<Setter Property="ClipToBounds" Value="False" />
</Style>
<!-- See the remark above Button.primary's own /template/ ContentPresenter rule: BoxShadow has no home on
a Button itself, so the fill moves down here alongside it rather than staying a plain Setter. -->
<Style Selector="Button.fab /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
<Setter Property="BoxShadow" Value="{StaticResource AccentGlow}" />
</Style>
<Style Selector="Button.fab:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
<Setter Property="Opacity" Value="0.82" />
</Style>
<!--
The dimmed area behind a sheet, and a Button rather than a Border because tapping it has to mean
something — see the add sheet in HostsScreen, which is dismissible for the reason the host-key sheet is
not.
62% of Canvas, written out because the palette holds no alpha variant of a surface. The pressed rule is
not decoration: a Button's default template lights its ContentPresenter when touched, so without it the
scrim would flash a pale rectangle across the whole screen on the way to closing — which reads as the
interface glitching rather than as a tap being received.
-->
<Style Selector="Button.scrim">
<Setter Property="Background" Value="#9E05050A" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="Button.scrim:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#9E05050A" />
</Style>
<Style Selector="Button.scrim:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="#9E05050A" />
</Style>
<!--
A card: one item, one rule, one thing you can act on. The design uses it for a key, a snippet, a
transfer and the panel at the foot of the keychain, and they are all the same shape deliberately —
the screens differ in what they hold rather than in how they are built.
-->
<Style Selector="Border.card">
<Setter Property="Background" Value="{StaticResource Sidebar}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="14,13" />
</Style>
<!--
A block of monospaced output — a snippet's command, a log, the terminal. One surface for all three, and
it is not one of the palette's chrome greys: see TerminalSurface in Palette.axaml.
-->
<Style Selector="Border.output">
<Setter Property="Background" Value="{StaticResource TerminalSurface}" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="12" />
</Style>
<!--
A small monospaced tag: a vault's name, a sync badge, what is open on the files screen. Not a control —
it is never pressed — so a Border rather than a Button, and the class exists so that the four screens
drawing one cannot disagree about its padding.
-->
<Style Selector="Border.tag">
<Setter Property="Background" Value="{StaticResource Chip}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="7,2" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Border.tag > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="9" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<!--
A tag that is also a button: the picker in the host editor. It borrows Border.tag's geometry rather than
inheriting it, because a Button is not a Border and Avalonia's selectors are structural — the numbers
are repeated and the two have to move together, which is why they sit adjacent.
Worn is filled, unworn is outlined, so a row of these reads as the host's tags rather than as a form
about them. 36 tall rather than 44: it is a chip, its neighbours are chips, and a row of 44-pixel pills
would be a wall. The whole chip is the target and they are spaced 6 apart, which is what keeps a miss
landing on nothing rather than on the wrong tag.
-->
<Style Selector="Button.chiptoggle">
<Setter Property="MinHeight" Value="36" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="11,0" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.chiptoggle.worn">
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<Style Selector="Button.chiptoggle.worn /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Chip}" />
</Style>
<Style Selector="Button.chiptoggle:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Hover}" />
</Style>
<!-- An outlined tag, for a fact about an item rather than a name: a type, an algorithm, a count. -->
<Style Selector="Border.tag.outline">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="5,1" />
</Style>
<Style Selector="Border.tag.outline > TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="8.5" />
</Style>
<!--
A category chip. A RadioButton rather than a ToggleButton because the options are exclusive and a screen
reader should say so; the bullet its default template draws is removed, since the chip's own fill is
what carries the state.
Checked is a filled surface with accent *text*, not an accent fill. v2 makes that distinction
everywhere — see the remark on AccentText in Palette.axaml — and a chip is where it matters most: a row
of four solid blue lozenges is a row of four things that all look like the primary action.
◆ Its own radius rather than Border.tag's or Button.chiptoggle's, despite the name. v2 drew this control
on the ladder's button-or-pill rung rather than its tag rung — 9, not 4 — and the v5 pass carries the
same rung forward to 10 rather than to Border.tag's 6: it is a much larger control at 34 tall against a
tag's line-height, and a radius picked for a 34-pixel chip is not the one a 20-pixel tag needs, whatever
the class is called. See the remark on Phone.axaml's own ladder, above.
-->
<Style Selector="RadioButton.chip">
<Setter Property="MinHeight" Value="34" />
<Setter Property="Padding" Value="13,6" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<!-- Sans since the 2026-10-04 redesign: a category is a word, "SSH keys 3", rather than an identifier. -->
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="Chip" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}"
MinHeight="{TemplateBinding MinHeight}">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="RadioButton.chip:checked">
<Setter Property="Background" Value="{StaticResource Active}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
◆ A segmented control, which this file used to argue against having.
The argument was that the one the v2 design drew — LOCAL / REMOTE over the file listing — had nothing to
switch on a phone, and the choices that were real were chip rows. The 2026-10-04 redesign has a choice
that is a segmented control in every sense: the open vault's HOSTS · KEYCHAIN · SNIPPETS, three equal,
exclusive views of one thing, always all three, never scrolled. Chips would make it a filter; a well with
one lit segment says it is where you are.
The well is Panel on BorderSubtle at 14, the lit segment Active at 10 — a step inside it on the ladder,
as a button sits inside a card. Sans and 14, since these are words rather than identifiers.
-->
<Style Selector="Border.segmented">
<Setter Property="Background" Value="{StaticResource Panel}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtle}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Padding" Value="4" />
</Style>
<Style Selector="RadioButton.segment">
<Setter Property="MinHeight" Value="40" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}" CornerRadius="{TemplateBinding CornerRadius}"
MinHeight="{TemplateBinding MinHeight}">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="RadioButton.segment:checked">
<Setter Property="Background" Value="{StaticResource Active}" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
</Style>
<Style Selector="RadioButton.segment:pressed">
<Setter Property="Background" Value="{StaticResource Hover}" />
</Style>
<!--
The eye beside a secret box: outlined like the box it sits beside, lit while it is revealing. Lit with
the accent ring the focused box wears, so "you can read this" is said in the same colour as "you are
typing here".
-->
<Style Selector="ToggleButton.reveal">
<Setter Property="Padding" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="{StaticResource Field}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="ToggleButton.reveal /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Field}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
</Style>
<Style Selector="ToggleButton.reveal:checked /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Active}" />
<Setter Property="BorderBrush" Value="{StaticResource TerminalTabAccent}" />
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
</Style>
<!--
One Material icon: a codepoint of the embedded font the desktop draws its icons with, standing where a
Unicode glyph stood (audit M7). Hidden from the accessibility tree — the codepoint reads as nothing, and the
control around it carries the name — and it inherits its colour, so an icon in a button is the button's.
-->
<Style Selector="TextBlock.icon">
<Setter Property="FontFamily" Value="{StaticResource IconFont}" />
<Setter Property="FontSize" Value="20" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
</Style>
<Style Selector="TextBox.field">
<Setter Property="Height" Value="44" />
<Setter Property="Padding" Value="14,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="{StaticResource Field}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" />
</Style>
<!--
◆ The two port boxes, which are NumericUpDowns and were the only unthemed controls on this head.
They sit in a two-column Grid with a plain TextBox.field beside them — port next to username in the host
editor, and port next to default-user in the group editor — so the mismatch was not subtle: a bright
Fluent border and a near-black fill against this theme's #101019 and hairline, at the one place on the
screen where the two are a hand's width apart and directly comparable.
Not `Classes="field"` on the two call sites, because that class is a TextBox class and this is not a
TextBox: NumericUpDown is a ButtonSpinner wrapping one, so half the chrome being corrected belongs to
the spinner and would not take a TextBox setter. Typing the control instead also means a third port box
added later is right without anybody remembering, which is the property the desktop's own bare
`Selector="NumericUpDown"` in App.axaml has had since it was written.
Three selectors rather than one for the same reason Button.icon.accent needs two: Fluent puts the fill
and the border on template parts, and a value set on the control alone is a TemplateBinding the theme's
own wins over. The numbers are TextBox.field's, exactly — 44 tall, 10 radius, Field on BorderMid — so
the pair reads as one row of two boxes rather than as a box and a visitor.
-->
<Style Selector="NumericUpDown">
<Setter Property="Height" Value="44" />
<Setter Property="MinHeight" Value="44" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="{StaticResource Field}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="NumericUpDown /template/ ButtonSpinner">
<Setter Property="Background" Value="{StaticResource Field}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="MinHeight" Value="44" />
</Style>
<!--
The inner box gives its chrome up entirely: the spinner above is drawing the border and the fill, and a
TextBox that kept its own would put a second rounded rectangle a pixel inside the first.
-->
<Style Selector="NumericUpDown /template/ TextBox">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="14,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" />
</Style>
<!--
Every box on this head that takes a secret, and it is a class rather than two attributes repeated five
times because the two attributes are not interchangeable and both are needed.
<b>PasswordChar is what the screen shows; ContentType is what the keyboard is told.</b> The desktop head
needs only the first — a Windows text box has no opinion about what is being typed into it. Android's
software keyboard does: left at its default it treats a passphrase box as ordinary prose, which means
the suggestion strip offers completions from everything that phone has ever been typed into, and the
IME's own learning dictionary remembers what was typed. Dots on screen and a word-suggestion bar above
them is the worst of both — the secret is hidden from the person typing it and offered to the room.
TextInputOptions.ContentType is the property the Android backend maps onto InputType, and Password is
what turns the suggestions off and keeps the entry out of the dictionary.
A field class is not implied. Two of the five boxes carry their own metrics — see LockedScreen — so this
one sets nothing about size or colour, and the other three say Classes="field secret".
-->
<Style Selector="TextBox.secret">
<Setter Property="PasswordChar" Value="•" />
<Setter Property="TextInputOptions.ContentType" Value="Password" />
</Style>
<!--
The Connections prompt's two lines, typed into the way a shell is: no fill and no border in any state, so
the text sits on the pane where its label leaves off and the caret is the only sign the line takes input.
44 tall all the same, which is this head's touch floor — the line has no box to aim at, so the whole
height of it has to be the target. See ConnectPrompt.axaml. Declared after .secret, which it combines with
on the password line and sets nothing in common with.
-->
<Style Selector="TextBox.prompt">
<Setter Property="MinHeight" Value="44" />
<Setter Property="Padding" Value="0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="CaretBrush" Value="{StaticResource AccentText}" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="14" />
</Style>
<Style Selector="TextBox.prompt /template/ Border#PART_BorderElement,
TextBox.prompt:pointerover /template/ Border#PART_BorderElement,
TextBox.prompt:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<!--
The live dot, and the class name is the same one the desktop sidebar uses so the two heads cannot
drift on what green means: a terminal is open on this host right now. Deliberately not reachability —
nothing pings anything, and a dot meaning "up" would be a claim the application never checks.
Live rather than Accent since v2. The accent is blue now and this is not an accent: it is a fact about
a host. See the remark on Live in Palette.axaml.
-->
<Style Selector="Ellipse.dot">
<Setter Property="Width" Value="7" />
<Setter Property="Height" Value="7" />
<Setter Property="Fill" Value="{StaticResource TextGhost}" />
</Style>
<Style Selector="Ellipse.dot.live">
<Setter Property="Fill" Value="{StaticResource Live}" />
</Style>
<!--
Amber, and it does not contradict the remark above. That one says green is a fact about a host rather
than an accent, and this is the colour for a fact that is not settled yet: green is what is true, purple
is what you can press, and a connection still being made is neither. The palette's own rule gives amber
to the caveat worth reading, which is exactly what this is.
Only the tab strips use it, and only for a tab with no shell behind it yet — the same amber, from the
same brush, as the track and the running step on the connecting screen, so that a tab and the screen it
opens agree about what is happening. See TerminalScreen.axaml.
-->
<Style Selector="Ellipse.dot.connecting">
<Setter Property="Fill" Value="{StaticResource Warn}" />
</Style>
<!--
Red for a shell whose connection broke, splitting the grey that used to cover every dead tab. A shell
somebody exited and a shell that was cut off are the two dead states with the least in common, and on a
phone the difference matters more than it does on a desk: a mobile link drops on its own, and the tab
that went quiet while the screen was off is the one worth dialling again. See
TerminalTabViewModel.IsDropped.
-->
<Style Selector="Ellipse.dot.dropped">
<Setter Property="Fill" Value="{StaticResource Danger}" />
</Style>
<!-- Every label, count, address and fingerprint in this design is monospace. See Palette.axaml. -->
<Style Selector="TextBlock.mono">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="TextBlock.label">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="9.5" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
◆ Wraps, like .body below it, and the omission was doing real damage rather than none.
A TextBlock that neither wraps nor trims does not shrink to fit and does not report that it did not:
it lays out at its full single-line width and the parent clips whatever runs past the edge. So a
.detail long enough to overflow ended mid-word against the screen, with no ellipsis to say anything
was missing — "A machine to connect to, filed under a group if you" on the add sheet, which reads as
a sentence somebody forgot to finish rather than as text that was cut.
It is a style rather than an attribute on the ones that overflow today, because which of the fifty-odd
.detail blocks overflows is a function of the device's width and the user's font scale, not of the
string: every one of them is a candidate at 320dp that is fine at 411dp, and fixing the handful that
happen to be visibly wrong on a Pixel 5 would leave the rest to be found one narrow phone at a time.
The nine that genuinely want one line — list rows and log lines, where a wrapped detail would push the
row's own height around — now say TextWrapping="NoWrap" beside their TextTrimming, which is the pairing
that was always meant and was previously being had by accident.
-->
<!--
◆ PROSE IS IN THE SANS FACE NOW, AND THAT IS THE RECORDED DECISION CATCHING UP RATHER THAN A NEW ONE.
docs/design-import-gaps.md ("The phone catches up") says the phone shares the desktop's faces, and on
the desktop a hint, a caption and a settings title are Montserrat while the mono is kept for what is
machine-shaped. These three still set MonoFont, so every paragraph on this head was JetBrains Mono —
long, wide-set lines that the 2026-10-02 audit (M2) found tiring to read on the enrolment, settings and
SFTP screens. They now take the application's default family, which is Montserrat (see
DodoSshAndroidApplication.CustomizeAppBuilder).
One size step up with the change: Montserrat at the old mono sizes set noticeably smaller on the line,
and 10-pixel captions were already at the floor. A block that shows an address, a path or a command
rather than a sentence says Classes="detail code" at its own site — see TextBlock.detail.code below — so
the split is still made where the content is known.
-->
<Style Selector="TextBlock.detail">
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
<Setter Property="FontSize" Value="11.5" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style Selector="TextBlock.body">
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="12.5" />
<Setter Property="LineHeight" Value="19" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style Selector="TextBlock.title">
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!--
The other half of that split: a detail line that is an address, a path, a fingerprint fragment or a
command. Combined with .detail rather than replacing it, so wrapping and colour stay the caption's.
-->
<Style Selector="TextBlock.detail.code">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="11" />
</Style>
<!--
A screen's own name, in the header. The one place this design is not monospaced: the design sets every
heading in its sans face and keeps the mono for what is machine-shaped — an address, a fingerprint, a
count, a command. That split is worth honouring even though both resolve to substituted fonts here.
-->
<Style Selector="TextBlock.heading">
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="17" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!--
A group heading over a list: PRODUCTION, STAGING. Letter-spaced, and quieter than the rows under it —
but TextDim rather than the design's own #565E7E. In a flat list these headings are the only thing
separating one block of hosts from the next, and at 10px with a letter-space the fainter step is under
3:1 on this background. Quieter than a host row, still readable, which is what "quieter" has to mean.
-->
<Style Selector="TextBlock.section">
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="LetterSpacing" Value="1" />
</Style>
<!--
A fingerprint, and it is a style rather than four repeated setters because the rule it encodes is a
product rule: never truncated, and always breakable across lines rather than clipped. Four screens show
one, and the day one of them clips is the day somebody approves the wrong key.
-->
<!--
◆ Declared twice, and this is the bug the rule above exists to prevent rather than a tidiness problem.
Avalonia's type selectors are exact: `TextBlock.fingerprint` does not match a SelectableTextBlock, which
derives from it. Every fingerprint on this head is selectable — you want to be able to copy one and
compare it — so with only the first selector the style applied to none of them, and a SHA256 fingerprint
rendered in the proportional font at the inherited size with TextWrapping left at NoWrap. At 360dp that
runs off the edge and clips, which is exactly the thing the comment above says must never happen.
The desktop head already carries the same pair for `mono` (App.axaml), for the same reason. Avalonia has
an `:is(TextBlock)` form that would match both in one selector; two explicit lines are used here because
the duplication is the thing worth seeing when somebody adds a third fingerprint site.
-->
<Style Selector="TextBlock.fingerprint">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="LineHeight" Value="18" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style Selector="SelectableTextBlock.fingerprint">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="Foreground" Value="{StaticResource Text}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="LineHeight" Value="18" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
</Styles>