:host { display: block; block-size: 100%; } .combat { display: grid; grid-template-columns: minmax(0, 1fr) clamp(13rem, 17vw, 17rem); gap: var(--ar-space-4); block-size: 100%; } /* ---------- stage ---------- */ // The stage is a container so the status row reflows on its own width: the // surrounding rails can squeeze it narrow while the viewport is still wide. .combat__stage { position: relative; display: grid; container-type: inline-size; grid-template-rows: auto minmax(0, 1fr) auto; gap: var(--ar-space-4); // The sprites are sized as a share of the stage, so the stage has to carry a // height of its own. With only a min-block-size the tallest cut-out -- the // road bandit -- sized the battlefield row from its intrinsic height and // pushed the page into a scrollbar. The subtracted 12.5rem is the shell // chrome around the main column: top bar, footer and its own padding. block-size: max(24rem, calc(100dvh - 12.5rem)); padding: var(--ar-space-4); overflow: hidden; border: 1px solid var(--ar-border); background-color: var(--ar-bg); background-image: url('/images/backgrounds/Aschestrasse.png'); background-position: center; background-repeat: no-repeat; background-size: cover; box-shadow: var(--ar-shadow-raised); } @supports ( background-image: image-set( url('/images/backgrounds/runtime/Aschestrasse-960.jpg') type('image/jpeg') 1x ) ) { .combat__stage { background-image: image-set( url('/images/backgrounds/runtime/Aschestrasse-960.jpg') type('image/jpeg') 1x ); } } .combat__stage::before { position: absolute; z-index: 0; inset: 0; content: ''; background: linear-gradient(180deg, rgb(4 6 8 / 0.72) 0%, rgb(4 6 8 / 0.1) 26%, rgb(4 6 8 / 0.66) 100%); pointer-events: none; } /* ---------- combatant status ---------- */ .combat__status { position: relative; z-index: 2; display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: start; gap: var(--ar-space-4); } .fighter { display: flex; align-items: center; gap: var(--ar-space-3); min-inline-size: 0; } .fighter--monster { justify-content: flex-end; } .fighter__icon { flex: 0 0 auto; inline-size: clamp(2.75rem, 5vw, 3.75rem); block-size: clamp(2.75rem, 5vw, 3.75rem); object-fit: cover; border-radius: 50%; box-shadow: 0 0 0 1px var(--ar-border-highlight), 0 0.3rem 0.9rem rgb(0 0 0 / 0.8); } .fighter__meter { display: grid; gap: 0.3rem; min-inline-size: 0; flex: 1 1 auto; max-inline-size: 22rem; } .fighter--monster .fighter__meter { justify-items: end; text-align: end; } .fighter__name { margin: 0; overflow: hidden; color: var(--ar-text); font-family: Georgia, 'Times New Roman', serif; font-size: clamp(0.95rem, 1.5vw, 1.15rem); letter-spacing: 0.02em; text-overflow: ellipsis; text-shadow: 0 0.1rem 0.5rem rgb(0 0 0 / 0.95); white-space: nowrap; } .fighter__level { color: var(--ar-gold); font-family: system-ui, sans-serif; font-size: var(--ar-font-sm); letter-spacing: 0.08em; text-transform: uppercase; } /* ---------- hp bars ---------- */ .bar { position: relative; display: block; inline-size: 100%; block-size: 1.25rem; overflow: hidden; border: 1px solid var(--ar-border); background: rgb(0 0 0 / 0.72); box-shadow: inset 0 0 0.6rem rgb(0 0 0 / 0.9); } .bar__fill { position: absolute; z-index: 0; inset-block: 0; inset-inline-start: 0; display: block; background: linear-gradient(180deg, #d0564a, #8e2b23); } .bar--monster .bar__fill { inset-inline-start: auto; inset-inline-end: 0; } .bar__text { position: relative; z-index: 1; display: grid; block-size: 100%; place-items: center; color: var(--ar-text); font-size: var(--ar-font-sm); font-variant-numeric: tabular-nums; letter-spacing: 0.04em; text-shadow: 0 0.05rem 0.25rem rgb(0 0 0 / 1); } /* ---------- round marker ---------- */ .combat__round { margin: 0; align-self: center; padding: var(--ar-space-1) var(--ar-space-5); border-block: 1px solid var(--ar-border-highlight); color: var(--ar-gold); background: linear-gradient(90deg, transparent, rgb(9 11 13 / 0.9) 18%, rgb(9 11 13 / 0.9) 82%, transparent); font-family: Georgia, 'Times New Roman', serif; font-size: clamp(0.95rem, 1.4vw, 1.1rem); letter-spacing: 0.14em; text-transform: uppercase; white-space: nowrap; } /* ---------- battlefield ---------- */ .combat__field { position: relative; z-index: 1; display: grid; // The single row is spelled out rather than left implicit: only then is it a // definite height, and only then do the sprites' percentage heights resolve // against the battlefield instead of against their own artwork. grid-template-columns: 1fr 1fr; grid-template-rows: minmax(0, 1fr); align-items: end; min-block-size: 0; } .sprite { display: block; max-inline-size: 100%; inline-size: auto; object-fit: contain; object-position: bottom; filter: drop-shadow(0 0.5rem 0.9rem rgb(0 0 0 / 0.7)); } /* Six 384px frames laid out horizontally; frame 0 is the resting stance. */ .sprite--player { justify-self: start; block-size: 80%; aspect-ratio: 1; transform: scaleX(-1); background-image: url('/images/combat/sprites/warrior-attack-sheet-384.png'); background-repeat: no-repeat; background-position: 0% 0; background-size: 600% 100%; } .sprite--player.sprite--hit { background-image: url('/images/combat/sprites/warrior-hit-sheet-384.png'); } /* 6 frames across a 600%-wide sheet land on 0/20/40/60/80/100%. */ @keyframes warrior-frames { from { background-position: 0% 0; } to { background-position: 120% 0; } } .sprite--monster { justify-self: end; block-size: calc(var(--sprite-scale, 0.6) * 100%); // Every beat below scales and rotates the cut-out. Pivoting at the feet keeps // it standing on the same spot instead of sinking behind the action bar. transform-origin: bottom center; } /* The monster is a single cut-out, so every beat below is transform-only. All of them animate translate/rotate/scale rather than `transform`, and any step that touches `filter` has to repeat the base drop-shadow from `.sprite`, because a keyframe filter replaces the whole list. */ /* Getting hit: a short recoil to the right, away from the player, with a red glow pulse. 240ms so it finishes inside the 260ms riposte pause. */ @keyframes monster-flinch { 0% { translate: 0 0; rotate: 0deg; filter: drop-shadow(0 0.5rem 0.9rem rgb(0 0 0 / 0.7)) drop-shadow(0 0 0 rgb(199 75 66 / 0)); } 20% { translate: 0.45rem -0.12rem; rotate: 1.1deg; filter: drop-shadow(0 0.5rem 0.9rem rgb(0 0 0 / 0.7)) drop-shadow(0 0 0.65rem rgb(199 75 66 / 0.6)); } 45% { translate: -0.28rem 0; rotate: -1deg; } 70% { translate: 0.18rem 0; rotate: 0.5deg; } 100% { translate: 0 0; rotate: 0deg; filter: drop-shadow(0 0.5rem 0.9rem rgb(0 0 0 / 0.7)) drop-shadow(0 0 0 rgb(199 75 66 / 0)); } } /* Striking back: a brief wind-up away from the player, then a lunge left. The impact sits at 28% of 540ms so it reads against the warrior hit sheet. */ @keyframes monster-lunge { 0% { translate: 0 0; rotate: 0deg; scale: 1; } 12% { translate: 0.55rem 0; rotate: 1.4deg; scale: 0.985; } 28% { translate: -2rem -0.45rem; rotate: -2.4deg; scale: 1.05; } 55% { translate: -0.8rem -0.1rem; rotate: -0.8deg; scale: 1.015; } 100% { translate: 0 0; rotate: 0deg; scale: 1; } } /* Idle: deliberately tiny, just enough that the cut-out does not read as card- board. The spec rules out permanent *strong* animation, not a slow breath. */ @keyframes monster-breathe { from { translate: 0 0; scale: 1; } to { translate: 0 -0.4%; scale: 1.004; } } /* Stage jolt when the player takes the blow. The slight scale gives the offset somewhere to go: the stage clips its children, but its own border moves too, so without it the shift would open a sliver at the frame. */ @keyframes stage-shake { 0%, 100% { translate: 0 0; scale: 1; } 15% { translate: -0.35% 0.08rem; scale: 1.012; } 35% { translate: 0.3% -0.06rem; scale: 1.012; } 60% { translate: -0.18% 0.04rem; scale: 1.009; } 80% { translate: 0.1% 0; scale: 1.004; } } /* ---------- action bar ---------- */ .combat__actions { position: relative; z-index: 2; display: flex; gap: var(--ar-space-3); min-block-size: clamp(6.5rem, 11vw, 8.5rem); } .action { position: relative; inline-size: clamp(6.5rem, 11vw, 8.5rem); aspect-ratio: 1; padding: 0; border: 0; background: transparent url('/images/hud/runtime/fight-action-frame-360.png') center / 100% 100% no-repeat; cursor: pointer; } .action__icon, .action__label, .action__key { position: absolute; inset-inline-start: 50%; translate: -50% -50%; } .action__icon { inset-block-start: 32%; inline-size: 34%; border-radius: 50%; opacity: 0.92; } .action__label { inset-block-start: 62%; color: var(--ar-text); font-family: Georgia, 'Times New Roman', serif; font-size: clamp(0.85rem, 1.2vw, 1rem); letter-spacing: 0.03em; white-space: nowrap; } .action__key { inset-block-start: 90%; color: var(--ar-text-muted); font-size: var(--ar-font-sm); font-variant-numeric: tabular-nums; } .action:hover:not(:disabled) .action__label, .action:focus-visible .action__label { color: var(--ar-gold); } .action:focus-visible { outline: 2px solid var(--ar-gold); outline-offset: 3px; } .action:disabled { cursor: progress; opacity: 0.55; } /* ---------- outcome overlay ---------- */ .outcome { position: absolute; z-index: 3; inset-block-start: 50%; inset-inline-start: 50%; display: grid; gap: var(--ar-space-2); justify-items: center; inline-size: min(26rem, 80%); padding: var(--ar-space-5); translate: -50% -50%; border: 1px solid var(--ar-border-highlight); background: rgb(9 11 13 / 0.94); box-shadow: var(--ar-shadow-raised); text-align: center; } .outcome p { margin: 0; color: var(--ar-text-muted); } .outcome__title { margin: 0; font-family: Georgia, 'Times New Roman', serif; font-size: clamp(1.5rem, 3vw, 2rem); font-weight: 400; letter-spacing: 0.12em; text-transform: uppercase; } .outcome--won { inline-size: min(32rem, 90%); } .outcome--won .outcome__title { color: var(--ar-gold); } .outcome--lost .outcome__title { color: var(--ar-danger); } .outcome__hint { font-size: var(--ar-font-sm); font-style: italic; } .outcome__button, .combat__notice--error button { padding: var(--ar-space-2) var(--ar-space-5); border: 1px solid var(--ar-border-highlight); border-radius: var(--ar-radius-sm); color: var(--ar-text); background: linear-gradient(180deg, #23282c, #14181b); cursor: pointer; font-family: Georgia, 'Times New Roman', serif; letter-spacing: 0.04em; } .outcome__button { margin-block-start: var(--ar-space-2); } .outcome__button:hover, .combat__notice--error button:hover { border-color: var(--ar-gold); color: var(--ar-gold); } /* ---------- combat log ---------- */ .combat__log { display: grid; grid-template-rows: auto minmax(0, 1fr); min-block-size: 0; border: 1px solid var(--ar-border); background: var(--ar-panel-muted); box-shadow: var(--ar-shadow-raised); } .combat__log-title { margin: 0; padding: var(--ar-space-3) var(--ar-space-4); border-block-end: 1px solid var(--ar-border); color: var(--ar-gold); font-family: Georgia, 'Times New Roman', serif; font-size: 1.05rem; font-weight: 400; letter-spacing: 0.1em; text-transform: uppercase; } .combat__log-body { padding: var(--ar-space-3) var(--ar-space-4) var(--ar-space-4); overflow-y: auto; } .combat__log-round { margin: var(--ar-space-4) 0 var(--ar-space-2); color: var(--ar-gold); font-size: var(--ar-font-sm); letter-spacing: 0.1em; text-transform: uppercase; } .combat__log-round:first-child { margin-block-start: 0; } .combat__log-entry { margin: 0 0 var(--ar-space-1); padding-inline-start: var(--ar-space-3); border-inline-start: 2px solid var(--ar-danger); color: var(--ar-text-muted); font-size: var(--ar-font-sm); line-height: 1.45; } .combat__log-entry--player { border-inline-start-color: var(--ar-border-highlight); } /* ---------- notices ---------- */ .combat__notice { grid-column: 1 / -1; padding: var(--ar-space-4); border: 1px solid var(--ar-border); background: var(--ar-panel); box-shadow: var(--ar-shadow-raised); } .combat__notice--error { display: flex; align-items: center; justify-content: space-between; gap: var(--ar-space-4); border-color: var(--ar-danger); } .combat__notice--error button { flex: 0 0 auto; } @media (prefers-reduced-motion: no-preference) { .sprite--player.sprite--attacking, .sprite--player.sprite--hit { animation: warrior-frames 540ms steps(6) 1; } /* Order matters: the state classes below share translate/rotate/scale with the breathing loop, so their `animation` shorthand has to come last. */ .sprite--monster { animation: monster-breathe 5.5s ease-in-out infinite alternate; } .sprite--flinch { animation: monster-flinch 240ms ease-out 1; } .sprite--lunge { animation: monster-lunge 540ms cubic-bezier(0.22, 0.9, 0.3, 1) 1; } .combat__stage--shaken { animation: stage-shake 200ms ease-out 1; } .bar__fill { transition: inline-size var(--ar-motion-base); } .action__label { transition: color var(--ar-motion-fast); } } @media (width < 60rem) { :host, .combat { block-size: auto; } .combat { grid-template-columns: minmax(0, 1fr); } .combat__stage { block-size: clamp(20rem, 55dvh, 34rem); } .combat__log { max-block-size: 18rem; } } // Below this the three-column status row cannot hold two names and two bars // side by side, so the fighters stack under a centred round marker. @container (width < 38rem) { .combat__status { grid-template-columns: minmax(0, 1fr); gap: var(--ar-space-2); } .combat__round { order: -1; justify-self: center; } .fighter--monster { justify-content: flex-start; } .fighter--monster .fighter__meter { justify-items: start; text-align: start; } .bar--monster .bar__fill { inset-inline-start: 0; inset-inline-end: auto; } .fighter__meter { max-inline-size: none; } } /* Reward summary: dark stone and bronze, large readable values, restrained rarity emphasis. No confetti, no popups, no slot-machine reveal (spec ยง50). */ .rewards { display: grid; gap: var(--ar-space-3); justify-items: center; inline-size: 100%; margin-block-start: var(--ar-space-3); padding-block-start: var(--ar-space-3); border-block-start: 1px solid var(--ar-border); } .rewards__title { margin: 0; color: var(--ar-text-muted); font-family: Georgia, 'Times New Roman', serif; font-size: var(--ar-font-sm); font-weight: 400; letter-spacing: 0.16em; text-transform: uppercase; } .rewards__currencies { display: flex; gap: var(--ar-space-6); margin: 0; } .rewards__currency { display: grid; gap: var(--ar-space-1); justify-items: center; } .rewards__currency dt { color: var(--ar-text-muted); font-size: var(--ar-font-sm); letter-spacing: 0.08em; text-transform: uppercase; } .rewards__currency dd { margin: 0; color: var(--ar-gold); font-family: Georgia, 'Times New Roman', serif; font-size: clamp(1.25rem, 2.5vw, 1.6rem); } .rewards__loot { display: flex; flex-wrap: wrap; gap: var(--ar-space-4); justify-content: center; margin: 0; padding: 0; list-style: none; }