コンテンツにスキップ

Console ⟷ Login Design Token Unification

Status: Design spec (revision 3). The light-mode color/shadow/font migration (§1/§3/§4) has already been implemented on branch console/unify-design-tokens, verified (lint/136 unit tests/build all green; Console visually checked light+dark via the mock dev server; the login theme’s font fix was verified against a real locally-running Keycloak instance with getComputedStyle(), not just visually) and is pending human sign-off on the open questions below before merge. Radius (§2) and the theme1.js–theme8.js cleanup (§6) are still unimplemented follow-ups. Issue: https://github.com/GCU-DEV/identity-hub/issues/65 Scope: keycloak/themes/gcu/login/ and console/src/themes/ only. No theme/CSS file is modified by this document itself (see the implementation branch above for the actual diff).

The GCU identity platform has two live, user-facing surfaces that must read as one brand: the Keycloak login theme (keycloak/themes/gcu/login/, PatternFly/keycloak.v2-based) and the Identity Console (console/src, React 19 / MUI 7, Able Pro–derived). The brand’s primary color ramp has already been unified between the two (see below), proving a derivation method that works end-to-end. Everything else — neutrals/greys, corner radius, shadow/elevation, typography, and light/dark scope — currently diverges: each surface picked its own values independently, with no shared source of truth.

Issue #65 (“专业后台 IdP” / UI-UX-investment proposal) argues the platform should treat UI/UX as a continuously-invested product capability — on par with professional IdPs like WorkOS/Okta — rather than a one-off template reskin, and names a unified, shared design-token system as one concrete example of what that investment looks like (alongside formalizing design time as a Console/MyPage deliverable and an ongoing “design debt” review cadence). This document executes on that one concrete example: it closes the token-divergence gap the same way the primary color ramp was already closed, so a future implementation PR has one spec to build against.

All file:line citations below are verified against the actual repo content at the time of writing, in worktree design-tokens / branch design/console-design-tokens. This is revision 2: an independent review caught two factual errors in revision 1 that would have shipped a real dark-mode regression and a false “already aligned” radius claim — both are corrected below, with the reasoning kept in place so a future reader can see what changed and why. Related: #27 (the original “Console theme color doesn’t match the real GCU brand” report this spec executes on).

Already aligned: the primary color ramp (precedent method)

Section titled “Already aligned: the primary color ramp (precedent method)”

console/src/themes/theme/default.js:9-12:

// GCU のブランドカラー(keycloak/themes/gcu/login/resources/css/login.css の --gcu-primary /
// --gcu-primary-dark と同じ色相・彩度から 10 段階に派生させたもの。main(idx5)=--gcu-primary、
// 900(idx9)=--gcu-primary-dark と完全一致させている。Able Pro テンプレート既定の #4680FF ではない。
let primaryColors = ['#EFF3FB', '#C4D5F0', '#99B6E5', '#6E98D9', '#4379CE', '#2D5FAE', '#27539B', '#214887', '#1B3C73', '#16315F'];
  • primaryColors[5] (main) = #2D5FAE = --gcu-primary (login.css:5), exact match.
  • primaryColors[9] (900) = #16315F = --gcu-primary-dark (login.css:6), exact match.
  • The other 8 steps are interpolated at the same hue/saturation family, not independently chosen.

This is the method to extend, and the direction matters: the login page’s two brand anchors (--gcu-primary, --gcu-primary-dark) are the source; Console’s 10-step ramp was derived from them, not the other way around. Any further token unification should follow the same direction — derive Console’s ramp from login’s existing CSS custom properties — rather than letting Console’s independently-evolved values become the new source of truth. This directional precedent is the main reason this spec adopts a login-anchored neutral ramp (below) rather than simply copying Console’s current secondaryColors array onto login.css.

What is NOT aligned, and what this spec does about it

Section titled “What is NOT aligned, and what this spec does about it”

Current state (verified):

  • login.css:7-11: --gcu-text:#39465f, --gcu-text-muted:#5e6e88, --gcu-border:#cddbf0, --gcu-bg:#f7fafe, --gcu-hover-bg:#eff4fb; login.css:12: --gcu-card-bg:#ffffff.
  • console/src/themes/theme/default.js:13: secondaryColors = ['#F8F9FA','#F8F9FA','#F3F5F7','#DBE0E5','#BEC8D0','#8996A4','#5B6B79','#3E4853','#1D2630','#131920'] (light). Line 21 is the dark array, confirmed to be the exact reversal of the light array (dark[i] = light[9-i]), matching the same reversal convention already used for primaryColors.
  • These are two unrelated palettes. Login’s neutrals carry a visible blue undertone (computed: #39465f ≈ H219.5°/S25%/L29.8%, in the same ~217–220° hue family as --gcu-primary #2d5fae ≈ H217°/S59%/L43% and --gcu-primary-dark #16315f ≈ H218°/S62%/L23%). Console’s current secondaryColors is comparatively achromatic by comparison and was never derived from either brand anchor. This isn’t a coincidence to route around: login.css:4’s comment, www.gcu.co.jp のブランドカラー (“www.gcu.co.jp’s brand color”), sits directly above the entire :root block — primary and these five neutrals together — meaning the neutrals are stated, in the repo’s own words, to be brand-sourced, not incidental choices. That is the strongest evidence for treating them as the anchor rather than Console’s.

Decision: derive one new 10-step neutral ramp, hue/sat-anchored to the same ~217° family as the primary ramp, pinning it to login’s existing anchor values the same way the primary ramp was pinned to --gcu-primary/--gcu-primary-dark. Both login.css’s custom properties and Console’s secondaryColors array migrate onto this one ramp — neither surface’s current incidental value wins as-is; both converge onto a formalized, shared token, which is the most faithful extension of the already-proven method (anchor-derived, not assertion-derived).

Honest limitation of this approach, not papered over: the primary-ramp precedent pinned 2 of its 10 stops (main, 900) and interpolated the rest, giving a fairly even lightness progression. This neutral ramp pins 6 of 10 stops (indices 0, 1, 3, 6, 8 to login’s literals, plus index 9 to the shadow literal in §3) and interpolates only 3 — the resulting lightness steps are uneven (roughly 98/96/92/87/73/59/45/38/30/11% in sequence: tight at the light end, then two ~14-point jumps, then an ~8-point step, then a big ~19-point drop to the shadow anchor). This is a direct consequence of pinning so many semantically-distinct values (a background, a hover tint, a border, two text colors, and a shadow color) onto one ramp instead of treating them as more loosely related tokens. An OKLCH-perceptual interpolation pinning fewer stops (e.g. only the lightest and darkest, the way the primary ramp did) was considered and would produce a more uniform ramp, at the cost of the shadow-token “zero formula change” elegance in §3 (which depends on darker landing on the exact shadow RGB) and of the five neutrals staying byte-identical in login.css. This spec prioritizes the latter two; a human should weigh in on whether that trade is right — see open questions, including whether text.primary landing at L≈30% (#39465F) is the right weight for a dense, table-heavy admin UI (it passes contrast comfortably, but “passes contrast” and “is the right text weight for this UI” are different questions).

Also worth naming plainly: the “direction matters” principle above is not applied uniformly across this document, and revision 1 implicitly treated it as settled when it wasn’t. §1 derives Console’s values from login. §3 changes login’s shadow offset to match Console’s z1 geometry. §4 changes login’s font to match Console’s stack. §2 (now) leaves both sides alone pending a human call. The real, underlying rule being applied, stated honestly: whichever surface’s existing value is better-justified for a given token wins, token by token — not “login always wins” or “Console always wins.” For colors, login’s values are better-justified (stated brand anchors, §1 evidence above). For shadow geometry and font stack, Console’s values are better-justified (an actual derivation formula; an actual CJK-aware stack already shipping). This is a reasonable rule, but it is a different, weaker claim than “one direction, consistently applied,” and this document should not have implied otherwise.

New ramp (light mode):

idx token name (Console) hex anchor / derivation
0 lighter #F7FAFE = --gcu-bg (login.css:10), unchanged literal — also becomes Console background.default (palette.js:32)
1 100 #EFF4FB = --gcu-hover-bg (login.css:11), unchanged literal
2 200 #DEE8F6 interpolated, same hue family
3 light #CDDBF0 = --gcu-border (login.css:9), unchanged literal — also becomes Console divider input (palette.js:29, via withAlpha(secondary.light, 0.65))
4 400 #A8B7CD interpolated — new border/input-outline stop (see OutlinedInput note below)
5 500 #8392AB interpolated
6 main #5E6E88 = --gcu-text-muted (login.css:8), unchanged literal — also becomes Console text.secondary (palette.js:25)
7 dark #4C5A74 interpolated
8 800 #39465F = --gcu-text (login.css:7), unchanged literal — also becomes Console text.primary in light mode (palette.js:24, extendedLight.secondary[800])
9 darker #0F172A = the literal rgb(15,23,42) already hardcoded in login’s card shadow (login.css:50, rgba(15, 23, 42, 0.08)) — pinning this stop here makes Console’s shadow formula (below) match login’s shadow color with zero formula change

Dark mode: deliberately left untouched. This is a correction from revision 1, which proposed the usual dark[i] = light[9-i] reversal (the convention every other ramp in default.js follows) and was wrong to. Here is why that reversal breaks, verified against palette.js:46-49:

  • Dark-mode background.paper reads extendedDark.secondary[100] — ramp index 1.
  • Dark-mode background.default reads extendedDark.secondary.lighter — ramp index 0.
  • Under a straight reversal of the new light ramp (['#0F172A','#39465F','#4C5A74','#5E6E88','#8392AB','#A8B7CD','#CDDBF0','#DEE8F6','#EFF4FB','#F7FAFE']), dark-mode index 1 becomes #39465F — the value this spec pins to light mode’s --gcu-text (chosen for mid-lightness body text on white). Today’s live value at that slot is #1D2630 (near-black). Every dark-mode card, dialog, drawer and popover surface (background.paper) would jump from L≈15% to L≈30% — a real, console-wide visual change, not the “marginally cooler/bluer” impact revision 1 claimed. Dark-mode background.default would similarly move from #131920 to #0F172A (a much smaller shift, coincidentally fine, but moving it for no reason this spec needs is still an unforced risk).
  • There is nothing on the login side to justify this: login has no dark mode (darkMode=false, §5) and never will under this spec’s own recommendation. The entire point of this document is making login and Console agree where both actually render — that only applies to light mode. Touching Console’s dark-mode neutrals doesn’t move either surface closer to the other; it only reshuffles Console’s own internal dark palette, and does so by accident of index arithmetic rather than by design intent.

Decision: secondaryColors in default.js’s ThemeMode.DARK branch is not changed at all — it keeps its current values exactly as they are today. primaryColors (the actual brand accent) already has a correct, independently-chosen dark variant and stays untouched by this decision; this is only about the neutral/grey ramp.

One existing Console mechanism this leaves alone, noted for completeness: dark-mode text.primary/text.secondary/text.disabled (palette.js:40-42) are not a 1:1 index read — they’re withAlpha(extendedDark.secondary.darker, 0.87 / 0.45 / 0.1), all three derived from the single (unchanged) darker stop at different opacities. Since the dark array isn’t changing, these are unaffected by this spec; today’s pre-existing dark-mode text.secondary contrast (≈4.1:1 against background.paper) is already marginally under WCAG AA’s 4.5:1 body-text threshold, independent of anything here — out of scope for this spec, flagged as a pre-existing, unrelated gap.

Light-mode contrast (actually computed, not hand-estimated): text.primary (#39465F) on background.paper/white = 9.48:1; on background.default (#F7FAFE) = 9.06:1. text.secondary (#5E6E88) on white = 5.17:1; on background.default = 4.94:1. All four comfortably clear WCAG AA’s 4.5:1 for body text (computed via the standard relative-luminance formula, not estimated).

2. Radius tiers — resolved by reading the override files

Section titled “2. Radius tiers — resolved by reading the override files”

Login uses two literal values: login.css:49 (.pf-v5-c-login__main, the card) = 8px; login.css:80,149 (.gcu-idp-button, .pf-v5-c-alert) = 6px.

Console’s single declared theme-level value is console/src/themes/index.jsx:47-48 (console/src/themes/theme/index.js, no x, is a different file — the dead preset-switcher discussed in §6, not the shape/composition file):

shape: { borderRadius: 8 }

Reading the override files directly gives these findings:

file what’s actually there
overrides/Button.js:88-111 No radius on the button’s own root. The only borderRadius: 8 values (lines 98, 105) style the ::after ripple/active-state pseudo-element only, not the visible button shape. The real button inherits the global 8.
overrides/OutlinedInput.js:29-63 No borderRadius anywhere in the file. Inputs silently inherit the global 8. Line 37 does set notchedOutline.borderColor: theme.vars.palette.secondary[400] — confirmed, this is the one place a secondary[400] border color is already live in Console today.
overrides/Alert.jsx File exists, has color-variant styling (getColorStyle), no borderRadius override at all — inherits the global 8.
overrides/Chip.js:49,61 root: 12, sizeSmall: 8 — a real second axis, but it’s a size variant (small chip vs. default chip), not a surface-vs-control axis, and neither value is 6.
overrides/Tab.js:10,15 4 — the one genuinely smaller value in the codebase, but on a different component family (tabs), not buttons/inputs/alerts.
overrides/Fab.js No root.borderRadius override anywhere. Lines 28/32 (borderRadius: '50px') sit inside getColorStyle()’s &::after / &:active::after ripple pseudo-elements (same pattern as Button.js:98,105) — not the Fab’s own visible shape, which is MUI’s own circular default, untouched here. Lines 67/74 (borderRadius: 4) are a second, generic ::after/:active::after overlay on the root — again pseudo-element only, with no “close-icon” class, comment, or semantic anywhere in the file.
overrides/IconButton.js:8, overrides/ListItemButton.jsx:9 Both explicitly restate 8 — same as the global, not a second tier.
overrides/Checkbox.jsx:69, overrides/LinearProgress.js:9,12 0 and 100 (pill) — special-cased, not a general compact-control tier.

Conclusion: Console has no existing 6px tier for any component type that corresponds to login’s buttons/inputs/alerts. The closest existing smaller value, 4px (Tab.js), is on an unrelated component type, not a disguised match. This is a genuine gap, not an already-aligned-but-unverified tier.

Correction from revision 1: the theme-level shape.borderRadius: 8 match is not the same thing as Console’s actual rendered card radius. Revision 1 concluded radius-lg = 8px was “already shared” by comparing shape.borderRadius: 8 to login’s card radius directly, without checking the component that actually renders Console’s cards. console/src/components/MainCard.jsx:50 sets borderRadius: 1.5 inside an MUI sx prop — MUI resolves bare numeric sx spacing values as value × theme.spacing()-style multipliers of theme.shape.borderRadius in this context, giving 1.5 × 8 = 12px, not 8px. MainCard is the standard card wrapper used across Console’s pages (also used by Localization.jsx:89’s Paper). So the real comparison is: login’s card = 8px, Console’s card = 12px — a genuine, previously-unnoticed gap, not an already-aligned point.

A related, also previously-missed structural difference: MainCard.jsx:19,51-52 defaults border = true and only applies customShadows.z1 when an explicit boxShadow/shadow prop is passed — so Console’s default card has a 1px divider border and no shadow, while login’s card has no border and a shadow (login.css:47-51). The two surfaces’ cards don’t just differ in radius; they’re built on a different visual language (bordered-flat vs. shadowed-floating). This document does not resolve that — it’s a bigger, more subjective design call than a token-value substitution, and is carried into “open questions for a human” below.

Decision:

  • No change to radius-lg/shape.borderRadius: 8 in this spec. The card-radius gap (8px login vs. 12px MainCard) is real but entangled with the border-vs-shadow structural difference above — resolving one without the other would be a partial, possibly wrong fix. Left as an open question, not auto-resolved here.
  • radius-sm = 6px — a new, formal token (shape.borderRadiusSM or equivalent), to be added to Console and applied to MuiButton and MuiAlert root overrides (both currently have zero radius override and silently inherit 8). MuiOutlinedInput is excluded from this recommendation (see the input-radius note below) pending the open question on what it should actually match. If MuiButton’s root radius changes, Button.js:98,105’s ::after ripple/active-state pseudo-element (currently hardcoded to 8, matching the old root) must change together with it, or the ripple will visibly misalign with the button’s new shape — this is a two-line-in-one-file change, not a one-line one.
  • Login-side input/button radius is unverified, not “6px by extension.” login.css only ever sets 6px on .gcu-idp-button (login.css:80) and .pf-v5-c-alert (login.css:149). The actual username/password fields and the Sign-in submit button are rendered by keycloak.v2’s own field.ftl/login.ftl macros as PatternFly pf-v5-c-form-control/pf-v5-c-button elements, which login.css never touches — their radius is whatever keycloak.v2/PatternFly 5 ships by default, not confirmed to be 6px, and not confirmed to be anything this repo controls today. So “make Console’s OutlinedInput match login’s inputs” has no actual login-side value to match yet. Either (a) scope the radius follow-up to Button+Alert only (the two component types login.css actually styles), or (b) first add an explicit radius rule to login.css for PF form controls/buttons so login itself has a real, intentional value there — this spec does not pick between the two; see open questions.
  • Scope whichever version of this is chosen as a separate, smaller follow-up PR, not bundled into the color/shadow/typography alignment. It touches override files each needing a human visual pass across every screen that renders a button, text field, or alert (i.e., nearly every console screen) — a materially different risk profile from the single-file, pure-value-substitution changes in §1/§3/§4. Keeping it separate keeps the main alignment PR small and reviewable, consistent with the “validate key pages” instruction in the project’s hard constraints.

Login: login.css:50, literal box-shadow: 0 4px 24px rgba(15, 23, 42, 0.08); — used on exactly one element (the login card).

Console: custom-shadows.jsx:11-14 (light mode): z1 = 0px 8px 24px ${withAlpha(palette.secondary.darker, 0.08)}. palette.secondary.darker resolves to secondaryColors[9], currently #131920 = rgb(19,25,32).

Decision: no shadow-formula change at all. Because the neutral ramp above pins secondaryColors[9] (darker, light mode only — see §1) to #0F172A = rgb(15,23,42) — the exact RGB login’s card shadow already hardcodes — custom-shadows.jsx’s existing formula produces rgba(15, 23, 42, 0.08) automatically once the light ramp changes, with zero code edit to custom-shadows.jsx itself (beyond an explanatory comment). This reuses the one real lever (palette.secondary.darker is already the shadow’s color input) to close a gap previously described as “same alpha, different base color.”

Caveat, corrected from revision 1: this aligns login’s card shadow with Console’s z1/z2 color, not with Console’s card. MainCard.jsx (§2) applies customShadows.z1 only when an explicit boxShadow/shadow prop is passed — by default Console’s cards have no shadow at all (a border instead). z1/z2 are the shadows actually used by Popover.js:8 and the drawer (MiniDrawerStyled.js:20,43) and nav flyouts/header menus. So after this change, login’s card shadow shares a color token with Console’s popovers and menus, not with Console’s cards — which still differ structurally (shadow-and-no-border vs. border-and-no-shadow, per §2). Stating this plainly rather than implying the two cards now match.

Remaining gap: login.css’s rgba(15, 23, 42, 0.08) is a hardcoded literal, not a variable, so it won’t auto-track future ramp changes. Add a --gcu-shadow-rgb: 15, 23, 42; custom property to login.css’s :root and rewrite the card rule as box-shadow: 0 8px 24px rgba(var(--gcu-shadow-rgb), 0.08); — same blur (24px) and alpha (0.08) as today; only the vertical offset moves from 4px to 8px to match Console’s z1 geometry (same hue and alpha; the vertical offset, not the spread, is what’s changing — z1’s blur/spread already matches at 24px/unset). This is a subtle, low-risk softening, not a visible color change. No second shadow tier is needed for login — it has exactly one shadow consumer (the card); Console’s colored button/ring shadows in custom-shadows.jsx:19-32 have no login-side counterpart to unify against.

Console: console/src/config.js:54: fontFamily: "'Inter var', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', Meiryo, sans-serif". Inter var is a self-hosted variable webfont, confirmed present at console/src/assets/fonts/inter/Inter-roman.var.woff2, Inter-italic.var.woff2, inter.css.

Login: confirmed zero font-family rules anywhere in login.css, and theme.properties:1 sets only parent=keycloak.v2 with no font override anywhere in the theme (the theme does have a scripts=js/device-fingerprint.js entry, theme.properties:8 — but no font asset or font-loading entry of any kind). The login page therefore silently falls back to whatever default font keycloak.v2/PatternFly ships, which does not include Noto Sans JP. Given the hard constraint that product copy is Japanese-default (DEFAULT_LOCALE = 'ja', config.js:5) and nothing may treat Japanese as secondary, this is a real, visible CJK-rendering inconsistency between the two surfaces, not a cosmetic nice-to-have.

Decision: add an explicit font-family declaration to login.css’s .login-pf body rule, using Console’s CJK fallback chain minus Inter var:

font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', Meiryo, sans-serif;

Deliberately drop Inter var: it is a webfont asset Console bundles and loads as part of its app build; theme.properties has no font-resource entry for it, so including the name would either silently fail to resolve (harmless but sloppy) or require shipping a new font asset + loading strategy to an unauthenticated, performance-sensitive first-touch page just for its Latin-glyph coverage — out of scope for closing the actually-reported gap (missing CJK fallback). The CJK-first stack alone is the precise fix.

Corrected from the version of this section that shipped first: body-level font-family alone does not reach every PatternFly component, confirmed against a real running instance. Keycloak was started locally with this theme live-mounted (make kc-up dev-realm) and getComputedStyle() was checked on the actual rendered login page in Japanese. #kc-page-title/.pf-v5-c-title computed to RedHatDisplay, helvetica, arial, sans-serif — PatternFly 5’s Title component reads --pf-v5-global--FontFamily--heading directly instead of inheriting body, so the page title was silently falling through the declared stack to the browser’s own CJK fallback. Buttons, form labels and form controls were confirmed fine (they use font-family: inherit, so the body rule does reach them). Fix: also override --pf-v5-global--FontFamily--text and --pf-v5-global--FontFamily--heading at :root — the same CSS-variable-override idiom this theme already uses for --keycloak-card-top-color — in addition to the body rule (kept for the elements that do inherit from it). Re-verified after the fix: #kc-page-title computes to the Hiragino/Noto Sans JP/Meiryo stack. Shipped on console/unify-design-tokens.

theme.properties:10-11:

# GCU ブランドはライトテーマのみ
darkMode=false

This is an explicit, authored decision with its own Japanese comment (“GCU brand is light-theme-only”), not a silently unexamined gap. Console’s dark mode is a separate, later capability: git log confirms b0b5767 console: add a manual light/dark theme toggle to the header and e7ec761 console: default theme mode to light, not system, both of which post-date the login theme’s darkMode=false line and were never retrofitted onto login. Console’s colorSchemes.light/.dark composition (themes/index.jsx:51-59) and DEFAULT_THEME_MODE = ThemeMode.LIGHT (config.js:49) confirm dark mode is real, shipped, and deliberately defaulted off, not forced.

Decision: keep darkMode=false on login, unchanged — record this explicitly as an intentional scope split rather than relitigating it. Reasoning:

  1. This whole token-unification spec only requires login’s light neutral/shadow values to converge with Console’s light palette (§1/§3) — nothing here needs or produces a login dark ramp.
  2. The hard constraint says to keep Console’s existing dark-mode capability and validate key pages against it, and explicitly says not to invent a new theming system or architecture — it does not ask for login dark mode, and building one would mean hand-maintaining a parallel :root override set against keycloak.v2/PatternFly’s own dark-mode CSS, which is unvalidated, out-of-scope territory.
  3. A pre-auth brand surface being deliberately light-locked while the authenticated admin tool offers a dark option for long-session comfort is a defensible, common product pattern, not an obvious oversight — but this is a product-facing judgment call, not something the repo’s files alone resolve, so it is also carried into “open questions for a human” below for explicit confirmation.

Action: add a one-line comment in theme.properties (non-functional) stating the decision is intentional, so a future reader does not “fix” it as an oversight.

6. Dead weight (cleanup, not a design decision)

Section titled “6. Dead weight (cleanup, not a design decision)”

console/src/themes/theme/theme1.js through theme8.js are unreachable. Verified: console/src/config.js:59 hardcodes presetColor: 'default'; a repo-wide search of console/src for presetColor shows it referenced only in config.js (the hardcoded default), themes/palette.js (pass-through parameter), themes/index.jsx (reads it from useConfig() state), and themes/theme/index.js (the switch statement that dispatches to theme1–theme8) — there is no settings UI, action, or reducer anywhere in console/src that ever sets presetColor to anything other than 'default'. The theme1–theme8 branches in themes/theme/index.js:16-31 are confirmed dead code.

Action: delete theme1.js–theme8.js and the corresponding imports/switch-cases in themes/theme/index.js:3-10,16-31, leaving only the default branch. Zero visible effect, zero risk. Ship as its own small commit/PR, separate from the token-value changes above.

file change
keycloak/themes/gcu/login/resources/css/login.css Replace the 5 neutral custom properties at lines 7-11 with the new ramp values: --gcu-text: #39465f (unchanged, now = ramp 800); --gcu-text-muted: #5e6e88 (unchanged, now = ramp main); --gcu-border: #cddbf0 (unchanged, now = ramp light); --gcu-bg: #f7fafe (unchanged, now = ramp lighter); --gcu-hover-bg: #eff4fb (unchanged, now = ramp 100). All five literal values are unchanged — they become the formalized anchors the new ramp is pinned to, rather than being overwritten. --gcu-card-bg stays #ffffff (already = Console background.paper, palette.js:31). Add --gcu-shadow-rgb: 15, 23, 42; to :root and change line 50’s box-shadow to 0 8px 24px rgba(var(--gcu-shadow-rgb), 0.08). Add --pf-v5-global--FontFamily--text/--heading to :root and a font-family declaration to .login-pf body (both needed — see §4’s verified finding on PatternFly’s Title component) per §4.
keycloak/themes/gcu/login/theme.properties No functional change. Add a one-line comment above darkMode=false (line 11) recording the light-only decision as intentional (§5).
console/src/themes/theme/default.js Replace the light secondaryColors array (line 13) with the 10 new ramp hex values in index order (§1 table). Do not touch the dark secondaryColors array (line 21) — leave it exactly as-is (§1’s dark-mode correction). primaryColors/errorColors/warningColors/infoColors/successColors are untouched in both modes.
console/src/themes/custom-shadows.jsx No code change. z1/z2 light-mode branches (lines 11-18) already read palette.secondary.darker; they inherit the new value automatically once default.js’s light array changes. Recommend a comment noting the shared-token relationship, mirroring the explanatory-comment style already used in default.js:9-11 (which should itself be extended to describe secondaryColors, not just primaryColors, once this ships).
console/src/themes/palette.js No code change. text.primary/secondary/disabled (lines 24-26) and divider (line 29) already derive from extendedLight.secondary[...]; they inherit the new ramp automatically.
console/src/themes/index.jsx No change for this PR (shape.borderRadius: 8 stays as-is, lines 47-48). The radius-sm = 6 token is explicitly out of scope here — see follow-up item below.
console/src/config.js / console/src/themes/typography.js No change required for the CJK stack itself (config.js:54 already carries the correct fallback chain); this item is about adding the same chain, minus Inter var, to the login side.

Follow-up PR #1 (separate, radius): add a radius-sm = 6px token and apply it via explicit styleOverrides.root.borderRadius: 6 to console/src/themes/overrides/Button.js, OutlinedInput.js, and Alert.jsx (none of the three has any existing radius override today — confirmed by direct read). Requires a full visual QA pass since these three components render on nearly every console screen.

Follow-up PR #2 (separate, cleanup): delete console/src/themes/theme/theme1.js through theme8.js and remove their dead imports/switch-cases in console/src/themes/theme/index.js:3-10,16-31.

Verification already done, not left as a TODO: the WCAG contrast numbers in §1 are computed (standard relative-luminance formula), not estimated, and dark mode is a non-issue because it isn’t changing (§1). grep -rln "palette\.secondary\|customShadows\|secondary\[" console/src --include="*.js" --include="*.jsx" returns exactly 40 files (confirmed by running it, not assumed) — all of them reference the palette object abstractly (theme.vars.palette.secondary[...], never a literal hex), so they pick up the new light-mode ramp automatically with no edits needed.

Known pre-existing issues this sweep surfaced, unrelated to this spec and explicitly out of scope — do not fix as part of this change: Checkbox.jsx/Radio.jsx read palette.secondary[300], a key secondaryColors’ object literal in default.js never defines (it jumps from 200 to light/400) — already resolves to undefined today, before any of this spec. Alert.jsx reads palette.grey[0], which also doesn’t exist on this palette. Separately, greping for the literal word secondary misses two classes of real, consciously accepted consumers of this change: (1) sx-prop string references like bgcolor: 'secondary.200'/'secondary.100' (header chips/menus — NavItem, Header/index.jsx, MobileSection, Localization, FullScreen), which shift from flat grey to the new blue-tinted #DEE8F6/#EFF4FB the same as every other consumer of those indices; (2) secondary.main/color="secondary" used as a real, user-visible color — not just a neutral — in Snackbar.jsx/Snackbar.js and any Chip/Button rendered with color="secondary". Both are intended, not overlooked: this spec’s whole point is that Console’s grey becomes brand-tinted everywhere it’s used.

  • Login: all five neutral custom properties keep their exact current literal values (they become the ramp’s anchors, not replacements) — zero visible color change on login. The card shadow softens very slightly (vertical offset 4px → 8px, same blur/alpha/hue) — a subtle, not dramatic, change. Japanese body text gains an explicit CJK font fallback it currently lacks — likely the most visible change on this surface, and a fix rather than a regression.
  • Console (light mode): every neutral surface shifts toward the same ~217° hue family as the brand’s primary color. The two lightest steps, which currently collapse to the same hex (#F8F9FA at both index 0 and 1), become visibly distinct (#F7FAFE/#EFF4FB). Borders/dividers shift from a flat grey (#DBE0E5) to a visibly blue-tinted line (#CDDBF0) around every card/table/input. Default body text (text.primary) lightens from a near-black (#1D2630) to a lighter steel-blue-grey (#39465F) — the single most visible change console-wide, and the one requiring the formal contrast re-check above. Shadows look essentially unchanged to the eye (both old and new base are near-black at 8% alpha) but now share an exact token with login’s shadow instead of a coincidental alpha match.
  • Console (dark mode): zero visible change. Corrected from revision 1, which claimed dark mode would read “marginally cooler/bluer” — in fact the originally-proposed reversal would have made dark-mode cards/dialogs/drawers visibly lighter (background.paper L≈15%→30%), not just cooler. This spec now leaves the dark secondaryColors array untouched, so dark mode renders exactly as it does today.
  • Card radius/border (login 8px+shadow+no-border vs. Console MainCard 12px+border+no-shadow): a real, previously-undocumented gap. Not changed by this spec — left as an open question below, since fixing radius alone without addressing the border-vs-shadow structural difference would be a partial fix.
  • Radius-sm and theme1-8.js cleanup: zero visible change in this PR (both are scoped to follow-ups / are pure dead-code removal).
  1. Light/dark scope decision (§5): Is “login is permanently light-only while Console supports light/dark” the correct long-term product position, or should login dark-mode support be scoped as a future (separate) project? The repo’s files show this as a deliberate, authored decision today, not an oversight — but confirming it should stay that way is a product call, not something file-reading alone can settle.
  2. Brand-identity sign-off and text weight (§1): login’s own neutrals keep their exact current hex values under this plan, but Console’s light-mode palette shifts to match them — a stakeholder who signed off on Console’s current (more neutral/achromatic) grey should confirm the new blue-tinted ramp is still acceptable brand-wide. Separately: text.primary landing at L≈30% (#39465F) passes WCAG contrast comfortably, but whether that’s the right text weight for a dense, table-heavy admin UI (as opposed to login’s sparse single-card page, which is what --gcu-text was originally chosen for) is a design judgment this document can’t settle by itself.
  3. Card radius and border/shadow treatment (§2): login’s card is 8px radius, no border, with a shadow; Console’s MainCard is 12px radius, a 1px border, no shadow by default. Should Console’s card move to 8px to match login, should login’s card move to 12px, or is “cards look structurally different between the two surfaces” an acceptable, permanent difference? This spec does not pick a direction.
  4. Radius-sm scope and login’s own native controls (§2): confirmed as a genuine gap for Button/Alert — but login.css never actually styles its own input fields or submit button (they’re unstyled PatternFly field.ftl defaults), so there’s no confirmed login-side value for MuiOutlinedInput to match yet. Should follow-up work (a) add an explicit radius rule to login’s PF form controls first, so there’s something real to match, or (b) scope the Console-side follow-up to Button+Alert only and leave inputs alone? Also: should login’s two-tier radius system be extended into Console (this spec’s default assumption), or should login instead be flattened to Console’s single 8px tier?
  5. Inter var on login (§4): this spec recommends NOT shipping the Inter var webfont to login (CJK-only fallback chain is enough to fix the reported gap). Confirm this is acceptable versus shipping full Latin-glyph parity with Console, which would require bundling the font asset into the Keycloak theme and is a larger, separate change.
  6. Font override mechanism (§4) — Resolved, verified against a real running instance, not just file-reading. body-level font-family alone was confirmed insufficient: Keycloak was started locally with this theme live-mounted (make kc-up dev-realm) and getComputedStyle() checked on the actual rendered login page (kc_locale=ja). #kc-page-title/.pf-v5-c-title computed to RedHatDisplay, helvetica, arial, sans-serif — PatternFly 5’s Title component reads --pf-v5-global--FontFamily--heading directly rather than inheriting body — so the page title was silently falling through to the browser’s own CJK fallback, exactly as this open question suspected. Buttons/labels/form controls were confirmed fine (they use font-family: inherit). Fix shipped on console/unify-design-tokens: override --pf-v5-global--FontFamily--text/--heading at :root (PatternFly’s own variable-override idiom, same pattern this theme already uses for --keycloak-card-top-color) in addition to the body rule. Re-verified after the fix: #kc-page-title now computes to the Hiragino/Noto Sans JP/Meiryo stack.
  7. Ramp uniformity trade-off (§1): this spec’s neutral ramp pins 6 of 10 stops to existing literals (vs. 2 of 10 for the primary-ramp precedent), producing an uneven lightness progression, in exchange for keeping login’s five values byte-identical and getting the shadow-token “zero formula change” in §3. Is that the right trade, or would a more perceptually-uniform ramp (fewer pinned stops, OKLCH interpolation) be worth the byte-level changes to login’s literals and a one-line formula change in custom-shadows.jsx instead?