From 05397b04c29b2cddd9ee8213e8f09f005cc68d51 Mon Sep 17 00:00:00 2001 From: Enrique Velez <145390508+enlike21@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:10:14 +0200 Subject: [PATCH 1/2] Split game.css into one stylesheet per section game.css had grown to ~1400 lines. It is now six files under public/styles/pages/game/ (base, hero, box, overlays, sidebar, route), each with its own responsive rules, loaded in order by the game template. The rules are the same: the computed styles of every element match the old file at desktop and phone widths, in the start, pause and game-over states. Closes #17 --- public/styles/pages/game.css | 1557 ------------------------- public/styles/pages/game/base.css | 82 ++ public/styles/pages/game/box.css | 309 +++++ public/styles/pages/game/hero.css | 188 +++ public/styles/pages/game/overlays.css | 472 ++++++++ public/styles/pages/game/route.css | 137 +++ public/styles/pages/game/sidebar.css | 401 +++++++ templates/game/index.html.twig | 8 +- 8 files changed, 1596 insertions(+), 1558 deletions(-) delete mode 100644 public/styles/pages/game.css create mode 100644 public/styles/pages/game/base.css create mode 100644 public/styles/pages/game/box.css create mode 100644 public/styles/pages/game/hero.css create mode 100644 public/styles/pages/game/overlays.css create mode 100644 public/styles/pages/game/route.css create mode 100644 public/styles/pages/game/sidebar.css diff --git a/public/styles/pages/game.css b/public/styles/pages/game.css deleted file mode 100644 index 12f411e..0000000 --- a/public/styles/pages/game.css +++ /dev/null @@ -1,1557 +0,0 @@ -/* ============================================ - GAME PAGE - "Al Filo" - Everything inside the game box (HUD, overlays, buttons, legend) is styled as pixel art to - match the canvas: Press Start 2P, hard 0-radius edges, and "stepped" borders built from - offset box-shadows (four 1-direction shadows leave the corners notched, the classic way to - fake a pixel frame in CSS). The sidebar keeps the regular site look. - No text-transform: uppercase on Spanish text - Press Start 2P has no accented capitals - (Í, É, Á...), so "CAÍDO" would render with a fallback glyph. - ============================================ */ - -.game-page { - --px-ink: #1b1420; - --px-panel: #241c38; - --px-panel-hi: #3a2e58; - --px-gold: #f2c25a; - --px-gold-hi: #ffe394; - --px-gold-lo: #a9781f; - --px-cream: #fff3d6; - --px-lilac: #a99cc8; - --px-red: #e2493b; - --px-teal: #2fd6c0; - --px-font: 'Press Start 2P', monospace; - padding: var(--spacing-xl) 0; - min-height: calc(100vh - 200px); -} - -.game-page .container { - max-width: 640px; - width: 100%; - transition: max-width 0.2s ease; -} - -/* Desktop gets a noticeably bigger playing field plus room for the sidebar */ -@media (min-width: 1024px) { - .game-page .container { - max-width: 1120px; - } -} - -/* Hero: a game title screen - banded night sky, stars, stepped pixel mountains */ -.game-hero { - position: relative; - overflow: hidden; - margin: 6px 6px calc(var(--spacing-lg) + 6px); - padding: 34px var(--spacing-md) 64px; - text-align: center; - background: linear-gradient(180deg, - #110c20 0 22%, #181230 22% 42%, #211940 42% 60%, #2c2152 60% 76%, #3a2c64 76% 100%); - box-shadow: - 0 -6px 0 0 var(--px-panel-hi), - 0 6px 0 0 var(--px-panel-hi), - -6px 0 0 0 var(--px-panel-hi), - 6px 0 0 0 var(--px-panel-hi), - 0 14px 0 0 rgba(0, 0, 0, 0.35); -} - -.game-hero-sky, -.game-hero-ridges { - position: absolute; - left: 0; - width: 100%; - pointer-events: none; -} - -.game-hero-sky { - top: 0; - height: 100%; -} - -/* The ridges only fill the bottom band, under the stat boxes, never behind the text */ -.game-hero-ridges { - bottom: 0; - height: 46%; -} - -.game-hero-stars rect { - fill: var(--px-cream); - animation: gameTwinkle 2.4s steps(1) infinite; -} - -.game-hero-stars rect:nth-child(3n) { - animation-delay: -0.8s; -} - -.game-hero-stars rect:nth-child(3n + 1) { - animation-delay: -1.6s; -} - -@keyframes gameTwinkle { - 0%, 70% { opacity: 1; } - 71%, 100% { opacity: 0.25; } -} - -.game-hero-moon rect { - fill: #e8e6f6; -} - -.game-hero-ridge-far { - fill: #3f3470; -} - -.game-hero-ridge-near { - fill: var(--px-ink); -} - -.game-hero-content { - position: relative; -} - -.game-page-badge { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 7px 10px 6px; - background: var(--px-ink); - color: var(--px-gold); - font-family: var(--px-font); - font-size: 8px; - line-height: 1.4; - margin-bottom: 18px; - box-shadow: - 0 -2px 0 0 var(--px-gold-lo), - 0 2px 0 0 var(--px-gold-lo), - -2px 0 0 0 var(--px-gold-lo), - 2px 0 0 0 var(--px-gold-lo); -} - -.game-hero-title { - font-family: var(--px-font); - font-size: 2.6rem; - font-weight: 400; - line-height: 1.2; - margin-bottom: 16px; - color: var(--px-cream); - text-shadow: 0 5px 0 #8a7fa8, 5px 10px 0 var(--px-ink); -} - -.game-hero-title span { - color: var(--px-gold); - text-shadow: 0 5px 0 var(--px-gold-lo), 5px 10px 0 var(--px-ink); -} - -.game-hero-lead { - color: #d8d0ea; - font-size: 1rem; - line-height: 1.6; - max-width: 520px; - margin: 0 auto 22px; - text-shadow: 0 2px 0 var(--px-ink); -} - -.game-hero-stats { - list-style: none; - display: inline-flex; - flex-wrap: wrap; - justify-content: center; - gap: 14px; - margin: 0; - padding: 0; -} - -.game-hero-stats li { - display: flex; - flex-direction: column; - gap: 8px; - min-width: 110px; - padding: 12px 12px 10px; - background: var(--px-panel); - box-shadow: - 0 -3px 0 0 var(--px-ink), - 0 3px 0 0 var(--px-ink), - -3px 0 0 0 var(--px-ink), - 3px 0 0 0 var(--px-ink), - inset 0 3px 0 0 var(--px-panel-hi); -} - -.game-hero-stats strong { - font-family: var(--px-font); - font-weight: 400; - font-size: 14px; - color: var(--px-gold); - text-shadow: 2px 2px 0 var(--px-ink); -} - -.game-hero-stats span { - font-family: var(--px-font); - font-size: 8px; - color: var(--px-lilac); -} - -/* Layout: single column on mobile, game + sidebar on desktop */ -.game-layout { - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-lg); - align-items: start; -} - -/* Grid items default to min-width: auto, which refuses to shrink below the width of their - content (the HUD row, in this case) - that was pushing the whole page into horizontal - scroll on narrow phones instead of letting the HUD wrap. */ -.game-main { - min-width: 0; -} - -@media (min-width: 1024px) { - .game-layout { - grid-template-columns: 1fr 300px; - } -} - -/* Game box: a pixel frame instead of the site's rounded glass card */ -.game-wrap { - background: var(--px-ink); - border-radius: 0; - margin: 6px; - box-shadow: - 0 -6px 0 0 var(--px-panel-hi), - 0 6px 0 0 var(--px-panel-hi), - -6px 0 0 0 var(--px-panel-hi), - 6px 0 0 0 var(--px-panel-hi), - 0 14px 0 0 rgba(0, 0, 0, 0.35); -} - -/* HUD */ -.game-hud { - display: flex; - align-items: center; - gap: var(--spacing-lg); - padding: 12px var(--spacing-md); - background: var(--px-panel); - border-bottom: 4px solid var(--px-ink); - box-shadow: inset 0 -4px 0 0 var(--px-panel-hi); -} - -.game-hud-item { - display: flex; - align-items: center; - gap: 10px; -} - -.game-hud-icon { - font-size: 0.95rem; - color: var(--px-lilac); - width: 16px; - text-align: center; -} - -.game-hud-icon-record { - color: var(--px-gold); -} - -.game-hud-item > div { - display: flex; - flex-direction: column; - gap: 6px; -} - -.game-hud-label { - font-family: var(--px-font); - font-size: 8px; - color: var(--px-lilac); -} - -.game-hud-value { - font-family: var(--px-font); - font-size: 13px; - color: var(--px-gold); - text-shadow: 2px 2px 0 var(--px-ink); -} - -.game-hud-stamina { - flex: 1; - min-width: 90px; -} - -.game-hud-stamina-inner { - width: 100%; -} - -.game-stamina-bar { - position: relative; - width: 100%; - height: 12px; - background: var(--px-ink); - overflow: hidden; - box-shadow: - 0 -2px 0 0 var(--px-panel-hi), - 0 2px 0 0 var(--px-panel-hi), - -2px 0 0 0 var(--px-panel-hi), - 2px 0 0 0 var(--px-panel-hi); -} - -/* Segment notches over the fill - reads as a row of blocks, like a retro health bar */ -.game-stamina-bar::after { - content: ''; - position: absolute; - inset: 0; - background: repeating-linear-gradient(90deg, transparent 0 10px, var(--px-ink) 10px 12px); - pointer-events: none; -} - -.game-stamina-fill { - height: 100%; - width: 100%; - background: linear-gradient(180deg, #9cf5b4 0 25%, #38c46e 25% 70%, #1f8a4f 70% 100%); - transition: width 0.15s steps(6); -} - -.game-stamina-bar.is-low .game-stamina-fill { - background: linear-gradient(180deg, #ff9b8a 0 25%, var(--px-red) 25% 70%, #9c2a22 70% 100%); - animation: gameStaminaLowPulse 0.7s steps(2) infinite; -} - -@keyframes gameStaminaLowPulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.45; } -} - -/* A quick celebratory flash when stamina is topped up by something earned - (a precise landing, a combo, a clip) rather than just the slow passive regen. */ -.game-stamina-fill.is-boosted { - animation: gameStaminaBoost 0.45s steps(3); -} - -@keyframes gameStaminaBoost { - 0% { filter: brightness(1); } - 30% { filter: brightness(1.9); } - 100% { filter: brightness(1); } -} - -/* Canvas holder */ -.game-canvas-holder { - position: relative; - width: 100%; - height: min(68vh, 560px); - min-height: 380px; - touch-action: none; - overflow: hidden; - user-select: none; - -webkit-user-select: none; -} - -@media (min-width: 1024px) { - .game-canvas-holder { - height: min(74vh, 680px); - min-height: 440px; - } -} - -#gameCanvas { - display: block; - width: 100%; - height: 100%; - touch-action: none; - cursor: crosshair; - -webkit-user-drag: none; - user-select: none; -} - -/* Power bar (vertical, shows charge while held) */ -.game-power-bar { - position: absolute; - right: 16px; - top: 16px; - bottom: 16px; - width: 14px; - background: var(--px-ink); - overflow: hidden; - opacity: 0; - pointer-events: none; - display: flex; - align-items: flex-end; - box-shadow: - 0 -3px 0 0 var(--px-cream), - 0 3px 0 0 var(--px-cream), - -3px 0 0 0 var(--px-cream), - 3px 0 0 0 var(--px-cream); -} - -.game-power-bar::after { - content: ''; - position: absolute; - inset: 0; - background: repeating-linear-gradient(0deg, transparent 0 10px, var(--px-ink) 10px 12px); - pointer-events: none; -} - -.game-power-bar.is-visible { - opacity: 1; -} - -.game-power-fill { - width: 100%; - height: 0%; - background: linear-gradient(90deg, var(--px-gold-hi) 0 30%, var(--px-gold) 30% 70%, var(--px-gold-lo) 70% 100%); -} - -.game-power-fill.is-maxed { - background: linear-gradient(90deg, #ffc08a 0 30%, #ff6b4a 30% 70%, #b8402a 70% 100%); - animation: gamePowerPulse 0.3s steps(2) infinite; -} - -@keyframes gamePowerPulse { - 0%, 100% { filter: brightness(1); } - 50% { filter: brightness(1.5); } -} - -/* Pause + sound buttons, top-left of the canvas */ -.game-corner-buttons { - position: absolute; - top: 14px; - left: 14px; - z-index: 3; - display: flex; - gap: 12px; -} - -.game-sound-btn { - width: 34px; - height: 34px; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - border: none; - border-radius: 0; - background: var(--px-panel); - color: var(--px-gold); - font-size: 0.8rem; - cursor: pointer; - box-shadow: - 0 -3px 0 0 var(--px-ink), - 0 3px 0 0 var(--px-ink), - -3px 0 0 0 var(--px-ink), - 3px 0 0 0 var(--px-ink), - inset -3px -3px 0 0 var(--px-ink), - inset 3px 3px 0 0 var(--px-panel-hi); -} - -.game-sound-btn:hover { - background: var(--px-panel-hi); -} - -.game-sound-btn.is-muted { - color: var(--px-lilac); -} - -.game-mode-tag { - margin-left: 8px; - padding: 2px 4px 1px; - background: var(--px-ink); - color: var(--px-gold); -} - -/* First-run tutorial prompt and achievement toasts, over the canvas */ -.game-tutorial, -.game-toast { - position: absolute; - left: 50%; - z-index: 3; - width: max-content; - max-width: calc(100% - 120px); - margin: 0; - padding: 10px 12px 9px; - transform: translateX(-50%); - background: var(--px-panel); - color: var(--px-cream); - font-family: var(--px-font); - font-size: 9px; - line-height: 1.8; - text-align: center; - pointer-events: none; - box-shadow: - 0 -3px 0 0 var(--px-ink), - 0 3px 0 0 var(--px-ink), - -3px 0 0 0 var(--px-ink), - 3px 0 0 0 var(--px-ink), - inset 0 0 0 3px var(--px-gold); -} - -.game-tutorial { - bottom: 18px; -} - -.game-toast { - top: 64px; - color: var(--px-gold); - opacity: 0; - transform: translate(-50%, 12px); - transition: opacity 0.2s steps(3), transform 0.2s steps(3); -} - -.game-toast.is-visible { - opacity: 1; - transform: translate(-50%, 0); -} - -/* Overlays: no blur, so the animated pixel scene stays visible behind the panel, - just dimmed and with faint scanlines for a CRT feel. */ -.game-overlay { - position: absolute; - inset: 0; - z-index: 4; - display: flex; - align-items: center; - justify-content: center; - background: - repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.18) 0 2px, transparent 2px 4px), - rgba(14, 8, 26, 0.5); - padding: 14px; - text-align: center; - overflow-y: auto; -} - -.game-overlay-hidden { - display: none; -} - -.game-overlay-card { - max-width: 400px; - margin: auto; - padding: 20px 20px 18px; - background: var(--px-panel); - box-shadow: - 0 -4px 0 0 var(--px-ink), - 0 4px 0 0 var(--px-ink), - -4px 0 0 0 var(--px-ink), - 4px 0 0 0 var(--px-ink), - inset 0 0 0 4px var(--px-gold), - inset 0 0 0 6px var(--px-gold-lo), - inset 0 0 0 8px var(--px-ink), - inset 0 10px 0 0 var(--px-panel-hi), - 8px 12px 0 0 rgba(0, 0, 0, 0.45); - animation: gameCardIn 0.28s steps(4); -} - -@keyframes gameCardIn { - 0% { transform: translateY(16px) scale(0.92); opacity: 0; } - 100% { transform: none; opacity: 1; } -} - -/* The climber's own sprite (drawn by js/game/main.js) instead of a font icon */ -.game-overlay-sprite { - display: block; - margin: 0 auto 10px; - image-rendering: pixelated; - image-rendering: crisp-edges; - animation: gameSpriteBob 1s steps(2) infinite; -} - -@keyframes gameSpriteBob { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-4px); } -} - -.game-overlay.is-win .game-overlay-sprite { - animation: gameSpriteBob 0.4s steps(2) infinite; -} - -.game-overlay-card h2 { - font-family: var(--px-font); - font-size: 18px; - font-weight: 400; - line-height: 1.4; - color: var(--px-gold); - text-shadow: 0 3px 0 var(--px-gold-lo), 3px 6px 0 var(--px-ink); - margin-bottom: 14px; -} - -.game-overlay-card .game-title { - font-size: 28px; - text-shadow: 0 4px 0 var(--px-gold-lo), 4px 8px 0 var(--px-ink); -} - -.game-overlay.is-win .game-overlay-card h2 { - animation: gameTitleFlash 0.6s steps(2) infinite; -} - -@keyframes gameTitleFlash { - 0%, 100% { color: var(--px-gold); } - 50% { color: var(--px-cream); } -} - -.game-overlay-card p { - font-family: var(--px-font); - color: var(--px-cream); - font-size: 9px; - line-height: 1.9; - margin-bottom: 10px; -} - -.game-overlay-card > .game-btn { - margin-top: 14px; -} - -/* Blinking "press start", arcade style */ -.game-press-start { - margin: 14px 0 0 !important; - color: var(--px-gold) !important; - animation: gameBlink 1s steps(1) infinite; -} - -/* No space bar on a touch screen */ -@media (hover: none) { - .game-press-start { - display: none; - } -} - -@keyframes gameBlink { - 0%, 60% { opacity: 1; } - 61%, 100% { opacity: 0; } -} - -.game-overlay-score { - font-size: 11px !important; - color: var(--px-cream) !important; -} - -.game-overlay-score strong { - display: block; - margin-top: 6px; - font-weight: 400; - font-size: 20px; - color: var(--px-gold); - text-shadow: 0 3px 0 var(--px-gold-lo), 3px 5px 0 var(--px-ink); -} - -.game-overlay-record { - color: var(--px-lilac) !important; - margin-bottom: 14px !important; -} - -.game-overlay-actions { - display: flex; - flex-direction: row; - justify-content: center; - gap: 16px; - margin-top: 12px; -} - -.game-overlay-actions .game-btn { - flex: 0 0 auto; - width: 52px; - height: 48px; - padding: 0; - font-size: 1rem; -} - -.game-mode-buttons { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 12px; - margin-top: 14px; -} - -.game-mode-buttons .game-btn { - font-size: 10px; -} - -.game-daily-best { - margin: 12px 0 0 !important; - color: var(--px-lilac) !important; - font-size: 8px !important; -} - -.game-pause-actions { - display: flex; - flex-direction: column; - align-items: stretch; - gap: 10px; - max-width: 240px; - margin: 0 auto 18px; -} - -.game-pause-actions .game-btn { - font-size: 10px; -} - -.game-settings { - display: flex; - flex-direction: column; - gap: 12px; - padding: 14px 12px 12px; - background: var(--px-ink); - text-align: left; -} - -.game-setting { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - font-family: var(--px-font); - font-size: 8px; - color: var(--px-cream); - cursor: pointer; -} - -.game-setting i { - color: var(--px-gold); - margin-right: 6px; -} - -/* Pixel slider: a flat ink track with a cream block thumb */ -.game-setting input[type="range"] { - flex: 1; - max-width: 150px; - height: 16px; - margin: 0; - background: transparent; - -webkit-appearance: none; - appearance: none; - cursor: pointer; -} - -.game-setting input[type="range"]::-webkit-slider-runnable-track { - height: 6px; - background: var(--px-panel-hi); - box-shadow: 0 0 0 2px var(--px-ink); -} - -.game-setting input[type="range"]::-moz-range-track { - height: 6px; - background: var(--px-panel-hi); - box-shadow: 0 0 0 2px var(--px-ink); -} - -.game-setting input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 12px; - height: 16px; - margin-top: -5px; - border: none; - border-radius: 0; - background: var(--px-gold); - box-shadow: 0 0 0 2px var(--px-ink); -} - -.game-setting input[type="range"]::-moz-range-thumb { - width: 12px; - height: 16px; - border: none; - border-radius: 0; - background: var(--px-gold); - box-shadow: 0 0 0 2px var(--px-ink); -} - -.game-setting-check { - justify-content: flex-start; -} - -.game-setting-check input { - width: 16px; - height: 16px; - margin: 0; - accent-color: var(--px-gold); -} - -.game-over-achievements { - list-style: none; - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 8px; - margin: 4px 0 12px; - padding: 0; -} - -.game-over-achievements li { - padding: 6px 8px 5px; - background: var(--px-ink); - color: var(--px-gold); - font-family: var(--px-font); - font-size: 8px; - box-shadow: inset 0 0 0 2px var(--px-gold-lo); -} - -/* Permanent controls legend strip under the canvas */ -.game-controls-legend { - display: flex; - flex-wrap: wrap; - gap: 10px var(--spacing-lg); - padding: 12px var(--spacing-md); - background: var(--px-panel); - border-top: 4px solid var(--px-ink); - box-shadow: inset 0 4px 0 0 var(--px-panel-hi); - font-family: var(--px-font); - font-size: 8px; - line-height: 2; - color: var(--px-lilac); -} - -.game-controls-legend span { - display: block; -} - -.game-controls-legend i { - color: var(--px-gold); - font-size: 0.8rem; - margin-right: 6px; -} - -/* Keycaps */ -.game-controls-legend kbd { - display: inline-block; - min-width: 18px; - padding: 4px 5px 3px; - border-radius: 0; - background: var(--px-cream); - color: var(--px-ink); - font-family: var(--px-font), 'Inter', sans-serif; - font-size: 9px; - line-height: 1; - text-align: center; - box-shadow: - 0 -2px 0 0 var(--px-ink), - 0 2px 0 0 var(--px-ink), - -2px 0 0 0 var(--px-ink), - 2px 0 0 0 var(--px-ink), - inset 0 -2px 0 0 #b8a98a; -} - -/* Buttons: chunky pixel buttons with a bevel that flips when pressed */ -.game-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 10px; - padding: 14px 18px; - border: none; - border-radius: 0; - font-family: var(--px-font); - font-size: 11px; - font-weight: 400; - line-height: 1.4; - cursor: pointer; - text-decoration: none !important; - margin: 4px; -} - -.game-btn-primary { - background: var(--px-gold); - color: var(--px-ink); - box-shadow: - 0 -4px 0 0 var(--px-ink), - 0 4px 0 0 var(--px-ink), - -4px 0 0 0 var(--px-ink), - 4px 0 0 0 var(--px-ink), - inset -4px -4px 0 0 var(--px-gold-lo), - inset 4px 4px 0 0 var(--px-gold-hi); -} - -.game-btn-primary:hover { - background: var(--px-gold-hi); - color: var(--px-ink); -} - -.game-btn-secondary { - background: var(--px-panel-hi); - color: var(--px-cream); - box-shadow: - 0 -4px 0 0 var(--px-ink), - 0 4px 0 0 var(--px-ink), - -4px 0 0 0 var(--px-ink), - 4px 0 0 0 var(--px-ink), - inset -4px -4px 0 0 #2a2140, - inset 4px 4px 0 0 #574a7e; -} - -.game-btn-secondary:hover { - background: #4a3c70; -} - -.game-btn:active { - transform: translateY(3px); -} - -.game-btn-primary:active { - box-shadow: - 0 -4px 0 0 var(--px-ink), - 0 4px 0 0 var(--px-ink), - -4px 0 0 0 var(--px-ink), - 4px 0 0 0 var(--px-ink), - inset 4px 4px 0 0 var(--px-gold-lo); -} - -.game-btn:focus-visible, -.game-sound-btn:focus-visible { - outline: 3px dashed var(--px-cream); - outline-offset: 6px; -} - -/* Panels: the sidebar cards and "La ruta" share the game box's pixel-frame look */ -.game-sidebar { - display: flex; - flex-direction: column; - gap: calc(var(--spacing-md) + 6px); - padding: 6px; -} - -.game-panel { - background: var(--px-panel); - padding: 18px 16px 16px; - box-shadow: - 0 -4px 0 0 var(--px-ink), - 0 4px 0 0 var(--px-ink), - -4px 0 0 0 var(--px-ink), - 4px 0 0 0 var(--px-ink), - inset 0 4px 0 0 var(--px-panel-hi), - 0 10px 0 0 rgba(0, 0, 0, 0.3); -} - -.game-panel-title { - display: flex; - align-items: center; - gap: 10px; - font-family: var(--px-font); - font-size: 11px; - font-weight: 400; - line-height: 1.4; - color: var(--px-gold); - text-shadow: 2px 2px 0 var(--px-ink); - margin: 0 0 16px; - padding-bottom: 12px; - border-bottom: 2px dashed var(--px-panel-hi); -} - -.game-panel-title i { - color: var(--px-gold); - font-size: 0.85rem; - text-shadow: none; -} - -/* How to play: numbered keycaps */ -.game-steps { - list-style: none; - counter-reset: game-step; - display: flex; - flex-direction: column; - gap: 12px; - margin: 0; - padding: 0; -} - -.game-steps li { - counter-increment: game-step; - display: flex; - align-items: flex-start; - gap: 12px; - font-size: 0.85rem; - color: #cfc6e2; - line-height: 1.5; -} - -.game-steps li::before { - content: counter(game-step); - flex: 0 0 auto; - width: 20px; - height: 20px; - margin: 1px 2px 0; - background: var(--px-cream); - color: var(--px-ink); - font-family: var(--px-font); - font-size: 9px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: - 0 -2px 0 0 var(--px-ink), - 0 2px 0 0 var(--px-ink), - -2px 0 0 0 var(--px-ink), - 2px 0 0 0 var(--px-ink), - inset 0 -2px 0 0 #b8a98a; -} - -.game-steps li strong { - color: var(--px-cream); -} - -/* Tips */ -.game-tips { - list-style: none; - display: flex; - flex-direction: column; - gap: 12px; - margin: 0; - padding: 0; -} - -.game-tips li { - display: flex; - align-items: flex-start; - gap: 12px; - font-size: 0.83rem; - color: #cfc6e2; - line-height: 1.5; -} - -.game-tips li i { - flex: 0 0 auto; - width: 16px; - text-align: center; - margin-top: 3px; - color: var(--px-gold); - font-size: 0.8rem; -} - -/* Leaderboard tabs */ -.game-tabs { - display: flex; - gap: 8px; - margin-bottom: 14px; -} - -.game-tab { - flex: 1; - padding: 8px 6px 7px; - border: none; - border-radius: 0; - background: var(--px-ink); - color: var(--px-lilac); - font-family: var(--px-font); - font-size: 8px; - cursor: pointer; - box-shadow: inset 0 -3px 0 0 var(--px-panel-hi); -} - -.game-tab.is-active { - background: var(--px-panel-hi); - color: var(--px-gold); - box-shadow: inset 0 -3px 0 0 var(--px-gold); -} - -.game-tab:focus-visible { - outline: 2px dashed var(--px-cream); - outline-offset: 2px; -} - -/* Skins: one button per climber look, showing the sprite itself */ -.game-skins { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; - margin-bottom: 18px; -} - -.game-skin { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - padding: 8px 4px 7px; - border: none; - border-radius: 0; - background: var(--px-ink); - color: var(--px-cream); - cursor: pointer; - box-shadow: inset 0 0 0 2px var(--px-panel-hi); -} - -.game-skin:hover:not(:disabled) { - box-shadow: inset 0 0 0 2px var(--px-gold-lo); -} - -.game-skin.is-selected { - background: #3a2f1a; - box-shadow: inset 0 0 0 2px var(--px-gold); -} - -.game-skin.is-locked { - cursor: not-allowed; -} - -.game-skin.is-locked .game-skin-sprite { - filter: brightness(0) opacity(0.35); -} - -.game-skin-sprite { - image-rendering: pixelated; - image-rendering: crisp-edges; -} - -.game-skin-name { - font-family: var(--px-font); - font-size: 7px; -} - -.game-skin-lock { - position: absolute; - top: 6px; - right: 6px; - color: var(--px-lilac); - font-size: 0.65rem; -} - -.game-subtitle { - display: flex; - justify-content: space-between; - font-family: var(--px-font); - font-size: 9px; - font-weight: 400; - color: var(--px-gold); - margin: 0 0 10px; -} - -.game-subtitle span { - color: var(--px-lilac); -} - -/* Achievements */ -.game-achievements { - list-style: none; - display: flex; - flex-direction: column; - gap: 6px; - max-height: 260px; - overflow-y: auto; - margin: 0 0 12px; - padding: 0 4px 0 0; -} - -.game-achievement { - display: flex; - align-items: center; - gap: 10px; - padding: 6px 8px; - background: var(--px-ink); - color: var(--px-lilac); - opacity: 0.6; -} - -.game-achievement.is-unlocked { - opacity: 1; - color: var(--px-cream); -} - -.game-achievement-icon { - flex: 0 0 auto; - width: 22px; - height: 22px; - display: flex; - align-items: center; - justify-content: center; - background: var(--px-panel-hi); - font-size: 0.7rem; -} - -.game-achievement.is-unlocked .game-achievement-icon { - background: var(--px-gold); - color: var(--px-ink); -} - -.game-achievement-text { - display: flex; - flex-direction: column; - gap: 3px; - min-width: 0; - font-size: 0.75rem; - line-height: 1.35; -} - -.game-achievement-text strong { - font-family: var(--px-font); - font-size: 8px; - font-weight: 400; - color: inherit; -} - -/* Leaderboard: an arcade high-score table */ -.game-leaderboard-list { - list-style: none; - display: flex; - flex-direction: column; - gap: 4px; - margin: 0 0 14px; - padding: 0; -} - -.game-leaderboard-list li { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 8px; - background: var(--px-ink); - font-size: 0.83rem; -} - -.game-leaderboard-list li:nth-child(even) { - background: #20182f; -} - -.game-leaderboard-list li.is-me { - background: #3a2f1a; - box-shadow: inset 0 0 0 2px var(--px-gold); -} - -.game-leaderboard-list li.game-leaderboard-empty { - color: var(--px-lilac); - font-family: var(--px-font); - font-size: 8px; - line-height: 1.8; - padding: 12px 10px; -} - -.game-leaderboard-rank { - flex: 0 0 auto; - width: 22px; - height: 22px; - display: flex; - align-items: center; - justify-content: center; - font-family: var(--px-font); - font-size: 9px; - color: var(--px-lilac); -} - -/* Top three get pixel medals */ -.game-leaderboard-list li:nth-child(-n + 3) .game-leaderboard-rank { - color: var(--px-ink); - box-shadow: - 0 -2px 0 0 var(--px-ink), - 0 2px 0 0 var(--px-ink), - -2px 0 0 0 var(--px-ink), - 2px 0 0 0 var(--px-ink), - inset -2px -2px 0 0 rgba(0, 0, 0, 0.25), - inset 2px 2px 0 0 rgba(255, 255, 255, 0.45); -} - -.game-leaderboard-list li:nth-child(1) .game-leaderboard-rank { - background: #f2c25a; -} - -.game-leaderboard-list li:nth-child(2) .game-leaderboard-rank { - background: #c9ccd6; -} - -.game-leaderboard-list li:nth-child(3) .game-leaderboard-rank { - background: #d08a4e; -} - -.game-leaderboard-list li.game-leaderboard-empty .game-leaderboard-rank { - display: none; -} - -.game-leaderboard-name { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--px-cream); - text-decoration: none; - font-weight: 600; -} - -.game-leaderboard-name:hover { - color: var(--px-gold); - text-decoration: underline; -} - -.game-leaderboard-me { - flex: 0 0 auto; - padding: 3px 4px 2px; - background: var(--px-gold); - color: var(--px-ink); - font-family: var(--px-font); - font-size: 7px; -} - -.game-leaderboard-height { - flex: 0 0 auto; - font-family: var(--px-font); - font-size: 9px; - color: var(--px-gold); -} - -.game-leaderboard-note { - font-size: 0.78rem; - color: var(--px-lilac); - margin: 0; - line-height: 1.5; -} - -.game-leaderboard-note a, -.game-leaderboard-note strong { - color: var(--px-gold); -} - -/* "La ruta": each zone is a rock block, rising left to right like a staircase, with the - climber's sprite standing on the highest one you've reached. */ -.game-route-panel { - margin: calc(var(--spacing-lg) + 4px) 6px 6px; -} - -.game-route-intro { - color: #cfc6e2; - font-size: 0.88rem; - line-height: 1.5; - margin: 0 0 18px; -} - -.game-route { - list-style: none; - display: grid; - grid-template-columns: repeat(8, minmax(0, 1fr)); - align-items: end; - gap: 6px; - margin: 0; - padding: 60px 0 0; -} - -.game-route-stop { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - text-align: center; -} - -.game-route-block { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: calc(18px + var(--step) * 12px); - background: - linear-gradient(180deg, var(--cap) 0 5px, var(--rock-hi) 5px 9px, var(--rock) 9px 70%, var(--rock-lo) 70% 100%); - box-shadow: - 0 -3px 0 0 var(--px-ink), - -3px 0 0 0 var(--px-ink), - 3px 0 0 0 var(--px-ink), - 0 3px 0 0 var(--px-ink); - filter: saturate(0.35) brightness(0.55); - color: var(--px-ink); -} - -.game-route-stop.is-reached .game-route-block { - filter: none; -} - -.game-route-stop.is-summit .game-route-block { - background: linear-gradient(180deg, #ffe394 0 4px, var(--px-gold) 4px 70%, var(--px-gold-lo) 70% 100%); -} - -.game-route-climber { - position: absolute; - left: 50%; - bottom: 100%; - margin-bottom: 1px; - transform: translateX(-50%); - image-rendering: pixelated; - image-rendering: crisp-edges; - filter: none; -} - -.game-route-name { - font-family: var(--px-font); - font-size: 7px; - line-height: 1.6; - color: var(--px-lilac); - overflow-wrap: break-word; -} - -.game-route-stop.is-reached .game-route-name { - color: var(--px-cream); -} - -.game-route-height { - font-family: var(--px-font); - font-size: 8px; - color: var(--px-gold-lo); -} - -.game-route-stop.is-reached .game-route-height { - color: var(--px-gold); -} - -/* Game-over: rank feedback + arcade-style "save your score" prompt */ -.game-overlay-rank { - color: var(--px-teal) !important; -} - -.game-save-prompt { - margin: 12px 0; - padding: 14px 14px 10px; - background: var(--px-ink); - border: 2px dashed var(--px-gold-lo); -} - -.game-save-prompt p { - color: var(--px-cream) !important; - margin-bottom: 12px !important; -} - -.game-save-prompt p i { - color: var(--px-gold); - margin-right: 4px; -} - -.game-save-prompt .game-btn { - width: calc(100% - 8px); - font-size: 9px; -} - -/* Responsive */ -@media (max-width: 768px) { - .game-page { - padding: var(--spacing-md) 0; - } - - .game-hero { - padding: 26px 14px 52px; - } - - .game-hero-title { - font-size: 1.8rem; - } - - .game-hero-stats li { - min-width: 92px; - } - - /* The staircase becomes a vertical climb, summit on top */ - .game-route { - display: flex; - flex-direction: column-reverse; - align-items: stretch; - gap: 10px; - padding: 0; - } - - .game-route-stop { - flex-direction: row; - gap: 12px; - text-align: left; - } - - .game-route-block { - flex: 0 0 auto; - width: calc(28px + var(--step) * 10px); - height: 22px; - } - - .game-route-name { - flex: 1; - font-size: 8px; - } - - /* No room for the sprite on a short row - a "Tú" tag marks where you are instead */ - .game-route-climber { - display: none; - } - - .game-route-stop.is-current .game-route-name::after { - content: 'Tú'; - display: inline-block; - margin-left: 10px; - padding: 3px 4px 2px; - background: var(--px-gold); - color: var(--px-ink); - font-size: 7px; - } - - .game-hud { - gap: var(--spacing-sm); - flex-wrap: wrap; - } - - .game-canvas-holder { - height: min(60vh, 480px); - min-height: 340px; - } - - .game-controls-legend { - flex-direction: column; - gap: 6px; - } -} - -@media (max-width: 540px) { - .game-hero-lead { - font-size: 0.88rem; - } - - .game-hero-stats { - gap: 10px; - } - - .game-hero-stats li { - min-width: 0; - padding: 10px 8px 8px; - } - - .game-hero-stats strong { - font-size: 11px; - } - - /* Game-over is a lot to fit in a short canvas on a phone: smaller title and sprite, - tighter gaps between lines, icon-only retry/share buttons (full labels stay for - screen readers via aria-label). */ - .game-mode-tag { - display: none; - } - - .game-overlay-card { - padding: 18px 14px 16px; - } - - .game-overlay-sprite { - margin-bottom: 8px; - } - - .game-overlay-card h2 { - font-size: 15px; - margin-bottom: 10px; - } - - .game-overlay-card .game-title { - font-size: 22px; - } - - .game-overlay-card p { - font-size: 8px; - margin-bottom: 6px; - } - - .game-overlay-score strong { - font-size: 18px; - } - - .game-save-prompt { - margin: 8px 0; - padding: 10px 10px 6px; - } -} - -/* Reduced motion: from the OS setting or the toggle in the pause menu */ -@media (prefers-reduced-motion: reduce) { - .game-page *, - .game-page *::before, - .game-page *::after { - animation: none !important; - transition: none !important; - } -} - -.game-reduced-motion .game-page *, -.game-reduced-motion .game-page *::before, -.game-reduced-motion .game-page *::after { - animation: none !important; - transition: none !important; -} diff --git a/public/styles/pages/game/base.css b/public/styles/pages/game/base.css new file mode 100644 index 0000000..71cf366 --- /dev/null +++ b/public/styles/pages/game/base.css @@ -0,0 +1,82 @@ +/* ============================================ + GAME PAGE - "Al Filo" + Everything inside the game box (HUD, overlays, buttons, legend) is styled as pixel art to + match the canvas: Press Start 2P, hard 0-radius edges, and "stepped" borders built from + offset box-shadows (four 1-direction shadows leave the corners notched, the classic way to + fake a pixel frame in CSS). The sidebar keeps the regular site look. + No text-transform: uppercase on Spanish text - Press Start 2P has no accented capitals + (Í, É, Á...), so "CAÍDO" would render with a fallback glyph. + ============================================ */ + +.game-page { + --px-ink: #1b1420; + --px-panel: #241c38; + --px-panel-hi: #3a2e58; + --px-gold: #f2c25a; + --px-gold-hi: #ffe394; + --px-gold-lo: #a9781f; + --px-cream: #fff3d6; + --px-lilac: #a99cc8; + --px-red: #e2493b; + --px-teal: #2fd6c0; + --px-font: 'Press Start 2P', monospace; + padding: var(--spacing-xl) 0; + min-height: calc(100vh - 200px); +} + +.game-page .container { + max-width: 640px; + width: 100%; + transition: max-width 0.2s ease; +} + +/* Desktop gets a noticeably bigger playing field plus room for the sidebar */ +@media (min-width: 1024px) { + .game-page .container { + max-width: 1120px; + } +} + +/* Layout: single column on mobile, game + sidebar on desktop */ +.game-layout { + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-lg); + align-items: start; +} + +/* Grid items default to min-width: auto, which refuses to shrink below the width of their + content (the HUD row, in this case) - that was pushing the whole page into horizontal + scroll on narrow phones instead of letting the HUD wrap. */ +.game-main { + min-width: 0; +} + +@media (min-width: 1024px) { + .game-layout { + grid-template-columns: 1fr 300px; + } +} + +@media (max-width: 768px) { + .game-page { + padding: var(--spacing-md) 0; + } +} + +/* Reduced motion: from the OS setting or the toggle in the pause menu */ +@media (prefers-reduced-motion: reduce) { + .game-page *, + .game-page *::before, + .game-page *::after { + animation: none !important; + transition: none !important; + } +} + +.game-reduced-motion .game-page *, +.game-reduced-motion .game-page *::before, +.game-reduced-motion .game-page *::after { + animation: none !important; + transition: none !important; +} diff --git a/public/styles/pages/game/box.css b/public/styles/pages/game/box.css new file mode 100644 index 0000000..9f7ceda --- /dev/null +++ b/public/styles/pages/game/box.css @@ -0,0 +1,309 @@ +/* ============================================ + GAME PAGE - game box + Frame, HUD, canvas, power bar, corner buttons, tutorial prompt and toasts. + ============================================ */ + +/* Game box: a pixel frame instead of the site's rounded glass card */ +.game-wrap { + background: var(--px-ink); + border-radius: 0; + margin: 6px; + box-shadow: + 0 -6px 0 0 var(--px-panel-hi), + 0 6px 0 0 var(--px-panel-hi), + -6px 0 0 0 var(--px-panel-hi), + 6px 0 0 0 var(--px-panel-hi), + 0 14px 0 0 rgba(0, 0, 0, 0.35); +} + +/* HUD */ +.game-hud { + display: flex; + align-items: center; + gap: var(--spacing-lg); + padding: 12px var(--spacing-md); + background: var(--px-panel); + border-bottom: 4px solid var(--px-ink); + box-shadow: inset 0 -4px 0 0 var(--px-panel-hi); +} + +.game-hud-item { + display: flex; + align-items: center; + gap: 10px; +} + +.game-hud-icon { + font-size: 0.95rem; + color: var(--px-lilac); + width: 16px; + text-align: center; +} + +.game-hud-icon-record { + color: var(--px-gold); +} + +.game-hud-item > div { + display: flex; + flex-direction: column; + gap: 6px; +} + +.game-hud-label { + font-family: var(--px-font); + font-size: 8px; + color: var(--px-lilac); +} + +.game-hud-value { + font-family: var(--px-font); + font-size: 13px; + color: var(--px-gold); + text-shadow: 2px 2px 0 var(--px-ink); +} + +.game-hud-stamina { + flex: 1; + min-width: 90px; +} + +.game-hud-stamina-inner { + width: 100%; +} + +.game-stamina-bar { + position: relative; + width: 100%; + height: 12px; + background: var(--px-ink); + overflow: hidden; + box-shadow: + 0 -2px 0 0 var(--px-panel-hi), + 0 2px 0 0 var(--px-panel-hi), + -2px 0 0 0 var(--px-panel-hi), + 2px 0 0 0 var(--px-panel-hi); +} + +/* Segment notches over the fill - reads as a row of blocks, like a retro health bar */ +.game-stamina-bar::after { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient(90deg, transparent 0 10px, var(--px-ink) 10px 12px); + pointer-events: none; +} + +.game-stamina-fill { + height: 100%; + width: 100%; + background: linear-gradient(180deg, #9cf5b4 0 25%, #38c46e 25% 70%, #1f8a4f 70% 100%); + transition: width 0.15s steps(6); +} + +.game-stamina-bar.is-low .game-stamina-fill { + background: linear-gradient(180deg, #ff9b8a 0 25%, var(--px-red) 25% 70%, #9c2a22 70% 100%); + animation: gameStaminaLowPulse 0.7s steps(2) infinite; +} + +@keyframes gameStaminaLowPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } +} + +/* A quick celebratory flash when stamina is topped up by something earned + (a precise landing, a combo, a clip) rather than just the slow passive regen. */ +.game-stamina-fill.is-boosted { + animation: gameStaminaBoost 0.45s steps(3); +} + +@keyframes gameStaminaBoost { + 0% { filter: brightness(1); } + 30% { filter: brightness(1.9); } + 100% { filter: brightness(1); } +} + +/* Canvas holder */ +.game-canvas-holder { + position: relative; + width: 100%; + height: min(68vh, 560px); + min-height: 380px; + touch-action: none; + overflow: hidden; + user-select: none; + -webkit-user-select: none; +} + +@media (min-width: 1024px) { + .game-canvas-holder { + height: min(74vh, 680px); + min-height: 440px; + } +} + +#gameCanvas { + display: block; + width: 100%; + height: 100%; + touch-action: none; + cursor: crosshair; + -webkit-user-drag: none; + user-select: none; +} + +/* Power bar (vertical, shows charge while held) */ +.game-power-bar { + position: absolute; + right: 16px; + top: 16px; + bottom: 16px; + width: 14px; + background: var(--px-ink); + overflow: hidden; + opacity: 0; + pointer-events: none; + display: flex; + align-items: flex-end; + box-shadow: + 0 -3px 0 0 var(--px-cream), + 0 3px 0 0 var(--px-cream), + -3px 0 0 0 var(--px-cream), + 3px 0 0 0 var(--px-cream); +} + +.game-power-bar::after { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient(0deg, transparent 0 10px, var(--px-ink) 10px 12px); + pointer-events: none; +} + +.game-power-bar.is-visible { + opacity: 1; +} + +.game-power-fill { + width: 100%; + height: 0%; + background: linear-gradient(90deg, var(--px-gold-hi) 0 30%, var(--px-gold) 30% 70%, var(--px-gold-lo) 70% 100%); +} + +.game-power-fill.is-maxed { + background: linear-gradient(90deg, #ffc08a 0 30%, #ff6b4a 30% 70%, #b8402a 70% 100%); + animation: gamePowerPulse 0.3s steps(2) infinite; +} + +@keyframes gamePowerPulse { + 0%, 100% { filter: brightness(1); } + 50% { filter: brightness(1.5); } +} + +/* Pause + sound buttons, top-left of the canvas */ +.game-corner-buttons { + position: absolute; + top: 14px; + left: 14px; + z-index: 3; + display: flex; + gap: 12px; +} + +.game-sound-btn { + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; + border-radius: 0; + background: var(--px-panel); + color: var(--px-gold); + font-size: 0.8rem; + cursor: pointer; + box-shadow: + 0 -3px 0 0 var(--px-ink), + 0 3px 0 0 var(--px-ink), + -3px 0 0 0 var(--px-ink), + 3px 0 0 0 var(--px-ink), + inset -3px -3px 0 0 var(--px-ink), + inset 3px 3px 0 0 var(--px-panel-hi); +} + +.game-sound-btn:hover { + background: var(--px-panel-hi); +} + +.game-sound-btn.is-muted { + color: var(--px-lilac); +} + +.game-mode-tag { + margin-left: 8px; + padding: 2px 4px 1px; + background: var(--px-ink); + color: var(--px-gold); +} + +/* First-run tutorial prompt and achievement toasts, over the canvas */ +.game-tutorial, +.game-toast { + position: absolute; + left: 50%; + z-index: 3; + width: max-content; + max-width: calc(100% - 120px); + margin: 0; + padding: 10px 12px 9px; + transform: translateX(-50%); + background: var(--px-panel); + color: var(--px-cream); + font-family: var(--px-font); + font-size: 9px; + line-height: 1.8; + text-align: center; + pointer-events: none; + box-shadow: + 0 -3px 0 0 var(--px-ink), + 0 3px 0 0 var(--px-ink), + -3px 0 0 0 var(--px-ink), + 3px 0 0 0 var(--px-ink), + inset 0 0 0 3px var(--px-gold); +} + +.game-tutorial { + bottom: 18px; +} + +.game-toast { + top: 64px; + color: var(--px-gold); + opacity: 0; + transform: translate(-50%, 12px); + transition: opacity 0.2s steps(3), transform 0.2s steps(3); +} + +.game-toast.is-visible { + opacity: 1; + transform: translate(-50%, 0); +} + +@media (max-width: 768px) { + .game-hud { + gap: var(--spacing-sm); + flex-wrap: wrap; + } + + .game-canvas-holder { + height: min(60vh, 480px); + min-height: 340px; + } +} + +@media (max-width: 540px) { + .game-mode-tag { + display: none; + } +} diff --git a/public/styles/pages/game/hero.css b/public/styles/pages/game/hero.css new file mode 100644 index 0000000..03ce9c9 --- /dev/null +++ b/public/styles/pages/game/hero.css @@ -0,0 +1,188 @@ +/* ============================================ + GAME PAGE - hero + The title-screen header: banded night sky, stars, stepped pixel mountains, stats. + ============================================ */ + +/* Hero: a game title screen - banded night sky, stars, stepped pixel mountains */ +.game-hero { + position: relative; + overflow: hidden; + margin: 6px 6px calc(var(--spacing-lg) + 6px); + padding: 34px var(--spacing-md) 64px; + text-align: center; + background: linear-gradient(180deg, + #110c20 0 22%, #181230 22% 42%, #211940 42% 60%, #2c2152 60% 76%, #3a2c64 76% 100%); + box-shadow: + 0 -6px 0 0 var(--px-panel-hi), + 0 6px 0 0 var(--px-panel-hi), + -6px 0 0 0 var(--px-panel-hi), + 6px 0 0 0 var(--px-panel-hi), + 0 14px 0 0 rgba(0, 0, 0, 0.35); +} + +.game-hero-sky, +.game-hero-ridges { + position: absolute; + left: 0; + width: 100%; + pointer-events: none; +} + +.game-hero-sky { + top: 0; + height: 100%; +} + +/* The ridges only fill the bottom band, under the stat boxes, never behind the text */ +.game-hero-ridges { + bottom: 0; + height: 46%; +} + +.game-hero-stars rect { + fill: var(--px-cream); + animation: gameTwinkle 2.4s steps(1) infinite; +} + +.game-hero-stars rect:nth-child(3n) { + animation-delay: -0.8s; +} + +.game-hero-stars rect:nth-child(3n + 1) { + animation-delay: -1.6s; +} + +@keyframes gameTwinkle { + 0%, 70% { opacity: 1; } + 71%, 100% { opacity: 0.25; } +} + +.game-hero-moon rect { + fill: #e8e6f6; +} + +.game-hero-ridge-far { + fill: #3f3470; +} + +.game-hero-ridge-near { + fill: var(--px-ink); +} + +.game-hero-content { + position: relative; +} + +.game-page-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 10px 6px; + background: var(--px-ink); + color: var(--px-gold); + font-family: var(--px-font); + font-size: 8px; + line-height: 1.4; + margin-bottom: 18px; + box-shadow: + 0 -2px 0 0 var(--px-gold-lo), + 0 2px 0 0 var(--px-gold-lo), + -2px 0 0 0 var(--px-gold-lo), + 2px 0 0 0 var(--px-gold-lo); +} + +.game-hero-title { + font-family: var(--px-font); + font-size: 2.6rem; + font-weight: 400; + line-height: 1.2; + margin-bottom: 16px; + color: var(--px-cream); + text-shadow: 0 5px 0 #8a7fa8, 5px 10px 0 var(--px-ink); +} + +.game-hero-title span { + color: var(--px-gold); + text-shadow: 0 5px 0 var(--px-gold-lo), 5px 10px 0 var(--px-ink); +} + +.game-hero-lead { + color: #d8d0ea; + font-size: 1rem; + line-height: 1.6; + max-width: 520px; + margin: 0 auto 22px; + text-shadow: 0 2px 0 var(--px-ink); +} + +.game-hero-stats { + list-style: none; + display: inline-flex; + flex-wrap: wrap; + justify-content: center; + gap: 14px; + margin: 0; + padding: 0; +} + +.game-hero-stats li { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 110px; + padding: 12px 12px 10px; + background: var(--px-panel); + box-shadow: + 0 -3px 0 0 var(--px-ink), + 0 3px 0 0 var(--px-ink), + -3px 0 0 0 var(--px-ink), + 3px 0 0 0 var(--px-ink), + inset 0 3px 0 0 var(--px-panel-hi); +} + +.game-hero-stats strong { + font-family: var(--px-font); + font-weight: 400; + font-size: 14px; + color: var(--px-gold); + text-shadow: 2px 2px 0 var(--px-ink); +} + +.game-hero-stats span { + font-family: var(--px-font); + font-size: 8px; + color: var(--px-lilac); +} + +@media (max-width: 768px) { + .game-hero { + padding: 26px 14px 52px; + } + + .game-hero-title { + font-size: 1.8rem; + } + + .game-hero-stats li { + min-width: 92px; + } +} + +@media (max-width: 540px) { + .game-hero-lead { + font-size: 0.88rem; + } + + .game-hero-stats { + gap: 10px; + } + + .game-hero-stats li { + min-width: 0; + padding: 10px 8px 8px; + } + + .game-hero-stats strong { + font-size: 11px; + } +} diff --git a/public/styles/pages/game/overlays.css b/public/styles/pages/game/overlays.css new file mode 100644 index 0000000..8b15775 --- /dev/null +++ b/public/styles/pages/game/overlays.css @@ -0,0 +1,472 @@ +/* ============================================ + GAME PAGE - overlays + Start, pause and game-over screens, settings, controls legend, keycaps and pixel buttons. + ============================================ */ + +/* Overlays: no blur, so the animated pixel scene stays visible behind the panel, + just dimmed and with faint scanlines for a CRT feel. */ +.game-overlay { + position: absolute; + inset: 0; + z-index: 4; + display: flex; + align-items: center; + justify-content: center; + background: + repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.18) 0 2px, transparent 2px 4px), + rgba(14, 8, 26, 0.5); + padding: 14px; + text-align: center; + overflow-y: auto; +} + +.game-overlay-hidden { + display: none; +} + +.game-overlay-card { + max-width: 400px; + margin: auto; + padding: 20px 20px 18px; + background: var(--px-panel); + box-shadow: + 0 -4px 0 0 var(--px-ink), + 0 4px 0 0 var(--px-ink), + -4px 0 0 0 var(--px-ink), + 4px 0 0 0 var(--px-ink), + inset 0 0 0 4px var(--px-gold), + inset 0 0 0 6px var(--px-gold-lo), + inset 0 0 0 8px var(--px-ink), + inset 0 10px 0 0 var(--px-panel-hi), + 8px 12px 0 0 rgba(0, 0, 0, 0.45); + animation: gameCardIn 0.28s steps(4); +} + +@keyframes gameCardIn { + 0% { transform: translateY(16px) scale(0.92); opacity: 0; } + 100% { transform: none; opacity: 1; } +} + +/* The climber's own sprite (drawn by js/game/main.js) instead of a font icon */ +.game-overlay-sprite { + display: block; + margin: 0 auto 10px; + image-rendering: pixelated; + image-rendering: crisp-edges; + animation: gameSpriteBob 1s steps(2) infinite; +} + +@keyframes gameSpriteBob { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-4px); } +} + +.game-overlay.is-win .game-overlay-sprite { + animation: gameSpriteBob 0.4s steps(2) infinite; +} + +.game-overlay-card h2 { + font-family: var(--px-font); + font-size: 18px; + font-weight: 400; + line-height: 1.4; + color: var(--px-gold); + text-shadow: 0 3px 0 var(--px-gold-lo), 3px 6px 0 var(--px-ink); + margin-bottom: 14px; +} + +.game-overlay-card .game-title { + font-size: 28px; + text-shadow: 0 4px 0 var(--px-gold-lo), 4px 8px 0 var(--px-ink); +} + +.game-overlay.is-win .game-overlay-card h2 { + animation: gameTitleFlash 0.6s steps(2) infinite; +} + +@keyframes gameTitleFlash { + 0%, 100% { color: var(--px-gold); } + 50% { color: var(--px-cream); } +} + +.game-overlay-card p { + font-family: var(--px-font); + color: var(--px-cream); + font-size: 9px; + line-height: 1.9; + margin-bottom: 10px; +} + +.game-overlay-card > .game-btn { + margin-top: 14px; +} + +/* Blinking "press start", arcade style */ +.game-press-start { + margin: 14px 0 0 !important; + color: var(--px-gold) !important; + animation: gameBlink 1s steps(1) infinite; +} + +/* No space bar on a touch screen */ +@media (hover: none) { + .game-press-start { + display: none; + } +} + +@keyframes gameBlink { + 0%, 60% { opacity: 1; } + 61%, 100% { opacity: 0; } +} + +.game-overlay-score { + font-size: 11px !important; + color: var(--px-cream) !important; +} + +.game-overlay-score strong { + display: block; + margin-top: 6px; + font-weight: 400; + font-size: 20px; + color: var(--px-gold); + text-shadow: 0 3px 0 var(--px-gold-lo), 3px 5px 0 var(--px-ink); +} + +.game-overlay-record { + color: var(--px-lilac) !important; + margin-bottom: 14px !important; +} + +.game-overlay-actions { + display: flex; + flex-direction: row; + justify-content: center; + gap: 16px; + margin-top: 12px; +} + +.game-overlay-actions .game-btn { + flex: 0 0 auto; + width: 52px; + height: 48px; + padding: 0; + font-size: 1rem; +} + +.game-mode-buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 14px; +} + +.game-mode-buttons .game-btn { + font-size: 10px; +} + +.game-daily-best { + margin: 12px 0 0 !important; + color: var(--px-lilac) !important; + font-size: 8px !important; +} + +.game-pause-actions { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 10px; + max-width: 240px; + margin: 0 auto 18px; +} + +.game-pause-actions .game-btn { + font-size: 10px; +} + +.game-settings { + display: flex; + flex-direction: column; + gap: 12px; + padding: 14px 12px 12px; + background: var(--px-ink); + text-align: left; +} + +.game-setting { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + font-family: var(--px-font); + font-size: 8px; + color: var(--px-cream); + cursor: pointer; +} + +.game-setting i { + color: var(--px-gold); + margin-right: 6px; +} + +/* Pixel slider: a flat ink track with a cream block thumb */ +.game-setting input[type="range"] { + flex: 1; + max-width: 150px; + height: 16px; + margin: 0; + background: transparent; + -webkit-appearance: none; + appearance: none; + cursor: pointer; +} + +.game-setting input[type="range"]::-webkit-slider-runnable-track { + height: 6px; + background: var(--px-panel-hi); + box-shadow: 0 0 0 2px var(--px-ink); +} + +.game-setting input[type="range"]::-moz-range-track { + height: 6px; + background: var(--px-panel-hi); + box-shadow: 0 0 0 2px var(--px-ink); +} + +.game-setting input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 16px; + margin-top: -5px; + border: none; + border-radius: 0; + background: var(--px-gold); + box-shadow: 0 0 0 2px var(--px-ink); +} + +.game-setting input[type="range"]::-moz-range-thumb { + width: 12px; + height: 16px; + border: none; + border-radius: 0; + background: var(--px-gold); + box-shadow: 0 0 0 2px var(--px-ink); +} + +.game-setting-check { + justify-content: flex-start; +} + +.game-setting-check input { + width: 16px; + height: 16px; + margin: 0; + accent-color: var(--px-gold); +} + +.game-over-achievements { + list-style: none; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + margin: 4px 0 12px; + padding: 0; +} + +.game-over-achievements li { + padding: 6px 8px 5px; + background: var(--px-ink); + color: var(--px-gold); + font-family: var(--px-font); + font-size: 8px; + box-shadow: inset 0 0 0 2px var(--px-gold-lo); +} + +/* Permanent controls legend strip under the canvas */ +.game-controls-legend { + display: flex; + flex-wrap: wrap; + gap: 10px var(--spacing-lg); + padding: 12px var(--spacing-md); + background: var(--px-panel); + border-top: 4px solid var(--px-ink); + box-shadow: inset 0 4px 0 0 var(--px-panel-hi); + font-family: var(--px-font); + font-size: 8px; + line-height: 2; + color: var(--px-lilac); +} + +.game-controls-legend span { + display: block; +} + +.game-controls-legend i { + color: var(--px-gold); + font-size: 0.8rem; + margin-right: 6px; +} + +/* Keycaps */ +.game-controls-legend kbd { + display: inline-block; + min-width: 18px; + padding: 4px 5px 3px; + border-radius: 0; + background: var(--px-cream); + color: var(--px-ink); + font-family: var(--px-font), 'Inter', sans-serif; + font-size: 9px; + line-height: 1; + text-align: center; + box-shadow: + 0 -2px 0 0 var(--px-ink), + 0 2px 0 0 var(--px-ink), + -2px 0 0 0 var(--px-ink), + 2px 0 0 0 var(--px-ink), + inset 0 -2px 0 0 #b8a98a; +} + +/* Buttons: chunky pixel buttons with a bevel that flips when pressed */ +.game-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 14px 18px; + border: none; + border-radius: 0; + font-family: var(--px-font); + font-size: 11px; + font-weight: 400; + line-height: 1.4; + cursor: pointer; + text-decoration: none !important; + margin: 4px; +} + +.game-btn-primary { + background: var(--px-gold); + color: var(--px-ink); + box-shadow: + 0 -4px 0 0 var(--px-ink), + 0 4px 0 0 var(--px-ink), + -4px 0 0 0 var(--px-ink), + 4px 0 0 0 var(--px-ink), + inset -4px -4px 0 0 var(--px-gold-lo), + inset 4px 4px 0 0 var(--px-gold-hi); +} + +.game-btn-primary:hover { + background: var(--px-gold-hi); + color: var(--px-ink); +} + +.game-btn-secondary { + background: var(--px-panel-hi); + color: var(--px-cream); + box-shadow: + 0 -4px 0 0 var(--px-ink), + 0 4px 0 0 var(--px-ink), + -4px 0 0 0 var(--px-ink), + 4px 0 0 0 var(--px-ink), + inset -4px -4px 0 0 #2a2140, + inset 4px 4px 0 0 #574a7e; +} + +.game-btn-secondary:hover { + background: #4a3c70; +} + +.game-btn:active { + transform: translateY(3px); +} + +.game-btn-primary:active { + box-shadow: + 0 -4px 0 0 var(--px-ink), + 0 4px 0 0 var(--px-ink), + -4px 0 0 0 var(--px-ink), + 4px 0 0 0 var(--px-ink), + inset 4px 4px 0 0 var(--px-gold-lo); +} + +.game-btn:focus-visible, +.game-sound-btn:focus-visible { + outline: 3px dashed var(--px-cream); + outline-offset: 6px; +} + +/* Game-over: rank feedback + arcade-style "save your score" prompt */ +.game-overlay-rank { + color: var(--px-teal) !important; +} + +.game-save-prompt { + margin: 12px 0; + padding: 14px 14px 10px; + background: var(--px-ink); + border: 2px dashed var(--px-gold-lo); +} + +.game-save-prompt p { + color: var(--px-cream) !important; + margin-bottom: 12px !important; +} + +.game-save-prompt p i { + color: var(--px-gold); + margin-right: 4px; +} + +.game-save-prompt .game-btn { + width: calc(100% - 8px); + font-size: 9px; +} + +@media (max-width: 768px) { + .game-controls-legend { + flex-direction: column; + gap: 6px; + } +} + +@media (max-width: 540px) { + /* Game-over is a lot to fit in a short canvas on a phone: smaller title and sprite, + tighter gaps between lines, icon-only retry/share buttons (full labels stay for + screen readers via aria-label). */ + + .game-overlay-card { + padding: 18px 14px 16px; + } + + .game-overlay-sprite { + margin-bottom: 8px; + } + + .game-overlay-card h2 { + font-size: 15px; + margin-bottom: 10px; + } + + .game-overlay-card .game-title { + font-size: 22px; + } + + .game-overlay-card p { + font-size: 8px; + margin-bottom: 6px; + } + + .game-overlay-score strong { + font-size: 18px; + } + + .game-save-prompt { + margin: 8px 0; + padding: 10px 10px 6px; + } +} diff --git a/public/styles/pages/game/route.css b/public/styles/pages/game/route.css new file mode 100644 index 0000000..d9b6591 --- /dev/null +++ b/public/styles/pages/game/route.css @@ -0,0 +1,137 @@ +/* ============================================ + GAME PAGE - "La ruta" + The zones of the climb as a staircase of rock blocks (a vertical list on phones). + ============================================ */ + +/* "La ruta": each zone is a rock block, rising left to right like a staircase, with the + climber's sprite standing on the highest one you've reached. */ +.game-route-panel { + margin: calc(var(--spacing-lg) + 4px) 6px 6px; +} + +.game-route-intro { + color: #cfc6e2; + font-size: 0.88rem; + line-height: 1.5; + margin: 0 0 18px; +} + +.game-route { + list-style: none; + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)); + align-items: end; + gap: 6px; + margin: 0; + padding: 60px 0 0; +} + +.game-route-stop { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + text-align: center; +} + +.game-route-block { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: calc(18px + var(--step) * 12px); + background: + linear-gradient(180deg, var(--cap) 0 5px, var(--rock-hi) 5px 9px, var(--rock) 9px 70%, var(--rock-lo) 70% 100%); + box-shadow: + 0 -3px 0 0 var(--px-ink), + -3px 0 0 0 var(--px-ink), + 3px 0 0 0 var(--px-ink), + 0 3px 0 0 var(--px-ink); + filter: saturate(0.35) brightness(0.55); + color: var(--px-ink); +} + +.game-route-stop.is-reached .game-route-block { + filter: none; +} + +.game-route-stop.is-summit .game-route-block { + background: linear-gradient(180deg, #ffe394 0 4px, var(--px-gold) 4px 70%, var(--px-gold-lo) 70% 100%); +} + +.game-route-climber { + position: absolute; + left: 50%; + bottom: 100%; + margin-bottom: 1px; + transform: translateX(-50%); + image-rendering: pixelated; + image-rendering: crisp-edges; + filter: none; +} + +.game-route-name { + font-family: var(--px-font); + font-size: 7px; + line-height: 1.6; + color: var(--px-lilac); + overflow-wrap: break-word; +} + +.game-route-stop.is-reached .game-route-name { + color: var(--px-cream); +} + +.game-route-height { + font-family: var(--px-font); + font-size: 8px; + color: var(--px-gold-lo); +} + +.game-route-stop.is-reached .game-route-height { + color: var(--px-gold); +} + +@media (max-width: 768px) { + /* The staircase becomes a vertical climb, summit on top */ + .game-route { + display: flex; + flex-direction: column-reverse; + align-items: stretch; + gap: 10px; + padding: 0; + } + + .game-route-stop { + flex-direction: row; + gap: 12px; + text-align: left; + } + + .game-route-block { + flex: 0 0 auto; + width: calc(28px + var(--step) * 10px); + height: 22px; + } + + .game-route-name { + flex: 1; + font-size: 8px; + } + + /* No room for the sprite on a short row - a "Tú" tag marks where you are instead */ + .game-route-climber { + display: none; + } + + .game-route-stop.is-current .game-route-name::after { + content: 'Tú'; + display: inline-block; + margin-left: 10px; + padding: 3px 4px 2px; + background: var(--px-gold); + color: var(--px-ink); + font-size: 7px; + } +} diff --git a/public/styles/pages/game/sidebar.css b/public/styles/pages/game/sidebar.css new file mode 100644 index 0000000..7980223 --- /dev/null +++ b/public/styles/pages/game/sidebar.css @@ -0,0 +1,401 @@ +/* ============================================ + GAME PAGE - sidebar + Pixel panels: leaderboard with tabs, skins, achievements, how to play and tips. + ============================================ */ + +/* Panels: the sidebar cards and "La ruta" share the game box's pixel-frame look */ +.game-sidebar { + display: flex; + flex-direction: column; + gap: calc(var(--spacing-md) + 6px); + padding: 6px; +} + +.game-panel { + background: var(--px-panel); + padding: 18px 16px 16px; + box-shadow: + 0 -4px 0 0 var(--px-ink), + 0 4px 0 0 var(--px-ink), + -4px 0 0 0 var(--px-ink), + 4px 0 0 0 var(--px-ink), + inset 0 4px 0 0 var(--px-panel-hi), + 0 10px 0 0 rgba(0, 0, 0, 0.3); +} + +.game-panel-title { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--px-font); + font-size: 11px; + font-weight: 400; + line-height: 1.4; + color: var(--px-gold); + text-shadow: 2px 2px 0 var(--px-ink); + margin: 0 0 16px; + padding-bottom: 12px; + border-bottom: 2px dashed var(--px-panel-hi); +} + +.game-panel-title i { + color: var(--px-gold); + font-size: 0.85rem; + text-shadow: none; +} + +/* How to play: numbered keycaps */ +.game-steps { + list-style: none; + counter-reset: game-step; + display: flex; + flex-direction: column; + gap: 12px; + margin: 0; + padding: 0; +} + +.game-steps li { + counter-increment: game-step; + display: flex; + align-items: flex-start; + gap: 12px; + font-size: 0.85rem; + color: #cfc6e2; + line-height: 1.5; +} + +.game-steps li::before { + content: counter(game-step); + flex: 0 0 auto; + width: 20px; + height: 20px; + margin: 1px 2px 0; + background: var(--px-cream); + color: var(--px-ink); + font-family: var(--px-font); + font-size: 9px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: + 0 -2px 0 0 var(--px-ink), + 0 2px 0 0 var(--px-ink), + -2px 0 0 0 var(--px-ink), + 2px 0 0 0 var(--px-ink), + inset 0 -2px 0 0 #b8a98a; +} + +.game-steps li strong { + color: var(--px-cream); +} + +/* Tips */ +.game-tips { + list-style: none; + display: flex; + flex-direction: column; + gap: 12px; + margin: 0; + padding: 0; +} + +.game-tips li { + display: flex; + align-items: flex-start; + gap: 12px; + font-size: 0.83rem; + color: #cfc6e2; + line-height: 1.5; +} + +.game-tips li i { + flex: 0 0 auto; + width: 16px; + text-align: center; + margin-top: 3px; + color: var(--px-gold); + font-size: 0.8rem; +} + +/* Leaderboard tabs */ +.game-tabs { + display: flex; + gap: 8px; + margin-bottom: 14px; +} + +.game-tab { + flex: 1; + padding: 8px 6px 7px; + border: none; + border-radius: 0; + background: var(--px-ink); + color: var(--px-lilac); + font-family: var(--px-font); + font-size: 8px; + cursor: pointer; + box-shadow: inset 0 -3px 0 0 var(--px-panel-hi); +} + +.game-tab.is-active { + background: var(--px-panel-hi); + color: var(--px-gold); + box-shadow: inset 0 -3px 0 0 var(--px-gold); +} + +.game-tab:focus-visible { + outline: 2px dashed var(--px-cream); + outline-offset: 2px; +} + +/* Skins: one button per climber look, showing the sprite itself */ +.game-skins { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 18px; +} + +.game-skin { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 8px 4px 7px; + border: none; + border-radius: 0; + background: var(--px-ink); + color: var(--px-cream); + cursor: pointer; + box-shadow: inset 0 0 0 2px var(--px-panel-hi); +} + +.game-skin:hover:not(:disabled) { + box-shadow: inset 0 0 0 2px var(--px-gold-lo); +} + +.game-skin.is-selected { + background: #3a2f1a; + box-shadow: inset 0 0 0 2px var(--px-gold); +} + +.game-skin.is-locked { + cursor: not-allowed; +} + +.game-skin.is-locked .game-skin-sprite { + filter: brightness(0) opacity(0.35); +} + +.game-skin-sprite { + image-rendering: pixelated; + image-rendering: crisp-edges; +} + +.game-skin-name { + font-family: var(--px-font); + font-size: 7px; +} + +.game-skin-lock { + position: absolute; + top: 6px; + right: 6px; + color: var(--px-lilac); + font-size: 0.65rem; +} + +.game-subtitle { + display: flex; + justify-content: space-between; + font-family: var(--px-font); + font-size: 9px; + font-weight: 400; + color: var(--px-gold); + margin: 0 0 10px; +} + +.game-subtitle span { + color: var(--px-lilac); +} + +/* Achievements */ +.game-achievements { + list-style: none; + display: flex; + flex-direction: column; + gap: 6px; + max-height: 260px; + overflow-y: auto; + margin: 0 0 12px; + padding: 0 4px 0 0; +} + +.game-achievement { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 8px; + background: var(--px-ink); + color: var(--px-lilac); + opacity: 0.6; +} + +.game-achievement.is-unlocked { + opacity: 1; + color: var(--px-cream); +} + +.game-achievement-icon { + flex: 0 0 auto; + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + background: var(--px-panel-hi); + font-size: 0.7rem; +} + +.game-achievement.is-unlocked .game-achievement-icon { + background: var(--px-gold); + color: var(--px-ink); +} + +.game-achievement-text { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + font-size: 0.75rem; + line-height: 1.35; +} + +.game-achievement-text strong { + font-family: var(--px-font); + font-size: 8px; + font-weight: 400; + color: inherit; +} + +/* Leaderboard: an arcade high-score table */ +.game-leaderboard-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 4px; + margin: 0 0 14px; + padding: 0; +} + +.game-leaderboard-list li { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 8px; + background: var(--px-ink); + font-size: 0.83rem; +} + +.game-leaderboard-list li:nth-child(even) { + background: #20182f; +} + +.game-leaderboard-list li.is-me { + background: #3a2f1a; + box-shadow: inset 0 0 0 2px var(--px-gold); +} + +.game-leaderboard-list li.game-leaderboard-empty { + color: var(--px-lilac); + font-family: var(--px-font); + font-size: 8px; + line-height: 1.8; + padding: 12px 10px; +} + +.game-leaderboard-rank { + flex: 0 0 auto; + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--px-font); + font-size: 9px; + color: var(--px-lilac); +} + +/* Top three get pixel medals */ +.game-leaderboard-list li:nth-child(-n + 3) .game-leaderboard-rank { + color: var(--px-ink); + box-shadow: + 0 -2px 0 0 var(--px-ink), + 0 2px 0 0 var(--px-ink), + -2px 0 0 0 var(--px-ink), + 2px 0 0 0 var(--px-ink), + inset -2px -2px 0 0 rgba(0, 0, 0, 0.25), + inset 2px 2px 0 0 rgba(255, 255, 255, 0.45); +} + +.game-leaderboard-list li:nth-child(1) .game-leaderboard-rank { + background: #f2c25a; +} + +.game-leaderboard-list li:nth-child(2) .game-leaderboard-rank { + background: #c9ccd6; +} + +.game-leaderboard-list li:nth-child(3) .game-leaderboard-rank { + background: #d08a4e; +} + +.game-leaderboard-list li.game-leaderboard-empty .game-leaderboard-rank { + display: none; +} + +.game-leaderboard-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--px-cream); + text-decoration: none; + font-weight: 600; +} + +.game-leaderboard-name:hover { + color: var(--px-gold); + text-decoration: underline; +} + +.game-leaderboard-me { + flex: 0 0 auto; + padding: 3px 4px 2px; + background: var(--px-gold); + color: var(--px-ink); + font-family: var(--px-font); + font-size: 7px; +} + +.game-leaderboard-height { + flex: 0 0 auto; + font-family: var(--px-font); + font-size: 9px; + color: var(--px-gold); +} + +.game-leaderboard-note { + font-size: 0.78rem; + color: var(--px-lilac); + margin: 0; + line-height: 1.5; +} + +.game-leaderboard-note a, +.game-leaderboard-note strong { + color: var(--px-gold); +} diff --git a/templates/game/index.html.twig b/templates/game/index.html.twig index 5f6675c..a8e69c7 100644 --- a/templates/game/index.html.twig +++ b/templates/game/index.html.twig @@ -6,7 +6,13 @@ {% block stylesheets %} {{ parent() }} - + {# The game page styles, split by section (see public/styles/pages/game/). Order matters. #} + + + + + + {% endblock %} {% block body %} From 799147cbc58ceba537eed1675c0878b1f9a4c52a Mon Sep 17 00:00:00 2001 From: Enrique Velez <145390508+enlike21@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:10:14 +0200 Subject: [PATCH 2/2] Add a repo rule: no file longer than 1000 lines bin/check-file-length runs in the CI quality job and fails on any tracked file over 1000 lines, apart from generated ones (lock files, config/reference.php...). Files that were already too long get a temporary cap so they can't keep growing, each tied to the issue that splits it (#15, #16, #18). Documented in CONTRIBUTING. --- .github/workflows/ci.yml | 3 ++ CONTRIBUTING.md | 14 +++++++++- bin/check-file-length | 59 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100755 bin/check-file-length diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82fd595..9a57a7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Check no file is longer than 1000 lines + run: bin/check-file-length + - name: Setup PHP uses: shivammathur/setup-php@v2 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32efb41..88fc8cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,8 +73,20 @@ Lanza lo mismo que lanza la CI: composer cs-fix # formatea el código PHP (reglas @Symfony) composer phpstan # análisis estático composer test # PHPUnit +bin/check-file-length # ningún archivo de más de 1000 líneas ``` +### Tamaño de los archivos + +Ningún archivo puede pasar de **1000 líneas**: la CI falla si alguno lo hace. Los archivos +largos cuestan de revisar y hacen que los PR choquen entre sí; si uno crece demasiado, +divídelo por responsabilidades (como `public/styles/pages/game/`, que antes era un solo +`game.css`). Quedan fuera los archivos generados (`composer.lock`, `config/reference.php`…). + +Algunos archivos ya eran más largos cuando se puso la regla. Tienen una excepción temporal, +con un tope para que no sigan creciendo, en `bin/check-file-length`; cada uno tiene su issue +para dividirlo, y al cerrarlo se quita su línea de la lista. + Si tocas el minijuego **Al Filo**: la simulación vive en `public/js/game/sim.js` y el servidor la repite línea a línea en `src/Service/Game/ClimbSimulation.php` para validar las partidas. Cualquier cambio en una debe ir en las dos, subiendo `SIM_VERSION`, y @@ -92,7 +104,7 @@ y de la plantilla `templates/game/index.html.twig`. - Una cosa por PR, con una descripción de qué cambia y por qué (la plantilla te guía). - Añade o actualiza tests cuando cambie el comportamiento. - Los mensajes de commit pueden ir en español o en inglés; que expliquen el porqué. -- La CI (estilo, PHPStan y tests) tiene que pasar, y un mantenedor revisará el PR antes de +- La CI (estilo, PHPStan, tests y tamaño de archivos) tiene que pasar, y un mantenedor revisará el PR antes de fusionarlo. Lo que se fusiona en `main` se despliega solo a producción, así que la revisión es obligatoria. diff --git a/bin/check-file-length b/bin/check-file-length new file mode 100755 index 0000000..500c007 --- /dev/null +++ b/bin/check-file-length @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Repository rule: no source file may be longer than MAX_LINES lines. +# Run by CI (.github/workflows/ci.yml) on every push and pull request; run it locally with +# bin/check-file-length +# Long files are hard to review and make PRs collide: split them by responsibility instead. + +set -euo pipefail + +MAX_LINES=1000 + +# Generated or third-party files: never edited by hand, so the rule doesn't apply. +is_excluded() { + case "$1" in + composer.lock|package-lock.json|symfony.lock) return 0 ;; + config/reference.php) return 0 ;; # generated by Symfony + LICENSE) return 0 ;; + assets/vendor/*) return 0 ;; # vendored by importmap + tests/fixtures/*|data/*) return 0 ;; # test fixtures and sample data + *.png|*.jpg|*.jpeg|*.gif|*.webp|*.ico|*.svg|*.woff|*.woff2) return 0 ;; + esac + return 1 +} + +# Temporary exceptions: files that were already too long when the rule was added. Each one +# has an issue to split it and a cap so it can't keep growing. Remove the line once split. +exception_cap() { + case "$1" in + public/js/game/render.js) echo 1700 ;; # issue #15 + public/js/game/main.js) echo 1350 ;; # issue #16 + public/styles/main.css) echo 1100 ;; # issue #18 + public/styles/pages/saved_routes/saved_routes.css) echo 1150 ;; # issue #18 + *) echo "" ;; + esac +} + +failed=0 +while IFS= read -r file; do + [ -f "$file" ] || continue + is_excluded "$file" && continue + lines=$(wc -l < "$file" | tr -d ' ') + cap=$(exception_cap "$file") + limit=${cap:-$MAX_LINES} + if [ "$lines" -gt "$limit" ]; then + if [ -n "$cap" ]; then + echo "✗ $file: $lines lines (temporary cap $cap until it is split - see bin/check-file-length)" + else + echo "✗ $file: $lines lines (max $MAX_LINES) - split it into smaller files" + fi + failed=1 + fi +done < <(git ls-files) + +if [ "$failed" -ne 0 ]; then + echo + echo "Some files are too long. See the \"Tamaño de los archivos\" section in CONTRIBUTING.md." + exit 1 +fi + +echo "✓ No file is longer than $MAX_LINES lines (apart from the listed temporary exceptions)."