Files
fusion/packages/dashboard/app/components/settings/SettingsFieldRow.css
gsxdsm 55745af80f Settings: one type scale, every setting searchable, nav grouped by topic (#2158)
Settings had three compounding problems: you couldn't find a setting,
and once you found it, it didn't look like its neighbour.

## Organization — the nav was grouped by the wrong thing

It was grouped by **scope** (Global / Runtimes / Project). Scope is an
*attribute* of a setting, not a category — nobody opens Settings
thinking "I need a project-authority setting." That single choice split
five concepts across two groups, including **two nav entries both
labelled "MCP Servers"**, distinguishable only by a small icon.

Now grouped by topic, with paired sections adjacent and stating their
own scope:

![Topic-first settings
navigation](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/nav-topic-groups.png)

```
PREFERENCES     Appearance · Keyboard Shortcuts · Notifications · General · Global
PROJECT         General · Project · Commands & Scripts · Worktrees · Merge
AI & MODELS     Models · Global · Models · Project · CLI Agents · Agents & Permissions · Prompts · Memory
AUTOMATION      Scheduling & Capacity · Scheduled Evals
INTEGRATIONS    Authentication · MCP Servers · Global · MCP Servers · Project · Plugins · Runtimes · Secrets
INFRASTRUCTURE  Node Sync · Node Routing · Remote Access · Backups
ADVANCED        Experimental Features
```

## Search — it now finds settings, not just sections

Search matched only hand-written `searchableText` keyword arrays per nav
entry, and those demonstrably rotted: Project Models accumulated 20
keywords across **two separate fixes** (FN-7907, then
title-summarization on 2026-07-14) because operators searched
"summarize" and got nothing.

Every setting is now indexed from **its real label and help text** — 123
settings across 18 sections, zero curated keywords. The same query that
was patched twice by hand:


![Search returns individual
settings](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/search-summarize.png)

Two of those four hits match on *help text*, which a keyword index
structurally cannot do. Picking a result jumps to the exact control,
**opens its collapsed disclosure**, and highlights it:

![Jump to
field](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/jump-highlight.png)


**This can't rot again.** `settings-search-index.test.ts` extracts every
`descriptor={{ key }}` from section sources and fails the build if one
isn't indexed. "All settings are searchable" is an enforced invariant,
not a promise.

## Consistency — one type scale

A text field's label and a toggle's label in the same section were
styled **three different ways**: `.form-group label`
(12px/600/uppercase/muted), a `.settings-content` override narrowing it
to 0.72rem, and `.checkbox-label` (13px/500/sentence-case) whose every
declaration carried `!important` purely to out-specify the other two.

| | Before | After |
|---|---|---|
| Sections on shared primitives | 1 | **18** |
| Indexed searchable settings | 0 | **123** |
| `form-group` in settings | 226 | **98** |
| `checkbox-label` in settings | 79 | **40** |


![Appearance](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/appearance.png)

![Scheduling](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/scheduling.png)

![Backups](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/backups.png)

![Worktrees](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/worktrees.png)

![Memory](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/memory.png)

![Merge](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/merge.png)

![Notifications](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/notifications.png)
![General ·
Project](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/general.png)

**Global `.form-group` is untouched** — 35 non-settings files style
forms with it, so settings migrate *off* it rather than restyle it
underneath the rest of the dashboard.

### A real latent bug this surfaced
`--font-size-sm` and `--font-size-md` were named by **12 declarations**
across AgentDetailView, DockTaskList, SetupWizardModal, and
ModelOnboardingModal — but **defined nowhere**. Those rules silently
no-op'd and inherited. The command-center token guard is the only thing
that catches this class of omission and it doesn't cover those files.
The scale is now complete (`2xs · xs · sm · base · md · lg`); `md` is
1.125rem to hold current rendering, since unstyled `h3` already inherits
the 1.17em UA default.

## What deliberately did *not* migrate

Some rows stay bespoke **on purpose**, not from incompleteness:

- **Password fields** (`ntfyAccessToken`, `githubAuthToken`,
`gitlabAuthToken`) — `SettingsTextRow` hardcodes `type="text"`, so
migrating would have **rendered stored tokens unmasked**.
- **Help text with embedded `<code>`/links** — `descriptor.help` is a
single string; flattening would drop markup or reword copy.
- **`<details>` progressive disclosure** in Merge — a descriptor's help
renders unconditionally, so migrating ~10 rows would delete the
disclosure and produce a wall of prose.
- **Dynamic flag lists** (Experimental) and bespoke editors/CRUD
(Prompts, Agents & Permissions, Plugins, KeyboardShortcuts' capture
widget) — no settings field name and no i18n key to anchor honestly.

## Data-model ambiguities surfaced (not papered over)

These need a human call and are **not** fixed here:

- **Five keys are declared in BOTH `DEFAULT_GLOBAL_SETTINGS` and
`DEFAULT_PROJECT_SETTINGS`**: `worktrunk`, `testMode`, `gitlabEnabled`,
`gitlabAuthToken`, `gitlabAuthTokenType`. No scope badge can be stamped
honestly, so those rows are left bespoke.
- **`globalMaxConcurrent` lives in the *project* blob** despite its
name, its dedicated global endpoint, and the "Global" header it renders
under. Its badge is omitted rather than assert a contradiction.
- **Two settings were editable from two screens**: `gitlabEnabled`
(General + Merge) and `githubTrackingDefaultRepo` (General + Global
General). Both are ambiguous-scope and custom widgets, so deduplication
is left as follow-up.


## Follow-ups from review

**`SettingsTextRow` gained `type`, so the token rows could migrate.** It
hardcoded `type="text"`, which is why every secret-bearing row (ntfy
access token, GitHub/GitLab tokens, the Cloudflare tunnel token) stayed
hand-rolled — migrating would have rendered stored secrets in plain
text. `password` rows now default to `autocomplete="off"` so a browser
never offers to save an API token, and masking is pinned by tests: a
regression there would not throw and would not look wrong in review, the
field would simply render the token.

That also made them findable. Searching "token" previously matched
nothing useful; it now returns 9 settings including *Access token* and
*Tunnel token*:


**Jump-to-field now reveals collapsed disclosures.** Rows inside a
closed `<details>` are in the DOM but invisible, so the jump scrolled to
and highlighted a control the operator could not see. The settings most
worth searching for are exactly the ones behind "Advanced".

**The scope banner is gone.** It claimed one scope for a whole section,
which was false wherever a section mixed them — Appearance is a "global"
nav entry whose task-presentation toggles are all project-scoped.
Per-row badges already say this accurately, so the banner and its dead
CSS are removed.

**Scheduling is split by scope.** `globalMaxConcurrent` moved to its own
`Scheduling · Global` section instead of sitting above the project
settings behind an in-section subheading. One section held two authority
levels, so "does this affect my other projects?" depended on which
subheading you had scrolled past — and a search result landing
mid-section shows no subheading at all.

![Scheduling ·
Project](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/scheduling.png)

**Text-entry padding is now genuinely uniform.** Settings shipped two
input treatments: `.input`/`.select` (6px 10px at 13px) and the global
`.form-group input` rule (8px 12px at 14px). Padding depended on whether
an ancestor happened to be a `.form-group`, and because `.form-group
input` (0,1,1) out-specifies `.input` (0,1,0), naming the standard class
on a nested control did nothing. Measured in-browser after the fix: **51
controls across four sections, one appearance, zero outliers.** The same
specificity trap was silently re-imposing the uppercase/muted label
treatment on migrated rows nested in a `.form-group`; fixed as an
invariant rather than per-row, since sections legitimately keep
`.form-group` around bespoke content.

## Reconciling with #2147 (please review this call)

PR #2147 landed while this branch was open and deliberately moved the
two import auto-translate controls **off** `SettingsToggleRow` onto
`checkbox-label`, pinning that markup with a test written to survive *"a
refactor back onto the primitive"*. Its objection was that the primitive
rendered a right-aligned toggle switch clashing with the section's
native checkboxes — two idioms in one section.

Both halves of that objection are now gone: the primitive renders a
native checkbox **before** its label, and every checkbox in that section
— including the neighbour the test asserts parity against — renders
through it. The idiom split is resolved by migrating all of them rather
than de-migrating these two, so the markup assertions now track the
primitive. **Every behavioural contract from #2147 is kept and still
pinned**, and one was a real bug on this branch: switching
auto-translate off wrote `false` where it must write `undefined`,
leaving an explicit opt-out in the settings blob instead of staying
unset.

#2147's curated translate keywords are preserved for the genuine
vocabulary gaps ("localize", "localization", "foreign language issues")
that appear in no copy. FN-8016's rewritten `taskPopupsBoardListOnly`
copy (default now enabled) is adopted into the migrated row and its
search entry.

Worth noting: #2147's own FNXC says the translate controls were
*"effectively unfindable"* because *"settings search only matches
curated terms plus advertised i18n keys"* — 25 keywords hand-added days
ago. That is precisely the rot this PR's derived index removes.


## Source Control — the duplicate had a cause

GitLab settings were split across **three** sections (General: enable +
URLs; Merge: auth token + type; Global General: all five at global
scope) and GitHub across two. That split is *why* `gitlabEnabled` ended
up writable from both General and Merge — two enable toggles for one
key, last-save-wins.

A `Source Control · Global` / `· Project` pair now owns all 17 keys,
adjacent under Integrations, with **one** GitLab disclosure and **one**
enable toggle:

![Source Control ·
Project](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/source-control.png)

Key ownership moved with them in `section-keys.ts` / `save-split.ts`, so
every key has exactly one owner (`section-keys.test.ts` enforces
disjointness).

**A latent bug surfaced by the move:** nine sites outside the registries
hardcode `"global-general"`/`"general"` and silently gate **scope
routing** — four in `save-split.ts`, five in `SettingsModal.tsx`. Left
stale, global GitLab edits would have been written as project overrides.
Also verified against the schema: **all five `gitlab*` keys AND
`githubTrackingDefaultRepo`** are declared in both defaults (more than
the four originally identified), so those rows carry no scope badge
rather than assert a scope the data model can't support.

## Help moved behind a "?" beside every label

Rendering every description inline turned dense sections into walls of
prose — median help is ~100 chars, some past 400. That pressure is what
made Merge invent its own "More details" disclosure, so one section
showed two idioms. Measured across sections, Merge's help was **not**
unusually long (median 103 vs Appearance's 168, which rendered inline),
so the disclosure wasn't earning its keep.

The copy is deferred **visually, not removed**: the bubble is always
rendered and only fades in, so it stays in the accessibility tree for
`aria-describedby`, stays findable with in-page find, and **the search
index keeps matching on help text**. Errors are never deferred — a
validation message you must hunt for is one you won't see.

Only ~24 of 136 `<small>` blocks were actually row help. The rest stay
inline on purpose and aren't help: validation errors, live status,
empty-state explanations, per-option descriptions inside a multi-select,
and copy explaining *why* a control is disabled.

`children: ReactNode` (not a string) is what let the `<code>`-bearing
and link-bearing rows migrate without rewording your copy — a string API
is precisely why they were hand-rolled before.

### Mobile, verified on a 390px viewport

![Help tip on
mobile](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/mobile-help-tip.png)

Driving a real phone-sized viewport caught two bugs that neither jsdom
nor code review did:

- **Bubble rendered off-screen.** The label line reads "Name [scope] ?",
so the "?" sits well right of centre; anchored to the trigger, the
bubble spanned x=338→658 against a 390px screen — 268px unreachable.
`max-width` clamps width but can't help when the *anchor* is near the
edge. It now anchors to the row (`inset-inline: 0`): re-measured at
x=20→370 inside 390. An earlier comment claimed this behaviour; only the
comment existed.
- **Two bubbles open at once.** Outside-`pointerdown` dismissal misses a
path: `click` fires with **no pointer event** when Enter/Space activates
a focused trigger, so a keyboard user opening a second tip left the
first open underneath. Tips now broadcast on open and close each other;
the regression test asserts the bare-click path specifically.

Also verified on touch: tapping outside dismisses, and opening a second
tip closes the first — no stranded bubbles.

**Checkbox wrapping.** On a 390px viewport a long label ("Keep task
popups on the view where they were opened") stranded its checkbox alone
on line 1, with the text on lines 2-3 and the badge on line 4. The head
was a flex row and the label was a flex ITEM, so once it no longer fit
beside the checkbox the whole label wrapped rather than its text. Label
+ badge + tip now form one group that absorbs the wrapping, leaving the
checkbox as the only sibling item; continuation lines align under the
first word.

Audited every checkbox and radio on all 31 screens at 390px afterwards:
**101 controls, all on the first line of their label text.** (The
audit's first pass flagged 41 — all false positives from measuring
`<label>` elements whose text is a bare text node, i.e. the label box
included the checkbox. Re-measuring the text nodes themselves via Range
cleared them.)



### Every row, including the ones that stayed bespoke

Merge was the last holdout — 18 `<details>` "More details" disclosures
plus 4 inline blocks, an idiom it invented and no other section used.
All 22 now use the same "?":


![Merge](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/merge.png)

Project Models likewise rendered lane help as prose while the global
lanes next door already used the tip; its help now hangs on the existing
lane label row beside the Override/Inherited badge (the badge is live
state and stays visible; the fallback chain behind it is what you open
deliberately).

**Audited all 31 screens programmatically** (label treatments, control
padding, row overflow, leftover banners, horizontal scroll):
- **one label treatment on every screen**, one control treatment, zero
overflowing rows, zero banners, no horizontal scroll
- the copy still rendered inline is deliberately not row help:
validation errors, live status, block descriptions, per-option text
inside multi-selects, and copy explaining *why* a control is disabled

**Known gap:** the three plugin runtime screens (Hermes / OpenClaw /
Paperclip) still render inline help. They delegate to
`HermesRuntimeCard` / `OpenClawRuntimeCard` / `PaperclipRuntimeCard` —
separate card components outside the settings tree — and their copy *is*
genuine row help ("Leave blank to resolve hermes from your PATH"). They
are advanced-only and were left out of this pass rather than swept in at
the end without review.

## Padding and label consistency (measured, not eyeballed)

Two idioms were still visible on one screen — Merge rendered "PLAN
APPROVAL MODE" in caps directly above "Auto-merge conflict retries" in
sentence case. Rows that deliberately stay bespoke inherited the global
`.form-group label` treatment.

| | Before | After |
|---|---|---|
| Label treatments | uppercase/muted/11.5px **and** sentence/14px | **70
labels, one treatment** |
| Control padding | `6px 10px` **and** `8px 12px` | **81 controls, one
treatment** |
| Row gaps | 12 / 16 / 20 by adjacency | **0** — every row owns its
space |

The cause was a specificity trap: `.form-group input` (0,1,1)
out-specifies `.input` (0,1,0), so naming the standard class on a nested
control did nothing. Fixed settings-scoped; the global `.form-group` is
untouched (35 non-settings files depend on it, where uppercase is that
context's convention).

## Advanced settings toggle — verified

Confirmed in-browser after the regroup: **OFF → 15 sections / 5 groups;
ON → 31 sections / 7 groups**, `data-show-advanced` flips, preference
persists. The now-empty **Infrastructure and Advanced group headers are
correctly hidden** when off — the case the regroup could have broken,
since those groups contain only advanced sections.

## Pre-existing failures found (verified NOT caused by this PR)

Each verified by running the identical file on the parent commit and
diffing the **failure sets**, not just the counts:

| Failure | Verified |
|---|---|
| `SettingsModal.scheduling-merge.test.tsx` — 55 failures | identical
set before/after |
| `SettingsModal.remote-notifications.test.tsx` — 16 failures |
identical set before/after |
| `settings-default-descriptions.test.tsx` — `sqliteMigrationNotice`,
`postgresMigrationInboxMessageSentAt` | fails on clean tree |
| i18n `parity.test.ts` — 12 violations
(`settings.general.autoTranslate*`, `taskDetail.plan.*`) | 12 before, 12
after |

**This PR adds zero new failures.**

Fixed along the way: a stale `AppearanceSection` assertion testing copy
FN-7945 deliberately rewrote (failing silently), the ungrammatical "1
matching sections", and `&ldquo;` rendering literally on screen.

## Verification

- `tsc --noEmit -p tsconfig.app.json` → **0 errors** (note: the default
`tsconfig.json` only covers `src/` and does **not** typecheck `app/`)
- `pnpm test:gate` → **63 passed**
- Settings surface → 68 failures, every one a verified subset of the
pre-existing baseline, diffed by failure SET not count (this branch
incidentally fixes 4)
- `pnpm test:gate` 63/63 · lint clean across 83 changed files · `tsc -p
tsconfig.app.json` 0 errors
- Rebased onto `main`: conflicts with #2147 and FN-8016 resolved
- Driven in a real browser at 1440px and 390px: search, jump-to-field,
help tips, advanced toggle, and every migrated section
- Driven in a real browser: search, jump-to-field, highlight, and every
migrated section

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Settings search now indexes individual settings controls, ranks
matches, and navigates directly to the exact row with a temporary
highlight.
* **UI Improvements**
* Migrated settings sections to shared row primitives
(toggle/select/number/text/textarea) for consistent spacing and
touch-friendly controls.
* Updated typography to a complete tokenized type scale; added the “?”
help tip and tokenized row highlight/error styling.
  * Navigation and search labels now show clear Global vs Project scope.
* **Bug Fixes**
* Improved search accuracy using label/help/keywords and fixed settings
search counts/pluralization and MCP scope labels.
* **Tests**
* Added/updated checks to ensure the settings search index stays
consistent with rendered rows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:00:27 -07:00

195 lines
7.9 KiB
CSS

/* SettingsFieldRow (U8 / KTD-10) — base layout for a single settings control:
* label + optional scope badge on the leading line, the control slot beside or
* below it, then help text and an error band. Shared by every typed row so the
* redesigned SettingsModal and the WorkflowSettingsPanel read identically.
* Mirrors the token/class conventions of WorkflowFieldsPanel.css. */
/*
FNXC:SettingsStyling 2026-07-15-17:35:
Every settings row must render from one type scale, because the pre-migration modal styled a text field's label and a toggle's label three different ways inside the same section: `.form-group label` (12px/600/uppercase/muted), a `.settings-content` override narrowing that to 0.72rem, and `.checkbox-label` (13px/500/sentence-case) whose declarations each carried `!important` purely to out-specify the first two.
Sizes name shared `--font-size-*` rungs rather than raw values so the scale stays auditable: label = sm, help/error = xs, scope badge = 2xs.
Sentence-case at weight 500 is the surviving idiom; the uppercase/muted treatment is dropped because a settings label names its control rather than heading a column.
Rows carry no horizontal padding — `.settings-content` already owns the inset, and a second one would double-indent every row.
*/
.settings-field-row {
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding: var(--space-sm) 0;
scroll-margin-block: var(--space-xl);
}
.settings-field-row.is-disabled {
opacity: 0.6;
}
/*
FNXC:SettingsStyling 2026-07-15-17:35:
Inline rows (booleans) sit the control on the label's line. The help text then hangs beneath BOTH, indented past the control so it aligns with the label it explains rather than with the checkbox — without the indent the copy starts under the box and the column of help text no longer lines up with the column of labels.
The indent is derived from the control's own width plus the head gap, so it tracks the checkbox rather than being a magic number.
*/
.settings-field-row--inline {
--settings-inline-control-width: 13px;
}
.settings-field-row--inline .settings-field-row-head {
gap: var(--space-sm);
}
.settings-field-row--inline .settings-field-row-label {
cursor: pointer;
}
.settings-field-row--inline .settings-field-row-error {
padding-inline-start: calc(var(--settings-inline-control-width) + var(--space-sm));
}
/*
FNXC:SettingsSearch 2026-07-15-17:35:
Jump-to-field: choosing a search result scrolls that row into view and flags it here, so the eye lands on the right control inside a long section instead of on the section's top.
The hold is deliberately longer than any `--duration-*` UI-transition token (those top out at 0.3s, which reads as a flicker on a row the operator is still hunting for), so the timing is a named local property rather than a borrowed token.
It must stay a duration-only value: `--transition-*` tokens bundle an easing keyword, and substituting one beside `ease-out` yields two easing functions, which invalidates the declaration and silently resolves to `animation: none` (the failure that froze 14 spinners in FN-5855).
The wash paints background and box-shadow only — never a border or padding — so a landing row does not reflow its neighbours by a pixel.
`scroll-margin-block` keeps the landed row clear of the section chrome rather than flush beneath it.
*/
.settings-field-row {
--settings-search-match-duration: 1.6s;
--settings-search-match-tint: color-mix(in srgb, var(--accent, #7c5cbf) 18%, transparent);
}
.settings-field-row.is-search-match {
border-radius: var(--radius-sm);
animation: settings-field-row-match var(--settings-search-match-duration) ease-out;
}
@keyframes settings-field-row-match {
0%,
60% {
background: var(--settings-search-match-tint);
box-shadow: 0 0 0 var(--space-xs) var(--settings-search-match-tint);
}
100% {
background: transparent;
box-shadow: 0 0 0 var(--space-xs) transparent;
}
}
/* FNXC:SettingsStyling 2026-07-15-17:35: A reduced-motion operator still needs to see which row matched, so the wash resolves to a static hold instead of being animated away entirely. */
@media (prefers-reduced-motion: reduce) {
.settings-field-row.is-search-match {
animation: none;
background: var(--settings-search-match-tint);
}
}
/*
FNXC:SettingsStyling 2026-07-16-00:10:
`align-items: flex-start` (not center) so a checkbox sits on the label's FIRST line. Centering aligns it to the midpoint of a label that has wrapped to two or three lines, which floats the box down beside the middle of the sentence.
`flex-wrap: nowrap` because the head now holds at most two items — the control and the label group — and the group is what absorbs the wrapping. Allowing the row itself to wrap is what stranded the checkbox on its own line.
*/
.settings-field-row-head {
display: flex;
align-items: flex-start;
gap: var(--space-xs);
flex-wrap: nowrap;
}
/*
FNXC:SettingsStyling 2026-07-16-00:10:
Label + scope badge + help tip flow as one run of prose: `min-width: 0` lets the group shrink below its content's intrinsic width (a flex item refuses to by default, which is what forces the wrap), and `flex: 1 1 auto` gives it the width left over beside the control.
The children stay inline, so the badge and the "?" trail the label's LAST word rather than being pushed onto their own line — and a wrapped second line starts under the first word, not under the checkbox.
*/
.settings-field-row-labelgroup {
flex: 1 1 auto;
min-width: 0;
display: block;
}
/*
FNXC:SettingsStyling 2026-07-16-00:10:
The group's children are inline, so the head's flex `gap` no longer separates them — it only ever applied between flex items, and the label/badge/tip are now text runs inside one item. Their spacing has to come from margins instead, or the badge renders flush against the label ("Open tasks in the right sidebarPROJECT").
Margin-inline-start rather than a gap so the spacing travels with each trailing element when the label wraps: the badge stays attached to the last word rather than to a column edge.
*/
.settings-field-row-labelgroup > * {
vertical-align: middle;
}
.settings-field-row-labelgroup > .settings-field-row-scope,
.settings-field-row-labelgroup > .settings-help {
margin-inline-start: var(--space-xs);
}
.settings-field-row-label {
font-size: var(--font-size-sm);
font-weight: 500;
line-height: var(--line-height-tight);
color: var(--text);
}
.settings-field-row-scope {
font-size: var(--font-size-2xs);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
background: var(--surface-muted, color-mix(in srgb, #ffffff 4%, transparent));
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 1px 6px;
}
.settings-field-row-scope--global {
color: var(--accent, #7c5cbf);
border-color: var(--accent, #7c5cbf);
}
.settings-field-row-control {
display: flex;
align-items: center;
gap: var(--space-xs);
}
/*
FNXC:SettingsStyling 2026-07-15-17:35:
Controls hold the 38px min-height the pre-migration `.settings-content` rule gave inputs, so a migrated row keeps its touch target and does not visibly shrink beside a not-yet-migrated one during the rollout.
*/
.settings-field-row-control > input:not([type="checkbox"]),
.settings-field-row-control > select,
.settings-field-row-control > textarea {
flex: 1;
min-width: 0;
min-height: 38px;
box-sizing: border-box;
}
.settings-field-row-clear {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
background: none;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
cursor: pointer;
padding: 2px;
transition: color var(--duration-fast) ease, border-color var(--duration-fast) ease;
}
.settings-field-row-clear:hover:not(:disabled) {
color: var(--text);
border-color: var(--text-muted);
}
.settings-field-row-clear:disabled {
cursor: default;
opacity: 0.5;
}
.settings-field-row-error {
margin: 0;
font-size: var(--font-size-xs);
line-height: 1.5;
color: var(--color-error, #f85149);
}