Add detail pages for characters, weapons, summons

This commit is contained in:
Justin Edmund 2025-09-17 07:13:36 -07:00
parent 45f04c2593
commit 4910b263e1
6 changed files with 1121 additions and 0 deletions

View file

@ -0,0 +1,25 @@
import type { PageServerLoad } from './$types'
import { get } from '$lib/api/core'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, fetch }) => {
try {
const character = await get(fetch, `/characters/${params.id}`)
if (!character) {
throw error(404, 'Character not found')
}
return {
character
}
} catch (err) {
console.error('Failed to load character:', err)
if (err instanceof Error && 'status' in err && err.status === 404) {
throw error(404, 'Character not found')
}
throw error(500, 'Failed to load character')
}
}

View file

@ -0,0 +1,298 @@
<svelte:options runes={true} />
<script lang="ts">
import { goto } from '$app/navigation'
import { getRarityLabel } from '$lib/utils/rarity'
import { getElementLabel, getElementIcon } from '$lib/utils/element'
import { getRaceLabel } from '$lib/utils/race'
import { getGenderLabel } from '$lib/utils/gender'
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
// Get character from server data
const character = $derived(data.character)
// Helper function to get character name
function getCharacterName(nameObj: any): string {
if (!nameObj) return 'Unknown Character'
if (typeof nameObj === 'string') return nameObj
return nameObj.en || nameObj.ja || 'Unknown Character'
}
// Helper function to get character image
function getCharacterImage(character: any): string {
if (!character?.granblue_id) return '/images/placeholders/placeholder-character-main.png'
return `/images/character-main/${character.granblue_id}_01.jpg`
}
</script>
<div class="character-detail">
<div class="page-header">
<button class="back-button" onclick={() => goto('/database/characters')}>
← Back to Characters
</button>
<h1>Character Details</h1>
</div>
{#if character}
<div class="character-content">
<div class="character-hero">
<div class="character-image">
<img
src={getCharacterImage(character)}
alt={getCharacterName(character.name)}
onerror={(e) => { e.currentTarget.src = '/images/placeholders/placeholder-character-main.png' }}
/>
</div>
<div class="character-info">
<h2 class="character-name">{getCharacterName(character.name)}</h2>
<div class="character-meta">
<div class="meta-item">
<span class="label">Rarity:</span>
<span class="value">{getRarityLabel(character.rarity)}</span>
</div>
<div class="meta-item">
<span class="label">Element:</span>
<div class="element-display">
{#if character.element}
<img
src={getElementIcon(character.element)}
alt={getElementLabel(character.element)}
class="element-icon"
/>
<span class="value">{getElementLabel(character.element)}</span>
{:else}
<span class="value"></span>
{/if}
</div>
</div>
<div class="meta-item">
<span class="label">Max Level:</span>
<span class="value">{character.max_level || '—'}</span>
</div>
<div class="meta-item">
<span class="label">Granblue ID:</span>
<span class="value">{character.granblue_id || '—'}</span>
</div>
</div>
</div>
</div>
<div class="character-details">
<h3>Details</h3>
<div class="details-grid">
<div class="detail-item">
<span class="label">Race:</span>
<span class="value">{getRaceLabel(character.race)}</span>
</div>
<div class="detail-item">
<span class="label">Gender:</span>
<span class="value">{getGenderLabel(character.gender)}</span>
</div>
<div class="detail-item">
<span class="label">Base HP:</span>
<span class="value">{character.base_hp || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Base Attack:</span>
<span class="value">{character.base_attack || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Max HP:</span>
<span class="value">{character.max_hp || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Max Attack:</span>
<span class="value">{character.max_attack || '—'}</span>
</div>
</div>
</div>
</div>
{:else}
<div class="not-found">
<h2>Character Not Found</h2>
<p>The character you're looking for could not be found.</p>
<button onclick={() => goto('/database/characters')}>Back to Characters</button>
</div>
{/if}
</div>
<style lang="scss">
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
.character-detail {
padding: spacing.$unit * 2;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: spacing.$unit * 2;
.back-button {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: spacing.$unit * 0.5 spacing.$unit;
border-radius: 4px;
cursor: pointer;
font-size: typography.$font-small;
margin-bottom: spacing.$unit;
transition: all 0.2s;
&:hover {
background: #e9ecef;
}
}
h1 {
font-size: typography.$font-xxlarge;
font-weight: typography.$bold;
margin: 0;
}
}
.loading,
.error,
.not-found {
text-align: center;
padding: spacing.$unit * 4;
.loading-spinner {
font-size: typography.$font-medium;
color: #666;
}
button {
background: #007bff;
color: white;
border: none;
padding: spacing.$unit * 0.5 spacing.$unit;
border-radius: 4px;
cursor: pointer;
margin-top: spacing.$unit;
&:hover {
background: #0056b3;
}
}
}
.character-content {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.character-hero {
display: flex;
gap: spacing.$unit * 2;
padding: spacing.$unit * 2;
border-bottom: 1px solid #e5e5e5;
.character-image {
flex-shrink: 0;
img {
width: 200px;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.character-info {
flex: 1;
.character-name {
font-size: typography.$font-xlarge;
font-weight: typography.$bold;
margin: 0 0 spacing.$unit 0;
color: #333;
}
.character-meta {
display: flex;
flex-direction: column;
gap: spacing.$unit * 0.5;
.meta-item {
display: flex;
align-items: center;
gap: spacing.$unit * 0.5;
.label {
font-weight: typography.$medium;
color: #666;
min-width: 100px;
}
.value {
color: #333;
}
.element-display {
display: flex;
align-items: center;
gap: spacing.$unit * 0.25;
.element-icon {
width: 25px;
height: auto;
}
}
}
}
}
}
.character-details {
padding: spacing.$unit * 2;
h3 {
font-size: typography.$font-large;
font-weight: typography.$bold;
margin: 0 0 spacing.$unit 0;
}
.details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: spacing.$unit;
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: spacing.$unit * 0.5;
background: #f8f9fa;
border-radius: 4px;
.label {
font-weight: typography.$medium;
color: #666;
}
.value {
color: #333;
}
}
}
}
@media (max-width: 768px) {
.character-hero {
flex-direction: column;
.character-image img {
width: 150px;
}
}
.details-grid {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1,25 @@
import type { PageServerLoad } from './$types'
import { get } from '$lib/api/core'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, fetch }) => {
try {
const summon = await get(fetch, `/summons/${params.id}`)
if (!summon) {
throw error(404, 'Summon not found')
}
return {
summon
}
} catch (err) {
console.error('Failed to load summon:', err)
if (err instanceof Error && 'status' in err && err.status === 404) {
throw error(404, 'Summon not found')
}
throw error(500, 'Failed to load summon')
}
}

View file

@ -0,0 +1,379 @@
<svelte:options runes={true} />
<script lang="ts">
import { goto } from '$app/navigation'
import { getRarityLabel } from '$lib/utils/rarity'
import { getElementLabel, getElementIcon } from '$lib/utils/element'
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
// Get summon from server data
const summon = $derived(data.summon)
// Helper function to get summon name
function getSummonName(nameObj: any): string {
if (!nameObj) return 'Unknown Summon'
if (typeof nameObj === 'string') return nameObj
return nameObj.en || nameObj.ja || 'Unknown Summon'
}
// Helper function to get summon image
function getSummonImage(summon: any): string {
if (!summon?.granblue_id) return '/images/placeholders/placeholder-summon-main.png'
return `/images/summon-main/${summon.granblue_id}.jpg`
}
</script>
<div class="summon-detail">
<div class="page-header">
<button class="back-button" onclick={() => goto('/database/summons')}>
← Back to Summons
</button>
<h1>Summon Details</h1>
</div>
{#if summon}
<div class="summon-content">
<div class="summon-hero">
<div class="summon-image">
<img
src={getSummonImage(summon)}
alt={getSummonName(summon.name)}
onerror={(e) => { e.currentTarget.src = '/images/placeholders/placeholder-summon-main.png' }}
/>
</div>
<div class="summon-info">
<h2 class="summon-name">{getSummonName(summon.name)}</h2>
<div class="summon-meta">
<div class="meta-item">
<span class="label">Rarity:</span>
<span class="value">{getRarityLabel(summon.rarity)}</span>
</div>
<div class="meta-item">
<span class="label">Element:</span>
<div class="element-display">
{#if summon.element}
<img
src={getElementIcon(summon.element)}
alt={getElementLabel(summon.element)}
class="element-icon"
/>
<span class="value">{getElementLabel(summon.element)}</span>
{:else}
<span class="value"></span>
{/if}
</div>
</div>
<div class="meta-item">
<span class="label">Max Level:</span>
<span class="value">{summon.max_level || '—'}</span>
</div>
<div class="meta-item">
<span class="label">Granblue ID:</span>
<span class="value">{summon.granblue_id || '—'}</span>
</div>
</div>
</div>
</div>
<div class="summon-details">
<h3>Stats</h3>
<div class="details-grid">
<div class="detail-item">
<span class="label">Base HP:</span>
<span class="value">{summon.base_hp || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Base Attack:</span>
<span class="value">{summon.base_attack || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Max HP:</span>
<span class="value">{summon.max_hp || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Max Attack:</span>
<span class="value">{summon.max_attack || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Plus Bonus:</span>
<span class="value">{summon.plus_bonus ? 'Yes' : 'No'}</span>
</div>
<div class="detail-item">
<span class="label">Series:</span>
<span class="value">{summon.series || '—'}</span>
</div>
</div>
</div>
<div class="summon-abilities">
<h3>Call Effect</h3>
<div class="abilities-section">
{#if summon.call_name || summon.call_description}
<div class="ability-item">
<h4 class="ability-name">{summon.call_name || 'Call Effect'}</h4>
<p class="ability-description">
{summon.call_description || 'No description available'}
</p>
</div>
{:else}
<p class="no-abilities">No call effect information available</p>
{/if}
</div>
<h3>Aura Effect</h3>
<div class="abilities-section">
{#if summon.aura_name || summon.aura_description}
<div class="ability-item">
<h4 class="ability-name">{summon.aura_name || 'Aura Effect'}</h4>
<p class="ability-description">
{summon.aura_description || 'No description available'}
</p>
</div>
{:else}
<p class="no-abilities">No aura effect information available</p>
{/if}
</div>
{#if summon.sub_aura_name || summon.sub_aura_description}
<h3>Sub Aura Effect</h3>
<div class="abilities-section">
<div class="ability-item">
<h4 class="ability-name">{summon.sub_aura_name || 'Sub Aura Effect'}</h4>
<p class="ability-description">
{summon.sub_aura_description || 'No description available'}
</p>
</div>
</div>
{/if}
</div>
</div>
{:else}
<div class="not-found">
<h2>Summon Not Found</h2>
<p>The summon you're looking for could not be found.</p>
<button onclick={() => goto('/database/summons')}>Back to Summons</button>
</div>
{/if}
</div>
<style lang="scss">
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
.summon-detail {
padding: spacing.$unit * 2;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: spacing.$unit * 2;
.back-button {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: spacing.$unit * 0.5 spacing.$unit;
border-radius: 4px;
cursor: pointer;
font-size: typography.$font-small;
margin-bottom: spacing.$unit;
transition: all 0.2s;
&:hover {
background: #e9ecef;
}
}
h1 {
font-size: typography.$font-xxlarge;
font-weight: typography.$bold;
margin: 0;
}
}
.loading,
.error,
.not-found {
text-align: center;
padding: spacing.$unit * 4;
.loading-spinner {
font-size: typography.$font-medium;
color: #666;
}
button {
background: #007bff;
color: white;
border: none;
padding: spacing.$unit * 0.5 spacing.$unit;
border-radius: 4px;
cursor: pointer;
margin-top: spacing.$unit;
&:hover {
background: #0056b3;
}
}
}
.summon-content {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.summon-hero {
display: flex;
gap: spacing.$unit * 2;
padding: spacing.$unit * 2;
border-bottom: 1px solid #e5e5e5;
.summon-image {
flex-shrink: 0;
img {
width: 200px;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.summon-info {
flex: 1;
.summon-name {
font-size: typography.$font-xlarge;
font-weight: typography.$bold;
margin: 0 0 spacing.$unit 0;
color: #333;
}
.summon-meta {
display: flex;
flex-direction: column;
gap: spacing.$unit * 0.5;
.meta-item {
display: flex;
align-items: center;
gap: spacing.$unit * 0.5;
.label {
font-weight: typography.$medium;
color: #666;
min-width: 100px;
}
.value {
color: #333;
}
.element-display {
display: flex;
align-items: center;
gap: spacing.$unit * 0.25;
.element-icon {
width: 25px;
height: auto;
}
}
}
}
}
}
.summon-details,
.summon-abilities {
padding: spacing.$unit * 2;
border-bottom: 1px solid #e5e5e5;
&:last-child {
border-bottom: none;
}
h3 {
font-size: typography.$font-large;
font-weight: typography.$bold;
margin: 0 0 spacing.$unit 0;
}
.details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: spacing.$unit;
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: spacing.$unit * 0.5;
background: #f8f9fa;
border-radius: 4px;
.label {
font-weight: typography.$medium;
color: #666;
}
.value {
color: #333;
}
}
}
.abilities-section {
margin-bottom: spacing.$unit * 2;
&:last-child {
margin-bottom: 0;
}
.ability-item {
padding: spacing.$unit;
background: #f8f9fa;
border-radius: 4px;
.ability-name {
font-size: typography.$font-medium;
font-weight: typography.$medium;
margin: 0 0 spacing.$unit * 0.5 0;
color: #333;
}
.ability-description {
font-size: typography.$font-small;
color: #666;
margin: 0;
line-height: 1.4;
}
}
.no-abilities {
text-align: center;
color: #666;
font-style: italic;
padding: spacing.$unit;
}
}
}
@media (max-width: 768px) {
.summon-hero {
flex-direction: column;
.summon-image img {
width: 150px;
}
}
.details-grid {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1,25 @@
import type { PageServerLoad } from './$types'
import { get } from '$lib/api/core'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, fetch }) => {
try {
const weapon = await get(fetch, `/weapons/${params.id}`)
if (!weapon) {
throw error(404, 'Weapon not found')
}
return {
weapon
}
} catch (err) {
console.error('Failed to load weapon:', err)
if (err instanceof Error && 'status' in err && err.status === 404) {
throw error(404, 'Weapon not found')
}
throw error(500, 'Failed to load weapon')
}
}

View file

@ -0,0 +1,369 @@
<svelte:options runes={true} />
<script lang="ts">
import { goto } from '$app/navigation'
import { getRarityLabel } from '$lib/utils/rarity'
import { getElementLabel, getElementIcon } from '$lib/utils/element'
import { getProficiencyLabel, getProficiencyIcon } from '$lib/utils/proficiency'
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
// Get weapon from server data
const weapon = $derived(data.weapon)
// Helper function to get weapon name
function getWeaponName(nameObj: any): string {
if (!nameObj) return 'Unknown Weapon'
if (typeof nameObj === 'string') return nameObj
return nameObj.en || nameObj.ja || 'Unknown Weapon'
}
// Helper function to get weapon image
function getWeaponImage(weapon: any): string {
if (!weapon?.granblue_id) return '/images/placeholders/placeholder-weapon-main.png'
// Handle element-specific weapons (primal weapons)
if (weapon.element === 0 && weapon.instance_element) {
return `/images/weapon-main/${weapon.granblue_id}_${weapon.instance_element}.jpg`
}
return `/images/weapon-main/${weapon.granblue_id}.jpg`
}
</script>
<div class="weapon-detail">
<div class="page-header">
<button class="back-button" onclick={() => goto('/database/weapons')}>
← Back to Weapons
</button>
<h1>Weapon Details</h1>
</div>
{#if weapon}
<div class="weapon-content">
<div class="weapon-hero">
<div class="weapon-image">
<img
src={getWeaponImage(weapon)}
alt={getWeaponName(weapon.name)}
onerror={(e) => { e.currentTarget.src = '/images/placeholders/placeholder-weapon-main.png' }}
/>
</div>
<div class="weapon-info">
<h2 class="weapon-name">{getWeaponName(weapon.name)}</h2>
<div class="weapon-meta">
<div class="meta-item">
<span class="label">Rarity:</span>
<span class="value">{getRarityLabel(weapon.rarity)}</span>
</div>
<div class="meta-item">
<span class="label">Element:</span>
<div class="element-display">
{#if weapon.element}
<img
src={getElementIcon(weapon.element)}
alt={getElementLabel(weapon.element)}
class="element-icon"
/>
<span class="value">{getElementLabel(weapon.element)}</span>
{:else}
<span class="value"></span>
{/if}
</div>
</div>
<div class="meta-item">
<span class="label">Proficiency:</span>
<div class="proficiency-display">
{#if weapon.proficiency}
<img
src={getProficiencyIcon(weapon.proficiency)}
alt={getProficiencyLabel(weapon.proficiency)}
class="proficiency-icon"
/>
<span class="value">{getProficiencyLabel(weapon.proficiency)}</span>
{:else}
<span class="value"></span>
{/if}
</div>
</div>
<div class="meta-item">
<span class="label">Max Level:</span>
<span class="value">{weapon.max_level || '—'}</span>
</div>
<div class="meta-item">
<span class="label">Granblue ID:</span>
<span class="value">{weapon.granblue_id || '—'}</span>
</div>
</div>
</div>
</div>
<div class="weapon-details">
<h3>Stats</h3>
<div class="details-grid">
<div class="detail-item">
<span class="label">Base HP:</span>
<span class="value">{weapon.base_hp || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Base Attack:</span>
<span class="value">{weapon.base_attack || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Max HP:</span>
<span class="value">{weapon.max_hp || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Max Attack:</span>
<span class="value">{weapon.max_attack || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Skill Level Cap:</span>
<span class="value">{weapon.skill_level_cap || '—'}</span>
</div>
<div class="detail-item">
<span class="label">Plus Bonus:</span>
<span class="value">{weapon.plus_bonus ? 'Yes' : 'No'}</span>
</div>
</div>
</div>
<div class="weapon-skills">
<h3>Skills</h3>
<div class="skills-grid">
{#if weapon.weapon_skills && weapon.weapon_skills.length > 0}
{#each weapon.weapon_skills as skill}
<div class="skill-item">
<h4 class="skill-name">{skill.name || 'Unknown Skill'}</h4>
<p class="skill-description">{skill.description || 'No description available'}</p>
</div>
{/each}
{:else}
<p class="no-skills">No skills available</p>
{/if}
</div>
</div>
</div>
{:else}
<div class="not-found">
<h2>Weapon Not Found</h2>
<p>The weapon you're looking for could not be found.</p>
<button onclick={() => goto('/database/weapons')}>Back to Weapons</button>
</div>
{/if}
</div>
<style lang="scss">
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
.weapon-detail {
padding: spacing.$unit * 2;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: spacing.$unit * 2;
.back-button {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: spacing.$unit * 0.5 spacing.$unit;
border-radius: 4px;
cursor: pointer;
font-size: typography.$font-small;
margin-bottom: spacing.$unit;
transition: all 0.2s;
&:hover {
background: #e9ecef;
}
}
h1 {
font-size: typography.$font-xxlarge;
font-weight: typography.$bold;
margin: 0;
}
}
.loading, .error, .not-found {
text-align: center;
padding: spacing.$unit * 4;
.loading-spinner {
font-size: typography.$font-medium;
color: #666;
}
button {
background: #007bff;
color: white;
border: none;
padding: spacing.$unit * 0.5 spacing.$unit;
border-radius: 4px;
cursor: pointer;
margin-top: spacing.$unit;
&:hover {
background: #0056b3;
}
}
}
.weapon-content {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.weapon-hero {
display: flex;
gap: spacing.$unit * 2;
padding: spacing.$unit * 2;
border-bottom: 1px solid #e5e5e5;
.weapon-image {
flex-shrink: 0;
img {
width: 200px;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.weapon-info {
flex: 1;
.weapon-name {
font-size: typography.$font-xlarge;
font-weight: typography.$bold;
margin: 0 0 spacing.$unit 0;
color: #333;
}
.weapon-meta {
display: flex;
flex-direction: column;
gap: spacing.$unit * 0.5;
.meta-item {
display: flex;
align-items: center;
gap: spacing.$unit * 0.5;
.label {
font-weight: typography.$semibold;
color: #666;
min-width: 100px;
}
.value {
color: #333;
}
.element-display, .proficiency-display {
display: flex;
align-items: center;
gap: spacing.$unit * 0.25;
.element-icon, .proficiency-icon {
width: 25px;
height: auto;
}
}
}
}
}
}
.weapon-details, .weapon-skills {
padding: spacing.$unit * 2;
border-bottom: 1px solid #e5e5e5;
&:last-child {
border-bottom: none;
}
h3 {
font-size: typography.$font-large;
font-weight: typography.$bold;
margin: 0 0 spacing.$unit 0;
}
.details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: spacing.$unit;
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: spacing.$unit * 0.5;
background: #f8f9fa;
border-radius: 4px;
.label {
font-weight: typography.$semibold;
color: #666;
}
.value {
color: #333;
}
}
}
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: spacing.$unit;
.skill-item {
padding: spacing.$unit;
background: #f8f9fa;
border-radius: 4px;
.skill-name {
font-size: typography.$font-medium;
font-weight: typography.$semibold;
margin: 0 0 spacing.$unit * 0.5 0;
color: #333;
}
.skill-description {
font-size: typography.$font-small;
color: #666;
margin: 0;
line-height: 1.4;
}
}
.no-skills {
grid-column: 1 / -1;
text-align: center;
color: #666;
font-style: italic;
}
}
}
@media (max-width: 768px) {
.weapon-hero {
flex-direction: column;
.weapon-image img {
width: 150px;
}
}
.details-grid, .skills-grid {
grid-template-columns: 1fr;
}
}
</style>