@multiplatform.one/keycloak-theme 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/css.ts ADDED
@@ -0,0 +1,655 @@
1
+ /**
2
+ * THE SPRINGBOARD SKIN FOR KEYCLOAK'S LOGIN PAGES, AS ONE STYLESHEET.
3
+ *
4
+ * ★ A STYLESHEET AND NOT A COMPONENT TREE, ON PURPOSE. Keycloak's login
5
+ * flow is ~38 FreeMarker pages. Keycloakify renders every one of them from
6
+ * its own React components, and the ones nobody ever looks at — WebAuthn
7
+ * errors, recovery-code confirmation, SAML post-forms, X.509 info — are
8
+ * exactly the ones that must not break, because a user only ever reaches
9
+ * them on the worst day of their week. Restyling a page means forking it,
10
+ * a fork means owning its form markup forever, and a form's markup is its
11
+ * flow: the `action`, the `name` on every input, the hidden
12
+ * `credentialId`, the `tabIndex` order. Every page this sheet themes is a
13
+ * page whose flow it provably cannot touch, because it never renders one.
14
+ *
15
+ * ★ WHAT IT TARGETS. keycloakify's pages emit PatternFly class names
16
+ * (`.pf-c-form-control`, `.pf-c-button`, `.card-pf`) and a stable set of
17
+ * ids (`#kc-form-login`, `#kc-page-title`, `#kc-social-providers`). Those
18
+ * are the theme's API surface, and they are what this sheet hooks. The
19
+ * pages are left entirely alone.
20
+ *
21
+ * ★ HOW IT COMPOSES WITH THE HOUSE SHEET. `apps/keycloak` already ships
22
+ * `KcHouseStyles`, which resolves structure — radius, control height, the
23
+ * 44px press-target floor, the focus ring — from the SAME tamagui knobs
24
+ * the catalog components use, and paints colour from the theme ramps. This
25
+ * sheet keeps all of that structure and replaces only the SURFACE. It does
26
+ * so twice over so it works either way:
27
+ *
28
+ * 1. it redefines the `--kc-*` custom properties `KcHouseStyles` invents,
29
+ * so every rule that sheet already wrote follows the springboard
30
+ * without either file knowing about the other, and
31
+ * 2. it states the springboard surface directly against the same
32
+ * PatternFly selectors, so the skin is complete on its own when the
33
+ * house sheet is absent.
34
+ *
35
+ * Its selectors carry one extra `html.login-pf` over the house sheet's,
36
+ * which puts every rule here at a higher specificity than its counterpart
37
+ * there. That is deliberate: it makes the layering independent of which
38
+ * <style> element React happens to mount first.
39
+ *
40
+ * ★ ONE SURFACE IN BOTH COLOUR SCHEMES. Every var block below is emitted
41
+ * for `html.login-pf` AND `html.login-pf.t_dark`, and `prefers-color-scheme`
42
+ * is never consulted. shc's `wallpaper.ts` carries the ruling and the
43
+ * defect that produced it — see ./tokens.ts `wallpaperColors`.
44
+ */
45
+
46
+ import type { GlyphMark } from "./glyph";
47
+ import {
48
+ accent,
49
+ caption,
50
+ field,
51
+ fontFamily as defaultFontFamily,
52
+ icon,
53
+ ink,
54
+ plate,
55
+ status,
56
+ trigger,
57
+ wallpaperColors,
58
+ wallpaperLayers,
59
+ } from "./tokens";
60
+
61
+ export interface SpringboardCssOptions {
62
+ /**
63
+ * The glyph plate to draw above the page title. Omit it and no plate is
64
+ * emitted at all — the rule is absent rather than empty, so a caller
65
+ * without a realm name gets stock spacing instead of a blank square.
66
+ */
67
+ mark?: GlyphMark;
68
+ /** Body face. Defaults to the no-webfont stack in ./tokens.ts. */
69
+ fontFamily?: string;
70
+ /**
71
+ * The keyboard focus ring. Defaults match `FOCUS_VISIBLE_RING` in
72
+ * `@multiplatform.one/theme`; they are options rather than an import so
73
+ * this package stays dependency-free and testable without tamagui.
74
+ */
75
+ focusRingWidthPx?: number;
76
+ focusRingOffsetPx?: number;
77
+ /**
78
+ * The press-target floor (`MIN_PRESS_TARGET` in `@multiplatform.one/theme`).
79
+ * DG-A11Y-01; restated here so the skin cannot shrink a control below it.
80
+ */
81
+ minPressTargetPx?: number;
82
+ /** Card width. The springboard has no card, so this is a login-page choice. */
83
+ cardMaxWidthPx?: number;
84
+ }
85
+
86
+ /** CSS string literal for `content:` — escapes the quote and the backslash. */
87
+ function cssString(value: string): string {
88
+ return JSON.stringify(value);
89
+ }
90
+
91
+ /**
92
+ * The `--kc-*` contract `KcHouseStyles` publishes, restated in springboard
93
+ * values. Redefining these is what carries the skin into every rule that
94
+ * sheet already wrote (inputs, buttons, checkbox, locale menu, alerts,
95
+ * info band) without this file having to repeat them.
96
+ */
97
+ function customProperties(): string {
98
+ return [
99
+ // Surfaces.
100
+ `--kc-page-bg: ${wallpaperColors.base};`,
101
+ `--kc-card-bg: ${plate.fill};`,
102
+ `--kc-input-bg: ${field.background};`,
103
+ // Ink.
104
+ `--kc-text: ${ink.primary};`,
105
+ `--kc-muted: ${ink.muted};`,
106
+ `--kc-placeholder: ${ink.faint};`,
107
+ // Hairlines.
108
+ `--kc-border: ${field.border};`,
109
+ `--kc-border-hover: ${field.borderHover};`,
110
+ `--kc-border-focus: ${field.borderFocus};`,
111
+ `--kc-border-strong: ${field.borderHover};`,
112
+ `--kc-divider: ${field.divider};`,
113
+ // Interaction.
114
+ `--kc-hover-bg: ${field.ghost};`,
115
+ `--kc-press-bg: ${field.ghostHover};`,
116
+ `--kc-ring: ${accent.sky};`,
117
+ `--kc-link: ${accent.sky};`,
118
+ // Call to action.
119
+ `--kc-cta-bg: ${accent.indigo};`,
120
+ `--kc-cta-fg: ${ink.primary};`,
121
+ `--kc-cta-hover: ${accent.indigoHover};`,
122
+ `--kc-cta-press: ${accent.indigoPress};`,
123
+ // Disabled — washed chrome, lowest legible ink.
124
+ `--kc-disabled-bg: ${field.ghost};`,
125
+ `--kc-disabled-border: ${field.border};`,
126
+ `--kc-disabled-text: ${ink.faint};`,
127
+ // Feedback.
128
+ `--kc-error-text: ${status.error};`,
129
+ `--kc-error-border: ${status.error};`,
130
+ `--kc-warning-border: ${status.warning};`,
131
+ `--kc-success-border: ${status.success};`,
132
+ `--kc-info-border: ${status.info};`,
133
+ `--kc-menu-shadow: rgba(0,0,0,0.45);`,
134
+ // Springboard's own, consumed only by this sheet.
135
+ `--sb-plate-fill: ${plate.fill};`,
136
+ `--sb-plate-highlight: ${plate.highlight};`,
137
+ `--sb-plate-radius: ${plate.radius}px;`,
138
+ `--sb-icon-size: ${icon.size}px;`,
139
+ `--sb-icon-radius: ${icon.radius}px;`,
140
+ `--sb-caption-size: ${caption.fontSize}px;`,
141
+ `--sb-caption-line: ${caption.lineHeight}px;`,
142
+ ].join("\n ");
143
+ }
144
+
145
+ /** The white-on-wallpaper halo every label outside the plate wears. */
146
+ const onWallpaperShadow = `0 ${caption.shadowOffsetY}px ${caption.shadowBlur}px ${wallpaperColors.textShadow}`;
147
+
148
+ /** The plate's elevation — `trigger`'s, not `icon`'s. See ./tokens.ts. */
149
+ const plateShadow = `0 ${trigger.shadowOffsetY}px ${trigger.shadowBlur}px rgba(0,0,0,0.45)`;
150
+
151
+ /** The squircle's own elevation — `icon`'s. */
152
+ const iconShadow = `0 ${icon.shadowOffsetY}px ${icon.shadowBlur}px ${icon.shadowColor}`;
153
+
154
+ /**
155
+ * The glyph plate above the page title, drawn as a `::before` on the title
156
+ * itself.
157
+ *
158
+ * ★ A PSEUDO-ELEMENT RATHER THAN MARKUP, and this is the decision that
159
+ * keeps the whole skin out of the pages. A React mark would have to be
160
+ * placed inside the card above the title, which means owning `Template` —
161
+ * and `Template` is where the locale switcher, the alert rail, the
162
+ * `socialProvidersNode` slot and the try-another-way form live. Forking it
163
+ * to insert one square is the same trade as forking a page: a permanent
164
+ * maintenance debt against upstream, taken on for decoration. A generated
165
+ * `content:` costs one rule, is implicitly `aria-hidden` (pseudo-elements
166
+ * are not in the accessibility tree), and the realm's name is already read
167
+ * out loud by the `<h1>` it hangs from.
168
+ *
169
+ * ★ IT ALSO COVERS `#kc-username`. keycloakify's Template swaps the `<h1>`
170
+ * for a `#kc-username` block whenever `auth.showUsername` is set and the
171
+ * flow is not a credential reset — the second step of a split
172
+ * username/password flow, and a page users very much do see. Both get the
173
+ * plate so the mark does not vanish between step one and step two.
174
+ */
175
+ function markRules(scope: string, mark: GlyphMark): string {
176
+ const monogramSize = Math.round(icon.size * 0.34);
177
+ const monogramLine = Math.round(icon.size * 0.4);
178
+ return `
179
+ /* ── the estate's glyph: the springboard's own squircle ────── */
180
+ ${scope} .login-pf-page #kc-page-title,
181
+ ${scope} .login-pf-page #kc-username {
182
+ display: flex;
183
+ flex-direction: column;
184
+ align-items: center;
185
+ gap: ${caption.marginTop}px;
186
+ }
187
+ ${scope} .login-pf-page #kc-page-title::before,
188
+ ${scope} .login-pf-page #kc-username::before {
189
+ content: ${cssString(mark.monogram)};
190
+ box-sizing: border-box;
191
+ width: var(--sb-icon-size);
192
+ height: var(--sb-icon-size);
193
+ border-radius: var(--sb-icon-radius);
194
+ background: ${mark.background};
195
+ color: ${mark.foreground};
196
+ box-shadow: ${iconShadow};
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ font-size: ${monogramSize}px;
201
+ line-height: ${monogramLine}px;
202
+ font-weight: 800;
203
+ letter-spacing: 0.5px;
204
+ user-select: none;
205
+ }`;
206
+ }
207
+
208
+ /** Build the springboard stylesheet. Pure — same options, same string. */
209
+ export function buildSpringboardCss(options: SpringboardCssOptions = {}): string {
210
+ const {
211
+ mark,
212
+ fontFamily = defaultFontFamily,
213
+ focusRingWidthPx = 2,
214
+ focusRingOffsetPx = 2,
215
+ minPressTargetPx = 44,
216
+ cardMaxWidthPx = 420,
217
+ } = options;
218
+
219
+ const scope = "html.login-pf";
220
+ const vars = customProperties();
221
+ // The card's own padding, named so the #kc-info band below can bleed back
222
+ // out to the card edge by exactly the amount the card came in by. The
223
+ // stock sheet hardcodes 40/30 there and gets a band that stops short of
224
+ // the corners under any other padding.
225
+ const cardPadY = plate.paddingVertical + 12;
226
+ const cardPadX = plate.paddingHorizontal + 8;
227
+
228
+ return `
229
+ /* ── springboard tokens, both colour schemes ───────────────── */
230
+ /* One dark surface in light and dark alike — see ./tokens.ts. */
231
+ ${scope},
232
+ ${scope}.t_dark {
233
+ ${vars}
234
+ }
235
+
236
+ /* ── the wallpaper ─────────────────────────────────────────── */
237
+ ${scope} {
238
+ background-color: ${wallpaperColors.base};
239
+ background-image: ${wallpaperLayers};
240
+ /* Fixed so the glows stay anchored to the viewport instead of tiling
241
+ down a page that grew a scrollbar (a long consent screen, a locale
242
+ list). The springboard's wallpaper is a layer behind everything, not
243
+ a background that scrolls with content. */
244
+ background-attachment: fixed;
245
+ background-repeat: no-repeat;
246
+ background-size: cover;
247
+ min-height: 100%;
248
+ }
249
+ ${scope} body {
250
+ background: transparent none;
251
+ color: ${ink.primary};
252
+ font-family: ${fontFamily};
253
+ -webkit-font-smoothing: antialiased;
254
+ }
255
+ ${scope} .login-pf-page {
256
+ background: transparent;
257
+ padding-bottom: 40px;
258
+ }
259
+
260
+ /* ── the realm title, sitting straight on the wallpaper ────── */
261
+ /* Caption anatomy from the launcher: white with a halo, because this
262
+ text has no plate under it and may land on either glow. */
263
+ ${scope} .login-pf-page #kc-header-wrapper {
264
+ color: ${wallpaperColors.text};
265
+ text-shadow: ${onWallpaperShadow};
266
+ font-weight: 700;
267
+ font-size: 22px;
268
+ line-height: 1.3;
269
+ letter-spacing: normal;
270
+ text-transform: none;
271
+ padding: 44px 16px 18px;
272
+ }
273
+
274
+ /* ── the card: the dock's glass shelf, grown to hold a form ── */
275
+ ${scope} .login-pf-page .card-pf {
276
+ max-width: ${cardMaxWidthPx}px;
277
+ margin: 0 auto;
278
+ background: var(--sb-plate-fill);
279
+ border: 1px solid var(--sb-plate-highlight);
280
+ border-radius: var(--sb-plate-radius);
281
+ /* The shelf's top-edge inner highlight ('inset 0 1px 0 #fff4' in the
282
+ launcher payload) plus the trigger's elevation under the slab. */
283
+ box-shadow:
284
+ inset 0 1px 0 ${plate.highlight},
285
+ ${plateShadow};
286
+ /* "blur-ish" is the launcher's own word for the dock material. Where
287
+ backdrop-filter is unsupported the fill alone still reads as a slab,
288
+ so this degrades rather than breaks. */
289
+ backdrop-filter: blur(22px) saturate(140%);
290
+ -webkit-backdrop-filter: blur(22px) saturate(140%);
291
+ padding: ${cardPadY}px ${cardPadX}px;
292
+ overflow: hidden;
293
+ }
294
+ ${scope} .login-pf-page .login-pf-header h1,
295
+ ${scope} .login-pf-page #kc-page-title {
296
+ color: ${ink.primary};
297
+ font-weight: 700;
298
+ font-size: 20px;
299
+ line-height: 1.3;
300
+ margin: 0 0 ${plate.gap}px;
301
+ }
302
+ ${mark ? markRules(scope, mark) : ""}
303
+
304
+ /* ── labels and prose ──────────────────────────────────────── */
305
+ ${scope} .login-pf-page label,
306
+ ${scope} .login-pf-page .pf-c-form__label,
307
+ ${scope} .login-pf-page .pf-c-form__label-text {
308
+ color: ${ink.secondary};
309
+ font-weight: 500;
310
+ font-size: 14px;
311
+ }
312
+ ${scope} .login-pf-page .instruction,
313
+ ${scope} .login-pf-page .subtitle,
314
+ ${scope} .login-pf-page .pf-c-form__helper-text,
315
+ ${scope} .login-pf-page #kc-info,
316
+ ${scope} .login-pf-page .login-pf-signup {
317
+ color: ${ink.muted};
318
+ }
319
+ ${scope} .login-pf-page #kc-attempted-username {
320
+ color: ${ink.primary};
321
+ font-weight: 600;
322
+ }
323
+
324
+ /* ── fields: inset into the slab, not laid on top of it ────── */
325
+ ${scope} .login-pf-page .pf-c-form-control,
326
+ ${scope} .login-pf-page input[type="text"],
327
+ ${scope} .login-pf-page input[type="password"],
328
+ ${scope} .login-pf-page input[type="email"],
329
+ ${scope} .login-pf-page input[type="tel"],
330
+ ${scope} .login-pf-page input[type="number"],
331
+ ${scope} .login-pf-page select,
332
+ ${scope} .login-pf-page textarea {
333
+ min-height: ${minPressTargetPx}px;
334
+ background-color: ${field.background};
335
+ color: ${ink.primary};
336
+ border: 1px solid ${field.border};
337
+ box-shadow: none;
338
+ }
339
+ ${scope} .login-pf-page .pf-c-form-control:hover,
340
+ ${scope} .login-pf-page select:hover,
341
+ ${scope} .login-pf-page textarea:hover {
342
+ border-color: ${field.borderHover};
343
+ }
344
+ ${scope} .login-pf-page .pf-c-form-control:focus,
345
+ ${scope} .login-pf-page select:focus,
346
+ ${scope} .login-pf-page textarea:focus {
347
+ border-color: ${field.borderFocus};
348
+ background-color: ${field.background};
349
+ }
350
+ ${scope} .login-pf-page .pf-c-form-control::placeholder,
351
+ ${scope} .login-pf-page input::placeholder {
352
+ color: ${ink.faint};
353
+ }
354
+ ${scope} .login-pf-page .pf-c-form-control[aria-invalid="true"] {
355
+ border-color: ${status.error};
356
+ }
357
+ /* ★ THE INPUT GROUP IS OPAQUE WHITE IN PATTERNFLY, and that one fact
358
+ defeats the whole field treatment above. '.pf-c-input-group' wraps the
359
+ password field and its visibility toggle; both of those are painted in
360
+ TRANSLUCENT springboard colours, so over a white parent the field
361
+ composites to mid-grey and the toggle to near-white — which is exactly
362
+ how it rendered before this rule existed. Clearing the parent is what
363
+ makes the two translucent children land on the slab instead. */
364
+ ${scope} .login-pf-page .pf-c-input-group {
365
+ background: transparent;
366
+ display: flex;
367
+ align-items: stretch;
368
+ gap: 8px;
369
+ }
370
+ ${scope} .login-pf-page .pf-c-input-group .pf-c-form-control {
371
+ flex: 1 1 auto;
372
+ }
373
+ ${scope} .login-pf-page .pf-c-input-group .pf-c-button.pf-m-control {
374
+ min-width: ${minPressTargetPx}px;
375
+ }
376
+
377
+ /* Chrome paints autofilled inputs a fixed pale yellow that no colour
378
+ property overrides — the inset shadow is the documented way to repaint
379
+ the field, and without it a remembered username lands as a white bar in
380
+ the middle of the slab. */
381
+ ${scope} .login-pf-page input:-webkit-autofill,
382
+ ${scope} .login-pf-page input:-webkit-autofill:hover,
383
+ ${scope} .login-pf-page input:-webkit-autofill:focus {
384
+ -webkit-box-shadow: 0 0 0 1000px ${field.background} inset;
385
+ -webkit-text-fill-color: ${ink.primary};
386
+ caret-color: ${ink.primary};
387
+ }
388
+
389
+ /* ── buttons ───────────────────────────────────────────────── */
390
+ ${scope} .login-pf-page .pf-c-button {
391
+ min-height: ${minPressTargetPx}px;
392
+ font-weight: 600;
393
+ }
394
+ ${scope} .login-pf-page .pf-c-button.pf-m-primary,
395
+ ${scope} .login-pf-page input.pf-m-primary[type="submit"],
396
+ ${scope} .login-pf-page input.pf-m-primary[type="button"] {
397
+ background-color: ${accent.indigo};
398
+ border-color: ${accent.indigo};
399
+ color: ${ink.primary};
400
+ box-shadow: ${iconShadow};
401
+ }
402
+ ${scope} .login-pf-page .pf-c-button.pf-m-primary:hover,
403
+ ${scope} .login-pf-page input.pf-m-primary[type="submit"]:hover {
404
+ background-color: ${accent.indigoHover};
405
+ border-color: ${accent.indigoHover};
406
+ }
407
+ ${scope} .login-pf-page .pf-c-button.pf-m-primary:active,
408
+ ${scope} .login-pf-page input.pf-m-primary[type="submit"]:active {
409
+ background-color: ${accent.indigoPress};
410
+ border-color: ${accent.indigoPress};
411
+ }
412
+ ${scope} .login-pf-page .pf-c-button.pf-m-primary:disabled,
413
+ ${scope} .login-pf-page input.pf-m-primary[type="submit"]:disabled {
414
+ background-color: ${field.ghost};
415
+ border-color: ${field.border};
416
+ color: ${ink.faint};
417
+ box-shadow: none;
418
+ }
419
+ ${scope} .login-pf-page .pf-c-button.pf-m-control,
420
+ ${scope} .login-pf-page .pf-c-button.pf-m-secondary,
421
+ ${scope} .login-pf-page .pf-c-button.btn-default:not(.pf-m-primary) {
422
+ background-color: ${field.ghost};
423
+ border-color: ${field.border};
424
+ color: ${ink.primary};
425
+ }
426
+ ${scope} .login-pf-page .pf-c-button.pf-m-control:hover,
427
+ ${scope} .login-pf-page .pf-c-button.pf-m-secondary:hover,
428
+ ${scope} .login-pf-page .pf-c-button.btn-default:not(.pf-m-primary):hover {
429
+ background-color: ${field.ghostHover};
430
+ border-color: ${field.borderHover};
431
+ color: ${ink.primary};
432
+ }
433
+ /* PatternFly draws control-button borders on an ::after overlay and
434
+ accents its bottom edge on interaction; on the slab that reads as a
435
+ stray underline beneath the password-visibility toggle. */
436
+ ${scope} .login-pf-page .pf-c-button::after,
437
+ ${scope} .login-pf-page .pf-c-button:hover::after,
438
+ ${scope} .login-pf-page .pf-c-button:focus::after,
439
+ ${scope} .login-pf-page .pf-c-button:active::after {
440
+ content: none;
441
+ }
442
+
443
+ /* ── the remember-me checkbox ──────────────────────────────── */
444
+ /* ★ THE RESET IS PART OF THE SKIN, not borrowed from the house sheet.
445
+ A native checkbox is a 13px white box drawn by the platform: no colour
446
+ property reaches it, so on the slab it reads as a bright chip with no
447
+ relationship to anything around it. 'appearance: none' is what makes it
448
+ paintable at all. The visual square stays 22px inside a transparent
449
+ border that pads the hit box out to the press-target floor, so the
450
+ control is small and the target is not (DG-A11Y-01).
451
+
452
+ The house sheet bakes its tick glyph as a data-URI at build time from
453
+ the ramp's CTA foreground, so the springboard's tick is restated here
454
+ rather than inherited through a custom property. */
455
+ ${scope} .login-pf-page input[type="checkbox"] {
456
+ appearance: none;
457
+ -webkit-appearance: none;
458
+ position: static;
459
+ box-sizing: content-box;
460
+ width: 22px;
461
+ height: 22px;
462
+ border: ${Math.round((minPressTargetPx - 22) / 2)}px solid transparent;
463
+ border-radius: ${Math.round((minPressTargetPx - 22) / 2) + 8}px;
464
+ background-clip: padding-box;
465
+ background-repeat: no-repeat;
466
+ background-position: center;
467
+ background-size: 15px 15px;
468
+ margin: 0 4px 0 0;
469
+ flex: 0 0 auto;
470
+ cursor: pointer;
471
+ background-color: ${field.background};
472
+ box-shadow: inset 0 0 0 1.5px ${field.borderHover};
473
+ }
474
+ ${scope} .login-pf-page .checkbox label,
475
+ ${scope} .login-pf-page .pf-c-check {
476
+ display: inline-flex;
477
+ align-items: center;
478
+ min-height: ${minPressTargetPx}px;
479
+ padding-left: 0;
480
+ margin: 0;
481
+ cursor: pointer;
482
+ }
483
+ ${scope} .login-pf-page input[type="checkbox"]:checked,
484
+ ${scope}.t_dark .login-pf-page input[type="checkbox"]:checked {
485
+ background-color: ${accent.indigo};
486
+ box-shadow: inset 0 0 0 1.5px ${accent.indigo};
487
+ background-image: url("data:image/svg+xml,${encodeURIComponent(
488
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${ink.primary}" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`,
489
+ )}");
490
+ }
491
+
492
+ /* ── links ─────────────────────────────────────────────────── */
493
+ ${scope} .login-pf-page a {
494
+ color: ${accent.sky};
495
+ }
496
+ ${scope} .login-pf-page a:hover {
497
+ color: ${accent.sky};
498
+ }
499
+
500
+ /* ── alerts ────────────────────────────────────────────────── */
501
+ ${scope} .login-pf-page .pf-c-alert.pf-m-inline {
502
+ background: ${field.background};
503
+ border: 1px solid ${field.border};
504
+ border-left-width: 3px;
505
+ border-left-color: ${status.info};
506
+ }
507
+ ${scope} .login-pf-page .pf-c-alert.pf-m-inline.pf-m-danger {
508
+ border-left-color: ${status.error};
509
+ }
510
+ ${scope} .login-pf-page .pf-c-alert.pf-m-inline.pf-m-warning {
511
+ border-left-color: ${status.warning};
512
+ }
513
+ ${scope} .login-pf-page .pf-c-alert.pf-m-inline.pf-m-success {
514
+ border-left-color: ${status.success};
515
+ }
516
+ ${scope} .login-pf-page .pf-c-alert__title {
517
+ color: ${ink.primary};
518
+ }
519
+ ${scope} .login-pf-page .pf-c-alert__icon {
520
+ color: ${ink.muted};
521
+ }
522
+ ${scope} .login-pf-page span#input-error,
523
+ ${scope} .login-pf-page [id^="input-error"],
524
+ ${scope} .login-pf-page .pf-m-error,
525
+ ${scope} .login-pf-page .required {
526
+ color: ${status.error};
527
+ }
528
+
529
+ /* ── identity providers: the SSO buttons ───────────────────── */
530
+ /* ★ CHROME ONLY. Every provider row is an <a href={p.loginUrl}> that
531
+ keycloakify renders from social.providers; nothing here changes what
532
+ it points at, what it is called, or whether it appears. */
533
+ ${scope} .login-pf-page #kc-social-providers h2 {
534
+ color: ${ink.muted};
535
+ font-size: 13px;
536
+ font-weight: 500;
537
+ }
538
+ ${scope} .login-pf-page hr,
539
+ ${scope} .login-pf-page #kc-social-providers hr {
540
+ border-top: 1px solid ${field.divider};
541
+ opacity: 1;
542
+ }
543
+ ${scope} .login-pf-page .kc-social-item,
544
+ ${scope} .login-pf-page .kc-social-links a,
545
+ ${scope} .login-pf-page #kc-social-providers li a {
546
+ min-height: ${minPressTargetPx}px;
547
+ background: ${field.ghost};
548
+ border: 1px solid ${field.border};
549
+ border-radius: var(--sb-icon-radius);
550
+ color: ${ink.primary};
551
+ text-decoration: none;
552
+ }
553
+ ${scope} .login-pf-page .kc-social-item:hover,
554
+ ${scope} .login-pf-page .kc-social-links a:hover,
555
+ ${scope} .login-pf-page #kc-social-providers li a:hover {
556
+ background: ${field.ghostHover};
557
+ border-color: ${field.borderHover};
558
+ color: ${ink.primary};
559
+ }
560
+ ${scope} .login-pf-page .kc-social-provider-name,
561
+ ${scope} .login-pf-page .kc-social-icon-text {
562
+ color: ${ink.primary};
563
+ }
564
+
565
+ /* ── the OTP credential tiles (login-otp.ftl) ──────────────── */
566
+ ${scope} .login-pf-page .pf-c-tile {
567
+ background: ${field.background};
568
+ border: 1px solid ${field.border};
569
+ border-radius: var(--sb-icon-radius);
570
+ color: ${ink.primary};
571
+ }
572
+ ${scope} .login-pf-page .pf-c-tile__input:checked + .pf-c-tile,
573
+ ${scope} .login-pf-page .pf-c-tile__input:checked + label {
574
+ border-color: ${accent.indigo};
575
+ background: ${field.ghostHover};
576
+ }
577
+ ${scope} .login-pf-page .pf-c-tile__title,
578
+ ${scope} .login-pf-page .pf-c-tile__icon {
579
+ color: ${ink.primary};
580
+ }
581
+
582
+ /* ── the registration / back-to-app footer band ────────────── */
583
+ /* The stock sheet bleeds #kc-info to the card edge as a #f0f0f0 band.
584
+ On the slab it becomes the dock's own darker step. */
585
+ ${scope} .login-pf-page #kc-info {
586
+ margin: ${plate.gap}px -${cardPadX}px -${cardPadY}px;
587
+ padding: 0;
588
+ color: ${ink.muted};
589
+ }
590
+ ${scope} .login-pf-page #kc-info-wrapper {
591
+ background: rgba(8, 11, 20, 0.42);
592
+ border-top: 1px solid ${field.divider};
593
+ border-radius: 0 0 ${plate.radius - 1}px ${plate.radius - 1}px;
594
+ padding: 14px ${cardPadX}px;
595
+ color: ${ink.muted};
596
+ text-align: center;
597
+ }
598
+ ${scope} .login-pf-page #kc-info-wrapper a {
599
+ color: ${accent.sky};
600
+ }
601
+
602
+ /* ── the locale menu ───────────────────────────────────────── */
603
+ ${scope} .login-pf-page #kc-current-locale-link {
604
+ color: ${ink.muted};
605
+ }
606
+ ${scope} .login-pf-page #kc-current-locale-link:hover {
607
+ background: ${field.ghost};
608
+ color: ${ink.primary};
609
+ }
610
+ ${scope} .login-pf-page .pf-c-dropdown__menu {
611
+ background: ${wallpaperColors.base};
612
+ border: 1px solid ${plate.highlight};
613
+ box-shadow: ${plateShadow};
614
+ }
615
+ ${scope} .login-pf-page .pf-c-dropdown__menu-item {
616
+ color: ${ink.primary};
617
+ }
618
+ ${scope} .login-pf-page .pf-c-dropdown__menu-item:hover {
619
+ background: ${field.ghost};
620
+ color: ${ink.primary};
621
+ }
622
+
623
+ /* ── focus ring (DG-A11Y-01: non-removable) ────────────────── */
624
+ /* !important outranks both PatternFly's focus styles and the house
625
+ ThemeProvider's global outline reset. The keyboard ring is the one
626
+ affordance a skin must never take away. */
627
+ ${scope} a:focus-visible,
628
+ ${scope} button:focus-visible,
629
+ ${scope} input:focus-visible,
630
+ ${scope} select:focus-visible,
631
+ ${scope} textarea:focus-visible,
632
+ ${scope} [tabindex]:focus-visible {
633
+ outline: ${focusRingWidthPx}px solid ${accent.sky} !important;
634
+ outline-offset: ${focusRingOffsetPx}px !important;
635
+ box-shadow: none !important;
636
+ }
637
+
638
+ /* ── small screens keep the slab ───────────────────────────── */
639
+ @media (max-width: 767px) {
640
+ ${scope} .login-pf-page .card-pf {
641
+ margin: 0 12px;
642
+ background: var(--sb-plate-fill);
643
+ border: 1px solid var(--sb-plate-highlight);
644
+ border-radius: var(--sb-plate-radius);
645
+ box-shadow:
646
+ inset 0 1px 0 ${plate.highlight},
647
+ ${plateShadow};
648
+ }
649
+ ${scope} .login-pf-page #kc-header-wrapper {
650
+ padding: 28px 16px 14px;
651
+ font-size: 19px;
652
+ }
653
+ }
654
+ `;
655
+ }