diff --git a/PROJECT.md b/PROJECT.md index 86ff629..d256fe4 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -4986,6 +4986,83 @@ shot simulations from the recording (20 seeds each, not a deterministic full-mat return all six soft Champion winners; wide winners against earlier foes and surprise misses remain. Phone balance still needs Oliver's next recording; no further numeric difficulty increase here. +## Session log: THE MENU HAS ONE FRONT DOOR (2026-09-07, LOCAL PREVIEW ONLY) + +Oliver approved the portrait menu audit with two corrections: **Vs a Friend stands on its own**, +and **Other Games needs a prominent splash entrance**. Implemented locally for him to click through +before any deployment; no commit or push made in this pass. + +- Splash keeps the direct league fixture and direct Gore start. Other Games and Vs a Friend are + substantial sibling buttons; World Board stays below. Gore's named court link opens its setup. +- Other Games contains only **The Climb, Training, Classic Tennis**, in two poster rows. No duplicate + cup/Gore, no Training/Casual categories, and Arcade is consistently named The Climb. +- Classic's difficulty is selected inside its setup. Training chooses the opponent BEFORE setup; + Serve Up always starts the selected match. The redundant cup setup is removed. First-time cup + identity entry resumes the cup instead of losing the destination in the game picker. +- Player appearance lives in the shared identity editor, accessible from profile or a compact setup + row. Save/Back returns to the caller; existing players see SAVE PLAYER. Swatches and menu back + buttons have 44px targets; the in-game exit retains its HUD art with a larger invisible hit area. +- Available courts show first; locked courts are disabled compact rows inside a native disclosure. + Picks persist immediately. Setup titles name the actual mode and training shows the chosen foe. +- Stats belongs to the profile and returns there. The profile retains career figures and a fold-out + league roster; duplicate lifetime totals live only in Stats. Playtest recording is a collapsed + native details block below the records; ON/FULL is visible in its summary when applicable. +- Play On is the primary action in the exit dialog. The world board correctly selects Cup after a + league match; Classic falls back to Gore, since there is no Classic leaderboard tab. + +Validation: `node test.js` — **142 pass**. `tools/check-menu.js` is a local browser-tool regression +(no package, excluded from the served image with the existing tools directory): **34 checks pass** +covering fresh cup onboarding, real mode starts, difficulty/opponent identity, editor returns, +locked courts, profile/stats/recording, room cancellation, and 320×568 / 390×844 portrait geometry. +Manual screenshots reviewed for splash, additional modes, setups, profile, stats and player editor. +No gameplay tuning or court mode-transition code changed. **Await Oliver's local review; not deployed.** + +## Session log: MENU PREVIEW POLISH (2026-09-07, LOCAL ONLY) + +Oliver's next local review supersedes the collapsed defaults above: **all three disclosures open +by default** (locked courts, profile league, playtest recording), while remaining user-toggleable. +Other Games and Vs a Friend now have truly centred labels, without the offsetting chevrons. +Cup and Gore share a 76px button height and the same title/subtitle layout. Each button fades from +its existing solid colour on the left into its original mode poster on the right, using CSS only. +The menu card's old 370px content-width cap is removed: width 100% with border-box padding fills +both narrow phones and wider preview panes without horizontal overflow; viewport minimum height +and sticky footers are retained. + +Validation: **39 browser checks pass**, including equal CTA heights, centred labels, open defaults, +and a full-width/full-height card at 768×1024, alongside the existing 320×568 and 390×844 flows. +Screenshots reviewed at both phone sizes. Still local; no commit, push or deployment. + +Local visual follow-up (same day): Oliver prefers roughly **one third colour / two thirds art**. +The Cup/Gore gradient now reveals the poster sooner. Other Games uses the existing arcade poster, +Vs a Friend uses its original multiplayer poster, both with the same soft colour-to-art treatment +and centred, shadowed labels. Button dimensions and navigation are unchanged; still local only. + +Local visual follow-up: a small blood drop grows at the Gore button's lower-right edge, falls +across the court link onto Vs a Friend, slowly traces down its face and drops off its bottom edge. +Two synchronized CSS pseudo-element animations repeat every 18 seconds after an initial 3-second +delay; the trail fades between drops. Their geometry shares the responsive friend-button height. +Decorative only (`pointer-events: none`), active only on a loaded splash, disabled for reduced +motion. No new assets, timers or gameplay changes. Browser checks cover alignment, synchronized +clocks, non-interception, reduced motion and stopping off-splash; still local, not deployed. + +Local logo follow-up: Oliver approved integrating the generated logo proposal. The splash now +uses `assets/logo-v2.png`, a transparent PNG with brighter red lettering, lime Tennis lettering +and a larger ball in the blood underline. The original `assets/logo.png` is retained. Reviewed +at 320×568 and 390×844: clean transparency, correct loading and no horizontal overflow. Local only. + +Logo colour correction: Oliver found the integrated version a little too bright. The splash +now tones it down with CSS `saturate(.78) brightness(.84)`, preserving the exact artwork and alpha. +After trying the proposed ball glow, Oliver decided to remove it entirely: **no tennis-ball glow +or pulse; blood is enough**. Only the small drop from the logo's blood underline remains, every +14 seconds (first after 4 seconds). It is a CSS decoration anchored to the responsive logo, +ignores pointer events, runs only on the loaded, visible splash and respects reduced motion. +The blood version passed all 42 menu browser checks before the glow was removed. Still local only. + +Release approval (2026-09-07): Oliver approved this menu/logo revision for staging and production. +Release cache version: `main.js?v=353`. Pre-release validation: 142 game tests and all 42 mobile +menu browser checks pass. Deploy staging first, verify the served release, then promote the same +commit to production. Staging URL: `https://splatter-tennis-staging.wuwi.one`. + # OPEN — leftovers, in rough priority order Nothing here is broken; these are known gaps and unverified risks. diff --git a/assets/logo-v2.png b/assets/logo-v2.png new file mode 100644 index 0000000..b7eeb7b Binary files /dev/null and b/assets/logo-v2.png differ diff --git a/index.html b/index.html index de9d636..82468bb 100644 --- a/index.html +++ b/index.html @@ -117,6 +117,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat system font and drew a smooth glyph inside a pixel HUD */ color: var(--cream); font: 700 14px Silkscreen, monospace; line-height: 1; cursor: pointer; -webkit-tap-highlight-color: transparent; } + #qChip::after { content: ""; position: absolute; top: -4px; right: -4px; min-width: 44px; min-height: 44px; width: 100%; height: 100%; } #quit button:active { background: var(--red); border-color: var(--bad); color: #f2ead6; } #qAsk { position: fixed; inset: 0; z-index: 6; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; } @@ -165,10 +166,8 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat #hot { position: fixed; top: 0; left: 0; width: 26vw; max-width: 150px; height: 15vh; max-height: 130px; z-index: 4; } #intro .card { font: 400 12px Silkscreen, monospace; color: var(--cream); text-align: center; - line-height: 1.9; padding: 16px 14px 20px; width: min(370px, 100vw - 28px); /* CONTENT width (content-box) beside 14px+14px padding: - 94vw put the card 5-28px past the viewport on every phone (0.94w+28 > w below - 466px) and #intro's overflow-y:auto computes overflow-x to auto — that was the - sliver of horizontal scroll Oliver caught on the splash */ + line-height: 1.9; width: 100%; min-width: 0; box-sizing: border-box; + /* Padding belongs inside the full-width card, including on wider preview panes. */ animation: drop 0.5s ease-out; } @keyframes drop { from { transform: translateY(-14px); opacity: 0; } } /* EVERY SCREEN FILLS THE VIEWPORT, and every screen is the same three bands: something pinned to @@ -180,9 +179,27 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat #intro .card { display: flex; flex-direction: column; min-height: 100dvh; padding: 0 14px; } /* the slack band: the title sits in the middle of whatever is left over */ .mid { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 12px 0; } - /* the logo is generated pixel art (assets/logo.png), not stacked CSS text */ - #intro .logo { display: block; width: min(300px, 74vw); height: auto; margin: 0 auto 6px; - image-rendering: pixelated; filter: drop-shadow(0 3px 0 rgba(0,0,0,0.45)); } + /* the logo is generated pixel art (assets/logo-v2.png), not stacked CSS text */ + #intro .logoArt { position: relative; width: min(300px, 74vw); margin: 0 auto 6px; } + #intro .logo { display: block; width: 100%; height: auto; + image-rendering: pixelated; filter: saturate(.78) brightness(.84) drop-shadow(0 3px 0 rgba(0,0,0,0.45)); } + .logoArt::after { content: ''; position: absolute; pointer-events: none; opacity: 0; } + /* The drop follows the blood underline in logo-v2.png at every viewport size. */ + .logoArt::after { left: 62%; top: 94%; width: 1.3%; height: 5%; + background: #861d18; border-radius: 35% 35% 55% 55%; transform-origin: top; + box-shadow: inset 1px 0 rgba(196,69,48,.45); } + #intro:not([hidden])[data-stage="splash"] #splash:not(.busy) .logoArt::after { + animation: logoBloodDrop 14s 4s infinite; } + @keyframes logoBloodDrop { + 0% { opacity: 0; transform: scale(.5, .15); } + 3% { opacity: .9; transform: scale(.8, .5); } + 10% { opacity: .9; transform: translateY(2px) scale(1, 1.1); animation-timing-function: ease-in; } + 14% { opacity: .8; transform: translateY(12px) scale(.7, 1.3); } + 17%, 100% { opacity: 0; transform: translateY(20px) scale(.6, .8); } + } + @media (prefers-reduced-motion: reduce) { + #intro:not([hidden])[data-stage="splash"] #splash:not(.busy) .logoArt::after { animation: none; } + } /* the slogan carries most of the gap between the logo's blood and the first button */ #intro .slogan { margin: 8px 0 20px; font-size: 11px; color: #a8a09e; letter-spacing: 0.5px; } #intro .slogan b { color: #c9564a; } @@ -191,13 +208,12 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat /* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame alive across every screen is the point — a screen that unmounts the whole overlay flashes the court through between menus. */ - #splash, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #stsub, #vs, #train, #player, #promo, #card { display: none; } + #splash, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #vs, #train, #player, #promo, #card { display: none; } #intro[data-stage="splash"] #splash { display: flex; flex-direction: column; flex: 1; } /* flex COLUMN, not block: it is what lets the footer's margin-top:auto reach the bottom of a screen whose content does not fill it */ #intro[data-stage="setup"] #setup, #intro[data-stage="mode"] #stmode, - #intro[data-stage="modesub"] #stsub, #intro[data-stage="stats"] #stats, #intro[data-stage="ranks"] #ranks, #intro[data-stage="secret"] #secret, @@ -221,18 +237,13 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat #intro.deal > .card > div:not(#splash) > *:nth-child(3) { animation-delay: 0.16s; } #intro.deal > .card > div:not(#splash) > *:nth-child(4) { animation-delay: 0.23s; } #intro.deal > .card > div:not(#splash) > *:nth-child(n+5) { animation-delay: 0.30s; } - /* the poster wall deals its ROOMS, not one lump: the list is the whole screen there, so letting - it arrive as a single band wastes the entrance. The list itself stops animating — a moving - child inside a moving parent starts at twice the offset and reads as a stutter. */ #intro.deal #stmode > #modeList { animation: none; } - #intro.deal #modeList > .mgroup { - animation: dealIn 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; } - #intro.deal #modeList > .mgroup:nth-child(1) { animation-delay: 0.09s; } - #intro.deal #modeList > .mgroup:nth-child(2) { animation-delay: 0.17s; } - #intro.deal #modeList > .mgroup:nth-child(3) { animation-delay: 0.25s; } + #intro.deal #modeList > button { animation: dealIn 0.28s ease-out backwards; } + #intro.deal #modeList > button:nth-child(2) { animation-delay: 0.07s; } + #intro.deal #modeList > button:nth-child(3) { animation-delay: 0.14s; } @media (prefers-reduced-motion: reduce) { - #intro.deal > .card > div:not(#splash) > *, - #intro.deal #modeList > .mgroup { animation: none; } } + #intro.deal > .card > div:not(#splash) > *, #intro.deal #modeList > button { animation: none; } + } /* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the display:grid above would otherwise win over the hidden attribute */ #intro[hidden] { display: none; } @@ -258,7 +269,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat .hdr h2 { padding-left: 2px; } /* (its face is set with the other titles above) */ /* UPRIGHT, not Bangers: the announcement face is drawn slanted, and a slanted arrow read as italic on every screen. Silkscreen's ASCII < > are straight pixel arrows. */ - .chev { width: 34px; height: 34px; flex: none; background: none; border: 2px solid var(--line); + .chev { width: 44px; height: 44px; flex: none; background: none; border: 2px solid var(--line); color: var(--acid); font: 700 14px Silkscreen, monospace; line-height: 1; cursor: pointer; -webkit-tap-highlight-color: transparent; } /* the BODY COUNT headline row: three boxed figures sharing one frame */ @@ -304,27 +315,27 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat /* the secondary doors NAME (Jersey) and carry a NUMBER (Silkscreen, acid) — the text reduction turned "RANKINGS / WORLD BOARD" into "BOARD / BEST 612": a label and a fact, no description */ .btn.sec { font: 400 18px/1.2 'Jersey 15', Silkscreen, monospace; letter-spacing: 1px; } - #qNo { font: 400 17px/1.2 'Jersey 15', Silkscreen, monospace; letter-spacing: 1px; } + #qYes { font: 400 17px/1.2 'Jersey 15', Silkscreen, monospace; letter-spacing: 1px; } .btn.sec i { display: block; margin-top: 3px; font: 700 10px Silkscreen, monospace; font-style: normal; color: var(--acid); letter-spacing: 1px; } /* THE PLAYER BLOCK: the live man on the left, everything that changes him on the right, so a tap and its result are never more than a thumb apart and the whole of "who am I" is one screenful. */ - #who, #who2 { display: flex; gap: 12px; align-items: flex-start; } - #whoCtl, #pCtl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; } + #who2 { display: flex; gap: 12px; align-items: flex-start; } + #pCtl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; } /* The live player. `position: static` is not decoration: the bare `canvas` rule above is the COURT — fixed, full-bleed, 100dvh — and it catches any canvas in the document, so this one flew to the top-left corner of the screen until it was told otherwise. Every geometry property that rule sets has to be undone here. The buffer is the sprite's own 96x224, so width follows height. */ - #look, #pLook { position: static; display: block; flex: none; margin: 0; + #pLook { position: static; display: block; flex: none; margin: 0; width: auto; height: 176px; touch-action: auto; image-rendering: pixelated; } /* the tour is now TWO WORDS, not two portraits — the live player next to them is the picture, and a second drawing of the same man (front-facing, and deaf to every colour on this screen) was both a duplicate and the reason the court list started below the fold */ - #tours, #pTours { display: flex; gap: 7px; } - #tours button, #pTours button { flex: 1; padding: 10px 2px; background: var(--panel); border: 2px solid var(--line); + #pTours { display: flex; gap: 7px; } + #pTours button { flex: 1; padding: 10px 2px; background: var(--panel); border: 2px solid var(--line); color: var(--dim); font: 400 10px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer; -webkit-tap-highlight-color: transparent; } - #tours button.on, #pTours button.on { border-color: var(--acid); background: rgba(188,212,85,0.09); color: var(--cream); } + #pTours button.on { border-color: var(--acid); background: rgba(188,212,85,0.09); color: var(--cream); } /* THE IDENTITY SCREEN (the leagues, phase 1): name row = the input plus the die. */ #nameRow { display: flex; gap: 8px; } #nameRow .tin { flex: 1; margin: 0; } @@ -538,110 +549,57 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat text-decoration: none; letter-spacing: 0.5px; } /* a row, not a stacked tile: three 3:2 tiles stacked fill a phone twice over, and the art still reads at this size. -webkit-tap-highlight-color kills iOS's blue flash over the pixel art. */ - /* MODE posters are pure BUTTONS, not selectors (Oliver: the remembered highlight made each one - "a checkbox and a button combined"). Every card is full-bright and equal because every card is - a live answer to the header's question; tapping answers and advances. The only memory shown is - the LAST tag — an indicator, not a selection. A border that means "SERVE UP will use this" - exists solely on the final screen, where a confirm button actually follows. */ - #modeList button.mode, #modeSub button.mode { display: flex; align-items: flex-end; width: 100%; min-height: 96px; - margin: 0 0 8px; padding: 7px 9px; background-size: cover; background-position: center bottom; - image-rendering: pixelated; border: 2px solid var(--line); color: inherit; font: inherit; - text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; - position: relative; } - /* THE CAPTION'S VEIL (2026-08-31). The blur went in and came straight back out — on - nearest-neighbour art it read as a smear, not as depth (Oliver: "sieht komisch aus"). What is - left is what he asked for instead: a gradient RISING FROM THE BOTTOM EDGE, solid where the - words are and gone by the time it reaches the art. Same job, no filter, no edge. */ - #modeList button.mode::after, #modeSub button.mode::after { - content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 62%; - background: linear-gradient(0deg, rgba(6,10,8,0.94) 0%, rgba(6,10,8,0.86) 22%, - rgba(6,10,8,0.5) 58%, rgba(6,10,8,0) 100%); - pointer-events: none; } - /* the words and the chips ride ABOVE the veil — a pseudo-element is the last thing painted */ - #modeList .cb, #modeSub .cb { position: relative; z-index: 2; } - #modeList b, #modeSub b { text-shadow: 0 1px 0 #000, 0 0 6px rgba(0,0,0,0.9); } - /* the subtitle carries the mode's one-line rules over busy art, so it gets WHITE at full - opacity, a size up from the court rows' 9px, and a solid black underlay — "barely readable" - at cream-on-crowd was the report from the first live look */ - #modeList i, #modeSub i { color: #fff; font-size: 10px; margin-bottom: 0; - text-shadow: 0 1px 0 #000, 1px 0 0 #000, -1px 0 0 #000, 0 -1px 0 #000, 0 0 7px #000; } - /* THE SECTIONS (THE SEASON): the wall is three rooms — one path up, a room to practise in, a - room where nothing counts — so each is a real section with its posters INSIDE it, not a label - floating over a stack. The heading is deliberately NOT the announcement face (Oliver: it read - as a second card headline and the two competed); Silkscreen, big and letter-spaced, keeps it - a label of a room. No rule, no left band either — the air above each heading does the - grouping, which is what air is for. */ - #modeList .mgroup { margin: 0 0 10px; } - #modeList .mgroup + .mgroup { margin-top: 17px; } - /* HOW A ROOM DEALS ITS POSTERS (the text reduction). The wall used to be six identical 148px - slabs and ran over two screens; the fixture is a HERO now, the training pair stands side by - side, and the casual three stack shorter. All of it fits one screen. */ - #modeList .mgroup .shelf { display: flex; flex-direction: column; } - #modeList .mgroup.pair .shelf { flex-direction: row; gap: 8px; } - #modeList .mgroup.pair button.mode { flex: 1; min-width: 0; min-height: 106px; margin-bottom: 0; } - #modeList .mgroup.stack button.mode { min-height: 96px; } - #modeList .mgroup.hero button.mode { min-height: 142px; } - /* the poster names: Jersey NAMES, and the hero is the one poster that CALLS (Bangers) */ - #modeList .cb b { font: 400 30px/1 'Jersey 15', Bangers, cursive; letter-spacing: 0.5px; } - #modeList .mgroup.pair .cb b { font-size: 27px; } - #modeList .mgroup.hero .cb b { font: 400 32px/0.95 Bangers, cursive; letter-spacing: 1px; } - /* ...and the hero's second line is the fixture, the only strap left on the wall */ - #modeList .mgroup.hero .cb i { color: #f0cfc4; font-size: 10px; margin-top: 3px; } - /* text-align rides down from the card, which centres everything on these screens — the section - header is a header, and headers on this screen are left-aligned (the .hdr title, the posters' - own captions) */ - #modeList .mgroup > header { margin: 0 0 11px; text-align: left; } - /* THE SHELF LABELS SHOUT, quietly (2026-08-31, Oliver tried three: Bangers first, then the HUD - face, then the platform's UI face, and came back to the announcement face). It works here now - because the header lost its rule and its band and gained air: the size does the hierarchy - (20 against the screen title's 26) instead of the typeface having to. */ - /* SCOPED TO THE HEADER, not to the section: the posters live INSIDE .mgroup now, so a bare - descendant selector here restyled every caption on the wall (it stayed invisible while this - rule still named Silkscreen — the moment the family changed, the posters changed with it). */ - #modeList .mgroup > header b { display: block; letter-spacing: 0.5px; color: var(--acid); - text-shadow: none; font: 400 17px/1.2 'Jersey 15', Bangers, cursive; - letter-spacing: 2px; } - /* ...and the strap goes back to the quiet register under it: a shouted line over a system-sans - line was two foreign voices in one header */ - #modeList .mgroup > header i { display: block; margin: 4px 0 0; letter-spacing: 0.5px; - color: var(--dim); font-style: normal; text-shadow: none; - font: 400 9px/1.4 Silkscreen, monospace; } - #modeList .mgroup button.mode:last-child { margin-bottom: 0; } - /* THE SECOND STEP (config.MENU) has its own SCREEN in the wizard: sub-mode cards are - full-height posters centred on their scene, and the difficulty chips are plain acid-outline - buttons stacked tall — art for EASY/MEDIUM/HARD would be forced, and the chip reads faster. */ - /* alone on their own screens, the posters get room to be posters — and the first card clears - the header by the same breath the h3 rows used to give the old single page */ - #stmode #modeList, #stsub #modeSub { margin-top: 12px; } - #stmode button.mode, #modeSub button.mode { min-height: 148px; } - #modeSub { display: flex; flex-wrap: wrap; gap: 8px; } - #modeSub button.mode { background-position: center; } - #modeSub button.chip { flex: 1 1 100%; display: flex; align-items: center; justify-content: space-between; - gap: 10px; padding: 26px 16px; background: var(--panel); border: 2px solid var(--line); - color: var(--cream); cursor: pointer; -webkit-tap-highlight-color: transparent; - position: relative; text-align: left; } - #modeSub button.chip b { font: 400 30px/1 'Jersey 15', Silkscreen, monospace; letter-spacing: 1px; } - /* how hard it is, counted: as many pips lit as the chip is steep, and the last one wears the - colour the game uses for a threat */ - .pips { display: flex; gap: 5px; } - .pips i { width: 10px; height: 10px; background: rgba(255,255,255,0.12); } - .pips i.on { background: var(--acid); } - #modeSub button.chip.hot .pips i.on { background: var(--bad); } - #courtList button.court { display: flex; align-items: center; gap: 11px; width: 100%; + /* The additional modes fit in two rows; the friend has a door of its own on the splash. */ + #modeList { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 18px 0; } + #modeList button.mode { display: flex; align-items: flex-end; position: relative; min-width: 0; + min-height: 150px; padding: 12px; border: 2px solid var(--line); color: var(--cream); + background-size: cover; background-position: center; image-rendering: pixelated; + text-align: left; cursor: pointer; } + #modeList button.mode:last-child { grid-column: 1 / -1; } + #modeList button.mode::after { content: ''; position: absolute; inset: 0; pointer-events: none; + background: linear-gradient(0deg, rgba(6,10,8,.94), rgba(6,10,8,.2) 65%, transparent); } + #modeList .cb { position: relative; z-index: 1; } + #modeList .cb b { font: 400 29px/1 'Jersey 15', cursive; letter-spacing: .5px; } + .playerLink { display: flex; align-items: center; justify-content: space-between; gap: 12px; + width: 100%; min-height: 60px; margin: 16px 0 0; padding: 10px 12px; text-align: left; + background: var(--panel); border: 2px solid var(--line); color: var(--cream); cursor: pointer; } + .playerLink b { display: block; font: 22px/1 'Jersey 15', cursive; } + .playerLink i { display: block; margin-top: 5px; font: 9px Silkscreen, monospace; + color: var(--dim); font-style: normal; } + .playerLink u { font: 17px 'Jersey 15', cursive; text-decoration: none; color: var(--acid); } + .opponentRow { display: flex; align-items: center; gap: 14px; margin-top: 18px; text-align: left; } + .opponentRow img { height: 50px; width: 48px; object-fit: contain; image-rendering: pixelated; } + .opponentRow b { display: block; font: 24px 'Jersey 15', cursive; } + .opponentRow i { font: 10px Silkscreen, monospace; font-style: normal; color: var(--dim); } + .setupNote { margin: 18px 0 0; font: 10px/1.6 Silkscreen, monospace; color: var(--dim); } + #sDifficulty[hidden], #sOpponent[hidden], #sClimb[hidden], #lockedCourts[hidden] { display: none; } + #difficultyList { margin: 0; } + #difficultyList button { font: 20px 'Jersey 15', cursive; } + .disclosure { margin: 14px 0; text-align: left; border-top: 1px solid var(--line); } + .disclosure > summary { display: flex; align-items: center; justify-content: space-between; + min-height: 48px; gap: 10px; list-style: none; cursor: pointer; color: var(--cream); + font: 18px/1.2 'Jersey 15', cursive; letter-spacing: .5px; } + .disclosure > summary::-webkit-details-marker { display: none; } + .disclosure > summary::after { content: '+'; color: var(--acid); font: 16px Silkscreen, monospace; } + .disclosure[open] > summary::after { content: '−'; } + #cdStats { display: flex; justify-content: space-between; align-items: center; margin: 16px 0 0; + min-height: 52px; border-color: #627340; color: var(--acid); text-align: left; font-size: 23px; } + #pNote[hidden] { display: none; } + :is(#courtList, #lockedCourtList) button.court { display: flex; align-items: center; gap: 11px; width: 100%; margin: 0 0 8px; padding: 7px; background: var(--panel); border: 2px solid var(--line); color: inherit; font: inherit; text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; } - #courtList button.court.on { border-color: var(--acid); background: rgba(188,212,85,0.09); } - #courtList img { width: 84px; height: 60px; display: block; image-rendering: pixelated; flex: none; + :is(#courtList, #lockedCourtList) button.court.on { border-color: var(--acid); background: rgba(188,212,85,0.09); } + :is(#courtList, #lockedCourtList) img { width: 84px; height: 60px; display: block; image-rendering: pixelated; flex: none; object-fit: cover; } - #courtList .cb, #modeList .cb, #modeSub .cb { flex: 1; min-width: 0; } - #courtList b, #modeList b, #modeSub b { display: block; font: 700 15px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; } + :is(#courtList, #lockedCourtList) .cb, #modeList .cb { flex: 1; min-width: 0; } + :is(#courtList, #lockedCourtList) b, #modeList b { display: block; font: 700 15px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; } /* the court NAMES itself (Jersey) and its two meters say the rest — the description line is gone */ - #courtList b { font: 400 22px/1.1 'Jersey 15', Silkscreen, monospace; letter-spacing: 0.5px; } - #courtList i { margin: 3px 0 5px; font: 700 9px Silkscreen, monospace; color: var(--bad); + :is(#courtList, #lockedCourtList) b { font: 400 22px/1.1 'Jersey 15', Silkscreen, monospace; letter-spacing: 0.5px; } + :is(#courtList, #lockedCourtList) i { margin: 3px 0 5px; font: 700 9px Silkscreen, monospace; color: var(--bad); letter-spacing: 1px; } - #courtList i[hidden] { display: none; } - #courtList i, #modeList i, #modeSub i { display: block; margin: 4px 0 6px; font: 400 10px Silkscreen, monospace; color: #c9c4b8; font-style: normal; } + :is(#courtList, #lockedCourtList) i[hidden] { display: none; } + :is(#courtList, #lockedCourtList) i, #modeList i { display: block; margin: 4px 0 6px; font: 400 10px Silkscreen, monospace; color: #c9c4b8; font-style: normal; } /* The two meters read straight off the court's own bounce numbers (see main.js), so they cannot drift away from what the ball actually does on it. */ .meter { display: flex; align-items: center; gap: 6px; margin-top: 4px; @@ -661,21 +619,105 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat z-index: 5; color: #ffd8ca; background: #59251f; padding: 4px 6px; font: 10px Silkscreen, monospace; pointer-events: none; } #recLamp[hidden], body:has(#intro:not([hidden])) #recLamp { display: none; } - #recorder { border-top: 1px solid var(--line); padding-top: 10px; margin-top: 14px; } + #recorder { margin-top: 24px; margin-bottom: 24px; } #recorder p { font: 11px/1.5 Silkscreen, monospace; color: var(--dim); } #recorder .btn { min-height: 44px; } #recorder .btn:disabled { opacity: 0.4; } #recNew[hidden] { display: none; } - /* Gore gets a front door; the records become quiet navigation, with full touch targets. */ - #mGore { display: flex; align-items: center; justify-content: center; gap: 12px; - padding: 9px 12px; border-color: #804338; background: #281b19; color: #f1d6bf; } - #mGore b { font: 400 23px/1.1 Bangers, cursive; letter-spacing: 1px; } - #mGore i { margin: 0; color: #d5aa91; font: 400 9px/1.5 Silkscreen, monospace; letter-spacing: 0; } + /* Equal-sized entrances: solid mode colour fades into each mode's original poster. */ + #mStart, #mGore { display: flex; flex-direction: column; align-items: center; justify-content: center; + box-sizing: border-box; height: 76px; min-height: 76px; padding: 10px 12px; gap: 4px; + background-size: cover; background-position: center; image-rendering: pixelated; + color: #f6e7dc; text-align: center; } + #mStart { background-image: linear-gradient(90deg, #a4231f 0%, #a4231f 18%, + rgba(164,35,31,.80) 30%, rgba(164,35,31,.12) 50%, rgba(0,0,0,.22) 100%), url('assets/mode_cup.png'); } + #mGore { border-color: #804338; background-color: #281b19; + background-image: linear-gradient(90deg, #281b19 0%, #281b19 18%, + rgba(40,27,25,.80) 30%, rgba(40,27,25,.12) 50%, rgba(0,0,0,.22) 100%), url('assets/mode_gore.png'); } + #mStart b, #mGore b { font: 400 26px/1.1 Bangers, cursive; letter-spacing: 1px; + text-shadow: 0 1px 3px rgba(0,0,0,.85); } + #mStart i, #mGore i { margin: 0; color: #f0d8c8; font: 400 9px/1.5 Silkscreen, monospace; + letter-spacing: .3px; text-shadow: 0 1px 3px #000; } .splashLinks { display: flex; gap: 4px; margin-top: 5px; } .splashLinks .btn.sec { flex: 1; width: auto; min-height: 44px; padding: 7px 2px; background: transparent; border-color: transparent; color: #ccc4b5; font-size: 16px; letter-spacing: 0; } - .splashLinks #mOther { flex: 1.5; } + .splashDoors { --door-height: 58px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 4px; } + .splashDoors .btn.sec { display: flex; align-items: center; justify-content: center; + min-height: var(--door-height); padding: 10px; text-align: center; font-size: 22px; letter-spacing: .3px; } + #mOther { border-color: #8eaa4d; color: #e6efbf; background-color: #263421; + background-image: linear-gradient(90deg, #263421 0%, #263421 18%, + rgba(38,52,33,.80) 30%, rgba(38,52,33,.20) 50%, rgba(0,0,0,.38) 100%), url('assets/mode_arcade.png'); } + #mVs { border-color: #607e6b; color: #edf1e8; background-color: #26322c; + background-image: linear-gradient(90deg, #26322c 0%, #26322c 18%, + rgba(38,50,44,.80) 30%, rgba(38,50,44,.20) 50%, rgba(0,0,0,.38) 100%), url('assets/mode_vs.png'); } + #mOther, #mVs { background-size: cover; background-position: center; image-rendering: pixelated; + text-shadow: 0 1px 2px #000, 0 0 5px #000; } + /* One occasional drop: Gore's lower edge -> friend button -> a slow run down its face. + 50px bridges the 44px court link, 4px row gap and the friend's 2px border. + Both pieces share one CSS clock; no game-loop work, DOM additions or touch interception. */ + #mVs { position: relative; --blood-gap: 50px; } + #mVs::before, #mVs::after { content: ''; position: absolute; pointer-events: none; opacity: 0; } + #mVs::before { z-index: 2; right: 14px; top: calc(-1 * var(--blood-gap)); width: 6px; height: 9px; + background: linear-gradient(90deg, #690b12 0%, #bb2027 55%, #e04436 75%, #8e1019 100%); + clip-path: polygon(40% 0, 60% 0, 100% 55%, 100% 78%, 70% 100%, 30% 100%, 0 78%, 0 55%); } + #mVs::after { z-index: 1; right: 16px; top: 0; width: 2px; height: calc(100% - 3px); + background: #9a1820; box-shadow: 1px 0 #510b10; transform-origin: top; } + #intro[data-stage="splash"] #splash:not(.busy) #mVs::before { animation: menuBloodDrop 18s 3s infinite; } + #intro[data-stage="splash"] #splash:not(.busy) #mVs::after { animation: menuBloodTrail 18s 3s infinite; } + @keyframes menuBloodDrop { + 0% { opacity: 0; transform: translateY(0) scale(.25, .4); } + 2% { opacity: 1; transform: translateY(1px) scale(.9, 1.1); } + 5% { opacity: 1; transform: translateY(4px) scale(1, 1.3); animation-timing-function: ease-in; } + 7% { opacity: 1; transform: translateY(var(--blood-gap)) scale(.8, 1.2); } + 7.5% { opacity: 1; transform: translateY(var(--blood-gap)) scale(1.7, .55); } + 9% { opacity: 1; transform: translateY(calc(var(--blood-gap) + 2px)) scale(1); animation-timing-function: linear; } + 32% { opacity: 1; transform: translateY(calc(var(--blood-gap) + var(--door-height) - 10px)) scale(1, 1.15); } + 35% { opacity: .8; transform: translateY(calc(var(--blood-gap) + var(--door-height) + 8px)) scale(.7, 1.3); } + 36%, 100% { opacity: 0; transform: translateY(calc(var(--blood-gap) + var(--door-height) + 16px)); } + } + @keyframes menuBloodTrail { + 0%, 7% { opacity: 0; transform: scaleY(0); } + 7.5% { opacity: .85; transform: scaleY(.04); } + 9% { opacity: .85; transform: scaleY(.06); animation-timing-function: linear; } + 32% { opacity: .85; transform: scaleY(1); } + 35% { opacity: .6; transform: scaleY(1); } + 48%, 100% { opacity: 0; transform: scaleY(1); } + } + @media (prefers-reduced-motion: reduce) { + #intro[data-stage="splash"] #splash:not(.busy) #mVs::before, + #intro[data-stage="splash"] #splash:not(.busy) #mVs::after { animation: none; } + } + .courtLink { display: block; width: 100%; min-height: 44px; padding: 6px 0; border: 0; + background: transparent; color: #d5aa91; font: 10px Silkscreen, monospace; cursor: pointer; } + .splashLinks { margin-top: 0; } + #splash #build { margin-top: 3px; } + @media (max-height: 680px) { + #intro .logoArt { width: min(220px, 55vw); } + #intro .slogan { font-size: 9px; margin: 4px 0 8px; } + #splash .mid { padding: 6px 0; } + #splash #plaque { margin-top: 8px; } + .splashDoors { --door-height: 50px; } + .splashDoors .btn.sec { font-size: 20px; } + #splash .foot { padding-top: 4px; } + #modeList button.mode { min-height: 128px; } + } + :is(#courtList, #lockedCourtList) button.court.lock { opacity: .7; } + #lockedCourtList button.court img { width: 56px; height: 40px; } + #lockedCourtList .meter { display: none; } + .tabs button { min-height: 44px; } + #pTours button { min-height: 44px; } + /* Colour choices span the screen: six swatches still have 44px targets on a 320px phone. */ + #who2 { align-items: center; } + #pLook { height: 144px; } + #pTours { flex-direction: column; } + .playerColours { display: flex; flex-direction: column; gap: 7px; margin: 12px 0 20px; } + .playerColours .swrow { gap: 5px; } + .playerColours .swrow button { min-height: 44px; aspect-ratio: auto; height: 44px; } + .playerColours .swlab:not(:first-child) { margin-top: 8px; } + #pName { min-width: 0; } + #intro h3 span { font-size: 9px; } + button:focus-visible, summary:focus-visible { outline: 2px solid var(--acid); outline-offset: 3px; } /* the splash's bottom band: no bar, no rule — the court art has to keep running under it */ .foot.bare { border: 0; background: none; } .foot.bare .btn.big { margin-bottom: 10px; } @@ -696,6 +738,8 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat #intro.deal #splash #plaque { animation-delay: 0.02s; } #intro.deal #splash #mStart { animation-delay: 0.12s; } #intro.deal #splash #mGore { animation-delay: 0.19s; } + #intro.deal #splash #mCourt { animation-delay: 0.23s; } + #intro.deal #splash .foot.bare > .splashDoors { animation-delay: 0.26s; } /* the navigation row is a CLASS, so it needs the same weight as the shorthand above to override its delay — the id selectors around it get there on their own */ #intro.deal #splash .foot.bare > .splashLinks { animation-delay: 0.26s; } @@ -711,7 +755,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat #boot u { display: block; margin-top: 7px; text-align: center; text-decoration: none; font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 2px; } #splash.busy #boot { display: block; } - #splash.busy .btn, #splash.busy .splashLinks, #splash.busy #build { display: none; } + #splash.busy .btn, #splash.busy .splashLinks, #splash.busy .splashDoors, #splash.busy .courtLink, #splash.busy #build { display: none; } /* NOTHING DATA-DRIVEN DURING THE BOOT (2026-08-31, Oliver: "die oberfläche ist hier regelmäßig in einem undefinierten zustand"). The plaque used to stand there as a skeleton, which is still a plaque saying LEVEL 1 to a level-25 player for as long as the profile takes to land. The @@ -744,6 +788,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat font-style: normal; color: var(--dim); letter-spacing: 1px; } /* a GRID, not a wrapping flex row: `flex: 1 1 62px` gave the last row's tiles the whole width to share, so a shelf of nine came out in three different sizes (Oliver) */ + #stW > .none { grid-column: 1 / -1; } #stW { display: grid; grid-template-columns: repeat(auto-fill, minmax(66px, 1fr)); gap: 6px; } .wtile { padding: 8px 2px; border: 2px solid var(--line); background: var(--panel); } .wtile img { width: 34px; height: 34px; object-fit: contain; display: block; margin: 0 auto 2px; @@ -818,12 +863,12 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
LEAVE THE MATCH?
- - + +
+ 
Fifteen. Thirty. Forty. Dead.
@@ -864,66 +909,41 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat already knows the answer to. It names the next cup fixture now and enters the draw; Gore has its own secondary door; the other modes live behind OTHER GAMES. --> - -THE CLUB WILL LEARN YOUR NAME.
+THE CLUB WILL LEARN YOUR NAME.
Record league, Climb and training points to investigate difficulty. Export here after playing.
-