@import "tailwindcss";
@import "tailwind-animations";
@plugin "daisyui" {
themes: false;
}
/* ==========================================================================
THEME — Aetherkeep by candlelight
Dark keep interior: warm soot-stained stone and oak, amethyst arcana,
antique gold leaf. Low chroma warm neutrals, heavy borders, carved radii.
========================================================================== */
@plugin "daisyui/theme" {
name: "aetherkeep";
default: true;
prefersdark: true;
color-scheme: dark;
--color-base-100: oklch(24% 0.018 62);
--color-base-200: oklch(19% 0.016 55);
--color-base-300: oklch(14% 0.014 48);
--color-base-content: oklch(90% 0.028 84);
--color-primary: oklch(72% 0.13 305);
--color-primary-content: oklch(17% 0.03 300);
--color-secondary: oklch(70% 0.095 158);
--color-secondary-content: oklch(16% 0.03 158);
--color-accent: oklch(74% 0.14 80);
--color-accent-content: oklch(18% 0.05 62);
--color-neutral: oklch(26% 0.026 52);
--color-neutral-content: oklch(91% 0.028 86);
--color-info: oklch(70% 0.1 242);
--color-success: oklch(68% 0.12 145);
--color-info-content: oklch(16% 0.05 245);
--color-success-content: oklch(16% 0.05 145);
--color-warning: oklch(78% 0.13 78);
--color-warning-content: oklch(19% 0.05 62);
--color-error: oklch(64% 0.16 26);
--color-error-content: oklch(96% 0.015 30);
--radius-selector: 0.5rem;
--radius-field: 0.3rem;
--radius-box: 0.6rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 2px;
--depth: 1;
--noise: 1;
}
@theme {
--font-gothic: "Grenze Gotisch", "Uncial Antiqua", serif;
--font-display: "Uncial Antiqua", "Cormorant SC", serif;
--font-heading: "Cormorant SC", serif;
}
/* ==========================================================================
ORNAMENT TOKENS
Silhouette clip paths and inline-SVG motifs shared by the ornament layer.
Data-URIs use a fixed antique gold (%23c9a35a); intensity is tuned per use
with opacity so the motifs sit quietly on dark surfaces.
========================================================================== */
:root {
/* Heraldic silhouettes */
--shield-clip: polygon(
4% 0,
96% 0,
96% 42%,
92% 58%,
82% 74%,
66% 88%,
50% 100%,
34% 88%,
18% 74%,
8% 58%,
4% 42%
);
--banner-clip: polygon(0 0, 100% 0, 100% 100%, 50% 74%, 0 100%);
--pennant-clip: polygon(0 0, 100% 0, 82% 50%, 100% 100%, 0 100%);
/* Gilding and forge palette */
--gold-line: color-mix(in oklab, var(--color-accent) 58%, transparent);
--gold-bright: color-mix(in oklab, var(--color-accent) 88%, white);
--iron-dark: color-mix(in oklab, var(--color-neutral) 42%, black);
--iron-light: color-mix(in oklab, var(--color-neutral) 88%, white);
--wax-red: oklch(42% 0.15 24);
/* Staggered ashlar courses: mortar joints on a 160x88 tile. */
--tex-ashlar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='88'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='0.5' stroke-width='2'%3E%3Cpath d='M0 44h160M0 88h160'/%3E%3Cpath d='M80 0v44M0 44v44M120 44v44'/%3E%3C/g%3E%3Cg fill='none' stroke='%23fff' stroke-opacity='0.06'%3E%3Cpath d='M0 45.5h160M0 1.5h160'/%3E%3C/g%3E%3C/svg%3E");
/* Aged fibre noise for the .grain overlay. */
--tex-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
/* Illuminated margin vine with interlace knots, 48x120 seamless tile. */
--orn-vine: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='120'%3E%3Cg fill='none' stroke='%23c9a35a' stroke-width='1.4' stroke-linecap='round'%3E%3Cpath d='M24 0c-13 15 13 30 0 45s13 30 0 45 13 22 0 30'/%3E%3Cpath d='M24 14c7-5 14-2 13 5-6 5-12 1-13-5z'/%3E%3Cpath d='M24 62c-7-5-14-2-13 5 6 5 12 1 13-5z'/%3E%3Cpath d='M24 100c7-4 13-1 12 5-5 4-11 1-12-5z'/%3E%3C/g%3E%3Cg fill='%23c9a35a' fill-opacity='0.8'%3E%3Ccircle cx='24' cy='45' r='2.6'/%3E%3Ccircle cx='24' cy='90' r='2.2'/%3E%3C/g%3E%3C/svg%3E");
/* Vine terminal that caps a hairline rule. */
--orn-terminal: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='14'%3E%3Cg fill='none' stroke='%23c9a35a' stroke-width='1.3' stroke-linecap='round'%3E%3Cpath d='M0 7h11'/%3E%3Cpath d='M11 7c3 0 4-4 8-4 4 0 5 5 1 6-3 .8-5-1.6-2-3'/%3E%3C/g%3E%3C/svg%3E");
/* Corner brackets, one path mirrored into each of the four corners. */
--orn-corner-tl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg fill='none' stroke='%23c9a35a' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M1 20V7c0-3.3 2.7-6 6-6h13'/%3E%3Cpath d='M7 1c0 3.3-2.7 6-6 6'/%3E%3C/g%3E%3Ccircle cx='4.6' cy='4.6' r='1.5' fill='%23c9a35a'/%3E%3C/svg%3E");
--orn-corner-tr: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg transform='translate(24,0) scale(-1,1)'%3E%3Cg fill='none' stroke='%23c9a35a' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M1 20V7c0-3.3 2.7-6 6-6h13'/%3E%3Cpath d='M7 1c0 3.3-2.7 6-6 6'/%3E%3C/g%3E%3Ccircle cx='4.6' cy='4.6' r='1.5' fill='%23c9a35a'/%3E%3C/g%3E%3C/svg%3E");
--orn-corner-bl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg transform='translate(0,24) scale(1,-1)'%3E%3Cg fill='none' stroke='%23c9a35a' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M1 20V7c0-3.3 2.7-6 6-6h13'/%3E%3Cpath d='M7 1c0 3.3-2.7 6-6 6'/%3E%3C/g%3E%3Ccircle cx='4.6' cy='4.6' r='1.5' fill='%23c9a35a'/%3E%3C/g%3E%3C/svg%3E");
--orn-corner-br: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg transform='translate(24,24) scale(-1,-1)'%3E%3Cg fill='none' stroke='%23c9a35a' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M1 20V7c0-3.3 2.7-6 6-6h13'/%3E%3Cpath d='M7 1c0 3.3-2.7 6-6 6'/%3E%3C/g%3E%3Ccircle cx='4.6' cy='4.6' r='1.5' fill='%23c9a35a'/%3E%3C/g%3E%3C/svg%3E");
}
body {
min-height: 100vh;
font-family: "Spectral", serif;
background-color: var(--color-base-300);
background-image:
radial-gradient(
circle at 50% -8%,
color-mix(in oklab, var(--color-primary) 15%, transparent) 0,
transparent 32rem
),
radial-gradient(
circle at 8% 92%,
color-mix(in oklab, var(--color-accent) 10%, transparent) 0,
transparent 26rem
),
repeating-linear-gradient(
135deg,
color-mix(in oklab, var(--color-accent) 3%, transparent) 0 1px,
transparent 1px 3rem
);
background-attachment: fixed;
}
.font-display,
.font-heading,
.font-gothic {
text-wrap: balance;
}
/* ==========================================================================
ORNAMENT · TEXTURES
Pure CSS / inline-SVG surfaces. No image assets.
========================================================================== */
/* Dark ashlar masonry with mortar joints and a top-down candlelight falloff. */
.stone-wall {
position: relative;
background-color: color-mix(
in oklab,
var(--color-neutral) 34%,
var(--color-base-300)
);
background-image:
linear-gradient(
180deg,
color-mix(in oklab, var(--color-accent) 11%, transparent) 0,
transparent 34%,
rgb(0 0 0 / 34%) 100%
),
repeating-linear-gradient(
92deg,
rgb(255 255 255 / 3%) 0 0.6rem,
transparent 0.6rem 1.7rem
),
var(--tex-ashlar);
background-size: auto, auto, 10rem 5.5rem;
box-shadow: inset 0 0 3rem rgb(0 0 0 / 45%);
}
/* Dark-tanned vellum for reading surfaces: warm tone, fibre grain, worn edges. */
.vellum {
position: relative;
background-color: color-mix(
in oklab,
var(--color-accent) 8%,
var(--color-base-100)
);
background-image:
radial-gradient(
ellipse at 22% 14%,
rgb(255 238 196 / 7%),
transparent 55%
),
radial-gradient(ellipse at 80% 86%, rgb(255 220 160 / 5%), transparent 52%),
repeating-linear-gradient(
102deg,
rgb(255 240 204 / 4%) 0 1px,
transparent 1px 4px
),
repeating-linear-gradient(
8deg,
rgb(0 0 0 / 5%) 0 1px,
transparent 1px 7px
),
radial-gradient(
ellipse 132% 124% at 50% 48%,
transparent 50%,
rgb(0 0 0 / 36%) 100%
);
box-shadow: inset 0 0 2.5rem rgb(0 0 0 / 32%);
}
/* Woven diagonal weave for large empty backdrops. */
.tapestry {
background-color: var(--color-base-300);
background-image:
radial-gradient(
ellipse at 50% 0,
color-mix(in oklab, var(--color-primary) 15%, transparent),
transparent 62%
),
repeating-linear-gradient(
45deg,
rgb(255 255 255 / 3%) 0 2px,
transparent 2px 5px
),
repeating-linear-gradient(
-45deg,
rgb(0 0 0 / 14%) 0 2px,
transparent 2px 5px
);
}
/* Aged fibre noise overlay. Kept at or below 6% so text stays crisp. */
.grain {
position: relative;
}
.grain::after {
content: "";
position: absolute;
z-index: 0;
inset: 0;
background-image: var(--tex-noise);
background-size: 180px 180px;
mix-blend-mode: overlay;
opacity: 0.05;
pointer-events: none;
}
/* ==========================================================================
ORNAMENT · GOTHIC ARCHITECTURE
Note: .arch-panel owns ::before (keystone) and .corner-fleurons owns
::before/::after — apply .corner-fleurons to the inner body element.
========================================================================== */
/* Panel crowned by a pointed/ogee arch with a carved keystone at the apex. */
.arch-panel {
position: relative;
border: var(--border) solid var(--gold-line);
border-radius: 46% 46% var(--radius-box) var(--radius-box) / 22% 22% 0 0;
padding-top: 1.75rem;
box-shadow:
0 1rem 2.25rem rgb(0 0 0 / 45%),
inset 0 0 0 1px rgb(255 255 255 / 6%),
inset 0 1.5rem 2rem rgb(0 0 0 / 22%);
}
.arch-panel::before {
content: "";
position: absolute;
z-index: 2;
top: -0.6rem;
left: 50%;
width: 1.35rem;
height: 1.35rem;
border: 1px solid var(--gold-line);
background: linear-gradient(
145deg,
var(--gold-bright),
color-mix(in oklab, var(--color-accent) 40%, var(--color-neutral))
);
box-shadow: 0 0 0.8rem
color-mix(in oklab, var(--color-accent) 45%, transparent);
pointer-events: none;
transform: translateX(-50%) rotate(45deg);
}
/* Flat-topped carved frame. Used where an arch curve would clip a content
grid (deck builder columns) but the panel still needs gold-rimmed framing. */
.frame-panel {
position: relative;
border: var(--border) solid var(--gold-line);
border-radius: var(--radius-box);
box-shadow:
0 1rem 2.25rem rgb(0 0 0 / 45%),
inset 0 0 0 1px rgb(255 255 255 / 6%),
inset 0 0 2rem rgb(0 0 0 / 25%);
}
/* Selected heraldic plaque: gold-lit rim and a warm bloom. */
.plaque-lit {
border-color: color-mix(in oklab, var(--color-accent) 85%, transparent);
box-shadow:
0 1rem 2.25rem rgb(0 0 0 / 45%),
inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 48%, transparent),
inset 0 1.5rem 2rem rgb(0 0 0 / 18%),
0 0 2rem color-mix(in oklab, var(--color-accent) 32%, transparent);
}
/* Arched inner window for card art and portraits. */
.arch-frame {
position: relative;
overflow: hidden;
border-radius: 46% 46% 0.25rem 0.25rem / 30% 30% 0 0;
box-shadow:
inset 0 0 0 1px var(--gold-line),
inset 0 0 1.5rem rgb(0 0 0 / 48%);
}
/* Vertical carved-column divider. */
.pillar-rule {
width: 0.7rem;
min-height: 1.5rem;
flex: none;
align-self: stretch;
border-radius: 0.35rem;
background:
linear-gradient(
90deg,
transparent 0 8%,
rgb(0 0 0 / 45%) 14%,
color-mix(
in oklab,
var(--color-accent) 20%,
var(--color-neutral)
) 34% 66%,
rgb(0 0 0 / 45%) 86%,
transparent 92%
),
repeating-linear-gradient(
180deg,
rgb(255 255 255 / 6%) 0 1px,
transparent 1px 6px
);
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 35%);
}
/* ==========================================================================
ORNAMENT · HERALDRY
========================================================================== */
/* Shield silhouette: flat top, curved point. The gold rim is drawn with
stacked zero-blur drop-shadows so it hugs the clipped silhouette. */
.shield,
.mystic-class-icon {
--shield-glow: var(--color-primary);
--shield-lift: 42%;
box-sizing: border-box;
border: 0;
border-radius: 0;
clip-path: var(--shield-clip);
filter: drop-shadow(0 1px 0 var(--gold-line)) drop-shadow(
0 -1px 0 var(--gold-line)
) drop-shadow(1px 0 0 var(--gold-line)) drop-shadow(-1px 0 0 var(--gold-line))
drop-shadow(
0 0.35rem 0.9rem color-mix(
in oklab,
var(--shield-glow) var(--shield-lift),
transparent
)
);
}
.mystic-class-icon {
padding: 8% 16% 32%;
object-fit: contain;
object-position: center;
box-shadow:
inset 0 1rem 0 -0.75rem rgb(255 255 255 / 18%),
inset 0 -1.4rem 1.2rem -0.9rem rgb(0 0 0 / 50%);
transition: filter 220ms ease, transform 220ms ease;
}
/* Square mount giving the shield art a correct percentage basis: percentage
padding resolves against the CONTAINING BLOCK's width, not the icon's own
box, so the art must be absolutely positioned inside a shield-sized mount. */
.shield-mount {
position: relative;
display: block;
flex: none;
aspect-ratio: 1;
}
.shield-mount > .mystic-class-icon {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.mystic-class-maiden {
--shield-glow: var(--color-info);
background: linear-gradient(
145deg,
color-mix(in oklab, var(--color-info) 72%, var(--color-neutral)),
color-mix(in oklab, var(--color-info) 35%, var(--color-neutral))
);
}
.mystic-class-witch {
--shield-glow: var(--color-primary);
background: linear-gradient(
145deg,
color-mix(in oklab, var(--color-primary) 82%, var(--color-neutral)),
color-mix(in oklab, var(--color-primary) 48%, var(--color-neutral))
);
}
.mystic-class-assassin {
--shield-glow: var(--color-error);
background: linear-gradient(
145deg,
color-mix(in oklab, var(--color-error) 68%, var(--color-neutral)),
color-mix(in oklab, var(--color-error) 28%, var(--color-neutral))
);
}
button:hover .mystic-class-icon,
button:focus-visible .mystic-class-icon,
.combat-hero:hover .mystic-class-icon {
--shield-lift: 78%;
transform: translateY(-0.12rem) scale(1.04);
}
/* Hanging cloth banner: swallowtail hem, two rivet rings on the hoist bar. */
.banner,
.city-building span {
position: relative;
color: var(--color-neutral-content);
background:
linear-gradient(
180deg,
rgb(255 255 255 / 10%),
transparent 32%,
rgb(0 0 0 / 32%)
),
repeating-linear-gradient(
90deg,
rgb(0 0 0 / 8%) 0 2px,
transparent 2px 6px
),
linear-gradient(
180deg,
color-mix(in oklab, var(--color-neutral) 94%, black),
color-mix(in oklab, var(--color-neutral) 66%, black)
);
font-family: var(--font-heading);
font-weight: 700;
letter-spacing: 0.12em;
text-align: center;
text-transform: uppercase;
clip-path: var(--banner-clip);
filter: drop-shadow(0 0.3rem 0.45rem rgb(0 0 0 / 45%));
}
.banner {
display: block;
padding: 0.6rem 0.6rem 1.1rem;
font-size: 0.8rem;
line-height: 1.1;
}
.banner::before,
.city-building span::before {
content: "";
position: absolute;
top: 0.2rem;
right: 0.45rem;
left: 0.45rem;
height: 0.32rem;
background-image:
radial-gradient(
circle at 0.16rem 50%,
var(--gold-bright) 0 0.13rem,
transparent 0.14rem
),
radial-gradient(
circle at calc(100% - 0.16rem) 50%,
var(--gold-bright) 0 0.13rem,
transparent 0.14rem
);
opacity: 0.85;
pointer-events: none;
}
/* Heraldic glyph medallion for carved map tablets. */
.node-medallion {
display: grid;
width: 4.5rem;
height: 5rem;
place-items: center;
color: var(--gold-bright);
background: linear-gradient(
160deg,
color-mix(in oklab, var(--color-primary) 72%, black),
color-mix(in oklab, var(--color-primary) 32%, black)
);
font-size: 2.25rem;
line-height: 1;
text-shadow: 0 0 0.8rem
color-mix(in oklab, var(--color-accent) 60%, transparent);
}
/* Small triangular pennant for badges and labels. */
.pennant {
position: relative;
padding-inline: 0.7rem 1.6rem;
border: 0;
border-radius: 0;
clip-path: var(--pennant-clip);
box-shadow: inset 0.18rem 0 0
color-mix(in oklab, var(--color-accent) 70%, transparent);
}
/* ==========================================================================
ORNAMENT · MANUSCRIPT ILLUMINATION
========================================================================== */
/* Illuminated initial: a gilded gothic capital, lit from within.
Deliberately paints no background box. An inline box's background is sized
from the font's own ascent/descent, not from line-height or padding, and
Grenze Gotisch reports a content area ~1.47x its font-size — so any filled
ground on ::first-letter renders as a slab that rides up over the eyebrow
text above the heading and cannot be trimmed back down. The gilding is
carried by colour and a layered aura instead, which stays inside the line
box at every heading size. */
.illuminated::first-letter {
color: var(--gold-bright);
font-family: var(--font-gothic);
font-size: 1.14em;
letter-spacing: 0.02em;
text-shadow:
0 0 0.15rem color-mix(in oklab, var(--color-accent) 70%, transparent),
0 0 0.7rem color-mix(in oklab, var(--color-accent) 45%, transparent),
0 0 1.4rem color-mix(in oklab, var(--color-primary) 40%, transparent);
}
/* Hairline rule with vine terminals; .fleuron-rule gilds the centred glyph. */
.mystic-rule,
.fleuron-rule {
display: flex;
align-items: center;
gap: 0.75rem;
}
.mystic-rule::before,
.mystic-rule::after,
.fleuron-rule::before,
.fleuron-rule::after {
content: "";
height: 0.9rem;
flex: 1;
background-image:
linear-gradient(
90deg,
transparent,
color-mix(in oklab, var(--color-accent) 65%, transparent) 78%
),
var(--orn-terminal);
background-repeat: no-repeat, no-repeat;
background-position: left center, right center;
background-size:
calc(100% - 1.75rem) 1px,
1.75rem 0.9rem;
}
.mystic-rule::after,
.fleuron-rule::after {
transform: scaleX(-1);
}
.fleuron-rule > * {
flex: none;
color: var(--color-accent);
text-shadow: 0 0 0.65rem
color-mix(in oklab, var(--color-accent) 60%, transparent);
}
/* Illuminated vine-and-knot margins for full screens. */
.margin-vines::before,
.margin-vines::after,
.room-screen::before,
.room-screen::after {
content: "";
position: fixed;
z-index: 0;
top: 4rem;
bottom: 0;
width: clamp(1.5rem, 6vw, 3.5rem);
background-image: var(--orn-vine);
background-repeat: repeat-y;
background-position: center top;
background-size: 100% auto;
opacity: 0.42;
pointer-events: none;
}
.margin-vines::before,
.room-screen::before {
left: 0;
}
.margin-vines::after,
.room-screen::after {
right: 0;
}
/* Gold corner brackets. ::before carries the top pair, ::after the bottom. */
.corner-fleurons {
--fleuron-size: 1.5rem;
--fleuron-inset: 0.4rem;
position: relative;
}
.corner-fleurons::before,
.corner-fleurons::after {
content: "";
position: absolute;
z-index: 1;
right: var(--fleuron-inset);
left: var(--fleuron-inset);
height: var(--fleuron-size);
background-repeat: no-repeat, no-repeat;
background-size: var(--fleuron-size) var(--fleuron-size);
opacity: 0.7;
pointer-events: none;
}
.corner-fleurons::before {
top: var(--fleuron-inset);
background-image:
var(--orn-corner-tl),
var(--orn-corner-tr);
background-position: left top, right top;
}
.corner-fleurons::after {
bottom: var(--fleuron-inset);
background-image:
var(--orn-corner-bl),
var(--orn-corner-br);
background-position: left bottom, right bottom;
}
/* ==========================================================================
ORNAMENT · CRAFT DETAILS
========================================================================== */
/* Crimson wax seal with an embossed sigil ring and an irregular poured edge. */
.wax-seal {
--seal-size: 4.5rem;
position: relative;
display: grid;
width: var(--seal-size);
height: var(--seal-size);
place-items: center;
gap: 0.2rem;
border: 0;
border-radius: 46% 54% 52% 48% / 50% 46% 54% 50%;
color: color-mix(in oklab, var(--color-accent) 62%, white);
background:
radial-gradient(circle at 34% 26%, rgb(255 255 255 / 24%), transparent 44%),
radial-gradient(circle at 64% 76%, rgb(0 0 0 / 36%), transparent 54%),
conic-gradient(
from 20deg,
var(--wax-red),
color-mix(in oklab, var(--wax-red) 70%, black) 22%,
var(--wax-red) 46%,
color-mix(in oklab, var(--wax-red) 78%, black) 70%,
var(--wax-red)
);
box-shadow:
0 0.4rem 0.9rem rgb(0 0 0 / 50%),
inset 0 -0.3rem 0.6rem rgb(0 0 0 / 32%),
inset 0 0.25rem 0.5rem rgb(255 255 255 / 10%);
text-shadow: 0 1px 0 rgb(0 0 0 / 45%);
}
.wax-seal-sm {
--seal-size: 2.25rem;
font-size: 0.9rem;
}
/* Anchors a seal onto the corner of a control. .wax-seal sets position:
relative unlayered, which would beat a Tailwind `absolute` utility. */
.wax-seal-affix {
position: absolute;
z-index: 2;
right: calc(var(--seal-size) / -2);
bottom: calc(var(--seal-size) / -2.6);
pointer-events: none;
transform: rotate(-8deg);
}
.wax-seal::before {
content: "";
position: absolute;
inset: 15%;
border: 1px solid currentColor;
border-radius: 50%;
opacity: 0.45;
pointer-events: none;
}
/* Gilded coin used for currency badges. */
.anima-coin {
border: 0;
border-radius: 9999px;
color: var(--color-accent-content);
background:
radial-gradient(circle at 34% 28%, rgb(255 255 255 / 42%), transparent 48%),
linear-gradient(
150deg,
var(--gold-bright),
color-mix(in oklab, var(--color-accent) 62%, var(--color-neutral))
);
box-shadow:
0 0.2rem 0.5rem rgb(0 0 0 / 45%),
inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 40%, black);
}
/* Forged iron strap: hammered sheen, darkened lower lip, deep seat shadow. */
.iron-band {
position: relative;
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 8%),
transparent 30%,
rgb(0 0 0 / 26%)
),
repeating-linear-gradient(
90deg,
rgb(255 255 255 / 3%) 0 2px,
transparent 2px 7px
),
linear-gradient(
180deg,
color-mix(in oklab, var(--color-neutral) 82%, black),
var(--iron-dark)
);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 10%),
inset 0 -2px 0 rgb(0 0 0 / 38%),
0 0.35rem 0.9rem rgb(0 0 0 / 38%);
}
/* Rivet rows along the top and bottom edges of a forged element. */
.rivets {
position: relative;
}
.rivets::before,
.rivets::after {
content: "";
position: absolute;
z-index: 1;
right: 0.6rem;
left: 0.6rem;
height: 0.34rem;
background-image: radial-gradient(
circle at 50% 50%,
var(--iron-light) 0 0.06rem,
color-mix(in oklab, var(--color-neutral) 30%, black) 0.07rem 0.16rem,
transparent 0.17rem
);
background-repeat: repeat-x;
background-position: left center;
background-size: 2.75rem 0.34rem;
opacity: 0.75;
pointer-events: none;
}
.rivets::before {
top: 0.28rem;
}
.rivets::after {
bottom: 0.28rem;
}
/* Warm pool of candlelight. Flicker is disabled under reduced motion. */
.candle-glow {
--candle-tint: color-mix(
in oklab,
var(--color-accent) 65%,
var(--color-warning)
);
position: relative;
}
.candle-glow::before {
content: "";
position: absolute;
z-index: 0;
inset: -60% -70%;
border-radius: 50%;
background: radial-gradient(
closest-side,
color-mix(in oklab, var(--candle-tint) 26%, transparent),
transparent 78%
);
pointer-events: none;
animation: candle-flicker 6.5s ease-in-out infinite;
}
@keyframes candle-flicker {
0%,
100% {
opacity: 0.72;
transform: scale(1);
}
28% {
opacity: 0.95;
transform: scale(1.035);
}
54% {
opacity: 0.66;
transform: scale(0.985);
}
76% {
opacity: 0.88;
transform: scale(1.02);
}
}
/* ==========================================================================
COMPONENTS · daisyUI surfaces restyled as stone, oak and iron
========================================================================== */
.navbar {
position: relative;
background-color: color-mix(
in oklab,
var(--color-neutral) 40%,
var(--color-base-300)
);
background-image:
linear-gradient(
180deg,
color-mix(in oklab, var(--color-accent) 8%, transparent),
transparent 45%,
rgb(0 0 0 / 32%)
),
repeating-linear-gradient(
90deg,
rgb(255 255 255 / 3%) 0 0.5rem,
transparent 0.5rem 1.6rem
),
var(--tex-ashlar);
background-size: auto, auto, 10rem 5.5rem;
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 8%),
inset 0 -3px 0 var(--iron-dark),
0 0.5rem 1.6rem rgb(0 0 0 / 45%);
}
.card,
.modal-box,
.stats {
background-image: linear-gradient(
145deg,
rgb(255 255 255 / 4%),
color-mix(in oklab, var(--color-accent) 5%, transparent)
);
box-shadow:
0 1rem 2.5rem rgb(0 0 0 / 42%),
inset 0 0 0 1px rgb(255 255 255 / 7%);
}
.card {
border-radius: var(--radius-box);
}
.card:hover {
box-shadow:
0 1.25rem 2.75rem rgb(0 0 0 / 48%),
0 0 1.5rem color-mix(in oklab, var(--color-accent) 16%, transparent);
}
/* Buttons read as an oak plank bound in iron straps. The vertical 1px grain
that used to sit here read as a candy stripe on saturated fills, so the
wood is now expressed with soft horizontal shading plus end straps only. */
.btn {
position: relative;
overflow: hidden;
border-bottom-width: 3px;
font-family: var(--font-heading);
font-weight: 700;
letter-spacing: 0.08em;
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 7%),
transparent 38%,
rgb(0 0 0 / 26%)
),
linear-gradient(
90deg,
rgb(0 0 0 / 26%) 0 0.3rem,
transparent 0.3rem calc(100% - 0.3rem),
rgb(0 0 0 / 26%) calc(100% - 0.3rem)
);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 10%),
inset 0 -0.16rem 0 rgb(0 0 0 / 24%);
transform: translateY(0);
transition:
transform 90ms ease,
border-bottom-width 90ms ease,
box-shadow 180ms ease,
filter 180ms ease;
}
/* Solid daisyUI variants become enamelled jewel plates in a gold bezel.
Scoped away from outline/ghost/soft/link, which set their own --btn-bg. */
:is(
.btn-primary,
.btn-secondary,
.btn-accent,
.btn-info,
.btn-success,
.btn-warning,
.btn-error,
.btn-neutral
):not(:is(.btn-outline, .btn-ghost, .btn-soft, .btn-link, .btn-dash)) {
--btn-bg: color-mix(in oklab, var(--btn-color) 40%, oklch(17% 0.02 52));
--btn-border: color-mix(in oklab, var(--color-accent) 55%, black);
--btn-fg: color-mix(
in oklab,
var(--color-base-content) 88%,
var(--btn-color)
);
box-shadow:
inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 30%, transparent),
inset 0 1px 0 rgb(255 255 255 / 12%),
inset 0 -0.18rem 0.5rem rgb(0 0 0 / 40%),
0 0.35rem 0.9rem rgb(0 0 0 / 40%);
}
:is(
.btn-primary,
.btn-secondary,
.btn-accent,
.btn-info,
.btn-success,
.btn-warning,
.btn-error,
.btn-neutral
):not(:is(.btn-outline, .btn-ghost, .btn-soft, .btn-link, .btn-dash)):disabled {
box-shadow: none;
}
/* Warm candlelight glint replaces the old white sheen sweep. */
.btn::after {
content: "";
position: absolute;
inset: -120% 45%;
width: 22%;
background: linear-gradient(
90deg,
transparent,
color-mix(in oklab, var(--color-accent) 34%, transparent),
transparent
);
pointer-events: none;
transform: rotate(24deg) translateX(-320%);
transition: transform 480ms ease;
}
.btn:hover:not(:disabled)::after,
.btn:focus-visible:not(:disabled)::after {
transform: rotate(24deg) translateX(320%);
}
.btn:hover:not(:disabled),
.btn:focus-visible:not(:disabled) {
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 14%),
0 0.45rem 0.9rem rgb(0 0 0 / 42%),
0 0 1rem color-mix(in oklab, var(--color-accent) 22%, transparent);
filter: saturate(1.06) brightness(1.04);
}
.btn:active:not(:disabled) {
border-bottom-width: 1px;
box-shadow: inset 0 0.2rem 0.45rem rgb(0 0 0 / 30%);
transform: translateY(2px) scale(0.985);
}
.input,
.textarea,
.select {
background-color: color-mix(in oklab, var(--color-base-300) 72%, transparent);
background-image: linear-gradient(
180deg,
rgb(0 0 0 / 22%),
transparent 38%
);
box-shadow: inset 0 2px 0.4rem rgb(0 0 0 / 32%);
}
.fieldset-legend {
font-family: var(--font-heading);
letter-spacing: 0.14em;
text-transform: uppercase;
}
.modal-backdrop {
background: color-mix(in oklab, var(--color-base-300) 55%, transparent);
backdrop-filter: blur(5px);
}
/* ==========================================================================
SCREENS · transitions
========================================================================== */
/* Every screen is rendered inside this stage so a departing screen can dim and
sink away before the next one rises, instead of cutting like a glitch. */
.screen-stage {
transform-origin: 50% 42%;
}
.screen-stage.is-leaving {
pointer-events: none;
}
/* The battle spoils arrive as an illuminated leaf laid over the battlefield. */
.reward-scroll {
border: var(--border) solid var(--gold-line);
box-shadow:
0 1.25rem 3rem rgb(0 0 0 / 55%),
0 0 2.5rem color-mix(in oklab, var(--color-accent) 22%, transparent),
inset 0 0 0 1px rgb(255 255 255 / 8%);
}
/* The fight stays visible behind the spoils, so the veil is kept thin. */
.reward-modal.modal-open {
background-color: color-mix(in oklab, var(--color-base-300) 45%, transparent);
}
.reward-modal .modal-backdrop {
background: transparent;
backdrop-filter: blur(2px);
}
/* ==========================================================================
SCREENS · room interiors
========================================================================== */
.room-screen {
position: relative;
animation: room-aura-shift 10s ease-in-out infinite alternate;
background:
linear-gradient(
90deg,
color-mix(in oklab, var(--room-color) 10%, transparent),
transparent 20% 80%,
color-mix(in oklab, var(--room-color) 10%, transparent)
),
radial-gradient(
ellipse at 50% 0,
color-mix(in oklab, var(--room-light) 26%, transparent),
transparent 58%
),
var(--tex-ashlar),
var(--color-base-200);
background-size: 100% 100%, 130% 130%, 10rem 5.5rem, auto;
}
.room-screen > div {
position: relative;
z-index: 1;
}
.room-keep {
--room-color: var(--color-primary);
--room-light: var(--color-accent);
}
.room-archive {
--room-color: var(--color-info);
--room-light: var(--color-primary);
}
.room-atelier {
--room-color: var(--color-accent);
--room-light: var(--color-warning);
}
.room-sanctum {
--room-color: var(--color-secondary);
--room-light: var(--color-info);
}
/* ==========================================================================
SCREENS · arcana pack
========================================================================== */
/* Dark tooled-leather pack bound in antique gold foil. */
.arcana-pack {
position: relative;
display: flex;
width: min(14rem, 72vw);
min-height: 20rem;
overflow: hidden;
border: var(--border) solid
color-mix(in oklab, var(--color-accent) 62%, black);
border-radius: 0.4rem;
padding: 0;
color: color-mix(
in oklab,
var(--color-base-content) 90%,
var(--color-accent)
);
background:
radial-gradient(
ellipse at 50% 12%,
color-mix(in oklab, var(--color-accent) 12%, transparent),
transparent 58%
),
repeating-linear-gradient(
118deg,
rgb(255 255 255 / 2%) 0 1px,
transparent 1px 6px
),
repeating-linear-gradient(
62deg,
rgb(0 0 0 / 10%) 0 1px,
transparent 1px 7px
),
linear-gradient(
150deg,
color-mix(in oklab, var(--color-neutral) 62%, oklch(16% 0.03 48)),
oklch(14% 0.025 44) 52%,
color-mix(in oklab, var(--color-neutral) 48%, oklch(13% 0.025 42))
);
box-shadow:
0 1.25rem 2rem rgb(0 0 0 / 55%),
0 0 1.5rem color-mix(in oklab, var(--color-accent) 20%, transparent),
inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 22%, transparent),
inset 0 0 2rem rgb(0 0 0 / 45%);
cursor: pointer;
transform: rotate(-1deg);
transition: transform 180ms ease, box-shadow 180ms ease;
}
.arcana-pack::before,
.arcana-pack::after {
content: "";
position: absolute;
z-index: 2;
width: 2.25rem;
height: 2.25rem;
border: 1px solid color-mix(in oklab, var(--color-accent) 62%, transparent);
pointer-events: none;
transform: rotate(45deg);
}
.arcana-pack::before {
top: 3.4rem;
left: -1.35rem;
}
.arcana-pack::after {
right: -1.35rem;
bottom: 3.4rem;
}
.arcana-pack:hover:not(:disabled),
.arcana-pack:focus-visible {
outline: none;
box-shadow:
0 1.7rem 2.4rem rgb(0 0 0 / 50%),
0 0 2.2rem color-mix(in oklab, var(--color-accent) 50%, transparent),
inset 0 0 1.5rem rgb(255 255 255 / 18%);
transform: translateY(-0.45rem) rotate(1deg) scale(1.025);
}
.arcana-pack:active:not(:disabled) {
transform: translateY(-0.1rem) rotate(0deg) scale(0.98);
}
.arcana-pack:disabled {
cursor: not-allowed;
filter: saturate(0.55);
opacity: 0.58;
}
/* Stitched leather seal strips that bind the pack top and bottom. */
/* Stitched leather binding strips that bind the pack top and bottom. */
.arcana-pack-seal {
position: relative;
z-index: 3;
display: block;
height: 1.05rem;
flex: none;
border-bottom: 1px solid
color-mix(in oklab, var(--color-accent) 55%, transparent);
background: repeating-linear-gradient(
90deg,
color-mix(in oklab, var(--color-accent) 50%, black) 0 0.32rem,
oklch(15% 0.02 46) 0.32rem 0.58rem
);
box-shadow: 0 0.2rem 0.45rem rgb(0 0 0 / 45%);
}
.arcana-pack-seal:last-child {
border-top: 1px solid
color-mix(in oklab, var(--color-accent) 65%, transparent);
border-bottom: 0;
box-shadow: 0 -0.2rem 0.45rem rgb(0 0 0 / 32%);
}
.arcana-pack-face {
position: relative;
z-index: 1;
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: space-between;
margin: 0.5rem;
border: 3px double color-mix(in oklab, var(--color-accent) 76%, transparent);
border-radius: 46% 46% 0.5rem 0.5rem / 24% 24% 0 0;
padding: 1.1rem 0.65rem 0.8rem;
text-align: center;
background: radial-gradient(
circle at 50% 42%,
color-mix(in oklab, var(--color-accent) 20%, transparent),
transparent 45%
);
}
.arcana-pack-maker {
font-size: 0.6rem;
letter-spacing: 0.2em;
text-transform: uppercase;
opacity: 0.72;
}
/* Kept as the sizing hook; the wax material comes from .wax-seal. */
.arcana-pack-sigil {
--seal-size: 7rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
}
.arcana-pack-sigil strong {
font-size: 3rem;
line-height: 1;
text-shadow: 0 0 0.75rem
color-mix(in oklab, var(--color-accent) 70%, transparent);
}
.arcana-pack-sigil span {
font-size: 1rem;
}
.arcana-pack h4 {
font-size: 1.65rem;
line-height: 1;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.arcana-pack h4 + p {
margin-top: 0.3rem;
font-size: 0.7rem;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.arcana-pack-rarity {
font-size: 0.62rem;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.72;
}
.arcana-pack-price {
min-width: 7.5rem;
border-color: color-mix(
in oklab,
var(--color-accent-content) 24%,
transparent
);
font-family: var(--font-heading);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.arcana-pack small {
font-size: 0.62rem;
letter-spacing: 0.18em;
text-transform: uppercase;
opacity: 0.66;
}
/* ==========================================================================
SCREENS · progression board (enamelled heraldic escutcheons)
========================================================================== */
.passive-board {
max-width: 64rem;
margin-inline: auto;
}
.passive-level {
position: relative;
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.25rem;
padding: 0.85rem 1.1rem;
border: var(--border) solid var(--gold-line);
border-radius: var(--radius-box);
background:
linear-gradient(
180deg,
rgb(255 255 255 / 5%),
transparent 40%,
rgb(0 0 0 / 20%)
),
color-mix(in oklab, var(--color-base-100) 88%, transparent);
box-shadow: inset 0 0 1.5rem rgb(0 0 0 / 35%);
}
/* Level marker rendered as a gilt-rimmed shield. */
.passive-level-number {
position: relative;
display: grid;
width: 3.1rem;
height: 3.4rem;
flex: none;
place-items: center;
border-radius: 0;
color: var(--gold-bright);
background: linear-gradient(
160deg,
var(--gold-bright),
color-mix(in oklab, var(--color-accent) 45%, var(--color-neutral))
);
font-family: var(--font-display);
font-size: 1.2rem;
clip-path: var(--shield-clip);
animation: shield-heartbeat 2.8s ease-in-out infinite;
}
.passive-level-number::before {
content: "";
position: absolute;
z-index: -1;
inset: 2px;
background: linear-gradient(
160deg,
color-mix(in oklab, var(--color-primary) 82%, black),
color-mix(in oklab, var(--color-primary) 42%, black)
);
clip-path: var(--shield-clip);
}
.passive-level div {
flex: 1;
}
.passive-level p {
font-family: var(--font-heading);
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.passive-level small {
color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
}
.passive-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 0.9rem;
}
/* Dark enamelled escutcheon: deep jewel-tone field under a gold rim, with an
enamel sheen and a gold inner hairline. Light-on-dark text. */
.passive-tile {
--passive-color: var(--color-primary);
--passive-content: var(--color-primary-content);
position: relative;
display: flex;
min-height: 12rem;
grid-column: span 2;
flex-direction: column;
align-items: flex-start;
gap: 0.65rem;
overflow: hidden;
padding: 1rem;
border: var(--border) solid
color-mix(in oklab, var(--color-accent) 62%, black);
border-radius: 34% 34% var(--radius-box) var(--radius-box) / 12% 12% 0 0;
color: color-mix(
in oklab,
var(--color-base-content) 92%,
var(--passive-color)
);
background:
radial-gradient(
ellipse at 78% 6%,
color-mix(in oklab, var(--passive-color) 30%, transparent),
transparent 46%
),
linear-gradient(
180deg,
color-mix(
in oklab,
var(--passive-color) var(--passive-mix, 34%),
oklch(19% 0.02 52)
),
color-mix(
in oklab,
var(--passive-color) calc(var(--passive-mix, 34%) / 2),
oklch(13% 0.015 48)
)
);
text-align: left;
box-shadow:
0 0.75rem 1.5rem rgb(0 0 0 / 46%),
inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 26%, transparent),
inset 0 1px 0 rgb(255 255 255 / 10%),
inset 0 -1.2rem 1.8rem rgb(0 0 0 / 34%);
transition: transform 180ms ease, filter 180ms ease;
}
.passive-tile:nth-child(n+4) {
grid-column: span 3;
}
.passive-tile:not(:disabled) {
cursor: pointer;
}
.passive-tile:not(:disabled):hover,
.passive-tile:not(:disabled):focus-visible {
outline: none;
filter: saturate(1.12) brightness(1.04);
transform: translateY(-0.25rem);
}
.passive-tile:not(:disabled):active {
box-shadow: inset 0 0.5rem 1rem rgb(0 0 0 / 28%);
transform: translateY(0.12rem) scale(0.98);
}
.passive-tile:disabled {
opacity: 0.62;
}
.passive-vigor {
--passive-mix: 30%;
--passive-color: var(--color-secondary);
--passive-content: var(--color-secondary-content);
}
.passive-energy {
--passive-color: var(--color-info);
--passive-content: var(--color-info-content);
}
.passive-strength {
--passive-mix: 24%;
--passive-color: var(--color-warning);
--passive-content: var(--color-warning-content);
}
.passive-critical {
--passive-color: var(--color-error);
--passive-content: var(--color-error-content);
}
.passive-poison {
--passive-color: var(--color-success);
--passive-content: var(--color-success-content);
}
/* Enamelled roundel boss on each escutcheon. */
.passive-icon {
display: grid;
width: 3rem;
height: 3rem;
place-items: center;
border: 1px solid color-mix(in oklab, var(--color-accent) 60%, transparent);
border-radius: 50%;
color: color-mix(in oklab, var(--passive-color) 65%, white);
background:
radial-gradient(circle at 32% 26%, rgb(255 255 255 / 16%), transparent 52%),
radial-gradient(
circle at 50% 60%,
color-mix(in oklab, var(--passive-color) 40%, transparent),
transparent 70%
),
rgb(0 0 0 / 30%);
box-shadow:
inset 0 -0.2rem 0.4rem rgb(0 0 0 / 34%),
0 0.2rem 0.4rem rgb(0 0 0 / 34%);
font-size: 1.4rem;
will-change: transform;
}
.passive-copy strong {
display: block;
font-family: var(--font-heading);
font-size: 1.05rem;
letter-spacing: 0.08em;
}
.passive-copy small {
display: block;
margin-top: 0.15rem;
color: currentColor;
font-size: 0.72rem;
opacity: 0.78;
}
.passive-rank,
.passive-pips {
display: flex;
gap: 0.25rem;
margin-top: auto;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
.passive-pips i {
width: 0.5rem;
height: 0.5rem;
border: 1px solid currentColor;
border-radius: 50%;
opacity: 0.55;
}
.passive-pips i.is-filled {
background: currentColor;
box-shadow: 0 0 0.35rem currentColor;
opacity: 1;
animation: pip-glint 2.4s ease-in-out infinite;
}
.passive-tile em {
position: absolute;
right: 0.9rem;
bottom: 0.75rem;
font-size: 0.65rem;
font-style: normal;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
@media (max-width: 767px) {
.passive-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.passive-tile,
.passive-tile:nth-child(n+4) {
grid-column: span 1;
}
.passive-level {
flex-wrap: wrap;
}
}
@media (max-width: 440px) {
.passive-grid {
grid-template-columns: 1fr;
}
}
/* ==========================================================================
SCREENS · the city at night
========================================================================== */
.cityscape {
position: relative;
display: grid;
grid-template-columns: 1.15fr 0.8fr 1.25fr 1fr;
align-items: end;
gap: clamp(0.25rem, 1.5vw, 1rem);
overflow: hidden;
padding: 4rem clamp(0.5rem, 3vw, 2rem) 2.5rem;
border-radius: 2rem 2rem 0.5rem 0.5rem;
box-shadow: inset 0 0 0 1px var(--gold-line), inset 0 0 4rem rgb(0 0 0 / 50%);
}
/* Night sky with a low moon behind the keep. */
.cityscape::after {
content: "";
position: absolute;
z-index: 0;
inset: 0 0 auto;
height: 66%;
background:
radial-gradient(
circle at 78% 24%,
color-mix(in oklab, var(--color-accent) 60%, white) 0 2.1rem,
color-mix(in oklab, var(--color-accent) 26%, transparent) 2.2rem 3rem,
transparent 8rem
),
radial-gradient(
ellipse at 50% 100%,
color-mix(in oklab, var(--color-primary) 22%, transparent),
transparent 68%
),
linear-gradient(
180deg,
color-mix(in oklab, var(--color-primary) 14%, var(--color-base-300)),
transparent
);
pointer-events: none;
}
/* Masonry footing the buildings stand on. */
.cityscape::before {
content: "";
position: absolute;
z-index: 0;
inset: auto 0 0;
height: 2.75rem;
border-top: 0.35rem double var(--gold-line);
background-color: color-mix(
in oklab,
var(--color-neutral) 40%,
var(--color-base-300)
);
background-image:
linear-gradient(180deg, rgb(255 255 255 / 6%), rgb(0 0 0 / 32%)),
var(--tex-ashlar);
background-size: auto, 8rem 4.4rem;
pointer-events: none;
}
.city-title {
position: absolute;
z-index: 2;
top: 1.1rem;
left: 50%;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
color: color-mix(in oklab, var(--color-accent) 80%, transparent);
transform: translateX(-50%);
white-space: nowrap;
}
.city-title > strong {
animation: magical-heartbeat 4s ease-in-out infinite;
}
.city-title strong {
color: color-mix(in oklab, var(--color-accent) 92%, white);
font-family: var(--font-gothic);
font-size: clamp(1.4rem, 3.4vw, 2.35rem);
font-weight: 700;
letter-spacing: 0.12em;
text-shadow:
0 0.15rem 0.4rem rgb(0 0 0 / 70%),
0 0 1.2rem color-mix(in oklab, var(--color-accent) 45%, transparent);
}
/* Gold fleuron rule beneath the city name. */
.city-title-rule {
width: clamp(9rem, 26vw, 18rem);
height: 0.7rem;
background-image:
linear-gradient(
90deg,
transparent,
color-mix(in oklab, var(--color-accent) 70%, transparent) 22% 78%,
transparent
),
var(--orn-terminal);
background-repeat: no-repeat, no-repeat;
background-position: center center, center center;
background-size: 100% 1px, 1.75rem 0.7rem;
}
.city-building {
position: relative;
z-index: 1;
display: flex;
min-width: 0;
width: 100%;
height: var(--building-height);
align-items: flex-end;
justify-content: center;
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
transition: transform 200ms ease, filter 200ms ease;
}
.city-building img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
object-position: bottom;
padding: 0.2rem;
filter: brightness(0.78) saturate(60%) sepia(30%) opacity(0.82) drop-shadow(
0 0.75rem 0.55rem rgb(0 0 0 / 55%)
);
transition: transform 200ms ease, filter 200ms ease;
}
.city-building span {
z-index: 1;
width: calc(100% - 0.5rem);
margin-bottom: 0.25rem;
padding: 0.5rem 0.25rem 0.85rem;
font-size: clamp(0.55rem, 1.4vw, 0.8rem);
line-height: 1;
}
.city-building i {
position: absolute;
z-index: 2;
top: 0.5rem;
right: 0.5rem;
font-style: normal;
}
.city-building:hover,
.city-building:focus-visible {
outline: none;
filter: drop-shadow(0 0 1rem var(--building-glow));
transform: translateY(-0.6rem) rotate(-0.4deg);
}
.city-building:hover img,
.city-building:focus-visible img {
transform: scale(1.065) translateY(-0.2rem);
}
.city-building:active {
filter: drop-shadow(0 0 0.45rem var(--building-glow));
transform: translateY(0.1rem) scale(0.975);
}
.city-building:active img {
transform: scale(0.98) translateY(0.25rem);
}
.city-keep {
--building-height: 78%;
--building-glow: color-mix(in oklab, var(--color-primary) 30%, transparent);
}
.city-arcana {
--building-height: 92%;
--building-glow: color-mix(in oklab, var(--color-info) 34%, transparent);
}
.city-sanctum {
--building-height: 84%;
--building-glow: color-mix(in oklab, var(--color-secondary) 32%, transparent);
}
.city-gate {
--building-height: 68%;
--building-glow: color-mix(in oklab, var(--color-accent) 32%, transparent);
}
@media (max-width: 639px) {
.navbar-start,
.navbar-end {
width: 24%;
}
.navbar-center {
width: 52%;
}
.cityscape {
padding-inline: 0.25rem;
padding-bottom: 2rem;
}
}
@media (max-height: 650px) {
.city-title {
top: 0.5rem;
}
.cityscape {
padding-top: 2.5rem;
}
}
/* ==========================================================================
SCREENS · combat furnishings
========================================================================== */
/* Flagstone arena floor. The combatants stand ON this band, so it fills the
lower half of the battlefield and fades up into the dark rather than
floating as a detached stripe. */
.battlefield {
position: relative;
overflow: hidden;
}
.battlefield::before {
content: "";
position: absolute;
z-index: 0;
inset: 38% -3rem 0;
border-top: 1px solid
color-mix(in oklab, var(--color-accent) 26%, transparent);
background-color: color-mix(in oklab, var(--color-neutral) 42%, black);
background-image:
linear-gradient(
180deg,
color-mix(in oklab, var(--color-accent) 12%, transparent),
transparent 32%,
rgb(0 0 0 / 45%)
),
var(--tex-ashlar);
background-size: auto, 11rem 6rem;
mask-image: linear-gradient(180deg, transparent, black 30%, black);
pointer-events: none;
}
.battlefield > div {
position: relative;
z-index: 1;
}
/* Wall sconce: iron cresset with a warm flame and a pool of candlelight. */
.sconce {
--candle-tint: color-mix(
in oklab,
var(--color-accent) 68%,
var(--color-warning)
);
position: absolute;
z-index: 0;
width: 0.85rem;
height: 1.5rem;
border-radius: 45% 45% 30% 30%;
background: linear-gradient(
180deg,
color-mix(in oklab, var(--candle-tint) 92%, white) 0 38%,
color-mix(in oklab, var(--color-neutral) 60%, black) 44%
);
box-shadow:
0 0 0.9rem color-mix(in oklab, var(--candle-tint) 75%, transparent),
0 0 2.4rem color-mix(in oklab, var(--candle-tint) 45%, transparent);
pointer-events: none;
}
.sconce::before {
content: "";
position: absolute;
z-index: -1;
inset: -420% -900% -260%;
background: radial-gradient(
closest-side,
color-mix(in oklab, var(--candle-tint) 22%, transparent),
transparent 76%
);
pointer-events: none;
animation: candle-flicker 6.5s ease-in-out infinite;
}
/* Oak-and-iron rail that carries the hand of cards. */
.hand-rail {
position: relative;
border-top: 0.28rem solid var(--iron-dark);
background-color: color-mix(
in oklab,
var(--color-neutral) 44%,
var(--color-base-300)
);
background-image:
linear-gradient(
180deg,
rgb(255 255 255 / 6%),
transparent 22%,
rgb(0 0 0 / 30%)
),
repeating-linear-gradient(
90deg,
rgb(0 0 0 / 9%) 0 1px,
transparent 1px 9px
),
repeating-linear-gradient(
180deg,
rgb(255 255 255 / 3%) 0 1px,
transparent 1px 1.4rem
);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 8%),
0 -0.6rem 1.5rem rgb(0 0 0 / 45%);
}
/* Chronicle strip that logs the fight. */
.chronicle {
position: relative;
border-block: 1px solid var(--gold-line);
background:
linear-gradient(
90deg,
color-mix(in oklab, var(--color-accent) 7%, transparent),
transparent 18% 82%,
color-mix(in oklab, var(--color-accent) 7%, transparent)
),
color-mix(in oklab, var(--color-base-300) 45%, transparent);
}
/* The latest two events sit in a fixed band; a longer line is clipped instead
of shoving the battlefield upward. */
.chronicle-latest {
height: 2.25rem;
}
/* Status pips wink in and out mid-fight, so their row reserves two lines and
the battlefield never reflows around a new shield or poison badge. */
.status-badges {
min-height: 2.75rem;
}
/* Iron-riveted enemy plate. */
.enemy-plate {
position: relative;
background-color: color-mix(in oklab, var(--color-base-100) 88%, transparent);
background-image: linear-gradient(
180deg,
rgb(255 255 255 / 5%),
transparent 38%,
rgb(0 0 0 / 26%)
);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 8%),
0 0.5rem 1.1rem rgb(0 0 0 / 40%);
}
.enemy-plate::before {
content: "";
position: absolute;
z-index: 1;
right: 0.3rem;
left: 0.3rem;
top: 0.22rem;
height: 0.3rem;
background-image: radial-gradient(
circle at 50% 50%,
var(--iron-light) 0 0.055rem,
color-mix(in oklab, var(--color-neutral) 28%, black) 0.065rem 0.14rem,
transparent 0.15rem
);
background-repeat: repeat-x;
background-size: 1.6rem 0.3rem;
opacity: 0.7;
pointer-events: none;
}
/* Currently targeted foe: gold-lit rivet frame. */
.enemy-plate-target {
border-color: color-mix(in oklab, var(--color-accent) 80%, transparent);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 10%),
0 0.5rem 1.1rem rgb(0 0 0 / 44%),
0 0 1.4rem color-mix(in oklab, var(--color-accent) 38%, transparent);
}
/* Iron-band frame around a vitality gauge. */
.gauge-frame {
display: flex;
align-items: center;
padding: 0.28rem;
border: var(--border) solid var(--iron-dark);
border-radius: 0.35rem;
}
.gauge-frame .progress {
display: block;
}
/* ==========================================================================
ANIMATION
========================================================================== */
@keyframes room-aura-shift {
from {
background-position: 0 0, 50% 0, 0 0, 0 0;
}
to {
background-position: 0 0, 50% 15%, 0 0, 0 0;
}
}
@keyframes magical-heartbeat {
0%,
88%,
100% {
filter: brightness(1);
transform: scale(1);
}
92% {
filter: brightness(1.25);
transform: scale(1.06);
}
96% {
transform: scale(1.02);
}
}
/* Scale-only pulse: keeps the shield rim filter intact. */
@keyframes shield-heartbeat {
0%,
88%,
100% {
transform: scale(1);
}
92% {
transform: scale(1.06);
}
96% {
transform: scale(1.02);
}
}
@keyframes pip-glint {
0%,
70%,
100% {
filter: brightness(1);
transform: scale(1);
}
80% {
filter: brightness(1.7);
transform: scale(1.3);
}
}
@keyframes combat-rune-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.room-screen,
.room-screen::before,
.room-screen::after,
.margin-vines::before,
.margin-vines::after,
.passive-level-number,
.passive-icon,
.passive-pips i.is-filled,
.candle-glow::before,
.sconce::before,
.city-title > strong {
animation: none;
}
.city-building,
.city-building img,
.game-card,
.game-card::before,
.mystic-class-icon,
.card-fan-slot {
transition: none;
}
.combat-effect span {
animation: none;
}
.btn,
.btn::after,
.passive-tile {
transition: none;
}
}
/* ==========================================================================
CARDS · illuminated manuscript leaves
========================================================================== */
/* The fan's slots are transformed (rotate + lift), which does not grow the
flow box, so the fan reserves extra height to enclose the rotated cards and
keep the pile counts clear of them. */
.card-fan {
display: flex;
justify-content: center;
align-items: flex-end;
min-height: 12.75rem;
padding-bottom: 1.25rem;
}
.card-fan-flip {
margin: 0 -0.75rem;
}
.card-fan-slot {
transform: rotate(var(--rot, 0deg)) translateY(var(--lift, 0));
transform-origin: bottom center;
transition: transform 200ms ease, filter 200ms ease;
}
.card-fan-slot:hover {
z-index: 10;
filter: drop-shadow(
0 0 10px color-mix(in oklab, var(--color-accent) 45%, transparent)
);
transform: rotate(0deg) translateY(-0.75rem);
}
.combat-card-slot {
perspective: 40rem;
}
.game-card {
--game-card-color: var(--color-primary);
position: relative;
overflow: hidden;
border-width: 2px;
border-color: color-mix(in oklab, var(--color-accent) 42%, transparent);
border-radius: 0.4rem;
box-shadow:
inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 55%, transparent),
inset 0 0 0 3px rgb(0 0 0 / 34%),
inset 0 0 0 4px color-mix(in oklab, var(--color-accent) 32%, transparent),
0 0.75rem 1.4rem rgb(0 0 0 / 38%),
0 0 1rem color-mix(in oklab, var(--game-card-color) 18%, transparent);
transform-style: preserve-3d;
transition:
border-color 200ms ease,
box-shadow 200ms ease,
transform 200ms ease;
}
/* Warm candlelight glide across the leaf. */
.game-card::before {
content: "";
position: absolute;
z-index: 5;
inset: -80% -120%;
background: linear-gradient(
110deg,
transparent 42%,
color-mix(in oklab, var(--color-accent) 32%, transparent) 49%,
transparent 56%
);
pointer-events: none;
transform: translateX(-35%) rotate(4deg);
transition: transform 600ms ease;
}
.game-card:hover::before,
.game-card:focus-visible::before {
transform: translateX(35%) rotate(4deg);
}
.game-card:hover,
.game-card:focus-visible {
border-color: var(--game-card-color);
outline: none;
box-shadow:
inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 75%, transparent),
inset 0 0 0 3px rgb(0 0 0 / 34%),
inset 0 0 0 4px color-mix(in oklab, var(--color-accent) 48%, transparent),
0 1.15rem 1.8rem rgb(0 0 0 / 44%),
0 0 1.5rem color-mix(in oklab, var(--game-card-color) 48%, transparent);
transform: translateY(-0.75rem) rotateX(5deg) scale(1.045);
}
.game-card:active {
box-shadow: inset 0 0.45rem 0.8rem rgb(0 0 0 / 30%);
transform: translateY(-0.2rem) rotateX(-3deg) scale(0.97);
}
.game-card-physical {
--game-card-color: var(--color-error);
}
.game-card-defense {
--game-card-color: var(--color-info);
}
.game-card-poison {
--game-card-color: var(--color-success);
}
.game-card-arcane {
--game-card-color: var(--color-primary);
}
.card-fan-card {
width: 8rem;
min-height: 11rem;
box-shadow: 0 0.5rem 1.25rem rgb(0 0 0 / 40%);
}
/* Card art sits in an arched window; .arch-frame supplies the silhouette. */
.card-art-frame {
--card-art-color: var(--color-primary);
--card-art-glow: color-mix(
in oklab,
var(--color-neutral-content) 55%,
transparent
);
border-bottom: 1px solid var(--card-art-color);
background:
radial-gradient(
circle at center,
color-mix(in oklab, var(--card-art-color) 42%, transparent),
transparent 68%
),
linear-gradient(
145deg,
var(--color-neutral),
color-mix(in oklab, var(--card-art-color) 62%, var(--color-neutral))
);
}
.card-art-physical {
--card-art-color: var(--color-error);
}
.card-art-defense {
--card-art-color: var(--color-info);
}
.card-art-poison {
--card-art-color: var(--color-success);
}
.card-art-arcane {
--card-art-color: var(--color-primary);
}
/* Gold-leaf shimmer marks a rare leaf. */
.card-art-rare {
--card-art-glow: color-mix(in oklab, var(--color-accent) 90%, transparent);
background-image:
repeating-linear-gradient(
115deg,
color-mix(in oklab, var(--color-accent) 20%, transparent) 0 2px,
transparent 2px 9px
),
radial-gradient(
circle at center,
color-mix(in oklab, var(--card-art-color) 42%, transparent),
transparent 68%
),
linear-gradient(
145deg,
color-mix(in oklab, var(--color-accent) 26%, var(--color-neutral)),
color-mix(in oklab, var(--card-art-color) 62%, var(--color-neutral))
);
box-shadow:
inset 0 0 0 1px color-mix(in oklab, var(--color-accent) 60%, transparent),
inset 0 0 1.4rem color-mix(in oklab, var(--color-accent) 22%, transparent);
}
.card-art-frame img {
filter: drop-shadow(0 0 0.2rem var(--card-art-glow)) drop-shadow(
0 0 0.65rem var(--card-art-glow)
);
}
.game-card .corner-fleurons {
--fleuron-size: 0.65rem;
--fleuron-inset: 0.18rem;
}
/* ==========================================================================
COMBAT EFFECTS
========================================================================== */
.combat-effects {
position: fixed;
z-index: 60;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.combat-effect {
--effect-color: var(--color-primary);
position: absolute;
width: 4rem;
height: 4rem;
will-change: transform, opacity;
}
.combat-effect span {
position: absolute;
inset: -0.5rem;
border: 2px solid var(--effect-color);
border-radius: 50%;
box-shadow:
0 0 1rem var(--effect-color),
inset 0 0 0.75rem var(--effect-color);
animation: combat-rune-spin 700ms linear infinite;
}
.combat-effect span::before,
.combat-effect span::after {
content: "✦";
position: absolute;
color: var(--effect-color);
font-size: 0.7rem;
text-shadow: 0 0 0.5rem currentColor;
}
.combat-effect span::before {
top: -0.4rem;
left: 50%;
}
.combat-effect span::after {
right: -0.3rem;
bottom: 0.2rem;
}
.combat-effect img {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: contain;
padding: 0.55rem;
filter: drop-shadow(0 0 0.35rem var(--effect-color)) drop-shadow(
0 0 0.75rem var(--effect-color)
);
}
.combat-effect-physical {
--effect-color: var(--color-error);
}
.combat-effect-defense {
--effect-color: var(--color-info);
}
.combat-effect-poison {
--effect-color: var(--color-success);
}
.combat-effect-arcane {
--effect-color: var(--color-primary);
}
.combat-effect-aoe span {
border-style: double;
border-width: 5px;
}
.enemy-art {
padding: 0.35rem;
border-radius: 45% 45% 50% 50%;
background: color-mix(in oklab, var(--color-error) 72%, transparent);
filter: drop-shadow(
0 0 0.55rem color-mix(in oklab, var(--color-error) 55%, transparent)
);
}
@media (max-width: 640px) {
.card-fan {
min-height: 8rem;
overflow-x: auto;
padding-bottom: 0.5rem;
justify-content: flex-start;
}
.card-fan::after {
content: "";
flex: 0 0 0.25rem;
}
.card-fan-flip {
margin: 0 -0.15rem;
}
.card-fan-slot {
transform: rotate(calc(var(--rot, 0deg) * 0.5))
translateY(calc(var(--lift, 0px) * 0.5));
}
.card-fan-card {
width: 4.25rem;
min-height: 7.25rem;
}
.card-art-frame img {
padding: 0.4rem;
}
.card-body {
padding: 0.5rem;
gap: 0.25rem;
}
.card-body p {
font-size: 0.55rem;
line-height: 1.1;
}
.card-body .font-heading {
font-size: 0.55rem;
}
.game-card .corner-fleurons {
--fleuron-size: 0.5rem;
}
}