@extends('layouts.app')

@section('title', '기획디자인')

@section('content')
<style>
/* ===== Planning Canvas Core ===== */
:root {
    --pc-sidebar: 280px;
    --pc-toolbar: 48px;
    --pc-blue: #3b82f6;
    --pc-indigo: #6366f1;
    --pc-violet: #8b5cf6;
    --pc-emerald: #10b981;
    --pc-amber: #f59e0b;
    --pc-rose: #f43f5e;
    --pc-slate: #64748b;
}

.pc-wrap {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 64px);
    background: #f8fafc;
    overflow: hidden;
}

/* Top Toolbar */
.pc-toolbar {
    height: var(--pc-toolbar);
    background: #1e293b;
    display: flex;
    align-items: center;
    padding: 0 12px;
    gap: 4px;
    flex-shrink: 0;
    z-index: 30;
}
.pc-toolbar-group {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 0 8px;
    border-right: 1px solid rgba(255,255,255,0.1);
}
.pc-toolbar-group:last-child { border-right: none; }
.pc-tb-btn {
    width: 32px; height: 32px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 6px; border: none; background: transparent;
    color: #94a3b8; cursor: pointer; transition: all 0.15s;
    font-size: 11px;
}
.pc-tb-btn:hover { background: rgba(255,255,255,0.1); color: #e2e8f0; }
.pc-tb-btn.active { background: var(--pc-blue); color: #fff; }
.pc-tb-btn svg { width: 16px; height: 16px; }
.pc-tb-title {
    flex: 1; text-align: center;
    font-size: 13px; font-weight: 600; color: #e2e8f0;
    letter-spacing: -0.3px;
}
.pc-tb-title input {
    background: transparent; border: none; color: #e2e8f0;
    font-size: 13px; font-weight: 600; text-align: center;
    outline: none; width: 300px;
}
.pc-tb-title input:focus { border-bottom: 1px solid var(--pc-blue); }
.pc-tb-badge {
    font-size: 10px; padding: 2px 6px; border-radius: 9999px;
    background: rgba(99,102,241,0.2); color: #a5b4fc; font-weight: 500;
}
.pc-tb-sep { width: 1px; height: 24px; background: rgba(255,255,255,0.1); margin: 0 4px; }

/* Body: Sidebar + Canvas */
.pc-body {
    display: flex;
    flex: 1;
    position: relative;
    min-height: 0;
    overflow: hidden;
}

/* Left Sidebar — Node Palette + Properties */
.pc-sidebar {
    width: var(--pc-sidebar);
    background: #fff;
    border-right: 1px solid #e2e8f0;
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    overflow: hidden;
    transition: width 0.2s;
}
.pc-sidebar.collapsed { width: 0; border-right: none; }
.pc-sidebar-tabs {
    display: flex; border-bottom: 1px solid #e2e8f0; flex-shrink: 0;
}
.pc-sidebar-tab {
    flex: 1; padding: 8px 0; text-align: center;
    font-size: 11px; font-weight: 600; color: #94a3b8;
    cursor: pointer; border-bottom: 2px solid transparent;
    transition: all 0.15s; background: none; border-top: none; border-left: none; border-right: none;
}
.pc-sidebar-tab.active { color: var(--pc-indigo); border-bottom-color: var(--pc-indigo); }
.pc-sidebar-collapse-btn {
    width: 28px; height: 28px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    background: none; border: none; color: #94a3b8; cursor: pointer;
    border-radius: 4px; margin-left: auto; transition: all 0.15s;
}
.pc-sidebar-collapse-btn:hover { background: #f1f5f9; color: #475569; }
.pc-sidebar-expand-btn {
    position: absolute; left: 0; top: 50%;
    transform: translateY(-50%);
    width: 16px; height: 48px; z-index: 50;
    background: #f1f5f9; border: 1px solid #e2e8f0;
    border-left: none; border-radius: 0 6px 6px 0;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; color: #94a3b8; transition: all 0.15s;
}
.pc-sidebar-expand-btn:hover { background: #e2e8f0; color: #475569; width: 20px; }
.pc-sidebar-panel {
    flex: 1; overflow-y: auto; padding: 12px;
    display: none;
}
.pc-sidebar-panel.active { display: block; }

/* Node Palette Items */
.pc-palette-section { margin-bottom: 16px; }
.pc-palette-section h4 {
    font-size: 10px; font-weight: 700; text-transform: uppercase;
    color: #94a3b8; margin-bottom: 8px; letter-spacing: 0.5px;
}
.pc-palette-grid {
    display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
}
.pc-palette-item {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: 10px 6px; border-radius: 8px; border: 1px solid #e2e8f0;
    background: #fff; cursor: grab; transition: all 0.15s;
    font-size: 10px; color: #64748b; user-select: none;
}
.pc-palette-item:hover { border-color: var(--pc-indigo); background: #f5f3ff; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(99,102,241,0.1); }
.pc-palette-item:active { cursor: grabbing; }
.pc-palette-icon {
    width: 36px; height: 36px; border-radius: 8px;
    display: flex; align-items: center; justify-content: center;
    font-size: 16px;
}

/* Canvas Area */
.pc-canvas-wrap {
    flex: 1; position: relative; overflow: hidden;
    background:
        radial-gradient(circle at 1px 1px, #e2e8f0 1px, transparent 1px);
    background-size: 24px 24px;
}
.pc-canvas {
    position: absolute;
    top: 0; left: 0;
    width: 6000px; height: 4000px;
    transform-origin: 0 0;
}

/* Canvas Nodes */
.pc-node {
    position: absolute;
    min-width: 160px;
    background: #fff;
    border-radius: 12px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.04);
    border: 2px solid transparent;
    cursor: move;
    user-select: none;
    transition: box-shadow 0.15s, border-color 0.15s;
    z-index: 10;
}
.pc-node:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
.pc-node.selected { border-color: var(--pc-indigo); box-shadow: 0 0 0 3px rgba(99,102,241,0.15), 0 4px 16px rgba(0,0,0,0.1); z-index: 20; }
.pc-node-header {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 12px 6px;
    border-radius: 12px 12px 0 0;
}
.pc-node-icon {
    width: 28px; height: 28px; border-radius: 8px;
    display: flex; align-items: center; justify-content: center;
    font-size: 14px; flex-shrink: 0;
}
.pc-node-title {
    font-size: 12px; font-weight: 700; color: #1e293b;
    flex: 1; min-width: 0;
    outline: none;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pc-node-title:focus {
    white-space: normal; word-break: break-all;
}
.pc-node-body {
    padding: 4px 12px 10px;
    font-size: 11px; color: #64748b; line-height: 1.5;
}
.pc-node-body [contenteditable] {
    outline: none; min-height: 16px;
}
.pc-node-tags {
    display: flex; flex-wrap: wrap; gap: 3px;
    padding: 0 12px 8px;
}
.pc-node-tag {
    font-size: 9px; padding: 1px 6px; border-radius: 9999px;
    background: #f1f5f9; color: #64748b; font-weight: 500;
}
.pc-node-footer {
    display: flex; align-items: center; justify-content: space-between;
    padding: 6px 12px; border-top: 1px solid #f1f5f9;
    font-size: 10px; color: #94a3b8;
}

/* Connection Ports */
.pc-port {
    position: absolute; width: 10px; height: 10px;
    background: #fff; border: 2px solid #cbd5e1; border-radius: 50%;
    cursor: crosshair; z-index: 15; transition: all 0.15s;
}
.pc-port:hover { border-color: var(--pc-indigo); background: var(--pc-indigo); transform: scale(1.3); }
.pc-port-top { top: -5px; left: 50%; margin-left: -5px; }
.pc-port-bottom { bottom: -5px; left: 50%; margin-left: -5px; }
.pc-port-left { left: -5px; top: 50%; margin-top: -5px; }
.pc-port-right { right: -5px; top: 50%; margin-top: -5px; }

/* SVG Connections */
.pc-connections {
    position: absolute; top: 0; left: 0;
    width: 100%; height: 100%;
    pointer-events: none; z-index: 5;
}
.pc-conn { stroke: #94a3b8; stroke-width: 2; fill: none; }
.pc-conn:hover { stroke: var(--pc-indigo); stroke-width: 3; }
.pc-conn-active { stroke: var(--pc-indigo); stroke-width: 2; stroke-dasharray: 6 3; }

/* Minimap */
.pc-minimap {
    position: absolute; bottom: 12px; right: 12px;
    width: 180px; height: 120px;
    background: rgba(255,255,255,0.95); border: 1px solid #e2e8f0;
    border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);
    z-index: 25; overflow: hidden;
}
.pc-minimap-viewport {
    position: absolute; border: 2px solid var(--pc-blue);
    background: rgba(59,130,246,0.05); border-radius: 2px;
}

/* Zoom Controls */
.pc-zoom-controls {
    position: absolute; bottom: 12px; left: 12px;
    display: flex; gap: 4px; z-index: 25;
}
.pc-zoom-btn {
    width: 32px; height: 32px; border-radius: 8px;
    background: #fff; border: 1px solid #e2e8f0;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; color: #64748b; font-size: 14px; font-weight: 700;
    transition: all 0.15s;
}
.pc-zoom-btn:hover { background: #f1f5f9; color: #1e293b; }
.pc-zoom-label {
    height: 32px; padding: 0 10px; border-radius: 8px;
    background: #fff; border: 1px solid #e2e8f0;
    display: flex; align-items: center; justify-content: center;
    font-size: 11px; font-weight: 600; color: #64748b;
}

/* Properties Panel */
.pc-props-empty {
    display: flex; flex-direction: column; align-items: center;
    justify-content: center; height: 100%; color: #94a3b8;
    font-size: 12px; gap: 8px;
}
.pc-prop-group { margin-bottom: 12px; }
.pc-prop-label {
    font-size: 10px; font-weight: 700; color: #94a3b8;
    text-transform: uppercase; letter-spacing: 0.5px;
    margin-bottom: 4px;
}
.pc-prop-input {
    width: 100%; padding: 6px 8px; border: 1px solid #e2e8f0;
    border-radius: 6px; font-size: 12px; outline: none;
    transition: border-color 0.15s;
}
.pc-prop-input:focus { border-color: var(--pc-indigo); }
.pc-prop-textarea {
    width: 100%; padding: 6px 8px; border: 1px solid #e2e8f0;
    border-radius: 6px; font-size: 12px; outline: none;
    resize: vertical; min-height: 60px;
}
.pc-color-swatches {
    display: flex; gap: 4px; flex-wrap: wrap;
}
.pc-color-swatch {
    width: 24px; height: 24px; border-radius: 6px;
    cursor: pointer; border: 2px solid transparent;
    transition: all 0.15s;
}
.pc-color-swatch.active { border-color: #1e293b; transform: scale(1.15); }
.pc-color-swatch:hover { transform: scale(1.1); }

/* View Mode Tabs */
.pc-view-tabs {
    display: flex; gap: 2px;
}
.pc-view-tab {
    padding: 4px 10px; border-radius: 6px;
    font-size: 11px; font-weight: 600; color: #94a3b8;
    cursor: pointer; border: none; background: transparent;
    transition: all 0.15s;
}
.pc-view-tab.active { background: rgba(255,255,255,0.15); color: #fff; }
.pc-view-tab:hover:not(.active) { color: #e2e8f0; }

/* Context Menu */
.pc-context-menu {
    position: fixed; z-index: 100;
    background: #fff; border: 1px solid #e2e8f0;
    border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.12);
    padding: 4px; min-width: 160px; display: none;
}
.pc-context-menu.show { display: block; }
.pc-cm-item {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 10px; border-radius: 6px;
    font-size: 12px; color: #374151; cursor: pointer;
    transition: background 0.1s;
}
.pc-cm-item:hover { background: #f3f4f6; }
.pc-cm-item.danger { color: #ef4444; }
.pc-cm-item.danger:hover { background: #fef2f2; }
.pc-cm-sep { height: 1px; background: #e5e7eb; margin: 4px 0; }

/* ===== Kanban Board ===== */
.pc-kanban {
    display: flex; gap: 12px; flex: 1; padding: 16px; overflow-x: auto; background: #f1f5f9;
}
.pc-kanban-col {
    min-width: 260px; width: 260px; flex-shrink: 0;
    background: #e8ecf1; border-radius: 10px; display: flex; flex-direction: column; max-height: 100%;
}
.pc-kanban-col-header {
    display: flex; align-items: center; gap: 8px; padding: 10px 12px; flex-shrink: 0;
}
.pc-kanban-col-dot {
    width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
}
.pc-kanban-col-title {
    font-size: 12px; font-weight: 700; color: #374151; flex: 1;
}
.pc-kanban-col-count {
    font-size: 10px; font-weight: 600; color: #94a3b8; background: #fff;
    padding: 1px 7px; border-radius: 9999px;
}
.pc-kanban-col-body {
    flex: 1; overflow-y: auto; padding: 4px 8px 8px; display: flex; flex-direction: column; gap: 6px;
    min-height: 60px;
}
.pc-kanban-col-body.drag-over { background: rgba(99,102,241,0.08); border-radius: 0 0 10px 10px; }
.pc-kanban-card {
    background: #fff; border-radius: 8px; padding: 10px 12px; cursor: grab;
    border: 1px solid #e2e8f0; transition: box-shadow 0.15s, border-color 0.15s;
    border-left: 3px solid var(--card-color, #6366f1);
}
.pc-kanban-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.pc-kanban-card.dragging { opacity: 0.4; }
.pc-kanban-card-title {
    font-size: 12px; font-weight: 600; color: #1e293b; margin-bottom: 4px;
    display: flex; align-items: center; gap: 6px;
}
.pc-kanban-card-title .emoji { font-size: 14px; }
.pc-kanban-card-desc {
    font-size: 11px; color: #64748b; line-height: 1.4;
    overflow: hidden; max-height: 36px; text-overflow: ellipsis;
    margin-bottom: 6px;
}
.pc-kanban-card-meta {
    display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.pc-kanban-card-tag {
    font-size: 9px; padding: 1px 6px; border-radius: 9999px;
    background: #f1f5f9; color: #64748b; font-weight: 500;
}
.pc-kanban-card-priority {
    font-size: 9px; padding: 1px 6px; border-radius: 9999px; font-weight: 600;
    margin-left: auto;
}
.pc-kanban-card-priority.high { background: #fef2f2; color: #ef4444; }
.pc-kanban-card-priority.critical { background: #fef2f2; color: #dc2626; }
.pc-kanban-card-priority.medium { background: #fffbeb; color: #d97706; }
.pc-kanban-card-priority.low { background: #f0fdf4; color: #22c55e; }

/* ===== Node Detail Modal ===== */
.pc-modal-overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 200;
    display: flex; align-items: center; justify-content: center;
}
.pc-modal {
    background: #fff; border-radius: 12px; width: 640px; max-width: 92vw;
    max-height: 85vh; overflow-y: auto; box-shadow: 0 24px 48px rgba(0,0,0,0.2);
}
.pc-modal-header {
    display: flex; align-items: center; gap: 12px; padding: 20px 24px 12px;
    border-bottom: 1px solid #f1f5f9;
}
.pc-modal-header .emoji { font-size: 24px; }
.pc-modal-header input {
    flex: 1; font-size: 18px; font-weight: 700; border: none; outline: none; color: #1e293b;
}
.pc-modal-header input::placeholder { color: #cbd5e1; }
.pc-modal-close {
    width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent;
    color: #94a3b8; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.pc-modal-close:hover { background: #f1f5f9; color: #374151; }
.pc-modal-body { padding: 16px 24px 24px; }
.pc-modal-row { display: flex; gap: 12px; margin-bottom: 14px; }
.pc-modal-label {
    width: 72px; flex-shrink: 0; font-size: 11px; font-weight: 600; color: #94a3b8;
    padding-top: 7px; text-align: right;
}
.pc-modal-field { flex: 1; }
.pc-modal-field input, .pc-modal-field select, .pc-modal-field textarea {
    width: 100%; padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 8px;
    font-size: 13px; outline: none; transition: border-color 0.15s;
}
.pc-modal-field input:focus, .pc-modal-field select:focus, .pc-modal-field textarea:focus {
    border-color: var(--pc-indigo);
}
.pc-modal-field textarea { resize: vertical; min-height: 80px; line-height: 1.6; }

/* Checklist */
.pc-checklist { list-style: none; padding: 0; margin: 0; }
.pc-checklist-item {
    display: flex; align-items: center; gap: 8px; padding: 5px 0;
    border-bottom: 1px solid #f8fafc;
}
.pc-checklist-item input[type="checkbox"] { accent-color: var(--pc-indigo); width: 15px; height: 15px; cursor: pointer; }
.pc-checklist-item .text {
    flex: 1; font-size: 13px; color: #374151; border: none; outline: none; background: transparent;
}
.pc-checklist-item .text.done { text-decoration: line-through; color: #94a3b8; }
.pc-checklist-item .remove {
    width: 20px; height: 20px; border: none; background: transparent;
    color: #cbd5e1; cursor: pointer; font-size: 14px; border-radius: 4px;
    display: flex; align-items: center; justify-content: center;
}
.pc-checklist-item .remove:hover { color: #ef4444; background: #fef2f2; }
.pc-checklist-add {
    display: flex; align-items: center; gap: 6px; padding: 6px 0;
    font-size: 12px; color: var(--pc-indigo); cursor: pointer; font-weight: 500;
}
.pc-checklist-add:hover { color: #4f46e5; }
.pc-checklist-progress {
    height: 4px; background: #e2e8f0; border-radius: 2px; overflow: hidden; margin-bottom: 8px;
}
.pc-checklist-progress-bar {
    height: 100%; background: var(--pc-emerald); border-radius: 2px; transition: width 0.3s;
}

/* ===== List/Table View ===== */
.pc-list-view {
    flex: 1; overflow: auto; background: #fff;
}
.pc-list-table {
    width: 100%; border-collapse: collapse; font-size: 12px;
}
.pc-list-table th {
    position: sticky; top: 0; background: #f8fafc; padding: 8px 12px;
    text-align: left; font-weight: 700; color: #64748b; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid #e2e8f0;
    cursor: pointer; user-select: none; white-space: nowrap;
}
.pc-list-table th:hover { color: #374151; }
.pc-list-table th .sort-icon { font-size: 10px; margin-left: 2px; }
.pc-list-table td {
    padding: 8px 12px; border-bottom: 1px solid #f1f5f9; color: #374151;
    vertical-align: middle;
}
.pc-list-table tr:hover td { background: #f8fafc; }
.pc-list-table tr.selected td { background: #eef2ff; }
.pc-list-status-badge {
    display: inline-flex; align-items: center; gap: 4px; font-size: 11px;
    padding: 2px 8px; border-radius: 9999px; font-weight: 500;
}

/* ===== Filter Bar ===== */
.pc-filter-bar {
    display: flex; align-items: center; gap: 8px; padding: 6px 16px;
    background: #fff; border-bottom: 1px solid #e2e8f0; flex-shrink: 0;
}
.pc-filter-bar select, .pc-filter-bar input {
    padding: 4px 8px; border: 1px solid #e2e8f0; border-radius: 6px;
    font-size: 11px; outline: none; background: #fff; color: #374151;
}
.pc-filter-bar select:focus, .pc-filter-bar input:focus { border-color: var(--pc-indigo); }
.pc-filter-label { font-size: 10px; font-weight: 600; color: #94a3b8; }
.pc-filter-clear {
    font-size: 11px; color: var(--pc-indigo); cursor: pointer; font-weight: 500;
    background: none; border: none; padding: 4px 8px;
}
.pc-filter-clear:hover { text-decoration: underline; }

/* ===== Storyboard View ===== */
.sb-wrap { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #e5e7eb; }
.sb-topbar {
    display: flex; align-items: center; gap: 8px; padding: 8px 16px;
    background: #fff; border-bottom: 1px solid #e2e8f0; flex-shrink: 0;
}
.sb-topbar label { font-size: 10px; font-weight: 600; color: #94a3b8; }
.sb-topbar input, .sb-topbar select {
    padding: 4px 8px; border: 1px solid #e2e8f0; border-radius: 6px;
    font-size: 12px; outline: none;
}
.sb-topbar input:focus { border-color: var(--pc-indigo); }
/* 스토리보드 전역 placeholder 스타일 */
.sb-topbar input::placeholder,
.sb-topbar select::placeholder,
.sb-editor input::placeholder,
.sb-editor textarea::placeholder,
.sb-editor select::placeholder { color: #c8d0da; font-style: italic; }
.sb-blk-text:empty::before,
.sb-blk-heading:empty::before { font-style: italic; }
.sb-topbar-sep { width: 1px; height: 24px; background: #e2e8f0; }
.sb-pages-nav {
    display: flex; align-items: center; gap: 4px;
    font-size: 12px; font-weight: 600; color: #374151;
}
.sb-pages-nav button {
    width: 28px; height: 28px; border-radius: 6px; border: 1px solid #e2e8f0;
    background: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center;
    font-size: 14px; color: #64748b;
}
.sb-pages-nav button:hover { background: #f1f5f9; }
.sb-body { flex: 1; display: flex; overflow: hidden; }
.sb-page-list {
    width: 140px; flex-shrink: 0; background: #f1f5f9; border-right: 1px solid #e2e8f0;
    overflow-y: auto; padding: 8px;
}
.sb-page-thumb {
    padding: 6px 8px; border-radius: 6px; cursor: pointer; margin-bottom: 4px;
    font-size: 10px; color: #64748b; border: 2px solid transparent;
    background: #fff; transition: all 0.15s;
}
.sb-page-thumb:hover { border-color: #cbd5e1; }
.sb-page-thumb.active { border-color: var(--pc-indigo); background: #eef2ff; color: #4338ca; }
.sb-page-thumb-num { font-weight: 700; font-size: 11px; color: #374151; }
.sb-editor { flex: 1; overflow: auto; padding: 12px; display: flex; justify-content: center; }

/* Storyboard Page (A4-like) */
.sb-page {
    width: 1100px; max-width: 100%; min-height: 750px; background: #fff; border-radius: 4px;
    box-shadow: 0 2px 12px rgba(0,0,0,0.1); display: flex; flex-direction: column;
    flex-shrink: 0; transition: width 0.2s;
}
/* 메뉴트리 패널 접기 토글 */
.sb-menu-toggle {
    width: 18px; flex-shrink: 0; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    background: #f1f5f9; border: none; color: #94a3b8;
    transition: all 0.15s; border-left: 1px solid #e2e8f0;
}
.sb-menu-toggle:hover { background: #e2e8f0; color: #475569; }
.sb-page-header {
    display: grid; grid-template-columns: 1fr auto auto auto auto auto;
    border-bottom: 2px solid #1e293b; font-size: 10px;
}
.sb-page-header > div {
    padding: 6px 10px; border-right: 1px solid #cbd5e1;
    display: flex; flex-direction: column; justify-content: center;
}
.sb-page-header > div:last-child { border-right: none; }
.sb-page-header .label { font-size: 8px; color: #94a3b8; font-weight: 600; text-transform: uppercase; }
.sb-page-header .value { font-size: 11px; font-weight: 700; color: #1e293b; }
.sb-page-header .value input {
    border: none; outline: none; font-size: 11px; font-weight: 700; color: #1e293b;
    width: 100%; background: transparent;
}
.sb-page-header .value input:focus { border-bottom: 1px solid var(--pc-indigo); }
.sb-page-body { display: flex; flex: 1; min-height: 0; }

/* Left Menu Panel */
.sb-menu-panel {
    flex-shrink: 0; border-right: none;
    padding: 12px 0; font-size: 11px; overflow-y: auto; background: #f8fafc;
}
.sb-menu-resizer {
    width: 5px; flex-shrink: 0; cursor: col-resize; background: #e2e8f0;
    transition: background .15s;
}
.sb-menu-resizer:hover, .sb-menu-resizer.active { background: #818cf8; }
.sb-menu-section { font-size: 8px; font-weight: 700; color: #94a3b8; padding: 4px 12px; text-transform: uppercase; }
.sb-menu-item {
    padding: 5px 12px 5px 16px; color: #64748b; cursor: default;
    font-size: 11px; line-height: 1.4;
}
.sb-menu-item.active { color: #4338ca; font-weight: 700; background: #eef2ff; border-right: 3px solid #4338ca; }
.sb-menu-child { padding-left: 28px; font-size: 10px; }
.sb-menu-child.active { color: #4338ca; font-weight: 700; }
.sb-menu-logo { padding: 8px 12px; font-size: 13px; font-weight: 800; color: #1e293b; letter-spacing: -0.5px; }

/* Main Content + Description */
.sb-content-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.sb-wireframe {
    flex: 1; padding: 16px; min-height: 300px; position: relative;
}
.sb-wireframe-placeholder {
    border: 2px dashed #d1d5db; border-radius: 8px; height: 100%; min-height: 280px;
    display: flex; align-items: center; justify-content: center; flex-direction: column;
    color: #94a3b8; font-size: 12px; gap: 8px; cursor: pointer; transition: all 0.15s;
}
.sb-wireframe-placeholder:hover { border-color: var(--pc-indigo); background: #fafafe; }
.sb-wireframe-content {
    width: 100%; min-height: 280px; border: 1px solid #e2e8f0; border-radius: 8px;
    padding: 16px; font-size: 13px; line-height: 1.7; color: #374151;
    outline: none; overflow: auto;
}
.sb-wireframe-content:focus { border-color: var(--pc-indigo); }
.sb-wireframe-img { max-width: 100%; border-radius: 8px; }
.sb-desc-toggle-bar {
    height: 24px; flex-shrink: 0; cursor: pointer;
    display: flex; align-items: center; justify-content: center; gap: 6px;
    background: #1e293b; color: #94a3b8; font-size: 9px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.5px; transition: background 0.15s;
}
.sb-desc-toggle-bar:hover { background: #334155; color: #e2e8f0; }
.sb-desc-toggle-label { color: inherit; }
.sb-desc-resizer {
    height: 5px; flex-shrink: 0; cursor: row-resize; background: #334155;
    transition: background .15s;
}
.sb-desc-resizer:hover, .sb-desc-resizer.active { background: #818cf8; }
.sb-desc-panel {
    padding: 12px 16px; background: #fafbfc;
    overflow-y: auto; flex-shrink: 0;
}
.sb-desc-item {
    display: flex; gap: 10px; padding: 6px 0; border-bottom: 1px solid #f1f5f9;
    font-size: 12px; line-height: 1.6;
}
.sb-desc-item:last-child { border-bottom: none; }
.sb-desc-num {
    width: 24px; height: 24px; border-radius: 50%; flex-shrink: 0;
    background: #1e293b; color: #fff; font-size: 10px; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
}
.sb-desc-text { flex: 1; color: #374151; }
.sb-desc-text textarea {
    width: 100%; border: none; outline: none; font-size: 12px; line-height: 1.6;
    color: #374151; resize: none; background: transparent; min-height: 20px;
}
.sb-desc-text textarea:focus { background: #fff; border-radius: 4px; }
.sb-desc-add {
    display: flex; align-items: center; gap: 6px; padding: 8px 0;
    font-size: 11px; color: var(--pc-indigo); cursor: pointer; font-weight: 500;
}
.sb-desc-add:hover { color: #4f46e5; }
.sb-desc-remove {
    width: 20px; height: 20px; border: none; background: transparent; color: #cbd5e1;
    cursor: pointer; font-size: 14px; border-radius: 4px; display: flex;
    align-items: center; justify-content: center; flex-shrink: 0;
}
.sb-desc-remove:hover { color: #ef4444; background: #fef2f2; }
.sb-desc-num { cursor: grab; }
.sb-desc-num:active { cursor: grabbing; }
/* Marker (description number badge on canvas) */
.sb-blk-marker {
    width: 28px; height: 28px; border-radius: 50%; background: #1e293b; color: #fff;
    font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center;
    user-select: none; cursor: move;
}

/* Block Editor (Wireframe) */
.sb-block-toolbar {
    display: flex; align-items: center; gap: 4px; padding: 6px 12px;
    border-bottom: 1px solid #e2e8f0; background: #fafbfc; flex-wrap: wrap; flex-shrink: 0;
}
.sb-block-toolbar-btn {
    padding: 3px 8px; border: 1px solid #e2e8f0; border-radius: 5px;
    font-size: 10px; cursor: pointer; background: #fff; color: #475569;
    display: flex; align-items: center; gap: 3px; white-space: nowrap;
    transition: all .12s;
}
.sb-block-toolbar-btn:hover { border-color: var(--pc-indigo); color: var(--pc-indigo); background: #eef2ff; }
.sb-block-toolbar-sep { width: 1px; height: 18px; background: #e2e8f0; margin: 0 2px; }
.sb-blocks-area {
    flex: 1; position: relative; overflow: auto; min-height: 400px;
    background: #fff url("data:image/svg+xml,%3Csvg width='20' height='20' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='1' cy='1' r='.5' fill='%23e2e8f0'/%3E%3C/svg%3E") repeat;
}
.sb-block {
    position: absolute; border: 1.5px solid transparent; border-radius: 6px;
    padding: 0; transition: border-color .1s, box-shadow .1s;
    background: #fff; cursor: move; user-select: none;
    overflow: hidden;
}
.sb-block:hover { border-color: #c7d2fe; box-shadow: 0 2px 8px rgba(99,102,241,0.08); }
.sb-block.selected { border-color: var(--pc-indigo); box-shadow: 0 0 0 2px rgba(99,102,241,0.15); }
.sb-block.sb-block-editing { cursor: default; user-select: text; overflow: visible; }
.sb-block-actions {
    position: absolute; right: 4px; top: -26px; display: flex; gap: 2px;
    opacity: 0; transition: opacity .12s; z-index: 10;
    background: #fff; border: 1px solid #e2e8f0; border-radius: 6px; padding: 2px 4px;
    box-shadow: 0 2px 6px rgba(0,0,0,0.08);
}
.sb-block:hover .sb-block-actions, .sb-block.selected .sb-block-actions { opacity: 1; }
.sb-block-action-btn {
    width: 20px; height: 20px; border: none; background: transparent; color: #94a3b8;
    border-radius: 4px; cursor: pointer; font-size: 11px; display: flex;
    align-items: center; justify-content: center;
}
.sb-block-action-btn:hover { background: #f1f5f9; color: #475569; }
.sb-block-action-btn.danger:hover { background: #fef2f2; color: #ef4444; }
/* Resize handles */
.sb-resize-handle {
    position: absolute; background: #fff; border: 1.5px solid var(--pc-indigo);
    border-radius: 2px; z-index: 5; opacity: 0; transition: opacity .1s;
}
.sb-block.selected .sb-resize-handle { opacity: 1; }
.sb-resize-handle.sb-rh-r { width: 6px; height: 20px; right: -4px; top: 50%; transform: translateY(-50%); cursor: e-resize; }
.sb-resize-handle.sb-rh-b { width: 20px; height: 6px; bottom: -4px; left: 50%; transform: translateX(-50%); cursor: s-resize; }
.sb-resize-handle.sb-rh-br { width: 10px; height: 10px; right: -5px; bottom: -5px; cursor: se-resize; border-radius: 3px; }
/* Size label */
.sb-block-size {
    position: absolute; bottom: -20px; left: 50%; transform: translateX(-50%);
    font-size: 9px; color: #94a3b8; white-space: nowrap; pointer-events: none;
    opacity: 0; transition: opacity .1s;
}
.sb-block.selected .sb-block-size { opacity: 1; }
/* Multi-select (lasso) */
.sb-block.sb-multi-selected { border-color: #f59e0b; box-shadow: 0 0 0 2px rgba(245,158,11,0.2); }
.sb-block.sb-multi-selected .sb-block-actions { opacity: 0; }
.sb-block.sb-multi-selected .sb-resize-handle { opacity: 0; }
.sb-lasso-rect {
    position: absolute; border: 1.5px dashed #6366f1; background: rgba(99,102,241,0.06);
    pointer-events: none; z-index: 100;
}

/* Floating Format Toolbar (Notion-style) */
.sb-format-bar {
    position: fixed; z-index: 9999;
    display: flex; align-items: center; gap: 2px;
    padding: 4px 6px; background: #1e293b; border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.25);
    animation: sbFormatFadeIn 0.12s ease;
}
@keyframes sbFormatFadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.sb-fmt-btn {
    width: 28px; height: 28px; border: none; background: transparent; color: #94a3b8;
    border-radius: 5px; cursor: pointer; font-size: 12px;
    display: flex; align-items: center; justify-content: center; transition: all 0.1s;
    position: relative;
}
.sb-fmt-btn:hover { background: rgba(255,255,255,0.12); color: #e2e8f0; }
.sb-fmt-btn.active { background: rgba(99,102,241,0.3); color: #a5b4fc; }
.sb-fmt-sep { width: 1px; height: 20px; background: rgba(255,255,255,0.12); margin: 0 2px; }
.sb-fmt-color-dot {
    width: 14px; height: 14px; border-radius: 50%; border: 2px solid transparent;
    cursor: pointer; transition: all 0.1s;
}
.sb-fmt-color-dot:hover { transform: scale(1.2); }
.sb-fmt-color-dot.active { border-color: #fff; }
.sb-fmt-dropdown {
    position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
    margin-top: 6px; background: #1e293b; border-radius: 8px; padding: 8px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.3); min-width: 140px; z-index: 10;
}
.sb-fmt-dropdown-title { font-size: 9px; color: #64748b; font-weight: 700; text-transform: uppercase; margin-bottom: 6px; letter-spacing: 0.5px; }
.sb-fmt-color-grid { display: flex; flex-wrap: wrap; gap: 4px; }
.sb-fmt-size-option {
    padding: 4px 8px; border-radius: 4px; font-size: 11px; color: #cbd5e1;
    cursor: pointer; transition: all 0.1s; display: flex; align-items: center; justify-content: space-between;
}
.sb-fmt-size-option:hover { background: rgba(255,255,255,0.1); }
.sb-fmt-size-option.active { background: rgba(99,102,241,0.3); color: #a5b4fc; }

/* Right-Click Context Menu */
.sb-ctx-menu {
    position: fixed; z-index: 10000;
    background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
    box-shadow: 0 8px 30px rgba(0,0,0,0.15); padding: 4px 0;
    min-width: 200px; animation: sbCtxFadeIn 0.1s ease;
}
@keyframes sbCtxFadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.sb-ctx-item {
    display: flex; align-items: center; gap: 8px; padding: 7px 14px;
    font-size: 12px; color: #334155; cursor: pointer; transition: background 0.08s;
}
.sb-ctx-item:hover { background: #f1f5f9; }
.sb-ctx-item.danger { color: #ef4444; }
.sb-ctx-item.danger:hover { background: #fef2f2; }
.sb-ctx-icon { width: 18px; text-align: center; font-size: 13px; flex-shrink: 0; }
.sb-ctx-label { flex: 1; }
.sb-ctx-shortcut { font-size: 10px; color: #94a3b8; }
.sb-ctx-sep { height: 1px; background: #e2e8f0; margin: 4px 0; }
.sb-ctx-sub { position: relative; }
.sb-ctx-sub-panel {
    position: absolute; left: 100%; top: -4px;
    background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.12); padding: 8px; min-width: 160px;
}
.sb-ctx-sub-panel .sb-ctx-item { padding: 5px 10px; }
.sb-ctx-color-grid { display: flex; flex-wrap: wrap; gap: 5px; padding: 4px; }
.sb-ctx-color-swatch {
    width: 22px; height: 22px; border-radius: 5px; cursor: pointer;
    border: 2px solid transparent; transition: all 0.1s;
}
.sb-ctx-color-swatch:hover { transform: scale(1.15); }
.sb-ctx-color-swatch.active { border-color: #4338ca; }

/* Block style inheritance — 부모 .sb-block에 설정된 color/font-size/font-weight 등을 자식에게 전달 */
.sb-block[style*="color"] .sb-blk-text,
.sb-block[style*="color"] .sb-blk-heading,
.sb-block[style*="color"] .sb-blk-callout-text,
.sb-block[style*="color"] .sb-blk-code,
.sb-block[style*="color"] .sb-blk-todo-text,
.sb-block[style*="color"] .sb-blk-mock-card-title,
.sb-block[style*="color"] .sb-blk-mock-card-body,
.sb-block[style*="color"] .sb-blk-mockup-label,
.sb-block[style*="color"] .sb-blk-table th,
.sb-block[style*="color"] .sb-blk-table td,
.sb-block[style*="color"] .sb-blk-mock-btn { color: inherit; }

.sb-block[style*="font-size"] .sb-blk-text,
.sb-block[style*="font-size"] .sb-blk-heading,
.sb-block[style*="font-size"] .sb-blk-callout-text,
.sb-block[style*="font-size"] .sb-blk-todo-text,
.sb-block[style*="font-size"] .sb-blk-mock-card-title,
.sb-block[style*="font-size"] .sb-blk-mock-card-body { font-size: inherit; }

.sb-block[style*="font-weight"] .sb-blk-text,
.sb-block[style*="font-weight"] .sb-blk-heading,
.sb-block[style*="font-weight"] .sb-blk-callout-text,
.sb-block[style*="font-weight"] .sb-blk-todo-text { font-weight: inherit; }

.sb-block[style*="font-style"] .sb-blk-text,
.sb-block[style*="font-style"] .sb-blk-heading,
.sb-block[style*="font-style"] .sb-blk-callout-text,
.sb-block[style*="font-style"] .sb-blk-todo-text { font-style: inherit; }

.sb-block[style*="text-align"] .sb-blk-text,
.sb-block[style*="text-align"] .sb-blk-heading,
.sb-block[style*="text-align"] .sb-blk-callout-text { text-align: inherit; }

/* Block type styles */
.sb-blk-text { padding: 6px 8px; font-size: 13px; line-height: 1.7; min-height: 24px; outline: none; color: #334155; }
.sb-blk-text:empty::before { content: attr(data-placeholder); color: #c8d0da; }
.sb-blk-heading { padding: 8px 8px 4px; font-size: 18px; font-weight: 700; line-height: 1.4; outline: none; color: #1e293b; }
.sb-blk-heading:empty::before { content: '제목을 입력하세요'; color: #c8d0da; }
.sb-blk-h2 { font-size: 15px; font-weight: 600; }
.sb-blk-divider { border: none; border-top: 1px solid #e2e8f0; margin: 8px 0; }
.sb-blk-callout {
    display: flex; gap: 8px; padding: 10px 12px; background: #eff6ff;
    border-radius: 6px; border-left: 3px solid #3b82f6;
}
.sb-blk-callout-icon { font-size: 16px; flex-shrink: 0; }
.sb-blk-callout-text { flex: 1; font-size: 12px; line-height: 1.6; outline: none; color: #334155; }
.sb-blk-callout-text:empty::before { content: '콜아웃 내용을 입력하세요'; color: #93c5fd; }
.sb-blk-table-wrap { overflow-x: auto; padding: 4px; }
.sb-blk-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.sb-blk-table th { background: #f1f5f9; font-weight: 600; color: #475569; text-align: left; }
.sb-blk-table th, .sb-blk-table td {
    border: 1px solid #e2e8f0; padding: 6px 8px; min-width: 60px; outline: none;
}
.sb-blk-table td:focus { background: #f8fafc; }
.sb-blk-mockup { padding: 8px; }
.sb-blk-mockup-label { font-size: 9px; color: #94a3b8; font-weight: 600; text-transform: uppercase; margin-bottom: 4px; }
.sb-blk-mock-btn {
    display: inline-block; padding: 6px 16px; border-radius: 6px; font-size: 12px;
    font-weight: 600; cursor: default;
}
.sb-blk-mock-input {
    width: 100%; padding: 7px 10px; border: 1px solid #d1d5db; border-radius: 6px;
    font-size: 12px; color: #6b7280; background: #f9fafb;
}
.sb-blk-mock-select {
    padding: 7px 10px; border: 1px solid #d1d5db; border-radius: 6px;
    font-size: 12px; color: #6b7280; background: #f9fafb;
}
.sb-blk-mock-card {
    border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; background: #fff;
}
.sb-blk-mock-card-title { font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 4px; outline: none; }
.sb-blk-mock-card-body { font-size: 11px; color: #64748b; line-height: 1.5; outline: none; }
.sb-blk-image-wrap { text-align: center; }
.sb-blk-image-wrap img { max-width: 100%; border-radius: 6px; }
.sb-blk-image-placeholder {
    border: 2px dashed #d1d5db; border-radius: 6px; padding: 24px;
    color: #94a3b8; font-size: 12px; cursor: pointer; text-align: center;
}
.sb-blk-image-placeholder:hover { border-color: var(--pc-indigo); }
.sb-blk-todo { display: flex; align-items: flex-start; gap: 6px; padding: 4px 8px; }
.sb-blk-todo input[type=checkbox] { margin-top: 4px; accent-color: var(--pc-indigo); }
.sb-blk-todo-text { flex: 1; font-size: 12px; outline: none; line-height: 1.6; }
.sb-blk-todo-text:empty::before { content: '할 일을 입력하세요'; color: #cbd5e1; }
.sb-blk-badge-row { display: flex; gap: 6px; flex-wrap: wrap; padding: 4px 8px; }
.sb-blk-badge {
    padding: 3px 10px; border-radius: 12px; font-size: 10px; font-weight: 600;
}
.sb-blk-code {
    padding: 10px 12px; background: #1e293b; border-radius: 6px; color: #e2e8f0;
    font-family: 'Fira Code', monospace; font-size: 12px; line-height: 1.6;
    outline: none; white-space: pre-wrap;
}
.sb-blk-code:empty::before { content: '코드를 입력하세요'; color: #64748b; }
.sb-blk-empty-area {
    border: 2px dashed #e2e8f0; border-radius: 8px; min-height: 200px;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    color: #94a3b8; font-size: 12px; gap: 8px; cursor: pointer; transition: all .15s;
}
.sb-blk-empty-area:hover { border-color: var(--pc-indigo); background: #fafafe; }

/* Template Dropdown */
.sb-tpl-dropdown {
    position: relative; display: inline-block;
}
.sb-tpl-trigger {
    padding: 3px 10px; border: 1px solid #e2e8f0; border-radius: 5px;
    font-size: 10px; cursor: pointer; background: #fff; color: #475569;
    display: flex; align-items: center; gap: 4px; white-space: nowrap;
    transition: all .12s;
}
.sb-tpl-trigger:hover { border-color: var(--pc-indigo); color: var(--pc-indigo); }
.sb-tpl-panel {
    position: fixed; z-index: 9990;
    width: 380px; max-height: 520px; background: #fff;
    border: 1px solid #e2e8f0; border-radius: 10px;
    box-shadow: 0 12px 32px rgba(0,0,0,0.15); display: flex; flex-direction: column;
}
.sb-tpl-tabs {
    display: flex; border-bottom: 1px solid #e2e8f0;
}
.sb-tpl-tab {
    flex: 1; padding: 8px; font-size: 11px; font-weight: 600; text-align: center;
    cursor: pointer; color: #94a3b8; border-bottom: 2px solid transparent;
    transition: all .12s;
}
.sb-tpl-tab:hover { color: #475569; }
.sb-tpl-tab.active { color: var(--pc-indigo); border-bottom-color: var(--pc-indigo); }
.sb-tpl-search {
    margin: 8px 10px 4px; padding: 6px 10px; border: 1px solid #e2e8f0;
    border-radius: 6px; font-size: 11px; outline: none; width: calc(100% - 20px);
}
.sb-tpl-search:focus { border-color: var(--pc-indigo); }
.sb-tpl-list { flex: 1; overflow-y: auto; padding: 4px 6px 8px; }
.sb-tpl-item {
    display: flex; align-items: center; gap: 8px; padding: 8px 10px;
    border-radius: 6px; cursor: pointer; transition: all .1s;
}
.sb-tpl-item:hover { background: #f1f5f9; }
.sb-tpl-item-icon {
    width: 32px; height: 32px; border-radius: 6px; background: #f1f5f9;
    display: flex; align-items: center; justify-content: center; font-size: 15px;
    flex-shrink: 0;
}
.sb-tpl-item-info { flex: 1; min-width: 0; }
.sb-tpl-item-name { font-size: 12px; font-weight: 600; color: #1e293b; }
.sb-tpl-item-desc { font-size: 10px; color: #94a3b8; margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sb-tpl-item-actions { display: flex; gap: 2px; flex-shrink: 0; }
.sb-tpl-item-btn {
    width: 22px; height: 22px; border: none; background: transparent; color: #cbd5e1;
    border-radius: 4px; cursor: pointer; font-size: 12px;
    display: flex; align-items: center; justify-content: center;
}
.sb-tpl-item-btn:hover { background: #fee2e2; color: #ef4444; }
.sb-tpl-save-bar {
    padding: 8px 10px; border-top: 1px solid #e2e8f0; display: flex; gap: 6px; align-items: center;
}
.sb-tpl-save-input {
    flex: 1; padding: 5px 8px; border: 1px solid #e2e8f0; border-radius: 5px;
    font-size: 11px; outline: none;
}
.sb-tpl-save-input:focus { border-color: var(--pc-indigo); }
.sb-tpl-save-btn {
    padding: 5px 12px; border: none; border-radius: 5px; font-size: 11px;
    font-weight: 600; cursor: pointer; background: var(--pc-indigo); color: #fff;
}
.sb-tpl-save-btn:hover { opacity: 0.9; }
.sb-tpl-empty { text-align: center; padding: 20px; color: #cbd5e1; font-size: 11px; }

/* Menu Tree Editor Modal */
.sb-menu-modal-overlay {
    position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,0.4);
    display: flex; align-items: center; justify-content: center;
}
.sb-menu-modal {
    background: #fff; border-radius: 12px; width: 480px; max-height: 80vh;
    display: flex; flex-direction: column; box-shadow: 0 25px 50px rgba(0,0,0,0.15);
}
.sb-menu-modal-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 16px 20px; border-bottom: 1px solid #e2e8f0;
}
.sb-menu-modal-header h3 { font-size: 15px; font-weight: 700; color: #1e293b; margin: 0; }
.sb-menu-modal-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
.sb-menu-modal-footer {
    display: flex; justify-content: flex-end; gap: 8px;
    padding: 12px 20px; border-top: 1px solid #e2e8f0;
}
.sb-mt-item {
    display: flex; align-items: center; gap: 6px; padding: 5px 0;
    border-bottom: 1px solid #f8fafc;
}
.sb-mt-item:hover { background: #f8fafc; border-radius: 6px; }
.sb-mt-drag {
    cursor: grab; color: #cbd5e1; flex-shrink: 0; font-size: 14px; padding: 0 2px;
    user-select: none;
}
.sb-mt-drag:active { cursor: grabbing; }
.sb-mt-icon { flex-shrink: 0; width: 18px; text-align: center; color: #94a3b8; font-size: 11px; cursor: pointer; }
.sb-mt-icon:hover { color: #475569; }
.sb-mt-name {
    flex: 1; border: none; outline: none; font-size: 13px; padding: 4px 6px;
    border-radius: 4px; color: #334155; background: transparent;
}
.sb-mt-name:focus { background: #f1f5f9; }
.sb-mt-actions { display: flex; gap: 2px; flex-shrink: 0; }
.sb-mt-btn {
    width: 24px; height: 24px; border: none; background: transparent;
    color: #94a3b8; cursor: pointer; border-radius: 4px; font-size: 13px;
    display: flex; align-items: center; justify-content: center;
}
.sb-mt-btn:hover { background: #f1f5f9; color: #475569; }
.sb-mt-btn.danger:hover { background: #fef2f2; color: #ef4444; }
.sb-mt-children { padding-left: 24px; border-left: 2px solid #e2e8f0; margin-left: 10px; }
.sb-mt-add-root {
    display: flex; align-items: center; gap: 6px; padding: 10px 0 4px;
    font-size: 12px; color: var(--pc-indigo); cursor: pointer; font-weight: 500;
}
.sb-mt-add-root:hover { color: #4f46e5; }

/* Phase Swimlane (Timeline View) */
.pc-swimlane {
    position: absolute; top: 0;
    height: 100%;
    border-left: 2px dashed rgba(99,102,241,0.2);
    padding: 8px 12px;
    pointer-events: none;
}
.pc-swimlane-label {
    font-size: 10px; font-weight: 700; color: var(--pc-indigo);
    text-transform: uppercase; letter-spacing: 1px; opacity: 0.5;
}
</style>

<div class="pc-wrap" id="planningCanvas" x-data="planningCanvas()" x-init="init()" @keydown.window="handleKeyDown($event)" @keyup.window="handleKeyUp($event)">

    {{-- ===== Top Toolbar ===== --}}
    <div class="pc-toolbar">
        {{-- Left: Tools --}}
        <div class="pc-toolbar-group">
            <button class="pc-tb-btn" :class="{ active: tool === 'select' }" @click="tool = 'select'" title="선택 (V)">
                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"/></svg>
            </button>
            <button class="pc-tb-btn" :class="{ active: tool === 'pan' }" @click="tool = 'pan'" title="이동 (H)">
                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11"/></svg>
            </button>
            <button class="pc-tb-btn" :class="{ active: tool === 'connect' }" @click="tool = 'connect'" title="연결 (C)">
                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
            </button>
        </div>

        {{-- Center: Title + View Mode --}}
        <div class="pc-tb-title">
            <input type="text" x-model="projectTitle" placeholder="프로젝트 이름을 입력하세요" spellcheck="false">
        </div>

        <div class="pc-toolbar-group">
            <div class="pc-view-tabs">
                <button class="pc-view-tab" :class="{ active: viewMode === 'free' }" @click="switchView('free')">자유배치</button>
                <button class="pc-view-tab" :class="{ active: viewMode === 'timeline' }" @click="switchView('timeline')">타임라인</button>
                <button class="pc-view-tab" :class="{ active: viewMode === 'flow' }" @click="switchView('flow')">플로우</button>
                <button class="pc-view-tab" :class="{ active: viewMode === 'kanban' }" @click="switchView('kanban')">칸반</button>
                <button class="pc-view-tab" :class="{ active: viewMode === 'list' }" @click="switchView('list')">리스트</button>
                <button class="pc-view-tab" :class="{ active: viewMode === 'storyboard' }" @click="switchView('storyboard')" style="color:#f59e0b;">스토리보드</button>
            </div>
        </div>

        {{-- Right: Actions --}}
        <div class="pc-toolbar-group">
            <button class="pc-tb-btn" @click="toggleSidebar()" title="사이드바 토글">
                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/></svg>
            </button>
            <button class="pc-tb-btn" @click="viewMode === 'storyboard' ? sbUndo() : undoAction()" title="실행 취소 (Ctrl+Z)">
                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
            </button>
            <button class="pc-tb-btn" @click="viewMode === 'storyboard' ? sbRedo() : redoAction()" title="다시 실행 (Ctrl+Y)">
                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10H11a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6"/></svg>
            </button>
            <div class="pc-tb-sep"></div>
            <button class="pc-tb-btn" @click="exportProject()" title="내보내기">
                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
            </button>
            <button class="pc-tb-btn" @click="saveProject()" title="저장 (Ctrl+S)">
                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/></svg>
            </button>
            <span class="pc-tb-badge" x-text="nodes.length + ' nodes'"></span>
        </div>
    </div>

    {{-- ===== Body ===== --}}
    <div class="pc-body">

        {{-- Sidebar Expand Button (visible when collapsed) --}}
        <button class="pc-sidebar-expand-btn" x-show="!sidebarOpen" x-cloak @click="sidebarOpen = true" title="패널 펼치기">
            <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
        </button>

        {{-- Left Sidebar --}}
        <div class="pc-sidebar" :class="{ collapsed: !sidebarOpen }">
            <div class="pc-sidebar-tabs">
                <button class="pc-sidebar-tab" :class="{ active: sidebarTab === 'palette' }" @click="sidebarTab = 'palette'">노드</button>
                <button class="pc-sidebar-tab" :class="{ active: sidebarTab === 'properties' }" @click="sidebarTab = 'properties'">속성</button>
                <button class="pc-sidebar-tab" :class="{ active: sidebarTab === 'projects' }" @click="sidebarTab = 'projects'">프로젝트</button>
                <button class="pc-sidebar-collapse-btn" @click="sidebarOpen = false" title="패널 접기">
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>
                </button>
            </div>

            {{-- Palette Panel --}}
            <div class="pc-sidebar-panel" :class="{ active: sidebarTab === 'palette' }">
                {{-- 기획 단계 --}}
                <div class="pc-palette-section">
                    <h4>기획 단계</h4>
                    <div class="pc-palette-grid">
                        <template x-for="item in paletteItems.planning" :key="item.type">
                            <div class="pc-palette-item"
                                 draggable="true"
                                 @dragstart="onPaletteDragStart($event, item)"
                                 @dblclick="addNodeAtCenter(item)">
                                <div class="pc-palette-icon" :style="'background:' + item.bg">
                                    <span x-text="item.emoji"></span>
                                </div>
                                <span x-text="item.label"></span>
                            </div>
                        </template>
                    </div>
                </div>

                {{-- 분석 도구 --}}
                <div class="pc-palette-section">
                    <h4>분석 & 설계</h4>
                    <div class="pc-palette-grid">
                        <template x-for="item in paletteItems.analysis" :key="item.type">
                            <div class="pc-palette-item"
                                 draggable="true"
                                 @dragstart="onPaletteDragStart($event, item)"
                                 @dblclick="addNodeAtCenter(item)">
                                <div class="pc-palette-icon" :style="'background:' + item.bg">
                                    <span x-text="item.emoji"></span>
                                </div>
                                <span x-text="item.label"></span>
                            </div>
                        </template>
                    </div>
                </div>

                {{-- 구조 요소 --}}
                <div class="pc-palette-section">
                    <h4>구조 & 흐름</h4>
                    <div class="pc-palette-grid">
                        <template x-for="item in paletteItems.structure" :key="item.type">
                            <div class="pc-palette-item"
                                 draggable="true"
                                 @dragstart="onPaletteDragStart($event, item)"
                                 @dblclick="addNodeAtCenter(item)">
                                <div class="pc-palette-icon" :style="'background:' + item.bg">
                                    <span x-text="item.emoji"></span>
                                </div>
                                <span x-text="item.label"></span>
                            </div>
                        </template>
                    </div>
                </div>

                {{-- 산출물 --}}
                <div class="pc-palette-section">
                    <h4>산출물</h4>
                    <div class="pc-palette-grid">
                        <template x-for="item in paletteItems.output" :key="item.type">
                            <div class="pc-palette-item"
                                 draggable="true"
                                 @dragstart="onPaletteDragStart($event, item)"
                                 @dblclick="addNodeAtCenter(item)">
                                <div class="pc-palette-icon" :style="'background:' + item.bg">
                                    <span x-text="item.emoji"></span>
                                </div>
                                <span x-text="item.label"></span>
                            </div>
                        </template>
                    </div>
                </div>
            </div>

            {{-- Properties Panel --}}
            <div class="pc-sidebar-panel" :class="{ active: sidebarTab === 'properties' }">
                <template x-if="!selectedNode">
                    <div class="pc-props-empty">
                        <svg class="w-8 h-8 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5"/>
                        </svg>
                        <span>노드를 선택하세요</span>
                    </div>
                </template>
                <template x-if="selectedNode">
                    <div>
                        <div class="pc-prop-group">
                            <div class="pc-prop-label">제목</div>
                            <input class="pc-prop-input" x-model="selectedNode.title" @input="onPropChange()">
                        </div>
                        <div class="pc-prop-group">
                            <div class="pc-prop-label">설명</div>
                            <textarea class="pc-prop-textarea" x-model="selectedNode.description" @input="onPropChange()"></textarea>
                        </div>
                        <div class="pc-prop-group">
                            <div class="pc-prop-label">상태</div>
                            <select class="pc-prop-input" x-model="selectedNode.status" @change="onPropChange()">
                                <option value="todo">대기</option>
                                <option value="progress">진행중</option>
                                <option value="review">검토중</option>
                                <option value="done">완료</option>
                            </select>
                        </div>
                        <div class="pc-prop-group">
                            <div class="pc-prop-label">색상</div>
                            <div class="pc-color-swatches">
                                <template x-for="c in nodeColors" :key="c.value">
                                    <div class="pc-color-swatch"
                                         :class="{ active: selectedNode.color === c.value }"
                                         :style="'background:' + c.value"
                                         @click="selectedNode.color = c.value; onPropChange()"></div>
                                </template>
                            </div>
                        </div>
                        <div class="pc-prop-group">
                            <div class="pc-prop-label">담당자</div>
                            <input class="pc-prop-input" x-model="selectedNode.assignee" placeholder="담당자 이름" @input="onPropChange()">
                        </div>
                        <div class="pc-prop-group">
                            <div class="pc-prop-label">마감일</div>
                            <input class="pc-prop-input" type="date" x-model="selectedNode.dueDate" @change="onPropChange()">
                        </div>
                        <div class="pc-prop-group">
                            <div class="pc-prop-label">태그 (콤마 구분)</div>
                            <input class="pc-prop-input" :value="(selectedNode.tags||[]).join(', ')"
                                   @change="selectedNode.tags = $event.target.value.split(',').map(s=>s.trim()).filter(Boolean); onPropChange()">
                        </div>
                        <div class="pc-prop-group">
                            <div class="pc-prop-label">우선순위</div>
                            <select class="pc-prop-input" x-model="selectedNode.priority" @change="onPropChange()">
                                <option value="low">낮음</option>
                                <option value="medium">보통</option>
                                <option value="high">높음</option>
                                <option value="critical">긴급</option>
                            </select>
                        </div>
                        <div style="margin-top: 16px; display:flex; flex-direction:column; gap:6px;">
                            <button class="w-full py-2 bg-indigo-50 text-indigo-600 text-xs font-medium rounded-lg hover:bg-indigo-100 transition"
                                    @click="openNodeModal(selectedNode)">상세 편집</button>
                            <button class="w-full py-2 bg-red-50 text-red-600 text-xs font-medium rounded-lg hover:bg-red-100 transition"
                                    @click="deleteSelectedNode()">노드 삭제</button>
                        </div>
                    </div>
                </template>
            </div>

            {{-- Projects Panel --}}
            <div class="pc-sidebar-panel" :class="{ active: sidebarTab === 'projects' }">
                <div style="margin-bottom: 12px;">
                    <button class="w-full py-2 bg-indigo-50 text-indigo-600 text-xs font-medium rounded-lg hover:bg-indigo-100 transition"
                            @click="newProject()">+ 새 프로젝트</button>
                </div>
                <template x-for="(proj, idx) in savedProjects" :key="proj.id">
                    <div class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 cursor-pointer mb-1"
                         :class="{ 'bg-indigo-50': currentProjectId === proj.id }"
                         @click="loadProject(proj.id)">
                        <div class="flex-1 min-w-0">
                            <div class="text-xs font-medium text-gray-800 truncate" x-text="proj.title || '제목 없음'"></div>
                            <div class="text-xs text-gray-400" x-text="proj.nodeCount + '개 노드 · ' + formatDate(proj.updatedAt)"></div>
                        </div>
                        <button class="text-gray-300 hover:text-red-500 transition text-xs" @click.stop="deleteProject(proj.id)">
                            <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
                        </button>
                    </div>
                </template>
                <div x-show="savedProjects.length === 0" class="text-center text-gray-400 text-xs py-8">
                    저장된 프로젝트가 없습니다
                </div>
            </div>
        </div>

        {{-- Main Content Area --}}
        <div style="flex:1; display:flex; flex-direction:column; min-width:0;">

        {{-- Filter Bar --}}
        <div class="pc-filter-bar" x-show="viewMode === 'kanban' || viewMode === 'list'">
            <span class="pc-filter-label">필터:</span>
            <input type="text" placeholder="검색 (Ctrl+F)" x-model="filterText" style="width:160px;" @keydown.escape="filterText=''">
            <select x-model="filterStatus">
                <option value="">전체 상태</option>
                <option value="todo">대기</option>
                <option value="progress">진행중</option>
                <option value="review">검토중</option>
                <option value="done">완료</option>
            </select>
            <select x-model="filterPriority">
                <option value="">전체 우선순위</option>
                <option value="critical">긴급</option>
                <option value="high">높음</option>
                <option value="medium">보통</option>
                <option value="low">낮음</option>
            </select>
            <select x-model="filterType">
                <option value="">전체 유형</option>
                <template x-for="item in allPaletteItems" :key="item.type">
                    <option :value="item.type" x-text="item.label"></option>
                </template>
            </select>
            <button class="pc-filter-clear" x-show="filterText || filterStatus || filterPriority || filterType"
                    @click="filterText=''; filterStatus=''; filterPriority=''; filterType='';">초기화</button>
            <span style="margin-left:auto; font-size:10px; color:#94a3b8;" x-text="filteredNodes.length + ' / ' + nodes.length + '개'"></span>
        </div>

        {{-- Canvas (free / timeline / flow) --}}
        <div class="pc-canvas-wrap" id="canvasWrap"
             x-show="viewMode === 'free' || viewMode === 'timeline' || viewMode === 'flow'"
             @mousedown="onCanvasMouseDown($event)"
             @mousemove="onCanvasMouseMove($event)"
             @mouseup="onCanvasMouseUp($event)"
             @wheel="onCanvasWheel($event)"
             @dragover.prevent
             @drop="onCanvasDrop($event)"
             @contextmenu.prevent="showContextMenu($event)">

            <div class="pc-canvas" id="canvas" :style="'transform: scale(' + zoom + ') translate(' + panX + 'px,' + panY + 'px)'">
                {{-- SVG Connections (프로그래밍 렌더링 — Alpine template은 SVG 네임스페이스 미지원) --}}
                <svg class="pc-connections" id="connectionsSvg" x-ref="connSvg" x-effect="renderConnections()"></svg>

                {{-- Timeline Swimlanes --}}
                <template x-if="viewMode === 'timeline'">
                    <template x-for="(phase, idx) in phases" :key="phase.id">
                        <div class="pc-swimlane" :style="'left:' + (idx * 400 + 100) + 'px; width: 400px;'">
                            <div class="pc-swimlane-label" x-text="phase.name"></div>
                        </div>
                    </template>
                </template>

                {{-- Nodes --}}
                <template x-for="node in nodes" :key="node.id">
                    <div class="pc-node"
                         :class="{ selected: selectedNode?.id === node.id }"
                         :style="'left:' + node.x + 'px; top:' + node.y + 'px; border-left: 4px solid ' + (node.color || '#6366f1')"
                         :id="'node-' + node.id"
                         @mousedown.stop="onNodeMouseDown($event, node)"
                         @dblclick.stop="openNodeModal(node)">

                        <div class="pc-node-header">
                            <div class="pc-node-icon" :style="'background:' + (node.bg || '#eef2ff')">
                                <span x-text="node.emoji || '📌'"></span>
                            </div>
                            <div class="pc-node-title"
                                 contenteditable="true"
                                 @blur="node.title = $event.target.textContent; autoSave()"
                                 @keydown.enter.prevent="$event.target.blur()"
                                 x-text="node.title"></div>
                            <div style="width: 8px; height: 8px; border-radius: 50; flex-shrink: 0;"
                                 :style="'background:' + statusColor(node.status)"></div>
                        </div>

                        <div class="pc-node-body" x-show="node.description">
                            <div x-text="node.description" style="overflow:hidden; max-height: 40px; text-overflow: ellipsis;"></div>
                        </div>

                        <div class="pc-node-tags" x-show="node.tags && node.tags.length > 0">
                            <template x-for="tag in (node.tags||[])" :key="tag">
                                <span class="pc-node-tag" x-text="tag"></span>
                            </template>
                        </div>

                        <div class="pc-node-footer">
                            <span x-text="node.typeLabel || node.type"></span>
                            <span x-text="priorityLabel(node.priority)"></span>
                        </div>

                        {{-- Connection Ports --}}
                        <div class="pc-port pc-port-top" @mousedown.stop="startConnection($event, node, 'top')"></div>
                        <div class="pc-port pc-port-bottom" @mousedown.stop="startConnection($event, node, 'bottom')"></div>
                        <div class="pc-port pc-port-left" @mousedown.stop="startConnection($event, node, 'left')"></div>
                        <div class="pc-port pc-port-right" @mousedown.stop="startConnection($event, node, 'right')"></div>
                    </div>
                </template>
            </div>

            {{-- Zoom Controls --}}
            <div class="pc-zoom-controls">
                <button class="pc-zoom-btn" @click="zoomIn()">+</button>
                <div class="pc-zoom-label" x-text="Math.round(zoom * 100) + '%'"></div>
                <button class="pc-zoom-btn" @click="zoomOut()">-</button>
                <button class="pc-zoom-btn" @click="zoomFit()" title="맞춤">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/></svg>
                </button>
            </div>

            {{-- Minimap --}}
            <div class="pc-minimap" id="minimap"></div>
        </div>

        {{-- Kanban View --}}
        <div class="pc-kanban" x-show="viewMode === 'kanban'">
            <template x-for="col in kanbanColumns" :key="col.key">
                <div class="pc-kanban-col">
                    <div class="pc-kanban-col-header">
                        <div class="pc-kanban-col-dot" :style="'background:' + col.color"></div>
                        <div class="pc-kanban-col-title" x-text="col.label"></div>
                        <div class="pc-kanban-col-count" x-text="kanbanNodesFor(col.key).length"></div>
                    </div>
                    <div class="pc-kanban-col-body"
                         @dragover.prevent="$event.currentTarget.classList.add('drag-over')"
                         @dragleave="$event.currentTarget.classList.remove('drag-over')"
                         @drop.prevent="onKanbanDrop($event, col.key); $event.currentTarget.classList.remove('drag-over')">
                        <template x-for="node in kanbanNodesFor(col.key)" :key="node.id">
                            <div class="pc-kanban-card"
                                 :style="'--card-color:' + (node.color || '#6366f1')"
                                 draggable="true"
                                 @dragstart="onKanbanDragStart($event, node)"
                                 @dragend="$event.target.classList.remove('dragging')"
                                 @dblclick="openNodeModal(node)">
                                <div class="pc-kanban-card-title">
                                    <span class="emoji" x-text="node.emoji || '📌'"></span>
                                    <span x-text="node.title"></span>
                                </div>
                                <div class="pc-kanban-card-desc" x-show="node.description" x-text="node.description"></div>
                                <div class="pc-kanban-card-meta">
                                    <template x-for="tag in (node.tags||[]).slice(0,3)" :key="tag">
                                        <span class="pc-kanban-card-tag" x-text="tag"></span>
                                    </template>
                                    <span class="pc-kanban-card-tag" x-show="node.assignee" x-text="'👤 ' + (node.assignee||'')"></span>
                                    <span class="pc-kanban-card-tag" x-show="node.dueDate" x-text="'📅 ' + formatShortDate(node.dueDate)"
                                          :style="isOverdue(node.dueDate) ? 'background:#fef2f2;color:#ef4444;' : ''"></span>
                                    <template x-if="node.checklist && node.checklist.length > 0">
                                        <span class="pc-kanban-card-tag" x-text="'☑ ' + node.checklist.filter(c=>c.done).length + '/' + node.checklist.length"></span>
                                    </template>
                                    <span class="pc-kanban-card-priority" :class="node.priority" x-text="priorityLabel(node.priority)"></span>
                                </div>
                            </div>
                        </template>
                    </div>
                </div>
            </template>
        </div>

        {{-- List/Table View --}}
        <div class="pc-list-view" x-show="viewMode === 'list'">
            <table class="pc-list-table">
                <thead>
                    <tr>
                        <th style="width:30px;"></th>
                        <th @click="sortList('title')">제목 <span class="sort-icon" x-text="sortIcon('title')"></span></th>
                        <th @click="sortList('type')" style="width:80px;">유형 <span class="sort-icon" x-text="sortIcon('type')"></span></th>
                        <th @click="sortList('status')" style="width:80px;">상태 <span class="sort-icon" x-text="sortIcon('status')"></span></th>
                        <th @click="sortList('priority')" style="width:80px;">우선순위 <span class="sort-icon" x-text="sortIcon('priority')"></span></th>
                        <th @click="sortList('assignee')" style="width:90px;">담당자 <span class="sort-icon" x-text="sortIcon('assignee')"></span></th>
                        <th @click="sortList('dueDate')" style="width:90px;">마감일 <span class="sort-icon" x-text="sortIcon('dueDate')"></span></th>
                        <th style="width:60px;">체크</th>
                    </tr>
                </thead>
                <tbody>
                    <template x-for="node in sortedFilteredNodes" :key="node.id">
                        <tr :class="{ selected: selectedNode?.id === node.id }"
                            @click="selectedNode = node; sidebarTab = 'properties';"
                            @dblclick="openNodeModal(node)"
                            style="cursor:pointer;">
                            <td><span x-text="node.emoji || '📌'"></span></td>
                            <td>
                                <div style="font-weight:600;" x-text="node.title"></div>
                                <div style="font-size:10px; color:#94a3b8; max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" x-text="node.description"></div>
                            </td>
                            <td><span class="pc-node-tag" x-text="node.typeLabel || node.type"></span></td>
                            <td>
                                <span class="pc-list-status-badge" :style="'background:' + statusColor(node.status) + '20; color:' + statusColor(node.status)">
                                    <span style="width:6px;height:6px;border-radius:50;display:inline-block;" :style="'background:' + statusColor(node.status)"></span>
                                    <span x-text="statusLabel(node.status)"></span>
                                </span>
                            </td>
                            <td><span class="pc-kanban-card-priority" :class="node.priority" x-text="priorityLabel(node.priority)"></span></td>
                            <td x-text="node.assignee || '-'" style="font-size:11px;"></td>
                            <td style="font-size:11px;" :style="isOverdue(node.dueDate) ? 'color:#ef4444;font-weight:600;' : ''"
                                x-text="node.dueDate ? formatShortDate(node.dueDate) : '-'"></td>
                            <td style="font-size:11px; color:#94a3b8;"
                                x-text="node.checklist && node.checklist.length ? node.checklist.filter(c=>c.done).length + '/' + node.checklist.length : '-'"></td>
                        </tr>
                    </template>
                </tbody>
            </table>
            <div x-show="filteredNodes.length === 0" class="text-center text-gray-400 text-sm py-16">노드가 없습니다</div>
        </div>

        {{-- Storyboard View --}}
        <div class="sb-wrap" x-show="viewMode === 'storyboard'">
            {{-- Block Toolbar (상단) --}}
            <div class="sb-block-toolbar">
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('heading')">H1</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('heading2')">H2</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('text')">T 텍스트</button>
                <div class="sb-block-toolbar-sep"></div>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('table')">▦ 테이블</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('callout')">💡 콜아웃</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('todo')">☑ 체크리스트</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('code')">{ } 코드</button>
                <div class="sb-block-toolbar-sep"></div>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('button')">🔘 버튼</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('input')">⊡ 입력필드</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('select')">▿ 셀렉트</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('card')">▣ 카드</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('badges')">● 뱃지</button>
                <div class="sb-block-toolbar-sep"></div>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('image')">🖼 이미지</button>
                <button class="sb-block-toolbar-btn" @click="sbAddBlock('divider')">― 구분선</button>
                <div class="sb-block-toolbar-sep"></div>
                {{-- Description Marker --}}
                <div style="display:flex; align-items:center; gap:3px;">
                    <input type="text" x-model="sbMarkerNum" placeholder="01" maxlength="3"
                           style="width:32px; padding:3px 4px; border:1px solid #e2e8f0; border-radius:5px; font-size:10px; text-align:center; font-weight:700;">
                    <button class="sb-block-toolbar-btn" @click="sbAddMarkerBlock()" title="번호 뱃지를 캔버스에 추가">
                        <span style="display:inline-flex;width:16px;height:16px;border-radius:50%;background:#1e293b;color:#fff;font-size:8px;font-weight:700;align-items:center;justify-content:center;" x-text="sbMarkerNum || '01'"></span>
                        번호
                    </button>
                </div>
                <div class="sb-block-toolbar-sep"></div>
                {{-- Template Dropdown --}}
                <div class="sb-tpl-dropdown" @click.outside="sbTplOpen = false">
                    <button class="sb-tpl-trigger" x-ref="sbTplBtn" @click="sbToggleTplPanel()">📋 템플릿 <span style="font-size:8px;">▾</span></button>
                    <div class="sb-tpl-panel" x-ref="sbTplPanel" x-show="sbTplOpen" x-cloak x-transition>
                        <div class="sb-tpl-tabs">
                            <div class="sb-tpl-tab" :class="{ active: sbTplTab === 'preset' }" @click="sbTplTab = 'preset'">기본 템플릿</div>
                            <div class="sb-tpl-tab" :class="{ active: sbTplTab === 'custom' }" @click="sbTplTab = 'custom'">내 템플릿</div>
                        </div>
                        <input class="sb-tpl-search" type="text" placeholder="템플릿 검색..." x-model="sbTplSearch">
                        <div class="sb-tpl-list">
                            {{-- Preset Templates --}}
                            <template x-if="sbTplTab === 'preset'">
                                <div>
                                    <template x-for="tpl in sbFilteredPresets" :key="tpl.name">
                                        <div class="sb-tpl-item" @click="sbInsertTemplate(tpl); sbTplOpen = false;">
                                            <div class="sb-tpl-item-icon" x-text="tpl.icon"></div>
                                            <div class="sb-tpl-item-info">
                                                <div class="sb-tpl-item-name" x-text="tpl.name"></div>
                                                <div class="sb-tpl-item-desc" x-text="tpl.desc"></div>
                                            </div>
                                        </div>
                                    </template>
                                    <div class="sb-tpl-empty" x-show="sbFilteredPresets.length === 0">검색 결과가 없습니다</div>
                                </div>
                            </template>
                            {{-- Custom Templates --}}
                            <template x-if="sbTplTab === 'custom'">
                                <div>
                                    <template x-for="(tpl, ti) in sbFilteredCustoms" :key="tpl.name">
                                        <div class="sb-tpl-item">
                                            <div class="sb-tpl-item-icon">📄</div>
                                            <div class="sb-tpl-item-info" @click="sbInsertTemplate(tpl); sbTplOpen = false;">
                                                <div class="sb-tpl-item-name" x-text="tpl.name"></div>
                                                <div class="sb-tpl-item-desc" x-text="tpl.blocks.length + '개 블록'"></div>
                                            </div>
                                            <div class="sb-tpl-item-actions">
                                                <button class="sb-tpl-item-btn" @click.stop="sbDeleteCustomTemplate(ti)" title="삭제">×</button>
                                            </div>
                                        </div>
                                    </template>
                                    <div class="sb-tpl-empty" x-show="sbFilteredCustoms.length === 0 && !sbTplSearch">
                                        저장된 템플릿이 없습니다<br>
                                        <span style="font-size:10px;">현재 페이지 블록을 아래에서 저장하세요</span>
                                    </div>
                                    <div class="sb-tpl-empty" x-show="sbFilteredCustoms.length === 0 && sbTplSearch">검색 결과가 없습니다</div>
                                </div>
                            </template>
                        </div>
                        {{-- Save current blocks as template --}}
                        <div class="sb-tpl-save-bar">
                            <input class="sb-tpl-save-input" type="text" placeholder="현재 블록을 템플릿으로 저장..." x-model="sbTplSaveName" @keydown.enter="sbSaveAsTemplate()">
                            <button class="sb-tpl-save-btn" @click="sbSaveAsTemplate()" :disabled="!sbTplSaveName.trim()">저장</button>
                        </div>
                    </div>
                </div>
            </div>

            {{-- Storyboard Top Bar --}}
            <div class="sb-topbar">
                <label>프로젝트</label>
                <input type="text" x-model="sb.docInfo.projectName" placeholder="프로젝트명" style="width:200px;" @input="autoSave()">
                <div class="sb-topbar-sep"></div>
                <label>단위업무</label>
                <input type="text" x-model="sb.docInfo.unitTask" placeholder="단위업무명" style="width:120px;" @input="autoSave()">
                <div class="sb-topbar-sep"></div>
                <label>버전</label>
                <input type="text" x-model="sb.docInfo.version" placeholder="D1.0" style="width:60px;" @input="autoSave()">
                <div class="sb-topbar-sep"></div>
                <div class="sb-pages-nav">
                    <button @click="sbPrevPage()" title="이전 페이지">&lt;</button>
                    <span x-text="(sb.currentPageIndex + 1) + ' / ' + sb.pages.length"></span>
                    <button @click="sbNextPage()" title="다음 페이지">&gt;</button>
                </div>
                <div class="sb-topbar-sep"></div>
                <button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#374151;"
                        @click="sbAddPage()">+ 페이지 추가</button>
                <button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#2563eb;"
                        @click="sbDuplicatePage()">페이지 복사</button>
                <button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#ef4444;"
                        x-show="sb.pages.length > 1"
                        @click="sbDeletePage()">페이지 삭제</button>
                <div style="margin-left:auto;"></div>
                <button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#374151;"
                        @click="sbEditMenu()">메뉴 편집</button>
                <button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#374151;"
                        @click="sbPrintPreview()">🖨 인쇄</button>
                <button style="padding:4px 10px; border:1px solid var(--pc-indigo); border-radius:6px; font-size:11px; cursor:pointer; background:var(--pc-indigo); color:#fff;"
                        @click="sbExportHtml()">HTML 내보내기</button>
            </div>

            <div class="sb-body">
                {{-- Page List (thumbnail) --}}
                <div class="sb-page-list">
                    <template x-for="(pg, idx) in sb.pages" :key="pg.id">
                        <div class="sb-page-thumb" :class="{ active: sb.currentPageIndex === idx }" @click="sb.currentPageIndex = idx">
                            <div class="sb-page-thumb-num" x-text="(idx + 1) + '.'"></div>
                            <div x-text="pg.screenName || '(화면명)'" style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"></div>
                        </div>
                    </template>
                </div>

                {{-- Page Editor --}}
                <div class="sb-editor">
                    <template x-if="sbCurrentPage">
                        <div class="sb-page" :style="'width:' + (sidebarOpen ? '1100' : '1400') + 'px'">
                            {{-- Page Header --}}
                            <div class="sb-page-header">
                                <div>
                                    <span class="label">단위업무명</span>
                                    <span class="value" x-text="sb.docInfo.unitTask || '-'"></span>
                                </div>
                                <div>
                                    <span class="label">버전</span>
                                    <span class="value" x-text="sb.docInfo.version || '-'"></span>
                                </div>
                                <div>
                                    <span class="label">Page</span>
                                    <span class="value" x-text="sb.currentPageIndex + 1"></span>
                                </div>
                                <div>
                                    <span class="label">경로</span>
                                    <span class="value"><input type="text" x-model="sbCurrentPage.path" placeholder="품질관리 > 제품검사관리" @input="autoSave()"></span>
                                </div>
                                <div>
                                    <span class="label">화면명</span>
                                    <span class="value"><input type="text" x-model="sbCurrentPage.screenName" placeholder="제품검사 목록" @input="autoSave()"></span>
                                </div>
                                <div>
                                    <span class="label">화면 ID</span>
                                    <span class="value"><input type="text" x-model="sbCurrentPage.screenId" placeholder="QM-001" @input="autoSave()"></span>
                                </div>
                            </div>

                            <div class="sb-page-body"
                                 @mousemove.window="if(_sbMenuResize){ sbMenuWidth = Math.max(80, Math.min(400, _sbMenuResize.startW + $event.clientX - _sbMenuResize.startX)); } if(_sbDescResize){ sbDescHeight = Math.max(60, Math.min(500, _sbDescResize.startH - ($event.clientY - _sbDescResize.startY))); }"
                                 @mouseup.window="if(_sbMenuResize){ _sbMenuResize = null; document.querySelector('.sb-menu-resizer.active')?.classList.remove('active'); } if(_sbDescResize){ _sbDescResize = null; document.querySelector('.sb-desc-resizer.active')?.classList.remove('active'); }">
                                {{-- Left Menu --}}
                                <template x-if="sbMenuOpen">
                                    <div class="sb-menu-panel" :style="'width:' + sbMenuWidth + 'px'">
                                        <div class="sb-menu-logo" x-text="sb.docInfo.projectName || 'LOGO'"></div>
                                        <div class="sb-menu-section">ERP 메뉴</div>
                                        <template x-for="menu in sb.menuTree" :key="menu.name">
                                            <div>
                                                <div class="sb-menu-item" :class="{ active: sbCurrentPage.path && sbCurrentPage.path.startsWith(menu.name) }" x-text="menu.name"></div>
                                                <template x-for="child in (menu.children || [])" :key="child.name">
                                                    <div class="sb-menu-item sb-menu-child"
                                                         :class="{ active: sbCurrentPage.path && sbCurrentPage.path.includes(child.name) }"
                                                         x-text="'- ' + child.name"></div>
                                                </template>
                                            </div>
                                        </template>
                                    </div>
                                </template>

                                {{-- Menu Toggle + Resizer --}}
                                <button class="sb-menu-toggle" @click="sbMenuOpen = !sbMenuOpen" :title="sbMenuOpen ? '메뉴 접기' : '메뉴 펼치기'">
                                    <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                                        <polyline :points="sbMenuOpen ? '15 6 9 12 15 18' : '9 6 15 12 9 18'"/>
                                    </svg>
                                </button>
                                <template x-if="sbMenuOpen">
                                    <div class="sb-menu-resizer"
                                         @mousedown.prevent="_sbMenuResize = { startX: $event.clientX, startW: sbMenuWidth }; $event.target.classList.add('active')"></div>
                                </template>

                                {{-- Content + Description --}}
                                <div class="sb-content-area">
                                    {{-- Block Editor Area (moved toolbar to sb-topbar above) --}}
                                    <input type="file" accept="image/*" x-ref="sbBlockImageInput" style="display:none;" @change="sbBlockUploadImage($event)">

                                    {{-- Block Editor Area (Free Canvas) --}}
                                    <div class="sb-blocks-area" x-ref="sbCanvas"
                                         @mousedown="sbCanvasMouseDown($event)"
                                         @mousemove="sbCanvasMouseMove($event)"
                                         @mouseup="sbCanvasMouseUp($event)"
                                         @click.self="if(!_sbLassoDone){ sbSelectedBlock = null; sbMultiSelected = []; sbFormatBar = null; sbCtxMenu = null; sbFmtDropdown = null; } _sbLassoDone = false;"
                                         @contextmenu.self.prevent="sbCtxMenu = null; sbCtxSub = null;"
                                         @dragover.prevent="$event.dataTransfer.dropEffect = 'copy'"
                                         @drop.prevent="sbDropMarker($event)"
                                         style="min-height: 600px; flex: 1;">
                                        {{-- Lasso rectangle --}}
                                        <div class="sb-lasso-rect" x-show="_sbLasso" x-cloak
                                             :style="_sbLasso ? 'left:'+_sbLasso.rx+'px;top:'+_sbLasso.ry+'px;width:'+_sbLasso.rw+'px;height:'+_sbLasso.rh+'px' : ''"></div>
                                        <template x-if="!sbPageBlocks.length">
                                            <div class="sb-blk-empty-area" style="position:absolute;inset:40px;" @click="sbAddBlock('text')">
                                                <svg style="width:28px;height:28px;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4v16m8-8H4"/></svg>
                                                <span>블록을 추가하여 화면을 구성하세요</span>
                                                <span style="font-size:10px; color:#cbd5e1;">상단 툴바에서 블록 유형을 선택하거나 여기를 클릭하세요</span>
                                            </div>
                                        </template>
                                        <template x-for="(blk, bi) in sbPageBlocks" :key="blk.id">
                                            <div class="sb-block"
                                                 :class="{ selected: sbSelectedBlock === blk.id, 'sb-multi-selected': sbMultiSelected.includes(blk.id), 'sb-block-editing': sbEditingBlock === blk.id }"
                                                 :style="'left:' + (blk.x||0) + 'px; top:' + (blk.y||0) + 'px; width:' + (blk.w||240) + 'px; min-height:' + (blk.h||40) + 'px;'
                                                     + (blk.style?.fontColor ? 'color:' + blk.style.fontColor + ';' : '')
                                                     + (blk.style?.bgColor ? 'background:' + blk.style.bgColor + ';' : '')
                                                     + (blk.style?.fontSize ? 'font-size:' + blk.style.fontSize + 'px;' : '')
                                                     + (blk.style?.bold ? 'font-weight:700;' : '')
                                                     + (blk.style?.italic ? 'font-style:italic;' : '')
                                                     + (blk.style?.textAlign ? 'text-align:' + blk.style.textAlign + ';' : '')
                                                     + (blk.style?.zIndex ? 'z-index:' + blk.style.zIndex + ';' : '')
                                                     + (blk.style?.opacity ? 'opacity:' + blk.style.opacity + ';' : '')
                                                     + (blk.style?.borderColor ? 'border-color:' + blk.style.borderColor + ';' : '')
                                                     + (blk.style?.borderRadius ? 'border-radius:' + blk.style.borderRadius + 'px;' : '')"
                                                 @mousedown.stop="sbBlockMouseDown(blk, bi, $event)"
                                                 @dblclick="sbEditingBlock = blk.id"
                                                 @contextmenu.prevent="sbShowCtxMenu(blk, bi, $event)">
                                                <div class="sb-block-actions">
                                                    <button class="sb-block-action-btn" @click.stop="sbDuplicateBlock(bi)" title="복제">⧉</button>
                                                    <button class="sb-block-action-btn danger" @click.stop="sbRemoveBlock(bi)" title="삭제">×</button>
                                                </div>
                                                {{-- Resize handles --}}
                                                <div class="sb-resize-handle sb-rh-r" @mousedown.stop="sbResizeStart(blk, 'r', $event)"></div>
                                                <div class="sb-resize-handle sb-rh-b" @mousedown.stop="sbResizeStart(blk, 'b', $event)"></div>
                                                <div class="sb-resize-handle sb-rh-br" @mousedown.stop="sbResizeStart(blk, 'br', $event)"></div>
                                                <div class="sb-block-size" x-text="(blk.w||240) + ' × ' + (blk.h||40)"></div>

                                                {{-- Text --}}
                                                <template x-if="blk.type === 'text'">
                                                    <div class="sb-blk-text" contenteditable="true" data-placeholder="텍스트를 입력하세요..."
                                                         @blur="blk.content = $event.target.innerText; autoSave();"
                                                         @keydown.enter="if(!$event.shiftKey){ $event.preventDefault(); sbAddBlockAfter(bi,'text'); }"
                                                         x-text="blk.content"></div>
                                                </template>

                                                {{-- Heading --}}
                                                <template x-if="blk.type === 'heading'">
                                                    <div class="sb-blk-heading" contenteditable="true"
                                                         @blur="blk.content = $event.target.innerText; autoSave();"
                                                         x-text="blk.content"></div>
                                                </template>
                                                <template x-if="blk.type === 'heading2'">
                                                    <div class="sb-blk-heading sb-blk-h2" contenteditable="true" data-placeholder="소제목을 입력하세요"
                                                         @blur="blk.content = $event.target.innerText; autoSave();"
                                                         x-text="blk.content"></div>
                                                </template>

                                                {{-- Divider --}}
                                                <template x-if="blk.type === 'divider'">
                                                    <hr class="sb-blk-divider">
                                                </template>

                                                {{-- Callout --}}
                                                <template x-if="blk.type === 'callout'">
                                                    <div class="sb-blk-callout">
                                                        <span class="sb-blk-callout-icon" contenteditable="true" @blur="blk.icon = $event.target.innerText; autoSave();" x-text="blk.icon || '💡'"></span>
                                                        <div class="sb-blk-callout-text" contenteditable="true"
                                                             @blur="blk.content = $event.target.innerText; autoSave();"
                                                             x-text="blk.content"></div>
                                                    </div>
                                                </template>

                                                {{-- Table --}}
                                                <template x-if="blk.type === 'table'">
                                                    <div class="sb-blk-table-wrap">
                                                        <table class="sb-blk-table">
                                                            <thead>
                                                                <tr>
                                                                    <template x-for="(col, ci) in blk.cols" :key="ci">
                                                                        <th contenteditable="true" @blur="blk.cols[ci] = $event.target.innerText; autoSave();" x-text="col"></th>
                                                                    </template>
                                                                    <th style="width:24px; background:transparent; border:none; padding:0;">
                                                                        <button class="sb-block-action-btn" @click.stop="sbTableAddCol(blk)" style="width:20px; height:20px;">+</button>
                                                                    </th>
                                                                </tr>
                                                            </thead>
                                                            <tbody>
                                                                <template x-for="(row, ri) in blk.rows" :key="ri">
                                                                    <tr>
                                                                        <template x-for="(cell, ci) in row" :key="ci">
                                                                            <td contenteditable="true" @blur="blk.rows[ri][ci] = $event.target.innerText; autoSave();" x-text="cell"></td>
                                                                        </template>
                                                                        <td style="border:none; padding:0;">
                                                                            <button class="sb-block-action-btn danger" @click.stop="blk.rows.splice(ri, 1); autoSave();" style="width:18px; height:18px; font-size:10px;">×</button>
                                                                        </td>
                                                                    </tr>
                                                                </template>
                                                            </tbody>
                                                        </table>
                                                        <div style="display:flex; gap:4px; margin-top:4px;">
                                                            <button class="sb-block-toolbar-btn" @click.stop="sbTableAddRow(blk)" style="font-size:9px;">+ 행 추가</button>
                                                        </div>
                                                    </div>
                                                </template>

                                                {{-- Mock: Button --}}
                                                <template x-if="blk.type === 'button'">
                                                    <div class="sb-blk-mockup">
                                                        <div class="sb-blk-mockup-label">Button</div>
                                                        <span class="sb-blk-mock-btn" contenteditable="true"
                                                              :style="'background:' + (blk.color || '#4338ca') + '; color:#fff;'"
                                                              @blur="blk.content = $event.target.innerText; autoSave();"
                                                              x-text="blk.content || '버튼'"></span>
                                                        <div style="display:flex; gap:4px; margin-top:6px;">
                                                            <template x-for="c in ['#4338ca','#10b981','#ef4444','#f59e0b','#64748b']" :key="c">
                                                                <span @click="blk.color = c; autoSave();" style="width:16px; height:16px; border-radius:50%; cursor:pointer; display:inline-block;"
                                                                      :style="'background:' + c + '; border:2px solid ' + (blk.color === c ? '#1e293b' : 'transparent')"></span>
                                                            </template>
                                                        </div>
                                                    </div>
                                                </template>

                                                {{-- Mock: Input --}}
                                                <template x-if="blk.type === 'input'">
                                                    <div class="sb-blk-mockup">
                                                        <div class="sb-blk-mockup-label" contenteditable="true" @blur="blk.label = $event.target.innerText; autoSave();" x-text="blk.label || 'Label'"></div>
                                                        <input class="sb-blk-mock-input" type="text" :placeholder="blk.placeholder || '입력 필드'"
                                                               @input="blk.placeholder = $event.target.placeholder" disabled>
                                                        <input type="text" style="margin-top:4px; font-size:10px; border:none; outline:none; color:#94a3b8; width:100%;"
                                                               x-model="blk.placeholder" placeholder="placeholder 텍스트 편집" @input="autoSave()">
                                                    </div>
                                                </template>

                                                {{-- Mock: Select --}}
                                                <template x-if="blk.type === 'select'">
                                                    <div class="sb-blk-mockup">
                                                        <div class="sb-blk-mockup-label" contenteditable="true" @blur="blk.label = $event.target.innerText; autoSave();" x-text="blk.label || 'Label'"></div>
                                                        <select class="sb-blk-mock-select" disabled>
                                                            <option x-text="blk.placeholder || '선택하세요'"></option>
                                                        </select>
                                                        <input type="text" style="margin-top:4px; font-size:10px; border:none; outline:none; color:#94a3b8; width:100%;"
                                                               x-model="blk.placeholder" placeholder="placeholder 텍스트 편집" @input="autoSave()">
                                                    </div>
                                                </template>

                                                {{-- Mock: Card --}}
                                                <template x-if="blk.type === 'card'">
                                                    <div class="sb-blk-mockup">
                                                        <div class="sb-blk-mockup-label">Card</div>
                                                        <div class="sb-blk-mock-card">
                                                            <div class="sb-blk-mock-card-title" contenteditable="true"
                                                                 @blur="blk.title = $event.target.innerText; autoSave();"
                                                                 x-text="blk.title || '카드 제목'"></div>
                                                            <div class="sb-blk-mock-card-body" contenteditable="true"
                                                                 @blur="blk.content = $event.target.innerText; autoSave();"
                                                                 x-text="blk.content || '카드 내용을 입력하세요'"></div>
                                                        </div>
                                                    </div>
                                                </template>

                                                {{-- Badges --}}
                                                <template x-if="blk.type === 'badges'">
                                                    <div class="sb-blk-mockup">
                                                        <div class="sb-blk-mockup-label">Badges</div>
                                                        <div class="sb-blk-badge-row">
                                                            <template x-for="(badge, bdi) in blk.items" :key="bdi">
                                                                <span class="sb-blk-badge" :style="'background:' + (badge.color || '#e0e7ff') + '; color:' + (badge.textColor || '#4338ca') + ';'"
                                                                      contenteditable="true"
                                                                      @blur="badge.text = $event.target.innerText; autoSave();"
                                                                      x-text="badge.text"></span>
                                                            </template>
                                                            <button class="sb-block-action-btn" @click.stop="blk.items.push({ text: '뱃지', color: '#e0e7ff', textColor: '#4338ca' }); autoSave();">+</button>
                                                        </div>
                                                    </div>
                                                </template>

                                                {{-- Todo --}}
                                                <template x-if="blk.type === 'todo'">
                                                    <div>
                                                        <template x-for="(item, ti) in blk.items" :key="ti">
                                                            <div class="sb-blk-todo">
                                                                <input type="checkbox" x-model="item.checked" @change="autoSave()">
                                                                <div class="sb-blk-todo-text" contenteditable="true"
                                                                     :style="item.checked ? 'text-decoration: line-through; color:#94a3b8;' : ''"
                                                                     @blur="item.text = $event.target.innerText; autoSave();"
                                                                     @keydown.enter.prevent="blk.items.splice(ti+1, 0, { text: '', checked: false }); autoSave(); $nextTick(() => { $event.target.parentElement.nextElementSibling?.querySelector('[contenteditable]')?.focus(); })"
                                                                     x-text="item.text"></div>
                                                                <button class="sb-block-action-btn danger" @click.stop="blk.items.splice(ti, 1); autoSave();" style="width:16px;height:16px;font-size:10px;">×</button>
                                                            </div>
                                                        </template>
                                                        <div style="padding:4px 8px;">
                                                            <button class="sb-block-toolbar-btn" @click.stop="blk.items.push({ text: '', checked: false }); autoSave();" style="font-size:9px;">+ 항목 추가</button>
                                                        </div>
                                                    </div>
                                                </template>

                                                {{-- Code --}}
                                                <template x-if="blk.type === 'code'">
                                                    <div class="sb-blk-code" contenteditable="true"
                                                         @blur="blk.content = $event.target.innerText; autoSave();"
                                                         x-text="blk.content"></div>
                                                </template>

                                                {{-- Image --}}
                                                <template x-if="blk.type === 'image'">
                                                    <div class="sb-blk-image-wrap">
                                                        <template x-if="blk.src">
                                                            <div style="position:relative; display:inline-block;">
                                                                <img :src="blk.src" style="max-width:100%; border-radius:6px;">
                                                                <button style="position:absolute; top:4px; right:4px; width:22px; height:22px; border-radius:50%; border:none; background:rgba(0,0,0,0.5); color:#fff; cursor:pointer; font-size:11px;"
                                                                        @click.stop="blk.src = ''; autoSave();">×</button>
                                                            </div>
                                                        </template>
                                                        <template x-if="!blk.src">
                                                            <div class="sb-blk-image-placeholder" @dblclick.stop="sbBlockImageTarget = blk; $refs.sbBlockImageInput.click();">
                                                                더블클릭하여 이미지 업로드
                                                            </div>
                                                        </template>
                                                    </div>
                                                </template>

                                                {{-- Marker (description number) --}}
                                                <template x-if="blk.type === 'marker'">
                                                    <div class="sb-blk-marker" x-text="blk.content || '01'"></div>
                                                </template>
                                            </div>
                                        </template>
                                    </div>

                                    {{-- Floating Format Toolbar (블록 선택 시 위에 나타남) --}}
                                    <template x-if="sbFormatBar && sbSelectedBlock">
                                        <div class="sb-format-bar" :style="'left:' + sbFormatBar.x + 'px; top:' + sbFormatBar.y + 'px;'"
                                             @mousedown.stop @click.stop>
                                            {{-- Bold / Italic --}}
                                            <button class="sb-fmt-btn" :class="{ active: sbGetBlk()?.style?.bold }"
                                                    @click="sbToggleStyle('bold')" title="굵게 (B)"><b>B</b></button>
                                            <button class="sb-fmt-btn" :class="{ active: sbGetBlk()?.style?.italic }"
                                                    @click="sbToggleStyle('italic')" title="기울임 (I)"><i>I</i></button>
                                            <div class="sb-fmt-sep"></div>
                                            {{-- Text Align --}}
                                            <button class="sb-fmt-btn" :class="{ active: !sbGetBlk()?.style?.textAlign || sbGetBlk()?.style?.textAlign === 'left' }"
                                                    @click="sbSetStyle('textAlign', 'left')" title="왼쪽 정렬">☰</button>
                                            <button class="sb-fmt-btn" :class="{ active: sbGetBlk()?.style?.textAlign === 'center' }"
                                                    @click="sbSetStyle('textAlign', 'center')" title="가운데 정렬">≡</button>
                                            <button class="sb-fmt-btn" :class="{ active: sbGetBlk()?.style?.textAlign === 'right' }"
                                                    @click="sbSetStyle('textAlign', 'right')" title="오른쪽 정렬">≡</button>
                                            <div class="sb-fmt-sep"></div>
                                            {{-- Font Color --}}
                                            <div style="position:relative;">
                                                <button class="sb-fmt-btn" @click.stop="sbFmtDropdown = sbFmtDropdown === 'color' ? null : 'color'" title="글자색">
                                                    <span style="font-weight:700;">A</span>
                                                    <span style="position:absolute;bottom:3px;left:6px;right:6px;height:3px;border-radius:1px;"
                                                          :style="'background:' + (sbGetBlk()?.style?.fontColor || '#334155')"></span>
                                                </button>
                                                <div class="sb-fmt-dropdown" x-show="sbFmtDropdown === 'color'" x-cloak @click.stop>
                                                    <div class="sb-fmt-dropdown-title">글자색</div>
                                                    <div class="sb-fmt-color-grid">
                                                        <template x-for="c in ['#1e293b','#334155','#64748b','#ef4444','#f97316','#eab308','#22c55e','#3b82f6','#8b5cf6','#ec4899','#06b6d4','#fff']" :key="'fc'+c">
                                                            <div class="sb-fmt-color-dot" :class="{ active: sbGetBlk()?.style?.fontColor === c }"
                                                                 :style="'background:' + c + (c==='#fff' ? ';border:1px solid #e2e8f0' : '')"
                                                                 @click="sbSetStyle('fontColor', c); sbFmtDropdown = null;"></div>
                                                        </template>
                                                    </div>
                                                </div>
                                            </div>
                                            {{-- Background Color --}}
                                            <div style="position:relative;">
                                                <button class="sb-fmt-btn" @click.stop="sbFmtDropdown = sbFmtDropdown === 'bgColor' ? null : 'bgColor'" title="배경색">
                                                    <span style="width:16px;height:14px;border-radius:3px;display:inline-block;"
                                                          :style="'background:' + (sbGetBlk()?.style?.bgColor || '#fff') + ';border:1px solid #475569;'"></span>
                                                </button>
                                                <div class="sb-fmt-dropdown" x-show="sbFmtDropdown === 'bgColor'" x-cloak @click.stop>
                                                    <div class="sb-fmt-dropdown-title">배경색</div>
                                                    <div class="sb-fmt-color-grid">
                                                        <template x-for="c in ['#fff','#f8fafc','#f1f5f9','#fef2f2','#fff7ed','#fefce8','#ecfdf5','#eff6ff','#f5f3ff','#fdf2f8','#ecfeff','transparent']" :key="'bg'+c">
                                                            <div class="sb-fmt-color-dot"
                                                                 :class="{ active: sbGetBlk()?.style?.bgColor === c }"
                                                                 :style="'background:' + (c === 'transparent' ? 'repeating-conic-gradient(#ddd 0% 25%, #fff 0% 50%) 50%/12px 12px' : c) + ';border:1px solid #e2e8f0;'"
                                                                 @click="sbSetStyle('bgColor', c === 'transparent' ? '' : c); sbFmtDropdown = null;"></div>
                                                        </template>
                                                    </div>
                                                </div>
                                            </div>
                                            <div class="sb-fmt-sep"></div>
                                            {{-- Font Size --}}
                                            <div style="position:relative;">
                                                <button class="sb-fmt-btn" @click.stop="sbFmtDropdown = sbFmtDropdown === 'fontSize' ? null : 'fontSize'" title="글자 크기"
                                                        style="width:auto;padding:0 6px;font-size:10px;color:#cbd5e1;">
                                                    <span x-text="(sbGetBlk()?.style?.fontSize || 13) + 'px'"></span>
                                                </button>
                                                <div class="sb-fmt-dropdown" x-show="sbFmtDropdown === 'fontSize'" x-cloak @click.stop style="min-width:100px;">
                                                    <div class="sb-fmt-dropdown-title">글자 크기</div>
                                                    <template x-for="s in [10,11,12,13,14,16,18,20,24]" :key="'fs'+s">
                                                        <div class="sb-fmt-size-option" :class="{ active: sbGetBlk()?.style?.fontSize === s }"
                                                             @click="sbSetStyle('fontSize', s); sbFmtDropdown = null;">
                                                            <span x-text="s + 'px'"></span>
                                                            <span style="font-size:9px;color:#64748b;" x-text="s <= 11 ? '작게' : s <= 13 ? '보통' : s <= 16 ? '크게' : '특대'"></span>
                                                        </div>
                                                    </template>
                                                </div>
                                            </div>
                                            <div class="sb-fmt-sep"></div>
                                            {{-- Layer (z-index) --}}
                                            <button class="sb-fmt-btn" @click="sbBringForward()" title="앞으로 가져오기" style="font-size:10px;">▲</button>
                                            <button class="sb-fmt-btn" @click="sbSendBackward()" title="뒤로 보내기" style="font-size:10px;">▼</button>
                                            <div class="sb-fmt-sep"></div>
                                            {{-- Reset --}}
                                            <button class="sb-fmt-btn" @click="sbResetStyle()" title="서식 초기화" style="font-size:11px;">↺</button>
                                        </div>
                                    </template>

                                    {{-- Right-Click Context Menu --}}
                                    <template x-if="sbCtxMenu">
                                        <div class="sb-ctx-menu" :style="'left:' + sbCtxMenu.x + 'px; top:' + sbCtxMenu.y + 'px;'"
                                             @click.outside="sbCtxMenu = null; sbCtxSub = null;"
                                             @contextmenu.prevent>
                                            <div class="sb-ctx-item" @click="sbCtxCopy()">
                                                <span class="sb-ctx-icon">⧉</span><span class="sb-ctx-label">복제</span><span class="sb-ctx-shortcut">Ctrl+C → V</span>
                                            </div>
                                            <div class="sb-ctx-item" @click="sbCtxCut()">
                                                <span class="sb-ctx-icon">✂</span><span class="sb-ctx-label">잘라내기</span><span class="sb-ctx-shortcut">Ctrl+X</span>
                                            </div>
                                            <div class="sb-ctx-item danger" @click="sbCtxDelete()">
                                                <span class="sb-ctx-icon">🗑</span><span class="sb-ctx-label">삭제</span><span class="sb-ctx-shortcut">Del</span>
                                            </div>
                                            <div class="sb-ctx-sep"></div>
                                            {{-- 글자색 서브메뉴 --}}
                                            <div class="sb-ctx-sub" @mouseenter="sbCtxSub = 'color'" @mouseleave="sbCtxSub = null">
                                                <div class="sb-ctx-item">
                                                    <span class="sb-ctx-icon">🎨</span><span class="sb-ctx-label">글자색</span><span class="sb-ctx-shortcut">▸</span>
                                                </div>
                                                <div class="sb-ctx-sub-panel" x-show="sbCtxSub === 'color'" x-cloak>
                                                    <div class="sb-ctx-color-grid">
                                                        <template x-for="c in ['#1e293b','#334155','#64748b','#ef4444','#f97316','#eab308','#22c55e','#3b82f6','#8b5cf6','#ec4899','#06b6d4','#fff']" :key="'ctx-fc-'+c">
                                                            <div class="sb-ctx-color-swatch" :class="{ active: sbCtxGetBlk()?.style?.fontColor === c }"
                                                                 :style="'background:' + c + (c==='#fff' ? ';border:1px solid #e2e8f0' : '')"
                                                                 @click="sbCtxSetStyle('fontColor', c)"></div>
                                                        </template>
                                                    </div>
                                                </div>
                                            </div>
                                            {{-- 배경색 서브메뉴 --}}
                                            <div class="sb-ctx-sub" @mouseenter="sbCtxSub = 'bgColor'" @mouseleave="sbCtxSub = null">
                                                <div class="sb-ctx-item">
                                                    <span class="sb-ctx-icon">🖌</span><span class="sb-ctx-label">배경색</span><span class="sb-ctx-shortcut">▸</span>
                                                </div>
                                                <div class="sb-ctx-sub-panel" x-show="sbCtxSub === 'bgColor'" x-cloak>
                                                    <div class="sb-ctx-color-grid">
                                                        <template x-for="c in ['#fff','#f8fafc','#f1f5f9','#fef2f2','#fff7ed','#fefce8','#ecfdf5','#eff6ff','#f5f3ff','#fdf2f8','#ecfeff','transparent']" :key="'ctx-bg-'+c">
                                                            <div class="sb-ctx-color-swatch"
                                                                 :class="{ active: sbCtxGetBlk()?.style?.bgColor === c }"
                                                                 :style="'background:' + (c === 'transparent' ? 'repeating-conic-gradient(#ddd 0% 25%, #fff 0% 50%) 50%/12px 12px' : c) + ';border:1px solid #e2e8f0;'"
                                                                 @click="sbCtxSetStyle('bgColor', c === 'transparent' ? '' : c)"></div>
                                                        </template>
                                                    </div>
                                                </div>
                                            </div>
                                            <div class="sb-ctx-sep"></div>
                                            {{-- 정렬 --}}
                                            <div class="sb-ctx-item" @click="sbCtxSetStyle('textAlign', 'left')">
                                                <span class="sb-ctx-icon">☰</span><span class="sb-ctx-label">왼쪽 정렬</span>
                                            </div>
                                            <div class="sb-ctx-item" @click="sbCtxSetStyle('textAlign', 'center')">
                                                <span class="sb-ctx-icon">≡</span><span class="sb-ctx-label">가운데 정렬</span>
                                            </div>
                                            <div class="sb-ctx-item" @click="sbCtxSetStyle('textAlign', 'right')">
                                                <span class="sb-ctx-icon">≡</span><span class="sb-ctx-label">오른쪽 정렬</span>
                                            </div>
                                            <div class="sb-ctx-sep"></div>
                                            {{-- 레이어 순서 --}}
                                            <div class="sb-ctx-item" @click="sbCtxBringForward()">
                                                <span class="sb-ctx-icon">▲</span><span class="sb-ctx-label">앞으로 가져오기</span>
                                            </div>
                                            <div class="sb-ctx-item" @click="sbCtxSendBackward()">
                                                <span class="sb-ctx-icon">▼</span><span class="sb-ctx-label">뒤로 보내기</span>
                                            </div>
                                            <div class="sb-ctx-sep"></div>
                                            {{-- 서식 --}}
                                            <div class="sb-ctx-item" @click="sbCtxToggleStyle('bold')">
                                                <span class="sb-ctx-icon"><b>B</b></span><span class="sb-ctx-label" x-text="sbCtxGetBlk()?.style?.bold ? '굵게 해제' : '굵게'"></span>
                                            </div>
                                            <div class="sb-ctx-item" @click="sbCtxToggleStyle('italic')">
                                                <span class="sb-ctx-icon"><i>I</i></span><span class="sb-ctx-label" x-text="sbCtxGetBlk()?.style?.italic ? '기울임 해제' : '기울임'"></span>
                                            </div>
                                            <div class="sb-ctx-sep"></div>
                                            <div class="sb-ctx-item" @click="sbCtxResetStyle()">
                                                <span class="sb-ctx-icon">↺</span><span class="sb-ctx-label">서식 초기화</span>
                                            </div>
                                        </div>
                                    </template>

                                    {{-- Desc Toggle + Resizer --}}
                                    <div class="sb-desc-toggle-bar" @click="sbDescOpen = !sbDescOpen">
                                        <span class="sb-desc-toggle-label">Description</span>
                                        <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                                            <polyline :points="sbDescOpen ? '18 15 12 9 6 15' : '6 9 12 15 18 9'"/>
                                        </svg>
                                    </div>
                                    <template x-if="sbDescOpen">
                                        <div class="sb-desc-resizer"
                                             @mousedown.stop.prevent="_sbDescResize = { startY: $event.clientY, startH: sbDescHeight }; $event.target.classList.add('active')"></div>
                                    </template>
                                    {{-- Description Panel --}}
                                    <template x-if="sbDescOpen">
                                        <div class="sb-desc-panel" :style="'height:' + sbDescHeight + 'px'">
                                            <template x-for="(desc, idx) in (sbCurrentPage.descriptions || [])" :key="idx">
                                                <div class="sb-desc-item">
                                                    <div class="sb-desc-num"
                                                         x-text="String(idx + 1).padStart(2, '0')"
                                                         draggable="true"
                                                         @dragstart="$event.dataTransfer.setData('text/plain', 'marker:' + String(idx + 1).padStart(2, '0')); $event.dataTransfer.effectAllowed = 'copy';"></div>
                                                    <div class="sb-desc-text">
                                                        <textarea x-model="desc.text" rows="2" placeholder="기능 설명을 입력하세요..." @input="autoSave()"></textarea>
                                                    </div>
                                                    <button class="sb-desc-remove" @click="sbCurrentPage.descriptions.splice(idx, 1); autoSave();">&times;</button>
                                                </div>
                                            </template>
                                            <div class="sb-desc-add" @click="sbAddDescription()">+ Description 항목 추가</div>
                                        </div>
                                    </template>
                                </div>
                            </div>
                        </div>
                    </template>
                </div>
            </div>
        </div>

        </div>{{-- /Main Content Area --}}
    </div>

    {{-- Menu Tree Editor Modal --}}
    <template x-if="sbMenuEditorOpen">
        <div class="sb-menu-modal-overlay" @click.self="sbMenuEditorOpen = false" @keydown.escape.window="sbMenuEditorOpen = false">
            <div class="sb-menu-modal" @click.stop>
                <div class="sb-menu-modal-header">
                    <h3>ERP 메뉴 트리 편집</h3>
                    <button class="sb-mt-btn" @click="sbMenuEditorOpen = false" style="width:28px;height:28px;font-size:16px;">×</button>
                </div>
                <div class="sb-menu-modal-body">
                    <template x-for="(menu, mi) in sbMenuDraft" :key="mi">
                        <div>
                            <div class="sb-mt-item" draggable="true"
                                 @dragstart="sbMenuDragStart('root', mi, $event)"
                                 @dragover.prevent="sbMenuDragOver('root', mi, $event)"
                                 @drop="sbMenuDrop('root', mi, $event)"
                                 @dragend="sbMenuDragEnd()">
                                <span class="sb-mt-drag" title="드래그하여 순서 변경">⠿</span>
                                <span class="sb-mt-icon" @click="sbMenuToggle(mi)" x-text="(menu.children && menu.children.length) ? (menu._open !== false ? '▾' : '▸') : '·'"></span>
                                <input type="text" class="sb-mt-name" x-model="menu.name" placeholder="메뉴명 입력" style="font-weight:600;">
                                <div class="sb-mt-actions">
                                    <button class="sb-mt-btn" @click="sbMenuAddChild(mi)" title="하위 메뉴 추가">+</button>
                                    <button class="sb-mt-btn danger" @click="sbMenuRemove(mi)" title="삭제">×</button>
                                </div>
                            </div>
                            <div class="sb-mt-children" x-show="menu._open !== false">
                                <template x-for="(child, ci) in (menu.children || [])" :key="ci">
                                    <div class="sb-mt-item" draggable="true"
                                         @dragstart="sbMenuDragStart('child', mi + '-' + ci, $event)"
                                         @dragover.prevent="sbMenuDragOver('child', mi + '-' + ci, $event)"
                                         @drop="sbMenuDrop('child', mi + '-' + ci, $event)"
                                         @dragend="sbMenuDragEnd()">
                                        <span class="sb-mt-drag" title="드래그하여 순서 변경">⠿</span>
                                        <span class="sb-mt-icon" style="color:#cbd5e1;">─</span>
                                        <input type="text" class="sb-mt-name" x-model="child.name" placeholder="하위 메뉴명">
                                        <div class="sb-mt-actions">
                                            <button class="sb-mt-btn danger" @click="menu.children.splice(ci, 1)" title="삭제">×</button>
                                        </div>
                                    </div>
                                </template>
                                <div class="sb-mt-add-root" style="padding:4px 0 2px; font-size:11px;" @click="sbMenuAddChild(mi)">+ 하위 메뉴 추가</div>
                            </div>
                        </div>
                    </template>
                    <div class="sb-mt-add-root" @click="sbMenuDraft.push({ name: '', children: [], _open: true })">+ 상위 메뉴 추가</div>
                </div>
                <div class="sb-menu-modal-footer">
                    <button style="padding:6px 16px; border:1px solid #e2e8f0; border-radius:6px; font-size:12px; cursor:pointer; background:#fff; color:#64748b;"
                            @click="sbMenuEditorOpen = false">취소</button>
                    <button style="padding:6px 16px; border:none; border-radius:6px; font-size:12px; cursor:pointer; background:var(--pc-indigo); color:#fff; font-weight:600;"
                            @click="sbMenuApply()">적용</button>
                </div>
            </div>
        </div>
    </template>

    {{-- Node Detail Modal --}}
    <template x-if="modalNode">
        <div class="pc-modal-overlay" @click.self="closeNodeModal()" @keydown.escape.window="closeNodeModal()">
            <div class="pc-modal">
                <div class="pc-modal-header">
                    <span class="emoji" x-text="modalNode.emoji || '📌'"></span>
                    <input type="text" x-model="modalNode.title" placeholder="제목을 입력하세요" @input="onModalChange()">
                    <button class="pc-modal-close" @click="closeNodeModal()">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
                    </button>
                </div>
                <div class="pc-modal-body">
                    <div class="pc-modal-row">
                        <div class="pc-modal-label">상태</div>
                        <div class="pc-modal-field">
                            <select x-model="modalNode.status" @change="onModalChange()">
                                <option value="todo">대기</option>
                                <option value="progress">진행중</option>
                                <option value="review">검토중</option>
                                <option value="done">완료</option>
                            </select>
                        </div>
                    </div>
                    <div class="pc-modal-row">
                        <div class="pc-modal-label">우선순위</div>
                        <div class="pc-modal-field">
                            <select x-model="modalNode.priority" @change="onModalChange()">
                                <option value="low">낮음</option>
                                <option value="medium">보통</option>
                                <option value="high">높음</option>
                                <option value="critical">긴급</option>
                            </select>
                        </div>
                    </div>
                    <div class="pc-modal-row">
                        <div class="pc-modal-label">담당자</div>
                        <div class="pc-modal-field">
                            <input type="text" x-model="modalNode.assignee" placeholder="담당자 이름" @input="onModalChange()">
                        </div>
                    </div>
                    <div class="pc-modal-row">
                        <div class="pc-modal-label">마감일</div>
                        <div class="pc-modal-field">
                            <input type="date" x-model="modalNode.dueDate" @change="onModalChange()">
                        </div>
                    </div>
                    <div class="pc-modal-row">
                        <div class="pc-modal-label">태그</div>
                        <div class="pc-modal-field">
                            <input type="text" :value="(modalNode.tags||[]).join(', ')" placeholder="콤마로 구분"
                                   @change="modalNode.tags = $event.target.value.split(',').map(s=>s.trim()).filter(Boolean); onModalChange()">
                        </div>
                    </div>
                    <div class="pc-modal-row">
                        <div class="pc-modal-label">설명</div>
                        <div class="pc-modal-field">
                            <textarea x-model="modalNode.description" placeholder="상세 설명을 입력하세요..." rows="4" @input="onModalChange()"></textarea>
                        </div>
                    </div>
                    <div class="pc-modal-row">
                        <div class="pc-modal-label">체크리스트</div>
                        <div class="pc-modal-field">
                            <template x-if="modalNode.checklist && modalNode.checklist.length > 0">
                                <div class="pc-checklist-progress">
                                    <div class="pc-checklist-progress-bar" :style="'width:' + checklistProgress(modalNode) + '%'"></div>
                                </div>
                            </template>
                            <ul class="pc-checklist">
                                <template x-for="(item, idx) in (modalNode.checklist || [])" :key="idx">
                                    <li class="pc-checklist-item">
                                        <input type="checkbox" x-model="item.done" @change="onModalChange()">
                                        <input type="text" class="text" :class="{ done: item.done }" x-model="item.text" @input="onModalChange()" placeholder="할 일 입력...">
                                        <button class="remove" @click="modalNode.checklist.splice(idx,1); onModalChange();">&times;</button>
                                    </li>
                                </template>
                            </ul>
                            <div class="pc-checklist-add" @click="addChecklistItem()">+ 항목 추가</div>
                        </div>
                    </div>
                    <div class="pc-modal-row">
                        <div class="pc-modal-label">색상</div>
                        <div class="pc-modal-field">
                            <div class="pc-color-swatches">
                                <template x-for="c in nodeColors" :key="c.value">
                                    <div class="pc-color-swatch"
                                         :class="{ active: modalNode.color === c.value }"
                                         :style="'background:' + c.value"
                                         @click="modalNode.color = c.value; onModalChange()"></div>
                                </template>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </template>

    {{-- Context Menu --}}
    <div class="pc-context-menu" id="contextMenu" @click.away="hideContextMenu()">
        <div class="pc-cm-item" @click="duplicateNode(); hideContextMenu();">복제</div>
        <div class="pc-cm-item" @click="bringToFront(); hideContextMenu();">맨 앞으로</div>
        <div class="pc-cm-sep"></div>
        <div class="pc-cm-item" @click="addNodeAtMouse(paletteItems.planning[0]); hideContextMenu();">빠른 노드 추가</div>
        <div class="pc-cm-sep"></div>
        <div class="pc-cm-item danger" x-show="selectedConnection" @click="deleteSelectedConnection(); hideContextMenu();">연결선 삭제</div>
        <div class="pc-cm-item danger" x-show="selectedNode" @click="deleteSelectedNode(); hideContextMenu();">노드 삭제</div>
    </div>
</div>

@endsection

@push('scripts')
<script>
function planningCanvas() {
    let idCounter = 0;
    const STORAGE_KEY = 'pc_projects';
    const CURRENT_KEY = 'pc_current';

    return {
        // State
        projectTitle: '새 기획 프로젝트',
        currentProjectId: null,
        nodes: [],
        connections: [],
        savedProjects: [],

        // Tools
        tool: 'select',
        viewMode: 'free',
        sidebarOpen: true,
        sidebarTab: 'palette',
        zoom: 1,
        panX: 0,
        panY: 0,

        // Selection
        selectedNode: null,
        selectedConnection: null,
        modalNode: null,

        // Filters
        filterText: '',
        filterStatus: '',
        filterPriority: '',
        filterType: '',

        // List sort
        listSortKey: 'title',
        listSortDir: 'asc',

        // Kanban
        kanbanColumns: [
            { key: 'todo', label: '대기', color: '#94a3b8' },
            { key: 'progress', label: '진행중', color: '#3b82f6' },
            { key: 'review', label: '검토중', color: '#f59e0b' },
            { key: 'done', label: '완료', color: '#10b981' },
        ],
        _kanbanDragNodeId: null,
        _connTick: 0,

        // Storyboard
        sbMenuEditorOpen: false,
        sbMenuDraft: [],
        _sbMenuDrag: null,
        sbSelectedBlock: null,
        sbEditingBlock: null,
        sbBlockImageTarget: null,
        _sbBlockDragIdx: null,
        _sbDrag: null,   // { blk, startX, startY, origX, origY }
        _sbResize: null, // { blk, dir, startX, startY, origW, origH }
        _sbClipboard: null, // copied block data
        sbMenuWidth: 160,
        sbMenuOpen: true,
        sbDescOpen: true,
        sbDescHeight: 200,
        sbMarkerNum: '01',
        _sbMenuResize: null,
        _sbDescResize: null,
        sbMultiSelected: [],   // 다중 선택 블록 id 배열
        _sbLasso: null,        // { startX, startY, rx, ry, rw, rh }
        _sbLassoDone: false,   // 올가미 완료 직후 click.self 방지 플래그
        _sbMultiDrag: null,    // { startX, startY, origins: [{id, x, y}] }
        _sbHistory: [],
        _sbHistoryIdx: -1,
        _sbHistoryPaused: false,
        // 서식 툴바 / 컨텍스트 메뉴
        sbFormatBar: null,      // { x, y } — 선택된 블록 위에 표시
        sbFmtDropdown: null,    // 'color' | 'bgColor' | 'fontSize' | null
        sbCtxMenu: null,        // { x, y, blockId, blockIdx } — 우클릭 메뉴
        sbCtxSub: null,         // 'color' | 'bgColor' | 'align' | null — 하위 메뉴
        sbTplOpen: false,
        sbTplTab: 'preset',
        sbTplSearch: '',
        sbTplSaveName: '',
        sbCustomTemplates: [],
        sb: {
            docInfo: { projectName: '', unitTask: '', version: 'D1.0' },
            menuTree: [
                { name: '대시보드', children: [] },
                { name: '판매관리', children: [] },
                { name: '생산관리', children: [] },
                { name: '출고관리', children: [] },
                { name: '품질관리', children: [
                    { name: '제품검사관리' },
                    { name: '실적신고관리' },
                    { name: '품질인정심사' },
                ]},
                { name: '자재관리', children: [] },
                { name: '기준정보', children: [] },
            ],
            pages: [],
            currentPageIndex: 0,
        },

        // Drag State
        dragging: false,
        dragNode: null,
        dragOffsetX: 0,
        dragOffsetY: 0,
        panning: false,
        panStartX: 0,
        panStartY: 0,
        spaceHeld: false,

        // Connection Drawing
        drawingConnection: false,
        connSource: null,
        connSourcePort: null,
        tempConnectionPath: '',
        contextMenuPos: { x: 0, y: 0 },

        // Undo/Redo
        history: [],
        historyIndex: -1,

        // Phases for timeline
        phases: [
            { id: 'discover', name: '발견 (Discover)' },
            { id: 'define', name: '정의 (Define)' },
            { id: 'design', name: '설계 (Design)' },
            { id: 'develop', name: '개발 (Develop)' },
            { id: 'deliver', name: '검증 (Deliver)' },
        ],

        // Colors
        nodeColors: [
            { value: '#6366f1', label: 'Indigo' },
            { value: '#3b82f6', label: 'Blue' },
            { value: '#10b981', label: 'Emerald' },
            { value: '#f59e0b', label: 'Amber' },
            { value: '#f43f5e', label: 'Rose' },
            { value: '#8b5cf6', label: 'Violet' },
            { value: '#06b6d4', label: 'Cyan' },
            { value: '#64748b', label: 'Slate' },
        ],

        // Node Palette
        paletteItems: {
            planning: [
                { type: 'vision', label: '비전/목표', emoji: '🎯', bg: '#eef2ff', color: '#6366f1', typeLabel: '비전' },
                { type: 'persona', label: '페르소나', emoji: '👤', bg: '#fef3c7', color: '#f59e0b', typeLabel: '페르소나' },
                { type: 'problem', label: '문제정의', emoji: '❗', bg: '#fce7f3', color: '#ec4899', typeLabel: '문제' },
                { type: 'hypothesis', label: '가설', emoji: '💡', bg: '#ecfdf5', color: '#10b981', typeLabel: '가설' },
                { type: 'requirement', label: '요구사항', emoji: '📋', bg: '#ede9fe', color: '#8b5cf6', typeLabel: '요구사항' },
                { type: 'constraint', label: '제약조건', emoji: '🔒', bg: '#fef2f2', color: '#ef4444', typeLabel: '제약' },
            ],
            analysis: [
                { type: 'usecase', label: '유스케이스', emoji: '🔄', bg: '#dbeafe', color: '#3b82f6', typeLabel: 'UC' },
                { type: 'userflow', label: '사용자흐름', emoji: '🚶', bg: '#e0f2fe', color: '#0ea5e9', typeLabel: 'Flow' },
                { type: 'datamodel', label: '데이터모델', emoji: '🗃️', bg: '#f0fdf4', color: '#22c55e', typeLabel: 'Data' },
                { type: 'api', label: 'API 설계', emoji: '🔌', bg: '#fefce8', color: '#eab308', typeLabel: 'API' },
            ],
            structure: [
                { type: 'decision', label: '의사결정', emoji: '⚖️', bg: '#fff7ed', color: '#f97316', typeLabel: '결정' },
                { type: 'milestone', label: '마일스톤', emoji: '🏁', bg: '#ede9fe', color: '#7c3aed', typeLabel: '마일스톤' },
                { type: 'phase', label: '단계/Phase', emoji: '📐', bg: '#e0e7ff', color: '#4f46e5', typeLabel: 'Phase' },
                { type: 'note', label: '메모', emoji: '📝', bg: '#f8fafc', color: '#64748b', typeLabel: '메모' },
            ],
            output: [
                { type: 'wireframe', label: '와이어프레임', emoji: '🖼️', bg: '#ecfeff', color: '#06b6d4', typeLabel: 'WF' },
                { type: 'prototype', label: '프로토타입', emoji: '🧪', bg: '#faf5ff', color: '#a855f7', typeLabel: 'Proto' },
                { type: 'document', label: '문서/산출물', emoji: '📄', bg: '#f0f9ff', color: '#0284c7', typeLabel: '문서' },
                { type: 'presentation', label: '발표자료', emoji: '📊', bg: '#fffbeb', color: '#d97706', typeLabel: 'PPT' },
            ],
        },

        get sbCurrentPage() {
            return this.sb.pages[this.sb.currentPageIndex] || null;
        },

        get sbPageBlocks() {
            const page = this.sbCurrentPage;
            if (!page) return [];
            if (!page.blocks) page.blocks = [];
            return page.blocks;
        },

        get sbPresetTemplates() {
            return [
                { name: '검색 + 목록 화면', icon: '🔍', desc: '검색조건 + 데이터 테이블', blocks: [
                    { type: 'heading2', content: '검색 조건' },
                    { type: 'input', label: '검색어', placeholder: '품명, 품번 등을 입력하세요' },
                    { type: 'badges', items: [
                        { text: '검색', color: '#4338ca', textColor: '#fff' },
                        { text: '초기화', color: '#f1f5f9', textColor: '#475569' },
                    ]},
                    { type: 'divider', content: '' },
                    { type: 'heading2', content: '목록' },
                    { type: 'table', cols: ['No', '항목명', '상태', '담당자', '등록일', '비고'], rows: [
                        ['1', '샘플 항목 A', '진행중', '홍길동', '2026-03-01', ''],
                        ['2', '샘플 항목 B', '대기', '김철수', '2026-03-02', ''],
                        ['3', '', '', '', '', ''],
                    ]},
                    { type: 'badges', items: [
                        { text: '총 3건', color: '#e0e7ff', textColor: '#4338ca' },
                    ]},
                ]},
                { name: '상세 정보 폼', icon: '📝', desc: '라벨+입력 필드 그룹', blocks: [
                    { type: 'heading', content: '상세 정보' },
                    { type: 'input', label: '품명', placeholder: '품명을 입력하세요' },
                    { type: 'input', label: '품번', placeholder: '품번을 입력하세요' },
                    { type: 'select', label: '분류', placeholder: '분류를 선택하세요' },
                    { type: 'input', label: '규격', placeholder: '규격 정보' },
                    { type: 'text', content: '' },
                    { type: 'divider', content: '' },
                    { type: 'badges', items: [
                        { text: '저장', color: '#4338ca', textColor: '#fff' },
                        { text: '취소', color: '#f1f5f9', textColor: '#475569' },
                    ]},
                ]},
                { name: 'CRUD 화면', icon: '⚙️', desc: '제목 + 검색 + 테이블 + 버튼 조합', blocks: [
                    { type: 'heading', content: '관리 화면 제목' },
                    { type: 'text', content: '화면 설명 텍스트를 입력하세요.' },
                    { type: 'divider', content: '' },
                    { type: 'heading2', content: '검색 조건' },
                    { type: 'input', label: '검색어', placeholder: '검색어 입력' },
                    { type: 'select', label: '상태', placeholder: '전체' },
                    { type: 'badges', items: [
                        { text: '검색', color: '#4338ca', textColor: '#fff' },
                        { text: '초기화', color: '#f1f5f9', textColor: '#475569' },
                        { text: '+ 신규등록', color: '#10b981', textColor: '#fff' },
                        { text: '엑셀 다운로드', color: '#f1f5f9', textColor: '#475569' },
                    ]},
                    { type: 'divider', content: '' },
                    { type: 'table', cols: ['선택', 'No', '항목명', '분류', '상태', '담당자', '수정일'], rows: [
                        ['☐', '1', '', '', '', '', ''],
                        ['☐', '2', '', '', '', '', ''],
                    ]},
                    { type: 'badges', items: [
                        { text: '선택 삭제', color: '#fef2f2', textColor: '#ef4444' },
                        { text: '일괄 수정', color: '#e0e7ff', textColor: '#4338ca' },
                    ]},
                ]},
                { name: '대시보드 카드', icon: '📊', desc: '통계 카드 + 요약 테이블', blocks: [
                    { type: 'heading', content: '대시보드' },
                    { type: 'card', title: '총 주문건', content: '1,234건 (+12.5% ↑)' },
                    { type: 'card', title: '매출 합계', content: '₩85,600,000' },
                    { type: 'card', title: '미처리 건수', content: '23건' },
                    { type: 'divider', content: '' },
                    { type: 'heading2', content: '최근 현황' },
                    { type: 'table', cols: ['구분', '이번 달', '전월', '증감률'], rows: [
                        ['수주', '450건', '380건', '+18.4%'],
                        ['출하', '420건', '395건', '+6.3%'],
                        ['반품', '12건', '18건', '-33.3%'],
                    ]},
                ]},
                { name: '결재/승인 폼', icon: '✅', desc: '결재라인 + 체크리스트 + 버튼', blocks: [
                    { type: 'heading', content: '결재 요청서' },
                    { type: 'table', cols: ['구분', '기안자', '검토', '승인'], rows: [
                        ['직급', '사원', '과장', '부장'],
                        ['성명', '', '', ''],
                        ['상태', '기안', '대기', '대기'],
                    ]},
                    { type: 'divider', content: '' },
                    { type: 'heading2', content: '요청 내용' },
                    { type: 'text', content: '결재 요청 상세 내용을 입력하세요.' },
                    { type: 'divider', content: '' },
                    { type: 'todo', items: [
                        { text: '첨부파일 확인', checked: false },
                        { text: '금액 확인', checked: false },
                        { text: '기안 내용 검토', checked: false },
                    ]},
                    { type: 'badges', items: [
                        { text: '결재 요청', color: '#4338ca', textColor: '#fff' },
                        { text: '임시 저장', color: '#f1f5f9', textColor: '#475569' },
                    ]},
                ]},
                { name: '탭 레이아웃', icon: '📑', desc: '탭 메뉴 + 콘텐츠 영역', blocks: [
                    { type: 'heading', content: '화면 제목' },
                    { type: 'badges', items: [
                        { text: '기본정보', color: '#4338ca', textColor: '#fff' },
                        { text: 'BOM', color: '#f1f5f9', textColor: '#475569' },
                        { text: '이력', color: '#f1f5f9', textColor: '#475569' },
                        { text: '첨부파일', color: '#f1f5f9', textColor: '#475569' },
                    ]},
                    { type: 'divider', content: '' },
                    { type: 'callout', icon: 'ℹ️', content: '선택한 탭의 콘텐츠가 여기에 표시됩니다.' },
                    { type: 'text', content: '' },
                ]},
                { name: '팝업/모달', icon: '💬', desc: '모달 다이얼로그 UI', blocks: [
                    { type: 'card', title: '모달 제목', content: '' },
                    { type: 'text', content: '모달 본문 내용을 입력하세요.' },
                    { type: 'input', label: '입력 항목', placeholder: '값을 입력하세요' },
                    { type: 'divider', content: '' },
                    { type: 'badges', items: [
                        { text: '확인', color: '#4338ca', textColor: '#fff' },
                        { text: '취소', color: '#f1f5f9', textColor: '#475569' },
                    ]},
                ]},
                { name: '로그인 화면', icon: '🔐', desc: '로그인 폼 UI', blocks: [
                    { type: 'heading', content: '로그인' },
                    { type: 'text', content: '시스템에 접속하려면 로그인하세요.' },
                    { type: 'input', label: '아이디', placeholder: 'user@company.com' },
                    { type: 'input', label: '비밀번호', placeholder: '••••••••' },
                    { type: 'todo', items: [
                        { text: '자동 로그인', checked: false },
                    ]},
                    { type: 'button', content: '로그인', color: '#4338ca' },
                    { type: 'text', content: '비밀번호를 잊으셨나요?' },
                ]},
                { name: '빈 페이지 (기본 구조)', icon: '📄', desc: '제목 + 설명 + 구분선', blocks: [
                    { type: 'heading', content: '' },
                    { type: 'text', content: '' },
                    { type: 'divider', content: '' },
                ]},
            ];
        },

        get sbFilteredPresets() {
            const q = this.sbTplSearch.toLowerCase().trim();
            if (!q) return this.sbPresetTemplates;
            return this.sbPresetTemplates.filter(t => t.name.toLowerCase().includes(q) || t.desc.toLowerCase().includes(q));
        },

        get sbFilteredCustoms() {
            const q = this.sbTplSearch.toLowerCase().trim();
            if (!q) return this.sbCustomTemplates;
            return this.sbCustomTemplates.filter(t => t.name.toLowerCase().includes(q));
        },

        get allPaletteItems() {
            return [
                ...this.paletteItems.planning,
                ...this.paletteItems.analysis,
                ...this.paletteItems.structure,
                ...this.paletteItems.output,
            ];
        },

        get filteredNodes() {
            return this.nodes.filter(n => {
                if (this.filterStatus && n.status !== this.filterStatus) return false;
                if (this.filterPriority && n.priority !== this.filterPriority) return false;
                if (this.filterType && n.type !== this.filterType) return false;
                if (this.filterText) {
                    const q = this.filterText.toLowerCase();
                    if (!(n.title||'').toLowerCase().includes(q) &&
                        !(n.description||'').toLowerCase().includes(q) &&
                        !(n.assignee||'').toLowerCase().includes(q) &&
                        !(n.tags||[]).some(t => t.toLowerCase().includes(q))) return false;
                }
                return true;
            });
        },

        get sortedFilteredNodes() {
            const items = [...this.filteredNodes];
            const key = this.listSortKey;
            const dir = this.listSortDir === 'asc' ? 1 : -1;
            const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
            const statusOrder = { todo: 0, progress: 1, review: 2, done: 3 };
            items.sort((a, b) => {
                let va = a[key] || '', vb = b[key] || '';
                if (key === 'priority') { va = priorityOrder[va] ?? 9; vb = priorityOrder[vb] ?? 9; }
                else if (key === 'status') { va = statusOrder[va] ?? 9; vb = statusOrder[vb] ?? 9; }
                else { va = String(va).toLowerCase(); vb = String(vb).toLowerCase(); }
                return va < vb ? -dir : va > vb ? dir : 0;
            });
            return items;
        },

        init() {
            this.loadSavedProjects();
            const currentId = localStorage.getItem(CURRENT_KEY);
            if (currentId && this.savedProjects.find(p => p.id === currentId)) {
                this.loadProject(currentId);
            } else {
                this.newProject();
            }
            this.sbInitPages();
            this.sbCustomTemplates = JSON.parse(localStorage.getItem('sb_custom_templates') || '[]');
        },

        // ===== Project Management =====
        newProject() {
            this.currentProjectId = 'proj_' + Date.now();
            this.projectTitle = '새 기획 프로젝트';
            this.nodes = [];
            this.connections = [];
            this.selectedNode = null;
            this.selectedConnection = null;
            this.history = [];
            this.historyIndex = -1;
            this.panX = 0;
            this.panY = 0;
            this.zoom = 1;
            // 스토리보드 초기화
            this.sb.docInfo = { projectName: '', unitTask: '', version: 'D1.0' };
            this.sb.pages = [];
            this.sb.currentPageIndex = 0;
            this.sbInitPages();
            localStorage.setItem(CURRENT_KEY, this.currentProjectId);
            this.pushHistory();
        },

        saveProject() {
            const proj = {
                id: this.currentProjectId,
                title: this.projectTitle,
                nodes: JSON.parse(JSON.stringify(this.nodes)),
                connections: JSON.parse(JSON.stringify(this.connections)),
                sb: JSON.parse(JSON.stringify(this.sb)),
                viewMode: this.viewMode,
                nodeCount: this.nodes.length,
                updatedAt: new Date().toISOString(),
            };
            let projects = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
            const idx = projects.findIndex(p => p.id === proj.id);
            if (idx >= 0) projects[idx] = proj;
            else projects.unshift(proj);
            localStorage.setItem(STORAGE_KEY, JSON.stringify(projects));
            localStorage.setItem(CURRENT_KEY, this.currentProjectId);
            this.loadSavedProjects();
        },

        autoSave() {
            clearTimeout(this._autoSaveTimer);
            this._autoSaveTimer = setTimeout(() => this.saveProject(), 2000);
        },

        loadProject(id) {
            const projects = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
            const proj = projects.find(p => p.id === id);
            if (!proj) return;
            this.currentProjectId = proj.id;
            this.projectTitle = proj.title;
            this.nodes = proj.nodes || [];
            this.connections = proj.connections || [];
            this.viewMode = proj.viewMode || 'free';
            this.selectedNode = null;
            this.selectedConnection = null;
            this.history = [];
            this.historyIndex = -1;
            // 스토리보드 데이터 복원
            if (proj.sb) {
                this.sb = JSON.parse(JSON.stringify(proj.sb));
            } else {
                this.sb.docInfo = { projectName: '', unitTask: '', version: 'D1.0' };
                this.sb.pages = [];
                this.sb.currentPageIndex = 0;
            }
            this.sbInitPages();
            localStorage.setItem(CURRENT_KEY, id);
            this.pushHistory();
            idCounter = Math.max(0, ...this.nodes.map(n => parseInt(n.id?.split('_')[1]) || 0)) + 1;
        },

        deleteProject(id) {
            if (!confirm('이 프로젝트를 삭제하시겠습니까?')) return;
            let projects = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
            projects = projects.filter(p => p.id !== id);
            localStorage.setItem(STORAGE_KEY, JSON.stringify(projects));
            this.loadSavedProjects();
            if (this.currentProjectId === id) this.newProject();
        },

        loadSavedProjects() {
            this.savedProjects = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
        },

        exportProject() {
            const data = {
                title: this.projectTitle,
                nodes: this.nodes,
                connections: this.connections,
                exportedAt: new Date().toISOString(),
            };
            const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = (this.projectTitle || 'planning') + '.json';
            a.click();
            URL.revokeObjectURL(url);
        },

        // ===== Node Operations =====
        generateId() { return 'n_' + (++idCounter); },

        addNode(paletteItem, x, y) {
            const node = {
                id: this.generateId(),
                type: paletteItem.type,
                typeLabel: paletteItem.typeLabel || paletteItem.label,
                title: paletteItem.label,
                description: '',
                emoji: paletteItem.emoji,
                bg: paletteItem.bg,
                color: paletteItem.color || '#6366f1',
                status: 'todo',
                priority: 'medium',
                tags: [],
                assignee: '',
                dueDate: '',
                checklist: [],
                x: x,
                y: y,
            };
            this.nodes.push(node);
            this.selectedNode = node;
            this.sidebarTab = 'properties';
            this.pushHistory();
            this.autoSave();
            return node;
        },

        addNodeAtCenter(paletteItem) {
            const wrap = document.getElementById('canvasWrap');
            const cx = (wrap.clientWidth / 2 - this.panX * this.zoom) / this.zoom;
            const cy = (wrap.clientHeight / 2 - this.panY * this.zoom) / this.zoom;
            this.addNode(paletteItem, cx - 80, cy - 40);
        },

        addNodeAtMouse(paletteItem) {
            const x = (this.contextMenuPos.x - this.panX * this.zoom) / this.zoom;
            const y = (this.contextMenuPos.y - this.panY * this.zoom) / this.zoom;
            this.addNode(paletteItem, x, y);
        },

        deleteSelectedNode() {
            if (!this.selectedNode) return;
            const id = this.selectedNode.id;
            this.connections = this.connections.filter(c => c.from !== id && c.to !== id);
            this.nodes = this.nodes.filter(n => n.id !== id);
            this.selectedNode = null;
            this.pushHistory();
            this.autoSave();
        },

        duplicateNode() {
            if (!this.selectedNode) return;
            const orig = this.selectedNode;
            const dup = { ...JSON.parse(JSON.stringify(orig)), id: this.generateId(), x: orig.x + 30, y: orig.y + 30 };
            this.nodes.push(dup);
            this.selectedNode = dup;
            this.pushHistory();
            this.autoSave();
        },

        bringToFront() {
            if (!this.selectedNode) return;
            const idx = this.nodes.findIndex(n => n.id === this.selectedNode.id);
            if (idx >= 0) {
                const [node] = this.nodes.splice(idx, 1);
                this.nodes.push(node);
            }
        },

        // ===== Connection Rendering (SVG namespace 직접 제어) =====
        renderConnections() {
            // _connTick 읽기 → x-effect 의존성 등록 (노드 이동 시 리렌더 트리거)
            void this._connTick;
            void this.connections.length;
            void this.selectedConnection;
            void this.drawingConnection;
            void this.tempConnectionPath;

            const svg = this.$refs.connSvg;
            if (!svg) return;
            // 현재 내용 클리어
            svg.innerHTML = '';

            const NS = 'http://www.w3.org/2000/svg';

            // 기존 연결선 (화살표 없이 단순 곡선)
            const self = this;
            this.connections.forEach(conn => {
                const d = this.getConnectionPath(conn);
                if (!d) return;
                const path = document.createElementNS(NS, 'path');
                path.setAttribute('d', d);
                const isSel = this.selectedConnection?.id === conn.id;
                path.setAttribute('stroke', isSel ? '#6366f1' : '#94a3b8');
                path.setAttribute('stroke-width', isSel ? '3' : '2');
                path.setAttribute('fill', 'none');
                path.setAttribute('stroke-linecap', 'round');
                path.style.pointerEvents = 'stroke';
                path.style.cursor = 'pointer';
                path.addEventListener('click', () => { self.selectConnection(conn); });
                svg.appendChild(path);
            });

            // 드래그 중인 임시 연결선
            if (this.drawingConnection && this.tempConnectionPath) {
                const temp = document.createElementNS(NS, 'path');
                temp.setAttribute('d', this.tempConnectionPath);
                temp.setAttribute('stroke', '#6366f1');
                temp.setAttribute('stroke-width', '2');
                temp.setAttribute('stroke-dasharray', '6 3');
                temp.setAttribute('fill', 'none');
                temp.setAttribute('stroke-linecap', 'round');
                temp.style.pointerEvents = 'none';
                svg.appendChild(temp);
            }
        },

        // ===== Connection Operations =====
        startConnection(e, node, port) {
            if (this.tool !== 'connect' && this.tool !== 'select') return;
            this.drawingConnection = true;
            this.connSource = node;
            this.connSourcePort = port;
            e.preventDefault();
        },

        getPortPos(node, port) {
            const el = document.getElementById('node-' + node.id);
            if (!el) return { x: node.x, y: node.y };
            const w = el.offsetWidth || 160;
            const h = el.offsetHeight || 80;
            switch (port) {
                case 'top': return { x: node.x + w / 2, y: node.y };
                case 'bottom': return { x: node.x + w / 2, y: node.y + h };
                case 'left': return { x: node.x, y: node.y + h / 2 };
                case 'right': return { x: node.x + w, y: node.y + h / 2 };
                default: return { x: node.x + w / 2, y: node.y + h / 2 };
            }
        },

        getConnectionPath(conn) {
            const fromNode = this.nodes.find(n => n.id === conn.from);
            const toNode = this.nodes.find(n => n.id === conn.to);
            if (!fromNode || !toNode) return '';
            const from = this.getPortPos(fromNode, conn.fromPort || 'right');
            const to = this.getPortPos(toNode, conn.toPort || 'left');
            const dx = Math.abs(to.x - from.x) * 0.5;
            return `M ${from.x} ${from.y} C ${from.x + dx} ${from.y}, ${to.x - dx} ${to.y}, ${to.x} ${to.y}`;
        },

        selectConnection(conn) {
            this.selectedConnection = conn;
            this.selectedNode = null;
        },

        // ===== Canvas Events =====
        onCanvasMouseDown(e) {
            if (e.target.closest('.pc-node') || e.target.closest('.pc-port')) {
                // 스페이스바 누른 채 노드 위에서도 패닝 가능
                if (this.spaceHeld) {
                    this.panning = true;
                    this.panStartX = e.clientX - this.panX * this.zoom;
                    this.panStartY = e.clientY - this.panY * this.zoom;
                    document.getElementById('canvasWrap')?.style.setProperty('cursor', 'grabbing');
                    e.preventDefault();
                }
                return;
            }
            if (this.tool === 'pan' || this.spaceHeld || e.button === 1) {
                this.panning = true;
                this.panStartX = e.clientX - this.panX * this.zoom;
                this.panStartY = e.clientY - this.panY * this.zoom;
                document.getElementById('canvasWrap')?.style.setProperty('cursor', 'grabbing');
                e.preventDefault();
            } else {
                this.selectedNode = null;
                this.selectedConnection = null;
            }
        },

        onCanvasMouseMove(e) {
            if (this.panning) {
                this.panX = (e.clientX - this.panStartX) / this.zoom;
                this.panY = (e.clientY - this.panStartY) / this.zoom;
            }
            if (this.dragging && this.dragNode) {
                const wrap = document.getElementById('canvasWrap');
                const rect = wrap.getBoundingClientRect();
                this.dragNode.x = (e.clientX - rect.left - this.panX * this.zoom) / this.zoom - this.dragOffsetX;
                this.dragNode.y = (e.clientY - rect.top - this.panY * this.zoom) / this.zoom - this.dragOffsetY;
                this._connTick++;
            }
            if (this.drawingConnection && this.connSource) {
                const wrap = document.getElementById('canvasWrap');
                const rect = wrap.getBoundingClientRect();
                const mx = (e.clientX - rect.left - this.panX * this.zoom) / this.zoom;
                const my = (e.clientY - rect.top - this.panY * this.zoom) / this.zoom;
                const from = this.getPortPos(this.connSource, this.connSourcePort);
                const dx = Math.abs(mx - from.x) * 0.5;
                this.tempConnectionPath = `M ${from.x} ${from.y} C ${from.x + dx} ${from.y}, ${mx - dx} ${my}, ${mx} ${my}`;
            }
        },

        onCanvasMouseUp(e) {
            if (this.dragging) {
                this.dragging = false;
                this.dragNode = null;
                this.pushHistory();
                this.autoSave();
            }
            if (this.panning) {
                this.panning = false;
                const wrap = document.getElementById('canvasWrap');
                if (this.spaceHeld) wrap?.style.setProperty('cursor', 'grab');
                else wrap?.style.removeProperty('cursor');
            }
            if (this.drawingConnection) {
                // Check if dropped on a node port
                const target = e.target.closest('.pc-node');
                if (target && this.connSource) {
                    const targetId = target.id.replace('node-', '');
                    if (targetId !== this.connSource.id) {
                        // Detect closest port
                        const port = e.target.classList.contains('pc-port-top') ? 'top'
                            : e.target.classList.contains('pc-port-bottom') ? 'bottom'
                            : e.target.classList.contains('pc-port-left') ? 'left'
                            : 'left';
                        const exists = this.connections.some(c => c.from === this.connSource.id && c.to === targetId);
                        if (!exists) {
                            this.connections.push({
                                id: 'c_' + Date.now(),
                                from: this.connSource.id,
                                to: targetId,
                                fromPort: this.connSourcePort,
                                toPort: port,
                            });
                            this.pushHistory();
                            this.autoSave();
                        }
                    }
                }
                this.drawingConnection = false;
                this.connSource = null;
                this.tempConnectionPath = '';
            }
        },

        onNodeMouseDown(e, node) {
            this.selectedNode = node;
            this.selectedConnection = null;
            this.sidebarTab = 'properties';

            if (this.tool === 'select' || this.tool === 'pan') {
                this.dragging = true;
                this.dragNode = node;
                const wrap = document.getElementById('canvasWrap');
                const rect = wrap.getBoundingClientRect();
                this.dragOffsetX = (e.clientX - rect.left - this.panX * this.zoom) / this.zoom - node.x;
                this.dragOffsetY = (e.clientY - rect.top - this.panY * this.zoom) / this.zoom - node.y;
            }
        },

        onCanvasWheel(e) {
            e.preventDefault();
            const delta = e.deltaY > 0 ? -0.05 : 0.05;
            this.zoom = Math.min(3, Math.max(0.2, this.zoom + delta));
        },

        // ===== Palette Drag & Drop =====
        _dragPaletteItem: null,

        onPaletteDragStart(e, item) {
            this._dragPaletteItem = item;
            e.dataTransfer.effectAllowed = 'copy';
        },

        onCanvasDrop(e) {
            if (!this._dragPaletteItem) return;
            const wrap = document.getElementById('canvasWrap');
            const rect = wrap.getBoundingClientRect();
            const x = (e.clientX - rect.left - this.panX * this.zoom) / this.zoom;
            const y = (e.clientY - rect.top - this.panY * this.zoom) / this.zoom;
            this.addNode(this._dragPaletteItem, x - 80, y - 30);
            this._dragPaletteItem = null;
        },

        // ===== View Modes =====
        switchView(mode) {
            this.viewMode = mode;
            if (mode === 'timeline') this.layoutTimeline();
            else if (mode === 'flow') this.layoutFlow();
        },

        layoutTimeline() {
            if (this.nodes.length === 0) return;
            this.nodes.forEach((node, i) => {
                const col = i % this.phases.length;
                const row = Math.floor(i / this.phases.length);
                node.x = col * 400 + 140;
                node.y = row * 140 + 60;
            });
            this.pushHistory();
        },

        layoutFlow() {
            if (this.nodes.length === 0) return;
            const cols = Math.ceil(Math.sqrt(this.nodes.length));
            this.nodes.forEach((node, i) => {
                node.x = (i % cols) * 240 + 100;
                node.y = Math.floor(i / cols) * 160 + 60;
            });
            this.pushHistory();
        },

        // ===== Zoom =====
        zoomIn() { this.zoom = Math.min(3, this.zoom + 0.1); },
        zoomOut() { this.zoom = Math.max(0.2, this.zoom - 0.1); },
        zoomFit() {
            if (this.nodes.length === 0) { this.zoom = 1; this.panX = 0; this.panY = 0; return; }
            const wrap = document.getElementById('canvasWrap');
            const minX = Math.min(...this.nodes.map(n => n.x)) - 50;
            const minY = Math.min(...this.nodes.map(n => n.y)) - 50;
            const maxX = Math.max(...this.nodes.map(n => n.x + 200)) + 50;
            const maxY = Math.max(...this.nodes.map(n => n.y + 120)) + 50;
            const w = maxX - minX;
            const h = maxY - minY;
            this.zoom = Math.min(1.5, Math.min(wrap.clientWidth / w, wrap.clientHeight / h));
            this.panX = -minX + 20 / this.zoom;
            this.panY = -minY + 20 / this.zoom;
        },

        // ===== Undo/Redo =====
        pushHistory() {
            const state = JSON.stringify({ nodes: this.nodes, connections: this.connections });
            this.history = this.history.slice(0, this.historyIndex + 1);
            this.history.push(state);
            this.historyIndex = this.history.length - 1;
            if (this.history.length > 50) { this.history.shift(); this.historyIndex--; }
        },

        undoAction() {
            if (this.historyIndex <= 0) return;
            this.historyIndex--;
            this._restoreState();
        },

        redoAction() {
            if (this.historyIndex >= this.history.length - 1) return;
            this.historyIndex++;
            this._restoreState();
        },

        _restoreState() {
            const state = JSON.parse(this.history[this.historyIndex]);
            this.nodes = state.nodes;
            this.connections = state.connections;
            this.selectedNode = null;
            this.selectedConnection = null;
        },

        // ===== Keyboard Shortcuts =====
        handleKeyDown(e) {
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;

            // 스페이스바: 패닝 모드 (누르고 있는 동안)
            if (e.key === ' ' || e.code === 'Space') {
                e.preventDefault();
                if (!this.spaceHeld) {
                    this.spaceHeld = true;
                    this._toolBeforeSpace = this.tool;
                    this.tool = 'pan';
                    document.getElementById('canvasWrap')?.style.setProperty('cursor', 'grab');
                }
                return;
            }

            if (e.key === 'Delete' || e.key === 'Backspace') {
                if (this.selectedConnection) {
                    this.deleteSelectedConnection();
                } else {
                    this.deleteSelectedNode();
                }
            }
            if (e.key === 'v' || e.key === 'V') this.tool = 'select';
            if (e.key === 'h' || e.key === 'H') this.tool = 'pan';
            if (e.key === 'c' || e.key === 'C') this.tool = 'connect';
            if ((e.ctrlKey || e.metaKey) && e.key === 'z') { e.preventDefault(); this.viewMode === 'storyboard' ? this.sbUndo() : this.undoAction(); return; }
            if ((e.ctrlKey || e.metaKey) && e.key === 'y') { e.preventDefault(); this.viewMode === 'storyboard' ? this.sbRedo() : this.redoAction(); return; }
            if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); this.saveProject(); }
            if ((e.ctrlKey || e.metaKey) && e.key === 'd') { e.preventDefault(); this.duplicateNode(); }
            // 스토리보드 블록 Ctrl+C / Ctrl+V / Delete (단일 + 다중)
            if (this.viewMode === 'storyboard' && (this.sbSelectedBlock || this.sbMultiSelected.length > 0)) {
                if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'C')) {
                    e.preventDefault();
                    this.sbCopyBlock();
                    return;
                }
                if ((e.ctrlKey || e.metaKey) && (e.key === 'x' || e.key === 'X')) {
                    e.preventDefault();
                    this.sbCutBlock();
                    return;
                }
                if ((e.ctrlKey || e.metaKey) && (e.key === 'v' || e.key === 'V')) {
                    e.preventDefault();
                    this.sbPasteBlock();
                    return;
                }
                if (e.key === 'Delete' || e.key === 'Backspace') {
                    e.preventDefault();
                    this.sbDeleteSelectedBlock();
                    return;
                }
            }
            if (this.viewMode === 'storyboard' && !this.sbSelectedBlock && this.sbMultiSelected.length === 0) {
                if ((e.ctrlKey || e.metaKey) && (e.key === 'v' || e.key === 'V') && this._sbClipboard) {
                    e.preventDefault();
                    this.sbPasteBlock();
                    return;
                }
            }
            // Ctrl+A 전체 선택
            if (this.viewMode === 'storyboard' && (e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) {
                const page = this.sbCurrentPage;
                if (page && page.blocks && page.blocks.length > 0) {
                    e.preventDefault();
                    this.sbSelectedBlock = null;
                    this.sbMultiSelected = page.blocks.map(b => b.id);
                    return;
                }
            }
            if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
                if (this.viewMode === 'kanban' || this.viewMode === 'list') {
                    e.preventDefault();
                    this.$nextTick(() => {
                        const input = document.querySelector('.pc-filter-bar input[type="text"]');
                        if (input) input.focus();
                    });
                }
            }
        },

        handleKeyUp(e) {
            if (e.key === ' ' || e.code === 'Space') {
                if (this.spaceHeld) {
                    this.spaceHeld = false;
                    this.panning = false;
                    this.tool = this._toolBeforeSpace || 'select';
                    document.getElementById('canvasWrap')?.style.removeProperty('cursor');
                }
            }
        },

        // ===== Connection Delete =====
        deleteSelectedConnection() {
            if (!this.selectedConnection) return;
            this.connections = this.connections.filter(c => c.id !== this.selectedConnection.id);
            this.selectedConnection = null;
            this.pushHistory();
            this.autoSave();
        },

        // ===== Storyboard =====
        sbInitPages() {
            if (!this.sb.pages || this.sb.pages.length === 0) {
                this.sb.pages = [this.sbNewPageData()];
                this.sb.currentPageIndex = 0;
            }
        },

        sbNewPageData() {
            return {
                id: 'sp_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
                path: '',
                screenName: '',
                screenId: '',
                wireframeContent: '',
                wireframeImage: '',
                blocks: [],
                descriptions: [],
            };
        },

        sbAddPage() {
            const newPage = this.sbNewPageData();
            this.sb.pages.push(newPage);
            this.sb.currentPageIndex = this.sb.pages.length - 1;
            this.autoSave();
        },

        sbDuplicatePage() {
            const page = this.sbCurrentPage;
            if (!page) return;
            const copy = JSON.parse(JSON.stringify(page));
            copy.id = 'pg_' + Date.now();
            copy.screenName = (copy.screenName || '화면') + ' (복사)';
            // 블록 id도 새로 생성
            if (copy.blocks) {
                copy.blocks.forEach(blk => {
                    blk.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
                });
            }
            this.sb.pages.splice(this.sb.currentPageIndex + 1, 0, copy);
            this.sb.currentPageIndex = this.sb.currentPageIndex + 1;
            this.autoSave();
        },

        sbDeletePage() {
            if (this.sb.pages.length <= 1) return;
            if (!confirm('이 페이지를 삭제하시겠습니까?')) return;
            this.sb.pages.splice(this.sb.currentPageIndex, 1);
            if (this.sb.currentPageIndex >= this.sb.pages.length) {
                this.sb.currentPageIndex = this.sb.pages.length - 1;
            }
            this.autoSave();
        },

        sbPrevPage() {
            if (this.sb.currentPageIndex > 0) this.sb.currentPageIndex--;
        },

        sbNextPage() {
            if (this.sb.currentPageIndex < this.sb.pages.length - 1) this.sb.currentPageIndex++;
        },

        sbAddDescription() {
            const page = this.sbCurrentPage;
            if (!page) return;
            if (!page.descriptions) page.descriptions = [];
            page.descriptions.push({ text: '' });
            this.autoSave();
        },

        // ===== Block Editor =====
        sbNewBlock(type) {
            const id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
            // 자동 배치: 기존 블록 아래에 겹치지 않게
            const page = this.sbCurrentPage;
            let autoY = 16;
            if (page && page.blocks && page.blocks.length > 0) {
                const maxBottom = Math.max(...page.blocks.map(b => (b.y || 0) + (b.h || 40)));
                autoY = maxBottom + 12;
            }
            const defW = { heading: 400, heading2: 350, text: 340, divider: 400, table: 500, card: 300, code: 400, badges: 350, todo: 300, image: 400, marker: 32 };
            const defH = { heading: 40, heading2: 36, text: 50, divider: 20, table: 140, card: 90, button: 50, input: 70, select: 70, callout: 60, code: 80, badges: 50, todo: 80, image: 200, marker: 32 };
            const base = { id, type, content: '', x: 16, y: autoY, w: defW[type] || 240, h: defH[type] || 40 };
            switch (type) {
                case 'heading': return { ...base };
                case 'heading2': return { ...base };
                case 'text': return { ...base };
                case 'divider': return { ...base };
                case 'callout': return { ...base, icon: '💡' };
                case 'code': return { ...base };
                case 'table': return { ...base, cols: ['항목', '내용', '비고'], rows: [['', '', ''], ['', '', '']] };
                case 'button': return { ...base, content: '버튼', color: '#4338ca' };
                case 'input': return { ...base, label: 'Label', placeholder: '값을 입력하세요' };
                case 'select': return { ...base, label: 'Label', placeholder: '선택하세요' };
                case 'card': return { ...base, title: '카드 제목', content: '카드 내용을 입력하세요' };
                case 'badges': return { ...base, items: [
                    { text: '신규', color: '#dbeafe', textColor: '#2563eb' },
                    { text: '진행중', color: '#dcfce7', textColor: '#16a34a' },
                    { text: '완료', color: '#e0e7ff', textColor: '#4338ca' },
                ]};
                case 'todo': return { ...base, items: [
                    { text: '항목 1', checked: false },
                    { text: '항목 2', checked: false },
                ]};
                case 'image': return { ...base, src: '' };
                case 'marker': return { ...base, content: base.content || '01' };
                default: return base;
            }
        },

        sbAddBlock(type) {
            const page = this.sbCurrentPage;
            if (!page) return;
            if (!page.blocks) page.blocks = [];
            this.sbPushHistory();
            const blk = this.sbNewBlock(type);
            page.blocks.push(blk);
            this.sbSelectedBlock = blk.id;
            this.autoSave();
        },

        sbAddMarkerBlock() {
            const num = this.sbMarkerNum || '01';
            const page = this.sbCurrentPage;
            if (!page) return;
            if (!page.blocks) page.blocks = [];
            this.sbPushHistory();
            const blk = this.sbNewBlock('marker');
            blk.content = num;
            page.blocks.push(blk);
            this.sbSelectedBlock = blk.id;
            // 다음 번호 자동 증가
            const n = parseInt(num, 10);
            if (!isNaN(n)) this.sbMarkerNum = String(n + 1).padStart(2, '0');
            this.autoSave();
        },

        sbDropMarker(e) {
            const data = e.dataTransfer.getData('text/plain');
            if (!data.startsWith('marker:')) return;
            const num = data.slice(7);
            const page = this.sbCurrentPage;
            if (!page) return;
            if (!page.blocks) page.blocks = [];
            this.sbPushHistory();
            const rect = this.$refs.sbCanvas.getBoundingClientRect();
            const x = e.clientX - rect.left + this.$refs.sbCanvas.scrollLeft;
            const y = e.clientY - rect.top + this.$refs.sbCanvas.scrollTop;
            const blk = this.sbNewBlock('marker');
            blk.content = num;
            blk.x = Math.max(0, x - 14);
            blk.y = Math.max(0, y - 14);
            page.blocks.push(blk);
            this.sbSelectedBlock = blk.id;
            this.autoSave();
        },

        sbAddBlockAfter(idx, type) {
            const page = this.sbCurrentPage;
            if (!page || !page.blocks) return;
            this.sbPushHistory();
            const blk = this.sbNewBlock(type);
            page.blocks.splice(idx + 1, 0, blk);
            this.sbSelectedBlock = blk.id;
            this.autoSave();
        },

        sbRemoveBlock(idx) {
            const page = this.sbCurrentPage;
            if (!page || !page.blocks) return;
            this.sbPushHistory();
            page.blocks.splice(idx, 1);
            this.sbSelectedBlock = null;
            this.autoSave();
        },

        sbDuplicateBlock(idx) {
            const page = this.sbCurrentPage;
            if (!page || !page.blocks) return;
            this.sbPushHistory();
            const copy = JSON.parse(JSON.stringify(page.blocks[idx]));
            copy.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
            copy.x = (copy.x || 0) + 20;
            copy.y = (copy.y || 0) + 20;
            page.blocks.splice(idx + 1, 0, copy);
            this.sbSelectedBlock = copy.id;
            this.autoSave();
        },

        // ===== Free Canvas: Drag & Resize =====
        sbBlockMouseDown(blk, bi, e) {
            // 더블클릭 편집 모드에서는 드래그 안 함
            if (this.sbEditingBlock === blk.id) return;
            this.sbPushHistory();
            // 다중 선택된 블록 중 하나를 클릭하면 그룹 드래그
            if (this.sbMultiSelected.includes(blk.id)) {
                const page = this.sbCurrentPage;
                const origins = this.sbMultiSelected.map(id => {
                    const b = page.blocks.find(x => x.id === id);
                    return b ? { id: b.id, x: b.x || 0, y: b.y || 0 } : null;
                }).filter(Boolean);
                this._sbMultiDrag = { startX: e.clientX, startY: e.clientY, origins };
                e.preventDefault();
                return;
            }
            // 단일 선택
            this.sbMultiSelected = [];
            this.sbSelectedBlock = blk.id;
            this.sbCtxMenu = null; // 컨텍스트 메뉴 닫기
            this.sbFmtDropdown = null;
            this._sbDrag = {
                blk,
                startX: e.clientX,
                startY: e.clientY,
                origX: blk.x || 0,
                origY: blk.y || 0,
            };
            this.sbUpdateFormatBar();
            e.preventDefault();
        },

        sbResizeStart(blk, dir, e) {
            this.sbSelectedBlock = blk.id;
            this.sbPushHistory();
            this._sbResize = {
                blk,
                dir,
                startX: e.clientX,
                startY: e.clientY,
                origW: blk.w || 240,
                origH: blk.h || 40,
            };
            e.preventDefault();
        },

        sbCanvasMouseDown(e) {
            // 블록이나 리사이즈 핸들 위가 아니면 올가미 시작
            const onBlock = e.target.closest('.sb-block');
            if (!onBlock) {
                this.sbEditingBlock = null;
                const rect = this.$refs.sbCanvas.getBoundingClientRect();
                const scrollL = this.$refs.sbCanvas.scrollLeft;
                const scrollT = this.$refs.sbCanvas.scrollTop;
                const sx = e.clientX - rect.left + scrollL;
                const sy = e.clientY - rect.top + scrollT;
                this._sbLasso = { startX: sx, startY: sy, rx: sx, ry: sy, rw: 0, rh: 0 };
                this.sbMultiSelected = [];
                this.sbSelectedBlock = null;
            }
        },

        sbCanvasMouseMove(e) {
            // 드래그/리사이즈 중에는 서식 툴바 숨기기
            if (this._sbDrag || this._sbResize || this._sbMultiDrag || this._sbLasso) {
                this.sbFormatBar = null;
            }
            // 올가미 드래그
            if (this._sbLasso) {
                const rect = this.$refs.sbCanvas.getBoundingClientRect();
                const scrollL = this.$refs.sbCanvas.scrollLeft;
                const scrollT = this.$refs.sbCanvas.scrollTop;
                const cx = e.clientX - rect.left + scrollL;
                const cy = e.clientY - rect.top + scrollT;
                const l = this._sbLasso;
                l.rx = Math.min(l.startX, cx);
                l.ry = Math.min(l.startY, cy);
                l.rw = Math.abs(cx - l.startX);
                l.rh = Math.abs(cy - l.startY);
                return;
            }
            // 다중 블록 드래그
            if (this._sbMultiDrag) {
                const md = this._sbMultiDrag;
                const dx = e.clientX - md.startX;
                const dy = e.clientY - md.startY;
                const page = this.sbCurrentPage;
                if (page && page.blocks) {
                    md.origins.forEach(o => {
                        const blk = page.blocks.find(b => b.id === o.id);
                        if (blk) {
                            blk.x = Math.max(0, o.x + dx);
                            blk.y = Math.max(0, o.y + dy);
                        }
                    });
                }
                return;
            }
            // 단일 블록 드래그
            if (this._sbDrag) {
                const d = this._sbDrag;
                const dx = e.clientX - d.startX;
                const dy = e.clientY - d.startY;
                d.blk.x = Math.max(0, d.origX + dx);
                d.blk.y = Math.max(0, d.origY + dy);
            }
            // 리사이즈
            if (this._sbResize) {
                const r = this._sbResize;
                const dx = e.clientX - r.startX;
                const dy = e.clientY - r.startY;
                if (r.dir === 'r' || r.dir === 'br') {
                    r.blk.w = Math.max(60, r.origW + dx);
                }
                if (r.dir === 'b' || r.dir === 'br') {
                    r.blk.h = Math.max(24, r.origH + dy);
                }
            }
        },

        sbCanvasMouseUp(e) {
            // 올가미 완료 → 범위 내 블록 선택
            if (this._sbLasso) {
                const l = this._sbLasso;
                if (l.rw > 5 && l.rh > 5) {
                    const page = this.sbCurrentPage;
                    if (page && page.blocks) {
                        this.sbMultiSelected = page.blocks.filter(blk => {
                            const bx = blk.x || 0, by = blk.y || 0;
                            const bw = blk.w || 240, bh = blk.h || 40;
                            // 블록이 올가미 사각형과 겹치는지 체크
                            return bx + bw > l.rx && bx < l.rx + l.rw &&
                                   by + bh > l.ry && by < l.ry + l.rh;
                        }).map(b => b.id);
                    }
                    // 1개만 선택되면 단일 선택으로 전환
                    if (this.sbMultiSelected.length === 1) {
                        this.sbSelectedBlock = this.sbMultiSelected[0];
                        this.sbMultiSelected = [];
                    }
                }
                this._sbLasso = null;
                this._sbLassoDone = true;
                return;
            }
            // 다중 드래그 완료
            if (this._sbMultiDrag) {
                this._sbMultiDrag = null;
                this.autoSave();
                return;
            }
            if (this._sbDrag) {
                this._sbDrag = null;
                this.autoSave();
                this.sbUpdateFormatBar();
            }
            if (this._sbResize) {
                this._sbResize = null;
                this.autoSave();
                this.sbUpdateFormatBar();
            }
        },

        sbCopyBlock() {
            const page = this.sbCurrentPage;
            if (!page || !page.blocks) return;
            // 다중 선택 복사
            if (this.sbMultiSelected.length > 0) {
                this._sbClipboard = this.sbMultiSelected.map(id => {
                    const b = page.blocks.find(x => x.id === id);
                    return b ? JSON.parse(JSON.stringify(b)) : null;
                }).filter(Boolean);
                return;
            }
            // 단일 복사
            const blk = page.blocks.find(b => b.id === this.sbSelectedBlock);
            if (!blk) return;
            this._sbClipboard = JSON.parse(JSON.stringify(blk));
        },

        sbCutBlock() {
            this.sbCopyBlock();
            this.sbDeleteSelectedBlock();
        },

        sbPasteBlock() {
            if (!this._sbClipboard) return;
            const page = this.sbCurrentPage;
            if (!page) return;
            if (!page.blocks) page.blocks = [];
            this.sbPushHistory();
            // 다중 붙여넣기
            if (Array.isArray(this._sbClipboard)) {
                const newIds = [];
                const copies = this._sbClipboard.map(b => {
                    const copy = JSON.parse(JSON.stringify(b));
                    copy.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
                    copy.x = (copy.x || 0) + 24;
                    copy.y = (copy.y || 0) + 24;
                    newIds.push(copy.id);
                    return copy;
                });
                page.blocks.push(...copies);
                this.sbSelectedBlock = null;
                this.sbMultiSelected = newIds;
                this._sbClipboard = copies; // 연속 오프셋 누적
                this.autoSave();
                return;
            }
            // 단일 붙여넣기
            const copy = JSON.parse(JSON.stringify(this._sbClipboard));
            copy.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
            copy.x = (copy.x || 0) + 24;
            copy.y = (copy.y || 0) + 24;
            page.blocks.push(copy);
            this.sbSelectedBlock = copy.id;
            this._sbClipboard = copy; // 연속 붙여넣기 시 오프셋 누적
            this.autoSave();
        },

        sbDeleteSelectedBlock() {
            const page = this.sbCurrentPage;
            if (!page || !page.blocks) return;
            this.sbPushHistory();
            // 다중 삭제
            if (this.sbMultiSelected.length > 0) {
                page.blocks = page.blocks.filter(b => !this.sbMultiSelected.includes(b.id));
                this.sbMultiSelected = [];
                this.sbSelectedBlock = null;
                this.autoSave();
                return;
            }
            // 단일 삭제
            const idx = page.blocks.findIndex(b => b.id === this.sbSelectedBlock);
            if (idx < 0) return;
            page.blocks.splice(idx, 1);
            this.sbSelectedBlock = null;
            this.autoSave();
        },

        // ===== Storyboard Undo/Redo =====
        sbPushHistory() {
            if (this._sbHistoryPaused) return;
            const page = this.sbCurrentPage;
            if (!page || !page.blocks) return;
            const snap = JSON.parse(JSON.stringify(page.blocks));
            // 현재 위치 이후의 히스토리 삭제 (새 분기)
            if (this._sbHistoryIdx < this._sbHistory.length - 1) {
                this._sbHistory = this._sbHistory.slice(0, this._sbHistoryIdx + 1);
            }
            this._sbHistory.push(snap);
            // 최대 50개
            if (this._sbHistory.length > 50) {
                this._sbHistory = this._sbHistory.slice(this._sbHistory.length - 50);
            }
            this._sbHistoryIdx = this._sbHistory.length - 1;
        },

        sbUndo() {
            if (this._sbHistory.length === 0) return;
            const page = this.sbCurrentPage;
            if (!page) return;
            // 처음 undo 시 현재(변경 후) 상태를 히스토리 끝에 저장
            const curSnap = JSON.parse(JSON.stringify(page.blocks || []));
            if (this._sbHistoryIdx === this._sbHistory.length - 1) {
                const lastSnap = JSON.stringify(this._sbHistory[this._sbHistoryIdx]);
                if (lastSnap !== JSON.stringify(curSnap)) {
                    this._sbHistory.push(curSnap);
                    this._sbHistoryIdx = this._sbHistory.length - 1;
                }
            }
            if (this._sbHistoryIdx <= 0) return;
            this._sbHistoryIdx--;
            this._sbHistoryPaused = true;
            page.blocks = JSON.parse(JSON.stringify(this._sbHistory[this._sbHistoryIdx]));
            this.sbSelectedBlock = null;
            this.sbMultiSelected = [];
            this._sbHistoryPaused = false;
            this.autoSave();
        },

        sbRedo() {
            if (this._sbHistoryIdx >= this._sbHistory.length - 1) return;
            const page = this.sbCurrentPage;
            if (!page) return;
            this._sbHistoryIdx++;
            this._sbHistoryPaused = true;
            page.blocks = JSON.parse(JSON.stringify(this._sbHistory[this._sbHistoryIdx]));
            this.sbMultiSelected = [];
            this.sbSelectedBlock = null;
            this._sbHistoryPaused = false;
            this.autoSave();
        },

        // ===== Format Toolbar & Context Menu =====
        sbGetBlk() {
            const page = this.sbCurrentPage;
            if (!page || !page.blocks) return null;
            return page.blocks.find(b => b.id === this.sbSelectedBlock) || null;
        },

        sbCtxGetBlk() {
            if (!this.sbCtxMenu) return null;
            const page = this.sbCurrentPage;
            if (!page || !page.blocks) return null;
            return page.blocks.find(b => b.id === this.sbCtxMenu.blockId) || null;
        },

        sbUpdateFormatBar() {
            if (!this.sbSelectedBlock || this.sbMultiSelected.length > 0) {
                this.sbFormatBar = null;
                return;
            }
            this.$nextTick(() => {
                const canvas = this.$refs.sbCanvas;
                if (!canvas) return;
                const blockEl = canvas.querySelector('.sb-block.selected');
                if (!blockEl) { this.sbFormatBar = null; return; }
                const rect = blockEl.getBoundingClientRect();
                // 블록 위 중앙에 배치
                const barW = 440;
                let x = rect.left + rect.width / 2 - barW / 2;
                let y = rect.top - 44;
                // 화면 밖 보정
                if (x < 8) x = 8;
                if (x + barW > window.innerWidth - 8) x = window.innerWidth - barW - 8;
                if (y < 8) y = rect.bottom + 8; // 위에 공간 없으면 아래에 표시
                this.sbFormatBar = { x, y };
            });
        },

        sbShowCtxMenu(blk, bi, e) {
            this.sbSelectedBlock = blk.id;
            this.sbMultiSelected = [];
            this.sbFormatBar = null;
            this.sbFmtDropdown = null;
            // 화면 밖 보정
            let x = e.clientX, y = e.clientY;
            const mw = 220, mh = 480;
            if (x + mw > window.innerWidth) x = window.innerWidth - mw - 8;
            if (y + mh > window.innerHeight) y = window.innerHeight - mh - 8;
            this.sbCtxMenu = { x, y, blockId: blk.id, blockIdx: bi };
            this.sbCtxSub = null;
        },

        sbEnsureStyle(blk) {
            if (!blk) return;
            if (!blk.style) blk.style = {};
        },

        sbSetStyle(key, value) {
            const blk = this.sbGetBlk();
            if (!blk) return;
            this.sbPushHistory();
            this.sbEnsureStyle(blk);
            blk.style[key] = value;
            this.autoSave();
        },

        sbToggleStyle(key) {
            const blk = this.sbGetBlk();
            if (!blk) return;
            this.sbPushHistory();
            this.sbEnsureStyle(blk);
            blk.style[key] = !blk.style[key];
            this.autoSave();
        },

        sbResetStyle() {
            const blk = this.sbGetBlk();
            if (!blk) return;
            this.sbPushHistory();
            blk.style = {};
            this.sbFmtDropdown = null;
            this.autoSave();
        },

        sbBringForward() {
            const page = this.sbCurrentPage;
            const blk = this.sbGetBlk();
            if (!page || !blk) return;
            this.sbPushHistory();
            this.sbEnsureStyle(blk);
            blk.style.zIndex = (blk.style.zIndex || 10) + 1;
            this.autoSave();
        },

        sbSendBackward() {
            const page = this.sbCurrentPage;
            const blk = this.sbGetBlk();
            if (!page || !blk) return;
            this.sbPushHistory();
            this.sbEnsureStyle(blk);
            blk.style.zIndex = Math.max(1, (blk.style.zIndex || 10) - 1);
            this.autoSave();
        },

        // Context menu actions
        sbCtxCopy() {
            this.sbSelectedBlock = this.sbCtxMenu.blockId;
            this.sbCopyBlock();
            this.sbPasteBlock();
            this.sbCtxMenu = null;
        },
        sbCtxCut() {
            this.sbSelectedBlock = this.sbCtxMenu.blockId;
            this.sbCutBlock();
            this.sbCtxMenu = null;
        },
        sbCtxDelete() {
            this.sbSelectedBlock = this.sbCtxMenu.blockId;
            this.sbDeleteSelectedBlock();
            this.sbCtxMenu = null;
        },
        sbCtxSetStyle(key, value) {
            const blk = this.sbCtxGetBlk();
            if (!blk) return;
            this.sbPushHistory();
            this.sbEnsureStyle(blk);
            blk.style[key] = value;
            this.sbCtxMenu = null;
            this.sbCtxSub = null;
            this.autoSave();
        },
        sbCtxToggleStyle(key) {
            const blk = this.sbCtxGetBlk();
            if (!blk) return;
            this.sbPushHistory();
            this.sbEnsureStyle(blk);
            blk.style[key] = !blk.style[key];
            this.sbCtxMenu = null;
            this.autoSave();
        },
        sbCtxResetStyle() {
            const blk = this.sbCtxGetBlk();
            if (!blk) return;
            this.sbPushHistory();
            blk.style = {};
            this.sbCtxMenu = null;
            this.autoSave();
        },
        sbCtxBringForward() {
            const blk = this.sbCtxGetBlk();
            if (!blk) return;
            this.sbPushHistory();
            this.sbEnsureStyle(blk);
            blk.style.zIndex = (blk.style.zIndex || 10) + 1;
            this.sbCtxMenu = null;
            this.autoSave();
        },
        sbCtxSendBackward() {
            const blk = this.sbCtxGetBlk();
            if (!blk) return;
            this.sbPushHistory();
            this.sbEnsureStyle(blk);
            blk.style.zIndex = Math.max(1, (blk.style.zIndex || 10) - 1);
            this.sbCtxMenu = null;
            this.autoSave();
        },

        sbTableAddRow(blk) {
            const colCount = blk.cols.length;
            blk.rows.push(Array(colCount).fill(''));
            this.autoSave();
        },

        sbTableAddCol(blk) {
            blk.cols.push('컬럼');
            blk.rows.forEach(row => row.push(''));
            this.autoSave();
        },

        sbBlockUploadImage(e) {
            const file = e.target.files[0];
            if (!file || !this.sbBlockImageTarget) return;
            const reader = new FileReader();
            const target = this.sbBlockImageTarget;
            reader.onload = () => {
                target.src = reader.result;
                this.autoSave();
            };
            reader.readAsDataURL(file);
            e.target.value = '';
            this.sbBlockImageTarget = null;
        },

        // ===== Template System =====
        sbInsertTemplate(tpl) {
            const page = this.sbCurrentPage;
            if (!page) return;
            if (!page.blocks) page.blocks = [];
            this.sbPushHistory();
            const blocks = JSON.parse(JSON.stringify(tpl.blocks));
            // 기존 블록 아래에 배치
            let curY = 16;
            if (page.blocks.length > 0) {
                curY = Math.max(...page.blocks.map(b => (b.y || 0) + (b.h || 40))) + 16;
            }
            const defW = { heading: 400, heading2: 350, text: 340, divider: 400, table: 500, card: 300, code: 400, badges: 350, todo: 300, image: 400, marker: 32 };
            const defH = { heading: 40, heading2: 36, text: 50, divider: 20, table: 140, card: 90, button: 50, input: 70, select: 70, callout: 60, code: 80, badges: 50, todo: 80, image: 200, marker: 32 };
            blocks.forEach(blk => {
                blk.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
                if (blk.x === undefined) blk.x = 16;
                if (blk.y === undefined) { blk.y = curY; curY += (blk.h || defH[blk.type] || 40) + 8; }
                if (blk.w === undefined) blk.w = defW[blk.type] || 240;
                if (blk.h === undefined) blk.h = defH[blk.type] || 40;
            });
            page.blocks.push(...blocks);
            this.autoSave();
        },

        sbSaveAsTemplate() {
            const name = this.sbTplSaveName.trim();
            if (!name) return;
            const page = this.sbCurrentPage;
            if (!page || !page.blocks || page.blocks.length === 0) {
                alert('현재 페이지에 블록이 없습니다.');
                return;
            }
            const blocks = JSON.parse(JSON.stringify(page.blocks));
            // id 제거 (삽입 시 새로 생성)
            blocks.forEach(blk => { delete blk.id; });
            this.sbCustomTemplates.push({ name, blocks });
            localStorage.setItem('sb_custom_templates', JSON.stringify(this.sbCustomTemplates));
            this.sbTplSaveName = '';
            this.sbTplTab = 'custom';
        },

        sbDeleteCustomTemplate(idx) {
            if (!confirm('이 템플릿을 삭제하시겠습니까?')) return;
            this.sbCustomTemplates.splice(idx, 1);
            localStorage.setItem('sb_custom_templates', JSON.stringify(this.sbCustomTemplates));
        },

        sbToggleTplPanel() {
            this.sbTplOpen = !this.sbTplOpen;
            if (this.sbTplOpen) {
                this.$nextTick(() => {
                    const btn = this.$refs.sbTplBtn;
                    const panel = this.$refs.sbTplPanel;
                    if (!btn || !panel) return;
                    const r = btn.getBoundingClientRect();
                    const pw = 380;
                    const ww = window.innerWidth;
                    const wh = window.innerHeight;
                    // 가로: 버튼 오른쪽 끝 기준, 화면 밖이면 왼쪽으로 이동
                    let left = r.right - pw;
                    if (left < 8) left = 8;
                    if (left + pw > ww - 8) left = ww - pw - 8;
                    // 세로: 버튼 아래, 공간 부족하면 위로
                    let top = r.bottom + 4;
                    if (top + 520 > wh) top = Math.max(8, r.top - 520 - 4);
                    panel.style.left = left + 'px';
                    panel.style.top = top + 'px';
                });
            }
        },

        sbEditMenu() {
            // deep copy menuTree → draft (+ _open 플래그 추가)
            this.sbMenuDraft = JSON.parse(JSON.stringify(this.sb.menuTree)).map(m => {
                m.children = m.children || [];
                m._open = true;
                return m;
            });
            this.sbMenuEditorOpen = true;
        },

        sbMenuApply() {
            // _open 플래그 제거 후 적용
            this.sb.menuTree = this.sbMenuDraft.map(m => {
                const { _open, ...rest } = m;
                rest.children = (rest.children || []).map(c => ({ name: c.name }));
                return rest;
            }).filter(m => m.name.trim() !== '');
            this.sbMenuEditorOpen = false;
            this.autoSave();
        },

        sbMenuToggle(mi) {
            this.sbMenuDraft[mi]._open = this.sbMenuDraft[mi]._open === false ? true : false;
        },

        sbMenuAddChild(mi) {
            if (!this.sbMenuDraft[mi].children) this.sbMenuDraft[mi].children = [];
            this.sbMenuDraft[mi].children.push({ name: '' });
            this.sbMenuDraft[mi]._open = true;
        },

        sbMenuRemove(mi) {
            if (this.sbMenuDraft[mi].children?.length > 0 && !confirm('하위 메뉴도 함께 삭제됩니다. 계속하시겠습니까?')) return;
            this.sbMenuDraft.splice(mi, 1);
        },

        sbMenuDragStart(level, key, e) {
            this._sbMenuDrag = { level, key };
            e.dataTransfer.effectAllowed = 'move';
        },

        sbMenuDragOver(level, key, e) {
            if (!this._sbMenuDrag) return;
            e.dataTransfer.dropEffect = 'move';
        },

        sbMenuDrop(level, key, e) {
            if (!this._sbMenuDrag) return;
            const src = this._sbMenuDrag;
            this._sbMenuDrag = null;
            if (src.level !== level) return;

            if (level === 'root') {
                const fromIdx = parseInt(src.key);
                const toIdx = parseInt(key);
                if (fromIdx === toIdx) return;
                const item = this.sbMenuDraft.splice(fromIdx, 1)[0];
                this.sbMenuDraft.splice(toIdx, 0, item);
            } else {
                // child: key format = "parentIdx-childIdx"
                const [spi, sci] = src.key.split('-').map(Number);
                const [dpi, dci] = key.split('-').map(Number);
                if (spi === dpi && sci === dci) return;
                if (spi === dpi) {
                    // 같은 부모 내 이동
                    const children = this.sbMenuDraft[spi].children;
                    const item = children.splice(sci, 1)[0];
                    children.splice(dci, 0, item);
                } else {
                    // 다른 부모로 이동
                    const item = this.sbMenuDraft[spi].children.splice(sci, 1)[0];
                    this.sbMenuDraft[dpi].children.splice(dci, 0, item);
                }
            }
        },

        sbMenuDragEnd() {
            this._sbMenuDrag = null;
        },

        sbExportHtml() {
            let html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' +
                (this.sb.docInfo.projectName || 'Storyboard') + '</title>' +
                '<style>body{font-family:Pretendard,-apple-system,sans-serif;margin:0;padding:20px;background:#e5e7eb;}' +
                '.page{width:1100px;margin:0 auto 24px;background:#fff;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.1);overflow:hidden;page-break-after:always;}' +
                '.hdr{display:grid;grid-template-columns:1fr auto auto auto auto auto;border-bottom:2px solid #1e293b;font-size:10px;}' +
                '.hdr>div{padding:6px 10px;border-right:1px solid #cbd5e1;}.hdr>div:last-child{border-right:none;}' +
                '.lbl{font-size:8px;color:#94a3b8;font-weight:600;}.val{font-size:11px;font-weight:700;color:#1e293b;}' +
                '.body{display:flex;min-height:500px;}.menu{width:160px;border-right:1px solid #e2e8f0;padding:12px 0;background:#f8fafc;font-size:11px;}' +
                '.menu-logo{padding:8px 12px;font-size:13px;font-weight:800;}.menu-sec{font-size:8px;font-weight:700;color:#94a3b8;padding:4px 12px;}' +
                '.menu-item{padding:5px 12px 5px 16px;color:#64748b;}.menu-item.active{color:#4338ca;font-weight:700;background:#eef2ff;border-right:3px solid #4338ca;}' +
                '.menu-child{padding-left:28px;font-size:10px;}.menu-child.active{color:#4338ca;font-weight:700;}' +
                '.content{flex:1;display:flex;flex-direction:column;}.wf{flex:1;padding:16px;}' +
                '.wf img{max-width:100%;border-radius:4px;}.wf-text{border:1px solid #e2e8f0;border-radius:4px;padding:12px;min-height:200px;font-size:13px;line-height:1.7;}' +
                '.desc{border-top:2px solid #1e293b;padding:12px 16px;background:#fafbfc;}' +
                '.desc-title{font-size:10px;font-weight:700;margin-bottom:8px;}.desc-item{display:flex;gap:10px;padding:6px 0;border-bottom:1px solid #f1f5f9;font-size:12px;line-height:1.6;}' +
                '.desc-num{width:24px;height:24px;border-radius:50%;background:#1e293b;color:#fff;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;}' +
                '@media print{body{background:#fff;padding:0;}.page{box-shadow:none;margin:0;border-radius:0;}}</style></head><body>';

            this.sb.pages.forEach((pg, idx) => {
                html += '<div class="page"><div class="hdr">';
                html += '<div><span class="lbl">단위업무명</span><br><span class="val">' + (this.sb.docInfo.unitTask || '-') + '</span></div>';
                html += '<div><span class="lbl">버전</span><br><span class="val">' + (this.sb.docInfo.version || '-') + '</span></div>';
                html += '<div><span class="lbl">Page</span><br><span class="val">' + (idx + 1) + '</span></div>';
                html += '<div><span class="lbl">경로</span><br><span class="val">' + (pg.path || '-') + '</span></div>';
                html += '<div><span class="lbl">화면명</span><br><span class="val">' + (pg.screenName || '-') + '</span></div>';
                html += '<div><span class="lbl">화면 ID</span><br><span class="val">' + (pg.screenId || '-') + '</span></div>';
                html += '</div><div class="body"><div class="menu">';
                html += '<div class="menu-logo">' + (this.sb.docInfo.projectName || 'LOGO') + '</div>';
                html += '<div class="menu-sec">ERP 메뉴</div>';
                this.sb.menuTree.forEach(m => {
                    const isActive = pg.path && pg.path.startsWith(m.name);
                    html += '<div class="menu-item' + (isActive ? ' active' : '') + '">' + m.name + '</div>';
                    (m.children || []).forEach(c => {
                        const cActive = pg.path && pg.path.includes(c.name);
                        html += '<div class="menu-item menu-child' + (cActive ? ' active' : '') + '">- ' + c.name + '</div>';
                    });
                });
                html += '</div><div class="content"><div class="wf">';
                if (pg.blocks && pg.blocks.length > 0) {
                    // 캔버스 높이 계산
                    const maxBottom = Math.max(...pg.blocks.map(b => (b.y || 0) + (b.h || 40)), 400);
                    html += '<div style="position:relative;min-height:' + maxBottom + 'px;">';
                    pg.blocks.forEach(blk => {
                        html += '<div style="position:absolute;left:' + (blk.x||0) + 'px;top:' + (blk.y||0) + 'px;width:' + (blk.w||240) + 'px;min-height:' + (blk.h||40) + 'px;">';
                        html += this.sbExportBlock(blk);
                        html += '</div>';
                    });
                    html += '</div>';
                } else if (pg.wireframeImage) {
                    html += '<img src="' + pg.wireframeImage + '">';
                } else if (pg.wireframeContent) {
                    html += '<div class="wf-text">' + pg.wireframeContent + '</div>';
                } else {
                    html += '<div class="wf-text" style="color:#94a3b8;">와이어프레임 영역</div>';
                }
                html += '</div>';
                if (pg.descriptions && pg.descriptions.length > 0) {
                    html += '<div class="desc"><div class="desc-title">Description</div>';
                    pg.descriptions.forEach((d, di) => {
                        html += '<div class="desc-item"><div class="desc-num">' + String(di + 1).padStart(2, '0') + '</div>';
                        html += '<div>' + (d.text || '').replace(/\n/g, '<br>') + '</div></div>';
                    });
                    html += '</div>';
                }
                html += '</div></div></div>';
            });

            html += '</body></html>';
            const blob = new Blob([html], { type: 'text/html' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = (this.sb.docInfo.projectName || 'storyboard') + '.html';
            a.click();
            URL.revokeObjectURL(url);
        },

        sbPrintPreview() {
            // HTML 내보내기와 동일하게 생성 후 새 창에서 인쇄
            const origExport = this.sbExportHtml.bind(this);
            let html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' +
                (this.sb.docInfo.projectName || 'Storyboard') + ' - 인쇄</title>' +
                '<style>body{font-family:Pretendard,-apple-system,sans-serif;margin:0;padding:0;background:#fff;}' +
                '.page{width:100%;background:#fff;overflow:hidden;page-break-after:always;border-bottom:1px solid #e2e8f0;}' +
                '.page:last-child{border-bottom:none;}' +
                '.hdr{display:grid;grid-template-columns:1fr auto auto auto auto auto;border-bottom:2px solid #1e293b;font-size:10px;}' +
                '.hdr>div{padding:6px 10px;border-right:1px solid #cbd5e1;}.hdr>div:last-child{border-right:none;}' +
                '.lbl{font-size:8px;color:#94a3b8;font-weight:600;}.val{font-size:11px;font-weight:700;color:#1e293b;}' +
                '.body{display:flex;min-height:500px;}.menu{width:160px;border-right:1px solid #e2e8f0;padding:12px 0;background:#f8fafc;font-size:11px;}' +
                '.menu-logo{padding:8px 12px;font-size:13px;font-weight:800;}.menu-sec{font-size:8px;font-weight:700;color:#94a3b8;padding:4px 12px;}' +
                '.menu-item{padding:5px 12px 5px 16px;color:#64748b;}.menu-item.active{color:#4338ca;font-weight:700;background:#eef2ff;border-right:3px solid #4338ca;}' +
                '.menu-child{padding-left:28px;font-size:10px;}.menu-child.active{color:#4338ca;font-weight:700;}' +
                '.content{flex:1;display:flex;flex-direction:column;min-width:0;}.wf{flex:1;padding:16px;overflow:hidden;}' +
                '.desc{border-top:2px solid #1e293b;padding:12px 16px;background:#fafbfc;}' +
                '.desc-title{font-size:10px;font-weight:700;margin-bottom:8px;}.desc-item{display:flex;gap:10px;padding:6px 0;border-bottom:1px solid #f1f5f9;font-size:12px;line-height:1.6;}' +
                '.desc-num{width:24px;height:24px;border-radius:50%;background:#1e293b;color:#fff;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;}' +
                '@media print{@page{size:A4 landscape;margin:8mm;} body{-webkit-print-color-adjust:exact;print-color-adjust:exact;}}</style></head><body>';

            this.sb.pages.forEach((pg, idx) => {
                html += '<div class="page"><div class="hdr">';
                html += '<div><span class="lbl">단위업무명</span><br><span class="val">' + (this.sb.docInfo.unitTask || '-') + '</span></div>';
                html += '<div><span class="lbl">버전</span><br><span class="val">' + (this.sb.docInfo.version || '-') + '</span></div>';
                html += '<div><span class="lbl">Page</span><br><span class="val">' + (idx + 1) + '</span></div>';
                html += '<div><span class="lbl">경로</span><br><span class="val">' + (pg.path || '-') + '</span></div>';
                html += '<div><span class="lbl">화면명</span><br><span class="val">' + (pg.screenName || '-') + '</span></div>';
                html += '<div><span class="lbl">화면 ID</span><br><span class="val">' + (pg.screenId || '-') + '</span></div>';
                html += '</div><div class="body"><div class="menu">';
                html += '<div class="menu-logo">' + (this.sb.docInfo.projectName || 'LOGO') + '</div>';
                html += '<div class="menu-sec">ERP 메뉴</div>';
                this.sb.menuTree.forEach(m => {
                    const isActive = pg.path && pg.path.startsWith(m.name);
                    html += '<div class="menu-item' + (isActive ? ' active' : '') + '">' + m.name + '</div>';
                    (m.children || []).forEach(c => {
                        const cActive = pg.path && pg.path.includes(c.name);
                        html += '<div class="menu-item menu-child' + (cActive ? ' active' : '') + '">- ' + c.name + '</div>';
                    });
                });
                html += '</div><div class="content"><div class="wf">';
                if (pg.blocks && pg.blocks.length > 0) {
                    const maxBottom = Math.max(...pg.blocks.map(b => (b.y || 0) + (b.h || 40)), 400);
                    html += '<div style="position:relative;min-height:' + maxBottom + 'px;">';
                    pg.blocks.forEach(blk => {
                        html += '<div style="position:absolute;left:' + (blk.x||0) + 'px;top:' + (blk.y||0) + 'px;width:' + (blk.w||240) + 'px;min-height:' + (blk.h||40) + 'px;">';
                        html += this.sbExportBlock(blk);
                        html += '</div>';
                    });
                    html += '</div>';
                }
                html += '</div>';
                if (pg.descriptions && pg.descriptions.length > 0) {
                    html += '<div class="desc"><div class="desc-title">Description</div>';
                    pg.descriptions.forEach((d, di) => {
                        html += '<div class="desc-item"><div class="desc-num">' + String(di + 1).padStart(2, '0') + '</div>';
                        html += '<div>' + (d.text || '').replace(/\n/g, '<br>') + '</div></div>';
                    });
                    html += '</div>';
                }
                html += '</div></div></div>';
            });

            html += '</body></html>';
            const printWin = window.open('', '_blank');
            printWin.document.write(html);
            printWin.document.close();
            printWin.onload = () => { printWin.print(); };
        },

        sbExportBlock(blk) {
            const esc = (s) => (s || '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
            // 블록 스타일 CSS 문자열 생성
            let sty = '';
            if (blk.style) {
                if (blk.style.fontColor) sty += 'color:' + blk.style.fontColor + ';';
                if (blk.style.bgColor) sty += 'background:' + blk.style.bgColor + ';';
                if (blk.style.fontSize) sty += 'font-size:' + blk.style.fontSize + 'px;';
                if (blk.style.bold) sty += 'font-weight:700;';
                if (blk.style.italic) sty += 'font-style:italic;';
                if (blk.style.textAlign) sty += 'text-align:' + blk.style.textAlign + ';';
            }
            const wrapStyle = sty ? ' style="' + sty + '"' : '';
            switch (blk.type) {
                case 'heading': return '<h2 style="font-size:18px;font-weight:700;color:#1e293b;margin:8px 0 4px;' + sty + '">' + esc(blk.content) + '</h2>';
                case 'heading2': return '<h3 style="font-size:15px;font-weight:600;color:#1e293b;margin:6px 0 4px;' + sty + '">' + esc(blk.content) + '</h3>';
                case 'text': return '<p style="font-size:13px;line-height:1.7;color:#334155;margin:4px 0;' + sty + '">' + esc(blk.content).replace(/\n/g, '<br>') + '</p>';
                case 'divider': return '<hr style="border:none;border-top:1px solid #e2e8f0;margin:8px 0;">';
                case 'callout': return '<div style="display:flex;gap:8px;padding:10px 12px;background:#eff6ff;border-radius:6px;border-left:3px solid #3b82f6;margin:4px 0;"><span style="font-size:16px;">' + esc(blk.icon || '💡') + '</span><span style="font-size:12px;line-height:1.6;color:#334155;">' + esc(blk.content) + '</span></div>';
                case 'code': return '<pre style="padding:10px 12px;background:#1e293b;border-radius:6px;color:#e2e8f0;font-size:12px;line-height:1.6;margin:4px 0;">' + esc(blk.content) + '</pre>';
                case 'table': {
                    let t = '<table style="width:100%;border-collapse:collapse;font-size:12px;margin:4px 0;"><thead><tr>';
                    (blk.cols || []).forEach(c => { t += '<th style="background:#f1f5f9;font-weight:600;color:#475569;text-align:left;border:1px solid #e2e8f0;padding:6px 8px;">' + esc(c) + '</th>'; });
                    t += '</tr></thead><tbody>';
                    (blk.rows || []).forEach(row => {
                        t += '<tr>';
                        row.forEach(cell => { t += '<td style="border:1px solid #e2e8f0;padding:6px 8px;">' + esc(cell) + '</td>'; });
                        t += '</tr>';
                    });
                    return t + '</tbody></table>';
                }
                case 'button': return '<div style="margin:4px 0;"><span style="display:inline-block;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;background:' + (blk.color || '#4338ca') + ';color:#fff;">' + esc(blk.content) + '</span></div>';
                case 'input': return '<div style="margin:4px 0;"><div style="font-size:9px;color:#94a3b8;font-weight:600;margin-bottom:2px;">' + esc(blk.label) + '</div><div style="padding:7px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;color:#6b7280;background:#f9fafb;">' + esc(blk.placeholder) + '</div></div>';
                case 'select': return '<div style="margin:4px 0;"><div style="font-size:9px;color:#94a3b8;font-weight:600;margin-bottom:2px;">' + esc(blk.label) + '</div><div style="padding:7px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;color:#6b7280;background:#f9fafb;">' + esc(blk.placeholder) + ' ▾</div></div>';
                case 'card': return '<div style="border:1px solid #e2e8f0;border-radius:8px;padding:12px;background:#fff;margin:4px 0;"><div style="font-size:13px;font-weight:600;color:#1e293b;margin-bottom:4px;">' + esc(blk.title) + '</div><div style="font-size:11px;color:#64748b;line-height:1.5;">' + esc(blk.content) + '</div></div>';
                case 'badges': {
                    let b = '<div style="display:flex;gap:6px;flex-wrap:wrap;margin:4px 0;">';
                    (blk.items || []).forEach(badge => {
                        b += '<span style="padding:3px 10px;border-radius:12px;font-size:10px;font-weight:600;background:' + (badge.color || '#e0e7ff') + ';color:' + (badge.textColor || '#4338ca') + ';">' + esc(badge.text) + '</span>';
                    });
                    return b + '</div>';
                }
                case 'todo': {
                    let t = '<div style="margin:4px 0;">';
                    (blk.items || []).forEach(item => {
                        t += '<div style="display:flex;align-items:center;gap:6px;padding:2px 0;font-size:12px;">';
                        t += item.checked ? '☑' : '☐';
                        t += ' <span' + (item.checked ? ' style="text-decoration:line-through;color:#94a3b8;"' : '') + '>' + esc(item.text) + '</span></div>';
                    });
                    return t + '</div>';
                }
                case 'image': return blk.src ? '<div style="margin:4px 0;text-align:center;"><img src="' + blk.src + '" style="max-width:100%;border-radius:6px;"></div>' : '';
                case 'marker': return '<div style="display:inline-flex;width:28px;height:28px;border-radius:50%;background:#1e293b;color:#fff;font-size:11px;font-weight:700;align-items:center;justify-content:center;">' + esc(blk.content) + '</div>';
                default: return '<p>' + esc(blk.content) + '</p>';
            }
        },

        // ===== Context Menu =====
        showContextMenu(e) {
            this.contextMenuPos = { x: e.clientX, y: e.clientY };
            const menu = document.getElementById('contextMenu');
            const wrapRect = document.getElementById('canvasWrap').getBoundingClientRect();
            this.contextMenuPos.x = (e.clientX - wrapRect.left);
            this.contextMenuPos.y = (e.clientY - wrapRect.top);
            menu.style.left = e.clientX + 'px';
            menu.style.top = e.clientY + 'px';
            menu.classList.add('show');
        },

        hideContextMenu() {
            document.getElementById('contextMenu').classList.remove('show');
        },

        // ===== Kanban =====
        kanbanNodesFor(statusKey) {
            return this.filteredNodes.filter(n => (n.status || 'todo') === statusKey);
        },

        onKanbanDragStart(e, node) {
            this._kanbanDragNodeId = node.id;
            e.target.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
        },

        onKanbanDrop(e, statusKey) {
            if (!this._kanbanDragNodeId) return;
            const node = this.nodes.find(n => n.id === this._kanbanDragNodeId);
            if (node) {
                node.status = statusKey;
                if (this.selectedNode?.id === node.id) this.selectedNode = node;
                if (this.modalNode?.id === node.id) this.modalNode = node;
                this.pushHistory();
                this.autoSave();
            }
            this._kanbanDragNodeId = null;
        },

        // ===== Node Detail Modal =====
        openNodeModal(node) {
            this.modalNode = node;
            this.selectedNode = node;
            if (!this.modalNode.checklist) this.modalNode.checklist = [];
            if (!this.modalNode.assignee) this.modalNode.assignee = '';
            if (!this.modalNode.dueDate) this.modalNode.dueDate = '';
        },

        closeNodeModal() {
            this.modalNode = null;
            this.pushHistory();
            this.autoSave();
        },

        onModalChange() {
            this.autoSave();
        },

        addChecklistItem() {
            if (!this.modalNode.checklist) this.modalNode.checklist = [];
            this.modalNode.checklist.push({ text: '', done: false });
        },

        checklistProgress(node) {
            if (!node.checklist || node.checklist.length === 0) return 0;
            return Math.round(node.checklist.filter(c => c.done).length / node.checklist.length * 100);
        },

        // ===== List View =====
        sortList(key) {
            if (this.listSortKey === key) {
                this.listSortDir = this.listSortDir === 'asc' ? 'desc' : 'asc';
            } else {
                this.listSortKey = key;
                this.listSortDir = 'asc';
            }
        },

        sortIcon(key) {
            if (this.listSortKey !== key) return '';
            return this.listSortDir === 'asc' ? '▲' : '▼';
        },

        // ===== Multi-Select =====
        multiSelected: [],

        toggleMultiSelect(node, e) {
            if (e.shiftKey) {
                const idx = this.multiSelected.findIndex(n => n.id === node.id);
                if (idx >= 0) this.multiSelected.splice(idx, 1);
                else this.multiSelected.push(node);
            } else {
                this.multiSelected = [node];
            }
        },

        // ===== Helpers =====
        toggleSidebar() { this.sidebarOpen = !this.sidebarOpen; },

        onPropChange() { this.autoSave(); },

        statusColor(status) {
            const map = { todo: '#94a3b8', progress: '#3b82f6', review: '#f59e0b', done: '#10b981' };
            return map[status] || '#94a3b8';
        },

        statusLabel(status) {
            const map = { todo: '대기', progress: '진행중', review: '검토중', done: '완료' };
            return map[status] || status;
        },

        priorityLabel(p) {
            const map = { low: '낮음', medium: '보통', high: '높음', critical: '긴급' };
            return map[p] || '';
        },

        formatDate(iso) {
            if (!iso) return '';
            const d = new Date(iso);
            return (d.getMonth() + 1) + '/' + d.getDate() + ' ' + d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0');
        },

        formatShortDate(dateStr) {
            if (!dateStr) return '';
            const d = new Date(dateStr);
            return (d.getMonth() + 1) + '/' + d.getDate();
        },

        isOverdue(dateStr) {
            if (!dateStr) return false;
            const d = new Date(dateStr);
            const today = new Date();
            today.setHours(0,0,0,0);
            return d < today;
        },
    };
}
</script>
@endpush
