<script setup lang="ts">
import { formatServerDateTime } from '@sammo-ts/common';
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';

import { useToast } from '../composables/useToast';
import DefaultLayout from '../layouts/DefaultLayout.vue';
import { createGameTrpc } from '../utils/gameTrpc';
import { trpc } from '../utils/trpc';
import { sealPassword } from '../utils/passwordEnvelope';

type Account = Awaited<ReturnType<typeof trpc.account.get.query>>;
type IconSyncProfile = Awaited<ReturnType<typeof trpc.account.changeIcon.mutate>>['profiles'][number];
type IconSyncState = 'idle' | 'pending' | 'success' | 'error';
type IconSyncRow = IconSyncProfile & {
    selected: boolean;
    state: IconSyncState;
    errorMessage: string;
};

const router = useRouter();
const account = ref<Account | null>(null);
const loading = ref(true);
const busy = ref(false);
const errorMessage = ref('');
const successMessage = ref('');
const { success: showSuccessToast, error: showErrorToast } = useToast();

watch(successMessage, (value) => value && showSuccessToast(value), { flush: 'sync' });
watch(errorMessage, (value) => value && showErrorToast(value), { flush: 'sync' });
const currentPassword = ref('');
const newPassword = ref('');
const newPasswordConfirm = ref('');
const deletePassword = ref('');
const iconData = ref('');
const iconFilename = ref('');
const iconServerModalOpen = ref(false);
const iconServerBusy = ref(false);
const iconServerStaticFeedback = ref(false);
const iconServerMessage = ref('');
const iconServerRows = ref<IconSyncRow[]>([]);
const iconServerDialog = ref<HTMLElement | null>(null);
let iconServerReturnFocus: HTMLElement | null = null;
let previousBodyOverflow = '';
let iconServerStaticTimer: ReturnType<typeof setTimeout> | null = null;

const sessionToken = (): string | null => window.localStorage.getItem('sammo-session-token');

const gradeLabel = computed(() => {
    if (!account.value) return '-';
    if (account.value.roles.some((role) => role.includes('admin') || role === 'superuser')) return '관리자';
    return '일반회원';
});

const selectedIconServerCount = computed(
    () => iconServerRows.value.filter((row) => row.selected && row.state !== 'success').length
);
const failedIconServerCount = computed(() => iconServerRows.value.filter((row) => row.state === 'error').length);

const runAction = async (action: () => Promise<void>): Promise<void> => {
    if (busy.value) return;
    busy.value = true;
    errorMessage.value = '';
    successMessage.value = '';
    try {
        await action();
    } catch (error) {
        errorMessage.value = error instanceof Error ? error.message : '요청을 처리하지 못했습니다.';
    } finally {
        busy.value = false;
    }
};

const loadAccount = async (): Promise<void> => {
    const token = sessionToken();
    if (!token) {
        await router.replace('/');
        return;
    }
    try {
        account.value = await trpc.account.get.query({ sessionToken: token });
    } catch (error) {
        errorMessage.value = error instanceof Error ? error.message : '계정 정보를 불러오지 못했습니다.';
    } finally {
        loading.value = false;
    }
};

const changePassword = async (): Promise<void> => {
    if (newPassword.value !== newPasswordConfirm.value) {
        errorMessage.value = '새 비밀번호 확인이 일치하지 않습니다.';
        return;
    }
    await runAction(async () => {
        const token = sessionToken();
        if (!token) throw new Error('로그인이 필요합니다.');
        const [currentCredential, newCredential] = await Promise.all([
            sealPassword(currentPassword.value),
            sealPassword(newPassword.value),
        ]);
        await trpc.account.changePassword.mutate({
            sessionToken: token,
            currentCredential,
            newCredential,
        });
        currentPassword.value = '';
        newPassword.value = '';
        newPasswordConfirm.value = '';
        successMessage.value = '비밀번호를 변경했습니다.';
    });
};

const disallowThirdPartyUse = async (): Promise<void> => {
    await runAction(async () => {
        const token = sessionToken();
        if (!token) throw new Error('로그인이 필요합니다.');
        await trpc.account.disallowThirdPartyUse.mutate({ sessionToken: token });
        if (account.value) account.value = { ...account.value, thirdPartyUse: false };
        successMessage.value = '개인정보 제3자 제공 동의를 철회했습니다.';
    });
};

const scheduleDeletion = async (): Promise<void> => {
    if (!window.confirm('탈퇴를 신청하면 현재 세션이 종료됩니다. 계속하시겠습니까?')) return;
    await runAction(async () => {
        const token = sessionToken();
        if (!token) throw new Error('로그인이 필요합니다.');
        const currentCredential = await sealPassword(deletePassword.value);
        const result = await trpc.account.scheduleDeletion.mutate({
            sessionToken: token,
            currentCredential,
        });
        window.localStorage.removeItem('sammo-session-token');
        successMessage.value = `${formatServerDateTime(result.deleteAfter, { format: 'date' })}까지 정보가 보존됩니다.`;
        await router.replace('/');
    });
};

const focusIconServerModal = async (preferDialog = false): Promise<void> => {
    await nextTick();
    if (preferDialog) {
        iconServerDialog.value?.focus();
        return;
    }
    const target =
        iconServerDialog.value?.querySelector<HTMLElement>('input:not(:disabled)') ??
        iconServerDialog.value?.querySelector<HTMLElement>('button:not(:disabled)');
    (target ?? iconServerDialog.value)?.focus();
};

const handleIconServerFocusIn = (event: FocusEvent): void => {
    if (!iconServerModalOpen.value || !iconServerDialog.value) return;
    if (event.target instanceof Node && iconServerDialog.value.contains(event.target)) return;
    void focusIconServerModal(iconServerBusy.value);
};

const openIconServerModal = (profiles: IconSyncProfile[], returnFocus?: HTMLElement | null): void => {
    iconServerReturnFocus =
        returnFocus ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null);
    iconServerRows.value = profiles.map((profile) => ({
        ...profile,
        selected: true,
        state: 'idle',
        errorMessage: '',
    }));
    iconServerMessage.value = profiles.length === 0 ? '현재 아이콘을 적용할 수 있는 실행 중인 서버가 없습니다.' : '';
    iconServerModalOpen.value = true;
    previousBodyOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    document.addEventListener('focusin', handleIconServerFocusIn);
    void focusIconServerModal(true);
};

const closeIconServerModal = (): void => {
    if (iconServerBusy.value) return;
    iconServerModalOpen.value = false;
    document.removeEventListener('focusin', handleIconServerFocusIn);
    document.body.style.overflow = previousBodyOverflow;
    const returnFocus = iconServerReturnFocus;
    iconServerReturnFocus = null;
    void nextTick(() => returnFocus?.focus());
};

const showIconServerStaticFeedback = async (): Promise<void> => {
    if (iconServerStaticTimer) {
        clearTimeout(iconServerStaticTimer);
    }
    iconServerStaticFeedback.value = false;
    await nextTick();
    iconServerStaticFeedback.value = true;
    iconServerStaticTimer = setTimeout(() => {
        iconServerStaticFeedback.value = false;
        iconServerStaticTimer = null;
    }, 300);
};

const handleIconServerModalKeydown = (event: KeyboardEvent): void => {
    if (event.key === 'Escape') {
        event.preventDefault();
        void showIconServerStaticFeedback();
        return;
    }
    if (event.key !== 'Tab' || !iconServerDialog.value) return;
    const focusable = Array.from(
        iconServerDialog.value.querySelectorAll<HTMLElement>('input:not(:disabled), button:not(:disabled)')
    );
    if (focusable.length === 0) {
        event.preventDefault();
        iconServerDialog.value.focus();
        return;
    }
    const first = focusable[0];
    const last = focusable.at(-1);
    if (!first || !last) return;
    if (!iconServerDialog.value.contains(document.activeElement)) {
        event.preventDefault();
        (event.shiftKey ? last : first).focus();
    } else if (
        event.shiftKey &&
        (document.activeElement === first || document.activeElement === iconServerDialog.value)
    ) {
        event.preventDefault();
        last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
    }
};

const syncIconToServer = async (row: IconSyncRow, token: string): Promise<void> => {
    row.state = 'pending';
    row.errorMessage = '';
    try {
        const issued = await trpc.auth.issueGameSession.mutate({
            sessionToken: token,
            profile: row.profileName,
        });
        const publicGameTrpc = createGameTrpc(row.profile, row.apiPort);
        const exchanged = await publicGameTrpc.auth.exchangeGatewayToken.mutate({
            gatewayToken: issued.gameToken,
        });
        const gameTrpc = createGameTrpc(row.profile, row.apiPort, exchanged.accessToken);
        await gameTrpc.general.adjustIcon.mutate();
        row.state = 'success';
    } catch (error) {
        row.state = 'error';
        row.errorMessage = error instanceof Error ? error.message : '서버 적용에 실패했습니다.';
    }
};

const applyIconToSelectedServers = async (retryFailedOnly = false): Promise<void> => {
    if (iconServerBusy.value) return;
    const targets = iconServerRows.value.filter(
        (row) => row.selected && row.state !== 'success' && (!retryFailedOnly || row.state === 'error')
    );
    if (targets.length === 0) {
        iconServerMessage.value = retryFailedOnly ? '재시도할 실패 서버가 없습니다.' : '적용할 서버를 선택해 주세요.';
        return;
    }
    const token = sessionToken();
    if (!token) {
        iconServerMessage.value = '로그인이 필요합니다.';
        return;
    }
    iconServerBusy.value = true;
    iconServerMessage.value = '';
    await focusIconServerModal(true);
    try {
        await Promise.all(targets.map((row) => syncIconToServer(row, token)));
        const failed = targets.filter((row) => row.state === 'error').length;
        iconServerMessage.value =
            failed === 0
                ? '선택한 서버에 아이콘을 적용했습니다.'
                : `${failed}개 서버에 적용하지 못했습니다. 실패한 서버만 다시 시도할 수 있습니다.`;
    } finally {
        iconServerBusy.value = false;
        await focusIconServerModal(true);
    }
};

const selectIcon = async (event: Event): Promise<void> => {
    const input = event.currentTarget as HTMLInputElement;
    const file = input.files?.[0];
    if (!file) return;
    if (file.size > 50 * 1024) {
        errorMessage.value = '아이콘 파일은 50KB 이하여야 합니다.';
        input.value = '';
        return;
    }
    iconFilename.value = file.name;
    iconData.value = await new Promise<string>((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
        reader.onerror = () => reject(new Error('아이콘 파일을 읽지 못했습니다.'));
        reader.readAsDataURL(file);
    });
};

const changeIcon = async (event?: Event): Promise<void> => {
    const returnFocus = event?.currentTarget instanceof HTMLElement ? event.currentTarget : null;
    await runAction(async () => {
        const token = sessionToken();
        if (!token) throw new Error('로그인이 필요합니다.');
        if (!iconData.value) throw new Error('아이콘 파일을 선택해주세요.');
        const result = await trpc.account.changeIcon.mutate({ sessionToken: token, imageData: iconData.value });
        if (account.value) account.value = { ...account.value, iconUrl: result.iconUrl };
        iconData.value = '';
        iconFilename.value = '';
        successMessage.value = result.flushPublished
            ? '전용 아이콘을 변경했습니다.'
            : '전용 아이콘을 변경했습니다. 로그인 갱신 알림은 지연될 수 있습니다.';
        await loadAccount();
        openIconServerModal(result.profiles, returnFocus);
    });
};

const setPreferredIcon = async (iconId: string): Promise<void> => {
    await runAction(async () => {
        const token = sessionToken();
        if (!token) throw new Error('로그인이 필요합니다.');
        await trpc.account.setPreferredIcon.mutate({ sessionToken: token, iconId });
        successMessage.value = '대표 전용 아이콘을 변경했습니다.';
        await loadAccount();
    });
};

const retireIcon = async (iconId: string): Promise<void> => {
    if (
        !window.confirm('목록에서 내린 아이콘은 다시 선택할 수 없습니다. 과거 기록의 이미지는 보존됩니다. 계속할까요?')
    ) {
        return;
    }
    await runAction(async () => {
        const token = sessionToken();
        if (!token) throw new Error('로그인이 필요합니다.');
        await trpc.account.retireIcon.mutate({ sessionToken: token, iconId });
        successMessage.value = '전용 아이콘을 목록에서 내렸습니다. 기존 URL과 과거 기록은 보존됩니다.';
        await loadAccount();
    });
};

const deleteIcon = async (event?: Event): Promise<void> => {
    const returnFocus = event?.currentTarget instanceof HTMLElement ? event.currentTarget : null;
    if (!window.confirm('아이콘을 제거할까요?')) return;
    await runAction(async () => {
        const token = sessionToken();
        if (!token) throw new Error('로그인이 필요합니다.');
        const result = await trpc.account.deleteIcon.mutate({ sessionToken: token });
        if (account.value) account.value = { ...account.value, iconUrl: null };
        successMessage.value = result.flushPublished
            ? '전용 아이콘을 제거했습니다.'
            : '전용 아이콘을 제거했습니다. 로그인 갱신 알림은 지연될 수 있습니다.';
        openIconServerModal(result.profiles, returnFocus);
    });
};

const prepareIconSync = async (event?: Event): Promise<void> => {
    const returnFocus = event?.currentTarget instanceof HTMLElement ? event.currentTarget : null;
    await runAction(async () => {
        const token = sessionToken();
        if (!token) throw new Error('로그인이 필요합니다.');
        const result = await trpc.account.prepareIconSync.query({ sessionToken: token });
        openIconServerModal(result.profiles, returnFocus);
    });
};

onMounted(() => {
    void loadAccount();
});

onBeforeUnmount(() => {
    document.removeEventListener('focusin', handleIconServerFocusIn);
    if (iconServerModalOpen.value) {
        document.body.style.overflow = previousBodyOverflow;
    }
    if (iconServerStaticTimer) {
        clearTimeout(iconServerStaticTimer);
    }
});
</script>

<template>
    <DefaultLayout>
        <div id="account-container">
            <table id="account-table" class="legacy-bg0">
                <caption class="section-title legacy-bg2">
                    계 정 관 리
                    <RouterLink class="skin-button back-button" to="/lobby">돌아가기</RouterLink>
                </caption>
                <colgroup>
                    <col class="label-column" />
                    <col span="5" />
                </colgroup>
                <thead>
                    <tr>
                        <th colspan="6" class="legacy-bg1">회 원 정 보</th>
                    </tr>
                </thead>
                <tbody v-if="account">
                    <tr>
                        <th class="legacy-bg1">ID</th>
                        <td colspan="5">{{ account.username }}</td>
                    </tr>
                    <tr>
                        <th class="legacy-bg1">닉네임</th>
                        <td colspan="5">{{ account.displayName }}</td>
                    </tr>
                    <tr>
                        <th class="legacy-bg1">등급</th>
                        <td colspan="2">{{ gradeLabel }}</td>
                        <td colspan="3">{{ account.roles.join(', ') || '-' }}</td>
                    </tr>
                    <tr>
                        <th class="legacy-bg1">가입일시</th>
                        <td colspan="2">{{ formatServerDateTime(account.createdAt) }}</td>
                        <td colspan="3">
                            개인정보 3자 제공 동의 : {{ account.thirdPartyUse ? '○' : '×' }}
                            <button
                                v-if="account.thirdPartyUse"
                                class="skin-button compact"
                                type="button"
                                :disabled="busy"
                                @click="disallowThirdPartyUse"
                            >
                                철회
                            </button>
                        </td>
                    </tr>
                    <tr>
                        <th class="legacy-bg1">인증 방식</th>
                        <td colspan="5">{{ account.oauthType }}</td>
                    </tr>
                    <tr>
                        <th class="legacy-bg1"></th>
                        <th class="legacy-bg1" colspan="2">회원 탈퇴</th>
                        <th class="legacy-bg1" colspan="3">비밀번호 변경</th>
                    </tr>
                    <tr>
                        <th class="legacy-bg1">정보<br />수정</th>
                        <td class="action-cell" colspan="2">
                            <form @submit.prevent="scheduleDeletion">
                                <label for="delete-password">현재 비밀번호</label>
                                <input
                                    id="delete-password"
                                    v-model="deletePassword"
                                    class="skin-input"
                                    type="password"
                                    autocomplete="current-password"
                                />
                                <button class="skin-button full-button" type="submit" :disabled="busy">탈퇴신청</button>
                            </form>
                        </td>
                        <td colspan="3">
                            <form class="password-form" @submit.prevent="changePassword">
                                <label for="current-password">현재 비밀번호</label>
                                <input
                                    id="current-password"
                                    v-model="currentPassword"
                                    class="skin-input"
                                    type="password"
                                    autocomplete="current-password"
                                />
                                <label for="new-password">새 비밀번호</label>
                                <input
                                    id="new-password"
                                    v-model="newPassword"
                                    class="skin-input"
                                    type="password"
                                    autocomplete="new-password"
                                />
                                <label for="confirm-password">비밀번호 확인</label>
                                <input
                                    id="confirm-password"
                                    v-model="newPasswordConfirm"
                                    class="skin-input"
                                    type="password"
                                    autocomplete="new-password"
                                />
                                <button class="skin-button full-button" type="submit" :disabled="busy">
                                    비밀번호 변경
                                </button>
                            </form>
                        </td>
                    </tr>
                    <tr>
                        <th class="legacy-bg1"></th>
                        <th class="legacy-bg1" colspan="2">현재 / 신규</th>
                        <th class="legacy-bg1" colspan="3">전용 아이콘 변경</th>
                    </tr>
                    <tr>
                        <th class="legacy-bg1">전용<br />아이콘</th>
                        <td class="icon-preview" colspan="2">
                            <img
                                v-if="account.iconUrl"
                                :src="account.iconUrl"
                                width="64"
                                height="64"
                                alt="현재 아이콘"
                            />
                            <span v-else>기본 아이콘</span>
                            <img v-if="iconData" :src="iconData" width="64" height="64" alt="새 아이콘 미리보기" />
                        </td>
                        <td class="icon-actions" colspan="3">
                            <input
                                class="skin-input filename"
                                :value="iconFilename"
                                readonly
                                aria-label="선택한 아이콘"
                            />
                            <label class="skin-button file-button">
                                찾아보기
                                <input type="file" accept=".avif,.webp,.jpg,.jpeg,.png,.gif" @change="selectIcon" />
                            </label>
                            <button
                                class="skin-button half-button"
                                type="button"
                                :disabled="busy || iconServerBusy"
                                @click="changeIcon"
                            >
                                아이콘 변경
                            </button>
                            <button
                                class="skin-button half-button"
                                type="button"
                                :disabled="busy || iconServerBusy"
                                @click="deleteIcon"
                            >
                                아이콘 제거
                            </button>
                        </td>
                    </tr>
                    <tr>
                        <th class="legacy-bg1">전콘<br />목록</th>
                        <td colspan="5">
                            <div class="account-icon-library">
                                <div v-for="icon in account.icons" :key="icon.id" class="account-icon-card">
                                    <img :src="icon.url" width="64" height="64" alt="전용 아이콘" />
                                    <span v-if="icon.picture === account.preferredPicture" class="preferred-label"
                                        >대표</span
                                    >
                                    <button
                                        v-else
                                        class="skin-button compact"
                                        type="button"
                                        :disabled="busy"
                                        @click="setPreferredIcon(icon.id)"
                                    >
                                        대표로 설정
                                    </button>
                                    <button
                                        class="skin-button compact"
                                        type="button"
                                        :disabled="busy"
                                        @click="retireIcon(icon.id)"
                                    >
                                        목록에서 내리기
                                    </button>
                                </div>
                                <span v-if="account.icons.length === 0">등록한 전용 아이콘이 없습니다.</span>
                            </div>
                            <p class="icon-policy">
                                {{ account.icons.length }} / {{ account.maxActiveIcons }}개 · 업로드는 24시간에 1회 ·
                                목록에서 내리기는 7일에 1회
                            </p>
                        </td>
                    </tr>
                </tbody>
                <tbody v-else>
                    <tr>
                        <td colspan="6" class="status-cell">{{ loading ? '불러오는 중...' : '계정 정보 없음' }}</td>
                    </tr>
                </tbody>
                <tfoot>
                    <tr>
                        <th class="legacy-bg1">도움말</th>
                        <td colspan="5" class="help-cell">
                            <p>아이콘은 64 x 64픽셀 ~ 128 x 128픽셀 사이, 50KB 이하 파일만 가능합니다.</p>
                            <p>
                                <button
                                    class="skin-button"
                                    type="button"
                                    :disabled="busy || iconServerBusy"
                                    @click="prepareIconSync"
                                >
                                    현재 아이콘 서버 적용
                                </button>
                            </p>
                            <p class="warning">탈퇴시 1개월간 정보가 보존되며, 1개월간 재가입이 불가능합니다.</p>
                        </td>
                    </tr>
                </tfoot>
            </table>
            <p v-if="successMessage" class="feedback success" role="status">{{ successMessage }}</p>
            <p v-if="errorMessage" class="feedback error" role="alert">{{ errorMessage }}</p>
        </div>
        <Teleport to="body">
            <Transition name="icon-server-modal">
                <div
                    v-if="iconServerModalOpen"
                    class="icon-server-backdrop"
                    :class="{ 'is-static': iconServerStaticFeedback }"
                    data-testid="icon-server-modal"
                    @click.self="showIconServerStaticFeedback"
                    @keydown="handleIconServerModalKeydown"
                >
                    <section
                        ref="iconServerDialog"
                        class="icon-server-dialog"
                        role="dialog"
                        tabindex="-1"
                        :aria-busy="iconServerBusy"
                        aria-modal="true"
                        aria-labelledby="icon-server-title"
                    >
                        <header class="icon-server-header">
                            <h2 id="icon-server-title">완료되었습니다.<br />새 아이콘을 적용할 서버를 선택하세요.</h2>
                            <button
                                class="icon-server-dismiss"
                                type="button"
                                aria-label="닫기"
                                data-testid="icon-server-close"
                                :disabled="iconServerBusy"
                                @click="closeIconServerModal"
                            >
                                &times;
                            </button>
                        </header>
                        <div class="icon-server-body">
                            <form class="icon-server-form" @submit.prevent="applyIconToSelectedServers(false)">
                                <label
                                    v-for="(row, index) in iconServerRows"
                                    :key="row.profileName"
                                    class="icon-server-option"
                                    :for="`icon-server-${index}`"
                                >
                                    <input
                                        :id="`icon-server-${index}`"
                                        v-model="row.selected"
                                        type="checkbox"
                                        :disabled="iconServerBusy || row.state === 'success'"
                                        :data-testid="`icon-server-option-${row.profileName}`"
                                    />
                                    <span>{{ row.korName }}</span>
                                    <span
                                        class="icon-server-result"
                                        :class="`is-${row.state}`"
                                        :data-testid="`icon-server-result-${row.profileName}`"
                                    >
                                        <template v-if="row.state === 'pending'">적용 중...</template>
                                        <template v-else-if="row.state === 'success'">적용됨</template>
                                        <template v-else-if="row.state === 'error'">
                                            실패<span v-if="row.errorMessage">: {{ row.errorMessage }}</span>
                                        </template>
                                    </span>
                                </label>
                            </form>
                            <p
                                v-if="iconServerMessage"
                                class="icon-server-message"
                                :class="{ 'is-error': failedIconServerCount > 0 }"
                                role="status"
                            >
                                {{ iconServerMessage }}
                            </p>
                        </div>
                        <footer class="icon-server-footer">
                            <button
                                class="modal-button secondary"
                                type="button"
                                :disabled="iconServerBusy"
                                @click="closeIconServerModal"
                            >
                                닫기
                            </button>
                            <button
                                v-if="failedIconServerCount > 0"
                                class="modal-button retry"
                                type="button"
                                data-testid="icon-server-retry"
                                :disabled="iconServerBusy"
                                @click="applyIconToSelectedServers(true)"
                            >
                                실패 서버 재시도
                            </button>
                            <button
                                class="modal-button primary"
                                type="button"
                                data-testid="icon-server-apply"
                                :disabled="iconServerBusy || selectedIconServerCount === 0"
                                @click="applyIconToSelectedServers(false)"
                            >
                                {{ iconServerBusy ? '적용 중...' : '서버 적용' }}
                            </button>
                        </footer>
                    </section>
                </div>
            </Transition>
        </Teleport>
    </DefaultLayout>
</template>

<style scoped>
#account-container {
    width: 550px;
    min-height: 575px;
    margin: 106px auto 30px;
    color: #fff;
    font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif;
    font-size: 14px;
}

#account-table {
    width: 100%;
    border: 1px solid gray;
    border-spacing: 0;
    table-layout: fixed;
    text-align: center;
}

.legacy-bg0 {
    background-color: #302016;
    background-image: var(--sammo-texture-walnut, url('https://sam-image.hided.net/game/back_walnut.jpg'));
}

.legacy-bg1 {
    background-color: #14241b;
    background-image: var(--sammo-texture-green, url('https://sam-image.hided.net/game/back_green.jpg'));
}

.legacy-bg2 {
    background-color: #172a52;
    background-image: var(--sammo-texture-blue, url('https://sam-image.hided.net/game/back_blue.jpg'));
}

#account-table caption {
    caption-side: top;
}

#account-table th,
#account-table td {
    border: 1px solid;
    border-color: gray #000 #000 gray;
    padding: 4px;
    font-size: 14px;
}

.label-column {
    width: 90px;
}

.section-title {
    position: relative;
    height: 50px;
    border: 1px solid gray;
    color: #fff;
    font-size: 30px;
    font-weight: 700;
    line-height: 50px;
    text-align: center;
}

.back-button {
    position: absolute;
    top: 6px;
    right: 6px;
    height: 40px;
    font-size: 14px;
    font-weight: 400;
    line-height: 38px;
}

.skin-button,
.skin-input {
    box-sizing: border-box;
    border: 1px solid;
    border-color: gray #000 #000 gray;
    background: #191919;
    color: #fff;
    font: inherit;
}

.skin-button {
    display: inline-block;
    padding: 0 4px;
    cursor: pointer;
    text-decoration: none;
}

.skin-button:hover,
.skin-button:focus {
    background: #303030;
}

.skin-button:focus-visible,
.skin-input:focus-visible {
    outline: 2px solid #f39c12;
    outline-offset: 1px;
}

.skin-button:disabled {
    cursor: default;
    opacity: 0.65;
}

.compact {
    width: 40px;
    margin-left: 10px;
}

.action-cell,
.icon-actions {
    position: relative;
}

.action-cell form {
    min-height: 76px;
}

.action-cell label {
    display: block;
}

.password-form {
    display: grid;
    grid-template-columns: 1fr 120px;
    justify-content: end;
    gap: 2px 6px;
    text-align: right;
}

.full-button {
    width: 100%;
    min-height: 26px;
}

.password-form .full-button {
    grid-column: 1 / -1;
}

.icon-preview {
    height: 72px;
}

.icon-preview img {
    width: 64px;
    height: 64px;
    object-fit: cover;
    vertical-align: middle;
}

.icon-actions {
    height: 72px;
}

.filename {
    position: absolute;
    top: 12px;
    left: 10px;
    width: 130px;
    height: 22px;
}

.file-button {
    position: absolute;
    top: 12px;
    right: 10px;
    height: 22px;
    line-height: 20px;
}

.file-button input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
}

.half-button {
    position: absolute;
    bottom: 0;
    width: 50%;
    min-height: 26px;
}

.half-button:first-of-type {
    left: 0;
}

.half-button:last-of-type {
    right: 0;
}

.help-cell {
    padding: 8px !important;
    text-align: left;
}

.help-cell p {
    margin: 0;
    line-height: 1.2;
}

.help-cell .warning {
    margin-top: 1em;
    color: #f0f;
}

.status-cell {
    height: 280px;
}

.feedback {
    margin: 8px 0;
    padding: 8px;
    border: 1px solid gray;
    background: #191919;
    text-align: center;
}

.success {
    color: #9cff9c;
}

.error {
    color: #ff9c9c;
}

.icon-server-backdrop {
    position: fixed;
    z-index: 1050;
    inset: 0;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    overflow-y: auto;
    background: rgb(0 0 0 / 50%);
    padding: 0 16px;
    transition: opacity 0.15s linear;
}

.icon-server-modal-enter-from,
.icon-server-modal-leave-to {
    opacity: 0;
}

.icon-server-dialog {
    width: 500px;
    max-width: calc(100vw - 32px);
    margin: 28px auto;
    border: 1px solid #444;
    border-radius: 8px;
    background: #303030;
    color: #fff;
    font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic';
    font-size: 16px;
    line-height: 24px;
    text-align: left;
    transform: none;
    transition: transform 0.3s ease-out;
}

.icon-server-modal-enter-from .icon-server-dialog,
.icon-server-modal-leave-to .icon-server-dialog {
    transform: translateY(-50px);
}

.icon-server-backdrop.is-static .icon-server-dialog {
    transform: scale(1.02);
}

.icon-server-header {
    position: relative;
    display: flex;
    align-items: flex-start;
    border-bottom: 1px solid #444;
    padding: 16px;
}

.icon-server-header h2 {
    width: 297px;
    flex: 0 0 297px;
    margin: 0;
    font-size: 20px;
    font-weight: 500;
    line-height: 1.5;
    white-space: nowrap;
}

.icon-server-dismiss {
    width: 26px;
    height: 30px;
    margin: auto 0 auto auto;
    border: 2px outset #fff;
    border-radius: 0;
    background: #6b6b6b;
    padding: 1px 6px;
    color: #fff;
    font: inherit;
    font-weight: 400;
    line-height: 24px;
    opacity: 1;
}

.icon-server-dismiss:disabled {
    cursor: default;
    opacity: 0.2;
}

.icon-server-body {
    padding: 16px;
}

.icon-server-form {
    display: flex;
    flex-wrap: wrap;
    gap: 7px;
}

.icon-server-option {
    display: inline-flex;
    align-items: flex-start;
    gap: 5px;
    margin-right: 7px;
    cursor: pointer;
    line-height: 24px;
}

.icon-server-option input {
    width: 16px;
    height: 16px;
    margin: 3px 0 0;
}

.icon-server-option:has(input:disabled) {
    cursor: default;
}

.icon-server-result {
    max-width: 260px;
    color: #aaa;
    font-size: 13px;
    line-height: 22px;
}

.icon-server-result.is-success {
    color: #9cff9c;
}

.icon-server-result.is-error,
.icon-server-message.is-error {
    color: #ff9c9c;
}

.icon-server-message {
    margin: 14px 0 0;
    color: #ddd;
    font-size: 14px;
}

.icon-server-footer {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    border-top: 1px solid #444;
    padding: 16px;
}

.modal-button {
    border: 1px solid transparent;
    min-height: 40px;
    border-radius: 6px;
    padding: 6px 12px;
    color: #fff;
    font: inherit;
    font-weight: 700;
    line-height: 1.5;
    white-space: nowrap;
    transition:
        color 0.15s ease-in-out,
        background-color 0.15s ease-in-out,
        border-color 0.15s ease-in-out,
        box-shadow 0.15s ease-in-out;
}

.modal-button.secondary {
    width: 54px;
    border-color: #3d3d3d;
    background: #444;
}

.modal-button.primary {
    width: 86px;
    border-color: #325172;
    background: #375a7f;
}

.modal-button.retry {
    border-color: #a65f00;
    background: #a65f00;
}

.modal-button:disabled {
    cursor: default;
    opacity: 0.65;
}

.modal-button:focus,
.icon-server-dismiss:focus,
.icon-server-option input:focus {
    outline: 0;
}

.account-icon-library {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    padding: 8px;
}

.account-icon-card {
    display: grid;
    width: 104px;
    justify-items: center;
    gap: 4px;
}

.preferred-label {
    color: #ffbf00;
    font-weight: 700;
}

.icon-policy {
    margin: 0 8px 8px;
}

@media (max-width: 600px) {
    #account-container {
        margin-left: 0;
    }

    .icon-server-backdrop {
        padding-right: 8px;
        padding-left: 8px;
    }

    .icon-server-dialog {
        max-width: calc(100vw - 16px);
        margin: 8px auto;
    }

    .icon-server-footer {
        flex-wrap: wrap;
    }

    .icon-server-header h2 {
        width: auto;
        min-width: 0;
        flex: 1 1 auto;
        white-space: normal;
    }

    .icon-server-dismiss {
        flex: 0 0 26px;
    }

    .icon-server-result,
    .icon-server-message {
        min-width: 0;
        overflow-wrap: anywhere;
    }
}
</style>
