diff --git a/frontend/scripts/build-tokens.ts b/frontend/scripts/build-tokens.ts
index d971ded83..b2fa3bf2c 100644
--- a/frontend/scripts/build-tokens.ts
+++ b/frontend/scripts/build-tokens.ts
@@ -16,6 +16,7 @@ import {
colorBrand,
colorDark,
colorLight,
+ colorOverlay,
colorStatus,
elevation,
focus,
@@ -102,6 +103,7 @@ function block(selector: string, lines: Entry[], comment?: string): string {
const SHARED: Entry[] = [
...entriesFor(colorBrand, "colorBrand", "--r-color-brand"),
...entriesFor(colorStatus, "colorStatus", "--r-color"),
+ ...entriesFor(colorOverlay, "colorOverlay", "--r-color-overlay"),
...entriesFor(fontFamily, "fontFamily", "--r-font-family"),
...entriesFor(fontSize, "fontSize", "--r-font-size"),
...entriesFor(lineHeight, "lineHeight", "--r-line-height"),
diff --git a/frontend/src/v2/components/Gallery/GameCard/GameCard.vue b/frontend/src/v2/components/Gallery/GameCard/GameCard.vue
index 7765409f3..c1c77859b 100644
--- a/frontend/src/v2/components/Gallery/GameCard/GameCard.vue
+++ b/frontend/src/v2/components/Gallery/GameCard/GameCard.vue
@@ -295,9 +295,9 @@ const morphStyle = computed(() => {
min-width: 28px !important;
min-height: 28px !important;
border-radius: 50% !important;
- background: rgba(0, 0, 0, 0.78) !important;
- border: 1px solid rgba(255, 255, 255, 0.12) !important;
- color: #fff !important;
+ background: var(--r-color-overlay-scrim-strong) !important;
+ border: 1px solid var(--r-color-overlay-border) !important;
+ color: var(--r-color-overlay-fg) !important;
transition:
background 0.12s ease,
border-color 0.12s ease,
@@ -305,7 +305,7 @@ const morphStyle = computed(() => {
}
.r-gc__platform-icon:hover {
background: rgba(0, 0, 0, 0.9) !important;
- border-color: rgba(255, 255, 255, 0.25) !important;
+ border-color: var(--r-color-overlay-border-strong) !important;
transform: scale(1.08);
}
@@ -314,14 +314,14 @@ const morphStyle = computed(() => {
position: absolute;
bottom: 7px;
left: 7px;
- background: rgba(0, 0, 0, 0.78);
- border: 1px solid rgba(255, 255, 255, 0.12);
+ background: var(--r-color-overlay-scrim-strong);
+ border: 1px solid var(--r-color-overlay-border);
border-radius: var(--r-radius-sm);
padding: 2px 6px;
font-size: 9.5px;
font-weight: var(--r-font-weight-semibold);
letter-spacing: 0.03em;
- color: rgba(255, 255, 255, 0.85);
+ color: var(--r-color-overlay-fg-secondary);
opacity: 0;
transition: opacity 0.12s ease;
max-width: calc(100% - 14px);
diff --git a/frontend/src/v2/components/Gallery/GameCard/StatusBadge.vue b/frontend/src/v2/components/Gallery/GameCard/StatusBadge.vue
index 9722389a1..125855b84 100644
--- a/frontend/src/v2/components/Gallery/GameCard/StatusBadge.vue
+++ b/frontend/src/v2/components/Gallery/GameCard/StatusBadge.vue
@@ -171,9 +171,9 @@ function onActivatorClick(e: MouseEvent) {
cursor: pointer;
padding: 0;
font-family: inherit;
- background: rgba(0, 0, 0, 0.55);
- border: 1px dashed rgba(255, 255, 255, 0.45);
- color: rgba(255, 255, 255, 0.85);
+ background: var(--r-color-overlay-scrim-soft);
+ border: 1px dashed var(--r-color-overlay-fg-muted);
+ color: var(--r-color-overlay-fg-secondary);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
opacity: 0;
@@ -189,9 +189,9 @@ function onActivatorClick(e: MouseEvent) {
.r-gc-status:hover,
.r-gc-status--open {
opacity: 1;
- background: rgba(0, 0, 0, 0.75);
- border-color: rgba(255, 255, 255, 0.85);
- color: #fff;
+ background: var(--r-color-overlay-scrim-strong);
+ border-color: var(--r-color-overlay-fg-secondary);
+ color: var(--r-color-overlay-fg);
transform: scale(1.05);
}
@@ -200,8 +200,8 @@ function onActivatorClick(e: MouseEvent) {
badge / rating pattern. */
.r-gc-status--set {
border-style: solid;
- border-color: rgba(255, 255, 255, 0.18);
- background: rgba(0, 0, 0, 0.78);
+ border-color: var(--r-color-overlay-border-strong);
+ background: var(--r-color-overlay-scrim-strong);
opacity: 1;
}
diff --git a/frontend/src/v2/components/GameActions/GameActionBtn.vue b/frontend/src/v2/components/GameActions/GameActionBtn.vue
index d3d91b4b6..79634e1ae 100644
--- a/frontend/src/v2/components/GameActions/GameActionBtn.vue
+++ b/frontend/src/v2/components/GameActions/GameActionBtn.vue
@@ -186,11 +186,11 @@ function onClick(e: MouseEvent) {
/* Dark glass so the button still reads when sitting on top of a bright
or busy cover image in the GameCard overlay. In GameDetails the
backdrop is already a dark blurred cover so this tone lands neutral
- there too. Light translucent glass (5–10% white) was invisible on
- any cover with white or pale art. */
- border: 1px solid rgba(255, 255, 255, 0.14);
- background: rgba(0, 0, 0, 0.55);
- color: rgba(255, 255, 255, 0.95);
+ there too. Overlay tokens never theme-flip — they stay dark over
+ any cover artwork. */
+ border: 1px solid var(--r-color-overlay-border);
+ background: var(--r-color-overlay-scrim-soft);
+ color: var(--r-color-overlay-fg);
display: inline-flex;
align-items: center;
justify-content: center;
@@ -209,9 +209,9 @@ function onClick(e: MouseEvent) {
border-color var(--r-motion-fast) var(--r-motion-ease-out);
}
.r-v2-game-btn:hover {
- background: rgba(0, 0, 0, 0.72);
- border-color: rgba(255, 255, 255, 0.28);
- color: #fff;
+ background: var(--r-color-overlay-scrim-strong);
+ border-color: var(--r-color-overlay-border-strong);
+ color: var(--r-color-overlay-fg);
}
.r-v2-game-btn:active {
transform: scale(0.94);
diff --git a/frontend/src/v2/lib/menus/RMenuItem/RMenuItem.vue b/frontend/src/v2/lib/menus/RMenuItem/RMenuItem.vue
index 6eb7f3a9b..c137f8cfc 100644
--- a/frontend/src/v2/lib/menus/RMenuItem/RMenuItem.vue
+++ b/frontend/src/v2/lib/menus/RMenuItem/RMenuItem.vue
@@ -6,7 +6,7 @@
// cases: 15px leading-icon slot (left), label, 9px rounded hover bg.
//
// Variants:
-// * default — white-ish text, hover background rgba(255,255,255,0.09)
+// * default — fg-secondary text, hover background var(--r-color-surface)
// * active — filled accent (favourited style, brand "--r-color-fav")
// * danger — red text, red-tinted hover (destructive actions)
import { computed } from "vue";
@@ -165,7 +165,7 @@ const tag = computed(() => {
opacity: 0.85;
}
.r-menu-item--danger:hover:not(.r-menu-item--disabled) {
- background: rgba(255, 80, 80, 0.12);
+ background: color-mix(in srgb, var(--r-color-danger) 12%, transparent);
color: var(--r-color-danger);
opacity: 1;
}
diff --git a/frontend/src/v2/lib/menus/RMenuPanel/RMenuPanel.vue b/frontend/src/v2/lib/menus/RMenuPanel/RMenuPanel.vue
index 99b1e277d..19a6f7ea2 100644
--- a/frontend/src/v2/lib/menus/RMenuPanel/RMenuPanel.vue
+++ b/frontend/src/v2/lib/menus/RMenuPanel/RMenuPanel.vue
@@ -31,24 +31,17 @@ const resolvedWidth =
diff --git a/frontend/src/v2/lib/overlays/RDialog/RDialog.vue b/frontend/src/v2/lib/overlays/RDialog/RDialog.vue
index 83a59ac46..34648f338 100644
--- a/frontend/src/v2/lib/overlays/RDialog/RDialog.vue
+++ b/frontend/src/v2/lib/overlays/RDialog/RDialog.vue
@@ -173,8 +173,8 @@ void props;
.r-dialog__panel {
display: flex;
flex-direction: column;
- background: rgba(16, 12, 28, 0.97);
- border: 1px solid rgba(255, 255, 255, 0.1);
+ background: var(--r-color-panel);
+ border: 1px solid var(--r-color-panel-border);
border-radius: var(--r-radius-card);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
@@ -282,15 +282,9 @@ void props;
gap: 8px;
}
-/* Light theme — dialog root carries .r-v2-light only when the app shell
- does (teleported content inherits the body data-theme, not our wrapper).
- Matches RMenuPanel's light variant so the two surfaces read as one
- family across the app. */
-:global(.r-v2.r-v2-light) .r-dialog__panel {
- background: rgba(255, 255, 255, 0.97);
- border-color: rgba(17, 17, 23, 0.1);
- color: var(--r-color-fg);
-}
+/* The --r-color-panel pair flips automatically with the theme; the only
+ light-mode-specific tweak left is the foreground colour, which the
+ panel surface inherits from var(--r-color-fg) — also already paired. */