fix: viewport-fixed layout on all pages for mobile

- Replace 100vh with 100dvh everywhere (accounts for mobile browser chrome)
- DocsPage: viewport-fixed with tabs pinned, tab content scrolls internally
- BotProfilePage: overflow-hidden outer, internal scroll container
- JoinBoutPage: viewport-fixed with internal overflow scroll
- HumanFightPage: viewport-fixed, flex-1 content area with internal scroll
- All other pages: vh → dvh for correct mobile sizing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 15:25:34 +00:00
co-authored by Claude Opus 4.6
parent 8a345f5e56
commit 896e81960f
10 changed files with 85 additions and 40 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ const tierClass = (t: number) => `tier-${t}`
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col px-3 sm:px-6 py-4 sm:py-6 overflow-hidden">
<div class="h-[calc(100dvh-4rem)] flex flex-col px-3 sm:px-6 py-4 sm:py-6 overflow-hidden">
<div class="max-w-5xl mx-auto w-full flex flex-col flex-1 min-h-0">
<!-- Header -->
+3 -3
View File
@@ -343,8 +343,8 @@ const tierClass = (t: number) => `tier-${t}`
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col px-6 py-6 overflow-y-auto">
<div class="max-w-lg lg:max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0">
<div class="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden">
<div class="max-w-lg lg:max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0 px-6 py-4 overflow-y-auto">
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
<p class="font-display text-text-muted animate-pulse">LOADING...</p>
@@ -355,7 +355,7 @@ const tierClass = (t: number) => `tier-${t}`
</div>
<template v-else>
<div class="lg:flex lg:gap-6 lg:items-stretch" style="min-height: calc(100vh - 8rem);">
<div class="lg:flex lg:gap-6 lg:items-stretch">
<!-- LEFT COLUMN: Character + Stats + Actions -->
<div class="lg:w-[380px] lg:shrink-0 flex flex-col">
+11 -7
View File
@@ -173,11 +173,11 @@ const registrationTest = `{
</script>
<template>
<div class="min-h-[calc(100vh-4rem)] px-6 py-8 max-w-4xl mx-auto">
<div class="slide-up">
<div class="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden">
<div class="flex flex-col flex-1 min-h-0 px-6 py-4 max-w-4xl mx-auto w-full">
<div class="text-center mb-8">
<h2 class="font-display font-black text-3xl tracking-wider gradient-text mb-2">
<div class="text-center mb-4 shrink-0">
<h2 class="font-display font-black text-3xl tracking-wider gradient-text mb-1">
BOT SETUP GUIDE
</h2>
<p class="font-mono text-text-muted text-xs">
@@ -185,12 +185,12 @@ const registrationTest = `{
</p>
</div>
<div v-if="error" class="p-4 border-2 bg-ko/5 border-ko/30 text-ko font-mono text-xs">
<div v-if="error" class="p-4 border-2 bg-ko/5 border-ko/30 text-ko font-mono text-xs shrink-0">
{{ error }}
</div>
<!-- Tab nav -->
<div class="flex flex-wrap gap-1 mb-6 border-b border-border">
<!-- Tab nav (fixed) -->
<div class="flex flex-wrap gap-1 mb-4 border-b border-border shrink-0">
<button
v-for="tab in tabs"
:key="tab"
@@ -204,6 +204,9 @@ const registrationTest = `{
</button>
</div>
<!-- Scrollable tab content -->
<div class="flex-1 min-h-0 overflow-y-auto">
<!-- GUIDE TAB -->
<div v-if="activeTab === 'guide'" class="space-y-6">
@@ -716,6 +719,7 @@ const registrationTest = `{
</div>
</div>
</div><!-- /scrollable -->
</div>
</div>
</template>
+60 -19
View File
@@ -59,6 +59,22 @@ const tierColors: Record<number, string> = {
0: '#8a8a9a', 1: '#4ade80', 2: '#38bdf8', 3: '#a855f7', 4: '#f97316', 5: '#ef4444',
}
function isCreator(bot: BotInfo | null): boolean {
return bot?.archetype === 'the_creator'
}
function getTierName(bot: BotInfo | null): string {
if (!bot) return ''
if (isCreator(bot)) return '₿ CREATOR'
return tierNames[bot.tier ?? 0]
}
function getTierColor(bot: BotInfo | null): string {
if (!bot) return '#8a8a9a'
if (isCreator(bot)) return '#ffd700'
return tierColors[bot.tier ?? 0]
}
function selectFight(fightId: string) {
const fight = fights.value.find(f => f.id === fightId)
if (!fight || !fight.botA || !fight.botB) return
@@ -95,7 +111,7 @@ onUnmounted(() => {
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col overflow-hidden relative">
<div class="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden relative">
<!-- Page background -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
@@ -196,7 +212,7 @@ onUnmounted(() => {
<div class="flex-1 flex flex-col items-center min-w-0">
<div v-if="fighterA" class="relative fighter-bob">
<!-- Circular glow behind fighter -->
<div class="fighter-glow fighter-glow-cyan" />
<div class="fighter-glow" :class="isCreator(fighterA) ? 'fighter-glow-gold' : 'fighter-glow-cyan'" />
<SpritePreview
:key="`a-${featured.id}-${fighterA.avatarSeed}`"
:seed="fighterA.avatarSeed || fighterA.name"
@@ -212,7 +228,7 @@ onUnmounted(() => {
<div class="flex-1 flex flex-col items-center min-w-0 -ml-6 sm:-ml-12 lg:-ml-20">
<div v-if="fighterB" class="relative fighter-bob-delayed" style="transform: scaleX(-1)">
<!-- Circular glow behind fighter -->
<div class="fighter-glow fighter-glow-pink" />
<div class="fighter-glow" :class="isCreator(fighterB) ? 'fighter-glow-gold' : 'fighter-glow-pink'" />
<SpritePreview
:key="`b-${featured.id}-${fighterB.avatarSeed}`"
:seed="fighterB.avatarSeed || fighterB.name"
@@ -230,14 +246,16 @@ onUnmounted(() => {
<!-- Names with VS orb in center -->
<div class="flex items-stretch w-full max-w-3xl mt-1 sm:mt-2 lg:mt-3 shrink-0 relative">
<!-- Fighter A cyan -->
<div class="flex-1 min-w-0 name-block-cyan border-2 border-neon-cyan/40 rounded-l-lg px-2 sm:px-3 pr-16 sm:pr-20 lg:pr-24 py-1.5 sm:py-2 lg:py-3 text-center">
<p class="font-funky text-base sm:text-2xl lg:text-4xl tracking-widest text-neon-cyan truncate pb-0.5 poster-name-cyan uppercase">
<!-- Fighter A -->
<div class="flex-1 min-w-0 border-2 rounded-l-lg px-2 sm:px-3 pr-16 sm:pr-20 lg:pr-24 py-1.5 sm:py-2 lg:py-3 text-center"
:class="isCreator(fighterA) ? 'name-block-gold border-yellow-500/40' : 'name-block-cyan border-neon-cyan/40'">
<p class="font-funky text-base sm:text-2xl lg:text-4xl tracking-widest truncate pb-0.5 uppercase"
:class="isCreator(fighterA) ? 'text-yellow-400 poster-name-gold' : 'text-neon-cyan poster-name-cyan'">
{{ fighterA?.name || '???' }}
</p>
<p class="font-pixel text-[6px] sm:text-[8px] lg:text-[11px] tracking-[0.2em]"
:style="{ color: tierColors[fighterA?.tier ?? 0] }">
{{ tierNames[fighterA?.tier ?? 0] }}
:style="{ color: getTierColor(fighterA) }">
{{ getTierName(fighterA) }}
</p>
</div>
@@ -253,14 +271,16 @@ onUnmounted(() => {
</div>
</div>
<!-- Fighter B pink -->
<div class="flex-1 min-w-0 name-block-pink border-2 border-neon-pink/40 border-l-0 rounded-r-lg px-2 sm:px-3 pl-16 sm:pl-20 lg:pl-24 py-1.5 sm:py-2 lg:py-3 text-center">
<p class="font-funky text-base sm:text-2xl lg:text-4xl tracking-widest text-neon-pink truncate pb-0.5 poster-name-pink uppercase">
<!-- Fighter B -->
<div class="flex-1 min-w-0 border-2 border-l-0 rounded-r-lg px-2 sm:px-3 pl-16 sm:pl-20 lg:pl-24 py-1.5 sm:py-2 lg:py-3 text-center"
:class="isCreator(fighterB) ? 'name-block-gold border-yellow-500/40' : 'name-block-pink border-neon-pink/40'">
<p class="font-funky text-base sm:text-2xl lg:text-4xl tracking-widest truncate pb-0.5 uppercase"
:class="isCreator(fighterB) ? 'text-yellow-400 poster-name-gold' : 'text-neon-pink poster-name-pink'">
{{ fighterB?.name || '???' }}
</p>
<p class="font-pixel text-[6px] sm:text-[8px] lg:text-[11px] tracking-[0.2em]"
:style="{ color: tierColors[fighterB?.tier ?? 0] }">
{{ tierNames[fighterB?.tier ?? 0] }}
:style="{ color: getTierColor(fighterB) }">
{{ getTierName(fighterB) }}
</p>
</div>
</div>
@@ -295,7 +315,10 @@ onUnmounted(() => {
</div>
<div class="flex items-center">
<div class="flex-1 flex items-center justify-end gap-1.5 min-w-0">
<p class="font-display font-bold text-xs text-neon-cyan truncate">{{ mainEvent.botA.name }}</p>
<p class="font-display font-bold text-xs truncate"
:class="isCreator(mainEvent.botA) ? 'text-yellow-400' : 'text-neon-cyan'">
{{ isCreator(mainEvent.botA) ? '₿ ' : '' }}{{ mainEvent.botA.name }}
</p>
<SpritePreview
:seed="mainEvent.botA.avatarSeed || mainEvent.botA.name"
:archetype="mainEvent.botA.archetype || 'standard'"
@@ -314,7 +337,10 @@ onUnmounted(() => {
class="shrink-0"
style="transform: scaleX(-1)"
/>
<p class="font-display font-bold text-xs text-neon-pink truncate">{{ mainEvent.botB.name }}</p>
<p class="font-display font-bold text-xs truncate"
:class="isCreator(mainEvent.botB) ? 'text-yellow-400' : 'text-neon-pink'">
{{ mainEvent.botB.name }}{{ isCreator(mainEvent.botB) ? ' ₿' : '' }}
</p>
</div>
</div>
</button>
@@ -346,8 +372,8 @@ onUnmounted(() => {
>
<div class="flex-1 flex items-center justify-end gap-1.5 min-w-0">
<p class="font-display font-bold text-[11px] truncate"
:class="fight.botA ? 'text-text-primary' : 'text-text-muted'">
{{ fight.botA?.name || '???' }}
:class="isCreator(fight.botA) ? 'text-yellow-400' : fight.botA ? 'text-text-primary' : 'text-text-muted'">
{{ isCreator(fight.botA) ? '₿ ' : '' }}{{ fight.botA?.name || '???' }}
</p>
<SpritePreview
v-if="fight.botA"
@@ -370,8 +396,8 @@ onUnmounted(() => {
style="transform: scaleX(-1)"
/>
<p class="font-display font-bold text-[11px] truncate"
:class="fight.botB ? 'text-text-primary' : 'text-text-muted'">
{{ fight.botB?.name || '???' }}
:class="isCreator(fight.botB) ? 'text-yellow-400' : fight.botB ? 'text-text-primary' : 'text-text-muted'">
{{ fight.botB?.name || '???' }}{{ isCreator(fight.botB) ? ' ₿' : '' }}
</p>
</div>
<div class="ml-1.5 shrink-0">
@@ -469,6 +495,14 @@ onUnmounted(() => {
0 0 30px rgba(255, 45, 120, 0.3),
0 0 60px rgba(255, 45, 120, 0.15);
}
.poster-name-gold {
-webkit-text-stroke: 1px rgba(200, 160, 0, 0.6);
text-shadow:
2px 2px 0 rgba(130, 100, 0, 0.8),
0 0 10px rgba(255, 215, 0, 0.6),
0 0 30px rgba(255, 215, 0, 0.3),
0 0 60px rgba(255, 180, 0, 0.15);
}
.poster-title {
text-shadow:
0 0 15px rgba(255, 45, 120, 0.6),
@@ -483,6 +517,9 @@ onUnmounted(() => {
.name-block-pink {
background: linear-gradient(225deg, rgba(255, 45, 120, 0.08) 0%, rgba(255, 45, 120, 0.02) 100%);
}
.name-block-gold {
background: linear-gradient(180deg, rgba(255, 215, 0, 0.12) 0%, rgba(255, 180, 0, 0.04) 100%);
}
/* ═══ VS Orb — 3D circle ═══ */
.vs-orb {
@@ -578,6 +615,10 @@ onUnmounted(() => {
background: radial-gradient(circle, rgba(255, 45, 120, 0.25) 0%, rgba(255, 45, 120, 0.10) 35%, rgba(255, 45, 120, 0.03) 60%, transparent 80%);
box-shadow: 0 0 60px rgba(255, 45, 120, 0.15), 0 0 120px rgba(255, 45, 120, 0.05);
}
.fighter-glow-gold {
background: radial-gradient(circle, rgba(255, 215, 0, 0.35) 0%, rgba(255, 215, 0, 0.15) 35%, rgba(255, 180, 0, 0.05) 60%, transparent 80%);
box-shadow: 0 0 60px rgba(255, 215, 0, 0.25), 0 0 120px rgba(255, 215, 0, 0.10);
}
/* Fighter bobbing */
.fighter-bob {
+1 -1
View File
@@ -315,7 +315,7 @@ onUnmounted(() => {
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden relative">
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden relative">
<!-- Crowd characters floating around edges -->
<template v-for="(c, i) in crowd" :key="i">
<SpritePreview
+4 -4
View File
@@ -190,11 +190,11 @@ function goToArena() {
</script>
<template>
<div class="min-h-[calc(100vh-4rem)] flex flex-col items-center px-4 py-4 overflow-y-auto">
<div class="max-w-lg w-full">
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center px-4 overflow-hidden">
<div class="max-w-lg w-full flex-1 min-h-0 overflow-y-auto py-4">
<!-- PHASE: WAITING -->
<div v-if="phase === 'waiting'" class="flex flex-col items-center justify-center min-h-[60vh] gap-4">
<div v-if="phase === 'waiting'" class="flex flex-col items-center justify-center flex-1 gap-4">
<div class="w-12 h-12 border-4 border-neon-pink/30 border-t-neon-pink rounded-full animate-spin" />
<p class="font-display text-neon-pink text-lg tracking-widest animate-pulse glow-pink">
FIGHT STARTING...
@@ -333,7 +333,7 @@ function goToArena() {
<!-- PHASE: REPLAY -->
<div v-else-if="phase === 'replay' && fight" class="w-full">
<div class="h-[calc(100vh-8rem)] min-h-0 relative">
<div class="h-[calc(100dvh-8rem)] min-h-0 relative">
<FightViewer :fight="fight" :autoplay="true" class="h-full" />
</div>
<div class="flex gap-2 mt-2">
+2 -2
View File
@@ -406,8 +406,8 @@ function handleSignOut() {
</script>
<template>
<div class="min-h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 py-8 overflow-y-auto">
<div class="max-w-md w-full slide-up">
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
<div class="max-w-md w-full slide-up overflow-y-auto max-h-full py-4">
<!-- STEP: LOGIN -->
<template v-if="step === 'login'">
+1 -1
View File
@@ -39,7 +39,7 @@ const record = (b: Bot) => `${b.wins}W - ${b.losses}L`
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col px-6 py-6 overflow-hidden">
<div class="h-[calc(100dvh-4rem)] flex flex-col px-6 py-6 overflow-hidden">
<div class="max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0">
<!-- Header -->
+1 -1
View File
@@ -57,7 +57,7 @@ function goFight() {
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
<div class="max-w-md w-full slide-up">
<div class="text-center mb-8">
+1 -1
View File
@@ -58,7 +58,7 @@ onMounted(() => {
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col px-6 py-6 overflow-hidden">
<div class="h-[calc(100dvh-4rem)] flex flex-col px-6 py-6 overflow-hidden">
<div class="max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0">
<!-- Header -->