Files
HolodoriCalc/src/routes/cards/+page.svelte
T
dal4segno 476a25caef
Publish GitHub Pages output / publish (push) Successful in 46s
feat: save analysis results and compare growth snapshots
2026-09-06 10:39:06 +09:00

1874 lines
57 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import cardSummary from '$lib/data/cards-summary.json';
import { skillAtLevel, sortCardsByActive, type CardSummary, type CardType, type HighestStat, type SkillLevel } from '$lib/models/card-list';
import type { CardListPreferences, CardListSortKey, CardSkillLevel, SortDirection, UserData } from '$lib/models/user-data';
import { loadBrowserData, saveBrowserData } from '$lib/storage/browser-storage';
type SkillConditionStatus = '일반' | '조건부';
type ActiveProbability = '높음' | '중간';
const allCards = cardSummary.cards as CardSummary[];
const cards = allCards.filter((card) => card.rarity === 5);
const typeOptions: CardType[] = ['큐트', '퓨어', '해피'];
const typeIconPaths: Record<CardType, string> = {
큐트: '/icons/types/cute.webp',
퓨어: '/icons/types/pure.webp',
해피: '/icons/types/happy.webp'
};
const statOptions: HighestStat[] = ['퍼포먼스', '테크닉', '센스'];
const skillConditionOptions: SkillConditionStatus[] = ['일반', '조건부'];
const activeProbabilityOptions: ActiveProbability[] = ['높음', '중간'];
const cardVariantOptions = ['일반', ...new Set(cards.map((card) => card.versionName).filter(Boolean))];
const generationOptions = [...new Set(cards.flatMap((card) => generationMemberships(card.generation)))];
let selectedTypes = [...typeOptions];
let selectedStats = [...statOptions];
let selectedActiveConditions = [...skillConditionOptions];
let selectedSpecialConditions = [...skillConditionOptions];
let selectedActiveProbabilities = [...activeProbabilityOptions];
let selectedCardVariants = [...cardVariantOptions];
let selectedGenerations = [...generationOptions];
let sortKey: CardListSortKey | null = null;
let sortDirection: SortDirection = 'asc';
let expandedCardId: string | null = null;
let selectedSkillLevel: CardSkillLevel = 1;
let collection: UserData['collection'] = {};
let memberBoards: UserData['memberBoards'] = {};
let boardCards: UserData['boardCards'] = {};
let savedDecks: UserData['savedDecks'] = [];
let memoryCount = 0;
let memberPowerUpPercent = 0;
let storageReady = false;
let currentUserData: UserData;
$: currentUserData = {
schemaVersion: 1,
updatedAt: new Date().toISOString(),
memoryCount,
memberPowerUpPercent,
collection,
memberBoards,
boardCards,
savedDecks,
preferences: currentPreferences()
};
$: if (storageReady) {
try {
saveBrowserData(currentUserData);
} catch {}
}
onMount(() => {
try {
applyUserData(loadBrowserData(defaultPreferences()));
storageReady = true;
} catch {}
});
$: filteredCards = sortCardsByActive(
cards.filter((card) =>
selectedTypes.includes(card.type)
&& selectedStats.includes(card.highestStat)
&& matchesSkillCondition(selectedActiveConditions, card.details.active ?? [])
&& selectedSpecialConditions.includes(skillConditionStatus(card.details.special))
&& matchesActiveProbability(selectedActiveProbabilities, card.details.active ?? [])
&& selectedCardVariants.includes(cardVariantStatus(card))
&& generationMemberships(card.generation).some((generation) => selectedGenerations.includes(generation))
),
sortKey,
sortDirection,
selectedSkillLevel
);
$: allSelected = selectedTypes.length === typeOptions.length
&& selectedStats.length === statOptions.length
&& selectedActiveConditions.length === skillConditionOptions.length
&& selectedSpecialConditions.length === skillConditionOptions.length
&& selectedActiveProbabilities.length === activeProbabilityOptions.length
&& selectedCardVariants.length === cardVariantOptions.length
&& selectedGenerations.length === generationOptions.length;
$: allDeselected = selectedTypes.length === 0
&& selectedStats.length === 0
&& selectedActiveConditions.length === 0
&& selectedSpecialConditions.length === 0
&& selectedActiveProbabilities.length === 0
&& selectedCardVariants.length === 0
&& selectedGenerations.length === 0;
$: if (expandedCardId && !filteredCards.some((card) => card.id === expandedCardId)) {
expandedCardId = null;
}
function selectAll() {
selectedTypes = [...typeOptions];
selectedStats = [...statOptions];
selectedActiveConditions = [...skillConditionOptions];
selectedSpecialConditions = [...skillConditionOptions];
selectedActiveProbabilities = [...activeProbabilityOptions];
selectedCardVariants = [...cardVariantOptions];
selectedGenerations = [...generationOptions];
}
function selectNone() {
selectedTypes = [];
selectedStats = [];
selectedActiveConditions = [];
selectedSpecialConditions = [];
selectedActiveProbabilities = [];
selectedCardVariants = [];
selectedGenerations = [];
}
function defaultPreferences(): CardListPreferences {
return {
types: [...typeOptions],
stats: [...statOptions],
activeConditions: [...skillConditionOptions],
specialConditions: [...skillConditionOptions],
activeProbabilities: [...activeProbabilityOptions],
cardVariants: [...cardVariantOptions],
generations: [...generationOptions],
sortKey: null,
sortDirection: 'asc',
skillLevel: 1
};
}
function currentPreferences(): CardListPreferences {
return {
types: [...selectedTypes],
stats: [...selectedStats],
activeConditions: [...selectedActiveConditions],
specialConditions: [...selectedSpecialConditions],
activeProbabilities: [...selectedActiveProbabilities],
cardVariants: [...selectedCardVariants],
generations: [...selectedGenerations],
sortKey,
sortDirection,
skillLevel: selectedSkillLevel
};
}
function applyUserData(data: UserData) {
selectedTypes = data.preferences.types.filter((value): value is CardType => typeOptions.includes(value as CardType));
selectedStats = data.preferences.stats.filter((value): value is HighestStat => statOptions.includes(value as HighestStat));
selectedActiveConditions = data.preferences.activeConditions.filter((value): value is SkillConditionStatus => skillConditionOptions.includes(value as SkillConditionStatus));
selectedSpecialConditions = data.preferences.specialConditions.filter((value): value is SkillConditionStatus => skillConditionOptions.includes(value as SkillConditionStatus));
selectedActiveProbabilities = data.preferences.activeProbabilities.filter((value): value is ActiveProbability => activeProbabilityOptions.includes(value as ActiveProbability));
selectedCardVariants = data.preferences.cardVariants.filter((value) => cardVariantOptions.includes(value));
selectedGenerations = [...new Set(data.preferences.generations.flatMap(generationMemberships))]
.filter((value) => generationOptions.includes(value));
sortKey = data.preferences.sortKey;
sortDirection = data.preferences.sortDirection;
selectedSkillLevel = data.preferences.skillLevel;
collection = data.collection;
memberBoards = data.memberBoards;
boardCards = data.boardCards;
savedDecks = data.savedDecks;
memoryCount = data.memoryCount;
memberPowerUpPercent = data.memberPowerUpPercent;
}
function cardVariantStatus(card: CardSummary): string {
return card.versionName || '일반';
}
function generationMemberships(generation: string): string[] {
return generation.split(/\s*\/\s*/).filter(Boolean).map(generationLabel);
}
function generationLabel(generation: string): string {
return generation
.replace(/^hololive EN /, '')
.replace(/^hololive ID /, 'ID ')
.replace(/^hololive /, '')
.replace(/^DEV_IS /, '')
.replace(/GAMERS/g, '게이머즈');
}
function skillConditionStatus(levels: SkillLevel[]): SkillConditionStatus {
return levels.some((level) => Boolean(level.condition)) ? '조건부' : '일반';
}
function matchesSkillCondition(selected: SkillConditionStatus[], levels: SkillLevel[]): boolean {
return levels.length === 0
? selected.length === skillConditionOptions.length
: selected.includes(skillConditionStatus(levels));
}
function activeProbabilityStatus(levels: SkillLevel[]): ActiveProbability | null {
if (levels.length === 0) return null;
return levels.some((level) => level.probabilityPercent === 55) ? '높음' : '중간';
}
function matchesActiveProbability(selected: ActiveProbability[], levels: SkillLevel[]): boolean {
const status = activeProbabilityStatus(levels);
return status === null
? selected.length === activeProbabilityOptions.length
: selected.includes(status);
}
function toggleCard(cardId: string) {
expandedCardId = expandedCardId === cardId ? null : cardId;
}
function toggleSort(nextSortKey: CardListSortKey) {
if (sortKey === nextSortKey) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
return;
}
sortKey = nextSortKey;
sortDirection = 'asc';
}
function sortButtonLabel(key: CardListSortKey, label: string) {
if (sortKey !== key) return `${label} 오름차순 정렬`;
return `${label} ${sortDirection === 'asc' ? '내림차순' : '오름차순'}으로 변경`;
}
function secondsLabel(seconds: number | null | undefined) {
return seconds == null ? '—' : `${seconds}초`;
}
function probabilityLabel(probabilityPercent: number | null | undefined) {
if (probabilityPercent === 55) return '높음';
if (probabilityPercent === 46) return '중간';
return probabilityPercent == null ? '—' : `${probabilityPercent}%`;
}
function activeEffectLabel(active: SkillLevel | undefined) {
return active?.effect || active?.description || '—';
}
function descriptionSegments(description: string, highlightNumbers = false) {
type DescriptionIcon = CardType | HighestStat;
type DescriptionSegment = { text: string; highlight?: boolean; attribute?: string; icon?: DescriptionIcon; bold?: boolean; target?: boolean; numeric?: boolean };
const segments: DescriptionSegment[] = [];
const pattern = /\[highlight\](.*?)\[\/highlight\]|\[attribute=(cute|pure|happy)\](.*?)\[\/attribute\]/g;
description = description.replace(/\[\/highlight\](%)/g, '$1[/highlight]');
let cursor = 0;
let match: RegExpExecArray | null;
function pushSegment(text: string, options: Omit<DescriptionSegment, 'text' | 'icon' | 'bold' | 'target' | 'numeric'> = {}) {
const targetNames = '(?:ID\\s*)?\\d+기생|GAMERS|holoX|Myth|Promise|Advent|ReGLOSS|Justice|FLOW GLOW';
const targetExpression = `(?:${targetNames})|(?:큐트|퓨어|해피)\\s*타입|자신|전원|멤버`;
const keywords = new RegExp(`(${targetExpression}|스코어 서포트 효과|전체 파라미터|스킬 발동률|퍼포먼스|테크닉|센스|스코어|라이프|\\d+(?:\\.\\d+)?(?:%|초|명|콤보|회)?|큐트|퓨어|해피)`, 'g');
const isTarget = new RegExp(`^(?:${targetExpression})$`);
const isNumeric = /^\d+(?:\.\d+)?(?:%|초|명|콤보|회)?$/;
let keywordCursor = 0;
let keywordMatch: RegExpExecArray | null;
while ((keywordMatch = keywords.exec(text)) !== null) {
if (keywordMatch.index > keywordCursor) {
segments.push({ text: text.slice(keywordCursor, keywordMatch.index), ...options });
}
const keyword = keywordMatch[0];
const typeIcon = typeOptions.find((type) => keyword.startsWith(type));
const statIcon = statOptions.find((stat) => keyword === stat);
const numeric = isNumeric.test(keyword);
segments.push({
text: keyword,
...options,
icon: typeIcon || statIcon,
bold: true,
target: isTarget.test(keyword),
numeric,
highlight: options.highlight || (numeric && highlightNumbers)
});
keywordCursor = keywords.lastIndex;
}
if (keywordCursor < text.length) segments.push({ text: text.slice(keywordCursor), ...options });
}
while ((match = pattern.exec(description)) !== null) {
if (match.index > cursor) pushSegment(description.slice(cursor, match.index));
if (match[1] !== undefined) pushSegment(match[1], { highlight: true });
if (match[3] !== undefined) pushSegment(match[3], { attribute: match[2] });
cursor = pattern.lastIndex;
}
if (cursor < description.length) pushSegment(description.slice(cursor));
return segments;
}
</script>
<svelte:head>
<title>카드 목록 · 홀로도리 계산기</title>
<meta name="description" content="홀로드림스 카드의 등급, 타입, 이름, 최고 스탯 목록" />
</svelte:head>
<main>
<header class="page-header">
<div>
<h1>카드 목록</h1>
<p class="description">카드의 기본 정보를 한눈에 확인하세요.</p>
</div>
<div class="header-side">
<p class="total">
<strong>{filteredCards.length}</strong>
{#if filteredCards.length === cards.length}
장
{:else}
<span>/ {cards.length}장</span>
{/if}
</p>
</div>
</header>
<section class="filters" aria-labelledby="filter-heading">
<div class="filter-header">
<h2 id="filter-heading">출력 조건</h2>
<div class="filter-actions">
<button class="filter-action" type="button" onclick={selectNone} disabled={allDeselected}>모두 취소</button>
<button class="filter-action" type="button" onclick={selectAll} disabled={allSelected}>모두 선택</button>
</div>
</div>
<div class="filter-groups">
<fieldset>
<legend>타입</legend>
<div class="filter-options">
{#each typeOptions as type}
<label class:checked={selectedTypes.includes(type)}>
<input type="checkbox" bind:group={selectedTypes} value={type} aria-label={type} />
<span class="checkmark" aria-hidden="true">✓</span>
<img class="type-icon" src={typeIconPaths[type]} alt="" title={type} />
</label>
{/each}
</div>
</fieldset>
<fieldset>
<legend>주스탯</legend>
<div class="filter-options">
{#each statOptions as stat}
<label class:checked={selectedStats.includes(stat)}>
<input type="checkbox" bind:group={selectedStats} value={stat} aria-label={stat} />
<span class="checkmark" aria-hidden="true">✓</span>
<span
class:stat-icon-performance={stat === '퍼포먼스'}
class:stat-icon-technique={stat === '테크닉'}
class:stat-icon-sense={stat === '센스'}
class="stat-icon"
aria-hidden="true"
title={stat}
></span>
</label>
{/each}
</div>
</fieldset>
<fieldset>
<legend>액티브 확률</legend>
<div class="filter-options">
{#each activeProbabilityOptions as probability}
<label class:checked={selectedActiveProbabilities.includes(probability)}>
<input type="checkbox" bind:group={selectedActiveProbabilities} value={probability} />
<span class="checkmark" aria-hidden="true">✓</span>
<span>{probability}</span>
</label>
{/each}
</div>
</fieldset>
<fieldset>
<legend>액티브 조건</legend>
<div class="filter-options">
{#each skillConditionOptions as conditionStatus}
<label class:checked={selectedActiveConditions.includes(conditionStatus)}>
<input type="checkbox" bind:group={selectedActiveConditions} value={conditionStatus} />
<span class="checkmark" aria-hidden="true">✓</span>
<span>{conditionStatus}</span>
</label>
{/each}
</div>
</fieldset>
<fieldset>
<legend>스페셜 조건</legend>
<div class="filter-options">
{#each skillConditionOptions as conditionStatus}
<label class:checked={selectedSpecialConditions.includes(conditionStatus)}>
<input type="checkbox" bind:group={selectedSpecialConditions} value={conditionStatus} />
<span class="checkmark" aria-hidden="true">✓</span>
<span>{conditionStatus}</span>
</label>
{/each}
</div>
</fieldset>
<fieldset>
<legend>이격</legend>
<div class="filter-options">
{#each cardVariantOptions as variantStatus}
<label class:checked={selectedCardVariants.includes(variantStatus)}>
<input type="checkbox" bind:group={selectedCardVariants} value={variantStatus} />
<span class="checkmark" aria-hidden="true">✓</span>
<span>{variantStatus}</span>
</label>
{/each}
</div>
</fieldset>
<fieldset class="generation-filter">
<legend>기수</legend>
<div class="filter-options">
{#each generationOptions as generation}
<label class:checked={selectedGenerations.includes(generation)}>
<input type="checkbox" bind:group={selectedGenerations} value={generation} />
<span class="checkmark" aria-hidden="true">✓</span>
<span>{generationLabel(generation)}</span>
</label>
{/each}
</div>
</fieldset>
</div>
</section>
<section class="table-panel" aria-label="카드 목록">
<div class="list-header">
<span>타입</span>
<span>주스탯</span>
<span>기수</span>
<span>이름</span>
<button class="sort-button" type="button" class:active={sortKey === 'coolTime'} aria-label={sortButtonLabel('coolTime', '주기')} onclick={() => toggleSort('coolTime')}>
<span>주기</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'coolTime' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
</button>
<button class="sort-button" type="button" class:active={sortKey === 'probability'} aria-label={sortButtonLabel('probability', '확률')} onclick={() => toggleSort('probability')}>
<span>확률</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'probability' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
</button>
<button class="sort-button" type="button" class:active={sortKey === 'duration'} aria-label={sortButtonLabel('duration', '지속')} onclick={() => toggleSort('duration')}>
<span>지속</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'duration' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
</button>
<button class="sort-button" type="button" class:active={sortKey === 'effect'} aria-label={sortButtonLabel('effect', '효과')} onclick={() => toggleSort('effect')}>
<span>효과</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'effect' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
</button>
<div class="header-level-control">
<span>개화</span>
<div class="level-toggle header-level-toggle" aria-label="목록에 표시할 스킬 개화 상태">
<button type="button" class:active={selectedSkillLevel === 1} aria-pressed={selectedSkillLevel === 1} onclick={() => selectedSkillLevel = 1}>명함</button>
<button type="button" class:active={selectedSkillLevel === 2} aria-pressed={selectedSkillLevel === 2} onclick={() => selectedSkillLevel = 2}>5 개화</button>
</div>
</div>
<span></span>
</div>
{#if filteredCards.length === 0}
<div class="empty">선택한 조건에 맞는 카드가 없습니다.</div>
{:else}
<div class="card-list">
{#each filteredCards as card (card.id)}
{@const expanded = expandedCardId === card.id}
{@const passive = skillAtLevel(card.details.passive, selectedSkillLevel)}
{@const active = skillAtLevel(card.details.active, selectedSkillLevel)}
{@const special = skillAtLevel(card.details.special, selectedSkillLevel)}
<article class:expanded class="card-entry">
<div class="card-summary">
<button
class="summary-expand-button"
type="button"
aria-expanded={expanded}
aria-controls={`detail-${card.id}`}
aria-label={`${card.name} 상세 정보 ${expanded ? '접기' : '펼치기'}`}
onclick={() => toggleCard(card.id)}
></button>
<span class="summary-cell type-cell">
<span class="mobile-label">타입</span>
<img class="type-icon" src={typeIconPaths[card.type]} alt={card.type} title={card.type} />
</span>
<span class="summary-cell stat-cell">
<span class="mobile-label">주스탯</span>
<span class:stat-performance={card.highestStat === '퍼포먼스'} class:stat-technique={card.highestStat === '테크닉'} class:stat-sense={card.highestStat === '센스'} class="stat-badge" aria-label={card.highestStat} title={card.highestStat}>
<span
class:stat-icon-performance={card.highestStat === '퍼포먼스'}
class:stat-icon-technique={card.highestStat === '테크닉'}
class:stat-icon-sense={card.highestStat === '센스'}
class="stat-icon"
aria-hidden="true"
></span>
</span>
</span>
<span class="summary-cell generation-cell">
<span class="mobile-label">기수</span>
<span class="generation-name">{generationLabel(card.generation)}</span>
</span>
<span class="summary-cell name-cell">
<span class="mobile-label">이름</span>
<span class="card-identity">
<span class="card-name">{card.name}</span>
{#if card.versionName}<span class="version-badge">{card.versionName}</span>{/if}
</span>
</span>
<span class="summary-cell active-cycle-cell active-summary-cell">
<span class="mobile-label">주기</span>
<span class="active-value">{secondsLabel(active?.coolTimeSeconds)}</span>
</span>
<span class="summary-cell active-probability-cell active-summary-cell">
<span class="mobile-label">확률</span>
<span class="active-value">{probabilityLabel(active?.probabilityPercent)}</span>
</span>
<span class="summary-cell active-duration-cell active-summary-cell">
<span class="mobile-label">지속</span>
<span class="active-value">{secondsLabel(active?.durationSeconds)}</span>
</span>
<span class="summary-cell active-effect-cell active-summary-cell">
<span class="mobile-label">효과</span>
<span class="active-effect" title={activeEffectLabel(active)}>{activeEffectLabel(active)}</span>
</span>
<span class="summary-cell level-cell">
<span class="skill-level-label">{selectedSkillLevel === 1 ? '명함' : '5 개화'}</span>
</span>
<span class="chevron" aria-hidden="true"></span>
</div>
{#if expanded}
<div class="card-details" id={`detail-${card.id}`}>
<div class="skill-grid">
<section class="skill-card passive-card">
<header>
<h4>패시브</h4>
</header>
{#if passive}
<p class="skill-description">
{#each descriptionSegments(passive.description) as segment}
<span class:highlight={segment.highlight} class:semantic-bold={segment.bold} class:numeric-value={segment.numeric} class:target-chip={segment.target} class:description-stat-term={segment.icon && statOptions.includes(segment.icon as HighestStat)} class:description-stat-performance={segment.icon === '퍼포먼스'} class:description-stat-technique={segment.icon === '테크닉'} class:description-stat-sense={segment.icon === '센스'} class:attribute-cute={segment.attribute === 'cute'} class:attribute-pure={segment.attribute === 'pure'} class:attribute-happy={segment.attribute === 'happy'} class:keyword-with-icon={segment.icon}>
{#if segment.icon && typeIconPaths[segment.icon as CardType]}<img class="description-type-icon" src={typeIconPaths[segment.icon as CardType]} alt="" aria-hidden="true" />{/if}
{#if segment.icon && statOptions.includes(segment.icon as HighestStat)}<span class="description-stat-icon stat-icon" class:stat-icon-performance={segment.icon === '퍼포먼스'} class:stat-icon-technique={segment.icon === '테크닉'} class:stat-icon-sense={segment.icon === '센스'} aria-hidden="true"></span>{/if}
{segment.text}
</span>
{/each}
</p>
{/if}
</section>
<section class="skill-card active-card">
<header>
<h4>액티브</h4>
{#if active?.condition}<span class="condition-badge">조건부</span>{/if}
</header>
{#if active}
<dl class="skill-spec active-spec" aria-label="액티브 스킬 발동 정보">
<div>
<dt>주기</dt>
<dd>{active.coolTimeSeconds !== null ? `${active.coolTimeSeconds}초` : '—'}</dd>
</div>
<div>
<dt>확률</dt>
<dd>{probabilityLabel(active.probabilityPercent)}</dd>
</div>
<div>
<dt>지속</dt>
<dd>{active.durationSeconds !== null ? `${active.durationSeconds}초` : '—'}</dd>
</div>
</dl>
<div class="effect-breakdown">
{#if active.condition}
<div class="effect-block condition-effect">
<p class="effect-label">조건</p>
<p class="skill-description">
{#each descriptionSegments(active.condition) as segment}
<span class:highlight={segment.highlight} class:semantic-bold={segment.bold} class:numeric-value={segment.numeric} class:target-chip={segment.target} class:description-stat-term={segment.icon && statOptions.includes(segment.icon as HighestStat)} class:description-stat-performance={segment.icon === '퍼포먼스'} class:description-stat-technique={segment.icon === '테크닉'} class:description-stat-sense={segment.icon === '센스'} class:attribute-cute={segment.attribute === 'cute'} class:attribute-pure={segment.attribute === 'pure'} class:attribute-happy={segment.attribute === 'happy'} class:keyword-with-icon={segment.icon}>
{#if segment.icon && typeIconPaths[segment.icon as CardType]}<img class="description-type-icon" src={typeIconPaths[segment.icon as CardType]} alt="" aria-hidden="true" />{/if}
{#if segment.icon && statOptions.includes(segment.icon as HighestStat)}<span class="description-stat-icon stat-icon" class:stat-icon-performance={segment.icon === '퍼포먼스'} class:stat-icon-technique={segment.icon === '테크닉'} class:stat-icon-sense={segment.icon === '센스'} aria-hidden="true"></span>{/if}
{segment.text}
</span>
{/each}
</p>
</div>
{/if}
<div class:result-effect={active.condition} class="effect-block">
<p class="effect-label">{active.condition ? '조건 충족 시 효과' : '효과'}</p>
<p class="skill-description">
{#each descriptionSegments(active.effect || active.description, selectedSkillLevel === 2) as segment}
<span class:highlight={segment.highlight} class:semantic-bold={segment.bold} class:numeric-value={segment.numeric} class:target-chip={segment.target} class:description-stat-term={segment.icon && statOptions.includes(segment.icon as HighestStat)} class:description-stat-performance={segment.icon === '퍼포먼스'} class:description-stat-technique={segment.icon === '테크닉'} class:description-stat-sense={segment.icon === '센스'} class:attribute-cute={segment.attribute === 'cute'} class:attribute-pure={segment.attribute === 'pure'} class:attribute-happy={segment.attribute === 'happy'} class:keyword-with-icon={segment.icon}>
{#if segment.icon && typeIconPaths[segment.icon as CardType]}<img class="description-type-icon" src={typeIconPaths[segment.icon as CardType]} alt="" aria-hidden="true" />{/if}
{#if segment.icon && statOptions.includes(segment.icon as HighestStat)}<span class="description-stat-icon stat-icon" class:stat-icon-performance={segment.icon === '퍼포먼스'} class:stat-icon-technique={segment.icon === '테크닉'} class:stat-icon-sense={segment.icon === '센스'} aria-hidden="true"></span>{/if}
{segment.text}
</span>
{/each}
</p>
</div>
</div>
{/if}
</section>
<section class="skill-card special-card">
<header>
<h4>스페셜</h4>
{#if special?.condition}<span class="condition-badge">조건부</span>{/if}
</header>
{#if special}
<dl class="skill-spec special-spec" aria-label="스페셜 스킬 발동 정보">
<div>
<dt>지속</dt>
<dd>{special.durationSeconds !== null ? `${special.durationSeconds}초` : '—'}</dd>
</div>
</dl>
<div class="skill-effects">
{#if special.condition}
<div class="effect-block condition-effect">
<p class="effect-label">조건</p>
<p class="skill-description">
{#each descriptionSegments(special.condition) as segment}
<span class:highlight={segment.highlight} class:semantic-bold={segment.bold} class:numeric-value={segment.numeric} class:target-chip={segment.target} class:description-stat-term={segment.icon && statOptions.includes(segment.icon as HighestStat)} class:description-stat-performance={segment.icon === '퍼포먼스'} class:description-stat-technique={segment.icon === '테크닉'} class:description-stat-sense={segment.icon === '센스'} class:attribute-cute={segment.attribute === 'cute'} class:attribute-pure={segment.attribute === 'pure'} class:attribute-happy={segment.attribute === 'happy'} class:keyword-with-icon={segment.icon}>
{#if segment.icon && typeIconPaths[segment.icon as CardType]}<img class="description-type-icon" src={typeIconPaths[segment.icon as CardType]} alt="" aria-hidden="true" />{/if}
{#if segment.icon && statOptions.includes(segment.icon as HighestStat)}<span class="description-stat-icon stat-icon" class:stat-icon-performance={segment.icon === '퍼포먼스'} class:stat-icon-technique={segment.icon === '테크닉'} class:stat-icon-sense={segment.icon === '센스'} aria-hidden="true"></span>{/if}
{segment.text}
</span>
{/each}
</p>
</div>
{/if}
<div class:result-effect={special.condition} class="effect-block">
<p class="effect-label">{special.condition ? '조건 충족 시 효과' : '효과'}</p>
<p class="skill-description">
{#each descriptionSegments(special.effect || special.description, selectedSkillLevel === 2) as segment}
<span class:highlight={segment.highlight} class:semantic-bold={segment.bold} class:numeric-value={segment.numeric} class:target-chip={segment.target} class:description-stat-term={segment.icon && statOptions.includes(segment.icon as HighestStat)} class:description-stat-performance={segment.icon === '퍼포먼스'} class:description-stat-technique={segment.icon === '테크닉'} class:description-stat-sense={segment.icon === '센스'} class:attribute-cute={segment.attribute === 'cute'} class:attribute-pure={segment.attribute === 'pure'} class:attribute-happy={segment.attribute === 'happy'} class:keyword-with-icon={segment.icon}>
{#if segment.icon && typeIconPaths[segment.icon as CardType]}<img class="description-type-icon" src={typeIconPaths[segment.icon as CardType]} alt="" aria-hidden="true" />{/if}
{#if segment.icon && statOptions.includes(segment.icon as HighestStat)}<span class="description-stat-icon stat-icon" class:stat-icon-performance={segment.icon === '퍼포먼스'} class:stat-icon-technique={segment.icon === '테크닉'} class:stat-icon-sense={segment.icon === '센스'} aria-hidden="true"></span>{/if}
{segment.text}
</span>
{/each}
</p>
</div>
</div>
{/if}
</section>
<section class="skill-card costume-card">
<header>
<span class="skill-symbol" aria-hidden="true">의</span>
<h4>의상 효과</h4>
</header>
{#if card.details.costume}
<p class="skill-description">
{#each descriptionSegments(card.details.costume.description) as segment}
<span class:highlight={segment.highlight} class:semantic-bold={segment.bold} class:numeric-value={segment.numeric} class:target-chip={segment.target} class:description-stat-term={segment.icon && statOptions.includes(segment.icon as HighestStat)} class:description-stat-performance={segment.icon === '퍼포먼스'} class:description-stat-technique={segment.icon === '테크닉'} class:description-stat-sense={segment.icon === '센스'} class:attribute-cute={segment.attribute === 'cute'} class:attribute-pure={segment.attribute === 'pure'} class:attribute-happy={segment.attribute === 'happy'} class:keyword-with-icon={segment.icon}>
{#if segment.icon && typeIconPaths[segment.icon as CardType]}<img class="description-type-icon" src={typeIconPaths[segment.icon as CardType]} alt="" aria-hidden="true" />{/if}
{#if segment.icon && statOptions.includes(segment.icon as HighestStat)}<span class="description-stat-icon stat-icon" class:stat-icon-performance={segment.icon === '퍼포먼스'} class:stat-icon-technique={segment.icon === '테크닉'} class:stat-icon-sense={segment.icon === '센스'} aria-hidden="true"></span>{/if}
{segment.text}
</span>
{/each}
</p>
{:else}
<p class="skill-description empty-effect">의상 효과 없음</p>
{/if}
</section>
</div>
</div>
{/if}
</article>
{/each}
</div>
{/if}
</section>
</main>
<style>
:global(*) {
box-sizing: border-box;
}
:global(html) {
background: #f5f7fb;
}
:global(body) {
margin: 0;
min-width: 320px;
min-height: 100vh;
color: #1d2433;
background:
radial-gradient(circle at 8% 0%, rgb(255 213 232 / 48%), transparent 26rem),
radial-gradient(circle at 94% 4%, rgb(193 233 255 / 52%), transparent 28rem),
#f5f7fb;
font-family: Pretendard, "Noto Sans KR", "Apple SD Gothic Neo", "Segoe UI", sans-serif;
}
main {
width: min(var(--page-max-width), calc(100% - var(--page-gutter)));
margin: 0 auto;
padding: var(--page-content-top) 0 80px;
}
.page-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: 36px;
}
h1 {
margin: 0;
font-size: var(--page-title-size);
line-height: 1.15;
letter-spacing: -0.045em;
}
.description {
margin: 10px 0 0;
color: #687185;
font-size: var(--page-description-size);
line-height: 1.6;
}
.total {
flex: 0 0 auto;
margin: 0;
padding: 11px 17px;
border: 1px solid rgb(255 255 255 / 72%);
border-radius: 999px;
color: #566074;
background: rgb(255 255 255 / 68%);
box-shadow: 0 8px 30px rgb(55 72 105 / 8%);
backdrop-filter: blur(12px);
}
.total strong {
margin-right: 3px;
color: #20283a;
font-size: 18px;
}
.total span {
color: #8a92a2;
}
.header-side {
display: grid;
justify-items: end;
gap: 9px;
}
.filters {
margin-bottom: 20px;
padding: 22px 24px 24px;
border: 1px solid rgb(255 255 255 / 72%);
border-radius: 20px;
background: rgb(255 255 255 / 72%);
box-shadow: 0 14px 42px rgb(55 72 105 / 8%);
backdrop-filter: blur(14px);
}
.filter-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 19px;
}
h2 {
margin: 0;
font-size: 17px;
letter-spacing: -0.025em;
}
.filter-actions {
display: flex;
gap: 8px;
}
.filter-action {
padding: 7px 12px;
border: 1px solid #dce1ea;
border-radius: 9px;
color: #566074;
background: #fff;
font: inherit;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: border-color 150ms ease, color 150ms ease, background 150ms ease;
}
.filter-action:hover:not(:disabled) {
border-color: #ef8bb5;
color: #d83f82;
background: #fff8fb;
}
.filter-action:disabled {
opacity: 0.42;
cursor: default;
}
.filter-groups {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 20px;
}
.generation-filter {
grid-column: 1 / -1;
}
.filter-groups > fieldset:not(.generation-filter) .filter-options {
flex-direction: column;
align-items: stretch;
}
.filter-groups > fieldset:not(.generation-filter) .filter-options label {
width: 100%;
}
fieldset {
min-width: 0;
margin: 0;
padding: 0;
border: 0;
}
legend {
margin-bottom: 10px;
padding: 0;
color: #737d90;
font-size: 14px;
font-weight: 750;
}
.filter-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.filter-options label {
display: inline-flex;
align-items: center;
gap: 7px;
min-height: 34px;
padding: 6px 11px 6px 8px;
border: 1px solid #e0e4ec;
border-radius: 10px;
color: #7a8394;
background: #fafbfc;
font-size: 13px;
font-weight: 700;
cursor: pointer;
user-select: none;
transition: border-color 150ms ease, color 150ms ease, background 150ms ease, box-shadow 150ms ease;
}
.filter-options label:hover {
border-color: #cbd2df;
background: #fff;
}
.filter-options label.checked {
border-color: #f3a5c6;
color: #c93475;
background: #fff3f8;
box-shadow: inset 0 0 0 1px rgb(239 85 150 / 7%);
}
.filter-options input {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.checkmark {
display: grid;
width: 18px;
height: 18px;
place-items: center;
border: 1px solid #cfd5df;
border-radius: 6px;
color: transparent;
background: #fff;
font-size: 13px;
line-height: 1;
}
label.checked .checkmark {
border-color: #ef5596;
color: #fff;
background: #ef5596;
}
.filter-options input:focus-visible + .checkmark {
outline: 3px solid rgb(73 134 231 / 25%);
outline-offset: 2px;
}
.table-panel {
overflow-x: auto;
overflow-y: hidden;
border: 1px solid #e3e7ef;
border-radius: 22px;
background: rgb(255 255 255 / 90%);
box-shadow: 0 22px 60px rgb(55 72 105 / 11%);
backdrop-filter: blur(16px);
}
.list-header,
.card-summary {
display: grid;
grid-template-columns: 62px 72px 110px minmax(160px, 1fr) 66px 66px 66px minmax(160px, 1.15fr) 190px 32px;
align-items: center;
}
.list-header {
min-width: 1260px;
min-height: 54px;
padding: 0 24px;
border-bottom: 1px solid #e8ebf1;
color: #7b8496;
background: #fafbfc;
font-size: 13px;
font-weight: 700;
}
.list-header .sort-button {
display: inline-flex;
align-items: center;
justify-content: flex-start;
gap: 5px;
width: fit-content;
padding: 6px 7px;
border: 0;
border-radius: 7px;
color: inherit;
background: transparent;
font: inherit;
cursor: pointer;
transition: color 150ms ease, background 150ms ease;
}
.list-header .sort-button:hover,
.list-header .sort-button.active {
color: #d83f82;
background: #fff0f7;
}
.list-header .sort-button:focus-visible {
outline: 3px solid rgb(73 134 231 / 25%);
outline-offset: 1px;
}
.sort-indicator {
color: #a4adbb;
font-size: 12px;
}
.list-header .sort-button.active .sort-indicator {
color: currentcolor;
}
.card-entry {
min-width: 1260px;
border-bottom: 1px solid #edf0f5;
}
.card-entry:last-child {
border-bottom: 0;
}
.card-entry.expanded {
background: #fcfdff;
}
.card-summary {
position: relative;
width: 100%;
min-height: 58px;
padding: 0 24px;
border: 0;
color: #343d50;
background: transparent;
font: inherit;
font-size: 15px;
text-align: left;
transition: background 150ms ease;
}
.summary-expand-button {
position: absolute;
z-index: 1;
inset: 0;
width: 100%;
border: 0;
background: transparent;
cursor: pointer;
}
.card-summary:hover,
.card-summary:has(.summary-expand-button:focus-visible) {
background: #fafbff;
}
.card-summary:has(.summary-expand-button:focus-visible) {
outline: 3px solid rgb(73 134 231 / 23%);
outline-offset: -3px;
}
.summary-cell {
position: relative;
z-index: 2;
min-width: 0;
pointer-events: none;
}
.name-cell {
display: flex;
align-items: center;
gap: 10px;
}
.active-summary-cell {
padding-right: 8px;
}
.active-value,
.active-effect {
color: #4d586d;
font-size: 13px;
font-weight: 750;
font-variant-numeric: tabular-nums;
}
.active-effect {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.level-cell {
display: flex;
justify-content: flex-end;
}
.header-level-control {
display: flex;
align-items: center;
gap: 9px;
}
.header-level-toggle {
flex: 0 0 auto;
}
.skill-level-label {
padding: 4px 9px;
border-radius: 999px;
color: #b43d72;
background: #fff0f7;
font-size: 12px;
font-weight: 800;
}
.mobile-label {
display: none;
}
.chevron {
position: relative;
z-index: 2;
width: 10px;
height: 10px;
justify-self: end;
border-right: 2px solid #9aa2b1;
border-bottom: 2px solid #9aa2b1;
transform: rotate(45deg) translate(-2px, -2px);
transition: transform 180ms ease, border-color 180ms ease;
}
.expanded .chevron {
border-color: #df4b8a;
transform: rotate(225deg) translate(-1px, -1px);
}
.card-details {
padding: 24px;
border-top: 1px solid #e7eaf1;
background:
linear-gradient(135deg, rgb(255 246 250 / 90%), rgb(247 251 255 / 92%)),
#fafcff;
}
.level-toggle {
display: inline-flex;
flex: 0 0 auto;
padding: 1px;
border: 1px solid #dfe3eb;
border-radius: 9px;
background: #fff;
}
.level-toggle button {
min-width: 50px;
padding: 3px 7px;
border: 0;
border-radius: 7px;
color: #7a8394;
background: transparent;
font: inherit;
font-size: 13px;
font-weight: 750;
line-height: 1;
cursor: pointer;
}
.level-toggle button.active {
color: #fff;
background: #e64f8f;
box-shadow: 0 4px 12px rgb(230 79 143 / 22%);
}
.level-toggle button:focus-visible {
outline: 3px solid rgb(73 134 231 / 25%);
outline-offset: 2px;
}
.skill-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.skill-card {
min-width: 0;
padding: 18px;
border: 1px solid #e3e7ef;
border-radius: 15px;
background: rgb(255 255 255 / 88%);
}
.skill-card header {
display: flex;
align-items: center;
gap: 9px;
margin-bottom: 13px;
}
.skill-symbol {
display: grid;
width: 27px;
height: 27px;
place-items: center;
border-radius: 8px;
color: #fff;
font-size: 13px;
font-weight: 850;
}
.skill-card h4 {
margin: 0;
color: #343d50;
font-size: 14px;
}
.condition-badge {
margin-left: auto;
padding: 4px 8px;
border-radius: 999px;
color: #3165c9;
background: #edf3ff;
font-size: 12px;
font-weight: 850;
}
.costume-card .skill-symbol {
background: #d99a27;
}
.skill-spec {
display: grid;
margin: -3px 0 11px;
overflow: hidden;
border: 1px solid #dfe5ef;
border-radius: 9px;
background: #f7f9fc;
}
.active-spec {
grid-template-columns: repeat(3, 1fr);
}
.special-spec {
grid-template-columns: 1fr;
}
.skill-spec div {
display: grid;
grid-template-columns: auto auto;
align-items: baseline;
justify-content: center;
gap: 6px;
min-width: 0;
padding: 7px 8px;
}
.skill-spec div + div {
border-left: 1px solid #dfe5ef;
}
.skill-spec dt,
.skill-spec dd {
margin: 0;
}
.skill-spec dt {
color: #7b8495;
font-size: 12px;
font-weight: 700;
}
.skill-spec dd {
color: #34445f;
font-size: 13px;
font-weight: 850;
font-variant-numeric: tabular-nums;
}
.effect-breakdown {
display: grid;
gap: 8px;
}
.effect-block {
padding: 10px 11px;
border: 1px solid #e7eaf0;
border-radius: 10px;
background: #f8f9fc;
}
.condition-effect {
border-color: #dfe8fb;
background: #f5f8ff;
}
.result-effect {
border-color: #f4d8e5;
background: #fff7fa;
}
.effect-label {
margin: 0 0 4px;
color: #8a93a4;
font-size: 12px;
font-weight: 800;
}
.condition-effect .effect-label {
color: #5376b8;
}
.result-effect .effect-label {
color: #c24a7c;
}
.skill-description {
margin: 0;
line-height: 1.7;
}
.skill-description {
color: #505a6e;
font-size: 14px;
}
.keyword-with-icon {
white-space: nowrap;
}
.semantic-bold {
color: #30394c;
font-weight: 800;
}
.numeric-value {
padding: 0 2px;
color: #30394c;
font-weight: 850;
font-variant-numeric: tabular-nums;
background: linear-gradient(transparent 62%, rgb(48 57 76 / 18%) 0);
}
.target-chip {
margin: 0 2px;
padding: 1px 5px;
border: 1px solid #dbe3f0;
border-radius: 5px;
color: #405678;
background: #f0f4fa;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
white-space: nowrap;
}
.description-stat-term {
display: inline-flex;
align-items: center;
gap: 2px;
margin: 0 2px;
padding: 1px 5px 1px 3px;
border-radius: 5px;
line-height: 1.45;
vertical-align: -2px;
white-space: nowrap;
}
.description-stat-performance {
color: #b94358;
background: #fff0f2;
}
.description-stat-technique {
color: #4167b5;
background: #eef3ff;
}
.description-stat-sense {
color: #7048ad;
background: #f5efff;
}
.description-type-icon,
.description-stat-icon {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 3px;
vertical-align: -3px;
}
.description-type-icon {
object-fit: contain;
}
.description-stat-term .description-stat-icon {
margin-right: 0;
vertical-align: 0;
}
.empty-effect {
color: #969dab;
}
.highlight {
padding: 0 2px;
color: #30394c;
font-weight: 850;
background: linear-gradient(transparent 65%, rgb(230 79 143 / 24%) 0);
}
.numeric-value.highlight {
color: #30394c;
background: linear-gradient(transparent 62%, rgb(230 79 143 / 28%) 0);
}
.attribute-cute,
.attribute-pure,
.attribute-happy {
font-weight: 800;
}
.attribute-cute {
color: #df3f84;
}
.attribute-pure {
color: #258f9a;
}
.attribute-happy {
color: #c57b10;
}
.empty {
display: grid;
height: 180px;
place-items: center;
color: #858e9f;
text-align: center;
}
.stat-badge {
display: inline-flex;
align-items: center;
min-height: 26px;
border-radius: 999px;
font-size: 13px;
font-weight: 750;
}
.type-icon {
display: block;
width: 26px;
height: 26px;
object-fit: contain;
}
.card-name {
color: #20283a;
font-weight: 700;
}
.generation-name {
color: #6f788a;
font-size: 13px;
font-weight: 750;
}
.card-identity {
display: inline-flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.version-badge {
flex: 0 0 auto;
padding: 2px 7px;
border-radius: 999px;
color: #287f91;
background: #e8f8fb;
font-size: 12px;
font-weight: 800;
}
.stat-badge {
padding: 3px 9px;
}
.stat-icon {
display: inline-block;
width: 19px;
height: 19px;
background-color: currentcolor;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
.stat-icon-performance {
color: #d95368;
mask-image: url('/icons/stats/performance.webp');
}
.stat-icon-technique {
color: #416fd1;
mask-image: url('/icons/stats/technique.webp');
}
.stat-icon-sense {
color: #8057c7;
mask-image: url('/icons/stats/sense.webp');
}
.stat-performance {
color: #d95368;
background: #fff0f2;
}
.stat-technique {
color: #416fd1;
background: #eef3ff;
}
.stat-sense {
color: #8057c7;
background: #f5efff;
}
/* Default dark theme */
:global(html) {
color-scheme: dark;
background: #0d1119;
}
:global(body) {
color: #e8ebf2;
background:
radial-gradient(circle at 8% 0%, rgb(230 79 143 / 14%), transparent 28rem),
radial-gradient(circle at 94% 4%, rgb(65 111 209 / 13%), transparent 30rem),
#0d1119;
}
.description,
.total,
legend,
.skill-spec dt,
.effect-label,
.empty {
color: #969fb0;
}
.total {
border-color: rgb(255 255 255 / 9%);
background: rgb(25 31 43 / 76%);
box-shadow: 0 8px 30px rgb(0 0 0 / 22%);
}
.total strong,
.card-name,
.skill-card h4,
.semantic-bold,
.numeric-value,
.highlight,
.numeric-value.highlight {
color: #edf0f6;
}
.generation-name {
color: #aab3c3;
}
.total span {
color: #747e90;
}
.filters {
border-color: rgb(255 255 255 / 8%);
background: rgb(22 28 39 / 84%);
box-shadow: 0 14px 42px rgb(0 0 0 / 24%);
}
.filter-action {
border-color: #343c4b;
color: #b5bdcb;
background: #1d2430;
}
.filter-action:hover:not(:disabled) {
border-color: #c85b8b;
color: #f08db7;
background: #2a1c27;
}
.filter-options label {
border-color: #303746;
color: #a4adbc;
background: #181e29;
}
.filter-options label:hover {
border-color: #465064;
background: #202735;
}
.filter-options label.checked {
border-color: #a94c75;
color: #f08db7;
background: #2a1b26;
box-shadow: inset 0 0 0 1px rgb(239 85 150 / 10%);
}
.checkmark {
border-color: #485163;
background: #111722;
}
.table-panel {
border-color: #2b3240;
background: rgb(22 27 37 / 94%);
box-shadow: 0 22px 60px rgb(0 0 0 / 30%);
}
.list-header {
border-color: #2b3240;
color: #8993a5;
background: #131923;
}
.list-header .sort-button:hover,
.list-header .sort-button.active {
color: #f08db7;
background: #2a1b26;
}
.active-value,
.active-effect {
color: #c2cad8;
}
.skill-level-label {
color: #f08db7;
background: #2a1b26;
}
.card-entry {
border-color: #282f3d;
}
.card-entry.expanded {
background: #191f2a;
}
.card-summary {
color: #cbd1dc;
}
.card-summary:hover,
.card-summary:has(.summary-expand-button:focus-visible) {
background: #1d2431;
}
.chevron {
border-color: #737d8f;
}
.card-details {
border-color: #303746;
background:
linear-gradient(135deg, rgb(47 27 40 / 62%), rgb(22 30 44 / 74%)),
#151b25;
}
.level-toggle {
border-color: #394252;
background: #111722;
}
.level-toggle button {
color: #9ca6b7;
}
.skill-card {
border-color: #343c4b;
background: rgb(27 33 45 / 92%);
}
.condition-badge {
color: #8fb1f5;
background: #202d47;
}
.skill-spec {
border-color: #343d4d;
background: #141a24;
}
.skill-spec div + div {
border-color: #343d4d;
}
.skill-spec dd {
color: #dbe1ec;
}
.effect-block {
border-color: #353d4c;
background: #171d28;
}
.condition-effect {
border-color: #344564;
background: #182235;
}
.result-effect {
border-color: #5a3347;
background: #291a24;
}
.condition-effect .effect-label {
color: #86a7e8;
}
.result-effect .effect-label {
color: #e184ad;
}
.skill-description {
color: #b9c1cf;
}
.numeric-value {
background: linear-gradient(transparent 62%, rgb(255 255 255 / 20%) 0);
}
.highlight,
.numeric-value.highlight {
background: linear-gradient(transparent 62%, rgb(230 79 143 / 42%) 0);
}
.target-chip {
border-color: #465268;
color: #c4d0e3;
background: #242d3b;
}
.description-stat-performance,
.stat-performance {
color: #f08091;
background: #3a2028;
}
.description-stat-technique,
.stat-technique {
color: #82a3ef;
background: #1f2b48;
}
.description-stat-sense,
.stat-sense {
color: #b896ef;
background: #302447;
}
.version-badge {
color: #7dd2df;
background: #17343b;
}
.empty-effect {
color: #778193;
}
@media (max-width: 1100px) {
.list-header,
.card-summary {
grid-template-columns: 62px 72px 110px minmax(160px, 1fr) 66px 66px 66px minmax(160px, 1.15fr) 190px 32px;
}
}
@media (max-width: 680px) {
main {
width: min(100% - 24px, 1120px);
padding: var(--page-content-top) 0 48px;
}
.page-header {
align-items: flex-start;
flex-direction: column;
}
.description {
display: none;
}
.total {
margin: 0;
}
.header-side {
width: 100%;
justify-items: start;
}
.filters {
padding: 18px;
border-radius: 18px;
}
.filter-groups {
grid-template-columns: 1fr;
gap: 18px;
}
.table-panel {
border-radius: 18px;
}
.list-header {
display: none;
}
.card-list {
padding: 5px 16px;
}
.card-entry {
min-width: 0;
padding: 0;
}
.card-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
min-width: 0;
gap: 14px 12px;
padding: 19px 0;
padding-right: 32px;
position: relative;
}
.card-summary:hover,
.card-summary:has(.summary-expand-button:focus-visible) {
background: transparent;
}
.summary-cell {
display: block;
}
.mobile-label {
display: block;
margin-bottom: 7px;
color: #9299a8;
font-size: 13px;
font-weight: 700;
}
.name-cell {
display: flex;
align-items: center;
flex-wrap: wrap;
grid-column: 1 / -1;
}
.active-summary-cell {
padding-right: 0;
}
.active-effect-cell {
grid-column: 1 / -1;
}
.active-effect {
overflow: visible;
text-overflow: clip;
white-space: normal;
}
.level-cell {
display: flex;
justify-content: flex-end;
grid-column: 1 / -1;
}
.level-cell:empty {
display: none;
}
.card-name {
font-size: 17px;
}
.chevron {
position: absolute;
top: 50%;
right: 5px;
}
.card-details {
margin: 0 -16px;
padding: 20px 16px 22px;
}
.skill-grid {
grid-template-columns: 1fr;
}
.skill-card {
padding: 16px;
}
}
</style>