diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css
index 22dda71e1e..4953003f59 100644
--- a/packages/dashboard/app/components/TaskCard.css
+++ b/packages/dashboard/app/components/TaskCard.css
@@ -471,22 +471,25 @@ warning badge, while generic and release-authorization holds retain the triage a
/*
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7592 replaces the overseer badge's uppercase text label with a small `Eye` icon so it
-reads as a compact glyph rather than a wide pill. Size the badge to the icon (no min-width,
-tight padding) and color it per `PlannerOverseerState` via the `data-planner-overseer-state`
-attribute so operators can distinguish watching/steering/recovering/awaiting-confirmation at
+reads as a compact glyph rather than a wide pill. Color it per `PlannerOverseerState` via the
+`data-planner-overseer-state` attribute so operators can distinguish watching/steering/recovering/awaiting-confirmation at
a glance without reading text. Colors reuse existing semantic tokens: neutral/info for the
passive "watching" state, warning for the more active "steering"/"recovering" states, and the
triage token for "awaiting-confirmation" (a human-decision hold), matching the hue conventions
used elsewhere in this file (e.g. .card-oversight-badge--*, .card-status-badge--triage).
+
+FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
+The Eye inherits shared text-badge padding, border, and line height. Its token-sized glyph must
+not enlarge that shared box, so icon-only and text header chips stay one height on desktop and mobile.
*/
.card-planner-overseer-state {
- padding: calc(var(--space-xs) / 2);
- line-height: 0;
+ box-sizing: border-box;
+ line-height: 1;
}
.card-planner-overseer-state svg {
- width: 12px;
- height: 12px;
+ width: 0.625rem;
+ height: 0.625rem;
}
.card-planner-overseer-state[data-planner-overseer-state="watching"] {
@@ -580,18 +583,27 @@ As a direct header child after .card-id, the size chip must match sibling header
color: var(--color-error-dark);
}
+/*
+FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
+Fast-mode's icon-only chip shares the text-badge box geometry instead of a fixed square. Keep the
+Zap at the badge text size so it remains centered without making the header row taller on desktop or mobile.
+*/
.card-execution-mode-badge {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 1rem;
- height: 1rem;
+ box-sizing: border-box;
+ font-size: 0.625rem;
+ font-weight: 600;
+ line-height: 1;
+ padding: calc(var(--space-xs) / 2) var(--space-sm);
+ border: var(--btn-border-width) solid transparent;
border-radius: var(--radius-pill);
}
.card-execution-mode-badge svg {
- width: 0.75rem;
- height: 0.75rem;
+ width: 0.625rem;
+ height: 0.625rem;
}
.card-execution-mode-badge--fast {
@@ -823,17 +835,25 @@ falling through to an unstyled transparent shell.
box-shadow: var(--shadow-md);
}
-/* Mission badge: clickable link to mission from task card */
+/*
+FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
+The clickable mission chip uses the shared text-badge box geometry, including its transparent
+border. This keeps its label and Target glyph on the same header baseline as status and icon-only chips
+at desktop and mobile breakpoints.
+*/
.card-mission-badge {
display: inline-flex;
align-items: center;
- gap: 3px;
+ box-sizing: border-box;
+ gap: var(--space-xs);
font-size: 0.625rem;
font-weight: 600;
+ line-height: 1;
font-family: var(--font-mono);
color: var(--badge-mission-text);
background: var(--badge-mission-bg);
- padding: 2px 6px;
+ padding: calc(var(--space-xs) / 2) var(--space-sm);
+ border: var(--btn-border-width) solid transparent;
border-radius: var(--radius-pill);
cursor: pointer;
transition: color var(--transition-fast), background var(--transition-fast);
@@ -843,6 +863,11 @@ falling through to an unstyled transparent shell.
white-space: nowrap;
}
+.card-mission-badge svg {
+ width: 0.625rem;
+ height: 0.625rem;
+}
+
.card-mission-badge:hover {
color: var(--badge-mission-text-hover);
background: var(--badge-mission-bg-hover);
@@ -1931,19 +1956,31 @@ The three-dot menu is the sole card move/action entry point. Keep this shared bu
row-gap: var(--space-xs);
}
- /* Card: smaller status badges for 280px width */
+ /*
+ FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
+ Every header chip uses this mobile text-badge scale. The former size-only block padding made
+ its box diverge; text, icon-only, PR, oversight, and mission chips must remain one height.
+ */
.card-status-badge,
.card-priority-badge,
.card-size-badge,
- /* FNXC:PlannerOversight 2026-07-04-00:00: mobile-scale the FN-7516 oversight
- badge alongside the other .card-meta-badges chips at narrow widths. */
+ .card-no-commits-expected-badge,
+ .card-planner-overseer-state,
+ .card-execution-mode-badge,
+ .card-pr-node-badge,
+ .card-mission-badge,
.card-oversight-badge {
font-size: 0.5625rem;
+ line-height: 1;
padding: calc(var(--space-xs) / 4) calc((var(--space-xs) * 3) / 2);
+ border: var(--btn-border-width) solid transparent;
}
- .card-size-badge {
- padding-block: calc((var(--space-xs) / 4) + var(--btn-border-width));
+ .card-planner-overseer-state svg,
+ .card-execution-mode-badge svg,
+ .card-mission-badge svg {
+ width: 0.5625rem;
+ height: 0.5625rem;
}
.card-stalled-review-reason {
diff --git a/packages/dashboard/app/components/__tests__/TaskCard.badge-height.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.badge-height.test.tsx
index 7f8d503d76..c364424778 100644
--- a/packages/dashboard/app/components/__tests__/TaskCard.badge-height.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskCard.badge-height.test.tsx
@@ -13,13 +13,14 @@ vi.mock("lucide-react", () => ({
Layers: () => null,
ChevronDown: () => null,
Folder: () => null,
- GitPullRequest: () => null,
+ GitPullRequest: () => ,
CircleDot: () => null,
- Target: () => null,
+ Target: () => ,
Bot: () => null,
Trash2: () => null,
RotateCw: () => null,
- Zap: () => null,
+ Zap: () => ,
+ Eye: () => ,
AlertTriangle: () => null,
ArrowDown: ({ style }: { style?: React.CSSProperties }) => ,
Flag: ({ style }: { style?: React.CSSProperties }) => ,
@@ -66,11 +67,11 @@ function mountCss() {
}
describe("TaskCard badge heights (FN-4369)", () => {
- it("keeps planning, merging, and priority pills at identical dimensions", () => {
+ it("keeps triage planning, merging, and priority pills at identical dimensions", () => {
const cleanupCss = mountCss();
const planning = render(
- ,
+ ,
).container.querySelector(".card-status-badge");
const merging = render(
@@ -110,4 +111,62 @@ describe("TaskCard badge heights (FN-4369)", () => {
cleanupCss();
});
+
+ it("keeps every populated header chip on the shared text-badge geometry", () => {
+ const cleanupCss = mountCss();
+ const { container } = render(
+ ,
+ );
+
+ const baseline = container.querySelector(".card-status-badge");
+ const chips = [
+ ".card-planner-overseer-state",
+ ".card-execution-mode-badge",
+ ".card-pr-node-badge",
+ ".card-mission-badge",
+ ".card-size-badge",
+ ".card-oversight-badge",
+ ].map((selector) => container.querySelector(selector));
+
+ expect(baseline).toBeTruthy();
+ chips.forEach((chip) => expect(chip).toBeTruthy());
+
+ const baselineStyles = getComputedStyle(baseline!);
+ for (const chip of chips) {
+ const styles = getComputedStyle(chip!);
+ expect(styles.height).toBe(baselineStyles.height);
+ expect(styles.paddingTop).toBe(baselineStyles.paddingTop);
+ expect(styles.paddingBottom).toBe(baselineStyles.paddingBottom);
+ expect(styles.borderTopWidth).toBe(baselineStyles.borderTopWidth);
+ expect(styles.borderBottomWidth).toBe(baselineStyles.borderBottomWidth);
+ expect(styles.lineHeight).toBe(baselineStyles.lineHeight);
+ expect(styles.minHeight).toBe(baselineStyles.minHeight);
+ }
+
+ cleanupCss();
+ });
});
diff --git a/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx
index 73b445174b..bc65613d35 100644
--- a/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx
@@ -543,10 +543,28 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
expectCssRuleToContain(mobileSection, ".card-send-back-btn", "line-height: 1;");
expectCssRuleToContain(mobileSection, ".card-send-back-btn", "transform: translateY(calc(var(--space-xs) / -4));");
expectCssRuleToContain(mobileSection, ".card-menu-btn", "line-height: 1;");
- expectCssRuleToContain(mobileSection, ".card-size-badge", "line-height: 1;");
- expectCssRuleToContain(mobileSection, ".card-size-badge", "font-size: 0.5625rem;");
- expectCssRuleToContain(mobileSection, ".card-size-badge", "padding: calc(var(--space-xs) / 4) calc((var(--space-xs) * 3) / 2);");
- expectCssRuleToContain(mobileSection, ".card-size-badge", "padding-block: calc((var(--space-xs) / 4) + var(--btn-border-width));");
+ /*
+ * FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
+ * jsdom does not apply the mobile media query. Inspect its shared declaration directly so
+ * text, icon-only, size, PR, mission, and oversight chips cannot regain divergent box geometry.
+ */
+ const mobileHeaderChipSelectors = [
+ ".card-status-badge",
+ ".card-priority-badge",
+ ".card-size-badge",
+ ".card-planner-overseer-state",
+ ".card-execution-mode-badge",
+ ".card-pr-node-badge",
+ ".card-mission-badge",
+ ".card-oversight-badge",
+ ];
+ for (const selector of mobileHeaderChipSelectors) {
+ expectCssRuleToContain(mobileSection, selector, "font-size: 0.5625rem;");
+ expectCssRuleToContain(mobileSection, selector, "line-height: 1;");
+ expectCssRuleToContain(mobileSection, selector, "padding: calc(var(--space-xs) / 4) calc((var(--space-xs) * 3) / 2);");
+ expectCssRuleToContain(mobileSection, selector, "border: var(--btn-border-width) solid transparent;");
+ expectCssRuleNotToContain(mobileSection, selector, "padding-block:");
+ }
expectCssRuleNotToContain(mobileSection, ".card-send-back-btn", "min-height:");
expectCssRuleNotToContain(mobileSection, ".card-menu-btn", "min-height:");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "width: 28px;");