Checkpoint for reps

This commit is contained in:
Justin Edmund 2025-09-12 05:22:33 -07:00
parent be0a8df439
commit c8da5c4762
4 changed files with 489 additions and 228 deletions

View file

@ -1,58 +1,124 @@
<script lang="ts">
import type { PartyView } from '$lib/api/schemas/party'
export let party: PartyView
import type { PartyView } from '$lib/api/schemas/party'
export let party: PartyView
const characters = party.characters || []
const grid = Array.from({ length: 3 }, (_, i) => characters.find((c: any) => c?.position === i))
const characters = party.characters || []
const grid = Array.from({ length: 3 }, (_, i) => characters.find((c: any) => c?.position === i))
function protagonistClass(): string {
const main = (party.weapons || []).find((w: any) => w?.mainhand || w?.position === -1)
const el = (main as any)?.element || (main as any)?.object?.element
switch (el) { case 1: return 'wind'; case 2: return 'fire'; case 3: return 'water'; case 4: return 'earth'; case 5: return 'dark'; case 6: return 'light'; default: return '' }
}
function protagonistClass(): string {
const main = (party.weapons || []).find((w: any) => w?.mainhand || w?.position === -1)
const el = (main as any)?.element || (main as any)?.object?.element
switch (el) {
case 1:
return 'wind'
case 2:
return 'fire'
case 3:
return 'water'
case 4:
return 'earth'
case 5:
return 'dark'
case 6:
return 'light'
default:
return ''
}
}
function characterImageUrl(c?: any): string {
const id = c?.object?.granblueId
if (!id) return ''
const uncap = c?.uncapLevel ?? 0
const trans = c?.transcendenceStep ?? 0
let suffix = '01'
if (trans > 0) suffix = '04'
else if (uncap >= 5) suffix = '03'
else if (uncap > 2) suffix = '02'
if (String(id) === '3030182000') {
const main = (party.weapons || []).find((w: any) => w?.mainhand || w?.position === -1)
const el = (main as any)?.element || (main as any)?.object?.element || 1
suffix = `${suffix}_0${el}`
}
return `/images/character-main/${id}_${suffix}.jpg`
}
function characterImageUrl(c?: any): string {
const id = c?.object?.granblueId
if (!id) return ''
const uncap = c?.uncapLevel ?? 0
const trans = c?.transcendenceStep ?? 0
let suffix = '01'
if (trans > 0) suffix = '04'
else if (uncap >= 5) suffix = '03'
else if (uncap > 2) suffix = '02'
if (String(id) === '3030182000') {
const main = (party.weapons || []).find((w: any) => w?.mainhand || w?.position === -1)
const el = (main as any)?.element || (main as any)?.object?.element || 1
suffix = `${suffix}_0${el}`
}
return `/images/character-main/${id}_${suffix}.jpg`
}
</script>
<div class="rep">
<ul class="characters">
<li class={`protagonist ${protagonistClass()}`}></li>
{#each grid as c, i}
<li class="character">{#if c}<img alt="Character" src={characterImageUrl(c)} />{/if}</li>
{/each}
</ul>
<ul class="characters">
<li class={`protagonist ${protagonistClass()}`}></li>
{#each grid as c, i}
<li class="character">
{#if c}<img alt="Character" src={characterImageUrl(c)} />{/if}
</li>
{/each}
</ul>
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/spacing' as *;
@use '$src/themes/rep' as rep;
.rep { aspect-ratio: 2/0.99; border-radius: 10px; grid-gap: $unit-half; height: $rep-height; opacity: .5; }
.character, .protagonist { aspect-ratio: 16/33; background: var(--card-bg); border-radius: 4px; box-sizing: border-box; display: grid; overflow: hidden; }
.character img { border-radius: 4px; width: 100%; }
.characters { display: grid; grid-template-columns: repeat(4, 1fr); gap: $unit-half; }
.protagonist { border-color: transparent; border-width: 1px; border-style: solid; aspect-ratio: 32/66; }
.protagonist img { position: relative; width: 100%; height: 100%; }
.protagonist.wind { background: var(--wind-portrait-bg); border-color: var(--wind-bg); }
.protagonist.fire { background: var(--fire-portrait-bg); border-color: var(--fire-bg); }
.protagonist.water { background: var(--water-portrait-bg); border-color: var(--water-bg); }
.protagonist.earth { background: var(--earth-portrait-bg); border-color: var(--earth-bg); }
.protagonist.light { background: var(--light-portrait-bg); border-color: var(--light-bg); }
.protagonist.dark { background: var(--dark-portrait-bg); border-color: var(--dark-bg); }
.protagonist.empty { background: var(--card-bg); }
.rep {
width: 100%;
height: 100%;
border-radius: 10px;
grid-gap: $unit-half;
}
.character,
.protagonist {
aspect-ratio: 16/33;
background: var(--card-bg);
border-radius: 4px;
box-sizing: border-box;
display: grid;
overflow: hidden;
}
.character img {
border-radius: 4px;
width: 100%;
}
.characters {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $unit-half;
}
.protagonist {
border-color: transparent;
border-width: 1px;
border-style: solid;
@include rep.aspect(32, 66);
}
.protagonist img {
position: relative;
width: 100%;
height: 100%;
}
.protagonist.wind {
background: var(--wind-portrait-bg);
border-color: var(--wind-bg);
}
.protagonist.fire {
background: var(--fire-portrait-bg);
border-color: var(--fire-bg);
}
.protagonist.water {
background: var(--water-portrait-bg);
border-color: var(--water-bg);
}
.protagonist.earth {
background: var(--earth-portrait-bg);
border-color: var(--earth-bg);
}
.protagonist.light {
background: var(--light-portrait-bg);
border-color: var(--light-bg);
}
.protagonist.dark {
background: var(--dark-portrait-bg);
border-color: var(--dark-bg);
}
.protagonist.empty {
background: var(--card-bg);
}
</style>

View file

@ -1,128 +1,259 @@
<script lang="ts">
import type { PartyView } from '$lib/api/schemas/party'
import WeaponRep from '$lib/components/reps/WeaponRep.svelte'
import SummonRep from '$lib/components/reps/SummonRep.svelte'
import CharacterRep from '$lib/components/reps/CharacterRep.svelte'
import type { PartyView } from '$lib/api/schemas/party'
import WeaponRep from '$lib/components/reps/WeaponRep.svelte'
import SummonRep from '$lib/components/reps/SummonRep.svelte'
import CharacterRep from '$lib/components/reps/CharacterRep.svelte'
import Icon from '$lib/components/Icon.svelte'
export let party: PartyView
export let href: string = `/teams/${party.shortcode}`
export let loading = false
export let party: PartyView
export let href: string = `/teams/${party.shortcode}`
export let loading = false
let currentView: 'weapons' | 'summons' | 'characters' = 'weapons'
let currentView: 'weapons' | 'summons' | 'characters' = 'summons'
function displayName(input: any): string {
if (!input) return '—'
const maybe = input.name ?? input
if (typeof maybe === 'string') return maybe
if (maybe && typeof maybe === 'object') return maybe.en || maybe.ja || '—'
return '—'
}
function displayName(input: any): string {
if (!input) return '—'
const maybe = input.name ?? input
if (typeof maybe === 'string') return maybe
if (maybe && typeof maybe === 'object') return maybe.en || maybe.ja || '—'
return '—'
}
</script>
<div class={`gridRep ${loading ? 'hidden' : 'visible'}`} on:mouseleave={() => currentView='weapons'}>
<a href={href} data-sveltekit-preload-data="hover">
<div class="details">
<div class="top">
<div class="info">
<h2 class:empty={!party.name}>{party.name || '(untitled)'}</h2>
<div class="properties">
<span class={`raid ${!party.raid ? 'empty' : ''}`}>{party.raid ? displayName(party.raid) : 'No raid'}</span>
{#if party.fullAuto}<span class="fullAuto"> · Full Auto</span>{/if}
{#if party.raid?.group?.extra}<span class="extra"> · EX</span>{/if}
</div>
</div>
</div>
</div>
<div class="gridContainer">
{#if currentView==='characters'}
<div class="characterGrid"><CharacterRep {party} /></div>
{:else if currentView==='summons'}
<div class="summonGrid"><SummonRep {party} /></div>
{:else}
<div class="weaponGrid"><WeaponRep {party} /></div>
{/if}
</div>
<ul class="indicators">
<li class:active={currentView==='characters'} on:mouseenter={() => currentView='characters'}>
<div class="indicator" />
<span class="sr-only">Characters</span>
</li>
<li class:active={currentView==='weapons'} on:mouseenter={() => currentView='weapons'}>
<div class="indicator" />
<span class="sr-only">Weapons</span>
</li>
<li class:active={currentView==='summons'} on:mouseenter={() => currentView='summons'}>
<div class="indicator" />
<span class="sr-only">Summons</span>
</li>
</ul>
</a>
<div
class={`gridRep ${loading ? 'hidden' : 'visible'}`}
role="link"
tabindex="0"
on:mouseleave={() => (currentView = 'summons')}
>
<a {href} data-sveltekit-preload-data="hover">
<div class="info">
<h2 class:empty={!party.name}>{party.name || '(untitled)'}</h2>
<div class="details">
<span class={`raid ${!party.raid ? 'empty' : ''}`}
>{party.raid ? displayName(party.raid) : 'No raid'}</span
>
<div class="pills">
{#if party.chargeAttack}
<span class="pill chargeAttack" title="Charge Attack">
<Icon name="charge-attack" size={16} />
</span>
{/if}
{#if party.fullAuto}
<span class="pill fullAuto" title="Full Auto">
<Icon name="full-auto" size={16} />
</span>
{/if}
{#if party.raid?.group?.extra}
<span class="pill extra" title="Extra">
<Icon name="extra-grid" size={16} />
</span>
{/if}
</div>
</div>
</div>
<div class="gridContainer">
{#if currentView === 'characters'}
<div class="characterGrid"><CharacterRep {party} /></div>
{:else if currentView === 'summons'}
<div class="summonGrid"><SummonRep {party} extendedView={true} /></div>
{:else}
<div class="weaponGrid"><WeaponRep {party} /></div>
{/if}
</div>
<ul class="indicators">
<li
class:active={currentView === 'characters'}
on:mouseenter={() => (currentView = 'characters')}
>
<div class="indicator"></div>
<span class="sr-only">Characters</span>
</li>
<li class:active={currentView === 'weapons'} on:mouseenter={() => (currentView = 'weapons')}>
<div class="indicator"></div>
<span class="sr-only">Weapons</span>
</li>
<li class:active={currentView === 'summons'} on:mouseenter={() => (currentView = 'summons')}>
<div class="indicator"></div>
<span class="sr-only">Summons</span>
</li>
</ul>
</a>
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/spacing' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/rep' as rep;
.gridRep { aspect-ratio: 3/2; border-radius: 10px; box-sizing: border-box; min-width: 320px; position: relative; width: 100%; opacity: 1; }
.gridRep.visible { transition: opacity .3s ease-in-out; opacity: 1; }
.gridRep.hidden { opacity: 0; transition: opacity .12s ease-in-out; }
.gridRep a {
display: grid;
grid-template-rows: auto 1fr;
gap: 8px;
padding: $unit-2x;
text-decoration: none;
color: inherit;
width: 100%;
height: 100%;
border: 1px solid transparent;
border-radius: 10px;
box-sizing: border-box;
background: var(--card-bg);
overflow: hidden;
}
.gridRep a:hover { background: var(--grid-rep-hover); border-color: rgba(0,0,0,.1); }
.gridRep a:hover .indicators { opacity: 1; }
.gridContainer { aspect-ratio: 2.1/1; width: 100%; align-self: start; }
.gridRep {
box-sizing: border-box;
min-width: 262px;
position: relative;
width: 100%;
opacity: 1;
.weaponGrid { aspect-ratio: 3.25/1; }
.characterGrid { aspect-ratio: 2.1/1; }
.summonGrid { aspect-ratio: 2/0.91; }
&.visible {
transition: opacity 0.3s ease-in-out;
opacity: 1;
}
.details { display: flex; flex-direction: column; gap: $unit; }
.details .top { display: flex; flex-direction: row; gap: calc($unit/2); align-items: center; }
.details .info { display: flex; flex-direction: column; flex-grow: 1; gap: calc($unit/2); max-width: calc(100% - 44px - $unit); }
.details h2 { color: var(--text-primary); font-size: 1.6rem; font-weight: 600; overflow: hidden; padding-bottom: 1px; text-overflow: ellipsis; white-space: nowrap; min-height: 24px; max-width: 258px; }
.details h2.empty { color: var(--text-tertiary); }
.properties { display: flex; font-size: 1.3rem; gap: $unit-half; }
.raid.empty { color: var(--text-tertiary); }
.fullAuto { color: var(--full-auto-label-text); white-space: nowrap; }
.extra { color: var(--extra-purple-light-text); white-space: nowrap; }
&.hidden {
opacity: 0;
transition: opacity 0.12s ease-in-out;
}
.indicators {
display: flex;
flex-direction: row;
gap: $unit;
margin-top: $unit;
margin-bottom: $unit;
justify-content: center;
opacity: 0;
list-style: none;
padding-left: 0;
}
.indicators li { flex-grow: 1; padding: $unit 0; position: relative; }
.indicator { transition: background-color .12s ease-in-out; height: $unit; border-radius: $unit-half; background-color: var(--button-contained-bg-hover); }
.indicators li:hover .indicator, .indicators li.active .indicator { background-color: var(--text-secondary); }
a {
display: grid;
grid-template-rows: auto 1fr;
gap: $unit;
padding: $unit;
text-decoration: none;
color: inherit;
width: 100%;
height: 100%;
border: 1px solid transparent;
border-radius: $card-corner;
box-sizing: border-box;
background: var(--card-bg);
overflow: hidden;
/* Visually hidden, accessible to screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
&:hover {
background: var(--grid-rep-hover);
border-color: rgba(0, 0, 0, 0.1);
}
&:hover .indicators {
opacity: 1;
}
}
}
.gridContainer {
/* Reserve a constant visual height for all reps; keeps card height stable */
aspect-ratio: calc(#{rep.$rep-body-ratio} / 1);
width: 100%;
align-self: start;
overflow: hidden;
}
/* inner wrappers simply fill; specific geometry lives inside reps */
.weaponGrid,
.characterGrid,
.summonGrid {
width: 100%;
height: 100%;
}
.info {
display: flex;
flex-direction: column;
gap: $unit-fourth;
padding: $unit-half 0;
h2 {
color: var(--text-primary);
font-size: 1.6rem;
font-weight: 600;
overflow: hidden;
padding-bottom: 1px;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
&.empty {
color: var(--text-tertiary);
}
}
.details {
display: flex;
flex-direction: row;
gap: $unit;
justify-content: space-between;
min-width: 0;
}
.raid {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 0 1 auto;
min-width: 0;
&.empty {
color: var(--text-tertiary);
}
}
.pills {
flex-shrink: 0;
.pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 $unit-half;
border-radius: $full-corner;
flex-shrink: 0;
&.chargeAttack {
background-color: var(--charge-attack-bg);
color: var(--charge-attack-text);
}
&.fullAuto {
background-color: var(--full-auto-bg);
color: var(--full-auto-text);
}
&.extra {
background-color: var(--extra-purple-bg);
color: var(--extra-purple-text);
}
}
}
}
.indicators {
display: flex;
flex-direction: row;
gap: $unit;
justify-content: center;
opacity: 0;
list-style: none;
padding-left: 0;
li {
flex-grow: 1;
padding: $unit 0;
position: relative;
&:hover .indicator,
&.active .indicator {
background-color: var(--text-secondary);
}
}
.indicator {
transition: background-color 0.12s ease-in-out;
height: $unit;
border-radius: $unit-half;
background-color: var(--button-contained-bg-hover);
}
}
/* Visually hidden, accessible to screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>

View file

@ -1,45 +1,110 @@
<script lang="ts">
import type { PartyView, GridSummonItemView } from '$lib/api/schemas/party'
import type { PartyView, GridSummonItemView } from '$lib/api/schemas/party'
export let party: PartyView
export let party: PartyView
export let extendedView = false
const summons = party.summons || []
const main = summons.find((s: any) => s?.main || s?.position === -1)
const grid = Array.from({ length: 4 }, (_, i) => summons.find((s: any) => s?.position === i))
const summons = party.summons || []
const main = summons.find((s: any) => s?.main || s?.position === -1)
const friend = extendedView
? summons.find((s: any) => s?.friend || s?.position === -2)
: undefined
function summonImageUrl(s?: any, isMain = false): string {
const id = s?.object?.granblueId
if (!id) return ''
const folder = isMain ? 'summon-main' : 'summon-grid'
return `/images/${folder}/${id}.jpg`
}
// In standard view: show positions 0-3 (4 summons)
// In extended view: show positions 0-5 (6 summons including subauras)
const gridLength = extendedView ? 6 : 4
const grid = Array.from({ length: gridLength }, (_, i) =>
summons.find((s: any) => s?.position === i)
)
function summonImageUrl(s?: any, isMain = false): string {
const id = s?.object?.granblueId
if (!id) return ''
const folder = isMain ? 'summon-main' : 'summon-grid'
return `/images/${folder}/${id}.jpg`
}
</script>
<div class="rep">
<div class="mainSummon">{#if main}<img alt="Main Summon" src={summonImageUrl(main, true)} />{/if}</div>
<ul class="summons">
{#each grid as s, i}
<li class="summon">{#if s}<img alt="Summon" src={summonImageUrl(s)} />{/if}</li>
{/each}
</ul>
<div class="rep" class:extended={extendedView}>
<div class="mainSummon">
{#if main}<img alt="Main Summon" src={summonImageUrl(main, true)} />{/if}
</div>
<ul class="summons">
{#each grid as s, i}
<li class="summon">
{#if s}<img alt="Summon" src={summonImageUrl(s)} />{/if}
</li>
{/each}
</ul>
{#if extendedView}
<div class="friendSummon">
{#if friend}<img alt="Friend Summon" src={summonImageUrl(friend, true)} />{/if}
</div>
{/if}
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/spacing' as *;
@use '$src/themes/rep' as rep;
.rep {
aspect-ratio: 2/1.045;
border-radius: 10px;
display: grid;
grid-template-columns: 1fr 2.25fr;
grid-gap: $unit-half;
height: $rep-height;
opacity: .5;
}
.summon, .mainSummon { background: var(--card-bg); border-radius: 4px; }
.mainSummon { aspect-ratio: 56/97; display: grid; }
.summons { display: grid; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr); gap: $unit-half; }
.summon { aspect-ratio: 184/138; display: grid; }
.summon img, .mainSummon img { border-radius: 4px; width: 100%; height: 100%; }
.rep {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 1fr #{rep.$summon-cols-proportion}fr;
grid-gap: $unit-half;
}
// Extended view layout: main summon | 6 grid summons | friend summon
.rep.extended {
display: flex;
gap: $unit-half;
grid-template-columns: none;
box-sizing: border-box;
}
.summon,
.mainSummon,
.friendSummon {
background: var(--card-bg);
border-radius: 4px;
}
.mainSummon {
@include rep.aspect(56, 97);
display: grid;
}
.extended .mainSummon,
.extended .friendSummon {
@include rep.aspect(56, 97);
display: grid;
flex: 0 0 auto;
}
.summons {
@include rep.grid(2, 2, $unit-half);
}
.extended .summons {
display: grid;
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: $unit-half;
flex: 1 1 0;
min-width: 0; /* allow grid to shrink without overflowing */
}
.summon {
@include rep.aspect(184, 138);
display: grid;
}
.summon img,
.mainSummon img,
.friendSummon img {
border-radius: 4px;
width: 100%;
height: 100%;
}
</style>

View file

@ -1,49 +1,48 @@
<script lang="ts">
import type { PartyView, GridWeaponItemView } from '$lib/api/schemas/party'
import type { PartyView, GridWeaponItemView } from '$lib/api/schemas/party'
export let party: PartyView
export let party: PartyView
const weapons = party.weapons || []
const mainhand: GridWeaponItemView | undefined = weapons.find((w: any) => w?.mainhand || w?.position === -1)
const grid = Array.from({ length: 9 }, (_, i) => weapons.find((w: any) => w?.position === i))
const weapons = party.weapons || []
const mainhand: GridWeaponItemView | undefined = weapons.find(
(w: any) => w?.mainhand || w?.position === -1
)
const grid = Array.from({ length: 9 }, (_, i) => weapons.find((w: any) => w?.position === i))
function weaponImageUrl(w?: any, isMain = false): string {
const id = w?.object?.granblueId
if (!id) return ''
const folder = isMain ? 'weapon-main' : 'weapon-grid'
const objElement = w?.object?.element
const instElement = w?.element
if (objElement === 0 && instElement) return `/images/${folder}/${id}_${instElement}.jpg`
return `/images/${folder}/${id}.jpg`
}
function weaponImageUrl(w?: any, isMain = false): string {
const id = w?.object?.granblueId
if (!id) return ''
const folder = isMain ? 'weapon-main' : 'weapon-grid'
const objElement = w?.object?.element
const instElement = w?.element
if (objElement === 0 && instElement) return `/images/${folder}/${id}_${instElement}.jpg`
return `/images/${folder}/${id}.jpg`
}
</script>
<div class="rep">
<div class="mainhand">{#if mainhand}<img alt="Mainhand" src={weaponImageUrl(mainhand, true)} />{/if}</div>
<ul class="weapons">
{#each grid as w, i}
<li class="weapon">{#if w}<img alt="Weapon" src={weaponImageUrl(w)} />{/if}</li>
{/each}
</ul>
<div class="mainhand">
{#if mainhand}<img alt="Mainhand" src={weaponImageUrl(mainhand, true)} />{/if}
</div>
<ul class="weapons">
{#each grid as w, i}
<li class="weapon">
{#if w}<img alt="Weapon" src={weaponImageUrl(w)} />{/if}
</li>
{/each}
</ul>
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/colors' as *;
@use '$src/themes/rep' as rep;
.rep {
height: $rep-height;
display: grid;
grid-template-columns: calc(#{$rep-height} * (200 / 420)) auto;
gap: $unit-half;
box-sizing: border-box;
}
.mainhand { background: var(--card-bg); border-radius: 4px; height: 100%; width: 100%; overflow: hidden; }
.mainhand img { width: 100%; height: 100%; object-fit: contain; }
.rep { width: 100%; height: 100%; display: grid; grid-template-columns: 1fr #{rep.$weapon-cols-proportion}fr; gap: $unit-half; }
.weapons { margin: 0; padding: 0; list-style: none; height: 100%; display: grid; grid-template-rows: repeat(3, calc((#{$rep-height} - (2 * #{$unit-half})) / 3)); grid-template-columns: repeat(3, calc((#{$rep-height} - (2 * #{$unit-half})) / 3 * (280 / 160))); gap: $unit-half; }
.weapon { background: var(--card-bg); border-radius: 4px; overflow: hidden; display: flex; align-items: center; justify-content: center; }
.weapon img { width: 100%; height: 100%; }
.mainhand { background: var(--unit-bg); border-radius: 4px; @include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h); overflow: hidden; }
.mainhand img { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; }
.weapons { margin: 0; padding: 0; list-style: none; height: 100%; @include rep.grid(3, 3, $unit-half); }
.weapon { background: var(--unit-bg); border-radius: 4px; overflow: hidden; @include rep.aspect(rep.$weapon-cell-w, rep.$weapon-cell-h); }
.weapon img { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; }
</style>