<template>
  <div class="app-container realtime-iot-page">
    <el-row :gutter="20">
      <el-col :xl="5" :lg="6" :md="7" :sm="24" :xs="24">
        <section class="glass-panel tree-panel">
          <div class="panel-heading">
            <div>
              <p class="panel-eyebrow">Live Device Navigator</p>
              <h3>实时设备树</h3>
            </div>
            <el-tag effect="plain" size="small">{{ monitorInfoOptions.length }} 个节点</el-tag>
          </div>

          <el-input
            v-model="workUnitName"
            class="tree-search"
            placeholder="请输入计量设备名称"
            clearable
            size="small"
            prefix-icon="el-icon-search"
          />

          <div v-loading="treeLoading" class="tree-wrapper">
            <el-tree
              ref="treeRef"
              :data="monitorInfoOptions"
              :props="monitorProps"
              :expand-on-click-node="false"
              :filter-node-method="filterNode"
              node-key="id"
              default-expand-all
              highlight-current
              @node-click="handleNodeClick"
            />
          </div>
        </section>
      </el-col>

      <el-col :xl="19" :lg="18" :md="17" :sm="24" :xs="24">
        <section class="hero-panel">
          <div class="hero-main">
            <div class="hero-copy">
              <p class="hero-eyebrow">Realtime Curve Command Center</p>
              <h2>环境监测实时曲线</h2>
              <p class="hero-desc">
                实时页专注增量接收、窗口裁剪与运行态感知。轮询和 WebSocket 共用同一条曲线追加管道，既能先落地，也能为后续流式升级留足扩展空间。
              </p>
            </div>
            <div class="hero-tags">
              <el-tag :type="statusTagType" effect="dark">{{ statusText }}</el-tag>
              <el-tag effect="plain">{{ sourceModeLabel }}</el-tag>
              <el-tag effect="plain">窗口 {{ realtimeConfig.windowSize }} 点</el-tag>
              <el-tag effect="plain">间隔 {{ realtimeConfig.pollingIntervalSec }} 秒</el-tag>
            </div>
          </div>

          <div class="hero-metrics">
            <div class="metric-card">
              <span class="metric-label">当前设备</span>
              <strong class="metric-value">{{ selectedMonitorName || '未选择' }}</strong>
              <p class="metric-hint">{{ monitorProfileText }}</p>
            </div>
            <div class="metric-card">
              <span class="metric-label">窗口点数</span>
              <strong class="metric-value">{{ realtimeRecords.length }}</strong>
              <p class="metric-hint">仅保留最近 N 个点，防止浏览器长期运行后膨胀</p>
            </div>
            <div class="metric-card">
              <span class="metric-label">最新记录时间</span>
              <strong class="metric-value metric-time">{{ lastRecordTime || '--' }}</strong>
              <p class="metric-hint">实时曲线右侧最新落点时间</p>
            </div>
            <div class="metric-card">
              <span class="metric-label">最近刷新</span>
              <strong class="metric-value metric-time">{{ lastRefreshTime || '--' }}</strong>
              <p class="metric-hint">{{ refreshHintText }}</p>
            </div>
          </div>
        </section>

        <section class="glass-panel control-panel">
          <div class="panel-heading compact">
            <div>
              <p class="panel-eyebrow">Runtime Control</p>
              <h3>实时控制台</h3>
            </div>
            <div class="panel-actions">
              <el-button type="primary" size="small" @click="handleStart" :disabled="!selectedMonitorId">启动监测</el-button>
              <el-button size="small" @click="handleStop">暂停监测</el-button>
              <el-button type="success" plain size="small" @click="handleApplyConfig" :disabled="!selectedMonitorId">应用配置</el-button>
              <el-button type="warning" plain size="small" @click="handleManualRefresh" :disabled="!selectedMonitorId">立即刷新</el-button>
            </div>
          </div>

          <el-form label-width="108px" size="small" class="control-form">
            <div class="control-grid">
              <!--
                这里先注释页面上的数据源模式切换，当前阶段统一走代码内配置，默认 API 轮询。
                后续后端 WebSocket 稳定后，只需要恢复此配置区即可重新开放切换能力。
              -->
              <!--
              <el-form-item label="数据源模式">
                <el-radio-group v-model="realtimeConfig.sourceMode">
                  <el-radio-button label="polling">API轮询</el-radio-button>
                  <el-radio-button label="websocket">WebSocket</el-radio-button>
                </el-radio-group>
              </el-form-item>
              -->

              <el-form-item label="轮询间隔">
                <el-input-number v-model="realtimeConfig.pollingIntervalSec" :min="1" :max="300" controls-position="right" />
                <span class="field-suffix">秒/次</span>
              </el-form-item>

              <el-form-item label="窗口大小">
                <el-input-number v-model="realtimeConfig.windowSize" :min="10" :max="2000" controls-position="right" />
                <span class="field-suffix">点</span>
              </el-form-item>

              <el-form-item label="设备名称">
                <el-input :model-value="selectedMonitorName || '请选择左侧具体设备节点'" readonly />
              </el-form-item>

              <!--
                这里先注释页面上的 WebSocket 地址输入框，避免业务侧误以为当前环境已经开放前端自助切换。
                地址仍保留在代码配置中，后续联调通过后可直接恢复。
              -->
              <!--
              <el-form-item label="WebSocket地址" class="control-grid-full">
                <el-input
                  v-model="realtimeConfig.wsUrl"
                  placeholder="例如：ws://127.0.0.1:8080/ems/ws/iot/realtime"
                  clearable
                />
              </el-form-item>
              -->
            </div>
          </el-form>

          <el-alert
            v-if="realtimeConfig.sourceMode === 'websocket'"
            type="warning"
            :closable="false"
            show-icon
            title="当前前端已预留 WebSocket 接入结构，只要后端提供实时地址和消息体，就能直接切换到流式模式。"
          />
          <el-alert
            v-else
            type="info"
            :closable="false"
            show-icon
            title="轮询模式会自动按最近记录时间增量补数，重复数据会在前端先去重再进入实时窗口。"
          />
          <p class="runtime-note">{{ runtimeMessage }}</p>
        </section>

        <section v-loading="chartLoading" class="glass-panel chart-panel">
          <div class="panel-heading compact">
            <div>
              <p class="panel-eyebrow">Realtime Canvas</p>
              <h3>动态曲线区</h3>
            </div>
            <div class="chart-status">
              <el-tag size="small" effect="plain">{{ selectedMonitorId || '未选设备' }}</el-tag>
              <el-tag size="small" :type="statusTagType" effect="plain">{{ statusText }}</el-tag>
              <el-tag size="small" effect="plain">指标 {{ chartPanels.length }} 项</el-tag>
            </div>
          </div>

          <div v-if="!selectedMonitorId" class="empty-block">
            <el-empty description="请先在左侧树中选择具体设备节点，再启动实时监测" />
          </div>

          <div v-else-if="!realtimeRecords.length" class="empty-block">
            <el-empty description="实时引擎已准备好，等待首批数据进入窗口" />
          </div>

          <div v-else-if="!chartPanels.length" class="empty-block">
            <el-empty description="已收到实时数据，但未识别到可绘制曲线的字段，请确认后端返回值结构" />
          </div>

          <template v-else>
            <div class="chart-summary-bar">
              <span>设备：{{ selectedMonitorName }}</span>
              <span>模式：{{ sourceModeLabel }}</span>
              <span>窗口：最近 {{ realtimeConfig.windowSize }} 点</span>
              <span>最后游标：{{ lastRecordTime || '--' }}</span>
            </div>

            <div class="chart-grid" :class="chartGridClass">
              <article v-for="panel in chartPanels" :key="panel.key" class="chart-card">
                <div class="chart-card-head">
                  <div>
                    <h4>{{ panel.label }}</h4>
                    <p>{{ panel.metric.unit ? `${panel.metric.label}(${panel.metric.unit})` : panel.metric.label }}</p>
                  </div>
                  <el-tag size="small" effect="plain">{{ panel.valueCount }} 个有效点</el-tag>
                </div>
                <Chart :ref="setChartRef(panel.key)" class="chart-canvas" :chart-option="panel.option" />
              </article>
            </div>
          </template>
        </section>
      </el-col>
    </el-row>
  </div>
</template>

<script setup lang="ts">
import { ElMessage } from 'element-plus';
import Chart from '@/components/Charts/Chart.vue';
import { getMonitorInfoTree } from '@/api/ems/base/baseMonitorInfo';
import { getRecordIotenvInstantList } from '@/api/ems/record/recordIotenvInstant';
import { getToken } from '@/utils/auth';
import { parseTime } from '@/utils/ruoyi';
import type { RecordIotenvInstantVO } from '@/api/ems/types';
import {
  buildIotCurveOption,
  buildMetricHintsByNode,
  connectCurveCharts,
  extractRealtimeIotRecords,
  mergeRealtimeWindow,
  normalizeIotCurveRecord,
  readMetricValue,
  resolveIotCurveMetrics,
  type CurveChartExpose,
  type IotCurveMetricDefinition,
  type NormalizedIotCurveRecord
} from '../iotCurveShared';

defineOptions({
  name: 'realtimeIOTCurve',
  dicts: ['collect_type']
});

type RealtimeSourceMode = 'polling' | 'websocket';
type RealtimeEngineStatus = 'idle' | 'running' | 'connecting' | 'stopped' | 'error';

interface ChartPanel {
  key: string;
  label: string;
  metric: IotCurveMetricDefinition;
  valueCount: number;
  option: Record<string, unknown>;
}

const treeRef = ref<ElTreeInstance>();
const chartRefMap = shallowRef<Record<string, CurveChartExpose | null>>({});
const disconnectChartGroup = ref<() => void>(() => undefined);

const createDefaultWsUrl = () => {
  const socketProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  return `${socketProtocol}//${window.location.host}/ems/ws/iot/realtime`;
};

const monitorProps = {
  children: 'children',
  label: 'label'
};

const treeLoading = ref(false);
const chartLoading = ref(false);
const requestPending = ref(false);
const monitorInfoOptions = ref<any[]>([]);
const workUnitName = ref('');
const selectedMonitorId = ref('');
const selectedMonitorName = ref('');
const selectedMonitorNode = ref<Record<string, unknown> | null>(null);
const realtimeRecords = ref<NormalizedIotCurveRecord[]>([]);
const lastRecordTime = ref('');
const lastRefreshTime = ref('');
const engineStatus = ref<RealtimeEngineStatus>('idle');
const isRealtimeRunning = ref(false);
const runtimeMessage = ref('待选择设备后启动监测');

const realtimeConfig = reactive({
  sourceMode: 'polling' as RealtimeSourceMode,
  pollingIntervalSec: 5,
  windowSize: 120,
  wsUrl: createDefaultWsUrl()
});

let pollingTimer: ReturnType<typeof setInterval> | null = null;
let socketController: ReturnType<typeof useWebSocket> | null = null;

const metricHints = computed(() => buildMetricHintsByNode(selectedMonitorNode.value));

const chartPanels = computed<ChartPanel[]>(() => {
  const metrics = resolveIotCurveMetrics(realtimeRecords.value, metricHints.value);
  return metrics.map((metric) => {
    const valueCount = realtimeRecords.value.filter((item) => readMetricValue(item, metric) !== null).length;
    return {
      key: metric.key,
      label: metric.label,
      metric,
      valueCount,
      option: buildIotCurveOption({
        metric,
        monitorName: selectedMonitorName.value || '设备',
        records: realtimeRecords.value,
        premium: true,
        subtitle: `${sourceModeLabel.value} | 最近 ${realtimeConfig.windowSize} 点`
      })
    };
  });
});

const sourceModeLabel = computed(() => (realtimeConfig.sourceMode === 'polling' ? 'API轮询' : 'WebSocket'));

const monitorProfileText = computed(() => {
  const pointType = String(selectedMonitorNode.value?.pointType || selectedMonitorNode.value?.metricName || '').trim();
  const unitName = String(selectedMonitorNode.value?.unitName || '').trim();
  if (pointType && unitName) {
    return `${pointType} / ${unitName}`;
  }
  if (pointType) {
    return pointType;
  }
  return '指标将根据实时数据字段自动识别';
});

const statusText = computed(() => {
  const statusMap: Record<RealtimeEngineStatus, string> = {
    idle: '待机',
    running: realtimeConfig.sourceMode === 'polling' ? '轮询中' : '流式接收中',
    connecting: '连接中',
    stopped: '已暂停',
    error: '异常'
  };
  return statusMap[engineStatus.value];
});

const statusTagType = computed(() => {
  const tagMap: Record<RealtimeEngineStatus, 'info' | 'success' | 'warning' | 'danger'> = {
    idle: 'info',
    running: 'success',
    connecting: 'warning',
    stopped: 'info',
    error: 'danger'
  };
  return tagMap[engineStatus.value];
});

const refreshHintText = computed(() => {
  return realtimeConfig.sourceMode === 'polling' ? '轮询结果入窗时间' : '流式消息最近更新时间';
});

const chartGridClass = computed(() => {
  return chartPanels.value.length <= 1 ? 'chart-grid-single' : 'chart-grid-multi';
});

const formatDateTime = (value: Date | number) => {
  return parseTime(value, '{y}-{m}-{d} {h}:{i}:{s}');
};

const setChartRef = (key: string) => {
  return (instance: any) => {
    if (instance) {
      chartRefMap.value[key] = instance as CurveChartExpose;
      return;
    }
    delete chartRefMap.value[key];
  };
};

const filterNode = (value, data) => {
  if (!value) {
    return true;
  }
  return String(data.label || '').includes(value);
};

const clearPollingTimer = () => {
  if (pollingTimer) {
    clearInterval(pollingTimer);
    pollingTimer = null;
  }
};

const closeSocketConnection = () => {
  if (socketController) {
    socketController.close();
    socketController = null;
  }
};

const stopRealtimeEngine = (status: RealtimeEngineStatus = 'stopped', message = '实时监测已暂停') => {
  clearPollingTimer();
  closeSocketConnection();
  isRealtimeRunning.value = false;
  engineStatus.value = status;
  runtimeMessage.value = message;
};

const renderChartLinkage = () => {
  disconnectChartGroup.value();
  if (chartPanels.value.length < 2) {
    return;
  }
  void nextTick(() => {
    disconnectChartGroup.value();
    disconnectChartGroup.value = connectCurveCharts(
      chartPanels.value.map((item) => chartRefMap.value[item.key]),
      'realtime-iot-chart-group'
    );
  });
};

const matchSelectedMonitor = (record: RecordIotenvInstantVO) => {
  const currentMonitorId = String(selectedMonitorId.value || '');
  const candidateIds = [record.monitorId, record.monitorCode]
    .filter((item) => item !== null && item !== undefined)
    .map((item) => String(item));
  return candidateIds.length === 0 || candidateIds.includes(currentMonitorId);
};

const appendRealtimeRecords = (records: RecordIotenvInstantVO[], sourceLabel: string) => {
  const filteredRecords = records.filter((item) => matchSelectedMonitor(item));
  if (!filteredRecords.length) {
    return;
  }

  // 轮询和 WebSocket 统一走同一个增量入窗过程，后续切数据源时不用再改图表层。
  realtimeRecords.value = mergeRealtimeWindow(realtimeRecords.value, filteredRecords, realtimeConfig.windowSize);
  lastRecordTime.value = realtimeRecords.value.at(-1)?.__timeKey || '';
  lastRefreshTime.value = formatDateTime(new Date());
  engineStatus.value = 'running';
  runtimeMessage.value = `${sourceLabel} 已写入 ${filteredRecords.length} 条增量数据`;
  renderChartLinkage();
};

const buildPollingQuery = () => {
  const endTime = new Date();
  const baselineSeconds = Math.max(realtimeConfig.windowSize * realtimeConfig.pollingIntervalSec, 300);
  const beginTime = lastRecordTime.value ? new Date(lastRecordTime.value) : new Date(endTime.getTime() - baselineSeconds * 1000);

  return {
    monitorId: selectedMonitorId.value,
    monitorIds: [],
    samplingInterval: 1,
    beginRecordTime: formatDateTime(beginTime),
    endRecordTime: formatDateTime(endTime),
    params: {}
  };
};

const fetchRealtimeData = async (sourceLabel: string) => {
  if (!selectedMonitorId.value || requestPending.value) {
    return;
  }

  chartLoading.value = realtimeRecords.value.length === 0;
  requestPending.value = true;
  try {
    const { data = [] } = await getRecordIotenvInstantList(buildPollingQuery());
    appendRealtimeRecords(data, sourceLabel);
    if (data.length === 0) {
      lastRefreshTime.value = formatDateTime(new Date());
      runtimeMessage.value = `${sourceLabel} 未获取到新数据`;
    }
  } catch (error) {
    engineStatus.value = 'error';
    runtimeMessage.value = `${sourceLabel} 拉取失败`;
    ElMessage.error(`${sourceModeLabel.value} 获取实时数据失败，请检查接口或连接配置`);
    console.error(error);
  } finally {
    requestPending.value = false;
    chartLoading.value = false;
  }
};

const resolveSocketUrl = () => {
  if (!realtimeConfig.wsUrl) {
    return '';
  }
  try {
    const socketOrigin = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
    const baseUrl = `${socketOrigin}${window.location.host}`;
    const url = new URL(realtimeConfig.wsUrl, baseUrl);
    const token = getToken();

    if (token) {
      url.searchParams.set('Authorization', `Bearer ${token}`);
    }
    if (import.meta.env.VITE_APP_CLIENT_ID) {
      url.searchParams.set('clientid', import.meta.env.VITE_APP_CLIENT_ID);
    }
    if (selectedMonitorId.value) {
      // 这里提前把 monitorId 带给后端，后续做按设备订阅时前端结构不需要再改。
      url.searchParams.set('monitorId', selectedMonitorId.value);
    }
    return url.toString();
  } catch (error) {
    console.error(error);
    return '';
  }
};

const startPollingMode = async () => {
  clearPollingTimer();
  engineStatus.value = 'running';
  runtimeMessage.value = '轮询引擎已启动';
  await fetchRealtimeData('轮询初始化');
  pollingTimer = setInterval(() => {
    void fetchRealtimeData('轮询增量');
  }, realtimeConfig.pollingIntervalSec * 1000);
};

const startWebSocketMode = async () => {
  const socketUrl = resolveSocketUrl();
  if (!socketUrl) {
    engineStatus.value = 'error';
    runtimeMessage.value = 'WebSocket 地址无效';
    isRealtimeRunning.value = false;
    ElMessage.warning('请先配置有效的 WebSocket 地址后再启动');
    return;
  }

  closeSocketConnection();
  engineStatus.value = 'connecting';
  runtimeMessage.value = '等待 WebSocket 建立连接';

  // 先补最近窗口，再接流式消息，避免首屏只看到空图。
  await fetchRealtimeData('初始化补数');

  socketController = useWebSocket(socketUrl, {
    autoReconnect: {
      retries: 3,
      delay: 2000
    },
    heartbeat: {
      message: JSON.stringify({ type: 'ping' }),
      interval: 10000,
      pongTimeout: 5000
    },
    onConnected() {
      engineStatus.value = 'running';
      runtimeMessage.value = 'WebSocket 已连接，等待流式数据';
      lastRefreshTime.value = formatDateTime(new Date());
    },
    onDisconnected() {
      if (isRealtimeRunning.value) {
        engineStatus.value = 'stopped';
        runtimeMessage.value = 'WebSocket 已断开';
      }
    },
    onError() {
      engineStatus.value = 'error';
      runtimeMessage.value = 'WebSocket 连接异常';
    },
    onMessage(_, event) {
      if (typeof event.data !== 'string' || event.data.includes('ping')) {
        return;
      }
      try {
        const payload = JSON.parse(event.data);
        appendRealtimeRecords(extractRealtimeIotRecords(payload), 'WebSocket');
      } catch (error) {
        console.warn('WebSocket 消息解析失败', error);
      }
    }
  });
};

const startRealtimeEngine = async () => {
  if (!selectedMonitorId.value) {
    ElMessage.warning('请先选择具体设备节点');
    return;
  }

  stopRealtimeEngine('idle', '准备重新启动实时监测');
  isRealtimeRunning.value = true;
  if (realtimeConfig.sourceMode === 'polling') {
    await startPollingMode();
    return;
  }
  await startWebSocketMode();
};

const resetRealtimeWindow = () => {
  realtimeRecords.value = [];
  lastRecordTime.value = '';
  lastRefreshTime.value = '';
};

const handleNodeClick = async (data) => {
  if (data.children && data.children.length > 0) {
    ElMessage.info('实时曲线仅支持选择具体设备节点；父节点保留给后续多设备订阅扩展');
    return;
  }

  selectedMonitorId.value = String(data.code || '');
  selectedMonitorName.value = String(data.label || '');
  selectedMonitorNode.value = data;
  resetRealtimeWindow();
  await startRealtimeEngine();
};

const handleStart = async () => {
  await startRealtimeEngine();
};

const handleStop = () => {
  stopRealtimeEngine('stopped', '已手动暂停实时监测');
};

const handleApplyConfig = async () => {
  realtimeRecords.value = realtimeRecords.value.slice(-Math.max(1, realtimeConfig.windowSize));
  renderChartLinkage();
  if (selectedMonitorId.value && isRealtimeRunning.value) {
    await startRealtimeEngine();
    return;
  }
  ElMessage.success('实时配置已生效');
};

const handleManualRefresh = async () => {
  await fetchRealtimeData('手动刷新');
};

const getTreeselect = async () => {
  treeLoading.value = true;
  try {
    // 这里不再硬编码能源/监测类型编号，避免未来新增或删减类型时还要反复改页面过滤条件。
    const response = await getMonitorInfoTree({});
    monitorInfoOptions.value = response.data || [];
  } finally {
    treeLoading.value = false;
  }
};

watch(workUnitName, (value) => {
  treeRef.value?.filter(value);
});

watch(chartPanels, () => {
  renderChartLinkage();
});

watch(
  () => realtimeConfig.windowSize,
  (value) => {
    realtimeRecords.value = realtimeRecords.value.slice(-Math.max(1, Number(value) || 1));
    renderChartLinkage();
  }
);

onMounted(async () => {
  await getTreeselect();
});

onDeactivated(() => {
  stopRealtimeEngine('stopped', '页面失活时已释放实时资源');
});

onBeforeUnmount(() => {
  disconnectChartGroup.value();
  stopRealtimeEngine('stopped', '页面卸载时已释放实时资源');
});
</script>

<style scoped>
.realtime-iot-page {
  --page-bg-start: #eef4ff;
  --page-bg-end: #f8fbff;
  --panel-border: rgba(148, 163, 184, 0.22);
  --panel-shadow: 0 18px 48px rgba(15, 23, 42, 0.08);
  --hero-shadow: 0 30px 76px rgba(37, 99, 235, 0.2);
  --text-primary: #0f172a;
  --text-secondary: #64748b;
  --accent: #2563eb;
  min-height: calc(100vh - 84px);
  background:
    radial-gradient(circle at top right, rgba(59, 130, 246, 0.18), transparent 28%),
    radial-gradient(circle at bottom left, rgba(20, 184, 166, 0.12), transparent 24%),
    linear-gradient(180deg, var(--page-bg-start), var(--page-bg-end));
}

.glass-panel,
.hero-panel {
  border: 1px solid var(--panel-border);
  border-radius: 24px;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(12px);
  box-shadow: var(--panel-shadow);
}

.tree-panel,
.control-panel,
.chart-panel {
  padding: 20px;
}

.hero-panel {
  padding: 24px;
  margin-bottom: 20px;
  background:
    linear-gradient(135deg, rgba(37, 99, 235, 0.98), rgba(14, 165, 233, 0.92)),
    linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0));
  color: #ffffff;
  box-shadow: var(--hero-shadow);
  position: relative;
  overflow: hidden;
}

.hero-panel::before,
.hero-panel::after {
  content: '';
  position: absolute;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
}

.hero-panel::before {
  width: 220px;
  height: 220px;
  inset: -90px auto auto -70px;
}

.hero-panel::after {
  width: 260px;
  height: 260px;
  inset: auto -70px -110px auto;
}

.panel-heading,
.hero-main,
.hero-metrics,
.control-grid,
.chart-grid,
.chart-summary-bar {
  display: grid;
}

.panel-heading {
  grid-template-columns: 1fr auto;
  gap: 12px;
  align-items: center;
  margin-bottom: 18px;
}

.panel-heading.compact {
  margin-bottom: 16px;
}

.panel-eyebrow,
.hero-eyebrow {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.panel-eyebrow {
  color: var(--accent);
}

.panel-heading h3,
.hero-copy h2 {
  margin: 0;
}

.hero-main {
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 16px;
  align-items: start;
}

.hero-copy h2 {
  font-size: 30px;
  line-height: 1.18;
}

.hero-eyebrow {
  color: rgba(255, 255, 255, 0.74);
}

.hero-desc {
  max-width: 780px;
  margin: 14px 0 0;
  color: rgba(255, 255, 255, 0.84);
  line-height: 1.75;
}

.hero-tags,
.panel-actions,
.chart-status {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: flex-end;
}

.hero-metrics {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
  margin-top: 24px;
}

.metric-card {
  padding: 16px 18px;
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.18);
}

.metric-label {
  display: block;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.72);
}

.metric-value {
  display: block;
  margin-top: 10px;
  font-size: 22px;
  font-weight: 700;
  line-height: 1.25;
}

.metric-time {
  font-size: 16px;
}

.metric-hint {
  margin: 10px 0 0;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.78);
}

.tree-search {
  margin-bottom: 16px;
}

.tree-wrapper {
  min-height: 640px;
  max-height: 72vh;
  overflow: auto;
  padding: 12px;
  border-radius: 18px;
  background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 0.88));
}

.control-panel,
.chart-panel {
  margin-bottom: 20px;
}

.control-form {
  margin-bottom: 12px;
}

.control-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 18px;
}

.control-grid-full {
  grid-column: 1 / -1;
}

.field-suffix {
  margin-left: 8px;
  color: var(--text-secondary);
}

.runtime-note {
  margin: 12px 2px 0;
  color: var(--text-secondary);
  font-size: 13px;
}

.chart-summary-bar {
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
  margin-bottom: 16px;
  padding: 14px 16px;
  border-radius: 16px;
  background: linear-gradient(90deg, rgba(37, 99, 235, 0.08), rgba(14, 165, 233, 0.05));
  color: var(--text-secondary);
}

.chart-grid {
  gap: 16px;
}

.chart-grid-single {
  grid-template-columns: 1fr;
}

.chart-grid-multi {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.chart-card {
  padding: 16px;
  border-radius: 22px;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.96)),
    linear-gradient(135deg, rgba(37, 99, 235, 0.06), rgba(14, 165, 233, 0.05));
  border: 1px solid rgba(226, 232, 240, 0.9);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.8),
    0 18px 48px rgba(37, 99, 235, 0.06);
}

.chart-card-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 12px;
}

.chart-card-head h4 {
  margin: 0;
  color: var(--text-primary);
  font-size: 16px;
}

.chart-card-head p {
  margin: 6px 0 0;
  color: var(--text-secondary);
  font-size: 12px;
}

.chart-canvas {
  width: 100%;
  height: 48vh;
  min-height: 380px;
  border-radius: 18px;
}

.empty-block {
  min-height: 56vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

@media (max-width: 1400px) {
  .hero-metrics {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 992px) {
  .hero-main,
  .control-grid,
  .chart-grid-multi {
    grid-template-columns: 1fr;
  }

  .hero-tags,
  .panel-actions,
  .chart-status {
    justify-content: flex-start;
  }

  .chart-canvas {
    height: 42vh;
    min-height: 320px;
  }
}

@media (max-width: 768px) {
  .hero-panel,
  .tree-panel,
  .control-panel,
  .chart-panel {
    padding: 16px;
    border-radius: 20px;
  }

  .hero-copy h2 {
    font-size: 24px;
  }

  .hero-metrics {
    grid-template-columns: 1fr;
  }

  .tree-wrapper {
    min-height: 360px;
  }
}
</style>
