<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  <title>正来的小站</title>
  <style>
    @font-face {
      font-family: "Noto Serif SC";
      src: url("https://fonts.gstatic.com/s/notoserifsc/v22/H4c8BXePl9DZ0Xe7gG9cyOj7oqP9qmtf.otf") format("opentype");
      font-weight: 200 900;
      font-style: normal;
      font-display: swap;
    }

    :root {
      --bg-app: #0a0a0a;
      --bg-surface: rgba(18, 18, 18, 0.85);
      --card-bg: rgba(255, 255, 255, 0.03);
      --card-border: rgba(255, 255, 255, 0.06);
      --divider: rgba(255, 255, 255, 0.08);
      --item-hover: rgba(255, 255, 255, 0.05);
      --text-primary: rgba(255, 255, 255, 0.92);
      --text-secondary: rgba(255, 255, 255, 0.55);
      --text-tertiary: rgba(255, 255, 255, 0.35);
      --accent: #ffffff;
      --accent-glow: rgba(255, 255, 255, 0.08);
      --shadow-main: 0 4px 24px rgba(0, 0, 0, 0.4);
      --font-stack: "Noto Serif SC", "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, -apple-system, serif;
      --radius-sm: 8px;
      --radius-md: 12px;
      --radius-lg: 16px;
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      -webkit-tap-highlight-color: transparent;
    }

    html,
    body {
      width: 100%;
      height: 100%;
      overflow: hidden;
      color: var(--text-primary);
      font-family: var(--font-stack);
    }

    html {
      background: var(--bg-app);
      font-size: 16px;
    }

    /* ─── Custom Cursor ─── */
    #cursor {
      position: fixed;
      pointer-events: none;
      z-index: 2147483647;
      width: 24px;
      height: 24px;
      transition: left 0.15s ease, top 0.15s ease, width 0.25s cubic-bezier(0.25,1,0.5,1), height 0.25s cubic-bezier(0.25,1,0.5,1);
      transform: translate(-50%, -50%);
      will-change: transform;
    }

    #cursor.expanded {
      transition: left 0.2s ease, top 0.2s ease, width 0.3s cubic-bezier(0.25,1,0.5,1), height 0.3s cubic-bezier(0.25,1,0.5,1), opacity 0.15s;
    }

    .cursor-corner {
      position: absolute;
      width: 12px;
      height: 12px;
      border-color: var(--text-primary);
      border-style: solid;
      border-width: 0;
    }

    .cursor-corner.tl { top: 0; left: 0; border-top-width: 2px; border-left-width: 2px; }
    .cursor-corner.tr { top: 0; right: 0; border-top-width: 2px; border-right-width: 2px; }
    .cursor-corner.bl { bottom: 0; left: 0; border-bottom-width: 2px; border-left-width: 2px; }
    .cursor-corner.br { bottom: 0; right: 0; border-bottom-width: 2px; border-right-width: 2px; }

    body {
      background: transparent;
      display: flex;
      flex-direction: column;
    }

    #bg-layer {
      position: fixed;
      inset: 0;
      z-index: 0;
      background-size: cover;
      background-position: center;
      background-repeat: no-repeat;
      opacity: 0.25;
      transition: opacity 0.6s;
    }

    /* ─── Layout ─── */
    .layout {
      display: flex;
      flex: 1;
      padding: 12px;
      gap: 12px;
      overflow: hidden;
      position: relative;
      z-index: 1;
    }

    /* ─── Sidebar ─── */
    .sidebar {
      width: 240px;
      flex-shrink: 0;
      display: flex;
      flex-direction: column;
      gap: 4px;
      padding: 20px 10px 10px 10px;
    }

    .sidebar-avatar {
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 24px 0 20px;
    }

    .sidebar-avatar-name {
      font-family: "Noto Serif SC", serif;
      font-size: 22px;
      font-weight: 900;
      color: var(--text-primary);
      letter-spacing: 2px;
      position: relative;
      padding: 8px 14px;
    }

    .sidebar-avatar-name::before,
    .sidebar-avatar-name::after {
      content: '';
      position: absolute;
      width: 10px;
      height: 10px;
    }

    .sidebar-avatar-name::before {
      top: 0;
      left: 0;
      border-top: 2px solid var(--text-tertiary);
      border-left: 2px solid var(--text-tertiary);
    }

    .sidebar-avatar-name::after {
      bottom: 0;
      right: 0;
      border-bottom: 2px solid var(--text-tertiary);
      border-right: 2px solid var(--text-tertiary);
    }

    .sidebar-divider {
      height: 1px;
      background: var(--divider);
      margin: 4px 14px;
    }

    .sidebar-item {
      padding: 10px 14px;
      font-size: 15px;
      color: var(--text-secondary);
      border-radius: 10px;
      cursor: pointer;
      transition: all 0.22s cubic-bezier(0.25, 1, 0.5, 1);
      display: flex;
      align-items: center;
      gap: 12px;
      background: transparent;
      border: none;
      width: 100%;
      text-align: left;
      position: relative;
      overflow: hidden;
    }

    .sidebar-item::before {
      content: "";
      position: absolute;
      inset: 0;
      background: var(--item-hover);
      border-radius: 10px;
      opacity: 0;
      scale: 0.92;
      transition: opacity 0.22s cubic-bezier(0.25, 1, 0.5, 1),
        scale 0.22s cubic-bezier(0.25, 1, 0.5, 1);
      z-index: 0;
    }

    .sidebar-item>* {
      position: relative;
      z-index: 1;
    }

    .sidebar-item:hover {
      color: var(--text-primary);
    }

    .sidebar-item:hover::before {
      opacity: 1;
      scale: 1;
    }

    .sidebar-item.active {
      color: var(--accent);
      font-weight: 600;
    }

    .sidebar-item.active::before {
      background: var(--accent-glow);
      opacity: 1;
      scale: 1;
    }

    .sidebar-icon {
      width: 18px;
      height: 18px;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.8;
      stroke-linecap: round;
      stroke-linejoin: round;
      opacity: 0.7;
      flex-shrink: 0;
    }

    .sidebar-item.active .sidebar-icon {
      opacity: 1;
    }

    .sidebar-nav-desktop {
      display: flex;
      flex-direction: column;
      gap: 4px;
    }

    .sidebar-nav-mobile {
      display: none;
    }

    .sidebar-footer {
      margin-top: auto;
      display: flex;
      flex-direction: column;
      gap: 4px;
    }

    .sidebar-version {
      padding: 6px 14px;
      font-size: 12px;
      color: var(--text-tertiary);
      letter-spacing: 0.5px;
      font-variant-numeric: tabular-nums;
    }

    .sidebar-tts-btn {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 8px 14px;
      font-size: 13px;
      color: var(--text-tertiary);
      cursor: pointer;
      border-radius: 8px;
      transition: all 0.2s;
      background: transparent;
      border: none;
      width: 100%;
      text-align: left;
    }

    .sidebar-tts-btn:hover {
      color: var(--text-secondary);
      background: var(--item-hover);
    }

    .sidebar-tts-btn.active {
      color: var(--accent);
      background: var(--accent-glow);
    }

    .sidebar-tts-btn svg {
      width: 16px;
      height: 16px;
      flex-shrink: 0;
    }

    .tts-highlight {
      background: rgba(255, 255, 255, 0.15);
      border-radius: 2px;
      transition: background 0.1s;
    }

    .tts-highlight-current {
      background: rgba(255, 255, 255, 0.35);
      border-radius: 2px;
    }

    /* ─── Content ─── */
    .content {
      flex: 1;
      padding: 32px 40px;
      overflow-y: auto;
      background: rgba(18, 18, 18, 0.2);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      border-radius: var(--radius-lg);
      border: 1px solid var(--divider);
      box-shadow: var(--shadow-main);
      display: flex;
      flex-direction: column;
    }

    .content::-webkit-scrollbar {
      width: 5px;
    }

    .content::-webkit-scrollbar-track {
      background: transparent;
    }

    .content::-webkit-scrollbar-thumb {
      background: rgba(128, 128, 128, 0.2);
      border-radius: 999px;
    }

    .section {
      display: none;
      opacity: 0;
      transform: translateY(16px);
    }

    .section.active {
      display: block;
      animation: sectionIn 0.35s cubic-bezier(0.1, 0.9, 0.2, 1) forwards;
    }

    #section-home {
      min-height: 100%;
      display: none;
    }

    #section-home.active {
      display: flex;
      align-items: center;
      justify-content: center;
    }

    @keyframes sectionIn {
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    .page-title {
      font-size: 28px;
      font-weight: 700;
      margin-bottom: 24px;
      color: var(--text-primary);
      letter-spacing: 0.5px;
    }

    /* ─── Card Group ─── */
    .card-group {
      background: var(--card-bg);
      border-radius: var(--radius-md);
      margin-bottom: 20px;
      border: 1px solid var(--card-border);
    }

    .card-group-header {
      padding: 14px 20px 8px;
      font-size: 13px;
      font-weight: 600;
      color: var(--text-secondary);
      text-transform: uppercase;
      letter-spacing: 0.8px;
    }

    .card-item {
      padding: 16px 20px;
      display: flex;
      align-items: center;
      gap: 14px;
      transition: background 0.15s;
    }

    .card-item:last-child {
      border-bottom: none;
    }

    .card-item:hover {
      background: var(--item-hover);
    }

    .card-item-clickable {
      cursor: pointer;
    }

    .card-item-icon {
      width: 40px;
      height: 40px;
      border-radius: 10px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 18px;
      flex-shrink: 0;
    }

    .card-item-info {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 3px;
      min-width: 0;
    }

    .card-item-label {
      font-size: 15px;
      font-weight: 500;
      color: var(--text-primary);
    }

    .card-item-desc {
      font-size: 13px;
      color: var(--text-secondary);
      line-height: 1.4;
    }

    .card-item-extra {
      font-size: 13px;
      color: var(--text-tertiary);
      flex-shrink: 0;
      text-align: right;
    }

    .card-item-tag {
      display: inline-block;
      padding: 2px 10px;
      font-size: 11px;
      font-weight: 600;
      border-radius: 999px;
      background: var(--accent-glow);
      color: var(--accent);
      letter-spacing: 0.5px;
    }

    /* ─── Hero Section ─── */
    .hero {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 80px;
      padding: 48px 0 56px;
    }

    .hero-left {
      display: flex;
      flex-direction: column;
      gap: 16px;
      max-width: 420px;
    }

    .hero-label {
      font-size: 14px;
      color: var(--accent);
      letter-spacing: 1px;
    }

    .hero-name {
      font-size: 52px;
      font-weight: 700;
      color: var(--text-primary);
      line-height: 1.15;
      letter-spacing: -0.5px;
    }

    .hero-bio {
      font-size: 15px;
      color: var(--text-secondary);
      line-height: 1.7;
    }

    .hero-location {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 14px;
      color: var(--text-secondary);
      cursor: pointer;
      transition: color 0.2s;
      width: fit-content;
    }

    .hero-location:hover {
      color: var(--text-primary);
    }

    .hero-location svg {
      width: 16px;
      height: 16px;
      flex-shrink: 0;
    }

    /* ─── Terminal Panel (Right) ─── */
    .hero-terminal {
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid var(--card-border);
      border-radius: var(--radius-md);
      padding: 24px 28px;
      width: 360px;
      display: flex;
      flex-direction: column;
      gap: 18px;
      flex-shrink: 0;
    }

    .terminal-line {
      display: flex;
      flex-direction: column;
      gap: 4px;
    }

    .terminal-cmd {
      font-size: 13px;
      color: var(--text-tertiary);
      font-family: "Cascadia Code", "Fira Code", "Consolas", monospace;
    }

    .terminal-cmd .prompt {
      color: var(--accent);
      margin-right: 6px;
    }

    .terminal-output {
      font-size: 14px;
      color: var(--text-secondary);
      line-height: 1.6;
      padding-left: 20px;
    }

    .terminal-output .highlight {
      color: var(--text-primary);
      font-weight: 600;
    }

    .terminal-output a {
      color: var(--text-secondary);
      text-decoration: none;
      transition: color 0.2s;
    }

    .terminal-output a:hover {
      color: var(--text-primary);
    }

    /* ─── Project Cards ─── */
    .project-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
      gap: 16px;
    }

    .project-card {
      background: var(--card-bg);
      border: 1px solid var(--card-border);
      border-radius: var(--radius-md);
      padding: 20px;
      transition: all 0.2s;
      cursor: pointer;
    }

    .project-card:hover {
      border-color: var(--accent);
      transform: translateY(-2px);
      box-shadow: 0 8px 24px var(--accent-glow);
    }

    .project-card-header {
      display: flex;
      align-items: center;
      gap: 12px;
      margin-bottom: 10px;
    }

    .project-card-icon {
      width: 36px;
      height: 36px;
      border-radius: 8px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 16px;
      flex-shrink: 0;
    }

    .project-card-name {
      font-size: 15px;
      font-weight: 600;
      color: var(--text-primary);
    }

    .project-card-desc {
      font-size: 13px;
      color: var(--text-secondary);
      line-height: 1.5;
      margin-bottom: 12px;
    }

    .project-card-tags {
      display: flex;
      gap: 6px;
      flex-wrap: wrap;
    }

    .project-tag {
      padding: 2px 10px;
      font-size: 11px;
      font-weight: 500;
      border-radius: 999px;
      background: var(--item-hover);
      color: var(--text-secondary);
      border: 1px solid var(--divider);
    }

    /* ─── Blog List ─── */
    .blog-list {
      display: flex;
      flex-direction: column;
    }

    .blog-item {
      padding: 18px 20px;
      display: flex;
      align-items: flex-start;
      gap: 16px;
      border-bottom: 1px solid var(--divider);
      cursor: pointer;
      transition: background 0.15s;
    }

    .blog-item:first-child {
      border-radius: var(--radius-md) var(--radius-md) 0 0;
    }

    .blog-item:last-child {
      border-bottom: none;
      border-radius: 0 0 var(--radius-md) var(--radius-md);
    }

    .blog-item:hover {
      background: var(--item-hover);
    }

    .blog-item-date {
      font-size: 12px;
      color: var(--text-tertiary);
      flex-shrink: 0;
      width: 80px;
      padding-top: 2px;
      font-variant-numeric: tabular-nums;
    }

    .blog-item-info {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 4px;
    }

    .blog-item-title {
      font-size: 15px;
      font-weight: 500;
      color: var(--text-primary);
      line-height: 1.4;
    }

    .blog-item-excerpt {
      font-size: 13px;
      color: var(--text-secondary);
      line-height: 1.5;
    }

    .blog-item-tag {
      padding: 1px 8px;
      font-size: 11px;
      border-radius: 999px;
      background: var(--accent-glow);
      color: var(--accent);
      font-weight: 500;
      display: inline-block;
      margin-top: 2px;
      align-self: flex-start;
    }

    /* ─── Blog Loading / Detail ─── */
    .blog-loading {
      text-align: center;
      padding: 48px 0;
      color: var(--text-tertiary);
      font-size: 14px;
    }

    .blog-detail {
      animation: sectionIn 0.3s ease forwards;
    }

    .blog-detail-header {
      display: flex;
      align-items: center;
      gap: 16px;
      padding: 12px 0;
      margin-bottom: 12px;
      background: transparent;
      border: none;
    }

    .blog-detail-icon {
      display: flex;
      align-items: center;
      justify-content: center;
      color: var(--text-secondary);
      flex-shrink: 0;
      transition: color 0.25s cubic-bezier(0.16, 1, 0.3, 1);
      cursor: pointer;
    }

    .blog-detail-icon:hover {
      color: var(--text-primary);
    }

    .blog-detail-icon svg {
      width: 42px;
      height: 42px;
    }

    .blog-detail-info {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 2px;
    }

    .blog-crumb {
      color: var(--text-secondary);
      font-size: 15px;
      font-weight: 500;
      cursor: pointer;
      transition: color 0.25s cubic-bezier(0.16, 1, 0.3, 1);
      letter-spacing: 0.2px;
      display: inline-flex;
      align-items: center;
      gap: 8px;
    }

    .blog-crumb:hover {
      color: var(--text-primary);
    }

    .blog-crumb-sep {
      color: var(--text-tertiary);
      font-weight: 400;
      font-size: 15px;
    }

    .blog-crumb-current {
      color: var(--text-primary);
      font-size: 15px;
      font-weight: 500;
    }

    .blog-detail-meta {
      font-size: 13px;
      color: var(--text-secondary);
      opacity: 0.6;
    }

    .blog-detail-article {
      color: var(--text-secondary);
      line-height: 1.85;
      font-size: 15px;
    }

    .blog-detail-article h1,
    .blog-detail-article h2,
    .blog-detail-article h3 {
      color: var(--text-primary);
      margin: 28px 0 12px;
      font-weight: 600;
    }

    .blog-detail-article h1 { font-size: 26px; }
    .blog-detail-article h2 { font-size: 21px; }
    .blog-detail-article h3 { font-size: 17px; }

    .blog-detail-article p { margin: 10px 0; }

    .blog-detail-article code {
      background: rgba(255,255,255,0.06);
      padding: 2px 6px;
      border-radius: 4px;
      font-size: 13px;
      font-family: "Cascadia Code", "Fira Code", "Consolas", monospace;
    }

    .blog-detail-article pre {
      background: rgba(0,0,0,0.35);
      padding: 18px 22px;
      border-radius: var(--radius-sm);
      overflow-x: auto;
      margin: 16px 0;
      border: 1px solid var(--card-border);
    }

    .blog-detail-article pre code {
      background: none;
      padding: 0;
      font-size: 13px;
      line-height: 1.65;
    }

    .blog-detail-article blockquote {
      border-left: 3px solid var(--accent-glow);
      padding-left: 16px;
      margin: 12px 0;
      color: var(--text-tertiary);
    }

    .blog-detail-article ul,
    .blog-detail-article ol {
      padding-left: 24px;
      margin: 10px 0;
    }

    .blog-detail-article li { margin: 4px 0; }

    .blog-detail-article strong { color: var(--text-primary); }
    .blog-detail-article hr {
      border: none;
      border-top: 1px solid var(--divider);
      margin: 24px 0;
    }

    /* ─── Friend Links ─── */
    .friend-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
      gap: 14px;
    }

    .friend-card {
      background: var(--card-bg);
      border: 1px solid var(--card-border);
      border-radius: var(--radius-md);
      padding: 18px;
      display: flex;
      align-items: center;
      gap: 14px;
      cursor: pointer;
      transition: all 0.2s;
    }

    .friend-card:hover {
      border-color: var(--accent);
      transform: translateY(-2px);
      box-shadow: 0 6px 20px var(--accent-glow);
    }

    .friend-avatar {
      width: 44px;
      height: 44px;
      border-radius: 50%;
      flex-shrink: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 20px;
      font-weight: 700;
      color: #fff;
    }

    .friend-info {
      display: flex;
      flex-direction: column;
      gap: 3px;
      min-width: 0;
    }

    .friend-name {
      font-size: 14px;
      font-weight: 600;
      color: var(--text-primary);
    }

    .friend-desc {
      font-size: 12px;
      color: var(--text-secondary);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    /* ─── Status Indicator ─── */
    .status-dot {
      display: inline-block;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: #34c759;
      margin-right: 6px;
      box-shadow: 0 0 6px rgba(52, 199, 89, 0.4);
    }

    /* ─── Responsive ─── */
    /* ── Mobile "More" Menu ── */
    .mobile-more-menu {
      display: none;
      position: fixed;
      bottom: calc(env(safe-area-inset-bottom, 0px) + 72px);
      right: calc(env(safe-area-inset-right, 0px) + 16px);
      background: var(--bg-surface);
      border: 1px solid var(--card-border);
      border-radius: var(--radius-md);
      padding: 6px;
      min-width: 160px;
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
      z-index: 100;
      animation: menuIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
    }

    @keyframes menuIn {
      from { opacity: 0; transform: translateY(8px) scale(0.95); }
      to { opacity: 1; transform: translateY(0) scale(1); }
    }

    .mobile-more-menu.show {
      display: block;
    }

    .mobile-more-item {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 10px 14px;
      font-size: 14px;
      color: var(--text-secondary);
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.15s;
      border: none;
      background: transparent;
      width: 100%;
      text-align: left;
    }

    .mobile-more-item:hover {
      background: var(--item-hover);
      color: var(--text-primary);
    }

    .mobile-more-item.active {
      color: var(--accent);
    }

    .mobile-more-item svg {
      width: 16px;
      height: 16px;
      flex-shrink: 0;
    }

    .mobile-version {
      display: none;
      position: fixed;
      bottom: calc(env(safe-area-inset-bottom, 0px) + 60px);
      right: calc(env(safe-area-inset-right, 0px) + 16px);
      font-size: 11px;
      color: var(--text-tertiary);
      z-index: 50;
      letter-spacing: 0.5px;
      font-variant-numeric: tabular-nums;
    }

    @media (max-width: 768px) {
      #cursor {
        display: none !important;
      }

      .layout {
        flex-direction: column;
        padding: 8px;
        gap: 8px;
        height: 100vh;
      }

      .sidebar {
        width: 100%;
        flex-direction: column;
        padding: 0;
        gap: 0;
        background: transparent;
        border-bottom: none;
      }

      .sidebar-avatar {
        padding: calc(env(safe-area-inset-top, 0px) + 10px) calc(env(safe-area-inset-right, 0px) + 12px) 6px calc(env(safe-area-inset-left, 0px) + 12px);
      }

      .sidebar-avatar-name {
        font-size: 18px;
      }

      .sidebar-avatar-name::before,
      .sidebar-avatar-name::after {
        width: 8px;
        height: 8px;
      }

      .sidebar-divider {
        display: none;
      }

      .sidebar-footer {
        display: none;
      }

      .sidebar-nav-desktop {
        display: none;
      }

      .sidebar-nav-mobile {
        display: flex;
        flex-direction: row;
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        background: transparent;
        z-index: 50;
        padding: 8px calc(env(safe-area-inset-right, 0px) + 12px) calc(env(safe-area-inset-bottom, 0px) + 8px) calc(env(safe-area-inset-left, 0px) + 12px);
        gap: 6px;
      }

      .sidebar-nav-mobile .sidebar-item {
        flex: 1;
        padding: 8px 10px;
        font-size: 13px;
        gap: 8px;
        width: auto;
        justify-content: center;
        align-items: center;
        border-radius: 10px;
        flex-direction: row;
        color: var(--text-secondary);
        position: relative;
        overflow: hidden;
      }

      .sidebar-nav-mobile .sidebar-item::before {
        border-radius: 10px;
      }

      .sidebar-nav-mobile .sidebar-item.active {
        color: var(--accent);
        font-weight: 600;
      }

      .sidebar-nav-mobile .sidebar-item.active::before {
        background: var(--accent-glow);
        opacity: 1;
        scale: 1;
      }

      .sidebar-nav-mobile .sidebar-icon {
        width: 18px;
        height: 18px;
        opacity: 0.7;
      }

      .sidebar-nav-mobile .sidebar-item.active .sidebar-icon {
        opacity: 1;
      }

      .content {
        flex: 1;
        padding: 20px calc(env(safe-area-inset-right, 0px) + 16px) calc(env(safe-area-inset-bottom, 0px) + 72px) calc(env(safe-area-inset-left, 0px) + 16px);
        background: rgba(18, 18, 18, 0.2);
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        border-radius: var(--radius-lg);
        border: 1px solid var(--divider);
        box-shadow: var(--shadow-main);
        overflow-y: auto;
      }

      .mobile-version {
        display: block;
      }

      .hero {
        flex-direction: column;
        text-align: center;
        gap: 32px;
      }

      .hero-left {
        max-width: none;
        align-items: center;
      }

      .hero-name {
        font-size: 32px;
      }

      .hero-terminal {
        width: 100%;
        max-width: 380px;
      }

      .project-grid,
      .friend-grid {
        grid-template-columns: 1fr;
      }

      .page-title {
        font-size: 22px;
      }

      .blog-detail-header {
        gap: 12px;
      }

      .blog-detail-icon svg {
        width: 32px;
        height: 32px;
      }
    }
  </style>
</head>

<body>
  <div id="cursor">
    <div class="cursor-corner tl"></div>
    <div class="cursor-corner tr"></div>
    <div class="cursor-corner bl"></div>
    <div class="cursor-corner br"></div>
  </div>
  <div id="bg-layer"></div>
  <div class="layout">
    <!-- ── Sidebar ── -->
    <aside class="sidebar">
      <div class="sidebar-avatar">
        <div class="sidebar-avatar-name">正来の小站</div>
      </div>
      <div class="sidebar-divider"></div>

      <div class="sidebar-nav-desktop">
        <button class="sidebar-item active" data-section="home">
          <svg class="sidebar-icon" viewBox="0 0 24 24">
            <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
            <polyline points="9 22 9 12 15 12 15 22" />
          </svg>
          主页
        </button>
        <button class="sidebar-item" data-section="blog">
          <svg class="sidebar-icon" viewBox="0 0 24 24">
            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
            <polyline points="14 2 14 8 20 8" />
            <line x1="16" y1="13" x2="8" y2="13" />
            <line x1="16" y1="17" x2="8" y2="17" />
          </svg>
          博客
        </button>
        <button class="sidebar-item" data-section="friends">
          <svg class="sidebar-icon" viewBox="0 0 24 24">
            <circle cx="12" cy="12" r="10" />
            <path d="M10.59 13.41a2 2 0 0 1 0-2.82l2-2a2 2 0 1 1 2.82 2.82l-1.29 1.3" />
            <circle cx="6" cy="18" r="2" />
            <circle cx="18" cy="18" r="2" />
          </svg>
          友链
        </button>
        <button class="sidebar-item" data-section="projects">
          <svg class="sidebar-icon" viewBox="0 0 24 24">
            <polyline points="16 18 22 12 16 6" />
            <polyline points="8 6 2 12 8 18" />
          </svg>
          项目
        </button>
      </div>

      <div class="sidebar-footer">
        <div class="sidebar-divider"></div>
        <button class="sidebar-tts-btn" id="tts-btn" onclick="toggleTTS()">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
            <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
            <path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
          </svg>
          朗读
        </button>
        <div class="sidebar-version">v1.0.0</div>
      </div>
    </aside>

    <!-- ── Content ── -->
    <main class="content" id="content">
      <!-- 主页 -->
      <section class="section active" id="section-home">
        <div class="hero">
          <div class="hero-left">
            <div class="hero-label">// About</div>
            <div class="hero-name">初めまして（?）、正来です。</div>
            <div class="hero-bio">镜子里的左后方是台北市的东边，在春天的清晨六点零五分，太阳开始升起。一个不需要任何人说任何字的画面，橘色和金黄色的光，踏着缓慢而确定的脚步。<br>蒙蒙的脑袋突然想象起一个有限的世界，一个完备、一致、可判定的世界。
                <br>生命是冷硬的，没有意外、没有惊喜，绝对的具体。<br>永远失去了对未知探索的好奇心，因为何必多此一举？<br>用冷水洗把脸，现在的水温还是足够让我瞬间清醒。<br>我飘到哪了？专心看路，你正在跑步。</div>
            <div class="hero-location" onclick="switchSection('friends')">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
              Earth
            </div>
          </div>

          <div class="hero-terminal">
            <div class="terminal-line">
              <span class="terminal-cmd"><span class="prompt">&gt;</span> whoami</span>
              <span class="terminal-output"><span class="highlight">正来</span></span>
            </div>
            <div class="terminal-line">
              <span class="terminal-cmd"><span class="prompt">&gt;</span> cat bio.txt</span>
              <span class="terminal-output">
                かいはつ者/ 眠りがち<br>
                喜欢捣鼓有趣的东西
              </span>
            </div>
            <div class="terminal-line">
              <span class="terminal-cmd"><span class="prompt">&gt;</span> ls -la skills/</span>
              <span class="terminal-output">
                <a href="#" onclick="switchSection('projects')">drwxr-xr-x 前端 Python 运维</a>
              </span>
            </div>
            <div class="terminal-line">
              <span class="terminal-cmd"><span class="prompt">&gt;</span> curl -s links | jq</span>
              <span class="terminal-output">
                <a href="https://github.com/Haraguse" target="_blank">GitHub</a>&nbsp;
                <a href="https://space.bilibili.com/1217022368" target="_blank">Bilibili</a>&nbsp;
                <a href="mailto:chihuyou90@gmail.com">Email</a>
              </span>
            </div>
          </div>
        </div>
      </section>

      <!-- 博客 -->
      <section class="section" id="section-blog">
        <div class="page-title">博客</div>
        <div class="card-group blog-list" id="blog-list">
          <div class="blog-loading">正在加载博客...</div>
        </div>

        <!-- 博客详情 -->
        <div class="blog-detail" id="blog-detail" style="display:none;">
          <div class="blog-detail-header">
            <div class="blog-detail-icon" onclick="closeBlogDetail(event)">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                <path d="M19 12H5M12 19l-7-7 7-7"/>
              </svg>
            </div>
            <div class="blog-detail-info">
              <div class="blog-crumb" onclick="closeBlogDetail(event)">博客<span class="blog-crumb-sep">›</span><span class="blog-crumb-current" id="blog-detail-title"></span></div>
              <div class="blog-detail-meta" id="blog-detail-meta"></div>
            </div>
          </div>
          <article class="blog-detail-article" id="blog-detail-content"></article>
        </div>
      </section>

      <!-- 友链 -->
      <section class="section" id="section-friends">
        <div class="page-title">友链</div>
        <p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">以下是我经常拜访的朋友们，排名不分先后</p>

        <div class="friend-grid" id="friend-grid">
          <div class="blog-loading">正在加载友链...</div>
        </div>
      </section>

      <!-- 项目 -->
      <section class="section" id="section-projects">
        <div class="page-title">项目</div>
        <div class="project-grid" id="project-grid">
          <div class="blog-loading">正在加载项目...</div>
        </div>
      </section>
    </main>
  </div>

  <!-- Mobile Bottom Nav -->
  <div class="sidebar-nav-mobile">
    <button class="sidebar-item active" data-section="home">
      <svg class="sidebar-icon" viewBox="0 0 24 24">
        <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
        <polyline points="9 22 9 12 15 12 15 22" />
      </svg>
      主页
    </button>
    <button class="sidebar-item" data-section="blog">
      <svg class="sidebar-icon" viewBox="0 0 24 24">
        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
        <polyline points="14 2 14 8 20 8" />
        <line x1="16" y1="13" x2="8" y2="13" />
        <line x1="16" y1="17" x2="8" y2="17" />
      </svg>
      博客
    </button>
    <button class="sidebar-item" data-section="friends">
      <svg class="sidebar-icon" viewBox="0 0 24 24">
        <circle cx="12" cy="12" r="10" />
        <path d="M10.59 13.41a2 2 0 0 1 0-2.82l2-2a2 2 0 1 1 2.82 2.82l-1.29 1.3" />
        <circle cx="6" cy="18" r="2" />
        <circle cx="18" cy="18" r="2" />
      </svg>
      友链
    </button>
    <button class="sidebar-item" data-section="projects">
      <svg class="sidebar-icon" viewBox="0 0 24 24">
        <polyline points="16 18 22 12 16 6" />
        <polyline points="8 6 2 12 8 18" />
      </svg>
      项目
    </button>
  </div>

  <!-- Floating Action Buttons -->
  <div class="fab-container">
    <button class="fab-item fab-more" id="fab-more" onclick="toggleMobileMore()" title="更多">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/>
      </svg>
    </button>
  </div>

  <!-- Mobile More Menu -->
  <div class="mobile-more-menu" id="mobile-more-menu">
    <button class="mobile-more-item" id="mobile-tts-btn" onclick="toggleTTS(); closeMobileMore();">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
        <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
        <path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
      </svg>
      朗读
    </button>
  </div>
  <div class="mobile-version">v1.0.0</div>

  <script>
    // ── Section Switching ──
    const sidebarItems = document.querySelectorAll('.sidebar-item[data-section]');
    const sections = document.querySelectorAll('.section');

    function switchSection(sectionName) {
      // Update sidebar
      sidebarItems.forEach(item => {
        item.classList.toggle('active', item.dataset.section === sectionName);
      });

      // Update sections
      sections.forEach(section => {
        section.classList.remove('active');
      });
      const target = document.getElementById('section-' + sectionName);
      if (target) {
        target.classList.add('active');
        document.getElementById('content').scrollTop = 0;
      }
    }

    sidebarItems.forEach(item => {
      item.addEventListener('click', () => {
        switchSection(item.dataset.section);
      });
    });

    // ── Custom Cursor ──
    (function () {
      const cursor = document.getElementById('cursor');
      const interactive = 'a, button, [onclick], .sidebar-item, .hero-location, .terminal-output a, .card-item-clickable, .blog-item, .project-card, .friend-card';
      let mouseX = 0, mouseY = 0;
      let currentX = 0, currentY = 0;

      document.addEventListener('mousemove', e => {
        mouseX = e.clientX;
        mouseY = e.clientY;
        // instant position update for following
        cursor.style.left = mouseX + 'px';
        cursor.style.top = mouseY + 'px';
      });

      document.addEventListener('mouseenter', () => {
        cursor.style.opacity = '1';
      }, true);

      document.addEventListener('mouseleave', () => {
        cursor.style.opacity = '0';
      }, true);

      // Expand on interactive elements
      document.querySelectorAll(interactive).forEach(el => {
        el.addEventListener('mouseenter', function () {
          const rect = this.getBoundingClientRect();
          cursor.classList.add('expanded');
          cursor.style.width = rect.width + 16 + 'px';
          cursor.style.height = rect.height + 16 + 'px';
          cursor.style.left = rect.left + rect.width / 2 + 'px';
          cursor.style.top = rect.top + rect.height / 2 + 'px';
        });
        el.addEventListener('mouseleave', function () {
          cursor.classList.remove('expanded');
          cursor.style.width = '24px';
          cursor.style.height = '24px';
          cursor.style.left = mouseX + 'px';
          cursor.style.top = mouseY + 'px';
        });
      });

      // Also handle dynamically added elements via event delegation
      document.body.addEventListener('mouseover', e => {
        const target = e.target.closest(interactive);
        if (target && !target.dataset.cursorBound) {
          target.dataset.cursorBound = '1';
          target.addEventListener('mouseenter', function () {
            const rect = this.getBoundingClientRect();
            cursor.classList.add('expanded');
            cursor.style.width = rect.width + 16 + 'px';
            cursor.style.height = rect.height + 16 + 'px';
            cursor.style.left = rect.left + rect.width / 2 + 'px';
            cursor.style.top = rect.top + rect.height / 2 + 'px';
          });
          target.addEventListener('mouseleave', function () {
            cursor.classList.remove('expanded');
            cursor.style.width = '24px';
            cursor.style.height = '24px';
            cursor.style.left = mouseX + 'px';
            cursor.style.top = mouseY + 'px';
          });
        }
      });
    })();

    // ── Init ──
    // Load random background from API (follow 302 redirect to final image)
    (async function () {
      const bg = document.getElementById('bg-layer');
      try {
        const resp = await fetch('https://t.alcy.cc/fj');
        bg.style.backgroundImage = `url('${resp.url}')`;
      } catch {
        bg.style.backgroundImage = "url('https://t.alcy.cc/fj')";
      }
    })();

    // ── Blog Loader ──
    const BLOG_STORE = {};

    function parseBlogFilename(name) {
      // 格式: YYYYMMDD-tag.md
      const match = name.match(/^(\d{4})(\d{2})(\d{2})-(.+)\.md$/);
      if (!match) return null;
      return {
        year: match[1], month: match[2], day: match[3],
        date: `${match[1]}-${match[2]}-${match[3]}`,
        tag: match[4],
        sortKey: match[1] + match[2] + match[3]
      };
    }

    function parseBlogContent(text) {
      const lines = text.split('\n');
      let title = '', excerptLines = [], bodyLines = [];
      let phase = 'title'; // title -> excerpt -> body

      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (phase === 'title') {
          const m = line.match(/^#\s+(.+)/);
          if (m) { title = m[1].trim(); phase = 'excerpt'; continue; }
          // 没有 # 标题则第一行当标题
          if (line.trim() && !title) { title = line.trim(); phase = 'excerpt'; continue; }
        } else if (phase === 'excerpt') {
          if (/^---+$/.test(line.trim())) { phase = 'body'; continue; }
          if (line.trim()) excerptLines.push(line);
          else if (excerptLines.length > 0) phase = 'body'; // 空行结束摘要
        } else {
          bodyLines.push(line);
        }
      }

      // 如果没有 --- 分割，excerpts 全部归入 body
      if (bodyLines.length === 0 && excerptLines.length > 0) {
        bodyLines = excerptLines;
        excerptLines = excerptLines.slice(0, 1); // 第一行做摘要
      }

      return {
        title,
        excerpt: excerptLines.join(' ').trim().slice(0, 120),
        body: bodyLines.join('\n').trim()
      };
    }

    function renderMarkdown(md) {
      return md
        .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
        .replace(/`([^`\n]+)`/g, '<code>$1</code>')
        .replace(/^### (.+)$/gm, '<h3>$1</h3>')
        .replace(/^## (.+)$/gm, '<h2>$1</h2>')
        .replace(/^# (.+)$/gm, '<h1>$1</h1>')
        .replace(/^\> (.+)$/gm, '<blockquote>$1</blockquote>')
        .replace(/^\- (.+)$/gm, '<li>$1</li>')
        .replace(/^(\d+)\. (.+)$/gm, '<li>$2</li>')
        .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
        .replace(/\n{2,}/g, '</p><p>')
        .replace(/\n/g, '<br>');
    }

    async function loadBlogs() {
      const listEl = document.getElementById('blog-list');
      listEl.innerHTML = '<div class="blog-loading">正在加载博客...</div>';

      // 尝试扫描 blogs 目录下的 .md 文件
      let fileNames = [];

      // 方式1：读取 manifest.json（Vercel / 生产环境）
      try {
        const manifestResp = await fetch('blogs/manifest.json');
        if (manifestResp.ok) {
          const manifest = await manifestResp.json();
          if (Array.isArray(manifest.files)) {
            fileNames = manifest.files.filter(f => /\.md$/i.test(f));
          }
        }
      } catch { /* 继续尝试其他方式 */ }

      // 方式2：目录列表（本地 server.js）
      if (fileNames.length === 0) {
        try {
          const idxResp = await fetch('blogs/', { method: 'GET' });
          const idxText = await idxResp.text();
          const links = idxText.match(/href="([^"]+\.md)"/g);
          if (links) {
            fileNames = links.map(l => {
              const match = l.match(/href="([^"]+)"/);
              return match ? match[1].replace(/\\/g, '/').split('/').pop() : null;
            }).filter(Boolean);
          }
        } catch { /* 继续回退 */ }
      }

      // 如果目录列表不可用，按日期模式探测
      if (fileNames.length === 0) {
        const probes = [];
        const _now = new Date();
        for (let offset = 0; offset < 365 * 2; offset++) {
          const d = new Date(_now.getFullYear(), _now.getMonth(), _now.getDate() - offset);
          const prefix = `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}-`;
          probes.push(prefix);
        }

        // 并发探测（HEAD 请求）
        const results = await Promise.allSettled(
          probes.map(async p => {
            // 尝试常见的 tag 名组合
            for (const tag of ['life', 'tech', 'note', 'diary', 'dev', 'css', 'ts', 'tools', 'misc']) {
              try {
                const r = await fetch(`blogs/${p}${tag}.md`, { method: 'HEAD' });
                if (r.ok) return `${p}${tag}.md`;
              } catch { /* ignore */ }
            }
            return null;
          })
        );

        fileNames = results
          .filter(r => r.status === 'fulfilled' && r.value)
          .map(r => r.value);
      }

      // 如果还是没找到，提示用户
      if (fileNames.length === 0) {
        listEl.innerHTML = '<div class="blog-loading">blogs 文件夹下暂无文章<br><small style="color:var(--text-tertiary)">请放入 YYYYMMDD-tag.md 格式的文件</small></div>';
        return;
      }

      // 加载每篇文章内容
      const posts = [];
      for (const name of fileNames) {
        try {
          const resp = await fetch(`blogs/${name}`);
          const text = await resp.text();
          const meta = parseBlogFilename(name);
          const content = parseBlogContent(text);
          if (meta && content.title) {
            posts.push({ ...meta, ...content, filename: name });
          }
        } catch { /* skip */ }
      }

      // 按日期降序排列
      posts.sort((a, b) => b.sortKey.localeCompare(a.sortKey));

      if (posts.length === 0) {
        listEl.innerHTML = '<div class="blog-loading">未找到有效博客文章</div>';
        return;
      }

      // 渲染列表
      BLOG_STORE.posts = posts;
      listEl.innerHTML = '';
      posts.forEach((post, i) => {
        const el = document.createElement('div');
        el.className = 'blog-item';
        el.dataset.index = i;
        el.innerHTML = `
          <span class="blog-item-date">${post.date}</span>
          <div class="blog-item-info">
            <span class="blog-item-title">${post.title}</span>
            <span class="blog-item-excerpt">${post.excerpt || '暂无简介'}</span>
            <span class="blog-item-tag">#${post.tag}</span>
          </div>`;
        el.addEventListener('click', () => showBlogDetail(i));
        listEl.appendChild(el);
      });

      // 重新绑定 cursor 交互
      listEl.querySelectorAll('.blog-item').forEach(el => el.dataset.cursorBound = '');
    }

    function showBlogDetail(index) {
      const post = BLOG_STORE.posts[index];
      if (!post) return;

      document.getElementById('blog-list').style.display = 'none';
      const detail = document.getElementById('blog-detail');
      detail.style.display = 'block';

      const article = document.getElementById('blog-detail-content');
      document.getElementById('blog-detail-title').textContent = post.title;
      document.getElementById('blog-detail-meta').textContent = `${post.date} · #${post.tag}`;
      article.innerHTML = renderMarkdown(post.body);

      document.getElementById('content').scrollTop = 0;
    }

    function closeBlogDetail() {
      document.getElementById('blog-detail').style.display = 'none';
      document.getElementById('blog-list').style.display = '';
    }

    // 页面加载后拉取博客
    loadBlogs();

    // ── Text-to-Speech ──
    let ttsEnabled = false;
    let cachedVoices = [];
    let ttsSpanRefs = [];

    function loadVoices() {
      cachedVoices = speechSynthesis.getVoices();
    }

    speechSynthesis.addEventListener('voiceschanged', loadVoices);
    loadVoices();

    function clearTTSHighlights() {
      ttsSpanRefs.forEach(span => {
        span.classList.remove('tts-highlight', 'tts-highlight-current');
      });
      ttsSpanRefs = [];
    }

    function toggleTTS() {
      ttsEnabled = !ttsEnabled;
      const btn = document.getElementById('tts-btn');
      const mobileBtn = document.getElementById('mobile-tts-btn');
      btn.classList.toggle('active', ttsEnabled);
      mobileBtn.classList.toggle('active', ttsEnabled);
      if (!ttsEnabled) {
        if (speechSynthesis.speaking) speechSynthesis.cancel();
        clearTTSHighlights();
      }
    }

    function toggleMobileMore() {
      const menu = document.getElementById('mobile-more-menu');
      const btn = document.getElementById('fab-more');
      menu.classList.toggle('show');
      if (btn) btn.classList.toggle('active', menu.classList.contains('show'));
    }

    function closeMobileMore() {
      const menu = document.getElementById('mobile-more-menu');
      const btn = document.getElementById('fab-more');
      menu.classList.remove('show');
      if (btn) btn.classList.remove('active');
    }

    document.addEventListener('click', (e) => {
      const menu = document.getElementById('mobile-more-menu');
      const btn = document.getElementById('fab-more');
      if (menu && !menu.contains(e.target) && !btn.contains(e.target)) {
        menu.classList.remove('show');
        if (btn) btn.classList.remove('active');
      }
    });

    document.addEventListener('mouseup', () => {
      if (!ttsEnabled) return;
      const selection = window.getSelection();
      const text = selection.toString().trim();
      if (!text) return;
      if (speechSynthesis.speaking) speechSynthesis.cancel();
      clearTTSHighlights();

      // 将选区文本逐字包裹为 span
      const range = selection.getRangeAt(0);
      const textNodes = [];
      const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, {
        acceptNode: (node) => {
          const nodeRange = document.createRange();
          nodeRange.selectNodeContents(node);
          return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) < 0 &&
                 range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0
            ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
        }
      });
      while (walker.nextNode()) textNodes.push(walker.currentNode);

      let charIndex = 0;
      const spanMap = []; // { span, charIndex }

      textNodes.forEach(textNode => {
        const nodeRange = document.createRange();
        nodeRange.selectNodeContents(textNode);
        const startOffset = textNode === range.startContainer ? range.startOffset : 0;
        const endOffset = textNode === range.endContainer ? range.endOffset : textNode.length;
        const fragment = document.createDocumentFragment();
        const subText = textNode.textContent.substring(startOffset, endOffset);

        for (let i = 0; i < subText.length; i++) {
          const span = document.createElement('span');
          span.textContent = subText[i];
          span.classList.add('tts-highlight');
          span.dataset.ttsIdx = charIndex;
          fragment.appendChild(span);
          spanMap.push({ span, idx: charIndex });
          charIndex++;
        }

        // 保留选中范围前后的文本
        const before = textNode.textContent.substring(0, startOffset);
        const after = textNode.textContent.substring(endOffset);
        const parent = textNode.parentNode;
        const replacement = document.createDocumentFragment();
        if (before) replacement.appendChild(document.createTextNode(before));
        replacement.appendChild(fragment);
        if (after) replacement.appendChild(document.createTextNode(after));
        parent.replaceChild(replacement, textNode);
      });

      ttsSpanRefs = spanMap.map(s => s.span);

      const utterance = new SpeechSynthesisUtterance(text);
      utterance.lang = 'zh-CN';
      utterance.rate = 1;
      utterance.pitch = 1;
      // Edge 下优先使用云希音色
      const isEdge = navigator.userAgent.includes('Edg/');
      if (isEdge && cachedVoices.length) {
        const yunxi = cachedVoices.find(v => v.name === 'Microsoft Yunxi Online (Natural) - Chinese (Mainland)');
        if (yunxi) utterance.voice = yunxi;
      }

      let lastIdx = -1;
      utterance.addEventListener('boundary', (e) => {
        if (e.name !== 'word' && e.name !== 'character') return;
        const idx = e.charIndex;
        // 清除上一个高亮
        if (lastIdx >= 0) {
          for (let i = lastIdx; i < idx && i < ttsSpanRefs.length; i++) {
            ttsSpanRefs[i].classList.remove('tts-highlight-current');
          }
        }
        // 高亮当前字
        if (idx < ttsSpanRefs.length) {
          ttsSpanRefs[idx].classList.add('tts-highlight-current');
          ttsSpanRefs[idx].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
        }
        lastIdx = idx;
      });

      utterance.addEventListener('end', () => {
        clearTTSHighlights();
      });

      utterance.addEventListener('error', () => {
        clearTTSHighlights();
      });

      speechSynthesis.speak(utterance);
    });

    // ── Friends Loader (XML) ──
    async function loadFriends() {
      const grid = document.getElementById('friend-grid');
      try {
        const resp = await fetch('friends.xml');
        const xmlText = await resp.text();
        const parser = new DOMParser();
        const xml = parser.parseFromString(xmlText, 'text/xml');
        const items = xml.querySelectorAll('friend');

        if (items.length === 0) {
          grid.innerHTML = '<div class="blog-loading">暂无友链数据</div>';
          return;
        }

        grid.innerHTML = '';
        items.forEach(item => {
          const name = item.querySelector('name')?.textContent || '';
          const desc = item.querySelector('desc')?.textContent || '';
          const url = item.querySelector('url')?.textContent || '#';
          const avatarRaw = (item.querySelector('avatar')?.textContent || '').trim();
          const isImgUrl = /^https?:\/\/|^\//.test(avatarRaw);
          const initial = name.charAt(0).toUpperCase();

          let avatarHtml;
          if (isImgUrl) {
            avatarHtml = `<div class="friend-avatar" style="padding:0;overflow:hidden;"><img src="${avatarRaw}" alt="${name}" style="width:100%;height:100%;object-fit:cover;display:block;"></div>`;
          } else {
            const colors = avatarRaw || '#888,#666';
            avatarHtml = `<div class="friend-avatar" style="background:linear-gradient(135deg,${colors});">${initial}</div>`;
          }

          const card = document.createElement('a');
          card.className = 'friend-card';
          card.href = url;
          card.target = '_blank';
          card.rel = 'noopener';
          card.style.textDecoration = 'none';
          card.innerHTML = `
            ${avatarHtml}
            <div class="friend-info">
              <span class="friend-name">${name}</span>
              <span class="friend-desc">${desc}</span>
            </div>`;
          grid.appendChild(card);
        });
      } catch (e) {
        grid.innerHTML = '<div class="blog-loading">友链加载失败</div>';
      }
    }

    loadFriends();

    // ── Projects Loader (XML) ──
    async function loadProjects() {
      const grid = document.getElementById('project-grid');
      try {
        const resp = await fetch('projects.xml');
        const xmlText = await resp.text();
        const parser = new DOMParser();
        const xml = parser.parseFromString(xmlText, 'text/xml');
        const items = xml.querySelectorAll('project');

        if (items.length === 0) {
          grid.innerHTML = '<div class="blog-loading">暂无项目数据</div>';
          return;
        }

        grid.innerHTML = '';
        items.forEach(item => {
          const name = item.querySelector('name')?.textContent || '';
          const desc = item.querySelector('desc')?.textContent || '';
          const url = item.querySelector('url')?.textContent || '';
          const avatarRaw = (item.querySelector('avatar')?.textContent || '').trim();
          const color = item.querySelector('color')?.textContent || 'rgba(255,255,255,0.08)';
          const tagsRaw = item.querySelector('tags')?.textContent || '';
          const tags = tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
          const isImgUrl = /^https?:\/\/|^\//.test(avatarRaw);

          let iconHtml;
          if (isImgUrl) {
            iconHtml = `<div class="project-card-icon" style="background:${color};padding:0;overflow:hidden;"><img src="${avatarRaw}" alt="${name}" style="width:100%;height:100%;object-fit:cover;display:block;"></div>`;
          } else {
            const gradientColors = avatarRaw || '#888,#666';
            const initial = name.charAt(0).toUpperCase();
            iconHtml = `<div class="project-card-icon" style="background:linear-gradient(135deg,${gradientColors}),${color};">${initial}</div>`;
          }

          const card = document.createElement(url ? 'a' : 'div');
          card.className = 'project-card';
          if (url) {
            card.href = url;
            card.target = '_blank';
            card.rel = 'noopener';
            card.style.textDecoration = 'none';
          }
          card.innerHTML = `
            <div class="project-card-header">
              ${iconHtml}
              <span class="project-card-name">${name}</span>
            </div>
            <p class="project-card-desc">${desc}</p>
            <div class="project-card-tags">${tags.map(t => `<span class="project-tag">${t}</span>`).join('')}</div>`;
          grid.appendChild(card);
        });
      } catch (e) {
        grid.innerHTML = '<div class="blog-loading">项目加载失败</div>';
      }
    }

    loadProjects();
  </script>
</body>

</html>