Game — Patch Notes / Changelog Layout
A dark, HUD-styled patch-notes and changelog layout for a fictional live-service title. A version rail lets players jump between major updates, hotfixes, and quality-of-life patches, while the notes pane groups each build into Added, Changed, Fixed, and Balance sections with colored accent tags and scoped bullet entries. A pinned highlights block surfaces the headline changes, and a live keyword search, category filter chips, and collapsible sections keep long changelogs readable down to mobile widths.
MCP
Code
:root {
--bg: #0a0b10;
--bg-2: #12131c;
--panel: #171926;
--panel-2: #1f2233;
--text: #e7e9f3;
--muted: #9aa0bf;
--line: rgba(231, 233, 243, 0.1);
--line-2: rgba(231, 233, 243, 0.18);
--accent: #00e5ff;
--accent-2: #7c4dff;
--accent-3: #ff3d71;
--success: #36e27a;
--warn: #ffc857;
--danger: #ff4d4d;
--glow: 0 0 18px rgba(0, 229, 255, 0.45);
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--font-display: "Orbitron", sans-serif;
--font-body: "Inter", sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--font-body);
background:
radial-gradient(1100px 500px at 85% -10%, rgba(124, 77, 255, 0.12), transparent 60%),
radial-gradient(900px 420px at 0% 0%, rgba(0, 229, 255, 0.08), transparent 55%),
var(--bg);
color: var(--text);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.page {
max-width: 1080px;
margin: 0 auto;
padding: 24px 20px 56px;
}
button {
font: inherit;
color: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ===== Header ===== */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding-bottom: 20px;
border-bottom: 1px solid var(--line);
margin-bottom: 20px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-mark {
width: 48px;
height: 48px;
display: grid;
place-items: center;
font-family: var(--font-display);
font-weight: 900;
font-size: 1rem;
color: #04141a;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
clip-path: polygon(0 0, 100% 0, 100% 72%, 72% 100%, 0 100%);
box-shadow: var(--glow);
}
.brand-text {
display: flex;
flex-direction: column;
}
.brand-game {
font-family: var(--font-display);
font-weight: 900;
font-size: 1.3rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.brand-sub {
color: var(--muted);
font-size: 0.8rem;
font-weight: 500;
letter-spacing: 0.04em;
}
.header-meta {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.live-badge {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.78rem;
font-weight: 600;
color: var(--muted);
background: var(--panel);
border: 1px solid var(--line-2);
padding: 7px 12px;
border-radius: 999px;
}
.live-badge strong {
color: var(--success);
font-family: var(--font-display);
font-weight: 700;
letter-spacing: 0.06em;
}
.live-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 10px rgba(54, 226, 122, 0.8);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.8); }
}
/* ===== Buttons ===== */
.btn {
border: 1px solid transparent;
font-family: var(--font-display);
font-weight: 700;
font-size: 0.82rem;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 11px 22px;
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
transition: filter 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), #00b3d6);
color: #04141a;
box-shadow: var(--glow);
}
.btn-primary:hover {
filter: brightness(1.12);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(1px);
}
.btn-ghost {
background: transparent;
border-color: var(--line-2);
color: var(--muted);
}
.btn-ghost:hover {
border-color: var(--accent);
color: var(--accent);
box-shadow: 0 0 12px rgba(0, 229, 255, 0.25);
}
.btn-small {
font-size: 0.68rem;
padding: 9px 14px;
width: 100%;
}
/* ===== Toolbar ===== */
.toolbar {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.search-wrap {
position: relative;
flex: 1 1 260px;
min-width: 220px;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
pointer-events: none;
}
.search-input {
width: 100%;
background: var(--panel);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
color: var(--text);
font: inherit;
font-size: 0.9rem;
padding: 11px 36px 11px 36px;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.search-input::placeholder {
color: var(--muted);
opacity: 0.7;
}
.search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 229, 255, 0.18);
}
.search-input::-webkit-search-cancel-button {
display: none;
}
.search-clear {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
border: none;
border-radius: 50%;
background: var(--panel-2);
color: var(--muted);
font-size: 1rem;
line-height: 1;
display: grid;
place-items: center;
}
.search-clear:hover {
color: var(--text);
background: rgba(255, 61, 113, 0.25);
}
.chip-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
background: var(--panel);
border: 1px solid var(--line-2);
color: var(--muted);
font-weight: 600;
font-size: 0.78rem;
letter-spacing: 0.04em;
padding: 8px 16px;
clip-path: polygon(8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px);
transition: color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
}
.chip:hover {
color: var(--text);
border-color: var(--accent);
}
.chip[aria-pressed="true"] {
color: #04141a;
background: var(--accent);
border-color: var(--accent);
box-shadow: 0 0 14px rgba(0, 229, 255, 0.4);
}
.chip-added[aria-pressed="true"] {
background: var(--success);
border-color: var(--success);
box-shadow: 0 0 14px rgba(54, 226, 122, 0.4);
}
.chip-changed[aria-pressed="true"] {
background: var(--accent-2);
border-color: var(--accent-2);
color: #fff;
box-shadow: 0 0 14px rgba(124, 77, 255, 0.45);
}
.chip-fixed[aria-pressed="true"] {
background: var(--warn);
border-color: var(--warn);
box-shadow: 0 0 14px rgba(255, 200, 87, 0.4);
}
.chip-balance[aria-pressed="true"] {
background: var(--accent-3);
border-color: var(--accent-3);
color: #fff;
box-shadow: 0 0 14px rgba(255, 61, 113, 0.45);
}
.result-count {
margin: 0;
width: 100%;
color: var(--muted);
font-size: 0.78rem;
min-height: 1.2em;
}
.result-count strong {
color: var(--accent);
}
/* ===== Layout ===== */
.layout {
display: grid;
grid-template-columns: 230px 1fr;
gap: 22px;
align-items: start;
}
/* ===== Version rail ===== */
.version-rail {
background: linear-gradient(180deg, var(--panel), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px 14px;
position: sticky;
top: 16px;
}
.rail-title {
font-family: var(--font-display);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
margin: 0 0 12px 4px;
}
.version-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.version-btn {
width: 100%;
text-align: left;
background: transparent;
border: 1px solid transparent;
border-left: 3px solid transparent;
border-radius: var(--r-md);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 3px;
transition: background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.version-btn:hover {
background: var(--panel-2);
border-color: var(--line);
}
.version-btn[aria-current="true"] {
background: linear-gradient(90deg, rgba(0, 229, 255, 0.12), transparent 70%);
border-color: var(--line);
border-left-color: var(--accent);
box-shadow: inset 0 0 24px rgba(0, 229, 255, 0.06);
}
.version-num {
font-family: var(--font-display);
font-weight: 700;
font-size: 0.92rem;
letter-spacing: 0.08em;
display: flex;
align-items: center;
gap: 8px;
}
.version-btn[aria-current="true"] .version-num {
color: var(--accent);
text-shadow: 0 0 12px rgba(0, 229, 255, 0.5);
}
.version-date {
font-size: 0.72rem;
color: var(--muted);
}
.version-kind {
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 2px 7px;
border-radius: 999px;
border: 1px solid currentColor;
}
.kind-major { color: var(--accent); }
.kind-hotfix { color: var(--accent-3); }
.kind-update { color: var(--accent-2); }
.rail-foot {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--line);
}
/* ===== Notes pane ===== */
.notes-pane {
min-width: 0;
display: flex;
flex-direction: column;
gap: 18px;
}
.patch-head {
background: linear-gradient(135deg, var(--panel) 0%, var(--panel-2) 100%);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
padding: 22px 24px;
position: relative;
overflow: hidden;
}
.patch-head::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(420px 140px at 100% 0%, rgba(0, 229, 255, 0.12), transparent 70%);
pointer-events: none;
}
.patch-eyebrow {
font-family: var(--font-display);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--accent);
margin: 0 0 6px;
}
.patch-title {
font-family: var(--font-display);
font-weight: 900;
font-size: clamp(1.4rem, 3.4vw, 2rem);
letter-spacing: 0.05em;
text-transform: uppercase;
margin: 0 0 6px;
}
.patch-meta {
margin: 0;
color: var(--muted);
font-size: 0.85rem;
}
.patch-meta strong {
color: var(--text);
}
/* Highlights pinned block */
.highlights {
background: linear-gradient(135deg, rgba(124, 77, 255, 0.14), rgba(0, 229, 255, 0.07)), var(--panel);
border: 1px solid rgba(124, 77, 255, 0.45);
border-radius: var(--r-lg);
padding: 18px 22px;
box-shadow: 0 0 24px rgba(124, 77, 255, 0.18);
}
.highlights-head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.pin-badge {
font-family: var(--font-display);
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #fff;
background: var(--accent-2);
padding: 4px 10px;
clip-path: polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px);
box-shadow: 0 0 12px rgba(124, 77, 255, 0.55);
}
.highlights-title {
font-family: var(--font-display);
font-size: 0.95rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
margin: 0;
}
.highlights-list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.highlights-list li {
position: relative;
padding-left: 22px;
font-size: 0.9rem;
}
.highlights-list li::before {
content: "";
position: absolute;
left: 2px;
top: 0.48em;
width: 9px;
height: 9px;
background: var(--accent);
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
box-shadow: 0 0 8px rgba(0, 229, 255, 0.7);
}
/* ===== Note sections ===== */
.note-section {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
transition: border-color 0.18s ease;
}
.note-section:hover {
border-color: var(--line-2);
}
.section-toggle {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
background: transparent;
border: none;
padding: 15px 20px;
text-align: left;
}
.section-toggle:hover {
background: rgba(231, 233, 243, 0.03);
}
.section-tag {
font-family: var(--font-display);
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
padding: 5px 12px;
clip-path: polygon(7px 0, 100% 0, 100% calc(100% - 7px), calc(100% - 7px) 100%, 0 100%, 0 7px);
}
.tag-added {
color: #03150a;
background: var(--success);
box-shadow: 0 0 12px rgba(54, 226, 122, 0.4);
}
.tag-changed {
color: #fff;
background: var(--accent-2);
box-shadow: 0 0 12px rgba(124, 77, 255, 0.45);
}
.tag-fixed {
color: #1c1503;
background: var(--warn);
box-shadow: 0 0 12px rgba(255, 200, 87, 0.4);
}
.tag-balance {
color: #fff;
background: var(--accent-3);
box-shadow: 0 0 12px rgba(255, 61, 113, 0.45);
}
.section-count {
color: var(--muted);
font-size: 0.78rem;
font-weight: 600;
}
.section-caret {
margin-left: auto;
color: var(--muted);
transition: transform 0.22s ease;
flex: none;
}
.section-toggle[aria-expanded="false"] .section-caret {
transform: rotate(-90deg);
}
.section-body {
padding: 4px 20px 18px;
border-top: 1px solid var(--line);
}
.section-body[hidden] {
display: none;
}
.entry-list {
list-style: none;
margin: 0;
padding: 0;
}
.entry-list li {
display: flex;
align-items: baseline;
gap: 10px;
padding: 9px 0;
border-bottom: 1px dashed var(--line);
font-size: 0.88rem;
}
.entry-list li:last-child {
border-bottom: none;
}
.entry-scope {
flex: none;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent);
background: rgba(0, 229, 255, 0.1);
border: 1px solid rgba(0, 229, 255, 0.35);
border-radius: var(--r-sm);
padding: 2px 8px;
}
.entry-text {
color: var(--text);
}
.entry-text mark {
background: rgba(0, 229, 255, 0.28);
color: var(--accent);
border-radius: 3px;
padding: 0 2px;
font-weight: 600;
}
/* Empty state */
.empty-state {
background: var(--panel);
border: 1px dashed var(--line-2);
border-radius: var(--r-lg);
padding: 42px 24px;
text-align: center;
color: var(--muted);
}
.empty-state .empty-icon {
font-family: var(--font-display);
font-size: 1.6rem;
font-weight: 900;
color: var(--accent-3);
display: block;
margin-bottom: 8px;
}
.empty-state p {
margin: 0;
font-size: 0.9rem;
}
/* ===== Footer ===== */
.site-footer {
margin-top: 36px;
border-top: 1px solid var(--line);
padding-top: 16px;
color: var(--muted);
font-size: 0.75rem;
text-align: center;
}
.site-footer p {
margin: 0;
}
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 16px);
background: var(--panel-2);
border: 1px solid var(--accent);
box-shadow: var(--glow);
color: var(--text);
font-size: 0.85rem;
font-weight: 600;
padding: 12px 20px;
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
max-width: min(92vw, 460px);
text-align: center;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ===== Responsive ===== */
@media (max-width: 820px) {
.layout {
grid-template-columns: 1fr;
}
.version-rail {
position: static;
}
.version-list {
flex-direction: row;
flex-wrap: wrap;
}
.version-list li {
flex: 1 1 140px;
}
.version-btn {
border-left: none;
border-bottom: 3px solid transparent;
}
.version-btn[aria-current="true"] {
border-bottom-color: var(--accent);
}
}
@media (max-width: 520px) {
.page {
padding: 16px 12px 44px;
}
.site-header {
flex-direction: column;
align-items: flex-start;
}
.header-meta {
width: 100%;
justify-content: space-between;
}
.btn-primary {
padding: 10px 16px;
font-size: 0.74rem;
}
.patch-head {
padding: 18px 16px;
}
.highlights {
padding: 14px 16px;
}
.section-toggle {
padding: 13px 14px;
flex-wrap: wrap;
}
.section-body {
padding: 4px 14px 14px;
}
.entry-list li {
flex-direction: column;
gap: 4px;
}
.chip {
padding: 7px 12px;
font-size: 0.72rem;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}/* Hollow Reign — Patch Notes / Changelog (fictional demo data) */
(function () {
"use strict";
/* ===== Data ===== */
var PATCHES = [
{
id: "1.4.2",
kind: "hotfix",
codename: "Ashen Tides Hotfix",
date: "June 9, 2026",
summary: "Emergency fixes for the Drowned Bastion raid and co-op desync reported after 1.4.0.",
highlights: [
"Drowned Bastion raid checkpoints now save correctly between sessions.",
"Co-op desync during the Tidecaller boss phase resolved.",
"Crash on launch for ultrawide displays fixed."
],
sections: {
added: [],
changed: [
{ scope: "Netcode", text: "Reduced position-sync interval from 120ms to 60ms in 4-player co-op." }
],
fixed: [
{ scope: "Raid", text: "Drowned Bastion checkpoint flags no longer reset after fast travel." },
{ scope: "Co-op", text: "Fixed clients desyncing when the Tidecaller submerges during phase two." },
{ scope: "Crash", text: "Fixed a crash on launch at 3440x1440 and wider resolutions." },
{ scope: "UI", text: "Patch banner no longer overlaps the minimap on the HUD scale 120% preset." }
],
balance: [
{ scope: "Enemies", text: "Tidecaller adds spawn 15% slower on Apocalypse difficulty." }
]
}
},
{
id: "1.4.0",
kind: "major",
codename: "Ashen Tides",
date: "June 2, 2026",
summary: "The Drowned Bastion raid, the Stormcaller class, crossplay parties, and a full weapon-tuning pass.",
highlights: [
"New 4-player raid: the Drowned Bastion, with three bosses and a hidden vault.",
"New playable class: Stormcaller — channel tempest sigils to chain lightning between marked foes.",
"Crossplay parties between PC, console, and cloud are now live.",
"Photo mode ships with 12 filters and free-cam orbit controls."
],
sections: {
added: [
{ scope: "Raid", text: "Drowned Bastion raid (Power 540+): three encounters, vault puzzle, and the Leviathan-class boss Maelketh." },
{ scope: "Classes", text: "Stormcaller class with two subclass trees: Tempest Herald and Gravewind." },
{ scope: "Weapons", text: "Six new weapons including the Riptide Crossbow and the greatblade Hullsplitter." },
{ scope: "Social", text: "Crossplay party finder with role tags (Tank / Support / Burst)." },
{ scope: "World", text: "Photo mode: free cam, depth of field, and 12 color-grade filters." },
{ scope: "Audio", text: "New dynamic raid score by the Nullforge audio team, with per-phase stems." }
],
changed: [
{ scope: "UI", text: "Inventory grid rebuilt: drag-to-compare, loadout pinning, and bulk salvage." },
{ scope: "World", text: "Hollowmere hub re-lit at night; vendor stalls relocated near the fast-travel obelisk." },
{ scope: "Performance", text: "Shader pre-compilation now runs during the first loading screen, cutting hitching in Emberfall by ~40%." },
{ scope: "Quests", text: "Side-quest tracker now groups objectives by region instead of acquisition order." }
],
fixed: [
{ scope: "Weapons", text: "Riptide Crossbow bolts no longer pass through Maelketh's shield during the flood phase." },
{ scope: "Quests", text: "Fixed 'Embers of the Old King' failing to advance if the brazier was lit before the dialogue ended." },
{ scope: "Co-op", text: "Revive prompts now appear reliably when a downed ally is on a slope." },
{ scope: "Audio", text: "Footstep audio no longer doubles when sprinting on metal grates." },
{ scope: "UI", text: "Fixed overlapping damage numbers when more than eight enemies are tagged." }
],
balance: [
{ scope: "Weapons", text: "Riptide Crossbow: base damage 64 → 58, reload speed +10%." },
{ scope: "Classes", text: "Stormcaller 'Chain Surge' bounce count 5 → 4 in PvP only." },
{ scope: "Classes", text: "Warden 'Bulwark Oath' damage reduction 30% → 26%; cooldown 22s → 18s." },
{ scope: "Enemies", text: "Hollow Sentinels telegraph their slam 0.3s longer on all difficulties." },
{ scope: "Economy", text: "Vault clears award 25% more Ashmarks on first weekly completion." }
]
}
},
{
id: "1.3.5",
kind: "update",
codename: "Quality of Reign",
date: "April 21, 2026",
summary: "A quality-of-life pass: controller remapping, stash search, and dozens of community-reported fixes.",
highlights: [
"Full controller remapping with three savable presets.",
"Stash search and sort-by-power arrive for all storage tabs."
],
sections: {
added: [
{ scope: "UI", text: "Stash search bar with fuzzy matching and rarity filters." },
{ scope: "Input", text: "Controller remapping with three savable presets per profile." },
{ scope: "World", text: "Training dummies in Hollowmere now display live DPS readouts." }
],
changed: [
{ scope: "UI", text: "Vendor buyback tab retains items for 7 days, up from 24 hours." },
{ scope: "Performance", text: "Texture streaming pool auto-scales on 8GB GPUs, reducing pop-in at Emberfall gates." }
],
fixed: [
{ scope: "Quests", text: "'The Salt Choir' no longer blocks progress if all three bells are rung out of order." },
{ scope: "Weapons", text: "Hullsplitter heavy attack correctly consumes one stamina bar instead of two." },
{ scope: "Co-op", text: "Trade window no longer cancels when either player opens the map." },
{ scope: "UI", text: "Fixed subtitle timing drift in pre-rendered cutscenes above 60 fps." }
],
balance: [
{ scope: "Economy", text: "Salvage yields +1 Ember Core on Legendary items." },
{ scope: "Classes", text: "Gravewind dodge-cancel window 0.2s → 0.25s." }
]
}
},
{
id: "1.3.0",
kind: "major",
codename: "Hollow Reign: Emberfall",
date: "March 3, 2026",
summary: "The Emberfall region, mounted traversal, and the seasonal Ashen Vanguard event.",
highlights: [
"New region: Emberfall — a burning caldera city with 14 side quests.",
"Mounts arrive: tame and ride the cinder-wolves of the ash plains.",
"Seasonal event: Ashen Vanguard, with an exclusive armor set."
],
sections: {
added: [
{ scope: "World", text: "Emberfall region: caldera city, ash plains, and the Kiln-Warden stronghold." },
{ scope: "World", text: "Mount system with three tamable cinder-wolf variants and mounted sprint." },
{ scope: "Events", text: "Ashen Vanguard seasonal event with weekly war-effort milestones." },
{ scope: "Weapons", text: "Flame-forged weapon tier with heat buildup mechanics." }
],
changed: [
{ scope: "UI", text: "World map rebuilt with region zoom levels and custom pins (up to 20)." },
{ scope: "Quests", text: "Main story quests now show recommended Power on the accept screen." }
],
fixed: [
{ scope: "World", text: "Players can no longer clip through the Hollowmere aqueduct using the grapple." },
{ scope: "Audio", text: "Mount footsteps respect surface materials (ash, stone, water)." },
{ scope: "Crash", text: "Fixed a rare crash when alt-tabbing during the Emberfall arrival cutscene." }
],
balance: [
{ scope: "Enemies", text: "Kiln-Warden elite health −12% in solo play; unchanged in co-op." },
{ scope: "Weapons", text: "Heat buildup decays 20% faster out of combat." },
{ scope: "Economy", text: "Mount taming reagents drop from all ash-plain bounties, not only world bosses." }
]
}
},
{
id: "1.2.1",
kind: "hotfix",
codename: "Stability Hotfix",
date: "January 19, 2026",
summary: "Targeted crash and progression fixes following the Hollow Depths update.",
highlights: [
"Fixed the most-reported crash in the Hollow Depths elevator sequence."
],
sections: {
added: [],
changed: [
{ scope: "Performance", text: "Reduced VRAM usage in the Hollow Depths by streaming distant geometry tiers." }
],
fixed: [
{ scope: "Crash", text: "Fixed a crash during the Hollow Depths elevator descent on DX12." },
{ scope: "Quests", text: "'Beneath the Reign' boss door now opens if the player dies during the key animation." },
{ scope: "UI", text: "Fixed the skill tree tooltip lingering after closing the menu with a controller." }
],
balance: []
}
},
{
id: "1.2.0",
kind: "update",
codename: "Hollow Depths",
date: "January 12, 2026",
summary: "The Hollow Depths endgame dungeon, weekly mutators, and leaderboards.",
highlights: [
"Endgame dungeon: the Hollow Depths, with rotating weekly mutators.",
"Region leaderboards for fastest clears, solo and co-op."
],
sections: {
added: [
{ scope: "Dungeon", text: "Hollow Depths endgame dungeon with five floors and weekly mutators." },
{ scope: "Social", text: "Clear-time leaderboards (solo / duo / full party) per region." },
{ scope: "UI", text: "Death recap screen showing the last three damage sources." }
],
changed: [
{ scope: "World", text: "Fast travel between discovered obelisks is now free below Power 300." },
{ scope: "UI", text: "Damage numbers can be set to Compact, Full, or Off." }
],
fixed: [
{ scope: "Weapons", text: "Charged bow shots no longer lose charge when opening doors." },
{ scope: "Co-op", text: "Party leader migration no longer drops the dungeon mutator state." }
],
balance: [
{ scope: "Enemies", text: "Depths mutator 'Frenzied' attack-speed bonus 35% → 25%." },
{ scope: "Classes", text: "Tempest Herald lightning DoT can now crit; base tick damage −8%." }
]
}
}
];
var SECTION_META = [
{ key: "added", label: "Added", tagClass: "tag-added" },
{ key: "changed", label: "Changed", tagClass: "tag-changed" },
{ key: "fixed", label: "Fixed", tagClass: "tag-fixed" },
{ key: "balance", label: "Balance", tagClass: "tag-balance" }
];
var KIND_LABEL = { major: "Major", hotfix: "Hotfix", update: "Update" };
/* ===== State ===== */
var state = {
versionId: PATCHES[0].id,
category: "all",
query: ""
};
/* ===== Element refs ===== */
var versionList = document.getElementById("version-list");
var notesPane = document.getElementById("notes-pane");
var searchInput = document.getElementById("search-input");
var searchClear = document.getElementById("search-clear");
var chipRow = document.getElementById("chip-row");
var resultCount = document.getElementById("result-count");
var toastEl = document.getElementById("toast");
/* ===== Helpers ===== */
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
function escapeHtml(str) {
return str.replace(/[&<>"']/g, function (ch) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
});
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/** Escape text and wrap query matches in <mark>. */
function markText(text, query) {
var safe = escapeHtml(text);
if (!query) return safe;
var re = new RegExp("(" + escapeRegExp(escapeHtml(query)) + ")", "gi");
return safe.replace(re, "<mark>$1</mark>");
}
function matchesQuery(entry, query) {
if (!query) return true;
var q = query.toLowerCase();
return (
entry.text.toLowerCase().indexOf(q) !== -1 ||
entry.scope.toLowerCase().indexOf(q) !== -1
);
}
function getPatch(id) {
for (var i = 0; i < PATCHES.length; i++) {
if (PATCHES[i].id === id) return PATCHES[i];
}
return PATCHES[0];
}
/* ===== Render: version rail ===== */
function renderVersionList() {
versionList.innerHTML = PATCHES.map(function (p) {
var current = p.id === state.versionId;
return (
'<li>' +
'<button class="version-btn" type="button" data-version="' + p.id + '" aria-current="' + current + '">' +
'<span class="version-num">v' + p.id +
' <span class="version-kind kind-' + p.kind + '">' + KIND_LABEL[p.kind] + "</span></span>" +
'<span class="version-date">' + escapeHtml(p.date) + "</span>" +
"</button></li>"
);
}).join("");
}
/* ===== Render: notes pane ===== */
function renderNotes() {
var patch = getPatch(state.versionId);
var query = state.query.trim();
var totalShown = 0;
var html = "";
// Patch header
html +=
'<header class="patch-head">' +
'<p class="patch-eyebrow">Patch ' + patch.id + " · " + KIND_LABEL[patch.kind] + "</p>" +
'<h1 class="patch-title">' + escapeHtml(patch.codename) + "</h1>" +
'<p class="patch-meta">Released <strong>' + escapeHtml(patch.date) + "</strong> — " +
escapeHtml(patch.summary) + "</p>" +
"</header>";
// Highlights (only when not searching/filtering away)
if (!query && state.category === "all" && patch.highlights.length) {
html +=
'<section class="highlights" aria-label="Patch highlights">' +
'<div class="highlights-head">' +
'<span class="pin-badge">Pinned</span>' +
'<h2 class="highlights-title">Highlights</h2>' +
"</div>" +
'<ul class="highlights-list">' +
patch.highlights.map(function (h) {
return "<li>" + escapeHtml(h) + "</li>";
}).join("") +
"</ul></section>";
}
// Sections
SECTION_META.forEach(function (meta) {
if (state.category !== "all" && state.category !== meta.key) return;
var entries = patch.sections[meta.key].filter(function (e) {
return matchesQuery(e, query);
});
if (!entries.length) return;
totalShown += entries.length;
html +=
'<section class="note-section" data-section="' + meta.key + '">' +
'<h2 style="margin:0">' +
'<button class="section-toggle" type="button" aria-expanded="true" aria-controls="body-' + meta.key + '">' +
'<span class="section-tag ' + meta.tagClass + '">' + meta.label + "</span>" +
'<span class="section-count">' + entries.length +
(entries.length === 1 ? " entry" : " entries") + "</span>" +
'<svg class="section-caret" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg>' +
"</button></h2>" +
'<div class="section-body" id="body-' + meta.key + '">' +
'<ul class="entry-list">' +
entries.map(function (e) {
return (
"<li>" +
'<span class="entry-scope">' + escapeHtml(e.scope) + "</span>" +
'<span class="entry-text">' + markText(e.text, query) + "</span>" +
"</li>"
);
}).join("") +
"</ul></div></section>";
});
// Empty state
if (totalShown === 0) {
html +=
'<div class="empty-state">' +
'<span class="empty-icon" aria-hidden="true">∅</span>' +
"<p>No entries match " +
(query ? "“" + escapeHtml(query) + "”" : "this filter") +
" in patch " + patch.id + ". Try another keyword or category.</p>" +
"</div>";
}
notesPane.innerHTML = html;
// Result counter
if (query || state.category !== "all") {
resultCount.innerHTML =
"Showing <strong>" + totalShown + "</strong> " +
(totalShown === 1 ? "entry" : "entries") +
(state.category !== "all" ? " in <strong>" + state.category + "</strong>" : "") +
(query ? ' matching “<strong>' + escapeHtml(query) + "</strong>”" : "") +
" — patch " + patch.id;
} else {
resultCount.textContent = "";
}
}
function render() {
renderVersionList();
renderNotes();
}
/* ===== Events ===== */
versionList.addEventListener("click", function (ev) {
var btn = ev.target.closest(".version-btn");
if (!btn) return;
var id = btn.getAttribute("data-version");
if (id === state.versionId) return;
state.versionId = id;
render();
var patch = getPatch(id);
toast("Viewing patch " + patch.id + " — " + patch.codename);
});
chipRow.addEventListener("click", function (ev) {
var chip = ev.target.closest(".chip");
if (!chip) return;
state.category = chip.getAttribute("data-cat");
Array.prototype.forEach.call(chipRow.querySelectorAll(".chip"), function (c) {
c.setAttribute("aria-pressed", String(c === chip));
});
renderNotes();
});
searchInput.addEventListener("input", function () {
state.query = searchInput.value;
searchClear.hidden = searchInput.value.length === 0;
renderNotes();
});
searchInput.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && searchInput.value) {
searchInput.value = "";
state.query = "";
searchClear.hidden = true;
renderNotes();
}
});
searchClear.addEventListener("click", function () {
searchInput.value = "";
state.query = "";
searchClear.hidden = true;
renderNotes();
searchInput.focus();
});
// Collapsible sections (delegated — pane re-renders often)
notesPane.addEventListener("click", function (ev) {
var toggle = ev.target.closest(".section-toggle");
if (!toggle) return;
var expanded = toggle.getAttribute("aria-expanded") === "true";
toggle.setAttribute("aria-expanded", String(!expanded));
var body = document.getElementById(toggle.getAttribute("aria-controls"));
if (body) body.hidden = expanded;
});
document.getElementById("btn-play").addEventListener("click", function () {
toast("Launching Hollow Reign… (demo only)");
});
document.getElementById("btn-rss").addEventListener("click", function () {
toast("Subscribed to patch-note updates (demo only)");
});
/* ===== Init ===== */
document.getElementById("live-version").textContent = PATCHES[0].id;
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hollow Reign — Patch Notes</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<!-- ===== Header ===== -->
<header class="site-header">
<div class="brand">
<div class="brand-mark" aria-hidden="true">HR</div>
<div class="brand-text">
<span class="brand-game">Hollow Reign</span>
<span class="brand-sub">Patch Notes · Nullforge Studios</span>
</div>
</div>
<div class="header-meta">
<span class="live-badge" id="live-badge">
<span class="live-dot" aria-hidden="true"></span>
Live build <strong id="live-version">1.4.2</strong>
</span>
<button class="btn btn-primary" type="button" id="btn-play">Play Now</button>
</div>
</header>
<!-- ===== Toolbar: search + category chips ===== -->
<section class="toolbar" aria-label="Filter patch notes">
<div class="search-wrap">
<svg class="search-icon" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"></circle>
<line x1="21" y1="21" x2="16.5" y2="16.5"></line>
</svg>
<input type="search" id="search-input" class="search-input"
placeholder="Search notes… e.g. crossbow, raid, fps" aria-label="Search patch notes" autocomplete="off" />
<button type="button" class="search-clear" id="search-clear" aria-label="Clear search" hidden>×</button>
</div>
<div class="chip-row" role="group" aria-label="Filter by category" id="chip-row">
<button class="chip" type="button" data-cat="all" aria-pressed="true">All</button>
<button class="chip chip-added" type="button" data-cat="added" aria-pressed="false">Added</button>
<button class="chip chip-changed" type="button" data-cat="changed" aria-pressed="false">Changed</button>
<button class="chip chip-fixed" type="button" data-cat="fixed" aria-pressed="false">Fixed</button>
<button class="chip chip-balance" type="button" data-cat="balance" aria-pressed="false">Balance</button>
</div>
<p class="result-count" id="result-count" role="status" aria-live="polite"></p>
</section>
<!-- ===== Main layout ===== -->
<div class="layout">
<!-- Version selector sidebar -->
<aside class="version-rail" aria-label="Patch versions">
<h2 class="rail-title">Versions</h2>
<nav>
<ul class="version-list" id="version-list"><!-- rendered by JS --></ul>
</nav>
<div class="rail-foot">
<button class="btn btn-ghost btn-small" type="button" id="btn-rss">Subscribe to updates</button>
</div>
</aside>
<!-- Notes pane -->
<main class="notes-pane" id="notes-pane" aria-live="polite">
<!-- rendered by JS: patch header, highlights, sections -->
</main>
</div>
<footer class="site-footer">
<p>Hollow Reign © Nullforge Studios — fictional title for UI demonstration.</p>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Patch Notes / Changelog Layout
A neon-on-charcoal changelog for Hollow Reign by the fictional Nullforge Studios. A version rail on the left lists every shipped build — major updates, hotfixes, and quality-of-life passes — each tagged with its kind and release date. Selecting a version swaps the notes pane to that patch’s codename, summary, and grouped changes, while a live-build badge marks the current channel.
The notes pane organizes each patch into four accent-tagged sections — Added, Changed, Fixed, and Balance — with every entry carrying a small scope label (Raid, Weapons, UI, Netcode…) so readers can scan by system. A pinned Highlights block floats the headline changes to the top, and each section header doubles as a collapse toggle with an entry count.
Interactions are pure vanilla JS: type in the search box to filter entries by keyword in real time with inline match highlighting, click a category chip to narrow the view to a single change type, and collapse or expand any section. A result counter reports how many entries match, an empty state appears when nothing does, and a toast confirms version switches and demo actions. The layout reflows to a single column with stacked controls below 520px.
Illustrative UI only — fictional games, studios, characters, and data. Not engine integrations.